Python метаклассы

Python метаклассы

Термин «метапрограммирование» означает, что программа может знать или манипулировать собой. Python поддерживает форму метапрограммирования для классов, называемых metaclasses .

Метаклассы - это эзотерическая концепция OOP, скрывающаяся практически за всем кодом Python. Вы используете их, знаете ли вы об этом или нет. По большей части вам не нужно знать об этом. Большинству программистов на Python редко приходится думать о метаклассах.

Однако, когда возникает необходимость, Python предоставляет возможность, которую поддерживают не все объектно-ориентированные языки: вы можете укрыться и определить собственные метаклассы. Использование пользовательских метаклассов несколько спорно, как это было предложено следующей цитатой из Тима Питерс, Python, который является автором Zen Питона:

_ _ «Метаклассы - это более глубокое волшебство, чем когда-либо должны волноваться 99% пользователей. Если вам интересно, нужны ли они вам, вы не будете (люди, которые действительно нуждаются в них, точно знают, что они им нужны, и не нуждаются в объяснении того, почему) ».

  • Tim Peters _ _

Есть Pythonistas (как известно, поклонники Python), которые считают, что вы никогда не должны использовать собственные метаклассы. Это может быть немного далеко, но, вероятно, это правда, что пользовательские метаклассы в основном не нужны. Если не совсем очевидно, что проблема требует их, то она, вероятно, будет чище и удобочитаемее, если будет решена более простым способом.

Тем не менее, понимание метаклассов Python имеет смысл, поскольку оно ведет к лучшему пониманию внутренних элементов классов Python в целом. Вы никогда не знаете: однажды вы можете оказаться в одной из тех ситуаций, когда вы просто знаете, что вам нужен пользовательский метакласс.

*Получите уведомление:* Не пропустите продолжение этого урока - https://realpython.com/bonus/newsletter-dont-miss-updates/[Нажмите здесь, чтобы присоединиться к информационному бюллетеню Real Python], и вы узнаете, когда выходит следующий взнос

Старый стиль против Классы нового стиля

В области Python класс can одно из двух вариантов. Официальной терминологии не принято, поэтому они неофициально называются классами старого и нового стиля.

Классы старого стиля

С классами старого стиля класс и тип не совсем одно и то же. Экземпляр класса старого стиля всегда реализуется из одного встроенного типа, называемого + instance +. Если + obj + является экземпляром класса старого стиля, + obj . class + обозначает класс, но + type (obj) + всегда + instance +. Следующий пример взят из Python 2.7:

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

Классы нового стиля

Классы нового стиля объединяют понятия класса и типа. Если + obj + является экземпляром класса нового стиля, + type (obj) + совпадает с + obj . class +:

>>>

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True

>>>

>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

Тип и класс

В Python 3 все классы являются классами нового стиля. Таким образом, в Python 3 разумно ссылаться на тип объекта и его класс взаимозаменяемо.

*Примечание:* В Python 2 классы по умолчанию в старом стиле. До Python 2.2 классы нового стиля вообще не поддерживались. Начиная с Python 2.2, они могут быть созданы, но должны быть явно объявлены как новый стиль.

Помните, что in Python, все является объектом. Классы также являются объектами. В результате класс должен иметь тип. Какой тип класса?

Учтите следующее:

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

Тип + x + - это класс + Foo +, как и следовало ожидать. Но тип + Foo +, сам класс, является + type +. В общем, тип любого класса нового стиля - + type +.

Тип встроенных классов, с которыми вы знакомы, также + type +:

>>>

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

В этом отношении тип + type + также является + type + (да, действительно):

>>>

>>> type(type)
<class 'type'>

+ type + является метаклассом, классы которого являются экземплярами. Так же, как обычный объект является экземпляром класса, любой класс нового стиля в Python и, следовательно, любой класс в Python 3, является экземпляром метакласса + type +.

В приведенном выше случае:

  • + x + является экземпляром класса + Foo +.

  • + Foo + является экземпляром метакласса + type +.

  • + type + также является экземпляром метакласса + type +, поэтому он сам является экземпляром.

Python цепочка классов

Определение класса динамически

Встроенная функция + type () + при передаче одного аргумента возвращает тип объекта. Для классов нового стиля это, как правило, совпадает с атрибутом https://docs.python.org/3/library/stdtypes.html#instance.class[object + class +]:

>>>

>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

Вы также можете вызвать + type () + с тремя аргументами - + type (<имя>, <base>, <dct>) +:

  • + <name> + указывает имя класса. Это становится атрибутом + name + класса.

  • + <base> + определяет кортеж базовых классов, от которых наследуется класс. Это становится атрибутом + Base + класса.

  • + <dct> + задает словарь пространства имен, содержащий определения для тела класса. Это становится атрибутом + dict + класса.

Вызов + type () + таким образом создает новый экземпляр метакласса + type +. Другими словами, он динамически создает новый класс.

В каждом из следующих примеров верхний фрагмент динамически определяет класс с помощью + type () +, в то время как приведенный ниже фрагмент определяет класс обычным способом с помощью оператора + class +. В каждом случае два фрагмента функционально эквивалентны.

Пример 1

В этом первом примере аргументы + <base> + и + <dct> +, переданные в + type () +, оба пусты. Не указывается наследование от какого-либо родительского класса, и изначально ничего не помещается в словарь пространства имен. Это простейшее определение класса:

>>>

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

Пример 2

