Search code examples
pythonoopsupermetaclass

What is the corret way to call super in dynamically added methods?


I defined a metaclass which add a method named "test" to the created classes:

class FooMeta(type):
    
    def __new__(mcls, name, bases, attrs):
        def test(self):
            return super().test()
        attrs["test"] = test
        cls = type.__new__(mcls, name, bases, attrs)
        return cls

Then I create two classes using this Metaclass

class A(metaclass=FooMeta):
    
    pass
      

class B(A):
    
    pass

When I run

a = A()
a.test()

a TypeError is raised at super().test():

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

Which means super() cannot infer the parent class correctly. If I change the super call into

def __new__(mcls, name, bases, attrs):
    def test(self):
        return super(cls, self).test()
    attrs["test"] = test
    cls = type.__new__(mcls, name, bases, attrs)
    return cls

then the raised error becomes:

AttributeError: 'super' object has no attribute 'test'

which is expected as the parent of A does not implement test method.


So my question is what is the correct way to call super() in a dynamically added method? Should I always write super(cls, self) in this case? If so, it is too ugly (for python3)!


Solution

  • Parameterless super() is very special in Python because it triggers some behavior during code compilation time itself: Python creates an invisible __class__ variable which is a reference to the "physical" class statement body were the super() call is embedded (it also happens if one makes direct use of the __class__ variable inside a class method).

    In this case, the "physical" class where super() is called is the metaclass FooMeta itself, not the class it is creating.

    The workaround for that is to use the version of super which takes 2 positional arguments: the class in which it will search the immediate superclass, and the instance itself.

    In Python 2 and other occasions one may prefer the parameterized use of super, it is normal to use the class name itself as the first parameter: at runtime, this name will be available as a global variable in the current module. That is, if class A would be statically coded in the source file, with a def test(...): method, you would use super(A, self).test(...) inside its body.

    However, although the class name won't be available as a variable in the module defining the metaclass, you really need to pass a reference to the class as the first argument to super. Since the (test) method receives self as a reference to the instance, its class is given by either self.__class__ or type(self).

    TL;DR: just change the super call in your dynamic method to read:

    class FooMeta(type):
        
        def __new__(mcls, name, bases, attrs):
            def test(self):
                return super(type(self), self).test()
            attrs["test"] = test
            cls = type.__new__(mcls, name, bases, attrs)
            return cls