Search code examples
pythonpython-3.xclassdecoratorclass-decorator

Class decorators for methods in classes


How do class decorators for methods in classes work? Here is a sample of what I've done through some experimenting:

from functools import wraps


class PrintLog(object):

    def __call__(self, func):
        @wraps(func)
        def wrapped(*args):
            print('I am a log')
            return func(*args)
        return wrapped


class foo(object):
    def __init__(self, rs: str) -> None:
        self.ter = rs

    @PrintLog()
    def baz(self) -> None:
        print('inside baz')


bar = foo('2')
print('running bar.baz()')
bar.baz()

And this works perfectly fine. However, I was under the impression that decorators do not need to be called with (), but when I remove the brackets from @PrintLog(), I get this error:

    def baz(self) -> None:
TypeError: PrintLog() takes no arguments

Is there something I am missing/do not understand? I've also tried passing in a throwaway arg with __init__(), and it works.

class PrintLog(object):

    def __init__(self, useless):
        print(useless)

    def __call__(self, func):
        @wraps(func)
        def wrapped(*args):
            print('I am a log')
            return func(*args)
        return wrapped

class foo(object):
    def __init__(self, rs: str) -> None:
        self.ter = rs

    @PrintLog("useless arg that I'm passing to __init__")
    def baz(self) -> None:
        print('inside baz')

Again, this works, but I don't want to pass any argument to the decorator.

tl;dr: This question in python 3.x.

Help appreciated!


Solution

  • Class decorators accept the function as a subject within the __init__ method (hence the log message), so your decorator code should look like:

    class PrintLog(object):
    
        def __init__(self, function):
            self.function = function
    
        def __call__(self):
            @wraps(self.function)
            def wrapped(*args):
                print('I am a log')
                return self.function(*args)
            return wrapped
    

    Sorry if this doesn’t work, I’m answering on my mobile device.

    EDIT:

    Okay so this is probably not what you want, but this is the way to do it:

    from functools import update_wrapper, partial, wraps
    
    
    class PrintLog(object):
    
        def __init__(self, func):
            update_wrapper(self, func)
            self.func = func
        
        def __get__(self, obj, objtype):
            """Support instance methods."""
            return partial(self.__call__, obj)
    
        def __call__(self, obj, *args, **kwargs):
            @wraps(self.func)
            def wrapped(*args):
                print('I am a log')
                return self.func(*args)
            return wrapped(obj, *args)
    
    
    class foo(object):
        def __init__(self, rs: str) -> None:
            self.ter = rs
    
        @PrintLog
        def baz(self) -> None:
            print('inside baz')
    
    
    bar = foo('2')
    print('running bar.baz()')
    bar.baz()
    

    The decorator has to have the __get__ method defined because you're applying the decorator to an instance method. How would a descriptor have the context of the foo instance?

    Ref: Decorating Python class methods - how do I pass the instance to the decorator?