Декоратор Python заставляет функцию забыть, что она принадлежит классу - PullRequest
56 голосов
/ 20 ноября 2008

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

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 ]

43 голосов
/ 20 ноября 2008

Ответ Клавдия верен, но вы также можете обмануть, получив имя класса из аргумента 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, поскольку это правильный класс. С другой стороны, это может быть приемлемо, и в этом случае я бы рекомендовал этот подход вместо предложения Клавдия.

25 голосов
/ 21 ноября 2008

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

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

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)), но этот общий шаблон будет работать как для методов, так и для функций. Однако не будет работать для классов в старом стиле (но не использовать их;)

16 голосов
/ 05 августа 2010

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

  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__()
7 голосов
/ 20 ноября 2008

Кажется, что во время создания класса 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)
6 голосов
/ 21 июля 2010

Я нашел другое решение очень похожей проблемы, используя библиотеку 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 ссылки на фреймы не хранятся.

6 голосов
/ 20 ноября 2008

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

0 голосов
/ 23 января 2019

Как показано в Ответ Асы Айерса , вам не нужно обращаться к объекту класса. Возможно, стоит знать, что начиная с 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 метода экземпляра получить доступ к классу?»

0 голосов
/ 28 января 2018

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

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

0 голосов
/ 20 ноября 2008

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

...