2008-11-20 2 views
54

Я пытаюсь написать декоратор протоколирование: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» внутри «регистратор», но у меня нет, я деа какой.

+0

не совсем ответ, но нашел эту статью, чтобы покрыть вещи в глубине http://bit.ly/1NsBLmx – apcelent 2015-12-18 05:38:49

ответ

41

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

+1

typo: вы забыли `return with_logging` в функции журнала. – 2008-11-20 20:14:42

+2

, кстати, functools.wraps не сохраняет атрибуты im_ *. Считаете ли вы, что это исключение можно считать ошибкой? – 2008-11-20 20:21:04

+1

Я не могу притворяться, что полностью понимаю, что происходит с @wraps, но это, безусловно, устраняет мою проблему. Огромное спасибо. – 2008-11-21 11:00:36

7

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

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) 
+0

wrapclass должен быть осторожным из-за статического метода. – 2008-11-20 20:16:58

+0

Это выглядит как хороший вариант использования для декораторов классов (новый в Python 2.6). Они работают точно так же, как декораторы функций. – babbageclunk 2008-11-21 07:47:24

6

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

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

25

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

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

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

6

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

16

Идея, предложенная здесь отличная, но есть некоторые недостатки:

  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): 
    # ... 

См full unit-tests for usage details.

И вот только код 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__() 
0

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

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

Смежные вопросы