303

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?

4
  • Is that a custom decorator that you have control over, or one that you can't change? Commented Jul 30, 2012 at 23:35
  • 1
    It's my decorator, so I have complete control over it Commented Jul 30, 2012 at 23:36
  • 2
    It gets called before init I think is the problem... Commented Jul 30, 2012 at 23:37
  • 11
    The problem is that self doesn't exist at function definition time. You need to make it into a partial function. Commented Jul 30, 2012 at 23:38

9 Answers 9

356

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 
Sign up to request clarification or add additional context in comments.

3 Comments

is there a way to pass @staticmethod directly in decorator? (in general). I found that we can not reference Even class in decorator.
@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...
How do you manage a decorator you don't have control over like @discord.ui.select in discord.py?
129

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! 

2 Comments

IMO, this is superior to stackoverflow.com/a/11731208/257924. 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 stackoverflow.com/a/56322968/257924 which abandoned that sugar and requires me to look deep inside the __init__ method.
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

2 Comments

@raphael In this setup I can't seem to access _lambda or pattern. How can I remedy that.
@Raphael: How can I do the same for a classmethod, since here all the methods are instance methods.
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! 

1 Comment

This is much more practical. E.g. the top-voted example hardcodes the "url" attribute into the decorator definition.
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() 

Comments

8

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://stackoverflow.com/questions/11731136/class-method-decorator-with-self-arguments") # call decorated function client.get() 

output:

checking authorisation for 'https://stackoverflow.com/questions/11731136/class-method-decorator-with-self-arguments'... work is done... 

Comments

4

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.

Comments

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) 

Comments

1

You can use a decorator with a wrapper function that decorates the given method on the fly when the wrapper is called as a bound method, at which point self becomes available.

The idea can be generalized into a high-order decorator with_self, which transforms an action function (my_check_authorization in the example below) into a method-wrapping decorator:

def with_self(func): def decorator(method): def wrapper(self, *args, **kwargs): return func(self)(method)(self, *args, **kwargs) return wrapper return decorator @with_self def my_check_authorization(self): return check_authorization("some_attr", self.url) 

so that you can then simply decorate your method with my_check_authorization:

class Client(object): def __init__(self, url): self.url = url @my_check_authorization def get(self): do_work() 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.