Шаг 169.
Python: сборник рецептов.
Метапрограммирование. Захват порядка определения атрибутов класса

    На этом шаге мы рассмотрим особенности решения этой задачи.

Задача

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

Решение

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

>>> from collections import OrderedDict
>>> # Набор дескрипторов для различных типов
>>> class Typed:
    _expected_type = type(None)
    def __init__(self, name=None):
        self._name = name

    def __set__(self, instance, value):
        if not isinstance(value, self._expected_type):
            raise TypeError('Expected ' +str(self._expected_type))
        instance.__dict__[self._name] = value

>>> class Integer(Typed):
    _expected_type = int

>>> class Float(Typed):
    _expected_type = float

>>> class String(Typed):
    _expected_type = str

>>> # Метакласс, который использует OrderedDict для тела класса
>>> class OrderedMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        d = dict(clsdict)
        order = []
        for name, value in clsdict.items():
            if isinstance(value, Typed):
                value._name = name
                order.append(name)
        d['_order'] = order
        return type.__new__(cls, clsname, bases, d)

    @classmethod
    def __prepare__(cls, clsname, bases):
        return OrderedDict()

>>> 

    В этом метаклассе порядок определения дескрипторов захватывается путем использования OrderedDict при выполнении тела класса. Получившийся порядок имен затем извлекается из словаря и сохраняется в атрибуте класса order. Далее он может быть использован методами класса самыми разнообразными способами. Например, вот простой класс, который использует эту информацию о порядке для реализации метода для сериализации данных экземпляра в строки CSV-данных:

>>> class Structure(metaclass=OrderedMeta):
    def as_csv(self):
        return ','.join(str(getattr(self,name)) for name in self._order)

>>> # Пример использования
>>> class Stock(Structure):
    name = String()
    shares = Integer()
    price = Float()
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

>>> 

    Вот как работает этот класс Stock:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.as_csv()
'GOOG,100,490.1'
>>> t = Stock('AAPL', 'a lot', 610.23)
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>
    t = Stock('AAPL', 'a lot', 610.23)
  File "<pyshell#11>", line 7, in __init__
    self.shares = shares
  File "<pyshell#2>", line 8, in __set__
    raise TypeError('Expected ' +str(self._expected_type))
TypeError: Expected <class 'int'>
>>> 


Обсуждение

    Краеугольный камень данного рецепта - метод prepare(), который определен в метаклассе OrderedMeta. Этот метод немедленно вызывается при старте определения класса вместе с именем класса и базовыми классами. Он должен вернуть объект отображения для использования во время выполнения тела класса. Порядок определения легко захватить и сохранить с помощью возвращения OrderedDict вместо обычного словаря.

    Можно дополнительно расширить эту функциональность, если вы хотите сделать свои собственные "словареподобные" объекты. Например, рассмотрите такой вариант решения, которое отвергает дублирующиеся определения:

>>> from collections import OrderedDict
>>> class NoDupOrderedDict(OrderedDict):
    def __init__(self, clsname):
        self.clsname = clsname
        super().__init__()
    def __setitem__(self, name, value):
        if name in self:
            raise TypeError('{} already defined in {}'.format(name, self.clsname))
        super().__setitem__(name, value)

>>> class OrderedMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        d = dict(clsdict)
        d['_order'] = [name for name in clsdict if name[0] != '_']
        return type.__new__(cls, clsname, bases, d)

    @classmethod
    def __prepare__(cls, clsname, bases):
        return NoDupOrderedDict(clsname)

>>> 

    Вот что произойдет, если вы используете этот метакласс и создадите класс с дублирующимися записями:

>>> class A(metaclass=OrderedMeta):
    def spam(self):
        pass

    print('**** A type error is expected now:')
    def spam(self):
        pass

**** A type error is expected now:
Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    class A(metaclass=OrderedMeta):
  File "<pyshell#22>", line 6, in A
    def spam(self):
  File "<pyshell#19>", line 7, in __setitem__
    raise TypeError('{} already defined in {}'.format(name, self.clsname))
TypeError: spam already defined in A
>>> 

    Последний важный аспект этого рецепта касается обращения с модифицированным словарем в методе метакласса __new__(). Хотя класс определен с использованием альтернативного словаря, вы все равно должны преобразовать этот словарь в правильный экземпляр dict при создании конечного объекта класса. В этом назначение инструкции d = dict(clsdict).

    Возможность захвата порядка определения - аспект, который весьма важен для некоторых приложений. Например, в реализации объектно-реляционного отображения (object relational mapper - ORM) классы могут быть написаны в стиле, который похож на показанный в примере:

class Stock(Model): 
    name = String() 
    shares = Integer() 
    price = Float()

    Лежащий в основе код может захотеть захватить порядок определения, чтобы отобразить объекты на кортежи или строки в таблице базы данных (похожим образом работает метод as_esv() в примере). Показанное решение весьма бесхитростно и чаще будет проще альтернатив (обычно это поддержание скрытых счетчиков в классах-дескрипторах).

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




Предыдущий шаг Содержание Следующий шаг