Статьи

Понимание декораторов Python за 12 простых шагов!

Хорошо, возможно, я шучу. Я считаю, что декораторы-инструкторы Python — это тема, с которой студенты постоянно сталкиваются при первом знакомстве. Это потому, что декораторы трудно понять! Получение декораторов требует понимания нескольких концепций функционального программирования, а также понимания некоторых уникальных особенностей определения функций Python и синтаксиса вызова функций.

Я не могу сделать декораторов легкими, но, возможно, пройдя каждый шаг головоломки по шагам за раз, я смогу помочь вам чувствовать себя более уверенно в понимании декораторов [1] . Поскольку декораторы сложны, это будет длинная статья — но придерживайтесь ее! Я обещаю сделать каждую часть максимально простой — и если вы поймете каждую часть, вы поймете, как работают декораторы! Я пытаюсь предположить минимальные знания Python, но это, вероятно, будет наиболее полезно для людей, которые имеют хотя бы случайное знакомство с Python.

Я должен также отметить, что я использовал модули doctest Python для запуска примеров кода Python в этой статье. Код выглядит как интерактивная консольная сессия Python ( >>> и указывают на операторы Python, в то время как выходные данные имеют свою собственную строку). Иногда появляются загадочные комментарии, начинающиеся с «doctest» — это просто директивы для doctest, и их можно смело игнорировать.

1. Функции

Функции в Python создаются с ключевым словом def и принимают имя и необязательный список параметров. Они могут возвращать значения с помощью ключевого слова return. Давайте сделаем и вызовем простейшую из возможных функций:

>>> def foo():
...     return 1
>>> foo()
1

Тело функции (как и для всех многострочных операторов в Python) является обязательным и обозначается отступом. Мы можем вызывать функции, добавляя скобки к имени функции.

2. Область применения

В Python функции создают новую область видимости. Pythonistas также может сказать, что функции имеют свое собственное пространство имен. Это означает, что Python сначала ищет в пространстве имен функции, чтобы найти имена переменных, когда он встречает их в теле функции. Python включает в себя несколько функций, которые позволяют нам смотреть на наши пространства имен. Давайте напишем простую функцию, чтобы исследовать разницу между локальной и глобальной областью действия.

>>> a_string = "This is a global variable"
>>> def foo():
...     print locals()
>>> print globals() # doctest: +ELLIPSIS
{..., 'a_string': 'This is a global variable'}
>>> foo() # 2
{}

Встроенная функция globals возвращает словарь, содержащий все имена переменных, о которых знает Python. (Для ясности в выводе я пропустил несколько переменных, которые автоматически создает Python.) В пункте №2 я вызвал свою функцию foo, которая печатает содержимое локального пространства имен внутри функции. Как мы видим, функция foo имеет свое собственное отдельное пространство имен, которое в настоящее время пусто.

3. правила переменного разрешения

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

>>> a_string = "This is a global variable"
>>> def foo():
...     print a_string # 1
>>> foo()
This is a global variable

В точке # 1 Python ищет локальную переменную в нашей функции и, не найдя ее, ищет глобальную переменную [2] с тем же именем.

С другой стороны, если мы пытаемся присвоить глобальную переменную внутри нашей функции, она не делает то, что нам нужно:

>>> a_string = "This is a global variable"
>>> def foo():
...     a_string = "test" # 1
...     print locals()
>>> foo()
{'a_string': 'test'}
>>> a_string # 2
'This is a global variable'

Как мы видим, к глобальным переменным можно обращаться (даже изменять, если они являются изменяемыми типами данных), но нельзя (по умолчанию) назначать. В точке # 1 внутри нашей функции мы фактически создаем новую локальную переменную, которая «затеняет» глобальную переменную с тем же именем. Мы можем увидеть это, напечатав локальное пространство имен внутри нашей функции foo и заметив, что теперь в нем есть запись. Мы также можем видеть обратно в глобальном пространстве имен в точке # 2, что, когда мы проверяем значение переменной a_string, оно вообще не изменялось.

4. Переменный срок службы

Также важно отметить, что переменные не только живут внутри пространства имен, они также имеют время жизни. Рассматривать

>>> def foo():
...     x = 1
>>> foo()
>>> print x # 1
Traceback (most recent call last):
  ...
