На этом шаге мы рассмотрим способ решения поставленной задачи.
Вы хотите инициализировать части определения класса один раз, во время определения класса, а не при создании экземпляров.
Выполнение действий по инициализации или начальному присваиванию значений во время определения класса - это классический пример использования метаклассов. Метакласс "срабатывает" как раз в момент определения, когда вы можете выполнить дополнительные шаги.
Вот пример, который использует эту идею для создания классов, похожих на именованные кортежи из модуля collections:
>>> import operator >>> class StructTupleMeta(type): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) for n, name in enumerate(cls._fields_): setattr(cls, name, property(operator.itemgetter(n))) >>> class StructTuple(tuple, metaclass=StructTupleMeta): _fields_ = [] def __new__(cls, *args): if len(args) != len(cls._fields_): raise ValueError('{} arguments required'.format(len(cls._fields_))) return super().__new__(cls,args) >>>
Этот код позволяет определять простые структуры на основе кортежей:
>>> class Stock(StructTuple): _fields_ = ['name', 'shares', 'price'] >>> class Point(StructTuple): _fields_ = ['x', 'y'] >>>
Вот как они работают:
>>> s = Stock('ACME', 50, 91.1) >>> s ('ACME', 50, 91.1) >>> s[0] 'ACME' >>> s.name 'ACME' >>> s.shares * s.price 4555.0 >>> s.shares = 23 Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> s.shares = 23 AttributeError: can't set attribute >>>
В этом рецепте класс StructTupleMeta принимает список имен атрибутов в атрибуте класса _fields и превращает их в методы свойства (property), которые осуществляют доступ к конкретному слоту кортежа. Функция operator.itemgetter() создает функцию доступа (акцессор), а функция property() превращает ее в свойство.
Главная сложность этого рецепта - знать, когда происходят различные шаги инициализации. Метод __init__() в StructTupleMeta вызывается только один раз для каждого определяемого класса. Аргумент cls - это класс, который только что был определен. Код использует переменную класса _fields, чтобы взять новый определенный класс и добавить в него несколько новых частей.
Класс StructTuple служит общим базовым классом для пользователей, которые его наследуют. Метод __new__() в этом классе отвечает за создание новых экземпляров. Использование __new__() здесь несколько необычное, поскольку частично связано с тем фактором, что мы модифицируем сигнатуру вызова кортежей, чтобы получить возможность создавать экземпляры с помощью обычно выглядящих вызовов:
s = Stock('ACME', 50, 91.1) # ОК s = Stock(('ACME', 50, 91.1)) # Ошибка
В отличие от __init__(), метод __new__() вызывается до того, как экземпляр создан. Поскольку кортежи неизменяемы, невозможно внести какие-либо изменения в них после создания. Функция __init__() запускается в процессе инициализации слишком поздно, чтобы выполнить эту задачу. Именно поэтому был определен метод __new__().
Хотя это короткий рецепт, его внимательное изучение вознаградит читателя глубоким пониманием того, как определяются классы в Python, как создаются экземпляры, а также знанием точек, в которых вызываются различные методы метаклассов и классов.
На следующем шаге мы рассмотрим реализацию множественной диспетчеризации с помощью аннотаций функций.