Шаг 140.
Python: сборник рецептов.
Классы и объекты. Упрощение инициализации структур данных

    На этом шаге мы рассмотрим особенности реализации такой инициализации.

Задача

    Вы создаете много классов, которые служат структурами данных, но уже устали от написания повторяющихся и шаблонных функций __init__().

Решение

    Часто вы можете обобщить инициализацию структур данных в единственной функции __init__(), определенной в общем базовом классе. Например:

>>> class Structure:
	# Переменная класса, которая определяет ожидаемые поля
	_fields= []
	def __init__(self, *args):
		if len(args) != len(self._fields):
			raise TypeError('Expected {} arguments'.format(len(self._fields)))
		# Устанавливает аргументы
		for name, value in zip(self._fields, args):
			setattr(self, name, value)

			
>>> # Пример определения класса
>>> class Stock(Structure):
	_fields = ['name', 'shares', 'price']

	
>>> class Point(Structure):
	_fields = ['x', 'y']

	
>>> class Circle(Structure):
	_fields = ['radius']
	def area(self):
		return math.pi * self.radius ** 2

>>>

    Если вы будете использовать эти классы, то обнаружите, что они легко конструируются. Например:

>>> s = Stock('ACME', 50, 91.1)
>>> p = Point(2, 3)
>>> c = Circle(4.5)
>>> s2 = Stock('ACME', 50)
Traceback (most recent call last):
  File "<pyshell#57>", line 1, in <module>
    s2 = Stock('ACME', 50)
  File "<pyshell#46>", line 6, in __init__
    raise TypeError('Expected {} arguments'.format(len(self._fields)))
TypeError: Expected 3 arguments
>>>

    Если вы решите реализовать поддержку именованных аргументов, то есть несколько способов проектирования реализации. Один из них - такое отображение именованных аргументов, чтобы они соответствовали только именам атрибутов, определенным в _fields. Например:

>>> class Structure:
	_fields= []
	def __init__(self, *args, **kwargs):
		if len(args) > len(self._fields):
			raise TypeError('Expected {} arguments'.format(len(self._fields)))
		# Установка всех позиционных аргументов
		for name, value in zip(self._fields, args):
			setattr(self, name, value)
		# Установка оставшихся именованных аргументов
		for name in self._fields[len(args):]:
			setattr(self, name, kwargs.pop(name))
		# Проверка на оставшиеся любые другие аргументы
		if kwargs:
			raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs)))

		
>>> # Пример использования
>>> class Stock(Structure):
	_fields = ['name', 'shares', 'price']

	
>>> s1 = Stock('ACME', 50, 91.1)
>>> s2 = Stock('ACME', 50, price=91.1)
>>> s3 = Stock('ACME', shares=50, price=91.1)
>>>

    Другой возможный выбор - использование именованных аргументов как средства добавления дополнительных атрибутов, не определенных в _fields, к структуре. Например:

>>> class Structure:
	# Переменная класса, которая определяет ожидаемые поля
	_fields= []
	def __init__(self, *args, **kwargs):
		if len(args) != len(self._fields):
			raise TypeError('Expected {} arguments'.format(len(self._fields)))
		# Установка аргументов
		for name, value in zip(self._fields, args):
			setattr(self, name, value)
		# Установка дополнительных аргументов (если они есть)
		extra_args = kwargs.keys() - self._fields
		for name in extra_args:
			setattr(self, name, kwargs.pop(name))
		if kwargs:
			raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))

		
>>> # Пример использования
>>> class Stock(Structure):
	_fields = ['name', 'shares', 'price']

	
>>> s1 = Stock('ACME', 50, 91.1)
>>> s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
>>>


Обсуждение

    Прием определения метода __init__() общего назначения может оказаться чрезвычайно полезным, если вы когда-либо будете писать программу, построенную на основе большого количества маленьких структур данных. Так вы напишете намного меньше кода, чем при ручном создании таких методов __init__():

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Circle:
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return math.pi * self.radius ** 2

    Тонкий аспект реализации касается механизма, который используется для присваивания значений с помощью функции setattr(). Вместо этого вы можете подумать об использовании к прямому доступу к словарю экземпляра. Например:

class Structure:
    # Переменная класса, которая определяет ожидаемые поля
    _fields= []

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Установка аргументов (дополнительная)
        self.__dict__.update(zip(self._fields,args))

    Хотя это работает, часто небезопасно делать предположения о реализации подкласса. Если подкласс решит использовать __slots__ или обернет конкретный атрибут в свойство (или дескриптор), прямой доступ к словарю экземпляра ломается. Приведенное в рецепте решение обладает максимально общей областью применения и не делает предположений по поводу подклассов.

    Потенциальный недостаток этого приема заключается в воздействии на документацию и справочные возможности IDE. Если пользователь вызывает справку по конкретному классу, требуемые аргументы не будут описаны обычным способом. Например:

>>> help(Stock)
Help on class Stock in module __main__:

class Stock(Structure)
. . .
| Methods inherited from Structure: 
|
| __init__(self, *args, **kwargs)
|
. . .
>>>

    Многие из этих проблем могут быть исправлены путем прикрепления или принудительного использования сигнатуры типов в функции __init__().

    Также нужно отметить, что можно автоматически инициализировать переменные экземпляра, используя вспомогательную функцию и так называемый "framehack". Например:

def init_fromlocals(self):
    import sys
    locs = sys._getframe(1).f_locals
    for k, v in locs.items():
         if k != 'self':
            setattr(self, k, v)

class Stock:
    def __init__(self, name, shares, price):
        init_fromlocals(self)

    В этом варианте функция init_fromlocals() использует sys._getframe(), чтобы подсмотреть локальные переменные вызывающего метода. Если использовать ее как первый шаг в методе __init__(), локальные переменные будут такими же, как и переданные аргументы, и смогут быть легко использованы для установки атрибутов с такими же именами. Хотя этот подход обходит проблему получения правильной сигнатуры вызова в IDE, он работает на 50% медленнее, чем представленное в рецепте решение, и использует больше сложной "подкапотной" магии. Если вашему коду не нужна эта дополнительная способность, в большинстве случаев более простое решение работает просто отлично.

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




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