Existence d'un tuple nommé mutable en Python?

121

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 similaire à 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 obtenu. Et selon les caractéristiques du tuple nommé, l'ordre de la sortie lorsqu'elle est représentée doit correspondre à l'ordre de la liste de paramètres lors de la construction de l'objet.

Alexandre
la source
3
Voir également: stackoverflow.com/q/5131044 . Y a-t-il une raison pour laquelle vous ne pouvez pas simplement utiliser un dictionnaire?
senshin
@senshin Merci pour le lien. Je préfère ne pas utiliser de dictionnaire pour la raison qui y est indiquée. Cette réponse est également liée à code.activestate.com/recipes/… , ce qui est assez proche de ce que je recherche.
Alexander
Contrairement à namedtuples, il semble que vous n'avez pas besoin d'être en mesure de référencer les attributs par index, c'est-à-dire que oui p[0]et p[1]serait d'autres façons de référencer xet yrespectivement, corriger?
martineau
Idéalement, oui, indexable par position comme un tuple simple en plus de par nom, et décompressé comme un tuple. Cette recette ActiveState est proche, mais je crois qu'elle utilise un dictionnaire normal au lieu d'un OrderedDict. code.activestate.com/recipes/500261
Alexander
2
Un namedtuple mutable est appelé une classe.
gbtimmon

Réponses:

132

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

Il a la même API et la même empreinte mémoire que namedtupleet il prend en charge les affectations (cela 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 supérieur recordclass(depuis 0.5), prenez en charge les indices de type:

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 exemple plus complet (il comprend également des comparaisons de performances).

Depuis la recordclassbibliothèque 0.9 fournit une autre variante - la recordclass.structclassfonction d'usine. Il peut produire des classes dont les instances occupent moins de mémoire que les __slots__instances basées sur. Cela peut être important pour les instances avec des valeurs d'attribut, qui n'ont pas prévu d'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 illustratif .

Intellimath
la source
4
J'aime ça. «Cette bibliothèque est en fait une« preuve de concept »pour le problème de l'alternative« mutable »du tuple nommé.»
Alexander
1
recordclassest plus lent, prend plus de mémoire et nécessite des extensions C par rapport à la recette d'Antti Haapala et namedlist.
GrantJ
recordclassest une version modifiable de collection.namedtuplequi hérite de son api, de son empreinte mémoire, mais prend en charge les affectations. namedlistest en fait une instance de la classe python avec des slots. C'est plus utile si vous n'avez pas besoin d'un accès rapide à ses champs par index.
intellimath
L'accès aux attributs par recordclassexemple (python 3.5.2) est environ 2-3% plus lent que pournamedlist
intellimath
Lors de l'utilisation d'une namedtuplesimple création de classe Point = namedtuple('Point', 'x y'), Jedi peut compléter automatiquement les attributs, alors que ce n'est pas le cas pour recordclass. Si j'utilise le code de création plus long (basé sur RecordClass), alors Jedi comprend la Pointclasse, mais pas son constructeur ou ses attributs ... Y a-t-il un moyen de recordclassbien travailler avec Jedi?
PhilMacKay
34

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)
futur funky
la source
1
Je cherchais quelque chose comme ça depuis des années. Excellent remplacement pour une bibliothèque de dict pointée comme dotmap
axwell
1
Cela nécessite plus de votes positifs. C'est exactement ce que l'OP recherchait, c'est dans la bibliothèque standard, et il ne pourrait pas être plus simple à utiliser. Merci!
Tom Zych
3
-1 L'OP a clairement indiqué avec ses tests ce dont il avait besoin et SimpleNamespaceéchoue aux tests 6-10 (accès par index, déballage itératif, itération, dict ordonné, remplacement sur place) et 12, 13 (champs, emplacements). Notez que la documentation (que vous avez liée dans la réponse) dit spécifiquement « SimpleNamespacepeut être utile en remplacement de class NS: pass. Cependant, pour un type d'enregistrement structuré, utilisez namedtuple()plutôt».
Ali
1
-1 aussi, SimpleNamespacecrée un objet, pas un constructeur de classe, et ne peut pas remplacer le namedtuple. La comparaison de type ne fonctionnera pas et l'empreinte mémoire sera beaucoup plus élevée.
RedGlyph
26

