Часть 2. Программирование метаклассов в Python

Познание тайн наследования и создания экземпляров

Первая статья Дэвида и Микеле по программированию метаклассов, опубликованная на developerWorks, вызывала множество отзывов, часть из которых была от растерянных читателей, которые пытаются разобраться в тонкостях метаклассов Python. Эта статья возвращается к работе с метаклассами и их отношению к другим понятиям ООП. В ней сравнивается создание экземпляров класса и наследование, рассматриваются различия между методами классов и метаметодами, а также объясняется, как разрешать конфликты метаклассов.

Девид (David) Мертц (Mertz), Developer, Gnosis Software, Inc.

Девид Мертц считает, что искусственные языки вполне естественны, а естественные кажутся немного искусственными. Вы можете связаться с ним по mertz@gnosis.cx; вы можете исследовать все стороны его жизни на его личной web-странице. Посмотрите его книгу, Text Processing in Python. Пожелания и предложения по поводу прошлых и будущих статей только приветствуются.



Мишель Симионато, Physicist, University of Pittsburgh

Мишель Симионато простой, заурядный физик-теоретик, которого привлекло к Python квантовая флуктуация, которая могла остаться без последствий, не встреть он Дэвида Мертца. К чему это привело - судить читателям. Мишель доступен по адресу: mis6+@pitt.edu.



04.12.2007

Метаклассы и их проблемы

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

Другие статьи этой серии

Эта статья была достаточно популярной, но в нашем сжатом изложении мы сделали несколько упущений. Некоторые детали использования метаклассов заслуживают более подробного разъяснения. Основываясь на отзывах наших читателей и дискуссии, развернувшейся в конференции comp.lang.python, мы ответим на некоторые из возникших вопросов в этой, второй статье. В частности, мы считаем, что для любого программиста, который собирается освоить метаклассы, важно иметь в виду следующие моменты:

  • Пользователям необходимо понимать различия и пересечения между программированием метаклассов и традиционным объектно-ориентированным программированием (как с единичным, так и с множественным наследованием).
  • В Python 2.2 добавлены встроенные функции staticmethod() и classmethod(), создающие методы, для вызова которых объект экземпляра не нужен. В некоторой степени назначение методов классов пересекается с (мета)методами, определенными в метаклассах. Однако точные соответствия и различия также порождают путаницу в умах многих программистов.
  • Пользователям следует понимать причины возникновения и способы разрешения конфликтов метаклассов. Это важно, если вы собираетесь использовать более одного специального метакласса. Мы разъясним понятие композиции метаклассов.

Создание экземпляров и наследование

Многие программисты плохо понимают различия между метаклассом и базовым классом. При поверхностном взгляде на "определение" класса оба выглядят похожими. Но если копнуть глубже, эти понятия расходятся.

Перед тем, как приступать к рассмотрению примеров, будет полезно определиться с терминологией. Экземпляр - это объект Python, "произведенный" классом; класс выступает своего рода шаблоном для экземпляра. Каждый экземпляр является экземпляром исключительно одного класса (но у класса может быть несколько экземпляров). Объект, который мы часто называем объектом-экземпляром (instance object) - или иногда "простым экземпляром" - является "окончательным" (final) в том смысле, что он уже не может выступать шаблоном для других объектов (однако он может быть фабрикой или делегатом, которые служат для частично тех же целей).

Некоторые объекты-экземпляры сами являются классами; а все классы являются экземплярами соответствующихметаклассов. Даже классы могут быть созданы только посредством механизма создания экземпляров. Обычно классы являются экземплярами встроенного стандартного метакласса type; , поэтому о программировании метаклассов следует говорить только тогда, когда вводим метаклассы, отличные от type. Мы также называем класс, который вызывается для создания экземпляра объекта, типом (type) этого объекта.

Понятие наследования в определенном смысле ортогонально понятию создания экземпляров. Здесь у класса может быть один или несколько родителей, а не только type. У родителей могут быть свои родители; в результате возникает транзитивное отношение подклассов, которым можно удобно пользоваться с помощью встроенной функции issubclass(). Например, пусть мы определили несколько классов и один экземпляр:

Листинг 1. Обычная иерархия наследования
>>> class A(object): a1 = "A"
...
>>> class B(object): a2 = "B"
...
>>> class C(A,B):    a3 = "C(A,B)"
...
>>> class D(C):      a4 = "D(C)"
...
>>> d = D()
>>> d.a5 = "instance d of D"

Теперь мы можем проверить отношения:

Листинг 2. Проверка родителей
>>> issubclass(D,C)
True
>>> issubclass(D,A)
True
>>> issubclass(A,B)
False
>>> issubclass(d,D)
[...]
TypeError: issubclass() arg 1 must be a class

Давайте теперь рассмотрим один очень интересный вопрос - он необходим для понимания разницы между суперклассами и метаклассами - как разрешаются атрибуты вида d.attr. Для простоты мы будем рассматривать только стандартное правило поиска, не углубляясь в .__getattr__(). На первом шаге такого разрешения выполняется поиск в d.__dict__ названия attr. Если оно найдено, то дело сделано; однако если его нет, должно произойти нечто особое, а именно:

