One use of __slots__
in Python is to disallow new attributes:
class Thing:
__slots__ = 'a', 'b'
thing = Thing()
thing.c = 'hello' # error
However, this doesn’t work if a class inherits from another slotless class:
class Whatever:
pass
class Thing(Whatever):
__slots__ = 'a', 'b'
thing = Thing()
thing.c = 'hello' # ok
That’s because it also inherits the __dict__
from its parent which allows additional attributes.
Is there any way of blocking the __dict__
from being inherited?
It seems to me that this would allow a sub class to be less generic that its parent, so it’s surprising that it doesn’t work this way naturally.
Comment
OK, the question arises as whether this would violate the https://en.wikipedia.org/wiki/Liskov_substitution_principle . This, in turn buys into a bigger discussion on inheritance.
Most books would, for example, suggest that a circle is an ellipse so a Circle class should inherit from an Ellipse class. However, since a circle is more restrictive, this would violate the Liskov Substitution Principle in that a sub class should not do less than the parent class.
In this case, I’m not sure about whether it applies here. Python has no access modifiers, so object data is already over-exposed. Further, without __slots__
Python objects are pretty promiscuous about adding additional attributes, and I’m not sure that’s really part of the intended discussion.
If you are willing to use a metaclass, you can prevent this. Simply insert an empty sequence for '__slots__'
in the namespace returned by __prepare__
this is a hook that prepares the namespace that will be used for the class, it defaults to a normal dict()
, and we can just force the subclass to have an empty (not unspecified) __slots__
class EmptySlotsMeta(type):
@classmethod
def __prepare__(metacls, name, bases):
return {"__slots__":()}
class Foo(metaclass=EmptySlotsMeta):
__slots__ = 'x', 'y'
def __init__(self, x=0, y=0):
self.x = x
self.y = y
class Bar(Foo):
pass
class Baz(Foo):
__slots__ = ('z',)
def __init__(self, x=0, y=0, z=0):
super().__init__(x, y)
self.z = z
Now, in a REPL:
>>> foo = Foo()
>>> bar = Bar()
>>> baz = Baz()
>>> foo.z = 99
Traceback (most recent call last):
File "<python-input-25>", line 1, in <module>
foo.z = 99
^^^^^
AttributeError: 'Foo' object has no attribute 'z' and no __dict__ for setting new attributes
>>> bar.z = 99
Traceback (most recent call last):
File "<python-input-26>", line 1, in <module>
bar.z = 99
^^^^^
AttributeError: 'Bar' object has no attribute 'z' and no __dict__ for setting new attributes
>>> baz.z = 99
Note, a class with this metaclass can still define their own __slots__
(as in Foo
or its second subclass Baz
above). That's probably desirable. As with most things in Python, you can put some guardrails around it but it isn't worth trying to make it bulletproof.
Note, although it is commonly used this way, __slots__
were not added for this use-case, that is, to restrict attributes. It exists mainly as a memory optimization, since a standard instance carries around a whole dict
object.
Edit:
I undeleted this on the OP's request, but I'm still not sure it answers the exact question, note:
class Whatever:
pass
class Foo(Whatever, metaclass=EmptySlotsMeta):
__slots__ = ('x','y')
Foo().z = 1 # totally works
But it has to work this way, because the superclass will almost certainly create attributes, and it needs a __dict__
to do that unless it had defined slots.