web-dev-qa-db-fra.com

Existence de mutable nommé Tuple en Python?

Quelqu'un peut-il modifier namedtuple ou fournir une classe alternative pour qu'elle fonctionne pour les objets mutables?

Principalement pour la lisibilité, je voudrais quelque chose de semblable à namedtuple qui fait ceci:

from Camelot import namedgroup

Point = namedgroup('Point', ['x', 'y'])
p = Point(0, 0)
p.x = 10

>>> p
Point(x=10, y=0)

>>> p.x *= 10
Point(x=100, y=0)

Il doit être possible de décaper l'objet résultant. Et selon les caractéristiques de Tuple nommé, l'ordre de la sortie lorsqu'il est représenté doit correspondre à l'ordre de la liste de paramètres lors de la construction de l'objet.

95
Alexander

Il existe une alternative modifiable à collections.namedtuple - recordclass .

Il a la même empreinte API et mémoire que namedtuple et prend en charge les affectations (il devrait également être plus rapide). Par exemple:

from recordclass import recordclass

Point = recordclass('Point', 'x y')

>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

Pour python 3.6 et versions supérieures recordclass (depuis 0.5), prenez en charge les typographies:

from recordclass import recordclass, RecordClass

class Point(RecordClass):
   x: int
   y: int

>>> Point.__annotations__
{'x':int, 'y':int}
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
>>> print(p.x, p.y)
1 2
>>> p.x += 2; p.y += 3; print(p)
Point(x=3, y=5)

Il existe un plus complet exemple (il inclut également des comparaisons de performances).

Depuis 0.9, la bibliothèque recordclass fournit une autre variante - la fonction d'usine recordclass.structclass. Il peut produire des classes dont les instances occupent moins de mémoire que les instances basées sur __slots__. Cela peut être important pour les instances avec des valeurs d'attribut, qui n'ont pas été conçues pour avoir des cycles de référence. Cela peut aider à réduire l'utilisation de la mémoire si vous devez créer des millions d'instances. Voici un exemple exemple .

108
intellimath

types.SimpleNamespace a été introduit dans Python 3.3 et prend en charge les exigences requises.

from types import SimpleNamespace
t = SimpleNamespace(foo='bar')
t.ham = 'spam'
print(t)
namespace(foo='bar', ham='spam')
print(t.foo)
'bar'
import pickle
with open('/tmp/pickle', 'wb') as f:
    pickle.dump(t, f)
23
funky-future

Il semble que la réponse à cette question est non.

Ci-dessous, c'est assez proche, mais ce n'est pas techniquement mutable. Cela crée une nouvelle instance namedtuple() avec une valeur x mise à jour:

Point = namedtuple('Point', ['x', 'y'])
p = Point(0, 0)
p = p._replace(x=10) 

D'autre part, vous pouvez créer une classe simple en utilisant __slots__ qui devrait bien fonctionner pour mettre à jour fréquemment les attributs d'instance de classe:

class Point:
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x = x
        self.y = y

Pour ajouter à cette réponse, je pense que __slots__ est utile ici car la mémoire est efficace lorsque vous créez de nombreuses instances de classe. Le seul inconvénient est que vous ne pouvez pas créer de nouveaux attributs de classe.

Voici un fil pertinent qui illustre l’efficacité de la mémoire - Dictionary vs Object - qui est plus efficace et pourquoi?

Le contenu cité dans la réponse de ce fil est une explication très succincte pourquoi __slots__ est plus efficace en mémoire - slots Python

21
kennes

Le dernier namedlist 1.7 passe tous vos tests avec les deux Python 2.7 et Python 3.5 du 11 janvier 2016. Il s’agit d’une pure python implementation alors que le recordclass est une extension C. Bien sûr, cela dépend de vos besoins, qu'une extension C soit préférée ou non.

Vos tests (mais voir aussi la note ci-dessous):

from __future__ import print_function
import pickle
import sys
from namedlist import namedlist

Point = namedlist('Point', 'x y')
p = Point(x=1, y=2)

print('1. Mutation of field values')
p.x *= 10
p.y += 10
print('p: {}, {}\n'.format(p.x, p.y))

print('2. String')
print('p: {}\n'.format(p))

print('3. Representation')
print(repr(p), '\n')

print('4. Sizeof')
print('size of p:', sys.getsizeof(p), '\n')

print('5. Access by name of field')
print('p: {}, {}\n'.format(p.x, p.y))

print('6. Access by index')
print('p: {}, {}\n'.format(p[0], p[1]))

print('7. Iterative unpacking')
x, y = p
print('p: {}, {}\n'.format(x, y))

print('8. Iteration')
print('p: {}\n'.format([v for v in p]))

print('9. Ordered Dict')
print('p: {}\n'.format(p._asdict()))

print('10. Inplace replacement (update?)')
p._update(x=100, y=200)
print('p: {}\n'.format(p))

print('11. Pickle and Unpickle')
pickled = pickle.dumps(p)
unpickled = pickle.loads(pickled)
assert p == unpickled
print('Pickled successfully\n')

