По умолчанию классы создаются с использованием класса type()
. Тело класса выполняется в новом пространстве имен, а имя класса локально привязано к результату выполнения type(name, bases, namespace)
.
Процесс создания класса можно настроить, передав ключевой аргумент metaclass
в строке определения класса или наследовав от существующего класса, который включал такой аргумент. В следующем примере MyClass
и MySubclass
являются экземплярами Meta
:
class Meta(type): pass class MyClass(metaclass=Meta): pass class MySubclass(MyClass): pass
Любые другие ключевые аргументы, указанные в определении класса, передаются во все операции metaclass
, описанные ниже.
Когда выполняется определение класса, происходят следующие шаги:
Если база, которая появляется в определении класса, не является экземпляром type
, то по ней ищется метод __mro_entries__
. Если он найден, то он вызывается с исходным базовым кортежем. Этот метод должен возвращать кортеж классов, который будет использоваться вместо этой базы. Кортеж может быть пустым, в этом случае исходная база игнорируется.
Соответствующий метакласс для определения класса определяется следующим образом:
type()
;type()
, то он используется непосредственно как метакласс;type()
задан как явный метакласс или определены базы, то используется наиболее производный метакласс.Наиболее производный метакласс выбирается из явно указанного метакласса (если есть) и метаклассов (то есть type(cls)
) всех указанных базовых классов. Самый производный метакласс - это тот, который является подтипом всех этих метаклассов-кандидатов. Если ни один из метаклассов-кандидатов не соответствует этому критерию, определение класса завершится ошибкой TypeError
.
После того, как соответствующий метакласс идентифицирован, подготавливается пространство имен класса. Если у метакласса есть атрибут __prepare__
, то он именуется как namespace=metaclass.__prepare__(name, base, ** kwds)
, где дополнительные ключевые аргументы, если таковые имеются, берутся из определения класса. Метод __prepare__
должен быть реализован как метод класса classmethod()
. Пространство имен, возвращаемое __prepare__
, передается в __new__
, но когда создается последний объект класса, пространство имен копируется в новый словарь dict
.
Если метакласс не имеет атрибута __prepare__
, то тогда пространство имен класса инициализируется как пустое упорядоченное отображение (словарь).
Тело класса выполняется (приблизительно) как exec(body, globals(), namespace)
. Ключевое отличие от обычного вызова функции exec()
состоит в том, что лексическая область видимости позволяет телу класса (включая любые методы) ссылаться на имена из текущей и внешней областей, когда определение класса происходит внутри функции.
Однако, даже когда определение класса происходит внутри функции, методы, определенные внутри класса, по-прежнему не могут видеть имена, определенные в области класса. Доступ к переменным класса должен осуществляться через первый параметр методов экземпляра или класса или через неявную ссылку __class__
с лексической областью видимости, описанную в "Создании объекта класса".
Как только пространство имен класса заполнено путем выполнения тела класса, объект класса создается путем вызова metaclass(name, bases, namespace, **kwds)
. Дополнительные ключевые слова **kwds
, переданные здесь, такие же, как те, которые переданы в __prepare__
.
Этот объект класса является тем, на который будет ссылаться форма с нулевым аргументом super().__class__
- это неявная ссылка на замыкание, созданная компилятором, если какие-либо методы в теле класса ссылаются либо на __class__
, либо на super()
. Это позволяет нулевой форме аргумента функции super()
правильно идентифицировать определяемый класс на основе лексической области видимости, в то время как класс или экземпляр, который был использован для выполнения текущего вызова, идентифицируется на основе первого аргумента, переданного методу.
Подробности реализации CPython: в CPython 3.6 и более поздних версиях ячейка __class__
передается метаклассу как запись __classcell__
в пространстве имен классов. Если он присутствует, то он должен быть распространен до type.__new__
, чтобы класс был правильно инициализирован. Невыполнение этого требования приведет к ошибке RuntimeError
в Python 3.8.
При использовании метакласса type
по умолчанию или любого метакласса, который в конечном итоге вызывает type.__ new__
после создания объекта класса, вызываются следующие дополнительные шаги настройки:
type.__new__
собирает все дескрипторы в пространстве имен класса, которые определяют метод __set_name__()
;__set_name__
вызываются с определяемым классом и присвоенным именем этого конкретного дескриптора;__init_subclass__()
для непосредственного родителя нового класса в порядке разрешения его методов.После того, как объект класса создан, он передается декораторам класса, включенным в определение класса (если есть) и полученный объект привязывается в локальном пространстве имен как определенный класс.
Когда новый класс создается по type.__new__
, то объект, указанный в качестве параметра пространства имен, копируется в новое упорядоченное сопоставление (словарь), а исходный объект отбрасывается. Новая копия упаковывается в прокси только для чтения, который становится атрибутом __dict__
объекта класса.
Метакласс чаще всего используется в качестве фабрики классов, а вообще возможности использования метаклассов безграничны.
Некоторые из идей включают:
Если не требуется сложные изменения класса, метаклассы использовать не стоит. Просто изменить класс можно двумя способами:
В 99% случаев лучше использовать эти методы, а в 98% изменения класса вообще не нужны.
Причина сложности кода, использующего метаклассы, заключается не в самих метаклассах. Код сложный потому, что обычно метаклассы используются для сложных задач, основанных на наследовании, интроспекции и манипуляции такими переменными, как __dict__
.
Основной целью метакласса является автоматическое изменение класса во время его создания. Обычно это делается для API, когда нужно создать классы, соответствующие текущему контексту. Например необходимо, что бы все классы в модуле имели свои атрибуты в верхнем регистре. Чтобы добиться такого поведения, определим метакласс и передадим его переменной __metaclass__
на уровне модуля или можно вручную передавать его в класс в качестве ключевого аргумента metaclass
.
class UpperAttrMetaclass(type): def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr): uppercase_attr = {} for name, val in future_class_attr.items(): if not name.startswith('__'): uppercase_attr[name.upper()] = val else: uppercase_attr[name] = val # повторно используем метод type.__new__, # это базовое ООП, в нем нет ничего волшебного return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr) class Foo(metaclass=UpperAttrMetaclass): bar = 'bip' print(hasattr(Foo, 'bar')) # False print(hasattr(Foo, 'BAR')) # True f = Foo() print(f.BAR) # 'bip'