2003 г
Программирование метаклассов на Python, часть 2
Автор: Мишель
Симионато (Michele Simionato), физик, Питтсбургский
университет
Перевод:Intersoft Lab
Метаклассы: что мы не рассмотрели в первой статье
В нашей первой
статье о программировании метаклассов на Python мы
представили концепцию метаклассов, показали некоторые их
возможности и продемонстрировали, как их использовать при
решении таких проблем, как динамическая настройка классов и
библиотек во время исполнения.
Эта статья оказалась довольно популярной, но в своем сжатом
заключении мы кое-что упустили. А определенные тонкости
использования метаклассов заслуживают дополнительного
разъяснения. Опираясь на читательский отклик и на обсуждение,
развернувшееся на comp.lang.python, в этой статье мы
рассмотрим некоторые более каверзные вопросы. В частности, мы
считаем, что следующие моменты важны для любого программиста,
желающего овладеть метаклассами:
- Пользователи должны понимать различия между
программированием метаклассов и традиционным
объектно-ориентированным программированием и их взаимосвязь
(при единичном и множественном наследовании).
- В Python 2.2 появились встроенные функции
staticmethod() и classmethod(),
предназначенные для создания методов, которые не требуют
экземпляра во время вызова. До некоторой степени методы
класса совпадают по назначению с (мета)методами,
определенными в метаклассах. Но отдельно взятые схожие черты
и различия также породили замешательство в умах многих
программистов.
- Пользователям следует понимать причину конфликтов
метаклассов и способы их разрешения. Это необходимо, если вы
хотите использовать более одного метакласса, определенного
пользователем. Мы объясним концепцию композиции
(composition) метаклассов.
Воплощение (instantiation) или наследование
(inheritance)
Многие программисты недопонимают различие между метаклассом
и базовым классом. На поверхностном уровне "определения
класса" они оба кажутся одинаковыми. Но если посмотреть
глубже, концепции расходяться.
Прежде чем рассмотреть примеры, стоит определиться с
терминологией. Экземпляр - это объект Python, который
был "произведен" классом; класс действует как разновидность
шаблона для экземпляра. Каждый экземпляр - это экземпляр
только одного класса (но класс может иметь множество
экземпляров). То, что мы часто называем экземпляром класса
(instance object) - или, возможно, "простым экземпляром" -
является "окончательным" в том смысле, что он не может
выступать в качестве шаблона для других объектов (но он все
равно может быть фабрикой или делегатом
(delegate), которые используются для частично
совпадающих целей).
Некоторые экземпляры - сами по себе классы; а все классы
являются экземплярами соответствующего метакласса.
Даже классы появляются исключительно посредством механизма
воплощения. Обычно классы - это экземпляры встроенного,
стандартного метакласса 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')
Хитрость, позволяющая найти атрибут, который не прикреплен
к экземпляру - поискать его в классе экземпляра, а после этого
во всех базовых классах. Порядок, в котором просматриваются
производные классы, называется порядком разрешения
метода (method resolution order) для этого
класса. Вы можете увидеть его с помощью (мета)метода
.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. Воплощение и наследование
Поскольку у 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. Комбинированные базовый класс и
метакласс
Согласно предшествующему объяснению, мы могли бы
представить, что 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. Метаклассы и магические методы
Из предыдущего обсуждения ясно, что метод
.__str__() в Printable не может заменить
метод .__str__() в C, который
наследуется из object и, следовательно, обладает
приоритетом; печать C по-прежнему дает стандартный
результат.
Если бы C наследовал свой метод
.__str__() из Printable, а не из
object, это породило бы проблему: у экземпляров
C нет атрибута .__name__, и печать
C сгенерировала бы ошибку. Разумеется, вы
по-прежнему могли бы определить метод .__str__() в
C, что изменило бы то, как печатается
C.
Методы класса в сравнении с метаметодами
Другая путаница происходит между методами класса Python и
методами, определенными в метаклассе, которые лучше называть
метаметодами.
Рассмотрим пример:
Листинг 10.
Метаметоды и методы класса
>>> 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()
Частично эта путаница вызвана тем, что C.mm в
терминологии Smalltalk назывался бы "методом класса 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. Магический метод
метакласса
>>> print C.__str__()
[...]
TypeError: descriptor '__str__' of 'object' object needs an argument
>>> print M.__str__(C)
This is class C
Важно заметить, что этот конфликт диспетчеризации не
ограничен магическими методами. Если мы изменим C,
добавив атрибут C.mm, возникнет та же проблема (не
имеет значения, является ли имя регулярным методом, методом
класса, статическим методом или простым
атрибутом):
Листинг 13. Немагический метод
метакласса
>>> 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, как показано на следующей диаграмме (см. ниже
в разделе Ресурсы
ссылку на книгу "Использование метаклассов" (Putting
metaclasses to work)):
Рис. 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:
Листинг 15. Разрешение
конфликта метаклассов вручную
>>> 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 предоставляет пользователям
автоматическое и надежное решение этих проблем (см. Ресурсы).
Заключение
В этой статье рассматривается ряд предостережений и
непростых ситуаций. Для работы с метаклассами требуется
определенное количество проб и ошибок, прежде чем их поведение
станет интуитивным. Однако, эти вопросы никоим образом не
безнадежны - эта довольно краткая статья затрагивает
большинство подводных камней. Поэкспериментируйте с этими
ситуациями сами. И к концу дня вы обнаружите, что метаклассы
предоставляют новую степень программного обобщения; выигрыш
сторицей окупает небольшой риск.
Ресурсы
- Авторы по-прежнему рекомендуют "Использование
метаклассов" (Putting
Metaclasses to Work) Айра Р. Формана (Ira R.
Forman) и Скотта Дэнфорта (Scott Danforth) (издательство
Addison-Wesley, 1999).
- Для знакомства с метаклассами, особенно на Python,
полезно эссе Гвидо ван Россума (Guido van Rossum)
"Унификация типов и классов в Python 2.2" (Unifying
types and classes in Python 2.2).
- Реймонд Хеттинджер (Raymond Hettinger) написал
великолепную статью о дескрипторном протоколе (article
on the descriptor protocol), который появился в Python
2.2. Дескрипторы - это способ изменения поведения обращения
к атрибуту/методу, который сам по себе является интересной
технологией программирования. Но особую ценность этой статьи
заключается в хэттинджеровском объяснении цепочки поиска,
которая лежит в основе Питоновской концепции ООП.
- Модуль Мишеля noconflict
module рассматривается в интерактивном Справочном
руководстве по Python на сайте компании ActiveState (Active
State Python Cookbook). Этот модуль позволяет пользователям
автоматически разрешить конфликты метатипов.
- Библиотека утилит Gnosis содержит ряд инструментов для
работы с метаклассами, как правило, в составе субпакета
gnosis.magic. Вы можете скачать последнюю
стабильную версию всего пакета с gnosis.cx.
- Вы также можете пролистать
экспериментальную ветвь, которая включает версию
noconflict.
- Соавтор Дэвида, Мишель, написал "Статью о новом
алгоритме порядка разрешения методов (MRO) в Python 2.3" (article on the new
method resolution order (MRO) algorithm in Python 2.3).
Хотя большинство разработчиков могут продолжать пребывать в
блаженном неведении о подробностях этих изменений, всем
программистам Python стоит понять концепцию MRO, и,
вероятно, у них появятся представление о том, что существуют
лучшие и худшие подходы.
- Статья, предшествующая этой - "Программирование
метаклассов на Python, Часть 1" (Metaclass
programming in Python, Part 1, developerWorks,
февраль 2003г.)
- В статье "Руководство
по интроспекции на Python" (Guide
to Python introspection, developerWorks,
декабрь 2002г.) показаны интроспективные возможности Python:
от основных до нетривиальных.
- Прочитайте колонку Дэвида "Очаровательный Python" (Charming
Python) в рубрике developerWorks в зоне Linux
- Познакомьтесь с другими статьями о Linux и о
программировании под Linux (articles
about Linux and Linux programming) в зоне Linux
developerWorks.
Об авторах
Мишель Симионато - простой, заурядный физик-теоретик,
которого привлекла к Python квантовая флуктуация, которая
могла остаться без последствий, не встреть он Дэвида Мертца.
Сейчас он захвачен Питоновским гравитационным полем. К чему
это привело - судить читателям. Вы можете связаться с Мишелем
по адресу: mis6+@pitt.edu,
или познакомиться с его Web-сайтом.
Дэвид Мертц опасался, что его мозг расплавится, пока он
писал о продолжениях и полусопрограммах, но он засунул серое
вещество обратно в черепную коробку и принялся за метаклассы.
Дэвид доступен по адресу: mertz@gnosis.cx, а жизнь его
описана на http://gnosis.cx/publish/.
Присылайте свои замечания и предложения касательно этой,
прошлых или будущих статей. Его книга "Текстовая обработка в
Python" (Text Processing in
Python) была недавно опубликована в издательстве
Addison-Wesley; познакомьтесь с ней.