3

I'm using the attrs python package, in combination with inheritance and slots. I want to call the parent class's method from within the derived method. The problem is demonstrated below:

import attr

@attr.s(slots=True)
class Base():
    def meth(self):
        print("hello")

@attr.s(slots=True)
class Derived(Base):
    def meth(self):
        super().meth()
        print("world")

d = Derived()
d.meth()

I get:

TypeError: super(type, obj): obj must be an instance or subtype of type

The problem seems to be triggered by a combination of attrs (undecorated classes with explicit __slots__=() work), slots (regular @attr.s-decorated classes work) and the plain super() call (super(Derived, self) works).

I would like to understand how super() behaves differently from the explicit super(Derived, self) version, since the documentation says they "do the same thing"

Tim Smith
  • 6,127
  • 1
  • 26
  • 32
Niobos
  • 880
  • 4
  • 15

1 Answers1

3

super() normally relies on the compiler to provide a __class__ closure cell, that is bound to the class object a method is derived on. The closure is created the moment you use the name super() in a method (or if you used __class__):

>>> class Foo(object):
...     def bar(self):
...         super()  # just using super or __class__ is enough
...
>>> Foo.bar.__closure__[0].cell_contents
<class '__main__.Foo'>
>>> Foo.bar.__closure__[0].cell_contents is Foo
True

That closure lets super() work without arguments (the self argument is taken from the local namespace).

However, attr generates a new class object when you specify you wanted to use __slots__; you can't add slots to a class after the fact, so a new class object is created that replaces the one you decorated.

The closure attached to meth is the original pre-decoration class and not the same class object as the new generated class:

>>> Derived.meth.__closure__[0].cell_contents
<class '__main__.Derived'>
>>> Derived.meth.__closure__[0].cell_contents is Derived
False

This breaks the expectations super() has, making it impossible to use the 0 argument variant. The super(Derived, self) variant explicitly looks up the name Derived as a global when called, finding the new generated class, so works.

I go into some more detail on how super() without arguments works, and why, in Why is Python 3.x's super() magic?

This was reported as issue #102 in the tracker, and fixed by altering the closure with some ctypes hackery. This fix will be part of the upcoming 17.3 release.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • And the explicit version works because it only evaluates `Derived` when running the code instead of when generating the class? – Niobos Sep 11 '17 at 15:44
  • @Niobos: exactly, you explicitly named `Derived`, which is looked up as a global each time the method is called, and that is bound to the new generated class. – Martijn Pieters Sep 11 '17 at 15:46