6

I'm having problems with a wrapper class, and can't figure out what I'm doing wrong. How do I go about getting that wrapper working with any class function with the 'self' argument?

This is for Python 3.7.3. The thing is I remember the wrapper working before, but it seems something has changed...maybe I'm just doing something wrong now that I wasn't before.

class SomeWrapper:

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        # this fails because self is not passed
        # ERROR: __init__() missing 1 required positional argument: 'self'
        func_ret = self.func(*args, **kwargs)

        # this is also wrong, because that's the wrong "self"
        # ERROR: 'SomeWrapper' object has no attribute 'some_func'
        # func_ret = self.func(self, *args, **kwargs)

        return func_ret


class SomeClass:

    SOME_VAL = False

    def __init__(self):
        self.some_func()
        print("Success")

    @SomeWrapper
    def some_func(self):
        self.SOME_VAL = True

    def print_val(self):
        print(self.SOME_VAL)


SomeClass().print_val()
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • The `__call__` method should return a callable, not the result of the callable being called. And the decorator should be an instance of it, not the class. – Klaus D. Jul 18 '19 at 03:57
  • Isn't that the case only with decorator functions? I was under the impression that you can just call the decorated function within ```__call__```. – andrey.georgiev Jul 18 '19 at 08:57
  • yes- you are right in this respect - and Klaus mistook it. – jsbueno Jul 19 '19 at 00:04
  • The class will be first called with the method-to-be-decorated passed as the "func" parameter to `__init__`. The resulting instance is callable, because the class has a `__call__` method, and it will be run when this instance is called from within the class instance. Yes, I am sure of that. The `self` information is not inserted, becuse the language mechanism to attach this information in a method call uses the class' `__get__` method, as I detail bellow. – jsbueno Jul 19 '19 at 00:20
  • @jsbueno I see. – alkasm Jul 19 '19 at 00:31

1 Answers1

6

So, what happens is that in python 3, for method declarations work as methods, when they are just defined as functions inside the class body, what happens is that the language makes use of the "descriptor protocol".

And to put it simply, an ordinary method is just a function, until it is retrieved from an instance: since the function has a __get__ method, they are recognized as descriptors, and the __get__ method is the one responsible to return a "partial function" which is the "bound method", and will insert the self parameter upon being called. Without a __get__ method, the instance of SomeWrapper when retrieved from an instance, has no information on the instance.

In short, if you are to use a class-based decorator for methods, you not only have to write __call__, but also a __get__ method. This should suffice:


from copy import copy

class SomeWrapper:

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
 
        func_ret = self.func(self.instance, *args, **kwargs)

        return func_ret

    def __get__(self, instance, owner):
        # self here is the instance of "somewrapper"
        # and "instance" is the instance of the class where
        # the decorated method is.
        if instance is None:
            return self
        bound_callable = copy(self)
        bound_callable.instance = instance
        return self

Instead of copying the decorator instance, this would also work:

from functools import partial

class SomeWrapper:
   ...
   
   def __call__(self, instance, *args, **kw):
       ...
       func_ret = self.func(instance, *args, **kw)
       ...
       return func_ret

   def __get__(self, instance, owner):
       ...
       return partial(self, instance)

Both the "partial" and the copy of self are callables that "know" from which instances they where "__got__" from.

Simply setting the self.instance attribute in the decorator instance and returning self would also work, but limited to a single instance of the method being used at a time. In programs with some level of parallelism or even if the code would retrieve a method to call it lazily (such as using it to a callback), it would fail in a spectacular and hard to debug way, as the method would receive another instance in its "self" parameter.

Community
  • 1
  • 1
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • I see, adding the ```__get__``` fixed it for me, thank you for the explanation. Would you say that one method is better than the other? – andrey.georgiev Jul 19 '19 at 01:07
  • I think given that thhe "copy" method creates two "kinds" of the decorator "bound" and "unbound", the method with "partial" could be better, just to avoid having two very similar objects, but different in nature, around. Performance wise they should be similar. – jsbueno Jul 19 '19 at 01:19
  • Also, check this answer by my for further ways to do it: https://stackoverflow.com/questions/10294014/python-decorator-best-practice-using-a-class-vs-a-function/10300995#10300995 - in this answer, `__get__` is not used because the decorator takes some parameters prior to decorating the function - the decoration than takes place in `__call__`, which was @KlausD. confusion in the first comment to the question. – jsbueno Jul 19 '19 at 01:21