NameError: name 'x' is not defined

Проблему вызывают не только правила области видимости в пункте 1 (хотя именно поэтому у нас есть NameError), это также связано с тем, как вызовы функций реализованы в Python и многих других языках. На данный момент нет никакого синтаксиса, который мы могли бы использовать, чтобы получить значение переменной x — его буквально не существует! Пространство имен, созданное для нашей функции foo, создается с нуля при каждом вызове функции и уничтожается при ее завершении.

5. Функциональные параметры

Python позволяет нам передавать параметры в функции. Имена параметров становятся локальными переменными в нашей функции.

>>> def foo(x):
...     print locals()
>>> foo(1)
{'x': 1}

У Python есть множество способов определить и передать параметры функции. Для полной скины вы захотите ознакомиться с документацией Python по определению функций . Я дам вам короткую версию здесь: параметры функций могут быть позиционными параметрами, которые являются обязательными, или именованными, параметры значений по умолчанию, которые являются необязательными .

>>> def foo(x, y=0): # 1
...     return x - y
>>> foo(3, 1) # 2
2
>>> foo(3) # 3
3
>>> foo() # 4
Traceback (most recent call last):
  ...
TypeError: foo() takes at least 1 argument (0 given)
>>> foo(y=1, x=3) # 5
2

 

В точке # 1 мы определяем функцию, которая имеет один позиционный параметр x и единственный именованный параметр y. Как мы видим в точке # 2, мы можем вызывать эту функцию, передавая значения в обычном порядке — значения передаются в параметры foo по позиции, даже если в определении функции определен как именованный параметр. Мы также можем вызвать функцию, не передавая вообще никаких аргументов для именованного параметра, как вы можете видеть в пункте # 3 — Python использует значение по умолчанию 0, которое мы объявили, если он не получает значение для именованного параметра y. Конечно, мы не можем пропустить значения для первого (обязательного, позиционного) параметра — точка № 4 демонстрирует, что это приводит к исключению.

Все ясно и понятно? Теперь это немного сбивает с толку — Python поддерживает именованные параметры также во время вызова функции, а не только во время определения функции. Посмотрите на пункт № 5 — здесь мы вызываем функцию с двумя именованными параметрами, даже если она была определена с одним именованным и одним позиционным параметром. Поскольку у нас есть имена для наших параметров, порядок их передачи не имеет значения.

Обратный случай, конечно, верно. Один из параметров нашей функции определен как именованный параметр, но мы передали ему значение по позиции — вызов foo (3,1) в точке # 4 передает 3 в качестве первого параметра нашему упорядоченному параметру x и передает second (целое число 1) ко второму параметру, даже если он был определен как именованный параметр.

Ого! Это похоже на множество слов, описывающих довольно простую концепцию: параметры функции могут иметь имена или позиции. Это означает немного разные вещи в зависимости от того, находимся ли мы в определении функции или во время вызова функции, и мы можем использовать именованные параметры для функций, определенных только с позиционными параметрами, и наоборот! Опять же — если это было слишком срочно, обязательно ознакомьтесь с документами .

6. Вложенные функции

Python позволяет создавать вложенные функции. Это означает, что мы можем объявить функции внутри функций, и все правила области видимости и времени жизни по-прежнему применяются нормально.

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     inner() # 2
...
>>> outer()
1

Это выглядит немного сложнее, но все же ведет себя довольно разумно. Подумайте, что происходит в точке # 1 — Python ищет локальную переменную с именем x, в противном случае она ищет в охватывающей области видимости, которая является другой функцией! Переменная x является локальной переменной для нашей внешней функции, но, как и раньше, наша внутренняя функция имеет доступ к закрытой области видимости (по крайней мере, для чтения и изменения). В точке № 2 мы называем нашу внутреннюю функцию. Важно помнить, что inner также является просто именем переменной, которое следует правилам поиска переменных Python — Python сначала просматривает область видимости external и находит локальную переменную с именем inner.

7. Функции — это объекты первого класса в Python

Это просто наблюдение, что в Python функции являются объектами, как и все остальное. Ах, функция, содержащая переменную, вы не такой уж особенный!

