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

Привет, у меня есть примерно что-то вроде следующего. В основном мне нужно получить доступ к классу метода экземпляра из декоратора, используемого в методе экземпляра в его определении.

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

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

Код as-is дает

AttributeError: 'function' object has no attribute 'im_class'

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

Спасибо.

Ответ 1

Если вы используете 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 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

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

Ответ 3

Как указали муравьи, вы не можете получить ссылку на класс внутри класса. Однако, если вам интересно различать разные классы (не манипулируя действительным объектом типа класса), вы можете передать строку для каждого класса. Вы также можете передать любые другие параметры, которые вам нравятся, с помощью декораторов в стиле класса.

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

Также см. Страницу Брюса Экеля о декораторах.

Ответ 4

Проблема заключается в том, что когда декоратор называется классом, еще не существует. Попробуйте следующее:

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

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

Ответ 5

Что 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

Ответ 6

Вот простой пример:

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

Ответ 7

Как указывает Марк:

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

Этот код показывает, как это может работать, используя автоматическую пост-обработку:

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}

Обратите внимание, что в этом примере:

  • Мы можем аннотировать любую функцию с любыми произвольными параметрами.
  • Каждый класс имеет свои открытые методы.
  • Мы также можем наследовать открытые методы.
  • методы могут переопределяться по мере обновления функции экспонирования.

Надеюсь, что это поможет

Ответ 8

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

Кажется, у него есть способность украшать методы и предоставлять вам доступ как к классу, так и к методу при этом. Обратите внимание, что вызов tht setattr(ob, wrapped.__name__, decorated) не является типичным способом использования venusian и несколько поражает цель.

В любом случае... приведенный ниже пример завершен и должен работать.

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

Ответ 9

У вас будет доступ к классу объекта, по которому метод вызывается в декорированном методе, который должен вернуть ваш декоратор. Например:

def decorator(method):
    # do something that requires view 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'>

Ответ 10

Функция не знает, является ли это метод в точке определения, когда выполняется код декоратора. Только при доступе через идентификатор класса/экземпляра он может знать свой класс/экземпляр. Чтобы преодолеть это ограничение, вы можете украсить объект дескриптора, чтобы задержать фактический код декодирования до времени доступа/вызова:

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'}

Ответ 11

Начиная с 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__, вы можете обратиться к документации "Создание объекта класса".