Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decorator class and missing required positional arguments

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()
like image 456
andrey.georgiev Avatar asked Jul 18 '19 03:07

andrey.georgiev


1 Answers

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.

like image 58
jsbueno Avatar answered Oct 18 '22 22:10

jsbueno