Декоратор Python заставляет функцию забыть, что она принадлежит классу

Я пытаюсь написать декоратор для ведения журнала:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    @logger
    def f():
        pass

C().f()

Я хотел бы это напечатать:

Entering C.f

но вместо этого я получаю это сообщение об ошибке:

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

Предположительно, это как-то связано с областью действия myFunc внутри logger, но я понятия не имею, что.

9 ответов

Решение

Клаудиу ответ правильный, но вы также можете обмануть, получив название класса из self аргумент. Это приведет к вводящим в заблуждение операторам журнала в случаях наследования, но расскажет вам класс объекта, метод которого вызывается. Например:

from functools import wraps  # use this to preserve function signatures and docstrings
def logger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__)
        return func(*args, **kwargs)
    return with_logging

class C(object):
    @logger
    def f(self):
        pass

C().f()

Как я уже сказал, это не будет работать должным образом в случаях, когда вы унаследовали функцию от родительского класса; в этом случае вы могли бы сказать

class B(C):
    pass

b = B()
b.f()

и получите сообщение Entering B.f где вы на самом деле хотите получить сообщение Entering C.f так как это правильный класс. С другой стороны, это может быть приемлемо, и в этом случае я бы рекомендовал этот подход вместо предложения Клавдия.

Функции становятся только методами во время выполнения. То есть когда получаешь C.f Вы получаете связанную функцию (и C.f.im_class is C). Когда ваша функция определена, она является простой функцией, она не привязана ни к какому классу. Эта несвязанная и диссоциированная функция - это то, что украшено регистратором.

self.__class__.__name__ даст вам имя класса, но вы также можете использовать дескрипторы, чтобы выполнить это несколько более общим способом. Этот шаблон описан в посте блога на Decorators и Descriptors, и реализация вашего декоратора logger, в частности, будет выглядеть так:

class logger(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, type=None):
        return self.__class__(self.func.__get__(obj, type))
    def __call__(self, *args, **kw):
        print 'Entering %s' % self.func
        return self.func(*args, **kw)

class C(object):
    @logger
    def f(self, x, y):
        return x+y

C().f(1, 2)
# => Entering <bound method C.f of <__main__.C object at 0x...>>

Очевидно, что результат может быть улучшен (например, с помощью getattr(self.func, 'im_class', None)), но этот общий шаблон будет работать как для методов, так и для функций. Однако это не будет работать для классов в старом стиле (но не используйте их;)

Идеи, предложенные здесь, превосходны, но имеют некоторые недостатки:

  1. inspect.getouterframes а также args[0].__class__.__name__ не подходят для простых функций и статических методов.
  2. __get__ должен быть в классе, который отклонен @wraps,
  3. @wraps сама должна лучше скрывать следы.

Итак, я объединил некоторые идеи с этой страницы, ссылки, документы и мою собственную голову,
и, наконец, нашел решение, в котором отсутствуют все три недостатка выше.

В следствии, method_decorator:

  • Знает класс, к которому привязан декорированный метод.
  • Скрывает следы декоратора, отвечая на системные атрибуты более правильно, чем functools.wraps() делает.
  • Содержит юнит-тесты для привязки несвязанных методов экземпляра, методов класса, статических методов и простых функций.

Использование:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

Смотрите полные юнит-тесты для деталей использования.

И вот только код method_decorator учебный класс:

class method_decorator(object):

    def __init__(self, func, obj=None, cls=None, method_type='function'):
        # These defaults are OK for plain functions
        # and will be changed by __get__() for methods once a method is dot-referenced.
        self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type

    def __get__(self, obj=None, cls=None):
        # It is executed when decorated func is referenced as a method: cls.func or obj.func.

        if self.obj == obj and self.cls == cls:
            return self # Use the same instance that is already processed by previous call to this __get__().

        method_type = (
            'staticmethod' if isinstance(self.func, staticmethod) else
            'classmethod' if isinstance(self.func, classmethod) else
            'instancemethod'
            # No branch for plain function - correct method_type for it is already set in __init__() defaults.
        )

        return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts.
            self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func.

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __getattribute__(self, attr_name): # Hiding traces of decoration.
        if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__().
            return object.__getattribute__(self, attr_name) # Stopping recursion.
        # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, e.g.: __module__, __class__, __name__, __doc__, im_*, func_*, etc.
        return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func.

    def __repr__(self): # Special case: __repr__ ignores __getattribute__.
        return self.func.__repr__()

