Перейти к тексту

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

При первом входе в developerWorks для Вас будет создан профиль. Выберите информацию отображаемую в Вашем профиле — скрыть или отобразить поля можно в любой момент.

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

  • Закрыть [x]

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

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

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

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

  • Закрыть [x]

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

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

Девид (David) Мертц (Mertz), Developer, Gnosis Software, Inc.
Девид Мертц считает, что искусственные языки вполне естественны, а естественные кажутся немного искусственными. Вы можете связаться с ним по mertz@gnosis.cx; вы можете исследовать все стороны его жизни на его личной web-странице. Посмотрите его книгу, Text Processing in Python. Пожелания и предложения по поводу прошлых и будущих статей только приветствуются.
Мишель Симионато, Physicist, University of Pittsburgh
Мишель Симионато простой, заурядный физик-теоретик, которого привлекло к Python квантовая флуктуация, которая могла остаться без последствий, не встреть он Дэвида Мертца. К чему это привело - судить читателям. Мишель доступен по адресу: mis6+@pitt.edu.

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

Дата:  06.12.2007
Уровень сложности:  средний
Активность:  2502 просмотров
Комментарии:  


Введение

В прошлом году я участвовал в конференции EuroPython 2006. Конференция была хороша, организация великолепна, разговоры проходили на очень высоком уровне, а люди были исключительно приятны. Но всё же я отметил в сообществе Python некоторые беспокоящие меня тенденции, которые побудили меня написать эту статью. Практически в тот же момент мой соавтор, Дэвид Мертц, отреагировал на похожую проблему, выпустив несколько исправлений Gnosis Utilities. Нас беспокоит тенденция движения в сторону заумности. К сожалению, если раньше заумность в сообществе Python ограничивалась большей частью Zope и Twisted, теперь она появляется везде.

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

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

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

Эта статья предназначена для двух групп читателей: рядовых программистов, которым было бы полезно знать приемы метапрограммирования, но их останавливают крайне сложные понятия; и умных программистов, которые слишком умны и должны были бы знать больше. Проблема с последними состоит в том, что умным быть очень просто, а для того, чтобы научиться быть проще, требуется много времени. Например, нам самим потребовалось несколько месяцев, чтобы понять, как использовать метаклассы, но на то, чтобы понять, как их не использовать, ушло несколько лет.


Об инициализации классов

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

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

Например, наш знакомый, отличный программист Ананд Пиллай (Anand Pillai), предложил путь к реализации пакета gnosis.xml.objectify в Gnosis Utilities, который делает именно это. Базовый класс gnosis.xml.objectify._XO_, адаптируемый (в реальном времени) для обработки "объектов узлов xml", "декорируется" несколькими расширенными моделями поведения, например:


Листинг 1. Динамическое расширение базового класса
                
setattr(_XO_, 'orig_tagname', orig_tagname)
setattr(_XO_, 'findelem', findelem)
setattr(_XO_, 'XPath', XPath)
setattr(_XO_, 'change_pcdata', change_pcdata)
setattr(_XO_,'addChild',addChild)
			

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

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


Листинг 2. Добавление декораторов класса в Python
                
features = [('XPath',XPath), ('addChild',addChild), ('is_root',is_root)]
@enhance(features)
class _XO_plus(gnosis.xml.objectify._XO_): pass
gnosis.xml.objectify._XO_ = _XO_plus
			

Однако такой синтаксис - это вопрос будущего, если он вообще будет реализован.


Когда метаклассы приводят к усложнению

Может показаться, что все разговоры в этой статье идут ни о чем. Почему бы, например, просто не определить метакласс XO как Enhance и покончить с этим. Enhance.__init__() может запросто добавить любые нужные для конкретного случая возможности. Это может выглядеть следующим образом:


Листинг 3. Определение XO как Enhance
                
class _XO_plus(gnosis.xml.objectify._XO_):
      __metaclass__ = Enhance
      features = [('XPath',XPath), ('addChild',addChild)]
