19

I have a big tree with hundreds of thousands of nodes, and I'm using __slots__ to reduce the memory consumption. I just found a very strange bug and fixed it, but I don't understand the behavior that I saw.

Here's a simplified code sample:

class NodeBase(object):
    __slots__ = ["name"]
    def __init__(self, name):
        self.name = name

class NodeTypeA(NodeBase):
    name = "Brian"
    __slots__ = ["foo"]

I then execute the following:

>>> node = NodeTypeA("Monty")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __init__
AttributeError: 'NodeTypeA' object attribute 'name' is read-only

There is no error if NodeTypeA.name is not defined (side note: that attribute was there by mistake, and had no reason for being there). There is also no error if NodeTypeA.__slots__ is never defined, and it therefore has a __dict__.

The thing I don't understand is: why does the existence of a class variable in a superclass interfere with setting an instance variable in a slot in the child class?

Can anybody explain why this combination results in the object attribute is read-only error? I know my example is contrived, and is unlikely to be intentional in a real program, but that doesn't make this behavior any less strange.

Thanks,
Jonathan

Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361
Jonathan
  • 1,864
  • 17
  • 26
  • `NodeTypeA` is creating a class variable `name` and not assigning a value to the instance variable defined in `NodeBase`. Is this intentional? – unholysampler Apr 22 '11 at 17:23
  • It wasn't intentional at first - I accidentally had this in my code, causing the bug I'm asking about. But then I got curious about why the code behaves like it does, so I intentionally put it in my code sample. – Jonathan Apr 22 '11 at 17:52

1 Answers1

26

A smaller example:

class C(object):
    __slots__ = ('x',)
    x = 0

C().x = 1

The documentation on slots states at one point:

__slots__ are implemented at the class level by creating descriptors (Implementing Descriptors) for each variable name. As a result, class attributes cannot be used to set default values for instance variables defined by __slots__; otherwise, the class attribute would overwrite the descriptor assignment.

When __slots__ is in use, attribute assignment to slot attributes needs to go through the descriptors created for the slot attributes. Shadowing the descriptors in a subclass causes Python to be unable to find the routine needed to set the attribute. Python can still see that an attribute is there, though (because it finds the object that's shadowing the descriptor), so it reports that the attribute is read-only.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    A similar answer is also posted here: http://stackoverflow.com/questions/820671/python-slots-and-attribute-is-read-only – Santa Apr 22 '11 at 17:25
  • Good point. I didn't realize that it simplified to that - I thought that inheritance was part of the problem. Clearly, as @Santa points out, this is then the same as that previous question. – Jonathan Apr 22 '11 at 17:55
  • 1
    Python does not create read-only descriptors here. What happens is that the instance doesn't have a `__dict__`, and the `name = "Brian"` string in `NodeTypeA` hides the slot descriptor created for the `name` slot in `NodeBase`. Python sees that the instance has a `name` attribute (because attribute lookup finds the `"Brian"` string), but the attribute isn't assignable (because `"Brian"` doesn't have a `__set__` method and the instance doesn't have a `__dict__`), so it reports that the attribute is read-only. – user2357112 Apr 09 '17 at 02:32
  • If you want to see the code, [`PyObject_GenericSetAttr`](https://github.com/python/cpython/blob/2.7/Objects/object.c#L1556) is the `__setattr__` implementation involved. – user2357112 Apr 09 '17 at 02:40
  • You can also see that no read-only descriptors are involved by inspecting the type dicts manually. For `NodeTypeA`, `NodeTypeA.__dict__['name']` is a regular string, not a descriptor (and for your `C` class, `C.__dict__['x']` is `0`). For `NodeBase`, `NodeBase.__dict__['name']` is a descriptor that supports reads, writes, and deletes, as you can see by calling `__get__`, `__set__`, and `__delete__` manually. – user2357112 Apr 09 '17 at 02:44
  • Now Python prevents the creation of such `C` class: `ValueError: 'x' in __slots__ conflicts with class variable`. – Géry Ogam Apr 08 '21 at 16:21