Кажется, что пока класс создается, Python создает обычные функциональные объекты. Они только потом превращаются в несвязанные объекты метода. Зная это, я могу найти единственный способ сделать то, что ты хочешь:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    def f(self):
        pass
C.f = logger(C.f)
C().f()

Это выводит желаемый результат.

Если вы хотите обернуть все методы в классе, то вы, вероятно, захотите создать функцию wrapClass, которую затем могли бы использовать следующим образом:

C = wrapClass(C)

Я нашел другое решение очень похожей проблемы, используя inspect библиотека. Когда вызывается декоратор, хотя функция еще не связана с классом, вы можете проверить стек и определить, какой класс вызывает декоратор. Вы можете, по крайней мере, получить строковое имя класса, если это все, что вам нужно (вероятно, еще нельзя ссылаться на него, так как он создается). Тогда вам не нужно ничего вызывать после того, как класс был создан.

import inspect

def logger(myFunc):
    classname = inspect.getouterframes(inspect.currentframe())[1][3]
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (classname, myFunc.__name__)
        return myFunc(*args, **keyargs)
    return new

class C(object):
    @logger
    def f(self):
        pass

C().f()

Хотя это не обязательно лучше, чем другие, это единственный способ выяснить, как найти имя класса будущего метода во время вызова декоратора. Обратите внимание на то, что ссылки на фреймы не хранятся в inspect библиотечная документация.

Функции класса всегда должны принимать self в качестве первого аргумента, поэтому вы можете использовать его вместо im_class.

def logger(myFunc):
    def new(self, *args, **keyargs):
        print 'Entering %s.%s' % (self.__class__.__name__, myFunc.__name__)
        return myFunc(self, *args, **keyargs)

    return new 

class C(object):
    @logger
    def f(self):
        pass
C().f()

сначала я хотел использовать self.__name__ но это не работает, потому что у экземпляра нет имени. ты должен использовать self.__class__.__name__ чтобы получить название класса.

Как показано в ответе Асы Айерс, вам не нужен доступ к объекту класса. Возможно, стоит знать, что, начиная с Python 3.3, вы также можете использовать __qualname__, который дает вам полное имя:

>>> def logger(myFunc):
...     def new(*args, **keyargs):
...         print('Entering %s' % myFunc.__qualname__)
...         return myFunc(*args, **keyargs)
... 
...     return new
... 
>>> class C(object):
...     @logger
...     def f(self):
...         pass
... 
>>> C().f()
Entering C.f

Это дает дополнительное преимущество работы также в случае вложенных классов, как показано в этом примере, взятом из PEP 3155:

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

Обратите внимание также, что в Python 3 im_class атрибут исчез, поэтому, если вы действительно хотите получить доступ к классу в декораторе, вам нужен другой метод. Подход, который я сейчас использую, включает object.__set_name__ и подробно в моем ответе на "Может ли Python-декоратор метода экземпляра получить доступ к классу?"

Вы также можете использовать new.instancemethod() создать метод экземпляра (связанный или несвязанный) из функции.

Вместо внедрения кода украшения во время определения, когда функция не знает своего класса, откладывайте выполнение этого кода до тех пор, пока функция не будет доступна / вызвана. Объект дескриптора облегчает внедрение собственного кода поздно, во время доступа / вызова:

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

    def __get__(self, obj, type_=None):
        return self.__class__(self.func.__get__(obj, type_), 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

Теперь мы можем проверять класс как во время доступа (__get__) и во время разговора (__call__). Этот механизм работает как для простых методов, так и для статических | классов:

>>> Foo().foo(1, b=2)
called Foo.foo with args=(1,) kwargs={'b': 2}

Полный пример по адресу: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

Другие вопросы по тегам