En tant qu'alternative très pythonique pour cette tâche, depuis Python-3.7, vous pouvez utiliser un dataclassesmodule qui non seulement se comporte comme un mutable NamedTuplecar ils utilisent des définitions de classe normales, mais ils prennent également en charge d'autres fonctionnalités de classes.

À partir 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 «doubles nommés mutables avec des valeurs par défaut». Étant 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 classes et d'autres fonctionnalités de classe Python.

Un décorateur de classe est fourni qui inspecte une définition de classe pour les variables avec des annotations de type comme défini dans PEP 526 , "Syntaxe pour les annotations de variables". Dans ce document, ces variables sont appelées champs. À l'aide de ces champs, le décorateur ajoute des définitions de méthode générées à la classe pour prendre en charge l'initialisation d'instance, une repr, des 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 il n'y a vraiment rien de spécial à propos de la classe: le décorateur ajoute des méthodes générées à la classe et renvoie la même classe qui lui a été donnée.

Cette fonctionnalité est introduite dans PEP-0557 que vous pouvez lire plus en détail sur le lien de documentation fourni.

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)
Kasravnd
la source
1
Il a été très clair avec les tests dans l'OP ce qui est nécessaire et dataclasséchoue aux tests 6-10 (accès par index, décompression itérative, itération, dict ordonné, remplacement sur place) et 12, 13 (champs, emplacements) en Python 3.7 .1.
Ali
1
bien que ce ne soit peut-être pas spécifiquement ce que l'OP recherchait, cela m'a certainement aidé :)
Martin CR
25

La dernière liste nommée 1.7 passe tous vos tests avec Python 2.7 et Python 3.5 à partir du 11 janvier 2016. Il s'agit d'une implémentation pure de python alors que recordclassc'est une extension C. Bien sûr, cela dépend de vos besoins si une extension C est 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. Dict commandé  
p: OrderedDict ([('x', 10), ('y', 12)])

10. Remplacement sur place (mise à jour?)  
p: Point (x = 100, y = 200)

11. Pickle and Unpickle  
Mariné avec succès

12. Champs  
p: ('x', 'y')

13. Machines à sous  
p: ('x', 'y')

La seule différence avec Python 3.5 est que le namedlistest devenu plus petit, la taille est de 56 (Python 2.7 rapporte 64).

Notez que j'ai changé votre test 10 pour un remplacement sur place. Le namedlista une _replace()méthode qui fait une copie superficielle, et cela a un sens parfait pour moi parce que namedtupledans la bibliothèque standard se comporte de la même manière. Changer la sémantique de la _replace()méthode serait déroutant. À mon avis, la _update()méthode devrait être utilisée pour les mises à jour sur place. Ou peut-être n'ai-je pas compris l'intention de votre test 10?

Ali
la source
Il y a une nuance importante. Les namedlistvaleurs de stockage dans l'instance de liste. La chose est que cpythonl » listest en fait un tableau dynamique. De par sa conception, il alloue plus de mémoire que nécessaire pour rendre la mutation de la liste moins chère.
intellimath
1
@intellimath namedlist est un peu abusif. Il n'hérite pas listet par défaut des utilisations__slots__ optimisation. Lorsque j'ai mesuré, l'utilisation de la mémoire était inférieure à recordclass: 96 octets contre 104 octets pour six champs sur Python 2.7
GrantJ
@GrantJ Oui. recorclassutilise plus de mémoire car il s'agit d'un tupleobjet semblable à une taille de mémoire variable.
intellimath
2
Les votes négatifs anonymes n'aident personne. Quel est le problème avec la réponse? Pourquoi le vote négatif?
Ali
J'adore la sécurité contre les fautes de frappe qu'il offre par rapport à types.SimpleNamespace. Malheureusement, pylint ne l'aime pas :-(
xverges
23

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

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

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

