Питон - статьи

         

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


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

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





Конфликты метаклассов


Стоит вам всерьез поработать с метаклассами, и вы хотя бы раз столкнетесь с конфликтом метаклассов/метатипов. Рассмотрим класс 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, предупреждая программиста о конфликте:





Типичная иерархия наследования


>
>
>
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"

Мы может протестировать это отношение:





Тестирование родословной


>
>
>
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.

Заметьте, что в этой процедуре поиска метаклассы не были упомянуты ни разу.



Наследование атрибутов


>
>
>
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 по-прежнему придерживается последовательности поиска, рассмотренной выше:





Атрибуты в экземплярах


>
>
>
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:





Атрибуты и метаклассы


>
>
>
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:





Комбинирование базового класса и метакласса


>
>
>
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. Оказывается, поиск по классу следует его порядку разрешения метода до того, как он осуществляется в его метаклассе:





Разрешение метаклассов и базовых классов


>
>
>
C.a, C.x ('B.a', 'M.x') >
>
>
c.a 'B.a' >
>
>
c.x [...] AttributeError: 'C' object has no attribute 'x'

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





Задание атрибута в метаклассе


>
>
>
class M(type): ... def __init__(cls, *args): ... cls.a = 'M.a' ... >
>
>
class C(B): __metaclass__=M ... >
>
>
C.a, C().a ('M.a', 'M.a')



Настройка вывода класса на печатающее устройство


>
>
>
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.



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


>
>
>
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" может быть вызван либо из метакласса, либо из класса, но не из экземпляра. Метод класса может быть вызван и из класса, и из его экземпляров (но не существует в метаклассе).





Вызов метаметода


>
>
>
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, делают это неявно):





Магический метод метакласса


>
>
>
print C.__str__() [...] TypeError: descriptor '__str__' of 'object' object needs an argument >
>
>
print M.__str__(C) This is class C

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





Немагический метод метакласса


>
>
>
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)



Конфликты метаклассов


>
>
>
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:





Разрешение конфликта метаклассов вручную


>
>
>
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 предоставляет пользователям автоматическое и надежное решение этих проблем (см. Ресурсы).



Метаклассы: что мы не рассмотрели в первой статье


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

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

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



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


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





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


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

Рассмотрим пример:





Ресурсы


Авторы по-прежнему рекомендуют "Использование метаклассов" (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.



Воплощение (instantiation) или наследование (inheritance)


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

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

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

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





В этой статье рассматривается ряд


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