2

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.

9
  • Maybe here you find the answer. Commented Apr 24 at 0:55
  • 1
    "It seems to me that this would allow a sub class to be less generic that its parent" - subclasses aren't supposed to be "less generic" in that way. Doing that violates Liskov substitutability - subclasses aren't supposed to prohibit operations their superclasses allow. Commented Apr 24 at 3:22
  • @user2357112 Can you give me more info on Liskov substitutability ? Commented Apr 24 at 3:41
  • @Manngo: Basically, it's the idea that you should be able to do superclass things with an instance of a superclass, without having to know whether the instance is actually an instance of a subclass, or what subclass it might be an instance of. The Liskov Substitution Principle is why, for example, languages with access modifiers won't let you put a more restrictive access modifier on a method when you override it - that would invalidate method calls that would be valid for the superclass. Commented Apr 24 at 6:23
  • 1
    A mutable Circle class that inherits from a mutable Ellipse class is only used as an example to start the discussion and highlight the problems, not as an example of what good code looks like. Commented Apr 24 at 7:43

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.