print('12. Fields\n')
print('p: {}\n'.format(p._fields))

print('13. Slots')
print('p: {}\n'.format(p.__slots__))

Sortie sur Python 2.7

1. Mutation des valeurs de champ 
 P: 10, 12 
 
 2. Chaîne 
 P: Point (x = 10, y = 12) 
 
 3. Représentation 
 Point (x = 10, y = 12) 
 
 4. Taille de 
 Taille de p: 64 
 
 5. Accès par nom de champ 
 P: 10, 12 
 
 6. Accès par index 
 P: 10, 12 
 
 7. Déballage itératif 
 P: 10, 12 
 
 8. Itération 
 P: [10, 12] 
 
 9. Ordonné Dict 
 P: OrdreDict ([('x', 10), ('y', 12)]) 
 
 10. Remplacement en place (mise à jour?) 
 P: Point (x = 100, y = 200) 
 
 11. Pickle and Unpickle 
 Mariné avec succès 
 
 12. Champs 
 P: ('x', 'y') 
 
 13. Slots 
 P: ('x', 'y') 

La seule différence avec Python 3.5 est que le namedlist est devenu plus petit et que sa taille est de 56 (Python 2.7 indique 64).

Notez que j'ai changé votre test 10 pour le remplacement sur place. Le namedlist a une méthode _replace() qui effectue une copie superficielle, ce qui me semble tout à fait logique car le namedtuple de la bibliothèque standard se comporte de la même manière. Changer la sémantique de la méthode _replace() serait déroutant. À mon avis, la méthode _update() devrait être utilisée pour les mises à jour sur place. Ou peut-être que je n'ai pas compris l'intention de votre test 10?

20
Ali

Comme alternative très pythonique à cette tâche, depuis Python-3.7, vous pouvez utiliser le module dataclasses qui se comporte non seulement comme un mutable NamedTuple car il utilise des définitions de classe normales, mais il prend également en charge les fonctionnalités des autres classes.

De PEP-0557:

Bien qu'elles utilisent un mécanisme très différent, les classes de données peuvent être considérées comme des "éléments nommés mutables avec des valeurs par défaut". Etant donné que les classes de données utilisent la syntaxe de définition de classe normale, vous êtes libre d'utiliser l'héritage, les métaclasses, les docstrings, les méthodes définies par l'utilisateur, les fabriques de classe et autres Python fonctions de classe.

Un décorateur de classe est fourni; il inspecte une définition de classe pour les variables avec des annotations de type telles que définies dans PEP 526 , "Syntaxe des annotations de variable". Dans ce document, ces variables s'appellent des champs. À l'aide de ces champs, le décorateur ajoute les définitions de méthode générées à la classe afin de prendre en charge l'initialisation d'instance, une repr, les méthodes de comparaison et éventuellement d'autres méthodes, comme décrit dans la section Spécification . Une telle classe s'appelle une classe de données, mais elle n'a vraiment rien de spécial: le décorateur ajoute les méthodes générées à la classe et retourne la même classe qui lui a été donnée.

Cette fonctionnalité est introduite dans PEP-0557 que vous pouvez lire à ce sujet dans plus de détails sur le lien de la documentation fournie.

Exemple:

In [20]: from dataclasses import dataclass

In [21]: @dataclass
    ...: class InventoryItem:
    ...:     '''Class for keeping track of an item in inventory.'''
    ...:     name: str
    ...:     unit_price: float
    ...:     quantity_on_hand: int = 0
    ...: 
    ...:     def total_cost(self) -> float:
    ...:         return self.unit_price * self.quantity_on_hand
    ...:    

Démo:

In [23]: II = InventoryItem('bisc', 2000)

In [24]: II
Out[24]: InventoryItem(name='bisc', unit_price=2000, quantity_on_hand=0)

In [25]: II.name = 'choco'

In [26]: II.name
Out[26]: 'choco'

In [27]: 

In [27]: II.unit_price *= 3

In [28]: II.unit_price
Out[28]: 6000

In [29]: II
Out[29]: InventoryItem(name='choco', unit_price=6000, quantity_on_hand=0)
10
Kasrâmvd

Ce qui suit est une bonne solution pour Python 3: une classe minimale utilisant __slots__ Et Sequence une classe de base abstraite; ne fait pas de détection d'erreur fantaisie ou autre, mais cela fonctionne et se comporte principalement comme un tuple mutable (sauf typecheck).

from collections import Sequence

class NamedMutableSequence(Sequence):
    __slots__ = ()

    def __init__(self, *a, **kw):
        slots = self.__slots__
        for k in slots:
            setattr(self, k, kw.get(k))

        if a:
            for k, v in Zip(slots, a):
                setattr(self, k, v)

    def __str__(self):
        clsname = self.__class__.__name__
        values = ', '.join('%s=%r' % (k, getattr(self, k))
                           for k in self.__slots__)
        return '%s(%s)' % (clsname, values)

    __repr__ = __str__

    def __getitem__(self, item):
        return getattr(self, self.__slots__[item])

    def __setitem__(self, item, value):
        return setattr(self, self.__slots__[item], value)

    def __len__(self):
        return len(self.__slots__)

