0

Problem Description I want to use a decorator to define a class method, but this requires me to manually give the 'self' object when I shouldn't have to provide that.

def func_wrapper(func): 
    def call_func(self):
        print(self.a)
        func()
    return call_func
def func():
    print('hello')

class test:
    def __init__(self, func):
        self.a = 0
        self.call_func = func_wrapper(func)

mytest = test(func)
#mytest.call_func() #why does this not work? 
mytest.call_func(mytest) #this works 

I want to be able to mytest.call_func() but this doesn't work, presumably because call_func is bound to the func_wrapper and not mytest. If I manually pass in the object, e.g. mytest.call_func(mytest) this will work, but I don't want to have to manually pass in the object - this creates inconsistent call signatures if one inherited the test class and wrote their own call_func method, because then the method would be properly bound to the class.

Solution Attempts

def func_wrapper2(func, obj): 
    def call_func():
        print(obj.a)
        func()
    return call_func
class test:
    def __init__(self, func):
        self.a = 0
        self.call_func = func_wrapper2(func, self)

Here is a solution which lets me test.call_func() as desired, but here func_wrapper is not a true decorator as it requires to be passed in the object as well.

Looking on the web I found this blog https://medium.com/@vadimpushtaev/decorator-inside-python-class-1e74d23107f6 which talks about this issue and recommends to define the decorator either in a nested class, or a helper class. However their solution doesn't seem to work and I am getting type errors from passing the wrong number of inputs.

class test2: 
    class test2helper: 
        @classmethod
        def func_wrapper(func):
            print(self.a)
            func()
    def __init__(self):
        self.a = 0
    @test2helper.func_wrapper
    def call_func(self):
        print('hello')

So what is the proper way to use decorators with class methods? Every way to do it seems to cause different issues with how the self is being handled. I am going to use the func_wrapper2 design unless there is a better way to do this.

Taw
  • 376
  • 1
  • 8

2 Answers2

1

You are missing one level:

class test2: 
    class test2helper: 
        @classmethod
        def decorator(cls, func):  # this must return a function!
            def func_wrapper(self):  # ... namely this one, the "wrapper"
                print(self.a)  # ... where you have access to the instance
                func(self)  # ... upon which the method is called
            return func_wrapper

    def __init__(self):
        self.a = 0

    @test2helper.decorator
    def call_func(self):
        print('hello')

>>> t = test2()
>>> t.call_func()
0
hello

Or, if you want to go with the earlier attempt without nested class:

def decorator(func):  # you are decorating an unbound function!
    def func_wrapper(obj): 
        print(obj.a)
        func(obj)  # which has to be passed all the arguments
    return func_wrapper

class test:
    def __init__(self):
        self.a = 0

    @decorator
    def call_func(self):
        print('hello')
schwobaseggl
  • 55,463
  • 4
  • 39
  • 63
1

You can define a class decorator to do what you want:

def class_decorator(cls):
    def call_func(self):
        print(self.a)
        return func()

    setattr(cls, 'call_func', call_func)
    return cls

def func():
    print('hello')


@class_decorator
class Test:
    def __init__(self, func):
        self.a = 0


mytest = Test(func)
mytest.call_func()  # This now works.

Output:

0
hello
martineau
  • 99,260
  • 22
  • 139
  • 249