6

I have a tiny class that extends a namedtuple, but the __dict__ property of its instances is always returning empty.

Point = namedtuple('Point', 'x y')
p1 = Point(20, 15)
print(p1, p1.__dict__)
# Point(x=20, y=15) OrderedDict([('x', 20), ('y', 15)]) <--- ok

class SubPoint(Point): pass
p2 = SubPoint(20, 15)
print(p2, p2.__dict__)
# SubPoint(x=20, y=15) {} <--- why is it empty?

p2 has the attributes, but its __dict__ is empty. They are listed correctly with dir(), though, which is strange. Note this work correctly when SubPoint extends a vanilla class.

What is happening, and how do I list the attributes in my subclass instance?

BoppreH
  • 8,014
  • 4
  • 34
  • 71

4 Answers4

4

The problem is that __slots__ is only limited to a class it is defined in, so base classes will always have their own __dict__ attribute unless you define __slots__ there too. (And also note that the __dict__ attribute of namedtuple is not a normal dict but a @property.)

From docs:

The action of a __slots__ declaration is limited to the class where it is defined. As a result, subclasses will have a __dict__ unless they also define __slots__ (which must only contain names of any additional slots).

So, when you defined __slots__ in the subclass then it failed to look for an attribute __dict__ in that class, so moved on to base class where it found the __dict__ attribute.

A simple demo:

class A:
    __slots__=  ('a', 'b')

    @property
    def __dict__(self):
        print ('inside A')
        return self.__slots__         

class B(A):
    pass

print(B().__dict__)

print ('-'*20)

class B(A):
    __slots__ = ()
print(B().__dict__)

output:

{}
--------------------
inside A
()
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
1

To see why __dict__ doesn't work, check the answer by Ashwini Chaudhary. This answer covers the second part of the question (how to list namedtuple attributes).

To list namedtuple attributes, there are _fields and _asdict:

>>> import collections as c
>>> Point = c.namedtuple("Point", ["x", "y"])
>>> p1 = Point(20, 15)
>>> print(p1._fields)
('x', 'y')
>>> print(p1._asdict())
{'x': 20, 'y': 15}
>>> class SubPoint(Point): pass
...
>>> p2 = SubPoint(20, 15)
>>> print(p2._fields)
('x', 'y')
>>> print(p2._asdict())
{'x': 20, 'y': 15}

Note that _fields is defined on the class, so you can also do:

>>> print(SubPoint._fields)
('x', 'y')

Obviously, _asdict needs an instance so it can use the values.

I used Python 3.9.7 for the examples, I'm not exactly sure when this stuff was added (maybe someone who knows can comment).

adigitoleo
  • 92
  • 1
  • 8
  • 1
    These existed back then as well, but the question was about a particular doubt not if something like this exists. :) – Ashwini Chaudhary Oct 06 '21 at 17:19
  • @AshwiniChaudhary Thanks, I've edited the answer. I probably should have just added some info to your answer since it covers the first part of the question. – adigitoleo Oct 20 '21 at 06:37
0

Declaring __slots__ = () in the subclass fixed the problem, but the reason is not clear. As far as I know __slots__ should not change the behavior of code in such subtle ways, so this is not a complete answer.

Point = namedtuple('Point', 'x y')
p1 = Point(20, 15)
print(p1, p1.__dict__)
# Point(x=20, y=15) OrderedDict([('x', 20), ('y', 15)]) <--- ok

class SubPoint(Point):
    __slots__ = ()
p2 = SubPoint(20, 15)
print(p2, p2.__dict__)
# SubPoint(x=20, y=15) OrderedDict([('x', 20), ('y', 15)]) <--- fixed

An answer with an explanation is welcome.

BoppreH
  • 8,014
  • 4
  • 34
  • 71
0
list(A.__annotations__.keys())
WolvhLorien
  • 355
  • 2
  • 5