Search code examples
pythonpython-decoratorsclass-method

Explicit class object needed as first parameter in self implemented classmethod decorator


I was writing a simple implementation of classmethod decorator for a better understanding of both decorator and classmethod. Here is the problem I faced. When I called the class method with an instance of my class, everything was fine, but calling the method with the class object fails with error:

>>**TypeError: wrapper() missing 1 required positional argument: 'cls'**

and when I called the method with the explicit class object as parameter it succeed, but a call to classmethod from class object should pass the class object itself as the first parameter, right?

import functools
import inspect

def myclassmethod(meth):
    @functools.wraps(meth)
    def wrapper(cls, *args, **kwargs):
        #print(f'obj:{cls}, cls:{cls.__class__}, isclass:{inspect.isclass(cls)}')
        return meth(cls if inspect.isclass(cls) else cls.__class__, *args, **kwargs)
    return wrapper

class MyDecoratedMethods(object):
    _name = 'ClassName'

    def __init__(self):
        self._name = 'InstanceName'

    def __repr__(self):
        return f'{self.__class__.__name__}({self._name!r})'

    @myclassmethod
    def classname(cls):
        return cls._name

MyDecoratedMethods().classname()
#MyDecoratedMethods.classname()
MyDecoratedMethods.classname(MyDecoratedMethods) # This works

Solution

  • To see what's going on, I removed the @functools.wraps(meth) line and then ran:

    print(MyDecoratedMethods.classname)
    # <function __main__.myclassmethod.<locals>.wrapper(cls, *args, **kwargs)>
    

    This shows us, that MyDecoratedMethods.classname is simply the function you created inside your decorator. And this function knows nothing about the class it is called from.

    However, we can override this behavior with Descriptors. A descriptor "knows" when it is accessed from a class or an instance and, most importantly, can distinguish between those cases (which is how regular methods are created).

    Here is a first try:

    class ClassMethod:
        def __init__(self, function):
            self.function = function
    
        def __get__(self, instance, cls):
            print(cls, instance)
    
    class MyDecoratedMethods(object):
        ...
    
        @ClassMethod
        def classname(cls):
            ...
    
    MyDecoratedMethods.classname
    # prints <class '__main__.MyDecoratedMethods'> None
    MyDecoratedMethods().classname
    # prints <class '__main__.MyDecoratedMethods'> <__main__.MyDecoratedMethods object ...>
    

    So we see that accessing the class method from the class sets instance to None and accessing it from an instance sets instance to that very object.

    But actually we don't need the instance at all. We can implement the logic without it.

    from functools import partial
    
    class ClassMethod:
        def __init__(self, function):
            self.function = function
    
        def __get__(self, instance, cls):
            # create a new function and set cls to the first argument
            return partial(self.function, cls)
    
    ...
    
    MyDecoratedMethods().classname()
    # "ClassName"
    MyDecoratedMethods.classname()
    # "ClassName"
    

    And we are done. Our custom descriptor accomplished two things:

    • It prevented the function from binding the instance to the first argument when calling it from an instance of the class (like a function would normally do to become a method)
    • It always binds the class to the first argument when it is accessed from either the class or an instance.

    Side note: Your approach to check whether an instance or the class called the function is also flawed (inspect.isclass(cls)). It will work for "normal" classes but not for meta classes, because then inspect.isclass returns True for the class and its instances.