Здесь + <base> + - это кортеж с одним элементом + Foo +, указывающий родительский класс, от которого наследуется + Bar +. Атрибут + attr + изначально помещается в словарь пространства имен:

>>>

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

>>>

>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

Пример 3

На этот раз + <base> + снова пусто. Два объекта помещаются в словарь пространства имен с помощью аргумента + <dct> +. Первый - это атрибут с именем + attr +, а второй - функция с именем + attr_val +, которая становится методом определенного класса:

>>>

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

>>>

>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

Пример 4

Только очень простые функции могут быть определены с помощью https://dbader.org/blog/python-lambda-functions [+ lambda + в Python]. В следующем примере внешне определена немного более сложная функция, чем присвоенная + attr_val + в словаре пространства имен через имя + f +:

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

Пользовательские метаклассы

Рассмотрим снова этот надутый пример:

>>>

>>> class Foo:
...     pass
...
>>> f = Foo()

Выражение + Foo () + создает новый экземпляр класса + Foo +. Когда интерпретатор встречает + Foo () +, происходит следующее:

  • Вызывается метод + call () + родительского класса + Foo +. Поскольку + Foo + является стандартным классом нового стиля, его родительским классом является метакласс + type +, поэтому вызывается метод + type + '`` + call () + `.

  • Этот метод + call () + в свою очередь вызывает следующее:

  • + новый () +

  • '+ инициализации () + `

Если + Foo + не определяет + new () + и + init () +, методы по умолчанию наследуются от предков + Foo +. Но если + Foo + определяет эти методы, они переопределяют методы из предка, что позволяет настраивать поведение при создании экземпляра + Foo +.

Далее пользовательский метод с именем + new () + определен и назначен как метод + new () + для + Foo +:

>>>

>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

Это изменяет поведение экземпляра класса + Foo +: каждый раз, когда создается экземпляр + Foo +, по умолчанию он инициализируется атрибутом с именем + attr +, который имеет значение + 100 +. (Подобный код обычно появляется в методе + init () +, а не в + new () +). Этот пример придуман для демонстрационных целей.)

Теперь, как уже было подтверждено, классы тоже являются объектами. Предположим, вы хотели аналогичным образом настроить поведение экземпляра при создании класса, подобного + Foo +. Если бы вы следовали приведенному выше шаблону, вы бы снова определили собственный метод и назначили его как метод + new () + для класса, экземпляром которого является + + Foo + . `+ Foo + является экземпляром метакласса + type +, поэтому код выглядит примерно так:

>>>

# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

Кроме того, как вы можете видеть, вы не можете переназначить метод + new () + метакласса + type +. Python не позволяет этого.

Это, наверное, так же хорошо. + type + - это метакласс, из которого получены все классы нового стиля. В любом случае, вам действительно не следует с этим возиться. Но тогда какой выход есть, если вы хотите настроить создание экземпляров класса?

Одним из возможных решений является пользовательский метакласс. По сути, вместо того, чтобы возиться с метаклассом + type +, вы можете определить свой собственный метакласс, который наследуется от + type +, и затем вы можете вместо этого обойти его.

Первый шаг - определить метакласс, производный от + type +, следующим образом:

>>>

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

Заголовок определения + class Meta (type): + указывает, что + Meta + происходит от + type +. Поскольку + type + является метаклассом, это также делает + Meta + метаклассом.

Обратите внимание, что для + Meta + был определен специальный метод + new () +. Это было невозможно сделать напрямую с метаклассом + type . Метод ` new () +` выполняет следующие действия:

  • Делегирует через + super () + методу + new () + родительского метакласса (+ type +) для фактического создания нового класса

  • Назначает пользовательский атрибут + attr + классу со значением + 100 +

  • Возвращает вновь созданный класс

Теперь другая половина voodoo: определите новый класс + Foo + и укажите, что его метакласс является пользовательским метаклассом + Meta +, а не стандартным метаклассом + type +. Это делается с помощью ключевого слова + metaclass + в определении класса следующим образом:

>>>

>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

_Voila! _ + Foo + автоматически выбрал атрибут + attr + из метакласса + Meta +. Конечно, любые другие классы, которые вы определяете аналогичным образом, будут действовать аналогично

>>>

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

Так же, как класс функционирует как шаблон для создания объектов, метакласс функционирует как шаблон для создания классов. Метаклассы иногда называют class фабрики.

Сравните следующие два примера:

*Объект Фабрика:*

>>>

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100
*Фабрика классов:*

>>>

>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

Это действительно необходимо?

Как простой пример фабрики классов, такова сущность работы метаклассов. Они позволяют настраивать создание экземпляров классов.

Тем не менее, это много хлопот, чтобы просто присвоить пользовательский атрибут + attr + каждому вновь созданному классу. Вам действительно нужен метакласс только для этого?

В Python есть, по крайней мере, еще пара способов, с помощью которых можно добиться того же самого:

*Простое наследование:*

>>>

>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100
*Класс Decorator:*

>>>

>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

Заключение

Как предполагает Тим ​​Питерс, «метаклассы» могут легко повернуть в область «решения в поисках проблемы». Обычно нет необходимости создавать собственные метаклассы. Если проблема под рукой может быть решена более простым способом, вероятно, так и должно быть. Тем не менее, полезно понимать метаклассы, чтобы вы понимали Python классы в целом и могли распознать, когда метакласс действительно является подходящим инструментом для использования.