gnosis.xml.objectify._XO_ = _XO_plus
			

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

  • Вы считаете, что явное лучше, чем косвенное.
  • У производных классов будут те же динамические атрибуты, что и у базового класса. Установка их еще раз для каждого производного класса не нужна, поскольку они и так будут доступны посредством наследования. Это может быть особенно серьезной проблемой в случаях, если код инициализации выполняется медленно или требует большого объема вычислений. Можно было бы добавить в коде метакласса проверку на предмет наличия атрибутов в родительском классе, но это повысит сложность и не даст реального контроля над каждым классом.
  • Собственные метаклассы делают ваши классы в каком-то смысле шаманскими и нестандартными: вы не захотите повышать шанс возникновения конфликтов метакласса, проблем со "__slots__", борьбы с классами расширений (Zope) и других сложностей, разрешение которых требует особой квалификации. Метаклассы куда более хрупки, чем можно себе представить. Мы редко используем их в рабочем коде даже после четырех лет использования в коде экспериментальном.
  • Вы чувствуете, что собственные метаклассы - это больше, чем нужно для выполнения простых задач инициализации класса, и предпочитаете использовать более простое решение.

Другими словами, собственные метаклассы необходимо использовать только тогда, когда у вас действительно есть намерение запускать код на производных классах незаметно для пользователей этих классов. Если это не ваш случай - забудьте про метаклассы и сделайте вашу жизнь (и жизнь ваших пользователей) более счастливой.


Декоратор classinitializer

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


Листинг 4. Императивный подход
                
def Enhance(cls, **kw):
    for k, v in kw.iteritems():
        setattr(cls, k, v)
class ClassToBeInitialized(object):
    pass
Enhance(ClassToBeInitialized, a=1, b=2)
			

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

Декоратор classinitializer предоставляет декларативное решение. Декоратор преобразует Enhance(cls,**kw) в метод, который можно использовать в определении класса:


Листинг 5. Декоратор magic является базовой операцией
                
>>> @classinitializer # add magic to Enhance
... def Enhance(cls, **kw):
...     for k, v in kw.iteritems():
...         setattr(cls, k, v)
>>> class ClassToBeInitialized(object):
...     Enhance(a=1, b=2)
>>> ClassToBeInitialized.a
1
>>> ClassToBeInitialized.b
2
			

Если вы работали с интерфейсами Zope, вы могли видеть примеры инициализаторов классов (zope.interface.implements). На самом деле classinitializer реализован с помощью приема, позаимствованного из zope.interface.advice, где его предложил Филип Дж. Эби (Phillip J. Eby). В этом приеме используется прерывание "__metaclass__", но не используется собственный метакласс. ClassToBeInitialized сохраняет исходный метакласс, т.е. обычный встроенный метакласс type классов нового стиля:

>>> type(ClassToBeInitialized)
<type 'type'>
			

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


Листинг 6. Переход на новый стиль
                
>>> class WasOldStyle:
...     Enhance(a=1, b=2)
>>> WasOldStyle.a, WasOldStyle.b
(1, 2)
>>> type(WasOldStyle)
<type 'type'>
			

Одна из целей декоратора classinitializer - скрыть сложные конструкции и дать простым смертным возможность с легкостью реализовать собственные инициализаторы класса, без знания подробностей работы создания классов и секретов прерывания _metaclass_. Еще одна мотивация состоит в том, что даже эксперту по Python очень неудобно изменять коды, обрабатывающие прерывание _metaclass_, при написании каждого нового инициализатора класса.

И, наконец, позвольте нам подчеркнуть, что декорированная версия Enhance достаточно умна для того, чтобы работать как недекорированная версия за рамками класса, если вы передаете ей явный аргумент класса:

>>> Enhance(WasOldStyle, a=2)
>>> WasOldStyle.a
2
			


(Слишком) сильное волшебство

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


Листинг 7. Декоратор classinitializer
                
