Search code examples
pythoninheritancepython-decoratorsclass-method

cls behaviour in inherited classmethod of a decorated class


I'm trying to make some validations for the class methods of a class using one of the parameters used when calling them.

To do this, I'm using a decorator for the class that will apply a decorator to the required methods, which will perform a validation function using one of the parameters in the function.

This all works well for the base class (for this example I will call it Parent).

However, if I make another class which inherits Parent, (for this example I will call it Child), the inherited decorated classmethod no longer behaves normally.

The cls parameter inside the classmethod for the Child class is not Child as expected, but is Parent instead.

Taking the following example

import inspect


def is_number(word):
    if word.isdigit():
        print('Validation passed')
    else:
        raise Exception('Validation failed')


class ClassDecorator(object):

    def __init__(self, *args):
        self.validators = args

    def __decorateMethod(self):
        def wrapped(method):
            def wrapper(cls, word, *args, **kwargs):
                for validator in self.validators:
                    validator(word)
                return method(word, *args, **kwargs)
            return wrapper
        return wrapped

    def __call__(self, cls):
        for name, method in inspect.getmembers(cls):
            if name == 'shout':
                decoratedMethod = self.__decorateMethod()(method)
                setattr(cls, name, classmethod(decoratedMethod))
        return cls


@ClassDecorator(is_number)
class Parent(object):

    @classmethod
    def shout(cls, word):
        print('{} is shouting {}'.format(cls, word))

    @classmethod
    def say(cls):
        print('{} is talking'.format(cls))


class Child(Parent):
    pass


Parent.shout('123')
Child.shout('321')

Will result in the following output:

Validation passed
<class '__main__.Parent'> is shouting 123
Validation passed
<class '__main__.Parent'> is shouting 321

My questions are:

  • Why does the classmethod for Child get called with Parent as cls
  • Is it possible using this design to get the wanted behaviour?

P.S.: I've tried this on both Python 2.7.10 and Python 3.5.2 and have gotten the same behaviour


Solution

  • You are decorating the bound class method; it is this object that holds on to Parent and passes it into the original shout function when called; whatever cls is bound to in your wrapper() method is not passed in and ignored.

    Unwrap classmethods first, you can get to the underlying function object with the __func__ attribute:

    def __call__(self, cls):
        for name, method in inspect.getmembers(cls):
            if name == 'shout':
                decoratedMethod = self.__decorateMethod()(method.__func__)
                setattr(cls, name, classmethod(decoratedMethod))
        return cls
    

    You now have to take into account that your wrapper is handling an unbound function too, so pass on the cls argument or manually bind:

    # pass in cls explicitly:
    return method(cls, word, *args, **kwargs)
    
    # or bind the descriptor manually:
    return method.__get__(cls)(word, *args, **kwargs)