КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 712687 томов
Объем библиотеки - 1401 Гб.
Всего авторов - 274526
Пользователей - 125069

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Шенгальц: Черные ножи (Альтернативная история)

Читать не интересно. Стиль написания - тягомотина и небывальщина. Как вы представляете 16 летнего пацана за 180, худого, болезненного, с больным сердцем, недоедающего, работающего по 12 часов в цеху по сборке танков, при этом имеющий силы вставать пораньше и заниматься спортом и тренировкой. Тут и здоровый человек сдохнет. Как всегда автор пишет о чём не имеет представление. Я лично общался с рабочим на заводе Свердлова, производившего

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

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

В начале

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

Вообще-то если честно, то я даже не собирался брать эту книгу... Однако - отсутствие иного выбора и низкая цена (после 3 или 4-го захода в книжный) все таки "сделали свое черное дело" и книга была куплена))

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

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

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

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).

Объектно-ориентированное программирование с помощью Python [Ирв Кальб] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

IRV KALB

OBJECT-ORIENTED

PYTHON
MASTER OOP BY
BUILDING GAMES AND GUIS

ИРВ КАЛЬБ

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАНИЕ С ПОМОЩЬЮ

PYTHON

2024

УДК 004.42
ББК 32.973.26-018.2
К17

Object-Oriented Python: Master OOP by Building Games and GUIs
Irv Kalb
Copyright © 2022 by Irv Kalb. Title of English-language original: Object-Oriented Python:
Master OOP by Building Games and GUIs, ISBN 9781718502062,
published by No Starch Press Inc. 2458th Street, San Francisco, California United States 94103.
The Russian-language edition. Copyright © 2024 by Eksmo Publishing House under license
by No Starch Press Inc. All rights reserved.

К17

Кальб, Ирв.
Объектно-ориентированное программирование с помощью
Python / Ирв Кальб ; [перевод с английского М. А. Райтмана]. —
Москва : Эксмо, 2024. — 512 с. — (Мировой компьютерный бестселлер).
ISBN 978-5-04-186627-3
Объектно-ориентированное программирование (ООП) — это метод,
основанный на представлении программы в виде совокупности объектов,
каждый из которых является экземпляром определенного класса, а классы
образуют иерархию наследования, что позволяет по-другому думать о вычислительных задачах и решать их с возможностью многократного использования. «Объектно-ориентированное программирование с помощью Python»
предназначено для программистов среднего уровня и представляет собой
практическое руководство, которое глубоко изучает основные принципы
ООП и показывает, как использовать инкапсуляцию, полиморфизм и наследование для написания игр и приложений с использованием Python.
Книга начинается с рассказа о ключевых проблемах, присущих процедурному программированию, затем вы познакомитесь с основами создания
классов и объектов в Python.
Затем вы научитесь создавать графические интерфейсы c помощью
pygame, благодаря чему вы сможете писать интерактивные игры и приложения с виджетами графического пользовательского интерфейса (GUI), анимацией, различными сценами и многоразовой игровой логикой.
УДК 004.42
ББК 32.973.26-018.2

ISBN 978-5-04-186627-3

© Райтман М.А., перевод на русский язык, 2024
© Оформление. ООО «Издательство «Эксмо», 2024

Моей замечательной жене Дорин.
Ты клей, который держит нашу семью вместе.
Много лет назад я сказал: «Я делаю», но я имел в виду: «Я сделаю».

ОБ А ВТОРЕ

Ирв Кальб — профессор в UCSC Silicon Valley Extension и Университете Кремниевой долины (ранее Политехнический
колледж Когсвелла), где он преподает вводные курсы программирования и курсы объектно- ориентированного программирования на языке Python. Ирв имеет степени бакалавра и магистра в области компьютерных наук, более 30 лет занимается
объектно- ориентированным программированием на различных языках и более 10 лет преподает. У него десятилетний
опыт разработки программного обеспечения с акцентом
на образовательное ПО. Как Furry Pants Productions он и его
жена создали и выпустили два обучающих диска с персонажем — далматинцем Дарби в главной роли. Ирв также является
автором Learn to Program with Python 3: A Step-by- Step Guide to
Programming («Учимся программировать на Python 3. Пошаговое руководство по программированию»).
Ирв активно участвовал в раннем развитии спорта Ultimate
®
Frisbee . Он возглавил создание многих версий официального
сборника правил и стал соавтором и издателем первой книги
об этом виде спорта — Ultimate: Fundamentals of the Sport
(«Ultimate: Основы спорта»).

6 Об авторе

О ТЕ Х НИЧЕС КОМ А ВТОРЕ

Монте Давидофф — независимый консультант по разработке
программного обеспечения. Его области специализации включают DevOps и Linux. Монте программирует на Python уже
более 20 лет. Он использовал Python для разработки разнообразного программного обеспечения, включая критически важные для бизнеса приложения и встроенное ПО.

О техническом авторе 7

К РАТКОЕ СОД Е РЖ А НИЕ

Об авторе

.......................................................................................................

О техническом авторе
Благодарности
Введение

.......................................................................................

6
7

...............................................................................................

17

.......................................................................................................

19

Часть I. Введение в объектно-ориентированное
программирование
1. Процедурные примеры Python

29

......................................................................

31

2. Моделирование физических объектов с помощью
объектно-ориентированного программирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3. Мысленные модели объектов и значение self
4. Управление несколькими объектами

..................................................

............................................................

Часть II. Графические пользовательские
интерфейсы с pygame
5. Введение в pygame

7. Виджеты pygame GUI

101

139

...................................................................................

6. Объектно-ориентированный pygame

89

141

...........................................................

183

................................................................................

211

Часть III. Инкапсуляция, полиморфизм
и наследование

233

8. Инкапсуляция

...........................................................................................

235

9. Полиморфизм

..........................................................................................

263

10. Наследование

........................................................................................

11. Управление памятью, используемой объектами

..........................................

Часть IV. Использование ООП в разработке игр
12. Карточные игры
13. Таймеры
8 Краткое содержание

297
335

363

......................................................................................

365

.................................................................................................

381

14. Анимация
15. Сцены

...............................................................................................

399

....................................................................................................

419

16. Полноценная игра: Dodger

.......................................................................

17. Шаблоны проектирования и резюме
Предметный указатель

457

.........................................................

491

..................................................................................

503

ПОД РОБНОЕ СОД Е РЖ А НИЕ

Об авторе

.......................................................................................................

О техническом авторе
Благодарности

.......................................................................................

6
7

...............................................................................................

17

Введение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Для кого эта книга? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Версия(-и) Python и установка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Как я объясняю ООП? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Структура книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Среды разработки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Виджеты и примеры игр . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

19
20
20
22
23
26
26

Часть I. Введение в объектно-ориентированное
программирование

29

1. Процедурные примеры Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Карточная игра «Больше-меньше» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Представление данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Реализация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Повторное использование кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Моделирование банковского счета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Анализ необходимых операций и данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Реализация 1. Одна учетная запись без функций . . . . . . . . . . . . . . . . . . . . .
Реализация 2. Одна учетная запись с функциями . . . . . . . . . . . . . . . . . . . . .
Реализация 3. Два счета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Реализация 4. Несколько счетов с использованием списков . . . . . .
Реализация 5. Список словарей учетных записей . . . . . . . . . . . . . . . . . . . .
Общие проблемы с процедурной реализацией . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Объектно-ориентированное решение — первый взгляд на класс . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31
32
32
33
35
36
36
37
39
42
44
47
50
51
52

2. Моделирование физических объектов с помощью
объектно-ориентированного программирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Построение программных моделей физических объектов . . . . . . . . . . . . . . . . . . . . . . . . .
Состояние и поведение: пример выключателя освещения . . . . . . . . . .
Введение в классы и объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Классы, объекты и экземпляры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Написание класса на Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55
56
56
58
60
61

10 Подробное содержание

Область видимости и переменные экземпляра . . . . . . . . . . . . . . . . . . . . . . . . .
Различия между функциями и методами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание объекта из класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Вызов методов объекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание нескольких экземпляров из одного класса . . . . . . . . . . . . . . . .
Типы данных Python реализованы как классы . . . . . . . . . . . . . . . . . . . . . . . . . .
Определение объекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание несколько более сложного класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Представление более сложного физического объекта как класса . . . . . . . . . . . . . .
Передача аргументов методу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Несколько экземпляров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Параметры инициализации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Использование классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ООП как решение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

63
64
66
66
67
69
70
70
73
79
81
83
85
86
86

3. Мысленные модели объектов и значение self . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Повторный обзор класса DimmerSwitch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Высокоуровневая мысленная модель № 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Более глубокая мысленная модель № 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
В чем смысл слова «self»? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89
90
91
92
95
99

4. Управление несколькими объектами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс банковского счета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Импорт кода класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание тестового кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание нескольких учетных записей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Несколько объектов учетной записи в списке . . . . . . . . . . . . . . . . . . . . . . . . .
Несколько объектов с уникальными идентификаторами . . . . . . . . . .
Создание интерактивного меню . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание объекта диспетчера объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создание объекта диспетчера объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основной код, создающий объект диспетчера объектов . . . . . . . . .
Лучшая обработка ошибок с исключениями . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
try и except . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Инструкция raise и пользовательские исключения . . . . . . . . . . . . . . . . . .
Использование исключений в нашей банковской программе . . . . . . . . . . . . . . . . . .
Класс счета с исключениями . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Оптимизированный класс банка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основной код, обрабатывающий исключения . . . . . . . . . . . . . . . . . . . . . . .
Вызов одного и того же метода для списка объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Интерфейс по сравнению с реализацией . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Часть II. Графические пользовательские
интерфейсы с pygame

101
101
105
107
107
110
112
115
118
120
123
125
125
126
128
128
130
132
134
136
137

139

5. Введение в pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Устанавливаем Pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Детали окон . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Система координат окна . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Цвета пикселей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

141
142
143
144
147

Подробное содержание

11

Программы, управляемые событиями . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Используем Pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем пустое окно . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Рисуем изображение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Обнаруживаем щелчок мыши . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Обрабатываем клавиатуру . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем анимацию, основанную на местоположении . . . . . . . . . . . . .
Используем rect pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Воспроизводим звуки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Воспроизводим звуковые эффекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Воспроизведение фоновой музыки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Рисуем фигуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Справка по примитивным фигурам . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

148
150
151
155
158
161
167
169
173
173
175
176
179
181

6. Объектно-ориентированный pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем заставку мяча с помощью Pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем класс Ball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Используем класс Ball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем много объектов Ball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем много, много объектов Ball . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем многократно используемую
объектно-ориентированную кнопку . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем класс кнопки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основной код, использующий SimpleButton . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем программу с несколькими кнопками . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем многократно используемое отображение текста . . . . . . . . . . . . . . . . . . . . . . .
Шаги для отображения текста . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем класс SimpleText . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Демоверсия Ball с SimpleText и SimpleButton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Сравнение интерфейса и реализации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Обратные вызовы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем обратный вызов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Используем обратный вызов с SimpleButton . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

183
183
184
186
188
190

7. Виджеты pygame GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Передаем аргументы функции или методу . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Позиционные параметры и параметры ключевых слов . . . . . . . . . . . .
Дополнительные примечания к параметрам ключевых слов . . . . .
Используем None в качестве значения по умолчанию . . . . . . . . . . . . .
Выбираем ключевые слова и значения по умолчанию . . . . . . . . . . . . . .
Значения по умолчанию в виджетах GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пакет pygwidgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Установка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Общий подход к разработке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Добавляем изображение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Добавляем кнопки, флажки и переключатели . . . . . . . . . . . . . . . . . . . . . . . . .
Вывод и ввод текста . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Другие классы pygwidgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
pygwidgets в примере программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Важность последовательного API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

211
212
213
214
216
217
218
218
219
220
221
222
226
229
231
231
231

12 Подробное содержание

190
191
194
196
197
198
199
201
203
204
205
206
209

Часть III. Инкапсуляция, полиморфизм
и наследование

233

8. Инкапсуляция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Инкапсуляция с помощью функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Инкапсуляция с помощью объектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Объекты владеют своими данными . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Интерпретации инкапсуляции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Прямой доступ и почему его следует избегать . . . . . . . . . . . . . . . . . . . . . . . .
Строгая интерпретация с помощью геттеров и сеттеров . . . . . . . . . . .
Безопасный прямой доступ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Делаем переменные экземпляра более закрытыми . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Неявно закрытый . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Более явно закрытый . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Декораторы и @property . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Инкапсуляция в классах pygwidgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
История из реального мира . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Абстракция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

235
236
237
237
238
238
244
246
247
247
248
249
254
255
257
260

9. Полиморфизм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Отправляем сообщения объектам реального мира . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Классический пример полиморфизма в программировании . . . . . . . . . . . . . . . . . . . .
Пример, использующий фигуры pygame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс квадратной формы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс круглой и треугольной формы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основная программа, создающая фигуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Расширяем шаблон . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
pygwidgets проявляет полиморфизм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Полиморфизм для операторов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Магические методы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Магические методы оператора сравнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Магические методы в классе Rectangle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Использование магических методов основной программой . . . . .
Магические методы математических операторов . . . . . . . . . . . . . . . . . . .
Векторный пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем строковое представление значений в объекте . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс Fraction с магическими методами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

263
264
265
266
267
268
272
274
275
276
277
278
280
282
284
285
288
291
295

10. Наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Наследование в объектно-ориентированном программировании . . . . . . . . . . . . .
Реализуем наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример работника и менеджера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Базовый класс: работник . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Подкласс: менеджер . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Тестовый код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Представление клиента о подклассе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Примеры наследования из реального мира . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
InputNumber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
DisplayMoney . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример использования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Наследование нескольких классов от одного базового класса . . . . . . . . . . . . . . . . .

297
298
300
301
301
302
305
306
307
308
311
314
317

Подробное содержание

13

Абстрактные классы и методы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Как pygwidgets применяет наследование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Иерархия классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Сложность программирования с наследованием . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

323
327
329
331
333

11. Управление памятью, используемой объектами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Жизненный цикл объекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Подсчет ссылок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Сбор мусора . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Переменные класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Константы переменных класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Переменные класса для подсчета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Собираем все воедино: пример программы «Шары» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Модуль констант . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Код основной программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Менеджер шаров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс шаров и объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Управляем памятью: слоты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

335
335
336
343
343
344
345
347
349
350
353
356
359
362

Часть IV. Использование ООП в разработке игр

363

12. Карточные игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс Card . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс Deck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Игра «Больше-меньше» . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основная программа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Объект Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Тестируем с помощью __name__ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Другие карточные игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Колода для блек-джека . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Игры с необычными колодами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

365
366
369
371
372
373
377
379
379
379
380

13. Таймеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Демонстрационная программа таймера . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Три подхода к реализации таймеров . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Подсчет фреймов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Таймер событий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем таймер путем вычисления прошедшего времени . . . . . . . .
Устанавливаем pyghelpers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Отображаем время . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CountUpTimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CountDownTimer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

381
382
383
383
384
386
388
389
392
393
397
398

14. Анимация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем классы анимации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс SimpleAnimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс SimpleSpriteSheetAnimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Объединяем два класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

399
400
400
405
410

14 Подробное содержание

Классы анимации в pygwidgets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс Animation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Класс SpriteSheetAnimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Общий базовый класс: PygAnimation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример программы анимации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

411
412
413
415
416
418

15. Сцены . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Концепция конечного автомата . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример pygame с конечным автоматом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Демоверсия программы, использующая менеджер сцен . . . . . . . . . . . . . . . . . . . . . . . . . .
Основная программа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем сцены . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Типичная сцена . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Игра «Камень-ножницы-бумага», использующая сцены . . . . . . . . . . . . . . . . . . . . . . . . . .
Взаимодействие между сценами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Запрашиваем информацию у целевой сцены . . . . . . . . . . . . . . . . . . . . . . . . .
Отправляем информацию целевой сцене . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Отправляем информацию всем сценам . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Проверяем взаимодействие между сценами . . . . . . . . . . . . . . . . . . . . . . . . . . .
Реализация менеджера сцен . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Метод run() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Основные методы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Взаимодействие между сценами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

419
420
423
430
432
433
437
439
444
445
446
446
447
447
449
451
453
454

16. Полноценная игра: Dodger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Модальные диалоговые окна . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Диалоговые окна Yes/No и Предупреждения . . . . . . . . . . . . . . . . . . . . . . . . .
Диалоговые окна с ответом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Создаем полноценную игру: Dodger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Обзор игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Реализация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Дополнения к игре . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Выводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

457
458
458
462
465
465
466
488
489

17. Шаблоны проектирования и резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Модель Представление Контроллер . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример отображения файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Пример статистического отображения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Преимущества шаблона MVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Резюме . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

491
492
492
493
499
500

Предметный указатель

503

..................................................................................

Б Л А ГОД А РНОС ТИ

Я хотел бы поблагодарить людей, которые помогли создать эту
книгу:
• Эла Свейгарта — за то, что приучил меня к pygame (особенно
за его код Pygbutton), и за то, что позволил мне использовать
концепцию его игры Dodger;
• Монте Давидоффа, который помог мне получить исходный
код и документацию к нему для правильной сборки с помощью GitHub, Sphinx и ReadTheDocs. Этот человек творил
чудеса, используя бесчисленное множество инструментов,
чтобы управляться с файлами;
• Монте Давидоффа (да, это все тот же парень) — за то, что оказался выдающимся техническим рецензентом. Монте дал
отличные технические и авторские предложения по всей
книге, и многие примеры кода стали более Pythonic и более
ООП-ориентированными именно после его комментариев;
• Тепа Сатья Кхиеу, который проделал потрясающую работу,
нарисовав все оригинальные схемы для этой книги.
Я не художник (я даже не играю ни одного на ТВ). Теп смог
взять мои примитивные карандашные наброски и превратить их в четкие, последовательные произведения искусства;
Благодарности 17

• Харрисона Янга, Кевина Лая и Эмили Эллис — за их вклад
в художественное оформление некоторых игр;
• ранних рецензентов: Илью Кацюка, Джейми Калб, Гергану
Анджелову и Джо Лангмюра, которые нашли и устранили
много опечаток и внесли отличные предложения по исправлениям и уточнениям;
• всех редакторов, которые работали над этой книгой: Лиз
Чедвик (редактор по развитию), Рейчел Хед (редактор)
и Кейт Камински (производственный редактор). Все они
внесли огромный вклад, задавая вопросы и часто переписывая и реорганизуя некоторые из моих объяснений. Они
также были чрезвычайно полезны в расставлении и удалении запятых [нужна ли она здесь?] и удлинении моих предложений, как здесь, чтобы убедиться, что точка встречается
там, где нужна (ОК, я остановлюсь!). Я боюсь, что никогда
не пойму, где следует использовать «который», а не «что»,
или как расставлять запятые и тире, но я рад, что они знают!
Спасибо также Морин Форис (верстальщице) за ее ценный
вклад в готовый продукт;
• всех студентов, которые побывали на моих занятиях в течение
многих лет в UCSC Silicon Valley Extension и в Университете
Кремниевой долины (ранее Политехнический колледж
Когсвелла). Их отзывы, предложения, улыбки, недовольство,
моменты озарения, разочарование, понимающие кивки
и даже большие пальцы вверх (в классах Zoom в эпоху ковида) были чрезвычайно полезны в формировании содержания
этой книги и моего общего стиля преподавания;
• наконец, мою семью, которая поддерживала меня в длительном процессе написания, тестирования, редактирования,
переписывания, редактирования, отладки, редактирования,
переписывания, редактирования (и так далее) этой книги
и связанного с ней кода. Без вас я бы не справился. Я не был
уверен, достаточно ли у нас книг в библиотеке, поэтому написал еще одну!

ВВЕ Д Е НИЕ

Эта книга о технике разработки программного
обеспечения под названием объектноориентированное программирование (ООП)
и о том, как его можно использовать с Python.
До ООП программисты применяли подход, известный
как процедурное или структурированное программирование, который включает в себя построение набора функций (процедур)
и передачу данных через вызовы к этим функциям. Парадигма
ООП дает эффективный способ объединить код и данные
в связные единицы, которые подходят для повторного использования.
В процессе подготовки к написанию этой книги я подробно
познакомился с существующей литературой и видеоматериалами, изучив конкретные подходы, используемые для объяснения
этой важной и широкомасштабной темы. Я обнаружил, что учителя и писатели обычно начинают с определения некоторых
ключевых терминов: класс, переменная экземпляра, метод, инкапсуляция, наследование, полиморфизм и так далее.
Хотя все это важные понятия и мы их подробно здесь рассмотрим, я все же начну с другого — с вопроса: «Какую проблему
мы решаем?» То есть если решением является ООП, то в чем
Введение 19

проблема? Чтобы ответить, я покажу несколько примеров программ, построенных с использованием процедурного программирования, а затем выявлю сложности, присущие этому стилю. А после покажу вам, как объектно-ориентированный подход может
упростить разработку и облегчить поддержку таких программ.

Для кого эта книга?
Эта книга предназначена для людей, которые уже знакомы
с Python и используют основные функции из его стандартной
библиотеки. Я предполагаю, что вы понимаете основной синтаксис языка и можете писать небольшие и средние программы
с помощью переменных, инструкций присваивания, операторов if/elif/else, а также циклы, функции и вызовы функций, списки, словари и так далее. Если вы не знакомы со всеми
этими понятиями, то я предлагаю вам сначала ознакомиться
с моей предыдущей работой, Learn to Program with Python 3 *.
Издание, которое вы сейчас читаете, среднего уровня, поэтому есть ряд более продвинутых тем, их я не стану затрагивать. Например, чтобы книга оставалась практичной, я не часто буду вдаваться в подробности о внутренней реализации
Python. Для простоты и ясности, а также для того, чтобы сосредоточиться на освоении методов ООП, примеры написаны
с использованием ограниченного подмножества языка. Есть более продвинутые и лаконичные способы программирования
на Python, которые выходят за рамки этой книги.
Я рассмотрю главные детали ООП, не зависящие от языка,
но обозначу области, где существуют различия между Python
и другими языками ООП. Ознакомившись с основами программирования в стиле ООП в этой книге, при желании вы сможете легко применить те же методы к другим языкам ООП.

Версия(-и) Python и установка
Все примеры кода в этой книге были написаны и протестированы с использованием версий Python с 3.6 по 3.9 и должны
хорошо работать с версией 3.6 или новее.

*

20 Введение

Не издавалась на русском языке. — Прим. ред.

Python доступен бесплатно по адресу https://www.python.
org. Если у вас не установлен Python или вы хотите обновиться
до последней версии, перейдите на этот сайт, найдите вкладку
Downloads и нажмите кнопку Download. Так вы загрузите установочный файл на свой компьютер. Дважды щелкните по нему
для установки Python.

УСТАНОВК А НА WINDOWS

При установке на систему Windows необходимо обратить
внимание на один важный параметр. При выполнении шагов
по установке вы увидите экран, как на рисунке ниже.

В нижней части диалогового окна находится флажок Add
Python 3.x to PATH (Добавить Python 3.x в переменную
PATH). Пожалуйста, не забудьте установить его (по умолчанию он не установлен). Данная настройка позволит правильно инсталлировать пакет pygame (который будет представлен
в книге позже).

ПРИМЕЧАНИЕ

Мне известен «PEP 8 — Руководство по стилю для кода Python»
и его конкретная рекомендация использовать Snake Case (змеиный регистр) для имен переменных и функций. Тем не менее
я использовал конвенцию об именовании Camel Case (горбатый регистр) в течение многих лет до того, как был написан
Введение 21

документ PEP 8, и мне было комфортно с ним на протяжении
моей карьеры. Поэтому все имена переменных и функций
в этой книге написаны с использованием горбатого регистра.

Как я объясняю ООП?
Примеры в первых нескольких главах используют текстовый
Python; в них программы получают входные данные от пользователя и выводят ему информацию исключительно в виде текста. Я представлю ООП, показывая, как моделировать физические объекты в коде на основе текста. Для начала мы
представим в виде объектов выключатели света, диммеры
и телевизионные пульты дистанционного управления. Затем
я покажу вам, как с помощью ООП моделировать банковские
счета и банк в целом.
Как только мы рассмотрим основы ООП, я представлю модуль pygame, который позволяет программистам писать игры
и приложения, использующие графический пользовательский интерфейс (GUI). В программах на основе графического интерфейса пользователь интуитивно взаимодействует с кнопками,
флажками, полями ввода и вывода текста и другими удобными
виджетами.
Я решил использовать pygame с Python, потому что эта комбинация позволяет мне демонстрировать концепции ООП визуально, используя элементы на экране. Pygame чрезвычайно портативна и работает практически на всех платформах
и операционных системах. Все образцы программ, использующих пакет pygame, были протестированы с недавно выпущенной версией pygame 2.0.
Я создал пакет под названием pygwidgets, который работает с pygame и реализует ряд базовых виджетов, все они построены с использованием подхода ООП. Я представлю этот
пакет позже в книге и дам пример кода, с которым вы можете
работать и экспериментировать. Такой подход позволит увидеть реальные, практические примеры ключевых объектноориентированных концепций, при этом внедряя эти приемы
для создания забавных игр, в которые можно поиграть. Я также
представлю мой пакет pyghelpers, содержащий код, что помогает написать более сложные игры и приложения.
22 Введение

Весь приведенный в книге код доступен для скачивания как
один файл с сайта https://addons.eksmo.ru/it/OOP-Code.zip.

Структура книги
Эта книга состоит из четырех частей. Часть I знакомит с объектно- ориентированным программированием.
• В главе 1 представлен обзор кода с использованием процедурного программирования. Я покажу вам, как реализовать текстовую карточную игру и смоделировать банк, выполняющий
операции по одному или нескольким счетам. Попутно я обсуждаю общие проблемы процедурного подхода.
• Глава 2 повествует о классах и объектах и показывает, как вы
можете представлять реальные вещи, такие как выключатели
света или пульт дистанционного управления телевизором,
на Python с помощью классов. Вы увидите, как объектноориентированный подход решает проблемы, обозначенные
в первой главе.
• В главе 3 представлены две ментальные модели, которые вы
можете использовать, чтобы думать о том, что происходит
за кулисами, когда вы создаете объекты на Python. Мы будем
использовать Python Tutor, чтобы просмотреть код и увидеть,
как создаются объекты.
• В главе 4 показан стандартный способ обработки нескольких
объектов одного типа путем введения понятия объекта диспетчера объектов. Мы расширим моделирование банковского
счета с помощью классов, и я покажу вам, как обрабатывать
ошибки с помощью исключений.
Часть II посвящена созданию графических интерфейсов
с помощью pygame.
• Глава 5 представляет пакет pygame и управляемую событиями модель программирования. Мы создадим несколько простых программ, чтобы вы могли начать с размещения
графики в окне и обработки ввода с клавиатуры и мыши,
а затем разработаем более сложную программу с прыгающими мячами.
Введение 23

• Глава 6 подробнее описывает использование ООП с программами pygame. Мы перепишем приложение с прыгающим
мячом в стиле ООП и разработаем некоторые простые элементы графического интерфейса.
• В главе 7 представлен модуль pygwidgets, который содержит
полные реализации многих стандартных элементов графического интерфейса (кнопок, флажков и т. д.), каждый из которых разрабатывается как класс.
Часть III посвящена основным принципам ООП.
• В главе 8 обсуждается инкапсуляция, которая включает в себя
сокрытие деталей реализации от внешнего кода и размещение всех связанных методов в одном месте — классе.
• Глава 9 вводит полиморфизм — идею о том, что несколько
классовмогут иметь методы с одинаковыми именами, — и показывает, как он позволяет вызывать методы в нескольких объектах, не зная типа каждого из них. Мы создадим программу
Shapes, чтобы продемонстрировать эту концепцию.
• Глава 10 описывает наследование, которое позволяет создавать набор подклассов, использующих общий код, встроенный в базовый класс, а не «изобретать колесо» с похожими
классами. Мы рассмотрим несколько реальных примеров,
когда пригодится наследование, например реализацию поля
ввода, которое принимает только числа, а затем перепишем
наш пример программы Shapes, чтобы использовать эту
функциональность.
• Глава 11 завершает эту часть книги обсуждением некоторых
дополнительных важных тем ООП, в основном связанных
с управлением памятью. Мы посмотрим на время жизни объекта и в качестве примера построим небольшую игру с шариками.
Часть IV посвящена нескольким темам, связанным с использованием ООП в разработке игр.
• Глава 12 демонстрирует, как мы можем перестроить карточную игру, разработанную в главе 1, в качестве программы
с графическим интерфейсом на основе pygame. Я также
24 Введение

покажу вам, как создавать многоразовые классы колод и карт,
которые вы можете использовать при создании других карточных игр.
• В главе 13 рассматривается вопрос о времени. Мы разработаем различные классы таймеров, которые позволят программе
продолжать работу, одновременно проверяя заданный лимит
времени.
• В главе 14 описываются классы анимации, которые можно
использовать для отображения последовательностей изображений. Мы рассмотрим два метода: создание анимации
из коллекции отдельных файлов изображений и извлечение
и использование нескольких изображений из одного файла
листа спрайта.
• Глава 15 объясняет концепцию конечного автомата, представляющего и контролирующего поток ваших программ, и менеджер сцен, который вы можете задействовать для создания
программы с несколькими сценами. Чтобы продемонстрировать использование каждого из них, мы построим две версии
игры «Камень, ножницы, бумага».
• В главе 16 обсуждаются различные типы модальных диалогов — еще одна важная функция взаимодействия с пользователем. Затем мы пройдемся по созданию полнофункциональной
видеоигры на основе ООП под названием Dodger, которая
демонстрирует многие методы, описанные в книге.
• В главе 17 представлена концепция шаблонов проектирования на примере шаблона контроллера представления модели,
а затем показана программа бросания костей, в которой
используется этот шаблон, чтобы позволить пользователю
визуализировать данные различными способами.
Завершается глава кратким подведением итогов работы над
книгой.

Среды разработки
При работе с этой книгой вы будете использовать командную
строку только для установки программного обеспечения. Все
инструкции по установке четко прописаны, поэтому вам
Введение 25

не потребуется изучать дополнительный синтаксис командной
строки.
Я твердо верю, что для разработки лучше использовать
не командную строку, а интерактивную среду разработки (IDE).
Интегрированная среда обрабатывает многие детали базовой
операционной системы за вас и позволяет писать, редактировать и запускать код с помощью одной программы. Интегрированные среды, как правило, являются кросс-платформенными,
что позволяет программистам легко перемещаться с Mac
на компьютер под управлением Windows или наоборот.
Краткие примеры программ в книге могут быть запущены
в среде разработки IDLE, которая устанавливается вместе
с Python. IDLE очень проста в использовании и хорошо работает для программ, которые состоят из одного файла. Когда
мы перейдем к более сложным программам, использующим
несколько файлов Python, лучше применять что-то посложнее; лично я использую среду разработки JetBrains PyCharm,
которая легче обрабатывает проекты с несколькими файлами. Community Edition доступен бесплатно по адресу
https://www.jetbrains.com/, и я настоятельно рекомендую
его. В PyCharm входит полностью интегрированный отладчик, который может быть чрезвычайно полезен при написании более крупных программ. Дополнительные сведения
об использовании отладчика см. в моем видео на YouTube
«Отладка Python 3 с помощью PyCharm» по адресу
https://www.youtube.com/watch?v=cxAOSQQwDJ4&t=43s/.
Виджеты и примеры игр
В книге представлены и доступны два пакета Python:
pygwidgets и pyghelpers. Используя их, вы сможете создавать
полноценные программы с графическим интерфейсом пользователя, но, что более важно, вы получите представление о том,
как каждый из виджетов написан как класс и используется как
объект.
Примеры игр в книге, включющие различные виджеты, начинаются с относительно простых и становятся все более
сложными. Глава 16 рассказывает о разработке и внедрении
полнофункциональной видеоигры с таблицей результатов, которая сохраняется в файл.

26 Введение

К концу этой книги вы должны уметь писать собственные
игры — карточные или видеоигры в стиле Pong, Hangman,
Breakout, Space Invaders и так далее. Объектно-ориентированное программирование дает вам возможность писать программы, которые могут легко отображать и контролировать несколько элементов одного типа, что регулярно требуется при
построении пользовательских интерфейсов и часто необходимо в игре.
Объектно- ориентированное программирование — это общий стиль, который можно применять во всех аспектах программирования, далеко за пределами игровых примеров, которые я использую для демонстрации техники ООП. Надеюсь,
вам понравится этот подход к изучению ООП.
Итак, начнем.

ЧАСТЬ I
ВВЕ Д Е НИЕ В ОБЪЕ К ТНО ОРИЕ НТИРОВА ННОЕ
ПРОГРА М МИРОВА НИЕ
Эта часть книги знакомит вас с объектно- ориентированным
программированием. Мы обсудим проблемы, присущие процедурному коду, а затем посмотрим, как объектноориентированное программирование решает их. Мышление
в объектах (с состоянием и поведением) даст вам новое представление о том, как писать код.
В главе 1 представлен обзор процедурного Python. Я начинаю с презентации текстовой карточной игры Higher or Lower
(«Больше-меньше»), затем прорабатываю несколько все более
сложных реализаций банковского счета на Python, чтобы помочь вам лучше понять распространенные проблемы программирования в процедурном стиле.
Глава 2 показывает, как мы можем представлять объекты реального мира в Python с помощью классов. Мы напишем программу, имитирующую выключатель света, изменим ее, чтобы
добавить возможности диммера (плавного затемнения), а затем
перейдем к более сложному моделированию пульта от телевизора.
Глава 3 дает вам два разных способа думать о том, что происходит за кулисами, когда вы создаете объекты на Python.
Глава 4 демонстрирует стандартный способ обработки нескольких объектов одного типа (например, в такой простой
игре, как шашки, где вы должны отслеживать много похожих
игровых фигур). Мы расширим программу банковского счета
из главы 1 и изучим, как обрабатывать ошибки.

1
ПРОЦ Е ДУ РНЫЕ ПРИМЕ РЫ
PY TH O N

Вводные курсы и книги обычно обучают разработке программного обеспечения с использованием стиля процедурного программирования,
который включает в себя разделение программы
на ряд функций (также известных как процедуры или подпрограммы). Вы передаете данные в функции, каждая из которых
выполняет одно или несколько вычислений и, как правило,
возвращает обратно результаты.
Эта книга о другой парадигме, известной как объектноориентированное программирование (ООП), которая позволяет
иначе думать о том, как строить программное обеспечение.
Объектно- ориентированное программирование дает возможность объединять код и данные, тем самым избегая некоторых
осложнений, присущих процедурному программированию.
В этой главе я рассмотрю ряд концепций в Basic Python, создав две небольшие программы, которые включают в себя различные конструкции Python. Первой будет небольшая карточная игра под названием «Больше-меньше»; второй станет
симуляция банка, выполняющего операции по одному, двум
и нескольким счетам. Обе будут построены при помощи
Процедурные примеры Python 31

процедурного программирования, то есть с использованием
стандартных методов данных и функций. Позже я перепишу
эти программы, применяя методы ООП. Цель данной главы —
продемонстрировать некоторые ключевые проблемы, присущие процедурному программированию. В последующих главах
будет объяснено, как ООП их решает.

Карточная игра «Больше-меньше»
Мой первый пример — простая карточная игра под названием
«Больше-меньше». В ней восемь карт случайным образом выбираются из колоды. Первая отображается лицевой стороной
вверх. Игра просит игрока предсказать, будет ли следующая
карта в выборе иметь большее или меньшее достоинство, чем
текущая. Допустим, что показанная карта имеет значение 3.
Игрок отвечает «больше», и показывается вторая карта. Если
ее достоинство выше, то игрок выиграл. В этом же примере,
если бы игрок ответил «меньше», он бы проиграл.
За каждый правильный ответ игрок получает 20 очков,
за неправильный — теряет 15. Если следующая карта, которую
нужно перевернуть, имеет то же значение, что и предыдущая,
игрок не угадал.
Представление данных
Программа должна представлять колоду из 52 карт, которую
я построю в виде списка. Каждый из 52 элементов в списке станет словарем (набором пар ключ/значение). Чтобы представить любую карту, каждый словарь будет содержать три пары
ключ/значение: 'ранг', 'масть' и 'значение'. Ранг — это название
карты (туз, 2, 3 … 10, валет, дама, король), но значение — это
целое число, используемое для сравнения карт (1, 2, 3 … 10, 11,
12, 13). Например, «валет треф» будет представлен в виде следующего словаря:
{'rank': 'Jack', 'suit': 'Clubs', 'value': 11}

Перед раундом игрок создает список, представляющий колоду, и перетасовывает его, чтобы рандомизировать порядок
карт. У меня нет графического представления карт, поэтому
каждый раз, когда пользователь выбирает «больше» или
32 Часть I. Введение в объектно- ориентированное программирование

«меньше», программа получает словарь карт из колоды и печатает ранг и масть для пользователя. Затем она сравнивает достоинство новой карты с предыдущей и дает обратную связь
на ответ пользователя.
Реализация
Листинг 1.1 показывает код игры «Больше-меньше».
ПРИМЕЧАНИЕ

Напоминаем, что код, связанный со всеми основными листингами в этой книге, доступен для скачивания по адресу https://
addons.eksmo.ru/it/OOP-Code.zip. Вы можете либо скачать
и запустить код, либо ввести его самостоятельно.

Файл: HigherOrLowerProcedural.py
# HigherOrLower
import random
# Константы карт
SUIT_TUPLE = ('Spades', 'Hearts', 'Clubs', 'Diamonds')
RANK_TUPLE = ('Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10',
'Jack', 'Queen', 'King')
NCARDS = 8
# Проходим по колоде, и эта функция возвращает случайную карту из колоды
def getCard(deckListIn):
thisCard = deckListIn.pop() # Снимаем одну карту с верхней части
# колоды и возвращаем
return thisCard
# Проходим по колоде, и эта функция возвращает перемешанную копию колоды
def shuffle(deckListIn):
deckListOut = deckListIn.copy() # создаем копию стартовой колоды
random.shuffle(deckListOut)
return deckListOut
# Основной код
print('Welcome to Higher or Lower.')
print('You have to choose whether the next card to be shown will be
higher or lower than the current card.')
print('Getting it right adds 20 points; get it wrong and you lose
15 points.')
print('You have 50 points to start.')
print()
Глава 1. Процедурные примеры Python 33

startingDeckList = []
 for suit in SUIT_TUPLE:
for thisValue, rank in enumerate(RANK_TUPLE):
cardDict = {'rank':rank, 'suit':suit, 'value':thisValue + 1}
startingDeckList.append(cardDict)
score = 50
while True: # несколько игр
print()
gameDeckList = shuffle(startingDeckList)

currentCardDict = getCard(gameDeckList)
currentCardRank = currentCardDict['rank']
currentCardValue = currentCardDict['value']
currentCardSuit = currentCardDict['suit']
print('Starting card is:', currentCardRank + ' of ' +
currentCardSuit)
print()






for cardNumber in range(0, NCARDS): # играем в одну игру
# из этого количества карт
answer = input('Will the next card be higher or lower than
the ' +
currentCardRank + ' of ' +
currentCardSuit + '? (enter h or l): ')
answer = answer.casefold() # переводим в нижний регистр
nextCardDict = getCard(gameDeckList)
nextCardRank = nextCardDict['rank']
nextCardSuit = nextCardDict['suit']
nextCardValue = nextCardDict['value']
print('Next card is:', nextCardRank + ' of ' + nextCardSuit)
if answer == 'h':
if nextCardValue > currentCardValue:
print('You got it right, it was higher')
score = score + 20
else:
print('Sorry, it was not higher')
score = score – 15
elif answer == 'l':
if nextCardValue < currentCardValue:
score = score + 20
print('You got it right, it was lower')
else:
score = score – 15

34 Часть I. Введение в объектно- ориентированное программирование

print('Sorry, it was not lower')
print('Your score is:', score)
print()
currentCardRank = nextCardRank
currentCardValue = nextCardValue # не нужна текущая масть


goAgain = input('To play again, press ENTER, or "q" to quit: ')
if goAgain == 'q':
break
print('OK bye')
Листинг 1.1. Игра «Больше-меньше» с использованием процедурного Python

Программа начинается с создания колоды в виде списка .
Каждая карта – это словарь, состоящий из ранга, масти и значения. Для каждого раунда игры я извлекаю первую карту из колоды и сохраняю компоненты в переменных . Для следующих
семи карт пользователю предлагается предсказать, будет ли
следующая карта выше или ниже, чем самая последняя . Следующая карта извлекается из колоды, а ее компоненты сохраняются во втором наборе переменных . Игра сравнивает ответ
пользователя на текущую карту и выдает обратную связь и баллы на основе результата . Когда пользователь сделал прогнозы для всех семи карт в выборе, мы спрашиваем, хочет ли он
сыграть снова .
Эта программа демонстрирует множество элементов программирования в целом и Python в частности: переменные, инструкции присваивания, функции и вызовы функций, инструкции if/else, инструкции print, а также циклы, списки,
строки и словари. В этой книге предполагается, что вы уже знакомы со всем, что показано в примере. Если программа содержит что-то новое или непонятное для вас, вероятно, стоит уделить время изучению соответствующего материала, прежде
чем двигаться дальше.
Повторное использование кода
Поскольку это игра на основе карт, код создает и использует
модель карточной колоды. Если бы мы хотели написать еще
одну игру такого рода, было бы здорово иметь возможность
повторно использовать этот код.

Глава 1. Процедурные примеры Python 35

В процедурной программе часто бывает трудно идентифицировать все фрагменты кода, связанные с одной частью программы, такие как колода и карты в этом примере. В листинге 1.1 код для колоды состоит из двух констант кортежей, двух
функций, некоторого основного кода для построения глобального списка, который представляет стартовую колоду
из 52 карт, и другого глобального списка, представляющего колоду, которая используется во время игры. Кроме того, обратите внимание, что даже в такой небольшой программе, как эта,
данные и код, который обрабатывает данные, могут не быть
тесно сгруппированы вместе.
Поэтому повторное использование кода колоды и карты
в другой программе не так просто или очевидно. В главе 12 мы
вернемся к этой программе и покажем, как решение ООП значительно упрощает повторное использование кода.

Моделирование банковского счета
Во втором примере процедурного программирования я представлю ряд вариантов программы, которая имитирует запуск
банка. В каждой новой версии программы я добавлю больше
функциональности. Обратите внимание, что эти программы
не готовы к выпуску; недопустимые пользовательские данные
или неправильное использование приведут к ошибкам. Цель
состоит в том, чтобы вы сосредоточились на понимании, как
код взаимодействует с данными, связанными с одним или
несколькими банковскими счетами.
Для начала подумайте, какие операции клиент хотел бы совершить с банковским счетом и какие данные потребуются для
представления счета.
Анализ необходимых операций и данных
Список операций, которые человек хотел бы совершить с банковским счетом, будет включать следующее.





Открыть счет.
Внести деньги.
Снять деньги.
Проверить баланс.

36 Часть I. Введение в объектно- ориентированное программирование

Далее приведен минимальный список данных, которые нам
потребуются для представления банковского счета.
• Имя клиента.
• Пароль.
• Баланс.
Обратите внимание, что все операции — это слова действия
(глаголы), а все элементы данных — это понятия (существительные). Реальный банковский счет, безусловно, способен выполнять гораздо больше операций и будет содержать дополнительные данные (такие как адрес владельца счета, номер телефона
и номер социального страхования), но для ясности я начну
с этих четырех действий и трех данных. Кроме того, чтобы
не усложнять, я определю все суммы целым числом долларов.
Я также должен отметить, что в реальном банковском приложении пароли не будут храниться открытым текстом (незашифрованными), как в этих примерах.
Реализация 1. Одна учетная запись без функций
В стартовой версии в листинге 1.2 есть только один счет.
Файл: Bank1_OneAccount.py
# Без ООП
# Банк. Версия 1
# Единственный счет
 accountName = 'Joe'
accountBalance = 100
accountPassword = 'soup'



while True:
print()
print('Press
print('Press
print('Press
print('Press
print('Press
print()

b
d
w
s
q

to
to
to
to
to

get the balance')
make a deposit')
make a withdrawal')
show the account')
quit')

action = input('What do you want to do? ')
action = action.lower() # переводим в нижний регистр
action = action[0] # используем первую букву
Глава 1. Процедурные примеры Python 37

print()
if action == 'b':
print('Get Balance:')
userPassword = input('Please enter the password: ')
if userPassword != accountPassword:
print('Incorrect password')
else:
print('Your balance is:', accountBalance)
elif action == 'd':
print('Deposit:')
userDepositAmount = input('Please enter amount to deposit: ')
userDepositAmount = int(userDepositAmount)
userPassword = input('Please enter the password: ')
if userDepositAmount < 0:
print('You cannot deposit a negative amount!')
elif userPassword != accountPassword:
print('Incorrect password')
else: # OK
accountBalance = accountBalance + userDepositAmount
print('Your new balance is:', accountBalance)
elif action == 's': # отображаем
print('Show:')
print('
Name', accountName)
print('
Balance:', accountBalance)
print('
Password:', accountPassword)
print()
elif action == 'q':
break
elif action == 'w':
print('Withdraw:')
userWithdrawAmount = input('Please enter the amount to
withdraw: ')
userWithdrawAmount = int(userWithdrawAmount)
userPassword = input('Please enter the password: ')
if userWithdrawAmount < 0:
print('You cannot withdraw a negative amount')
elif userPassword != accountPassword:

38 Часть I. Введение в объектно- ориентированное программирование

print('Incorrect password for this account')
elif userWithdrawAmount > accountBalance:
print('You cannot withdraw more than you have in your
account')
else: # OK
accountBalance = accountBalance – userWithdrawAmount
print('Your new balance is:', accountBalance)
print('Done')
Листинг 1.2. Моделирование банка для одного счета

Программа начинается с инициализации трех переменных
для представления данных одной учетной записи . Затем отображается меню, которое позволяет выбрать операции .
Основной код программы действует непосредственно на глобальные переменные счета.
В этом примере все действия находятся на основном уровне,
в коде нет функций. Программа работает нормально, но может
показаться немного длинной. Типичный способ сделать длинные программы более четкими — перемещение соответствующего кода в функции и вызов этих функций. Мы изучим это
в следующей версии банковской программы.
Реализация 2. Одна учетная запись с функциями
В версии программы в листинге 1.3 код разбивается на отдельные функции, по одной для каждого действия. Опять же, эта
симуляция предназначена для одного аккаунта.

Файл: Bank2_OneAccountWithFunctions.py
# Без ООП
# Банк 2
# Единственный счет
accountName = ''
accountBalance = 0
accountPassword = ''
 def newAccount(name, balance, password):
global accountName, accountBalance, accountPassword
accountName = name
Глава 1. Процедурные примеры Python 39

accountBalance = balance
accountPassword = password
def show():
global accountName, accountBalance, accountPassword
print('
Name', accountName)
print('
Balance:', accountBalance)
print('
Password:', accountPassword)
print()
 def getBalance(password):
global accountName, accountBalance, accountPassword
if password != accountPassword:
print('Incorrect password')
return None
return accountBalance
 def deposit(amountToDeposit, password):
global accountName, accountBalance, accountPassword
if amountToDeposit < 0:
print('You cannot deposit a negative amount!')
return None
if password != accountPassword:
print('Incorrect password')
return None
accountBalance = accountBalance + amountToDeposit
return accountBalance
 def withdraw(amountToWithdraw, password):

global accountName, accountBalance, accountPassword
if amountToWithdraw < 0:
print('You cannot withdraw a negative amount')
return None
if password != accountPassword:
print('Incorrect password for this account')
return None
if amountToWithdraw > accountBalance:
print('You cannot withdraw more than you have in your account')
return None


accountBalance = accountBalance – amountToWithdraw
return accountBalance
newAccount("Joe", 100, 'soup') # создаем аккаунт

40 Часть I. Введение в объектно- ориентированное программирование

while True:
print()
print('Press
print('Press
print('Press
print('Press
print('Press
print()

b
d
w
s
q

to
to
to
to
to

get the balance')
make a deposit')
make a withdrawal')
show the account')
quit')

action = input('What do you want to do? ')
action = action.lower() # переводим в нижний регистр
action = action[0] # используем первую букву
print()
if action == 'b':
print('Get Balance:')
userPassword = input('Please enter the password: ')
theBalance = getBalance(userPassword)
if theBalance is not None:
print('Your balance is:', theBalance)




elif action == 'd':
print('Deposit:')
userDepositAmount = input('Please enter amount to deposit: ')
userDepositAmount = int(userDepositAmount)
userPassword = input('Please enter the password: ')
newBalance = deposit(userDepositAmount, userPassword)
if newBalance is not None:
print('Your new balance is:', newBalance)
--- скрыты вызовы соответствующих функций --print('Done')
Листинг 1.3. Моделирование банка для одного счета с функциями

В этой версии я построил функцию для каждой из операций, которые мы определили для банковского счета (создать аккаунт ,
проверить баланс , внести деньги , снять деньги ), и сделал
так, чтобы основной код содержал вызовы различных функций.
В результате основная программа читается гораздо более
удобно. Например, если пользователь вводит d, чтобы указать,
что он хочет внести депозит , код теперь вызывает функцию
с именем deposit() , передавая сумму депозита и пароль
учетной записи, введенные пользователем.

Глава 1. Процедурные примеры Python 41

Однако если вы посмотрите на определение любой из этих
функций — например, withdraw(), — то увидите, что код использует оператор global  для доступа (чтения или записи)
к переменным, которые представляют банковский счет.
В Python оператор global требуется только в том случае, если
вы хотите изменить значение глобальной переменной в функции. Тем не менее я использую его здесь, чтобы ясно показать,
что эти функции относятся к глобальным переменным, даже
если они просто получают значение.
Общий принцип программирования требует, чтобы функции никогда не изменяли глобальные переменные. Функция
должна использовать только данные, которые передаются
в нее, производить расчеты на основе их и, возможно, возвращать результат или результаты. Функция withdraw() в этой
программе действительно работает, но она нарушает вышеуказанное правило, изменяя значение глобальной переменной
accountBalance  (в дополнение к доступу к значению глобальной переменной accountPassword).
Реализация 3. Два счета
Версия программы моделирования банка в листинге 1.4
использует тот же подход, что и листинг 1.3, но добавляет возможность иметь два счета.
Файл: Bank3_TwoAccounts.py
# Без ООП
# Банк 3
# Два счета
account0Name = ''
account0Balance = 0
account0Password = ''
account1Name = ''
account1Balance = 0
account1Password = ''
nAccounts = 0
def newAccount(accountNumber, name, balance, password):

global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if accountNumber == 0:
account0Name = name
account0Balance = balance

42 Часть I. Введение в объектно- ориентированное программирование

account0Password = password
if accountNumber == 1:
account1Name = name
account1Balance = balance
account1Password = password



def show():
global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if account0Name != '':
print('Account 0')
print('
Name', account0Name)
print('
Balance:', account0Balance)
print('
Password:', account0Password)
print()
if account1Name != '':
print('Account 1')
print('
Name', account1Name)
print('
Balance:', account1Balance)
print('
Password:', account1Password)
print()



def getBalance(accountNumber, password):
global account0Name, account0Balance, account0Password
global account1Name, account1Balance, account1Password
if accountNumber == 0:
if password != account0Password:
print('Incorrect password')
return None
return account0Balance
if accountNumber == 1:
if password != account1Password:
print('Incorrect password')
return None
return account1Balance
--- скрыты дополнительные функции deposit() и withdraw() ----- скрыт основной код, который вызывает функции выше --print('Done')
Листинг 1.4. Моделирование банка для двух счетов с функциями

Уже при двух счетах вы можете увидеть, что этот способ быстро выходит из-под контроля. Во-первых, мы устанавливаем
три глобальные переменные для каждого счета в ,  и . Кроме того, в каждой функции теперь есть оператор if,
Глава 1. Процедурные примеры Python 43

выбирающий набор глобальных переменных для доступа или
изменения. Всякий раз, когда мы хотим добавить еще один
счет, нам нужно добавить еще один набор глобальных переменных и больше операторов if в каждой функции. Это просто непрактичный подход. Нам нужен другой способ обработки произвольного количества счетов.
Реализация 4. Несколько счетов с использованием
списков
Чтобы упростить размещение нескольких счетов,
в листинге 1.5 я буду представлять данные с помощью списков.
В этой версии программы я использую три параллельных
списка: accountNamesList, accountPasswordsList
и accountBalancesList.
Файл: Bank4_N_Accounts.py
# Без ООП
# Банк 4
# Любое количество счетов — со списками
 accountNamesList = []
accountBalancesList = []
accountPasswordsList = []
def newAccount(name, balance, password):
global accountNamesList, accountBalancesList, accountPasswordsList

accountNamesList.append(name)
accountBalancesList.append(balance)
accountPasswordsList.append(password)
def show(accountNumber):
global accountNamesList, accountBalancesList, accountPasswordsList
print('Account', accountNumber)
print('
Name', accountNamesList[accountNumber])
print('
Balance:', accountBalancesList[accountNumber])
print('
Password:', accountPasswordsList[accountNumber])
print()
def getBalance(accountNumber, password):
global accountNamesList, accountBalancesList, accountPasswordsList
if password != accountPasswordsList[accountNumber]:
print('Incorrect password')
return None
return accountBalancesList[accountNumber]

44 Часть I. Введение в объектно- ориентированное программирование

--- скрыты дополнительные функции --# создаем два образца учетных записей
 print("Joe's account is account number:", len(accountNamesList))
newAccount("Joe", 100, 'soup')
 print("Mary's account is account number:", len(accountNamesList))
newAccount("Mary", 12345, 'nuts')
while True:
print()
print('Press
print('Press
print('Press
print('Press
print('Press
print('Press
print()

b
d
n
w
s
q

to
to
to
to
to
to

get the balance')
make a deposit')
create a new account')
make a withdrawal')
show all accounts')
quit')

action = input('What do you want to do? ')
action = action.lower() # переводим в нижний регистр
action = action[0] # используем первую букву
print()



if action == 'b':
print('Get Balance:')
userAccountNumber = input('Please enter your account number: ')
userAccountNumber = int(userAccountNumber)
userPassword = input('Please enter the password: ')
theBalance = getBalance(userAccountNumber, userPassword)
if theBalance is not None:
print('Your balance is:', theBalance)
--- скрыт дополнительный пользовательский интерфейс --print('Done')
Листинг 1.5. Моделирование банка с параллельными списками

В начале программы я создал все три списка как пустой список . Чтобы создать новую учетную запись, я добавляю соответствующее значение к каждому из них .
Поскольку сейчас мы имеем дело с несколькими счетами,
я использую базовую концепцию номера банковского счета.
Каждый раз, когда пользователь создает учетную запись, код
применяет функцию len() в одном из списков и возвращает
Глава 1. Процедурные примеры Python 45

этот номер в качестве номера учетной записи пользователя , . При создании аккаунта для первого пользователя длина accountNamesList равна нулю. Таким образом, первой созданной учетной записи будет присвоен номер счета 0, второй
учетной записи — номер счета 1 и так далее. Затем, как и в реальном банке, для выполнения любой операции после создания
счета (например, пополнение или вывод средств) пользователь
должен указать номер своего счета .
Однако этот код все еще работает с глобальными данными;
теперь это три глобальных списка данных.
Представьте, что вы видите эти данные в виде электронной
таблицы. Пример приведен в табл. 1.1.

Таблица 1.1. Таблица наших данных
Номер счета

Имя

Пароль

Баланс

0

Joe

soup

100

1

Mary

nuts

3550

2

Bill

frisbee

1000

3

Sue

xxyyzz

750

4

Henry

PW

10 000

Данные поддерживаются в виде трех глобальных списков
Python, где каждый список представляет собой столбец в этой
таблице. Например, как видно из выделенного столбца, все пароли сгруппированы в один список. Имена пользователей содержатся в другом списке, а остатки на балансе — в третьем.
При таком подходе, чтобы получить информацию об одном аккаунте, вам нужно получить доступ к этим спискам с общим значением индекса.
Хотя этот способ работает, он кажется крайне неудобным.
Данные не сгруппированы логически. Например, некорректно
хранить пароли всех пользователей вместе. Кроме того, каждый раз, когда вы добавляете новый атрибут к учетной записи,
например адрес или номер телефона, вам нужно создать и получить доступ к другому глобальному списку.
Вместо этого вы бы наверняка предпочли группировку, которая представляет собой строку в той же электронной таблице, как в табл. 1.2.
46 Часть I. Введение в объектно- ориентированное программирование

Таблица 1.2. Таблица наших данных
Номер счета

Имя

Пароль

Баланс

0

Joe

soup

100

1

Mary

nuts

3550

2

Bill

frisbee

1000

3

Sue

xxyyzz

750

4

Henry

PW

10 000

При таком подходе каждая строка содержит данные, связанные с одним банковским счетом. Хотя это одни и те же данные,
такая группировка гораздо более естественно представляет
счет.
Реализация 5. Список словарей учетных записей
Чтобы реализовать этот последний подход, я буду использовать
немного более сложную структуру данных. Я создам список, где
каждый счет (каждый элемент списка) — это словарь, который
выглядит так:
{'name':, 'password':, 'balance':}

ПРИМЕЧАНИЕ

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

Файл: Bank5_Dictionary.py
# Без ООП
# Банк 5
# Любое количество счетов – со списком словарей
accountsList = [] 
def newAccount(aName, aBalance, aPassword):
global accountsList
Глава 1. Процедурные примеры Python 47

newAccountDict = {'name':aName, 'balance':aBalance,
'password':aPassword}
accountsList.append(newAccountDict) 
def show(accountNumber):
global accountsList
print('Account', accountNumber)
thisAccountDict = accountsList[accountNumber]
print('
Name', thisAccountDict['name'])
print('
Balance:', thisAccountDict['balance'])
print('
Password:', thisAccountDict['password'])
print()
def getBalance(accountNumber, password):
global accountsList
thisAccountDict = accountsList[accountNumber] 
if password != thisAccountDict['password']:
print('Incorrect password')
return None
return thisAccountDict['balance']
--- скрыты дополнительные функции deposit() и withdraw() --# создаем два образца учетных записей
print("Joe's account is account number:", len(accountsList))
newAccount("Joe", 100, 'soup')
print("Mary's account is account number:", len(accountsList))
newAccount("Mary", 12345, 'nuts')
while True:
print()
print('Press
print('Press
print('Press
print('Press
print('Press
print('Press
print()

b
d
n
w
s
q

to
to
to
to
to
to

get the balance')
make a deposit')
create a new account')
make a withdrawal')
show all accounts')
quit')

action = input('What do you want to do? ')
action = action.lower() # переводим в нижний регистр
action = action[0] # используем первую букву
print()
if action == 'b':
print('Get Balance:')
userAccountNumber = input('Please enter your account number: ')

48 Часть I. Введение в объектно- ориентированное программирование

userAccountNumber = int(userAccountNumber)
userPassword = input('Please enter the password: ')
theBalance = getBalance(userAccountNumber, userPassword)
if theBalance is not None:
print('Your balance is:', theBalance)
elif action == 'd':
print('Deposit:')
userAccountNumber= input('Please enter the account number: ')
userAccountNumber = int(userAccountNumber)
userDepositAmount = input('Please enter amount to deposit: ')
userDepositAmount = int(userDepositAmount)
userPassword = input('Please enter the password: ')
newBalance = deposit(userAccountNumber, userDepositAmount,
userPassword)
if newBalance is not None:
print('Your new balance is:', newBalance)
elif action == 'n':
print('New Account:')
userName = input('What is your name? ')
userStartingAmount = input('What is the amount of your initial
deposit? ')
userStartingAmount = int(userStartingAmount)
userPassword = input('What password would you like to use for
this account? ')
userAccountNumber = len(accountsList)
newAccount(userName, userStartingAmount, userPassword)
print('Your new account number is:', userAccountNumber)
--- скрыт дополнительный пользовательский интерфейс --print('Done')
Листинг 1.6. Моделирование банка со списком словарей

При таком подходе все данные, связанные с одним счетом,
можно найти в одном словаре . Для каждого нового счета мы
создаем словарь и добавляем его в список счетов . Каждому
счету присваивается номер (простое целое число), который
должен быть указан при выполнении любого действия со счетом. Например, пользователь указывает номер своего счета
при внесении депозита, а функция getBalance() использует
его в качестве индекса в списке счетов .

Глава 1. Процедурные примеры Python 49

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

Общие проблемы с процедурной реализацией
Примеры, приведенные в этой главе, имеют общую проблему:
все данные, которыми оперируют функции, хранятся в одной
или нескольких глобальных переменных. Использование большого количества глобальных данных с процедурным программированием — плохая практика кодирования по следующим
причинам.
1. Любую функцию, которая использует и/или изменяет глобальные данные, сложно повторно использовать в другой программе. Функция, получающая доступ к глобальным данным,
работает с данными, которые живут на другом (более высоком)
уровне, чем код самой функции. Для доступа к ним ей потребуется инструкция global. Вы не можете просто взять функцию,
которая полагается на глобальные данные, и повторно использовать ее в другой программе; она может быть повторно
использована только в программе с похожими глобальными
данными.
2. Многие процедурные программы, как правило, имеют большие
коллекции глобальных переменных. По определению глобальная переменная может быть использована или изменена
любым фрагментом кода в любом месте программы.
Назначения глобальным переменным часто широко разбросаны по процедурным программам как в основном коде, так
и внутри функций. Поскольку значения переменных могут
изменяться в любом месте, отладка и обслуживание программ,
написанных таким образом, чрезвычайно сложны.
3. Функции, написанные для использования глобальных данных,
часто имеют доступ к слишком большому объему данных. Когда
функция использует глобальный список, словарь или любую
50 Часть I. Введение в объектно- ориентированное программирование

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

Объектно-ориентированное решение — первый
взгляд на класс
Листинг 1.7 — это объектно- ориентированный подход, который
объединяет весь код и связанные с ним данные одного счета.
Здесь много новых понятий, и я подробно рассмотрю их начиная со следующей главы. Хотя я не ожидаю, что вы полностью
поймете этот пример, обратите внимание, что это комбинация
кода и данных в одном скрипте (называемая классом *). Вот ваш
первый взгляд на объектно- ориентированный код.
Файл: Account.py
# Класс счета
class Account():
def __init__(self, name, balance, password):
self.name = name
self.balance = int(balance)
self.password = password
def deposit(self, amountToDeposit, password):
if password != self.password:
print('Sorry, incorrect password')
return None
if amountToDeposit < 0:
print('You cannot deposit a negative amount')
return None
self.balance = self.balance + amountToDeposit
return self.balance

*

Классом называется не весь скрипт, а именно комбинация кода и данных. —
Прим. науч. ред.
Глава 1. Процедурные примеры Python 51

def withdraw(self, amountToWithdraw, password):
if password != self.password:
print('Incorrect password for this account')
return None
if amountToWithdraw < 0:
print('You cannot withdraw a negative amount')
return None
if amountToWithdraw > self.balance:
print('You cannot withdraw more than you have in your
account')
return None
self.balance = self.balance – amountToWithdraw
return self.balance
def getBalance(self, password):
if password != self.password:
print('Sorry, incorrect password')
return None
return self.balance
# Код для отладки
def show(self):
print('
Name:', self.name)
print('
Balance:', self.balance)
print('
Password:', self.password)
print()
Листинг 1.7. Первый пример класса на Python

А сейчас взгляните на функции и посмотрите, как они похожи на наши предыдущие примеры процедурного программирования. Функции имеют те же имена, что и в предыдущем
коде, — show(), getBalance(), deposit() и withdraw(), —
но вы также увидите еще и слово self (или self.). О том, что
это значит, вы узнаете в следующих главах.

Выводы
Эта глава начинается с процедурной реализации кода для карточной игры под названием «Больше-меньше». В главе 12

52 Часть I. Введение в объектно- ориентированное программирование

я покажу вам, как сделать объектно- ориентированную версию
игры с графическим пользовательским интерфейсом.
Далее я представил проблему имитации банка с одним, а затем несколькими банковскими счетами. Я показал несколько
различных способов использования процедурного программирования для реализации модели и описал некоторые проблемы, возникающие в результате этого подхода. Наконец, я дал
первое представление о том, как будет выглядеть код, описывающий банковский счет, если бы он был создан с помощью
класса.

2
МОД Е ЛИРОВА НИЕ
ФИЗИЧЕС К И Х ОБЪЕ К ТОВ
С ПОМОЩ ЬЮ ОБЪЕ К ТНО ОРИЕ НТИРОВА ННОГО
ПРОГРА М МИРОВА НИЯ

В этой главе мы рассмотрим общие понятия,
лежащие в основе объектно- ориентированного
программирования. Я покажу простой пример
программы, написанной с использованием процедурного программирования, представлю классы как
основу для написания кода ООП и объясню, как элементы
класса работают вместе. Затем я перепишу первый процедурный пример как класс в объектно- ориентированном стиле
и покажу, как вы создаете объект из класса. В остальной части
главы я пройду через несколько все более сложных классов,
представляющих физические объекты, чтобы продемонстрировать, как ООП решает проблемы процедурного программирования, с которыми мы столкнулись в главе 1. Это должно дать
вам четкое понимание базовых объектно- ориентированных
концепций и того, как они могут улучшить ваши навыки программирования.
Моделирование физических объектов с помощью объектно- ориентированного программирования 55

Построение программных моделей физических
объектов
Чтобы описать физический объект в нашем повседневном
мире, мы часто ссылаемся на его атрибуты. Говоря о столе, вы
можете описать его цвет, размеры, вес, материал и так далее.
Некоторые объекты имеют атрибуты, которые применяются
только к ним, а не к другим. Машину можно описать по количеству дверей, а рубашку — нет. Ящик может быть опечатан или
открыт, пуст или заполнен, но эти характеристики нельзя применить к деревянному блоку. Кроме того, некоторые объекты
способны выполнять действия. Автомобиль может ехать вперед, назад и повернуть влево или вправо.
Чтобы смоделировать реальный объект в коде, нам нужно
решить, какие данные будут представлять атрибуты этого объекта и какие операции он может выполнять. Эти два понятия
часто называют состоянием и поведением объекта соответственно: состояние — это данные, которые объект запоминает,
а поведение — это действия, которые он может совершить.
Состояние и поведение: пример выключателя
освещения
Листинг 2.1 — программная модель стандартного двухпозиционного выключателя освещения, написанная на процедурном
языке Python. Это тривиальный пример, но он будет демонстрировать состояние и поведение.
Файл: LightSwitch_Procedural.py
# Процедурный выключатель света
 def turnOn():
global switchIsOn
# включаем свет
switchIsOn = True
 def turnOff():
global switchIsOn
# выключаем свет
switchIsOn = False
# Основной код
 switchIsOn = False # глобальная логическая переменная

56 Часть I. Введение в объектно- ориентированное программирование

# код теста
print(switchIsOn)
turnOn()
print(switchIsOn)
turnOff()
print(switchIsOn)
turnOn()
print(switchIsOn)
Листинг 2.1. Модель выключателя света, написанная процедурным кодом

Переключатель может находиться только в одном из двух положений: «вкл.» или «выкл.». Чтобы смоделировать состояние,
нам нужна только одна логическая переменная. Назовем эту переменную switchIsOn  и скажем, что True означает «вкл.»,
а False — «выкл.». Когда переключатель поступает с завода, он
находится в выключенном положении, поэтому мы изначально
установили переключатель IsOn на False.
Далее мы посмотрим на поведение. Этот переключатель может выполнять только два действия: «включить» и «выключить». Поэтому мы строим две функции, turnOn() 
и turnOff() , которые устанавливают значение одной логической переменной равным True и False соответственно.
Я добавил тестовый код в конце, чтобы включить и выключить переключатель несколько раз. Результат — это именно то,
что мы ожидаем:
False
True
False
True

Это чрезвычайно простой пример, но он, начиная с таких
вот небольших функций, упрощает переход к ООП. Как я объяснил в главе 1, поскольку мы использовали глобальную переменную для представления состояния (в данном случае переменную switchIsOn), этот код будет работать только для
конкретного выключателя освещения, но одна из основных целей написания функций — создать многоразовый код. Поэтому
я заново построю код выключателя света, используя объектноориентированное программирование, однако сначала мне нужно проработать часть базовой теории.
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 57

Введение в классы и объекты
Первым шагом к пониманию того, что такое объект и как он
работает, является понимание взаимосвязи между классом
и объектом. Я дам формальные определения позже, но на данный момент вы можете думать о классе как о шаблоне или
плане, который определяет, как будет выглядеть объект при
его создании. Мы создаем объекты из класса.
Представьте, что мы начали выпечку тортов по требованию.
То есть мы делаем торт только тогда, когда поступает заказ
на него. Мы специализируемся на тортах Bundt и потратили
много времени на разработку формы как на рис. 2.1, чтобы убедиться, что наши торты не только вкусные, но и красивые.
Форма определяет, как будет выглядеть торт Bundt, когда мы
создадим его, но она, конечно, не сам торт. Форма символизирует класс. Когда поступает заказ, мы создаем торт Bundt из нашей формы (рис. 2.2). Торт — это предмет, изготовленный
с применением формы.
Используя ее, мы можем создать любое количество тортов.
Они могут иметь разные атрибуты: различные вкусы, различные виды глазури и дополнительные опции, такие как шоколадные чипсы, но все торты будут одной и той же формы.

Рис. 2.1. Форма для торта как метафора для класса

58 Часть I. Введение в объектно- ориентированное программирование

Рис. 2.2. Торт как метафора для объекта, сделанного из класса формы

Втабл. 2.1 приведены некоторые другие примеры из реального мира, которые помогают прояснить связь между классом
и объектом.

Таблица 2.1. Примеры реальных классов и объектов
Класс

Объект, созданный из класса

Чертеж дома

Дом

Сэндвич в меню

Сэндвич в вашей руке

Штамп, используемый для
изготовления 25-центовой монеты

Четвертак

Рукопись книги, написанной
автором

Физическая или электронная копия
книги

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 59

Классы, объекты и экземпляры
Давайте посмотрим, как это работает в коде.

Класс
Код, который определяет, что объект запомнит (его данные или состояние)
и что он сможет сделать (его функции или поведение).

Чтобы получить представление о том, как выглядит класс,
вот код выключателя света, записанный как класс:

# OO_LightSwitch
class LightSwitch():
def __init__(self):
self.switchIsOn = False
def turnOn(self):
# включаем переключатель
self.switchIsOn = True
def turnOff(self):
# выключаем переключатель
self.switchIsOn = False

Мы пройдемся по деталям чуть позже, но стоит заметить,
что этот код определяет одну переменную, self.switchIsOn,
которая инициализирована в одной функции и содержит две
другие, определяющие поведение: turnOn() и turnOff().
Если вы пишете код класса и пытаетесь запустить его, ничего не происходит, так же как и при запуске программы
на Python, которая состоит только из функций и не вызывает
их. Необходимо явно приказать Python создать объект
из класса.
Чтобы создать объект LightSwitch из нашего класса
LightSwitch, мы обычно используем такую строку:

oLightSwitch = LightSwitch()

60 Часть I. Введение в объектно- ориентированное программирование

Здесь говорится: найдите класс LightSwitch, создайте объект LightSwitch из этого класса и назначьте полученный объект переменной oLightSwitch.
ПРИМЕЧАНИЕ

В этой книге для обозначения переменной, которая представляет объект, я, как правило, использую префикс в нижнем регистре o. Это не обязательно, но помогает напомнить себе, что
переменная представляет объект.
Другое слово, которое вы найдете в ООП, — экземпляр
(instance). Слова экземпляр и объект по существу взаимозаменяемы; однако, если быть точными, мы бы сказали, что объект
LightSwitch является экземпляром класса LightSwitch.

Инстанциирование
Процесс создания объекта из класса.

В предыдущей инструкции назначения мы прошли процесс
инстанциирования для создания объекта LightSwitch из класса LightSwitch. Мы также можем обозначить этот процесс как
глагол; мы инстанциируем LightSwitch из класса LightSwitch.
Написание класса на Python
Обсудим различные части класса и детали создания экземпляров и использования объекта. В листинге 2.2 показана общая
форма класса на Python.
class ():
def __init__(self, , ..., ):
# код инициализации
# Функции, которые обращаются к данным
# Каждая имеет вид:
def (self, , ..., ):
# тело функции
# ...другие функции
def (self, , ..., ):
# тело функции
Листинг 2.2. Типичная форма класса на Python
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 61

Определение класса начинается с оператора class, указывающего имя, которое вы хотите дать классу. Для таких названий принято использовать горбатый регистр с первой заглавной буквой (например, LightSwitch). После имени вы можете
добавить набор скобок, но оператор должен заканчиваться
двоеточием, чтобы указать, что вы собираетесь начать тело
класса. (Я объясню, что может быть в скобках, в главе 10, когда
мы будем обсуждать наследование.)
В теле класса можно определить любое количество функций. Все функции считаются частью класса, и код, который их
определяет, должен начинаться отступом. Каждая функция
представляет определенное поведение, которое может выполнять объект, созданный из к ласса. Все функции должны иметь
хотя бы один параметр, который по соглашению называется
self (я объясню, что означает это имя, в главе 3). Функциям
ООП дано специальное название: метод.

Метод
Функция, определенная внутри класса. Метод всегда имеет хотя бы один
параметр, который обычно называется self.

Первый метод в каждом классе должен иметь специальное
имя __init__. Всякий раз, когда вы создаете объект из класса,
этот метод будет вызываться автоматически. Поэтому он — логичное место для размещения любого кода инициализации, который вы хотите запустить, когда создаете экземпляр объекта
из класса. Имя __init__ зарезервировано Python для этой самой задачи и должно выглядеть именно так, с двумя подчеркиваниями до и после слова init (которое пишется строчными
буквами). На самом деле метод __init__() не является обязательным. Однако, как правило, считается хорошей практикой
включать его и использовать для инициализации.
ПРИМЕЧАНИЕ

Когда вы создаете экземпляр объекта из класса, Python заботится о создании объекта (выделении памяти) за вас.
Специальный метод __init__() называется методом initializer,
где вы задаете переменным начальные значения. (Для большинства других языков ООП требуется метод с именем new(),
который часто называют конструктором.)

62 Часть I. Введение в объектно- ориентированное программирование

Область видимости и переменные экземпляра
В процедурном программировании существует два основных
уровня области видимости: переменные, созданные в основном
коде, имеют глобальную область видимости и доступны в любом
месте программы, в то время как переменные, созданные внутри функции, имеют локальную область видимости и живут
только до тех пор, пока выполняется функция. При выходе
из функции все локальные переменные (переменные с локальной областью видимости) буквально исчезают.
Объектно- ориентированное программирование и классы
представляют третий уровень области видимости, обычно называемый областью видимости объекта, иногда областью видимости класса или, реже, областью видимости экземпляра. Все
они означают одно и то же: область видимости состоит из всего
кода внутри определения класса.
Методы могут использовать как локальные переменные, так
и переменные экземпляра. В методе любая переменная, имя которой не начинается с self, является локальной переменной
и исчезает при выходе из него, то есть другие методы в классе
больше не могут ее использовать. Переменные экземпляра имеют
область видимости объекта, то есть они доступны всем методам, определенным в классе. Переменные экземпляра и область
видимости объекта — это ключи к пониманию того, как объекты запоминают данные.

Переменная экземпляра
В методе любая переменная, имя которой начинается, по соглашению,
с префикса self. (например, self.x). Переменные экземпляра имеют
область видимости объекта.

Подобно локальным и глобальным переменным, переменные экземпляра создаются, когда им в первый раз присваивается значение, и не требуют специального объявления. Метод
__init__() — логичное место для инициализации переменных
экземпляра. Вот пример класса, где метод __init__() инициализирует переменную экземпляра self.count (читается как
«self dot count») в ноль, и другой метод, increment(), который
просто добавляет 1 к self.count:

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 63

class MyClass():
def __init__(self):
self.count = 0 # создаем self.count и установим ее на 0
def increment(self):
self.count = self.count + 1 # инкремент переменной

При создании экземпляра объекта из класса MyClass запускается метод __init__() со значением переменной экземпляра self.count, равным нулю. При вызове метода increment()
значение self.count изменяется с нуля на единицу. При повторном вызове increment() значение изменяется с одного
на два и так далее.
Каждый объект, созданный из класса, получает свой набор
переменных экземпляра независимо от любых других объектов,
созданных из этого класса. В случае класса LightSwitch существует только одна переменная экземпляра, self.switchIsOn,
поэтому каждый объект LightSwitch будет иметь собственный
self.switchIsOn. Таким образом, вы можете иметь несколько
объектов LightSwitch, каждый из которых имеет свое независимое значение True или False для своей переменной
self.switchIsOn.
Различия между функциями и методами
Подводя итог, можно отметить три ключевых различия между
функцией и методом.
1. Все методы класса должны иметь отступы под инструкцией
class.
2. Все методы имеют специальный первый параметр, который
(так принято) называется self.
3. Методы в классе могут использовать переменные экземпляра,
записанные в виде self..
Теперь, когда вы знаете, что такое методы, я покажу вам, как
создать объект из класса и как использовать различные методы, доступные в классе.

64 Часть I. Введение в объектно- ориентированное программирование

ПРОЦЕСС СОЗ Д АНИЯ ЭК ЗЕМПЛЯРА

На рис. 2.3 показаны этапы создания экземпляра объекта
LightSwitch из класса LightSwitch, начинающегося в инструкции присваивания в Python, затем переходящего в код
класса, затем обратно через Python и, наконец, обратно
в инструкцию присваивания.
Код инстанциирования

Python

Класс LightSwitch

oLightSwitch = LightSwitch()

Выделяет место в памяти
для объекта LightSwitch
Вызывает метод __init__()
класса LightSwitch,
передавая новый объект
__init__() метод работает,
устанавливает значение
“self” для нового объекта
Возвращает новый объект

Инициализирует любые
переменные экземпляра

oLightSwitch = LightSwitch()

Назначает новый объект oLightSwitch

Рис. 2.3. Процесс создания экземпляра объекта
Процесс состоит из пяти этапов.
1. Наш код просит Python создать объект из класса
LightSwitch.
2. Python выделяет место в памяти для объекта
LightSwitch, затем вызывает метод __init__() класса LightSwitch, передавая вновь созданный объект.
3. Метод __init__() класса LightSwitch выполняется.
Новый объект присваивается параметру self. Код
__init__() инициализирует любые переменные экземпляра в объекте (в данном случае переменную экземпляра self.switchIsOn).
4. Python возвращает новый объект исходному вызывающему.
5. Результат исходного вызова присваивается переменной
oLightSwitch, поэтому теперь она представляет объект.

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 65

Создание объекта из класса
Как я уже говорил, класс просто определяет, как будет выглядеть объект. Чтобы использовать класс, вы должны сказать
Python создать объект из класса. Типичный способ сделать
это — использовать оператор присваивания, как тут:
= ()

Эта единственная строка кода вызывает последовательность шагов, которая заканчивается тем, что Python возвращает вам новый экземпляр класса, который вы обычно храните
в переменной. Эта переменная ссылается на результирующий
объект.
Вы можете сделать класс доступным двумя способами: поместить код класса в один файл с основной программой или
во внешний файл и использовать оператор импорта, чтобы
подключить содержимое файла. В этой главе я покажу первый
подход, а второй — в главе 4. Единственное правило заключается в том, что определение класса должно предшествовать любому коду, который создает экземпляр объекта из класса.
Вызов методов объекта
После создания объекта из класса для вызова метода объекта
используется обобщенный синтаксис:
.()

В листинге 2.3 показан класс LightSwitch, код для создания
экземпляра объекта из класса и код для включения и выключения объекта LightSwitch путем вызова его методов turnOn()
и turnOff().
Файл: OO_LightSwitch_with_Test_Code.py
# OO_LightSwitch
class LightSwitch():
def __init__(self):
self.switchIsOn = False
def turnOn(self):
# включаем выключатель
self.switchIsOn = True

66 Часть I. Введение в объектно- ориентированное программирование

def turnOff(self):
# выключаем выключатель
self.switchIsOn = False
def show(self): # добавлено для тестирования
print(self.switchIsOn)
# Основной код
oLightSwitch = LightSwitch() # создаем объект LightSwitch
# Вызовы методов
oLightSwitch.show()
oLightSwitch.turnOn()
oLightSwitch.show()
oLightSwitch.turnOff()
oLightSwitch.show()
oLightSwitch.turnOn()
oLightSwitch.show()
Листинг 2.3. Класс LightSwitch и тестовый код для создания объекта
и вызова его методов

Сначала мы создаем объект LightSwitch и назначаем его переменной oLightSwitch. Затем мы используем эту переменную
для вызова других методов, доступных в классе LightSwitch.
Мы бы прочитали эти строки как «oLightSwitch dot show»,
«oLightSwitch dot turnOn» и так далее. Если запустим этот
код, он выведет следующее:
False
True
False
True

Напомним, что этот класс имеет одну переменную экземпляра с именем self.switchIsOn, но ее значение запоминается
и легкодоступно, когда выполняются разные методы одного
и того же объекта.
Создание нескольких экземпляров из одного класса
Одной из ключевых особенностей ООП является то, что вы
можете создать сколько угодно объектов из одного класса,
так же как вы можете испечь бесконечное количество тортов
в форме Bundt.

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 67

Итак, если вам нужно два объекта переключения света, или
три, или больше, вы можете просто создать дополнительные
объекты из класса LightSwitch примерно так:
oLightSwitch1 = LightSwitch() # создаем объект LightSwitch
oLightSwitch2 = LightSwitch() # создаем еще один объект LightSwitch

Важно то, что каждый объект, который вы создаете из класса, поддерживает свою собственную версию данных. В этом случае
каждый из oLightSwitch1 и oLightSwitch2 имеет свою переменную экземпляра, self.switchIsOn. Любые изменения, внесенные в данные одного объекта, не повлияют на данные другого. Вы можете вызвать любой из методов в классе с помощью
любого объекта. Пример в листинге 2.4 создает два объекта переключателей света и вызывает методы для разных объектов.
Файл: OO_LightSwitch_Two_Instances.py
# OO_LightSwitch
class LightSwitch():
--- пропущенный код класса LightSwitch, как в листинге 2-3 --# Основной код
oLightSwitch1 = LightSwitch() # создаем объект LightSwitch
oLightSwitch2 = LightSwitch() # создаем еще один объект LightSwitch
# код теста
oLightSwitch1.show()
oLightSwitch2.show()
oLightSwitch1.turnOn() # Переключатель 1 включен
# Переключатель 2 должен быть выключен при запуске,
# этот код делает это более очевидным
oLightSwitch2.turnOff()
oLightSwitch1.show()
oLightSwitch2.show()
Листинг 2.4. Создайте два экземпляра класса и вызовите методы каждого

Вот вывод, который вы получите при запуске программы:
False
False
True
False

68 Часть I. Введение в объектно- ориентированное программирование

Код предписывает oLightSwitch1 включить себя и предписывает oLightSwitch2 выключить себя. Обратите внимание,
что код в классе не имеет глобальных переменных. Каждый
объект LightSwitch получает собственный набор любых переменных экземпляра (в данном случае только одну), определенных в классе.
Хотя это может показаться небольшим улучшением по сравнению с наличием двух простых глобальных переменных, которые могут быть использованы для одного и того же, последствия этого изменения огромны. Вы получите лучшее
представление об этом в главе 4, где я буду рассказывать, как создавать и поддерживать большое количество экземпляров, созданных из класса.
Типы данных Python реализованы как классы
Возможно, вас не удивит, что все встроенные типы данных
в Python реализованы как классы. Можно привести простой
пример:
>>> myString = 'abcde'
>>> print(type(myString))


Мы присваиваем переменной строковое значение. Когда вызываем функцию type() и выводим результаты, мы видим, что
у нас есть экземпляр класса str string. Класс str дает нам ряд
методов, которые можно вызвать для строк, включая
myString.upper(), myString.lower(), myString.strip()
и так далее.
Списки работают аналогичным образом:
>>> myList = [10, 20, 30, 40]
>>> print(type(myList))


Все списки являются экземплярами класса list, который
имеет множество методов, включая myList.append(),
myList.count(), myList.index() и так далее.
Когда пишете класс, вы определяете новый тип данных. Ваш
код предоставляет подробную информацию, показывая, какие
данные он хранит и какие операции может выполнять. После
создания экземпляра вашего класса и назначения его
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 69

переменной вы можете использовать встроенную функцию
type() для определения класса, используемого для создания экземпляра, так же как со встроенным типом данных. Здесь мы
создаем экземпляр объекта LightSwitch и выводим его тип
данных:
>>> oLightSwitch = LightSwitch()
>>> print(type(oLightSwitch))


Как и со встроенными типами данных Python, мы можем использовать переменную oLightSwitch для вызова методов, доступных в классе oLightSwitch.
Определение объекта
Подводя итог этому разделу, я дам свое официальное определение объекту.

Объект
Данные плюс код, который действует на них с течением времени.

Класс определяет, как будет выглядеть объект при создании
экземпляра. Объект — это набор переменных экземпляра и код
методов в классе, из которого был создан экземпляр объекта.
Любое количество объектов может быть инстанциировано
из класса, и каждый из них имеет собственный набор переменных экземпляра. При вызове метода объекта метод запускается
и использует набор переменных экземпляра в этом объекте.

Создание несколько более сложного класса
Давайте построим на концепции, представленной ранее, второй, немного более сложный пример, в котором мы сделаем
класс выключателя-диммера. Выключатель-диммер имеет переключатель включить/выключить, но также у него есть мультипозиционный ползунок, который влияет на яркость света.
Ползунок может перемещаться по диапазону значений яркости. Для простоты наш цифровой ползунок диммера имеет
11 положений, от 0 (полностью выключено) до 10 (полностью
включено). Чтобы увеличить или уменьшить яркость лампы
70 Часть I. Введение в объектно- ориентированное программирование

в максимальной степени, вы должны перемещать ползунок через каждую возможную настройку.
Класс DimmerSwitch обладает большей функциональностью, чем класс LightSwitch, и должен запоминать больше данных:
• состояние переключателя (включено или выключено);
• уровень яркости (от 0 до 10).
А вот поведение объекта DimmerSwitch:
• включить;
• выключить;
• увеличить яркость;
• уменьшить яркость;
• вывести яркость (для отладки).
Класс DimmerSwitch использует стандартный шаблон, показанный ранее в листинге 2.2: он начинается с оператора класса
и первого метода с именем __init__(), а затем определяет ряд
дополнительных методов, по одному для каждого из перечисленных действий. Полный код для этого класса представлен
в листинге 2.5.
Файл: DimmerSwitch.py
# Класс DimmerSwitch
class DimmerSwitch():
def __init__(self):
self.switchIsOn = False
self.brightness = 0
def turnOn(self):
self.switchIsOn = True
def turnOff(self):
self.switchIsOn = False
def raiseLevel(self):
if self.brightness < 10:
self.brightness = self.brightness + 1
def lowerLevel(self):
if self.brightness > 0:
self.brightness = self.brightness – 1
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 71

# Дополнительный метод для отладки
def show(self):
print('Switch is on?', self.switchIsOn)
print('Brightness is:', self.brightness)
Листинг 2.5. Немного более сложный класс DimmerSwitch

В этом методе __init__() мы имеем две переменные экземпляра: знакомую self.switchIsOn и новую, self.brightness,
которая запоминает уровень яркости. Мы присваиваем начальные значения обеим переменным экземпляра. Все остальные
методы могут получить доступ к текущему значению каждой
из них. В дополнение к turnOn() и turnOff() мы включаем два
новых метода для этого класса: raiseLevel() и lowerLevel(), —
которые делают именно то, что подразумевают их имена. Метод show() используется во время разработки и отладки и просто выводит текущие значения переменных экземпляра.
Основной код в листинге 2.6 тестирует наш класс, создавая
объект DimmerSwitch (oDimmer), затем вызывая различные методы.
Файл: OO_DimmerSwitch_with_Test_Code.py
# Класс DimmerSwitch с тестовым кодом
class DimmerSwitch():
--- скрыт фрагмент кода класса DimmerSwitch, как
в листинге 2.5 --# Основной код
oDimmer = DimmerSwitch()
# включаем переключатель и поднимаем уровень яркости 5 раз
oDimmer.turnOn()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.show()
# Уменьшаем уровень яркости 2 раза и выключаем переключатель
oDimmer.lowerLevel()
oDimmer.lowerLevel()

72 Часть I. Введение в объектно- ориентированное программирование

oDimmer.turnOff()
oDimmer.show()
# включаем переключатель и поднимаем уровень яркости 3 раза
oDimmer.turnOn()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.raiseLevel()
oDimmer.show()
Листинг 2.6. Класс DimmerSwitch с тестовым кодом

Когда мы запускаем этот код, в результате получаем вывод:
Switch is on? True
Brightness is: 5
Switch is on? False
Brightness is: 3
Switch is on? True
Brightness is: 6

Основной код создает объект oDimmer, затем вызывает различные методы. Каждый раз, когда мы вызываем метод show(),
состояние включения/выключения и уровень яркости выводятся на экран. Здесь важно помнить, что oDimmer представляет объект. Он разрешает доступ ко всем методам в классе, из которого он был создан (класс DimmerSwitch), и он имеет набор
всех переменных экземпляра, определенных в классе (self.
switchIsOn и self.brightness). Опять же, переменные экземпляра сохраняют свои значения между вызовами методов объекта, поэтому переменная экземпляра self.brightness увеличивается на 1 для каждого вызова oDimmer.raiseLevel().

Представление более сложного физического
объекта как класса
Рассмотрим более сложный физический объект — телевизор.
В этом примере мы рассмотрим подробнее, как работают аргументы в классах.
Телевизору требуется гораздо больше данных, чем выключателю света, чтобы представлять его состояние, и у него больше
действий. Чтобы создать класс телевизора, мы должны подумать о том, как обычно используется телевизор и что он должен

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 73

помнить. Рассмотрим некоторые важные кнопки на типичном
пульте дистанционного управления телевизором (рис. 2.4).

Питание

Громкость

Канал

Отключение
звука

Получить
информацию

1

2

3

4

5

6

7

8

9

0

Рис. 2.4. Упрощенный пульт от телевизора

Исходя из этого, мы можем определить, что для отслеживания своего состояния класс ТВ должен будет поддерживать следующие данные:
• состояние питания (вкл. или выкл.);
• состояние отключения звука (отключено?);
• список доступных каналов;
• текущая настройка канала;
• текущая настройка громкости;
• диапазон доступных уровней громкости.
А действия, которые должен обеспечить телевизор, включают в себя:
• включение и выключение питания;
• повышение и понижение громкости;
74 Часть I. Введение в объектно- ориентированное программирование






переключение каналов вверх и вниз;
выключение/включение звука;
получение информации о текущих настройках;
переход к указанному каналу.

Код для нашего класса телевизора показан в листинге 2.7.
Мы включаем метод инициализации __init__(), за которым
следует метод для каждого действия.
Файл: TV.py
# класс TV
class TV():
def __init__(self): 
self.isOn = False
self.isMuted = False
# Некий список каналов по умолчанию
self.channelList = [2, 4, 5, 7, 9, 11, 20, 36, 44, 54, 65]
self.nChannels = len(self.channelList)
self.channelIndex = 0
self.VOLUME_MINIMUM = 0 # константа
self.VOLUME_MAXIMUM = 10 # константа
self.volume = self.VOLUME_MAXIMUM // # целочисленная переменная
def power(self): 
self.isOn = not self.isOn # переключатель
def volumeUp(self):
if not self.isOn:
return
if self.isMuted:
self.isMuted = False # изменение громкости включает звук,
# если тот отключен
if self.volume < self.VOLUME_MAXIMUM:
self.volume = self.volume + 1
def volumeDown(self):
if not self.isOn:
return
if self.isMuted:
self.isMuted = False # изменение громкости включает звук,
# если тот отключен
if self.volume > self.VOLUME_MINIMUM:
self.volume = self.volume – 1
def channelUp(self): 

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 75

if not self.isOn:
return
self.channelIndex = self.channelIndex + 1
if self.channelIndex > self.nChannels:
self.channelIndex = 0 # после последнего канала вернуться
# к первому каналу
def channelDown(self): 
if not self.isOn:
return
self.channelIndex = self.channelIndex – 1
if self.channelIndex < 0:
self.channelIndex = self.nChannels – 1 # перед первым
# каналом – последний
def mute(self): 
if not self.isOn:
return
self.isMuted = not self.isMuted
def setChannel(self, newChannel):
if newChannel in self.channelList:
self.channelIndex = self.channelList.index(newChannel)
# если newChannel нет в нашем списке каналов, ничего не делать
def showInfo(self): 
print()
print('TV Status:')
if self.isOn:
print(' TV is: On')
print(' Channel is:', self.channelList[self.channelIndex])
if self.isMuted:
print(' Volume is:', self.volume, '(sound is muted)')
else:
print(' Volume is:', self.volume)
else:
print('
TV is: Off')
Листинг 2.7. Класс телевизора со множеством переменных экземпляров и методов

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

76 Часть I. Введение в объектно- ориентированное программирование

экземпляра в методе __init__(). Это позволяет избежать риска ошибки при попытке использовать переменную экземпляра
в методе до того, как она была определена.
Метод power() показывает, что происходит при нажатии
кнопки питания на пульте управления. Если телевизор выключен, нажатие кнопки питания включает его; если телевизор
включен, нажатие кнопки питания выключает его. Для кодирования этого поведения я использовал переключатель, который
является логический переменной, используется для представления одного из двух состояний и может легко переключаться
между ними. С помощью этого переключателя оператор not переключает переменную self.isOn с True на False или с False
на True. Код метода mute() выполняет аналогичные действия, при этом переменная self.muted переключается между
выключенным и включенным звуком, но сначала должна проверить, что телевизор включен. Если телевизор выключен, вызов
метода mute() не имеет никакого эффекта.
Интересно отметить, что мы не следим за текущим каналом.
Вместо этого мы отслеживаем индекс текущего канала, который позволяет нам получить текущий канал в любое время
с помощью self.channelList[self.channelIndex].
Методы channelUp() и channelDown() в основном увеличивают и уменьшают индекс канала, но в них также есть некоторый умный код, позволяющий идти по кругу. Если вы находитесь на последнем индексе в списке каналов и пользователь
просит перейти к следующему каналу вверх, телевизор переходит к первому каналу в списке. Если вы находитесь на первом
индексе в списке каналов и пользователь просит перейти к следующему каналу вниз, телевизор переходит к последнему каналу в списке.
Метод showInfo() выводит текущее состояние телевизора
на основе значений переменных экземпляра (вкл./выкл., текущий канал, текущая настройка громкости и настройка отключения звука).
В листинге 2.8 мы создадим объект телевизора и вызовем методы этого объекта.

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 77

Файл: OO_TV_with_Test_Code.py
# Класс TV с тестовым кодом
--- фрагмент кода класса TV, как в листинге 2.7 --# Основной код
oTV = TV() # создаем ТВ-объект
# включаем телевизор и показываем статус
oTV.power()
oTV.showInfo()
# Дважды меняем канал, дважды увеличиваем громкость, показываем статус
oTV.channelUp()
oTV.channelUp()
oTV.volumeUp()
oTV.volumeUp()
oTV.showInfo()
# Выключаем телевизор, показываем статус, включаем телевизор,
# показываем статус
oTV.power()
oTV.showInfo()
oTV.power()
oTV.showInfo()
# Убавляем громкость, отключаем звук, показываем состояние
oTV.volumeDown()
oTV.mute()
oTV.showInfo()
# Переключаем канал на 11, отключаем звук, показываем состояние
oTV.setChannel(11)
oTV.mute()
oTV.showInfo()
Листинг 2.8. Класс телевизора с тестовым кодом

Когда запускаем этот код, вот что мы получаем в качестве
вывода:

TV Status:
TV is: On
Channel is: 2
Volume is: 5

78 Часть I. Введение в объектно- ориентированное программирование

TV Status:
TV is: On
Channel is: 5
Volume is: 7
TV Status:
TV is: Off
TV Status:
TV is: On
Channel is: 5
Volume is: 7
TV Status:
TV is: On
Channel is: 5
Volume is: 6 (sound is muted)
TV Status:
TV is: On
Channel is: 11
Volume is: 6

Все методы работают корректно, и мы получаем ожидаемый
результат.
Передача аргументов методу
При вызове любой функции количество аргументов должно
соответствовать количеству параметров, перечисленных в операторе def:
def myFunction(param1, param2, param3):
# тело функции
# вызов функции:
myFunction(argument1, argument2, argument3)

То же правило применяется к методам и вызовам методов.
Тем не менее вы можете заметить, что всякий раз, когда мы делаем вызов методу, кажется, что мы указываем на один аргумент меньше, чем количество параметров. Например, определение метода power() в нашем телевизионном классе выглядит
следующим образом:
def power(self):
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 79

Это означает: метод power() ожидает, что будет передано
одно значение и все переданное будет присвоено переменной
self. Тем не менее, когда мы начали с включения телевизора
в листинге 2.8, мы сделали такой вызов:
oTV.power()

Когда мы делаем вызов, мы не передаем ничего явно внутри
скобок.
Это может показаться еще более странным в случае метода
setChannel(). Метод написан так, что принимает два параметра:
def setchannel(self, newchannel):
if newChannel in self.channelList:
self.channelIndex = self.channelList.index(newChannel)

Но мы вызвали setChannel() следующим образом:
oTV.setChannel(11)

Похоже, что передается только одно значение.
Вы можете ожидать, что Python сгенерирует здесь ошибку
из-за несоответствия в количестве переданных аргументов
(один) и количестве ожидаемых параметров (два). На практике
Python делает небольшую закулисную работу, чтобы облегчить
синтаксис.
Давайте рассмотрим это подробнее. Ранее я говорил, что
для вызова метода объекта вы используете следующий общий
синтаксис:
.()

Python берет , указанный в вызове, и подставляет
его в качестве первого аргумента. Любые значения в скобках
вызова метода считаются последующими аргументами. Таким
образом Python делает вид, что вы написали это вместо своего
вызова:
(, )

80 Часть I. Введение в объектно- ориентированное программирование

На рис. 2.5 показано, как это работает в нашем примере
кода, опять же с помощью метода setChannel() класса TV.
# Метод в классе TV
def setChannel(self, newChannel):


# Вызов
зо
oTV.setChannel(11)
V

Рис. 2.5. Вызов метода

Хотя похоже, что мы предоставляем только один аргумент
(для newChannel), на самом деле передано два аргумента: oTV
и 11, — и метод предоставляет два параметра для получения
этих значений (self и newChannel соответственно). Python меняет за нас порядок аргументов при вызове. Поначалу это может показаться странным, но очень быстро вой дет в привычку.
Запись вызова с помощью объекта в первую очередь значительно упрощает для программиста определение того, на какой
объект выполняется действие.
Это тонкая, но важная особенность. Помните, что объект
(в данном случае oTV) сохраняет текущие настройки всех своих
переменных экземпляра. Передача объекта в качестве первого
аргумента позволяет методу работать со значениями переменных экземпляра этого объекта.
Несколько экземпляров
Каждый метод записывается с собой в качестве первого параметра, поэтому переменная self получает объект, используемый в каждом вызове. Это имеет большое значение: оно позволяет любому методу в классе работать с различными объектами.
Я объясню, как это работает, используя пример.
В листинге 2.9 мы создадим два объекта телевизора и сохраним их в двух переменных, oTV1 и oTV2. Каждый телевизионный объект имеет настройку громкости, список каналов, настройку канала и так далее. Мы будем вызывать несколько
разных методов для разных объектов. В конце мы вызовем метод showInfo() для каждого объекта TV, чтобы увидеть результирующие настройки.

Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 81

Файл: OO_TV_TwoInstances.py
# Два объекта TV с вызовами к их методам
class TV():
--- фрагмент кода класса TV, как в листинге 2.7 --# Основной код
oTV1 = TV() # создаем один объект TV
oTV2 = TV() # создаем еще один объект TV
# включаем оба телевизора
oTV1.power()
oTV2.power()
# увеличиваем громкость TV1
oTV1.volumeUp()
oTV1.volumeUp()
# увеличиваем громкость TV2
oTV2.volumeUp()
oTV2.volumeUp()
oTV2.volumeUp()
oTV2.volumeUp()
oTV2.volumeUp()
# Переключаем канал TV2, затем отключаем звук
oTV2.setChannel(44)
oTV2.mute()
# Теперь отображаем состояние обоих телевизоров
oTV1.showInfo()
oTV2.showInfo()
Листинг 2.9. Создание двух экземпляров класса TV и вызов методов каждого

Если мы запустим этот код, он сгенерирует следующий вывод:
Status of TV:
TV is: On
Channel is: 2
Volume is: 7
Status of TV:
TV is: On
Channel is: 44
Volume is: 10 (sound is muted)

82 Часть I. Введение в объектно- ориентированное программирование

Каждый объект TV поддерживает собственный набор переменных экземпляра, определенных в классе. Таким образом, переменными экземпляра каждого объекта TV можно манипулировать независимо от переменных любого другого объекта TV.
Параметры инициализации
Возможность передавать аргументы вызовам метода также
работает при создании экземпляра объекта. До сих пор, когда
мы создавали наши объекты, мы всегда устанавливали переменные их экземпляров на постоянные значения. Однако часто
нужно создавать разные объекты с разными стартовыми значениями. Например, представьте, что мы хотим сделать экземпляры различных телевизоров и идентифицировать их по их
бренду и местоположению. Таким образом, мы можем различать телевизор Samsung в гостиной и телевизор Sony в спальне.
Константы не сработали бы для нас в этой ситуации.
Чтобы инициализировать объект с разными значениями,
мы добавляем параметры в определение метода __init__(),
например вот так:
# класс TV
class TV():
def __init__(self, brand, location): # передаем бренд
# и расположение телевизора
self.brand = brand
self.location = location
--- оставшаяся инициализация телевизора --...

Во всех методах параметры являются локальными переменными, поэтому они буквально исчезают, когда метод заканчивается. Например, в методе __init__() показанного здесь класса
TV бренд и местоположение — локальные переменные, которые
исчезают по завершении метода. Однако мы часто хотим сохранить значения, передающиеся через параметры, чтобы использовать их в других методах.
Для того чтобы объект запомнил начальные значения, стандартный подход заключается в сохранении любых значений,
переданных в переменные экземпляра. Поскольку переменные
экземпляра имеют область видимости объекта, их можно использовать в других методах класса. Соглашение Python
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 83

заключается в том, что имя переменной экземпляра должно совпадать с именем параметра, но с префиксом self и точкой:
def __init__(self, someVariableName):
self.someVariableName = someVariableName

В классе TV строка после оператора def говорит Python
взять значение параметра brand и назначить его переменной
экземпляра с именем self.brand. Следующая строка делает
то же самое с параметром location и переменной экземпляра
self.location. После этих назначений мы можем использовать self.brand и self.location в других методах.
Используя этот подход, мы можем создать несколько объектов из одного класса, но начать каждый с разных данных. Таким образом мы можем создать два наших телевизионных объекта, как тут:
oTV1 = TV('Samsung', 'Family room')
oTV2 = TV('Sony', 'Bedroom')

При выполнении первой строки Python сначала выделяет
место для объекта TV. Затем он переупорядочивает аргументы,
как обсуждалось в предыдущем разделе, и вызывает метод
__init__() класса TV с тремя аргументами: вновь выделенным
объектом oTV1, брендом и местоположением.
При инициализации объекта oTV1 для self.brand задается
строка 'Samsung', а для self.location — строка 'Family
room'. При инициализации oTV2 его переменной self.brand
присваивается строка 'Sony', а его self.location присваивается строка 'Bedroom'.
Можно изменить метод showInfo(), чтобы сообщить модель и местоположение телевизора.
Файл: OO_TV_TwoInstances_with_Init_Params.py
def showInfo(self):
print()
print('Status of TV:', self.brand)
print(' Location:', self.location)
if self.isOn:
...

84 Часть I. Введение в объектно- ориентированное программирование

И тогда увидим это в выводе:
Status of TV: Sony
Location: Family room
TV is: On
Channel is: 2
Volume is: 7
Status of TV: Samsung
Location: Bedroom
TV is: On
Channel is: 44
Volume is: 10 (sound is muted)

Мы выполнили те же вызовы метода, что и в предыдущем
примере в листинге 2.9. Разница заключается в том, что каждый ТВ-объект теперь инициализирован брендом и местоположением, и вы можете видеть эту информацию, напечатанную
в ответ на каждый вызов измененного метода showInfo().

Использование классов
Используя все, что узнали из этой главы, теперь мы можем
создавать классы и несколько независимых экземпляров
из этих классов. Вот примеры того, как это можно делать.
• Скажем, мы хотели смоделировать студента на курсе. У нас
может быть класс Student, который имеет переменные
экземпляра для name, emailAddress, currentGrade и так
далее. Каждый объект ученика, который мы создаем из этого
класса, будет иметь собственный набор переменных экземпляра, и значения, данные переменным экземпляра, будут
различными для каждого ученика.
• Рассмотрим игру, где есть несколько игроков. Игрок может
быть смоделирован классом Player с переменными экземпляра для name, points, health, location и так далее. Каждый
игрок будет иметь одинаковые возможности, но методы могут
работать по-разному на основе различных значений в переменных экземпляра.
• Представьте себе адресную книгу. Можно создать класс
Person с переменными экземпляра name, address,
Глава 2. Моделирование физических объектов с помощью объектно- ориентированного программирования 85

phoneNumber и birthday. Мы можем создать столько объек-

тов из класса Person, сколько захотим, по одному для каждого знакомого человека. Переменные экземпляра в каждом
объекте Person будут содержать разные значения. Затем мы
можем написать код для поиска по всем объектам Person
и извлечь информацию об одном или нескольких из них,
которые мы ищем.
В будущих главах я изучу эту концепцию создания нескольких объектов из одного класса и дам вам инструменты, которые
помогут управлять коллекцией объектов.

ООП как решение
В конце главы 1 я упомянул три проблемы, которые присущи
процедурному программированию. Будем надеяться, что после
проработки примеров в этой главе вы увидите, как объектноориентированное программирование решает такие задачи.
1. Хорошо написанный класс может быть легко повторно использован во многих различных программах. Классы не нуждаются
в доступе к глобальным данным. Вместо этого объекты предоставляют код и данные на одном уровне.
2. Объектно- ориентированное программирование может значительно сократить количество требуемых глобальных переменных, поскольку класс обеспечивает структуру, в которой
данные и код, действующий на них, существуют в одной группировке. Это также облегчает отладку кода.
3. Объекты, созданные из класса, имеют доступ только к своим
данным — их набору переменных экземпляра в классе. Даже
если у вас есть несколько объектов, созданных из одного
класса, они не имеют доступа к данным друг друга.

Выводы
В этой главе я представил введение в объектноориентированное программирование, продемонстрировав
связь между классом и объектом. Класс определяет форму и возможности объекта. Объект — это одиночный экземпляр класса,
который имеет собственный набор всех данных, определенных
в переменных экземпляра класса. Каждый фрагмент данных,
86 Часть I. Введение в объектно- ориентированное программирование

который должен содержать объект, хранится в переменной
экземпляра, которая имеет область видимости объекта. Это
означает, что он доступен во всех методах, определенных
в классе. Все объекты, созданные из одного класса, получают
свой набор всех переменных экземпляра, и, поскольку они
могут содержать разные значения, вызов методов на разных
объектах может привести к разному поведению.
Я показал, как создавать объект из класса через инструкцию
присваивания. После создания экземпляра объекта его можно
использовать для вызова любого метода, определенного в классе этого объекта. Я также показал, как можно создавать экземпляры нескольких объектов из одного класса.
В этой главе в демонстрационных классах реализованы физические объекты (выключатели света, телевизоры). Это хороший способ познакомиться с понятиями класса и объекта. Однако в будущих главах я покажу объекты, которые
не представляют физические сущности.

3
МЫС ЛЕ ННЫЕ МОД Е ЛИ
ОБЪЕ К ТОВ И З Н АЧЕ НИЕ S E LF

Надеюсь, новые концепции и терминология,
которые я ввел ранее, начинают обретать
смысл. Некоторые люди, не знакомые с ООП, испытывают трудности с представлением о том, что
такое объект и как методы объекта работают с его переменными экземпляра. Специфика довольно сложна, поэтому
может быть полезно разработать ментальную модель того, как
работают объекты и классы.
В этой главе я покажу две ментальные модели ООП. Прежде
всего я хочу прояснить, что ни одна из этих моделей не является точным представлением того, как объекты работают
в Python. Эти модели предназначены просто для того, чтобы
дать вам возможность подумать, как выглядит объект и что происходит при вызове метода. В этой главе также будет более подробно рассказано о слове self и показано, как оно используется для работы методов с несколькими объектами, созданными
из одного класса. На протяжении всей остальной части книги
вы получите гораздо более глубокое понимание объектов
и классов.

Мысленные модели объектов и значение self 89

Повторный обзор класса DimmerSwitch
В следующих примерах мы продолжим работу с классом
DimmerSwitch из главы 2 (см. листинг 2.5). Класс DimmerSwitch
уже имеет две переменные экземпляра: self.isOn и self.
brightness. Единственное изменение, которое мы сделаем, —
это добавим переменную экземпляра self.label, чтобы каждый создаваемый нами объект можно было легко идентифицировать в выходных данных при запуске программы. Эти
переменные создаются и получают начальные значения
в методе __init__(). Затем к ним обращаются или изменяют
их в пяти других методах класса.
В листинге 3.1 приведен тестовый код для создания трех
объектов DimmerSwitch из класса DimmerSwitch, которые мы
станем использовать в наших ментальных моделях. Я буду вызывать различные методы для каждого из объектов
DimmerSwitch.
Файл: OO_DimmerSwitch_Model1.py
# создаем первый DimmerSwitch, включаем его и поднимаем уровень
# яркости дважды
oDimmer1 = DimmerSwitch('Dimmer1')
oDimmer1.turnOn()
oDimmer1.raiseLevel()
oDimmer1.raiseLevel()
# создаем второй DimmerSwitch, включаем его и поднимаем уровень
# яркости в 3 раза
oDimmer2 = DimmerSwitch('Dimmer2')
oDimmer2.turnOn()
oDimmer2.raiseLevel()
oDimmer2.raiseLevel()
oDimmer2.raiseLevel()
# создаем третий DimmerSwitch, используя настройки по умолчанию
oDimmer3 = DimmerSwitch('Dimmer3')
# просим каждый переключатель вывести свои показатели
oDimmer1.show()
oDimmer2.show()
oDimmer3.show()
Листинг 3.1. Создание трех объектов DimmerSwitch и вызов различных методов для
каждого из них

90 Часть I. Введение в объектно- ориентированное программирование

При запуске с классом DimmerSwitch этот код выдает следующие выходные данные:
Label: Dimmer1
Light is on? True
Brightness is: 2
Label: Dimmer2
Light is on? True
Brightness is:3
Label: Dimmer3
Light is on? False
Brightness is: 0

Это именно то, чего мы и ожидали. Каждый объект
DimmerSwitch не зависит от любых других объектов
DimmerSwitch, и каждый объект содержит и изменяет собственные переменные экземпляра.

Высокоуровневая мысленная модель № 1
В этой первой модели каждый объект можно рассматривать как
автономную единицу, содержащую тип данных, набор переменных экземпляра, определенных в классе, и копию всех методов,
определенных в классе (рис. 3.1).
oDimmer1

oDimmer2

oDimmer3

Тип:
DimmerSwitch

Тип:
DimmerSwitch

Тип:
DimmerSwitch

Данные:
label: Dimmer1
isOn: True
brightness: 2

Данные:
label: Dimmer2
isOn: True
brightness: 3

Данные:
label: Dimmer3
isOn: False
brightness: 0

Методы:
_init_()
turnOn()
turnOff()
raiseLevel()
lowerLevel()
show()

Методы:
_init_()
turnOn()
turnOff()
raiseLevel()
lowerLevel()
show()

Методы:
_init_()
turnOn()
turnOff()
raiseLevel()
lowerLevel()
show()

Рис. 3.1. В мысленной модели № 1 каждый объект является единицей,
имеющей тип, данные и методы

Данные и методы каждого объекта упакованы вместе. Область видимости переменной экземпляра определяется как все
Глава 3. Мысленные модели объектов и значение self 91

методы в классе, поэтому все методы имеют доступ к переменным экземпляра, связанным с этим объектом.
Если эта мысленная модель делает концепции ясными,
то вы разобрались. Хотя это не то, как на самом деле объекты
реализованы, это вполне разумный способ представлять, как
переменные и методы экземпляра объекта работают вместе.

Более глубокая мысленная модель № 2
Эта вторая модель исследует объекты на более низком уровне
и лучше объясняет, что такое объект.
Каждый раз, когда создаете экземпляр объекта, вы получаете обратно значение из Python. Обычно возвращаемое значение
хранится в переменной, которая ссылается на объект. В листинге 3.2 мы создадим три объекта DimmerSwitch. После создания каждого из них мы добавим код для проверки результата, напечатав тип и значение каждой переменной.
Файл: OO_DimmerSwitch_Model2_Instantiation.py
# создаем три объекта DimmerSwitch
oDimmer1 = DimmerSwitch('Dimmer1')
print(type(oDimmer1))
print(oDimmer1)
print()
oDimmer2 = DimmerSwitch('Dimmer2')
print(type(oDimmer2))
print(oDimmer2)
print()
oDimmer3 = DimmerSwitch('Dimmer3')
print(type(oDimmer3))
print(oDimmer3)
print()
Листинг 3.2. Создание трех объектов DimmerSwitch и печать типа
и значения каждого из них

Вот результат:







92 Часть I. Введение в объектно- ориентированное программирование

Первая строка в каждой группе сообщает нам тип данных.
Вместо встроенного типа, такого как integer или float, мы видим, что все три объекта имеют определенный программистом
тип DimmerSwitch. (Значение __main__ указывает, что код
DimmerSwitch был найден в нашем единственном файле
Python, а не импортирован из какого-либо другого файла.)
Вторая строка каждой группировки содержит строку символов. Каждая строка представляет собой расположение в памяти компьютера. Место в памяти — это место, где можно найти
все данные, связанные с каждым объектом. Обратите внимание, что каждый объект находится на собственном месте в памяти. Если вы запустите этот код на своем компьютере, то, скорее всего, получите другие значения, но фактические адреса
не принципиальны для понимания концепции.
Все объекты DimmerSwitch имеют одинаковый тип: класс
DimmerSwitch. Чрезвычайно важным выводом является то, что
все объекты ссылаются на код одного и того же класса, который действительно существует только в одном месте. Когда
программа начинает работать, Python считывает все определения классов и запоминает расположение всех классов и их методов.
Веб-сайт Python Tutor (http://PythonTutor.com) предоставляет
некоторые полезные инструменты, которые могут помочь вам
визуализировать работу небольших программ, позволяя пошагово выполнять каждую строку вашего кода. На рис. 3.2 показан снимок экрана с запуском класса DimmerSwitch и тестового
кода с помощью инструмента визуализации, останавливающего
выполнение перед созданием экземпляра первого объекта
DimmerSwitch.
Здесь видно, что Python запоминает расположение класса
DimmerSwitch и всех его методов. Хотя классы могут содержать
сотни или даже тысячи строк кода, ни один объект фактически
не получает копию кода класса. Наличие только одной копии
кода очень важно, так как это сохраняет размер ООП-программ
небольшим. При создании экземпляра объекта Python выделяет достаточно памяти для каждого объекта, чтобы представить
собственный набор переменных экземпляра, определенных
в классе. Как правило, создание экземпляра объекта из класса
эффективно использует память.

Глава 3. Мысленные модели объектов и значение self 93

Рис. 3.2. Python запоминает все классы и все методы в каждом классе

На снимке экрана на рис. 3.3 показан результат выполнения
всего тестового кода, приведенного в листинге 3.2.

Рис. 3.3. Выполнение кода из листинга 3.2 демонстрирует, что объекты
не включают код в соответствии с мысленной моделью № 2

Это соответствует нашей второй мысленной модели. В правой части снимка экрана код для класса DimmerSwitch отображается только один раз. Каждый объект знает класс,
94 Часть I. Введение в объектно- ориентированное программирование

из которого был создан экземпляр, и содержит собственный набор переменных экземпляра, определенных в классе.
ПРИМЕЧАНИЕ

Хотя ниже приведена деталь реализации, это может помочь
в дальнейшем понимании объектов. Внутренне все переменные
экземпляра объекта хранятся в словаре Python в виде пар имя/
значение. Можно проверить, какие переменные экземпляра
существуют в объекте, вызвав встроенную функцию vars() для
любого объекта. Например, в тестовом коде из листинга 3.2,
если хотите увидеть внутреннее представление переменных
экземпляра, вы можете добавить следующую строку в конце:
print('oDimmer1 variables:', vars(oDimmer1))

Когда запустите этот код, вы увидите следующие выходные данные:
oDimmer1 variables: {'label': 'Dimmer1', 'isOn':
True, 'brightness': 2}

В чем смысл слова «self»?
Философы боролись с вопросом «Что означает Я» на протяжении веков, поэтому с моей стороны было бы довольно претенциозно пытаться объяснить его всего на нескольких страницах. В Python, однако, переменная с именем self (я) имеет
узкоспециализированное и четкое значение. В этом разделе
я покажу, как самому себе присваивается значение и как код
методов в классе работает с переменными экземпляра любого
объекта, созданного из класса.
ПРИМЕЧАНИЕ

Имя переменной self не является ключевым словом в Python,
но используется по соглашению — может быть использовано
любое другое имя, и код будет работать нормально. Тем
не менее использование self — общепринятая практика
в Python, и я буду придерживаться ее на протяжении всей этой
книги. Если вы хотите, чтобы ваш код был понят другими программистами Python, используйте имя self в качестве первого
параметра во всех методах класса. (Другие языки ООП имеют
ту же концепцию, но используют другие имена, такие как this
или me.)
Глава 3. Мысленные модели объектов и значение self 95

Предположим, вы пишете класс с именем SomeClass, а затем
создаете объект из этого класса, как показано ниже:
oSomeObject = SomeClass()

Объект oSomeObject содержит набор всех переменных экземпляра, определенных в классе. Каждый метод класса
SomeClass имеет определение, которое выглядит следующим
образом:
def someMethod(self, ):

А вот общая форма вызова такого метода:
oSomeObject.someMethod()

Как мы знаем, Python переупорядочивает аргументы при вызове метода таким образом, чтобы объект передавался в качестве первого аргумента. Это значение получается в первом параметре метода и помещается в переменную self (рис. 3.4).
def someMethod(self, ):

oSomeObject.someMethod()
Рис. 3.4. Как Python переупорядочивает аргументы при вызове метода

Поэтому всякий раз, когда вызывается метод, self будет
установлен на объект в вызове. То есть код метода может работать с переменными экземпляра любого объекта, созданного
из класса. Это делается с помощью формы:
self.

Это по существу говорит об использовании объекта, на который ссылается self, и доступе к переменной экземпляра, указанной в . Поскольку каждый метод использует self в качестве первого параметра, каждый
метод в классе использует один и тот же подход.
Для иллюстрации этой концепции воспользуемся классом
DimmerSwitch. В следующем примере мы создадим экземпляр
двух объектов DimmerSwitch, а затем пройдемся по тому, что
96 Часть I. Введение в объектно- ориентированное программирование

происходит, когда мы поднимаем уровень яркости этих объектов, вызывая метод raiseLevel() для каждого из них.
Код метода, который мы вызываем:
def raiseLevel(self):
if self.brightness < 10:
self.brightness = self.brightness + 1

В листинге 3.3 показан пример тестового кода для двух объектов DimmerSwitch.
Файл: OO_DimmerSwitch_Model2_Method_Calls.py
# создаем два объекта DimmerSwitch
oDimmer1 = DimmerSwitch('Dimmer1')
oDimmer2 = DimmerSwitch('Dimmer2')
# просим oDimmer1 поднять свой уровень яркости
oDimmer1.raiseLevel()
# просим oDimmer2 поднять свой уровень яркости
oDimmer2.raiseLevel()
Листинг 3.3. Вызов одного и того же метода на разных объектах

DimmerSwitch

В этом листинге сначала создадим два объекта DimmerSwitch.
Затем идут два вызова метода raiseLevel(): сперва мы вызываем его для oDimmer1, а после — для oDimmer2.

Рис. 3.5. Программа в листинге 3.3 остановилась при вызове oDimmer1.

raiseLevel()

Глава 3. Мысленные модели объектов и значение self 97

На рис. 3.5 показан результат выполнения тестового кода
из листинга 3.3 в Python Tutor, при этом оно было остановлено
во время первого вызова raiseLevel().
Обратите внимание, что self и oDimmer1 относятся к одному и тому же объекту. Когда метод выполняется и использует
любую self., он будет использовать переменные экземпляра oDimmer1. Следовательно, когда
этот метод выполняется, self.brightness относится к переменной экземпляра яркости в oDimmer1.
Если мы продолжим выполнять тестовый код в листинге 3.3,
то переходим ко второму вызову raiseLevel() для oDimmer2.
На рис. 3.6 я остановил выполнение внутри этого вызова метода.
Обратите внимание, что на этот раз self относится к тому же
объекту, что и oDimmer2. Теперь self.brightness относится
к переменной экземпляра яркости oDimmer2.
Независимо от того, какой объект мы используем или какой
метод вызываем, значение объекта присваивается переменной
self в вызываемом методе. Вы должны думать о self как о текущем объекте — том, с которым был вызван метод. Всякий раз,
когда метод выполняется, он использует набор переменных экземпляра для объекта, указанного в вызове.

Рис. 3.6. Программа в листинге 3.3 остановилась при вызове oDimmer2.

raiseLevel()

98 Часть I. Введение в объектно- ориентированное программирование

Выводы
В этой главе я представил два разных способа мышления
об объектах. Эти мысленные модели должны помочь в развитии базового понимания того, что происходит, когда вы
создаете несколько экземпляров объекта из класса.
Первая модель показала, что вы можете думать об объекте
как о наборе всех переменных экземпляра и всех методов класса, завернутых в одну упаковку.
Вторая модель дает гораздо более подробную информацию
о реализации, объясняя, что код класса существует только в одном месте. Важный вывод: создание новых объектов из класса
космически эффективно. При создании нового экземпляра объекта Python выделяет память для представления переменных
экземпляра, определенных в классе. Дубликаты кода класса
не создаются, да и не требуются.
Ключом к работе методов с несколькими объектами является то, что первый параметр всех методов, self, всегда устанавливается в объект, используемый при вызове этого метода. При
таком подходе каждый метод использует переменные экземпляра для текущего объекта.

4
У ПРА В ЛЕ НИЕ НЕС КОЛЬК И МИ
ОБЪЕ К ТА МИ

В этой главе будут показаны методы управления любым количеством объектов, созданных
из одного класса. Сначала я рассмотрю пример
ООП реализации банковского счета из главы 1.
Подход ООП позволяет данным и коду счета быть
на одном уровне, устраняя необходимость зависеть от глобальных данных. Затем я разделю программу на основной код, который представляет меню верхнего уровня, и отдельный объект
банка, управляющий счетами, в дополнение к любому количеству объектов счета. Мы также обсудим лучший способ обработки ошибок с помощью исключений.

Класс банковского счета
Нашему классу банковского счета потребуются как минимум
имя, пароль и баланс в качестве его данных. В качестве поведения пользователь должен иметь возможность создавать счет,
вносить и выводить деньги, проверять свой баланс.
Мы определим и инициализируем переменные для имени,
пароля и баланса, а также построим методы для реализации
Управление несколькими объектами 101

каждой из операций. После этого сможем создать любое количество объектов Account. Как и начальный класс из главы 1,
это упрощенный класс Account, который использует только целые числа для баланса и сохраняет пароль в открытом виде.
Хотя вы не будете использовать такие упрощения в реальных
банковских приложениях, сейчас это позволит нам сосредоточиться на соответствующих аспектах ООП.
Новый код для класса Account представлен в листинге 4.1.
Файл: Account.py
# Класс счета





class Account():
def __init__(self, name, balance, password):
self.name = name
self.balance = int(balance)
self.password = password
def deposit(self, amountToDeposit, password):
if password != self.password:
print('Sorry, incorrect password')
return None
if amountToDeposit < 0:
print('You cannot deposit a negative amount')
return None
self.balance = self.balance + amountToDeposit
return self.balance



def withdraw(self, amountToWithdraw, password):
if password != self.password:
print('Incorrect password for this account')
return None
if amountToWithdraw < 0:
print('You cannot withdraw a negative amount')
return None
if amountToWithdraw > self.balance:
print('You cannot withdraw more than you have in your
account')
return None
self.balance = self.balance – amountToWithdraw
return self.balance

102 Часть I. Введение в объектно- ориентированное программирование





def getBalance(self, password):
if password != self.password:
print('Sorry, incorrect password')
return None
return self.balance
# Добавлено для отладки
def show(self):
print('
Name:', self.name)
print('
Balance:', self.balance)
print('
Password:', self.password)
print()
Листинг 4.1. Минимальный код класса счета

ПРИМЕЧАНИЕ

Обработка ошибок в листинге 4.1 очень проста. При обнаружении условия ошибки мы выводим сообщение об ошибке и возвращаем специальное значение None. Позже в этой главе
я покажу лучший способ обработки ошибок.
Обратите внимание, как методы манипулируют данными
и запоминают их. Данные передаются в каждый метод через параметры, которые являются локальными переменными и существуют только во время выполнения метода. Данные запоминаются в переменных экземпляра, которые имеют область
видимости объекта и поэтому запоминают свои значения при
вызовах различных методов.
Во-первых, мы имеем метод  __init__() с тремя параметрами. При создании объекта из этого класса требуются три
элемента данных: имя, баланс и пароль. Инстанциирование может выглядеть следующим образом:
oAccount = Account('Joe Schmoe', 1000, 'magic')

При создании экземпляра объекта значения трех аргументов передаются в метод __init__(), который в свою очередь
присваивает эти значения переменным экземпляра с аналогичными именами: self.name, self.balance и self.password.
Мы будем обращаться к этим переменным экземпляра в других
методах.
Метод deposit()  позволяет пользователю вносить депозит на счет. После создания экземпляра объекта Account
Глава 4. Управление несколькими объектами 103

и сохранения его в oAccount мы можем вызвать метод
deposit() следующим образом:
newBalance = oAccount.deposit(500, 'magic')

Этот вызов говорит внести $500 и задает пароль «magic». Метод выполняет две проверки валидности запроса при внесении
депозита. Первый гарантирует правильность пароля путем
сравнения пароля, предоставленного в вызове, с паролем, установленным при создании объекта Account. Это хороший пример того, как используется оригинальный пароль, сохраненный в переменной экземпляра self.password. Вторая
проверка валидности позволяет убедиться, что мы не вносим
отрицательную сумму (что на самом деле является выводом
средств).
Если одна из этих проверок не прошла, то сейчас мы возвращаем специальное значение None, чтобы показать, что произошла какая-то ошибка. Если оба теста пройдены, мы увеличиваем переменную экземпляра self.balance на сумму депозита.
Поскольку баланс хранится в self.balance, он запоминается
и доступен для будущих вызовов. Наконец, мы возвращаем новый баланс.
Метод withdraw()  работает очень похоже и будет вызван
следующим образом:
oAccount.withdraw(250, 'magic')

Метод withdraw() проверяет, что мы указали правильный
пароль, сравнивая его с переменной экземпляра self.
password. Он также проверяет, что мы не просим вывести отрицательную сумму или больше, чем у нас есть на счете, с использованием переменной экземпляра self.balance. Как
только эти проверки пройдены, метод уменьшает self.
balance на сумму вывода. Возвращает результирующий баланс.
Чтобы проверить баланс , нам нужно только указать правильный пароль для счета:
currentBalance = oAccount.getBalance('magic')

Если указанный пароль совпадает с сохраненным в переменной экземпляра self.password, метод возвращает значение
в параметре self.balance.
104 Часть I. Введение в объектно- ориентированное программирование

Наконец, для отладки мы добавили метод show()  для отображения текущих значений self.name, self.balance
и self.password, сохраненных для счета.
Класс Account — наш первый пример представления чеголибо, что не является физическим объектом. Банковский
счет — это не то, что вы можете видеть, чувствовать или трогать. Тем не менее он прекрасно вписывается в мир компьютерных объектов, потому что имеет данные (имя, баланс, пароль)
и действия, которые работают с этими данными (создать, внести, вывести, получить баланс, показать).

Импорт кода класса
Существует два способа использования класса, который вы
создали в собственном коде. Как мы видели в предыдущих главах, самый простой способ — поместить весь код класса непосредственно в основной исходный файл Python. Но это затрудняет повторное использование класса.
Второй подход заключается в том, чтобы поместить код
класса в отдельный файл и импортировать его в программу, которая его использует. Мы поместили весь код для нашего класса Account в Account.py, но если мы попытаемся запустить
Account.py самостоятельно, ничего не произойдет, потому что
это просто определение класса. Чтобы использовать его, мы
должны создать экземпляр одного или нескольких объектов
и выполнить вызовы методов объекта. По мере того как наши
классы становятся больше и сложнее, сохранение каждого
из них в отдельный файл становится предпочтительным способом работы с ними.
Чтобы использовать класс Account, мы должны собрать другой файл .py и импортировать код из Account.py, как мы делаем
с другими встроенными пакетами, такими как random и time.
Часто программисты Python называют главную программу, которая импортирует другие файлы классов, main.py или
Main_.py. Затем мы должны убедиться, что Account.
py и основной файл программы находятся в одной папке. В начале основной программы мы добавляем код класса Account,
начиная с выражения import (обратите внимание, что мы
оставляем расширение файла *.py):
from Account import *
Глава 4. Управление несколькими объектами 105

Использование оператора импорта со звездочкой (*) обозначает «все содержимое импортируемого файла». Импортируемый файл может содержать несколько классов. В таком случае,
где это возможно, следует указывать конкретный класс или
классы, которые вы хотите импортировать, вместо того чтобы
импортировать весь файл. Вот синтаксис для импорта конкретных классов:
from import , , ...

Импорт кода класса имеет два преимущества.
1. Модуль можно использовать повторно, поэтому, если мы хотим
использовать Account.py в другом проекте, нам просто нужно
сделать копию файла и поместить его в папку этого проекта.
Повторное использование кода таким образом является одним
из основных элементов объектно- ориентированного
программирования.
2. Если ваш код класса включен в основную программу, каждый
раз, когда вы запускаете программу, Python компилирует весь
код в вашем классе (переводит его на язык более низкого
уровня, который легче запускать на вашем компьютере), даже
если вы не внесли никаких изменений в класс.
Однако, когда вы запускаете свою основную программу с импортированным кодом класса, Python оптимизирует этап компиляции без необходимости делать что-либо. Он создает папку
с именем __pycache __ в папке проекта, затем компилирует код
в файл класса и сохраняет его в папке __pycache __ с вариантом
оригинального имени файла Python. Например, для файла
Account.py Python создаст файл, используя имя Account.cpython-39.
pyc (или аналогичный на основе используемой версии Python).
Расширение .pyc обозначает Python Compiled . Python перекомпилирует ваш файл класса только в том случае, если изменится
исходный файл класса. Если исходник вашей учетной записи
Account.py не изменился, Python знает, что не нужно его перекомпилировать, и может более эффективно использовать версию файла pyc.

106 Часть I. Введение в объектно- ориентированное программирование

Создание тестового кода
Мы протестируем наш новый класс в четырех основных программах. Первая создаст объекты Account, используя отдельно
именованные переменные. Вторая будет хранить объекты
в списке, а третья — номера счетов и объекты в словаре. Наконец, четвертая версия разделит функциональность, поэтому
у нас будет основная программа, которая взаимодействует
с пользователем, и объект Bank, управляющий различными
счетами.
В каждом примере основная программа импортирует
Account.py. Папка проекта должна содержать основную программу и файл Account.py. В следующем обсуждении различные версии основной программы будут называться Main_Bank_VersionX.py,
где X — номер версии.
Создание нескольких учетных записей
В этой первой версии мы создадим два примера счетов и заполним их жизнеспособными данными для тестирования. Сохраним каждый счет в явной переменной, представляющей объект.
Файл: BankOOP1_IndividualVariables/Main_Bank_Version1.py
# Тестовая программа, использующая счета
# Версия 1, использующая явные переменные для каждого объекта Account
# Берем весь код из файла класса Account
from Account import *
# создаем два счета
 oJoesAccount = Account('Joe', 100, 'JoesPassword')
print("Created an account for Joe")
 oMarysAccount = Account('Mary', 12345, 'MarysPassword')
print("Created an account for Mary")
 oJoesAccount.show()
oMarysAccount.show()
print()
# вызываем разные методы для разных счетов
print('Calling methods of the two accounts ...')
 oJoesAccount.deposit(50, 'JoesPassword')
oMarysAccount.withdraw(345, 'MarysPassword')
oMarysAccount.deposit(100, 'MarysPassword')

Глава 4. Управление несколькими объектами 107

# отображаем счета
oJoesAccount.show()
oMarysAccount.show()
Листинг 4.2. Основная программа для тестирования класса Account

Мы создаем счет для Джо  и счет для Мэри  и храним результаты в двух объектах Account. Затем мы вызываем метод
show(), чтобы показать, что счета были созданы правильно .
Джо вкладывает $50. Мэри снимает $345, а затем вносит $100 .
Если мы запустим программу сейчас, то получим вот такой вывод:
Created an account for Joe
Created an account for Mary
Name: Joe
Balance: 100
Password: JoesPassword
Name: Mary
Balance: 12345
Password: MarysPassword
Calling methods of the two accounts ...
Name: Joe
Balance: 150
Password: JoesPassword
Name: Mary
Balance: 12100
Password: MarysPassword

Теперь расширим тестовую программу, чтобы создать третий счет в интерактивном режиме, попросив пользователя
внести некоторые данные. В листинге 4.3 показан код для
этого.
# создаем новый счет с информацией от пользователя
print()
userName = input('What is the name for the new user account? ') 
userBalance = input('What is the starting balance for this account? ')
userBalance = int(userBalance)
userPassword = input('What is the password you want to use for this
account? ')
oNewAccount = Account(userName, userBalance, userPassword) 

108 Часть I. Введение в объектно- ориентированное программирование

# отображаем вновь созданный счет пользователя
oNewAccount.show() 
# вносим 100 на новый счет
oNewAccount.deposit(100, userPassword) 
usersBalance = oNewAccount.getBalance(userPassword)
print()
print('After depositing 100, the user's balance is:', usersBalance)
# отображаем новый счет
oNewAccount.show()
Листинг 4.3. Расширение тестовой программы для создания счета на лету

Тестовый код запрашивает у пользователя имя, начальный
баланс и пароль . Он использует эти значения для создания
нового счета и хранит вновь созданный объект в переменной
oNewAccount . Затем мы вызываем метод show() для нового
объекта . Вносим $100 на счет и выводим новый баланс, вызывая метод getBalance() . Когда запускаем полную программу, мы получаем вывод из листинга 4.2, а также следующий вывод:
What is the name for the new user account? Irv
What is the starting balance for this account? 777
What is the password you want to use for this account?
IrvsPassword
Name: Irv
Balance: 777
Password: IrvsPassword
After depositing 100, the user's balance is: 877
Name: Irv
Balance: 777
Password: IrvsPassword

Здесь важно отметить, что каждый объект Account поддерживает собственный набор переменных экземпляра. Каждый
объект (oJoesAccount, oMarysAccount и oNewAccount) является глобальной переменной, которая содержит набор из трех переменных экземпляра. Если мы расширим наше определение
класса Account, включив в него такую информацию, как адрес,
номер телефона и дата рождения, каждый объект получит набор этих дополнительных переменных экземпляра.
Глава 4. Управление несколькими объектами 109

Несколько объектов учетной записи в списке
Представление каждой учетной записи в отдельной глобальной
переменной работает, но это не очень хороший подход, когда
нам нужно обрабатывать большое количество объектов. Банку
необходим способ обработки произвольного количества счетов. Всякий раз, когда нам нужно произвольное количество
фрагментов данных, типичным решением будет список.
В этой версии тестового кода мы начнем с пустого списка
объектов Account. Каждый раз, когда пользователь открывает
счет, мы создаем экземпляр объекта Account и добавляем полученный объект в наш список. Номер счета для любого данного
счета будет индексом счета в списке, начиная с 0. Мы снова начнем с создания одного тестового аккаунта для Джо и другого
для Мэри, как показано в листинге 4.4.
Файл: BankOOP2_ListOfAccountObjects/Main_Bank_Version2.py
# Тестовая программа, использующая счета
# Версия 2, с использованием списка счетов
# Берем весь код из файла класса Account
from Account import *
# начинаем с пустого списка счетов
accountsList = [ ] 
# создаем два счета
oAccount = Account('Joe', 100, 'JoesPassword') 
accountsList.append(oAccount)
print("Joe's account number is 0")
oAccount = Account('Mary', 12345, 'MarysPassword') 
accountsList.append(oAccount)
print("Mary's account number is 1")
accountsList[0].show() 
accountsList[1].show()
print()
# вызываем разные методы для разных счетов
print('Calling methods of the two accounts ...')
accountsList[0].deposit(50, 'JoesPassword') 
accountsList[1].withdraw(345, 'MarysPassword') 
accountsList[1].deposit(100, 'MarysPassword') 

110 Часть I. Введение в объектно- ориентированное программирование

# отображаем счета
accountsList[0].show() 
accountsList[1].show()
# создаем новый счет с информацией от пользователя
print()
userName = input('What is the name for the new user account? ')
userBalance = input('What is the starting balance for this account? ')
userBalance = int(userBalance)
userPassword = input('What is the password you want to use for this
account? ')
oAccount = Account(userName, userBalance, userPassword)
accountsList.append(oAccount) # добавляем к списку счетов
# отображаем вновь созданный счет пользователя
print('Created new account, account number is 2')
accountsList[2].show()
# вносим 100 на новый счет
accountsList[2].deposit(100, userPassword)
usersBalance = accountsList[2].getBalance(userPassword)
print()
print('After depositing 100, the user's balance is:', usersBalance)
# отображаем новый счет
accountsList[2].show()
Листинг 4.4. Изменен тестовый код для хранения объектов в списке

Начнем с создания пустого списка учетных записей . Создаем учетную запись для Джо, сохраняем возвращенное значение в переменную oAccount и немедленно добавляем этот объект в наш список счетов . Так как он первый в списке, номер
счета Джо равен 0. Как и в реальном банке, в любое время, когда Джо хочет совершить какие-либо операции со своим счетом, он указывает его номер. Мы используем этот номер, чтобы
показать баланс , внести депозит , а затем показать баланс
снова . Мы также создаем для Мэри счет с номером 1  и выполняем некоторые тестовые операции с ее счетом в  и .
Результаты идентичны коду теста из листинга 4.3. Однако между двумя тестовыми программами есть одно очень существенное
различие: теперь существует только одна глобальная переменная
accountsList. Каждый аккаунт имеет уникальный номер, который используется для доступа к определенному счету. Мы сделали
важный шаг в сокращении числа глобальных переменных.
Глава 4. Управление несколькими объектами 111

Еще одна важная вещь, которую стоит отметить здесь, это
то, что мы внесли некоторые довольно значительные изменения в основную программу, но ничего не трогали в файле класса Account. ООП часто позволяет скрывать детали на разных
уровнях. Если предположить, что код класса Account сам заботится о реквизитах, связанных с индивидуальным счетом,
то можно сосредоточиться на том, как сделать основной код
лучше.
Обратите внимание, что мы используем переменную
oAccount как временную переменную. То есть всякий раз, когда
создаем новый объект Account, мы назначаем результат переменной oAccount. И сразу после этого добавляем oAccount
к нашему списку учетных записей. Мы никогда не используем
переменную oAccount в вызовах любого метода конкретного
объекта пользователя. Таким образом мы можем повторно использовать переменную oAccount для получения значения следующей создаваемой учетной записи.
Несколько объектов с уникальными идентификаторами
Объекты Account должны быть индивидуально идентифицируемыми, чтобы каждый пользователь мог вносить депозиты,
выводить средства и получать баланс своего конкретного
счета. Использование списка для наших банковских счетов
работает, но есть серьезный недостаток. Представьте, что есть
пять счетов, пронумерованных 0, 1, 2, 3 и 4. Если владелец аккаунта 2 решит закрыть его, мы, скорее всего, используем стандартную операцию pop() в списке, чтобы удалить аккаунт 2.
Это вызовет эффект домино: счет, который был на позиции 3,
теперь находится на позиции 2, а счет, который был на позиции 4, теперь находится на позиции 3. Однако пользователи
этих счетов по-прежнему имеют свои первоначальные номера
счетов: 3 и 4. В результате клиент, владеющий счетом 3, теперь
получит информацию о бывшем счете 4, а номер счета 4 становится недействительным индексом.
Для работы с большим количеством объектов с уникальными идентификаторами мы обычно используем словарь. В отличие от списка, словарь позволит нам удалять счета без изменения связанных с ними номеров. Мы строим каждую пару ключ/
значение с номером счета в качестве ключа и объектом Account
в качестве значения. Таким образом, если нам необходимо
112 Часть I. Введение в объектно- ориентированное программирование

удалить тот или иной счет, это никак не повлияет на все остальные. Словарь счетов будет выглядеть следующим образом:
{0 : , 1 : , ... }

Затем мы можем легко получить связанный объект Account
и вызвать такой метод:
oAccount = accountsDict[accountNumber]
oAccount.someMethodCall()

Кроме того, мы можем использовать accountNumber непосредственно для вызова метода для любого объекта Account:
accountsDict[accountNumber].someMethodCall()

В листинге 4.5 показан тестовый код с использованием словаря объектов Account. Опять же, мы, хотя и вносим множество изменений в наш тестовый код, не меняем ни одной строки в классе Account. В тестовом коде вместо использования
жестко закодированных номеров учетных записей мы добавляем счетчик nextAccountNumber, который будем увеличивать
после создания нового объекта Account.
Файл: BankOOP3_DictionaryOfAccountObjects/
Main_Bank_Version3.py
# Тестовая программа, использующая счета
# Версия 3 с использованием словаря счетов
# Берем весь код из файла класса Account
from Account import *
accountsDict = {} 
nextAccountNumber = 0 
# создаем два счета
oAccount = Account('Joe', 100, 'JoesPassword')
joesAccountNumber = nextAccountNumber
accountsDict[joesAccountNumber] = oAccount 
print('Account number for Joe is:', joesAccountNumber)
nextAccountNumber = nextAccountNumber + 1 
oAccount = Account('Mary', 12345, 'MarysPassword')
marysAccountNumber = nextAccountNumber
accountsDict[marysAccountNumber] = oAccount 
Глава 4. Управление несколькими объектами 113

print('Account number for Mary is:', marysAccountNumber)
nextAccountNumber = nextAccountNumber + 1
accountsDict[joesAccountNumber].show()
accountsDict[marysAccountNumber].show()
print()
# вызываем разные методы для разных счетов
print('Calling methods of the two accounts ...')
accountsDict[joesAccountNumber].deposit(50, 'JoesPassword')
accountsDict[marysAccountNumber].withdraw(345, 'MarysPassword')
accountsDict[marysAccountNumber].deposit(100, 'MarysPassword')
# отображаем счета
accountsDict[joesAccountNumber].show()
accountsDict[marysAccountNumber].show()
# создаем новый счет с информацией от пользователя
print()
userName = input('What is the name for the new user account? ')
userBalance = input('What is the starting balance for this account? ')
userBalance = int(userBalance)
userPassword = input('What is the password you want to use for this
account? ')
oAccount = Account(userName, userBalance, userPassword)
newAccountNumber = nextAccountNumber
accountsDict[newAccountNumber] = oAccount
print('Account number for new account is:', newAccountNumber)
nextAccountNumber = nextAccountNumber + 1
# отображаем вновь созданный счет пользователя
accountsDict[newAccountNumber].show()
# вносим 100 на новый счет
accountsDict[newAccountNumber].deposit(100, userPassword)
usersBalance = accountsDict[newAccountNumber].getBalance(userPassword)
print()
print('After depositing 100, the user's balance is:', usersBalance)
# отображаем новый счет
accountsDict[newAccountNumber].show()
Листинг 4.5. Изменен тестовый код для хранения номеров и объектов счетов в словаре

Запуск этого кода дает результаты, почти идентичные результатам предыдущих примеров. Мы начинаем с пустого словаря счетов  и инициализируем нашу переменную
114 Часть I. Введение в объектно- ориентированное программирование

nextAccountNumber на 0 . Каждый раз, когда создаем новый

счет, мы добавляем запись в словарь счетов, используя текущее
значение nextAccountNumber в качестве ключа и объект
Account в качестве значения . Это делается для каждого клиента, например Мэри . Каждый раз, когда создаем новый счет,
мы увеличиваем nextAccountNumber, чтобы подготовиться
к созданию следующего счета . Номера счетов в словаре являются ключами, и, если клиент закрывает свой счет, мы можем
удалить этот ключ и значение из словаря, не затрагивая других.
Создание интерактивного меню
Если наш класс Account работает правильно, мы сделаем
основной код интерактивным, попросив пользователя сообщить нам, какую операцию он хотел бы сделать: получить
баланс, внести депозит, сделать вывод или открыть новый счет.
В ответ наш основной код будет собирать необходимую информацию от пользователя, начиная с его номера счета, и вызывать соответствующий метод объекта пользователя Account.
В качестве кратчайшего пути мы снова заполним два счета,
один для Джо и один для Мэри. В листинге 4.6 показан расширенный основной код, который использует словарь для отслеживания всех счетов. Для краткости я пропустил код, создающий счета для Джо и Мэри и добавляющий их в словарь счетов
(так как это то же самое, что в листинге 4.5).
Файл: BankOOP4_InteractiveMenu/Main_Bank_Version4.py
# Интерактивная тестовая программа создания словаря счетов
# Версия 4 с интерактивным меню
from Account import *
accountsDict = {}
nextAccountNumber = 0
--- пропущено создание учетных записей, добавление их в словарь --while True:
print()
print('Press
print('Press
print('Press
print('Press

b
d
o
w

to
to
to
to

get the balance')
make a deposit')
open a new account')
make a withdrawal')
Глава 4. Управление несколькими объектами 115

print('Press s to show all accounts')
print('Press q to quit')
print()
action = input('What do you want to do? ') 
action = action.lower()
action = action[0] # захватываем первую букву
print()
if action == 'b':
print('*** Get Balance ***')
userAccountNumber = input('Please enter your account number: ')
userAccountNumber = int(userAccountNumber)
userAccountPassword = input('Please enter the password: ')
oAccount = accountsDict[userAccountNumber]
theBalance = oAccount.getBalance(userAccountPassword)
if theBalance is not None:
print('Your balance is:', theBalance)
elif action == 'd': 
print('*** Deposit ***')
userAccountNumber = input('Please enter the account number: ') 
userAccountNumber = int(userAccountNumber)
userDepositAmount = input('Please enter amount to deposit: ')
userDepositAmount = int(userDepositAmount)
userPassword = input('Please enter the password: ')
oAccount = accountsDict[userAccountNumber] 
theBalance = oAccount.deposit(userDepositAmount, userPassword) 
if theBalance is not None:
print('Your new balance is:', theBalance)
elif action == 'o':
print('*** Open Account ***')
userName = input('What is the name for the new user account? ')
userStartingAmount = input('What is the starting balance for
this account? ')
userStartingAmount = int(userStartingAmount)
userPassword = input('What is the password you want to use for
this account? ')
oAccount = Account(userName, userStartingAmount, userPassword)
accountsDict[nextAccountNumber] = oAccount
print('Your new account number is:', nextAccountNumber)
nextAccountNumber = nextAccountNumber + 1
print()
elif action == 's':
print('Show:')
for userAccountNumber in accountsDict:

116 Часть I. Введение в объектно- ориентированное программирование

oAccount = accountsDict[userAccountNumber]
print(' Account number:', userAccountNumber)
oAccount.show()
elif action == 'q':
break
elif action == 'w':
print('*** Withdraw ***')
userAccountNumber = input('Please enter your account number: ')
userAccountNumber = int(userAccountNumber)
userWithdrawalAmount = input('Please enter the amount to
withdraw: ')
userWithdrawalAmount = int(userWithdrawalAmount)
userPassword = input('Please enter the password: ')
oAccount = accountsDict[userAccountNumber]
theBalance = oAccount.withdraw(userWithdrawalAmount,
userPassword)
if theBalance is not None:
print('Withdrew:', userWithdrawalAmount)
print('Your new balance is:', theBalance)
else:
print('Sorry, that was not a valid action. Please try again.')
print('Done')
Листинг 4.6. Добавление интерактивного меню

В этой версии мы представляем пользователю меню опций.
Когда он выбирает действие , код задает вопросы о предполагаемой транзакции, чтобы собрать всю информацию, необходимую для вызова учетной записи. Например, если пользователь хочет внести депозит , программа запрашивает номер
счета, сумму для внесения и пароль счета . Мы используем номер счета в качестве ключа в словаре объектов Account, чтобы
получить соответствующий объект Account . С его помощью
мы затем вызываем метод deposit(), передавая сумму депозита и пароль пользователя .
Опять же, мы изменили код на уровне основного кода и оставили наш класс Account нетронутым.

Глава 4. Управление несколькими объектами 117

Создание объекта диспетчера объектов
Код в листинге 4.6 на самом деле делает две разные вещи. Сначала программа предоставляет простой интерфейс меню.
Затем, когда выбрано действие, она собирает данные и вызывает метод объекта Account. Вместо того чтобы делать одну
большую основную программу, выполняющую две разные
задачи, мы можем разделить этот код на две более мелкие логические единицы, каждая из которых имеет четко определенную роль. Система меню становится основным кодом, решающим, какие действия предпринять, а остальная часть кода
имеет дело с тем, чем занимается банк. Банк может быть смоделирован как объект, управляющий другими объектами (счетами), известный как объект диспетчера объектов.
Объект диспетчера объектов
Объект, который ведет список или словарь управляемых объектов (обычно
одного класса) и вызывает методы этих объектов.

Это разделение может быть сделано легко и логично: мы берем весь код, связанный с банком, и помещаем его в новый
класс Bank. Затем, в начале основной программы, создаем экземпляр одного объекта Bank из нового класса Bank.
Класс Bank будет управлять списком или словарем объектов
Account. Таким образом объект Bank будет единственным кодом, который напрямую взаимодействует с объектами Account
(рис. 4.1).
Основной код

Объект банка

Объект
учетной
записи

Объект
учетной
записи

Объект
учетной
записи



Объект
учетной
записи

Рис. 4.1. Основной код управляет объектом Bank, который в свою очередь
управляет множеством объектов Account

118 Часть I. Введение в объектно- ориентированное программирование

Чтобы создать эту иерархию, нам нужен некоторый основной код, который обрабатывает систему меню самого высокого
уровня. В ответ на выбор действия основной код вызовет метод
объекта Bank (например, deposit() или withdraw()). Объект
Bank будет собирать необходимую информацию (номер счета,
пароль, сумму для внесения или вывода), обращаться к своему
словарю счетов, чтобы найти соответствующий счет пользователя, и вызывать соответствующий метод для этого счета.
В этом разделении труда есть три уровня:
1) основной код, который создает и связывается с одним
объектом Bank;
2) объект Bank, управляющий словарем объектов Account
и вызывающий методы этих объектов;
3) сами объекты Account.
При таком подходе у нас есть только одна глобальная переменная — объект Bank. На самом деле основной код понятия
не имеет, что объекты учетной записи вообще существуют.
И наоборот, каждый объект Account не имеет понятия (и не заботится) о том, что такое пользовательский интерфейс верхнего уровня программы. Объект Bank получает сообщения от основного кода и связывается с соответствующим объектом
Account.
Ключевое преимущество этого подхода в том, что мы разбили большую программу на более мелкие подпрограммы: в данном случае основной код и два класса. Это значительно упрощает программирование, поскольку объем работ меньше,
а обязанности для каждого элемента более понятны. Кроме
того, наличие только одной глобальной переменной гарантирует, что код более низкого уровня не будет случайно влиять
на данные на глобальном уровне.
В компьютерной литературе конструкция, показанная
на рис. 4.1, часто известна как композиция или композиция объектов.

Композиция
Логическая структура, в которой один объект управляет одним или
несколькими другими объектами.

Глава 4. Управление несколькими объектами 119

Вы можете думать, что один объект состоит из других объектов. Например, объект автомобиля состоит из объекта двигателя, объекта рулевого колеса, некоторого количества объектов
двери, четырех объектов колеса и шины и так далее. Обсуждение часто сосредоточено на отношениях между объектами.
В этом примере можно сказать, что автомобиль «имеет» рулевое колесо, двигатель, некоторое количество дверей и так далее. Следовательно, объект автомобиля представляет собой составной элемент других объектов.
У нас будет три отдельных файла. Основной код живет в собственном файле. Он импортирует код нашего нового файла
Bank.py, который содержит класс Банка (листинг 4.7). Класс
Bank импортирует код файла Account.py и использует его для создания экземпляров объектов Account по мере необходимости.
Создание объекта диспетчера объектов
В листинге 4.7 представлен код нового класса Bank, который
является объектом диспетчера объектов.
Файл: BankOOP5_SeparateBankClass/Bank.py
# Банк, управляющий словарем объектов Account
from Account import *
class Bank():
def __init__(self):
self.accountsDict = {} 
self.nextAccountNumber = 0
def createAccount(self, theName, theStartingAmount, thePassword): 
oAccount = Account(theName, theStartingAmount, thePassword)
newAccountNumber = self.nextAccountNumber
self.accountsDict[newAccountNumber] = oAccount
# увеличиваем на единицу для подготовки к созданию следующей
# учетной записи
self.nextAccountNumber = self.nextAccountNumber + 1
return newAccountNumber
def openAccount(self): 
print('*** Open Account ***')
userName = input('What is the name for the new user account? ')
userStartingAmount = input('What is the starting balance for
this account? ')

120 Часть I. Введение в объектно- ориентированное программирование

userStartingAmount = int(userStartingAmount)
userPassword = input('What is the password you want to use for
this account? ')
userAccountNumber = self.createAccount(userName,
userStartingAmount, userPassword) 
print('Your new account number is:', userAccountNumber)
print()
def closeAccount(self): 5
print('*** Close Account ***')
userAccountNumber = input('What is your account number? ')
userAccountNumber = int(userAccountNumber)
userPassword = input('What is your password? ')
oAccount = self.accountsDict[userAccountNumber]
theBalance = oAccount.getBalance(userPassword)
if theBalance is not None:
print('You had', theBalance, 'in your account, which is
being returned to you.')
# удаляем учетную запись пользователя из словаря учетных
# записей
del self.accountsDict[userAccountNumber]
print('Your account is now closed.')
def balance(self):
print('*** Get Balance ***')
userAccountNumber = input('Please enter your account number: ')
userAccountNumber = int(userAccountNumber)
userAccountPassword = input('Please enter the password: ')
oAccount = self.accountsDict[userAccountNumber]
theBalance = oAccount.getBalance(userAccountPassword)
if theBalance is not None:
print('Your balance is:', theBalance)
def deposit(self):
print('***Deposit ***')
accountNum = input('Please enter the account number: ')
accountNum = int(accountNum)
depositAmount = input('Please enter amount to deposit: ')
depositAmount = int(depositAmount)
userAccountPassword = input('Please enter the password: ')
oAccount = self.accountsDict[accountNum]
theBalance = oAccount.deposit(depositAmount, userAccountPassword)
if theBalance is not None:
print('Your new balance is:', theBalance)
def show(self):
print('*** Show ***')
Глава 4. Управление несколькими объектами 121

for userAccountNumber in self.accountsDict:
oAccount = self.accountsDict[userAccountNumber]
print(' Account:', userAccountNumber)
oAccount.show()
def withdraw(self):
print('*** Withdraw ***')
userAccountNumber = input('Please enter your account number: ')
userAccountNumber = int(userAccountNumber)
userAmount = input('Please enter the amount to withdraw: ')
userAmount = int(userAmount)
userAccountPassword = input('Please enter the password: ')
oAccount = self.accountsDict[userAccountNumber]
theBalance = oAccount.withdraw(userAmount, userAccountPassword)
if theBalance is not None:
print('Withdrew:', userAmount)
print('Your new balance is:', theBalance)
Листинг 4.7. Класс Bank с отдельными методами для различных банковских операций

Я сосредоточусь на наиболее важных вещах, на которые стоит обратить внимание в классе Bank. Во-первых, в методе
__init__() класса Bank инициализируются две переменные:
self.accountsDict и self.nextAccountNumber . Префикс
self. обозначает их как переменные экземпляра, то есть класс
Bank может ссылаться на них в любом из своих методов.
Во-вторых, существуют два метода создания учетной записи:
createAccount() и openAccount(). Метод createAccount()
создает экземпляр новой учетной записи  с именем пользователя, начальной суммой и паролем, переданным для новой
учетной записи. Метод openAccount() запрашивает у пользователя вопросы для получения этих трех элементов информации 
и вызывает метод createAccount() в рамках одного класса.
Вызов одного метода другим в том же классе — обычное явление. Но вызываемый метод не знает, был ли он вызван изнутри
или снаружи класса; ему известно только, что первый аргумент — это объект, на котором он должен работать. Поэтому вызов метода должен начинаться с self., ведь self всегда относится к текущему объекту. Как правило, для вызова из одного
метода в другой в том же классе необходимо записать:
def myMethod(self, ):
...
self.methodInSameClass()

122 Часть I. Введение в объектно- ориентированное программирование

После сбора информации от пользователя для openAccount()
у нас есть следующая строка :
userAccountNumber = self.createAccount(userName, userStartingAmount,
userPassword)

Здесь openAccount() вызывает createAccount() из того же
класса для создания учетной записи. Метод createAccount()
запускается, создает экземпляр объекта Account и возвращает
номер счета в openAccount(), который возвращает этот номер
счета пользователю.
Наконец, новый метод closeAccount() позволяет пользователю закрыть существующий счет . Это дополнительный функционал, который мы предлагаем из нашего основного кода.
Класс Bank воплощает абстрактный банк, а не физический
объект. Это еще один хороший пример класса, который
не представляет физическую структуру.
Основной код, создающий объект диспетчера объектов
Основной код, который создает и совершает вызовы объекта
Bank, представлен в листинге 4.8.
Файл: BankOOP5_SeparateBankClass/Main_Bank_Version5.py
# Основная программа, контролирующая банк, состоящий из счетов
# Берем весь код класса банка
from Bank import *
# создаем экземпляр банка
oBank = Bank()
# Основной код
# создаем два тестовых счета
joesAccountNumber = oBank.createAccount('Joe', 100, 'JoesPassword')
print("Joe's account number is:", joesAccountNumber)
marysAccountNumber = oBank.createAccount('Mary', 12345, 'MarysPassword')
print("Mary's account number is:", marysAccountNumber)
while True:
print()
print('To get an account balance, press b')
print('To close an account, press c')

Глава 4. Управление несколькими объектами 123

print('To
print('To
print('To
print('To
print('To
print()

make a deposit, press d')
open a new account, press o')
quit, press q')
show all accounts, press s')
make a withdrawal, press w ')



action = input('What do you want to do? ')
action = action.lower()
action = action[0] # берем первую букву
print()



if action == 'b':
oBank.balance()



elif action == 'c':
oBank.closeAccount()
elif action == 'd':
oBank.deposit()
elif action == 'o':
oBank.openAccount()
elif action == 's':
oBank.show()
elif action == 'q':
break
elif action == 'w':
oBank.withdraw()
else:
print('Sorry, that was not a valid action. Please try again.')
print('Done')
Листинг 4.8. Основной код, который создает объект Bank и совершает вызовы к нему

Обратите внимание, что код в листинге 4.8 представляет систему меню верхнего уровня. Он запрашивает у пользователя
действие , затем вызывает соответствующий метод в объекте
Bank для выполнения работы . Вы можете легко расширить
объект Bank, чтобы обработать некоторые дополнительные запросы, например запросить часы работы банка, адрес или номер телефона. Эти данные могут просто храниться как
124 Часть I. Введение в объектно- ориентированное программирование

дополнительные переменные экземпляра внутри объекта Bank.
Bank ответит на эти вопросы без необходимости связываться
с каким-либо объектом Account.
При выполнении запроса на закрытие  основной код вызывает метод closeAccount() объекта Bank для закрытия счета. Объект Bank удаляет конкретный счет из своего словаря
счетов, используя следующую строку:
del self.accountsDict[userAccountNumber]

Напомним, что объект — это данные плюс код, который действует на них с течением времени. Возможность удаления объекта проистекает из этого определения. Мы можем создать объект (в данном случае объект Account), когда захотим,
а не только когда запустится программа. Мы создаем новый
объект Account всякий раз, когда пользователь решает открыть счет. Наш код может использовать этот объект, вызывая
его методы. Мы также можем удалить объект в любое время, когда пользователь решит закрыть свой счет. Это пример того,
что объект (например, Account) имеет продолжительность
жизни, начиная с момента его создания и заканчивая удалением.

Лучшая обработка ошибок с исключениями
До сих пор в классе Account, если метод обнаруживал ошибку
(например, когда пользователь вносит отрицательную сумму,
вводит неправильный пароль, выводит отрицательную сумму
и т. д.), мы выбирали решение вернуть None в качестве сигнала
о том, что что-то пошло не так. В этом разделе обсудим лучший
способ обработки ошибок, используя блоки try/except
и вызывая исключения.
try и except
Когда в функции или методе из стандартной библиотеки
Python возникает ошибка времени выполнения или ненормальное состояние, эта функция или метод сигнализирует
об ошибке, вызывая исключение (иногда называется выбросить
или сгенерировать исключение). Мы можем обнаруживать
исключения и реагировать на них, используя конструкцию
try/except. Вот общая форма:
Глава 4. Управление несколькими объектами 125

try:
# код, который может вызвать ошибку (вызвать исключение)
except : # если случается исключение
# код для обработки исключения

Если код внутри блока try работает корректно и не генерирует исключение, исключающее предложение пропускается
и выполнение продолжается после блока except. Однако, если
код в блоке try приводит к исключению, управление передается оператору except. Если исключение соответствует одному
из нескольких исключений, перечисленных в операторе
except, управление передается в код блока except. Это часто
называют поймать исключение. Такой блок с отступом обычно
содержит код для сообщения и/или восстановления после
ошибки.
Вот простой пример, где мы запрашиваем число у пользователя и пытаемся преобразовать его в целое:
age = input('Please enter your age: ')
try: # попытка преобразования в целое число
age = int(age)
except ValueError: # если возникло исключение при попытке преобразования
print('Sorry, that was not a valid number')

Вызовы стандартной библиотеки Python могут генерировать стандартные исключения, такие как TypeError,
ValueError, NameError, ZeroDivisionError и т. д. В этом примере, если пользователь вводит буквы или число с плавающей
точкой, встроенная функция int() вызывает исключение
ValueError и управление передается коду в блоке except.
Инструкция raise и пользовательские исключения
Если код обнаруживает ошибку во время выполнения, можно
использовать инструкцию raise, чтобы подать сигнал
об исключении. Существует много форм оператора raise,
но стандартный подход заключается в использовании этого
синтаксиса:
raise ('')

Для у вас есть три варианта. Во-первых,
если есть стандартное исключение, которое соответствует
126 Часть I. Введение в объектно- ориентированное программирование

обнаруженной ошибке (TypeError, ValueError, NameError,
ZeroDivisionError и т. д.), его можно использовать. Вы также
можете добавить собственную строку описания:
raise ValueError('You need to specify an integer')

Во-вторых, можно использовать общее исключение
Exception:
raise Exception('The amount cannot be a floating-point number')

Однако такой подход не слишком удачен, потому что стандартная практика написания исключений состоит в поиске исключений по имени, а этот способ не дает конкретного имени.
Третий вариант, и, возможно, лучший, — создать свое исключение. Это легко сделать, но необходима техника, называемая
наследованием (которую мы подробно обсудим в главе 10). Вот
все, что вам нужно, чтобы создать собственное исключение:
# Определите пользовательское исключение
class (Exception):
pass

Вы указываете уникальное имя для вашего исключения,
которое затем можете вызвать в коде. Создание собственных
исключений означает, что вы можете явно проверить их
по имени на более высоком уровне кода. В следующем разделе мы перепишем код примера из нашего банка, чтобы создать пользовательское исключение в классах Bank
и Account и проверить и сообщить об ошибке в основном
коде. Основной код сообщит об ошибке, но позволит программе продолжить работу.
В типичном случае оператор raise вызывает выход из текущей функции или метода и возвращает управление вызывающему абоненту. Если вызывающий объект содержит исключающее условие, которое ловит исключение, выполнение
продолжается внутри этого исключения. В противном случае
данная функция или метод завершается. Этот процесс повторяется до тех пор, пока исключение не попадет в исключение
само. Управление передается обратно через последовательность вызовов, и, если исключение не ловится никаким except,
программа завершается и Python отображает ошибку.
Глава 4. Управление несколькими объектами 127

Использование исключений в нашей
банковской программе
Теперь мы можем переписать все три уровня программы
(основной, Банк и Счет), чтобы сигнализировать об ошибках
с помощью операторов raise и обрабатывать ошибки с помощью блоков try/except.
Класс счета с исключениями
Листинг 4.9 представляет собой новую версию класса Account,
переписанную для использования исключений и оптимизированную таким образом, чтобы код не повторялся. Мы начнем
с определения пользовательского исключения
AbortTransaction, которое будет создано, если мы обнаружим
какую-либо ошибку, когда пользователь пытается выполнить
транзакцию в нашем банке.
Файл: BankOOP6_UsingExceptions/Account.py (изменено для работы
с предстоящим Bank.py)
# Класс счета
# Ошибки обозначены операторами "raise"
# Определяем пользовательское исключение
class AbortTransaction(Exception): 
'''raise this exception to abort a bank transaction'''
pass
class Account():
def __init__(self, name, balance, password):
self.name = name
self.balance = self.validateAmount(balance) 
self.password = password
def validateAmount(self, amount):
try:
amount = int(amount)
except ValueError:
raise AbortTransaction('Amount must be an integer') 
if amount self.balance:
raise AbortTransaction('You cannot withdraw more than you
have in your account')
self.balance = self.balance – amountToWithdraw
return self.balance
# Добавлено для отладки
def show(self):
print('
Name:', self.name)
print('
Balance:', self.balance)
print('
Password:', self.password)
Листинг 4.9. Измененный класс счета, который вызывает исключения

Мы начинаем с определения пользовательского исключения
AbortTransaction , чтобы применить его в этом классе
и в другом коде, который импортирует этот класс.
В методе __init__() класса Account мы гарантируем, что
сумма, указанная в качестве начального баланса, действительна, вызвав validateAmount() . Этот метод использует блок
try/except, чтобы гарантировать, что начальная сумма может
быть успешно преобразована в целое число. Если вызов int()
завершается неудачей, возникает исключение ValueError, которое попадает в секцию except. Вместо того чтобы просто
разрешить общему ValueError быть возвращенным вызывающему, код этого блока except  выполняет инструкцию raise,
вызывая наше исключение AbortTransaction, и вмещает
в себя более содержательную строку сообщения об ошибке.
Если преобразование в целое число успешно, мы выполняем
еще один тест. Если пользователь задал отрицательную сумму,
мы также вызываем исключение AbortTransaction , но с другой строкой сообщения об ошибке.
Глава 4. Управление несколькими объектами 129

Метод checkPasswordMatch()  вызывается методами объекта Bank для проверки соответствия пароля, введенного пользователем, паролю, сохраненному в Account. В случае несоответствия мы выполняем другую инструкцию raise с тем же
исключением, но предоставляем более содержательную строку
сообщения об ошибке.
Это позволяет упростить код deposit()  и withdraw() ,
поскольку данные методы предполагают, что сумма и пароль
были проверены до их вызова. Существует дополнительная
проверка в withdraw(), чтобы убедиться, что пользователь
не пытается вывести больше денег, чем находится на счете;
если это не так, мы вызываем исключение AbortTransaction
с соответствующим описанием.
Поскольку в данном классе нет кода для обработки исключения AbortTransaction, каждый раз, когда оно возникает,
управление передается обратно вызывающему. Если у вызывающего нет кода для обработки исключения, то управление передается обратно предыдущему вызывающему и так далее вверх
по стеку вызовов. Как мы увидим, наш основной код справится
с этим исключением.
Оптимизированный класс банка
Полный код класса Bank доступен для скачивания. В листинге 4.10 я показываю некоторые примеры методов, которые
демонстрируют методы try/except с вызовами методов
в ранее обновленном классе Account.
Файл: BankOOP6_UsingExceptions/Bank.py (изменено для работы
с предыдущим Account.py)
# Банк, управляющий словарем объектов Account
from Account import *
class Bank():
def __init__(self, hours, address, phone): 
self.accountsDict = {}
self.nextAccountNumber = 0
self.hours = hours
self.address = address
self.phone = phone
def askForValidAccountNumber(self): 

130 Часть I. Введение в объектно- ориентированное программирование

accountNumber = input('What is your account number? ')
try: 
accountNumber = int(accountNumber)
except ValueError:
raise AbortTransaction('The account number must be an
integer')
if accountNumber not in self.accountsDict:
raise AbortTransaction('There is no account ' +
str(accountNumber))
return accountNumber
def getUsersAccount(self): 
accountNumber = self.askForValidAccountNumber()
oAccount = self.accountsDict[accountNumber]
self.askForValidPassword(oAccount)
return oAccount
--- пропущены дополнительные методы --def deposit(self): 
print('*** Deposit ***')
oAccount = self.getUsersAccount()
depositAmount = input('Please enter amount to deposit: ')
theBalance = oAccount.deposit(depositAmount)
print('Deposited:', depositAmount)
print('Your new balance is:', theBalance)
def withdraw(self): 
print('*** Withdraw ***')
oAccount = self.getUsersAccount()
userAmount = input('Please enter the amount to withdraw: ')
theBalance = oAccount.withdraw(userAmount)
print('Withdrew:', userAmount)
print('Your new balance is:', theBalance)
def getInfo(self): 
print('Hours:', self.hours)
print('Address:', self.address)
print('Phone:', self.phone)
print('We currently have', len(self.accountsDict), 'account(s)
open.')
# Специальный метод только для администратора банка
def show(self):
print('*** Show ***')
print('(This would typically require an admin password)')
for userAccountNumber in self.accountsDict:
oAccount = self.accountsDict[userAccountNumber]
Глава 4. Управление несколькими объектами 131

print('Account:', userAccountNumber)
oAccount.show()
print()
Листинг 4.10. Измененный класс Bank

Класс Bank начинается с метода __init__() , который сохраняет всю необходимую информацию в переменных экземпляра.
Новый метод askForValidAccountNumber()  вызывается
из ряда других, чтобы запросить у пользователя номер счета
и попытаться проверить заданный номер. Для начала он включает блок try/except , чтобы убедиться, что число является
целым. Если это не так, блок except обнаруживает ошибку как
исключение ValueError, но сообщает об ошибке более четко,
создавая пользовательские исключение AbortTransaction
с содержательным сообщением. Далее он проверяет, что указанный номер счета банку знаком. В противном случае он также вызывает исключение AbortTransaction, но выдает другую
строку сообщения об ошибке.
Обновленный метод getUsersAccount()  сначала вызывает предыдущий askForValid AccountNumber(), а затем использует номер счета, чтобы найти соответствующий объект
Account. Обратите внимание, что в этом методе нет try/
except. Если исключение создано
в askForValidAccountNumber() (или на более низком уровне),
этот метод немедленно вернется к своему вызывающему.
Методы deposit()  и withdraw()  вызывают
getUsersAccount() в том же классе. Аналогичным образом, если
их вызов getUsersAccount() вызывает исключение, метод завершится и передаст исключение в цепочку вызывающему методу.
Если все тесты проходят успешно, код deposit() и withdraw()
вызывает аналогично именованные методы в указанном объекте
Account для выполнения фактической транзакции.
Метод getInfo()  сообщает информацию о банке (часы,
адрес, телефон) и не имеет доступа к индивидуальным счетам.
Основной код, обрабатывающий исключения
В листинге 4.11 показан обновленный основной код, переписанный для обработки пользовательского исключения. Здесь
о любых возникших ошибках сообщается пользователю.
132 Часть I. Введение в объектно- ориентированное программирование

Файл: BankOOP6_UsingException/Main_Bank_Version6.py
# Основная программа, контролирующая банк, состоящий из счетов
from Bank import *
# создаем экземпляр банка
 oBank = Bank('9 to 5', '123 Main Street, Anytown, USA', '(650) 555-1212')
# Основной код
 while True:
print()
print('To get an account balance, press b')
print('To close an account, press c')
print('To make a deposit, press d')
print('To get bank information, press i')
print('To open a new account, press o')
print('To quit, press q')
print('To show all accounts, press s')
print('To make a withdrawal, press w')
print()
action = input('What do you want to do? ')
action = action.lower()
action = action[0] # берем первую букву
print()




try:
if action == 'b':
oBank.balance()
elif action == 'c':
oBank.closeAccount()
elif action == 'd':
oBank.deposit()
elif action == 'i':
oBank.getInfo()
elif action == 'o':
oBank.openAccount()
elif action == 'q':
break
elif action == 's':
oBank.show()
elif action == 'w':
oBank.withdraw()
except AbortTransaction as error:
# выводим текст сообщения об ошибке
print(error)
print('Done')
Листинг 4.11. Основной код, который обрабатывает ошибки с try/except
Глава 4. Управление несколькими объектами 133

Основной код начинается с создания единственного объекта
Bank . Затем в цикле он представляет пользователю меню
верхнего уровня и спрашивает его, какое действие он хочет выполнить . Вызывает подходящий метод для каждой команды.
Важно, что в этом списке мы добавили блок try вокруг всех
вызовов методов объекта oBank . Таким образом, если какойлибо вызов метода вызывает исключение AbortTransaction,
управление будет передано оператору except .
Исключения являются объектами. В части except обрабатывается исключение AbortTransaction, которое было вызвано
на любом более низком уровне. Мы присваиваем значение исключения переменной error. Когда напечатаем эту переменную, пользователь увидит соответствующее сообщение
об ошибке. Поскольку исключение было обработано в части
except, программа продолжает действовать, и пользователю
задается вопрос, что он хочет сделать.

Вызов одного и того же метода для списка
объектов
В отличие от нашего банковского примера, в случаях, когда
отдельные объекты не нужно однозначно идентифицировать,
использование списка объектов работает очень хорошо. Предположим, вы кодируете игру, и вам нужно иметь некоторое
количество плохих парней, космических кораблей, пуль, зомби
или чего-то еще. Каждый подобный объект, как правило, будет
иметь какие-то данные, которые он запоминает, и какие-то действия, которые он может выполнить. До тех пор пока для каждого объекта не требуется уникальный идентификатор, стандартный способ обработки — создать множество экземпляров
объекта из класса и поместить их в список:
objectList = [] # начинаем с пустого списка
for i in range(nObjects):
oNewObject = MyClass() # создаем новый экземпляр
objectList.append(oNewObject) # сохраняем объект в списке

В игре мы представляем мир как большую сетку, электронную таблицу. Мы хотим разместить монстров в случайных местах. В листинге 4.12 показано начало класса Monster с его методом __init__() и методом Move(). Когда создается
134 Часть I. Введение в объектно- ориентированное программирование

экземпляр Monster, ему сообщается количество строк и столбцов в сетке и максимальная скорость, и он выбирает случайное
начальное местоположение и скорость.
Файл: MonsterExample.py
import random
class Monster()
def __init__(self, nRows, nCols, maxSpeed):
self.nRows = nRows # сохраняем значение
self.nCols = nCols # сохраняем значение
self.myRow = random.randrange(self.nRows) # выбираем
# случайную строку
self.myCol = random.randrange(self.nCols) # выбираем
# случайную колонку
self.mySpeedX = random.randrange(-maxSpeed, maxSpeed + 1)
# выбираем скорость по оси X
self.mySpeedY = random.randrange(-maxSpeed, maxSpeed + 1)
# выбираем скорость по оси Y
# задаем другие переменные экземпляра, такие как здоровье,
# сила и т. д.
def move(self):
self.myRow = (self.myRow + self.mySpeedY) % self.nRows
self.myCol = (self.myCol + self.mySpeedX) % self.nCols
Листинг 4.12. Класс монстров, который может быть использован для создания многих
экземпляров монстров

С помощью этого класса Monster мы можем создать список
объектов Monster, вот так:
N_MONSTERS = 20
N_ROWS = 100 # может быть любого размера
N_COLS = 200 # может быть любого размера
MAX_SPEED = 4
monsterList = [] # начинаем с пустого списка
for i in range(N_MONSTERS):
oMonster = Monster(N_ROWS, N_COLS, MAX_SPEED) # создаем монстра
monsterList.append(oMonster) # добавляем монстра в наш список

Этот цикл создаст 20 экземпляров класса Monster, и каждый из них будет знать собственное начальное местоположение
в сетке и свою индивидуальную скорость. Когда у вас уже есть
список объектов и вы хотите, чтобы каждый выполнил одно
Глава 4. Управление несколькими объектами 135

и то же действие, можно написать простой цикл, где вы вызываете один и тот же метод каждого объекта в списке:
for objectVariable in objectVariablesList:
objectVariable.someMethod()

Например, если мы хотим, чтобы каждый из наших объектов Monster перемещался, мы могли бы использовать цикл, как
этот:
for oMonster in monsterList:
oMonster.move()

Поскольку каждый объект Monster запоминает свое местоположение и скорость, в методе move() каждый монстр может
переместиться и запомнить свое новое местоположение.
Эта техника построения списка объектов и вызова одного
и того же метода всех объектов в списке чрезвычайно полезна,
и это стандартный подход к работе с коллекцией похожих объектов. Мы будем использовать его довольно часто, когда позднее доберемся до создания игр с помощью pygame.

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

Интерфейс
Коллекция методов, предоставляемых классом (и параметры, которые
ожидает каждый метод). Интерфейс показывает, что может сделать объект,
созданный из класса.
Реализация
Фактический код класса, который показывает, как объект делает то, что он
делает.

136 Часть I. Введение в объектно- ориентированное программирование

Если вы создатель или сопровождающий класса, вам нужно
полностью понять реализацию — код всех методов и то, как они
работают вместе, чтобы повлиять на переменные экземпляра.
Если вы просто пишете код для использования класса, вам нужно только позаботиться об интерфейсе — различных методах,
доступных в классе, значениях, которые необходимо передать
в каждый метод, и любых значениях, возвращающихся из методов. Если вы пишете код самостоятельно (как «команда из одного человека»), то будете как исполнителем класса, так и пользователем его интерфейса.
Пока интерфейс класса не изменяется, реализацию класса
можно изменить в любое время. То есть, если вы обнаружите,
что метод может быть реализован быстрее или эффективнее,
изменение соответствующего кода внутри класса не будет
иметь никаких плохих побочных эффектов ни на какую другую
часть программы.

Выводы
Объект диспетчера объектов — это объект, управляющий другими объектами. Это делается путем создания одной или
нескольких переменных экземпляра, которые представляют
собой списки или словари, состоящие из других объектов. Диспетчер объектов может вызывать методы любого конкретного
объекта или всех управляемых объектов. Этот метод дает полный контроль над всеми управляемыми объектами только
менеджеру объектов.
При возникновении ошибки в методе или функции можно
создать исключение. Оператор raise возвращает управление
вызывающему методу. Вызывающий может обнаружить потенциальную ошибку, поместив вызов в блок try, и отреагировать
на любую такую ошибку с помощью блока except.
Интерфейс класса — это документирование всех методов
и связанных с ними параметров в классе. Реализация — это фактический код класса. То, что вам нужно знать, зависит от вашей
роли. Автор/сопровождающий класса должен понимать детали
кода, в то время как любой, кто использует класс, должен понимать только интерфейс, который предоставляет класс.

ЧАСТЬ II
ГРАФИЧЕС К ИЕ
ПОЛЬЗОВАТЕ ЛЬС К ИЕ
ИНТЕ РФЕ ЙС Ы С PYGA M E
Эти главы познакомят вас с pygame, внешним пакетом, который
добавляет функциональность, общую для GUI-программ.
Pygame позволяет вам писать на языке Python программы,
в которых есть окна, управляемые мышью и клавиатурой,
и которые воспроизводят звуки и многое другое.
Глава 5 дает базовое понимание того, как pygame работает,
и предоставляет стандартный шаблон для создания основанных на pygame программ. Для начала мы создадим несколько
простых программ: приложение, которое управляет изображением с помощью клавиатуры, а затем приложение с прыгающими мячами.
Глава 6 объясняет, как лучше всего использовать pygame
в объектно- ориентированной структуре. Вы увидите, как переписать программу с прыгающими мячами с помощью объектноориентированных методов и разработать простые кнопки
и поля ввода текста.
Глава 7 описывает модуль pygwidgets, который содержит
полные реализации многих стандартных виджетов пользовательского интерфейса, таких как кнопки, поля ввода и вывода,
переключатели, флажки и прочее, каждый из которых задействует объектно- ориентированное программирование. Весь
код доступен, чтобы вы могли использовать его для создания
собственных приложений. Я приведу несколько примеров.

5
ВВЕ Д Е НИЕ В PYGA M E

Язык Python был спроектирован для обработки
ввода и вывода текста. Это обеспечивает возможность получать текст и отправлять его пользователю, файлу и интернету. Однако у базового
языка нет возможности работать с более современными понятиями, такими как окна, щелчки мыши, звуки и так
далее. А что если вы захотите использовать Python для более
современной, а не для текстовой программы? В этой главе
я познакомлю вас с pygame, бесплатным внешним пакетом
с открытым исходным кодом, который был разработан для расширения возможностей Python, чтобы программисты могли
создавать игровые программы. Также вы можете использовать
pygame для создания других видов интерактивных программ
с графическим интерфейсом пользователя (GUI). Он дает возможность создавать окна, показывать изображения, распознавать движения и щелчки мыши, воспроизводить звуки и многое
другое. Если коротко, он позволяет программистам Python
создавать такие виды игр и приложений, с которыми уже знакомы нынешние пользователи компьютеров.
В мои намерения не входит превратить вас в разработчиков
игр, хотя это могло бы быть забавным результатом. Скорее,
Введение в pygame 141

я буду использовать окружение pygame, чтобы сделать конкретные объектно- ориентированные методы программирования
более ясными и наглядными. Когда вы работаете с pygame, чтобы объекты стали видимыми в окне, и имеете дело с пользователем, взаимодействующим с этими объектами, следует глубже
понять, как эффективно использовать методы ООП.
В этой главе представлено общее введение в pygame, поэтому большая часть информации и примеров этой главы будет использовать процедурное кодирование. Начиная со следующей
главы, я объясню, как эффективно использовать ООП с помощью pygame.

Устанавливаем Pygame
Pygame — это бесплатный загружаемый пакет. Для установки

пакетов Python мы будем использовать пакетный менеджер pip
(сокращение от pip installs packages — pip устанавливает
пакеты). Как уже упоминалось во введении, я предполагаю, что
вы загрузили официальную версию Python с python.org. Программа pip включена туда, поэтому у вас она уже есть.
В отличие от стандартного приложения, вы должны запускать pip из командной строки. На Mac запустите приложение
Терминал (расположенное в подпапке Утилиты внутри папки
Приложения). В системе Windows щелкните по пиктограмме
Windows, введите cmd и нажмите ENTER.

ПРИМЕЧАНИЕ

Эта книга не проверялась на системах Linux. Тем не менее большая часть (если не все) содержимого должна работать с минимальной подстройкой. Чтобы установить pygame в дистрибутив
Linux, откройте Терминал любым привычным для вас
способом.
Введите следующие команды в командную строку:
python3 -m pip install -U pip --user
python3 -m pip install -U pygame --user

Первая команда гарантирует, что у вас последняя версия
программы pip. Вторая строка устанавливает самую последнюю версию pygame.
142 Часть II. Графические пользовательские интерфейсы с pygame

Если у вас возникают какие-то проблемы с установкой
pygame, посмотрите документацию pygame по адресу
https://www.pygame.org/wiki/GettingStarted. Чтобы проверить, что pygame был установлен правильно, откройте IDLE
(среда разработки, которая поставляется в комплекте с реализацией по умолчанию Python) и в окне оболочки введите:
import pygame

Если вы видите сообщение, в котором говорится что-то
типа «Hello from the pygame community», или если сообщение
вообще отсутствует, тогда pygame был установлен правильно.
Отсутствие сообщения об ошибке свидетельствует о том, что
Python смог найти и загрузить пакет pygame и он готов к использованию. Если вы хотите посмотреть образец игры, использующей pygame, введите следующую команду (которая запустит версию игры Космические захватчики):
python3 -m pygame.examples.aliens

Прежде чем мы перейдем к использованию pygame, я должен
объяснить две важные концепции. Во-первых, я покажу, как отдельные пиксели адресуются в программах, которые используют GUI. Во-вторых, я рассмотрю управляемые событиями программы и как они отличаются от типичных текстовых. Затем
мы напишем код для нескольких приложений, которые демонстрируют ключевые признаки pygame.

Детали окон
Экран компьютера состоит из большого количества строк и столбцов маленьких точек — пикселей (от слова элемент изображения
(picture element)). Пользователь взаимодействует с GUI-программой
с помощью одного или более окон, каждое из которых представляет собой прямоугольную часть экрана. Программы могут контролировать цвет любого отдельного пикселя в своих окнах. Если
вы работаете с несколькими GUI-программами, каждая обычно
отображается в собственном окне. В этом разделе я рассмотрю,
как адресовать и изменять отдельные пиксели в окне. Данные концепции не зависят от Python; они общие для всех компьютеров
и используются во всех языках программирования.
Глава 5. Введение в pygame 143

Система координат окна
Возможно, вы знакомы с декартовой системой координат
в сетке, как на рис. 5.1.
Ось y
6
5
4
3
2
1
Ось x
–6

–5

–4

–3

–2

–1

1

2

3

4

5

6

–1
–2
–3
–4
–5
–6

Рис. 5.1. Стандартная декартова система координат

Местоположение любой точки в декартовой сетке можно
определить, указав ее координаты по осям х и у (в этом порядке). Началом считается точка, определяемая как (0, 0) и находящаяся в центре сетки.
Координаты окна компьютера работают аналогичным образом (рис. 5.2).
Тем не менее есть несколько ключевых отличий.
1. Начальная точка (0, 0) находится в верхнем левом углу окна.
2. Ось у перевернута так, что значения у начинаются с нуля в верхней части окна и увеличиваются по мере продвижения вниз.

144 Часть II. Графические пользовательские интерфейсы с pygame

3. Значения х и у — всегда целые числа. Каждая пара (х, у) определяет отдельный пиксель окна. Эти значения всегда указываются относительно верхнего левого угла окна, а не экрана.
Таким образом, пользователь может перемещать окно в любую
часть экрана, не влияя при этом на координаты элементов программы, отображаемой в окне.
0

Max x

0

Max y

Рис. 5.2. Система координат окна компьютера

У полного экрана компьютера есть свой набор координат (х, у)
для каждого пикселя, и он использует тот же тип системы координат, но программам редко, если вообще такое случается, приходится иметь дело с координатами экрана.
Когда мы пишем приложение pygame, необходимо указать
ширину и высоту окна, которое хотим создать. Внутри окна мы
можем адресовать любой пиксель, используя его координаты х
и у, как показано на рис. 5.3.
На рис. 5.3 изображен черный пиксель на позиции (3, 5). Это
значение х, равное 3 (обратите внимание, что на самом деле это
четвертый столбец, поскольку координаты начинаются со значения 0), и значение у, равное 5 (по факту шестая строка). Каждый пиксель в окне обычно называют точкой. Для ссылки
на точку в окне вы, как правило, будете использовать кортеж
Python. Например, у вас может быть подобный оператор присваивания, где значение х первое:

pixelLocation = (3, 5)

Глава 5. Введение в pygame 145

0

1

2

3

4

5

6

7

8

9

10

11

12

13 …

0
1
2
3
4
5
6
7
8
9
10
11
12
13


Рис. 5.3. Отдельная точка (отдельный пиксель) в окне компьютера

Чтобы представить изображение в окне, нам необходимо
указать координаты его начальной точки — всегда верхний левый угол изображения — в виде пары (х, у), как на рис. 5.4, где
мы нарисовали изображение в местоположении (3, 5).
При работе с изображением нам часто придется иметь дело
с ограничивающим прямоугольником — самым маленьким прямоугольником, который можно создать и который полностью
охватывает все пиксели изображения. В pygame прямоугольник
представлен с помощью набора четырех значений: х, у, ширина,
высота. Значения прямоугольника для изображения на рис. 5.4:
3, 5, 11, 7. Я покажу вам в следующем примере программы, как
его использовать. Даже если изображение не является прямоугольником (например, если это круг или овал), все равно следует учитывать его ограничивающий прямоугольник для позиционирования и обнаружения коллизий.
146 Часть II. Графические пользовательские интерфейсы с pygame

0

1

2

3

4

5

6

7

8

9

10

11

12

13 …

0
1
2
3
4
5
6
7
8
9
10
11
12
13


Рис. 5.4. Изображение в окне

Цвета пикселей
Давайте изучим, как представлены цвета на экране компьютера. Если у вас есть опыт работы с такими графическими программами, как Photoshop, вы, вероятно, уже знаете, как это
работает, но, возможно, вы все равно захотите быстренько
освежить память.
Каждый пиксель на экране состоит из сочетания трех цветов: красного, зеленого и синего, что часто называется RGB .
Цвет, отображаемый в каждом пикселе, состоит из некоторого
количества красного, зеленого и синего, где количество каждого из них указывается как значение от 0 (что означает отсутствие цвета) до 255 (что означает полную интенсивность цвета). Таким образом, существует 256 × 256 × 256 возможных
сочетаний, или 16 777 216 (часто обозначается просто как
«16 миллионов») цветов для каждого пикселя.
Глава 5. Введение в pygame 147

Цветам в pygame присваиваются RGB-значения, и мы записываем их как кортежи Python из трех чисел. Вот каким образом мы создаем константы для основных цветов:
RED = (255, 0, 0) # полный красный, нет зеленого, нет синего
GREEN = (0, 255, 0) # нет красного, полный зеленый, нет синего
BLUE = (0, 0, 255) # нет красного, нет зеленого, полный синий

Ниже представлены определения еще нескольких цветов.
Вы можете создать оттенок, используя любое сочетание трех
чисел от 0 до 255:
BLACK = (0, 0, 0) # нет красного, нет зеленого, нет синего
WHITE = (255, 255, 255) # полный красный, полный зеленый,
# полный синий
DARK_GRAY = (75, 75, 75)
MEDIUM_GRAY = (128, 128, 128)
LIGHT_GRAY = (175, 175, 175)
TEAL = (0, 128, 128) # нет красного, половинный зеленый,
# половинный синий
YELLOW = (255, 255, 0)
PURPLE = (128, 0, 128)

В pygame вам понадобится указывать цвета, когда вы хотите
заполнить фон окна, нарисовать цветную форму или текст
и так далее. Предварительное определение цветов как кортежей констант позволит вам легко обнаружить их позднее
в коде.

Программы, управляемые событиями
В большинстве программ, которые были рассмотрены в книге
до этого момента, основной код располагался в цикле while.
Программа останавливается при вызове встроенной функции
input() и ждет от пользователя входных данных для работы.
Выходные данные программы, как правило, обрабатываются
с помощью вызовов print().
В интерактивных GUI-программах эта модель больше не работает. GUI представляют новую модель вычисления, известную как модель, управляемая событиями. Программы, управляемые событиями, не полагаются на input() и print(); вместо
этого пользователь взаимодействует с элементами в окне, используя на свое усмотрение клавиатуру и/или мышь или другое
148 Часть II. Графические пользовательские интерфейсы с pygame

указывающее устройство. Они могут нажимать на различные
кнопки и пиктограммы, выбирать из меню, вводить данные
в текстовые поля или отдавать команды с помощью щелчков
или нажатий клавиш, чтобы управлять каким-либо аватаром
в окне.
ПРИМЕЧАНИЕ

Вызов print()все еще может быть чрезвычайно полезен для
отладки, когда используется для записи промежуточных
результатов.
Центральным в программировании, управляемом событиями, является понятие события. События сложно определить,
и их лучше всего описывать с помощью примеров, таких как
щелчок мыши и нажатие клавиши (каждый из которых фактически состоит из двух событий: «мышь вниз» и «мышь вверх»
и «клавиша вниз» и «клавиша вверх» соответственно). Ниже
представлено мое рабочее определение.

Событие
Что-то, что происходит во время работы программы и на что она хочет или
должна отреагировать. Большинство событий порождаются действиями
пользователя.

Управляемая событиями GUI-программа постоянно работает в бесконечном цикле. Каждый раз проходя цикл, программа
проверяет наличие каких-либо новых событий, на которые ей
необходимо отреагировать, и исполняет соответствующий код
для их обработки. Также во время каждого цикла программе
необходимо перерисовывать все элементы в окне, чтобы обновить то, что видит пользователь.
Например, предположим, что у нас есть простая GUI-программа, которая отображает две кнопки: Bark и Meow. При
щелчке по кнопке Bark она воспроизводит звук лающей собаки,
а кнопка Meow воспроизводит звук мяукающей кошки (рис. 5.5).

Рис. 5.5. Простая программа с двумя кнопками
Глава 5. Введение в pygame 149

Пользователь может щелкать по этим кнопкам в любом порядке и в любое время. Чтобы обработать действия пользователя, программа работает циклически и постоянно проверяет,
был ли щелчок по какой-либо из двух кнопок. Когда она получает событие «мышь вниз» на кнопке, программа понимает, что
по кнопке щелкнули, и рисует изображение вдавленной кнопки. Когда она получает событие «мышь вверх» на кнопке, она
запоминает новое состояние и перерисовывает кнопку в ее изначальный вид, а также воспроизводит соответствующий звук.
Поскольку основной цикл выполняется очень быстро, пользователь воспринимает воспроизводимый звук сразу же после
щелчка по кнопке. Каждый раз, проходя цикл, программа перерисовывает обе кнопки, показывая изображение, соответствующее текущему состоянию каждой кнопки.

Используем Pygame
На первый взгляд pygame может показаться чрезвычайно большим пакетом с различными доступными вызовами.
Но на самом деле вам необходимо понять не так много, чтобы
приступить к работе с небольшой программой. Для знакомства
с pygame я сначала дам шаблон, который можно использовать
для всех создаваемых вами программ pygame. Затем я буду опираться на него, постепенно добавляя ключевые части функциональных возможностей.
В этом разделе я вам покажу, как:
• завести пустое окно;
• показать изображение;
• обнаружить щелчок мыши;
• обнаружить как одиночные, так и непрерывные нажатия
клавиш;
• создать простую анимацию;
• воспроизвести звуковые эффекты и фоновые звуки;
• рисовать фигуры.
В следующей главе мы продолжим обсуждение pygame и вы
увидите, как:
• создавать анимацию для многих объектов;
• создавать кнопку и реагировать на нее;
• создавать поле отображения текста.
150 Часть II. Графические пользовательские интерфейсы с pygame

Создаем пустое окно
Как я уже упоминал ранее, программы pygame работают постоянно и циклически, проверяя наличие событий. Возможно,
будет полезным воспринимать вашу программу как анимацию,
в которой каждый проход основного цикла представляет собой
один фрейм. Пользователь может щелкнуть на что-то во время
любого фрейма, и ваша программа должна не только реагировать на эти входные данные, но также отслеживать все, что
необходимо рисовать в окне. Например, в одном примере программы позднее в этой главе мы будем перемещать мяч по окну
таким образом, чтобы в каждом фрейме мяч рисовался
на немного другой позиции.
Листинг 5.1 представляет собой стандартный шаблон, который вы можете использовать как отправную точку для всех ваших программ pygame. Эта программа открывает окно и закрашивает все содержимое черным цветом. Единственное, что
может сделать пользователь, — это щелкнуть по кнопке «закрыть», чтобы выйти из программы.
Файл: PygameDemo0_WindowOnly/PygameWindowOnly.py
# pygame демо 0 – только окно
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
#5 – Инициализируем переменные
#6 – Бесконечный цикл
Глава 5. Введение в pygame 151

while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка "закрыть"? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
#8 – Выполняем действия "в рамках фрейма"
#9 – Очищаем окно
window.fill(BLACK)
#10 – Рисуем все элементы окна
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 5.1. Шаблон для создания программ pygame

Давайте пройдемся по различным частям этого шаблона.
1. Импортируем пакеты.
Шаблон начинается с операторов import. Сначала мы импортируем сам пакет pygame, затем некоторые определенные внутри pygame константы, которые применим позднее. Последний
импорт — это пакет sys, его мы будем использовать для выхода
из программы.
2. Определяем константы.
Затем мы определяем любые константы для нашей программы.
Сначала значение RGB для BLACK, которое мы будем использовать, чтобы нарисовать фон окна. Затем определимконстанты
для ширины и высоты окна в пикселях и константы для
частоты обновления программы. Это число определяет максимальное количество циклов (и, соответственно, перерисовки
окна) программы в секунду. Наше значение 30 довольно
типично. Если объем работы, выполняемой в вашем основном
цикле, чрезмерен, программа может функционировать медленнее, чем это значение, но она никогда не будет работать
152 Часть II. Графические пользовательские интерфейсы с pygame

быстрее. Слишком высокая частота обновления может привести к слишком быстрой работе программы. В нашем примере
с мячом это означает, что мяч может скакать в окне быстрее,
чем предполагалось.
3. Инициализируем окружение pygame.
В этом разделе мы вызываем функцию, которая говорит pygame
инициализировать себя самого. Затем просим pygame создать
окно для нашей программы с помощью функции pygame.
display.set_mode() и передаем желаемую ширину и высоту
окна. И наконец, вызываем еще одну функцию pygame, чтобы
создать объект часов, который будет использоваться в конце
нашего основного цикла для поддержания максимальной
частоты фреймов.
4. Загружаем элементы: изображения, звуки и так далее.
Это раздел-заполнитель, в который мы в итоге добавляем код,
чтобы загружать внешние изображения, звуки и так далее
из диска для использования в программе. Здесь мы не задействуем какие-либо внешние элементы, потому на данный
момент этот раздел пустой.
5. Инициализируем переменные.
Здесь мы наконец инициализируем любые переменные, которые наша программа будет использовать. На текущий момент
таких не имеется, поэтому кода тут нет.
6. Бесконечный цикл.
Здесь мы начинаем основной цикл. Это простой бесконечный
цикл while True. И вновь вы можете воспринимать каждую
итерацию основного цикла как фрейм в анимации.
7. Проверяем наличие событий и обрабатываем их; обычно называется цикл события.
В этом разделе мы вызываем pygame.event.get(), чтобы
получить список событий, которые произошли с момента
последней проверки (с момента последнего запуска основного
цикла), затем проводим итерацию списка событий. Каждое
событие, о котором сообщается программе, является объектом, а у каждого объекта события есть тип. Если никаких событий не произошло, данный раздел пропускается.
В этой минимальной программе, где единственное действие,
доступное пользователю, — закрытие окна, есть только один
тип события, наличие которого мы проверяем, — это константа
Глава 5. Введение в pygame 153

pygame.QUIT, генерируемая pygame, когда пользователь щелка-

ет по кнопке закрытия. Если мы нашли это событие, pygame завершает работу, освобождая ресурсы, которые она использовала. Затем мы выходим из программы.
8. Выполняем действия «в рамках фрейма».
В этом разделе мы наконец помещаем код, который необходимо
выполнить, в каждом фрейме. Он может включать перемещение
объектов в окне или проверку наличия коллизий между элементами. В этой конкретной программе нам здесь нечего делать.
9. Очищаем окно.
В каждой итерации основного цикла программа должна перерисовывать все в окне, так что для начала мы должны его очистить. Самый простой подход — просто заполнить окно цветом, что мы здесь и делаем, вызывая window.fill(), указав
черный фон. Мы также нарисуем фоновую картинку, но пока
отложим это.
10. Рисуем все элементы окна.
Здесь мы поместим код, чтобы рисовать все, что захотим отобразить в окне. В этом шаблоне программы рисовать нечего.
В реальных программах объекты рисуются в том порядке,
в котором они появляются в коде, слоями от самого заднего
к самому переднему. Например, предположим, что мы хотим нарисовать два частично пересекающихся круга, А и В. Если сначала нарисуем А, он появится за В и какие-то его части будут
скрыты. Если мы сначала нарисуем В, а затем А, произойдет обратное и мы увидим А перед В. Это естественное отображение,
эквивалентное слоям в графических программах, таких как
Photoshop.
11. Обновляем окно.
Это строка говорит pygame взять все включенные нами
рисунки и отобразить их в окне. Pygame фактически выполняет
все изображения на этапах 8, 9 и 10 во внеэкранном буфере.
Когда вы говорите pygame обновиться, он берет содержимое
этого внеэкранного буфера и помещает его в реальное окно.
12. Делаем короткую паузу.
Компьютеры очень быстры, и, если цикл будет переходить
к следующей итерации без пауз, программа может работать
быстрее, чем указанная частота фреймов. Строка в этом разделе говорит pygame подождать, пока пройдет заданное
154 Часть II. Графические пользовательские интерфейсы с pygame

количество времени, чтобы фреймы программы работали
в рамках указанной нами частоты. Это важно, чтобы программа работала с постоянной скоростью независимо от скорости компьютера.
Когда вы запускаете эту программу, она просто выводит пустое окно, заполненное черным цветом. Чтобы завершить программу, щелкните по кнопке «закрыть» в строке заголовка.
Рисуем изображение
Давайте нарисуем что-нибудь в окне. Чтобы показать графическое изображение, необходимо выполнить два действия: сначала
загружаем изображение в память компьютера, затем отображаем его в окне приложения.
При работе с pygame все изображения (и звуки) необходимо
хранить в файлах, внешних по отношению к вашему коду.
Pygame поддерживает многие стандартные графические форматы файлов, включая .png, .jpg и .gif. В этой программе мы загрузим картинку мяча из файла ball.png. Напомню, что код
и связанные со всеми основными листингами элементы в этой
книге доступны к загрузке по адресу https://addons.eksmo.ru/it/
OOP-Code.zip.

Рис. 5.6. Предложенная иерархия папки проекта

Поскольку нам нужен лишь один графический файл в этой
программе, хорошей идеей будет использовать последовательный подход к обработке графических и звуковых файлов, поэтому я вам покажу один из них здесь. Сначала создайте папку
проекта. Поместите в нее вашу основную программу вместе
со всеми связанными файлами, содержащими классы и функции Python. Затем внутри папки проекта создайте папку images,
куда вы поместите любые файлы изображений, которые хотите
использовать в вашей программе. Также создайте папку sounds
и поместите в нее любые звуковые файлы, которые вы хотите
здесь использовать. На рис. 5.6 представлена предложенная
Глава 5. Введение в pygame 155

структура. Все примеры программ в этой книге будут использовать этот макет папки проекта.
Путь (pathname) — это строка, которая однозначно определяет местоположение файла или папки на компьютере. Чтобы
загрузить графический или звуковой файл в программу, вы
должны указать путь к файлу. Существует два типа путей: относительный и абсолютный.
Относительный путь — это путь относительно текущей папки, часто называемой текущий рабочий каталог. Когда вы выполняете программу, используя интегрированную среду разработки, такую как IDLE или PyCharm, она устанавливает
текущую папку в ту, которая содержит вашу основную программу Python, чтобы вы могли с легкостью использовать относительные пути. В этой книге я подразумеваю, что вы используете интегрированную среду разработки, и буду представлять все
пути как относительные.
Относительным путем для графического файла (например,
ball.png) в той же папке, что и ваш основной файл Python, будет
всего лишь имя файла в виде строки (например, «ball.png»).
Если использовать предложенную структуру проекта, относительный путь для файла будет «images/ball.png».
Это говорит о том, что внутри папки проекта есть другая
с именем images, а внутри нее находится файл под названием
ball.png. В строках путей имена папок разделяются с помощью
символов косой черты.
Тем не менее если вы собираетесь запускать вашу программу
из командной строки, то вам необходимо создать абсолютные
пути для всех файлов. Абсолютный путь — это тот путь, который
начинается с корневого каталога файловой системы и включает
полную иерархию папок для вашего файла. Чтобы выстроить абсолютный путь к любому файлу, вы можете использовать подобный код, который выстраивает строку абсолютного пути для
файла ball.png в каталоге images внутри папки проекта:
from pathlib import Path
# помещаем в раздел #2, определяя константу
BASE_PATH = Path(__file__).resolve().parent
# выстраиваем путь к файлу в папке images
pathToBall = BASE_PATH + 'images/ball.png'

156 Часть II. Графические пользовательские интерфейсы с pygame

Теперь мы создадим код в программе мяча, начиная с ранее
упомянутого 12-шагового шаблона и добавляя всего лишь две
новые строки кода, как показано в листинге 5.2.
Файл: PygameDemo1_OneImage/PygameOneImage.py
# pygame демо 1 – рисуем одно изображение
--- пропуск --#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
 ballImage = pygame.image.load('images/ball.png')
#5 – Инициализируем переменные
--- пропуск ---



#10 – Рисуем все элементы окна
# рисуем мяч на позиции 100 вдоль (х) и 200 вниз по (у)
window.blit(ballImage, (100, 200))
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) # ожидание pygame
Листинг 5.2. Загрузить одно изображение и нарисовать его в каждом фрейме

Сначала мы с помощью pygame ищем файл, содержащий
изображение мяча, и загружаем его в память . Переменная
ballImage теперь относится к изображению мяча. Обратите
внимание, что этот оператор присваивания выполняется только один раз перед началом основного цикла.
ПРИМЕЧАНИЕ

В официальной документации pygame каждое изображение,
включая окно приложения, известно как поверхность . Я буду
использовать более специализированные термины: называть
окно приложения просто окном, а любую картинку, загруженную из внешнего файла, — изображением. Термин поверхность
приберегу для любой картинки, нарисованной на ходу.
Глава 5. Введение в pygame 157

Затем мы рисуем мяч  каждый раз, когда проходим основной цикл. Мы указываем место, которое представляет собой
позицию для размещения верхнего левого угла ограничивающего прямоугольника изображения, обычно в виде кортежа х
и у координат.
Функция под названием blit() — это очень старая отсылка
к словосочетанию передача битового блока (bit block transfer),
но в данном контексте оно на самом деле обозначает лишь «нарисовать». Поскольку программа ранее загрузила изображение
мяча, pygame знает размер изображения, поэтому нам необходимо лишь сообщить ему, где именно его рисовать. В листинге 5.2 мы присвоили оси х значение 100, а оси у — значение 200.
Когда вы запускаете программу, на каждой итерации цикла
(30 раз в секунду) каждый пиксель в окне становится черным,
затем мяч рисуется поверх фона. С точки зрения пользователя
это выглядит так, будто ничего не происходит: мяч просто
остается в одной точке с верхним левым углом его ограничивающего прямоугольника в местоположении (100, 200).
Обнаруживаем щелчок мыши
Далее мы позволим нашей программе обнаружить щелчок
мыши и отреагировать на него. Пользователь сможет щелкнуть
по мячу, чтобы он появился в другом месте окна. Когда программа обнаруживает щелчок мыши по мячу, она случайным
образом выбирает новые координаты и рисует мяч в этом
новом местоположении. Вместо того чтобы использовать
жестко закодированные координаты (100, 200), мы создадим
две переменные ballX и ballY и будем ссылаться на координаты мяча в окне в виде кортежа (ballX, ballY). В листинге 5.3
приведен код.
Файл: PygameDemo2_ImageClickAndMove/
PygameImageClickAndMove.py
# pygame демо 2 – одно изображение, щелчок и перемещение
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
 import random

158 Часть II. Графические пользовательские интерфейсы с pygame

#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
 BALL_WIDTH_HEIGHT = 100
MAX_WIDTH = WINDOW_WIDTH – BALL_WIDTH_HEIGHT
MAX_HEIGHT = WINDOW_HEIGHT – BALL_WIDTH_HEIGHT
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
#5 – Инициализируем переменные
 ballX = random.randrange(MAX_WIDTH)
ballY = random.randrange(MAX_HEIGHT)
 ballRect = pygame.Rect(ballX, ballY, BALL_WIDTH_HEIGHT,
BALL_WIDTH_HEIGHT)
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()





# определяем, щелкнул ли пользователь
if event.type == pygame.MOUSEBUTTONUP:
# mouseX, mouseY = event.pos
# Могли бы это сделать принеобходимости
# проверяем, был ли щелчок в пределах прямоугольника мяча
# Если это так, выбираем случайным образом новое
# местоположение
if ballRect.collidepoint(event.pos):
ballX = random.randrange(MAX_WIDTH)
ballY = random.randrange(MAX_HEIGHT)
ballRect = pygame.Rect(ballX, ballY,
BALL_WIDTH_HEIGHT, BALL_WIDTH_HEIGHT)

Глава 5. Введение в pygame 159

#8 – Выполняем действия "в рамках фрейма"
#9 – Очищаем окно
window.fill(BLACK)



#10 – Рисуем все элементы окна
# рисуем мяч в произвольном местоположении
window.blit(ballImage, (ballX, ballY))
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) # ожидание pygame
Листинг 5.3. Обнаружение щелчка мыши и действие на основании этого

Поскольку нам необходимо сгенерировать случайные числа
для координат мяча, мы импортируем пакет random .
Затем добавляем новую константу, чтобы определить высоту
и ширину нашего изображения как 100 пикселей . Также мы
создаем еще две константы, чтобы ограничить максимальную
ширину и высоту координат. Используя их вместо размера окна,
мы делаем так, чтобы наш мяч все время отображался полностью внутри окна (помните, что, когда ссылаемся на местоположение изображения, мы указываем позицию его верхнего левого угла). Используем эти константы, чтобы выбрать случайные
значения для начальных координат х и у .
Затем вызываем pygame.Rect(), чтобы создать прямоугольник . Для определения прямоугольника необходимо четыре
параметра: координаты х, координаты у, ширина и высота, —
в следующем порядке:
= pygame.Rect(, , , )

Это возвращает прямоугольный объект pygame или rect.
Мы будем использовать прямоугольник мяча в обработке событий.
Нам также необходимо добавить код, чтобы проверить,
щелкнул ли пользователь мышью. Как было упомянуто, щелчок
мыши фактически состоит из двух различных событий: события «мышь вниз» и события «мышь вверх». Поскольку событие
«мышь вверх» обычно используется для подачи сигнала
160 Часть II. Графические пользовательские интерфейсы с pygame

об активации, мы здесь будем искать лишь это событие. О нем
сигнализирует новое значение event.type pygame.
MOUSEBUTTONUP . Когда выявим, что событие «мышь вверх»
произошло, мы проверим, находилось ли местоположение,
по которому щелкнул пользователь, внутри текущего прямоугольника мяча.
Когда pygame обнаружит, что событие произошло, он создаст объект события, содержащий большое количество данных. В данном случае нас волнуют лишь координаты х и у того,
где произошло событие. Мы извлекаем позицию (х, у) щелчка
с помощью event.pos, которая предоставляет кортеж из двух
значений.
ПРИМЕЧАНИЕ

Если нам необходимо разделить координаты х и у щелчка, мы
можем распаковать кортеж и сохранить значения в виде двух
переменных следующим образом:
mouseX, mouseY = event.pos

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

Метод возвращает булево выражение True, если заданная точка находится внутри прямоугольника. Если пользователь щелкнул
по мячу, мы случайным образом выбираем новые значения для
ballX и ballY. Мы используем их, чтобы создать новый прямоугольник для мяча в новом произвольном местоположении.
Единственное изменение здесь заключается в том, что мы
всегда рисуем мяч в местоположении, заданном кортежем
(ballX, ballY) . В результате каждый раз, когда пользователь
щелкает внутри прямоугольника мяча, мяч перемещается
в какую-то новую, произвольно выбранную точку в окне.
Обрабатываем клавиатуру
Следующий шаг заключается в том, чтобы помочь пользователю управлять некоторыми аспектами программы с помощью
клавиатуры. Существует два различных способа управления
взаимодействиями пользователя с клавиатурой: с помощью
одиночных нажатий и когда пользователь удерживает клавишу,
Глава 5. Введение в pygame 161

чтобы обозначить, что действие должно продолжаться, пока
зажата клавиша (также известно как непрерывная обработка).
Распознаем одиночные нажатия клавиш
Как и в случае со щелчками мышью, каждое нажатие клавиши
генерирует два события: клавиша вниз и клавиша вверх. У этих
двух событий есть два различных типа событий: pygame.KEYDOWN
и pygame.KEYUP.
В листинге 5.4 продемонстрирован небольшой пример программы, которая позволяет пользователю переместить изображение мяча в окне с помощью клавиатуры. Цель пользователя
состоит в перемещении изображения мяча таким образом, чтобы оно перекрывалось целевым изображением.
Файл: PygameDemo3_MoveByKeyboard/
PygameMoveByKeyboardOncePerKey.py
# pygame демо 3 – одно изображение, управление клавиатурой
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import random
#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
BALL_WIDTH_HEIGHT = 100
MAX_WIDTH = WINDOW_WIDTH – BALL_WIDTH_HEIGHT
MAX_HEIGHT = WINDOW_HEIGHT – BALL_WIDTH_HEIGHT
 TARGET_X = 400
TARGET_Y = 320
TARGET_WIDTH_HEIGHT = 120
N_PIXELS_TO_MOVE = 3
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
 targetImage = pygame.image.load('images/target.jpg')

162 Часть II. Графические пользовательские интерфейсы с pygame

#5 – Инициализируем переменные
ballX = random.randrange(MAX_WIDTH)
ballY = random.randrange(MAX_HEIGHT)
targetRect = pygame.Rect(TARGET_X, TARGET_Y, TARGET_WIDTH_HEIGHT,
TARGET_ WIDTH_HEIGHT)
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# определяем, нажал ли пользователь клавишу
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
ballX = ballX – N_PIXELS_TO_MOVE
elif event.key == pygame.K_RIGHT:
ballX = ballX + N_PIXELS_TO_MOVE
elif event.key == pygame.K_UP:
ballY = ballY – N_PIXELS_TO_MOVE
elif event.key == pygame.K_DOWN:
ballY = ballY + N_PIXELS_TO_MOVE



#8 – Выполняем действия "в рамках фрейма"
# определяем, перекрывает ли мяч целевое изображение
ballRect = pygame.Rect(ballX, ballY, BALL_WIDTH_HEIGHT,
BALL_WIDTH_HEIGHT)
if ballRect.colliderect(targetRect):
print('Ball is touching the target')




#9 – Очищаем окно
window.fill(BLACK)



#10 – Рисуем все элементы окна
window.blit(targetImage, (TARGET_X, TARGET_Y)) # рисуем
# целевое изображение
window.blit(ballImage, (ballX, ballY)) # рисуем мяч
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) # ожидание pygame

Листинг 5.4. Обнаружение одиночного нажатия клавиши и действие на основании этого
Глава 5. Введение в pygame 163

Сначала мы добавим несколько новых констант , чтобы
определить координаты х и у верхнего левого угла целевого
прямоугольника и ширину и высоту целевого изображения. Затем перезагружаем изображение целевого прямоугольника .
В цикле, где ищем события, мы добавляем проверку для нажатия клавиши на наличие типа события pygame.KEYDOWN . Если обнаружено событие «клавиша вниз», мы просматриваем его, чтобы
найти, какая клавиша была нажата. У каждой клавиши есть соответствующая ей константа в pygame, поэтому здесь мы проверяем,
нажал ли пользователь клавиши стрелок влево, вверх, вниз или
вправо. Для каждой из них мы соответствующим образом изменяем
значение координат х и у мяча на небольшое количество пикселей.
Затем создаем объект pygame rect для мяча на основании
его координат х и у и его высоты и ширины . Мы можем проверить, перекрывают ли друг друга два прямоугольника, с помощью следующего вызова:
= .colliderect()

Этот вызов сравнивает два прямоугольника и возвращает
True, если они перекрывают друг друга, и False, если нет. Мы

сравниваем прямоугольник мяча с целевым прямоугольником , и, если они перекрывают друг друга, программа в окне
оболочки выводит: «Мяч касается цели».
Последнее изменение касается того, где мы рисуем и целевое изображение, и мяч. Сначала рисуется целевое изображение, чтобы, если они пересекаются, мяч отображался поверх
целевого изображения .
При выполнении программы, если прямоугольник мяча перекрывает прямоугольник целевого изображения, выводится
сообщение в окне оболочки. Если вы уберете мяч с целевого
изображения, сообщение прекратит выводиться.
Работаем с непрерывным нажатием клавиш
в непрерывном режиме
Вторым способом обработки взаимодействия с клавиатурой
в pygame является опрос клавиатуры. Он включает запрос
у pygame списка, представляющего, какие клавиши на текущий
момент нажаты в каждом фрейме, используя следующий вызов:
= pygame.key.get_pressed()
164 Часть II. Графические пользовательские интерфейсы с pygame

Этот вызов возвращает кортеж из нулей и единиц, представляющий состояние каждой клавиши: 0 — если клавиша вверх,
1 — если клавиша вниз. Затем вы можете использовать константы, определенные в pygame, в качестве индекса в возвращенном кортеже, чтобы увидеть, нажата ли конкретная клавиша.
Например, следующие строки можно использовать для определения состояния клавиши А:
keyPressedTuple = pygame.key.get_pressed()
# используем константу, чтобы получить соответствующий
# элемент кортежа
aIsDown = keyPressedTuple[pygame.K_a]

Полный список констант, представляющий все клавиши,
определенные в pygame, вы можете найти по адресу
https://www.pygame.org/docs/ref/key.html.
Код в листинге 5.5 показывает, как мы можем использовать
этот метод для непрерывного перемещения изображения вместо одного перемещения за одно нажатие клавиши. В этой версии мы перемещаем обработку клавиатуры из раздела #7 в раздел #8. Остальная часть кода идентична предыдущей версии
листинга 5.4.
Файл: PygameDemo3_MoveByKeyboard/
PygameMoveByKeyboardContinuous.py
# pygame демо 3 – одно изображение, непрерывный режим, перемещать,
# пока зажата клавиша
--- пропуск --#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()



#8 – Выполняем действия "в рамках фрейма"
# Проверяем нажатия клавиш пользователем
keyPressedTuple = pygame.key.get_pressed()
if keyPressedTuple[pygame.K_LEFT]: # перемещаемся влево
ballX = ballX – N_PIXELS_TO_MOVE
Глава 5. Введение в pygame 165

if keyPressedTuple[pygame.K_RIGHT]: # перемещаемся вправо
ballX = ballX + N_PIXELS_TO_MOVE
if keyPressedTuple[pygame.K_UP]: # перемещаемся вверх
ballY = ballY – N_PIXELS_TO_MOVE
if keyPressedTuple[pygame.K_DOWN]: # перемещаемся вниз
ballY = ballY + N_PIXELS_TO_MOVE
# определяем, перекрывает ли мяч целевое изображение
ballRect = pygame.Rect(ballX, ballY,
BALL_WIDTH_HEIGHT, BALL_WIDTH_HEIGHT)
if ballRect.colliderect(targetRect):
print('Ball is touching the target')
--- пропуск --Листинг 5.5. Обработка зажатых клавиш

Код обработки клавиатуры в листинге 5.5 не полагается
на события, поэтому мы помещаем новый код за пределами цикла for, который проводит итерацию всех событий, возвращенных pygame .
Поскольку мы осуществляем эту проверку в каждом фрейме,
движение мяча будет казаться непрерывным, пока пользователь удерживает клавишу. Например, если пользователь нажимает и удерживает клавишу правой стрелки, этот код будет добавлять 3 к значению координаты ballX в каждом фрейме,
а пользователь будет видеть, что мяч плавно перемещается
вправо. Когда он прекратит нажимать клавишу, перемещение
остановится.
Другое изменение состоит в том, что этот подход позволяет вам проверять наличие нескольких клавиш, нажатых одновременно. Например, если пользователь нажимает и удерживает клавиши левой и нижней стрелок, мяч будет
перемещаться по диагонали влево. Вы можете проверить наличие такого количества нажатых клавиш, какое пожелаете.
Однако количество одновременно нажатых клавиш, которые
можно обнаружить, ограничено операционной системой,
компьютерной клавиатурой и многими другими факторами.
Обычно лимит составляет четыре клавиши, но ваш лимит
может отличаться.

166 Часть II. Графические пользовательские интерфейсы с pygame

Создаем анимацию, основанную на местоположении
Далее мы построим анимацию, основанную на местоположении. Этот код позволит нам перемещать изображение по диагонали, а затем сделать так, чтобы оно отскакивало от краев
окна. Это был любимый метод заставок на старых ЭЛТ-мониторах, позволявший избегать выгорания статичного изображения.
Мы немного изменим местоположение нашего изображения
в каждом фрейме. Также проверим, поместят ли эти перемещения какую-либо часть изображения за пределы одной из границ
окна, и, если это так, изменим перемещение в этом направлении. Например, если изображение перемещается вниз и будет
пересекать нижнюю часть окна, мы изменим направление и заставим изображение двигаться вверх.
И вновь мы воспользуемся тем же начальным шаблоном.
В листинге 5.6 представлен полный исходный код.
Файл: PygameDemo4_OneBallBounce/PygameOneBallBounceXY.py
# pygame демо 4(а) – одно изображение, отскакивает от границ окна
# с использованием координат (х, у)
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import random
#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
BALL_WIDTH_HEIGHT = 100
N_PIXELS_PER_FRAME = 3
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
#5 – Инициализируем переменные
MAX_WIDTH = WINDOW_WIDTH – BALL_WIDTH_HEIGHT
Глава 5. Введение в pygame 167

MAX_HEIGHT = WINDOW_HEIGHT – BALL_WIDTH_HEIGHT
 ballX = random.randrange(MAX_WIDTH)
ballY = random.randrange(MAX_HEIGHT)
xSpeed = N_PIXELS_PER_FRAME
ySpeed = N_PIXELS_PER_FRAME
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()



#8 – Выполняем действия "в рамках фрейма"
if (ballX < 0) or (ballX >= MAX_WIDTH):
xSpeed = -xSpeed # обращаем направление Х
if (ballY < 0) or (ballY >= MAX_HEIGHT):
ySpeed = -ySpeed # обращаем направление Y



# обновляем местоположение мяча, используя скорость в двух
# направлениях
ballX = ballX + xSpeed
ballY = ballY + ySpeed
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(BLACK)
#10 – Рисуем все элементы окна
window.blit(ballImage, (ballX, ballY))
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 5.6. Основанная на местоположении анимация с мячом, отскакивающим
от границ окна

Начинаем с создания и инициализации двух переменных
xSpeed и ySpeed , которые определяют, насколько далеко

и в каком направлении должно перемещаться изображение
168 Часть II. Графические пользовательские интерфейсы с pygame

в каждом фрейме. Мы инициализируем обе переменные количеством перемещаемых во фрейме пикселей (3), чтобы изображение начало перемещаться на три пикселя вправо (положительное направление х) и на три пикселя вниз (положительное
направление у).
В ключевой части программы мы обрабатываем координаты
х и у отдельно . Сначала мы проверяем, является ли значение
координаты х мяча меньше нуля, что обозначает, что часть
изображения выходит за левый край, или больше пикселя
MAX_WIDTH, то есть фактически выходит за правый край. Если
один из этих вариантов верен, мы изменяем знак скорости в направлении х, то есть изображение пойдет в противоположном
направлении. Например, если мяч двигался вправо и зашел
за правый край, мы изменим значение скорости xSpeed с 3
на –3, чтобы мяч стал двигаться влево, и наоборот.
Затем проводим аналогичную проверку координат у, чтобы
мяч отскакивал от верхнего и нижнего края, если это необходимо.
И наконец, обновим позицию мяча, добавив xSpeed к координате ballX и ySpeed к координате ballY . Это помещает его
в новое местоположение по обеим осям.
В нижней части основного цикла мы рисуем мяч. Поскольку
значения ballX и ballY обновляются в каждом фрейме, кажется, что мяч плавно анимируется. Попробуйте это. Каждый раз,
когда мяч достигает края, кажется, будто он отскакивает.
Используем rect pygame
Далее я представлю различные способы достижения одного
и того же результата. Вместо того чтобы отслеживать текущие
координаты х и у мяча в отдельных переменных, мы будем
использовать rect мяча, обновлять rect в каждом фрейме
и проверять, приведет ли обновление к выходу за границы окна
какой-либо части rect. В результате мы получим меньше переменных, и, поскольку мы начнем с вызова для получения rect
изображения, оно будет работать с изображениями любого размера.
Когда вы создаете объект rect, в дополнение к запоминанию left, top, width и height в качестве атрибутов этот
объект также вычисляет и поддерживает для вас несколько других атрибутов. Вы можете получить доступ к любому из этих
Глава 5. Введение в pygame 169

атрибутов непосредственно по имени, используя точечный синтаксис, как показано в табл. 5.1. (Я представлю больше информации по этой теме в главе 8.)
Таблица 5.1. Прямой доступ к атрибутам rect
Атрибут

Описание



Координаты х левого края rect

.y

Координаты у верхнего края rect

.left

Координаты х левого края rect (то же, что и .х)

.top

Координаты у верхнего края rect (то же, что и .y)

.right

Координаты х правого края rect

.bottom

Координаты у нижнего края rect

.topleft

Кортеж из двух целых чисел: координаты верхнего левого угла

rect
.bottomleft

Кортеж из двух целых чисел: координаты нижнего левого угла

rect
.topright

Кортеж из двух целых чисел: координаты верхнего правого
угла rect

.bottomright

Кортеж из двух целых чисел: координаты нижнего правого угла

rect
.midtop

Кортеж из двух целых чисел: координаты средней точки
верхнего края rect

.midleft

Кортеж из двух целых чисел: координаты средней точки левого
края rect

.midbottom

Кортеж из двух целых чисел: координаты средней точки
нижнего края rect

.midright

Кортеж из двух целых чисел: координаты средней точки
правого края rect

.center

Кортеж из двух целых чисел: координаты центра rect

.centerx

Координаты х центра ширины rect

.centery

Координаты у центра высоты rect

.size

Кортеж из двух целых чисел: (ширина, высота) rect

.width

Ширина rect

.height

Высота rect

.w

Ширина rect (то же, что и .width)

.h

Высота rect (то же, что и .height)

170 Часть II. Графические пользовательские интерфейсы с pygame

Объект rect pygame также допустимо представить в виде
списка из четырех элементов и получить к нему доступ. В частности, вы можете использовать индекс, чтобы получить или
установить любую отдельную часть rect. Например, используя
ballRect, можно получить доступ к отдельным элементам следующим образом:
• ballRect[0] — это значение х (но вы также можете использовать ballRect.left);
• ballRect[1] — это значение у (но вы также можете использовать ballRect.top);
• ballRect[2] — это ширина (но вы также можете использовать ballRect.width);
• ballRect[3] — это высота (но вы также можете использовать ballRect.height).
Листинг 5.7 является альтернативной версией нашей программы с прыгающими мячами, которая поддерживает всю информацию о мяче в прямоугольном объекте.
Файл: PygameDemo4_OneBallBounce/
PygameOneBallBounceRects.py
# pygame демо 4(b) – одно изображение, отскакивает от границ окна
# с помощью rect
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import random
#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
N_PIXELS_PER_FRAME = 3
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
Глава 5. Введение в pygame 171

#5 – Инициализируем переменные
 ballRect = ballImage.get_rect()
MAX_WIDTH = WINDOW_WIDTH – ballRect.width
MAX_HEIGHT = WINDOW_HEIGHT – ballRect.height
ballRect.left = random.randrange(MAX_WIDTH)
ballRect.top = random.randrange(MAX_HEIGHT)
xSpeed = N_PIXELS_PER_FRAME
ySpeed = N_PIXELS_PER_FRAME
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()



#8 – Выполняем действия "в рамках фрейма"
if (ballRect.left < 0) or (ballRect.right >= WINDOW_WIDTH):
xSpeed = -xSpeed # обращаем направление Х
if (ballRect.top < 0) or (ballRect.bottom >= WINDOW_HEIGHT):
ySpeed = -ySpeed # обращаем направление Y
# обновляем местоположение мяча, используя скорость в двух
# направлениях
ballRect.left = ballRect.left + xSpeed
ballRect.top = ballRect.top + ySpeed
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(BLACK)



#10 – Рисуем все элементы окна
window.blit(ballImage, ballRect)
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 5.7. Основанная на местоположении анимация с мячом, отскакивающим
от границ окна, с использованием rect

172 Часть II. Графические пользовательские интерфейсы с pygame

Этот подход использования объекта rect не лучше и не хуже
применения отдельных переменных. Итоговая программа работает точно так же, как и исходная версия. Важный урок здесь
в том, как вы можете использовать атрибуты объекта rect и манипулировать ими.
После загрузки изображения мяча мы вызываем метод get_
rect(), чтобы получить ограничивающий прямоугольник
изображения. Этот вызов возвращает объект rect, который
мы сохраняем в переменную под названием ballRect. Мы используем ballRect.width и ballRect.height, чтобы получить
прямой доступ к ширине и высоте изображения мяча. (В предыдущей версии мы использовали константу 100 для ширины
и высоты.) Извлечение этих значений из загруженного изображения делает наш код гораздо более адаптивным, поскольку
это означает, что мы можем использовать графику любого размера.
Код также задействует атрибуты прямоугольника вместо использования отдельных переменных для проверки выхода
за края любой части прямоугольника мяча. Мы применим
ballRect.left и ballRect.right, чтобы увидеть, выходит ли
ballRect за правый или левый край . Проводим ту же проверку с помощью ballRect.top и ballRect.bottom. Вместо обновления отдельных координат х и у мы обновляем значения left
и right ballRect.
Другое едва различимое, но важное изменение заключается
в выполнении вызова, чтобы нарисовать мяч . Вторым аргументом в вызове blit() может быть либо кортеж (х, у), либо
rect. Код внутри blit() использует левую верхнюю позицию
в rect в качестве координат х и у.

Воспроизводим звуки
Существует два типа звуков, которые вы, возможно, захотите
воспроизвести в вашей программе: короткие звуковые
эффекты и фоновая музыка.
Воспроизводим звуковые эффекты
Все звуковые эффекты обязаны находиться во внешних файлах
и должны быть либо .wav, либо .ogg формата. Воспроизведение
относительно короткого звукового эффекта состоит из двух
Глава 5. Введение в pygame 173

шагов: один раз загрузить звук из внешнего звукового файла;
затем в подходящее время воспроизвести его.
Чтобы загрузить звуковой эффект в память, используйте
строку, подобную этой:
= pygame.mixer.Sound()

Чтобы воспроизвести звуковой эффект, вам лишь нужно вызвать метод play():
.play()

Мы изменим листинг 5.7, чтобы добавить звуковой эффект
«бум», который станет воспроизводиться каждый раз, когда
мяч будет отскакивать от стороны окна. В папке проекта есть
папка Sounds на том же уровне, что и основная программа. Сразу же после загрузки изображения мяча мы загружаем звуковой
файл, добавляя следующий код:
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
bounceSound = pygame.mixer.Sound('sounds/boing.wav')

Чтобы воспроизводить звуковой эффект «бум» каждый раз,
когда меняем либо горизонтальное, либо вертикальное направление мяча, мы изменяем раздел #8, чтобы он выглядел следующим образом:
#8 – Выполняем действия "в рамках фрейма"
if (ballRect.left < 0) or (ballRect.right >= WINDOW_WIDTH):
xSpeed = -xSpeed # обращаем направление Х
bounceSound.play()
if (ballRect.top < 0) or (ballRect.bottom >= WINDOW_HEIGHT):
ySpeed = -ySpeed # обращаем направление Y
bounceSound.play()

Когда выполняется условие, при котором надо воспроизводить звуковой эффект, вы добавляете метод звука play(). Существует еще множество вариантов управления звуковыми эффектами; подробности вы найдете в официальной документации
по адресу https://www.pygame.org/docs/ref/mixer.html.

174 Часть II. Графические пользовательские интерфейсы с pygame

Воспроизведение фоновой музыки
Воспроизведение фоновой музыки включает две строки кода,
использующие вызовы модуля pygame.mixer.music. Во-первых, вам необходимо загрузить звуковой файл в память:
pygame.mixer.music.load ()

— это строка пути, по которому
можно найти звуковой файл. Вы можете использовать файлы
.mp3, которые подходят лучше всего, а также файлы формата
.wav или .ogg. Когда вы хотите начать воспроизведение музыки,
вам необходимо осуществить этот вызов:
pygame.mixer.music.play ( )

Чтобы неоднократно воспроизводить какую-то фоновую музыку, вы можете передать –1 в , чтобы музыка
проигрывалась непрерывно. , как правило,
устанавливается в значение 0, чтобы обозначить, что вы хотите
проигрывать звук с самого начала.
Вы можете скачать измененную версию программы прыгающего мяча, которая должным образом загружает файлы звуковых эффектов и фоновой музыки и запускает воспроизведение
фонового звука. Единственные изменения внесены в раздел #4,
как показано здесь.
#4 – Загружаем элементы: изображения, звуки и т. д.
ballImage = pygame.image.load('images/ball.png')
bounceSound = pygame.mixer.Sound('sounds/boing.wav')
pygame.mixer.music.load('sounds/background.mp3')
pygame.mixer.music.play(-1, 0.0)

Pygame позволяет гораздо более сложно обрабатывать фоновые звуки. Вы можете найти полную документацию по адресу
https://www.pygame.org/docs/ ref/music.html#modulepygame.mixer.music.
ПРИМЕЧАНИЕ

Чтобы будущие примеры были более четко сосредоточены
на ООП, я опущу вызовы для воспроизведения звуковых эффектов и фоновой музыки. Но добавление звуков значительно улучшает пользовательский игровой опыт, и я настоятельно
рекомендую включать их.
Глава 5. Введение в pygame 175

Рисуем фигуры
Pygame предлагает некоторое количество встроенных функ-

ций, которые позволяют вам рисовать конкретные фигуры,
известные как примитивы, которые включают линии, круги,
овалы, дуги, многоугольники и прямоугольники. В табл. 5.2
представлен список этих функций. Обратите внимание, что
существует два вызова, которые рисуют сглаженные линии,
то есть те, которые содержат смешанные цвета по краям,
чтобы они выглядели плавными и менее неровными. Есть два
ключевых преимущества при использовании этих функций
рисования: они исполняются невероятно быстро и позволяют
вам рисовать простые фигуры без необходимости создания или
загрузки изображений из внешних файлов.
Таблица 5.2. Функции для рисования фигур
Функция

Описание

pygame.draw.aaline()

Рисует сглаженные линии

pygame.draw.aalines()

Рисует серию сглаженных линий

pygame.draw.arc()

Рисует дугу

pygame.draw.circle()

Рисует круг

pygame.draw.ellipse()

Рисует овал

pygame.draw.line()

Рисует линию

pygame.draw.lines()

Рисует серию линий

pygame.draw.polygon()

Рисует многоугольник

pygame.draw.rect()

Рисует прямоугольник

На рис. 5.7 показаны выходные данные примера программы,
который демонстрирует вызовы этих функций графических
примитивов.
В листинге 5.8 показан код примера программы с использованием 12-шагового шаблона, который привел к выходным данным на рис. 5.7.

176 Часть II. Графические пользовательские интерфейсы с pygame

Файл: PygameDemo5_DrawingShapes.py
# pygame демо 5 – рисунок
--- пропуск --while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Нажата кнопка закрытия? Выходим из pygame и завершаем
# программу
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
#8 – Выполняем действия "в рамках фрейма"
#9 – Очищаем окно
window.fill(GRAY)


#10 – Рисуем все элементы окна
# рисуем прямоугольник
pygame.draw.line(window, BLUE,
pygame.draw.line(window, BLUE,
pygame.draw.line(window, BLUE,
pygame.draw.line(window, BLUE,

(20,
(20,
(20,
(60,

20),
20),
60),
20),

(60,
(20,
(60,
(60,

20),
60),
60),
60),

4)
4)
4)
4)

#
#
#
#

верх
лево
право
низ

# рисуем Х в прямоугольнике
pygame.draw.line(window, BLUE, (20, 20), (60, 60), 1)
pygame.draw.line(window, BLUE, (20, 60), (60, 20), 1)
# рисуем закрашенный круг и пустой круг
pygame.draw.circle(window, GREEN, (250, 50), 30, 0) # закрашенный
pygame.draw.circle(window, GREEN, (400, 50), 30, 2) # контур
# в 2 пикселя
# рисуем закрашенный прямоугольник и пустой прямоугольник
pygame.draw.rect(window, RED, (250, 150, 100, 50), 0) # закрашенный
pygame.draw.rect(window, RED, (400, 150, 100, 50), 1) # контур
# в 1 пиксель
# рисуем закрашенный овал и пустой овал
pygame.draw.ellipse(window, YELLOW, (250, 250, 80, 40), 0)
# закрашенный
pygame.draw.ellipse(window, YELLOW, (400, 250, 80, 40), 2)
# контур в 2 пикселя

Глава 5. Введение в pygame 177

# рисуем шестисторонний многоугольник
pygame.draw.polygon(window, TEAL, ((240, 350), (350, 350),
(410, 410), (350, 470),
(240, 470), (170, 410)))
# рисуем дугу
pygame.draw.arc(window, BLUE, (20, 400, 100, 100), 0, 2, 5)
# рисуем сглаженные линии: одну линию, затем список точек
pygame.draw.aaline(window, RED, (500, 400), (540, 470), 1)
pygame.draw.aalines(window, BLUE, True,
((580, 400), (587, 450),
(595, 460), (600, 444)), 1)
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) # ожидание pygame
Листинг 5.8. Программа для демонстрации вызовов функций графических примитивов
в pygame

Рис. 5.7. Пример программы, который демонстрирует использование вызовов
для графических примитивных фигур

Рисунок всех примитивов приведен в разделе #10 . Мы вызываем функции pygame, чтобы нарисовать прямоугольник
178 Часть II. Графические пользовательские интерфейсы с pygame

с двумя диагоналями, закрашенные и пустые круги, закрашенные и пустые прямоугольники, закрашенные и пустые овалы,
шестисторонний многоугольник, дугу и две сглаженные линии.
Справка по примитивным фигурам
Вам для справки представлена документация по методам
pygame для изображения этих примитивов. Во всех следующих
случаях аргумент color ожидает, что вы ему передадите кортеж значений RGB.
Сглаженная линия
pygame.draw.aaline(window, color, startpos, endpos, blend=True)

Рисует в окне сглаженную линию. Если значение blend равно True, оттенки будут смешаны с существующими цветами
пикселей вместо того, чтобы переписывать пиксели.
Сглаженные линии
pygame.draw.aalines(window, color, closed, points, blend=True)

Рисует в окне последовательность сглаженных линий. Аргумент closed — это простое булево выражение; если он принимает значение True, между первой и последней точками будет
нарисована линия, чтобы замкнуть фигуру. Аргумент points —
это список кортежей координат (х, у), которые должны быть соединены отрезками линий (должно быть как минимум две точки). Если значение булева аргумента blend равно True, то он
станет смешивать оттенки с существующими оттенками пикселей вместо того, чтобы переписывать их.
Дуга
pygame.draw.arc(window, color, rect, angle_start, angle_stop, width=0)

Рисует в окне дугу. Дуга будет помещена внутри заданного
rect. Два аргумента angle — это исходный и окончательный
углы (в радианах, с нулем справа). Аргумент width — это толщина для рисования контура.

Глава 5. Введение в pygame 179

Круг
pygame.draw.circle(window, color, pos, radius, width=0)

Рисует в окне круг. pos — это центр круга, а radius — это радиус. Аргумент width — это толщина для рисования контура.
Если width равен 0, тогда круг будет закрашен.
Овал
pygame.draw.ellipse(window, color, rect, width=0)

Рисует вокне овал. Заданный rect — это область, которую
будет заполнять овал. Аргумент width — это толщина для рисования контура. Если width равен 0, тогда овал будет закрашен.
Линия
pygame.draw.line(window, color, startpos, endpos, width=1)

Рисует в окне линию. Аргумент width — это толщина линии.
Линии
pygame.draw.lines(window, color, closed, points, width=1)

Рисует в окне последовательность линий. Аргумент closed —
это простое булево выражение; если он принимает значение
True, между первой и последней точками нарисуется линия,
чтобы замкнуть фигуру. Аргумент points — это список кортежей координат (х, у), которые должны быть соединены отрезками линий (как минимум двух). Аргумент width — это толщина
линии. Обратите внимание, что указание ширины линии больше 1 не заполняет пробелы между линиями. Таким образом,
широкие линии и острые углы не смогут плавно соединяться.
Многоугольник
pygame.draw.polygon(window, color, pointslist, width=0)

Рисует в окне многоугольник. pointslist указывает вершины многоугольника. Аргумент width — это толщина для
180 Часть II. Графические пользовательские интерфейсы с pygame

рисования контура. Если width равен 0, тогда многоугольник
будет закрашен.
Прямоугольник
pygame.draw.rect(window, color, rect, width=0)

Рисует в окне прямоугольник. rect — это область прямоугольника. Аргумент width — это толщина для рисования контура. Если width равен 0, тогда прямоугольник будет закрашен.
ПРИМЕЧАНИЕ

Чтобы получить дополнительную информацию, пройдите
по ссылке http://www.pygame.org/docs/ref/draw.html.
Набор вызовов примитивов позволяет гибко рисовать любые фигуры, которые вы пожелаете. И вновь напоминаю: порядок осуществляемых вами вызовов важен. Думайте о порядке
вызовов как о слоях; нарисованные раньше элементы могут
быть перекрыты более поздними вызовами любой другой функции графического примитива.

Выводы
В этой главе я познакомил вас с основами pygame. Вы установили pygame на ваш компьютер, затем узнали о модели управляемого событиями программирования и об использовании
этих событий, что сильно отличается от кодирования текстовых программ. Я объяснил систему координат пикселей в окне
и способ представления цветов в коде.
Чтобы начать с pygame с самого начала, я представил 12-шаговый шаблон, который лишь выводит окно и может быть использован для создания любой основанной на pygame программы. Используя эту структуру, мы затем построили примеры
программ, которые показали, как рисовать изображение в окне
(с помощью blit()), обнаруживать события мыши и обрабатывать входные данные клавиатуры. Следующая демонстрация объяснила, как создать основанную на местоположении анимацию.
Прямоугольники невероятно важны в pygame, поэтому
я рассказал о том, как можно использовать атрибуты объекта
rect. Я также предоставил некоторые примеры кода, чтобы

Глава 5. Введение в pygame 181

показать, как воспроизводить звуковые эффекты и фоновую музыку для увеличения удовольствия пользователя от взаимодействия с программой. И наконец, я показал, как использовать
методы pygame для изображения примитивных фигур в окне.
Несмотря на то что я представил много понятий в pygame,
практически все, что я показал в этой главе, было, по сути, процедурным. Объект rect является примером объектно-ориентированного кода, встроенного непосредственно в pygame. В следующей главе я покажу, как использовать ООП в коде, чтобы
применять pygame более эффективно.

6
ОБЪЕ К ТНО ОРИЕ НТИРОВА ННЫЙ PYGA M E

В этой главе я продемонстрирую, как вы можете
эффективно использовать методы ООП
с фреймворком pygame. Мы начнем с примера
процедурного кода, затем разобьем его на отдельный класс и основной код, который вызывает методы
этого класса. После чего мы создадим два класса SimpleButton
и SimpleText, которые реализуют базовые виджеты интерфейса пользователя: кнопку и поле для отображения текста.
Я также познакомлю вас с понятием обратного вызова.

Создаем заставку мяча с помощью Pygame
В главе 5 мы создали старомодную заставку, в которой мяч прыгал внутри окна (листинг 5.6, если вам необходимо освежить
память).
Код работает, но данные для мяча и код для управления мячом переплетены, то есть существует много кода инициализации, а код для обновления и рисования мяча вложен в 12-шаговую структуру.

Объектно- ориентированный pygame 183

Более модульный подход заключается в разбитии кода
на класс Ball и основную программу, которая создает экземпляр объекта Ball и осуществляет вызовы его методов. Здесь
мы разделим это, и я покажу вам, как создавать несколько мячей из класса Ball.
Создаем класс Ball
Начнем с извлечения всего кода, относящегося к мячу, из основной программы и перемещения его в отдельный класс Ball.
Просматривая исходный код, мы можем увидеть, что разделы,
относящиеся к мячу, включают:
• раздел #4, который загружает изображение мяча;
• раздел #5, который создает и инициализирует все переменные, имеющие какое-либо отношению к мячу;
• раздел #8, который включает код для перемещения мяча,
обнаруживая края для отскока и изменяя скорость
и направление;
• раздел #10, который рисует мяч.
Из этого мы можем сделать вывод, что нашему классу Ball
потребуются следующие методы:
• create() — загружает изображение, устанавливает местоположение и инициализирует все переменные экземпляра;
• update() — изменяет местоположение мяча в каждом фрейме на основании скорости х и у мяча;
• draw() — рисует мяч в окне.
Первый шаг состоит в создании папки проекта, в которой
вам понадобятся файл Ball.py для класса Ball, файл основного
кода Main_BallBounce.pу и папка images, содержащая файл изображения ball.png.
В листинге 6.1 показан код класса Ball.
Файл: PygameDemo6_BallBounceObjectOriented/Ball.py
import pygame
from pygame.locals import *
import random
# класс Ball
class Ball():

184 Часть II. Графические пользовательские интерфейсы с pygame





def __init__(self, window, windowWidth, windowHeight):
self.window = window # запоминаем окно, чтобы мы смогли
# нарисовать позднее
self.windowWidth = windowWidth
self.windowHeight = windowHeight
self.image = pygame.image.load('images/ball.png')
# прямоугольник состоит из [х, y, ширина, высота]
ballRect = self.image.get_rect()
self.width = ballRect.width
self.height = ballRect.height
self.maxWidth = windowWidth – self.width
self.maxHeight = windowHeight – self.height



# выбираем произвольную начальную позицию
self.x = random.randrange(0, self.maxWidth)
self.y = random.randrange(0, self.maxHeight)



# выбираем произвольную скорость между -4 и 4, но не ноль
# в обоих направлениях х и y
speedsList = [-4, -3, -2, -1, 1, 2, 3, 4]
self.xSpeed = random.choice(speedsList)
self.ySpeed = random.choice(speedsList)



def update(self):
# проверяем наличие ударов о стену. Если они есть, изменяем
# направление
if (self.x < 0) or (self.x >= self.maxWidth):
self.xSpeed = -self.xSpeed
if (self.y < 0) or (self.y >= self.maxHeight):
self.ySpeed = -self.ySpeed
# обновляем х и y Ball, используя скорость в двух направлениях
self.x = self.x + self.xSpeed
self.y = self.y + self.ySpeed



def draw(self):
self.window.blit(self.image, (self.x, self.y))
Листинг 6.1. Новый класс Ball

Когда мы создаем экземпляр объекта Ball, метод __init__()
получает три части данных: окно, в котором рисовать, ширину
окна и высоту окна . Мы сохраняем переменную window в переменную экземпляра self.window, чтобы можно было ее
Глава 6. Объектно- ориентированный pygame 185

использовать в дальнейшем в методе draw(), и делаем то же самое с переменными экземпляра self.windowHeight и self.
windowWidth. Затем загружаем изображение мяча, используя
путь к файлу, и получаем rect изображения мяча . Нам необходимо, чтобы rect пересчитал максимальные значения для х
и у, чтобы мяч всегда полностью отображался в окне. Далее мы
выбираем случайное начальное местоположение для мяча .
И наконец, устанавливаем скорость направлений х и у в произвольное значение от –4 до 4 (но не 0), представляющее количество пикселей для перемещения в каждом фрейме . Из-за всех
этих чисел мяч может перемещаться по-разному каждый раз,
когда мы запускаем программу. Эти значения сохраняются в переменных экземпляра, чтобы их могли использовать другие методы.
В основной программе мы вызываем метод update() в каждом фрейме основного цикла и именно сюда помещаем код, который проверяет, ударялся ли мяч о границу окна . Если он
это делает, мы меняем скорость в этом направлении и изменяем координаты х и у (self.х и self.y) на текущую скорость
в направлениях х и у.
Также вызываем метод draw(), который просто вызывает
blit(), чтобы нарисовать мяч в его текущих координатах х
и у  в каждом фрейме основного цикла.
Используем класс Ball
Теперь вся связанная с мячом функциональность была помещена в код класса Ball. Все, что необходимо сделать основной
программе, — это создать мяч, затем в каждом фрейме вызвать
методы update()и draw(). В листинге 6.2 показан значительно
упрощенный основной программный код.
Файл: PygameDemo6_BallBounceObjectOriented/Main_BallBounce.py
# pygame демо 6(а) – используя класс Ball, отбивать один мяч
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import random
 from Ball import * # вводим код класса Ball

186 Часть II. Графические пользовательские интерфейсы с pygame

#2 – Определяем константы
BLACK = (0, 0, 0)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
#5 – Инициализируем переменные
 oBall = Ball(window, WINDOW_WIDTH, WINDOW_HEIGHT)
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()



#8 – Выполняем действия "в рамках фрейма"
oBall.update() # обновляем Ball
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(BLACK)



#10 – Рисуем все элементы окна
oBall.draw() # рисуем Ball
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 6.2. Новая основная программа, которая создает экземпляр Ball и вызывает
его методы

Если вы сравните эту новую основную программу с исходным кодом в листинге 5.6, то увидите, что она значительно проще и яснее. Мы используем оператор import, чтобы ввести код
класса Ball . Создаем объект Ball, передавая в него уже

Глава 6. Объектно- ориентированный pygame 187

готовое окно, а также его ширину и высоту , сохраняем итоговый объект Ball в переменную под названием oBall.
Ответственность за передвижение мяча теперь лежит
на коде класса Ball, поэтому здесь нам только необходимо вызвать метод update() объекта oBall . Поскольку объект Ball
знает величину окна, величину изображения мяча плюс его местоположение и скорость, он может выполнять все необходимые вычисления, чтобы перемещать мяч и отбивать его
от стен.
Основной код вызывает метод draw() объекта oBall ,
а фактически рисунок выполняется объектом oBall.
Создаем много объектов Ball
Теперь давайте внесем мелкое, но важное изменение в основную программу, чтобы создать насколько объектов Ball. Это
одно из реальных преимуществ ООП: чтобы создать три мяча,
нам необходимо лишь создать экземпляры трех объектов Ball
из класса Ball. Здесь воспользуемся базовым подходом и создадим список объектов Ball. В каждом фрейме мы будем проводить итерацию списка объектов Ball, говорить каждому из них
обновлять его местоположение, затем снова проводить итерацию, чтобы сказать каждому из них нарисовать себя.
В листинге 6.3 продемонстрирована измененная основная программа, которая создает и обновляет три объекта Ball.
Файл: PygameDemo6_BallBounceObjectOriented/
Main_BallBounceManyBalls.py
# pygame демо 6(b) – используя класс Ball, отбивать много мячей
--- пропуск --N_BALLS = 3
--- пропуск --#5 – Инициализируем переменные
 ballList = []
for oBall in range(0, N_BALLS):
# Каждый раз, проходя цикл, создаем объект Ball
oBall = Ball(window, WINDOW_WIDTH, WINDOW_HEIGHT)
ballList.append(oBall) # добавляем новый Ball в список мячей
#6 – Бесконечный цикл
while True:

188 Часть II. Графические пользовательские интерфейсы с pygame

--- пропуск ---



#8 – Выполняем действия "в рамках фрейма"
for oBall in ballList:
oBall.update() # обновляем Ball
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(BLACK)



#10 – Рисуем все элементы окна
for oBall in ballList:
oBall.draw() # рисуем Ball
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 6.3. Создание, перемещение и отображение трех мячей

Мы начинаем с пустого списка объектов Ball . Затем наш
цикл создает три объекта Ball, каждый из которых добавляем
в список объектов Ball, ballList. Каждый объект Ball выбирает и запоминает случайное начальное местоположение
и случайную скорость как для направления х, так и для направления у.
Внутри основного цикла мы проводим итерацию всех объектов Ball и обновляем их , изменяя координаты х и у каждого
объекта Ball на новое местоположение. Затем мы снова проводим итерацию списка, вызывая метод draw() для каждого объекта Ball .
Когда запускаем программу, мы видим три мяча, каждый
из которых стартует из произвольного местоположения и перемещается с произвольной скоростью х и у. Каждый мяч безошибочно отскакивает от границ окна.
Используя объектно- ориентированный подход, мы не вносим никакие изменения в класс Ball, просто меняем основную программу, чтобы она управляла списком объектов Ball
вместо одного объекта Ball. Это распространенный и очень
положительный побочный эффект кода ООП: хорошо написанные классы можно часто повторно использовать без изменений.

Глава 6. Объектно- ориентированный pygame 189

Создаем много, много объектов Ball
Мы можем изменить значение константы N_BALLS с 3 на некоторое гораздо большее число, например 300, чтобы быстро
создать множество мячей (рис. 6.1). Изменяя лишь одну константу, мы вносим огромные изменения в поведение программы. Каждый мяч поддерживает собственную скорость
и местоположение и рисует сам себя.

Рис. 6.1. Создание, перемещение и отображение 300 объектов Ball

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

Создаем многократно используемую объектноориентированную кнопку
Простая кнопка — один из наиболее узнаваемых элементов графического интерфейса пользователя. Ее стандартное поведение — реагировать на щелчок мыши. Пользователь нажимает
на картинку кнопки, а затем отпускает.
190 Часть II. Графические пользовательские интерфейсы с pygame

Кнопки обычно состоят из как минимум двух изображений:
одно для представления состояния вверх (или нормального состояния кнопки) и другое для представления состояния вниз
(или нажатого состояния кнопки). Последовательность щелчков можно разбить на следующие шаги.
1. Пользователь перемещает указатель мыши на кнопку.
2. Пользователь нажимает кнопку мыши.
3. Программа реагирует, изменяя изображение в состояние вниз.
4. Пользователь отпускает кнопку мыши.
5. Программа реагирует, отображая изображение кнопки вверх.
6. Программа осуществляет некоторое действие на основании
нажатия кнопки.
Хорошие GUI позволяют пользователю нажимать на кнопку,
временно уйти с нее, изменяя рисунок на состояние вверх, а затем при все еще зажатой кнопке мыши вернуться к изображению, так что кнопка изменяет свой вид обратно на изображение вниз. Если пользователь щелкает по кнопке, но затем
уводит мышь в сторону и отпускает кнопку мыши, это не считается щелчком. Это означает, что программа принимает действие, только когда пользователь нажимает и отпускает,
в то время как курсор помещен на изображение кнопки.
Создаем класс кнопки
Поведение кнопки должно быть общим и последовательным для
всех кнопок, используемых в GUI, поэтому мы создадим класс,
который позаботится об особенностях поведения. Создав простой класс кнопки, мы можем создавать экземпляры любого количества кнопок, и они будут работать точно таким же образом.
Давайте рассмотрим, какие действия классов кнопок необходимо поддерживать. Нам нужны методы, чтобы:
1) загружать изображения состояний «вверх» и «вниз», затем инициализировать любые переменные экземпляра, чтобы отслеживать состояние кнопки;
2) сообщать кнопке о событиях, которые обнаружила основная
программа, и проверять, есть ли какие-либо кнопки, на которые необходимо отреагировать;
3) рисовать текущее изображение, представляющее кнопку.
Глава 6. Объектно- ориентированный pygame 191

В листинге 6.4 представлен код класса SimpleButton. (Мы
создадим более сложный класс кнопки в главе 7.) У этого класса
есть три метода: __init__(), handleEvent() и draw(), которые реализуют упомянутые действия. Код метода
handleEvent() действительно немного сложнее, но, как только
вы заставите его работать, он окажется невероятно прост в использовании. Не стесняйтесь работать над ним, но имейте
в виду, что реализация кода не так важна. Важным моментом
здесь является понимание и использование различных методов.
Файл: PygameDemo7_SimpleButton/SimpleButton.py
# Класс SimpleButton
#
# Используем подход "конечного автомата"
#
import pygame
from pygame.locals import *
class SimpleButton():
# Используется для отслеживания состояния кнопки
STATE_IDLE = 'idle' # кнопка вверх, мышь не на кнопке
STATE_ARMED = 'armed' # кнопка вниз, мышь на кнопке
STATE_DISARMED = 'disarmed' # щелчок по кнопке, откат
def __init__(self, window, loc, up, down): 
self.window = window
self.loc = loc
self.surfaceUp = pygame.image.load(up)
self.surfaceDown = pygame.image.load(down)
# получаем rect кнопки (используется, чтобы увидеть,
# находится ли мышь на кнопке)
self.rect = self.surfaceUp.get_rect()
self.rect[0] = loc[0]
self.rect[1] = loc[1]
self.state = SimpleButton.STATE_IDLE
def handleEvent(self, eventObj): 
# Это метод вернет значение True, если пользователь щелкнет
# по кнопке.
# Обычно возвращает False
if eventObj.type not in (MOUSEMOTION, MOUSEBUTTONUP,
MOUSEBUTTONDOWN): 

192 Часть II. Графические пользовательские интерфейсы с pygame

# Кнопка реагирует только на относящиеся к мыши события
return False
eventPointInButtonRect = self.rect.collidepoint(eventObj.pos)
if self.state == SimpleButton.STATE_IDLE:
if (eventObj.type == MOUSEBUTTONDOWN) and
eventPointInButtonRect:
self.state = SimpleButton.STATE_ARMED
elif self.state == SimpleButton.STATE_ARMED:
if (eventObj.type == MOUSEBUTTONUP) and
eventPointInButtonRect:
self.state = SimpleButton.STATE_IDLE
return True # был щелчок!
if (eventObj.type == MOUSEMOTION) and
(not eventPointInButtonRect):
self.state = SimpleButton.STATE_DISARMED
elif self.state == SimpleButton.STATE_DISARMED:
if eventPointInButtonRect:
self.state = SimpleButton.STATE_ARMED
elif eventObj.type == MOUSEBUTTONUP:
self.state = SimpleButton.STATE_IDLE
return False
def draw(self): 4
# рисуем текущий вид кнопки в окне
if self.state == SimpleButton.STATE_ARMED:
self.window.blit(self.surfaceDown, self.loc)
else: # IDLE или DISARMED
self.window.blit(self.surfaceUp, self.loc)
Листинг 6.4. Класс SimpleButton

Метод __init__() начинается с сохранения всех значений,
переданных переменным экземпляра , чтобы использовать
другие методы. Затем он инициализирует еще несколько переменных экземпляра.
Каждый раз, когда программа обнаруживает какое-либо событие, она вызывает метод handleEvent() . Сначала он проверяет, является ли событие одним из: MOUSEMOTION,
MOUSEBUTTONUP или MOUSEBUTTONDOWN .
Глава 6. Объектно- ориентированный pygame 193

Остальная часть реализуется в качестве конечного автомата,
метода, который я рассмотрю более подробно в главе 15. Код немного сложен, и не стесняйтесь изучить, как он работает,
но на данный момент обратите внимание, что он использует переменные экземпляра self.state (в течение нескольких вызовов), чтобы обнаружить, щелкнул ли пользователь по кнопке.
Метод handleEvent() возвращает значение True, когда пользователь завершает щелчок мыши, нажимая кнопку, а затем позднее отпуская ее на той же кнопке. Во всех остальных случаях
handleEvent() возвращает False.
И наконец, метод draw() использует состояние переменной
экземпляра объекта self.state, чтобы решить, какое изображение (вверх или вниз) рисовать .
Основной код, использующий SimpleButton
Чтобы использовать SimpleButton в основном коде, мы сначала создаем экземпляр из класса SimpleButton перед началом
основного цикла с помощью строки, подобной этой:
oButton = SimpleButton(window, (150, 30),
'images/buttonUp.png',
'images/buttonDown.png')

Она создает объект SimpleButton, указывая местоположение для его отображения (как обычно, координаты представлены для верхнего левого угла ограничивающего прямоугольника) и предоставляя пути к изображениям кнопки вверх и вниз.
В основном цикле каждый раз, когда происходит событие, нам
необходимо вызывать метод handleEvent(), чтобы увидеть,
щелкнул ли пользователь по кнопке. Если пользователь щелкает по кнопке, программа должна выполнить какое-то действие.
Также в основном цикле необходимо вызвать метод draw(),
чтобы отобразить кнопку в окне.
Мы создадим небольшую тестовую программу, которая будет генерировать интерфейс пользователя, как на рис. 6.2, чтобы включить один экземпляр в SimpleButton.
Каждый раз, когда пользователь завершает щелчок по кнопке, программа выводит строку текста в оболочке, где написано,
что кнопка была нажата. В листинге 6.5 содержится основной
программный код.

194 Часть II. Графические пользовательские интерфейсы с pygame

Рис. 6.2. Интерфейс пользователя программы с одним экземпляром

SimpleButton

Файл: PygameDemo7_SimpleButton/Main_SimpleButton.py
# Pygame демо 7 – тест SimpleButton
--- пропуск --#5 – Инициализируем переменные
# создаем экземпляр SimpleButton
 oButton = SimpleButton(window, (150, 30),
'images/buttonUp.png',
'images/buttonDown.png')
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()




# передаем событие кнопке, смотрим, была ли она нажата
if oButton.handleEvent(event):
print('User has clicked the button')
#8 – Выполняем действия "в рамках фрейма"
#9 – Очищаем окно
window.fill(GRAY)



#10 – Рисуем все элементы окна
oButton.draw() # рисуем кнопку
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 6.5. Основная программа, которая создает SimpleButton и реагирует
на него
Глава 6. Объектно- ориентированный pygame 195

И вновь мы начинаем со стандартного шаблона pygame
из главы 5. Перед началом основного цикла мы создаем экземпляр SimpleButton , указывая окно, в котором нужно рисовать, местоположение, путь к изображению вверх и путь к изображению вниз.
Каждый раз, проходя основной цикл, нам необходимо реагировать на обнаруженные в основной программе события. Чтобы реализовать это, мы вызываем метод handleEvent() класса
SimpleButton  и передаем event из основной программы.
Метод handleEvent() отслеживает все действия пользователя с кнопкой (нажатие, отпускание, откат, возврат). Когда
handleEvent() принимает значение True, указывая, что произошел щелчок, мы осуществляем действие, связанное с нажатием кнопки. Здесь мы просто выводим сообщение .
И наконец, вызываем метод кнопки draw() , чтобы нарисовать изображение, представляющее подходящее состояние
кнопки (вверх или вниз).
Создаем программу с несколькими кнопками
С помощью класса SimpleButton мы способны создать экземпляры необходимого нам количества кнопок. Например, можем
изменить нашу основную программу, чтобы она включала три
экземпляра SimpleButton, как показано на рис. 6.3.

Рис. 6.3. Основная программа с тремя объектами SimpleButton

Чтобы сделать это, нам не нужно вносить какие-либо изменения в файл класса SimpleButton. Мы просто изменяем основной код, чтобы создать экземпляры трех объектов
SimpleButton вместо одного.
oButtonA = SimpleButton(window, (25, 30),
'images/buttonAUp.png',
'images/buttonADown.png')
oButtonB = SimpleButton(window, (150, 30),
'images/buttonBUp.png',
'images/buttonBDown.png')
oButtonC = SimpleButton(window, (275, 30),

196 Часть II. Графические пользовательские интерфейсы с pygame

'images/buttonCUp.png',
'images/buttonCDown.png')

Теперь нам нужно вызвать метод handleEvent() для всех
трех кнопок:

# передаем событие каждой кнопке, смотрим, была ли
# она нажата
if oButtonA.handleEvent(event):
print('User clicked button A.')
elif oButtonB.handleEvent(event):
print('User clicked button B.')
elif oButtonC.handleEvent(event):
print('User clicked button C.')

И наконец, рисуем кнопки:
oButtonA.draw()
oButtonB.draw()
oButtonC.draw()

Когда запустите программу, вы увидите окно с тремя кнопками. Щелчок по любой из них выводит сообщение, содержащее имя кнопки, которая была нажата.
Ключевая идея здесь заключается в том, что, поскольку мы используем три экземпляра одного и того же класса SimpleButton,
поведение всех кнопок будет идентичным. Важное преимущество этого подхода — любое изменение кода в классе
SimpleButton повлияет на все кнопки класса, для которых
были созданы экземпляры. Основной программе не стоит беспокоиться ни о каких деталях внутренней работы кода кнопки,
ей просто необходимо вызывать метод handleEvent() для каждой кнопки основного цикла. Каждая кнопка будет возвращать
значение True или False, обозначая, была ли нажата кнопка.

Создаем многократно используемое
отображение текста
В программе pygame есть два различных типа текста: отображаемый и вводимый. Отображаемый текст — это выходные данные вашей программы, эквивалентные вызову функции
print(), за исключением того, что они отображаются в окне
Глава 6. Объектно- ориентированный pygame 197

pygame. Вводимый текст — это строка входных данных от поль-

зователя, эквивалентная вызову input(). В этом разделе я буду
рассматривать отображаемый текст. А в следующей главе мы
посмотрим, как работать с вводом текста.
Шаги для отображения текста
Отображение текста в окне представляет собой достаточно
сложный процесс в pygame, потому что надо не просто показать его в виде строки в оболочке, но также выбрать местоположение, шрифты, размеры шрифтов и прочие атрибуты. Например, вы можете использовать подобный код:
pygame.font.init()
myFont = pygame.font.SysFont('Comic Sans MS', 30)
textSurface = myfont.render('Some text', True, (0, 0, 0))
window.blit(textSurface, (10, 10))

Мы начнем с инициализации системы шрифтов в pygame;
сделаем это перед началом основного цикла. Затем скажем
pygame загрузить конкретный шрифт из системы, указав его
имя. Здесь мы запросим шрифт Comic Sans размером 30 пунктов.
Следующий шаг — ключевой: мы используем этот шрифт,
чтобы визуализировать наш текст, что создаст графическое изображение текста, в pygame называемое поверхностью. Мы предоставим текст, который хотим вывести, булево выражение, сообщающее, хотим ли мы сгладить его, и цвет в формате RGB.
Здесь (0, 0, 0) указывает, что мы хотим, чтобы текст был
черным. И наконец, используя blit(), мы нарисуем изображение текста в окне в некотором местоположении (х, у).
Этот код хорошо работает для отображения в окне предоставленного текста в заданном местоположении. Однако, если
текст не меняется, вы потратите много времени впустую, вновь
создавая textSurface при каждой итерации основного цикла.
Также необходимо помнить о множестве деталей, и вы должны
все их выполнить правильно, чтобы нарисовать текст должным
образом. Большую часть этих сложностей мы можем скрыть,
создав класс.

198 Часть II. Графические пользовательские интерфейсы с pygame

Создаем класс SimpleText
Идея заключается в том, чтобы создать набор методов, который позаботится о загрузке шрифта и отображении текста
в pygame, что обозначает, что нам больше не нужно запоминать
детали реализации. В листинге 6.6 содержится новый класс под
названием SimpleText, который выполняет эту работу.
Файл: PygameDemo8_SimpleTextDisplay/SimpleText.py
# Класс SimpleText
import pygame
from pygame.locals import *
class SimpleText():



def __init__(self, window, loc, value, textColor):
pygame.font.init()
self.window = window
self.loc = loc
self.font = pygame.font.SysFont(None, 30)
self.textColor = textColor
self.text = None # так что вызов setText ниже
# приведет к созданию изображения текста
self.setValue(value) # Настраиваем исходный текст для
# отображения





def setValue(self, newText):
if self.text == newText:
return # ничего не менять
self.text = newText # сохраняем новый текст
self.textSurface = self.font.render(self.text, True,
self.textColor)



def draw(self):
self.window.blit(self.textSurface, self.loc)
Листинг 6.6. Класс SimpleText для отображения текста

Вы можете воспринимать объект SimpleText как поле
в окне, где хотите, чтобы отображался текст. Вы можете его использовать для отображения неизменного текста метки или
для отображения текста, который меняется в процессе работы
программы.

Глава 6. Объектно- ориентированный pygame 199

У класса SimpleText есть лишь три метода. Метод
__init__()  ожидает окно, где можно рисовать, место в нем,
где должен быть текст, любой исходный текст, который вы хотите отобразить в поле, и цвет текста. Вызов pygame.font.
init() запускает систему шрифтов pygame. Вызов в первом
созданном экземпляре объекта SimpleText фактически выполняет инициализацию. Любые дополнительные объекты
SimpleText также будут осуществлять этот вызов, но, поскольку шрифты уже были инициализированы, он завершится моментально. Мы создадим новый объект Font с помощью
pygame.font.SysFont(). Указав None вместо конкретного
имени шрифта, будем использовать любой стандартный системный шрифт.
Метод setValue() визуализирует изображение текста, чтобы показать его, и сохраняет это изображение в переменной экземпляра self.textSurface . Во время выполнения программы каждый раз, когда хотите изменить показываемый текст, вы
вызываете метод setValue(), передавая новый текст для отображения. У метода setValue() есть также оптимизация: он
запоминает последний визуализированный текст и, прежде
чем сделать что-то еще, проверяет, является ли новый текст
тем же самым, что и предыдущий. Если текст не изменился, ничего не надо делать, и метод просто возвращает его. Если есть
новый текст, он визуализирует его на поверхность, чтобы нарисовать.
Метод draw() рисует в окне в заданном местоположении
изображение, содержащее переменную экземпляра self.
textSurface. Этот метод должен вызываться в каждом фрейме.
У данного подхода существует множество преимуществ.
• Класс скрывает все детали визуализации текста pygame, так
что пользователю этого класса нет необходимости знать,
какие специфичные для pygame вызовы необходимы для отображения текста.
• Каждый объект SimpleText запоминает окно, в котором он
рисует, местоположение, куда текст должен быть помещен,
и цвет текста. Следовательно, вам необходимо указать эти
значения лишь один раз, когда вы создаете экземпляр
SimpleText, обычно перед началом основного цикла.

200 Часть II. Графические пользовательские интерфейсы с pygame

• Каждый объект SimpleText также оптимизирован, чтобы
запомнить как текст, который ему было сказано нарисовать
в прошлый раз, так и изображение (self.textSurface),
созданное им из текущего текста. Ему всего лишь необходимо
визуализировать новую поверхность, когда текст изменяется.
• Чтобы отобразить несколько частей текста в окне, вам необходимо лишь создать экземпляры нескольких объектов
SimpleText. Это ключевое понятие объектноориентированного программирования.

Демоверсия Ball с SimpleText и SimpleButton
Теперь мы изменим листинг 6.2, чтобы использовать классы
SimpleText и SimpleButton. Обновленная программа
в листинге 6.7 отслеживает количество прохождений основного цикла и сообщает эту информацию в верхней части окна.
Щелчок по кнопке перезагрузки приводит к сбросу счетчика.
Файл: PygameDemo8_SimpleTextDisplay/Main_BallTextAndButton.py
# pygame демо 8 – SimpleText, SimpleButton и Ball
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import random
 from Ball import * # вводим код класса Ball
from SimpleText import *
from SimpleButton import *
#2 – Определяем константы
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.

Глава 6. Объектно- ориентированный pygame 201

#5 – Инициализируем переменные
 oBall = Ball(window, WINDOW_WIDTH, WINDOW_HEIGHT)
oFrameCountLabel = SimpleText(window, (60, 20),
'Program has run through this many loops: ', WHITE)
oFrameCountDisplay = SimpleText(window, (500, 20), '', WHITE)
oRestartButton = SimpleButton(window, (280, 60),
'images/restartUp.png', 'images/restartDown.png')
frameCounter = 0
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()





if oRestartButton.handleEvent(event):
frameCounter = 0 # кнопка нажата, сбрасываем счетчик
#8 – Выполняем действия "в рамках фрейма"
oBall.update() # обновляем ball
frameCounter = frameCounter + 1 # увеличиваем каждый фрейм
oFrameCountDisplay.setValue(str(frameCounter))
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(BLACK)



#10 – Рисуем все элементы окна
oBall.draw() # рисуем ball
oFrameCountLabel.draw()
oFrameCountDisplay.draw()
oRestartButton.draw()
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND)
Листинг 6.7. Пример основной программы, чтобы отобразить Ball, SimpleText
и SimpleButton

В верхней части программы мы импортируем код классов
Ball, SimpleText и SimpleButton . Перед началом основного

цикла создаем экземпляр Ball , два экземпляра класса
202 Часть II. Графические пользовательские интерфейсы с pygame

SimpleText (oFrameCountLabel для неизменной метки сообще-

ния и oFrameCountDisplay для изменяющихся отображений
фреймов) и экземпляр класса SimpleButton, который храним
в oRestartButton. Мы также инициализируем переменную
frameCounter в значении ноль, которую будем увеличивать
каждый раз во время прохождения основного цикла.
В основном цикле проверяем, нажал ли пользователь кнопку перезагрузки . Если True, сбрасываем счетчик фреймов.
Обновляем позицию мяча . Увеличиваем счетчик фреймов, затем вызываем метод setValue() текстового поля, чтобы
отобразить новое число фреймов . И наконец, рисуем мяч, вызывая метод draw() для каждого объекта .
При создании экземпляров объектов SimpleText последний
аргумент — цвет текста, и мы указали, что объекты должны визуализироваться в цвете WHITE, чтобы они были видны на фоне
BLACK. В следующей главе я покажу, как расширить класс
SimpleText, чтобы он включал больше атрибутов, не усложняя
при этом интерфейс класса. Мы создадим более полнофункциональный текстовый объект с подходящим количеством значений
по умолчанию для каждого из этих атрибутов, но при этом позволяющий переопределять их по умолчанию.

Сравнение интерфейса и реализации
Примеры SimpleButton и SimpleText поднимают важную
тему сравнения интерфейса и реализации. Как было упомянуто
в главе 4, интерфейс относится к тому, как что-то используется,
в то время как реализация относится к тому, как что-то работает (внутренне).
В среде ООП интерфейс представляет собой набор методов
в классе и связанных с ними параметров, также известный как
программный интерфейс приложения (API). Реализация — это
фактический код всех методов класса.
Внешний пакет, такой как pygame, скорее всего будет поставляться с документацией API, объясняющей доступные вызовы и аргументы, которые вы должны передавать при каждом
вызове. Полная документация pygame API доступна по адресу
https://www.pygame.org/docs/.
Когда пишете код, который вызывает pygame, вам не нужно
беспокоиться о реализации используемых вами методов.
Глава 6. Объектно- ориентированный pygame 203

Например, когда вызываете blit() для рисования изображения, вы на самом деле не в курсе, как blit() делает свою работу; вам лишь необходимо знать, что делает вызов и какие аргументы необходимо передать. С другой стороны, вы можете
быть уверены, что конструктор, который написал метод
blit(), тщательно продумал, как сделать работу blit() максимально эффективной.
В мире программирования мы часто примеряем на себя две
роли — как конструктора, так и разработчика приложения, поэтому нам необходимо приложить усилия для разработки таких
API, которые не только применимы в текущей ситуации,
но также окажутся достаточно общими, чтобы их могли использовать наши будущие программы и программы, написанные
другими людьми. Наши классы SimpleButton и SimpleText являются хорошими примерами, так как они написаны в таком
общем виде, что их можно с легкостью многократно применять.
Я расскажу подробнее о преимуществах интерфейса по сравнению с реализацией в главе 8, когда мы будем изучать инкапсуляцию.

Обратные вызовы
При использовании объекта SimpleButton мы управляем проверкой факта нажатия кнопки и реакцией на нее следующим
образом:
if oButton.handleEvent(event):
print('The button was clicked')

Этот подход к обработке событий хорошо работает с классом SimpleButton. Однако некоторые другие пакеты Python
и многие другие языки программирования обрабатывают события иным способом: с помощью обратного вызова.
Обратный вызов (callback)
Функция или метод объекта, который вызывается, когда происходит
конкретное действие, событие или условие.

Простым способом понять это будет вспомнить популярное
кино 1984 года «Охотники за привидениями». Слоган фильма —
«Кому вы собираетесь звонить?». В кинофильме «Охотники
204 Часть II. Графические пользовательские интерфейсы с pygame

за привидениями» запускали рекламу на телевидении, которая
сообщала людям, что если они увидели привидение (это событие, которое необходимо искать), то должны позвонить Охотникам за привидениями (обратный вызов), чтобы избавиться
от него. После получения вызова Охотники за привидениями
предпринимали соответствующие действия, чтобы избавиться
от привидения.
В качестве примера рассмотрим объект кнопки, который
инициализирован для обратного вызова. Когда пользователь
щелкает по кнопке, она вызывает функцию или метод обратного вызова. Функция или метод исполняются каждый раз, когда
код должен отреагировать на нажатие кнопки.
Создаем обратный вызов
Чтобы установить обратный вызов, когда создаете объект или
вызываете один из методов объекта, вы передаете имя функции или метода объекта для вызова. В качестве примера возьмем стандартный пакет GUI для Python под названием tkinter.
Код, необходимый для создания кнопки с помощью этого
пакета, сильно отличается от того, что я показывал; ниже представлен пример:
import tkinter
def myFunction():
print('myCallBackFunction was called')
oButton = tkinter.Button(text='Click me',
command=myFunction)

Когда создаете кнопку tkinter, вы должны передать функцию (или метод объекта), который будет вызван обратно, когда
пользователь щелкнет по ней. Здесь мы передаем myFunction
в качестве функции для обратного вызова. (Он использует параметры ключевых слов, которые мы более подробно обсудим
в главе 7.) Кнопка tkinter запоминает функцию в качестве обратного вызова, и, когда пользователь щелкает по итоговой
кнопке, он вызывает функцию myFunction().
Вы также можете использовать обратный вызов, когда инициируете действие, которое может занять некоторое время.
Вместо того чтобы ждать окончания действия, что приведет
Глава 6. Объектно- ориентированный pygame 205

к заморозке программы на некий период времени, вы предоставляете обратный вызов, который будет вызван по завершении действия. Например, представьте, что вы хотите сделать
запрос через сеть Интернет. Вместо того чтобы совершать вызов и ждать, когда он вернет данные, что может занять продолжительное время, лучше использовать пакеты, которые позволят вам осуществить вызов и установить обратный вызов.
Таким образом, программа может продолжить работу, а пользователь не окажется заблокирован. Это задействует многопоточный Python и выходит за рамки данной книги, но метод использования обратного вызова представляет собой общий способ
для подобных случаев.
Используем обратный вызов с SimpleButton
Для демонстрации этого понятия внесем незначительные изменения в класс SimpleButton, чтоб он мог принять обратный
вызов. В качестве дополнительного необязательного параметра
вызывающий может предоставить функцию или метод объекта
для обратного вызова, когда происходит нажатие объекта
SimpleButton. Каждый экземпляр SimpleButton запоминает
обратный вызов в переменной экземпляра. Когда пользователь
завершает щелчок, экземпляр SimpleButton вызывает обратный вызов.
Основная программа в листинге 6.8 создает три экземпляра
класса SimpleButton, каждый из которых обрабатывает нажатие кнопки по-разному. Первая кнопка oButtonA не обеспечивает
обратный вызов; oButtonB обеспечивает обратный вызов функции; а oButtonС определяет обратный вызов метода объекта.
Файл: PygameDemo9_SimpleButtonWithCallback/
Main_SimpleButtonCallback.py
# pygame демо 9 – 3-кнопочный тест с обратными вызовами
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
from SimpleButton import *
import sys
#2 – Определяем константы
GRAY = (200, 200, 200)

206 Часть II. Графические пользовательские интерфейсы с pygame

WINDOW_WIDTH = 400
WINDOW_HEIGHT = 100
FRAMES_PER_SECOND = 30
# Определяем функцию, которую необходимо использовать в качестве
# "обратного вызова"
def myCallBackFunction(): 
print('User pressed Button B, called myCallBackFunction')
# Определяем класс с методом, который необходимо использовать
# в качестве "обратного вызова"
class CallBackTest(): 
--- любые другие методы в этом классе--def myMethod(self):
print('User pressed ButtonC, called myMethod of the
CallBackTest object')
#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
#5 – Инициализируем переменные
oCallBackTest = CallBackTest() 
# создаем экземпляры SimpleButton
# Нет обратного вызова
oButtonA = SimpleButton(window, (25, 30), 
'images/buttonAUp.png',
'images/buttonADown.png')
# Указываем функцию для "обратного вызова"
oButtonB = SimpleButton(window, (150, 30),
'images/buttonBUp.png',
'images/buttonBDown.png',
callBack=myCallBackFunction)
# Указываем метод объекта для "обратного вызова"
oButtonC = SimpleButton(window, (275, 30),
'images/buttonCUp.png',
'images/buttonCDown.png',
callBack=oCallBackTest.myMethod)
counter = 0
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
Глава 6. Объектно- ориентированный pygame 207

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# передаем событие кнопке, смотрим, была ли она нажата
if oButtonA.handleEvent(event): 
print('User pressed button A, handled in the main loop')
# У oButtonB и oButtonС есть обратные вызовы,
# не нужно проверять результаты этих вызовов
oButtonB.handleEvent(event) 
oButtonC.handleEvent(event) 
#8 – Выполняем действия "в рамках фрейма"
counter = counter + 1
#9 – Очищаем окно, прежде чем рисовать его заново
window.fill(GRAY)
#10 – Рисуем все элементы окна
oButtonA.draw()
oButtonB.draw()
oButtonC.draw()
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) # ожидание pygame
Листинг 6.8. Версия основной программы, которая обрабатывает нажатия кнопок тремя
разными способами

Мы начинаем с примитивной функции
myCallBackFunction(), которая просто выводит сообщение

о том, что она была вызвана. Далее у нас есть класс
CallBackTest, содержащий метод myMethod(), который выводит собственное сообщение о том, что он был вызван. Создаем объект oCallBackTest из класса CallBackTest . Нам необходим этот объект, чтобы мы могли установить обратный
вызов oCallBack.myMethod().
Затем создаем три объекта SimpleButton, каждый из которых использует свой подход . У первого, oButtonA, нет обратного вызова. Второй, oButtonB, устанавливает обратный вызов
208 Часть II. Графические пользовательские интерфейсы с pygame

функции myCallBackFunction(). Третий, oButtonС, устанавливает обратный вызов oCallBack.myMethod().
В основном цикле мы проверяем, щелкнул ли пользовать
по какой-то из этих кнопок, с помощью вызова метода
handleEvent() для каждой кнопки. Поскольку у oButtonA нет
обратного вызова, мы должны проверить, равно ли возвращаемое значение True , и, если это так, осуществить действие.
При нажатии на кнопку oButtonB  будет вызвана функция
myCallBackFunction(), которая выведет свое сообщение. При
нажатии на кнопку oButtonС  будет вызван метод myMethod()
объекта oCallBackTest, который выведет собственное сообщение.
Некоторые программисты предпочитают использовать обратный вызов, потому что цель вызова устанавливается при создании объекта. Важно понимать этот метод, особенно если вы
используете пакет, требующий его применения. Однако во всем
своем демонстрационном коде я буду использовать изначальный подходпроверки возвращаемого значения с помощью вызова handleEvent().

Выводы
В этой главе я показал, как начать работу с процедурной программой и извлечь соответствующий код для создания класса.
Чтобы продемонстрировать это, мы создали класс Ball, затем
изменили основной код нашей демопрограммы из предыдущей
главы, чтобы вызвать методы класса и сообщить объекту Ball,
что делать, не заботясь о том, как он достигает результата.
Имея весь соответствующий код в отдельном классе, легко
создать список объектов и экземпляры и управлять необходимым количеством объектов.
Затем мы создали класс SimpleButton и класс SimpleText,
которые скрывают сложность внутри своей реализации и создают код, подходящий для многократного использования.
В следующей главе я создам на основании этих классов «профессиональные» кнопки и отображения текста.
И наконец, я представил понятие обратного вызова, в котором вы передаете функцию или метод для вызова в объект. Обратный вызов вызывается позднее, когда происходит событие
или завершается действие.

7
ВИД Ж Е ТЫ PYGA M E GU I

Pygame позволяет программистам брать тек-

стовый язык Python и использовать его для основанных на GUI программах. Окна, указывающие
устройства, щелчки, перетаскивания и звуки стали
стандартными составляющими в нашем опыте взаимодействия с компьютерами. С сожалению, пакет pygame не поставляется вместе со встроенными базовыми элементами пользовательского интерфейса, поэтому нам необходимо создавать
их самостоятельно. Делаем мы это с помощью pygwidgets, библиотеки виджетов GUI.
В этой главе объясняется, как стандартные виджеты, например изображения, кнопки и поля ввода или вывода, можно создать в качестве классов и как клиентский код использует их.
Создание каждого элемента в качестве класса позволяет включать несколько экземпляров каждого элемента при создании
GUI. Прежде чем мы начнем знакомство с виджетами GUI,
я все же сначала должен рассказать еще об одной функции
Python: передаче данных в вызов функции или метода.

Виджеты pygame GUI 211

Передаем аргументы функции или методу
Отношения между аргументами в вызове функции и определяемыми в ней параметрами один к одному, так что значение первого аргумента присваивается первому параметру, значение
следующего — второму и так далее.
Рис. 7.1, взятый из главы 3, показывает, что то же самое верно
при вызове метода объекта. Как мы можем видеть, первый параметр, который всегда self, устанавливается для объекта в вызове.
def someMethod(self, ):

oSomeObject.someMethod()
Рис. 7.1. Как аргументы, передаваемые в метод, сопоставляются со своими
параметрами

Тем не менее Python (и некоторые другие языки) позволяет сделать некоторые аргументы необязательными. Если необязательный аргумент не указан в вызове, мы можем вместо него предоставить значение по умолчанию для использования в функции или
методе. Я объясню с помощью аналогии из реального мира.
Если вы покупаете гамбургер в ресторане «Бургер Кинг»,
то получите его с кетчупом, горчицей и солеными огурчиками.
Но «Бургер Кинг» известен своим высказыванием: «Вы можете
сделать это по-своему». Если хотите какое-то другое сочетание
приправ, вы должны сообщить об этом, когда делаете заказ.
Мы начнем с написания функции orderBurgers(), имитирующей заказ бургера обычным способом, которым мы определяем функции без реализации значений по умолчанию:
def orderBurgers(nBurgers, ketchup, mustard, pickles):

Вы должны указать количество гамбургеров, которое хотите
заказать, но в идеале, если вы хотите использовать значения
по умолчанию True для добавления кетчупа, горчицы и соленых огурчиков, вам не нужно передавать больше никаких аргументов. Таким образом, чтобы заказать два гамбургера со стандартными дополнениями, ваш вызов может выглядеть
следующим образом:
orderBurgers(2) # с кетчупом, горчицей и солеными огурчиками

212 Часть II. Графические пользовательские интерфейсы с pygame

Однако в Python это приведет к ошибке, поскольку количество аргументов в вызове и количество указанных в функции
параметров не совпадает:
TypeError: orderBurgers() missing 3 required positional
arguments: 'ketchup', 'mustard', and 'pickles'

Давайте посмотрим, как Python дает нам возможность устанавливать необязательные параметры, которым можно присвоить значения по умолчанию, если ничего не указано.
Позиционные параметры и параметры ключевых слов
В Python есть два разных типа параметров: позиционные
и параметры ключевых слов. Позиционные параметры представляют собой уже знакомый нам тип, в котором у каждого аргумента в вызове есть сопоставимый параметр в определении
функции или метода.
Параметр ключевого слова позволяет указывать значение
по умолчанию. Вы пишете параметр ключевого слова в виде
имени переменной, знака равенства и значения по умолчанию
следующим образом:
def someFunction (=
):

У вас может быть несколько параметров ключевых слов,
каждый со своим именем и значением по умолчанию.
У функции или метода могут быть как позиционные параметры, так и параметры ключевых слов, в таком случае вы должны указать все позиционные параметры перед любыми параметрами ключевых слов:
def someOtherFunction(positionalParam1, positionalParam2, ...

=,
=, ...):

Давайте перепишем orderBurgers(), чтобы она использовала один позиционный параметр и три параметра ключевых
слов со значениями по умолчанию, следующим образом:
def orderBurgers(nBurgers, ketchup=True, mustard=True, pickles=True):

Глава 7. Виджеты pygame GUI 213

Когда мы вызываем эту функцию, nBurgers является позиционным параметром и, следовательно, должен указываться
в качестве аргумента при каждом вызове. Другие три — это параметры ключевых слов. Если никакие значения не будут переданы для ketchup, mustard и pickles, функция станет использовать значение по умолчанию True для каждой переменной
этих параметров. Теперь мы можем заказать два бургера со всеми приправами следующим образом:
orderBurgers(2)

Если мы хотим нечто отличающееся от значения по умолчанию, то можем указать в нашем вызове имя параметра ключевого слова и другое значение. Например, если хотим в наши два
бургера добавить лишь кетчуп, мы можем выполнить вызов следующим образом:
orderBurgers(2, mustard=False, pickles=False)

Когда функция выполняется, значения переменных mustard
и pickles установлены равными False. Поскольку мы не указали значение для ketchup, ему по умолчанию присваивается значение True.
Вы также можете выполнить вызов, указывая все аргументы
позиционно, включая те, что записаны в качестве параметров
ключевых слов. Python будет использовать порядок ваших аргументов, чтобы присвоить каждому параметру верное значение:
orderBurgers(2, True, False, False)

В этом вызове мы снова указываем два бургера с кетчупом
без горчицы и соленых огурчиков.
Дополнительные примечания к параметрам
ключевых слов
Давайте быстренько пробежимся по некоторым соглашениям
и советам для использования параметров ключевых слов.
Исходя из соглашения Python, когда вы используете параметры
ключевых слов и ключевые слова с аргументами, знак равенства между ключевым словом и значением не должен содержать
пробелов перед и после него, чтобы показать, что это
214 Часть II. Графические пользовательские интерфейсы с pygame

не является типичным оператором присваивания. Здесь
строки отформатированы правильно:
def orderBurgers(nBurgers, ketchup=True, mustard=True,
pickles=True):
orderBurgers(2, mustard=False)

Эти строки тоже будут хорошо работать, но они не следуют
соглашению форматирования и хуже читаются:
def orderBurgers(nBurgers, ketchup = True, mustard = True,
pickles = True):
orderBurgers(2, mustard = False)

При вызове функции, у которой есть и позиционные параметры, и параметры ключевых слов, вы сначала должны представить значения для всех позиционных параметров до любого
необязательного параметра ключевого слова.
Аргументы ключевых слов в вызовах могут указываться
в любом порядке. Вызовы для нашей функции orderBurgers()
можно осуществить различными способами, например таким:
orderBurgers(2, mustard=False, pickles=False) # только кетчуп

или:
orderBurgers(2, pickles=False, mustard=False, ketchup=False) # пустой

Всем параметрам ключевых слов будут присвоены соответствующие значения, вне зависимости от порядка аргументов.
Хотя все значения по умолчанию в примере orderBurgers()
были булевыми выражениями, у параметра ключевого слова может быть значение по умолчанию любого типа данных. Например, мы могли написать функцию, чтобы позволить покупателю заказать мороженое следующим образом:
def orderIceCream(flavor, nScoops=1, coneOrCup='cone', sprinkles=False):

Вызывающий должен указать вкус, но по умолчанию он получил один шарик в рожке без присыпки. Вызывающий может
переопределить эти значения по умолчанию другими значениями ключевых слов.
Глава 7. Виджеты pygame GUI 215

Используем None в качестве значения по умолчанию
Иногда полезно знать, передал ли вызывающий значение для
параметра ключевого слова. В этом примере вызывающий заказывает пиццу. Он должен как минимум указать размер. Вторым
параметром будет вид, который по умолчанию "regular",
но может быть и "deepdish". В качестве третьего параметра
вызывающий может дополнительно передать одну желаемую
начинку. Если он хочет начинку, нам следует взимать дополнительную плату.
В листинге 7.1 мы будем использовать позиционные параметры для size и параметры ключевых слов style и topping.
Значение по умолчанию для style — строка "regular". Поскольку выбор начинки необязателен, мы будем использовать
специальное значение Python None в качестве значения
по умолчанию, но вызывающий может передать начинку по своему выбору.
Файл: OrderPizzaWithNone.py
def orderPizza(size, style='regular', topping=None):
# Произвести некоторые расчеты на основании размера и вида
# проверяем, была ли указана начинка
PRICE_OF_TOPPING = 1.50 # цена на любую начинку
if size == 'small':
price = 10.00
elif size == 'medium':
price = 14.00
else: # большой
price = 18.00
if style == 'deepdish':
price = price + 2.00 # берем дополнительную плату за высокие
# бортики



line = 'You have ordered a ' + size + ' ' + style + ' pizza with '
if topping is None: # проверяем, была ли передана начинка
print(line + 'no topping')
else:
print(line + topping)
price = price + PRICE_OF_TOPPING
print('The price is $', price)
print()

216 Часть II. Графические пользовательские интерфейсы с pygame

# Вы можете заказать пиццу следующими способами:
 orderPizza('large') # большая, обычная по умолчанию, без начинки
orderPizza('large', style='regular') # то же, что и выше
 orderPizza('medium', style='deepdish', topping='mushrooms')
orderPizza('small', topping='mushrooms') # вид по умолчанию обычный
Листинг 7.1. Версия основной программы, которая обрабатывает нажатия кнопок тремя
разными способами

Первый и второй вызовы будут рассматриваться ка к одно
и то же со значением переменной topping, равной None .
В третьем и четвертом вызовах значения topping установлены
на "mushrooms" . Поскольку "mushrooms" — это не None,
в этих вызовах код будет добавлять дополнительную плату
за начинку пиццы .
Используя None в качестве значения по умолчанию для параметра ключевого слова, вы можете увидеть, предоставил ли вызывающий значение в вызов. Это очень тонкое использование
параметров ключевых слов, но оно будет очень полезно в предстоящем обсуждении.
Выбираем ключевые слова и значения по умолчанию
Использование значений по умолчанию упрощает вызовы
функций и методов, но есть и обратная сторона. Ваш выбор
каждого ключевого слова для его параметра очень важен. Как
только программисты начинают выполнять вызовы, которые
переопределяют значения по умолчанию, становится очень
сложно изменить имя параметра ключевого слова, потому что
оно должно быть изменено во всех вызовах функции или метода
в жесткой конфигурации. В противном случае работающий код
сломается. Для более широко распределенного кода это потенциально может привести к значительным неудобствам для
использующих его программистов. Итог: не меняйте имя параметра ключевого слова без острой необходимости. То есть
выбирайте с умом!
Также очень важно использовать значения по умолчанию,
которые подойдут широкому диапазону пользователей. (Лично
я ненавижу горчицу! Каждый раз, когда я иду в «Бургер Кинг»,
мне приходится помнить, что я должен уточнить, чтобы
Глава 7. Виджеты pygame GUI 217

не клали горчицу, или я получу то, что считаю несъедобным
бургером. На мой взгляд, они сделали неправильный выбор
по умолчанию.)
Значения по умолчанию в виджетах GUI
В следующем разделе я представлю набор классов, которые вы
можете использовать, чтобы легко создавать элементы GUI
в pygame, такие как кнопки и текстовые поля. Эти классы инициализируются с помощью нескольких позиционных параметров, но в них также будут соответствующие необязательные
параметры ключевых слов, все с разумными значениями
по умолчанию, чтобы позволить программистам создавать
виджеты GUI, указывая лишь несколько позиционных аргументов. Более точное управление можно получить за счет указания
значений для перезаписи значений по умолчанию параметров
ключевых слов.
В качестве углубленного примера мы рассмотрим виджет
для отображения текста в окне приложения. Он может отображаться различными шрифтами, размерами шрифтов, цветами,
фоновыми цветами и так далее. Создадим класс DisplayText,
у которого будут значения по умолчанию для всех этих атрибутов, но который предоставит коду клиента возможность указывать разные значения.

Пакет pygwidgets
В оставшейся части главы мы сосредоточимся на пакете
pygwidgets, который был написан с двумя целями.
1. Чтобы продемонстрировать множество объектноориентированных методов программирования.
2. Чтобы позволить программистам с легкостью создавать
и использовать виджеты GUI в программах pygame.
Пакет pygwidgets содержит следующие классы.
TextButton

Кнопка, созданная со стандартным оформлением с помощью текстовой строки.
CustomButton

Кнопка с пользовательской графикой.
218 Часть II. Графические пользовательские интерфейсы с pygame

TextCheckBox

Флажок со стандартным оформлением, созданный из текстовой строки.
CustomCheckBox

Флажок с пользовательской графикой.
TextRadioButton

Переключатели со стандартным оформлением, созданные
из текстовой строки.
CustomRadioButton

Переключатели с пользовательской графикой.
DisplayText

Поле, использованное для отображения выводимого текста.
InputText

Поле, где пользователь может вводить текст.
Dragger

Позволяет пользователю перетаскивать изображение.
Image

Отображает изображение в указанном месте.
ImageCollection

Отображает набор изображений в указанном месте.
Animation

Отображает последовательность изображений.
SpriteSheetAnimation

Отображает последовательность изображений из одного более крупного изображения.
Установка
Чтобы установить pygwidgets, откройте командную строку
и введите следующее:
python3 -m pip install -U pip --user
python3 -m pip install -U pygwidgets --user

Эти команды загружают и устанавливают последнюю версию pygwidgets из каталога пакетов Python (PyPI). Она помещается в папку (с именем site-packages), которая доступна всем
вашим программам Python. Как только пакет был установлен,
вы можете использовать pygwidgets, включая следующего оператора в начало вашей программы:
import pygwidgets
Глава 7. Виджеты pygame GUI 219

Это импортирует весь пакет. После завершения импорта вы
можете создать экземпляры объектов из их классов и вызвать
методы этих объектов.
Самая актуальная документация pygwidgets находится
по адресу https://pygwidgets.readthedocs.io/en/latest/. Если
хотите просмотреть исходный код пакета, он доступен в репозитории GitHub по адресу https://github.com/IrvKalb/
pygwidgets/.
Общий подход к разработке
Как показано в главе 5, одно из первых действий, которые вы
выполняете в каждой программе pygame, — определение окна
приложения. Следующие строки создают окно приложения
и сохраняют ссылки на него в переменной с именем window:
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

Как вскоре увидим, каждый раз, когда мы создаем экземпляр
любого виджета, нам будет необходимо передавать переменную
window, чтобы виджет мог нарисовать себя в окне приложения.
Большинство виджетов в pygwidgets работают похожим образом, обычно включая следующие три шага.
1. Перед началом основного цикла while создайте экземпляр
виджета с некоторыми аргументами инициализации.
2. В основном цикле каждый раз, когда происходит какое-либо
событие, вызывайте метод виджета handleEvent() (передавая
в объект события).
3. В нижней части основного цикла вызовите метод виджета draw().
Шаг 1 при использовании любого виджета заключается в создании его экземпляра с помощью строки, подобной этой:
oWidget = pygwidgets.(window, loc,

)

Первым аргументом всегда является окно приложения. Второй аргумент — это всегда координаты в окне для отображения
виджета в виде кортежа: (x, y).
Шаг 2 заключается в обработке любого события, которое
могло повлиять на виджет, и вызове метода объекта
handleEvent() внутри каждого цикла. Если происходит
220 Часть II. Графические пользовательские интерфейсы с pygame

какое-либо событие (например, щелчок мышью или нажатие
кнопки) и виджет обрабатывает событие, этот вызов вернет
значение True. Код в верхней части основного цикла while
обычно выглядит следующим образом:
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if oWidget.handleEvent(event):
# пользователь сделал что-то с oWidget, на что
# мы должны отреагировать; добавляем здесь код

Шаг 3 состоит в добавлении строки в нижней части цикла
while для вызова метода виджета draw(), чтобы он появился
в окне:
oWidget.draw()

Поскольку в шаге 1 мы указали окно для рисования, местоположение и все детали, которые влияют на появление виджета, нам не нужно ничего передавать в вызов draw().
Добавляем изображение
Первым примером будет простейший виджет: мы будем использовать класс Image, чтобы отобразить изображение в окне.
Когда вы создаете экземпляр объекта Image, единственными
необходимыми аргументами являются окно, местоположение
в окне для рисования изображения и путь к файлу изображения. Создайте объект Image перед началом основного цикла
следующим образом:
oImage = pygwidgets.Image(window, (100, 200), 'images/SomeImage.png')

Используемый здесь путь предполагает, что папка проекта,
содержащая основную программу, также содержит папк у с именем images, внутри которой находится файл SomeImage.png. Затем в основном цикле вам лишь необходимо вызвать метод объекта draw():
oImage.draw()
Глава 7. Виджеты pygame GUI 221

Метод draw() класса Image содержит вызов blit() для фактического рисования изображения, поэтому вам никогда не нужно
напрямую вызывать blit(). Чтобы переместить изображение,
вы можете вызвать метод setLoc() (сокращение от «установка
местоположения»), указывая координаты х и у в виде кортежа:
oImage.setLoc((newX, newY))

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

МОДУЛЬ СПРАЙТ

В pygame есть встроенный модуль sprite для отображения
изображений в окне. Они называются спрайтами. Модуль
спрайт предоставляет класс Sprite для обработки отдельных спрайтов и класс Group для обработки нескольких объектов Sprite. Вместе эти классы обеспечивают
великолепные функциональные возможности, и, если вы намереваетесь программировать pygame в тяжелом режиме,
возможно, стоит потратить время на знакомство с ними. Тем
не менее, чтобы объяснить основополагающие понятия ООП,
я решил не использовать эти классы. Взамен я перейду к общим элементам GUI, чтобы их можно было применять в любой среде и на любом языке. Если вы хотите больше узнать
о модуле sprite, ознакомьтесь с руководством по адресу
https://www.pygame.org/docs/tut/SpriteIntro.html.

Добавляем кнопки, флажки и переключатели
Когда создаете экземпляр кнопки, флажка или переключателя
виджета в pygwidgets, у вас есть два варианта: создать экземпляр текстовой версии, которая рисует собственную графику
и добавляет текстовую метку на основе переданной вами
строки, или создать экземпляр пользовательской версии,
в которой вы предоставляете изображение. В табл. 7.1 продемонстрированы различные доступные классы кнопок.
222 Часть II. Графические пользовательские интерфейсы с pygame

Таблица 7.1. Текстовые и пользовательские классы кнопок в pygwidgets
Текстовая версия (создает
графику по ходу действия)

Пользовательская версия
(использует вашу графику)

Кнопка

TextButton

CustomButton

Флажок

TextCheckBox

CustomCheckBox

Переключатель

TextRadioButton

CustomRadioButton

Разница между текстовой и пользовательской версиями
этих классов имеет значение только во время создания экземпляра. Как только вы создали объект из текстового или пользовательского класса кнопки, все остальные методы пары классов
идентичны. Чтобы прояснить это, давайте рассмотрим классы
TextButton и CustomButton.
TextButton
Ниже представлено фактическое определение метода
__init__() класса TextButton в pygwidgets:
def __init__(self, window, loc, text,
width=None,
height=40,
textColor=PYGWIDGETS_BLACK,
upColor=PYGWIDGETS_NORMAL_GRAY,
overColor=PYGWIDGETS_OVER_GRAY,
downColor=PYGWIDGETS_DOWN_GRAY,
fontName=DEFAULT_FONT_NAME,
fontSize=DEFAULT_FONT_SIZE,
soundOnClick=None,
enterToActivate=False,
callback=None
nickname=None):

Однако вместо того, чтобы читать код класса, программист
скорее всего обратится к его документации. Как упоминалось
ранее, полная документация pygwidgets находится по адресу
https://pygwidgets.readthedocs.io/en/latest/.
Также вы можете посмотреть документацию класса, вызвав
встроенную функцию help() в оболочке Python следующим образом:
>>> help(pygwidgets.TextButton)

Глава 7. Виджеты pygame GUI 223

Когда создаете экземпляр TextButton, вам необходимо
лишь передать окно, местоположение в окне и текст для отображения на кнопке. Если укажете только эти позиционные параметры, ваша кнопка будет использовать разумные значения
по умолчанию для ширины и высоты, фоновых цветов для четырех состояний кнопки (различные оттенки серого), типа
и размера шрифта. По умолчанию, когда пользователь щелкает
по кнопке, никакие звуковые эффекты не воспроизводятся.
Код для создания TextButton с помощью этих значений
по умолчанию выглядит следующим образом:
oButton = pygwidgets.TextButton(window, (50, 50), 'Text Button')

Код в методе __init__() класса TextButton использует методы рисования pygame, чтобы сконструировать собственную
графику для всех четырех состояний (вверх, вниз, мышь наведена и недоступна для нажатия). Следующая строка создает версию кнопки «вверх», которая выглядит как на рис. 7.2.

Рис. 7.2. Кнопка TextButton,
использующая значения по умолчанию

Вы можете переопределить любой или все параметры
по умолчанию с помощью значений ключевых слов следующим
образом:
oButton = pygwidgets.TextButton(window, (50, 50),
'Text Button',
width=200,
height=30,
textColor=(255, 255, 128),
upColor=(128, 0, 0),
fontName='Courier',
fontSize=14,
soundOnClick='sounds/blip.wav',
enterToActivate=True)

Эта установка создаст кнопку, которая будет выглядеть как
на рис. 7.3.

Рис. 7.3. Кнопка TextButton, использующая аргументы ключевых слов для
шрифта, размера, цвета и так далее

224 Часть II. Графические пользовательские интерфейсы с pygame

Поведение переключения изображения этих двух кнопок работает абсолютно так же; единственное отличие заключается
во внешнем виде изображений.
CustomButton
Класс CustomButton позволяет вам использовать собственную
графику для кнопки. Чтобы создать экземпляр CustomButton,
вам необходимо лишь передать окно, координаты и путь к изображению состояния «вверх» кнопки. Ниже приведен пример:
restartButton = pygwidgets.CustomButton(window, (100, 430),
'images/RestartButtonUp.png')

Состояния down, over и disabled — необязательные аргументы ключевых слов, и, если для любого из них не будет передано значение, CustomButton будет использовать копию изображения up. Более типично (и настоятельно рекомендуется)
передавать путь к дополнительным изображениям следующим
образом:
restartButton = pygwidgets.CustomButton(window, (100, 430),
'images/RestartButtonUp.png',
down='images/RestartButtonDown.png',
over='images/RestartButtonOver.png',
disabled='images/RestartButtonDisabled.png',
soundOnClick='sounds/blip.wav',
nickname='restart')

Здесь мы также указали звуковой эффект, который должен
воспроизводиться, когда пользователь щелкает по кнопке, и мы
предоставили внутренний псевдоним, который мы сможем использовать позднее.
Используем кнопки
После создания экземпляра вы можете применить некоторый
типичный код, чтобы использовать объект кнопки oButton
независимо от того, является ли он TextButton или
CustomButton:
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
Глава 7. Виджеты pygame GUI 225

sys.exit()
if oButton.handleEvent(event):
# Пользователь щелкнул по кнопке


--- пропуск --oButton.draw() # внизу цикла while сказать ему нарисовать

Каждый раз, когда обнаруживаем событие, нам необходимо
вызвать метод кнопки handleEvent(), чтобы разрешить ей отреагировать на действия пользователя. Обычно этот вызов возвращает False, но вернет True, когда пользователь завершит
щелчок по кнопке. В нижней части основного цикла while нам
необходимо вызвать метод кнопки draw(), чтобы разрешить ей
нарисовать себя.
Вывод и ввод текста
Как мы видели в главе 6, обработка входных и выходных данных текста в pygame достаточно сложна, но я здесь познакомлю
вас с новыми классами поля отображения текста и поля ввода
текста. У них обоих минимальное количество необходимых
(позиционных) параметров и разумные значения по умолчанию для других атрибутов (шрифт, размер шрифта, цвет и так
далее), которые легко переопределяются.
Вывод текста
Пакет pygwidgets содержит класс DisplayText для отображения текста, который является более полнофункциональной
версией класса SimpleText из главы 6. Когда вы создаете экземпляр поля DisplayText, единственные обязательные аргументы — окно и местоположение. Первый параметр ключевого
слова — value, который можно указать с помощью строки
в качестве начального текста для отображения в поле. Обычно
это используется для значения по умолчанию конечного пользователя или для никогда не меняющегося текста, такого как
метка или инструкции. Поскольку value — первый параметр
ключевого слова, он может быть задан и как позиционный аргумент, и как аргумент ключевого слова. Например, вот это:
oTextField = pygwidgets.DisplayText(window, (10, 400), 'Hello World')

будет работать так же, как и вот это:
226 Часть II. Графические пользовательские интерфейсы с pygame

oTextField = pygwidgets.DisplayText(window, (10, 400),
value='Hello World')

Вы также можете настроить вид выводимого текста, указывая любой или все необязательные параметры ключевых слов.
Например:
oTextField = pygwidgets.DisplayText(window, (10, 400),
value='Some title text',
fontName='Courier',
fontSize=40,
width=150,
justified='center',
textColor=(255, 255, 0))

У класса DisplayText есть определенное количество дополнительных методов. Наиболее важный из них — setValue(),
который вы вызываете для изменения нарисованного в поле
текста:
oTextField.setValue('Any new text you want to see')

В нижней части основного цикла while вам необходимо вызвать метод объекта draw():
oTextField.draw()

И, конечно же, вы можете создать столько объектов
DisplayText, сколько вам необходимо.
Ввод текста
В типичной текстовой программе Python, чтобы получить входные данные от пользователя, вы вызвали бы функцию input(),
которая останавливает программу, пока пользователь вводит
текст в окне оболочки. Но в мире управляемых событиями GUIпрограмм основной цикл не останавливается никогда. Следовательно, мы должны использовать иной подход.
Для вводимого пользователем текста GUI-программа обычно представляет поле, в котором пользователь может печатать.
Поле ввода должно иметь дело со всеми клавишами клавиатуры, некоторые из них отображаются, в то время как другие используются для редактирования или передвижений курсора
внутри поля. Оно также должно позволять пользователю,
Глава 7. Виджеты pygame GUI 227

удерживающему клавишу, повторить ее. Класс pygwidgets
InputText обеспечивает все эти функциональные возможности.
Единственными обязательными аргументами для создания
экземпляра объекта InputText являются окно и местоположение:
oInputField = pygwidgets.InputText(window, (10, 100))

Однако вы можете настроить атрибуты текста объекта
InputText, указав необязательные параметры ключевых слов:
oInputField = pygwidgets.InputText(window, (10, 400),
value='Starting Text',
fontName='Helvetica',
fontSize=40,
width=150,
textColor=(255, 255, 0))

После создания экземпляра поля InputText типичный код
основного цикла будет выглядеть следующим образом:
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if oInputField.handleEvent(event):
# Пользователь нажал Enter или Return
userText = oInputField.getValue() # получаем
# введенный пользователем текст

--- пропуск --oInputField.draw() # внизу основного цикла while

Для каждого события нам необходимо вызвать метод
handleEvent() поля InputText, чтобы позволить ему реаги-

ровать на нажатия клавиш и щелчки мыши. Обычно этот вызов
возвращает значение False, но, когда пользователь нажимает
клавишу Enter или Return, он возвращает True. Затем мы можем извлечь текст, который пользователь ввел, вызвав метод
объекта getValue().
В нижней части основного цикла while нам необходимо вызвать метод draw(), чтобы разрешить полю нарисовать себя.
228 Часть II. Графические пользовательские интерфейсы с pygame

Если окно содержит несколько полей ввода, нажатия клавиш обрабатываются полем с текущим фокусом клавиатуры, которое изменяется, когда пользователь щелкает по другому
полю. Если вы хотите разрешить полю иметь начальный фокус
клавиатуры, то можете установить параметр ключевого слова
initialFocus в значение True в выбранном вами объекте
InputText, когда создаете его. Далее, если у вас в окне есть несколько полей InputText, типичный подход к дизайну интерфейса пользователя предполагает наличие кнопки OK или
Submit. Когда щелкаете по ней, вы можете затем вызвать метод
getValue() для каждого поля.

ПРИМЕЧАНИЕ

На момент написания книги класс InputText не обеспечивал
выделение нескольких символов с помощью перетаскивания
мыши. Если эта функциональная возможность была добавлена
в более поздней версии, для программ, использующих
InputText, не потребуется никаких изменений, поскольку код
будет полностью находиться внутри этого класса. Любое новое
поведение станет автоматически поддерживаться во всех объектах InputText.

Другие классы pygwidgets
Как вы видели в начале этого раздела, pygwidgets содержит
некоторое количество других классов.
Класс ImageCollection позволяет показывать любое отдельное изображение из набора. Например, предположим,
у вас есть изображение персонажа, стоящего лицом, спиной
и повернутым влево и вправо. Чтобы представить все потенциальные изображения, вы можете создать словарь следующим
образом:
imageDict = {'front':'images/front.png', 'left':'images/left.png',
'back':'images/back.png', 'right':'images/right.png'}

Затем создайте объект ImageCollection, указывая этот словарь и ключ изображения, с которого хотите начать. Чтобы перейти к другому изображению, вы вызываете метод replace()
и передаете другой ключ. Вызов метода draw() в нижней части
цикла всегда показывает текущее изображение.
Глава 7. Виджеты pygame GUI 229

Класс Dragger показывает одно изображение, но позволяет
пользователю перетаскивать его в любое место в окне. Вы должны вызвать его метод handleEvent() в цикле события. Когда
пользователь завершит перетаскивание, handleEvent() вернет значение True и вы сможете вызвать метод getMouseUpLoc()
объекта Dragger, чтобы получить местоположение, в котором
пользователь отпустил кнопку мыши.
Классы Animation и SpriteSheetAnimation обрабатывают
создание и отображение анимации. Оба они требуют набора
изображений для итерации. Класс Animation получает картинки из отдельных файлов, в то время как классу
SpriteSheetAnimation требуется одно изображение с равномерно расположенными внутренними картинками. Мы более
подробно изучим эти классы в главе 14.

Рис. 7.4. Окно программы, показывающее объекты, для которых были
созданы экземпляры из различных классов в pygwidgets

230 Часть II. Графические пользовательские интерфейсы с pygame

pygwidgets в примере программы
На рис. 7.4 показан скриншот примера программы, демонстрирующей объекты, для которых были созданы экземпляры
из множества классов в pygwidgets, включая Image,
DisplayText, InputText, TextButton, CustomButton,
TextRadioButton, CustomRadioButton, TextCheckBox,
CustomCheckBox, ImageCollection и Dragger.
Исходный код этого примера программы можно найти
в папке pygwidgets_test в репозитории GitHub по адресу
https://github.com/IrvKalb/pygwidgets/.

Важность последовательного API
Одно последнее замечание по поводу создания API для набора
классов: когда это возможно, прекрасной идей будет обеспечение согласованности параметров методов в различных,
но похожих классах. Хорошими примерами являются первые
два параметра метода __init__() каждого класса
в pygwidgets — window и loc, именно в этом порядке. Если
в некоторых вызовах порядок будет иной, может оказаться
сложнее использовать пакет как единое целое.
Кроме того, если различные классы реализуют одни и те же
функциональные возможности, стоит использовать одинаковые имена методов. Например, у многих классов в pygwidgets
есть метод с именем setValue() и другой с именем
getValue(). В следующих двух главах я подробнее расскажу, почему этот тип согласованности так важен.

Выводы
Эта глава познакомила вас с объектно- ориентированным пакетом pygwidgets виджетов графического интерфейса пользователя. Мы начали с обсуждения значений по умолчанию для
параметров в методах, и я объяснил, что параметры ключевых
слов позволяют использовать значения по умолчанию, если
в вызове не было указано соответствующее значение аргумента.
Затем я познакомил вас с модулем pygwidgets, содержащим
некоторое количество предустановленных классов виджетов
GUI, и показал вам, как использовать какие-то из них.
Глава 7. Виджеты pygame GUI 231

И наконец, я показал программу, предоставляющую примеры
большинства этих виджетов.
Существует два ключевых преимущества при написании подобных классов в pygwidgets. Во-первых, классы могут скрыть
сложность методов. Как только вы заставите ваш к ласс работать правильно, вам никогда не придется снова беспокоиться
о внутренних деталях. Во-вторых, вы можете повторно использовать код, создавая столько экземпляров класса, сколько вам
необходимо. Ваш класс может обеспечивать основные функциональные возможности, включая параметры ключевых слов
с тщательно подобранными значениями по умолчанию. Однако
значения по умолчанию можно легко переопределить, чтобы
обеспечить возможность настройки.
Вы можете опубликовать интерфейсы ваших классов для
других программистов (и себя), чтобы использовать их в различных проектах. Хорошая документация и совместимость
идут рука об руку, чтобы сделать эти типы классов очень удобными для использования.

ЧАСТЬ III
ИНК А ПС УЛЯЦ ИЯ,
ПОЛИМОРФИЗМ
И Н АС ЛЕ ДОВА НИЕ
Три основных принципа объектно- ориентированного программирования — инкапсуляция, полиморфизм и наследование.
Следующие три главы объяснят каждое из этих понятий по очереди, описывая основополагающие концепции и демонстрируя
примеры их реализации в Python. Чтобы язык программирования мог называть себя языком ООП, он должен поддерживать
все три требования.
В главе 8 объясняется инкапсуляция: сокрытие деталей
и размещение всего в одном месте.
В главе 9 обсуждается полиморфизм: как у нескольких классов могут быть методы с одинаковыми именами.
Глава 10 охватывает наследование: создание на основании
уже существующего кода.
И наконец, глава 11 подробно рассматривает несколько тем
(в основном связанных с управлением памятью), которые логически не вписываются в предыдущие три главы, но полезны
и важны для ООП.

8
ИНК А ПС УЛЯЦ ИЯ

Первый из трех основных принципов объектно- ориентированного программирования —инкапсуляция. Это слово может вызвать в воображении образ космической капсулы, клеточной
оболочки или медицинской гелевой капсулы, в которых находящийся внутри ценный груз защищен от воздействий
внешней среды. В программировании инкапсуляция имеет схожее, но даже более детализированное значение — сокрытие внутренних деталей состояния и поведения от внешнего кода и нахождение кода в одном месте.
В этой главе мы посмотрим, как работает инкапсуляция
с помощью функций и с помощью методов объектов. Я рассмотрю различные интерпретации инкапсуляции: использование
прямого доступа по сравнению с применением геттеров и сеттеров. Я покажу, как Python позволяет помечать переменную
экземпляра как закрытую, указывая, что она не должна быть
доступна коду, внешнему по отношению к классу, и коснусь особенности Python под названием декоратор. И наконец, я рассмотрю понятие абстракции при проектировании классов.

Инкапсуляция 235

Инкапсуляция с помощью функций
Функции — яркий пример инкапсуляции, потому что, когда вы
вызываете функцию, вас обычно не волнует, как она работает
внутри. Грамотно написанная функция содержит набор шагов,
которые составляют одну более крупную задачу, которая вам
и важна. Имя функции должно описывать действие, реализующее ее код. Возьмем встроенную функцию len()из стандартной библиотеки Python, которая используется для поиска количества символов в строке или элементов в списке. Вы передаете
строку или список, а она возвращает число. Когда пишете код,
который вызывает эту функцию, вас не волнует, как len()
выполняет свою работу. Вы не задумываетесь о том, содержит
код функции две или две тысячи строк, использует ли он одну
локальную переменную или сотню. Вам лишь необходимо
знать, какой аргумент передать и как использовать возвращенный результат.
То же самое верно и для написанных вами функций, как, например, эта, вычисляющая и возвращающая среднее значение
списка чисел:
def calculateAverage(numbersList):
total = 0.0
for number in numbersList:
total = total + number
nElements = len(numbersList)
average = total / nElements
return average

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

236 Часть III. Инкапсуляция, полиморфизм и наследование

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

Клиент
Любое программное обеспечение, которое создает объект из класса
и вызывает методы этого объекта.

Мы должны также учитывать, что существует разница в подходе внутри и снаружи объекта или класса. Когда вы работаете
внутри класса (пишете код методов в классе), необходимо позаботиться о том, как различные его методы совместно используют переменные экземпляра. Вы учитываете эффективность ваших алгоритмов. Думаете о том, как должен выглядеть
интерфейс: какие методы следует предоставить, каковы параметры для каждого из них и что должно быть использовано
в качестве значений по умолчанию. Если коротко, вы обязаны
позаботиться о проектировании и реализации методов.
С внешней стороны, как программист клиента, вы должны
знать интерфейс класса. Вы заботитесь о том, что делают методы класса, какие аргументы должны быть переданы и какие
данные передаются обратно от каждого метода.
Таким образом, класс обеспечивает инкапсуляцию за счет:
• сокрытия деталей реализации в своих методах и переменных
экземпляра;
• обеспечения всех функциональных возможностей, необходимых клиенту от объекта через его интерфейс (методы, определенные в классе).
Объекты владеют своими данными
В объектно- ориентированном программировании данные внутри объекта принадлежат ему. Программисты ООП обычно
согласны, что хороший принцип проектирования состоит
в том, чтобы клиентский код касался лишь интерфейса класса
Глава 8. Инкапсуляция 237

и не заботился о реализации методов. В листинге 8.1 рассмотрен пример простого класса Person.
class Person():
def __init__(self, name, salary):
self.name = name
self.salary = salary
Листинг 8.1. Владение данными в классе Person

Значения переменных экземпляра self.name и self.
salary устанавливаются каждый раз, когда мы создаем экземпляры новых объектов Person, следующим образом:
oPerson1 = Person('Joe Schmoe', 90000)
oPerson2 = Person('Jane Smith', 99000)

Каждый объект Person владеет собственным набором двух
переменных экземпляра.

Интерпретации инкапсуляции
Именно здесь возникают небольшие противоречия. Мнения
программистов по поводу доступности переменной экземпляра могут отличаться. Python представляет свободную
интерпретацию инкапсуляции, разрешая прямой доступ
к переменным экземпляра с использованием синтаксиса простых точек. Клиентский код способен получить законный
доступ к переменной экземпляра объекта по имени, используя
синтаксис ..
Тем не менее строгая интерпретация инкапсуляции говорит, что клиентское программное обеспечение никогда не должно извлекать или изменять значение переменной экземпляра напрямую. Взамен единственный способ, которым клиент может
извлечь содержащееся в объекте значение, заключается в использовании метода, предоставленного для этой цели классом.
Давайте рассмотрим оба подхода.
Прямой доступ и почему его следует избегать
Как уже упоминалось, Python разрешает прямой доступ к переменным экземпляра. В листинге 8.2 создаются экземпляры
тех же двух объектов из класса Person листинга 8.1, что
238 Часть III. Инкапсуляция, полиморфизм и наследование

и в предыдущем разделе, но затем осуществляется прямой
доступ к их переменным экземпляра self.salary.
Файл: PersonGettersSettersAndDirectAccess/
Main_PersonDirectAccess.py
# Пример основной программы Person с использованием прямого доступа
from Person import *
oPerson1 = Person('Joe Schmoe', 90000)
oPerson2 = Person('Jane Smith', 99000)
# получаем значения переменных зарплаты
 print(oPerson1.salary)
print(oPerson2.salary)
# меняем переменную зарплаты
 oPerson1.salary = 100000
oPerson2.salary = 111111
# обновляем зарплаты и выводим снова
print(oPerson1.salary)
print(oPerson2.salary)
Листинг 8.2. Пример основного кода, использующий прямой доступ к переменной
экземпляра

Python разрешает писать подобный код, который обращается к объектам, чтобы напрямую получить  и установить  любую переменную экземпляра с помощью стандартного синтаксиса точки. Большинство программистов Python считают эту
технику абсолютно приемлемой. Гвидо ван Россум (создатель
Python) однажды высказался относительно этого вопроса: «Мы
все здесь взрослые люди», — имея в виду, что программисты
должны знать, что они делают и чем рискуют, когда пытаются
напрямую получить доступ к переменным экземпляра.
Тем не менее я твердо убежден, что прямой доступ к переменной экземпляра объекта представляет собой невероятно
опаснуюпрактику, поскольку нарушает основную идею инкапсуляции. Чтобы проиллюстрировать этот случай, давайте рассмотрим несколько примеров сценариев, в которых прямой доступ может вызвать проблемы.

Глава 8. Инкапсуляция 239

Меняем имя переменной экземпляра
Первая проблема прямого доступа состоит в том, что изменение имени переменной экземпляра разрушит любой клиентский код, который напрямую использует исходное имя. Это
может произойти, когда разработчик класса решает, что первоначальный выбор имени переменной не был оптимальным
по следующим причинам.
• Имя недостаточно четко описывает данные, которые оно
представляет.
• Переменная является булевым выражением, и он хочет поменять местами значения True и False, переименовав переменную (например, closed в open, allowed в disallowed,
active в disabled).
• В исходном имени была ошибка в написании или заглавной
букве.
• Переменная была изначально булевым выражением, но позднее он осознал, что ему нужно представить более двух
значений.
В любом из этих случаев, если разработчик меняет имя переменной экземпляра в классе с self.
на self., клиентское программное обеспечение,
которое использует исходное имя напрямую, выйдет из строя.
Изменение переменной экземпляра на вычисление
Вторая ситуация, когда прямой доступ представляет проблему, — если коду класса необходимо измениться, чтобы соответствовать новым требованиям. Предположим, что при написании класса вы используете переменную экземпляра, чтобы
представить фрагмент данных, но функциональные возможности меняются таким образом, что вместо этого вам требуется
алгоритм для вычисления значения. Возьмем в качестве примера наш класс Account из главы 4. Чтобы сделать банковские
счета более реалистичными, мы могли бы добавить процентную ставку. Вы можете подумать, что это простой вопрос добавления переменной экземпляра для процентной ставки с именем
self.interestRate. Затем, используя прямой доступ, клиентское программное обеспечение может получить доступ к этому
значению объекта Account с помощью:
240 Часть III. Инкапсуляция, полиморфизм и наследование

oAccount.interestRate

Это сработает, но лишь на какое-то время. Позднее банк может принять новую политику: допустим, процентная ставка будет зависеть от количества денег на счету. Процентную ставку
можно вычислить следующим образом:
def calculateInterestRate(self):
# Предполагается, что self.balance был установлен
# в другом методе
if self.balance < 1000:
self.interestRate = 1.0
elif self.balance < 5000:
self.interestRate = 1.5
else:
self.interestRate = 2.0

Вместо того чтобы просто полагаться на единственное значение процентной ставки в self.interestRate, метод
calculateInterestRate() определяет текущую ставку на основании баланса счета.
Любое клиентское программное обеспечение, которое напрямую получает доступ к oAccount.interestRate и использует значение переменной экземпляра, может затем получить
устаревшее значение в зависимости от времени последнего вызова calculateInterestRate(). И любое клиентское программное обеспечение, которое устанавливает новую
interestRate, может обнаружить, что новое значение было загадочным образом изменено неким другим кодом, который вызывает calculateInterestRate(), или когда владелец счета
вносит средства на счет или выводит с него деньги.
Однако, если метод вычисления процентной ставки был назван getInterestRate() и клиентская программа вызывает
его, процентная ставка всегда будет вычисляться в процессе
работы и не появится риск возникновения потенциальной
ошибки.
Проверяем данные
Третья причина для избегания прямого доступа при установке
значения состоит в том, что клиентский код может слишком
легко установить переменную экземпляра в неверное значение.
Лучший подход — вызов метода в классе, чья работа состоит
Глава 8. Инкапсуляция 241

в установке значения. Как разработчик вы можете включить
в этот метод проверочный код, чтобы убедиться, что значение
устанавливается должным образом. Рассмотрим код
из листинга 8.3, чья цель заключается в управлении членами
клуба.
Файл: ValidatingData_ClubExample/Club.py
# Класс Club
class Club():
def __init__(self, clubName, maxMembers):
self.clubName = clubName 
self.maxMembers = maxMembers
self.membersList = []
def addMember(self, name): 
# проверяем, что осталось достаточно места
if len(self.membersList) < self.maxMembers:
self.membersList.append(name)
print('OK.', name, 'has been added to the',
self.clubName, 'club')
else:
print('Sorry, but we cannot add', name, 'to the',
self.clubName, 'club.')
print('This club already has the maximum of',
self.maxMembers, 'members.')
def report(self): 
print()
print('Here are the', len(self.membersList),
'members of the', self.clubName, 'club:')
for name in self.membersList:
print('
' + name)
print()
Листинг 8.3. Пример класса Club

Код Club отслеживает имя клуба, список его членов и их
максимальное количество в переменных экземпляра . Как
только экземпляры созданы, вы можете вызывать методы, чтобы добавлять членов в клуб  и сообщать о них . (Несложно
добавить больше методов: чтобы удалять членов, изменять имена и так далее, но этих двух достаточно, чтобы понять суть вопроса.)
242 Часть III. Инкапсуляция, полиморфизм и наследование

Ниже представлен тестовый код, который использует класс
Club.
Файл: ValidatingData_ClubExample/Main_Club.py
# Пример основной программы Club
from Club import *
# создаем клуб с максимум 5 членами
oProgrammingClub = Club('Programming', 5)
oProgrammingClub.addMember('Joe Schmoe')
oProgrammingClub.addMember('Cindy Lou Hoo')
oProgrammingClub.addMember('Dino Richmond')
oProgrammingClub.addMember('Susie Sweetness')
oProgrammingClub.addMember('Fred Farkle')
oProgrammingClub.report()

Мы создаем клуб программирования, который допускает
максимум пять членов, и затем добавляем их. Код работает хорошо и сообщает о добавленных в клуб членах:
OK.
OK.
OK.
OK.
OK.

Joe Schmoe has been added to the Programming club
Cindy Lou Hoo has been added to the Programming club
Dino Richmond has been added to the Programming club
Susie Sweetness has been added to the Programming club
Fred Farkle has been added to the Programming club

Теперь давайте добавим шестого члена:
# Попытка добавить дополнительного члена
oProgrammingClub.addMember('Iwanna Join')

Эта попытка была отклонена, и мы видим соответствующее
сообщение об ошибке:
Sorry, but we cannot add Iwanna Join to the Programming club.
This club already has the maximum of 5 members.

Код addMember() осуществляет всю необходимую проверку,
чтобы убедиться, что вызов для добавления нового члена работает правильно или генерирует сообщение об ошибке. Однако
с помощью прямого доступа клиент может изменить фундаментальную природу класса Club. Например, клиент может
Глава 8. Инкапсуляция 243

умышленно или случайно изменить максимальное количество
членов:
oProgrammingClub.maxMembers = 300

Далее, предположим, вы в курсе, что класс Club представляет членов в виде списка, и знаете имя переменной экземпляра,
которое представляет членов. В таком случае вы можете написать клиентский код, чтобы добавлять напрямую в список членов, не вызывая метод, следующим образом:
oProgrammingClub.memberList.append('Iwanna Join')

Эта строка приведет к превышению предполагаемого предела количества членов, поскольку она избегает кода, который гарантирует, что запрос на добавление нового участника правомерен.
Клиентский код, использующий прямой доступ, может даже
привести к ошибке внутри объекта Club. Например, переменная экземпляра self.maxMembers должна быть целым числом.
Используя прямой доступ, клиентский код может изменить
свое значение на строку. Любой последующий вызов
addMember() приведет к сбою в первой строке этого метода,
где он пытается сравнить длину списка с максимальным количеством членов, потому что Python не в состоянии сравнивать
целое число со строкой.
Разрешение прямого доступа к переменным экземпляра извне объекта может быть рискованным, так как оно обходит меры
безопасности, спроектированные для защиты данных объекта.
Строгая интерпретация с помощью геттеров и сеттеров
Строгий подход к инкапсуляции утверждает, что клиентский
код никогда не получает доступ к переменной экземпляра
напрямую. Если класс хочет разрешить клиентскому программному обеспечению доступ к информации, находящейся внутри объекта, стандартным подходом будет включить методы
геттера и сеттера в класс.
Геттер
Метод, который извлекает данные из объекта, экземпляр которого был
создан из класса.
244 Часть III. Инкапсуляция, полиморфизм и наследование

Сеттер
Метод, который присваивает данные объекту, экземпляр которого был
создан из класса.

Методы геттера и сеттера спроектированы, чтобы позволить пишущим клиентское программное обеспечение получать
данные из объекта и устанавливать их в объект, не обладая явными знаниями реализации класса, в частности не обязательно зная или используя имя какой-либо переменной экземпляра.
У кода класса Person в листинге 8.1 есть переменная экземпляра self.salary. В листинге 8.4 мы добавляем геттер и сеттер
в класс Person, что позволит вызывающему получить и установить зарплату, не предоставляя прямого доступа к переменной
экземпляра self.salary класса Person.
Файл: PersonGettersSettersAndDirectAccess/Person.py
class Person():
def __init__(self, name, salary):
self.name = name
self.salary = salary



# Позволяем вызывающему извлечь зарплату
def getSalary(self):
return self.salary



# Позволяем вызывающему установить новую зарплату
def setSalary(self, salary):
self.salary = salary
Листинг 8.4. Пример класса Person с геттером и сеттером

Части имен этих методов get  и set  необязательны,
но используются по соглашению. Обычно вы сопровождаете такие слова описанием данных, к которым представляется доступ, — в данном случае Salary. Хотя типично использовать
имя переменной экземпляра, к которой предоставляется доступ, это также необязательно.
В листинге 8.5 продемонстрирован некий тестовый код, создающий экземпляры двух объектов Person, затем получающий и устанавливающий новые зарплаты с помощью этих методов геттера и сеттера.

Глава 8. Инкапсуляция 245

Файл: PersonGettersSettersAndDirectAccess/
Main_PersonGetterSetter.py
# Пример основной программы Person, использующей

геттеры и сеттеры

from Person import *
 oPerson1 = Person('Joe Schmoe', 90000)
oPerson2 = Person('Jane Smith', 99000)
# получаем зарплаты, используя геттер, и выводим на экран

print(oPerson1.getSalary())
print(oPerson2.getSalary())
# меняем зарплаты с помощью сеттера
 oPerson1.setSalary(100000)
oPerson2.setSalary(111111)
# получаем зарплаты и снова выводим на экран
print(oPerson1.getSalary())
print(oPerson2.getSalary())
Листинг 8.5. Пример основного кода, использующего методы геттера и сеттера

Сначала мы создаем два объекта Person из класса Person .
Затем используем методы геттера и сеттера для извлечения 
и изменения  зарплат в объектах Person.
Геттеры и сеттеры предоставляют формальный способ получать и устанавливать значения в объекте. Они усиливают
слои защиты, которые позволяют получить доступ к переменным экземпляра лишь в том случае, если написавший класс хочет это разрешить.

ПРИМЕЧАНИЕ

Литература Python использует термины асессор для метода геттера и мутатор для метода сеттера. Это лишь другие названия
тех же самых вещей. Я буду использовать более общие термины
геттер и сеттер.

Безопасный прямой доступ
Существуют определенные обстоятельства, в которых кажется
разумным получить доступ к переменной экземпляра напрямую:
когда абсолютно ясно, что означает переменная экземпляра,
246 Часть III. Инкапсуляция, полиморфизм и наследование

нужна незначительная проверка данных или она вообще
не требуется и отсутствует вероятность, что имя когда-то будет
изменено. Хорошим примером этого является класс Rect (прямоугольник) в пакете pygame. Прямоугольник в pygame определяется с помощью четырех значений: х, у, ширины и высоты —
следующим образом:
oRectangle = pygame.Rect(10, 20, 300, 300)

После создания этого прямоугольного объекта использование oRectangle.x, oRectangle.y, oRectangle.width
и oRectangle.height напрямую в качестве переменных кажется приемлемым.

Делаем переменные экземпляра более
закрытыми
В Python все переменные экземпляра общедоступны (то есть
к ним можно получить доступ с помощью внешнего по отношению к классу кода). А если вы хотите разрешить доступ только
к отдельным переменным экземпляра вашего класса, а не ко
всем? Некоторые языки ООП позволяют явным образом отмечать определенные переменные экземпляра как public или
private, но в Python нет этих ключевых слов. Тем не менее
существует два способа, с помощью которых программисты,
разрабатывающие классы в Python, могут указать, что их переменные экземпляра и методы должны быть закрытыми.
Неявно закрытый
Чтобы отметить переменную экземпляра как ту, к которой
никогда не должен быть разрешен доступ извне, по соглашению
вы начинаете ее имя с одного подчеркивания:
self._name
self._socialSecurityNumber
self._dontTouchThis

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

Глава 8. Инкапсуляция 247

доступ напрямую. Код все еще может работать, если осуществляется доступ к переменным, но это не гарантируется.
Аналогичное соглашение используется для имен методов:
def _internalMethod(self):
def _dontCallMeFromClientSoftware(self):

Повторю: существует лишь соглашение; нет никакого принуждения. Если какое-то клиентское программное обеспечение
вызывает метод с именем, начинающимся с подчеркивания,
Python разрешит это, но существует высокая вероятность, что
это действие приведет к непредвиденным ошибкам.
Более явно закрытый
Python разрешает более явный уровень приватизации. Чтобы
запретить клиентскому программному обеспечению напрямую
осуществлять доступ к данным, нужно создать имя переменной
экземпляра, которое начинается с двух подчеркиваний.
Предположим, вы создаете класс с именем PrivatePerson
с переменной экземпляра self.__privateData, к которой никогда не должен осуществляться доступ извне объекта:
# Класс PrivatePerson
class PrivatePerson():



def __init__(self, name, privateData):
self.name = name
self.__privateData = privateData
def getName(self):
return self.name
def setName(self, name):
self.name = name

Затем мы можем создать объект PrivatePerson, передавая
некоторые данные, которые хотим сохранить закрытыми .
Попытки получить доступ к переменной экземпляра
__privateData напрямую из клиентского программного обеспечения подобным образом:
usersPrivateData = oPrivatePerson.__privateData

248 Часть III. Инкапсуляция, полиморфизм и наследование

сгенерируют ошибку:
AttributeError: 'PrivatePerson' object has no attribute
'__privateData'

Аналогичным образом, если вы создаете имя метода, которое начинается с двух подчеркиваний, любая попытка клиентского программного обеспечения вызвать метод сгенерирует
ошибку.
Python предоставляет эту возможность, выполняя декорирование имени. За кулисами Python изменяет любое имя, начинающееся с двух подчеркиваний, добавляя в его начало подчеркивание
и имя класса, так что __ становится ___.
Например, в классе PrivatePerson Python поменяет
self.__privateData на self._PrivatePerson__privateData.
Таким образом, если клиент попытается использовать имя
oPrivatePerson.__privateData, это имя не будет распознано.
Это едва уловимое изменение разработано в качестве защиты
от прямого доступа, но вы должны обратить внимание, что оно
не гарантирует абсолютную закрытость. Если программист клиента знает, как это работает, он все еще может получить доступ к переменной экземпляра с помощью .___
(или в нашем примере oPrivatePerson._PrivatePerson__
privateData).

Декораторы и @property
На высоком уровне декоратор является методом, который принимает другой метод в качестве аргумента и расширяет способ
работы исходного метода. (Декораторы также могут быть функциями, декорирующими функции или методы, но я сосредоточусь на методах.) Декораторы — это сложная тема, и они
в целом выходят за рамки данной книги. Однако существует
набор встроенных декораторов, которые представляют компромисс между прямым доступом и использованием геттеров
и сеттеров в классе.
Декоратор записывается в виде строки, которая начинается
с символа @, сопровождаемого именем декоратора, и помещается непосредственно перед оператором def метода. Это применяет декоратор к методу, добавляя его поведение:

Глава 8. Инкапсуляция 249

@
def (self, )

Мы будем использовать два встроенных декоратора и применим их к двум методам в классе, чтобы реализовать свойство.

Свойство
Атрибут класса, который для клиентского кода представляется как
переменная экземпляра, но вместо этого приводит к вызову метода, когда
к нему осуществляется доступ.

Свойство позволяет разработчикам класса применять косвенный метод, благодаря которому фокусник отвлекает внимание зрителей: аудитория думает, что видит одно, в то время как
за кулисами происходит нечто совсем иное. При написании класса для использования декораторов разработчик пишет методы
геттера и сеттера и добавляет отдельный встроенный декоратор
к каждому из них. Первым методом является геттер, и он сопровождается встроенным декоратором @property. Имя метода
определяет имя свойства, которое будет использоваться клиентским кодом. Вторым методом является сеттер, и он сопровождается встроенным декоратором @.setter. Ниже
представлен минимальный образец класса:
class Example():
def __init__(self, startingValue):
self._x = startingValue
@property
def x(self): # декорированный метод геттер
return self._x
@x.setter
def x(self, value): # декорированный метод сеттер
self._x = value

В классе Example х — это имя свойства. После стандартного
метода __init__() необычным является то, что у нас есть два
метода, оба с одинаковым именем: именем свойства. Первый
является геттером, в то время как второй — сеттером. Метод
сеттера необязателен, и, если он не будет представлен, свойство будет доступно только для чтения.
250 Часть III. Инкапсуляция, полиморфизм и наследование

Для класса Example ниже представлен образец клиентского
кода:
oExample = Example(10)
print(oExample.x)
oExample.x = 20

В этом коде мы создаем экземпляр класса Example, вызываем print() и выполняем простое присваивание. С точки зрения клиента, код хорошо читается. Когда мы пишем
oExample.x, это выглядит, как будто мы используем прямой доступ к переменной экземпляра. Однако, когда клиентский код
осуществляет доступ к значению свойства объекта (в правой части оператора присваивания или в качестве аргумента в вызове
функции или метода), Python преобразует это в вызов метода
геттера объекта. Когда в левой части оператора присваивания
появляется объект точка свойство, Python вызывает метод сеттера. Методы геттера и сеттера воздействуют на реальную переменную экземпляра self._x.
Ниже приведен более реалистичный пример, который должен помочь прояснить это. В листинге 8.6 продемонстрирован
класс Student, который включает свойство grade. Свойство декорировано с помощью методов геттера и сеттера, закрытой
переменной экземпляра является __grade.
Файл: PropertyDecorator/Student.py
# Использование свойства для получения (непрямого) доступа к данным
# объекта
class Student():
def __init__(self, name, startingGrade=0):
self.__name = name
self.grade = startingGrade 
@property 
def grade(self): 
return self.__grade
@grade.setter 
def grade(self, newGrade): 
try:
newGrade = int(newGrade)
Глава 8. Инкапсуляция 251

except (TypeError, ValueError) as e:
raise type(e)('New grade: ' + str(newGrade) +
', is an invalid type.')
if (newGrade < 0) or (newGrade > 100):
raise ValueError('New grade: ' + str(newGrade) +
', must be between 0 and 100.')
self.__grade = newGrade
Листинг 8.6. Класс Student с декораторами свойства

У метода __init__() есть небольшая хитрость, поэтому давайте сначала изучим другие методы. Обратите внимание, что
у нас есть два метода с именем grade(). Перед определением
первого метода grade() мы добавляем декоратор @property .
Это определяет имя grade в качестве свойства любого объекта,
созданного из этого класса. Первый метод  — это геттер, который просто возвращает значение текущей оценки, хранящееся
в закрытой переменной экземпляра self.__grade, но может
включать любой код, который может потребоваться для вычисления значения и его возврата.
Второму методу grade() предшествует декоратор @grade.
setter . Этот второй метод  принимает новое значение в качестве параметра, выполняет ряд проверок, чтобы убедиться,
что значение допустимо, затем устанавливает новое значение
в self.__grade.
Метод __init__() сначала сохраняет имя студента в переменной экземпляра. Следующая строка  кажется простой,
но она немного необычна. Как мы уже видели, мы обычно храним значения параметров в переменных экземпляра. Следовательно, у нас может возникнуть соблазн написать эту строку
как:
self.__grade = startingGrade

Но вместо этого мы сохраняем начальную оценку в свойство
grade. Поскольку grade является свойством, Python преобразует этот оператор присваивания в вызов метода сеттера , преимущество которого заключается в проверке входных данных
перед сохранением значения в переменной экземпляра
self.__grade.
В листинге 8.7 представлен текстовый код, который использует класс Student.

252 Часть III. Инкапсуляция, полиморфизм и наследование

Файл: PropertyDecorator/Main_Property.py
# Основной пример свойства Student
 oStudent1= Student('Joe Schmoe')
oStudent2= Student ('Jane Smith')
# получаем оценки студентов с помощью свойства "grade" и выводим
# на экран
 print(oStudent1.grade)
print(oStudent2.grade)
print()
# Настраиваем новые значения с помощью свойства "grade"
 oStudent1.grade = 85
oStudent2.grade = 92
 print(oStudent1.grade)
print(oStudent2.grade)
Листинг 8.7. Класс Student с декораторами свойства

В тестовом коде мы сначала создаем два объекта Student 
и выводим на экран оценки каждого из них . Выглядит, как
будто мы напрямую обращаемся к каждому объекту, чтобы получить значения оценок, но, поскольку grade является свойством, Python возвращает эти строки в вызовы метода геттера
и возвращает значение закрытой переменной экземпляра
self.__grade для каждого объекта.
Затем мы устанавливаем новые значения оценок для каждого объекта Student . Здесь это выглядит, будто мы устанавливаем значения напрямую в каждые данные объекта, но, повторюсь, поскольку grade является свойством, Python возвращает
эти строки в вызовы метода сеттера. Этот метод проверяет
каждое значение, прежде чем выполнить предписанное. Тестовый код заканчивается выводом новых значений оценок на экран .
Когда возвращаем тестовый код, мы получаем эти выходные
данные, как и ожидали:
0
0
85
92

Глава 8. Инкапсуляция 253

Используя декораторы @property и @.
setter, вы получаете лучшее из миров прямого доступа и геттера-и-сеттера. Клиентское программное обеспечение может
быть написано таким образом, что кажется, будто оно имеет
прямой доступ к переменным экземпляра, но, как программирующие класс, ваши декорированные методы получают и устанавливают фактические переменные экземпляра, которыми
владеет объект, и даже разрешают проверку входных данных.
Этот подход поддерживает инкапсуляцию, поскольку клиентский код не получает прямой доступ к переменной экземпляра.
Хотя этот метод используется многими профессиональными разработчиками Python, лично мне он кажется немного сомнительным, потому что, когда я читаю коды других разработчиков, применяющих такой подход, не сразу вижу,
используют ли они прямые доступы к переменным экземпляра
или задействуют свойства, которые Python преобразовывает
в вызовы декорированных методов. Я предпочитаю использовать стандартные методы геттера и сеттера, что и буду делать
в остальной части этой книги.

Инкапсуляция в классах pygwidgets
Определение инкапсуляции в начале этой главы сосредоточилось на двух областях: сокрытии внутренних деталей и размещении соответствующего кода в одном месте. В pygwidgets все
классы спроектированы с учетом этих соображений. В качестве примеров возьмем классы TextButton и CustomButton.
Методы этих двух классов инкапсулируют все функциональные возможности кнопок GUI. Хотя исходный код этих классов
доступен, программисту клиента нет необходимости просматривать его, чтобы эффективно их использовать. Также клиентскому коду не нужно пытаться получить доступ к любой
из переменных экземпляра: вся функциональность кнопок доступна через вызов методов этих классов. Такой подход соответствует строгой интерпретации инкапсуляции, то есть для
клиентского программного обеспечения единственным способом получить доступ к данным объекта будет вызов методов
этого объекта. Программист клиента может воспринимать эти
классы как черные ящики, поскольку нет никаких причин смотреть на то, как они выполняют свои задачи.
254 Часть III. Инкапсуляция, полиморфизм и наследование

ПРИМЕЧАНИЕ

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

История из реального мира
Несколько лет назад я принимал участие в проектировании
и разработке очень крупного образовательного проекта, который был выстроен в среде под названием Director в Macromedia
(позднее Adobe) с помощью объектно- ориентированного языка
Lingo.
Director был спроектирован для расширения с помощью
XTRAs, которые могли добавить функциональные возможности аналогично тому, как плагины добавляются в браузеры.
Эти XTRAs были разработаны и проданы рядом сторонних поставщиков. При проектировании мы планировали хранить навигационную и прочую связанную с дисциплинами информацию в базе данных. Я просмотрел все разнообразие XTRAs,
которые были доступны, и приобрел конкретную XTRA, которую я буду называть XTRA1.

Запрашивает
информацию

Основная
программа

XTRA
базы данных

Объект
базы данных

Возвращает
результаты

Получает
доступ
к базе
данных

Создает
SQL

Возвращает
результаты

Возвращает
результаты

База
данных

Рис. 8.1. Архитектура доступа к базе данных с помощью объекта и XTRA
Глава 8. Инкапсуляция 255

Каждая XTRA поставлялась с документацией ее API, которая
показывала, как выполнять запросы к базе данных с помощью
языка структурированных запросов (SQL). Я решил создать класс
Database, который включал все функциональные возможности
доступа к базе данных с помощью XTRA1 API. Таким образом весь
код, который напрямую взаимодействовал с XTRA, находился
в классе Database. На рис. 8.1 показана общая архитектура.
При запуске программа создала один экземпляр класса
Database. Основной код был клиентом объекта Database. Каждый раз, когда основной код хотел получить информацию
из базы данных, вместо того чтобы самостоятельно форматировать запрос SQL, он вызывал метод объекта Database, предоставляя детали по поводу необходимой ему информации. Методы объекта Database преобразовывали каждый запрос
в запрос SQL, выполняемый в XTRA1, чтобы получить данные
из базы данных. Таким образом, только код объекта Database
знал, как получить доступ к XTRA, используя ее API.
Программа работала хорошо, и клиенты с удовольствием использовали продукт. Но время от времени мы сталкивались
с ошибками в данных, которые получали из базы. Я связался
с разработчиком XTRA1 и предоставил ему множество воспроизводимых примеров проблем. К сожалению, разработчик
не стал этим заниматься.
Из-за отсутствия ответа мы в итоге решили приобрести другую базу данных XTRA, XTRA2, для этой цели. XTRA2 работала
аналогичным образом, но были едва уловимые отличия в том, как
она была инициализирована, и это потребовало некоторых незначительных изменений в способе построения SQL-запросов.
Поскольку класс Database инкапсулировал все детали взаимодействия с XTRA, мы смогли выполнить все необходимые изменения, чтобы работать с XTRA2, только в классе Database.
Мы не меняли ни единой строки в основной программе (клиентском коде).
В данном случае я был как разработчиком класса Database,
так и разработчиком клиентского программного обеспечения.
Если бы мой клиентский код использовал имена переменных
экземпляра в классе, пришлось бы просматривать программу,
изменяя каждую строку соответствующего кода. Использование инкапсуляции с классом сэкономило мне бесчисленное количество часов доработки и тестирования.
256 Часть III. Инкапсуляция, полиморфизм и наследование

В продолжении этой истории, хотя XTRA2 работала хорошо, эта компания в итоге вышла из бизнеса, и мне пришлось заново пройти через тот же процесс. И вновь из-за инкапсуляции
для работы с XTRA3 был изменен лишь код класса Database.

Абстракция
Абстракция — это еще одно понятие ООП, тесно связанное
с инкапсуляцией; многие разработчики считают ее четвертым
принципом ООП.
В то время как инкапсуляция связана с реализацией, сокрытием деталей кода и данных, которые составляют класс, абстракция связана с представлением клиента о классе. Речь идет
о восприятии класса извне.
Абстракция
Управление сложностью с помощью сокрытия ненужных деталей.

По сути, абстракция призывает убедиться, что представление пользователя о системе максимально простое.
Абстракция невероятно широко распространена среди потребительских продуктов. Многие люди используют телевизоры, компьютеры, микроволновые печи, машины и так далее
каждый день. Мы привыкаем к распространяемому на нас пользовательскому интерфейсу этих продуктов. С помощью своих
элементов управления они предоставляют абстракцию собственной функциональности. Вы жмете на педаль газа, чтобы
машина двигалась вперед. В микроволновой печи вы устанавливаете количество времени и нажимаете «Старт», чтобы разогреть какую-то еду. Но лишь некоторые из нас знают, как эти
продукты устроены внутри.
Вот пример абстракции из мира компьютерной науки.
В программировании стек — это механизм для запоминания
данных в порядке последний вошел первый вышел (LIFO). Подумайте о стопке тарелок, в которой чистые тарелки добавляются наверх, и пользователи могут взять одну сверху, когда им понадобится тарелка. В стеке есть две стандартные операции:
push добавляет элемент в верхнюю часть стека, а pop удаляет
самый верхний элемент из стека.
Стек особенно полезен, когда ваша программа осуществляет
навигацию, потому что с его помощью можно оставить след
Глава 8. Инкапсуляция 257

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

Клиент добавлял бы информацию, вызывая метод push()
следующим образом:
oStack.push()

И он извлекал бы самые последние данные, вызывая метод
pop() следующим образом:
= oStack.pop()

Клиенту не нужно знать или беспокоиться о том, как эти методы реализованы или как были сохранены данные. Реализация Stack будет полностью обрабатываться методами Stack.
Хотя клиент может представлять класс Stack как черный
ящик, написание такого класса в Python довольно тривиально.
В листинге 8.8 показано, как его реализовать.
Файл: Stack/Stack.py
# Класс Stack
class Stack():
'''Класс Stack реализует алгоритм последний вошел первый вышел
LIFO'''
def __init__(self, startingStackAsList=None):
if startingStackAsList is None:

258 Часть III. Инкапсуляция, полиморфизм и наследование



self.dataList = [ ]
else:
self.dataList = startingStackAsList[:] # создаем копию



def push(self, item):
self.dataList.append(item)



def pop(self):
if len(self.dataList) == 0:
raise IndexError
element = self.dataList.pop()
return element



def peek(self):
# извлекаем верхний элемент, не удаляя его
item = self.dataList[-1]
return item



def getSize(self):
nElements = len(self.dataList)
return nElements



def show(self):
# отображаем стек в вертикальной ориентации
print('Stack is:')
for value in reversed(self.dataList):
print(' ', value)
Листинг 8.8. Стек в качестве класса Python

Класс Stack отслеживает все данные, использующие переменную экземпляра списка с именем self.dataList . Клиенту не нужно знать этот уровень детализации, push() просто
добавляет элемент во внутренний список, используя операцию
Python append(), в то время как pop() выталкивает последний элемент из внутреннего списка. Поскольку это легко сделать, такая реализация класса Stack также реализует три дополнительных метода:
• peek() позволяет вызывающему получать данные из верхней части стека, не удаляя их из него;
• getSize() возвращает число элементов в стеке;
• show() выводит на экран содержимое стека в таком виде,
как клиент его себе представляет: данные отображаются вертикально, при этом помещенные последними отображаются
сверху. Это может оказаться полезным при отладке клиентского кода, который включает несколько вызовов push()и pop().
Глава 8. Инкапсуляция 259

Приведенный пример невероятно прост, но, когда вы наберетесь больше опыта в написании классов, они будут становиться сложнее. В ходе работы вы можете найти более явные
и более эффективные способы написания некоторых методов
и, возможно, перепишете их. Поскольку объекты обеспечивают
как инкапсуляцию, так и абстракцию, вам как автору класса
не стоит стесняться изменять его код и данные, если при этом
опубликованные интерфейсы не меняются. Изменения реализации методов не должны оказывать негативного воздействия
на клиентское программное обеспечение, а скорее, обязаны позволять вам вносить улучшения, не воздействуя на какой-либо
клиентский код. По сути, если вы найдете способы сделать ваш
код более эффективным и опубликуете новую версию, может
показаться, что клиентский код ускоряется, при этом не требуется вносить в него никаких изменений.
Свойство — великолепный пример абстракции. Как вы видели ранее, со свойствами программист клиента может использовать синтаксис, который проясняет его намерения (получить
и установить значение объекта). Реализация в методах, которые в результате вызываются, может быть гораздо сложнее,
но она полностью скрыта от клиентского кода.

Выводы
Инкапсуляция — первый из основных принципов объектноориентированного программирования, позволяющий классам
скрывать их реализации и данные от клиентского кода и гарантирующий, что класс предоставляет все функциональные возможности, которые необходимы клиенту, в одном месте.
Ключевая концепция ООП состоит в том, что объекты владеют своими данными, и именно поэтому я рекомендую вам
предоставлять методы геттера и сеттера, если вы хотите, чтобы клиентский код получил доступ к данным, содержащимся
в переменной экземпляра. Python разрешает прямой доступ
к переменным экземпляра с помощью синтаксиса точки,
но я настоятельно рекомендую избегать его по причинам, которые изложил в этой главе.
Существуют соглашения для обозначения переменных экземпляра и методов как закрытых, использующие начальное
подчеркивание или двойное подчеркивание, в зависимости
260 Часть III. Инкапсуляция, полиморфизм и наследование

от уровня необходимой вам приватизации. В качестве компромисса Python позволяет использовать декоратор @property. Он
создает впечатление, будто клиентский код может напрямую
получить доступ к переменной экземпляра, в то время как за кулисами Python превращает такие ссылки в вызовы декорированных методов геттера и сеттера в классе.
Пакет pygwidgets предоставляет множество прекрасных
примеров инкапсуляции. Как программист клиента вы видите
класс со стороны и работаете с интерфейсами, которые он предоставляет. Абстракция — управление сложностью за счет сокрытия деталей — помогает проектировщику класса создавать
хороший интерфейс, рассматривая его с точки зрения клиента.
Однако в Python часто доступен исходный код, поэтому при желании вы можете посмотреть реа лизацию.

9
ПОЛИМОРФИЗМ

Эта глава посвящена второму основополагающему принципу ООП: полиморфизму. Термин
происходит из греческого языка: приставка поли
обозначает «много», а морфизм — «форму» или
«структуру».
Итак, полиморфизм по сути обозначит много форм. Я не имею
в виду меняющего форму инопланетянина из «Звездного
пути» — на самом деле это нечто совсем противоположное. Полиморфизм в ООП заключается не в том, чтобы одна вещь принимала множество форм, а в том, что у нескольких классов могут быть методы с абсолютно идентичными именами. В итоге
это даст нам интуитивно понятный способ для осуществления
действий с коллекцией объектов вне зависимости от того,
из какого класса был получен каждый из них.
Программисты ООП часто используют термин «отправить
сообщение», когда говорят о клиентском коде, вызывающем метод объекта. Что должен делать объект, когда он получает сообщение, решает он сам. С помощью полиморфизма мы можем отправлять одно и то же сообщение нескольким объектам,
и каждый будет реагировать по-своему, в зависимости от того,
для чего он был спроектирован, и от доступных ему данных.
Полиморфизм 263

В этой главе я рассмотрю, как эта возможность позволяет создавать пакеты классов, которые легко расширяемы и предсказуемы. Мы также будем использовать полиморфизм с операторами, чтобы заставить одни и те же операторы выполнять
различные действия в зависимости от типов данных, с которыми они работают. И наконец, я покажу вам, как использовать
функцию print(), чтобы получить ценную отладочную информацию от объектов.

Отправляем сообщения объектам
реального мира
Давайте рассмотрим полиморфизм в реальном мире, используя
пример автомобилей. У них у всех есть педаль газа. Когда водитель нажимает на нее, педаль отправляет машине сообщение
«ускориться». В автомобиле может быть двигатель внутреннего
сгорания или электрический мотор, или он может быть гибридным. У каждого из этих типов машин есть своя реализация
того, что происходит, когда она получает сообщение об ускорении, и каждая из них ведет себя соответствующим образом.
Полиморфизм позволяет облегчить адаптацию новой технологии. Если кто-то собирается разработать атомный автомобиль, пользовательский интерфейс машины останется таким
же: водитель все так же будет нажимать на педаль газа, чтобы
отправить сообщение, но уже совсем другой механизм будет заставлять атомный автомобиль двигаться быстрее.
В качестве другого примера из реального мира представьте,
что вы входите в большую комнату с группой переключателей,
управляющих множеством различных источников света. Некоторые из них представляют собой лампы накаливания старого
образца, другие — флуоресцентные или же новейшие LED-лампы. Когда переводите все переключатели в положение вверх,
вы отправляете сообщение «включить» всем лампам. Базовые
механизмы, которые заставляют лампы накаливания, флуоресцентные и LED излучать свет, сильно различаются, но каждый
из них достигает поставленной пользователем цели.

264 Часть III. Инкапсуляция, полиморфизм и наследование

Классический пример полиморфизма
в программировании
С точки зрения ООП полиморфизм заключается в том, как клиентский код может вызывать метод с точно таким же именем
в различных объектах, и каждый объект будет делать все, что
необходимо, для реализации значения этого метода для этого
объекта.
В качестве классического примера полиморфизма рассмотрим код, который представляет различные виды домашних
животных. Предположим, у вас есть коллекция собак, кошек
и птиц, и каждая из них понимает некоторый набор базовых
команд. Если вы попросите этих животных заговорить (то есть
пошлете сообщение «говорить» каждому из них), собака скажет
«гав», кошка скажет «мяу», а птица — «чик-чирик». В листинге 9.1 показано, как мы можем реализовать этот код.
Файл: PetsPolymorphism.py
# Полиморфизм Pets
# Три класса, все с различными методами "говорить"
class Dog():
def __init__(self, name):
self.name = name


def speak(self):
print(self.name, 'says bark, bark, bark!')
class Cat():
def __init__(self, name):
self.name = name



def speak(self):
print(self.name, 'says meeeoooow')
class Bird():
def __init__(self, name):
self.name = name



def speak(self):
print(self.name, 'says tweet')
oDog1 = Dog('Rover')
oDog2 = Dog('Fido')
Глава 9. Полиморфизм 265

oCat1 = Cat('Fluffy')
oCat2 = Cat('Spike')
oBird = Bird('Big Bird')
 petsList = [oDog1, oDog2, oCat1, oCat2, oBird]
# Отправляем одно и то же сообщение (вызываем один и тот же метод)
# всем домашним животным
for oPet in petsList:

oPet.speak()
Листинг 9.1. Отправка сообщения «говорить» объектам, для которых были созданы
экземпляры из различных классов

У каждого класса есть метод speak(), но содержимое у них
разное   . Каждый класс делает все, что необходимо, чтобы
выполнить свою версию метода; имя метода одинаковое,
но реализации его различаются.
Для упрощения работы помещаем все объекты домашних
животных в список . Чтобы заставить их говорить, мы далее
перебираем все объекты и отправляем одно и то же сообщение,
вызывая метод с точно таким же именем в каждом объекте ,
не беспокоясь о типе объекта.

Пример, использующий фигуры pygame
Далее рассмотрим демонстрацию полиморфизма с использованием pygame. В главе 5 я применил pygame, чтобы нарисовать
примитивные фигуры, такие как прямоугольники, круги, многоугольники, овалы и линии. Здесь мы создадим демонстрационную программу, которая будет случайным образом создавать
и рисовать различные фигуры в окне. Затем пользователь
может щелкнуть по любой фигуре, и программа сообщит тип
и площадь фигуры, по которой щелкнули. Поскольку фигуры
создаются произвольным образом, каждый раз при выполнении программы размеры, местоположение, количество и позиции фигур будут разными. На рис. 9.1 показан образец выходных данных демонстрационной программы.
Мы реализуем программу с классом для каждой из трех различных фигур: Square, Circle и Triangle. Ключевой момент
заключается в том, что здесь все классы фигур содержат методы с одинаковыми именами: __init__(), draw(), getType(),
getArea() и clickedInside(), которые выполняют
266 Часть III. Инкапсуляция, полиморфизм и наследование

одинаковые задачи. Однако реализация каждого метода различна, потому что каждый класс имеет дело со своей фигурой.

Рис. 9.1. Основанный на pygame пример использования полиморфизма для
изображения различных фигур

Класс квадратной формы
Я начну с самой простой фигуры. В листинге 9.2 показан код
класса Square.
Файл: Shapes/Square.py
# Класс Square
import pygame
import random
# Настраиваем цвета
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)



class Square():
def __init__(self, window, maxWidth, maxHeight):
self.window = window
Глава 9. Полиморфизм 267

self.widthAndHeight = random.randrange(10, 100)
self.color = random.choice((RED, GREEN, BLUE))
self.x = random.randrange(1, maxWidth – 100)
self.y = random.randrange(25, maxHeight – 100)
self.rect = pygame.Rect(self.x, self.y, self.widthAndHeight,
self.widthAndHeight)
self.shapeType = 'Square'


def clickedInside(self, mousePoint):
clicked = self.rect.collidepoint(mousePoint)
returnclicked



def getType(self):
return self.shapeType



def getArea(self):
theArea = self.widthAndHeight * self.widthAndHeight
return theArea



def draw(self):
pygame.draw.rect(self.window, self.color,
(self.x, self.y, self.widthAndHeight,
self.widthAndHeight))
Листинг 9.2. Класс Square

В методе __init__()  мы установили несколько переменных экземпляра для использования в методах класса. Это позволит нам оставить код методов очень простым. Поскольку метод __init__() сохранил прямоугольник как Square, метод
clickedInside() просто проверяет, был ли выполнен щелчок мыши внутри этого прямоугольника, возвращая True или
False.
Метод getType() просто возвращает информацию о том,
что нажатый элемент является квадратом. Метод getArea()
умножает ширину на высоту и возвращает итоговую площадь.
Метод draw() использует draw.rect()pygame, чтобы нарисовать фигуру в произвольно выбранном цвете.
Класс круглой и треугольной формы
Далее давайте посмотрим на код классов Circle и Triangle.
Важная вещь, которую следует отметить, состоит в том, что
имена методов этих классов такие же, как и у класса Square,
но код в них (особенно в clickedInside()и getArea()) сильно
отличается. В листинге 9.3 показан класс Circle. В листинге 9.4
268 Часть III. Инкапсуляция, полиморфизм и наследование

показан класс Triangle, который создает прямоугольные треугольники произвольного размера, края которых параллельны
осям х и у, а прямой угол находится в верхнем левом углу.
Файл: Shapes/Circle.py
# Класс Circle
import pygame
import random
import math

# Настраиваем цвета
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Circle():
def __init__(self, window, maxWidth, maxHeight):
self.window = window
self.color = random.choice((RED, GREEN, BLUE))
self.x = random.randrange(1, maxWidth – 100)
self.y = random.randrange(25, maxHeight – 100)
self.radius = random.randrange(10, 50)
self.centerX = self.x + self.radius
self.centerY = self.y + self.radius
self.rect = pygame.Rect(self.x, self.y,
self.radius * 2, self.radius * 2)
self.shapeType = 'Circle'


def clickedInside(self, mousePoint):
distance = math.sqrt(((mousePoint[0] – self.centerX) ** 2) +
((mousePoint[1] – self.centerY) ** 2))
if distance

Больше

__gt__()

=

Больше или равно

__ge__()

Чтобы разрешить оператору сравнения == проверить наличие равенства между двумя объектами Square, вам необходимо
будет написать метод в классе Square, подобный этому:
def __eq__(self, oOtherSquare):
if not isinstance(oOtherSquare, Square):
raise TypeError('Second object was not a Square')
if self.heightAndWidth == oOtherSquare.heightAndWidth:
return True # сопоставимо
else:
return False # не сопоставимо

Когда Python обнаруживает сравнение ==, где первым объектом является Square, он вызывает этот метод в классе Square.
Глава 9. Полиморфизм 279

Поскольку Python — это свободно типизированный язык (он
не требует от вас определения типов переменных), тип данных
второго параметра может быть любой. Тем не менее, чтобы
сравнение работало правильно, второй параметр также должен
быть объектом Square. Мы осуществим проверку, используя
функцию isinstance(), которая работает с определенными
программистом классами таким же образом, как она работает
со встроенными классами. Если второй объект не Square, мы
вызываем исключение.
Затем мы сравниваем heightAndWidth текущего объекта
(self) с heightAndWidth второго объекта (oOtherSquare).
В этом случае использование прямого доступа к переменным
экземпляра двух объектов абсолютно допустимо, потому что
оба объекта одного типа и, следовательно, они должны содержать одинаковые переменные экземпляра.
Магические методы в классе Rectangle
Для расширения мы создадим программу, которая рисует
несколько прямоугольных форм с помощью класса Rectangle.
Пользователь сможет щелкнуть по любым двум прямоугольникам, а программа сообщит, одинакова ли площадь этих прямоугольников или площадь первого больше или меньше площади
второго прямоугольника. Мы используем операторы ==, < и >
и будем ожидать, что результаты для каждого сравнения окажутся представлены в виде булева выражения True или False.
В листинге 9.6 содержится код класса Rectangle, который реализует магические методы для этих операторов.
Файл: MagicMethods/Rectangle/Rectangle.py
# Класс Rectangle
import pygame
import random
# Настраиваем цвета
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Rectangle():
def __init__(self, window):
self.window = window

280 Часть III. Инкапсуляция, полиморфизм и наследование

self.width = random.choice((20, 30, 40))
self.height = random.choice((20, 30, 40))
self.color = random.choice((RED, GREEN, BLUE))
self.x = random.randrange(0, 400)
self.y = random.randrange(0, 400)
self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
self.area = self.width * self.height
def clickedInside(self, mousePoint):
clicked = self.rect.collidepoint(mousePoint)
return clicked
# Вызываем магический метод, когда сравниваются
# два объекта Rectangle с помощью оператора ==
def __eq__ (self, oOtherRectangle): 
if not isinstance(oOtherRectangle, Rectangle):
raise TypeError('Second object was not a Rectangle')
if self.area == oOtherRectangle.area:
return True
else:
return False
# Вызываем магический метод, когда сравниваются
# два объекта Rectangle с помощью оператора <
def __lt__(self, oOtherRectangle): 
if not isinstance(oOtherRectangle, Rectangle):
raise TypeError('Second object was not a Rectangle')
if self.area < oOtherRectangle.area:
return True
else:
return False
# Вызываем магический метод, когда сравниваются
# два объекта Rectangle с помощью оператора >
def __gt__(self, oOtherRectangle): 
if not isinstance(oOtherRectangle, Rectangle):
raise TypeError('Second object was not a Rectangle')
if self.area > oOtherRectangle.area:
return True
else:
return False
def getArea(self):
return self.area
def draw(self):
pygame.draw.rect(self.window, self.color, (self.x, self.y,
self.width, self.height))
Листинг 9.6. Класс Rectangle
Глава 9. Полиморфизм 281

Методы __eq__() , __lt__()  и __gt__()  позволяют
клиентскому коду использовать стандартные операторы сравнения между объектами Rectangle. Чтобы сравнить два прямоугольника для выявления равенства, вы напишете:
if oRectangle1 == oRectangle2:

Когда эта строка выполняется, вызывается метод __eq__()
первого объекта, а второй объект передается в качестве второго параметра. Функция возвращает либо True, либо False. Аналогичным образом для сравнения «меньше чем» вы напишете
строку, подобную этой:
if oRectangle1 < oRectangle2:

Затем метод __lt__() проверит, будет ли площадь первого
прямоугольника меньше площади второго прямоугольника.
Если клиентский код использует оператор > для сравнения двух
прямоугольников, то вызывается метод __gt__().
Использование магических методов основной
программой
В листинге 9.7 показан код основной программы, который
тестирует магические методы.
Файл: MagicMethods/Rectangle/Main_RectangleExample.py
import pygame
import sys
from pygame.locals import *
from Rectangle import *
# Настраиваем константы
WHITE = (255, 255, 255)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30
N_RECTANGLES = 10
FIRST_RECTANGLE = 'first'
SECOND_RECTANGLE = 'second'
# Настраиваем окно
pygame.init()
window = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT), 0, 32)
clock = pygame.time.Clock()

282 Часть III. Инкапсуляция, полиморфизм и наследование

rectanglesList = []
for i in range(0, N_RECTANGLES):
oRectangle = Rectangle(window)
rectanglesList.append(oRectangle)
whichRectangle = FIRST_RECTANGLE
# Основной цикл
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == MOUSEBUTTONDOWN:
for oRectangle in rectanglesList:
if oRectangle.clickedInside(event.pos):
print('Clicked on', whichRectangle, 'rectangle.')
if whichRectangle == FIRST_RECTANGLE:
oFirstRectangle = oRectangle 
whichRectangle = SECOND_RECTANGLE
elif whichRectangle == SECOND_RECTANGLE:
oSecondRectangle2 = oRectangle 
# Пользователь выбрал 2 прямоугольника,
# сравниваем их
if oFirstRectangle == oSecondRectangle: 
print('Rectangles are the same size.')
elif oFirstRectangle < oSecondRectangle: 
print('First rectangle is smaller than
second rectangle.')
else: # должен быть больше 
print('First rectangle is larger than
second rectangle.')
whichRectangle = FIRST_RECTANGLE
# Очищаем окно и рисуем все прямоугольники
window.fill(WHITE)
for oRectangle in rectanglesList: 
oRectangle.draw()
pygame.display.update()
clock.tick(FRAMES_PER_SECOND)
Листинг 9.7. Основная программа, которая рисует и затем сравнивает объекты

Глава 9. Полиморфизм 283

Пользователь программы щелкает по паре прямоугольников, чтобы сравнить их размеры. Мы сохраняем выбранные
прямоугольники в двух переменных  .
Проверяем наличие равенства с помощью оператора == ,
который решает вызвать метод __eq__() класса Rectangle.
Если размер прямоугольников одинаковый, мы выводим соответствующее сообщение. Если они не равны, мы проверяем, будет ли первый прямоугольник меньше второго, с помощью оператора < , что приводит к вызову метода __lt__(). Если это
сравнение также не дает True, мы выводим сообщение, что первый прямоугольник больше, чем второй . В этой программе
нам не понадобилось использовать оператор >; однако, поскольку другой клиентский код может реализовать сравнения
размеров по-другому, мы включили для полноты картины метод __gt__().
И наконец, рисуем все прямоугольники в нашем списке .
Поскольку мы включили методы __eq__(), __lt__()
и __gt__() в класс Rectangle, то смогли использовать стандартные операторы сравнения интуитивно понятным и легко
читаемым способом.
Ниже приведены выходные данные щелчков по нескольким
различным прямоугольникам:
Clicked on first rectangle.
Clicked on second rectangle.
Rectangles are the same size.
Clicked on first rectangle.
Clicked on second rectangle.
First rectangle is smaller than second rectangle.
Clicked on first rectangle.
Clicked on second rectangle.
First rectangle is larger than second rectangle.

Магические методы математических операторов
Вы можете написать дополнительные магические методы,
чтобы определить, что происходит, когда клиентский код
использует другие арифметические операторы между объектами, экземпляры которых были созданы из вашего класса.
В табл. 9.2 показаны методы, вызываемые для базовых арифметических операторов.

284 Часть III. Инкапсуляция, полиморфизм и наследование

Таблица 9.2. Символы, значения и имена магических методов
математических операторов
Символ

Значение

Имя магического метода

+

Сложение

__add__()



Вычитание

__sub__()

*

Умножение

__mul__()

/

Деление (результат
с плавающей точкой)

__truediv__()

//

Деление целого

__floordiv__()

%

Остаток

__mod__()

Abs

Абсолютное значение

__abs__()

Например, чтобы обработать оператор +, вам необходимо
будет реализовать метод в классе следующим образом:
def __add__(self, oOther):
# Определение, что происходит при добавлении двух
# из числа объектов

Полный список магических и dunder-методов можно найти
в официальной документации по адресу https://docs.python.org/
3/reference/datamodel.html.
Векторный пример
В математике вектор — это упорядоченная пара значений х и у,
которая часто представляется на графике в виде направленного отрезка. В этом разделе мы создадим класс, который
использует магические методы математических операторов для
векторов. Существует ряд математических операций, которые
можно выполнять с векторами. На рис. 9.2 показан пример сложения двух векторов.
Сложение двух векторов приводит к появлению нового вектора, чье значение х равно сумме значений х двух суммированных векторов и чье значение у представляет сумму значений
у двух суммированных векторов. На рис. 9.2 мы суммируем вектор (3, 2) и вектор (1, 3), чтобы получить вектор (4, 5).
Два вектора считаются равными, если их значения х и у одинаковы. Размер вектора вычисляется как гипотенуза прямоугольного треугольника, одна сторона которого представляет
Глава 9. Полиморфизм 285

собой длину х, а вторая сторона — длину у. Мы можем применить теорему Пифагора, чтобы вычислить длину, и использовать длины для сравнения размеров двух векторов.

Ось у
6
5
4

= Вектор 4, 5

3

Вектор 1, 3

2
плюс
1
Вектор 3, 2
–6

–5

–4

–3

–2

–1

1

2

3

Ось х
4

5

6

–1
–2
–3
–4
–5
–6

Рис. 9.2. Сложение векторов в декартовой системе координат

В листинге 9.8 класс Vector демонстрирует соответствующие магические методы для выполнения вычислений и сравнений между двумя объектами Vector. (У каждого из этих методов есть дополнительный код, использующий вызов
isinstance(), чтобы удостовериться, что второй объект является Vector. Эти проверки включены в загружаемый файл,
но я опустил их здесь, чтобы сэкономить место.)

286 Часть III. Инкапсуляция, полиморфизм и наследование

Файл: MagicMethods/Vectors/Vector.py
# Класс Vector
import math
class Vector():
'''Класс Vector представляет два значения в качестве вектора,
разрешая многие математические вычисления'''
def __init__(self, x, y):
self.x = x
self.y = y

def __add__(self, oOther): # вызываем оператор +
return Vector(self.x + oOther.x, self.y + oOther.y)
def __sub__(self, oOther): # вызываем оператор —
return Vector(self.x – oOther.x, self.y – oOther.y)


def __mul__(self, oOther): # вызываем оператор *
# Специальный код допускает умножение на вектор или скаляр
if isInstance(oOther, Vector): # умножаем два вектора
return Vector((self.x * oOther.x), (self.y * oOther.y))
elif isinstance(oOther, (int, float)): # умножаем на скаляр
return Vector((self.x * oOther), (self.y * oOther))
else:
raise TypeError('Second value must be a vector or scalar')
def __abs__(self):
return math.sqrt((self.x ** 2) + (self.y ** 2))
def __eq__(self, oOther): # вызываем оператор ==
return (self.x == oOther.x) and (self.y == oOther.y)
def __ne__(self, oOther): # вызываем оператор !=
return not (self == oOther) # вызываем метод __eq__
def __lt__(self, oOther): # вызываем оператор <
if abs(self) < abs(oOther): # вызываем метод __abs__
return True
else:
return False
def __gt__(self, oOther): # вызываем оператор >
if abs(self) > abs(oOther): # вызываем метод __abs__
return True
else:
return False
Листинг 9.8. Класс Vector, который реализует ряд магических методов
Глава 9. Полиморфизм 287

Этот класс реализует арифметические операторы и операторы сравнения в качестве магических методов. Клиентский код
будет использовать стандартные символы для вычислений и сравнения между двумя объектами Vector. Например, сложение
векторов на рис. 9.2 можно обработать следующим образом:
oVector1 = Vector(3, 2)
oVector2 = Vector(1, 3)
oNewVector = oVector1 + oVector2 # используем оператор +
# для сложения векторов

Когда выполняется третья строка, вызывается метод
__add__()  для сложения двух объектов Vector, что приводит к созданию нового объекта Vector. В методе __mul__() 
проводится специальная проверка, которая позволяет оператору * либо умножить два Vector, либо умножить один Vector
на скалярную величину (в зависимости от типа второго значения).

Создаем строковое представление значений
в объекте
Стандартный подход к отладке заключается в добавлении вызовов print()для вывода значений переменных в определенных
точках вашей программы:
print('My variable is', myVariable)

Однако, если вы попробуете использовать print() для помощи в отладке содержимого объекта, результаты не будут особо полезными. Например, здесь мы создаем объект Vector
и выводим его на экран:
oVector = Vector(3, 4)
print('My vector is', oVector)

Вот что выводится:


Это говорит нам, что есть объект, экземпляр которого был
создан из класса Vector, и показывает адрес памяти этого объекта. Однако в большинстве случаев мы на самом деле хотим
знать значения переменных экземпляра в объекте на данный
288 Часть III. Инкапсуляция, полиморфизм и наследование

момент. К счастью, можно использовать для этого магические
методы.
Существует два магических метода, которые полезны для получения информации (в виде строк) из объекта.
• Метод __str__()используется для создания строкового представления объекта, которое может легко прочитать человек.
Если клиентский код вызывает встроенную функцию str()
и передает объект, Python вызовет магический метод __str__(),
если он есть в этом классе.
• Метод __repr__() используется для создания однозначных,
машиночитаемых строковых представлений объекта. Если
клиентский код вызывает встроенную функцию repr()
и передает объект, Python попытается вызвать магический
метод __repr__() в этом классе, если он есть.
Я покажу метод __str__(), так как он более широко используется для простой отладки. Когда вы вызываете функцию
print(), Python вызывает встроенную функцию str(), чтобы
преобразовать каждый аргумент в строку. Для аргумента, у которого нет метода __str__(), эта функция форматирует строку, которая содержит тип объекта, слова «object at» и адрес
памяти, затем возвращает итоговую строку. Вот почему мы видим выше выходные данные, содержащие адрес памяти.
Вместо этого вы можете написать собственную версию
__str__() и заставить ее воспроизводить любую строку, которая необходима для отладки кода вашего класса. Общий подход
заключается в создании строки, содержащей значения всех переменных экземпляра, которые вы хотите видеть, и возвращающей эту строку для вывода на экран. Например, мы можем
добавить следующий метод в класс Vector из листинга 9.8, чтобы получить информацию о любом объекте Vector:
class Vector():
--- пропущены все предыдущие методы --def __str__(self):
return 'This vector has the value (' + str(self.x) + ', ' +
str(self.y) + ')'

Если создаете экземпляр Vector, вы затем можете вызвать
функцию print() и передать объект Vector:
Глава 9. Полиморфизм 289

oVector = Vector(10, 7)
print(oVector)

Вместо того чтобы просто выводить на экран адрес памяти
объекта Vector, вы получите хорошо отформатированный отчет о значениях двух переменных экземпляра, содержащихся
в объекте:
This vector has the value (10, 7)

Основной код в листинге 9.9 создает несколько объектов
Vector, проводит некоторые векторные вычисления и выводит
результаты некоторых вычислений Vector.
Файл: MagicMethods/Vectors/Vector.py
# Тестовый код Vector
from Vector import *
v1 = Vector(3, 4)
v2 = Vector(2, 2)
v3 = Vector(3, 4)

# Эти строки выводят булево выражение или числовые значения
print(v1 == v2)
print(v1 == v3)
print(v1 < v2)
print(v1 > v2)
print(abs(v1))
print(abs(v2))
print()
# Эти строки выводят объекты Vector (вызов метода __str__())
print('Vector 1:', v1)
print('Vector 2:', v2)
print('Vector 1 + Vector 2:', v1 + v2)
print('Vector 1 – Vector 2:', v1 – v2)
print('Vector 1 times Vector 2:', v1 * v2)
print('Vector 2 times 5:', v1 * 5)
Листинг 9.9. Образец основного кода, который создает и сравнивает Vectors,
производит математические действия и выводит Vectors на экран

290 Часть III. Инкапсуляция, полиморфизм и наследование

Код генерирует следующие выходные данные:
False
True
False
True
5.0
2.8284271247461903
Vector 1: This vector has the value (3, 4)
Vector 2: This vector has the value (2, 2)
Vector 1 + Vector 2: This vector has the value (5, 6)
Vector 1 – Vector 2: This vector has the value (1, 2)
Vector 1 times Vector 2: This vector has the value (6, 8)
Vector 2 times 5: This vector has the value (15, 20)

Первый набор вызовов print() выводит булево выражение
и числовые значения, которые являются результатом вызова
магических методов математического оператора и оператора
сравнения. Во втором наборе мы выводим два объекта Vector,
затем вычисляем и выводим на экран некоторые новые
Vectors. Внутренне функция print() сначала вызывает функцию Python str() для каждого выводимого на экран элемента;
это приводит к вызову магического метода Vector __str__(),
который создает отформатированную строку с соответствующей информацией.

Класс Fraction с магическими методами
Давайте объединим некоторые из этих магических методов
в более сложном примере. В листинге 9.10 показан код класса
Fraction. Каждый объект Fraction — дробь, которая состоит
из числителя (верхняя часть) и знаменателя (нижняя часть).
Класс отслеживает дроби, сохраняя отдельные части в переменных экземпляра наряду с примерным десятичным значением дроби. Методы позволяют вызывающему получить сокращенное значение дроби, вывести на экран дробь вместе с ее
значением с плавающей точкой, сравнить две дроби для выявления равенства и добавить два объекта Fraction.

Глава 9. Полиморфизм 291

Файл: MagicMethods/Fraction.py
# Класс Fraction
import math
class Fraction():
def __init__(self, numerator, denominator): 
if not isinstance(numerator, int):
raise TypeError('Numerator', numerator,
'must be an integer')
if not isinstance(denominator, int):
raise TypeError('Denominator', denominator,
'must be an integer')
self.numerator = numerator
self.denominator = denominator
# Используем математический пакет, чтобы найти наибольший
# общий делитель
greatestCommonDivisor = math.gcd(self.numerator,
self.denominator)
if greatestCommonDivisor > 1:
self.numerator = self.numerator // greatestCommonDivisor
self.denominator = self.denominator // greatestCommonDivisor
self.value = self.numerator / self.denominator
# Нормализуем знак числителя и знаменателя
self.numerator = int(math.copysign(1.0, self.value)) *
abs(self.numerator)
self.denominator = abs(self.denominator)
def getValue(self): 
return self.value
def __str__(self): 
'''Создаем строковое представление дроби'''
output = ' Fraction: ' + str(self.numerator) + '/' + \
str(self.denominator) + '\n' + \
' Value: ' + str(self.value) + '\n'
return output
def __add__(self, oOtherFraction): 
'''Складываем два объекта Fraction'''
if not isinstance(oOtherFraction, Fraction):
raise TypeError('Second value in attempt to add is not
a Fraction')
# Используем математический пакет, чтобы найти наименьшее
# общее кратное

292 Часть III. Инкапсуляция, полиморфизм и наследование

newDenominator = math.lcm(self.denominator,
oOtherFraction.denominator)
multiplicationFactor = newDenominator // self.denominator
equivalentNumerator = self.numerator * multiplicationFactor
otherMultiplicationFactor = newDenominator //
oOtherFraction.denominator
oOtherFractionEquivalentNumerator =
oOtherFraction.numerator * otherMultiplicationFactor
newNumerator = equivalentNumerator +
oOtherFractionEquivalentNumerator
oAddedFraction = Fraction(newNumerator, newDenominator)
return oAddedFraction
def __eq__(self, oOtherFraction): 
'''Проверяем на равенство'''
if not isinstance(oOtherFraction, Fraction):
return False # не сравнимо с дробью
if (self.numerator == oOtherFraction.numerator) and \
(self.denominator == oOtherFraction.denominator):
return True
else:
return False
Листинг 9.10. Класс Fraction, который реализует некоторые магические методы

Когда создаете объект Fraction, вы передаете числитель
и знаменатель , и метод __init__() сразу же вычисляет сокращенн ую дробь и ее значение с плавающей точкой. В любой
момент клиентский код может вызвать getValue(), чтобы извлечь это значение . Клиентский код может также вызвать
print(), чтобы вывести объект на экран, и Python вызовет метод __str__(), чтобы отформатировать строку для вывода
на экран .
Клиент может добавить два различных объекта Fraction
вместе с помощью оператора +. Когда это происходит, вызывается метод __add__() . Он использует метод math.lcd()
(наименьший общий знаменатель), чтобы убедиться, что у итогового объекта Fraction наименьший общий знаменатель.
И наконец, клиентский код может использовать оператор ==,
чтобы проверить, равны ли два объекта Fraction. Когда вы задействуете этот оператор, вызывается метод __eq__() ,
Глава 9. Полиморфизм 293

который проверяет значения двух Fraction и возвращает True
или False.
Ниже представлен код, который создает экземпляры объектов Fraction и тестирует различные магические методы:

# Тестовый код
oFraction1 = Fraction(1, 3) # создаем объект Fraction
oFraction2 = Fraction(2, 5)
print('Fraction1\n', oFraction1) # выводим на экран объект...
# вызываем __str__
print('Fraction2\n', oFraction2)
oSumFraction = oFraction1 + oFraction2 # вызываем __add__
print('Sum is\n', oSumFraction)
print('Are fractions 1 and 2 equal?', (oFraction1 == oFraction2))
# ожидаем False
print()
oFraction3 = Fraction(-20, 80)
oFraction4 = Fraction(4, -16)
print('Fraction3\n', oFraction3)
print('Fraction4\n', oFraction4)
print('Are fractions 3 and 4 equal?', (oFraction3 == oFraction4))
# ожидаем True
print()
oFraction5 = Fraction(5, 2)
oFraction6 = Fraction(500, 200)
print('Sum of 5/2 and 500/200\n', oFraction5 + oFraction6)

При запуске этот код выдает:
Fraction1
Fraction: 1/3
Value: 0.3333333333333333
Fraction2
Fraction: 2/5
Value: 0.4
Sum is
Fraction: 11/15
Value: 0.7333333333333333
Are fractions 1 and 2 equal? False

294 Часть III. Инкапсуляция, полиморфизм и наследование

Fraction3
Fraction: -1/4
Value: -0.25
Fraction4
Fraction: -1/4
Value: -0.25
Are fractions 3 and 4 equal? True
Sum of 5/2 and 500/200
Fraction: 5/1
Value: 5.0

Выводы
Эта глава была посвящена ключевому понятию ООП — полиморфизму. Проще говоря, полиморфизм — это возможность
нескольких классов реализовывать методы с одинаковыми именами. Каждый класс содержит конкретный код, который
делает все необходимое, чтобы были созданы экземпляры объектов из этого класса. В качестве демонстрационной программы я показал вам, как создать некоторое количество различных классов фигур, каждый из которых содержит методы
__init__(), getArea(), clickedInside() и draw(). Код
этих методов был разным, потому что специфичен для типа
фигуры.
Как вы видели, существует два ключевых преимущества при
использовании полиморфизма. Во-первых, он расширяет понятие абстракции до коллекции классов, давая программисту клиента возможность игнорировать реализацию. Во-вторых, он позволяет создать систему классов, которые работают
аналогичным образом, что делает систему предсказуемой для
программистов клиента.
Я также рассмотрел идею полиморфизма в операторах, объяснив, как один и тот же оператор может выполнять различные операции с различными типами данных. Я продемонстрировал, как для достижения этого используются магические
методы Python и как вы можете создать методы, чтобы реализовать эти операторы в собственных классах. Чтобы
Глава 9. Полиморфизм 295

продемонстрировать магические методы арифметических операторов и операторов сравнения, я показал класс Vector
и класс Fraction, а также продемонстрировал, как вы можете
использовать метод __str__() для помощи в отладке содержимого объекта.

10
Н АС ЛЕ ДОВА НИЕ

Третий принцип ООП — наследование, которое
представляет собой механизм для получения
нового класса из существующего. Вместо того
чтобы начинать с нуля и потенциально дублировать код, наследование позволяет программисту
писать код для нового класса, который расширяет возможности или отличает его от существующего класса.
Давайте начнем с примера из реального мира, который демонстрирует, что такое наследование. Вы посещаете кулинарную школу. Один из уроков включает исчерпывающую демонстрацию процесса приготовления гамбургеров. Вы узнаете все,
что можно узнать, о различных кусках мяса, измельчении мяса,
лучших типах булочек, лучших листьях салата, помидорах
и приправах — практически обо всем, что только можно было
себе представить. Вы также можете узнать о лучшем способе
приготовления гамбургера: насколько долго его готовить, когда и как часто переворачивать и так далее.
Следующий урок в учебном плане касается чизбургеров.
Преподаватель мог бы начать с нуля и пройти весь материал
по приготовлению гамбургеров снова. Но вместо этого он предполагает, что вы усвоили знания, полученные на предыдущих
Наследование 297

уроках, и уже знаете все, что необходимо знать для создания великолепного гамбургера. Следовательно, этот урок сосредоточится на типах сыра, которые необходимо использовать, когда
этот сыр добавлять, в каком количестве и так далее.
Смысл истории в том, что нет необходимости изобретать велосипед; вместо этого вы можете просто добавить новое к тому,
что уже знаете.

Наследование в объектно-ориентированном
программировании
Наследование в ООП представляет собой возможность создавать класс, который выстраивается на основании существующего класса (расширяет его). При создании больших программ вы часто будете использовать классы, которые
предоставляют очень полезные общие возможности. Иногда
вам захочется создать класс, похожий на уже существующий,
но делающий некоторые вещи немного по-другому. Наследование позволяет вам именно это: создать класс, который
включает все методы и переменные экземпляра существующего класса, но добавив новые и отличающиеся функциональные возможности.
Наследование — невероятно мощная концепция. Когда классы спроектированы правильно, использование наследования
кажется простым. Однако возможность проектировать классы,
чтобы использовать их понятным образом, — это навык, которым сложно овладеть. Как средство реализации наследование
требует много практики, чтобы использовать его правильно
и эффективно.
Используя наследование, мы поговорим о взаимоотношениях между двумя классами, которые обычно называются базовый
класс и подкласс.

Базовый класс
Класс, от которого наследуют; он служит отправной точкой для подкласса.

Подкласс
Класс, который наследует; он улучшает базовый класс.

298 Часть III. Инкапсуляция, полиморфизм и наследование

Хотя это наиболее распространенные термины для описания двух классов в Python, вы также можете услышать другие их
названия, такие как:
• суперкласс и подкласс;
• базовый класс и производный класс;
• родительский класс и дочерний класс.
На рис. 10.1 показана стандартная диаграмма, демонстрирующая эти взаимоотношения.
Базовый класс
Наследует от
Подкласс

Рис. 10.1. Подкласс наследует от базового класса

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

Базовый класс
Подкласс

Рис. 10.2. Базовый класс включен в подкласс

Как конструктор вы можете думать о базовом классе как
о включенном в подкласс. Таким образом, базовый класс фактически становится частью более крупного подкласса. Как клиент
подкласса вы думаете о подклассе как об отдельном структурном элементе, и вам не нужно знать, что существует еще базовый класс.
При обсуждении наследования мы часто говорим, что существуют отношения is a между подклассом и базовым классом.
Например, студент — это человек, апельсин — это фрукт, машина — это транспорт и так далее. Подкласс — это специализированная версия базового класса, которая наследует все свойства

Глава 10. Наследование 299

и поведение базового класса, но также предоставляет дополнительные детали и функциональнее возможности.
И самое важное — подкласс расширяет базовый класс одним
или обоими указанными ниже способами (которые будут вскоре объяснены).
• Подкласс переопределяет метод, который определен в базовом
классе. То есть подкласс может предоставить метод с тем же
именем, что и в базовом классе, но с другой функциональностью. Это называется переопределением метода. Когда клиентский код вызывает переопределенный метод, вызывается
метод в подклассе. (Однако код метода в подклассе все еще
может вызвать метод с тем же именем в базовом классе.)
• Подкласс может добавлять новые методы и переменные
экземпляра, которых нет в базовом классе.
Один из способов представить подкласс — с помощью фразы
кодирование по разнице. Поскольку подкласс наследует все переменные экземпляра и методы базового класса, ему не нужно повторять весь этот код; следовательно, подклассу необходимо
лишь содержать код, который отличает его от базового класса.
Поэтому код подкласса содержит лишь новые переменные экземпляра (и инициализацию), переопределяющие методы, и/
или новые методы, которых нет в базовом классе.

Реализуем наследование
Синтаксис наследования в Python прост и элегантен. Базовому
классу не нужно знать, что он используется как базовый.
Только подкласс должен указать, что он хочет наследовать
от базового класса. Ниже представлен общий синтаксис:
class ():
# Методы базового класса
class ():
# Методы подкласса

В операторе подкласса class внутри скобок вы указываете
имя базового класса, от которого он должен наследовать. В данном случае мы хотим, чтобы наследовал
300 Часть III. Инкапсуляция, полиморфизм и наследование

от базового класса . Ниже приведен пример с реальными именами классов:
class Widget():
# Методы Widget
class WidgetWithFrills(Widget):
# Методы WidgetWithFrills

Класс Widget обеспечит общие функциональные возможности. Класс WidgetWithFrills (виджет с оборочками) включит все из класса Widget и определит любые дополнительные
методы и переменные экземпляра, которые ему необходимы,
с более специфичными возможностями.

Пример работника и менеджера
Чтобы прояснить ключевые понятия, начну с очень простого,
а затем перейду к более сложным практическим примерам.
Базовый класс: работник
В листинге 10.1 определен базовый класс под названием
Employee.
Файл: EmployeeManagerInheritance/
EmployeeManagerInheritance.py
# Наследование Employee Manager
#
# Определяем класс Employee, который мы будем использовать как
# базовый класс
class Employee():
def __init__(self, name, title, ratePerHour=None):
self.name = name
self.title = title
if ratePerHour is not None:
ratePerHour = float(ratePerHour)
self.ratePerHour = ratePerHour
def getName(self):
return self.name
def getTitle(self):
return self.title
Глава 10. Наследование 301

def payPerYear(self):
# 52 недели * 5 дней в неделю * 8 часов в день
pay = 52 * 5 * 8 * self.ratePerHour
return pay
Листинг 10.1. Класс Employee, который будет использоваться как базовый класс

В классе Employee есть методы __init__(), getName(),
getTitle()и payPerYear(). В нем также есть три переменные
экземпляра self.name, self.title и self.ratePerHour,
которые устанавливаются в методе __init__(). Мы извлекаем
имя и название с помощью методов геттера. У этих работников
почасовая оплата, поэтому self.payPerYear()выполняет
вычисления, чтобы определить годовую оплату на основе почасовой ставки. В этом классе все должно быть вам знакомо; здесь
нет ничего нового. Вы можете создать экземпляр объекта
Employee сам по себе, и он будет работать прекрасно.
Подкласс: менеджер
Для класса Manager мы рассмотрим разницу между менеджером
и работником: менеджер — наемный работник с некоторым
количеством подчиненных. Если он хорошо выполняет свою
работу, то получает 10-процентный бонус за год. Класс Manager
может расширить класс Employee, поскольку менеджер является работником, но обладает дополнительными возможностями и обязанностями.
В листинге 10.2 показан код нашего класса Manager. Он должен содержать лишь код, который отличается от класса
Employee, поэтому вы не увидите в нем методов getName()
и getTitle(). Любые вызовы этих методов с объектом
Manager будут обрабатываться методами класса Employee.
Файл: EmployeeManagerInheritance/
EmployeeManagerInheritance.py
# Определяем подкласс Manager, который наследует от Employee
 class Manager(Employee):
def __init__(self, name, title, salary, reportsList=None):

self.salary = float(salary)
if reportsList is None:
reportsList = []
self.reportsList = reportsList

super().__init__(name, title)

302 Часть III. Инкапсуляция, полиморфизм и наследование



def getReports(self):
return self.reportsList



def payPerYear(self, giveBonus=False):
pay = self.salary
if giveBonus:
pay = pay + (.10 * self.salary) # добавляем бонус 10%
print(self.name, 'gets a bonus for good work')
return pay



Листинг 10.2. Класс Manager, реализованный как подкласс класса Employee

В операторе class  вы можете увидеть, что этот класс наследует от класса Employee, потому что Employee находится
внутри скобок после имени Manager.
Метод __init__() класса Employee ожидает имя, название
и дополнительную ставку за час. Менеджер является наемным
работником и управляет некоторым количеством работников,
поэтому метод __init__() класса Manager ожидает имя, название, зарплату и список работников. Придерживаясь принципа
кодирования по разнице, метод __init__() начинает с инициализации всего, что метод __init__() класса Employee
не делает. Следовательно, мы сохраняем salary и reportsList
в переменных экземпляра с одинаковыми именами .
Далее мы хотим вызвать метод __init__() базового класса
Employee . Здесь я вызываю встроенную функцию super(),
которая просит Python выяснить, какой класс является базовым (часто упоминается как суперкласс), и вызывает метод
__init__() этого класса. Она также настраивает аргументы,
чтобы они включали self в качестве первого аргумента в этом
вызове. Следовательно, вы можете рассматривать эту строку
как преобразование в:
Employee.__init__(self, name, title)

По сути, кодирование этой строки подобным образом будет
работать отлично; но использование вызова super()— гораздо
более чистый способ записи вызова без необходимости указания имени базового класса.
В результате новый метод __init__() класса Manager инициализирует две переменные экземпляра (self.salary
и self.reportsList), которые отличаются от находящихся
Глава 10. Наследование 303

в классе Employee, а метод __init__() класса Employee инициализирует переменные экземпляра self.name и self.title,
являющиеся общими для любого создаваемого объекта
Employee или Manager. Для Manager, у которого есть зарплата,
self.ratePerHour устанавливается в значение None .
ПРИМЕЧАНИЕ

Предыдущие версии Python требуют от вас написания этого
кода третьим способом, поэтому в старых программах и документации вы можете увидеть это:
super(Employee, self).__init__(name, salary)

Этот код выполняет абсолютно то же самое. Однако более
новый синтаксис с простым вызовом super()гораздо проще
запомнить. Использование super()также делает его менее подверженным ошибкам, если вы решите изменить имя вашего
базового класса.
В классе Manager есть добавленный метод геттера
getReports(), который позволяет клиентскому коду извле-

кать список Employee, подотчетных Manager. Метод
payPerYear() вычисляет и возвращает оплату Manager.
Обратите внимание, что в обоих классах Employee
и Manager есть метод с именем payPerYear(). Если вы вызовете метод payPerYear(), используя экземпляр Employee, будет
выполняться метод класса Employee, который вычисляет оплату на основании почасовой ставки. Если вы вызовете метод
payPerYear() для экземпляра Manager, задействуется метод
класса Manager, осуществлющий другое вычисление. Метод
payPerYear() в классе Manager переопределяет метод с тем же
именем в базовом классе. Переопределение метода в подклассе
указывает подкласс, чтобы отличать его от базового. Имя переопределяющего метода должно быть абсолютно таким же, как
имя переопределяемого метода (хотя у него может быть другой
список параметров). В переопределяющем методе вы можете:
• полностью заменить переопределяемый метод в базовом
классе. Мы видим это в методе payPerYear() класса Manager;
• выполнить некоторую работу самостоятельно и вызвать
наследуемый и переопределяемый метод с тем же именем
в базовом классе. Мы видим это в методе __init__() класса
Manager.
304 Часть III. Инкапсуляция, полиморфизм и наследование

Фактическое содержимое переопределяющего метода зависит от ситуации. Если клиент вызывает метод, которого не существует в подклассе, вызов метода будет отправлен в базовый
класс. Например, обратите внимание, что в классе Manager нет
метода с именем getName(), но он существует в базовом классе
Employee. Если клиент вызывает getName() для экземпляра
Manager, этот вызов обрабатывается базовым классом
Employee.
Метод payPerYear() класса Manager содержит следующий код:



if giveBonus:
pay = pay + (.10 * self.salary) # добавляем бонус 10%
print(self.name, 'gets a bonus for good work')

Переменная экземпляра self.name была определена в классе Employee, но в классе Manager она до этого не упоминалась.
Это демонстрирует, что определенные в базовом классе переменные экземпляра доступны к использованию в методах подкласса. Здесь мы вычисляем оплату менеджера, которая работает правильно, потому что у payPerYear() есть доступ
к переменным экземпляра, определенным внутри его собственного класса (self.salary), и к переменным экземпляра, определенным внутри базового класса (вывод на экран с использованием self.name ).
Тестовый код
Давайте протестируем наши объекты Employee и Manager
и вызовем методы каждого из них.
Файл: EmployeeManagerInheritance/
EmployeeManagerInheritance.py
# создаем объекты
oEmployee1 = Employee('Joe Schmoe', 'Pizza Maker', 16)
oEmployee2 = Employee('Chris Smith', 'Cashier', 14)
oManager = Manager('Sue Jones', 'Pizza Restaurant Manager',
55000, [oEmployee1, oEmployee2])
# вызываем методы объектов Employee
print('Employee name:', oEmployee1.getName())
print('Employee salary:', '{:,.2f}'.format(oEmployee1.payPerYear()))
print('Employee name:', oEmployee2.getName())
print('Employee salary:', '{:,.2f}'.format(oEmployee2.payPerYear()))

Глава 10. Наследование 305

print()
# вызываем методы объекта Manager
managerName = oManager.getName()
print('Manager name:', managerName)
# Даем менеджеру бонус
print('Manager salary:', '{:,.2f}'.format(oManager.payPerYear(True)))
print(managerName, '(' + oManager.getTitle() + ')', 'direct reports:')
reportsList = oManager.getReports()
for oEmployee in reportsList:
print(' ', oEmployee.getName(),
'(' + oEmployee.getTitle() + ')')

Когда выполняем этот код, мы видим следующие выходные
данные, как мы и ожидали:
Employee
Employee
Employee
Employee

name: Joe Schmoe
salary: 33,280.00
name: Chris Smith
salary: 29,120.00

Manager name: Sue Jones
Sue Jones gets a bonus for good work
Manager salary: 60,500.00
Sue Jones (Pizza Restaurant Manager) direct reports:
Joe Schmoe (Pizza Maker)
Chris Smith (Cashier)

Представление клиента о подклассе
До настоящего момента обсуждение было сосредоточено
на деталях реализации. Но класс может выглядеть по-разному в зависимости от того, являетесь ли вы разработчиком
класса или пишете код для его использования. Давайте изменим фокус и посмотрим на наследование с точки зрения клиента. Что касается клиентского кода, подкласс обладает
всеми функциональными возможностями базового класса
плюс все, что определено самим подклассом. Может оказаться полезным представить итоговую коллекцию методов
в виде слоев краски на стене. Когда клиент просматривает
класс Employee, он видит все определенные в этом классе
методы (рис. 10.3).

306 Часть III. Инкапсуляция, полиморфизм и наследование

Employee
_init_()
Клиент
getName()
getTitle()
payPerYear()

Рис. 10.3. Что увидит клиент, просматривая интерфейс класса Employee

Когда мы вводим класс Manager, который наследует от класса Employee, это похоже на добавление краски, чтобы подправить места, где хотим добавить или изменить методы. Для методов, которые не нужно менять, мы просто оставляем старые
слои краски (рис. 10.4).
Manager

Employee

_init_()

_init_()

Клиент
getName()
getTitle()
payPerYear()

payPerYear()

getReports()

Рис. 10.4. Что увидит клиент, просматривая интерфейс класса Manager

Как разработчики мы знаем, что класс Manager наследует
от класса Employee и переопределяет некоторые методы. Как
клиент мы лишь видим пять методов. Клиенту не нужно знать,
что некоторые методы реализуются в классе Manager, а источник других находится в классе Employee.

Примеры наследования из реального мира
Давайте рассмотрим два примера наследования из реального
мира. Сначала я покажу вам, как создать поле ввода, которое
разрешает вам вносить только числа. Затем создам поле
вывода, которое форматирует денежные значения.

Глава 10. Наследование 307

InputNumber
В этом первом примере мы создадим поле ввода, которое разрешает пользователю вводить только числовые данные. В качестве общего принципа проектирования пользовательского
интерфейса гораздо лучше ограничить ввод, чтобы разрешить
пользователю вносить только правильно отформатированные
данные, вместо того чтобы разрешать любые входные данные
и проверять позднее их правильность. Ввод в это поле букв или
других символов, десятичных знаков и отрицательных чисел
не должен быть разрешен.
Пакет pygwidgets содержит класс InputText, который позволяет пользователю вводить любые символы. Мы напишем
класс InputNumber, чтобы в качестве входных данных разрешить лишь допустимые числа. Новый класс InputNumber унаследует большую часть своего кода от InputText. Нам только понадобится переопределить три метода InputText: __init__(),
handleEvent() и getValue(). В листинге 10.3 продемонстрирован класс InputNumber, который переопределяет эти методы.
Файл: MoneyExamples/InputNumber.py
# Класс InputNumber – разрешает пользователю вводить только числа
#
# Демонстрация наследования
import pygame
from pygame.locals import *
import pygwidgets
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
# Кортеж допустимых клавиш редактирования
LEGAL_KEYS_TUPLE = (pygame.K_RIGHT, pygame.K_LEFT, pygame.K_HOME,
pygame.K_END, pygame.K_DELETE, pygame.K_BACKSPACE,
pygame.K_RETURN, pygame.K_KP_ENTER)
# Допустимые клавиши для ввода
LEGAL_UNICODE_CHARS = ('0123456789.-')
#
# InputNumber наследует от InputText
#
class InputNumber(pygwidgets.InputText):
def __init__(self, window, loc, value='', fontName=None, 
fontSize=24, width=200, textColor=BLACK,

308 Часть III. Инкапсуляция, полиморфизм и наследование

backgroundColor=WHITE, focusColor=BLACK,
initialFocus=False, nickName=None, callback=None,
mask=None, keepFocusOnSubmit=False,
allowFloatingNumber=True, allowNegativeNumber=True):
self.allowFloatingNumber = allowFloatingNumber
self.allowNegativeNumber = allowNegativeNumber
# Вызываем метод __init__() нашего базового класса
super().__init__(window, loc, value, fontName, fontSize, 
width, textColor, backgroundColor,
focusColor, initialFocus, nickName, callback,
mask, keepFocusOnSubmit)
# Переопределяем handleEvent, чтобы фильтровать подходящие клавиши
def handleEvent(self, event): 
if (event.type == pygame.KEYDOWN):
# Если это не клавиша редактирования или числовая клавиша,
# игнорируем ее
# Значение Юникода присутствует только при нажатии клавиши
# вниз
allowableKey = (event.key in LEGAL_KEYS_TUPLE) or
(event.unicode in LEGAL_UNICODE_CHARS))
if not allowableKey:
return False
if event.unicode == '-': # пользователь ввел знак минус
if not self.allowNegativeNumber:
# Если нет отрицательных величин, не надо передавать
return False
if self.cursorPosition > 0:
return False # нельзя поместить знак минус после
# 1-го символа
if '-' in self.text:
return False # нельзя ввести второй знак минус
if event.unicode == '.':
if not self.allowFloatingNumber:
# Если нет плавающих точек, не передаем точку
return False
if '.' in self.text:
return False # нельзя ввести вторую точку
# разрешаем клавише перейти к базовому классу
result = super().handleEvent(event)
return result
def getValue(self): 
userString = super().getValue()
Глава 10. Наследование 309

try:
if self.allowFloatingNumber:
returnValue = float(userString)
else:
returnValue = int(userString)
except ValueError:
raiseValueError('Entry is not a number, needs to have at
least one digit.')
return returnValue
Листинг 10.3. InputNumber разрешает пользователю вводить только числовые данные

Метод __init__() разрешает те же параметры, что находятся в базовом классе InputText, плюс еще несколько . Он добавляет булевы выражения allowFloatingNumber, чтобы определить, можно ли позволить пользователю вводить числа
с плавающей точкой, и allowNegativeNumber, чтобы определить, можно ли вводить числа, начинающиеся со знака минус.
Оба по умолчанию принимают значение True, поэтому случай
по умолчанию разрешает пользователю вводить числа с плавающей точкой и как положительные, так и отрицательные
числа. Вы могли бы их использовать, чтобы ограничить пользователя, например, вводом только положительных целых чисел,
установив оба значения на False. Метод __init__() сохраняет
значения этих необязательных параметров в переменных экземпляра, затем вызывает метод __init__() базового класса
с помощью вызова super().
Значимый код находится в методе handleEvent(), который ограничивает допустимые клавиши до небольшого подмножества: числа от нуля до девяти, знак минус, точка (десятичная точка), Enter и несколько клавиш редактирования.
Когда пользователь нажимает клавишу, вызывается этот метод
и передается событие KEYDOWN или KEYUP. Сначала код удостоверяется, что нажатая клавиша принадлежит ограниченному
набору. Если пользователь нажимает клавишу не из этого набора (например, любую букву), мы возвращаем значение False,
чтобы обозначить, что не произошло ничего важного в этом
виджете и что клавиша игнорируется.
Метод handleEvent() проводит еще несколько проверок,
чтобы убедиться, что вводимые числа допустимы (например,
не содержат двух точек, содержат лишь один знак минус и так
далее). Каждый раз, когда обнаруживается нажатие допустимой
310 Часть III. Инкапсуляция, полиморфизм и наследование

клавиши, код вызывает метод handleEvent() базового класса
InputText, чтобы выполнить все необходимое с этой клавишей
(отобразить или отредактировать поле).
Когда пользователь нажимает Return или Enter, клиентский код вызывает метод getValue(), чтобы получить ввод
пользователя. Метод getValue() в этом классе вызывает
getValue() в классе InputText, чтобы получить строку
из поля, затем пытается преобразовать эту строку в число. Если
это преобразование не удается, он вызывает исключение.
Переопределяя методы, мы создали очень эффективный новый класс многократного использования, который расширяет
функциональные возможности класса InputText, не меняя при
этом ни единой строки в базовом классе. InputText продолжит
функционировать как класс сам по себе без каких-либо изменений его функциональных возможностей.
DisplayMoney
В качестве второго примера из реального мира мы создадим
поле для отображения суммы денег. Для универсальности будем
отображать сумму с выбранным символом валюты, поместим
его слева или справа от текста (в зависимости от обстоятельств) и отформатируем число, добавив запятые между каждыми тремя цифрами, за которыми следует точка и затем две
десятичные цифры. Например, мы хотели бы отобразить
1234,56 доллара США в виде $1234,56.
В пакете pygwidgets уже есть класс DisplayText. Мы можем создать экземпляр объекта из этого класса, используя следующий интерфейс:
def __init__(self, window, loc=(0, 0), value='',
fontName=None, fontSize=18, width=None, height=None,
textColor=PYGWIDGETS_BLACK, backgroundColor=None,
justified='left', nickname=None):

Давайте предположим, что у нас некоторый код, который создает объект DisplayText с именем oSomeDisplayText, используя соответствующие аргументы. Каждый раз, когда хотим
обновить текст в объекте DisplayText, мы должны вызвать метод setValue() следующим образом:
oSomeDisplayText.setValue('1234.56')

Глава 10. Наследование 311

Функциональнее возможности отображения текста (в виде
строки) с помощью объекта DisplayText уже существуют. Мы
хотим создать новый класс с именем DisplayMoney, который
аналогичен DisplayText, но добавляет функциональность, поэтому мы наследуем от DisplayText.
В классе DisplayMoney будет улучшенная версия метода
setValue(), которая переопределяет метод базового класса
setValue(). Версия DisplayMoney внесет необходимое форматирование, добавляя символ валюты, запятые, дополнительно
сокращая до двух десятичных цифр, и так далее. В конце метод
вызовет унаследованный метод setValue() базового класса
DisplayText и передаст версию строки отформатированного
текста для отображения в окне.
Мы также добавим некоторые дополнительные параметры
установки в метод __init__(), чтобы клиентский код:
• выбирал символ валюты (по умолчанию $);
• помещал символ валюты слева или справа (по умолчанию
слева);
• отображал или скрывал два десятичных разряда (по умолчанию отображает).
В листинге 10.4 продемонстрирован код нашего нового класса DisplayMoney.
Файл: MoneyExamples/DisplayMoney.py
# Класс DisplayMoney – отображает число в виде денежной суммы
#
# Демонстрация наследования
import pygwidgets
BLACK = (0, 0, 0)
#
# Класс DisplayMoney наследует от класса DisplayText
#
 class DisplayMoney(pygwidgets.DisplayText):


def __init__(self, window, loc, value=None,
fontName=None, fontSize=24, width=150, height=None,
textColor=BLACK, backgroundColor=None,

312 Часть III. Инкапсуляция, полиморфизм и наследование

justified='left', value=None, currencySymbol='$',
currencySymbolOnLeft=True, showCents=True):






self.currencySymbol = currencySymbol
self.currencySymbolOnLeft = currencySymbolOnLeft
self.showCents = showCents
if value is None:
value = 0.00
# вызываем метод __init__() нашего базового класса
super().__init__(window, loc, value,
fontName, fontSize, width, height,
textColor, backgroundColor, justified)
def setValue(self, money):
if money == '':
money = 0.00
money = float(money)
if self.showCents:
money = '{:,.2f}'.format(money)
else:
money = '{:,.0f}'.format(money)
if self.currencySymbolOnLeft:
theText = self.currencySymbol + money
else:
theText = money + self.currencySymbol



# вызываем метод setValue нашего базового класса
super().setValue(theText)
Листинг 10.4. DisplayMoney отображает число, отформатированное в виде
денежного значения

В определении класса мы явно наследуем от pygwidgets.
DisplayText . Класс DisplayMoney содержит лишь два метода: __init__() и setValue(). Они переопределяют методы
с аналогичными именами в базовом классе.
Клиент создает экземпляр объекта DisplayMoney следующим образом:
oDisplayMoney = DisplayMoney(window, (100, 100), 1234.56)

С помощью этой строки метод __init__() в DisplayMoney 
выполнит и переопределит метод __init__() в базовом классе.
Глава 10. Наследование 313

Он выполняет некоторую инициализацию, включая сохранение любых клиентских предпочтений символов валюты, стороны отображения символа и отображения с точностью до сотых, все в переменных экземпляра . Метод заканчивается
вызовом метода __init__() базового класса DisplayText 
(который он находит, вызывая super()) и передает данные,
требуемые этим методом.
Позднее клиент, чтобы отобразить значение, выполняет вызов следующим образом:
oDisplayMoney.setValue(12233.44)

Метод setValue() в классе DisplayMoney выполняется,
чтобы создать версию суммы денег, отформатированную в виде
значения валюты. Метод завершается вызовом унаследованного метода setValue() в классе DisplayText , чтобы установить новый текст для отображения.
Когда идет вызов любого другого метода с экземпляром
DisplayMoney, выполняется версия, находящаяся
в DisplayText. И самое главное, каждый раз во время прохождения цикла клиентский код должен вызывать
oDisplayMoney.draw(), который рисует поле в окне. Поскольку в DisplayMoney нет метода draw(), этот вызов будет отправляться базовому классу DisplayText, в котором есть метод
draw().
Пример использования
На рис. 10.5 показаны выходные данные примера программы,
в которой используется как класс InputNumber, так и класс
DisplayMoney. Пользователь вводит число в поле InputNumber.
Когда он нажмет OK или Enter, это значение будет отображаться в двух полях DisplayMoney. Первое показывает число
с десятичными разрядами, второе округляет до ближайшего
доллара, используя различные исходные настройки.
В листинге 10.5 содержится весь код основной программы.
Обратите внимание, что код создает один объект InputNumber
и два объекта DisplayMoney.

314 Часть III. Инкапсуляция, полиморфизм и наследование

Рис. 10.5. Клиентская программа, в которой пользователь вводит сумму
в поле InputNumber и сумма отображается в двух полях DisplayMoney

Файл: MoneyExamples/Main_MoneyExample.py
# Денежный пример
#
# Демонстрирует переопределение унаследованных методов DisplayText
# и InputText
#1 – Импортируем пакеты
import pygame
from pygame.locals import *
import sys
import pygwidgets
from DisplayMoney import *
from InputNumber import *
#2 – Определяем константы
BLACK = (0, 0, 0)
BLACKISH = (10, 10, 10)
GRAY = (128, 128, 128)
WHITE = (255, 255, 255)
BACKGROUND_COLOR = (0, 180, 180)
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
FRAMES_PER_SECOND = 30

Глава 10. Наследование 315

#3 – Инициализируем окружение pygame
pygame.init()
window = pygame.display.set_mode([WINDOW_WIDTH, WINDOW_HEIGHT])
clock = pygame.time.Clock()
#4 – Загружаем элементы: изображения, звуки и т. д.
#5 – Инициализируем переменные
title = pygwidgets.DisplayText(window, (0, 40),
'Demo of InputNumber and DisplayMoney
fields', fontSize=36, width=WINDOW_WIDTH,
justified='center')
inputCaption = pygwidgets.DisplayText(window, (20, 150),
'Input money amount:', fontSize=24,
width=190, justified='right')
inputField = InputNumber(window, (230, 150), '', width=150)
okButton = pygwidgets.TextButton(window, (430, 150), 'OK')
outputCaption1 = pygwidgets.DisplayText(window, (20, 300),
'Output dollars & cents: ', fontSize=24,
width=190, justified='right')
moneyField1 = DisplayMoney(window, (230, 300), '', textColor=BLACK,
backgroundColor=WHITE, width=150)
outputCaption2 = pygwidgets.DisplayText(window, (20, 400),
'Output dollars only: ', fontSize=24,
width=190, justified='right')
moneyField2 = DisplayMoney(window, (230, 400), '', textColor=BLACK,
backgroundColor=WHITE, width=150,
showCents=False)
#6 – Бесконечный цикл
while True:
#7 – Проверяем наличие событий и обрабатываем их
for event in pygame.event.get():
# Если событием был щелчок по кнопке закрытия, выходим
# из pygame и программы
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# Нажатие Return/Enter или щелчок по OK приводит к действию
if inputField.handleEvent(event) or okButton.handleEvent(event): 
try:
theValue = inputField.getValue()
except ValueError: # любая оставшаяся ошибка
inputField.setValue('(not a number)')

316 Часть III. Инкапсуляция, полиморфизм и наследование

else: # ввод был ОК
theText = str(theValue)
moneyField1.setValue(theText)
moneyField2.setValue(theText)
#8 – Выполняем действия "в рамках фрейма"
#9 – Очищаем окно
window.fill(BACKGROUND_COLOR)
#10 – Рисуем все элементы окна
title.draw()
inputCaption.draw()
inputField.draw()
okButton.draw()
outputCaption1.draw()
moneyField1.draw()
outputCaption2.draw()
moneyField2.draw()
#11 – Обновляем окно
pygame.display.update()
#12 – Делаем паузу
clock.tick(FRAMES_PER_SECOND) #ожидание pygame
Листинг 10.5. Основная программа для демонстрации классов InputNumber
и DisplayMoney

Пользователь вводит число в поле InputNumber. Когда он печатает, любые неподходящие символы отфильтровываются и игнорируются методом handleEvent(). Когда пользователь щелкает по ОК , код считывает входные данные и передает их в два
поля DisplayMoney. Первое отображает сумму в долларах и центах (с двумя десятичными цифрами), в то время как второе показывает значение только в долларах. Оба добавляют $ в качестве
символа валюты и разделяют запятыми каждые три цифры.

Наследование нескольких классов от одного
базового класса
Несколько различных классов способны наследовать от одного
и того же базового класса. Вы можете создать самый общий
базовый класс, затем выстроить любое количество подклассов,
которые наследуют от него. Рис. 10.6 является представлением
этих взаимоотношений.
Глава 10. Наследование 317

Базовый класс
Наследует от

Подкласс

Подкласс

Подкласс



Рис. 10.6. Три и более различных подклассов, наследующих от общего
базового класса

Каждый из различных подклассов может быть затем вариантом (более специфичной версией) общего базового класса.
Каждый подкласс переопределяет любые желаемые или необходимые методы в базовом классе, независимо от других подклассов.
Давайте разберем пример, используя программу Shapes
из главы 9, которая создавала и рисовала круги, квадраты и треугольники. Код также разрешал пользователю щелкать по любой фигуре в окне, чтобы увидеть площадь этой фигуры.
Программа была реализована с тремя различными классами
фигур: Circle, Square и Triangle. Если мы вернемся к этим
трем классам, то обнаружим, что у каждого из них есть абсолютно одинаковый метод:
def getType(self):
return self.shapeType

Далее, рассматривая методы __init__() трех классов, мы
обнаружили некий общий код, который запоминает окно, выбирает произвольный цвет и произвольное местоположение:
self.window = window
self.color = random.choice((RED, GREEN, BLUE))
self.x = random.randrange(1, maxWidth – 100)
self.y = random.randrange(1, maxHeight – 100)

И наконец, каждый класс устанавливает переменную экземпляра self.shapeType в соответствующую строку.
Всякий раз, находя набор классов, который реализует абсолютно одинаковый метод и/или содержит общий код в методе
с общим именем, мы должны признать его хорошим кандидатом для наследования.

318 Часть III. Инкапсуляция, полиморфизм и наследование

Давайте извлечем общий код из трех классов и создадим общий базовый класс с именем Shape, продемонстрированный
в листинге 10.6.
Файл: InheritedShapes/ShapeBasic.py
# Класс Shape – базовый
import random
# Настраиваем цвета
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
class Shape():


def __init__(self, window, shapeType, maxWidth, maxHeight):
self.window = window
self.shapeType = shapeType
self.color = random.choice((RED, GREEN, BLUE))
self.x = random.randrange(1, maxWidth – 100)
self.y = random.randrange(25, maxHeight – 100)



def getType(self):
return self.shapeType
Листинг 10.6. Класс Shape для использования в качестве базового класса

Класс состоит только из двух методов: __init__() и
getType(). Метод __init__()  запоминает данные, переда-

ваемые в переменные экземпляров, затем произвольным образом выбирает цвет и начальное местоположение (self.x
и self.y). Метод getType() лишь возвращает тип фигуры,
заданной инициализацией.
Теперь мы можем написать любое число подклассов, которые наследуют от Shape. Создадим три подкласса, которые будут вызывать метод __init__() класса Shape, передавая строку, определяющую его тип и размер окна. Метод getType()
появится лишь в классе Shape, поэтому любой клиентский вызов getType() будет обрабатываться этим методом в унаследованном классе Shape. Мы начнем с кода класса Square, который продемонстрирован в листинге 10.7.

Глава 10. Наследование 319

Файл: InheritedShapes/Square.py
# Класс Square
import pygame
from Shape import *
class Square(Shape): 
def __init__(self, window, maxWidth, maxHeight):
super().__init__(window, 'Square', maxWidth, maxHeight) 
self.widthAndHeight = random.randrange(10, 100)
self.rect = pygame.Rect(self.x, self.y, self.widthAndHeight,
self.widthAndHeight)
def clickedInside(self, mousePoint): 
clicked = self.rect.collidepoint(mousePoint)
return clicked
def getArea(self): 
theArea = self.widthAndHeight * self.widthAndHeight
return theArea
def draw(self): 
pygame.draw.rect(self.window, self.color,
(self.x, self.y, self.widthAndHeight,
self.widthAndHeight))
Листинг 10.7. Класс Square, который наследует от класса Shape

Класс Square начинается с наследования от класса Shape .
Метод __init__() вызывает метод __init__() его базового
класса (или суперкласса) , определяя фигуру как квадрат
и произвольно выбирая ее размер.
Далее у нас идут три метода, реализация которых специфична для квадрата. Методу clickedInside() всего лишь требуется вызвать rect.collidepoint(), чтобы определить, был ли
щелчок внутри прямоугольника . Метод getArea() просто
умножает widthAndHeight на widthAndHeight . И наконец,
метод draw() рисует прямоугольник с помощью значения
widthAndHeight .
В листинге 10.8 продемонстрирован класс Circle, который
также был изменен, чтобы наследовать от класса Shape.

320 Часть III. Инкапсуляция, полиморфизм и наследование

Файл: InheritedShapes/Circle.py
# Класс Circle
import pygame
from Shape import *
import math
class Circle(Shape):
def __init__(self, window, maxWidth, maxHeight):
super().__init__(window, 'Circle', maxWidth, maxHeight)
self.radius = random.randrange(10, 50)
self.centerX = self.x + self.radius
self.centerY = self.y + self.radius
self.rect = pygame.Rect(self.x, self.y, self.radius * 2,
self.radius * 2)
def clickedInside(self, mousePoint):
theDistance = math.sqrt(((mousePoint[0] – self.centerX) ** 2) +
((mousePoint[1] – self.centerY) ** 2))
if theDistance