Search code examples
pythonmetaclassclass-methodmagic-methods

override numeric magic method as classmethod


I try to use metaclass to implement numeric magic methods for several classes, then override some of them in the "class instance".

from abc import ABCMeta
class Meta(ABCMeta):
  def __add__(self, other):
    print('meta')

class C(metaclass=Meta):
  @classmethod
  def __add__(cls, other):
    print('C')

C + 3 # meta
C.__add__(3) # C

It shows that the + operator is directly called with Meta's __add__, but C.__add__ calls getattr(C, '__add__') and correctly use the overriden class method.
I've tried several code pieces to confirm that.

class Meta(ABCMeta):
  def call(self):
    print('meta')

class C(metaclass=Meta):
  @classmethod
  def call(cls):
    print('C')

C.call() # C
class C:
  @classmethod
  def __add__(cls, other):
    print('C')

C + 3 # TypeError
C.__add__(3) # C

I guess something special happends when I "register" my implmentation of __add__, so the decorator @classmethod does not work.
So, how does python evaluates the expression C + 3? If I want to override __add__ for C, is there any solution other than inherit another metaclass from Meta?


Solution

  • So, how does python evaluates the expression C + 3?

    What takes place here is that Python's method and attribute search priority are different from ordinary methods (like call), and methods that are intended to be called indirectly by the language, through the use of an operator (like __add__)

    For the later, Python defers directly to the slot in the class of the instance being added. That is: When you use "C" with the + operator, its class is "Meta", and Meta.__add__ is called.

    In an ordinary attribute access using the . notation (either C.call or even C.__add__(insteaf of using +)), the normal rules for attribute access - which are implemented in object.__getattribute__ are followed - I've summarized the precedence in this answer. But in short: for dotted access, the classmethod in the class "instance" is retrieved before the method defined in the "class'class" (just as would happen for an attribute in the __dict__ of an "ordinary instance" compared to the same attribute in its class).

    As for: "If I want to override __add__ for C, is there any solution other than inherit another metaclass from Meta?"

    The "obvious way" would be to have a separate metaclass for each class you want to implement a "class __add__" method to. However, you can write Meta.__add__ so that it, upon being called, check for the existence of an __add__ method in the class, and forwards the execution of the opertation to it.

    from abc import ABCMeta
    
    class Meta(ABCMeta):
        def __add__(cls, other):
            print('meta')
            cls_add = getattr(cls, "__add__", None)
            if not cls_add:
                return NotImplemented    
            # checks for __add__ both as an ordinary and as a classmethod
            if (unbound:= getattr(method, "__func__", None)) is not  None and unbound is __class__.__add__ :
                # getattr returned an __add__ method from the metaclass (this very same method here)
                raise NotImplemented
            if unbound:  # if this is not None, __add__ is a class method
                return method(other)
            return method(cls, other) # __add__ is an ordinary method, and we have to fill the 'self' parameter with cls