2

I'm trying to create a wrapper for methods. The decorator itself is implemented as a class that overwrites the __call__ method.

This works for decorating functions, but if a method is decorated this way, self is not bound to the decorated function.

The following is a minimal example:

import functools


class Wrapper:

    def __init__(self, func) -> None:
        super().__init__()
        self.__func = func
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        print("PreBar")
        result = self.__func(*args, **kwargs)
        print("PostBar")
        return result


def MyDecorator(func):
    return Wrapper(func)


class Foo:
    baz = "Baz"

    @MyDecorator
    def bar(self):
        print(self.baz)
        return self.baz


if __name__ == '__main__':
    foo = Foo()
    print(foo.bar)
    print(f"Result of foo.bar(): {foo.bar()}")

    print(Foo.bar)
    print(f"Result of Foo.bar(foo): {Foo.bar(foo)}")

This produces the following error, meaning that self is not automatically passed to the __call__ invocation as with normal methods:

Traceback (most recent call last):
  File "/mypath/test.py", line 35, in <module>
    print(f"Result of foo.bar(): {foo.bar()}")
  File "/mypath/test.py", line 14, in __call__
    result = self.__func(*args, **kwargs)
TypeError: bar() missing 1 required positional argument: 'self'

Question: How can the returned callable wrapper object be bound to self? Wrapping the object in a function to let python do the binding is an option, but not in my case, since the actual object is needed.

The question How can I decorate an instance method with a decorator class? is similar to what i want to achieve. The solution provided there works if only the binding is required. What i require though, is that the object instance that is returned when accessing the attribute is of type Wrapper, not of type functools.partial.

The second answer to that question also does not work, since the instance returned by __get__ is of type method and not Wrapper.

Mark G
  • 250
  • 4
  • 10
  • Possible duplicate of [How can I decorate an instance method with a decorator class?](https://stackoverflow.com/questions/30104047/how-can-i-decorate-an-instance-method-with-a-decorator-class) – Maurice Meyer Sep 09 '19 at 15:23
  • What's the point of `MyDecorator`? You could decorate `bar` with `Wrapper` directly. – chepner Sep 09 '19 at 15:29
  • @chepner You are right. I could have omitted the function for my minimal example. The original code uses a class MyDecorator, because the decorator takes parameters. – Mark G Sep 10 '19 at 12:02

1 Answers1

0

The following post addresses the Problem by simply wrapping the object in a function such that python can bind self with the native mechanism: Decorating method (class methods overloading)

As mentioned in the question this is not an option for me. The post also mentions the alternative: Implement the descriptor protocol for the Wrapper class. The question Decorating a method addresses this as well. The problem there is, that the approach used there only works with one instance of the class.

I came up with the following solution:

import functools


class Wrapper:

    def __init__(self, func, instance=None) -> None:
        super().__init__()
        self.__func = func
        self.__instance = instance
        self.__bindings = {}
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        print("PreBar")
        if self.__instance is not None:
            args =(self.__instance, ) + args
        result = self.__func(*args, **kwargs)
        print("PostBar")
        return result

    def __get__(self, instance, owner):
        try:
            return self.__bindings[(instance, owner)]
        except KeyError:
            binding = self.__bindings[(instance, owner)] = Wrapper(self.__func, instance)
            return binding


def MyDecorator(func):
    return Wrapper(func)


class Foo:
    baz = "Baz"

    @MyDecorator
    def bar(self):
        print(self.baz)
        return self.baz


if __name__ == '__main__':
    foo = Foo()
    print(foo.bar)
    print(f"Result of foo.bar(): {foo.bar()}")

    print(Foo.bar)
    print(f"Result of Foo.bar(foo): {Foo.bar(foo)}")

The difference being that the __get__ method for function descriptors creates a new Wrapper instance for each (instance, owner) pair. This way the wrapper is always bound to exactly one instance, or even the class itself.

Mark G
  • 250
  • 4
  • 10