web-dev-qa-db-fra.com

Impossible de définir l'attribut pour les sous-classes de namedtuple

Il semble que ceci ou ceci sont des fils liés, mais que nous n’avons toujours pas compris les choses :)

J'essaie de créer une sous-classe de namedtuple et de fournir différents initialiseurs afin de pouvoir construire des objets de différentes manières. Par exemple:

>>> from collections import namedtuple
>>> class C(namedtuple("C", "x, y")) :
...     __slots__ = ()
...     def __init__(self, obj) : # Initialize a C instance by copying values from obj
...         self.x = obj.a
...         self.y = obj.b
...     def __init__(self, x, y) : # Initialize a C instance from the parameters
...         self.x = x
...         self.y = y

Cependant, cela ne fonctionne pas:

>>> c = C(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __init__
AttributeError: can't set attribute

Après quelques recherches (par exemple, voir this thread), j’ai essayé d’utiliser des constructeurs au lieu d’initialiseurs:

>>> from collections import namedtuple
>>> class C(namedtuple("C", "x, y")) :
...     __slots__ = ()
...     def __new__(cls, obj) :
...       self = super(C, cls).__new__(cls, obj.a, obj.b)
...     def __new__(cls, x, y) :
...       self = super(C, cls).__new__(cls, x, y)

qui semblait construire un objet mais je ne peux pas lire ses attributs:

>>> c = C(1,2)
>>> c.x, c.y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'x'

Où est-ce que je vais mal ici? Comment créer une sous-classe avec plusieurs constructeurs ou initialiseurs?

17
Jens

Les n-uplets nommés sont immuables, vous ne pouvez donc pas les manipuler dans l'initialiseur __init__. Votre seule option est de remplacer la méthode __new__:

class C(namedtuple('C', 'x, y')):
    __slots__ = ()
    def __new__(cls, obj):
        return super(C, cls).__new__(cls, obj.x, obj.y)

Notez que, puisque __new__ est une méthode par défaut pour les nouvelles instances, vous devez renvoyer la nouvelle instance créée. Si vous n'utilisez pas return dans la méthode __new__, la valeur de retour par défaut est None, ce qui vous indique votre erreur.

Démo avec un objet avec les attributs x et y:

>>> class C(namedtuple('C', 'x, y')):
...     __slots__ = ()
...     def __new__(cls, obj):
...         return super(C, cls).__new__(cls, obj.x, obj.y)
... 
>>> O.x, O.y
(10, 20)
>>> C(O)
C(x=10, y=20)

Python ne prend pas en charge la surcharge de méthode; généralement, vous utilisez des arguments de mot-clé facultatifs ou des méthodes de classe supplémentaires en tant que méthodes d'usine.

Le module datetime , par exemple, propose plusieurs méthodes d'usine permettant de créer des objets ne correspondant pas au constructeur standard. datetime.datetime.fromtimestamp() crée une instance datetime.datetime à partir d'une seule valeur numérique, de même que datetime.datetime.fromordinal() ; sauf qu'ils interprètent le nombre de différentes manières.

Si vous voulez supporter les arguments variables, faites:

class C(namedtuple('C', 'x, y')):
    __slots__ = ()

    def __new__(cls, x, y=None):
        if y is None:
            # assume attributes
            x, y = x.x, x.y
        return super(C, cls).__new__(cls, x, y)

Ici, y est un argument optionnel, défini par défaut sur None s'il n'est pas fourni par l'appelant:

>>> C(3, 5):
C(x=3, y=5)
>>> C(O)
C(x=10, y=20)

L’alternative, utilisant une méthode de classe, serait:

class C(namedtuple('C', 'x, y')):
    @classmethod
    def from_attributes(cls, obj):
        return cls(obj.x, obj.y)

Il existe maintenant deux méthodes d’usine; un défaut et un nommé:

>>> C(3, 5):
C(x=3, y=5)
>>> C.from_attributes(O)
C(x=10, y=20)
25
Martijn Pieters

Deux choses: premièrement, vous ne tirez pas vraiment grand profit de nomtuple ici, pour autant que je sache. Alors peut-être devriez-vous simplement passer à une classe normale. En outre, vous ne pouvez pas surcharger le

Deuxièmement, d'autres possibilités qui pourraient vous aider avec votre problème:

Modèle de conception d'usine - au lieu de mettre les différents paramètres dans le constructeur, utilisez une classe qui prend différents types de paramètres et appelle le constructeur avec des arguments appropriés, en dehors de l'objet . recordtype - un mutable namedtuple, qui autorise les valeurs par défaut, mais vous permet également d'écrire votre sous-classe comme vous le souhaitiez initialement . bunch - pas exactement un Tuple nommé, mais vous permet de créer des objets quelque peu arbitraires. 

2
Corley Brigman

Il existe une solution de contournement pour modifier l'attribut d'un nom nommé.

import collections

def updateTuple(NamedTuple,nameOfNamedTuple):
    ## Convert namedtuple to an ordered dictionary, which can be updated
    NamedTuple_asdict = NamedTuple._asdict()

    ## Make changes to the required named attributes
    NamedTuple_asdict['path']= 'www.google.com'

    ## reconstruct the namedtuple using the updated ordered dictionary
    updated_NamedTuple = collections.namedtuple(nameOfNamedTuple, NamedTuple_asdict.keys())(**NamedTuple_asdict)

    return updated_NamedTuple

Tuple = collections.namedtuple("Tuple", "path")
NamedTuple = Tuple(path='www.yahoo.com')
NamedTuple = updateTuple(NamedTuple, "Tuple")
1
chahuja

Je vous suggère d'utiliser la méthode _replace

from collections import namedtuple
C = namedtuple('C', 'x, y')
c = C(x=10, y=20)
# c.x = 30 won't work
c = c._replace(x=30)
0
Yonatan Simson