import sys
def classinitializer(proc):
   # basic idea stolen from zope.interface.advice, P.J. Eby
   def newproc(*args, **kw):
       frame = sys._getframe(1)
       if '__module__' in frame.f_locals and not \
           '__module__' in frame.f_code.co_varnames: # we are in a class
           if '__metaclass__' in frame.f_locals:
               raise SyntaxError("Don't use two class initializers or\n"
                 "a class initializer together with a __metaclass__ hook")
           def makecls(name, bases, dic):
               try:
                   cls = type(name, bases, dic)
               except TypeError, e:
                   if "can't have only classic bases" in str(e):
                       cls = type(name, bases + (object,), dic)
                   else:  # other strange errs, e.g. __slots__ conflicts
                       raise
               proc(cls, *args, **kw)
               return cls
           frame.f_locals["__metaclass__"] = makecls
       else:
           proc(*args, **kw)
 newproc.__name__ = proc.__name__
 newproc.__module__ = proc.__module__
 newproc.__doc__ = proc.__doc__
 newproc.__dict__ = proc.__dict__
 return newproc
			

Из реализации становится понятно, как работает инициализатор класса: когда вы вызываете инициализатор класса внутри класса, вы фактически определяете прерывание _metaclass_, которое будет вызываться метаклассом класса (обычно это type). Метакласс создаст класс (в новом стиле) и передаст его процедуре инициализатора класса.

Сложности и предостережения

Поскольку инициализаторы класса (пере)определяют прерывание _metaclass_, они не очень хорошо работают с классами, которые определяют прерывание _metaclass_ явным образом (в противоположность косвенному наследованию). Если прерывание _metaclass_ определено после инициализатора класса, оно молча переопределяет его.


Листинг 8. Домашняя страница index.html нового проекта
                
>>> class C:
...     Enhance(a=1)
...     def __metaclass__(name, bases, dic):
...         cls = type(name, bases, dic)
...         print 'Enhance is silently ignored'
...         return cls
...
Enhance is silently ignored
>>> C.a
Traceback (most recent call last):
  ...
AttributeError: type object 'C' has no attribute 'a'
			

К сожалению, общего решения этой проблемы не существует; мы просто документируем её. С другой стороны, если вы вызываете инициализатор класса после прерывания _metaclass_, вы получите ошибку:


Листинг 9. Локальный метакласс вызывает ошибку
                
>>> class C:
...     def __metaclass__(name, bases, dic):
...         cls = type(name, bases, dic)
...         print 'calling explicit __metaclass__'
...         return cls
...     Enhance(a=1)
...
Traceback (most recent call last):
   ...
SyntaxError: Don't use two class initializers or
a class initializer together with a __metaclass__ hook
			

Выдача ошибки лучше, чем тихое переопределение прерывания _metaclass_. Поэтому если вы попробуете использовать два инициализатора класса одновременно, или если вызовете один инициализатор класса дважды, вы получите ошибку:


Листинг 10. Двойное расширение вызывает проблемы
                
>>> class C:
...     Enhance(a=1)
...     Enhance(b=2)
Traceback (most recent call last):
  ...
SyntaxError: Don't use two class initializers or
a class initializer together with a__metaclass__ hook
			

Что хорошо, все проблемы наследования прерываний _metaclass_ и собственных метаклассов обрабатываются:


Листинг 11. Удачное расширение наследуемого метакласса
                
>>> class B: # a base class with a custom metaclass
...     class __metaclass__(type):
...         pass
>>> class C(B): # class with both custom metaclass AND class initializer
...     Enhance(a=1)
>>> C.a
1
>>> type(C)
<class '_main.__metaclass__'>
			

Инициализатор класса не мешает метаклассу C, который наследуется базовым классом B, и наследуемый метакласс не мешает инициализатору класса, который отлично выполняет свою работу. Вместо этого у вас возникнут проблемы, если вы попытаетесь вызвать Enhance напрямую из базового класса.


Подведение итогов

После того как все механизмы определены, настройка инициализации класса становится простой и элегантной. Это может выглядеть примерно так:


Листинг 12. Простейшая форма расширения
                
