Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't set attribute for subclasses of namedtuple

It looks like this or this are somewhat related threads, but still haven't figured things out :)

I'm trying to create a subclass of namedtuple and provide different initializers so that I can construct objects in different ways. For example:

>>> 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 

However, that doesn't work:

>>> 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 

After some poking around (for example, see this thread) I tried to use constructors instead of initializers:

>>> 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) 

which seemed to construct an object but then I can't read its attributes:

>>> 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' 

Where am I going wrong here? How can I create a subclass with multiple constructors or initializers?

like image 718
Jens Avatar asked Oct 08 '13 20:10

Jens


People also ask

Can you subclass Namedtuple?

As namedtuples are a subclass of tuples, the fields can be accessed via the index or by the name of the field. The index value of a field is tied to the order during the declaration of the namedtuple. Consider the above Address example. You can access the street field by name or by using 0 as the index value.

How do I change attributes in Namedtuple?

Namedtuples are immutable objects, so you cannot change the attribute values.

How do I change Namedtuple value?

Since a named tuple is a tuple, and tuples are immutable, it is impossible to change the value of a field. In this case, we have to use another private method _replace() to replace values of the field. The _replace() method will return a new named tuple.


2 Answers

Named tuples are immutable, so you cannot manipulate them in the __init__ initializer. Your only option is to override the __new__ method:

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

Note that because __new__ is a factory method for new instances, you do need to return the newly created instance. If you do not use return in the __new__ method, the default return value is None, which gives you your error.

Demo with an object with x and y attributes:

>>> 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 does not support method overloading; generally you either use optional keyword arguments or extra class methods as factory methods.

The datetime module, for example, has several such factory methods to let you create objects that do not fit the standard constructor. datetime.datetime.fromtimestamp() creates a datetime.datetime instance from a single numeric value, and so does datetime.datetime.fromordinal(); except that they interpret the number in different ways.

If you wanted to support variable arguments, do:

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) 

Here, y is an optional argument, defaulting to None if not supplied by the caller:

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

The alternative, using a class method, would be:

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

Now there are two factory methods; one default and one named:

>>> C(3, 5): C(x=3, y=5) >>> C.from_attributes(O) C(x=10, y=20) 
like image 186
Martijn Pieters Avatar answered Oct 16 '22 02:10

Martijn Pieters


I suggest you use the the _replace method

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) 
like image 21
Yonatan Simson Avatar answered Oct 16 '22 03:10

Yonatan Simson