ajoseps
ajoseps

Reputation: 2121

How do classes inherited from namedtuple maintain access to parent properties with a redefined __init__?

Minimum Reproducible Example:

from collections import namedtuple

class Test(namedtuple('Test', ['a','b'])):
           def __init__(self, a, b):
                self.c = self.a + self.b
           def __str__(self):
                return self.c

print(Test('FIRST', 'SECOND'))

OUTPUT:

FIRSTSECOND

I thought when an __init__ function is defined, it overwrites the parent implementation. If that is the case, how do self.a and self.b exist with the correct values? If I forego the a and b parameters in __init__, I get a TypeError: __init__() takes 1 positional argument but 3 were given. I need to provide the parameters, but they're not being set explicitly in __init__ either, and I have no called to super().

Upvotes: 0

Views: 236

Answers (1)

chepner
chepner

Reputation: 531868

self.a and self.b are set by the named tuple's __new__ method before __init__ is called. This is because a named tuple is immutable (aside from the ability to add additional attributes, as Test.__init__ does), so trying to set a and b after the tuple is created would fail. Instead, the values are passed to __new__ so that the values are available when the tuple is being created.

Here's an example of __new__ being overriden to swap the a and b values.

class Test(namedtuple('Test', ['a','b'])):
    def __new__(cls, a, b, **kwargs):
        return super().__new__(cls, b, a, **kwargs)

    def __init__(self, a, b):
        self.c = self.a + self.b

    def __str__(self):
        return self.c

print(Test('FIRST', 'SECOND'))  # outputs SECONDFIRST

Trying to do the same with __init__ would fail:

class Test(namedtuple('Test', ['a','b'])):
    def __init__(self, a, b):
        self.a, self.b = b, a
        self.c = self.a + self.b

    def __str__(self):
        return self.c

print(Test('FIRST', 'SECOND'))  # outputs SECONDFIRST

results in

Traceback (most recent call last):
  File "/Users/chepner/advent-of-code-2020/tmp.py", line 11, in <module>
    print(Test('FIRST', 'SECOND'))
  File "/Users/chepner/advent-of-code-2020/tmp.py", line 5, in __init__
    self.a, self.b = b, a
AttributeError: can't set attribute

To make c immutable as well (while keeping it distinct from the tuple itself), use a property.

class Test(namedtuple('Test', ['a','b'])):
    @property
    def c(self):
        return self.a + self.b

    def __str__(self):
        return self.c

Note that c is not visible or accessible when treating an instance of Test as a regular tuple:

>>> x = Test("First", "Second")
>>> x
Test(a='First', b='Second')
>>> len(x)
2
>>> tuple(x)
('First', 'Second')
>>> x[0]
'First'
>>> x[1]
'Second'
>>> x[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
>>> x.c = "foo"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

Upvotes: 1

Related Questions