class _XO_plus(gnosis.xml.objectify._XO_):
    Enhance(XPath=XPath, addChild=addChild, is_root=is_root)
gnosis.xml.objectify._XO_ = _XO_plus
			

В этом примере все еще используется "вставка", которая в общем случае не нужна; т.е. мы вставляем расширенный класс в определенное имя в пространстве имен модуля. Это нужно для данного конкретного модуля, но в большинстве случаев такой необходимости нет. В любом случае аргумент Enhance() не обязательно фиксировать в коде, как это сделано выше, точно так же можно использовать Enhance(**feature_set) для полностью динамического решения.

Еще один момент, который необходимо помнить, состоит в том, что ваша функция Enhance() способна на существенно большее, чем предложенная выше простая версия. Этот же декоратор будет счастлив настроить более сложные функции расширения. Например, вот функция, добавляющая "записи" в класс:


Листинг 13. Изменение расширения класса
                
@classinitializer
def def_properties(cls, schema):
    """
    Add properties to cls, according to the schema, which is a list
    of pairs (fieldname, typecast). A typecast is a
    callable converting the field value into a Python type.
    The initializer saves the attribute names in a list cls.fields
    and the typecasts in a list cls.types. Instances of cls are expected
    to have private attributes with names determined by the field names.
    """
    cls.fields = []
    cls.types = []
    for name, typecast in schema:
        if hasattr(cls, name): # avoid accidental overriding
            raise AttributeError('You are overriding %s!' % name)
        def getter(self, name=name):
            return getattr(self, '_' + name)
        def setter(self, value, name=name, typecast=typecast):
            setattr(self, '_' + name, typecast(value))
        setattr(cls, name, property(getter, setter))
        cls.fields.append(name)
        cls.types.append(typecast)
			

Вопросы о том, (a) что расширяется, (b) как работает это волшебство и (c) что делает базовый класс, остаются независимыми друг от друга:


Листинг 14. Настройка класса записи
                
>>> class Article(object):
...    # fields and types are dynamically set by the initializer
...    def_properties([('title', str), ('author', str), ('date', date)])
...    def __init__(self, values): # add error checking if you like
...        for field, cast, value in zip(self.fields, self.types, values):
...            setattr(self, '_' + field, cast(value))

>>> a=Article(['How to use class initializers', 'M. Simionato', '2006-07-10'])
>>> a.title
'How to use class initializers'
>>> a.author
'M. Simionato'
>>> a.date
datetime.date(2006, 7, 10)
			


Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Об авторах

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

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

Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Спасибо. Эта запись была помечена для модератора.


Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Сообщение о нарушении не было отправлено. Попробуйте, пожалуйста, позже.


developerWorks: вход


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


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

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

 


При первом входе в developerWorks для Вас будет создан профиль. Выберите информацию отображаемую в Вашем профиле — скрыть или отобразить поля можно в любой момент.

Выберите ваше отображаемое имя

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

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

(Должно содержать от 3 до 31 символа.)


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

 


Оценить эту статью

Комментарии

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=273682
ArticleTitle=Часть 3. Программирование метаклассов в Python
publish-date=12062007
author1-email=mertz@gnosis.cx
author1-email-cc=tomyoung@us.ibm.com
author2-email=mis6+@pitt.edu
author2-email-cc=

Теги

Help
Используйте форму поиска, чтобы найти любой контент с данным тегом в My developerWorks. Используйте ползунок, чтобы отразить больше или меньше тегов.

КнопкаПопулярные теги отображает самые распространенные теги для данной области контента (например: Java, Linux, WebSphere).

Кнопка Мои теги отображает Ваши теги для данной области контента (например: Java, Linux, WebSphere).

Используйте форму поиска, чтобы найти любой контент с данным тегом в My developerWorks. Кнопка Популярные теги отображает самые распространенные теги для данной области контента (например: Java, Linux, WebSphere). Кнопка Мои теги отображает Ваши теги для данной области контента (например: Java, Linux, WebSphere).