Декораторы
В главе 32, посвященной расширенным возможностям классов, мы встречались со статическими методами и методами классов, кратко рассмотрели декораторный синтаксис @, предлагаемый Python для их объявления, и предварительно ознакомились с методиками реализации декораторов. Декораторы функций также бегло упоминались в главе 38 при исследовании способности встроенной функции property выступать в качестве декоратора и в главе 29 во время изучения понятия абстрактных суперклассов.
В этой главе раскрытие декораторов продолжается с того места, где оно было оставлено ранее. Здесь мы подробнее обсудим внутреннюю работу декораторов и ознакомимся с более развитыми способами реализации новых декораторов. Как выяснится, в декораторах будут регулярно обнаруживаться многие концепции, которые исследовались ранее, особенно предохранение состояния.
Тема довольно-таки сложная, а создание декораторов больше интересует скорее разработчиков инструментов, чем прикладных программистов. Тем не менее, учитывая все возрастающее распространение декораторов в популярных фреймворках для Python, их базовое понимание может помочь в прояснении исполняемой ими роли, даже если вы являетесь обычным пользователем декораторов.
Помимо описания деталей создания декораторов настоящая глава служит более реалистичным учебным пособием по применению Python. Поскольку приводимые в ней примеры оказываются крупнее, чем в большинстве других глав книги, они лучше иллюстрируют способы объединения кода в более завершенные системы и инструменты. В качестве дополнительного преимущества некоторые написанные здесь программы могут использоваться как универсальные инструменты при повседневном программировании.
Что такое декоратор?
Декорирование представляет собой способ указания управляющего или дополняющего кода для функций и классов. Сами декораторы принимают вид вызываемых объектов (например, функций), обрабатывающих другие вызываемые объекты. Как было показано ранее в книге, декораторы Python имеют две связанные друг с другом формы, ни одна из которых не требует Python З.Х или классов нового стиля.
• Декораторы функций, добавленные в Python 2.4, делают повторное привязывание имен во время определения функций, предоставляя уровень логики, который может управлять функциями и методами или последующими обращениями к ним.
• Декораторы классов, добавленные в Python 2.6 и 3.0, делают повторное привязывание имен во время определения классов, предоставляя уровень логики, который может управлять классами или экземплярами, созданными при последующих обращениях к классам.
Выражаясь кратко, декораторы предлагают способ вставки автоматически запускаемого кода в конце операторов определения функций и классов — в конце def для декораторов функций и в конце class для декораторов классов. Такой код может исполнять множество ролей, как будет описано в дальнейших разделах.
Управление вызовами и экземплярами
В типичной ситуации такой автоматически запускаемый код может применяться для дополнения обращений к функциям и классам. Это организуется за счет ввода в действие объектов оболочек (известных как посредники), предназначенных для вызова в более позднее время.
Посредники вызовов
Декораторы функций вводят в действие объекты оболочек, которые позволяют перехватывать последующие вызовы функций и обрабатывать их по мере надобности, обычно передавая сами вызовы исходной функции для выполнения управляемого действия.
Посредники интерфейсов
Декораторы классов вводят в действие объекты оболочек, которые позволяют перехватывать последующие вызовы для создания экземпляров и при необходимости обрабатывать их, обычно передавая сами вызовы исходному классу для создания управляемого экземпляра.
Декораторы достигают таких эффектов за счет автоматической повторной привязки имен функций и классов к другим вызываемым объектам в конце операторов def и class. При обращении в более позднее время привязанные вызываемые объекты могут выполнять задачи наподобие отслеживания и измерения времени вызовов функций, управления доступом к атрибутам экземпляров и т.д.
Управление функциями и классами
Хотя большинство примеров в главе имеют дело с использованием объектов оболочек для перехвата последующих обращений к функциям и классам, это не единственный способ применения декораторов.
Администраторы функций
Декораторы функций также могут использоваться для управления объектами функций взамен или в дополнение к их последующим вызовам, скажем, чтобы регистрировать функции в API-интерфейсах. Однако основное внимание здесь будет уделяться их более распространенному применению в качестве оболочек вызовов.
Администраторы классов
Декораторы классов также могут использоваться для непосредственного управления объектами классов взамен или в дополнение к вызовам, создающим экземпляры, например, чтобы дополнить класс новыми методами. Поскольку такая роль плотно пересекается с ролью метаклассов, в следующей главе будут приведены дополнительные сценарии применения. Как выяснится, оба инструмента запускаются в конце процесса создания классов, но декораторы классов часто предлагают более легковесное решение.
Другими словами, декораторы функций могут использоваться для управления вызовами функций и объектами функций, а декораторы классов — для управления экземплярами классов и собственно классами. За счет возвращения самого декорируемого объекта вместо оболочки декораторы становятся для функций и классов простым шагом, предпринимаемым после создания.
Независимо от роли, которую исполняют декораторы, они обеспечивают удобный и явный способ реализации инструментов, полезный на стадии разработки программ и в действующих производственных системах.
Использование и определение декораторов
В зависимости от вашего рабочего задания вы можете иметь дело с декораторами как пользователь или как поставщик (вы также могли бы заниматься сопровождением, но это лишь означает принятие нейтральной позиции). Мы увидим, что в состав Python входят встроенные декораторы, исполняющие специализированные роли — объявление статических методов и методов классов, создание свойств и т.д. Вдобавок многие популярные инструментальные комплекты для Python содержат декораторы, предназначенные для решения таких задач, как управление логикой работы с базами данных или пользовательскими интерфейсами. В подобных случаях мы вполне можем обойтись без знания о том, как реализуются декораторы.
Для более общих задач программисты могут самостоятельно создавать произвольные декораторы. Скажем, декораторы функций могут применяться для дополнения функций кодом, который отслеживает или регистрирует вызовы в журнале, производит проверку допустимости аргументов на стадии отладки, автоматически получает и освобождает блокировки в потоках, измеряет время выполнения вызовов функций в целях оптимизации и делает многое другое. Любое мыслимое поведение, которое вы пожелали бы добавить к вызову функции (в действительности поместив его внутрь оболочки), является кандидатом для специальных декораторов функций.
С другой стороны, декораторы функций предназначены для дополнения только вызовов конкретных функций или методов, а не всего объектного интерфейса. С последней ролью лучше справляются декораторы классов — учитывая возможность перехвата ими вызовов, создающих экземпляры, их можно использовать для реализации любых дополнений объектных интерфейсов или задач управления. Например, специальные декораторы классов способны отслеживать, проверять допустимость либо иным образом дополнять каждую ссылку на атрибут, предпринятую для объекта. Они также могут применяться для реализации объектов-посредников, классов-одиночек и других общих кодовых схем. На самом деле мы обнаружим, что многие декораторы классов сильно напоминают — и фактически являются главным приложением — кодовой схемы делегирования, которая обсуждалась в главе 31.
Для чего используются декораторы?
Подобно многим расширенным инструментам Python с чисто технической точки зрения декораторы никогда не считаются строго обязательными: мы часто в состоянии реализовать их функциональность с использованием вызовов простых вспомогательных функций или других приемов. И на базовом уровне мы всегда можем вручную написать код повторной привязки имен, которую декораторы выполняют автоматически.
Тем не менее, декораторы предлагают для таких задач явный синтаксис, который проясняет намерение, способен минимизировать избыточность дополняющего кода и может содействовать в обеспечении корректного применения API-интерфейсов.
• Декораторы имеют чрезвычайно явный синтаксис, что позволяет заметить их гораздо быстрее, чем вызовы вспомогательных функций, которые могут находиться произвольно далеко от целевых функций или классов.
• Декораторы применяются один раз при определении целевой функции или класса; нет необходимости снабжать каждое обращение к классу или функции дополнительным кодом, который может потребовать внесения изменений в будущем.
• Учитывая оба предшествующих пункта, декораторы снижают вероятность того, что пользователь API-интерфейса забудет дополнить функцию или класс в соответствии с требованиями данного API-интерфейса.
Другими словами, помимо своей специальной модели декораторы предлагают ряд преимуществ в плане сопровождения и согласованности кода. Кроме того, как инструменты структурирования, декораторы естественным образом содействуют инкапсуляции кода, которая сокращает избыточность и облегчает внесение изменений в будущем.
Декораторы также обладают потенциальными недостатками — когда они вставляют логику оболочки, то могут изменить типы декорированных объектов и повлечь за собой дополнительные вызовы в случае использования в качестве посредников для вызовов либо интерфейсов. С другой стороны, те же соображения применимы к любой методике добавления логики оболочки к объектам.
Позже в главе мы исследуем такие компромиссы в контексте реального кода. Несмотря на то что решение использовать декораторы все же в чем-то субъективно, их преимущества достаточно убедительны для того, чтобы они быстро стали рекомендованной практикой в мире Python. Давайте рассмотрим детали, которые помогут вам сделать собственный выбор.
j. _ - Декораторы или макросы. Декораторы Python имеют сходство с тем, что в
'■к. других языках называется аспектно-ориентированным программированием,
| которое предусматривает вставку кода, подлежащего автоматическому
^ запуску до и после выполнения вызова функции. Синтаксис декораторов также очень близко напоминает синтаксис аннотаций Java (и вероятно позаимствован оттуда), хотя модель Python, как правило, считается более гибкой и универсальной.
Некоторые сравнивают декораторы и с макросами, но это не совсем уместно и может даже вводить в заблуждение. Макросы (скажем, директива препроцессора #define в языке С) обычно ассоциируются с текстовой заменой и расширением и предназначены для генерации кода. Наоборот, декораторы Python являются операциями времени выполнения, основанными на повторной привязке имен, вызываемых объектах и зачастую посредниках. Наряду с тем, что декораторы и макросы могут иметь временами перекрывающиеся сценарии применения, они фундаментально отличаются областью действия, реализацией и кодовыми схемами. Их сравнение сродни сравнению оператора import в Python с директивой #include в С, когда похожим образом путаются основанная на объектах операция времени выполнения и вставка текста.
Конечно, со временем термин макрос был несколько ослаблен — для некоторых теперь он может также означать любую заготовленную последовательность шагов или процедуру — и пользователи других языков могут счесть аналогию с дескрипторами в любом случае полезной. Но им, вероятно, следует иметь в виду и то, что декораторы имеют дело с вызываемыми объектами, которые управляют вызываемыми объектами, а не с расширением текста. Язык Python, как правило, лучше постигать и использовать в переводе на идиомы Python.
Основы
Давайте начнем с того, что рассмотрим поведение декорирования в фигуральном смысле. Вскоре мы напишем реальный и более содержательный код, но поскольку большая часть магии декораторов сводится к автоматической операции повторной привязки, важно сначала понять такое соответствие.
Декораторы функций
Декораторы функций были доступны, начиная с версии Python 2.4. Как упоминалось ранее в книге, они в основном представляют собой всего лишь синтаксический сахар, который обеспечивает запуск одной функции через другую в конце оператора def и повторно привязывает имя исходной функции к результату.
Использование
Декоратор функции является своего рода объявлением времени выполнения о функции, чье определение следует за декоратором. Декоратор записывается в строке прямо перед оператором def, определяющим функцию или метод, и состоит из символа @, за которым находится ссылка на метафункцию — функцию (или другой вызываемый объект), управляющую другой функцией.
В переводе на код декораторы функций автоматически отображают показанный ниже синтаксис:
@decorator # Декорирование функции
def F(arg):
F(99) # Вызов функции
на следующую эквивалентную форму, где decorator — это вызываемый объект с одним аргументом, который возвращает вызываемый объект с таким же количеством аргументов, как у функции F (если не саму F):
def F(arg):
F = decorator (F) # Повторная привязка имени функции к результату декоратора
F (99) # По существу вызывается decorator (F) (99)
Такая автоматическая повторная привязка имен работает с любым оператором def, определяет он простую функцию или метод внутри класса. Когда функция F позже вызывается, фактически производится обращение к объекту, возвращенному декоратором, который может быть либо другим объектом, реализующим необходимую логику оболочки, либо самой исходной функцией.
Другими словами, декорирование по существу отображает показанное далее первое выражение на второе — хотя декоратор в действительности выполняется только раз во время декорирования:
func(6, 7)
decorator(func)(6, 7)
Автоматическая повторная привязка имен объясняет синтаксис декорирования статических методов и свойств, встречавшийся ранее в книге:
class С:
@staticmethod
def meth(...): ... # meth = staticmethod (meth)
class C:
0property
def name (self): ... # name = property (name)
В обоих случаях имя метода повторно привязывается к результату встроенного декоратора в конце оператора def. Дальнейший вызов исходного имени инициирует обращение к любому объекту, который возвратил декоратор. В приведенных особых случаях исходные имена повторно привязываются к статическому методу и дескриптору свойства, но как объясняется в следующем разделе, процесс намного более универсален.
Реализация
Сам декоратор является вызываемым объектом, возвращающим вызываемый объект. То есть он возвращает объект, к которому производится обращение позже, когда декорированная функция вызывается через свое исходное имя — либо объект оболочки для перехвата будущих вызовов, либо исходную функцию, дополненную каким-нибудь образом. На самом деле декораторы способны быть вызываемым объектом любого типа и возвращать вызываемый объект любого типа: может применяться любое сочетание функций и классов, хотя некоторые лучше подходят в определенных контекстах.
Например, чтобы использовать протокол декорирования для управления функцией сразу же после ее создания, мы могли бы реализовать декоратор такого вида:
def decorator(F):
# Обработка функции F
return F
@decorator
def func () : . . . # func = decorator (func)
Поскольку исходная декорированная функция снова присваивается своему имени, в итоге к определению функции просто добавляется шаг после создания. Структура подобного рода может применяться для регистрации функции в API-интерфейса, присваивания атрибутов функций и т.д.
В более типичной ситуации для вставки логики перехвата последующих обращений к функции мы могли бы реализовать декоратор, который возвращает объект, отличающийся от исходной функции — посредник для вызовов в более позднее время:
def decorator(F):
# Сохранение либо использование функции F
# Возвращение другого вызываемого объекта:
# вложенного def, класса с_call_ и т.д.
^decorator
def func () : . . . # func = decorator (func)
Декоратор вызывается во время декорирования, а вызываемый объект, который он возвращает, вызывается при обращении к исходному имени функции в будущем. Сам декоратор принимает декорированную функцию; возвращенный вызываемый объект принимает любые аргументы, переданные позже имени декорированной функции. При надлежащей реализации все работает аналогично методам уровня класса: в первом аргументе возвращенного вызываемого объекта оказывается подразумеваемый объект экземпляра.
Ниже показана общая кодовая схема, воплощающая эту идею — декоратор возвращает объект-оболочку, который предохраняет исходную функцию в объемлющей области видимости:
def decorator(F): # При декорировании в
def wrapper(*args): # При вызове внутренней функции
# Использование функции F и аргументов ,
# F(*args) вызывает исходную функцию return wrapper
@decorator # func = decorator(func)
def func(x, y) : # func передается функции F декоратора
func (6, 7) # 6, 7 передается аргументу *args оболочки
Когда позже происходит обращение к имени func, в действительности вызывается функция wrapper, возвращенная декоратором decorator; функция wrapper затем может запустить исходную функцию func, т.к. она по-прежнему доступна в объемлшцей области видимости. При реализации подобным образом каждая декорированная функция производит новую область видимости для предохранения состояния.
Чтобы сделать то же самое посредством классов, мы можем перегрузить операцию вызова и применять атрибуты экземпляра вместо объемлющих областей видимости:
class decorator:
def _init_(self, func) : # При декорировании @
self.func = func def _call_(self, *args) : # При вызове внутренней функции
# Использование self.func и аргументов
# self. func(*args) вызывает исходную функцию
0decorator
def func(x, у) : # func = decorator (func)
... # func передается_init_
func (6, 7) # 6, 7 передается аргументу *args метода_call_
Когда позже происходит обращение к имени func, то в действительности вызывается метод перегрузки операций_call_экземпляра, созданного декоратором
decorator; метод_call_затем запускает исходную функцию func, потому что она
по-прежнему доступна в атрибуте экземпляра. В случае такой реализации каждая декорированная функция выпускает новый экземпляр для предохранения состояния.
Поддержка декорирования методов
С предыдущей реализацией, основанной на классе, связан один тонкий момент. Несмотря на то что она нормально работает при перехвате вызовов простых функций, этого не происходит в случае ее применения к функциям методов на уровне класса:
class decorator:
def _init_(self, func) : # func - метод без экземпляра
self. func = func def _call_(self, *args) : # self - экземпляр декоратора
# self.func(*args) терпит неудачу!
# Экземпляр С не находится в args!
class С:
@decorator
def method(self, x, у) : # method = decorator(method)
... # Повторная привязка к экземпляру декоратора
При такой реализации декорированный метод повторно привязывается к экземпляру класса декоратора вместо простой функции.
Проблема заключается в том, что аргумент self в методе_call_декоратора
получает экземпляр класса decorator, когда метод вызывается позже, и экземпляр класса С никогда не помещается в *args. В результате направление вызова исходному методу становится невозможным — объект декоратора храпит исходную функцию метода, но не располагает экземпляром, чтобы ей передать.
Для поддержки и функций, и методов лучше подойдет альтернатива в виде вложенной функции:
def decorator (F) : # F - функция или метод без экземпляра
def wrapper(*args): # Для метода экземпляр класса находится в args[0]
# F(*args) запускает func или method
return wrapper
@decorator
def func(x, y) : # func = decorator (func)
func (6, 7) # В действительности вызывается wrapper (6, 7)
class С:
@decorator
def method (self, x, y) : # method = decorator (method)
... # Повторная привязка к простой функции
X = С()
X.method(6, 7) # В действительности вызывается wrapper (X, 6, 7)
Здесь метод wrapper принимает в своем первом аргументе экземпляр класса С, поэтому он может направляться на исходный метод и получать доступ к информации состояния.
Формально такая версия с вложенной функцией работает из-за того, что Python создает объект связанного метода и потому передает экземпляр целевого класса аргументу self, только когда атрибут метода ссылается на простую функцию. Когда взамен он ссылается на экземпляр вызываемого класса, то данный экземпляр передается в self, чтобы предоставить вызываемому классу доступ к собственной информации состояния. Мы увидим, что это тонкое отличие может иметь значение в более реалистичных примерах, рассматриваемых позже в главе.
Также обратите внимание, что вложенные функции являются, пожалуй, самым прямолинейным способом поддержки декорирования функций и методов, но отнюдь не единственным. Скажем, дескрипторы из предыдущей главы при вызове получают экземпляры дескриптора и целевого класса.
Несмотря на более высокую сложность, далее в главе будет показано, каким образом задействовать данный инструмент и в таком контексте.
Декораторы классов
Декораторы функций настолько доказали свою полезность, что модель была расширена для поддержки декорирования классов, начиная с версий Python 2.6 и 3.0. Поначалу декораторам классов оказывалось сопротивление, т.к. их роль частично совпадала с ролью метаклассов, однако, в конце концов, их официально приняли из-за обеспечения ими более простого способа для достижения многих тех же целей.
Декораторы классов тесно связаны с декораторами функций; по сути, они используют одинаковый синтаксис и очень похожие кодовые схемы. Тем не менее, вместо помещения в оболочку индивидуальных функций или методов декораторы классов управляют классами или снабжают вызовы, создающие экземпляры, дополнительной логикой, которая управляет экземплярами, созданными из класса, или дополняет их. Во второй роли они могут управлять полными объектными интерфейсами.
Использование
Синтаксически декораторы классов указываются прямо перед операторами class в той же манере, как декораторы функций находятся непосредственно перед операторами def. Формально для декоратора decorator, который обязан быть вызываемым объектом, принимающим один аргумент и возвращающий вызываемый объект, показанный ниже синтаксис декоратора классов:
Qdecorator # Декорирование класса
class С:
х = С(99) # Создание экземпляра
эквивалентен следующему — класс автоматически передается функции декоратора, а ее результат присваивается имени класса:
class С:
С = decorator (С) # Повторная привязка имени класса к результату декоратора х = С (99) # По существу вызывается decorator (С) (99)
Совокупный эффект заключается в том, что обращение в будущем к имени класса для создания экземпляра приводит к запуску возвращенного декоратором вызываемого объекта, который может обращаться к самому исходному классу или нет.
Реализация
Новые декораторы классов реализуются с помощью многих тех же методик, которые применялись с декораторами функций, хотя часть их могут включать в себя два уровня дополнения — для управления как вызовами, создающими экземпляры, так и доступом к интерфейсу экземпляра. Поскольку декоратор классов также является вызываемым объектом, который возвращает вызываемый объект, его будет вполне достаточно для большинства сочетаний функций и классов.
Однако как бы декоратор классов ни был реализован, его результат запускается при последующем создании экземпляра.
Например, для простого управления классом сразу после его создания понадобится возвращать сам исходный класс:
def decorator(С):
# Обработка класса С return С
Qdecorator
class С: . . . # С = decorator(С)
Чтобы взамен вставить уровень оболочки, который перехватывает будущие вызовы, создающие экземпляры, необходимо возвращать другой вызываемый объект:
def decorator(С):
# Сохранение либо использование класса С
# Возвращение другого вызываемого объекта:
# вложенного def, класса с_call_ и т.д.
@decorator
class С: . . . # С = decorator(С)
Вызываемый объект, возвращаемый таким декоратором классов, обычно создает и возвращает новый экземпляр исходного класса, каким-то образом дополненный для управления его интерфейсом. Скажем, следующий декоратор вставляет объект, который перехватывает доступ к неопределенным атрибутам экземпляра класса:
def decorator (els) : # При декорировании в
class Wrapper:
def __init_(self, *args) : # При создании экземпляров
self.wrapped = cls(*args)
def _getattr_(self, name) : # При извлечении атрибутов
return getattr(self.wrapped, name) return Wrapper
Gdecorator
class С: # С = decorator(С)
def _init_(self, x, у) : # Запускается методом Wrapper._init_
self.attr = 'spam'
# В действительности вызывается Wrapper(6, 1)
х = С(б, 7) print(х.attr)
# Запускается Wrapper._getattr_, выводится spam
В приведенном примере декоратор повторно привязывает имя класса к другому классу, который предохраняет исходный класс в объемлющей области видимости и при обращении к исходному классу создает и внедряет его экземпляр. Когда позже из экземпляра извлекается какой-нибудь атрибут, операция перехватывается методом _getattr_объекта-оболочки и ее выполнение делегируется внедренному экземпляру исходного класса. Кроме того, каждый декорированный класс создает новую область видимости, которая запоминает исходный класс. Позже в главе мы расширим этот пример, превратив его в более полезный код.
Подобно декораторам функций декораторы классов обычно реализуются в виде “фабричных” функций, создающих и возвращающих вызываемые объекты, в форме классов, которые используют методы_init_или_call_для перехвата операций вызова, либо в виде какой-то их комбинации. Фабричные функции, как правило, предохраняют состояние в ссылках из объемлющих областей видимости, а классы — в атрибутах.
Поддержка множества экземпляров
Как и с декораторами функций, для декораторов классов одни комбинации вызываемых типов работают лучше, чем другие. Рассмотрим следующую ошибочную альтернативу для декоратора классов из предыдущего примера:
class Decorator:
def _init_(self, С) : # При декорировании @
self.С = С
def _call_(self, *args) : # При создании экземпляров
self.wrapped = self.С(*args) return self
def _getattr_(self, attrname) : # При извлечении атрибутов
return getattr(self.wrapped, attrname)
0Decorator
class C: . . . # С = Decorator(С)
x = C()
у = C() # Переписывает x!
В коде обрабатывается множество декорированных классов (каждый создает новый экземпляр Decorator) и перехватываются вызовы, создающие экземпляры (каждый запускает метод_call_). Тем не менее, в отличие от предыдущей показанная
выше версия терпит неудачу при обработке множества экземпляров заданного класса — каждый вызов, создающий экземпляр, переписывает ранее сохраненный экземпляр. Исходная версия не поддерживает множество экземпляров, потому что каждый вызов, создающий экземпляр, создает новый независимый объект-оболочку. В более общем случае любая из следующих схем поддерживает множество внутренних экземпляров:
def decorator (С) : # При декорировании в
class Wrapper:
def _init_(self, *args) : # При создании экземпляров: новый объект Wrapper
self.wrapped = C(*args) # Внедрение экземпляра в экземпляр return Wrapper
class Wrapper: ...
def decorator (С) : # При декорировании @
def onCall (*args) : # При создании экземпляров: новый объект Wrapper
return Wrapper (С (*args) ) # Внедрение экземпляра в экземпляр return onCall
Позже в главе мы исследуем это явление в более реалистичном контексте; однако, на практике мы обязаны позаботиться о надлежащем комбинировании вызываемых типов для поддержки нашего намерения и разумном выборе политик предохранения состояния.
Вложение декораторов
Временами одного декоратора недостаточно. Например, предположим, что вы реализовали два декоратора функций, предназначенных для применения на стадии разработки — один для проверки типов аргументов перед вызовом функции и еще один для проверки типа возвращаемого значения после вызова функции. Вы можете использовать любой из них независимо, но что делать, если желательно задействовать оба декоратора в одиночной функции? В действительности вам необходим способ вложения декораторов, чтобы результат одного декоратора был функцией, декорированной другим декоратором. До тех пор, пока при последующих вызовах выполняются оба шага, совершенно не важно, какой из декораторов будет вложенным.
Для поддержки множества вложенных шагов дополнения декораторный синтаксис позволяет добавлять к декорированной функции или методу несколько уровней логики оболочки. Когда применяется такая возможность, каждый декоратор должен находиться в собственной строке. Декораторный синтаксис в форме:
@А
@В
0С
def f ( . . . ) :
выполняется аналогично такому синтаксису:
def f(.. .) :
f - А (В (С (f) ) )
Здесь исходная функция проходит через три разных декоратора, а результирующий вызываемый объект присваивается исходному имени. Каждый декоратор обрабатывает результат предыдущего декоратора, который может быть исходной функцией или вставленным объектом-оболочкой.
Если все декораторы вставляют объекты-оболочки, тогда совокупный эффект заключается в том, что при обращении к имени исходной функции будут вызываться три разных уровня оболочки для дополнения исходной функции тремя разными способами. Декоратор, указанный последним, применяется первым и является наиболее глубоко вложенным декоратором, когда позже происходит вызов имени исходной функции.
Как и в случае с декораторами функций, множество декораторов классов дают в результате множество вызовов вложенных функций и возможно множество уровней и шагов логики оболочки вокруг вызовов, создающих экземпляры. Скажем, следующий код:
@spam @eggs class С:
X = СО эквивалентен такому коду:
class С:
С = spam(eggs(С))
X = СО
И снова каждый декоратор волен возвращать либо исходный класс, либо вставленный объект-оболочку. Благодаря объектам-оболочкам, когда в конечном итоге запрашивается экземпляр исходного класса С, вызов перенаправляется объектам уровня оболочки, предоставляемым декораторами spam и eggs, которые способны исполнять совершенно разные роли — например, они могут отслеживать и проверять допустимость доступа к атрибутам, причем оба шага выполнялись бы при будущих запросах.
Скажем, приведенные ниже ничего не делающие декораторы просто возвращают декорированную функцию:
def dl(F): return F def d2(F): return F def d3(F): return F
@dl
@d2
@d3
def func () : # func = dl (d2 (d3 (func)))
print('spam')
func () # Выводится spam
С классами работает такой же синтаксис, как у этих ничего не делающих декораторов.
Тем не менее, когда декораторы вставляют объекты функций оболочки, они могут дополнять исходную функцию при вызове — следующий код выполняет конкатенацию с ее результатом на уровнях декораторов по мере провождения уровней от внутреннего к внешнему:
def dl (F) : return lambda: 'X' + F() def d2(F): return lambda: 'Yf + F() def d3(F) : return lambda: 'Z' + F()
0dl
0d2
0d3
def func() : # func = dl (d2 (d3 (func)))
return 'spam'
print(func ()) # Выводится XYZspam
Для реализации уровней оболочки мы используем функции lambda (каждая предохраняет внутреннюю функцию в объемлющей области видимости); на практике объекты-оболочки могут принимать форму функций, вызываемых классов и т.д. При надлежащем проектировании вложение декораторов позволяет комбинировать шаги дополнения разнообразными способами.
Аргументы декораторов
Декораторы функций и классов также могут выглядеть как принимающие аргументы, хотя фактически эти аргументы передаются вызываемому объекту, который в действительности возвращает декоратор, а тот в свою очередь возвращает вызываемый объект. В итоге обычно устанавливается множество уровней предохранения состояния. Например, следующий код:
0decorator(А, В) def F(arg):
F (99)
автоматически отображается на показанную ниже эквивалентную форму, где decorator представляет собой вызываемый объект, который возвращает действительный декоратор. Возвращенный декоратор в свою очередь возвращает вызываемый объект, запускаемый позже для обращений к имени исходной функции:
def F(arg):
F = decorator (A, В) (F) # Повторная привязка F к возвращаемому значению decorator F (99) # По существу вызывается decorator (А, В) (F) (99)
Аргументы декораторов распознаются до того, как происходит декорирование, и обычно применяются для предохранения информации состояния с целью использования в будущих вызовах. Скажем, функция декоратора в рассматриваемом примере может принимать такую форму:
def decorator (А, В):
# Сохранение либо использование А, В def actualDecorator(F):
# Сохранение либо использование функции F
# Возвращение вызываемого объекта: вложенного def, класса с_call_ и т.д.
return callable
return actualDecorator
Внешняя функция в этой структуре обычно сохраняет аргументы декоратора как информацию состояния для применения в фактическом декораторе, в вызываемом объекте, который он возвращает, или в обоих. Фрагмент кода предохраняет аргумент информации состояния в ссылках из объемлющих областей видимости, но часто используются также и атрибуты класса.
Другими словами, аргументы декораторов нередко подразумевают наличие трех уровней вызываемых объектов: вызываемого объекта для приема аргументов декоратора, возвращающего вызываемый объект, который служит в качестве декоратора и возвращает вызываемый объект для обработки обращений к исходной функции или классу. Каждый из трех уровней может быть функцией либо классом и предохранять состояние в форме ссылок из объемлющих областей видимости или атрибутов класса.
Аргументы декораторов могут применяться при предоставлении значений для инициализации атрибутов, сообщений с трассировкой вызовов, имен атрибутов, подлежащих проверке допустимости, и многого другого — сюда входят конфигурационные параметры любого вида для объектов либо их посредников. Конкретные примеры использования аргументов декораторов будут приведены далее в главе.
Декораторы одновременно управляют функциями и классами
Хотя в остатке главы внимание в основном будет сосредоточено на помещении в оболочку будущих обращений к функциям и классам, важно помнить о том, что механизм декорирования является более универсальным — он представляет собой протокол для прогона функций и классов через любой вызываемый объект немедленно после их создания. Как таковое, декорирование также может применяться для вызова произвольной обработки после создания:
def decorator(О):
# Сохранение или дополнение функции или класса О return О
@decorator
def F() : . . . # F = decorator(F)
^decorator
class C: . . . # С = decorator(С)
До тех пор, пока мы возвращаем исходный декорированный объект вместо посредника, имеется возможность управлять самими функциями и классами, а не только обращениями к ним в более позднее время. Далее в главе мы увидим более реалистичные примеры, в которых данная идея используется для регистрации вызываемых объектов в API-интерфейсе с декорированием и присваиванием атрибутам функций при их создании.
Реализация декораторов функций
Что касается кода, то в остатке главы мы собираемся исследовать рабочие примеры, которые продемонстрируют только что изложенные концепции декораторов. В текущем разделе иллюстрируется работа нескольким декораторов функций, а в следующем — работа декораторов классов. Затем мы закончим более крупными учебными примерами применения декораторов классов и функций — полными реализациями защиты классов и проверки вхождения значений аргументов в заданный диапазон.
Отслеживание вызовов
Для начала давайте возродим пример трассировки вызовов из главы 32. Ниже определяется и применяется декоратор функции, который подсчитывает количество вызовов декорированной функции и для каждого вызова выводит трассировочное сообщение:
# Файл decoratorl .ру
class tracer:
def_init_(self, func) : # При декорировании в: сохранение исходной функции
self.calls = О self.func = func
def_call_(self, *args) : # При последующих вызовах: запуск исходной функции
self.calls += 1
print('call %s to %s' % (self.calls, self.func._name_))
self.func(*args)
0tracer
def spam (a, b, c) : # spam = tracer (spam)
print (a + b + c) # spam помещается в объект декоратора
Обратите внимание, что каждая функция, декорированная классом tracer, будет создавать новый экземпляр с собственным объектом сохраненной функции и счетчиком вызовов. Также взгляните на использование синтаксиса *args для упаковки и распаковки произвольно большого числа переданных аргументов. Такая универсальность делает возможным применение этого декоратора для помещения в оболочку любой функции с любым количеством позиционных аргументов; текущая версия пока еще не работает с ключевыми аргументами или методами уровня класса и не возвращает результатов, но позже в разделе мы устраним указанные недостатки.
Если теперь мы импортируем функцию модуля и протестируем ее в интерактивном сеансе, то получим показанное далее поведение — каждый вызов изначально генерирует трассировочное сообщение, потому что класс декоратора перехватывает вызов. Код работает в Python 2.Х и З.Х, как и весь код в главе, если не указано иное (я сделал вывод нейтральным к версиям, а декораторы не требуют классов нового стиля; кроме того, были сокращены некоторые шестнадцатеричные адреса):
>>> from decoratorl import spam
>>> spam(l, 2, 3) # В действительности вызывается объект-оболочка tracer call 1 to spam 6
»> spam('af, fbf , ’ с') # Вызывается метод_call_ в классе
call 2 to spam abc
»> spam.calls # Количество вызовов в информации состояния объекта-оболочки
2
>» spam
<decoratorl.tracer object at 0x02D9A730>
Во время выполнения класс tracer сохраняет декорированную функцию и перехватывает будущие ее вызовы, чтобы добавить уровень логики, которая подсчитывает вызовы и выводит для каждого сообщение. Обратите внимание, что общее количество вызовов отображается как атрибут декорированной функции. На самом деле в случае декорирования spam является экземпляром класса tracer, что может иметь последствия для программ, предпринимающих проверку типов, но в целом благоприятные (декораторы могли бы копировать атрибут_пате_исходной функции, но
такая подделка ограничена и способна привести к путанице).
Для вызовов функция декораторный синтаксис @ может оказаться удобнее, чем модификация каждого вызова для учета дополнительного уровня логики, и он избегает случайного вызова исходной функции напрямую. Вот эквивалентная реализация без декораторов:
calls = О
def tracer(func, *args): global calls calls += 1
print ('call %s to %s' % (calls, func._name_))
func(*args)
def spam(a, b, c): print (a, b, c)
>>> spam(l, 2,3) # Нормальный вызов без отслеживания: случайный?
12 3
>>> tracer (spam, 1,2,3) # Специальный вызов с отслеживанием без декораторов
call 1 to spam
12 3
Такая альтернативная версия может использоваться с любой функцией без специального синтаксиса @, но в отличие от версии с декоратором она требует добавочного синтаксиса в каждом месте, где функция вызывается в коде. Кроме того, ее намерение может быть не настолько очевидным, а гарантия того, что дополнительный уровень будет задействован для нормальных вызовов, попросту отсутствует. Хотя декораторы никогда не считаются обязательными (мы всегда можем вручную повторно привязывать имена), они часто являются самым удобным и унифицированным вариантом.
Варианты предохранения состояния для декораторов
В последнем примере предыдущего раздела поднята важная проблема. Декораторы функций предлагают разнообразные варианты предохранения информации состояния, предоставленной во время декорирования, для применения в течение фактического вызова функции. Как правило, они должны поддерживать множество декорированных объектов и множество вызовов, но есть несколько способов достижения таких целей: для предохранения состояния можно использовать атрибуты экземпляров, глобальные переменные, нелокальные переменные замыканий и атрибуты функций.
Атрибуты экземпляров классов
Ниже показана расширенная версия предыдущего примера с добавленной поддержкой ключевых аргументов посредством синтаксиса * *, которая к тому же возвращает результат внутренней функции для охвата большего числа сценариев применения (тем, кто читает книгу непоследовательно, сначала потребуется изучить ключевые аргументы в главе 18 первого тома):
class tracer: # Состояние через атрибуты экземпляра
def _init_(self, func) : # При декорировании @
self .calls = 0 # Сохранить функцию для вызова в будущем self. func = func
def _call_(self, *args, **kwargs) : # При вызове исходной функции
self.calls += 1
print('call %s to %sf % (self.calls, self.func._name_))
return self.func(*args, **kwargs)
@tracer
def spam(a, b, с) : # To же, что и spam = tracer (spam) print (a + b + с) # Запускается tracer._init_
@tracer
def eggs(x, у) : # To же, что и eggs = tracer (eggs)
print (x ** y) # eggs помещается в объект tracer
spam(l, 2,3) # Вызывается экземпляр tracer:
# запускается tracer._call_
spam(a=4, b=5, c=6) # spam - атрибут экземпляра
eggs {2, 16) # Вызывается экземпляр tracer, self. func - это eggs
eggs (4, y=4) # self. calls - значение для каждого случая декорирования
Как и в первоначальной версии, для явного хранения состояния здесь используются атрибуты экземпляров класса. Внутренняя функция и счетчик вызовов представляют информацию для каждого экземпляра — каждый случай декорирования получает собственную копию. При запуске как сценария в Python 2.Х или З.Х вывод данной версии будет следующим; обратите внимание на то, что функции spam и eggs имеют свои счетчики вызовов, поскольку каждый случай декорирования создает новый экземпляр класса:
c:\code> python decorator2.ру
call 1 to spam 6
call 2 to spam 15
call 1 to eggs 65536
call 2 to eggs 256
Несмотря на то что такая кодовая схема удобна для декорирования функций, она по-прежнему сопряжена с проблемами, когда применяется к методам — недостаток, который мы устраним позже.
Объемлющие области видимости и глобальные переменные
Функции замыканий — со ссылками из объемлющих областей видимости de f и вложенными операторами def — часто могут обеспечить тот же самый эффект, особенно для статических данных вроде декорированной исходной функции. Однако в этом примере нам также понадобится счетчик в объемлющей области видимости, который изменяется при каждом вызове, что невозможно в Python 2.Х (вспомните из главы 17 первого тома, что оператор nonlocal доступен только в Python З.Х).
В Python 2.Х мы можем использовать либо классы и атрибуты, как в предыдущем разделе, либо другие варианты. Одним из кандидатов будет перенос переменных состояния в глобальную область видимости с объявлениями, давая в результате код, который работает в Python 2.Х и З.Х:
calls = О
def tracer(func): # Состояние через объемлющую область
# видимости и глобальную переменную def wrapper(*args, **kwargs): # Вместо атрибутов класса
global calls # calls - глобальная переменная, не для каждой функции calls += 1
print ('call %s to %sf % (calls, func._name_))
return func(*args, **kwargs) return wrapper
@tracer
def spam (a, b, с) : # To же, что и spam = tracer (spam)
print (a + b + c)
0tracer
def eggs(x, y) : # Го же, что и eggs = tracer (eggs)
print (x ** y)
spam(l, 2, 3) # На самом деле вызывается wrapper, присвоенный spam
spam(a=4, b=5, c=6) # wrapper вызывает spam
eggs {2, 16) # На самом деле вызывается wrapper, присвоенный eggs
eggs(4, y=4) # Глобальная переменная calls не является
# отдельной для каждого случая декорирования!
К сожалению, перемещение счетчика в общую глобальную область видимости, чтобы сделать возможным его изменение, также означает, что он будет разделяться всеми внутренними функциями. В отличие от атрибутов экземпляров классов глобальные счетчики относятся ко всей программе, а не к каждой функции — счетчик инкрементируется для вызова любой отслеживаемой функции. Вы сможете заметить разницу, если сравните вывод этой версии с выводом предыдущей версии — единственный разделяемый глобальный счетчик вызовов некорректно обновляется обращениями к каждой декорированной функции:
c:\code> python decorator3.ру
call 1 to spam 6
call 2 to spam 15
call 3 to eggs 65536
call 4 to eggs 256
Объемлющие области видимости и нелокальные переменные
В ряде случаев разделяемое глобальное состояние может быть именно тем, что нужно. Тем не менее, если на самом деле вы хотите иметь счетчик для каждой функции, тогда можете применять либо классы, как ранее, либо функции замыканий (они же фабричные функции) и оператор nonlocal в Python З.Х, описанный в главе 17 первого тома. Поскольку оператор nonlocal разрешает изменять переменные из областей видимости объемлющих функций, они могут выступать в качестве изменяемых данных для каждого случая декорирования, но только в Python З.Х:
def tracer(func): # Состояние через объемлющую область
# видимости и нелокальную переменную calls = 0 # Вместо атрибутов класса или глобальных переменных def wrapper(*args, **kwargs): # calls - для каждой функции,
# не глобальная переменная
nonlocal calls calls += 1
print ('call fcs to Is' % (calls, func._name_))
return func(*args, **kwargs) return wrapper
@tracer
def spam (a, b, с) : # To же, что и spam = tracer (spam) print (a + b + c)
Qtracer
def eggs (x, у) : # To же, что и eggs = tracer (eggs) print(x ** y)
spam(l, 2, 3) # На самом деле вызывается wrapper, привязанный к spam
spam(a=4, b=5, c=6) # wrapper вызывает spam
eggs (2, 16) # На самом деле вызывается wrapper, привязанный к eggs
eggs (4, y=4) # Нелокальная переменная _является_ отдельной
# для каждого случая декорирования!
Теперь из-за того, что переменные из объемлющей области видимости не являются глобальными переменными, относящимися ко всей программе, каждая внутренняя функция снова получает собственный счетчик, как было при использовании классов и атрибутов. Вот новый вывод, полученный в результате запуска под управлением Python З.Х:
c:\code> ру -3 decorator4.ру
call 1 to spam б
call 2 to spam 15
call 1 to eggs 65536
call 2 to eggs 256
Атрибуты функций
Наконец, даже если вы не работаете с Python З.Х и не располагаете доступом к оператору nonlocal или хотите обеспечить переносимость своего кода между линейками Python З.Х и Python 2.Х, то все равно имеете возможность избежать использования глобальных переменных и классов, взамен задействовав для определенного изменяемого состояния атрибуты функций. Во всех версиях, начиная с Python 2.1, мы можем присоединять к функциям произвольные атрибуты за счет присваивания им значений с помощью выражения вида функция, атрибут = значение. Поскольку фабричная функция при любом вызове создает новую функцию, ее атрибуты становятся состоянием для каждого вызова. Кроме того, вам нужно применять такую методику только для переменных состояния, которые должны изменяться; ссылки из объемлющих областей видимости по-прежнему предохраняются и нормально работают.
В нашем примере мы можем просто использовать для состояния wrapper. calls. Следующая версия работает аналогично предыдущей версии с nonlocal, т.к. счетчик снова относится к каждой декорированной функции, но она также выполняется в Python 2.Х:
def tracer(func): # Состояние через объемлющую область
# видимости и атрибуты функций def wrapper(*args, **kwargs): # calls - для каждой функции,
# не глобальная переменная
wrapper.calls += 1
print(’call %s to %s’ % (wrapper.calls, func._name_))
return func(*args, **kwargs) wrapper.calls = 0 return wrapper
@tracer
def spam (a, b, с) : # To же, что и spam = tracer (spam)
print (a + b + c)
@tracer
def eggs(x, у) : # To же, что и eggs = tracer (eggs) print(x ** y)
spam(l, 2, 3) # На самом деле вызывается wrapper, присвоенный spam
spam(a=4/ b=5, c=6) # wrapper вызывает spam
eggs {2, 16) # На самом деле вызывается wrapper, присвоенный eggs
eggs(4, y=4) # Значение wrapper.calls _относится_
# к каждому случаю декорирования
Как объяснялось в главе 17 первого тома, код работает лишь потому, что имя wrapper сохранено в области видимости объемлющей функции tracer. Когда позже мы инкрементируем wrapper. calls, то не изменяем само имя wrapper, поэтому объявление nonlocal не требуется. Данная версия выполняется в обеих линейках Python:
c:\code> ру -2 decorator5.ру
. . . такой же вывод, как у предыдущей версии, но работает и в Python 2.Х. . .
Представленная выше схема чуть не была переведена в категорию сноски, т.к. она может оказаться даже менее ясной, чем версия с nonlocal в Python З.Х, и поэтому ее лучше приберечь для ситуаций, где другие схемы ничем не помогут. Однако атрибуты функций также обладают существенными преимуществами. С одной стороны, они делают возможным доступ к сохраненному состоянию снаружи кода декоратора; нелокальные переменные могут быть видны только внутри самой вложенной функции, но атрибуты функций имеют более широкую видимость. С другой стороны, они гораздо более переносимы.\ схема также работает в Python 2.Х, что делает ее нейтральной к версиям.
Атрибуты функций будут снова задействованы при ответе на один из контрольных вопросов главы, где их видимость снаружи вызываемых объектов становится преимуществом. Поскольку изменяемое состояние связано с контекстом применения, атрибуты функций эквивалентны нелокальным переменным из объемлющих областей видимости. Как обычно, выбор среди множества инструментов является неотъемлемой частью задачи программирования.
Из-за того, что декораторы часто подразумевают наличие множества уровней вызываемых объектов, вы можете комбинировать функции с объемлющими областями видимости, классы с атрибутами и атрибуты функций, чтобы добиться многосторонности кодовых структур. Тем не менее, позже вы увидите, что иногда задача оказывается более тонкой, чем ожидалось — каждая декорированная функция должна иметь собственное состояние, а каждый декорированный класс может требовать состояния и для себя, и для каждого создаваемого экземпляра.
В следующем разделе подробно объясняется, что если мы хотим применять декораторы функций также к методам уровня класса, тогда обязаны позаботиться о разграничении, которое Python делает между декораторами, реализованными как вызываемые объекты экземпляров классов, и декораторами, записанными в виде функций.
Грубые ошибки, связанные с классами, часть I: декорирование методов
Когда я занимался реализацией первого основанного на классах декоратора функций tracer в файле decoratorl .ру, то наивно полагал, что его также удастся применять к любому методу. Я рассуждал, что декорированные методы должны работать аналогично, а добавляемый автоматически аргумент экземпляра self просто будет включен в начало *args. Единственный реальный недостаток такого предположения заключается в том, что оно глубоко ошибочно! В случае применения к методу класса первая версия tracer терпит неудачу, т.к. self является экземпляром класса декоратора, а экземпляр декорированного целевого класса вообще не помещается в *args. Сказанное справедливо и для Python З.Х, и для Python 2.Х.
Я представил данное явление ранее в главе, но теперь мы можем взглянуть на него в контексте реалистичного рабочего кода. Для следующего декоратора отслеживания на основе класса:
class tracer:
def _init_(self, func) : # При декорировании @
self, calls = 0 # Сохранение функции для вызова в будущем
self. func = func
def _call_(self, *args, **kwargs): # При обращении к исходной функции
self.calls += 1
print('call %s to %s’ % (self.calls, self.func._name_))
return self.func(*args, **kwargs)
декорирование простых функций работает, как было объявлено ранее:
@tracer
def spam (а, b, с) : # spam = tracer (spam)
print (a + b + с) # Запускается tracer._init_
>>> spam(l, 2,3) # Выполняется tracer._call_
call 1 to spam
6
>>> spam(a=4, b=5, c=6) # spam хранится в атрибуте экземпляра
call 2 to spam
15
Однако декорирование методов уровня класса потерпит неудачу (более рассудительные последовательные читатели могут признать приведенный далее код как адаптацию нашего класса Person, который взят из учебного руководства по ООП, предложенного в главе 28):
class Person:
def _init_(self, name, pay) :
self, name = name self .pay = pay
@tracer
def giveRaise(self, percent): # giveRaise = tracer (giveRaise)
self.pay *= (1.0 + percent)
@tracer
def lastName(self): # lastName = tracer(lastName)
return self.name.split()[-1]
>>> bob = Person ('Bob Smith' , 50000) # tracer запоминает функции методов
»> bob.giveRaise(.25) # Запускается tracer._call_(???, .25)
call 1 to giveRaise
TypeError: giveRaise () missing 1 required positional argument: 'percent'
Ошибка типа: в giveRaise() отсутствует 1 обязательный позиционный аргумент: percent
»> print (bob. lastName ()) # Запускается tracer._call_(???)
call 1 to lastName
TypeError: lastName() missing 1 required positional argument: 'self'
Ошибка типа: в lastName () отсутствует 1 обязательный позиционный аргумент: self
Корень проблемы здесь кроется в аргументе self метода__call__класса
tracer — он является экземпляром tracer или же экземпляром Person? На самом деле нам необходимы в коде оба экземпляра: экземпляр tracer для состояния декоратора и экземпляр Person для перенаправления на исходный метод. В действительности self обязан быть объектом tracer, чтобы предоставить доступ к информации состояния tracer (его атрибутам calls и func); это справедливо для декорирования как простой функции, так и метода.
К сожалению, когда имя декорированного метода повторно привязывается к объекту экземпляра класса с помощью_call_, интерпретатор Python передает в self
только экземпляр tracer; он вообще не передает экземпляр Person в списке аргументов. Кроме того, поскольку экземпляру tracer ничего не известно об экземпляре Person, который мы пытаемся обработать посредством вызовов методов, создать связанный метод с экземпляром не удастся, следовательно, нет способа для корректного координирования вызова. Это не ошибка, но очень тонкая особенность.
В итоге предыдущий код передает декорированному методу слишком мало аргументов, что приводит к ошибке. Для проверки добавьте в метод_call_декоратора вывод всех его аргументов — вы увидите, что self представляет собой экземпляр tracer, а экземпляр Person отсутствует:
>>> bob.giveRaise (. 25)
<_main_.tracer object at 0x02A486D8> (0.25,) {}
call 1 to giveRaise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in _call_
TypeError: giveRaiseO missing 1 required positional argument: 'percent' Трассировка (самый последний вызов указан последним) :
Файл <stdin>, строка 1, в <модуль>
Файл <stdin>/ строка 9, в_call_
Ошибка типа: в giveRaise () отсутствует 1 обязательный позиционный аргумент: percent
Как упоминалось ранее, так происходит из-за того, что интерпретатор Python передает подразумеваемый экземпляр аргументу self, только когда имя метода привязано к простой функции; если оно является экземпляром вызываемого класса, тогда взамен передается экземпляр этого класса. Формально интерпретатор Python создает объект связанного метода, содержащий экземпляр целевого класса, только когда метод представляет собой простую функцию, а не вызываемый экземпляр еще одного класса.
Использование вложенных функций для декорирования методов
Если вы хотите, чтобы декораторы функций работали с простыми функциями и с методами уровня классов, то самое прямолинейное решение предусматривает применение одной из других методик предохранения состояния, которые были описаны ранее. Речь идет о реализации декоратора функций в виде вложенного оператора def, чтобы не полагаться на единственный аргумент экземпляра self, который будет как экземпляром класса оболочки, так и экземпляром целевого класса.
В показанной ниже альтернативной версии затруднение решается с использованием нелокальной переменной Python З.Х; чтобы код работал в Python 2.Х, его необходимо переписать, применяя атрибуты функций для изменяемого состояния calls. Поскольку декорированные методы повторно привязываются к простым функциям, а не объектам экземпляром, интерпретатор Python корректно передает объект Person в первом аргументе, а декоратор передает его из первого элемента *args аргументу self действительного декорированного метода:
# Декоратор отслеживания вызовов для функций и методов
def tracer (func) : # Использовать с_call_ функцию, но не класс
calls = 0 # Иначе self - только экземпляр декоратора!
def onCall (*args, **kwargs) : # Или применить [onCall. calls += 1]
# для Python 2.X+3.X
nonlocal calls calls += 1
print ('call %s to %s' % (calls, func._name_))
return func(*args, **kwargs) return onCall
if _name_ == '_main_' :
# Применяется к простым функциям @tracer
def spam (a, b, c) : # spam = tracer (spam)
print (a + b + c) # onCall запоминает spam
@tracer def eggs(N):
return 2 ** N
spam(l, 2, 3) # Запускается onCall (1 , 2, 3)
spam(a=4, b=5, c=6) print(eggs(32))
# Применяется также к функциям методов уровня класса! class Person:
def _init_(self, name, pay) :
self, name = name self.pay = pay @tracer
def giveRaise(self, percent): self.pay *= (1.0 + percent)
# giveRaise = tracer(giveRaise)
# onCall запоминает giveRaise
# lastName = tracer (lastName)
# методы. . .
# Запускается onCall (sue, .10)
@tracer
def lastName(self):
return self.name.split()[-1] print('methods...') bob = Person('Bob Smith', 50000) sue = Person('Sue Jones', 100000) print(bob.name, sue.name) sue.giveRaise(.10)
print(int(sue.pay))
print(bob.lastName(), sue.lastName()) # Запускается onCall (bob),
# lastName в области видимости
Мы также поместили код самотестирования файла внутрь оператора проверки
_name_, так что декоратор можно импортировать и использовать где-то в другом
месте. Эта версия работает одинаково с функциями и методами, но выполняется только под управлением Python З.Х из-за применения nonlocal:
c:\code> ру -3 calltracer.ру
call 1 to spam б
call 2 to spam 15
call 1 to eggs 4294967296 methods...
Bob Smith Sue Jones call 1 to giveRaise 110000
call 1 to lastName call 2 to lastName Smith Jones
Отследите приведенные результаты, чтобы лучше понимать модель; в следующем разделе предлагается альтернативная версия, которая поддерживает классы, но также характеризуется гораздо большей сложностью.
Использование дескрипторов для декорирования методов
Несмотря на то что решение с вложенными функциями, продемонстрированное в предыдущем разделе, является наиболее прямолинейным способом поддержки декораторов, которые применяются к функциям и методам уровня класса, возможны другие схемы. Скажем, здесь также может помочь средство дескрипторов, исследованное в предыдущей главе.
Вспомните из нашего обсуждения в той главе, что дескриптор обычно представляет собой атрибут класса, которому присваивается объект с методом_get_, автоматически запускаемым всякий раз, когда производится ссылка и извлечение атрибута. Наследование от object, принятое в классах нового стиля, требуется для дескрипторов в Python 2.Х, но не в Python З.Х:
class Descriptor(object):
def_get_(self, instance, owner): ...
class Subject:
attr = Descriptor ()
X = Subject ()
X.attr # Грубо говоря, запускает Descriptor._get_(Subject.attr, X, Subject)
Дескрипторы также могут иметь методы доступа _set_и_del_, но здесь они
не нужны. В настоящей главе более важен метод_get_дескриптора, поскольку во
время вызова он принимает экземпляры класса дескриптора и целевого класса, что хорошо подходит для декорирования методов, когда при координировании вызовов нам необходимо как состояние декоратора, так и экземпляр исходного класса. Рассмотрим следующую альтернативную версию декоратора отслеживания, который также оказывается дескриптором в случае использования для метода уровня класса:
class tracer(object): # Декоратор + дескриптор
def _init_(self, func) : # При декорировании @
self .calls = 0 # Сохранение функции для вызова в будущем
self, func = func
def_call_(self, *args, **kwargs) : # При обращении к исходной функции
self.calls += 1
print('call %s to %sf % (self.calls, self.func._name_))
return self.func(*args, **kwargs)
def _get_(self, instance, owner) : # При извлечении атрибутов методов
return wrapper(self, instance)
class wrapper:
def _init_(self, desc, subj): # Сохранение обоих экземпляров
self.desc = desc # Направление вызовов декоратору/дескриптору
self.subj = subj
def _call_(self, *args, **kwargs):
return self.desc(self.subj, *args, **kwargs) # Запускается tracer._call_
@tracer
def spam (a, b, c) : # spam = tracer (spam)
. . .то же код, что и раньше. . . # Использует только_call_
class Person:
0tracer
def giveRaise(self, percent): # giveRaise = tracer(giveRaise)
. . .то же код, что и раньше. . . # Делает giveRaise дескриптором
Текущая версия декоратора tracer работает аналогично предшествующей версии с вложенными функциями, но действие варьируется в зависимости от контекста применения.
• Декорированные функции вызывают только его метод_call_и никогда его
метод_get_.
• Декорированные методы вызывают сначала его метод_get_, чтобы выполнить
извлечение имени метода (для I .метод); возвращенный методом_get_ объект хранит экземпляр целевого класса и затем вызывается с целью завершения выражения вызова, тем самым запуская метод_call_декоратора (для ()).
Например, вызов в тестовом коде:
sue. giveRaise (. 10) # Запускается_get_, затем_call_
выполняет сначала tracer._get_, потому что атрибут giveRaise в классе Person
был повторно привязан к дескриптору декоратором функций методов. Затем выражение вызова запускает метод_call_возвращенного объекта wrapper, который
в свою очередь вызывает tracer._call_. Другими словами, вызовы декорированных методов инициируют процесс из четырех шагов: tracer._get_, за которым
следуют три операции вызова — wrapper._call_, tracer._call_и в заключение исходного декорированного метода.
Объект wrapper хранит экземпляры дескриптора и целевого класса, так что он может направить управление исходному экземпляру класса декоратора/дескриптора. В действительности объект wrapper сохраняет экземпляр целевого класса, доступный во время извлечения атрибута метода, и добавляет его в список аргументов более позднего вызова, который передается методу_call_декоратора. В этом приложении
требуется направление вызова обратно экземпляру класса дескриптора, чтобы все обращения к внутреннему методу использовали ту же самую информацию состояния счетчика calls в объекте экземпляра дескриптора.
В качестве альтернативы для обеспечения того же эффекта мы могли бы применить вложенную функцию или ссылки из объемлющих областей видимости — следующая версия работает так же, как предыдущая, за счет обмена местами атрибутов класса и объекта для вложенной функции и ссылок. Она требует значительно меньше кода, но при каждом вызове декорированного метода следует такому же процессу из четырех шагов:
class tracer(object) :
def _init_(self, func) : # При декорировании @
self .calls = 0 # Сохранение функции для вызова в будущем
self, func = func
def _call_(self, *args, **kwargs) : # При обращении к исходной функции
self.calls += 1
print('call %s to %s' % (self.calls, self.func._name_))
return self.func(*args, **kwargs)
def _get_(self, instance, owner) : # При извлечении метода
def wrapper(*args, **kwargs): # Сохранение обоих экземпляров
return self(instance, *args, **kwargs) # Запускается_call_
return wrapper
Добавьте операторы print к методам альтернативных версий, чтобы самостоятельно отследить многошаговый процесс извлечения/вызова, и запустите их с тем же тестовым кодом, как ранее делалось для версии с вложенными функциями (исходный код находится в файле calltracer-descr .ру). В любой из двух версий основанная на дескрипторах схема также гораздо хитроумнее, чем вариант с вложенными функциями, и вероятно потому рассматривается здесь во вторую очередь. Говоря более прямо, если сложность данной версии не заставляет вас мучиться от кошмаров по ночам, то издержки в плане производительности наверняка должны! Тем не менее, в других контекстах такая кодовая схема может оказаться полезной.
Стоит также отметить, что мы можем реализовать декоратор на основе дескриптора проще, как показано ниже, но тогда он был бы применим только к методам, а не к простым функциям. Это внутренне присущее ограничение дескрипторов атрибутов (и всего лишь противоположность задачи, которую мы пытаемся решить: приложение к функциям и методам):
class tracer (object) : # Для методов, но не для функций!
def _init_(self, meth) : # При декорировании @
self.calls = 0 self .meth = meth
def _get_(self, instance, owner) : # При извлечении метода
def wrapper (*args, **kwargs) : # При вызове метода: посредник
# с self и экземпляром
self.calls += 1
print ('call %s to %s' % (self.calls, self.meth._name_))
return self.meth(instance, *args, **kwargs) return wrapper
class Person:
0tracer # Применяется к методам класса
def giveRaise(self, percent): # giveRaise = tracer(giveRaise)
... # Делает giveRaise дескриптором
@tracer # Но терпит неудачу с простыми функциями
def spam (а, b, с) : # spam = tracer (spam)
... # Здесь никаких извлечений атрибутов не происходит
В остатке данной главы мы обираемся довольно бессистемно относиться к использованию классов или функций при реализации декораторов функций, пока они применяются только к функциям. Некоторые декораторы могут не требовать экземпляра исходного класса, и по-прежнему будут работать с функциями и методами, если они реализованы в виде классов. Что-нибудь наподобие собственного декоратора staticmethod в Python, например, не требует экземпляра целевого класса (более того, весь его смысл связан с тем, чтобы устранить экземпляр из вызова).
Однако мораль этой истории в том, что если вы хотите, чтобы декораторы работали с простыми функциями и методами, то вероятно лучше задействовать обрисованную здесь кодовую схему на основе вложенных функций, а не класс с перехватом вызовов.
Измерение времени вызовов
Для получения более полного представления о том, на что способны декораторы функций, давайте возьмем другой сценарий использования. Наш следующий декоратор измеряет время вызовов декорированной функции — время одного вызова и суммарное время всех вызовов. Декоратор применяется к двум функциям с целью сравнения относительной скорости, с которой выполняются списковые включения и встроенный вызов тар:
# Файл timerdecol.ру
# Предостережение: область все же отличается - список в Python 2.Х,
# итерируемый объект в Python З.Х
# Предостережение: в таком виде timer не будет работать с методами
# (см. ответ на контрольный вопрос) import time, sys
force = list if sys. version_infо [0] == 3 else (lambda X: X)
class timer:
def_init_(self, func):
self.func = func self.alltime = 0
def _call_(self, *args, **kargs):
start = time.clock() result = self.func(*args, **kargs) elapsed = time.clock () - start self.alltime += elapsed
print('%s: %.5f, %.5f* % (self.func._name_, elapsed, self.alltime))
return result
@timer
def listcomp(N):
return [x * 2 for x in range (N) ]
@timer
def mapcall(N):
return force(map((lambda x: x * 2), range(N)))
result = listcomp (5) # Время для этого вызова, всех вызовов
# и возвращаемое значение
listcomp(50000) listcomp(500000) listcomp(1000000) print(result)
print ('allTime = %s' % listcomp. alltime) # Суммарное время для всех
# вызовов listcomp
print ('')
result = mapcall(5) mapcall(50000) mapcall(500000) mapcall(1000000) print(result)
print ('allTime = %s* % mapcall.alltime) # Суммарное время для всех
# вызовов mapcall
print('\n**map/comp = %s' % round(mapcall.alltime / listcomp.alltime, 3))
При запуске в Python З.Х или 2.Х вывод кода самотестирования данного файла выглядит так, как показано ниже. Для каждого вызова он содержит имя функции, время выполнения этого вызова и время выполнения всех вызовов, совершенных до сих пор. Кроме того, указывается возвращаемое значение первого вызова, совокупное время выполнения каждой функции и в конце соотношение времени выполнения списковых включений и встроенного вызова шар:
c:\code> ру -3 timerdecol.ру
listcomp: 0.00001, 0.00001 listcomp: 0.00499, 0.00499 listcomp: 0.05716, 0.06215 listcomp: 0.11565, 0.17781 [0, 2, 4, 6, 8]
allTime = 0.17780527629411225
mapcall: 0.00002, 0.00002 mapcall: 0.00988, 0.00990 mapcall: 0.10601, 0.11591 mapcall: 0.21690, 0.33281 [0, 2, 4, 6, 8]
allTime = 0.3328064956447921 **map/comp = 1.872
Конечно, показатели времени варьируются в зависимости от линейки Python и компьютера, используемого для тестирования, а совокупное время доступно в виде атрибута экземпляра класса. Как обычно, вызовы тар почти в два раза медленнее списковых включений, когда последним удается избежать вызова функции (или, что эквивалентно, потребность в вызовах функций может сделать шар медленнее).
Декораторы или измерение времени для каждого вызова
Для сравнения возьмем представленный в главе 21 первого тома подход без декораторов к измерению времени выполнения итерационных альтернатив. Там мы оценивали две методики измерения времени каждого вызова собственной и библиотечной реализаций. Здесь мы вводим в действие измерение времени для случая тестового кода декоратора с 1 миллионом списковых включений, хотя и с дополнительными затратами со стороны управляющего кода, включая внешний цикл и вызовы функций:
>>> def listcomp (N) : [х * 2 for х in range (N) ]
>>> import timer # Методики из главы 21 первого тома
»> timer.total(1, listcomp, 1000000)
(0.1461295268088542, None)
»> import timeit
>» timeit.timeit(number=l, stmt=lambda: listcomp(1000000))
0.14964829430189397
В этом конкретном случае подход, в котором декораторы не применяются, позволил бы использовать целевые функции с измерением времени или без него, но также бы усложнил сигнатуру вызовов, когда измерение времени желательно — нам пришлось бы добавлять код к каждому вызову, а не один раз к def. Более того, в схеме без декораторов отсутствовал бы прямой способ гарантировать, что все вызовы построителя списка в программе прогоняются через логику таймера, кроме поиска и возможного изменения их всех. В итоге могли бы возникнуть затруднения со сбором накопленных данных для всех вызовов.
В целом декораторы могут оказаться предпочтительным вариантом, когда функции уже введены в действие как часть более крупной системы, а потому их вызовы нелегко передавать функциям анализа. С другой стороны, поскольку декораторы нагружают каждый вызов дополняющей логикой, подход без декораторов может быть эффективнее, если вы хотите дополнять вызовы более избирательно. Как обычно, разные инструменты исполняют разные роли.
Переносимость вызовов таймера и новые варианты, которые появились в версии Python 3.3. Просмотрите еще раз более полную обработку и выбор функций модуля time в главе 21 первого тома плюс врезку “Новые вызовы таймеров, появившиеся в версии Python 3.3” в той же главе, где обсуждаются новые и усовершенствованные функции таймера в данном модуле, которые стали доступными, начиная с версии Python 3.3 (скажем, perf counter). Здесь мы принимаем упрощенный подход ради краткости и нейтральности к версиям, но time . clock может быть не наилучшим выбором на ряде платформ даже до выхода Python 3.3, и вне среды Windows могут требоваться проверки на предмет платформы и версии.
Тонкости тестирования
Обратите внимание на то, что в сценарии timerdecol .ру применяется force для обеспечения его переносимости между линейками Python 2.Х и Python З.Х. Как было описано в главе 14 первого тома, встроенная функция тар возвращает итерируемый объект, который генерирует результаты по запросу в Python З.Х, но действительный список в Python 2.Х. Следовательно, сама по себе функция тар из Python З.Х не сравнивается напрямую с работой списковых включений. Фактически, если вызов тар не помещен внутрь вызова list для принудительного выпуска результатов, то тест тар практически вообще не отнимает какое-то время в Python З.Х — функция тар возвращает итерируемый объект без итерирования!
В то же время добавление вызова list также и в Python 2.Х налагает на тар несправедливый штраф — результаты теста тар будут включать время, требуемое для построения двух списков, а не одного. Чтобы обойти проблему, сценарий выбирает объемлющую функцию тар согласно номеру версии Python в sys: для Python З.Х выбирается list, а для Python 2.Х используется пустая функция, которая просто возвращает свой входной аргумент без изменений. Это добавляет весьма несущественное постоянное время в Python 2.Х, которое вероятно окажется полностью перекрытым затратами на итерации внутреннего цикла в хронометрируемой функции.
Наряду с тем, что такой прием делает сравнение списковых включений и встроенной функции тар более справедливым в любой линейке Python 2.Х или Python
З.Х, поскольку range также является итератором в Python З.Х, результаты для Python
2.Х и З.Х не могут сравниваться напрямую, если только не изъять данный вызов из хронометрируемого кода. Они относительно сопоставимы — и в любом случае будут отражать рекомендуемый код в каждой линейке, — но итерация range в Python З.Х добавляет дополнительное время. Сведения подобного рода были представлены при воссоздании эталонных тестов в главе 21 первого тома; производство сопоставимых чисел часто оказывается нетривиальной задачей.
Наконец, как мы ранее поступали для декоратора отслеживания, декоратор измерения времени можно было бы сделать многократно используемым в других модулях,
поместив код самотестирования в конец файла внутрь проверки_name_, чтобы он
выполнялся только при запуске файла, но не при его импортировании. Тем не менее, этим здесь мы заниматься не будем, т.к. собираемся добавить в код еще одну возможность.
Добавление аргументов к декоратору
Декоратор timer из предыдущего раздела работоспособен, но было бы неплохо, если бы он стал более конфигурируемым — например, в универсальном инструменте подобного рода может быть полезной возможность предоставления выходной метки и включения/отключения трассировочных сообщений. Здесь пригодятся аргументы декоратора: при надлежащей реализации мы можем применять их для указания конфигурационных параметров, которые допускают варьирование в зависимости от декорированной функции. Скажем, вот как можно было бы добавлять метку:
def timer(label=' ') : def decorator(func):
def onCall(*args): # Многоуровневое предохранение состояния:
... # аргументы передаются функции
func(*args) # func хранится в объемлющей области видимости
print (label, ... # label хранится в объемлющей области видимости return onCall
return decorator # Возвращается действительный декоратор
@timer('==>') # Подобно listcomp = timer ('==>’) (listcomp)
def listcomp(N): . . . # listcomp повторно привязывается
# к новой функции onCall
listcomp(...) # В действительности вызывается onCall
В коде добавляется объемлющая область видимости, чтобы предохранить аргумент декоратора для использования в будущем вызове. Когда функция listcomp определена, интерпретатор Python на самом деле вызывает decorator — результат выполнения timer перед тем, как фактически происходит декорирование, — со значением label, доступным в объемлющей области видимости декоратора. То есть timer возвращает декоратор, запоминающий аргумент декоратора и исходную функцию, а также возвращающий вызываемый объект onCall, который в конечном итоге обращается к исходной функции при вызовах в более позднее время. Поскольку такая структура создает новые функции decorator и onCall, их объемлющие области видимости сохраняют состояние для каждого случая декорирования.
Мы можем задействовать данную структуру в нашем таймере, чтобы сделать возможной передачу метки и флага управления трассировочными сообщениями на стадии декорирования. Ниже приведен пример такой реализации, помещенный в файл модуля по имени timerdeco2 .ру, который можно импортировать как универсальный инструмент; для организации второго уровня предохранения состояния вместо вложенной функции в нем применяется класс, но совокупный результат аналогичен:
import time
def timer (label=' ', trace=True) : # При аргументах декоратора:
# сохранение аргументов
class Timer:
def _init_(self, func) : # При декорировании сохранение
# декорированной функции
self.func = func self.alltime = 0
def _call_(self, *args, **kargs) : # При вызовах: вызов исходной функции
start = time.clock()
result = self.func(*args, **kargs)
elapsed = time.clock() - start
self.alltime += elapsed
if trace:
format = '%s %s: %.5f, %.5f'
values = (label, self.func._name_, elapsed, self.alltime)
print(format % values) return result return Timer
Почти все, что мы здесь сделали, связано с помещением исходного класса Timer внутрь объемлющей функции с целью создания области видимости, которая предохраняет аргументы декоратора для каждого ввода в действие. Внешняя функция timer вызывается перед тем, как происходит декорирование, и она просто возвращает класс Timer, предназначенный для того, чтобы служить фактическим декоратором. При декорировании создается экземпляр Timer, который запоминает саму декорированную функцию, но также имеет доступ к аргументам декоратора в области видимости объемлющей функции.
Измерение времени с аргументами декоратора
На этот раз вместо помещения кода самотестирования в файл timerdeco2 .ру мы будем запускать декоратор в другом файле. Вот клиент нашего декоратора таймера, находящийся в файле модуля testseqs .ру, который снова применяет его к итерационным альтернативам:
import sys
from timerdeco2 import timer
force = list if sys. version_infо [0] == 3 else (lambda X: X)
@timer(label='[CCC]==>1)
def listcomp(N): # Подобно listcomp = timer (...) (listcomp) return [x * 2 for x in range(N)] # listcomp(...) triggers Timer._call_
Gtimer(trace=True, label='[МММ]==>1) def mapcall(N):
return force(map((lambda x: x * 2), range(N)))
for func in (listcomp, mapcall) :
result = func(5) # Время для этого вызова, всех вызовов
# и возвращаемое значение
func(50000) func(500000) func(1000000) print(result)
print (' allTime = %s\n' % func.alltime) # Суммарное время для всех вызовов print('**map/comp = %s' % round(mapcall.alltime / listcomp.alltime, 3))
Опять-таки, чтобы сделать сравнение справедливым, в случае Python З.Х обращение к тар помещается внутрь вызова list. При запуске в Python З.Х или 2.Х файл выводит следующие результаты — каждая декорированная функция теперь имеет собственную метку, определяемую с помощью аргументов декоратора, которая будет более полезной, когда нам необходимо отыскать трассировочные сообщения в выводе крупной программы:
c:\code> ру -3 testseqs.py
[ССС]==> listcomp: 0.00001, 0.00001 [ССС]==> listcomp: 0.00504, 0.00505 [ССС]==> listcomp: 0.05839, 0.06344 [ССС]==> listcomp: 0.12001, 0.18344 [0, 2, 4, 6, 8]
allTime = 0.1834406801777564
[МММ]==> mapcall: 0.00003, 0.00003 [МММ]==> mapcall: 0.00961, 0.00964 [МММ]==> mapcall: 0.10929, 0.11892 [МММ]==> mapcall: 0.22143, 0.34035 [0, 2, 4, 6, 8]
allTime = 0.3403542519173618 **map/comp = 1. 855
Как обычно, мы также можем тестировать в интерактивном сеансе, чтобы поемов реть на конфигурационные аргументы декоратора в работе:
>>> from timerdeco2 import timer
>>> @timer(trace=False) # Без трассировочных сообщений,
# накопление суммарного времени
. . . def listcomp(N):
. . . return [x * 2 for x in range (N) ]
»> x = listcomp(5000)
>>> x = listcomp(5000)
>>> x = listcomp(5000)
>>> listcomp.alltime
0.0037191417530599152 >>> listcomp
<timerdeco2.timer.<locals>.Timer object at 0x02957518>
>>> @timer(trace=True, label=l\t=>') # Включение трассировочных сообщений,
# специальная метка
. . . def listcomp(N):
. . . return [x * 2 for x in range (N) ]
>>> x = listcomp(5000)
=> listcomp: 0.00106, 0.00106 >>> x = listcomp(5000)
=> listcomp: 0.00108, 0.00214 >>> x = listcomp(5000)
=> listcomp: 0.00107, 0.00321 »> listcomp. all time 0.003208920466562404
В текущем виде декоратор функций для измерения времени может использоваться с любой функцией, как в модулях, так и в интерактивном сеансе. Другими словами, он автоматически квалифицируется как универсальный инструмент для измерения времени выполнения кода в наших сценариях. Дополнительные примеры применения аргументов декораторов ищите в разделах “Реализация закрытых атрибутов” и “Базовый декоратор проверки вхождения значений в диапазон для позиционных аргументов” далее в главе.
Поддержка методов. Рассматриваемый выше декоратор таймера работает с любой функцией, но чтобы его можно было применять также к методам уровня класса, требуется лишь небольшая переделка. Как иллюстрировалось в разделе “Грубые ошибки, связанные с классами, часть I: декорирование методов” ранее в главе, необходимо избегать использования вложенного класса. Однако поскольку такое изменение умышленно оставлено в качестве одного из контрольных вопросов главы, здесь я воздержусь давать полный ответ.
Is
Реализация декораторов классов
До сих пор мы занимались реализацией декораторов функций для управления обращениями к функциям, но, как было показано, в версиях Python 2.6 и 3.0 декораторы были расширены, чтобы работать также с классами. Наряду с тем, что декораторы классов концептуально похожи на декораторы функций, взамен они применяются к классам — либо для управления самим классами, либо для перехвата вызовов, создающих экземпляры, с целью управления экземплярами. Также подобно декораторам функций декораторы классов в действительности являются лишь необязательным синтаксическим сахаром, хотя многие считают, что они делают намерение программиста более очевидным и сводят к минимуму ошибочные вызовы или случайный их пропуск.
Классы-одиночки
Из-за того, что декораторы классов способны перехватывать вызовы, создающие экземпляры, они могут использоваться либо для управления всеми экземплярами класса, либо для дополнения интерфейсов этих экземпляров. В целях демонстрации далее приведен первый пример декоратора классов, который решает задачу управления всеми экземплярами класса. В коде реализован паттерн проектирования “Одиночка”, при котором в любой момент времени существует самое большее один экземпляр класса. Функция singleton определяет и возвращает функцию для управления экземплярами, а посредством синтаксиса @ целевой класс помещается в оболочку данной функции:
# Python З.Х и 2.Х: глобальная таблица
instances = {}
def singleton(aClass) : # При декорировании 0
def onCall(*args, **kwargs): # При создании экземпляров
if aClass not in instances: # Один элемент словаря на класс
instances[aClass] = aClass(*args, **kwargs) return instances[aClass] return onCall
Чтобы обеспечить применение модели с единственным экземпляром понадобится декорировать желаемые классы (весь рассматриваемый в разделе код находится в файле singletons .ру):
0singleton # Person = singleton(Person)
class Person: # Повторная привязка Person к onCall
def_init_(self, name, hours, rate) : # onCall запоминает Person
self, name = name self.hours = hours self.rate = rate def pay(self):
return self.hours * self.rate
@singleton # Spam = singleton (Spam)
class Spam: # Повторная привязка Spam к onCall
def _init_(self, val) : # onCall запоминает Spam
self.attr = val
bob = Person('Bob', 40, 10) # В действительности вызывается onCall
print (bob.name, bob.payO)
sue = Person ('Sue ', 50, 20) # To же самое, единственный объект
print (sue.name, sue.payO)
X = Spam(val=42) # Один экземпляр Person,
# один экземпляр Spam
Y = Spam(99)
print(X.attr, Y.attr)
Теперь, когда класс Person или Spam используется в будущем с целью создания экземпляра, предоставленный декоратором уровень логики оболочки направляет вызовы, создающие экземпляры, функции onCall, которая гарантирует наличие единственного экземпляра для каждого класса независимо от того, сколько таких вызовов было сделано. Вот вывод кода (Python 2.Х добавляет круглые скобки для кортежа):
c:\code> python singletons.ру
Bob 400 Bob 400 42 42
Реализация альтернативных версий
Интересно отметить, что вы можете реализовать более самодостаточное решение, если имеете возможность применять оператор nonlocal (доступный только в Python
З.Х) для изменения имен в объемлющей области видимости, как было описано ранее. В показанной ниже альтернативной версии достигается идентичный эффект за счет использования для каждого класса одной объемлющей области видимости взамен одной записи в глобальной таблице. Версия работает точно так же, но не полагается на имена в глобальной области видимости вне декоратора (обратите внимание, что здесь при проверке на равенство None можно было бы применять is вместо ==, но в любом случае проверка тривиальна):
# Только в Python З.Х: оператор nonlocal
def singleton(aClass): # При декорировании @
instance = None
def onCall(*args, **kwargs): # При создании экземпляров
nonlocal instance # Оператор nonlocal в Python З.Х
if instance == None:
instance = aClass(*args, **kwargs) # Одна объемлющая область
# видимости на класс
return instance return onCall
В Python З.Х или 2.Х (Python 2.6 и последующих версиях) вы также можете реализовать самодостаточное решение с помощью либо атрибутов функции, либо класса. Вариант с атрибутами функции воплощен в первой части представленного далее кода, где задействован тот факт, что будет существовать одна функция onCall для каждого случая декорирования — пространство имен объекта исполняет такую же роль, как объемлющая область видимости. Во второй части кода используется один экземпляр для каждого случая декорирования вместо объемлющей области видимости, объекта функции или глобальной таблицы. На самом деле вторая часть полагается на ту же самую кодовую схему, которую мы позже увидим как распространенную грубую ошибку с классами декораторов — здесь мы хотим иметь только один экземпляр, но обычно так не происходит:
# Python З.Х и 2.Х: атрибуты функций, классы (альтернативные версии)
def singleton(aClass) : # При декорировании @
def onCall(*args, **kwargs): # При создании экземпляров
if onCall. instance None:
onCall.instance = aClass(*args, **kwargs) # Одна функция на класс return onCall.instance onCall.instance = None return onCall
class singleton:
def _init_(self, aClass) : # При декорировании @
self.aClass « aClass self.instance * None
def _call_(self, *args, **kwargs) : # При создании экземпляров
if self. instance *** None:
self.instance = self.aClass(*args, **kwargs) # Один экземпляр
# на класс
return self.instance
Чтобы превратить этот декоратор в полноценный универсальный инструмент, нужно сохранить его в импортируемом файле модуля и поместить код самотестирования внутрь проверки _name_— шаги, которые мы оставили в качестве упражнения для самостоятельной проработки. Финальная версия, основанная на классе, обеспечивает переносимость и дополнительную структуру, которая может лучше поддерживать будущее развитие, но применение ООП во всех контекстах может оказаться неоправданным.
Отслеживание объектных интерфейсов
Пример с классом-одиночкой в предыдущем разделе иллюстрировал использование декораторов классов для управления всеми экземплярами класса. Еще один распространенный сценарий применения для декораторов классов предусматривает дополнение интерфейса каждого созданного экземпляра. Декораторы классов могут по существу устанавливать для экземпляров уровень логики оболочки или “посредника”, который каким-то образом управляет доступом к их интерфейсам.
Скажем, в главе 31 был показан метод перегрузки операций_getattr_как способ оборачивания полных объектных интерфейсов внедренных экземпляров с целью реализации кодовой схемы делегирования. Похожие примеры встречались во время раскрытия темы управляемых атрибутов в предыдущей главе. Вспомните, что метод _getattr_запускается при извлечении неопределенного имени атрибута; мы можем использовать такую привязку для перехвата обращений к методам в классе контроллера и их передачи внедренному объекту.
Для справки вот исходный пример делегирования без декораторов, который работает с двумя объектами встроенных типов:
class Wrapper:
def _init_(self, object):
self.wrapped = object # Сохранение объекта
def _getattr_(self, attrname):
print(*Trace, attrname) # Отслеживание извлечения
return getattr(self.wrapped, attrname) # Делегирование извлечения
»> x = Wrapper ([1,2,3]) # Оборачивание списка
>» x.append(4) # Делегирование списковому методу
Trace: append
>>> х.wrapped # Вывод элементов
[I, 2, 3, 4]
»> х = Wrapper ({"а11: 1, "Ъ" : 2}) # Оборачивание словаря
»> list (х.keys ()) # Делегирование словарному методу
Trace: keys # Использовать list() в Python З.Х
['а', 'b']
В этом коде класс Wrapper перехватывает доступ к любым именованным атрибутам внутреннего объекта, выводит трассировочное сообщение и применяет встроенную функцию getattr для передачи запроса внутреннему объекту. В частности, он отслеживает доступ к атрибутам, осуществляемый снаружи класса внутреннего объекта; операции доступа внутри методов внутреннего объекта не перехватываются и выполняются нормально по определению. Поведение такой модели полного интерфейса отличается от поведения декораторов функций, которые оборачивают только один конкретный метод.
Отслеживание интерфейсов с помощью декораторов классов
Декораторы классов предоставляют альтернативный и удобный способ реализации этой методики с_getattr_для оборачивания целого интерфейса. Например, начиная с версий Python 2.6 и 3.0, предыдущий пример класса можно реализовать в виде декоратора класса, который запускает операцию создания экземпляра внутреннего класса вместо передачи готового экземпляра конструктору класса-оболочки. Он также расширен для поддержки ключевых аргументов через **kargs и подсчета количества операций доступа, сделанных в целях иллюстрации изменяемого состояния:
def Tracer (aClass) : # При декорировании @
class Wrapper:
def _init_(self, *args, **kargs): # При создании экземпляров
self.fetches = 0
self.wrapped = aClass(*args, **kargs) # Использование имени из
# объемлющей области видимости
def _getattr_(self, attrname):
print ('Trace: ' + attrname) # Перехват всех атрибутов кроме собственных self.fetches += 1
return getattr (self .wrapped, attrname) if Делегирование
# внутреннему объекту
return Wrapper
if _name_ == '_main_1 :
@Tracer
class Spam: # Spam = Tracer (Spam)
def display (self) : # Spam повторно привязывается к Wrapper
print('Spam! ' * 8)
@Tracer class Person:
def_init_(self, name, hours, rate) : # Wrapper запоминает Person
self, name = name self.hours = hours self.rate = rate
def pay(self):
return self.hours *
# Операции доступа извне класса отслеживаются self.rate # Операции доступа внутри
food = Spam ()
food.display()
print([food.fetches])
bob = Person('Bob', 40, 50) print(bob.name) print(bob.pay())
# методов не отслеживаются
# Запускается Wrapper ()
# Запускается_getattr_
# bob - на самом деле экземпляр Wrapper
# Wrapper содержит внедренный экземпляр Person
print('')
sue = Person('Sue 1, rate=100, hours=60) print(sue.name) print(sue.pay())
# sue - другой экземпляр Wrapper
# с другим экземпляром Person
print(bob.name) print(bob.pay())
# bob имеет отличающееся состояние
# Атрибуты Wrapper не отслеживаются
print([bob.fetches, sue.fetches])
Важно отметить, что реализация сильно отличается от декоратора отслеживания, который встречался ранее (вопреки одинаковым именам!). В разделе “Реализация декораторов функций” выше в главе мы взглянули на декораторы, которые позволяют отслеживать и измерять время выполнения вызовов заданной функции или метода. Напротив, за счет перехвата вызовов, создающих экземпляры, декоратор класса здесь дает возможность отслеживать целый объектный интерфейс, т.е. доступ к любым атрибутам экземпляра.
Ниже приведен вывод, выдаваемый кодом в Python З.Х и 2.Х (в Python 2.6 и последующих версиях). Операции извлечения атрибутов из экземпляров классов Spam и
Person вызывают логику_getattr_в классе Wrapper, потому что food и bob на
самом деле являются экземплярами Wrapper благодаря перенаправлению вызовов, создающих экземпляры:
c:\code> python interfacetracer.ру
Trace: display
Spam!Spam!Spam!Spam!Spam!Spam!Spam!Spam!
[1]
Trace: name
Bob
Trace: pay
2000
Trace: name
Sue
Trace: pay
6000
Trace: name
Bob
Trace: pay
2000
[4, 2]
Обратите внимание на то, что есть один класс Wrapper с предохранением состояния для каждого случая декорирования, созданный вложенным оператором class в функции Tracer, а также на то, что каждый экземпляр получает собственный счетчик операций извлечения в силу генерирования нового экземпляра Wrapper. Как мы увидим позже, организовать это сложнее, чем можно было ожидать.
Применение декораторов классов к встроенным типам
Также важно отметить, что выше декорировался определяемый пользователем класс. В точности как в первоначальном примере из главы 31, мы можем использовать декоратор для помещения в оболочку встроенного типа вроде списка. Условие заключается в том, что мы либо создаем подкласс, чтобы разрешить синтаксис декорирования, либо выполняем декорирование вручную — декораторный синтаксис требует оператора class для строки @. В следующем интерактивном сеансе х снова является экземпляром Wrapper из-за косвенности декорирования:
>>> from interfacetracer import Tracer »> @ Tracer
. . . class MyList (list) : pass # MyList = Tracer (MyList)
»> x = MyList ([1, 2,3]) # Запускается Wrapper ()
»> x.append(4) # Запускаются_getattr_, append
Trace: append »> x.wrapped
[1, 2, 3, 4]
>>> WrapList = Tracer (list) # Или выполнить декорирование вручную
»> х = WrapList([4, 5,6]) # Иначе требуется оператор для создания подкласса
»> х.append(7)
Trace: append >>> х.wrapped [4, 5, б, 7]
Подход с декораторами дает возможность перенести создание экземпляров внутрь самого декоратора, не требуя взамен передачи готового объекта. Хотя отличие может казаться незначительным, оно позволяет сохранить нормальный синтаксис создания экземпляров и претворить в жизнь все преимущества декораторов в целом. Вместо того чтобы требовать прогона всех вызовов, создающих экземпляры, через объект-оболочку вручную, нам понадобится лишь дополнить определения классов декоратор-ным синтаксисом:
@Тгасег # Подход с декораторами
class Person: ...
bob = Person(1 Bob', 40, 50)
sue = Person('Sue', rate=100, hours=60)
class Person: ... # Подход без декораторов
bob = Wrapper (Person (1 Bob', 40, 50))
sue = Wrapper(Person('Sue', rate=100, hours=60))
Исходя из предположения, что вы создадите более одного экземпляра класса и хотите применить дополнение к каждому экземпляру, декораторы обычно будут выигрышным вариантом с точки зрения как размера кода, так и его сопровождения.
| Примечание, касающееся нестыковки версий для атрибутов. Предыдущий де-| коратор отслеживания работает при явном доступе к именам атрибутов во всех версиях Python. Тем не менее, как выяснилось в главах 38, 32 и других местах, метод_getattr_перехватывает неявный доступ встроенных операций к методам перегрузки операций наподобие_str_и
_герг_в классических классах Python 2.Х, но не в классах нового стиля
Python З.Х.
В классах Python З.Х экземпляры наследуют стандартные версии для некоторых, но не всех таких имен из класса (на самом деле из суперкласса object). Кроме того, в Python З.Х неявно вызываемые атрибуты для встроенных операций вроде вывода и -I- не прогоняются через метод
_getattr_или родственный ему_getattribute_. В классах нового
стиля встроенные операции начинают поиск в классах, полностью пропуская просмотр нормального экземпляра.
Сказанное означает, что в том виде, как есть, класс оболочки отслеживания, основанный на_getattr_, будет автоматически отслеживать и
распространять вызовы перегрузки операций для встроенных операций в Python 2.Х, но не в Python З.Х. Чтобы увидеть это, отобразим х прямо в конце предыдущего интерактивного сеанса — в Python 2.Х атрибут
_герг_отслеживается и список выводится ожидаемым образом, но в
Python З.Х отслеживание не происходит и список выводится с использованием стандартного отображения для класса Wrapper:
»> х # Python 2.Х
Trace: _repr_
[4, 5, 6, 7]
>» х # Python З.Х
cinterfacetracer.Tracer.<locals>.Wrapper object at 0x02946358>
Чтобы то же самое работало в Python З.Х, методы перегрузки операций, как правило, должны избыточно переопределяться в классе оболочки вручную, посредством инструментов или за счет определения в суперклассах. Мы снова увидим это в работе при создании декоратора Private позже в главе, когда будем исследовать способы добавления методов, требующих такой код, в Python З.Х.