Чи може декоратор методу екземпляра отримати доступ до класу?


109

У мене є щось приблизно так. В основному мені потрібно отримати доступ до класу методу екземпляра від декоратора, який використовується методом екземпляра в його визначенні.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

Код, як є, дає:

AttributeError: об’єкт 'function' не має атрибута 'im_class'

Я знайшов подібне запитання / відповіді - Python decorator змушує функцію забути, що вона належить до класу та Get class в Python decorator - але вони покладаються на обхід, який захоплює екземпляр під час виконання, витягуючи перший параметр. У моєму випадку я буду викликати метод на основі інформації, зібраної з його класу, тому я не можу дочекатися входу дзвінка.

Відповіді:


68

Якщо ви використовуєте Python 2.6 або новішу версію, ви можете використовувати декоратор класів, можливо, щось подібне (попередження: неперевірений код).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

Декоратор методу позначає метод як такий, який представляє інтерес, додаючи атрибут "use_class" - функції та методи також є об'єктами, тому ви можете приєднати до них додаткові метадані.

Після створення класу декоратор класу проходить всі методи і робить все необхідне для позначених методів.

Якщо ви хочете, щоб усі методи впливали, тоді ви можете залишити декоратор методу і просто використовувати декоратор класу.


2
Дякую, я думаю, що це маршрут, яким потрібно пройти. Лише один додатковий рядок коду для будь-якого класу, я хотів би використовувати цей декоратор. Можливо, я міг би використовувати спеціальний метаклас і виконувати цю саму перевірку під час нового ...?
Карл Г

3
Кожен, хто намагається використовувати це за допомогою staticmethod або classmethod, захоче прочитати цей PEP: python.org/dev/peps/pep-0232 Не впевнений, що це можливо, тому що ви не можете встановити атрибут для класу / статичного методу, і я думаю, що вони збиваються. створити будь-які спеціальні атрибути функції, коли вони застосовуються до функції.
Карл Г

Тільки те, що я шукав, для моєї ОСБ на базі DBM ... Дякую, чувак.
Coyote21

Ви повинні використовувати inspect.getmro(cls)для обробки всіх базових класів у декораторі класів для підтримки успадкування.
шламар

1
О, на самому ділі, схоже , inspectна допомогу stackoverflow.com/a/1911287/202168
Anentropic

16

Оскільки python 3.6 ви можете використовувати object.__set_name__для цього дуже простий спосіб. Документ заявляє, що __set_name__"викликається в момент створення власника класу власника ". Ось приклад:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

Зауважте, що його викликають у час створення класу:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

Якщо ви хочете дізнатися більше про те, як створюються класи та зокрема, коли саме вони __set_name__викликаються, ви можете звернутися до документації на тему "Створення об'єкта класу" .


1
Як би це виглядало для використання декоратора з параметрами? Напр.@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald Ви можете підійти до цього подібним до звичайних декораторів, які беруть аргументи . Просто матиdef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Метт Едінг

Вау, дуже дякую Не знав про те, __set_name__хоча я вже давно використовую Python 3.6+.
kawing-chiu

У цього методу є один недолік: статична перевірка цього зовсім не розуміє. Міпі подумає, що helloце не метод, а натомість є об'єктом типу class_decorator.
kawing-chiu

@ kawing-chiu Якщо нічого іншого не працює, ви можете використовувати a if TYPE_CHECKINGдля визначення class_decoratorяк звичайного декоратора, що повертає правильний тип.
тиріон

15

Як зазначали інші, клас не створений під час виклику декоратора. Однак можна помітити об'єкт функції за допомогою параметрів декоратора, а потім повторно прикрасити функцію в методі метакласу __new__. Вам потрібно буде отримати доступ до __dict__атрибуту функції безпосередньо, як принаймні для мене, що func.foo = 1призвело до AttributeError.


6
setattrслід використовувати замість доступу__dict__
schlamar

7