D'un autre côté, vous pouvez créer une classe simple en utilisant __slots__qui devrait bien fonctionner pour la mise à jour fréquente des 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__c'est une bonne utilisation ici car c'est une mémoire 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 thread 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__ mémoire est plus efficace - slots Python

Kennes
la source
1
Proche, mais maladroit. Disons que je voulais faire une affectation + =, je devrais alors faire: p._replace (x = px + 10) vs px + = 10
Alexander
1
ouais, ça ne change pas vraiment le tuple existant, ça crée une nouvelle instance
kennes
7

Ce qui suit est une bonne solution pour Python 3: Une classe minimale en utilisant __slots__et Sequenceclasse de base abstraite; ne fait pas de détection d'erreur sophistiquée ou autre, mais cela fonctionne et se comporte principalement comme un tuple mutable (sauf pour la vérification de type).

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 avoir 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__ cessera de fonctionner.

La solution dans Python 2 est de ne pas hériter de Sequence, mais object. Si isinstance(Point, Sequence) == Truevous le souhaitez, vous devez enregistrer le NamedMutableSequencecomme classe de base pour Sequence:

Sequence.register(NamedMutableSequence)
Antti Haapala
la source
3

Implémentons cela 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 de permettre à l'opération de continuer.

Alors est-ce que c'est 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 la trouve. Donc, si vous définissez cela comme étant dans votre package, cela devrait fonctionner.

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

Pickle échouera si vous faites ce qui suit, ou rendez la définition temporaire (sort de la portée lorsque la fonction se termine, par exemple):

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

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

MadMan2064
la source
Si vous ajoutez une __iter__méthode avec for k in self._attrs_: yield getattr(self, k), cela prendra en charge la décompression comme un tuple.
snapshoe
Il est également assez facile d'ajouter __len__, __getitem__et des __setiem__méthodes pour prendre en charge l'obtention de valus par index, comme p[0]. Avec ces derniers bits, cela semble être la réponse la plus complète et la plus correcte (pour moi en tout cas).
snapshoe
__len__et __iter__sont bons. __getitem__et __setitem__peut vraiment être mappé à self.__dict__.__setitem__etself.__dict__.__getitem__
MadMan2064
2

Les tuples sont par définition immuables.

Vous pouvez cependant créer une sous-classe de dictionnaire où vous pouvez 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}
Roland Smith
la source
2

Si vous voulez un comportement similaire à celui des namedtuples mais mutable, essayez namedlist

Notez que pour être mutable, il ne peut pas être un tuple.

agomcas
la source
Merci pour le lien. Cela semble être le plus proche jusqu'à présent, mais je dois l'évaluer plus en détail. Btw, je suis tout à fait conscient que les tuples sont immuables, c'est pourquoi je recherche une solution comme namedtuple.
Alexander
0

À condition que les performances aient peu d'importance, on pourrait utiliser un hack idiot comme:

from collection import namedtuple

Point = namedtuple('Point', 'x y z')
mutable_z = Point(1,2,[3])
Srg
la source
1
Cette réponse n'est pas très bien expliquée. Cela semble déroutant si vous ne comprenez pas la nature mutable des listes. --- Dans cet exemple ... pour réassigner z, vous devez mutable_z.z.pop(0)alors appeler mutable_z.z.append(new_value). Si vous vous trompez, vous vous retrouverez avec plus d'un élément et votre programme se comportera de manière inattendue.
byxor le
1
@byxor que, ou vous pouvez simplement: mutable_z.z[0] = newValue. C'est en effet un hack, comme indiqué.
Srg
Oh oui, je suis surpris d'avoir manqué la manière la plus évidente de le réattribuer.
byxor
J'aime ça, vrai hack.
WebOrCode