На этом шаге мы рассмотрим особенности решения этой задачи.
Вы хотите автоматически записывать порядок, в котором внутри тела класса определяются атрибуты и методы, что полезно при различных операциях (например, при сериалиации, отображении в базы данных и т. п.).
Захват информации о теле определения класса легко реализуется через использование метакласса. Вот пример метакласса, который использует 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() в примере). Показанное решение весьма бесхитростно и чаще будет проще альтернатив (обычно это поддержание скрытых счетчиков в классах-дескрипторах).
На следующем шаге мы рассмотрим определение метакласса, принимающего необязательные аргументы.