Як пропонує Марк:

  1. Будь-який декоратор називається ДО того, як буде побудований клас, тому його невідомо.
  2. Ми можемо позначити ці методи і пізніше зробити будь-який необхідний процес.
  3. У нас є два варіанти післяобробки: автоматично в кінці визначення класу або десь до запуску програми. Я віддаю перевагу 1-му варіанту, використовуючи базовий клас, але ви можете слідувати 2-му підходу.

Цей код показує, як це може працювати за допомогою автоматичної післяобробки:

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

Вихід:

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

Зауважте, що в цьому прикладі:

  1. Ми можемо коментувати будь-яку функцію з будь-якими довільними параметрами.
  2. У кожного класу є свої відкриті методи.
  3. Ми також можемо успадкувати відкриті методи.
  4. методи можуть бути переважаючими, оскільки функція експозиції оновлюється.

Сподіваюся, це допомагає


4

Як вказали Мурахи, ви не можете отримати посилання на клас всередині класу. Однак якщо вам цікаво розрізняти різні класи (не маніпулюючи об’єктом фактичного типу класу), ви можете передавати рядок для кожного класу. Ви також можете передавати декораторам будь-які інші параметри, використовуючи декоратори в стилі класу.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

Друкує:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

Також дивіться сторінку Брюса Еккеля про декораторів.


Дякую за підтвердження мого гнітючого висновку, що це неможливо. Я також міг би використовувати рядок, який повністю кваліфікував модуль / клас ('module.Class'), зберігати рядок (и) до повного завантаження класів, а потім отримувати класи з імпортом. Це здається моторошним НЕ СУХОМ способом виконання мого завдання.
Карл Г

Вам не потрібно використовувати клас для такого роду декораторів: ідіоматичний підхід полягає у використанні одного додаткового рівня вкладених функцій всередині функції декоратора. Однак, якщо ви ходите з класами, може бути приємніше не використовувати великі літери в назві класу, щоб самому декор виглядав "стандартно", тобто @decorator('Bar')на відміну від @Decorator('Bar').
Ерік Каплун

4

Що робить flask-classy , це створити тимчасовий кеш, який він зберігає у методі, то він використовує щось інше (факт, що Flask реєструє класи за допомогою registerкласового методу), щоб фактично завершити метод.

Ви можете повторно використовувати цей шаблон, на цей раз, використовуючи метаклас, щоб ви могли обернути метод під час імпорту.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

Для фактичного класу (ви можете зробити те ж саме, використовуючи метаклас):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

Джерело: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


це корисна закономірність, але це не стосується проблеми того, що декоратор методів може посилатися на батьківський клас методу, до якого він застосовується
Anentropic

Я оновив свою відповідь, щоб бути більш чітким, як це може бути корисним для отримання доступу до класу під час імпорту (тобто, використовуючи метаклас + кешування параметрів декоратора в методі).
шарлакс

3

Проблема полягає в тому, що коли називається декоратор, клас ще не існує. Спробуйте це:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

Ця програма виведе:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

Як бачите, вам доведеться придумати інший спосіб робити те, що ви хочете.


коли визначено функцію, функція ще не існує, але вона може рекурсивно викликати функцію всередині себе. Я здогадуюсь, що це мовна особливість, специфічна для функцій та недоступна для класів.
Карл Г

DGGправда: Функція викликається тільки і функція, таким чином, отримує доступ до себе, лише після її створення повністю. У цьому випадку клас не може бути завершеним, коли викликається декоратор, оскільки клас повинен чекати результату декоратора, який буде зберігатися як один з атрибутів класу.
u0b34a0f6ae

3

Ось простий приклад:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

Вихід:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

Це старе питання, але натрапило на венеціанську. http://venusian.readthedocs.org/en/latest/

Здається, має можливість прикрасити методи та надати вам доступ як до класу, так і до методу, роблячи це. Зауважте, що виклик tht setattr(ob, wrapped.__name__, decorated)не є типовим способом використання венеціанців і дещо перемагає мету.

