Dharmvir Tiwari
Dharmvir Tiwari

Reputation: 916

Decorators in python: Difference in the args interpretation in class and function based decorators

I am trying to create a generic function lifecycle handler in python. Working in brief:

The issue which I encountered while doing the cleanup is as follows: Function-based decorator:

def handler(exception=Exception,cleanup=None):
def func_wrapper(func):
    def wrapper(*args,**kwargs):
        response=None
        try:
            print(args)
            response=func(*args,**kwargs)
        except exception as ex:
            print(f'Exception occurred:{str(ex)}\nCleaning up resources')
            #Passing the object for cleanup, it fails for class based decorators as it does not passes self as argument
            cleanup(args[0])
        return response
    return wrapper
return func_wrapper

The data which is supposed to be cleaned up is stored in the class instance and is cleaned based on the method provided. For example:

Output:

Exception occurred:division by zero
Cleaning up resources
Cleaning:John Doe

I was more inclinded towards Class based decorator.

class LifeCycleHandler:
    def __init__(self,*,func=None,exception=Exception,cleanup=None):
        self.__exception=exception
        self.__cleanup=cleanup
        self.__func=func

    def __call__(self,*args,**kwargs):
        response=None
        try:
            print(args)
            response=self.__func(*args,**kwargs)
        except self.__exception as ex:
            print(f'Exception occurred:{str(ex)}\n cleaning up resources')
            #Passing the object for cleanup
            self.__cleanup(args[0])
        return response
def lifecycle_handler(exception=Exception,cleanup=None):
    def wrapper(func): 
        response=LifeCycleHandler(func=func,exception=exception,cleanup=cleanup)
        return response
    return wrapper

With class based decorator with similar functionality i faced the following error:

()
Exception occurred:test_data() missing 1 required positional argument: 'self'
 cleaning up resources
Traceback (most recent call last):
  File "test.py", line 27, in __call__
    response=self.__func(*args,**kwargs)
TypeError: test_data() missing 1 required positional argument: 'self'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test.py", line 54, in <module>
    test.test_data()
  File "test.py", line 31, in __call__
    self.__cleanup(args[0])
IndexError: tuple index out of range

Can someone guide me regarding the the argument interpretation for callable classes?

Upvotes: 2

Views: 65

Answers (1)

Ke Zhang
Ke Zhang

Reputation: 987

If my comment correct, you could add __get__ to LifeCycleHandler.

def __get__(self, obj, type=None):
    return functools.partial(self.__call__, obj)

This will make test_data become a non-data descriptor. I assume you already know descriptor. If not, it's definitely worth to check it.

Back to your question, from trace back, your assumed that python will help you pass the caller instance which is instance of Test as second argument to __call__. That's not true. However, that's true in __get__.

Your core logic (try/except block) needs are:

  • instance of Test, because you need access to main_data.
  • instance of LifeCycleHandler in, because you need access to your self.__func.
  • args in which isn't acceptable in __get__, but you could have them in __call__.

For example, you have test code below:

t = Test(123)
t.test_data()

t.test_data will invoke __get__. In its arguments, self is an instance of LifeCycleHandler and obj is t (instance of Test). __get__ returned a callable function(__call__) in which its first argument is partially feed by obj.

Upvotes: 1

Related Questions