Colin
Colin

Reputation: 10820

Python decorator as class member

I want to create a decorator for class functions that will produce logging messages at the start and end of the functions with information specific to the class instance running the function. I've tried doing this by creating the decorator as class member, but that doesn't work because the decorator expects the function as the first argument, but as a class method it needs to be self. Here is what I've written so far that doesn't work:

import functools, logging

logging.basicConfig(format='%(asctime)s - %(levelname)s - %(funcName)s:%(message)s', 
                            datefmt='%Y-%m-%d %I:%M:%S %p',
                            level=logging.DEBUG)

class Foo:
    def __init__(self, bar):
        self.bar = bar
        
    def log(func):
        @functools.wraps(func)
        def wrapper_log(*args, **kwargs):
            logging.info(f"Started (instance {self.bar})")
            func(*args, **kwargs)
            logging.info(f"Finished (instance {self.bar}")
            return func(*args, **kwargs)
        return wrapper_log
        
    @log
    def test(self, a, b, c):
        pass
    
foo = Foo("bar")
foo.test(1, "b", [3, 4])

I can't move it out of the class and pass the class instance as an argument by decorating the class functions with @log(self) because self doesn't exist outside of the functions. How can I do this?

Upvotes: 0

Views: 434

Answers (1)

gioxc88
gioxc88

Reputation: 359

You can define the decorator outside the class and it will work just fine, but you need to refer explicitly to self in the wrapper signature.

The reason why this work is because all the definitions inside a function are evaluated only when the function is called. By the time the function test is called it has already been bounded to the instance and self will exists in its namespace.

import functools, logging

logging.basicConfig(format='%(asctime)s - %(levelname)s - %(funcName)s:%(message)s', 
                            datefmt='%Y-%m-%d %I:%M:%S %p',
                            level=logging.DEBUG)

def log(func):
    @functools.wraps(func)
    def wrapper_log(self, *args, **kwargs):
        logging.info(f"Started (instance {self.bar})")
        func(self, *args, **kwargs)
        logging.info(f"Finished (instance {self.bar}")
        return func(self, *args, **kwargs)
    return wrapper_log

class Foo:
    def __init__(self, bar):
        self.bar = bar
        
    @log
    def test(self, a, b, c):
        pass
    
foo = Foo("bar")
foo.test(1, "b", [3, 4])

This will output

2020-10-05 11:31:20 PM - INFO - wrapper_log:Started (instance bar)
2020-10-05 11:31:20 PM - INFO - wrapper_log:Finished (instance bar

Upvotes: 2

Related Questions