Class method decorator with self arguments?
Asked Answered
P

8

288

How do I pass a class field to a decorator on a class method as an argument? What I want to do is something like:

class Client(object):
    def __init__(self, url):
        self.url = url

    @check_authorization("some_attr", self.url)
    def get(self):
        do_work()

It complains that self does not exist for passing self.url to the decorator. Is there a way around this?

Pearcy answered 30/7, 2012 at 23:30 Comment(4)
Is that a custom decorator that you have control over, or one that you can't change?Portend
It's my decorator, so I have complete control over itPearcy
It gets called before init I think is the problem...Breastbone
The problem is that self doesn't exist at function definition time. You need to make it into a partial function.Chrisy
T
341

Yes. Instead of passing in the instance attribute at class definition time, check it at runtime:

def check_authorization(f):
    def wrapper(*args):
        print args[0].url
        return f(*args)
    return wrapper

class Client(object):
    def __init__(self, url):
        self.url = url

    @check_authorization
    def get(self):
        print 'get'

>>> Client('http://www.google.com').get()
http://www.google.com
get

The decorator intercepts the method arguments; the first argument is the instance, so it reads the attribute off of that. You can pass in the attribute name as a string to the decorator and use getattr if you don't want to hardcode the attribute name:

def check_authorization(attribute):
    def _check_authorization(f):
        def wrapper(self, *args):
            print getattr(self, attribute)
            return f(self, *args)
        return wrapper
    return _check_authorization
Thew answered 30/7, 2012 at 23:38 Comment(3)
is there a way to pass @staticmethod directly in decorator? (in general). I found that we can not reference Even class in decorator.Blandina
@ShivKrishnaJaiswal what exactly do you mean by passing @staticmethod directly in decorator? You can get rid of object reference requirement by using the @staticmethod decorator, however, it won't solve the OP's problem.... Sure, you can decorate there wrapper within the decorator as @staticmethod and it should work if used correctly (tested on python 3.9), but I see no reason to do it this way. Such a decorator will become unusable on functions without the class. Moreover, you can use @staticmethod even over already decorated method if needed...Grist
How do you manage a decorator you don't have control over like @discord.ui.select in discord.py?Calise
M
116

A more concise example might be as follows:

#/usr/bin/env python3
from functools import wraps

def wrapper(method):
    @wraps(method)
    def _impl(self, *method_args, **method_kwargs):
        method_output = method(self, *method_args, **method_kwargs)
        return method_output + "!"
    return _impl

class Foo:
    @wrapper
    def bar(self, word):
        return word

f = Foo()
result = f.bar("kitty")
print(result)

Which will print:

kitty!
Musty answered 29/4, 2016 at 18:13 Comment(1)
IMO, this is superior to https://mcmap.net/q/107436/-class-method-decorator-with-self-arguments. It demonstrates how the internal function _impl can access self to manipulate that self for whatever purpose. I needed to build a simple method decorator that incremented a self.id on a subset of the methods in a class, and only those methods in a class that had the "@" decoration syntax applied to it. That Syntactic Sugar pays it forward to my Future Self, as compared to https://mcmap.net/q/107436/-class-method-decorator-with-self-arguments which abandoned that sugar and requires me to look deep inside the __init__ method.Tartaric
P
49
from re import search
from functools import wraps

def is_match(_lambda, pattern):
    def wrapper(f):
        @wraps(f)
        def wrapped(self, *f_args, **f_kwargs):
            if callable(_lambda) and search(pattern, (_lambda(self) or '')): 
                f(self, *f_args, **f_kwargs)
        return wrapped
    return wrapper

class MyTest(object):

    def __init__(self):
        self.name = 'foo'
        self.surname = 'bar'

    @is_match(lambda x: x.name, 'foo')
    @is_match(lambda x: x.surname, 'foo')
    def my_rule(self):
        print 'my_rule : ok'

    @is_match(lambda x: x.name, 'foo')
    @is_match(lambda x: x.surname, 'bar')
    def my_rule2(self):
        print 'my_rule2 : ok'



test = MyTest()
test.my_rule()
test.my_rule2()

ouput: my_rule2 : ok

Palmer answered 3/5, 2013 at 11:52 Comment(2)
@raphael In this setup I can't seem to access _lambda or pattern. How can I remedy that.Itis
@Raphael: How can I do the same for a classmethod, since here all the methods are instance methods.Scorn
I
17

Another option would be to abandon the syntactic sugar and decorate in the __init__ of the class.

def countdown(number):
    def countdown_decorator(func):
        def func_wrapper():
            for index in reversed(range(1, number+1)):
                print(index)
            func()
        return func_wrapper
    return countdown_decorator

class MySuperClass():
    def __init__(self, number):
        self.number = number
        self.do_thing = countdown(number)(self.do_thing)
    
    def do_thing(self):
        print('im doing stuff!')


myclass = MySuperClass(3)