class Point(NamedMutableSequence):
    __slots__ = ('x', 'y')

Exemple:

>>> p = Point(0, 0)
>>> p.x = 10
>>> p
Point(x=10, y=0)
>>> p.x *= 10
>>> p
Point(x=100, y=0)

Si vous le souhaitez, vous pouvez également utiliser une méthode pour créer la classe (bien que l’utilisation d’une classe explicite soit plus transparente):

def namedgroup(name, members):
    if isinstance(members, str):
        members = members.split()
    members = Tuple(members)
    return type(name, (NamedMutableSequence,), {'__slots__': members})

Exemple:

>>> Point = namedgroup('Point', ['x', 'y'])
>>> Point(6, 42)
Point(x=6, y=42)

Dans Python 2, vous devez l’ajuster légèrement. Si vous héritez de Sequence, la classe aura un __dict__ et le __slots__ cesseront de fonctionner.

La solution dans Python 2 est de ne pas hériter de Sequence, mais de object. Si. isinstance(Point, Sequence) == True est souhaité, vous devez enregistrer le NamedMutableSequence en tant que classe de base pour Sequence:

Sequence.register(NamedMutableSequence)
6
Antti Haapala

Implémentons ceci avec la création de type dynamique:

import copy
def namedgroup(typename, fieldnames):

    def init(self, **kwargs): 
        attrs = {k: None for k in self._attrs_}
        for k in kwargs:
            if k in self._attrs_:
                attrs[k] = kwargs[k]
            else:
                raise AttributeError('Invalid Field')
        self.__dict__.update(attrs)

    def getattribute(self, attr):
        if attr.startswith("_") or attr in self._attrs_:
            return object.__getattribute__(self, attr)
        else:
            raise AttributeError('Invalid Field')

    def setattr(self, attr, value):
        if attr in self._attrs_:
            object.__setattr__(self, attr, value)
        else:
            raise AttributeError('Invalid Field')

    def rep(self):
         d = ["{}={}".format(v,self.__dict__[v]) for v in self._attrs_]
         return self._typename_ + '(' + ', '.join(d) + ')'

    def iterate(self):
        for x in self._attrs_:
            yield self.__dict__[x]
        raise StopIteration()

    def setitem(self, *args, **kwargs):
        return self.__dict__.__setitem__(*args, **kwargs)

    def getitem(self, *args, **kwargs):
        return self.__dict__.__getitem__(*args, **kwargs)

    attrs = {"__init__": init,
                "__setattr__": setattr,
                "__getattribute__": getattribute,
                "_attrs_": copy.deepcopy(fieldnames),
                "_typename_": str(typename),
                "__str__": rep,
                "__repr__": rep,
                "__len__": lambda self: len(fieldnames),
                "__iter__": iterate,
                "__setitem__": setitem,
                "__getitem__": getitem,
                }

    return type(typename, (object,), attrs)

Cela vérifie les attributs pour voir s'ils sont valides avant d'autoriser la poursuite de l'opération.

Alors, est-ce décapable? Oui si (et seulement si) vous procédez comme suit:

>>> import pickle
>>> Point = namedgroup("Point", ["x", "y"])
>>> p = Point(x=100, y=200)
>>> p2 = pickle.loads(pickle.dumps(p))
>>> p2.x
100
>>> p2.y
200
>>> id(p) != id(p2)
True

La définition doit être dans votre espace de noms et doit exister suffisamment longtemps pour que pickle puisse la trouver. Donc, si vous définissez cela comme étant dans votre paquet, cela devrait fonctionner.

Point = namedgroup("Point", ["x", "y"])

Pickle échouera si vous effectuez les opérations suivantes ou si vous rendez la définition temporaire (la portée disparaît à la fin de la fonction, par exemple):

some_point = namedgroup("Point", ["x", "y"])

Et oui, cela préserve l'ordre des champs listés dans la création du type.

3
MadMan2064

Si vous souhaitez un comportement similaire à celui de namedtuples mais mutable, essayez namedlist

Notez que pour être mutable ne peut pas être un Tuple.

2
agomcas

Les tuples sont par définition immuables.

Vous pouvez toutefois créer une sous-classe de dictionnaire où vous pourrez accéder aux attributs avec la notation par points.

In [1]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:class AttrDict(dict):
:
:    def __getattr__(self, name):
:        return self[name]
:
:    def __setattr__(self, name, value):
:        self[name] = value
:--

In [2]: test = AttrDict()

In [3]: test.a = 1

In [4]: test.b = True

In [5]: test
Out[5]: {'a': 1, 'b': True}
1
Roland Smith

Si les performances importent peu, on pourrait utiliser un bidouillage idiot comme:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
0
Srg