Шаг 174.
Python: сборник рецептов.
Метапрограммирование. Инициализация членов класса во время определения

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

Задача

    Вы хотите инициализировать части определения класса один раз, во время определения класса, а не при создании экземпляров.

Решение

    Выполнение действий по инициализации или начальному присваиванию значений во время определения класса - это классический пример использования метаклассов. Метакласс "срабатывает" как раз в момент определения, когда вы можете выполнить дополнительные шаги.

    Вот пример, который использует эту идею для создания классов, похожих на именованные кортежи из модуля 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, как создаются экземпляры, а также знанием точек, в которых вызываются различные методы метаклассов и классов.

    На следующем шаге мы рассмотрим реализацию множественной диспетчеризации с помощью аннотаций функций.




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