>>> issubclass(int, object) # all objects in Python inherit from a common baseclass
True
>>> def foo():
...     pass
>>> foo.__class__ # 1
<type 'function'>
>>> issubclass(foo.__class__, object)
True

 Возможно, вы никогда не думали о том, что ваши функции имеют атрибуты, но функции в Python — это объекты, как и все остальное. (Если вы обнаружите, что запутанное ожидание, пока вы не услышите, что классы являются объектами в Python, точно так же, как и все остальное!) Возможно, это объясняется академическим способом — функции — это просто обычные значения, как и любой другой вид значений в Python. Это означает, что вы можете передавать функции в функции в качестве параметров или возвращать функции из функций в качестве возвращаемых значений! Если вы никогда не задумывались об этом, рассмотрите следующий вполне допустимый Python:

>>> def add(x, y):
...     return x + y
>>> def sub(x, y):
...     return x - y
>>> def apply(func, x, y): # 1
...     return func(x, y) # 2
>>> apply(add, 2, 1) # 3
3
>>> apply(sub, 2, 1)
1

Этот пример может показаться вам не слишком странным — add и sub — обычные функции Python, которые получают два значения и возвращают вычисленное значение. В точке # 1 вы можете видеть, что переменная, предназначенная для получения функции, является обычной переменной, как и любая другая. В точке # 2 мы вызываем функцию, переданную в apply — круглые скобки в Python являются оператором вызова и вызывают значение, которое содержит имя переменной. И в пункте 3 вы можете видеть, что передача функций в качестве значений не имеет специального синтаксиса в Python — имена функций — это просто метки переменных, как и любая другая переменная.

Вы могли видеть подобное поведение раньше — Python использует функции в качестве аргументов для часто используемых операций, таких как настройка отсортированной встроенной функции путем предоставления функции для параметра key. Но как насчет возврата функций в качестве значений? Рассматривать:

>>> def outer():
...     def inner():
...         print "Inside inner"
...     return inner # 1
...
>>> foo = outer() #2
>>> foo # doctest:+ELLIPSIS
<function inner at 0x...>
>>> foo()
Inside inner

Это может показаться немного более странным. В точке # 1 я возвращаю переменную inner, которая оказывается меткой функции. Здесь нет специального синтаксиса — наша функция возвращает внутреннюю функцию, которую иначе нельзя было бы вызвать. Помните переменную время жизни? Функция inner заново переопределяется каждый раз, когда вызывается функция external, но если inner не возвращен из функции, он просто перестанет существовать, когда выйдет из области видимости.

В точке # 2 мы можем поймать возвращаемое значение, которое является нашей внутренней функцией, и сохранить его в новой переменной foo. Мы можем видеть, что если мы вычислим foo, он действительно содержит нашу внутреннюю функцию, и мы можем вызвать ее, используя оператор вызова (скобки, помните?). Это может выглядеть немного странно, но до сих пор ничего сложного понять, верно? Держись, потому что вещи собираются повернуть за странные!

8. Закрытия

Давайте не будем начинать с определения, давайте начнем с другого примера кода. Что, если мы немного подправим наш последний пример:

>>> def outer():
...     x = 1
...     def inner():
...         print x # 1
...     return inner
>>> foo = outer()
>>> foo.func_closure # doctest: +ELLIPSIS
(<cell at 0x...: int object at 0x...>,)

Из нашего последнего примера мы видим, что inner — это функция, возвращаемая external, хранящаяся в переменной с именем foo, и мы можем вызывать ее с помощью foo (). Но это будет работать? Давайте сначала рассмотрим правила определения объема.

Все работает в соответствии с правилами области видимости Python — x является локальной переменной в нашей внешней функции. Когда inner печатает x в точке # 1, Python ищет локальную переменную для inner и, не найдя ее, смотрит во внешнюю область видимости, которая является функцией external, и находит ее там.

Но как насчет вещей с точки зрения переменной жизни? Наша переменная x является локальной для функции external, что означает, что она существует, только когда функция external работает. Мы не можем вызывать inner до тех пор, пока не вернемся external, поэтому, согласно нашей модели работы Python, x не должен больше существовать к тому времени, когда мы вызываем inner, и, возможно, должна произойти какая-то ошибка времени выполнения.

