Проблемы, решаемые магией
До сих пор мы рассматривали основы метаклассов. Однако реальное применение метаклассов имеет свои тонкости. Сложность с использованием метаклассов заключается в том, что в типичной модели ООП классы в действительности делают не много. Структура наследования классов удобна для инкапсуляции и объединения данных и методов, но обычно реально используются именно экземпляры.
Существует две общие категории задач программирования, для которых, на наш взгляд, метаклассы являются чрезвычайно полезными.
Первый, и вероятно более общий случай, это когда в период проектирования вы не знаете точно, что нужно делать классу. Очевидно, что у вас появится представление об этом, но некая определенная деталь может зависеть от информации, которая станет доступной позднее. Само "позднее" может быть двух видов: (а) когда библиотечный модуль будет использоваться приложением; (б) во время исполнения, когда будет существовать некая ситуация. Этот категория близка к тому, что часто называют "Аспектно-ориентированным программированием" (АОП). Продемонстрируем, что мы имеем в виду на следующем элегантном примере:
Листинг 7. Конфигурирование метакласса во время исполнения
% 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 (условный объект экземпляра). Однако, если аргументы времени исполнения передаются в приложение, можно получить несколько отличный результат:
Листинг 8. Добавление метакласса внешней сериализации
% 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, который его определяет. Соответствующий метакласс, предназначенный для этой цели, будет выглядеть приблизительно так:
Листинг 9. Добавление атрибута с метаклассом
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 (и, следовательно, в каждом последующем экземпляре).