myclass.do_thing()

which would print

3
2
1
im doing stuff!
Injurious answered 27/5, 2019 at 9:10 Comment(1)
This is much more practical. E.g. the top-voted example hardcodes the "url" attribute into the decorator definition.Hemistich
D
11

I know this issue is quite old, but the below workaround hasn't been proposed before. The problem here is that you can't access self in a class block, but you can in a class method.

Let's create a dummy decorator to repeat a function some times.

import functools
def repeat(num_rep):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_rep):
                value = func(*args, **kwargs)
            return 
        return wrapper_repeat
    return decorator_repeat
class A:
    def __init__(self, times, name):
        self.times = times
        self.name = name
    
    def get_name(self):
        @repeat(num_rep=self.times)
        def _get_name():
            print(f'Hi {self.name}')
        _get_name()
Decorous answered 29/4, 2021 at 20:59 Comment(0)
G
7

I know this is an old question, but this solution has not been mentioned yet, hopefully it may help someone even today, after 8 years.

So, what about wrapping a wrapper? Let's assume one cannot change the decorator neither decorate those methods in init (they may be @property decorated or whatever). There is always a possibility to create custom, class-specific decorator that will capture self and subsequently call the original decorator, passing runtime attribute to it.

Here is a working example (f-strings require python 3.6):

import functools

# imagine this is at some different place and cannot be changed
def check_authorization(some_attr, url):
        def decorator(func):
                @functools.wraps(func)
                def wrapper(*args, **kwargs):
                        print(f"checking authorization for '{url}'...")
                        return func(*args, **kwargs)
                return wrapper
        return decorator

# another dummy function to make the example work
def do_work():
        print("work is done...")

###################
# wrapped wrapper #
###################
def custom_check_authorization(some_attr):
        def decorator(func):
                # assuming this will be used only on this particular class
                @functools.wraps(func)
                def wrapper(self, *args, **kwargs):
                        # get url
                        url = self.url
                        # decorate function with original decorator, pass url
                        return check_authorization(some_attr, url)(func)(self, *args, **kwargs)
                return wrapper
        return decorator
        
#############################
# original example, updated #
#############################
class Client(object):
        def __init__(self, url):
                self.url = url
    
        @custom_check_authorization("some_attr")
        def get(self):
                do_work()

# create object
client = Client(r"https://mcmap.net/q/107436/-class-method-decorator-with-self-arguments")

# call decorated function
client.get()

output:

checking authorisation for 'https://mcmap.net/q/107436/-class-method-decorator-with-self-arguments'...
work is done...
Grist answered 4/2, 2021 at 14:4 Comment(0)
P
5

You can't. There's no self in the class body, because no instance exists. You'd need to pass it, say, a str containing the attribute name to lookup on the instance, which the returned function can then do, or use a different method entirely.

Prismatoid answered 30/7, 2012 at 23:38 Comment(0)
W
3

It will be very useful to have a general-purpose utility, that can turn any decorator for functions, into decorator for methods. I thought about it for an hour, and actually come up with one:

from typing import Callable
Decorator = Callable[[Callable], Callable]

def decorate_method(dec_for_function: Decorator) -> Decorator:

    def dec_for_method(unbounded_method) -> Callable:
        # here, `unbounded_method` will be a unbounded function, whose
        # invokation must have its first arg as a valid `self`. When it 
        # return, it also must return an unbounded method.
        def decorated_unbounded_method(self, *args, **kwargs):
            @dec_for_function
            def bounded_method(*args, **kwargs):
                return unbounded_method(self, *args, **kwargs)
            return bounded_method(*args, **kwargs)

        return decorated_unbounded_method

    return dec_for_method

The usage is:

# for any decorator (with or without arguments)
@some_decorator_with_arguments(1, 2, 3)
def xyz(...): ...

# use it on a method:
class ABC:
  @decorate_method(some_decorator_with_arguments(1, 2, 3))
  def xyz(self, ...): ...

Test:

def dec_for_add(fn):
    """This decorator expects a function: (x,y) -> int.

    If you use it on a method (self, x, y) -> int, it will fail at runtime.
    """
    print(f"decorating: {fn}")
    def add_fn(x,y):
        print(f"Adding {x} + {y} by using {fn}")
        return fn(x,y)
    return add_fn


@dec_for_add
def add(x,y):
    return x+y

add(1,2)  # OK!


class A:
    @dec_for_add
    def f(self, x, y):
        # ensure `self` is still a valid instance
        assert isinstance(self, A)
        return x+y

# TypeError: add_fn() takes 2 positional arguments but 3 were given
# A().f(1,2)
    

class A:
    @decorate_method(dec_for_add)
    def f(self, x, y):
        # ensure `self` is still a valid instance
        assert isinstance(self, A)
        return x+y

# Now works!!
A().f(1,2)
Wentz answered 23/3, 2022 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.