Содержание


Программирование метаклассов в Python

Переход объектно-ориентированного программирования на следующий уровень

Comments

Обзор объектно-ориентированного программирования

Давайте начнем с краткого обзора того, что представляет собой ООП. В объектно-ориентированных языках программирования вы можете определить классы, которые соединяют связанные между собой данные и модели поведения. Классы могут наследовать некоторые или все характеристики от родительских классов, но в них могут также определяться собственные атрибуты (данные) и методы (модели поведения). В конечном счете классы обычно служат шаблонами для создания экземпляров (иногда также называемых просто объектами). Как правило, в различных экземплярах одного класса содержатся различные данные, но все они находятся в одной форме - например, у обоих объектов сотрудников Employeebob и jane есть заработная плата .salary и комната .room_number, но величина заработной платы и номер комнаты у них различаются.

Некоторые языки ООП, в том числе Python, предусматривают создание интроспективных объектов (также называются рефлексивными). Интроспективные объекты – это объекты, которые могут описывать сами себя: К какому классу принадлежит данный экземпляр? Какие родительские элементы есть у этого класса? Какие методы и атрибуты доступны данному объекту? Интроспекция позволяет функциям или методам, обрабатывающим объект, принимать решение на основе того, какой вид объекта обрабатывается. Даже без интроспекции функции часто ветвятся на основе данных экземпляра - например, путь к jane.room_number отличается от пути к bob.room_number, поскольку они находятся в разных комнатах. С помощью интроспекции вы можете безопасно рассчитать размер премии для jane, не выполняя при этом расчет для bob, например, потому, что у jane есть атрибут .profit_share , или потому, что bob является экземпляром подкласса Hourly(Employee).

Ответ метапрограммирования

Базовая система ООП, вкратце описанная выше, весьма мощна. Однако в приведенном описании просматривается еще один момент: классы в Python (и в других языках) сами являются объектами, которые могут быть переданы и интроспектированы. Поскольку объекты, как было отмечено выше, создаются с помощью классов, используемых в качестве шаблонов, то по каким шаблонам создаются классы? Ответ очевиден -- это метаклассы.

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

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

