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






Кажется, что пока создается класс, 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)
wrapclass должен быть осторожен из-за статического метода.
Похоже, это хороший вариант использования декораторов классов (новинка в Python 2.6). Они работают точно так же, как декораторы функций.
Функции класса всегда должны принимать 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__, чтобы получить имя класса.
Ответ Клаудиу правильный, но вы также можете обмануть, убрав имя класса из аргумента 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, поскольку это правильный класс. С другой стороны, это может быть приемлемо, и в этом случае я бы рекомендовал этот подход вместо предложения Клаудиу.
опечатка: вы забыли return with_logging в функции логгера.
кстати, functools.wraps не сохраняет атрибуты im_ *. Как вы думаете, это упущение можно считать ошибкой?
Я не могу притвориться, что полностью понимаю, что происходит с @wraps, но это определенно решает мою проблему. Огромное спасибо.
Петр: Спасибо, что указали на недостающий возврат; Я отредактировал свой пост, чтобы исправить это. Что касается атрибутов im_ *, мне пришлось бы подумать обо всех последствиях копирования этих атрибутов, прежде чем сказать, что это определенно ошибка. Однако я не могу придумать вескую причину, чтобы их пропустить.
Чарльз: Я опубликовал еще один вопрос о переполнении стека, объясняющий использование оберток: stackoverflow.com/questions/308999/what-does-functoolswraps- делать
Вы также можете использовать new.instancemethod() для создания метода экземпляра (связанного или несвязанного) из функции.
Функции становятся методами только во время выполнения. То есть, когда вы получаете 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)), но этот общий шаблон будет работать как для методов, так и для функций. Однако нет будет работать для классов старого стиля (но просто не используйте их;)
Для всех, кто ищет функцию, предоставляющую TypeError: foo takes exactly x arguments или attribute missing, и понял, что аргумент self не передается в вашу декорированную функцию, это решение, спасибо @ianb
Этот подход требует другого регистратора для привязанного метода, несвязанного метода и ведения журнала функций.
@KeatsKelleher, как так? В моих тестах оформление и вызов функции, кажется, работают нормально ...
Я нашел другое решение очень похожей проблемы с помощью библиотеки 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 не следует хранить ссылки на фреймы.
это именно то, что я хочу - информация о методе и классе, к которому он будет привязан, до он будет вызываться в первый раз.
Предлагаемые здесь идеи прекрасны, но имеют некоторые недостатки:
inspect.getouterframes и args[0].__class__.__name__ не подходят для простых функций и статических методов.__get__ должен принадлежать к классу, который отклонен @wraps.@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__()
Это был единственный подход, который у меня сработал. Вместо этого мне нужна ссылка на экземпляр объекта
Вместо того, чтобы вводить декоративный код во время определения, когда функция не знает своего класса, отложите запуск этого кода до тех пор, пока функция не будет доступна / вызвана. Объект дескриптора облегчает внедрение собственного кода поздно, во время доступа / вызова:
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
Как показано в Ответ Асы Айерс, вам не нужно обращаться к объекту класса. Возможно, стоит знать, что начиная с 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 метода экземпляра получить доступ к классу?».
Не совсем ответ, но нашел эту статью, чтобы подробно рассказать о вещах bit.ly/1NsBLmx