Can't set attribute for subclasses of namedtuple
Asked Answered
I

5

61

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?

Interclavicle answered 8/10, 2013 at 20:17 Comment(7)
Why do you have double __init__ and __new__ methods? Only the second one counts, it overwrites the first. Python does not 'overload' method signatures.Calculus
No overloading... So that means that my original goal of creating instances of C in different ways (depending on overloaded constructors) is not actually doable?Interclavicle
It is perfectly doable, just using different paradigms.Calculus
See my comment below, under your answer. You say it's doable using factory methods, but not using multiple "constructors"?Interclavicle
You can use just one constructor, but the constructor can vary behaviour based on the arguments you pass in.Calculus
Makes sense, thanks. I keep forgetting that this is a dynamically typed language, so things are a little more flexible in some respects ;-) In the example, the first parameter would be either an x or an obj, and the second parameter would be either y or None as a default value. I'll noodle on that, see what I like better.Interclavicle
Possible duplicate of AttributeError: can't set attribute in pythonHurried
C
52

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)
Calculus answered 8/10, 2013 at 20:27 Comment(6)
Thank you Martijn. Without overloading I've decided to use a constructor (which receives x and y) and a second factory method (which receives an obj). Not pretty, perhaps I prefer the C++ style constructor overloading, but I guess that's as much as I can do with Python.Interclavicle
What about using the _replace method? docs.python.org/2/library/…Hurried
@YonatanSimson: what about using it? Can you elaborate? Note that instance._replace() returns a new instance, the original is left unchanged. ._replace() won't make a factory function more efficient.Calculus
@MartijnPieters True. You can do this: c = c._replace(x=30). I feel it is better to use existing functionality even if there is some overhead. Writing your own method might have the same overhead costs without having the same level testing and confidenceHurried
@YonatanSimson: how would you use that here? The user is asking for a factory method, to create a new object. _replace() works great on an existing instance, but that's not what is being asked for here.Calculus
@MartijnPieters I concede you have a point. Though it is also a matter of ones approach. I prefer to avoid reinventing the wheel if it already exists. From my POV the user is asking how to reinvent an existing wheel which is not something I would recommend doing.Hurried
H
63

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)
Hurried answered 10/12, 2018 at 16:10 Comment(2)
This preserves the immutable nature and logic behind named tuples. Much more preferable solutionRailroader
Kind of strange that it has the underscore prefix but I guess it was a design choice not to mixture with the named tuple attributes and methods provided by the developer.Dislike
C
52

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)
Calculus answered 8/10, 2013 at 20:27 Comment(6)
Thank you Martijn. Without overloading I've decided to use a constructor (which receives x and y) and a second factory method (which receives an obj). Not pretty, perhaps I prefer the C++ style constructor overloading, but I guess that's as much as I can do with Python.Interclavicle
What about using the _replace method? docs.python.org/2/library/…Hurried
@YonatanSimson: what about using it? Can you elaborate? Note that instance._replace() returns a new instance, the original is left unchanged. ._replace() won't make a factory function more efficient.Calculus
@MartijnPieters True. You can do this: c = c._replace(x=30). I feel it is better to use existing functionality even if there is some overhead. Writing your own method might have the same overhead costs without having the same level testing and confidenceHurried
@YonatanSimson: how would you use that here? The user is asking for a factory method, to create a new object. _replace() works great on an existing instance, but that's not what is being asked for here.Calculus
@MartijnPieters I concede you have a point. Though it is also a matter of ones approach. I prefer to avoid reinventing the wheel if it already exists. From my POV the user is asking how to reinvent an existing wheel which is not something I would recommend doing.Hurried
J
6

Two things: one, you're not really getting much out of namedtuple here, as far as i can tell. So maybe you should just switch to a normal class. Also, you can't overload the

Second, other possibilities which might help with your problem:

Factory design pattern - instead of putting the different parameters in the constructor, have a class that takes different kinds of parameters and calls the constructor with appropriate arguments, outside the object. recordtype - a mutable namedtuple, that allows defaults but would also let you write your subclass the way you originally wanted. bunch - not exactly a named tuple, but lets you create somewhat arbitrary objects.

Judicature answered 8/10, 2013 at 20:57 Comment(0)
P
3

There is a workaround to changing the attribute of a namedtuple.

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")
Playground answered 21/6, 2016 at 8:11 Comment(3)
This creates a new instance of the named tuple, and all existing references to the named tuple will still access the original and not the new one. Thus, they won't see the change.Interclavicle
Couldn't the new instance be assigned to the old variable?Playground
If you keep track of all variables at runtime that point at the original tuple, then yes.Interclavicle
X
0

I create a named tuple then later calculate leverage based on foods list in the named tuple. Finally, I replace the named tuple in the list

def get_leverage(antecedent, consequent):
     #Compute support for antecedent AND consequent
    supportAC = np.logical_and(antecedent, consequent).mean()
    # Compute support for antecedent
    supportA = antecedent.mean()
    # Compute support for consequent
    supportC = consequent.mean()
    # Return leverage
    return supportAC - supportA*supportC


DataRecord = namedtuple("DataRecord", "foods support confidence lift leverage conviction")
lstData=[]
for record in association_results:
    foods=[i for i in record.items]
    support=round(record.support,2)
    confidence=round(record.ordered_statistics[0].confidence,2)
    lift=round(record.ordered_statistics[0].lift,2)    
    dataRecord = DataRecord(foods,support,confidence,lift,0,0)
    lstData.append(dataRecord)


print(lstData[:3])


for index,record in enumerate(lstData):
    old_food=""
    lstLeverage=[]
    for food in record.foods:
        #print(food)
        if len(food)>0 and len(old_food)>0:
            leverage=get_leverage(onehot[food],onehot[old_food])
            lstLeverage.append(leverage)
        old_food=food
    #print(np.mean(lstLeverage))
    record=record._replace(leverage=round(np.mean(lstLeverage),3))
    lstData[index]=record
    #print(record)

print(lstData[:3]) 
Xylol answered 11/4, 2022 at 20:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.