WhiskeyAndRye WhiskeyAndRye - 6 months ago 20
Python Question

How to Retrieve a Metaclass Method

Working in Python, how can a method owned by a metaclass (metamethod) be retrieved through a class that it instantiated? In the following scenario, it's easy – just use

getattr
or dot notation:

* all examples use the version safe
with_metaclass


class A(type):
class A(type):
"""a metaclass"""

def method(cls):
m = "this is a metamethod of '%s'"
print(m % cls.__name__)

class B(with_metaclass(A, object)):
"""a class"""
pass

B.method()
# prints: "this is a metamethod of 'B'"


However this is peculiar since
'method'
can't be found anywhere in
dir(B)
. As a result of this fact, overriding such a method dynamically becomes difficult, because the metaclass isn't in the lookup chain for
super
:

class A(type):
"""a metaclass"""

def method(cls):
m = "this is a metamethod of '%s'"
print(m % cls.__name__)

class B(with_metaclass(A, object)):
"""a class"""

@classmethod
def method(cls):
super(B, cls).method()

B.method()

# raises: "AttributeError: 'super' object has no attribute 'method'"


So, what's the easiest way to properly override a metamethod? I've come up my own answer to this question, but look forward to any suggestions or alternate answers. Thanks in advance for your responses.

Answer

As you put it, and discovered in practice, A.method is not on B's lookup chain - The relation of classes and metaclasses is not one of inheritance - it is one of 'instances' as a class is an instance of the metaclass.

Python is a fine language in which it behaves in expected ways, with very few surprises - and it is the same in this circumstances: If we were dealing with 'ordinary' objects, your situation would be the same as having an instance B of the class A. And B.method would be present in B.__dict__ - a "super" call placed on a method defined for such an instance could never get to A.method - it would actually yield an error. As B is a class object, super inside a method in B's __dict__ is meaningful - but it will search on B's __mro__ chain of classes (in this case, (object,)) - and this is what you hit.

This situation should not happen often, and I don't even think it should happen at all; semantically it is very hard to exist any sense in a method that would be meaningful both as a metaclass method and as a method of the class itself. Moreover, if method is not redefined in B note it won't even be visible (nor callable) from B's instances.

Maybe your design should:

a. have a baseclass Base, using your metaclass A, that defines method instead of having it defined in A - and then define class B(Base): instead

b. Or have the metaclass A rather inject method in each class it creates, with code for that in it's __init__ or __new__ method - along:

def method(cls):
    m = "this is an injected method of '%s'"
    print(m % cls.__name__)

class A(type):
    def __init__(cls, name, bases, dct):
        dct['method'] = classmethod(method)

This would be my preferred approach - but it does not allow one to override this method in a class that uses this metaclass - without some extra logic in there, the approach above would rather override any such method explicit in the body. The simpler thing is to have a base class Base as above, or to inject a method with a different name, like base_method on the final class, and hardcode calls to it in any overriding method:

class B(metaclass=A):
   @classmethod
   def method(cls):
        cls.base_method()
        ...

(Use an extra if on the metaclass's __init__ so that the default method is aliased to base_method )

What you literally asked for begins here

Now, if you really has a use case for methods in the metaclass to be called from the class, the "one and obvious" way is to simply hardcode the call, as it was done before the existence of super

You can do either:

class B(metaclass=A):
   @classmethod
   def method(cls):
        A.method(cls)
        ...

Which is less dynamic, but less magic and more readable - or:

class B(metaclass=A):
   @classmethod
   def method(cls):
        cls.__class__.method(cls)
        ...

Which is more dynamic (the __class__ attribute works for cls just like it would work in the case B was just some instance of A like my example in the second paragraph: B.__class__ is A)

In both cases, you can guard yourself against calling a non existing method in the metaclass with a simple if hasattr(cls.__class__, "method"): ...