Python 1.5.2 (#0, Jun 27 1999, 11:23:01) [...]
Copyright 1991-1995 Stichting Mathematisch Centrum, Amsterdam
>>> def class_with_method(func):
...     class klass: pass
...     setattr(klass, func.__name__, func)
...     return klass
...
>>> def say_foo(self): print 'foo'
...
>>> Foo = class_with_method(say_foo)
>>> foo = Foo()
>>> foo.say_foo()
foo

Функция фабрики class_with_method() динамически создает и возвращает класс, который содержит методы и функции, переданные фабрике. Перед тем, как класс будет возвращен, функция производит с ним некоторые операции. В модуле new представлено более краткое написание, но без возможности собственного кода в теле фабрики классов, например:

>>> from new import classobj
>>> Foo2 = classobj('Foo2',(Foo,),{'bar':lambda self:'bar'})
>>> Foo2().bar()
'bar'
>>> Foo2().say_foo()
foo

Во всех этих случаях (Foo, Foo2) поведение класса не прописывается непосредственно в коде, вместо этого оно создается вызовом функции в реальном времени, с динамическими аргументами. Нужно подчеркнуть, что мы динамически создаем не экземпляры классов, а сами классы.

Метаклассы: решение, которое ищет проблему?

Метаклассы - это очень глубокая материя, о которой 99% пользователей даже не нужно задумываться. Если вы не понимаете, зачем они вам нужны – значит, они вам не нужны (люди, которым они на самом деле требуются, точно знают, что они им нужны, и им не нужно объяснять - почему). - Тим Питерс (Tim Peters), гуру по Python

Методы (классов), как и обычные функции, могут возвращать объекты. Поэтому в некотором смысле очевидно, что фабрики классов могут так же просто быть классами, как они могут быть функциями. В частности, в Python 2.2+ реализован специальный класс type, который является именно такой фабрикой классов. Конечно же, читатели вспомнят менее амбициозную функцию type(), встроенную в старые версии Python -- к счастью, поведение старой функции type() сохранено классом type (другими словами, type(obj) возвращает тип/класс объекта obj). Новый класс type работает как фабрика классов, точно так же, как функция new.classobj:

>>> X = type('X',(),{'foo':lambda self:'foo'})
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

Однако, поскольку теперь type - это (мета)класс, мы можем свободно создать его подкласс:

>>> class ChattyType(type):
...     def __new__(cls, name, bases, dct):
...         print "Allocating memory for class", name
...         return type.__new__(cls, name, bases, dct)
...     def __init__(cls, name, bases, dct):
...         print "Init'ing (configuring) class", name
...         super(ChattyType, cls).__init__(name, bases, dct)
...
>>> X = ChattyType('X',(),{'foo':lambda self:'foo'})
Allocating memory for class X
Init'ing (configuring) class X
>>> X, X().foo()
(<class '__main__.X'>, 'foo')

Магические методы .__new__() и .__init__() носят особый характер, но концептуально они такие же, как и у любого другого класса. Метод .__init__() позволяет настроить созданный объект; метод .__new__() позволяет настроить место его размещения. Последнее, конечно, используется не так часто, но возможно для любого класса, выполненного в новом стиле Python 2.2 (как правило, наследуется, но не замещается).

У потомков type есть одна особенность, требующая особого внимания; на ней спотыкаются все, кто первый раз работает с метаклассами. Первый аргумента этих методов обычно называется cls, а не self, потому что методы работают с созданным классом, а не с метаклассом. На самом деле здесь нет ничего особенного; все методы связываются со своими экземплярами, а экземпляром метакласса является класс. Ситуацию немного проясняет неспециальное имя:

>>> class Printable(type):
...     def whoami(cls): print "I am a", cls.__name__
...
>>> Foo = Printable('Foo',(),{})
>>> Foo.whoami()
I am a Foo
>>> Printable.whoami()
Traceback (most recent call last):
TypeError:  unbound method whoami() [...]

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

>>> class Bar:
...     __metaclass__ = Printable
...     def foomethod(self): print 'foo'
...
>>> Bar.whoami()
I am a Bar
>>> Bar().foomethod()
foo

Решения проблем с помощью магии

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

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

Первая, и, вероятно, более распространенная категория - когда на момент проектирования вы не знаете точно, что будет делать класс. Очевидно, у вас будут некоторые идеи на этот счет, но отдельные детали могут зависеть от информации, которая будет доступна только позже. Это "позже" может быть двух видов: (a) Когда приложение использует модуль библиотеки; (b) Когда в ходе работы возникает определенная ситуация. Эта категория близка к тому, что часто называется "аспектно-ориентированным программированием" (АОП). Мы продемонстрируем это на простом примере:

% cat dump.py
#!/usr/bin/python
import sys
if len(sys.argv) > 2:
    module, metaklass  = sys.argv[1:3]
    m = __import__(module, globals(), locals(), [metaklass])
    __metaclass__ = getattr(m, metaklass)

class Data:
    def __init__(self):
        self.num = 38
        self.lst = ['a','b','c']
        self.str = 'spam'
    dumps   = lambda self: `self`
    __str__ = lambda self: self.dumps()

data = Data()
print data

% dump.py
<__main__.Data instance at 1686a0>

Как и можно было бы предположить, это приложение выводит довольно общее описание объекта data (обычный объект экземпляра). Однако если этому приложению в реальном времени передать аргументы, мы можем получить совсем другой результат:

% dump.py gnosis.magic MetaXMLPickler
<?xml version="1.0"?>
<!DOCTYPE PyObject SYSTEM "PyObjects.dtd">
<PyObject module="__main__" class="Data" id="720748">
<attr name="lst" type="list" id="980012" >
  <item type="string" value="a" />
  <item type="string" value="b" />
  <item type="string" value="c" />
</attr>
<attr name="num" type="numeric" value="38" />
<attr name="str" type="string" value="spam" />
</PyObject>

В этом примере используется сериализация gnosis.xml.pickle, но в большинстве современных пакетов gnosis.magic также содержатся сериализаторы метаклассов MetaYamlDump, MetaPyPickler и MetaPrettyPrint. Кроме того, пользователь "приложения" dump.py может потребовать использования любого "MetaPickler" из любого пакета Python, где таковой предусмотрен. Текст метакласса, соответствующего этой цели, будет выглядеть следующим образом:

class MetaPickler(type):
    "Metaclass for gnosis.xml.pickle serialization"
    def __init__(cls, name, bases, dict):
        from gnosis.xml.pickle import dumps
        super(MetaPickler, cls).__init__(name, bases, dict)
        setattr(cls, 'dumps', dumps)

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

Пожалуй, самое распространенное применение метаклассов схоже с использованием MetaPicklers: добавление, удаление и замещение методов, определенных в созданном классе. В нашем примере "оригинальный" метод Data.dump() заменяется другим, созданным вне приложения, в момент создания класса Data (и, следовательно, в каждом последующем экземпляре).

Другие способы решения проблем с помощью магии

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

Примером декларативной среды, построенной на базе классов, является gnosis.xml.validity. В этой среде вы можете объявлять различные "классы проверки", которые выражают набор ограничений для правильно построенных документов XML. Эти объявления очень близки к объявлениям, содержащимся в DTD. Например, определение документа "диссертация" может выражаться следующим кодом:

from gnosis.xml.validity import *
class figure(EMPTY):      pass
class _mixedpara(Or):     _disjoins = (PCDATA, figure)
class paragraph(Some):    _type = _mixedpara
class title(PCDATA):      pass
class _paras(Some):       _type = paragraph
class chapter(Seq):       _order = (title, _paras)
class dissertation(Some): _type = chapter

Если вы попытаетесь создать экземпляр класса dissertation без необходимых вложенных элементов компонентов, будет возвращена описательная ошибка; то же самое верно и для каждого вложенного элемента. Правильные вложенные элементы будут созданы из более простых аргументов, когда есть только один способ недвусмысленного «поднятия» аргументов к правильному типу.

Даже несмотря на то, что классы проверки часто (неформально) основываются на уже существующих DTD, экземпляры этих классов выводят сами себя как неоформленные фрагменты документа XML, например:

>>> from simple_diss import *
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch
<chapter><title>It Starts</title>
<paragraph>When it began</paragraph>
</chapter>

Используя метаклассы для создания классов проверки, мы можем создать DTD из объявления класса (и добавить при этом к классам дополнительный метод):

>>> from gnosis.magic import DTDGenerator, \
...                          import_with_metaclass, \
...                          from_import
>>> d = import_with_metaclass('simple_diss',DTDGenerator)
>>> from_import(d,'**')
>>> ch = LiftSeq(chapter, ('It Starts','When it began'))
>>> print ch.with_internal_subset()
<?xml version='1.0'?>
<!DOCTYPE chapter [
<!ELEMENT figure EMPTY>
<!ELEMENT dissertation (chapter)+>
<!ELEMENT chapter (title,paragraph+)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT paragraph ((#PCDATA|figure))+>
]>
<chapter><title>It Starts</title>
<paragraph>When it began</paragraph>
</chapter>

Пакет gnosis.xml.validity ничего не знает о DTD и внутренних подмножествах. В целом эти концепции и возможности представлены метаклассом DTDGenerator, без каких-либо изменений в gnosis.xml.validity и simple_diss.py. DTDGenerator не подставляет собственный метод .__str__() в создаваемые им классы - вы все еще можете распечатать неоформленный фрагмент XML - но этот метакласс позволяет легко изменять такие магические методы.

Инструменты для метаклассов

В пакете gnosis.magic содержится несколько утилит для работы с метаклассами, а также несколько примеров метаклассов, которые можно использовать в аспект-ориентированном программировании. Наиболее важной из этих утилит является import_with_metaclass(). Эта функция, использованная в приведенном выше примере, позволяет вам импортировать модуль стороннего производителя, но создать все классы этого модуля с использованием собственного метакласса, а не type. Вы можете определить в создаваемом вами (или вообще взятом откуда-то еще) метаклассе любую новую возможность, которую вы хотите ввести в модуль стороннего производителя. gnosis.magic содержит несколько подключаемых метаклассов сериализации; в других пакетах имеются средства трассировки, сохранения объектов, протоколирования ошибок и т.п.

Функция import_with_metclass() иллюстрирует ряд достоинств программирования с использованием метаклассов:

def import_with_metaclass(modname, metaklass):
    "Module importer substituting custom metaclass"
    class Meta(object): __metaclass__ = metaklass
    dct = {'__module__':modname}
    mod = __import__(modname)
    for key, val in mod.__dict__.items():
        if inspect.isclass(val):
            setattr(mod, key, type(key,(val,Meta),dct))
    return mod

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

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