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