Dekorator metody klasowej z własnymi argumentami?

Jak przekazać pole klasy do dekoratora w metodzie klasy jako argument? To co chcę zrobić to coś w stylu:

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

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

Skarży się, że jaźń nie istnieje dla przekazania self.url dekoratorowi. Można to jakoś obejść?

Author: martineau, 2012-07-30

6 answers

Tak. Zamiast przekazywać atrybut instancji w czasie definicji klasy, sprawdź go w czasie wykonywania:

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

Dekorator przechwytuje argumenty metody; pierwszy argument jest instancją, więc odczytuje atrybut poza tym. Możesz przekazać nazwę atrybutu jako ciąg znaków do dekoratora i użyć getattr, jeśli nie chcesz kodować na twardo nazwy atrybutu:

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
 240
Author: li.davidm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-19 23:17:24

Bardziej zwięzły przykład może być następujący:

#/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)

Który wydrukuje:

kitty!
 47
Author: maxywb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-26 15:46:48
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

 41
Author: Raphaël,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-19 13:41:31

Inną opcją byłoby porzucenie cukrów składniowych i udekorowanie w __init__ klasy.

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()

Które wydrukują

3
2
1
im doing stuff!
 9
Author: Arwalk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-20 08:34:55

Nie możesz. nie ma self w ciele klasy, ponieważ żadna instancja nie istnieje. Trzeba by przekazać, powiedzmy, str zawierający nazwę atrybutu do wyszukiwania na instancji, co może zrobić zwracana funkcja, lub użyć zupełnie innej metody.

 5
Author: Julian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-30 23:38:03

Wiem, że to stare pytanie, ale o tym rozwiązaniu jeszcze nie wspomniano, mam nadzieję, że może komuś pomóc nawet dzisiaj, po 8 latach.

A co z opakowaniem opakowania? Załóżmy, że nie można zmienić dekoratora ani ozdobić tych metod w init (mogą być @ Nieruchomości urządzone lub cokolwiek). Zawsze istnieje możliwość stworzenia niestandardowego, specyficznego dla klasy dekoratora, który uchwyci siebie, a następnie wywoła oryginał dekorator, przekazując do niego atrybut runtime.

Oto działający przykład (F-stringi wymagają Pythona 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()

Wyjście:

checking authorisation for 'https://stackoverflow.com/questions/11731136/class-method-decorator-with-self-arguments'...
work is done...
 0
Author: Eamileann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-02-04 14:04:08