У будь-якому випадку ... приклад, наведений нижче, завершений і повинен працювати.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

Функція не знає, чи це метод у точці визначення, коли запускається код декоратора. Тільки коли доступ до нього здійснюється через ідентифікатор класу / екземпляра, він може знати свій клас / екземпляр. Щоб подолати це обмеження, ви можете прикрасити об'єктом дескриптор відстрочку фактичного коду прикраси до часу доступу / виклику:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

Це дозволяє прикрасити окремі (статичні | класи) методи:

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

Тепер ви можете використовувати код декоратора для самоаналізу ...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... і для зміни поведінки функції:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

До жаль, цей підхід функціонально еквівалентний Will McCutchen «s настільки ж непридатним відповідь . І ця, і та відповідь отримують бажаний клас під час виклику методу, а не час декорування методу , як того вимагає оригінальне запитання. Єдиним розумним засобом отримання цього класу в достатньо ранній час є вступ до всіх методів на час визначення класу (наприклад, за допомогою декоратора класу або метакласу). </sigh>
Сесіль Карі

1

Як вказували інші відповіді, декоратор - це річ, що функціонує, ви не можете отримати доступ до класу, до якого належить цей метод, оскільки клас ще не створений. Однак цілком нормально використовувати декоратор, щоб "позначити" функцію, а потім використовувати методи метакласу, щоб потім попрацювати з методом, тому що на __new__етапі клас був створений його метакласом.

Ось простий приклад:

Ми використовуємо @fieldдля позначення методу як спеціального поля та обробляємо його в метакласі.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

У вас є помилка друку в цьому рядку: if inspect.isfunction(v) and getattr(k, "is_field", False):він повинен бути getattr(v, "is_field", False)замість цього.
EvilTosha

0

Ви матимете доступ до класу об'єкта, щодо якого викликається метод, в оформленому методі, який повинен повернути ваш декоратор. Так:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

Використовуючи клас ModelA, ось що це робить:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
Дякую, але це саме рішення, на яке я посилався у своєму питанні, яке не працює для мене. Я намагаюся реалізувати схему спостерігача за допомогою декораторів, і я ніколи не зможу викликати метод у правильному контексті від мого диспетчера спостереження, якщо я не матиму клас у якийсь момент, додаючи метод диспетчеру спостереження. Отримання класу при виклику методу не допомагає мені в першу чергу правильно називати метод.
Карл Г

О, вибачте за мою лінь, що не прочитав все ваше запитання.
Буде МакКутчен

0

Я просто хочу додати свій приклад, оскільки в ньому є все те, про що я міг би подумати, щоб отримати доступ до класу з оформленого методу. Він використовує дескриптор, як пропонує @tyrion. Декоратор може приймати аргументи і передає їх дескриптору. Він може мати справу як з методом у класі, так і з функцією без класу.

import datetime as dt
import functools

def dec(arg1):
    class Timed(object):
        local_arg = arg1
        def __init__(self, f):
            functools.update_wrapper(self, f)
            self.func = f

        def __set_name__(self, owner, name):
            # doing something fancy with owner and name
            print('owner type', owner.my_type())
            print('my arg', self.local_arg)

        def __call__(self, *args, **kwargs):
            start = dt.datetime.now()
            ret = self.func(*args, **kwargs)
            time = dt.datetime.now() - start
            ret["time"] = time
            return ret
        
        def __get__(self, instance, owner):
            from functools import partial
            return partial(self.__call__, instance)
    return Timed

class Test(object):
    def __init__(self):
        super(Test, self).__init__()

    @classmethod
    def my_type(cls):
        return 'owner'

    @dec(arg1='a')
    def decorated(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
        return dict()

    def call_deco(self):
        self.decorated("Hello", world="World")

@dec(arg1='a function')
def another(*args, **kwargs):
    print(args)
    print(kwargs)
    return dict()

if __name__ == "__main__":
    t = Test()
    ret = t.call_deco()
    another('Ni hao', world="shi jie")
    
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.