Оказывается, что вопреки нашим ожиданиям наша возвращенная внутренняя функция работает. Python поддерживает функцию, называемую замыканием функций, которая означает, что внутренние функции, определенные в неглобальной области видимости, запоминают, как выглядели их окружающие пространства имен во время определения . Это можно увидеть, посмотрев на атрибут func_closure нашей внутренней функции, который содержит переменные во вложенных областях.

Помните — функция inner заново определяется каждый раз, когда вызывается функция external. Прямо сейчас значение x не меняется, поэтому каждая внутренняя функция, которую мы получаем, делает то же самое, что и другая внутренняя функция — но что, если мы немного подправим ее?

>>> def outer(x):
...     def inner():
...         print x # 1
...     return inner
>>> print1 = outer(1)
>>> print2 = outer(2)
>>> print1()
1
>>> print2()
2

Из этого примера вы можете видеть, что замыкания — тот факт, что функции запоминают свою ограничивающую область — могут использоваться для создания пользовательских функций, которые, по сути, имеют жестко закодированный параметр. Мы не передаем числа 1 или 2 нашей внутренней функции, но создаем собственные версии нашей внутренней функции, которая «запоминает», какое число она должна напечатать.

Это одно мощное средство — вы даже можете думать о нем как о похожем на объектно-ориентированные методы в некотором смысле: external — это конструктор для inner с x, действующим как закрытая переменная-член. И их можно использовать множество — если вы знакомы с параметром key для отсортированной функции Python, вы, вероятно, написали лямбда-функцию для сортировки списка списков по второму элементу вместо первого. Теперь вы можете написать функцию itemgetter, которая принимает индекс для извлечения и возвращает функцию, которая может быть соответствующим образом передана параметру ключа.

Но давайте не будем делать ничего обыденного с замыканиями! Вместо этого давайте растянем еще раз и напишем декоратор!

9. Декораторы!

Декоратор — это просто замыкание, которое принимает функцию в качестве параметра и возвращает функцию замены. Мы начнем просто и перейдем к полезным декораторам.

>>> def outer(some_func):
...     def inner():
...         print "before some_func"
...         ret = some_func() # 1
...         return ret + 1
...     return inner
>>> def foo():
...     return 1
>>> decorated = outer(foo) # 2
>>> decorated()
before some_func
2

Посмотрите внимательно наш пример декоратора. Мы определили функцию с именем external, которая принимает один параметр some_func. Внутри внешнего мы определяем вложенную функцию с именем inner. Внутренняя функция напечатает строку, затем вызовет some_func, поймав ее возвращаемое значение в точке # 1. Значение some_func может отличаться при каждом вызове external, но независимо от того, какая это функция, мы будем ее вызывать. Наконец, inner возвращает возвращаемое значение some_func + 1 — и мы можем видеть, что когда мы вызываем нашу возвращенную функцию, хранящуюся в оформленном в точке # 2, мы получаем результаты prnnint, а также возвращаемое значение 2 вместо исходного возвращаемого значения 1 мы ожидаем получить, позвонив в Foo.

Можно сказать, что переменная украшен — это декорированная версия foo — это foo плюс что-то. Фактически, если бы мы написали полезный декоратор, мы могли бы захотеть полностью заменить foo на декорированную версию, чтобы мы всегда получали нашу версию «плюс что-то» для foo. Мы можем сделать это, не изучая новый синтаксис, просто переназначив переменную, содержащую нашу функцию:

>>> foo = outer(foo)
>>> foo # doctest: +ELLIPSIS
<function inner at 0x...>

Теперь любые вызовы foo () не получат оригинальный foo, они получат нашу украшенную версию! Есть идея? Давайте напишем более полезный декоратор.

Представьте, что у нас есть библиотека, которая дает нам координаты объектов. Возможно, они в основном состоят из координатных пар x и y. К сожалению, объекты координат не поддерживают математические операторы, и мы не можем изменить источник, поэтому мы не можем сами добавить эту поддержку. Однако мы собираемся выполнить несколько математических операций, поэтому мы хотим создать функции add и sub, которые принимают два объекта координат и выполняют соответствующие математические операции. Эти функции было бы легко написать (для иллюстрации приведу пример класса Coordinate)

