Книга: Марк Лутц - Изучаем Python, 5-е изд., Т. 2
Назад: Декораторы
Дальше: Все хорошее когда-нибудь заканчивается

Метаклассы

 

В предыдущей главе мы исследовали декораторы и ознакомились с разнообразными примерами их использования. В этой главе мы продолжим уделять внимание построению инструментов и изучим еще одну сложную тему: метаклассы.
Метаклассы в некотором смысле просто расширяют модель вставки кода, поддерживаемую декораторами. Как выяснилось в предыдущей главе, декораторы функций и классов позволяют перехватывать и дополнять вызовы функций и вызовы, создающие экземпляры классов. В том же духе метаклассы дают возможность перехватывать и дополнять создание классов — они предоставляют API-интерфейс для вставки дополнительной логики, подлежащей выполнению в завершение оператора class, хотя и не такими способами, как декораторы. Соответственно, они обеспечивают универсальный протокол для управления объектами классов в программе.
Подобно всему, что рассматривается в текущей части книги, метаклассы относятся к сложным темам, которые можно исследовать по мере необходимости. На практике метаклассы позволяют получить высокий уровень контроля над тем, как работает набор классов. Это мощная концепция, и метаклассы не предназначены для большинства прикладных программистов. Откровенно говоря, тема метаклассов также и не для слабонервных — некоторые части главы могут требовать особого внимания (а иные вполне можно было бы приписать перу Доктора Сьюза (https : //ru. wikipedia. org/wiki/floKTop_Cbio3)!).
С другой стороны, метаклассы открывают двери для различных кодовых схем, возможно трудных или неосуществимых по-другому. Они особенно интересны программистам, которые стремятся реализовывать гибкие API-интерфейсы либо инструменты программирования для применения другими. Однако даже если вы не входите в указанную категорию, метаклассы могут многое вам рассказать о модели классов Python в целом (как будет показано, они влияют также на наследование), а их знание будет необходимым условием для понимания кода, где они задействованы. Подобно прочим продвинутым инструментам метаклассы начали появляться в программах на Python чаще, чем рассчитывали их создатели.
Как и в предыдущей главе, часть нашей цели заключается в том, чтобы также продемонстрировать более реалистичные примеры кода, нежели показанные ранее в книге. Хотя метаклассы относятся к основному языку и сами по себе не образуют отдельную предметную область, в главе планируется пробудить у вас интерес к исследованию более крупных примеров программирования прикладных приложений по окончании чтения настоящей книги.
Поскольку в книге это последняя глава с техническим содержанием, в ней также завершается ряд цепочек обсуждений, касающихся самого языка Python, с которыми мы часто сталкивались ранее и которые будут доведены до конца в последующем заключении. Разумеется, то, чем вы займетесь после чтения данной книги, зависит лишь от вас, но в проекте с открытым кодом важно помнить общую картину, занимаясь мелкими деталями.
Нужно ли иметь дело с метаклассами?
Возможно, метаклассы являются самой сложной темой в этой книге, а то и во всем языке Python. Ниже приведено мнение Тима Петерса, заслуженного разработчика основной части языка Python (и также автора известного девиза в import this), которое было высказано им в группе новостей comp. lang. python.
[Метаклассы] — это более темная магия, которая не касается 99% пользователей. Если вы размышляете, нужны ли они вам, то наверняка нет (люди, которым действительно необходимы метаклассы, совершенно точно знают, что они нужны, и им не требуется объяснение причин).
Другими словами, метаклассы предназначены главным образом для подгруппы программистов, занимающихся построением API-интерфейсов и инструментов, которые будут использоваться другими. Во многих случаях (если только не в большинстве) они, скорее всего, не будут лучшим вариантом в работе приложений, что особенно справедливо, когда вы пишете код, который впоследствии будет эксплуатироваться другими людьми. Реализация чего-нибудь лишь потому, что оно выглядит “крутым”, обычно не служит разумным оправданием, разве только если вы экспериментируете или учитесь.
Тем не менее, метаклассы имеют широкий спектр потенциальных сценариев применения и важно знать, когда они полезны. Например, они могут использоваться для расширения классов средствами вроде отслеживания, постоянства объектов, регистрации исключений и многого другого. С помощью метаклассов также можно конструировать порции класса во время выполнения на основе конфигурационных файлов, применять декораторы функций к каждому методу класса обобщенным образом, проверять соответствие ожидаемым интерфейсам и т.д.
В своих более грандиозных воплощениях метаклассы можно даже использовать при реализации альтернативных кодовых схем, таких как аспектно-ориентированное программирование, инструменты объектно-реляционного отображения для баз данных и т.п. Хотя часто существуют другие способы достижения таких результатов (как выяснится, роли декораторов классов и метаклассов нередко пересекаются), метаклассы предоставляют формальную модель, приспособленную для таких задач. Конечно, нам не хватит места в этой главе, чтобы исследовать все приложения подобного рода, но после изучения основ рекомендуется поискать дополнительные сценарии применения в веб-сети.
Вероятно, наиболее подходящая причина исследования метаклассов в настоящей книге заключается в том, что они помогают прояснить механизм классов Python в общем. Скажем, мы увидим, что метаклассы являются неотъемлемой частью модели наследования нового стиля в языке, которая здесь окончательно формализуется. Независимо от того, будете вы реализовывать либо использовать их в своей работе или нет, даже поверхностное знание метаклассов способно содействовать более глубокому пониманию языка Python в целом 1.
1 Здесь уместно привести сообщение об ошибке, выдаваемое Python З.Х: “TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases” (“Ошибка типа: конфликт метаклассов: метакласс производного класса должен быть (нестрогим) подклассом метаклассов всех его базовых классов”). Оно отражает ошибочное использование модуля как суперкласса, но метаклассы могут оказаться не настолько необязательными, как предполагали разработчики — мы вернемся к данной теме при подведении итогов по книге в следующей главе.
Повышение уровней "магии"
Основное внимание в книге было сосредоточено на методиках реализации приложений — модулях, функциях и классах, на написание которых большинство программистов тратят свое время, достигая стоящих перед ними целей. Большая часть пользователей Python могут взаимодействовать с классами, создавать экземпляры и даже иногда перегружать операции, но вряд ли они будут слишком глубоко вникать в детали того, как действительно работают их классы.
Однако в этой книге также рассматривались разнообразные инструменты, которые позволяют управлять поведением Python обобщенным образом и часто имеют больше отношения к внутренним механизмам Python или построению инструментов, чем к области прикладного программирования. Ниже приведен обзор, который поможет понять место, занимаемое метаклассами.
Атрибуты и инструменты интроспекции
Специальные атрибуты, такие как_class_и_diet_, позволяют просматривать внутренние детали реализации объектов Python с целью их обобщенной обработки — для получения списка всех атрибутов объекта, отображения имени класса и т.д. Как мы увидим, инструменты наподобие dir и getattr способны исполнять похожие роли, когда должны поддерживаться “виртуальные” атрибуты вроде слотов.
Методы перегрузки операций
Особым образом именованные методы, такие как_str_и_add_, реализованные в классах, перехватывают и снабжают линиями поведения встроенные операции, которые применяются к экземплярам классов, включая вывод, операции выражений и т.д. Они запускаются автоматически в ответ на встроенные операции и предоставляют классам возможность соответствовать ожидаемым интерфейсам.
Методы перехвата доступа к атрибутам
Специальная категория методов перегрузки операций предлагает способ перехвата доступа к атрибутам экземпляров обобщенным образом:_getattr_,
_setattr_,_delattr_и_getattribute_позволяют классам оболочек
(т.е. посредникам) вставлять автоматически запускаемый код, который может проверять допустимость запросов атрибутов и делегировать их внутренним объектам. Они делают возможным выполнение вычислений любого количества атрибутов при доступе — либо избранных атрибутов, либо вообще всех.
Свойства классов
Встроенная функция property позволяет ассоциировать с конкретным атрибутом код, который автоматически запускается, когда происходит извлечение, присваивание или удаление атрибута. Несмотря на меньшую универсальность по сравнению с описанными выше инструментами, свойства делают возможным автоматическое выполнение кода при доступе к специфическим атрибутам.
Дескрипторы атрибутов классов
Вообще говоря, property является лаконичным путем определения дескриптора атрибута, который автоматически вызывает функции при доступе. Дескрипторы позволяют реализовывать в отдельном классе методы-обработчики_get_,_set_и_delete_, запускаемые автоматически, когда происходит доступ к атрибуту, которому присвоен экземпляр этого класса. Они обеспечивают универсальный способ вставки произвольного кода, который неявно выполняется при доступе к конкретному атрибуту как часть нормальной процедуры нахождения атрибутов.
Декораторы функций и классов
Как было показано в главе 39, особый синтаксис вида $вызваемый_объект для декораторов позволяет добавлять логику, подлежащую автоматическому запуску, когда происходит вызов функции или создание экземпляра класса. Такая логика оболочки может отслеживать либо измерять время работы вызовов, проверять допустимость значений аргументов, управлять всеми экземплярами класса, дополнять экземпляры добавочной линией поведения вроде проверки допустимости операций извлечения атрибутов и т.д. Декораторный синтаксис вставляет логику повторной привязки имен, которая должна выполняться в конце операторов определения функций и классов — имена декорированных функций и классов могут повторно привязываться либо к дополненным исходным объектам, либо к объектам-посредникам, которые перехватывают последующие обращения.
Метаклассы
Последний раздел магии, представленной в главе 32, который мы обсудим здесь.
Как упоминалось во введении данной главы, метаклассы являются продолжением рассматриваемой истории — они позволяют вставлять логику, подлежащую автоматическому выполнению в конце оператора class, когда создается объект класса. Хотя механизм метаклассов сильно напоминает декораторы классов, он не привязывает имя класса к результату, возвращенному вызываемым объектом декоратора, а взамен поручает процедуру создания самого класса специализированной логике.
Язык привязок
Другими словами, метаклассы в конечном итоге представляют собой лишь еще один способ определения автоматически запускаемого кода. Благодаря инструментам, перечисленным в предыдущем разделе, Python дает нам возможность вставлять логику в разнообразные контексты, среди которых выполнение операций, доступ к атрибутам, вызовы функций, создание экземпляров классов и теперь создание объектов классов. Это язык с изобилием привязок — средство, подобно любому другому открытое для злоупотреблений, но также обеспечивающее гибкость, о которой мечтает ряд программистов, и которая может требоваться в определенных программах.
Как было показано, со многими из расширенных инструментов Python связаны пересекающиеся роли. Например, атрибутами часто можно управлять с помощью свойств, дескрипторов или методов перехвата доступа к атрибутам. Далее в главе вы увидите, что декораторы классов и метаклассы также нередко могут использоваться взаимозаменяемо. В качестве краткого предварительного обзора:
• хотя декораторы классов часто применяются для управления экземплярами, они также могут использоваться для управления классами почти как метаклассы;
• аналогично наряду с тем, что метаклассы предназначены для дополнения процедуры создания классов, они также могут вставлять посредников для управления экземплярами почти как декораторы классов.
На самом деле главное функциональное отличие между указанными двумя инструментами касается момента, когда они активизируются в рамках процесса создания классов. В предыдущей главе объяснялось, что декораторы классов запускаются после того, как декорированный класс был создан. Таким образом, они часто применяются для добавления логики, подлежащей выполнению во время создания экземпляров. Когда декораторы классов снабжают класс линией поведения, то обычно делают это через изменения или посредников, а не более непосредственное отношение.
Как вы увидите здесь, по контрасту с декораторами классов метаклассы запускаются во время создания классов, чтобы создать и возвратить новый клиентский класс. Следовательно, они часто используются для управления или дополнения самих классов и могут даже предоставлять методы для обработки созданных из них классов через прямое отношение между экземплярами.
Скажем, метаклассы можно применять для автоматического декорирования всех методов классов, регистрации всех используемых классов в API-интерфейсе, автоматического добавления к классам пользовательского интерфейса, создания или расширения классов на основе упрощенных спецификаций в текстовых файлах и т.д. Поскольку они способны управлять тем, как создаются классы, и через посредников поведением, которым обзаводятся их экземпляры, применимость метаклассов потенциально очень широка.
Тем не менее, в главе будет показано, что во многих распространенных ролях эти два инструмента скорее похожи, чем различаются. Так как выбор инструментов для работы иногда отчасти субъективен, знание альтернатив может помочь подобрать правильный инструмент для решения имеющейся задачи. Чтобы лучше понять варианты, давайте посмотрим, что нам могут предложить метаклассы.
Недостаток "вспомогательных" функций
Также подобно декораторам из предыдущей главы с теоретической точки зрения метаклассы часто необязательны. Обычно мы можем достичь того же самого эффекта, пропуская объекты классов через управляющие функции (временами называемые вспомогательными функциями), что во многом похоже на возможность реализации целей декораторов за счет прогона функций и экземпляров через управляющий код. Однако, как и декораторы, метаклассы:
• обеспечивают более формальную и явную структуру;
• помогают гарантировать, что прикладные программисты не забудут дополнить свои классы в соответствии с требованиями API-интерфейса;
• снижают избыточность кода и связанные с ней накладные расходы на сопровождение, вынося логику настройки классов в единственное место, т.е. метакласс.
Для иллюстрации предположим, что нам нужно автоматически вставлять метод в набор классов. Разумеется, мы могли бы решить задачу посредством обычного наследования, если целевой метод известен на момент написания кода классов. В таком случае мы можем просто реализовать метод в суперклассе и заставить все необходимые классы наследоваться от суперкласса:
# Нормальное наследование: слишком статично
class Clientl (Extras) : . . . # Клиенты наследуют дополнительный метод extra class Client2(Extras) : ... class Client3(Extras) : ...
class Extras:
def extra(self, args):
X = Clientl() # Создание экземпляра
X.extra () # Запуск дополнительного метода extra
Тем не менее, иногда при написании кода классов спрогнозировать такое дополнение невозможно. Возьмем сценарий, когда классы дополняются в ответ на выбор, сделанный в пользовательском интерфейсе во время выполнения, или согласно спецификациям, введенным в конфигурационном файле. Несмотря на то что мы могли бы предусмотреть в каждом классе из нашего воображаемого набора код для ручной проверки этого, у клиентов придется выяснять множество вопросов (вызовы required здесь абстрактны и должны быть чем-то заполнены):
def extra(self, arg): ...
class Clientl: ... # Дополнения клиентов: крайне разбросаны
if required():
Clientl.extra = extra
class Client2: ... if required():
Client2.extra = extra
class Client3: ... if required():
Client3.extra = extra
X = Clientl ()
X.extra ()
Мы можем добавлять методы в класс после оператора class из-за того, что метод уровня класса является всего лишь функцией, которая ассоциирована с классом и имеет первый аргумент, предназначенный для приема экземпляра self. Хотя прием работает, он может стать непригодным для более крупных наборов методов и возлагает все бремя дополнения на клиентские классы (вдобавок допуская, что они не забудут об этом!).
С точки зрения сопровождения было бы лучше изолировать логику выбора в одном месте. Мы могли бы инкапсулировать часть дополнительной работы, пропуская классы через управляющую функцию, которая бы должным образом расширяла класс и выполняла всю работу по проверке и конфигурированию во время выполнения:
def extra(self, arg): ...
defextras(Class): # Управляющая функция: слишком много ручной работы
if required():
Class.extra = extra class Clientl: ... extras(Clientl)
class Client2: ... extras(Client2)
class Client3: ... extras(Client3)
X = Clientl()
X.extra()
В коде класс прогоняется через управляющую функцию немедленно после создания. Несмотря на то что управляющие функции подобного рода позволяют здесь достичь нашей цели, они по-прежнему будут тяжелой ношей для разработчиков классов, которые обязаны понимать требования и придерживаться их в своем коде. Было бы лучше, если бы существовал простой способ принудительно навязать дополнение в целевых классах, чтобы разработчикам не приходилось иметь дело с дополнением настолько явно, и стало бы меньше шансов вообще о нем забыть. Другими словами, мы бы хотели иметь возможность вставлять какой-то код, подлежащий автоматическому запуску в конце оператора class для дополнения класса.
Именно такую работу выполняют метаклассы — объявляя метакласс, мы сообщаем интерпретатору Python о том, что процесс создания объекта класса должен быть направлен указанному нами другому классу:
def extra(self, arg): ...
class Extras(type):
def _init_(Class, classname, superclasses, attributedict):
if required():
Class.extra = extra
class Clientl(metaclass=Extras) : ... # Только объявление метакласса
# (форма Python З.Х)
class Client2(metaclass=Extras): ... # Клиентский класс является
# экземпляром метакласса
class Client3(metaclass=Extras) : ...
X = Clientl () # X - экземпляр Clientl
X.extra ()
Поскольку интерпретатор Python автоматически активизирует метакласс в конце оператора class, когда новый класс создан, он может необходимым образом дополнять, регистрировать или по-другому управлять классом. Кроме того, единственное требование для клиентских классов заключается в том, что они объявляют метакласс; каждый класс, который так поступает, автоматически получит любое дополнение, предоставляемое метаклассом, и теперь, и в будущем, если метакласс изменится.
Конечно, приведенное обоснование выглядит стандартным и вам придется выработать собственное суждение — действительно, разработчики клиентских классов в той же степени же легко могут забыть об указании метакласса, как и вызывать управляющую функцию! Однако явная природа метаклассов может уменьшить вероятность такого забывания. Более того, метаклассы обладают дополнительными возможностями, которые пока еще не раскрывались. Хотя в таком небольшом примере это трудно заметить, метаклассы обычно лучше справляются с задачами подобного рода по сравнению с более ручными подходами.
Метаклассы против декораторов классов: раунд 1
Важно также отметить, что декораторы классов, описанные в предыдущей главе, иногда пересекаются с метаклассами — с точки зрения полезности и выгоды. Несмотря на то что декораторы классов часто используются для управления экземплярами, они также могут дополнять классы независимо от любых созданных экземпляров. Синтаксис делает их применение аналогично явным и возможно более очевидным, чем вызовы управляющих функций.
Скажем, пусть мы реализовали управляющую функцию, которая возвращает дополненный класс вместо того, чтобы модифицировать его на месте. Это позволило бы достичь более высокой степени гибкости, т.к. управляющая функция могла бы возвращать любой тип объекта, который реализует ожидаемый интерфейс класса:
def extra(self, arg): ...
def extras(Class): if required():
Class.extra = extra return Class
class Clientl: ...
Clientl = extras(Clientl)
class Client2: ...
Client2 = extras(Client2)
class Client3: ...
Client3 = extras(Client3)
X = Clientl ()
X.extra ()
Если вы думаете, что код начинает напоминать декораторы классов, то абсолютно правы. В предыдущей главе мы делали особый акцент на роли декораторов классов в дополнении вызовов, создающих экземпляры. Тем не менее, из-за того, что они работают, выполняя автоматическую повторную привязку имени класса к результату функции, нет никаких причин, по которым мы не могли бы их использовать для дополнения класса, изменяя его перед тем, как будут созданы любые экземпляры. То есть декораторы классов могут применять дополнительную логику к классам, а не только к экземплярам, во время создания классов:
def extra(self, arg): ...
def extras(Class): if required():
Class.extra = extra return Class
@extras
class Clientl: ... # Clientl = extras(Clientl)
@extras
class Client2: ... # Повторная привязка класса независимо от экземпляров
@extras
class Client3: ...
X = Clientl() # Создание экземпляра дополненного класса
X.extra () # X - экземпляр исходного класса Clientl
Здесь декораторы по существу автоматизируют повторное привязывание имен из предыдущего примера. Как и для метаклассов, поскольку декоратор возвращает исходный класс, экземпляры создаются из него, а не из объекта-оболочки. Фактически в приведенном примере создание экземпляров вообще не перехватывается.
В продемонстрированном особом случае — добавление метолов к классу, когда он создается — выбор между метаклассами и декораторами весьма произволен. Декораторы могут использоваться для управления экземплярами и классами, и сильнее всего они пересекаются с метаклассами во второй из указанных ролей, но такое различение не абсолютно. На самом деле роли каждого инструмента частично определяются их механизмами.
Как вы увидите далее, декораторы формально соответствуют методам_init_
метаклассов, применяемым для инициализации вновь созданных классов. Однако помимо инициализации классов метаклассы имеют дополнительные привязки для настройки и способны выполнять произвольные задачи создания классов, решение которых с помощью декораторов может оказаться более сложным. В итоге они могут стать сложнее, но и будут лучше подходить для дополнения классов по мере их формирования.
Например, в метаклассах также есть метод_new_, используемый для создания
класса, который не имеет аналога в декораторах; создание нового класса в декораторе потребовало бы дополнительного шага. Кроме того, метаклассы могут предоставлять линии поведения, получаемые классами в форме методов, что тоже не имеет прямого эквивалента в декораторах; декораторы обязаны снабжать класс поведением менее прямыми способами.
И наоборот, поскольку метаклассы предназначены для управления классами, их использование для управления только экземплярами не настолько оптимально. Ввиду того, что они также несут ответственность за создание самого класса, в ролях управления экземплярами возникает добавочный шаг.
Позже в главе мы исследуем отличия в коде и доведем неполный код из текущего раздела до реалистичного рабочего примера. Тем не менее, чтобы понять работу метаклассов, сначала необходимо получить более четкое представление о лежащей в их основе модели.
Магия тут и магия там
Список в разделе “Повышение уровней ‘магии’” ранее в главе охватывал разновидности магии за рамками тех, которые у программистов принято считать полезными. Кто-то может добавить к этому списку инструменты Python для функционального программирования, такие как замыкания и генераторы, и даже базовую поддержку ООП. Первые опираются на предохранение области видимости и автоматическое создание генераторных объектов, а ООП — на поиск атрибутов при наследовании и особый первый аргумент в функциях. Хотя они тоже базируются на магии, но представляют парадигмы, которые облегчают задачу программирования, предлагая абстракцию поверх лежащей в основе аппаратной архитектуры.
Например, поддержка ООП — более ранняя парадигма Python — широко принята в мире разработки программного обеспечения. Она предоставляет модель для написания программ, которая является более полной, явной и богато структурированной, чем инструменты функционального программирования. То есть некоторые уровни магии считаются более обоснованными, нежели другие; в конце концов, если бы не толика магии, то программы все еще состояли бы из машинного кода (или физических коммутаторов).
Как правило, накопление новой магии подвергает системы риску превысить порог сложности — скажем, добавление парадигмы функционального программирования к языку, который всегда был объектно-ориентированным, либо введение избыточности или замысловатых способов достижения целей, которые редко преследуются на практике большинством пользователей. Такая магия способна поднять планку слишком высоко для основной части аудитории вашего инструмента.
Кроме того, одна магия навязывается своим пользователям сильнее другой. Например, шаг трансляции компилятора обычно не требует от пользователей быть разработчиками компилятора. И наоборот, встроенная функция Python по имени super предполагает наличие полноценного мастерства и ввода в действие возможно неясного и неестественного алгоритма MRO. Представленный в этой главе алгоритм наследования нового стиля похожим образом считает обязательным знание дескрипторов, метаклассов и MRO — самих по себе сложных инструментов. Даже неявные “привязки” вроде дескрипторов остаются неявными лишь до первого их отказа или цикла сопровождения. Откровенная магия подобного рода усиливает предварительные условия, требуемые инструментом, и снижает удобство его использования.
В системах с открытым кодом только время и количество загрузок способны определить, где могут находиться пороги сложности. Отыскание надлежащего баланса между мощностью и сложностью зависит от переменчивого мнения в такой же степени, как и от технологии. Однако, оставив в стороне субъективные факторы, навязываемая пользователям новая магия неизбежно делает более крутой кривую обучения для системы — тема, к которой мы вернемся в заключительных словах финальной главы.
Модель метаклассов
Для понимания метаклассов сначала необходимо чуть больше узнать о модели типов Python и о том, что происходит в конце оператора class. Как вы увидите, они тесно взаимосвязаны.
Классы являются экземплярами type
До сих пор в книге мы выполняли большую часть работы, создавая экземпляры встроенных типов вроде списков и строк, а также экземпляры классов, которые реализовывали самостоятельно. Вы видели, что экземпляры классов не только имеют ряд собственных атрибутов информации состояния, но и наследуют поведенческие атрибуты от классов, из которых они были созданы. То же самое остается справедливым для встроенных типов; например, экземпляры списка располагают собственными значениями и вдобавок наследуют методы из спискового типа.
Хотя мы можем многое делать с такими объектами экземпляров, модель типов Python оказывается несколько богаче, чем было формально описано. По правде говоря, в показанной до сих пор модели имеется пробел: если экземпляры создаются из классов, тогда что создает наши классы? Выясняется, что классы тоже являются экземплярами чего-то:
• в Python З.Х объекты классов, определяемых пользователей, представляют собой экземпляры объекта по имени type, который сам по себе является классом.
• в Python 2.Х классы нового стиля наследуются от obj ect, который представляет собой подкласс type; классические классы являются экземплярами type и не создаются из какого-то класса.
Мы исследовали понятие типов в главе 9 первого тома, а взаимоотношение между классами и типами в главе 32, но давайте здесь проанализируем основы, чтобы увидеть, как они применяются к метаклассам.
Вспомните, что встроенная функция type возвращает тип любого объекта (который тоже объект), когда вызывается с единственным аргументом. Для встроенных типов наподобие списков типом экземпляра будет встроенный списковый тип, но типом спискового типа оказывается сам type — объект type на верхушке иерархии создает индивидуальные типы, а индивидуальные типы создают экземпляры. Вы можете убедиться в этом самостоятельно в интерактивной оболочке. Скажем, в Python З.Х типом экземпляра списка является списковый класс, типом которого будет класс type:
C:\code> py -3 # В Python З.Х:
»> type([]), type(type([])) # Экземпляр списка создается
# из спискового класса (cclass 'list'>, cclass 'type1>) # Списковый класс создается из класса type >» type (list) , type (type) # To же самое, но с именами типов
(cclass ' type'>, cclass ’type'>) # Типом type является type: верхушка иерархии
Как выяснилось в главе 32 при изучении изменений, внесенных в классы нового стиля, то же самое в целом справедливо в Python 2.Х, но типы не полностью совпадают с классами. Здесь type представляет собой уникальный вид встроенного объекта, который покрывает иерархию типов и используется для создания типов:
С: \code> ру -2
»> type([]), type(type([])) # В Python 2.Х тип type немного отличается
(ctype 'list'>, ctype 'type'>)
>» type (list) , type (type)
(ctype 'type’>, ctype 1type’>)
Между прочим, отношение типы/экземпляры сохраняется также для определяемых пользователем классов: экземпляры создаются из классов, а классы создаются из type. Тем не менее, в Python З.Х понятие “типа” объединено с понятием “класса”. На самом деле по существу это два синонима: классы являются типами, а типы - классами. То есть:
• типы определяются классами, которые унаследованы от type;
• определяемые пользователем классы являются экземплярами класса type;
• определяемые пользователем классы представляют собой типы, которые генерируют собственные экземпляры.
Ранее было показано, что такая эквивалентность влияет на код, который проверяет тип экземпляров: типом экземпляра будет класс, из которого экземпляр был сгенерирован. Она также имеет значение для способа, которым классы создаются, что оказывается важным моментом для понимания темы текущей главы. Поскольку по умолчанию классы обычно создаются из корневого класса type, большинству программистов не приходится думать об эквивалентности типов и классов. Однако это открывает новые возможности по настройке классов и их экземпляров.
Например, все определяемые пользователем классы в Python З.Х (и классы нового стиля в Python 2.Х) являются экземплярами класса type, а объекты экземпляров — экземплярами своих классов. В действительности классы теперь имеют атрибут _class_, ссылающийся на type, точно так же, как экземпляр имеет атрибут
_class_, ссылающийся на класс, из которого он был создан:
С: \code> ру -3
>» class С: pass # Объект класса Python З.Х (нового стиля)
»> Х=С<) # Объект экземпляра класса
»> type(X) # Экземпляр является экземпляром класса cclass '_main_.С’>
>» X._class__# Класс экземпляра
cclass 1_main_.С'>
>» type (С) # Класс является экземпляром type
cclass 'type'>
>>> С._class__# Классом класса является type
cclass 'type'>
Обратите особое внимание на последние две строки — классы представляют собой экземпляры класса type, в точности как нормальные экземпляры являются экземплярами класса, определяемого пользователем. В Python З.Х это работает одинаково для встроенных типов и типов классов, определяемых пользователем. На самом деле классы вообще не считаются отдельной концепций: они представляют собой просто определяемые пользователем типы и сам type определен посредством класса.
В Python 2.Х ситуация аналогична для классов нового стиля, производных от object, потому что тогда становится доступным поведение классов Python З.Х (как вы уже видели, Python З.Х автоматически добавляет object в кортеж суперклассов
_bases_корневых классов верхнего уровня, чтобы квалифицировать их как классы
нового стиля):
С: \code> ру -2 »> class С (object) : pass »> X = С()
# Классы нового стиля в Python 2.Х,
# классы тоже имеют атрибут_class
>>> type(X)
<class '_main_.Cf>
>>> X._class_
<class '_main_.C’>
>>> type(C)
ctype 'type'>
>» C._class_
ctype 'type'>
Тем не менее, классические классы в Python 2.Х несколько отличаются. Из-за того, что они отражают первоначальную модель классов из более старых версий Python,
классические классы не имеют ссылки _class_ и подобно встроенным типам
Python 2.Х являются экземплярами объекта type, а не класса type (некоторые шестнадцатеричные адреса сокращены):
С: \code> ру -2
>>> class С: pass # Классические классы в Python 2.Х,
>» Х=С() # сами классы не имеют атрибута_class_
»> type(X)
ctype 'instance'>
>» X._class_
cclass _main_.C at 0x005F85A0>
>» type(C)
ctype 'classobj'>
>>> C._class_
AttributeError: class С has no attribute '_class_'
Ошибка атрибута: класс С не имеет атрибута_class_
Метаклассы являются подклассами type
Почему нас должен волновать тот факт, что классы являются экземплярами класса type в Python З.Х? Оказывается, что это привязка, которая предоставляет нам возможность реализации метаклассов. Поскольку в настоящее время понятия типа и класса совпадают, мы можем создавать подклассы type для его настройки с помощью обычных методик ООП и синтаксиса классов. А из-за того, что классы в действительности представляют собой экземпляры класса type, создание классов на основе настроенных подклассов type позволяет реализовывать специальные виды классов.
Обращаясь к деталям, все работает вполне естественно — в Python З.Х и в классах нового стиля Python 2.Х:
• type является классом, который генерирует классы, определяемые пользователем;
• метаклассы представляют собой подклассы класса type;
• объекты классов являются экземплярами класса type или какого-то из его подклассов;
• объекты экземпляров генерируются из класса.
Другими словами, для управления способом создания классов и дополнения их поведения нам необходимо лишь указать, что определяемый пользователем класс должен создаваться из определяемого пользователем метакласса, а не нормального класса type.
Обратите внимание, что такое отношение между типом и экземпляром не вполне соответствует обычному наследованию. Определяемые пользователем классы могут также иметь суперклассы, из которых они и их экземпляры наследуют атрибуты. Как вы уже знаете, наследуемые суперклассы перечисляются внутри круглых скобок в операторе class и появляются в кортеже_bases_класса. Однако с типом, из которого создается класс, и экземпляром которого он является, имеется другое отношение. Процедура наследования выполняет поиск в словарях пространств имен экземпляров и классов, но классы могут также получать линию поведения от своего типа, который не виден поиску при нормальном наследовании.
Чтобы заложить основу для понимания такого отличия, в следующем разделе описана процедура, которую интерпретатор Python придерживается для реализации отношения “экземпляр типа”.
Протокол оператора class
Создание подклассов класса type для его настройки — на самом деле лишь половина магии, стоящей за метаклассами. Нам по-прежнему необходимо как-то направлять создание класса метаклассу вместо стандартного type. Для полного понимания, каким образом все организовано, нам также нужно знать, как операторы class делают свою работу.
Мы уже выяснили, что когда интерпретатор Python добирается до оператора class, он выполняет его вложенный блок кода, чтобы создать атрибуты — все имена, которым присваиваются значения на верхнем уровне вложенного блока кода, становятся атрибутами в результирующем объекте класса. Такими именами обычно являются функции методов, создаваемые вложенными операторами def, но они также могут быть произвольными атрибутами, которым присваиваются значения для создания данных класса, разделяемых всеми экземплярами.
Говоря формально, чтобы это произошло, интерпретатор Python следует стандартному протоколу: в конце оператора class и после выполнения всего вложенного в него кода в словаре пространств имен, соответствующем локальной области видимости класса, Python обращается к объекту type для создания объекта класс:
класс = type {имя_класса, суперклассы, словарь_атрибутов)
В type определен метод перегрузки операций_call_, который при вызове объекта type по очереди запускает два других метода:
type._new_(класс_Ьуре, имя_класса, суперклассы, словарь_атрибутов)
type._init_{класс, имя_класса, суперклассы, словарь_атрибутов)
Метод_new_ создает и возвращает новый объект класс, после чего метод
_init_инициализирует вновь созданный объект. Как вскоре будет показано, именно они выступают в качестве привязок, которые метаклассы, являющиеся подклассами type, обычно применяют для настройки классов.
Скажем, пусть имеется определение класса Spam следующего вида:
class Eggs: ... # Здесь находятся наследуемые имена
class Spam (Eggs) : # Наследуется от Eggs
data = 1 # Атрибут данных класса
def meth (self, arg) : # Атрибут метода класса return self.data + arg
Интерпретатор Python внутренне запустит вложенный блок кода для создания двух атрибутов класса (data и meth) и затем обратится к объекту type, чтобы сгенерировать объект класса в конце оператора class:
Spam = type (' Spam’, (Eggs,), {'data1: 1, 'meth': meth, '_module_': ' main_'})
На самом деле вы можете вызвать type подобным образом самостоятельно и создать класс динамически — хотя здесь с поддельной функцией метода и пустым кортежем суперклассов (object добавляется автоматически в Python З.Х и 2.Х):
>>> х = type('Spam' , () , { 'data' : 1, 'meth' : (lambda x, у: x.data + у)})
»> i = x ()
»> x, i
(cclass '_main_.Spam'>, с_main_.Spam object at 0x029E7780>)
>>> i.data, i.meth(2)
(1, 3)
Созданный класс оказывается точно таким же, как полученный в результате выполнения оператора class:
>» х._bases_
(cclass 'object'>,)
>>> [(a, v) for (a, v) in x. diet_. items () if not a. startswith ('_')]
[('data', 1), ('meth', cfunction clambda> at 0x0297A158>)]
Тем не менее, поскольку вызов type производится автоматически в конце оператора class, он представляет собой идеальную привязку для дополнения или другой обработки класса. Хитрость заключается в замене стандартного класса type специальным подклассом, который будет перехватывать такой вызов. В следующем разделе показано, как это сделать.
Объявление метаклассов
Как вы только что видели, по умолчанию классы создаются посредством класса type. Чтобы сообщить интерпретатору Python о необходимости создания класса с помощью специального метакласса, вам просто понадобится объявить метакласс для перехвата нормального вызова, создающего экземпляр, в определяемом пользователем классе. Способ достижения цели зависит от используемой версии Python.
Объявление в Python З.Х
В Python З.Х желаемый метакласс указывается как ключевой аргумент в заголовке оператора class:
class Spam(metaclass=Meta): # Версия Python З.Х (только)
Наследуемые суперклассы тоже можно перечислять в заголовке оператора class. Например, определяемый ниже новый класс Spam унаследован от суперкласса Eggs, но также является экземпляром, созданным метаклассом Meta:
class Spam (Eggs, metaclass=Meta) : # Допускаются обычные суперклассы:
# должны указываться первыми
В такой форме суперклассы должны указываться перед метаклассом; фактически здесь применяются правила упорядочения, используемые для ключевых аргументов в вызовах функций.
Объявление в Python 2.Х
Мы можем достичь того же эффекта и в Python 2.Х, но указывая метакласс по-другому — вместо ключевого аргумента применяя атрибут класса:
class Spam(object) : # Версия Python 2.Х (только), object необязателен? _metaclass_ = Meta
class Spam (Eggs, object) : # Допускаются обычные суперклассы:
# подразумевается object
_metaclass_ = Meta
Формально некоторые классы в Python 2.Х вовсе не обязаны явно наследоваться от object, чтобы задействовать метаклассы. Обобщенный механизм координирования метаклассов появился одновременно с классами нового стиля, но сам он к ним не привязан. Однако он их производит — при наличии объявления_metaclass_интерпретатор Python 2.Х автоматически делает результирующий класс классом нового
стиля, добавляя object в его последовательность_bases_. При отсутствии такого
объявления интерпретатор Python 2.Х просто использует инструмент создания классических классов в качестве стандартного метакласса. По этой причине некоторые классы в Python 2.Х требуют только атрибута_metaclass_.
С другой стороны, метаклассы подразумевают, что ваш класс будет классом нового стиля в Python 2.Х, даже без явного наследования от object. Они будут вести себя несколько иначе в сравнении с тем, как было обрисовано в главе 32. К тому же вам предстоит увидеть, что Python 2.Х может требовать, чтобы они либо их суперклассы явно наследовались от obj ect, поскольку в подобном контексте классу нового стиля нельзя иметь только классические суперклассы. С учетом этого наследование от object не помешает как своего рода предупреждение о природе класса и во избежание потенциальных проблем может считаться обязательным.
Также в Python 2.Х доступна глобальная переменная_metaclass_уровня модуля для связывания всех классов в модуле с метаклассом. В Python З.Х она больше не поддерживается, т.к. задумывалась в качестве временной меры, чтобы облегчить переход к применению по умолчанию классов нового стиля, не наследуя каждый класс от object. Кроме того, в Python З.Х также игнорируется атрибут класса Python 2.Х, а форма с ключевым аргументом из Python З.Х трактуется как синтаксическая ошибка в Python 2.Х, и потому простой путь к обеспечению переносимости отсутствует. Тем не менее, за исключением отличающегося синтаксиса объявление метакласса в Python 2.Х и З.Х дает один и тот же результат, который мы обсудим далее.
Координирование метаклассов в Python З.Х и 2.Х
Когда конкретный метакласс объявлен в соответствии с синтаксисом, описанным в предшествующих разделах, вызов для создания объекта класс, выполняемый в конце оператора class, модифицируется так, чтобы обращаться к метаклассу, а не к type:
класс = Meta{имя_класса, суперклассы, словарь_атрибутов)
Из-за того, что метакласс является подклассом type, метод_call_класса type
делегирует вызовы для создания и инициализации нового объекта класс метаклассу, если в нем определены специальные версии следующих методов:
Meta._new_(Meta, имя_класса, суперклассы, словарь_атрибутов)
Meta._init_{класс, имя_класса, суперклассы, словарь_атрибутов)
В целях демонстрации ниже приведен пример из предыдущего разделах, дополненный спецификацией метаклассов Python З.Х:
class Spam (Eggs, metaclass=Meta) : # Наследуется от Eggs, экземпляр Meta
data = 1 # Атрибут данных класса
def meth (self, arg) : # Атрибут метода класса return self.data + arg
В конце этого оператора class интерпретатор Python внутренне запускает следующий код, чтобы создать объект класса — опять-таки вызов, который вы могли бы сделать вручную, но он автоматически выполняется механизмом оператора class:
Spam = Meta (' Spam', (Eggs,), {'data': 1, 'meth': meth, '_module_': '_main_'})
Если метакласс определяет собственные версии_new_и_init_, тогда они будут вызываться по очереди унаследованным из класса type методом_call_, чтобы
создать и инициализировать новый класс. Совокупный эффект заключается в том, что происходит автоматический запуск методов, предоставляемых метаклассом, как часть процесса создания класса. В следующем разделе показано, каким образом мы можем приступить к решению финальной части головоломки, связанной с метаклассами.
: В настоящей главе для метаклассов используется синтаксис с ключевыми
I аргументами Python З.Х, но не синтаксис с атрибутом класса Python 2.Х.
! Читателям, работающим с Python 2.Х, придется соответствующим обра
зом переводить код. Дело в том, что обеспечить нейтральность к версиям здесь непросто — Python З.Х не распознает атрибут класса, a Python 2.Х не допускает синтаксис с ключевыми аргументами — и указание для каждого примера двух листингов не решит проблему переносимости (зато увеличит размер главы!).
Реализация метаклассов
До сих пор мы видели, что Python направляет вызовы для создания классов метаклассу, если он указан и подготовлен. Однако как на самом деле мы будем реализовывать метакласс, который настраивает type?
Оказывается, что большая часть истории вам уже известна — метаклассы реализуются с помощью нормальных операторов class и семантики Python. По определению это просто классы, унаследованные от type. Существенные отличия заключаются лишь в том, что Python вызывает их автоматически в конце оператора class, и что они обязаны придерживаться интерфейса, ожидаемого суперклассом type.
Базовый метакласс
Возможно, самым простым метаклассом будет подкласс type с методом_new_,
который создает объект класса, запуская стандартную версию в type. Метод_new_
такого метакласса запускается методом_call_, унаследованным из type; он обычно выполняет любую требующуюся настройку и вызывает метод_new_суперкласса
type, чтобы создать и возвратить новый объект класса:
class Meta(type):
def _new_(meta, classname, supers, classdict):
# Запускается унаследованным методом type._call_
return type._new_(meta, classname, supers, classdict)
В действительности этот метакласс ничего не делает (мы могли бы также позволить создать класс стандартному классу type), но он демонстрирует способ перехвата привязки метакласса с целью настройки. Поскольку метакласс вызывается в конце оператора class, а метод_call_объекта type координируется для вызова методов _new_и_init_, предоставленный нами код в методах способен управлять
всеми классами, создаваемыми из метакласса.
Ниже снова приводится наш пример, где в метакласс и класс добавлены операторы вывода, предназначенные для отслеживания:
class MetaOne(type):
def _new_(meta, classname, supers, classdict):
print('In MetaOne.new:', meta, classname, supers, classdict, sep='\n...') return type._new_(meta, classname, supers, classdict)
class Eggs: pass
# создание класса
# Наследуется от Eggs, экземпляр MetaOne
# Атрибут данных класса
# Атрибут метода класса
# создание экземпляра
print('making class') class Spam(Eggs, metaclass=MetaOne): data = 1
def meth(self, arg):
return self.data + arg
print('making instance')
X = Spam()
print('data:', X.data, X.meth(2))
Здесь класс Spam наследуется от Eggs и является экземпляром MetaOne, но X — экземпляр Spam. При запуске кода в Python З.Х обратите внимание, что метакласс вызывается в конце оператора class перед созданием экземпляра — метаклассы предназначены для обработки классов, а классы ориентированы на обработку нормальных
экземпляров:
c:\code> ру -3 metaclass 1 .ру _
making class In MetaOne.new:
...<class '_main_.MetaOne'>
. . . Spam
...(<class ’_main_.Eggs'>,)
...{'data': 1, 'meth': <function Spam.meth at 0x02A191E0>, '_module_':
'_main_' }
making instance data: 1 3
Замечание по представлению. Ради экономии места в этой главе адреса приводятся в
сокращенном виде, а некоторые несущественные встроенные имена_X_в словарях
пространств имен опускаются. Кроме того, как отмечалось выше, из-за отличающегося синтаксиса объявления здесь не преследуется цель обеспечить переносимость в Python 2.Х. Для запуска в Python 2.Х применяйте форму с атрибутом класса и при желании измените операции вывода. Пример работает в Python 2.Х с показанными ниже модификациями (файл metaclassl-2x.py). Обратите внимание на то, что класс Eggs или Spam должен быть явно унаследован от object, иначе интерпретатор Python 2.Х выдаст предупреждение, т.к. класс нового стиля не может иметь только классические базовые классы — при наличии сомнений используйте object в клиентах метаклассов Python 2.Х:
from_future_ import print_function # Для Python 2.X (только)
class Eggs(object) : # Одно из указаний object необязательно
class Spam(Eggs, object):
_metaclass_ = MetaOne
Настройка создания и инициализации
Метаклассы также способны подключаться к протоколу_init_, запускаемому
методом_call_объекта type. В общем случае метод_new_создает и возвращает
объект класса, а метод_init_инициализирует уже созданный класс, передаваемый
в качестве аргумента. Метаклассы могут применять любую из двух или обе привязки для управления классом на стадии создания:
class MetaTwo(type):
def_new_(meta, classname, supers, classdict):
print('In MetaTwo.new: ', classname, supers, classdict, sep='\n...') return type._new_(meta, classname, supers, classdict)
def _init_(Class, classname, supers, classdict) :
print('In MetaTwo.init:1, classname, supers, classdict, sep='\n...') print(1 ...init class object:', list(Class._diet_,keys()))
class Eggs: pass
print('making class')
class Spam(Eggs, metaclass=MetaTwo) : # Наследуется от Eggs, экземпляр MetaTwo data = 1 # Атрибут данных класса
def meth (self, arg) : # Атрибут метода класса
return self.data + arg
print('making instance')
X = Spam()
print('data:', X.data, X.meth(2))
В данной ситуации метод инициализации класса запускается после метода создания класса, но оба метода вызываются в конце оператора class до создания любых экземпляров. И наоборот, метод_init_в Spam будет выполняться во время создания экземпляра, а метод_init_метакласса не затрагивает и не запускает его:
c:\code> ру -3 metaclass2.ру
making class In MetaTwo.new:
...Spam
...(cclass '_main_.Eggs'>,)
making instance data: 1 3
Другие методики реализации метаклассов
Хотя переопределение методов_new_и_init_суперкласса type являются
самым распространенным способом вставить логику внутрь процесса создания объектов классов с привязкой к метаклассу, возможны и другие схемы.
Использование простых фабричных функций
Например, в действительности метаклассы вообще не обязаны быть классами. Как уже известно, оператор class выдает простой вызов для создания класса в заключение его обработки. Из-за этого в качестве метакласса в принципе может применяться любой вызываемый объект при условии, что он принимает переданные аргументы и возвращает объект, совместимый с целевым классом. Фактически простая фабричная функция может справиться с задачей наравне с подклассом type:
# Простая функция тоже может служить в качестве метакласса
def MetaFunc(classname, supers, classdict):
print('In MetaFunc: ', classname, supers, classdict, sep='\n...') return type(classname, supers, classdict)
class Eggs: pass
print('making class')
class Spam(Eggs, metaclass=MetaFunc): # В конце запускается простая функция data =1 # Функция возвращает класс
def meth(self, arg):
return self.data + arg
print('making instance')
X = Spam()
print('data:', X.data, X.meth(2))
Функция MetaFunc вызывается в конце оператора class и возвращает ожидаемый новый объект класса. Она просто перехватывает вызов, который по умолчанию перехватывается методом_call_объекта type:
c:\code> ру -3 metaclass3.ру
making class In MetaFunc:
. ..Spam
... (<class '_main_.Eggsf>,)
...{'data': 1, 'meth': <function Spam.meth at 0x029471E0>, '_module_':
'_main_' }
making instance data: 1 3
Перегрузка операций вызова, создающих классы, с помощью нормальных классов
Поскольку экземпляры нормальных классов способны реагировать на операции вызова посредством перегрузки операций, они также могут исполнять некоторые роли метаклассов во многом подобно функции из предыдущего раздела. Вывод приведенной далее версии аналогичен выводу предшествующих версий, но она основана на простом классе, который вообще не наследуется от type и предоставляет своим экземплярам метод_call_, перехватывающий обращения к метаклассу с использованием обычной перегрузки операций. Обратите внимание, что методы_new_и
_init_должны здесь иметь отличающиеся имена, иначе они будут запускаться при
создании экземпляра Meta, а не когда позже он вызывается в роли метакласса:
# Экземпляр нормального класса тоже может служить метаклассом
class MetaObj:
def_call_(self, classname, supers, classdict):
print('In MetaObj.call: !, classname, supers, classdict, sep='\n...')
Class = self._New_(classname, supers, classdict)
self._Init_(Class, classname, supers, classdict)
return Class
def _New_(self, classname, supers, classdict):
print('In MetaObj.new: classname, supers, classdict, sep='\n... ') return type(classname, supers, classdict)
def _Init_(self, Class, classname, supers, classdict):
print('In MetaObj.init:', classname, supers, classdict, sep='\n...') print(f...init class object:', list(Class._diet_.keys()))
class Eggs: pass
print('making class')
class Spam(Eggs, metaclass=MetaObj () ) : # MetaObj - экземпляр нормального класса data =1 # Вызывается в конце оператора class
def meth(self, arg):
return self.data + arg
print('making instance')
X = Spam()
print('data:', X.data, X.meth(2))
Во время выполнения вызовы трех методов координируются через метод_call_
экземпляра, унаследованный из нормального класса, но без какой-либо зависимости от механизма или семантики type:
c:\code> ру -3 metaclass4.ру
making class In MetaObj.call:
. . .Spam
. . . (<class ’_main_.Eggs'>,)
In MetaObj.init:
...Spam
...(cclass '_main_.Eggs'>,)
...{'data': 1, 'meth': cfunction Spam.meth at 0x029492F0>, '_module_': 1 main_'}
...init class object: ['_module_'_doc_', 'data', '_qualname_', 'meth']
making instance data: 1 3
На самом деле в этой кодовой модели мы можем применять обычное наследование от суперклассов для получения метода перехвата вызовов — суперкласс здесь исполняет в точности ту же самую роль, что и type, во всяком случае, с точки зрения координирования метаклассов:
# Экземпляры нормально наследуют метод перехвата вызовов из классов
# и их суперклассов class SuperMetaObj:
def _call_(self, classname, supers, classdict):
print('In SuperMetaObj.call: ', classname, supers, classdict, sep='\n...')
Class = self._New_(classname, supers, classdict)
self._Init_(Class, classname, supers, classdict)
return Class
class SubMetaObj(SuperMetaObj):
def_New_(self, classname, supers, classdict):
print('In SubMetaObj.new: ', classname, supers, classdict, sep='\n...') return type(classname, supers, classdict)
def _Init_(self, Class, classname, supers, classdict):
print('In SubMetaObj.init: f, classname, supers, classdict, sep='\n...') print('...init class object:', list(Class._diet_.keys()))
class Spam (Eggs, metaclass=SubMetaObj () ) : # Обращается к экземпляру Sub
# через Super._call_
...остальной код не изменился...
c:\code> ру -3 metaclass4-super.ру
making class In SuperMetaObj.call:
. . .как и ранее...
In SubMetaObj.new:
...как и ранее...
In SubMetaObj.init:
...как и ранее... making instance data: 1 3
Несмотря на то что показанные альтернативные формы работоспособны, большинство метаклассов выполняют свою работу, переопределяя методы _new_ и
_init_суперкласса type; на практике такого объема контроля вполне достаточно,
и результирующий код зачастую получается проще, чем в других схемах. Кроме того, метаклассы имеют доступ к дополнительным инструментам, таким как методы классов, которые мы рассмотрим позже, и это может оказывать более прямое влияние на поведение классов, нежели ряд других схем.
Тем не менее, далее вы увидите, что простой метакласс, основанный на вызываемом объекте, часто способен работать во многом подобно декоратору классов, что позволяет метаклассам управлять не только классами, но и экземплярами. Однако сначала в следующем разделе будет представлен пример, демонстрирующий концепции распознавания имен метаклассами.
Перегрузка операций вызова, создающих классы, с помощью метаклассов
Поскольку метаклассы принимают участие в нормальных механизмах ООП, они также способны напрямую перехватывать операцию вызова, создающую класс в конце оператора class, за счет переопределения метода_call_объекта type. При
переопределении методов_new_и_call_важно не забывать о вызове их стандартных версий в type, если они предназначены для создания класса в конце, а метод _call_должен обращаться к type, чтобы запустить другие два метода:
# Классы тоже могут перехватывать вызовы (но встроенные
# операции ищутся в метаклассах, а не в суперклассах!)
class SuperMeta(type):
def _call_(meta, classname, supers, classdict):
print('In SuperMeta.call: classname, supers, classdict, sep='\n...’) return type._call_(meta, classname, supers, classdict)
def _init_(Class, classname, supers, classdict):
print('In SuperMeta init:', classname, supers, classdict, sep='\n...') print('...init class object:', list(Class._diet_.keys ()))
print('making metaclass1)
class SubMeta(type, metaclass=SuperMeta):
def _new_(meta, classname, supers, classdict):
print('In SubMeta.new: ', classname, supers, classdict, sep='\n...') return type._new_(meta, classname, supers, classdict)
def_init_(Class, classname, supers, classdict):
print('In SubMeta init:', classname, supers, classdict, sep='\n...') print('...init class object:', list(Class._diet_.keys ()))
class Eggs: pass
print('making class')
class Spam(Eggs, metaclass=SubMeta): # Обращается к SubMeta
# через SuperMeta._call_
data = 1
def meth(self, arg) :
return self.data + arg
print('making instance')
X = Spam()
print('data:', X.data, X.meth(2))
В коде присутствует несколько странностей, которые вскоре будут объяснены. Тем не менее, во время выполнения все три переопределенных метода по очереди запускаются для Spam, как было в предыдущем разделе. По существу это снова то, что по умолчанию делает объект type, но имеется дополнительный вызов метакласса для подкласса метакласса (метаподкласса?):
c:\code> ру -3 metaclass5.ру
making metaclass In SuperMeta init:
...SubMeta ... (<class 'type 1>,)
...{'_init_*: <function SubMeta._init_at 0x028F92F0>, ...}
...init class object: ['_doc_', '_module_', '_new_', ’_init_, ...]
making class In SuperMeta.call:
...Spam
... (<class '_main_.Eggs'>,)
...init class object: ['_qualname_', '_module_', '_doc_', 'data', 'meth']
making instance data: 1 3
Пример усложняется тем фактом, что в нем переопределяется метод, вызываемый встроенной операцией — в данном случае вызов запускается автоматически для создания класса. Метаклассы используются для создания объектов классов, но при вызове в роли метаклассов лишь генерируют экземпляры самих себя. По указанной причине поиск имен при наличии метаклассов может несколько отличаться о того, к чему мы
привыкли. Скажем, метод_call_ищется встроенными операциями в классе (т.е.
типе) объекта; для метаклассов это означает метакласс метакласса!
Далее будет показано, что метаклассы также нормально наследуют имена из других метаклассов, но, как и в случае обычных классов, похоже, это применимо только к явным извлечениям имен, а не к неявному поиску имен для встроенных операций наподобие вызовов. Последнее выглядит как просмотр класса метакласса, доступного в
его ссылке_class_, которой будет либо стандартный type, либо метакласс. Здесь
возникает та же самая проблема координирования встроенных операций, которую мы часто встречали в книге при работе с экземплярами нормальных классов. Для установки такой ссылки требуется ключевой аргумент metaclass в SubMeta, хотя он также инициирует шаг создания метакласса для самого метакласса.
Проследите все вызовы в выводе. Метод_call_из SuperMeta не запускается для
вызова SuperMeta при создании SubMeta (взамен это направляется type), но запускается для вызова SubMeta при создании Spam. Обычного наследования от SuperMeta не будет достаточно для перехвата вызовов SubMeta, и по причинам, которые мы увидим позже, поступать так с методами перегрузки операций на самом деле неправильно: затем Spam получает метод_call_из SuperMeta, приводя к тому, что вызовы
для создания экземпляров Spam потерпят неудачу до того, как будет создан хоть какой-нибудь экземпляр. Тонко, но верно!
Вот иллюстрация проблемы в более простых терминах — нормальный суперкласс пропускается для встроенных имен, но не для явных извлечений и вызовов; последние полагаются на обычное наследование имен атрибутов:
class SuperMeta(type):
def _call_(meta, classname, supers, classdict): # По имени,
# не встроенное
print('In SuperMeta.call:', classname)
return type._call_(meta, classname, supers, classdict)
class SubMeta (SuperMeta) : # Создается стандартным type
def _init_(Class, classname, supers, classdict): # Переопределение
# type._init_
print('In SubMeta init:', classname) print(SubMeta._class_)
print ( [n._name_ for n in SubMeta._mro_])
print()
print (SubMeta._call_) # He дескриптор данных, если найден по имени
print()
SubMeta._call_(SubMeta, 'ххх', (), {}) # Явные вызовы работают:
# наследование классов
print()
SubMeta ('ууу',(),{}) # Но неявные обращения к встроенным именам
# не работают: type
c:\code> ру -3 metaclass5b.py
<class 'type'>
['SubMeta', 'SuperMeta', 'type', 'obj ect']
<function SuperMeta._call_ at 0x029B9158>
In SuperMeta.call: xxx In SubMeta init: xxx
In SubMeta init: yyy
Разумеется, рассмотренный конкретный пример является особым случаем: перехват встроенной операции, выполняемой на метаклассе, вероятно, будет тем редким
сценарием использования, связанным с_call_. Но это подчеркивает основную
асимметрию и явную противоречивость: нормальное наследование атрибутов не задейству-ется в полной мере при координировании встроенных операций — как для экземпляров, так и для классов.
Однако чтобы по-настоящему понять тонкости приведенного выше примера, необходимо получить более формальное представление о том, что метаклассы означают для распознавания имен Python в целом.
Наследование и экземпляр
Поскольку метаклассы указываются способами, похожими на указание наследуемых суперклассов, поначалу они могут слегка сбивать с толку. Описанные ниже ключевые моменты помогут подытожить и прояснить модель.
Метаклассы наследуются от класса type (обычно)
Несмотря на то что метаклассы исполняют специальную роль, они реализуются посредством операторов class и следуют обычной модели ООП в Python. Например, будучи подклассами type, они могут переопределять методы объекта type, настраивая их должным образом. Метаклассы, как правило, переопределяют методы_new_и_init_класса type для настройки создания и
инициализации классов. Хотя и реже, они могут переопределять также метод _call_, если требуется напрямую перехватывать вызов создания класса в конце (пусть и со сложностями, изложенными в предыдущем разделе). Метаклассы могут даже быть простыми функциями или другими вызываемыми объектами, возвращающими произвольные объекты, а не подклассами type.
Объявления метаклассов наследуется подклассами
Объявление metaclass=MeTawiacc в определяемом пользователем классе наследуется его нормальными подклассами, так что метакласс будет запускаться для создания каждого класса, который наследует эту спецификацию в цепочке наследования суперклассов.
Атрибуты метаклассов не наследуются экземплярами классов
Объявления метаклассов указывают отношение между экземплярами, которое отличается от того, что мы называли наследованием до сих пор. Поскольку классы являются экземплярами метаклассов, определяемое метаклассом поведение применяется к классу, но не к создаваемым впоследствии экземплярам класса. Экземпляры получают поведение от своих классов и суперклассов, но не от метаклассов. Формально процедура наследования атрибутов для обычных экземпляров выполняет поиск только в словарях_diet_экземпляра, его класса и всех суперклассов класса; для обычных экземпляров метаклассы в поиск при наследовании не включаются.
Атрибуты метаклассов получаются классами
Напротив, классы получают методы своих метаклассов благодаря отношению между экземплярами. Это источник поведения классов, которое обрабатывает сами классы. Формально классы обзаводятся атрибутами метаклассов посредством своих ссылок_class_в точности, как нормальные экземпляры получают имена от своих классов, но сначала предпринимается попытка наследования
через поиск в_diet_: когда одно и то же имя доступно классу в метаклассе
и в суперклассе, то используется версия из суперкласса (через наследование), а
не из метакласса (через экземпляр). Тем не менее, атрибут_class_класса не
следует в его собственные экземпляры: атрибуты метаклассов делаются доступными их классам-экземплярам, но не экземплярам этих классов-экземпляров (тут снова уместно сослаться на Доктора Сьюза...).
Возможно, перечисленные выше моменты будет легче понять, написав код. В целях демонстрации рассмотрим такой пример:
# Файл metainstance.ру
class MetaOne(type):
def _new_(meta, classname, supers, classdict): if Переопределение
# метода type
print('In MetaOne.new:’, classname)
return type._new_(meta, classname, supers, classdict)
def toast(self): return 'toast'
class Super(metaclass=MetaOne): # Метакласс наследуется также и подклассами def spam (self) : # MetaOne запускается дважды для двух классов return 'spam'
class Sub (Super) : # Суперкласс: наследование или отношение между экземплярами def eggs (self) : # Классы наследуют атрибуты от суперклассов return 'eggs' # Но не от метаклассов
Когда такой код запускается (как сценарий или модуль), метакласс обрабатывает создание для обоих клиентских классов, а экземпляры наследуют атрибуты класса, но не атрибуты метакласса:
>>> from metainstance import * # Выполняются операторы class:
# метакласс запускается дважды
In MetaOne.new: Super In MetaOne.new: Sub
>>> X = Sub() # Нормальный экземпляр класса, определяемого пользователем
>» X.eggs () # Унаследован из Sub
'eggs'
>>> X.spam() # Унаследован из Super
'spam'
>>> X. toast() # Не наследуется из метакласса
AttributeError: 'Sub' object has no attribute 'toast*
Ошибка атрибута: объект Sub не имеет атрибута toast
В противоположность этому кллссы наследуют имена от своих суперклассов и обзаводятся именами из метакласса (который в данном примере сам унаследован из суперкласса):
>>> Sub.eggs (X) # Собственный метод 'eggs 1
>» Sub. spam(X) # Унаследован из Super 'spam'
>>> Sub. toast () # Получен из метакласса 'toast'
>>> Sub.toast(X) # He метод нормального класса
TypeError: toast() takes 1 positional argument but 2 were given
Ошибка типа: toast() принимает 1 позиционный аргумент, но было передано 2
Обратите внимание, что последний из предшествующих вызовов терпит неудачу, когда мы передаем экземпляр, поскольку имя распознается как метод метакласса, а не метод нормального класса. Фактически и объект, из которого извлекается имя, и его источник становятся здесь решающими. Методы, полученные из метакласса, привязываются к целевому классу, в то время как методы из нормальных классов являются несвязанными, если извлекаются через класс, но связанными, когда извлекаются через экземпляр:
>>> Sub.toast
cbound method MetaOne.toast of <class 'metainstance.Sub'>>
>>> Sub.spam
<function Super.spam at 0x0298A2F0>
>>> X.spam
cbound method Sub.spam of cmetainstance.Sub object at 0x02987438»
Последние два правила изучались ранее в главе 31 при рассмотрении связанных методов; первое правило новое, но напоминает одно из правил для методов класса. Чтобы понять, почему все работает именно так, нам необходимо заняться исследованием также отношения между экземплярами.
Метакласс или суперкласс
Давайте обратимся к более доступной форме. Взгляните, что происходит в показанном ниже взаимодействии: будучи экземпляром метакласса А, класс В получает атрибут из А, но этот атрибут не делается доступным для наследования собственными экземплярами класса В — получение имен экземплярами метакласса отличается от нормального наследования, применяемого для экземпляров класса:
>» class A (type) : attr = 1
>» class В(metaclass=A) : pass # В - экземпляр метакласса и получает
# атрибут attr из метакласса
»> I = В () # I наследует атрибуты из класса, но не из метакласса!
»> В.attr 1
»> I.attr
AttributeError: 'В' object has no attribute 'attr'
Ошибка атрибута: объект В не имеет атрибута attr »> 'attr' in В. diet_, 'attr' in A._diet_
(False, True)
По контрасту с этим, если трансформировать А из метакласса в суперкласс, тогда имена, унаследованные из суперкласса А, становятся доступными для создаваемых позже экземпляров класса В и обнаруживаются путем поиска внутри словарей пространств имен в классах в дереве — т.е. за счет проверки_diet_объектов в порядке распознавания методов (MRO), что очень похоже на пример mapattrs .ру из главы 32:
»> class A: attr = 1
>>> class В (А) : pass # I наследует атрибуты из класса и суперклассов
>» I = в()
»> В.attr
1
»> I.attr
1
>>> 'attr' in В._diet_, 'attr' in A. diet_
(False, True)
Вот почему метаклассы часто выполняют свою работу, манипулируя словарем пространств имен нового класса, если им нужно повлиять на поведение последующих объектов экземпляров — экземпляры будут видеть имена в классе, но не в его метаклассе. Однако посмотрите, что происходит, если идентичное имя доступно в обоих источниках атрибутов — вместо имени, полученного из экземпляра, используется унаследованное имя:
>>> class M(type) : attr = 1 >>> class A: attr = 2
>» class В (A, metaclass=M) : pass # Суперклассы имеют приоритет
# над метаклассами
»> I = В()
>>> В.attr, I.attr
(2, 2)
>>> 'attr' in В._diet_, 'attr' in A._diet_, 'attr' in M._diet_
(False, True, True)
Это верно независимо от относительной высоты источников наследования и экземпляров — интерпретатор Python проверяет_diet_каждого класса в порядке
MRO (наследование), прежде чем переходить к получению из экземпляра (отношение между экземплярами):
>>> class M(type): attr = 1 >>> class A: attr = 2 >>> class В (A) : pass
»> class C(B, metaclass=M) : pass # Суперкласс на два уровня выше метакласса:
# все равно выигрывает
»> I = С()
# Сведения MRO ищите в главе 32
]
mro
>>> I.attr, С.attr
(2, 2)
»> [x._паше for x in C._
['C', 'B\ 'A', 'object'] "
В действительности классы получают атрибуты метаклассов через свои ссылки
_class_тем же самым способом, каким нормальные экземпляры наследуют их из
классов через свои атрибуты_class_, что имеет смысл, поскольку классы также являются экземплярами метаклассов. Главное отличие заключается в том, что наследование экземпляра не проходит по ссылке_class_класса, но взамен ограничивает
свой охват словарем_diet_каждого класса в дереве согласно MRO — следуя только
_bases_на уровне каждого класса и применяя ссылку_class_экземпляра только раз:
>>> I._class_
<class ' main .С'>
>>> С._bases_
(<class '_main_.B'>,;
>>> С. class
main .M'>
<class >» С 1
class .attr
# Следует наследованию: класс экземпляра
# Следует наследованию: суперклассы класса
# Следует получению из экземпляра: метакласс
# Еще один способ добраться до атрибутов метакласса
После изучения всего того, что приводилось выше, возможно вы заметите почти явную симметрию, которая подводит нас к теме следующего раздела.
Наследование: вся история
Как выясняется, наследование экземпляра работает аналогично независимо от того, создан “экземпляр” из нормального класса или представляет собой класс, созданный из метакласса, являющегося подклассом type. Такое единственное правило поиска атрибутов благоприятствует более широкому и похожему понятию иерархий наследования метаклассов. Для иллюстрации основ этого концептуального объединения в приведенном ниже взаимодействии экземпляр наследует атрибуты из всех своих классов, класс — из классов и метаклассов, а метаклассы — из более высоких метаклассов (суперметаклассов?):
# Дерево наследования метаклассов
>>> class Ml(type): attrl = 1 >>> class М2 (Ml) : attr2 = 2
# Получает имена_bases_, _
class
mro
»> class Cl: attr3 =3 # Дерево наследования суперклассов
»> class C2 (Cl,metaclass=M2) : attr4 = 4 # Получает имена_bases_,
# class , mro
»> I = C2()
# I получает_class_, но не остальные имена
# Экземпляр наследует имена из дерева суперклассов
>>> I.attr3, I.attr4
(3, 4)
>» C2.attrl, C2.attr2, C2.attr3, C2.attr4
(1, 2, 3, 4)
# Метакласс тоже наследует имена!
»> М2.attrl, M2.attr2
(1, 2)
# Класс получает имена
# из обоих деревьев !
Оба пути наследования — из класса и из метакласса — задействуют те же самые ссылки, хотя не рекурсивно; экземпляры не наследуют имена метакласса своего класса, но могут запросить их явно:
>>> I._class__# Ссылки следуют на экземпляр без_bases_
cclass '_main_.С2'>
>» С2._bases_
(cclass '_main_.Cl'>,)
>>> C2._class__# Ссылки следуют на класс после_bases_
cclass '_main_.M2’>
>>> М2._bases_
(cclass '_main_.Ml’>,)
>>> I._class_.attrl # Направление наследования на дерево
# метаклассов класса
1
»> I.attrl # Хотя_class_ класса нормально не проходится
AttributeError: ’С2' object has no attribute 'attrl'
Ошибка атрибута: объект С2 не имеет атрибута attrl
»> М2._class__# Оба дерева имеют MRO и ссылки на экземпляры
cclass Чуре'>
»> [х._паше for х in С2. mro_] # Дерево_bases_ из I._class_
['С2', 'Cl?, 'object']
>» [х._name for х in М2._mro_] # Дерево_bases_ из С2._class_
['М2', 'Ml', 'type', 'object']
Если вас интересуют метаклассы или вы должны использовать код с ними, тогда изучите предложенные примеры еще раз. В сущности, наследование следует по
_bases_перед переходом к единственному_class_, нормальные экземпляры
не имеют_bases_, а классы имеют то и другое — нормальные они или метаклассы.
На самом деле этот пример важно понять, чтобы освоить распознавание имен Python в целом, как объясняется в следующем разделе.
Алгоритм наследования Python: простая версия
Теперь, когда вам известно о метаклассах, мы в состоянии окончательно формализовать правила наследования, которые они дополняют. Формально наследование вводит в действие две разных, но похожих процедуры поиска, и основано на MRO. Поскольку_bases_применяется для создания упорядочения_mro_во время создания классов, а_mro_класса включает самого себя, обобщение из предыдущего
раздела будет таким же, как приведенное далее начальное определение алгоритма наследования нового стиля Python.
Для поиска явного имени атрибута выполнить описанные ниже шаги.
1. Начиная с экземпляра I, провести поиск в экземпляре, затем в его классе и далее во всех суперклассах класса, используя:
а) словарь_diet_экземпляра I;
б) словари_diet_всех классов в_mro_, найденном в_class_экземпляра I, слева направо.
2. Начиная с класса С, провести поиск в классе, затем во всех его суперклассах и далее в его дереве метаклассов, используя:
а) словари_diet_всех классов в_mro_, найденном в самом классе С, слева
направо;
б) словари_diet_всех метаклассов в_mro_, найденном в_class_класса С, слева направо.
3. В шагах 1 и 2 предоставить приоритет дескрипторам данных, которые найдены в источниках в пункте б) (см. далее).
4. Для встроенных имен в шагах 1 и 2 пропустить пункт а) и начать поиск с пункта
б) (см. далее).
Первые два шага выполняются только для обычного явного извлечения атрибутов. Предусмотрены исключения для встроенных имен и дескрипторов, которые вскоре будут прояснены. Вдобавок, как объяснялось в главе 38, для отсутствующих или всех имен
может также применяться метод_getattr_или_getattribute_.
Большинству программистов нужно знать лишь первое из этих правил и возможно первый шаг второго, которые вместе соответствуют наследованию классишских классов Python 2.Х. Для метаклассов добавлен дополнительный шаг (26), но по существу он такой же, как остальные — безусловно, довольно тонкая равнозначность, но метаклассы не настолько новы, как может показаться. Фактически они — всего лишь один компонент более крупной модели.
Особый случай для дескрипторов
По крайней мере, это нормальный — и упрощенный — случай. В предыдущем разделе я специально выделил шаг 3, т.к. он не применяется к большей части кода и значительно усложняет алгоритм. Тем не менее, оказывается, что в наследовании также предусмотрен особый случай взаимодействия с дескрипторами атрибутов, описанными в главе 38. Если кратко, то дескрипторы, известные как дескрипторы данных (те,
которые определяют методы_set_для перехвата операций присваивания) имеют
приоритет, так что их имена переопределяют другие источники наследования.
Такое исключение служит нескольким практическим целям. Например, оно используется для гарантии того, что специальные атрибуты_class_и_diet_не могут
быть переопределены теми же самыми именами в собственном словаре_diet_экземпляра:
>» class С: pass # Особый случай наследования #2. . .
>>> I = С() # Дескрипторы данных класса имеют приоритет »> I._class_, I. diet_
(cclass '_main_.О, {})
>>> I. diet_['name1] = 'bob' # Динамические данные в экземпляре
>>> I._diet_[’_class_’] = ’spam1 # Присваивание ключам, не атрибутам
»> I. diet_[’_diet_'] = {}
>>> I.name # I.name поступает из I._diet_, как обычно
'bob' # Ho I._class_ и I._diet_ - нет!
»> I._class_, I._diet_
(cclass 1_main_.C'>, {'_class_’spam', '_diet_': {}, 'name1: 'bob'})
Исключительная ситуация с дескрипторами данных проверяется в качестве предварительного шага перед предшествующими двумя правилами наследования. Она более важна для разработчиков интерпретатора Python, чем для программистов на Python, и так или иначе может быть проигнорирована в большинстве прикладного кода — если только вы сами не реализуете собственные дескрипторы данных, которые следуют тому же правилу приоритета для особого случая наследования:
>>> class D:
def get_(self, instance, owner): print('_get_')
def_set_(self, instance, value): print('_set_')
>>> class С: d = D() # Атрибут дескриптора данных
»> I = С()
>>> I.d # Доступ к унаследованному дескриптору данных
_get_
»> I.d = 1 _set_
>>> I._diet_['d* ] = 'spam* # Определение того же имени в словаре
# пространств имен экземпляра
»> I.d # Но оно не скрывает дескриптор данных в классе! _get_
И наоборот, если этот дескриптор ^определяет_set_, то имя в словаре экземпляра скроет имя в классе согласно нормальному наследованию:
>» class D:
def_get_(self, instance, owner): print('_get_')
>>> class C: d = D()
»> I = C()
>>> I.d # Доступ к унаследованному дескриптору не данных _get_
>>> I._diet_['d'] = 'spam' # Скрывает имена в классе согласно правилам
# нормального наследования
»> I.d
'spam'
В обоих случаях интерпретатор Python автоматически запускает метод_get_
дескриптора, когда он находится по наследованию, а не возвращает сам объект дескриптора — часть магии, относящейся к атрибутам, с которой мы сталкивались ранее в книге. Однако особый статус, предоставляемый дескрипторам данных, также изменяет смысл наследования атрибутов и соответственно смысл имен в вашем коде.
Алгоритм наследования Python: чуть более полная версия
С учетом особого случая дескрипторов данных и общего вызова дескрипторов, разложенного на деревья классов и метаклассов, полный алгоритм наследования нового стиля Python может быть сформулирован в следующем виде. Это сложная процедура, которая предполагает знание дескрипторов, метаклассов и MRO, но все-таки является финальным арбитром при распознавании имен атрибутов (приведенные далее шаги предпринимаются один за другим в соответствии с нумерацией или согласно их порядку слева направо в объединениях “или”).
Для поиска явного имени атрибута выполнить описанные ниже шаги.
1. Начиная с экземпляра I, провести поиск в экземпляре, в его классе и в суперклассах класса следующим образом.
а) Искать в словарях_diet_всех классов в_mro_, найденном в_class_
экземпляра I.
б) Если на шаге а) был найден дескриптор данных, тогда вызвать его метод _get_и завершить работу.
в) Иначе возвратить значение в словаре_diet_экземпляра I.
г) Иначе вызвать дескриптор не данных или возвратить значение, найденное на шаге а).
2. Начиная с класса С, провести поиск в классе, в его суперклассах и в его дереве метаклассов следующим образом.
а) Искать в словарях _diet__всех метаклассов в _mro_, найденном в
_class_класса С.
б) Если на шаге а) был найден дескриптор данных, тогда вызвать его метод _get_и завершить работу.
в) Иначе вызвать дескриптор данных или возвратить значение в словаре _diet_класса из собственного_mro_класса С.
г) Иначе вызвать дескриптор не данных или возвратить значение, найденное на шаге а).
3. В шагах 1 и 2 встроенные имена по существу используют только источники в пунктах а) (см. далее).
Снова обратите внимание здесь на то, что алгоритм применим только к обычному явному извлечению атрибутов. Неявный поиск имен методов для встроенных имен не соблюдает описанные выше правила и по существу в обоих случаях использует только источники из шагов а), что будет демонстрироваться в следующем разделе.
Как всегда, подразумеваемый суперкласс object предоставляет ряд стандартных методов на верхушке каждого дерева классов и метаклассов (т.е. в конце каждой последовательности MRO). И помимо всего этого может быть запущен метод_getattr_
(если определен), когда атрибут не найден, и метод_getattribute_для каждой
операции извлечения атрибутов, хотя они являются расширениями модели поиска имен, предназначенными для особых случаев. В главе 38 приводилась дополнительная информация об указанных инструментах и дескрипторах, а в главе 32 рассматривался особый случай просмотра MRO для super.
Наследование присваивания
Также обратите внимание, что в предыдущем разделе наследование определялось в терминах ссылки на атрибут (поиск), но его части применимы также и к присваиванию атрибута. Как уже известно, присваивание обычно изменяет значения атрибутов в самом целевом объекте, но процедура наследования также инициируется с целью первоначальной проверки во время присваивания для ряда инструментов управления атрибутами, рассмотренных в главе 38, включая дескрипторы и свойства. Когда такие инструменты присутствуют, они перехватывают операции присваивания атрибутов и могут произвольно направлять их.
Скажем, при выполнении присваивания атрибутов для классов нового стиля дескриптор данных с методом_set_получается из класса по наследованию с использованием MRO и имеет приоритет перед нормальной моделью хранения. В переводе на язык правил из предыдущего раздела:
• когда такие операции присваивания применяются к экземпляру, они по существу следуют шагам а)-в) правила 1, выполняя поиск в дереве классов экземпляра, хотя на шаге б) вместо_get_вызывается_set_, а на шаге в) работа завершается и взамен попытки извлечения производится сохранение в экземпляре;
• когда такие операции присваивания применяются к классу, они запускают такую же процедуру в отношении дерева метаклассов класса: примерно то же самое, что и правило 2, но на шаге в) работа завершается и происходит сохранение в классе.
Поскольку дескрипторы являются основой для других расширенных инструментов управления атрибутами, таких как свойства и слоты, предварительная проверка со стороны процедуры наследования при присваивании задействуется в многочисленных контекстах. Совокупный эффект заключается в том, что в классах нового стиля дескрипторы трактуются как особый случай наследования в случае ссылки и присваивания.
Особый случай для встроенных имен
Итак, мы рассмотрели почти всю историю. Как выяснилось, встроенные имена не следуют описанным выше правилам. Для встроенных имен экземпляры и классы могут пропускаться, что представляет собой особый случай, который отличается от нормального или явного наследования имен. Из-за того, что это расхождение, специфичное к контексту, его легче продемонстрировать в коде, чем вплести в единственный алгоритм. В следующем взаимодействии str является встроенным именем,_str_— эквивалентом в виде явного имени и экземпляр пропускается только для встроенного имени:
>>> class С: # Особый случай наследования #2. . .
attr = 1 # Для встроенных имен пропускается шаг def_str_(self): return(’class')
»> I = C()
>>> I._str_0 , str (I) # Оба имени из класса, если нет в экземпляре
('class ', 'class 1)
>>> I._str_= lambda: 'instance'
>>> I._str_0 , str (I) # Явное=>экземпляр, встроенное=>класс!
('instance', 'class’)
>>> I.attr # Асимметрично с нормальными или явными именами
1
»> I.attr = 2; I.attr
2
Ранее в файле metaclassb .ру было показано, что то же самое остается верным для классов: поиск явных имен начинается с класса, но встроенных — с класса для класса, который является его метаклассом и по умолчанию принимается как type:
>>> class D(type):
def_str_(self): return('D class')
>>> class C(D): pass
»> C._str_(C) , str(C) # Явное=>суперкласс, встроенное=>метакласс!
('D class', "cclass '_main_.C'>")
»> class С (D) :
def_str_(self) : retum('C class')
>>> C._str_(C) , str(C) # Явное=>класс, встроенное=>метакласс!
('Cclass', "cclass '_main_.C'>")
>» class С (metaclass=D) :
def_str_(self) : return('C class')
>» C._str_(C) , str(C) # Встроенное=>метакласс, определяемый пользователем
('С class ', 'D class ')
На самом деле иногда бывает нелегко узнать, откуда поступает имя в такой модели, потому что все классы также наследуются от object — в том числе стандартный метакласс type. В следующем явном вызове класс С, по-видимому, получает стандартный метод_str_из object, а не из метакласса, согласно первому источнику наследования классов (собственного MRO класса); наоборот, встроенное имя делает пропуск вперед до метакласса, как и ранее:
>>> class С(metaclass=D): pass
»> С._str_(С), str(C) # Явное=>object, встроенное=>метакласс
("<class 1_main_.C'>", 'D class')
»> C._str_
<slot wrapper '_str_' of 'object' objects>
»> for к in (С, C._class_, type) : print([x,_name_for x in k. mro_])
[1 С', 'object']
[1D', 'type', 'object']
['type', 'object']
Все изложенное подводит нас к финальной цитате из import this — принципу, похоже, конфликтующему со статусом, который предоставлен дескрипторам и встроенным именам в механизме наследования атрибутов, относящемся к классам нового стиля:
Особые случаи не настолько особенные, чтобы нарушать правила.
Разумеется, некоторые практические нужды служат основанием для исключений. Мы здесь воздержимся от обоснований, но вы обязаны тщательно обдумать последствия объектно-ориентированного языка, который применяет наследование — свою фундаментальную операцию — в такой непрямой и противоречивой манере. По меньшей мере, это должно подчеркнуть важность сохранения вашего кода простым, чтобы не делать его зависимым от подобных запутанных правил. Как всегда, пользователи вашего кода и программисты, сопровождающие его, будут только счастливы.
Чтобы получить более достоверные сведения, просмотрите внутреннюю реализацию наследования в Python — полную историю вы найдете в файлах object, с и typeobj ect. с, первый из которых предназначен для нормальных экземпляров, а второй для классов. Конечно, использование Python не требует глубокого погружения в его внутреннее устройство, но это основной источник истины в сложной и развивающейся системе, а временами наилучший из того, что вы сумеете найти. Сказанное особенно справедливо в граничных ситуациях, порожденных накопленными исключениями. Давайте теперь перейдем к последнему фрагменту магии, связанной с метаклассами.
Методы метаклассов
Будучи столь же важными, как наследование имен, методы в метаклассах обрабатывают их классы-экземпляры — не обычные объекты экземпляров, известные нам как self, а сами классы. В результате методы метаклассов становятся похожими по духу и форме на методы классов, исследованные в главе 32, хотя опять-таки они доступны только в области экземпляров метаклассов, не при нормальном наследовании экземпляров. Например, неудача в конце следующего взаимодействия является результатом действия правил для наследования явных имен из предыдущего раздела:
»> class A (type) :
def x(cls): print('ax', els) # Метакласс (экземпляры=классы) def у (els) : print ('ay' , els) # у переопределяется экземпляром В
»> class В (metaclass=A) :
def у (self) : print('by' , self) # Нормальный класс
# (нормальные экземпляры) def z(self) : print('bz' , self) # Словарь пространств имен хранит у и z
»> В.х # х получается из метакласса
<bound method А.х of cclass '_main_.В'»
>>> В.у # у и z определены в самом классе cfunction В.у at 0x0295FlE0>
»> В.2
cfunction B.z at 0x0295F378>
»> B.x() # Вызов метода метакласса: получает класс
ах <class '_main_.В’>
>>> I = В() # Вызовы методов экземпляра: получают экземпляр
»> 1.у()
by с_main_.В object at 0х02963ВЕ0>
»> I.z()
bz с_main_.В object at 0x02963BE0>
>» I.x() # Экземпляр не видит имена метакласса
AttributeError: 'В' object has no attribute ' x'
Ошибка атрибута: объект В не имеет атрибута х
Методы метаклассов или методы классов
Несмотря на отличие в видимости наследованию, во многом подобно методам классов методы метаклассов предназначены для управления данными уровня классов. На самом деле их роли могут частично совпадать (почти как в целом у метаклассов и декораторов классов), но методы метаклассов доступны исключительно через класс и для привязки к классу не требуют явного объявления classmethod на уровне класса. Другими словами, методы метаклассов можно воспринимать как неявные методы классов с ограниченной видимостью:
>» class A(type) :
def a (els) : # Метод метакласса : получает класс
cls.x = els. у + cls.z
»> class В (metaclass=A) :
Yf 2 = 11, 22
@classmethod # Метод класса: получает класс
def b(cls) :
return els .х
>>> В.a() # Вызов метода метакласса; является видимым только классу
»> В.х # Создает данные класса в В, доступные нормальным экземплярам
33
»> I = в()
>» I .х, I .у, I. z
(33, 11, 22)
>>> I.b() # Метод класса: передается класс, не экземпляр;
# является видимым экземпляру
33
>>> 1.а() # Методы метакласса: доступны только через класс
AttributeError: 'В' object has no attribute 'a'
Ошибка атрибута: объект В не имеет атрибута а
Перегрузка операций в методах метакласса
Точно так же, как нормальные классы, метаклассы могут задействовать перегрузку операций, чтобы сделать встроенные операции применимыми к их классам-экземплярам. Например, метод индексирования_getitem_в показанном ниже метаклассе
представляет собой метод метакласса, который предназначен для обработки самих
классов — т.е. классов, являющихся экземплярами метакласса, а не экземпляров этих классов, создаваемых впоследствии. Надо сказать, что согласно обрисованным ранее алгоритмам наследования нормальные экземпляры классов вообще не наследуют имена, полученные через отношение экземпляра метакласса, хотя они могут обращаться к именам, присутствующим в их собственных классах:
>» class A(type) :
def_getitem_(els, i) : # Метод метакласса для обработки классов:
return els.data[i] # Встроенные операции пропускают класс,
# используют метакласс
# Явные имена инициируют поиск в классе и метаклассе »> class В (metaclass=A) : # Сначала используются дескрипторы данных
# в метаклассе
data = ' spam ’
>>> В[0] # Имена экземпляра метакласса: видимы только классу
' s'
>>> В._getitem_
<bound method А._getitem_ of <class '_main_.B'>>
»> I = B()
>>> I.data, B.data # Имена, полученные нормальным наследованием:
# видимы экземпляру и классу
('spam', 'spam')
»> I[0]
TypeError: 'В' object does not support indexing Ошибка типа: объект В не поддерживает индексирование
В метаклассе также допускается определять метод_getattr_, но его можно использовать для обработки только классов-экземпляров, а не нормальных экземпляров этих классов — как обычно, он даже не будет получен экземплярами класса:
>» class A (type) :
def_getattr_(els, name) : # Получается getitem класса В
return getattr (els .data, name) # Но не выполняется одинаково
# для встроенных операций
»> class В (metaclass=A) : data = 'spam'
>>> В.upper()
'SPAM'
>>> В.upper
<built-in method upper of str object at 0x029E7420>
>>> B._getattr_
cbound method A._getattr_ of <class ’_main_.B'»
»> I = B()
>>> I.upper
AttributeError: 'В' object has no attribute ’upper'
Ошибка атрибута: объект В не имеет атрибута upper »> I._getattr_
AttributeError: 'В' object has no attribute '_getattr_'
Ошибка атрибута: объект В не имеет атрибута_getattr_
Тем не менее, перенос метода_getattr_в метакласс не помогает справиться с
проблемой отсутствия в нем перехвата встроенных операций. В приведенном далее продолжении явные указываемые атрибуты направляются методу_getattr_метакласса, но встроенные операции — нет, вопреки тому факту, что в первом примере данного раздела операция индексирования направлялась методу_getitem_метакласса. Это наводит на мысль, что_getattr_нового стиля является особым случаем
особого случая, и дальнейшее рекомендуемое упрощение кода избегает зависимости от таких граничных случаев:
»> В.data = [1, 2, 3]
»> В.append(4) # Явно указанные нормальные имена направляются getattr метакласса
»> В.data
[1/ 2, 3, 4]
>>> В._getitem (0) # Явно указанные особые имена
# направляются getattr метакласса
1
»> В[0] # Но встроенные операции тоже пропускают getattr метакласса?!
TypeError: 'A' object does not support indexing
Ошибка типа: объект А не поддерживает индексирование
Вероятно, вы уже можете признать, что метаклассы интересно исследовать, но довольно легко утратить представляемую ими общую картину. Ради экономии пространства дополнительные детали здесь не приводятся. Для целей настоящей главы гораздо важнее показать, зачем вообще может возникнуть потребность в применении такого инструмента. Давайте перейдем к нескольким более крупным примерам, которые продемонстрируют роли метаклассов в действии. Как обнаружится, подобно многим инструментам в Python метаклассы в первую очередь направлены на облегчение работы по сопровождению за счет устранения избыточности.
Пример: добавление методов в классы
В этом и следующем разделе мы займемся изучением примеров двух распространенных сценариев использования для метаклассов: добавление методов в класс и автоматическое декорирование всех методов. Они представляют лишь две из многочисленных ролей метаклассов, рассмотреть которые в главе полностью невозможно из-за ее ограниченного объема; более сложные приложения ищите в веб-сети. Однако приводимые далее примеры являются типичными иллюстрациями работы метаклассов, которых вполне достаточно, чтобы пояснить особенности их применения.
Кроме того, они оба дают возможность противопоставить декораторы и метаклассы — в первом примере сравниваются основанные на метаклассах и декораторах реализации дополнения классов и помещения экземпляров в оболочки, а во втором сначала применяется декоратор с метаклассом, а затем с еще одним декоратором. Вы увидите, что оба инструмента часто взаимозаменяемы и даже дополняют друг друга.
Ручное дополнение
Ранее в главе мы взглянули на скелетный код, который дополнял классы за счет добавления к ним методов разнообразными способами. Как выяснилось, простого наследования на основе классов достаточно, если добавочные методы статически известны при реализации класса. Достичь того же самого эффекта часто удается с помощью композиции через внедрение объектов. Тем не менее, для динамических сценариев временами требуются другие методики — обычно хватает вспомогательных функций, но метаклассы обеспечивают явную структуру и минимизируют затраты при сопровождении в будущем.
Давайте воплотим эти идеи в работающем коде. Рассмотрим следующий пример ручного дополнения класса — в нем добавляются два метода к двум классам после того, как они были созданы:
# Расширение вручную - добавление новых методов в классы
class Clientl:
def _init_(self, value) :
self, value = value def spam(self):
return self.value * 2
class Client2: value = ' ni? '
def eggsfunc(obj):
return obj.value * 4
def hamfunc(obj, value): return value + ’ham’
Clientl.eggs = eggsfunc Clientl.ham = hamfunc
Client2.eggs = eggsfunc Client2.ham = hamfunc
X = Clientl('Ni!1) print(X.spam()) print(X.eggs()) print(X.ham('bacon'))
Y = Client2 () print(Y.eggs()) print(Y.ham('bacon'))
Прием работает, потому что методы всегда можно присваивать классу после того, как он был создан, до тех пор, пока присваиваемые методы являются функциями с дополнительным первым аргументом для получения целевого экземпляра self. Данный аргумент может использоваться для обращения к информации состояния, доступной из экземпляра класса, хотя функция определяется независимо от класса.
В результате запуска код мы получаем вывод метода, реализованного внутри первого класса, а также двух методов, добавленных в класс после его создания:
c:\code> ру -3 extend-manual.ру
Ni!Ni!
Ni!Ni!Ni!Ni! baconham ni?ni?ni?ni? baconham
Такая схема хорошо подходит в отдельных случаях и может применяться для произвольного наполнения класса во время выполнения. Однако она страдает от потенциально значительного недостатка: нам приходится повторять код дополнения для каждого класса, которому нужны новые методы. В нашем случае добавление двух методов к обоим классам было не слишком обременительным, но в более сложных сценариях такой подход может оказаться отнимающим много времени и подверженным ошибкам. Если мы когда-либо забудем сделать это согласованно или возникнет необходимость внести в дополнение какие-то изменения, тогда могут возникнуть проблемы.
Дополнение на основе метаклассов
Несмотря на работоспособность ручного дополнения, в более крупных программах было бы лучше, если бы мы могли применять такие изменения к полному набору классов автоматически. В таком случае мы избежали бы шанса плохо сделать работу для любого отдельно взятого класса. Вдобавок реализация дополнения в единственном месте лучше поддерживает будущие изменения — все классы будут подхватывать изменения автоматически.
Один из способов достижения указанной цели предусматривает использование метаклассов. Если мы реализуем дополнение в метаклассе, тогда каждый класс, который объявляет этот метакласс, будет единообразно и корректно дополняться и автоматически подхватывать любые будущие изменения. Сказанное демонстрируется в следующем коде:
# Расширение с помощью метакласса - лучше поддерживает будущие изменения
def eggsfunc(obj):
return obj.value * 4
def hamfunc(obj, value) : return value + 'ham'
class Extender(type):
def _new_(meta, classname, supers, classdict) :
classdict['eggs'] = eggsfunc classdict['ham' ] = hamfunc
return type._new_(meta, classname, supers, classdict)
class Clientl(metaclass=Extender) :
def _init_(self, value):
self.value = value def spam(self) :
return self.value * 2
class Client2(metaclass=Extender): value = ' ni? '
X = Clientl('Ni!') print(X.spam()) print(X.eggs()) print(X.ham('bacon'))
Y = Client2 () print(Y.eggs()) print(Y.ham('bacon'))
Теперь оба клиентских класса расширяются новыми методами, поскольку они являются экземплярами метакласса, который выполняет дополнение. Запуск данной версии дает такой же вывод, как и ранее — мы не изменяли то, что делает код, а всего лишь провели его рефакторинг с целью более аккуратной инкапсуляции дополнения:
c:\code> ру -3 extend-meta.ру
Ni!Ni!
Ni!Ni!Ni!Ni! baconham ni?ni?ni?ni? baconham
Обратите внимание, что метакласс в приведенном примере по-прежнему решает довольно статичную задачу: добавление двух известных методов к каждому классу, который объявляет метакласс. В сущности, если все, что нам нужно — это всегда добавлять те же самые два метода к набору классов, то мы могли бы также реализовать их в обычном суперклассе и наследовать его в подклассах. Но на практике структура на базе метаклассов поддерживает более динамичное поведение. Например, целевой класс также можно было бы конфигурировать на основе произвольной логики во время выполнения:
• Класс можно также конфигурировать на основе проверок во время выполнения
class MetaExtend(type):
def _new_(meta, classname, supers, classdict):
if sometest():
classdict['eggs'] = eggsfuncl else:
classdict['eggs'] = eggsfunc2 if someothertest ():
classdict['ham'] = hamfunc else:
classdict['ham'] = lambda *args: 'Not supported'
# He поддерживается return type._new_(meta, classname, supers, classdict)
Метаклассы против декораторов классов: раунд 2
Запомните еще раз: в плане функциональности декораторы классов из предыдущего раздела часто пересекаются с метаклассами, обсуждаемыми в настоящей главе. Это происходит из того факта, что:
• декораторы классов повторно привязывают имена классов к результату функции в конце оператора class после того, как класс был создан;
• метаклассы работают путем прогона процедуры создания объектов классов через объект в конце оператора class, чтобы создать новый класс.
Несмотря на то что модели несколько отличаются, на деле они нередко могут достигать одинаковых целей, хотя и разными способами. Как вы теперь видите, декораторы классов напрямую соответствуют методам_init_метаклассов, вызываемым
для инициализации вновь созданных классов. Декораторы не имеют прямого аналога для методов_new_метаклассов (вызываемых в первую очередь для создания классов) или других методов метаклассов (применяемых для обработки классов-экземпляров), но многие или большинство сценариев использования для таких инструментов не требуют этих дополнительных шагов.
По указанным причинам оба инструмента в принципе могут применяться для управления экземплярами класса и самим классом. Тем не менее, на практике метаклассы влекут за собой добавочные шаги для управления экземплярами, а декораторы — добавочные шаги для создания новых классов. Следовательно, наряду с тем, что их роли часто пересекаются, метаклассы лучше всего использовать для управления объектами классов. Давайте займемся воплощением изложенных идей в коде.
Дополнение на основе декораторов
В случаях чистого дополнения декораторы нередко способны заменять метаклассы. Скажем, пример метакласса из предыдущего раздела, который добавлял методы в класс при его создании, можно было бы реализовать также в виде декоратора класса; в таком режиме декораторы грубо соответствуют методу_init_метаклассов, потому что к моменту вызова декоратора объект класса уже был создан. Как и для метаклассов, исходный тип класса предохраняется, поскольку уровень оболочки не вставляется. Вывод, полученный в результате запуска файла extend-deco.ру со следующим содержимым, будет таким же, как у ранее показанного кода метакласса:
# Расширение с помощью декоратора: то же самое, что и предоставление
# метода _init_ в метаклассе
def eggsfunc(obj):
return obj.value * 4
def hamfunc(obj, value): return value + 'ham'
def Extender(aClass) :
# Управляет классом, не экземпляром
# Эквивалентно методу_init_ метакласса
aClass.eggs = eggsfunc aClass.ham = hamfunc return aClass
0Extender class Clientl:
# Clientl = Extender(Clientl) ue) : # Повторная привязка в конце оператора class
def _init_(self, val
self.value = value def spam(self):
2
return self.value *
@Extender class Client2: value = 1 ni? '
# X - экземпляр Clientl
X = Clientl('Ni!') print(X.spam()) print(X.eggs()) print(X.ham('bacon'))
Y = Client2 () print(Y.eggs()) print(Y.ham('bacon'))
Другими словами, по крайней мере, в определенных случаях, декораторы способны управлять классами так же легко, как метаклассы. Однако обратное не настолько прямолинейно; метаклассы могут применяться для управления экземплярами, но лишь за счет написания некоторого объема дополнительной логики, что и демонстрируется в следующем разделе.
Управление экземплярами вместо классов
Как только что было показано, декораторы классов часто могут выступать в той же роли управления классами, как и метаклассы. Метаклассы нередко способны исполнять ту же роль управления экземплярами, что и декораторы, но это требует добавочного кода и может выглядеть менее естественным. То есть:
• декораторы классов могут управлять классами и экземплярами, но не создавать классы обычным образом;
• метаклассы могут управлять классами и экземплярами, но для экземпляров требуется дополнительная работа.
Тем не менее, определенные приложения может быть эффективнее реализовывать с помощью первого или второго инструмента. Скажем, рассмотрим приведенный ниже пример декоратора классов, взятый из предыдущей главы; он используется для вывода трассировочного сообщения всякий раз, когда извлекается нормально именованный атрибут экземпляра класса:
# Декоратор классов для трассировки внешних операций,
# извлекающих атрибуты экземпляров
def Tracer (aClass) : # При декорировании @
class Wrapper:
def _init_(self, *args, **kargs): # При создании экземпляров
self.wrapped = aClass(*args, **kargs) # Использование имени из
# объемлющей области видимости
def _getattr_(self, attrname):
print('Trace: ', attrname) # Перехват всех атрибутов кроме .wrapped return getattr(self.wrapped, attrname) # Делегирование внутреннему
# объекту
return Wrapper 0Tracer
class Person: # Person = Tracer(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
bob = Person (' Bob', 40, 50) # bob - на самом деле экземпляр Wrapper print(bob.name) # Wrapper содержит внедренный экземпляр Person print(bob.pay()) # Запускается_getattr_
После запуска кода декоратор применяет повторную привязку имени класса для помещения объектов экземпляра внутрь объекта, который выдает трассировочные сообщения в следующем выводе:
c:\code> ру -3 manage-inst-deco.ру
Trace: name Bob
Trace: pay 2000
Хотя метакласс способен достичь того же эффекта, концептуально он выглядит менее прямолинейным. Метаклассы явно спроектированы для управления созданием объектов классов и обладают интерфейсом, приспособленным для этой цели. Чтобы использовать метакласс именно для управления экземплярами, мы также обязаны взять на себя ответственность и за создание класса — избыточный шаг, если в противном случае было бы достаточно нормальной процедуры создания класса. Показанный ниже метакласс (файл manage-inst-meta.ру) дает тот же самый эффект, что и предыдущий декоратор:
# Управление экземплярами как в предыдущем примере, но с помощью метакласса
def Tracer(classname, supers, classdict): # При вызове, создающем класс
aClass = type(classname, supers, classdict) # Создание клиентского класса
# При создании экземпляров
# Извлечение внутри методов не отслеживается
self.rate - rate
def pay(self):
return self.hours * self.rate
bob = Person(1 Bob', 40, 50) print(bob.name) print(bob.pay())
# bob - на самом деле экземпляр Wrapper
# Wrapper содержит внедренный экземпляр Person
# Запускается_getattr_
Код работает, но полагается на две уловки. Во-первых, в нем должна применяться простая функция вместо класса, потому что подклассы type обязаны придерживаться протоколов создания объектов. Во-вторых, в нем должен вручную создаваться целевой класс обращением к type; вообще-то необходимо возвращать оболочку экземпляра, но метаклассы также ответственны за создание и возвращение целевого класса. По правде говоря, в этом примере мы использовали протокол метаклассов для имитирования декораторов, а не наоборот; поскольку оба инструмента запускаются при завершении оператора class, во многих ролях они являются всего лишь вариациями на тему. Версия с метаклассом выдает тот же самый вывод, что и версия с декоратором:
c:\code> ру -3 manage-inst-meta.py
Trace: name
Bob
Trace: pay
2000
Вы должны самостоятельно исследовать обе версии примеров, чтобы оценить связанные с ними компромиссы. Однако в целом метаклассы вероятно лучше подходят для управления классами, потому что так они были спроектированы; декораторы классов способны управлять либо экземплярами, либо классами, хотя могут оказаться не наилучшим выбором для исполнения более сложных ролей, которые в настоящей книге не раскрываются. Дополнительные примеры применения метаклассов ищите в веб-сети, но имейте в виду, что одни могут быть более подходящими, чем другие (а некоторые из их авторов могут знать Python меньше, чем вы!).
Эквивалентность метаклассов и декораторов классов?
В предыдущем разделе выяснилось, что при использовании для управления экземплярами метаклассы требуют добавочного шага по созданию класса и потому не могут полностью замещать декораторы во всех сценариях применения. Но как насчет обратного: являются ли декораторы заменой для метаклассов?
На тот случай, если книга еще взорвала вам мозг, взгляните также на следующую альтернативную реализацию — декоратор классов, который возвращает экземпляр метакласса:
# Декоратор может обращаться к метаклассу, хотя не наоборот без type()
»> class Metaclass (type) :
def_new_(meta, clsname, supers, attrdict) :
print (1 In M._new_: ')
print([clsname, supers, list(attrdict.keys())]) return type._new_(meta, clsname, supers, attrdict)
>>> def decorator(els):
return Metaclass (els._name , els._bases_, diet (els. diet_))
>>> class A: x = 1
>>> @decorator class В(A): у = 2
def m(self): return self.x + self.у
In M._new_:
['В', (<class '_main_.A'>,), ['_qualname_', '_doc_', 'm', 'y1,
'_module_’ ] ]
>» B.x, B.y
(1, 2)
»> I = B()
>>> I.x, I.y, I.m()
(1, 2, 3)
Это почти доказывает эквивалентность двух инструментов, но на самом деле только в терминах координирования во время создания класса. Декораторы снова исполняют те же роли, что и методы_init_метаклассов. Поскольку данный декоратор возвращает экземпляр метакласса, здесь по-прежнему предполагаются метаклассы или, во всяком случае, их суперкласс type. Кроме того, после создания класса инициируется дополнительное обращение к метаклассу и такая схема не является идеальной в реальном коде — вы также могли бы перенести данный метакласс в первый шаг создания:
>>> class В (A, me tael as s=Me tael ass) : . . . # Тот же самый эффект, но
# создает только один класс
Тем не менее, здесь присутствует некоторая избыточность инструментов, а роли декораторов и метаклассов на практике нередко пересекаются. И хотя декораторы напрямую не поддерживают понятие методов уровня классов в обсуждаемых ранее метаклассах, похожие результаты можно получить с помощью методов и состояния в объектах-посредниках, создаваемых декораторами, но последнее наблюдение мы оставляем для самостоятельного исследования.
Обратное может не выглядеть применимым — метакласс в целом нельзя отсылать декоратору, отличающемуся от метакласса, потому что до тех пор, пока обращение к метаклассу не завершится, класс еще не существует — хотя метакласс может принимать форму простого вызываемого объекта, который запускает type для создания класса напрямую и его передачи декоратору. Другими словами, решающей привязкой в такой модели является вызов type, выданный для создания класса. С учетом этого метаклассы и декораторы классов часто функционально эквивалентны с варьирующимися моделями протокола координирования:
»> def Metaclass (clsname, supers, attrdict) :
return decorator(type(clsname, supers, attrdict))
>>> def decorator(cle): ...
>>> class В (A, metaclase*Metaclass) : . . . # Метаклассы могут обращаться
# к декораторам и наоборот
В действительности метаклассы не обязательно должны возвращать экземпляр type — подойдет любой объект, согласующийся с ожиданиями разработчика класса — и это еще больше размывает отличие между декораторами и метаклассами:
>» def func (name, supers, attrs) : return 'spam'
>>> class С(metaclass=func) : # Класс, чей метакласс делает его строкой! attr * ' huh? '
>» С, С.upper()
('spam’, 'SPAM’)
>>> def func (els) : return 'spam'
>>> @func
class С: # Класс, чей декоратор делает его строкой!
attr = • huh? '
>>> С, С.upper()
('spam', 'SPAM')
Оставив в стороне трюки подобного рода с метаклассами и декораторами, на практике их роли часто определяются временем, как было указано ранее.
• Поскольку декораторы запускаются после создания класса, в ролях с созданием классов они влекут за собой дополнительный шаг во время выполнения.
• Поскольку метаклассы должны создавать классы, в ролях с управлением экземплярами они влекут за собой дополнительный шаг на этапе реализации.
В итоге ни один из инструментов полностью не заменяет другой. Строго говоря, метаклассы могут быть функциональным надмножеством, т.к. они способны обращаться к декораторам во время создания классов, но метаклассы также могут оказаться существенно сложнее для понимания и реализации, а многие роли совпадают полностью. На поверку необходимость возложить на себя весь процесс создания классов вероятно гораздо менее важна, чем внедрение в сам процесс.
Однако вместо того, чтобы ползти дальше по этой кроличьей норе, давайте перейдем к исследованию ролей метаклассов, которые могут быть более типичными и практичными. Следующий раздел оканчивает главу еще одним распространенным сценарием использования — автоматическое применение операций к методам класса во время создания классов.
Пример: применение декораторов к методам
Как было показано в предыдущем разделе, поскольку метаклассы и декораторы запускаются в конце оператора class, они часто могут применяться взаимозаменяемо, несмотря на разный синтаксис. Выбор между двумя инструментами во многих контекстах произволен. Их также можно использовать в комбинации как дополняющие друг друга инструменты. В этом разделе мы исследуем пример именно такой комбинации — применение декоратора функций ко всем методам класса.
Трассировка с помощью декорирования вручную
В предыдущей главе мы реализовали два декоратора функций — первый трассировал и подсчитывал вызовы декорированной функции, а второй измерял время выполнения таких вызовов. Там они принимали разнообразные формы, часть которых были применимы к функциям и методам, а часть нет. Мы собрали финальные формы обоих декораторов в файл модуля с целью многократного использования:
# Файл decotools .ру: смешанные декораторные инструменты import time
def tracer (func) : # Использовать функцию, а не класс с методом_call_
calls = 0 # Иначе self - только экземпляр декоратора
def onCall(*args, **kwargs): nonlocal calls calls += 1
print (’call %s to %s' % (calls, func._name_))
return func(*args, **kwargs) return onCall
def timer(label='trace=True): # При наличии аргументов декоратора:
# предохранить аргументы
def onDecorator (func) : # При синтаксисе 0: предохранить
# декорированную функцию
def onCall(*args, **kargs): # При вызовах: вызвать исходную функцию start = time, clock () # Состоянием являются области видимости
# и атрибут функции result = func(*args, **kargs)
elapsed = time.clockO - start onCall.alltime += elapsed if trace:
format = '%s%s: %.5f, %.5f•
values = (label, func._name_, elapsed, onCall.alltime)
print(format % values) return result onCall.alltime = 0 return onCall return onDecorator
Как выяснилось в предыдущей главе, для применения таких декораторов вручную мы просто импортируем их из модуля и записываем код декорирования @ перед каждым методом, для которого необходима трассировка или измерение времени:
from decotools import tracer
class Person:
0tracer
def _init_(self, name, pay) :
self, name = name self.pay = pay
@tracer
def giveRaise(self, percent): # giveRaise = tracer(giverRaise)
self.pay *= (1.0 + percent) # onCall запоминает giveRaise
0tracer
def lastName(self): # lastName = tracer (lastName)
return self.name.split()[-1]
bob = Person('Bob Smith', 50000) sue = Person('Sue Jones', 100000) print(bob.name, sue.name)
sue.giveRaise(. 10) # Запускается onCall (sue, .10)
print('%.2f' % sue.pay)
print(bob.lastName(), sue.lastName()) # Запускается onCall (bob),
# запоминается lastName
Запуск кода приводит к получению следующего вывода — вызовы декорированных методов направляются логике, которая перехватывает и затем делегирует их выполнение, т.к. имена исходных методов были привязаны к декоратору:
c:\code> ру -3 decoall-manual.ру
call 1 to _init_
call 2 to _init_
Bob Smith Sue Jones call 1 to giveRaise
110000.00 call 1 to lastName call 2 to lastName Smith Jones
Трассировка с помощью метаклассов и декораторов
Схема с ручным декорированием из предыдущего раздела работоспособна, но требует от нас добавлять синтаксис декорирования перед каждым методом, подлежащим трассировке, и позже удалять его, когда его трассировка больше не нужна. Если мы хотим трассировать все методы класса, то в крупных программах это может стать утомительным. В более динамичных контекстах, где дополнение зависит от параметров времени выполнения, схема с ручным декорированием может вообще оказаться неприемлемой. Было бы лучше, если бы мы каким-то образом могли применять декоратор трассировки ко всем методам класса автоматически.
Именно такой прием возможен благодаря метаклассам — поскольку они запускаются при создании класса, то становятся естественным средством для добавления декорирующих оболочек к методам класса. Просматривая словарь атрибутов класса и проверяя их на предмет принадлежности к объектам функций, мы можем автоматически прогонять методы через декоратор и повторно привязывать исходные имена к результатам. Эффект будет каким же, как автоматическая привязка имен методов к декораторам, но мы можем применять его более глобально:
# Метакласс, который добавляет декоратор трассировки к каждому методу
# клиентского класса
from types import FunctionType from decotools import tracer
class MetaTrace(type):
def _new_(meta, classname, supers, classdict):
for attr, attrval in classdict.items () :
if type(attrval) is FunctionType: # Метод?
classdict[attr] = tracer(attrval) # Декорировать его
return type._new_(meta, classname, supers, classdict) # Создать класс
class Person(metaclass=MetaTrace):
def _init_(self, name, pay) :
self .name = name self.pay = pay
def giveRaise(self, percent): self.pay *= (1.0 + percent) def lastName(self):
return self.name.split()[-1]
bob = Person('Bob Smith', 50000)
sue = Person(fSue Jones', 100000)
print(bob.name, sue.name)
sue.giveRaise(.10)
print('%.2f' % sue.pay)
print(bob.lastName(), sue.lastName())
Запуск кода приводит к получению тех же результатов, что и ранее — вызовы методов направляются сначала декоратору трассировки для отслеживания и затем передаются исходному методу:
c:\code> ру -3 decoall-meta.py
call 1 to _init_
call 2 to _init_
Bob Smith Sue Jones call 1 to giveRaise
110000.00 call 1 to lastName call 2 to lastName Smith Jones
Итог, который вы здесь видите, является комбинацией работы декоратора и метакласса — метакласс автоматически применяет декоратор функций к каждому методу во время создания класса, а декоратор функций автоматически перехватывает вызовы методов для того, чтобы выводить трассировочные сообщения. Комбинация “просто работает” благодаря универсальности обоих инструментов.
Применение любого декоратора к методам
Предыдущий пример метакласса имел дело только с одним конкретным декоратором функций — декоратором трассировки. Тем не менее, его несложно обобщить для применения любого декоратора ко всем методам класса. Все, что нам понадобится сделать — это добавить внешнюю область видимости, чтобы предохранить желаемый декоратор, почти как мы поступали с декораторами в предыдущей главе. Ниже демонстрируется такое обобщение, которое используется для применения декоратора трассировки еще раз:
# Фабрика метаклассов: применение любого декоратора ко всем методам класса
from types import FunctionType from decotools import tracer, timer
def decorateAll(decorator) : class MetaDecorate(type):
def_new_(meta, classname, supers, classdict):
for attr, attrval in classdict.items () : if type(attrval) is FunctionType:
classdict[attr] = decorator(attrval)
return type._new_(meta, classname, supers, classdict)
return MetaDecorate
class Person(metaclass=decorateAll(tracer)): # Применение декоратора
# ко всем методам
def _init_(self, name, pay) :
self, name = name self.pay = pay def giveRaise(self, percent): self.pay *= (1.0 + percent) def lastName(self):
return self.name.split()[-1]
bob = Person('Bob Smith', 50000)
sue = Person('Sue Jones', 100000)
print(bob.name, sue.name)
sue.giveRaise(.10)
print('%.2f* % sue.pay)
print(bob.lastName(), sue.lastName())
В результате запуска кода в том виде, как есть, снова получается тот же вывод, что и в предшествующих примерах. Мы по-прежнему в конечном итоге декорируем каждый метод в клиентском классе посредством декоратора функций, но делаем это в более обобщенной манере:
c:\code> ру -3 decoall-meta-any.ру
call 1 to _init_
call 2 to _init_
Bob Smith Sue Jones call 1 to giveRaise
110000.00 call 1 to lastName call 2 to lastName Smith Jones
Теперь для применения к методам другого декоратора мы можем просто заменить имя декоратора в строке заголовка оператора class. Например, чтобы задействовать реализованный ранее декоратор функций для измерения времени, мы могли бы при определении класса использовать любую из последних двух строк заголовка, показанных ниже — первая принимает аргументы со стандартными значениями, а вторая задает текст метки:
class Person(metaclass=decorateAll(tracer)): # Применение tracer
class Person(metaclass=decorateAll(timer())): # Применение timer,
# стандартные значения class Person (metaclass=decorateAll (timer (label=' **'))): # Аргументы декоратора
Обратите внимание, что представленная схема не способна поддерживать аргументы декоратора, не имеющие стандартных значений, которые отличаются для каждого метода в клиентском классе, но можно передавать аргументы декоратора, применяемые ко всем таким методам, как здесь было сделано. Для тестирования используйте последнее из этих объявлений метаклассов, чтобы применить декоратор timer, и добавьте следующие строки в конец сценария с целью вывода дополнительных информационных атрибутов:
# Если используется timer: суммарное время для каждого метода
print('-'*40)
print(1 %.5f' % Person._init_.alltime)
print('%.5f' % Person.giveRaise.alltime) print('%.5f' % Person.lastName.alltime)
Ниже приведен новый вывод — теперь метакласс помещает методы в оболочку декоратора измерения времени, так что мы можем видеть, сколько времени отнимает каждый вызов, для каждого метода класса:
c:\code> ру -3 decoall-meta-any2.ру
**_init_: 0.00001, 0.00001
**_init_: 0.00001, 0.00001
Bob Smith Sue Jones **giveRaise: 0.00002, 0.00002
110000.00
**lastName: 0.00002, 0.00002 **lastName: 0.00002, 0.00004 Smith Jones
0.00001
0.00002
0.00004
Метаклассы против декораторов классов: раунд 3 (и последний)
Как и следовало ожидать, декораторы классов здесь тоже пересекаются с метаклассами. В показанной далее версии метакласс из предыдущего примера заменяется декоратором классов. То есть в ней определяется и используется декоратор классов, который применяет декоратор функций ко всем методам класса. Хотя предшествующее высказывание может быть больше похоже на цитату из дзен-буддизма, чем на техническое описание, все работает вполне естественно — декораторы Python поддерживают произвольное вложение и сочетание:
# Фабрика декораторов классов: применение любого декоратора ко всем методам класса
from types import FunctionType from decotools import tracer, timer
def decorateAll(decorator): def DecoDecorate(aClass):
for attr, attrval in aClass._diet_.items():
if type(attrval) is FunctionType:
setattr(aClass, attr, decorator(attrval)) # He_diet_
return aClass return DecoDecorate
@decorateAll(tracer) # Использование декоратора классов
class Person: # Применение декоратора функций к методам
def _init_(self, name, pay): # Person = decorateAll (..) (Person)
self.name = name # Person = DecoDecorate(Person)
self.pay = pay def giveRaise(self, percent): self.pay *= (1.0 + percent) def lastName(self):
return self.name.split () [-1]
bob = Person('Bob Smith', 50000)
sue = Person('Sue Jones', 100000)
print(bob.name, sue.name)
sue.giveRaise(.10)
print('%.2f' % sue.pay)
print(bob.lastName(), sue.lastName())
Когда код выполняется в имеющемся виде, декоратор классов применяет декоратор функций для трассировки к каждому методу и при их вызовах выводит трассировочные сообщения (вывод получается такой же, как у версии с метаклассом):
c:\code> ру -3 decoall-deco-any.py
call 1 to _init_
call 2 to _init_
Bob Smith Sue Jones call 1 to giveRaise
110000.00 call 1 to lastName call 2 to lastName Smith Jones
Обратите внимание, что декоратор класса возвращает исходный дополненный класс, а не оболочку для него (что обычно бывает при помещении внутрь нее объектов экземпляров). Как и в версии с метаклассом, мы предохраняем тип исходного класса — экземпляр Person является экземпляром Person, а не какого-то класса оболочки. Фактически данный декоратор классов имеет дело только с созданием класса; вызовы, создающие экземпляры, вообще не перехватываются.
Такое различие может иметь значение в программах, которые требуют проверки типов для экземпляров, чтобы выдавать исходный класс, а не оболочку. При дополнении класса вместо экземпляра декораторы классов могут предохранять тип исходного класса. Методы класса не являются его исходными функциями, потому что они повторно привязаны к декораторам, но это вероятно менее важно на практике и также верно в альтернативной версии с метаклассом.
Также обратите внимание, что подобно версии с метаклассом имеющаяся структура не способна поддерживать аргументы декоратора функций, которые отличаются для каждого метода в декорированном классе, но может обрабатывать такие аргументы, если они применяются ко всем методам. Чтобы использовать эту схему для применения, например, декоратора измерения времени, будет достаточно одной из двух последних строк декорирования, показанных ниже, если она находится прямо перед определением нашего класса — в первой используются аргументы декоратора со стандартными значениями, а во второй один аргумент указывается явно:
@decorateAll (tracer) # Декорировать все методы с помощью tracer
@decorateAll (timer () ) # Декорировать все методы с помощью timer,
# стандартные значения @decorateAll(timer(label='@01)) # То же, но с передачей аргумента декоратора
Как и ранее, воспользуйтесь последней из строк декорирования и добавьте следующий код в конец сценария, чтобы протестировать пример с другим декоратором (разумеется, здесь возможны более эффективные схемы тестирования, но мы уже приблизились к концу главы; при желании можете внести улучшения):
# Если используется timer: суммарное время для каждого метода
print(1 -1*40)
print('%.5f' % Person._init_.alltime)
print(f%.5f* % Person.giveRaise.alltime) print('%.5f1 % Person.lastName.alltime)
Появляется вывод того же самого вида — для каждого метода получается время выполнения каждого и всех его вызовов, но декоратору измерения времени был передан другой аргумент метки:
c:\code> py -3 decoal1-deco-any2.py
00_init_: 0.00001, 0.00001
00_init_: 0.00001, 0.00001
Bob Smith Sue Jones 00giveRaise: 0.00002, 0.00002
110000.00
001astName: 0.00002, 0.00002 001astName: 0.00002, 0.00004 Smith Jones
0.00001
0.00002
0.00004
Наконец, декораторы можно объединять, так что каждый будет запускаться для каждого вызова метода, но вероятно потребуется внести изменения в имеющиеся реализации. В таком виде непосредственное вложение их вызовов приводит к трассировке или измерению времени создания при применении другого декоратора. Указание их двух в отдельных строках дает в результате трассировку или измерение времени выполнения для оболочки другого декоратора перед запуском исходного метода. В принципе, метаклассы в этом отношении выглядят не лучше.
0decorateAll (tracer (timer (label=' 00 1))) # Трассирует применение декоратора timer class Person:
0decorateAll(tracer)
# Трассирует оболочку onCall, измеряет
# время выполнения методов
0decorateAll(timer(labels' 00')) class Person:
0decorateAll(timer(labels'00'))
0decorateAll(tracer)
# Измеряет время выполнения оболочки
# onCall, трассирует методы
class Person:
Дальнейшие размышления на данную тему вы должны продолжить самостоятельно — в книге на это просто не хватает места.
Как видите, метаклассы и декораторы классов не только часто оказываются взаимозаменяемыми, но также обычно дополняют друг друга. Оба инструмента предлагают сложные, но мощные способы настройки и управления классами и объектами экземпляров, поскольку оба, в конечном счете, дают возможность вставлять код в процесс создания классов. Хотя некоторые более продвинутые приложения могут быть более эффективно реализованы с помощью того или другого инструмента, выбор какого-то одного или же комбинации двух инструментов в значительной степени зависит от вас.
Резюме
В главе мы обсуждали метаклассы и исследовали примеры их использования. Метаклассы позволяют внедряться в протокол создания классов Python с целью управления или дополнения определяемых пользователем классов. Из-за того, что метаклассы автоматизируют этот процесс, они могут обеспечивать для разработчиков API-интерфейсов более удачные решения, нежели написанный вручную код или вспомогательные функции; поскольку метаклассы инкапсулируют такой код, они способны минимизировать затраты на сопровождение эффективнее других подходов.
Попутно мы также выяснили, что роли декораторов классов и метаклассов часто пересекаются: так как оба инструмента запускаются в конце оператора class, временами они могут применяться взаимозаменяемо. Декораторы классов и метаклассы можно использоваться для управления классами и объектами экземпляров, хотя в ряде сценариев применения с каждым инструментом могут быть связаны компромиссы.
Поскольку в текущей главе раскрывалась сложная тема, мы ограничимся лишь несколькими контрольными вопросами для закрепления основ (откровенно говоря, если вы зашли настолько далеко в главе, посвященной метаклассам, то вероятно уже заслуживаете уважения!). Учитывая тот факт, что эта часть является последней в книге, мы отказываемся от упражнений, обычно приводимых в конце частей. Обязательно ознакомьтесь с приложениями, в которых описаны изменения Python, решения упражнений из предшествующих частей и т.д.; в решениях упражнений вы найдете образцы типичных прикладных программ для самостоятельного изучения.
Завершив отвечать на контрольные вопросы, вы официально доберетесь до конца технического материала настоящей книги. В следующей — финальной — главе предлагается несколько кратких заключительных мыслей, чтобы подвести итоги по книге в целом. После того, как вы проработаете последние контрольные вопросы, жду встречи с вами в благословенном мире Python.
Проверьте свои знания: контрольные вопросы
1. Что такое метакласс?
2. Как объявить метакласс класса?
3. Каким образом декораторы классов пересекаются с метаклассами при управлении классами?
4. Каким образом декораторы классов пересекаются с метаклассами при управлении экземплярами?
5. Что бы вы предпочли иметь среди своего оружия — декораторы или метаклассы? (Пожалуйста, сформулируйте свой ответ в стиле популярного скетча “Испанская инквизиция”.)
Проверьте свои знания: ответы
1. Метакласс — это класс, используемый для создания класса. Нормальные классы нового стиля по умолчанию являются экземплярами класса type. Метаклассы обычно представляют собой подклассы класса type, которые переопределяют методы протокола создания классов, чтобы настраивать вызов создания класса, выдаваемый в конце оператора class; как правило, они переопределяют методы_new_и_init_для внедрения в протокол создания классов.
Метаклассы можно реализовывать и другими способами (скажем, как простые функции), но они всегда несут ответственность за создание и возвращение объекта для нового класса. Метаклассы также могут иметь методы и данные, чтобы снабжать линией поведения свои классы — и основывать второе направление поиска при наследовании, — но атрибуты метаклассов доступны только их классам-экземплярам, но не экземплярам этих классов-экземпляров.
2. В Python З.Х используйте ключевой аргумент в строке заголовка оператора class: class C(metaclass=M). В Python 2.Х взамен применяйте атрибут клас-
са:_metaclass_ = MB Python З.Х в строке заголовка оператора class перед
ключевым аргументом metaclass могут также указываться имена нормальных суперклассов; кроме того, в Python 2.Х обычно вы должны явно наследовать от object, хотя иногда это необязательно.
3. Поскольку декораторы классов и метаклассы автоматически запускаются в конце оператора class, оба инструмента могут использоваться для управления классами. Декораторы повторно привязывают имя класса к результату, полученному от вызываемого объекта, а метаклассы прогоняют процедуру создания класса через вызываемый объект, но обе привязки применяются для похожих целей. Чтобы управлять классами, декораторы просто дополняют и возвращают объекты исходных классов. Метаклассы дополняют класс после того, как они создали его. Декораторы в такой роли могут обладать небольшим недостатком, если должен определяться новый класс, потому что исходный класс уже был создан.
4. Из-за того, что декораторы классов и метаклассы автоматически запускаются в конце оператора class, мы можем использовать оба инструмента для управления экземплярами классов путем вставки объекта оболочки (посредника), который перехватывает вызовы, создающие экземпляр. Декораторы могут повторно привязывать имя класса к вызываемому объекту, запускаемому при создании экземпляров, который предохраняет объект исходного класса. Метаклассы способны делать то же самое, но могут обладать небольшим недостатком в такой роли, потому что они обязаны также создавать объект класса.
5. Наше главное оружие — декораторы... декораторы и метаклассы... метаклассы и декораторы... Два наших оружия — метаклассы и декораторы... и безжалостная эффективность... Три наших оружия — метаклассы, декораторы и безжалостная эффективность... и почти фанатичная преданность Python... Четыре наших... О нет... Среди наших оружий... Среди нашего оружия... есть такие элементы, как метаклассы, декораторы ... Я вернусь.
ГЛАВА 41
Назад: Декораторы
Дальше: Все хорошее когда-нибудь заканчивается