Присоединение декоратора ко всем функциям в классе
Мне на самом деле не нужно это делать, но мне просто интересно, есть ли способ привязать декоратор ко всем функциям в классе в общем, вместо того, чтобы явно указывать это для каждой функции.
Я полагаю, что тогда это становится своего рода аспектом, а не декоратором, и это кажется немного странным, но он думал о чем-то вроде времени или аутентификации, и это было бы довольно аккуратно.
11 ответов
Самый простой способ сделать это или внести другие изменения в определение класса - это определить метакласс.
В качестве альтернативы, просто примените ваш декоратор в конце определения класса:
class Something:
def foo(self): pass
for name, fn in inspect.getmembers(Something):
if isinstance(fn, types.UnboundMethodType):
setattr(Something, name, decorator(fn))
Для Python 3 замените types.UnboundMethodType на types.FunctionType.
Конечно, на практике вы захотите применять свой декоратор более избирательно, и как только вы захотите украсить все методы, кроме одного, вы обнаружите, что проще и гибче просто использовать синтаксис декоратора традиционным способом.
Каждый раз, когда вы думаете об изменении определения класса, вы можете использовать декоратор класса или метакласс. например, используя метакласс
import types
class DecoMeta(type):
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.iteritems():
if isinstance(attr_value, types.FunctionType):
attrs[attr_name] = cls.deco(attr_value)
return super(DecoMeta, cls).__new__(cls, name, bases, attrs)
@classmethod
def deco(cls, func):
def wrapper(*args, **kwargs):
print "before",func.func_name
result = func(*args, **kwargs)
print "after",func.func_name
return result
return wrapper
class MyKlass(object):
__metaclass__ = DecoMeta
def func1(self):
pass
MyKlass().func1()
Выход:
before func1
after func1
Примечание: он не будет украшать staticmethod и classmethod
Следующий код работает для python2.x и 3.x
import inspect
def decorator_for_func(orig_func):
def decorator(*args, **kwargs):
print("Decorating wrapper called for method %s" % orig_func.__name__)
result = orig_func(*args, **kwargs)
return result
return decorator
def decorator_for_class(cls):
for name, method in inspect.getmembers(cls):
if (not inspect.ismethod(method) and not inspect.isfunction(method)) or inspect.isbuiltin(method):
continue
print("Decorating function %s" % name)
setattr(cls, name, decorator_for_func(method))
return cls
@decorator_for_class
class decorated_class:
def method1(self, arg, **kwargs):
print("Method 1 called with arg %s" % arg)
def method2(self, arg):
print("Method 2 called with arg %s" % arg)
d=decorated_class()
d.method1(1, a=10)
d.method2(2)
Обновление для Python 3:
class DecoMeta(type):
def __new__(cls, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType):
attrs[attr_name] = cls.deco(attr_value)
return super(DecoMeta, cls).__new__(cls, name, bases, attrs)
@classmethod
def deco(cls, func):
def wrapper(*args, **kwargs):
print("before",func.__name__)
result = func(*args, **kwargs)
print("after",func.__name__)
return result
return wrapper
(и спасибо Дункану за это)
Я повторю здесь свой ответ по аналогичной проблеме
Это можно сделать разными способами. Я покажу, как это сделать через мета-класс, декоратор классов и наследование.
путем изменения мета - класс
import functools
class Logger(type):
@staticmethod
def _decorator(fun):
@functools.wraps(fun)
def wrapper(*args, **kwargs):
print(fun.__name__, args, kwargs)
return fun(*args, **kwargs)
return wrapper
def __new__(mcs, name, bases, attrs):
for key in attrs.keys():
if callable(attrs[key]):
# if attrs[key] is callable, then we can easily wrap it with decorator
# and substitute in the future attrs
# only for extra clarity (though it is wider type than function)
fun = attrs[key]
attrs[key] = Logger._decorator(fun)
# and then invoke __new__ in type metaclass
return super().__new__(mcs, name, bases, attrs)
class A(metaclass=Logger):
def __init__(self):
self.some_val = "some_val"
def method_first(self, a, b):
print(a, self.some_val)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
# __init__ (<__main__.A object at 0x7f852a52a2b0>,) {}
b.method_first(5, b="Here should be 5")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 5) {'b': 'Here should be 5'}
# 5 some_val
b.method_first(6, b="Here should be 6")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 6) {'b': 'Here should be 6'}
# 6 some_val
b.another_method(7)
# another_method (<__main__.A object at 0x7f852a52a2b0>, 7) {}
# 7
b.static_method(7)
# 7
Кроме того, покажет два подход, как сделать это без изменения метаинформации класса (через класс декоратор и наследование класса). Первый подход через декоратор классов put_decorator_on_all_methods
принимает декоратор для обертывания всех вызываемых объектов класса.
def logger(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def put_decorator_on_all_methods(decorator, cls=None):
if cls is None:
return lambda cls: put_decorator_on_all_methods(decorator, cls)
class Decoratable(cls):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
return decorator(value)
return value
return Decoratable
@put_decorator_on_all_methods(logger)
class A:
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(8)
# >>> static_method (8,) {}
# >>> 8
И недавно я столкнулся с той же проблемой, но я не мог поместить декоратор в класс или изменить его каким-либо другим способом, за исключением того, что мне было разрешено добавлять такое поведение только через наследование (я не уверен, что это лучший выбор, если вы можете изменить кодовую базу по своему желанию).
Вот класс Logger
заставляет все вызываемые члены подклассов записывать информацию об их вызовах, см. код ниже.
class Logger:
def _decorator(self, f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Logger):
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
Или, более абстрактно, вы можете создать экземпляр базового класса на основе некоторого декоратора.
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
class Decoratable:
def __init__(self, dec):
self._decorator = dec
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Decoratable):
def __init__(self, dec):
super().__init__(dec)
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A(decorator)
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
Конечно, метаклассы - это самый питонический путь, когда вы хотите изменить способ, которым питон создает объекты. Что можно сделать, переопределив __new__
метод вашего класса. Но есть некоторые моменты вокруг этой проблемы (особенно для python 3.X), о которых я хотел бы упомянуть:
types.FunctionType
не защищает специальные методы от декорирования, так как они являются функциональными типами. В качестве более общего способа вы можете просто украсить объекты, имена которых не начинаются с двойного подчеркивания (__
). Еще одно преимущество этого метода заключается в том, что он также охватывает те объекты, которые существуют в пространстве имен, и начинается с__
но не функционируют как__qualname__
,__module__
, так далее.namespace
аргумент в__new__
Заголовок не содержит атрибуты класса в__init__
, Причина в том, что__new__
выполняется до__init__
(Инициализация).Нет необходимости использовать
classmethod
как декоратор, как в большинстве случаев вы импортируете свой декоратор из другого модуля.- Если ваш класс содержит глобальный элемент (вне
__init__
) за отказ от оформления наряду с проверкой, не начинается ли название с__
Вы можете проверить тип сtypes.FunctionType
чтобы быть уверенным, что вы не украшаете нефункциональный объект.
Вот образец метакассы, который вы можете использовать:
class TheMeta(type):
def __new__(cls, name, bases, namespace, **kwds):
# if your decorator is a class method of the metaclass use
# `my_decorator = cls.my_decorator` in order to invoke the decorator.
namespace = {k: v if k.startswith('__') else my_decorator(v) for k, v in namespace.items()}
return type.__new__(cls, name, bases, namespace)
Демо-версия:
def my_decorator(func):
def wrapper(self, arg):
# You can also use *args instead of (self, arg) and pass the *args
# to the function in following call.
return "the value {} gets modified!!".format(func(self, arg))
return wrapper
class TheMeta(type):
def __new__(cls, name, bases, namespace, **kwds):
# my_decorator = cls.my_decorator (if the decorator is a classmethod)
namespace = {k: v if k.startswith('__') else my_decorator(v) for k, v in namespace.items()}
return type.__new__(cls, name, bases, namespace)
class MyClass(metaclass=TheMeta):
# a = 10
def __init__(self, *args, **kwargs):
self.item = args[0]
self.value = kwargs['value']
def __getattr__(self, attr):
return "This class hasn't provide the attribute {}.".format(attr)
def myfunction_1(self, arg):
return arg ** 2
def myfunction_2(self, arg):
return arg ** 3
myinstance = MyClass(1, 2, value=100)
print(myinstance.myfunction_1(5))
print(myinstance.myfunction_2(2))
print(myinstance.item)
print(myinstance.p)
Выход:
the value 25 gets modified!!
the value 8 gets modified!!
1
This class hasn't provide the attribute p. # special method is not decorated.
Для проверки 3-го пункта из вышеупомянутых заметок вы можете раскомментировать строку a = 10
и делать print(myinstance.a)
и увидеть результат, а затем изменить понимание словаря в __new__
а затем снова увидеть результат:
namespace = {k: v if k.startswith('__') and not isinstance(v, types.FunctionType)\
else my_decorator(v) for k, v in namespace.items()}
В некоторых случаях вам, возможно, захочется сделать еще кое-что похожее. Иногда вы хотите активировать вложение для чего-то вроде отладки, но не для всех классов, но для каждого метода объекта, вам может понадобиться запись того, что он делает.
def start_debugging():
import functools
import datetime
filename = "debug-{date:%Y-%m-%d_%H_%M_%S}.txt".format(date=datetime.datetime.now())
debug_file = open(filename, "a")
debug_file.write("\nDebug.\n")
def debug(func):
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
debug_file.write(f"Calling {func.__name__}({signature})\n")
value = func(*args, **kwargs)
debug_file.write(f"{func.__name__!r} returned {value!r}\n") # 4
debug_file.flush()
return value
return wrapper_debug
for obj in (self):
for attr in dir(obj):
if attr.startswith('_'):
continue
fn = getattr(obj, attr)
if not isinstance(fn, types.FunctionType) and \
not isinstance(fn, types.MethodType):
continue
setattr(obj, attr, debug(fn))
Эта функция будет проходить через некоторые объекты (в настоящее время только self) и заменяет все функции и методы, которые не начинаются с _, на декоратор отладки.
Используемый для этого метод простой итерации dir(self) выше не рассматривается, но полностью работает. И может вызываться извне из объекта и намного более произвольно.
В Python 3 вы также можете написать простую функцию, которая перезаписывает / применяет декоратор к определенным методам, например:
from functools import wraps
from types import MethodType
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print("logging:", func.__name__, res)
return res
return wrapper
class Test:
def foo(self):
return 42
...
def aspectize(cls, decorator):
for name, func in cls.__dict__.items():
if not name.startswith("__"):
setattr(cls, name, MethodType(decorator(func), cls)) # MethodType is key
aspectize(Test, logged)
t = Test()
t.foo() # printing "logging: foo 42"; returning 42
Я пришел к этому вопросу из:
Как украсить все функции класса, не печатая их снова и снова для каждого метода?
И я хочу добавить одно примечание:
Ответы с декораторами классов или заменой методов класса, подобных этому:
/questions/14547757/kak-ya-mogu-ukrasit-vse-funktsii-klassa-ne-vvodya-ego-snova-i-snova-dlya-kazhdogo-dobavlennogo-metoda-piton/14547767#14547767
Не будут работать с .
Ты получишьTypeError, unexpected argument
потому что ваш метод получитself/cls
в качестве первого аргумента. Возможно:
оформленный класс не знает о декораторах собственных методов и не может быть выделен даже с помощьюinspect.ismethod
.
Я пришел к такому быстрому решению:
я не проверял его внимательно, но он проходит мои (не столь исчерпывающие) тесты.
Использование динамических декораторов уже является плохим подходом, поэтому в качестве временного решения его можно использовать.
TLD:TD Добавитьtry/exception
использовать сstaticmethod
def log_sent_data(function):
@functools_wraps(function)
def decorator(*args, **kwargs):
# Quickfix
self, *args = args
try: # If method has self/cls/descriptor
result = function(self, *args, **kwargs)
except TypeError:
if args: # If method is static but has positional args
result = function(*args, **kwargs)
else: # If method is static and doesn't has positional args
result = function(**kwargs)
# End of quickfix
return result
return decorator
Объединяя информацию из разных ответов, вот метакласс DecorateMethods:
class DecorateMethods(type):
""" Decorate all methods of the class with the decorator provided """
def __new__(cls, name, bases, attrs, **kwargs):
try:
decorator = kwargs['decorator']
except KeyError:
raise ValueError('Please provide the "decorator" argument, eg. '
'MyClass(..., metaclass=DecorateMethods, decorator=my_decorator)')
exclude = kwargs.get('exclude', [])
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType) and \
attr_name not in exclude and \
not attr_name.startswith('__'):
attrs[attr_name] = decorator(attr_value)
return super(DecorateMethods, cls).__new__(cls, name, bases, attrs)
Используется в качестве:
class MyClass(metaclass=DecorateMethods, decorator=my_decorator, exclude=["METHOD_TO_BE_EXCLUDED"]):
...
Он хорошо работает с юнит-тестами, в отличие от решений, основанных на функциях.
Вы можете переопределить __getattr__
метод. На самом деле он не присоединяет декоратор, но позволяет возвращать декорированный метод. Возможно, вы захотите сделать что-то вроде этого:
class Eggs(object):
def __getattr__(self, attr):
return decorate(getattr(self, `_` + attr))
Там скрывается какая-то ужасная рекурсия, от которой вы захотите защититься, но это только начало.