Присоединение декоратора ко всем функциям в классе

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

Я полагаю, что тогда это становится своего рода аспектом, а не декоратором, и это кажется немного странным, но он думал о чем-то вроде времени или аутентификации, и это было бы довольно аккуратно.

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), о которых я хотел бы упомянуть:

  1. types.FunctionType не защищает специальные методы от декорирования, так как они являются функциональными типами. В качестве более общего способа вы можете просто украсить объекты, имена которых не начинаются с двойного подчеркивания (__). Еще одно преимущество этого метода заключается в том, что он также охватывает те объекты, которые существуют в пространстве имен, и начинается с __ но не функционируют как __qualname__, __module__, так далее.
  2. namespace аргумент в __new__Заголовок не содержит атрибуты класса в __init__, Причина в том, что __new__ выполняется до __init__ (Инициализация).

  3. Нет необходимости использовать classmethod как декоратор, как в большинстве случаев вы импортируете свой декоратор из другого модуля.

  4. Если ваш класс содержит глобальный элемент (вне __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"]):
    ...

Он хорошо работает с юнит-тестами, в отличие от решений, основанных на функциях.


Кредит на ответы в 1, 2 и другие ответы на этот вопрос.

Вы можете переопределить __getattr__ метод. На самом деле он не присоединяет декоратор, но позволяет возвращать декорированный метод. Возможно, вы захотите сделать что-то вроде этого:

class Eggs(object):
    def __getattr__(self, attr):
        return decorate(getattr(self, `_` + attr))

Там скрывается какая-то ужасная рекурсия, от которой вы захотите защититься, но это только начало.

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