>>> class Coordinate(object):
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return "Coord: " + str(self.__dict__)
>>> def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)
>>> def sub(a, b):
...     return Coordinate(a.x - b.x, a.y - b.y)
>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> add(one, two)
Coord: {'y': 400, 'x': 400}

 Но что, если у наших функций сложения и вычитания также должно быть некоторое поведение проверки границ? Возможно, вы можете суммировать или вычитать только на основе положительных координат, и любой результат должен быть ограничен также положительными координатами. Так что в настоящее время

>>> one = Coordinate(100, 200)
>>> two = Coordinate(300, 200)
>>> three = Coordinate(-100, -100)
>>> sub(one, two)
Coord: {'y': 0, 'x': -200}
>>> add(one, three)
Coord: {'y': 100, 'x': 0}

но мы бы предпочли, чтобы разность единиц и двух составляла { x : 0, y : 0}, а сумма единиц и трех составляла { x : 100, y : 200} без изменения одного, двух или трех. Вместо того, чтобы добавлять проверку границ во входные аргументы каждой функции и возвращаемое значение каждой функции, давайте напишем декоратор проверки границ! 

>>> def wrapper(func):
...     def checker(a, b): # 1
...         if a.x < 0 or a.y < 0:
...             a = Coordinate(a.x if a.x > 0 else 0, a.y if a.y > 0 else 0)
...         if b.x < 0 or b.y < 0:
...             b = Coordinate(b.x if b.x > 0 else 0, b.y if b.y > 0 else 0)
...         ret = func(a, b)
...         if ret.x < 0 or ret.y < 0:
...             ret = Coordinate(ret.x if ret.x > 0 else 0, ret.y if ret.y > 0 else 0)
...         return ret
...     return checker
>>> add = wrapper(add)
>>> sub = wrapper(sub)
>>> sub(one, two)
Coord: {'y': 0, 'x': 0}
>>> add(one, three)
Coord: {'y': 200, 'x': 100}

Этот декоратор работает так же, как и раньше — возвращает модифицированную версию функции, но в этом случае он делает что-то полезное, проверяя и нормализуя входные параметры и возвращаемое значение, подставляя 0 для любых отрицательных значений x или y.

Вопрос в том, сделает ли это наш код чище: изолировать проверку границ в его собственной функции и применить ее ко всем функциям, к которым мы стремимся, обернув их декоратором. Альтернативой может быть вызов функции для каждого входного аргумента и выходного результата перед возвратом внутри каждой математической функции, и нельзя отрицать, что использование декоратора является по меньшей мере менее повторяющимся с точки зрения количества кода, необходимого для применения проверки границ к функции , На самом деле — если наши собственные функции мы украшаем, мы могли бы сделать приложение-декоратор немного более очевидным.

10. Символ @ применяет декоратор к функции

Python 2.4 предоставил поддержку для обертывания функции в декораторе, предварительно ожидая определения функции с именем декоратора и символом @. В приведенных выше примерах кода мы украсили нашу функцию, заменив переменную, содержащую функцию, упакованной версией.

>>> add = wrapper(add)

Этот шаблон можно использовать в любое время, чтобы обернуть любую функцию. Но если мы определяем функцию, мы можем «украсить» ее символом @, например:

>>> @wrapper
... def add(a, b):
...     return Coordinate(a.x + b.x, a.y + b.y)

Важно признать, что это ничем не отличается от простой замены исходной переменной add на возвращаемую из функции-оболочки — Python просто добавляет синтаксический сахар, чтобы сделать происходящее очень явным.

11. * args и ** kwargs

Мы написали полезный декоратор, но он жестко запрограммирован для работы только с определенным типом функций, который принимает два аргумента. Наша внутренняя функция проверки принимает два аргумента и передает аргументы функции, захваченной в замыкании. Что если бы мы хотели декоратор, который делал что-то для любой возможной функции? Давайте напишем декоратор, который увеличивает счетчик для каждого вызова функции каждой декорированной функции без изменения каких-либо декорированных функций. Это означает, что он должен будет принять вызывающую подпись любой из функций, которые он декорирует, а также вызвать функции, которые он декорирует, передавая те аргументы, которые ему были переданы.

