Search code examples
pythonmetaclasspython-internals

Decorating class methods by overriding __new__ doesn't work?


I want to decorate all the methods of my class. I have written a sample small decorator for illustration purpose here.

Decorator:

def debug(func):
    msg = func.__name__

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(msg)
        return func(*args, **kwargs)
    return wrapper 

def debugmethods(cls):
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls

Now I want to decorate all the methods of my class. One simple way is to use @debugmethods annotation on top of my class but I am trying to understand two other different approaches for doing so.

a) Overriding __new__

class Spam:
    def __new__(cls, *args, **kwargs):
        clsobj = super().__new__(cls)
        clsobj = debugmethods(clsobj)
        return clsobj

    def __init__(self):
        pass

    def foo(self):
        pass

    def bar(self):
        pass

spam = Spam()
spam.foo()

b) Writing metaclass

class debugmeta(type):
    def __new__(cls, clsname, bases, clsdict):
        clsobj = super().__new__(cls, clsname, bases, clsdict)
        clsobj = debugmethods(clsobj)
        return clsobj

class Spam(metaclass = debugmeta):     
    def foo(self):
        pass

    def bar(self):
        pass

spam = Spam()
spam.foo()

I am not sure

  1. Why " a) overriding __new__ " doesn't work ?
  2. Why signature of method __new__ is different in metaclass?

Can someone help me understand what am I missing here.


Solution

  • You appear to be confused between __new__ and metaclasses. __new__ is called to create a new object (an instance from a class, a class from a metaclass), it is not a 'class created' hook.

    The normal pattern is:

    • Foo(...) is translated to type(Foo).__call__(Foo, ...), see special method lookups for why that is. The type() of a class is it's metaclass.
    • The standard type.__call__ implementation used when Foo is a custom Python class will call __new__ to create a new instance, then call the __init__ method on that instance if the result is indeed an instance of the Foo class:

      def __call__(cls, *args, **kwargs):  # Foo(...) -> cls=Foo
          instance = cls.__new__(cls, *args, **kwargs)  # so Foo.__new__(Foo, ...)
          if isinstance(instance, cls):
              instance.__init__(*args, **kwargs)        # Foo.__init__(instance, ...)
          return instance
      

      So Foo.__new__ is not called when the Foo class itself is created, only when instances of Foo are created.

    You don't usually need to use __new__ in classes, because __init__ suffices to initialise the attributes of instances. But for immutable types, like int or tuple, you can only use __new__ to prepare the new instance state, as you can't alter the attributes of an immutable object once it is created. __new__ is also helpful when you want change what kinds of instances ClassObj() produce (such as creating singletons or producing specialised subclasses instead).

    The same __call__ -> __new__ and maybe __init__ process applies to metaclasses. A class Foo: ... statement is implemented by calling the metaclass to create a class object, passing in 3 arguments: the class name, the class bases, and the class body, as a dictionary usually. With class Spam(metaclass = debugmeta): ..., that means debugmeta('Spam', (), {...}) is called, which means debugmeta.__new__(debugmeta, 'Spam', (), {...}) is called.

    Your first attempt a, setting Spam.__new__ doesn't work, because you are not creating a class object there. Instead, super().__new__(cls) creates an empty Spam() instance with no attributes, so vars() returns an empty dictionary and debugmethods() ends up doing nothing.

    If you want to hook into class creation, then you want a metaclass.