>>> d.__dict__, d.a5, d.a1
({'a5': 'instance d'}, 'instance d', 'A')

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

>>> [k.__name__ for k in d.__class__.mro()]
['D', 'C', 'A', 'B', 'object']

Другими словами, при доступе к d.attr сначала просматривается d.__dict__, затем D.__dict__, C.__dict__, A.__dict__, B.__dict__ и, наконец, object.__dict__. Если название не найдено ни в одном из указанных мест, выдается ошибка AttributeError.

Обратите внимание, что метаклассы в процедуре поиска не участвуют.


Метаклассы и родители

Рассмотрим простой пример обычного наследования. Мы определили базовый класс Noble с подклассами Prince, Duke, Baron и т.д.

Листинг 3. Наследование атрибута
>>> for s in "Power Wealth Beauty".split(): exec '%s="%s"'%(s,s)
...
>>> class Noble(object):      # ...in fairy tale world
...     attributes = Power, Wealth, Beauty
...
>>> class Prince(Noble):
...     pass
...
>>> Prince.attributes
('Power', 'Wealth', 'Beauty')

Класс Prince наследует атрибуты класса Noble. Экземпляр Prince следует описанной выше цепочке поиска:

Листинг 4. Атрибуты в экземпляре
>>> charles=Prince()
>>> charles.attributes        # ...remember, not the real world
('Power', 'Wealth', 'Beauty')

Если у класса Duke будут собственные метаклассы, он может получить некоторые атрибуты следующим образом:

>>> class Nobility(type): attributes = Power, Wealth, Beauty
...
>>> class Duke(object): __metaclass__ = Nobility
...

Будучи классом, Duke является экземпляром метакласса Nobility, и поиск атрибута проходит как и с любым другим объектом:

>>> Duke.attributes
('Power', 'Wealth', 'Beauty')

Однако Nobilityне является суперклассом для Duke, поэтому экземплярDuke не сможет найти Nobility.attributes:

Листинг 5. Атрибуты и метаклассы
>>> Duke.mro()
[<class '__main__.Duke'>, <type 'object'>]
>>> earl = Duke()
>>> earl.attributes
[...]
AttributeError: 'Duke' object has no attribute 'attributes'

Доступность атрибутов метакласса не является транзитивной; другими словами, атрибуты метакласса доступны его экземплярам, но не экземплярам экземпляров. Это и является основным отличием между метаклассами и суперклассами. На рисунке показаны различия наследования и создания экземпляров:

Рисунок 1. Создание экземпляров и наследование
Рисунок 1. Создание экземпляров и наследование

Поскольку у earl все-таки есть класс, вы можете косвенно вызвать указанные атрибуты, однако:

>>> earl.__class__.attributes

На рисунке 1 выделены простые случае, в которых участвует либо наследование, либо метаклассы, но не оба этих механизма. Однако иногда у класса C может быть и собственный метакласс M, и базовый класс B:

Листинг 6. Сочетание метаклассов и суперклассов
>>> class M(type):
...     a = 'M.a'
...     x = 'M.x'
...
>>> class B(object): a = 'B.a'
...
>>> class C(B): __metaclass__=M
...
>>> c=C()

Графически это будет выглядеть следующим образом:

Рисунок 2. Сочетание суперкласса и метакласса
Рисунок 2. Сочетание суперкласса и метакласса

Исходя из данного выше объяснения, мы можем ожидать, что C.a вернет либоM.a, либо B.a. Как выясняется, поиск по классу следует порядку разрешения методов до того, как начинает поиск в создавшем его метаклассе:

Листинг 7. Разрешение в метаклассах и суперклассах
>>> C.a, C.x
('B.a', 'M.x')
>>> c.a
'B.a'
>>> c.x
[...]
AttributeError: 'C' object has no attribute 'x'

Вы тем не менее можете задать значение атрибута с помощью метакласса, для этого вам достаточно задать его для создаваемого экземпляра объекта класса, а не для атрибута метакласса:

Листинг 8. Определение атрибутов метакласса
>>> class M(type):
...     def __init__(cls, *args):
...         cls.a = 'M.a'
...
>>> class C(B): __metaclass__=M
...
>>> C.a, C().a
('M.a', 'M.a')

Еще о магии классов

Тот факт, что ограничения на создание экземпляров слабее, чем ограничения наследования, очень важен для реализации специальных методов типа .__new__(), .__init__(), .__str__() и т.п. Мы рассмотрим метод .__str__(); анализ для других специальных методов будет аналогичным.

Читатели, вероятно, знают, что выводимое представление объекта класса может быть изменено путем переопределения метода .__str__(). Таким же образом выводимое представление класса может быть изменено путем переопределения методов .__str__() его метакласса. Например:

Листинг 9. Настройка вывода класса
>>> class Printable(type):
...    def __str__(cls):
...        return "This is class %s" % cls.__name__
...
>>> class C(object): __metaclass__ = Printable
...
>>> print C       # equivalent to print Printable.__str__(C)
This is class C
>>> c = C()
>>> print c       # equivalent to print C.__str__(c)
<C object at 0x40380a6c>