Так уж сложилось, что в Python есть синтаксическая поддержка именно этой функции. Обязательно прочитайте Учебное пособие по Python для получения более подробной информации, но оператор *, используемый при определении функции, означает, что любые дополнительные позиционные аргументы, переданные функции, заканчиваются в переменной, предваряемой *. Так:

>>> def one(*args):
...     print args # 1
>>> one()
()
>>> one(1, 2, 3)
(1, 2, 3)
>>> def two(x, y, *args): # 2
...     print x, y, args
>>> two('a', 'b', 'c')
a b ('c',)

Первая функция просто печатает любые (если таковые имеются) позиционные аргументы, переданные ей. Как вы можете видеть в пункте # 1, мы просто ссылаемся на переменные args внутри функции — * args используется только в определении функции, чтобы указать, что позиционные аргументы должны храниться в переменной args. Python также позволяет нам указывать некоторые переменные и перехватывать любые дополнительные параметры в аргументах, как мы видим в пункте # 2.

Оператор * также может использоваться при вызове функций, и здесь это означает аналогию. Переменная, предваряемая * при вызове функции, означает, что содержимое переменной следует извлечь и использовать в качестве позиционных параметров. Опять примером:

>>> def add(x, y):
...     return x + y
>>> lst = [1,2]
>>> add(lst[0], lst[1]) # 1
3
>>> add(*lst) # 2
3
 

Код в точке № 1 делает то же самое, что и код в точке № 2. Python автоматически делает для нас в точке № 2 то, что мы могли бы сделать вручную для себя. Это не так уж плохо — * args означает либо извлечение позиционных переменных из итерируемого при вызове функции, либо при определении функции принимают любые дополнительные позиционные переменные.

Все становится немного сложнее, когда мы вводим **, что делает для словарей и пар ключ / значение именно то, что * делает для итерируемых и позиционных параметров. Просто, правда?

>>> def foo(**kwargs):
...     print kwargs
>>> foo()
{}
>>> foo(x=1, y=2)
{'y': 2, 'x': 1}

Когда мы определяем функцию, мы можем использовать ** kwargs, чтобы указать, что все незафиксированные аргументы ключевых слов должны храниться в словаре, называемом kwargs. Как и прежде, ни имя args, ни kwargs не являются частью синтаксиса Python, но принято объявлять эти имена переменных при объявлении функций. Так же, как * мы можем использовать ** при вызове функции, а также при ее определении.

>>> dct = {'x': 1, 'y': 2}
>>> def bar(x, y):
...     return x + y
>>> bar(**dct)
3

 

12. Более общие декораторы

Учитывая наши новые возможности, мы можем написать декоратор, который «записывает» аргументы в функции. Для простоты мы просто напечатаем на стандартный вывод:

>>> def logger(func):
...     def inner(*args, **kwargs): #1
...         print "Arguments were: %s, %s" % (args, kwargs)
...         return func(*args, **kwargs) #2
...     return inner

Обратите внимание, что наша внутренняя функция принимает любое произвольное число и тип параметров в точке # 1 и передает их в упакованную функцию в точке # 2. Это позволяет нам обернуть или украсить любую функцию.

>>> @logger
... def foo1(x, y=1):
...     return x * y
>>> @logger
... def foo2():
...     return 2
>>> foo1(5, 4)
Arguments were: (5, 4), {}
20
>>> foo1(1)
Arguments were: (1,), {}
1
>>> foo2()
Arguments were: (), {}
2

Вызов наших функций приводит к выходной строке «logging», а также к ожидаемому возвращаемому значению каждой функции.

Подробнее о декораторах

Если вы следовали последнему примеру, вы понимаете декораторов! Поздравляем — иди и используй свои новые силы навсегда!

Вы могли бы также подумать о дальнейшем изучении Брюса Эккеля, который имеет превосходное эссе о декораторах и реализует их в Python с объектами вместо функций. Возможно, вам будет проще прочитать код ООП, чем нашу чисто функциональную версию. У Брюса также есть последующее эссе по предоставлению параметров декораторам, которые также легче реализовать с объектами, чем с функциями. Наконец, вы также можете исследовать встроенную функцию обертывания functools, которая (смущает) является декоратором, который можно использовать в наших декораторах для изменения сигнатуры наших замещающих функций, чтобы они больше походили на декорированную функцию.