Senior Pimiento

Декораторы в Python

Декораторы и Паттерн Декоратор

Возможно использовать декораторы в Python чтобы реализовать паттерн декоратор, но это крайне редкое их использование. Декораторы в Python больше всего похожи на макросы.

Декораторы функций

class myDecorator(object):

    def __init__(self, f):
        print("inside my_decorator.__init__()")
        f()

    def __call__(self):
        print("inside my_decorator.__call__()")


@myDecorator
def aFunction():
    print("inside aFunction")

print("Finished decorating aFunction()")

aFunction()
inside my_decorator.__init__()
inside aFunction
Finished decorating aFunction()
inside my_decorator.__call__()

Изменение имени функции, возвращённой из декоратора

Если мы используем декоратор-функцию, то возвращённая им функция будет иметь совсем другое название, для решения это проблемы мы можем переопределить аттрибут _name__

def entry_exit(f):
    def wrapper():
        print("Entering", f.__name__)
        f()
        print("Exited", f.__name__)
    wrapper.__name__ = f.__name__
    return wrapper

@entry_exit
def original_name():
    print("inside original name")

original_name()
Entering original_name
inside original name
Exited original_name

Декораторы с аргументами

class decorator_with_arguments(object):

    def __init__(self, arg1, arg2, arg3):
        """
        If there are decorator arguments, the function
        to be decorated is not passed to the constructor!
        """
        print("Inside __init__()")
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, f):
        """
        If there are decorator arguments, __call__() is only called once,
        as part of the decoration process! You can only give it a single argument,
        which is the function object.
        """
        print("Inside __call__()")
        def wrapper(*args):
            print("Inside wrapper()")
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            f(*args)
            print("After f(*args)")
        return wrapper

@decorator_with_arguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
    print("sayHello arguments: ", a1, a2, a3, a4)

print("After decoration")

print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("after first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("after second sayHello() call")
Inside __init__()
Inside __call__()
After decoration
Preparing to call sayHello()
Inside wrapper()
Decorator arguments: hello world 42
sayHello arguments:  say hello argument list
After f(*args)
after first sayHello() call
Inside wrapper()
Decorator arguments: hello world 42
sayHello arguments:  a different set of arguments
After f(*args)
after second sayHello() call

Модуль decorator

Так как написание корректного декоратора требует значительного опыта и не так уж просто как кажется, то был создан модуль decorator.

  1. Определение декоратора

    Декораторы могут быть двух видов:

  • signature-preserving декораторы, т.е., вызываемые объекты, принимающие функцию на фход и возвращающие функцию на выход с сохранением сигнатуры
  • signature-changing декораторы, т.е, декораторы, которые изменяют сигнатуру входной функции или декораторы, возвращающие non-callable объект К signature-changing декораторам относятся, например, декораторы @staticmethod и @classmethod, т.к. они принимают на вход функцию и возвращают объект дескриптора, который не является ни функцией, ни вызываемым объектом.
  1. Описание проблемы

    Предположим, что мы хотим трассировать выполнение функции:

try:
    from functools import update_wrapper
except ImportError:             # using Python version < 2.5

    def decorator_trace(f):
        def newf(*args, **kw):
            print "calling %s with args %s, %s" % (f.__name__, args, kw)
            return f(*args, **kw)
        newf.__name__ = f.__name__
        newf.__dict__.update(f.__dict__)
        newf.__doc__ = f.__doc__
        newf.__module__ = f.__module__
        return newf
else:                           # using Python 2.5+

    def decorator_trace(f):
        def newf(*args, **kw):
            print "calling %s with args %s, %s" % (f.__name__, args, kw)
            return f(*args, **kw)
        return update_wrapper(newf, f)
  1. Решение проблемы

    Решение проблемы с постояным отслеживанием чтобы все аттрибуты функции под декоратором остались прежними (имя, документация) заключается в использовании фабрики генераторов, которая спрячет всю сложность создания signature-preserving декораторов. Вот как мы можем реализовать функцию decoratortrace:

# Во-первых, импортируем модуль decorator

from decorator import decorator


# Затем объявляем вспомогательную функцию с сигнатурой (f, *args, **kwargs),

# которая вызывает оригинальную функцию f с аргументами args и kwargs

# и реализует возможность трассировки

def trace(f, *args, **kwargs):
    print("calling %s with args %s, %s" % (f.__name__, args, kwargs))
    return f(*args, **kwargs)


# decorator может конвертировать вспомогательную функцию в signature-preserving объект-декоратор

# т.е., это вызываемый объект, принимающий на вход функцию и возвращающий обёрнутую

# в декоратор функцию с такой же точно сигнатурой, как и у оригинальной функции.

@decorator(trace)
def f1(x):
    pass

f1(0)
    calling f1 with args (0,), {}