Может ли декоратор метода экземпляра получить доступ к классу?

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: объект 'функция' не имеет атрибута 'im_class'

Я нашел аналогичный вопрос / ответы - декоратор Python заставляет функцию забыть, что он принадлежит классу, и класс Get в декораторе Python, но они полагаются на обходной путь, который захватывает экземпляр во время выполнения, выхватывая первый параметр. В моем случае я буду вызывать метод на основе информации, полученной из его класса, поэтому я не могу дождаться поступления вызова.

Карл Дж.
источник

Ответы:

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
Спасибо, я думаю, это путь, по которому нужно идти. Всего одна дополнительная строка кода для любого класса, который я бы хотел использовать в этом декораторе. Может быть, я мог бы использовать собственный метакласс и выполнить ту же проверку во время нового ...?
Carl G
3
Любой, кто пытается использовать это с помощью staticmethod или classmethod, захочет прочитать этот PEP: python.org/dev/peps/pep-0232 Не уверен, что это возможно, потому что вы не можете установить атрибут для класса / статического метода, и я думаю, что они пожирают вверх любые пользовательские атрибуты функции, когда они применяются к функции.
Carl G
Именно то, что я искал, для моей ORM на основе DBM ... Спасибо, чувак.
Coyote21,
Вы должны использовать inspect.getmro(cls)для обработки всех базовых классов в декораторе классов для поддержки наследования.
schlamar
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
Как бы это выглядело при использовании декоратора с параметрами? Eg@class_decorator('test', foo='bar')
luckydonald
2
@luckydonald Вы можете подойти к нему так же, как к обычным декораторам, которые принимают аргументы . Just havedef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Мэтт
Вау, большое спасибо. Не знал, __set_name__хотя давно использую Python 3.6+.
Кавинг-Чиу,
У этого метода есть один недостаток: статический чекер этого не понимает. Mypy будет думать, что helloэто не метод, а объект типа class_decorator.
Кавинг-Чиу,
@ kawing-chiu Если ничего не работает, вы можете использовать if TYPE_CHECKINGдля определения class_decoratorв качестве обычного декоратора, возвращающего правильный тип.
Тирион
15

Как указывали другие, класс не был создан во время вызова декоратора. Однако можно аннотировать объект функции с помощью параметров декоратора, а затем повторно декорировать функцию в __new__методе метакласса . Вам нужно будет получить доступ к __dict__атрибуту функции напрямую, поскольку, по крайней мере, для меня это func.foo = 1привело к AttributeError.

Марк Виссер
источник
6
setattrследует использовать вместо доступа__dict__
schlamar
7

Как предлагает Марк:

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

Этот код показывает, как это может работать с использованием автоматической постобработки:

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'), хранить строку (строки) до тех пор, пока все классы не будут полностью загружены, а затем извлекать классы самостоятельно с помощью импорта. Это кажется ужасно не СУХИМ способом выполнить мою задачу.
Carl G
Вам не нужно использовать класс для такого рода декоратора: идиоматический подход заключается в использовании одного дополнительного уровня вложенных функций внутри функции декоратора. Однако, если вы используете классы, было бы лучше не использовать заглавные буквы в имени класса, чтобы само оформление выглядело «стандартным», то есть @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

Charlax
источник
это полезный шаблон, но он не решает проблему того, что декоратор метода может ссылаться на родительский класс метода, к
которому
Я обновил свой ответ, чтобы более четко указать, как это может быть полезно для получения доступа к классу во время импорта (т.е. с использованием метакласса + кеширование параметра декоратора в методе).
charlax
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

Как видите, вам придется придумать другой способ делать то, что вы хотите.

Муравьи Аасма
источник
когда кто-то определяет функцию, она еще не существует, но можно рекурсивно вызывать функцию изнутри себя. Я предполагаю, что это особенность языка, специфичная для функций и недоступная для классов.
Carl G
DGGenuine: функция вызывается только после того, как она была полностью создана, и, таким образом, функция получает доступ к самой себе. В этом случае класс не может быть завершен при вызове декоратора, поскольку класс должен дождаться результата декоратора, который будет сохранен как один из атрибутов класса.
u0b34a0f6ae 02
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/

Кажется, у него есть возможность украшать методы и при этом предоставлять вам доступ как к классу, так и к методу. Обратите внимание, что призыв 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'}
Aurzenligl
источник
К сожалению, этот подход функционально эквивалентен 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
Спасибо, но это именно то решение, на которое я ссылался в своем вопросе, который не работает для меня. Я пытаюсь реализовать шаблон наблюдателя с использованием декораторов, и я никогда не смогу вызвать метод в правильном контексте из диспетчера наблюдений, если у меня нет класса в какой-то момент при добавлении метода в диспетчер наблюдения. Получение класса при вызове метода в первую очередь не помогает мне правильно вызвать метод.
Carl G
Ого, извините за мою лень, что не прочитал весь ваш вопрос.
Уилл Маккатчен,
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")
    
Джао
источник