Сообщить об ошибке.

Определение метаклассов metaclass в Python

По умолчанию классы создаются с использованием класса type(). Тело класса выполняется в новом пространстве имен, а имя класса локально привязано к результату выполнения type(name, bases, namespace).

Процесс создания класса можно настроить, передав ключевой аргумент metaclass в строке определения класса или наследовав от существующего класса, который включал такой аргумент. В следующем примере MyClass и MySubclass являются экземплярами Meta:

class Meta(type):
    pass

class MyClass(metaclass=Meta):
    pass

class MySubclass(MyClass):
    pass

Любые другие ключевые аргументы, указанные в определении класса, передаются во все операции metaclass, описанные ниже.

Когда выполняется определение класса, происходят следующие шаги:

Разрешение записей MRO.

Если база, которая появляется в определении класса, не является экземпляром 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'