Глава 10. Объектно-ориентированное программирование
мер арифметических. В таких ситуациях удобнее воспользоваться богатым набором встроенных операторов Python.
В этом разделе показано, как использовать перегрузку операторов для определения того, как операторы Python должны работать с объектами ваших типов. Вы уже часто использовали перегрузку операторов для разных типов:
ØØ
оператор + для суммирования числовых значений, конкатенации списков, конкатенации строк и прибавления значения к каждому элементу массива NumPy;
ØØ
оператор [] для обращения к элементам списков, кортежей, строк и массивов, а также для обращения к значению, связанному с конкретным ключом в словаре;
ØØ
оператор * для умножения числовых значений, повторения последовательностей и умножения каждого элемента в массивах NumPy на конкретное значение.
Большую часть операторов можно перегружать. Для каждого перегружаемого оператора класс object определяет специальный метод: например, __add__ для оператора сложения (+) или __mul__ для оператора умножения (*). Переопределение этих методов позволит вам определить, как этот оператор должен работать с объектами вашего класса. За полным списком специальных методов обращайтесь по адресу:
https://docs.python.org/3/reference/datamodel.html#special-method-names
Ограничения перегрузки операторов
Для перегрузки операторов установлены следующие ограничения:
ØØ
Приоритет операторов не может изменяться при перегрузке. Тем не менее круглые скобки позволяют принудительно установить нужный порядок вычисления выражения.
ØØ
Группировка операторов (слева направо или справа налево) не может изменяться при перегрузке.
ØØ
«Арность» оператора (признак, указывающий на то, является оператор унарным или бинарным) изменяться не может.
ØØ
Создавать новые операторы нельзя — возможна только перегрузка существующих операторов.
10.10. Перегрузка операторов 443
ØØ
Смысл операции, выполняемой оператором с объектами встроенных типов, изменяться не может. Например, вы не сможете изменить оператор + так, чтобы он вычитал одно целое число из другого.
ØØ
Перегрузка операторов работает только с объектами пользовательских классов или в комбинациях объекта пользовательского класса и объекта встроенного типа.
Комплексные числа
Для демонстрации перегрузки операторов мы определим класс Complex, представляющий комплексное число1. Комплексные числа (например, –3 + 4i или 6,2 – 11,73i) записываются в форме
действительнаяЧасть + мнимаяЧасть * i,
где i — квадратный корень из –1. Комплексные числа, как и int, float и Decimal, являются арифметическими типами. В этом разделе мы создадим класс Complex, который перегружает только оператор сложения + и расширенное присваивание +=, поэтому объекты Complex можно складывать с использованием синтаксиса математических операций Python.
10.10.1. Класс Complex в действии
Для начала воспользуемся классом Complex для демонстрации его возможностей. Класс будет подробно рассмотрен в следующем разделе. Импортируйте класс Complex из файла complexnumber.py:
In [1]: from complexnumber import Complex
Затем создадим и выведем пару объектов Complex. Фрагменты [3] и [5] неявно вызывают метод __repr__ класса Complex для получения строкового представления каждого объекта:
In [2]: x = Complex(real=2, imaginary=4)
In [3]: x
Out[3]: (2 + 4i)
1 В Python существует встроенная поддержка комплексных чисел, так что класс создается исключительно в демонстрационных целях.
444 Глава 10. Объектно-ориентированное программирование
In [4]: y = Complex(real=5, imaginary=-1)
In [5]: y
Out[5]: (5 - 1i)
Мы выбрали формат строки __repr__, показанный в фрагментах [3] и [5], для имитации строк __repr__, генерируемых встроенным типом Python complex1.
Теперь воспользуемся оператором + для суммирования объектов x и y класса Complex. Выражение суммирует действительные части двух операндов (2 и 5) и мнимые части двух операндов (4i и -1i), после чего возвращает новый объект Complex с результатом:
In [6]: x + y
Out[6]: (7 + 3i)
Оператор + не изменяет ни один из своих операндов:
In [7]: x
Out[7]: (2 + 4i)
In [8]: y
Out[8]: (5 - 1i)
Наконец, воспользуемся оператором += для прибавления y к x и сохранения результата в x. Оператор += изменяет свой левый операнд, но не изменяет правый:
In [9]: x += y
In [10]: x
Out[10]: (7 + 3i)
In [11]: y
Out[11]: (5 - 1i)
10.10.2. Определение класса Complex
Итак, вы увидели класс Complex в действии. Теперь обратимся к его определению и посмотрим, как были реализованы эти возможности.
1 Python использует j вместо i для обозначения √–1 . Например, 3+4j (без пробелов вокруг оператора) создает объект complex с атрибутами real и imag. Строка __repr__ для этого значения complex имеет вид '(3+4j)'.
10.10. Перегрузка операторов 445
Метод __init__
Метод __init__ получает параметры для инициализации атрибутов данных, представляющих действительную и мнимую часть:
1 # complexnumber.py
2 """Класс Complex с перегруженными операторами."""
3
4 class Complex:
5 """Класс Complex, представляющий комплексное число
6 с действительной и мнимой частью."""
7
8 def __init__(self, real, imaginary):
9 """Инициализирует атрибуты класса Complex."""
10 self.real = real
11 self.imaginary = imaginary
12
Перегруженный оператор +
Следующий перегруженный специальный метод __add__ определяет, как оператор + должен работать с двумя объектами Complex:
13 def __add__(self, right):
14 """Переопределяет оператор +."""
15 return Complex(self.real + right.real,
16 self.imaginary + right.imaginary)
17
Методы, перегружающие бинарные операторы, должны предоставить два параметра: первый (self) предназначен для левого, а второй (right) — для правого операнда. Метод __add__ класса Complex получает два объекта Complex в аргументах и возвращает новый объект Complex с суммами действительных и мнимых частей операндов.
Содержимое исходных операндов при этом не изменяется, что соответствует нашим интуитивным представлениям о том, как должен вести себя этот оператор. Суммирование двух чисел не изменяет ни одно из исходных слагаемых.
Перегруженное расширенное присваивание +=
Строки 18–22 перегружают специальный метод __iadd__ для определения того, как оператор += должен суммировать два объекта Complex:
446 Глава 10. Объектно-ориентированное программирование
18 def __iadd__(self, right):
19 """Перегружает оператор +=."""
20 self.real += right.real
21 self.imaginary += right.imaginary
22 return self
23
Расширенное присваивание изменяет свои левые операнды, поэтому метод __iadd__ изменяет объект self, соответствующий левому операнду, после чего возвращает self.
Метод __repr__
Строки 24–28 возвращают строковое представление объекта Complex.
24 def __repr__(self):
25 """Возвращает строковое представление для repr()."""
26 return (f'({self.real} ' +
27 ('+' if self.imaginary >= 0 else '-') +
28 f' {abs(self.imaginary)}i)')
10.11. Иерархия классов исключений и пользовательские исключения
В предыдущей главе был описан механизм обработки исключений. Каждое исключение представляет собой объект класса, входящего в иерархию классов исключений Python1, или объект класса, производного от одного из этих классов. Классы исключений наследуют — непосредственно или опосредованно — от базового класса BaseException и определяются в модуле exceptions.
В Python определены четыре основных подкласса BaseException — SystemExit, KeyboardInterrupt, GeneratorExit и Exception:
ØØ
Исключения SystemExit завершают выполнение программы (или завершают интерактивный сеанс); если исключение остается неперехваченным, для него не выводится трассировка, как для других типов исключений.
1 https://docs.python.org/3/library/exceptions.html.
10.11. Иерархия классов исключений и пользовательские исключения 447
ØØ
Исключения KeyboardInterrupt происходят при вводе пользователем команды прерывания — Ctrl + C (или control + C) в большинстве систем.
ØØ
Исключения GeneratorExit происходят при закрытии генератора — как правило, когда генератор завершает производство значений или его метод close вызывается явно.
ØØ
Exception — базовый класс для всех основных исключений, с которыми вы столкнетесь. Вы уже видели исключения различных подклассов Exception: ZeroDivisionError, NameError, ValueError, StatisticsError, TypeError, IndexError, KeyError, RuntimeError и AttributeError. Часто исключения StandardError можно перехватить и обработать, чтобы программа продолжила выполнение.
Перехват исключений базовых классов
Одно из преимуществ иерархии классов исключений заключается в том, что обработка исключений может перехватывать исключения конкретного типа или использовать тип базового класса для перехвата исключений базового класса и исключений всех его подклассов. Например, обработчик исключений базового класса Exception может перехватывать объекты любого подкласса Exception. Размещение обработчика, перехватывающего тип Exception, до других обработчиков except является логической ошибкой, потому что все исключения будут перехвачены еще до достижения других обработчиков. Таким образом, все последующие обработчики будут недостижимы.
Пользовательские классы исключений
Когда вы выдаете исключение в своем коде, обычно рекомендуется использовать один из существующих классов исключений из стандартной библиотеки Python. Тем не менее при помощи средств наследования, представленных ранее в этой главе, вы сможете создавать собственные классы исключений, наследующие (непосредственно или опосредованно) от класса Exception. В общем случае так поступать не рекомендуется, особенно начинающим программистам. Прежде чем создавать пользовательские классы исключений, поищите существующие классы исключений в иерархии исключений Python. Новые классы исключений должны определяться только в случае, если правила перехвата и обработки этих исключений отличаются от других существующих типов исключений (что встречается достаточно редко).
448 Глава 10. Объектно-ориентированное программирование
10.12. Именованные кортежи
Мы использовали кортежи для сведения нескольких атрибутов данных в один объект. Модуль collections стандартной библиотеки Python также предоставляет именованные кортежи, позволяющие ссылаться на компоненты кортежа по именам (вместо индексов).
Создадим простой именованный кортеж, который может использоваться для представления карты в колоде. Начнем с импортирования функции namedtuple:
In [1]: from collections import namedtuple
Функция namedtuple создает подкласс встроенного типа кортежа. В первом аргументе функции передается имя нового типа, а во втором — список строк, представляющих идентификаторы, которые будут использоваться для обращения к компонентам нового типа:
In [2]: Card = namedtuple('Card', ['face', 'suit'])
У вас появился новый тип кортежа с именем Card, который может использоваться в любом месте, где может использоваться кортеж. Создадим объект Card, обратимся к его компонентам и выведем его строковое представление:
In [3]: card = Card(face='Ace', suit='Spades')
In [4]: card.face
Out[4]: 'Ace'
In [5]: card.suit
Out[5]: 'Spades'
In [6]: card
Out[6]: Card(face='Ace', suit='Spades')
Другие возможности именованных кортежей
Каждый тип именованного кортежа содержит дополнительные методы. Метод класса _make этого типа (то есть метод, вызываемый для класса) получает итерируемый объект значений и возвращает объект с типом именованного кортежа:
In [7]: values = ['Queen', 'Hearts']
In [8]: card = Card._make(values)
In [9]: card
Out[9]: Card(face='Queen', suit='Hearts')
10.13. Краткое введение в новые классы данных Python 3.7 449
Например, это может быть полезно при работе с типом именованного кортежа, представляющим записи в CSV-файле. При чтении и разборе на лексемы записей CSV их можно преобразовать в объекты именованных кортежей.
Для заданного объекта с типом именованного кортежа можно получить представление имен и значений объекта в виде словаря OrderedDict. OrderedDict сохраняет порядок вставки пар «ключ-значение» в словарь:
In [10]: card._asdict()
Out[10]: OrderedDict([('face', 'Queen'), ('suit', 'Hearts')])
За дополнительной информацией о возможностях именованных кортежей обращайтесь по адресу:
https://docs.python.org/3/library/collections.html#collections.namedtuple
10.13. Краткое введение в новые классы данных Python 3.7
Хотя именованные кортежи позволяют ссылаться на свои компоненты по имени, они остаются кортежами, а не классами. Если вы хотите пользоваться преимуществами именованных кортежей, а также возможностями, предоставляемыми традиционными классами Python, то имеет смысл воспользоваться новыми классами данных1 Python 3.7 из модуля dataclasses стандартной библиотеки Python.
Классы данных входят в число важнейших нововведений Python 3.7. Они помогают быстрее строить классы с использованием более компактной записи и с автоматическим генерированием «шаблонного» кода, общего для большинства классов. Они могут стать предпочтительным вариантом определения большинства классов Python. В этом разделе будут представлены основные принципы классов данных. В конце раздела будут приведены ссылки на дополнительную информацию.
Классы данных и автоматическое генерирование кода
Большинство классов, которые вы определяете, предоставляют метод __init__ для создания и инициализации атрибутов объекта, а также метод __repr__ для
1 https://www.python.org/dev/peps/pep-0557/.
450 Глава 10. Объектно-ориентированное программирование
определения нестандартного строкового представления объекта. Если класс содержит много атрибутов данных, создание этих атрибутов может стать довольно утомительным и однообразным.
Классы данных автоматически генерируют атрибуты данных, а также методы __init__ и __repr__ за вас. Это может быть особенно полезно для классов, основная задача которых — агрегирование взаимосвязанных элементов данных. Так, в приложении, обрабатывающем записи в формате CSV, может присутствовать класс, представляющий поля каждой записи в виде атрибутов данных объекта. Классы данных также могут генерироваться динамически по списку имен полей.
Классы данных также автоматически генерируют метод __eq__, который перегружает оператор ==. Любой класс, содержащий метод __eq__, также неявно поддерживает !=. Все классы наследуют реализацию по умолчанию метода __ne__ (не равно), который возвращает величину, обратную __eq__ (или NotImplemented, если класс не определяет __eq__). Классы данных не генерируют методы для операторов сравнения <, <=, > и >= автоматически, но такая возможность существует.
10.13.1. Создание класса данных Card
Давайте снова реализуем класс Card из раздела 10.6.2 в виде класса данных. Новый класс определяется в файле carddataclass.py. Определение класса данных потребует нового синтаксиса. В последующих разделах мы используем новый класс данных Card в классе DeckOfCards, чтобы показать, что он взаимозаменяем с исходным классом Card, а затем обсудим некоторые преимущества классов данных перед именованными кортежами и традиционными классами Python.
Импортирование из модулей dataclasses и typing
Модуль dataclasses стандартной библиотеки Python определяет декораторы и функции для реализации классов данных. Мы используем декоратор @dataclass (импортируемый в строке 4), чтобы указать, что новый класс является классом данных, и автоматически сгенерировать различный код. Вспомним, что исходный класс Card определял переменные класса FACES и SUITS, в которых хранились списки строк для инициализации Card. Мы используем ClassVar и List из модуля typing стандартной библиотеки Python (импортируется в строке 5) для обозначения того, что FACES и SUITS являются
10.13. Краткое введение в новые классы данных Python 3.7 451
переменными класса, которые содержат ссылки на списки. Вскоре мы поговорим о них более подробно:
1 # carddataclass.py
2 """Класс данных Card с атрибутами класса, атрибутами данных,
3 автоматически сгенерированными и явно определенными методами."""
4 from dataclasses import dataclass
5 from typing import ClassVar, List
6
Использование декоратора @dataclass
Чтобы указать, что класс является классом данных, поставьте перед его определением декоратор @dataclass1:
7 @dataclass
8 class Card:
Также декоратор @dataclass может содержать круглые скобки с аргументами, которые помогают классу данных определить, какие именно методы должны быть сгенерированы автоматически. Например, декоратор @dataclass(order=True) заставит класс данных автоматически сгенерировать методы для перегруженных операторов сравнения <, <=, > и >=. Это может пригодиться, если вы собираетесь сортировать свои объекты класса данных.
Аннотации переменных: атрибуты класса
В отличие от обычных классов, в классах данных как атрибуты класса, так и атрибуты данных объявляются внутри класса, но вне методов класса. В обычном классе только атрибуты класса объявляются таким способом, а атрибуты данных обычно создаются в __init__. Классам данных необходима дополнительная информация, или рекомендации (hints), с тем чтобы они могли отличить атрибуты класса от атрибутов данных, что также влияет на подробности реализации автоматически генерируемых методов.
Строки 9–11 определяют и инициализируют атрибуты класса FACES и SUITS:
9 FACES: ClassVar[List[str]] = ['Ace', '2', '3', '4', '5', '6', '7',
10 '8', '9', '10', 'Jack', 'Queen', 'King']
11 SUITS: ClassVar[List[str]] = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
12
1 https://docs.python.org/3/library/dataclasses.html#module-level-decorators-classes-and-functions.
452 Глава 10. Объектно-ориентированное программирование
Используемый в строках 9 и 11 синтаксис
: ClassVar[List[str]]
является аннотацией переменной12 (иногда называемой рекомендацией типа), которая указывает, что FACES является атрибутом класса (ClassVar), содержащим ссылку на список строк (List[str]). SUITS также является атрибутом класса, который содержит ссылку на список строк.
Переменные класса инициализируются в своих определениях и относятся к классу, а не к отдельным объектам класса. При этом методы __init__, __repr__ и __eq__ предназначены для использования с объектами класса. Когда класс данных генерирует эти методы, он анализирует все аннотации переменных и включает в реализацию метода только атрибуты данных.
Аннотации переменных: атрибуты данных
Обычно атрибуты данных создаются в методе __init__ класса (или методах, вызываемых __init__) присваиваниями в форме self.имя_атрибута = значение. Так как класс данных автоматически генерирует свой метод __init__, необходим другой способ задания атрибутов данных в определении класса данных. Вы не можете просто разместить их имена внутри класса, потому что при этом происходит ошибка NameError:
In [1]: from dataclasses import dataclass
In [2]: @dataclass
...: class Demo:
...: x # Попытка создания атрибута данных x
...:
-------------------------------------------------------------------------
NameError Traceback (most recent call last)
in ()
----> 1 @dataclass
2 class Demo:
3 x # Попытка создания атрибута данных x
4
1 https://www.python.org/dev/peps/pep-0526/.
2 Аннотации переменных — относительно новая языковая возможность, необязательная для обычных классов. В наследном коде Python они практически не встречаются.
10.13. Краткое введение в новые классы данных Python 3.7 453
in Demo()
1 @dataclass
2 class Demo:
----> 3 x # Попытка создания атрибута данных x
4
NameError: name 'x' is not defined
Каждый атрибут данных, как и атрибут класса, должен быть объявлен с аннотацией переменной. Строки 13–14 определяют атрибуты данных face и suit. Аннотация переменной ": str" означает, что каждый атрибут содержит ссылку на объект строки:
13 face: str
14 suit: str
Определение свойств и других методов
Классы данных являются классами, то есть они могут содержать свойства и методы и участвовать в иерархиях классов. Для этого класса данных Card мы определяем то же свойство image_name, доступное только для чтения, и специальные методы __str__ и __format__, как в исходном классе Card ранее в этой главе:
15 @property
16 def image_name(self):
17 """Возвращает имя файла с изображением карты."""
18 return str(self).replace(' ', '_') + '.png'
19
20 def __str__(self):
21 """Возвращает строковое представление для str()."""
22 return f'{self.face} of {self.suit}'
23
24 def __format__(self, format):
25 """Возвращает отформатированное строковое представление."""
26 return f'{str(self):{format}}'
Примечания по поводу аннотаций переменных
Аннотации переменных могут задаваться с использованием имен встроенных типов (например, str, int и float), типов классов или типов, определенных в модуле typing (вроле приведенных выше ClassVar и List). Даже с аннотациями типов Python является языком с динамической типизацией. Таким об454