0

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!

Codeman
  • 477
  • 3
  • 13
  • This question duplicates https://stackoverflow.com/questions/9416947/python-class-based-decorator-with-parameters-that-can-decorate-a-method-or-a-fun – Roman Suprotkin Nov 30 '21 at 07:09

2 Answers2

0

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?

0

There is a big picture you're missing.

@decorator
def foo(...):
   function_definition

is almost identical (except for some internal mangling) to

temp = foo
foo = decorator(temp)

It doesn't matter what the decorator is, as long as it can act like a function.

Your example is equivalent to:

baz = PrintLog("useless thing")(<saved defn of baz>)

Since PrintLog is a class, PrintLog(...) creates an instance of PrintLog. That instance has a __call__ method, so it can act like a function.

Some decorators are designed to take arguments. Some decorators are designed not to take arguments. Some, like @lru_cache, are pieces of Python magic which look to see if the "argument" is a function (so the decorator is being used directly) or a number/None, so that it returns a function that then becomes the decorator.

Frank Yellin
  • 9,127
  • 1
  • 12
  • 22
  • So are you saying that class decorators need the `()` to instantiate them? Or should i do something like `PL = PrintLog(); @PL`? – Codeman Nov 30 '21 at 08:26
  • No. I'm saying you need to read the documentation for the decorator before using it, and not make assumptions. Some decorators are functions and don't take arguments. Some are classes with a `__call__` method and do need arguments. – Frank Yellin Nov 30 '21 at 17:35
  • Alright. I think I get why i had that problem now, but is there any helpful documentation for decorators and such? – Codeman Dec 01 '21 at 03:12
  • Look up "Python decorator" in your favorite search engine and you'll find all sorts of information at whatever level your looking for. – Frank Yellin Dec 01 '21 at 03:15