Ситуация может быть представлена следующей схемой:

Рисунок 3. Метаклассы и магические методы
Рисунок 3. Метаклассы и магические методы

Из описанного выше понятно, что метод .__str__() в Printable не может переопределить метод .__str__()в C, который наследуется от object и, таким образом, имеет приоритет; вывод c по-прежнему вернет стандартный результат.

Если бы C наследовал свой метод .__str__() от Printable, а не от object, это могло бы вызвать проблему: в экземпляре C нет атрибута .__name__, и при попытке вывода c возникла бы ошибка. Конечно же, можно было бы определить в C метод .__str__(), который изменял бы способ вывода c.


Методы классов и метаметоды

Еще одно типичное непонимание касается различий между методами классов Python и методами, описанными в метаклассах, которые лучше называть метаметодами.

Рассмотрим следующий пример:

Листинг 10. Метаметоды и методы классов classmethods
>>> class M(Printable):
...     def mm(cls):
...         return "I am a metamethod of %s" % cls.__name__
...
>>> class C(object):
...     __metaclass__=M
...     def cm(cls):
...         return "I am a classmethod of %s" % cls.__name__
...     cm=classmethod(cm)
...
>>> c=C()

Одной из причин путаницы является тот факт, что в терминологии Smalltalk C.mm назывался бы "методом класса C." Однако методы класса Python - это совсем другой зверь.

Метаметод "mm" может быть вызван либо из метакласса, либо из класса, но не из экземпляра. Метод класса может быть вызван и из класса, и из его экземпляров (однако в метаклассе его не существует).

Листинг 11. Вызов метаметода
>>> print M.mm(C)
I am a metamethod of C
>>> print C.mm()
I am a metamethod of C
>>> print c.mm()
[...]
AttributeError: 'C' object has no attribute 'mm'
>>> print C.cm()
I am a classmethod of C
>>> print c.cm()
I am a classmethod of C

Кроме того, метаметод вызывается dir(M), но не dir(C) , тогда как метод класса вызывается и dir(C), и dir(c).

Вы можете вызывать только методы метакласса, определенные в порядке разрешения методов класса, путем передачи в метакласс (встроенные элементы типа print делают это за кулисами):

Листинг 12. Метод метакласса Magic
>>> print C.__str__()
[...]
TypeError: descriptor '__str__' of 'object' object needs an argument
>>> print M.__str__(C)
This is class C

Важно отметить, что конфликт такой передачи не ограничен «магическими» методами. Если мы изменим C, добавив атрибут C.mm, возникнет та же проблема (неважно, является ли имя обычным методом, методом класса, статическим методом или простым атрибутом):

Листинг 13. Метод метакласса не из magic
>>> C.mm=lambda self: "I am a regular method of %s" % self.__class__
>>> print C.mm()
[...]
TypeError: unbound method <lambda>() must be called with
    C instance as first argument (got nothing instead)

Конфликтующие метаклассы

Если вы соберетесь серьезно работать с метаклассами, вы обязательно хотя бы однажды столкнетесь с конфликтами метаклассов и метатипов. Рассмотрим класс A с метаклассом M_A и класс B с метаклассом M_B; и предположим, что мы вывели C из A и B. Вопрос следующий: что является метаклассом для C? Это M_A или M_B?

Правильный ответ - M_C, где M_C является метаклассом, который наследует от M_A и M_B, как на следующем графике (в разделе Ресурсы этой статьи можно найти ссылку на книгуИспользование метаклассов, где обсуждается этот вопрос):

Рисунок 4. Исключение конфликта метаклассов
Рисунок 4. Исключение конфликта метаклассов

Однако Python (пока) не создает автоматически метакласс M_C. Вместо этого он генерирует ошибку TypeError, предупреждая программиста о конфликте:

Листинг 14. Конфликт метаклассов
>>> class M_A(type): pass
...
>>> class M_B(type): pass
...
>>> class A(object): __metaclass__ = M_A
...
>>> class B(object): __metaclass__ = M_B
...
>>> class C(A,B): pass    # Error message less specific under 2.2
[...]
TypeError: metaclass conflict: the metaclass of a derived class must
    be a (non-strict) subclass of the metaclasses of all its bases

Конфликт метатипа можно исключить, вручную создав метакласс, необходимый для C:

Listing 15. Manually resolving metaclass conflict
>>> M_AM_B = type("M_AM_B", (M_A,M_B), {})
>>> class C(A,B): __metaclass__ = M_AM_B
...
>>> type(C)
<class 'M_AM_B'>

Решение конфликтов метатипа становится более сложным, когда вы хотите "вставить" в класс дополнительные метаклассы, помимо тех, которые являются его родителями. Кроме того, в зависимости от метаклассов родительских классов, могут возникать дублирующиеся метаклассы - как идентичные метаклассы в различных родительских ветвях, так и отношения суперкласс/подкласс между метаклассами. Существует модуль noconflict, который помогает пользователям автоматически и уверенно решить проблемы такого рода (см. раздел Ресурсы).


Заключение

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=273020
ArticleTitle=Часть 2. Программирование метаклассов в Python
publish-date=12042007