КулЛиб - Классная библиотека! Скачать книги бесплатно 

Основы программирования на python (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

С. А. Чернышев

основы
ПРОГРАММИРОВАНИЯ
НА PYTHON
УЧЕБНОЕ ПОСОБИЕ ДЛЯ ВУЗОВ

Рекомендовано Учебно-методическим отделом высшего образования в качестве

учебного пособия для студентов высших учебных заведений, обучающихся по ИТ

направлениям

Книга доступна на образовательной платформе «Юрайт» urait.ru,

а также в мобильном приложении «Юрайт.Библиотека»

Москва • Юрайт • 2022

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

УДК 004.432(075.8)
ББК 32.973.2я73
4-49
Автор:
Чернышев Станислав Андреевич — кандидат технических наук,
доцент кафедры интеллектуальных систем и защиты информации
Института информационных технологий и автоматизации СанктПетербургского государственного университета промышленных технологий
и дизайна, доцент кафедры информатики факультета информатики
и прикладной
математики
Санкт-Петербургского
государственного
экономического университета.
Рецензенты:
Гордеев А. В. — профессор, доктор технических наук, профессор кафедры
вычислительных систем и сетей Института вычислительных систем
и программирования Санкт-Петербургского государственного университета
аэрокосмического приборостроения;
Штеренберг С. И. — кандидат технических наук, доцент кафедры
защищенных систем связи факультета инфокоммуникационных сетей
Санкт-Петербургского
государственного
университета
и систем
телекоммуникаций имени профессора М. А. Бонч-Бруевича.

4-49

Чернышев, С. А.
Основы программирования на Python: учебное пособие для вузов /
С. А. Чернышев. — Москва : Издательство Юрайт, 2022. — 286 с. — (Высшее
образование). — Текст : непосредственный.

ISBN 978-5-534-14350-8
В курсе подробно описывается не только большое количество базовых
понятий и операторов языка программирования Python, но и ряд нюансов,
с которыми так или иначе предстоит встретиться при его использовании
в процессе написания программных продуктов. Материал подается
по принципу «от простого к сложному» и сопровождается большим
количеством примеров и упражнений, что позволяет сформировать
у студентов практические навыки программирования и тестирования
разрабатываемых приложений. Все исходные коды рассматриваемых
примеров можно скачать с репозитория автора на GitHub.
Соответствует актуальным требованиям федерального государственного
образовательного стандарта высшего образования.
Курс предназначен для студентов высших учебных заведений, которые
обучаются по инженерно-техническим направлениям,
УДК 004.432(075.8)
ББК 32.973.2я73

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

ISBN 978-5-534-14350-8

© Чернышев С. А., 2021
© ООО «Издательство Юрайт», 2022

постобработка: iCombo, 24.02.2022 (rutracker.org)

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Оглавление
Введение..................................................................................................................8
Тема 1. Краткая история Python, его особенности
и типы данных...................................................................................................12
1.1. Краткая история языка программирования Python........................... 12
1.2. В Python все является объектом............................................................. 16
1.3. В Python нет переменных........................................................................ 16
1.4. Интернированные (intern) объекты в Python.....................................25
1.5. Глобальная блокировка интерпретатора..............................................27
1.6. Подходы к сборке мусора в Python.........................................................30
1.6.1. Алгоритм подсчета ссылок............................................................ 31
1.6.2. Garbage Collector (GC).................................................................... 32
1.6.3. Слабые ссылки................................................................................33
1.7. Встроенные типы данных Python...........................................................35
1.7.1. Строки................................................................................................37
1.7.2. Списки.............................................................................................. 40
1.7.3. Словари............................................................................................ 44
1.7.4. Кортежи............................................................................................ 48
1.7.5. Файлы............................................................................................... 50
1.7.6. Множества........................................................................................54
Резюме................................................................................................................ 58
Вопросы и задания для самопроверки........................................................... 58
Упражнения....................................................................................................... 59

Тема 2. Синтаксис, операторы и управляющие конструкции... 62
2.1. Основные операторы в Python............................................................... 62
2.2. Использование отступов в Python.........................................................68
2.3. Комментарии............................................................................................. 69
2.4. Правила именования переменных (имен).......................................... 70
2.5. Оператор if..................................................................................................70
2.6. Цикл while..................................................................................................72
2.6.1. Работа цикла с операторами break, continue, pass................... 73
2.7. Цикл for....................................................................................................... 75
2.8. Различные способы написания циклов................................................ 77
2.8.1. Использование встроенной функции range.............................. 77
2.8.2. Использование встроенной функции zip...................................79
2.8.3. Использование встроенной функции enumerate..................... 80

3

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2.9. Итерации и включения............................................................................81
2.9.1. Итератор и итерируемый объект................................................ 81
2.9.2. Списковые включения (list comprehension).............................. 82
2.10. Источники документации Python...................................................... 85
Резюме................................................................................................................ 90
Вопросы для самопроверки.............................................................................. 91
Упражнения....................................................................................................... 92

Тема 3. Функции в Python.............................................................................94
3.1. Области видимости.................................................................................. 96
3.1.1. Области видимости и вложенные функции.............................. 97
3.1.2. Пример работы с областями видимости....................................98
3.1.3. Замыкания........................................................................................99
3.2. Аргументы функции............................................................................... 101
3.2.1. Значения аргументов по умолчанию....................................... 104
3.2.2. Режимы сопоставления аргументов функции........................ 105
3.3. Возвращение результатов выполнения функцией........................... 107
3.4. Рекурсия................................................................................................... 109
3.5. Аннотация функций............................................................................... 110
3.6. Лямбда-функции (выражения)............................................................ 112
3.7. Декораторы...............................................................................................113
3.8. Генераторы...............................................................................................117
3.8.1. Генераторные функции................................................................117
3.8.2. Генераторные выражения.......................................................... 120
Резюме.............................................................................................................. 121
Вопросы и задания для самопроверки.........................................................121
Упражнения..................................................................................................... 123

Тема 4. Модули и пакеты............................................................................125
4.1. Принцип работы импортирования......................................................126
4.1.1. Поиск файла подключаемого модуля....................................... 126
4.1.2. Компиляция импортируемого модуля в байт-код.................. 128
4.1.3. Выполнение кода модуля............................................................ 129
4.2. Создание и использование модулей................................................... 129
4.2.1. Пространства имен модулей.......................................................132
4.2.2. Перезагрузка модулей.................................................................134
4.2.3. Запуск модуля как автономной программы........................... 135
4.2.4. Внутренние имена в модулях.................................................... 136
4.3. Создание и использование пакетов модулей.................................... 138
Резюме.............................................................................................................. 141
Вопросы и задания для самопроверки......................................................... 141
Упражнения..................................................................................................... 142

Тема 5. Классы и объектно-ориентированное
программирование....................................................................................... 143
5.1. Определение класса............................................................................... 143
5.2. Имена (переменные) экземпляров класса........................................ 145

4

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

5.3. Методы экземпляра класса................................................................... 145
5.4. Имена (переменные) класса.................................................................146
5.5. Статические методы............................................................................... 148
5.6. Методы класса......................................................................................... 148
5.7. Приватные методы и переменные.......................................................149
5.8. Наследование.......................................................................................... 150
5.9. Множественное наследование............................................................ 155
5.10. Абстрактные классы и переопределение методов......................... 157
5.11. Перегрузка операций........................................................................... 159
5.11.1. Перегрузка_ add__ ,____ or ,__ sub...................................... 161
5.11.2. Перегрузка_ getitem__ и setitem..... ......................................161
5.11.3. Перегрузка_ iter__ и____ next................................................. 163
5.11.4. Перегрузка_ contains............ ..................................................... 167
5.11.5. Перегрузка_ getattr___ и setattr... .........................................167
5.11.6. Перегрузка_ repr__ и____ str.... .............................................. 169
5.11.7. Перегрузка_ call.... ..................................................................... 170
5.11.8. Перегрузка методов сравнения............................................... 172
5.11.9. Перегрузка_ 1еn__ и____ bool.... ............................................ 172
5.11.10. Декоратор ©property или свойства...................................... 173
5.12. Вложенные классы и пространство имен........................................ 174
5.13. Перечисления (Enum)..........................................................................176
5.13.1. IntEnum......................................................................................... 179
5.13.2. IntFlag........................................................................................... 180
5.13.3. Flag.................................................................................................180
5.13.4. Расширенные возможности перечислений........................... 181
Резюме.............................................................................................................. 182
Вопросы для самопроверки............................................................................ 183
Упражнения..................................................................................................... 183

Тема 6. Исключения (Exception).............................................................185
6.1. Пользовательские исключения............................................................ 186
6.2. Основы обработки и генерации исключений................................... 187
6.2.1. Унифицированный оператор try/except/finally......................188
6.2.2. Оператор raise............................................................................... 191
6.2.3. Оператор assert............................................................................. 193
6.2.4. Оператор with/as и протокол управления контекстами.....193
6.3. Встроенные классы исключений......................................................... 195
Резюме.............................................................................................................. 196
Вопросы и задания для самопроверки......................................................... 197
Упражнения..................................................................................................... 197

Тема 7. Потоки, процессы и асинхронное
программирование....................................................................................... 199
7.1. Многопоточное программирование................................................... 199
7.1.1. Модуль threading и класс Thread................................................ 200
7.1.2. Потоки Timer................................................................................. 201
5

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

7.1.3. Класс RLock. Управление доступом к ресурсам...................... 202
7.1.4. Синхронизация потоков............................................................. 204
7.1.5. Семафоры.......................................................................................206
7.2. Multiprocessing......................................................................................... 208
7.2.1. Модуль multiprocessing и класс Process..................................... 208
7.2.2. Взаимодействие между процессами......................................... 209
7.2.3. Пул процессов................................................................................ 212
7.3. Асинхронное программирование........................................................213
7.3.1. Планирование времени вызова функций................................ 215
7.3.2. Асинхронное получение результатов....................................... 216
7.3.3. Параллельное выполнение задач..............................................217
7.4. Что и когда использовать?.................................................................... 218
Резюме.............................................................................................................. 219
Вопросы и задания для самопроверки......................................................... 219
Упражнения..................................................................................................... 219

Тема 8. Разработка графического пользовательского
интерфейса...................................................................................................... 221
8.1. Установка PySide2.................................................................................. 221
8.2. Основы разработки GUI........................................................................ 222
8.2.1. Hello World..................................................................................... 222
8.2.2. QPushButton и QLabel...................................................................223
8.2.3. QLineEdit........................................................................................ 224
8.2.4. QCheckBox..................................................................................... 226
8.2.5. Компоновка элементов GUI........................................................228
8.3. Пользовательские виджеты и сигнал-слотовый механизм............ 231
8.4. Использование Qt Designer для разработки GUI.............................. 236
Резюме.............................................................................................................. 240
Вопросы и задания для самопроверки......................................................... 241
Упражнения..................................................................................................... 241

Тема 9. Сетевое программирование.....................................................243
9.1. Архитектура «клиент-сервер» ............................................................. 245
9.1.1. Логика работы без установления соединения........................ 245
9.1.2. Логика работы с установлением соединения..........................246
9.2. Модуль socket........................................................................................... 247
9.3. Пример клиента и сервера, работающих без установления
соединения............................................................................................... 248
9.4. Пример клиента и сервера, работающих с установлением
соединения............................................................................................... 250
9.5. Фреймворк для сетевых серверов — socketserver............................ 252
9.5.1. TCP эхо-сервер с использованием TCPServer..........................252
9.5.2. UDP эхо-сервер с использованием UDPServer........................ 253
Резюме.............................................................................................................. 254
Вопросы и задания для самопроверки.........................................................254
Упражнения..................................................................................................... 255

6

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 10. Хранение данных и обмен данными.................................256
10.1. Создание базы данных SQLite............................................................ 256
10.2. Использование переменных в запросах.......................................... 260
10.2.1. Запросы с позиционными параметрами............................... 260
10.2.2. Запросы с именованными параметрами............................... 261
10.3. Транзакции............................................................................................ 261
10.4. Уровни изоляции (доступа)............................................................... 265
Резюме.............................................................................................................. 268
Вопросы для самопроверки........................................................................... 269
Упражнения..................................................................................................... 269

Тема 11. Тестирование................................................................................270
11.1. Тестирование с использованием библиотеки PyTest..................... 271
11.1.1. Простые тесты.............................................................................271
11.1.2. Запуск одного или нескольких тестов.................................... 273
11.1.3. Запуск подмножества тестов................................................... 274
11.1.4. Использование фикстур............................................................ 276
11.1.5. Параллельный запуск тестов................................................... 279
11.2. Тестирование с использованием unittest......................................... 279
11.2.1. Пример простого теста............................................................. 279
11.2.2. Использование объектов-подделок....................................... 280
Резюме.............................................................................................................. 281
Вопросы и задания для самопроверки......................................................... 281
Упражнения..................................................................................................... 281

Список используемых источников........................................................282
Новинки по дисциплине «Программирование
на языке Python» и смежным дисциплинам................................... 286

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Введение
Данная книга познакомит вас с основами языка программиро­
вания Python. Конечно, в одной книге невозможно рассмотреть все
аспекты и нюансы этого языка программирования, но автор про­
делал значительную работу, стараясь максимально осветить его ба­
зовые возможности.
Все исходные коды приведенных в книге примеров можно ска­
чать из репозитория автора: https://github.com/MADTeacher/
python_basics. Для их запуска вам понадобится open-source экоси­
стема Anaconda с версией Python 3.8 или старше1, которая включает
в себя такой инструмент, как Jupyter Notebook, а также интегриро­
ванная среда разработки PyCharm2. Их установка, настройка и ис­
пользование не представляют сложности, поэтому и не рассматри­
ваются в данной книге, но если у вас возникнут какие-то трудности,
то в сети интернет достаточно материала для их решения.
Это сделано специально, так как в процессе работы в любой орга­
низации (да даже над упражнениями данной книги) вам все равно,
так или иначе, придется осуществлять поиск нужной информации
в интернете. Неважно, какую позицию в дальнейшем придется за­
нимать, от умения быстро найти ту информацию, которая позво­
лит решить возникающие проблемы, будет зависеть очень многое.
И именно в этом месте кроется первая ловушка, в которую попа­
дают начинающие разработчики — они бездумно копируют най­
денный код и переносят его в рабочий проект. Пусть на начальных
стадиях это и поможет двигаться быстрыми шагами, но вы даже
не представляете, какую бомбу замедленного действия подклады­
ваете всем участникам проекта. Конечно, есть code review, одной
из задач которого является поиск таких моментов. Но все мы люди
и имеем свойство ошибаться. Особенно, когда трещат сроки проек­
та и приходится постоянно задерживаться на работе, чтобы успеть
в обозначенный договором срок. К тому же в такие моменты за­
частую не до code review. И мало того, если сборка проекта будет
падать с указанием ошибок на ваш кусок кода, вы не сможете отве­
тить на вопрос: «Что этот код в действительности делает?» (а не ка­
1 Она доступна для скачивания по ссылке: https://www.anaconda.com/products/
individual.
2 https://www.jetbrains.com/ru-ru/pycharm/download/

8

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

кую вашу проблему он решил). Поэтому, если хотите перенести
найденный код в проект, не копируйте его, а перепечатывайте вруч­
ную. Да, это медленнее, но это позволит вам иметь представление,
что же на самом деле происходит (или найти ошибки), а не верить
на слово тому человеку, который выложил этот код в открытый
доступ.
Теперь коснемся упражнений к разделам. Упражнения к темам
с 1 по 7 предназначены для выполнения одним человеком. Вы и да­
лее, конечно, можете продолжать выполнять упражнения в оди­
ночку, но лучше скооперироваться с кем-нибудь и выполнять их,
как минимум, в паре. Этот опыт вам пригодится в дальнейшем, по­
скольку разработка современных информационных систем ведется
командой, а не одиночками. Таким образом, работая в паре, вы на­
учитесь:
1) декомпозировать задачи, чтобы их можно было выполнять
параллельно, а после интегрировать в одно приложение;
2) работать в команде, что подразумевает постоянное обсужде­
ние возникающих трудностей и нахождение компромисса относи­
тельно их решения. Так, например, для начала попробуйте догово­
риться о стиле кодирования, которого будете придерживаться (или
возьмите за основу РЕР 8);
3) тайм-менеджменту, а именно предварительной оценке необ­
ходимого времени для решения задач и упражнения в целом. Не пе­
реживайте, если поначалу не будет получаться. Все дело в опыте
(и ваша задача — его получить).
Само собой, это еще не весь список того, какой опыт вы полу­
чите при выполнении упражнений с кем-то, а не в одиночку, так
как с собой договориться намного проще, чем с другим человеком.
В первой теме книги затрагиваются такие моменты, как: встро­
енные переменные, менеджмент памяти, глобальная блокировка
интерпретатора, интернированные объекты и зачем они нужны,
а также почему принято считать, что в Python нет переменных.
Вторая тема посвящена синтаксису Python. В ней рассматрива­
ется, какие операторы существуют и как используются, правила
именования переменных, уровни отступов, что такое ветвление
и циклы, для чего используется протокол итерации, как он работа­
ет и т. д.
В третьей теме речь пойдет о функциях и всем, что с ними свя­
зано: областях видимости, замыканиях, аннотациях, генераторах,
декораторах и т. д.
Четвертая тема приоткрывает завесу тайн над модулями и паке­
тами, описывает, как они импортируются и что при этом происхо­
дит, а также способы их создания.
В пятой теме рассматривается, что такое объектно-ориентиро­
ванное программирование и то, как его концепции (инкапсуляция,
9

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

наследование и полиморфизм) реализованы в Python. Какие суще­
ствуют различия между: классом и экземпляром класса, перемен­
ной (методом) класса и экземпляром класса и т. д.
В шестой теме затрагивается такой аспект всех современных
языков программирования, как исключения, которые являются вы­
сокоуровневым механизмом управления потоком выполнения и мо­
гут генерироваться интерпретатором или самим программистом
«вручную».
В седьмой теме обсуждаются многопоточное, многопроцессор­
ное и асинхронное программирование средствами языка програм­
мирования Python, их различия и ситуации, в которых их следует
использовать.
В восьмой теме рассматривается работа с библиотекой PySide2,
которая позволяет разрабатывать приложения с графическим поль­
зовательским интерфейсом и является официальной реализацией
кроссплатформенного фреймворка Qt для Python, развитие которой
поддерживается The Qt Company.
Девятая тема — сетевое программирование средствами Python,
базовые протоколы обмена данными с установлением и без уста­
новления соединения между клиентами и сервером.
В десятой теме рассматривается работа с встроенной реляцион­
ной базой данных — SQLite, средствами модуля sqlite3.
В одиннадцатой теме рассматриваются способы тестирования
разрабатываемых приложений на языке программирования Python
и некоторые инструменты для этого.
Хочу выразить огромную благодарность свой невесте, ученикам
(Антонову Александру, Быкову Алексею, Дуку Герману, Нагорных
Максиму и Твардовскому Георгию) за оказанную помощь и под­
держку в процессе написания данной книги.
В результате изучения материалов данного учебного пособия об­
учающиеся должны:

знать





стандартные модули языка программирования Python,
основные типы, структуры данных и особенности работы с ними,
синтаксис языка программирования,
ключевые особенности интерпретатора CPython;

уметь
• применять стандартные структуры данных при разработке приложе­
ний,
• разрабатывать многопоточные, асинхронные и многопроцессорные
приложения,
• разрабатывать приложения с графическим пользовательским интер­
фейсом,
• писать тесты для проверки правильности функционирования разраба­
тываемых приложений,

10

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

• разрабатывать приложения, устойчивые к возникающим ошибкам
в процессе их работы,
• разрабатывать приложения, способные взаимодействовать между со­
бой по сети;

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

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 1
КРАТКАЯ ИСТОРИЯ PYTHON,
ЕГО ОСОБЕННОСТИ И ТИПЫ ДАННЫХ
В данной теме будут рассматриваться история создания языка
программирования Python, актуальность на текущий момент време­
ни и различные нюансы его реализации, с которыми лучше позна­
комиться до того момента, как начнете на нем программировать.
К таким нюансам относятся: глобальная блокировка интерпретато ­
ра при написании многопоточных программ с разделяемыми ресур­
сами на CPU, способ хранения значений «переменных» и т. д.
Также рассматриваются базовые встроенные типы данных Python
и способы работы с ними.
В результате изучения данной темы обучающиеся должны:

знать
• историю развития языка программирования Python,
• встроенные типы, структуры данных и особенности работы с ними,
• ключевые особенности интерпретатора CPython;

уметь
• применять различные операции к типам и структурам данных,
• работать с переменными изменяемого и неизменяемого типа данных;

владеть
• навыками работы с переменными изменяемого типа данных,
• основными методами работы с существующими типами данных.

1.1. Краткая история языка программирования Python
Python — интерпретируемый язык программирования с неявной
динамической типизацией, первая версия которого вышла в 1991 г.
Автор языка — нидерландский программист Гвидо ван Россум
(рис. 1.1), в Python-сообществе более известный как «великодуш­
ный пожизненный диктатор» (Benevolent Dictator For Life (BDFL)),
полномочия которого он сложил с себя в июле 2018 г. Термин BDFL
применяется к основателю проекта, который оставляет за собой
право на окончательное принятие решения (добавление функцио­
нальности, развитие архитектуры проекта и т. д.). Название Python
12

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

язык программирования получил в честь комик-группы из Велико­
британии — Монти Пайтон (Monty Python).

Рис 7.7. Гвидо ван Россум

На текущий момент времени существует две версии языка про­
граммирования: python3 (версия 3.0 вышла в 2008 г., а на момент на­
писания пособия актуальная версия — 3.9) и python2 (версия 2.0 вы­
шла в 2000 г., а «актуальная» версия — 2.7). При этом pythonЗ в ряде
случаев обратно не совместим с предыдущей версией языка. Таким об­
разом, начиная с 2008 года Python-сообщество было разделено на два
лагеря, поскольку большинство компаний не имело возможности от­
казаться от legacy-кода и библиотек в тех проектах, которые были уже
запущены (поддерживались) и приносили прибыль, в угоду полному
переносу на python3. Начиная же с 2020 г. была официально пре­
кращена поддержка python2 [1], в связи с чем сообщество перестало
устранять критические ошибки и уязвимости в данной версии языка
программирования и полностью переключилось на развитие python3.
Согласно индексу ТЮВЕ [2], который рассчитывает популяр­
ность языка программирования на основе поисковых запро­
сов типа: « + "< language > programming"», в 2019 г. Python вышел
на 3-е место (и продолжает занимать его, см. рис. 1.2), уступая толь­
ко Java и С [3]. А по данным аналитической компании RedMonk,
которая ориентируется в своей работе на разработчиков программ­
ного обеспечения, в январе 2020 г. Python вышел на второе место
по популярности среди языков программирования, уступая только
JavaScript [4]. RedMonk же формирует свой индекс популярности
на основе базы данных GitHub Archive с ее перекрестной проверкой
по базам Stack Overflow.
13

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

30

Ratings (%)

25
20

15
10

5

0

2002

2004

2006

2008

2010

2012

2014

2016

2018

2020

Рис. 1.2. Популярность языков программирования с 2002 по 2020 г.,
согласно индексу TIOBE [3]

— Java; — С; — Python; — C+ + ; — С#; — Visual Basic;
— JavaScript; — PHP; — SQL; — R

По данным (рис. 1.3), представленным в аналитической статье
Stack Overflow (перевод статьи доступен в [5]), можно судить, что
рост популярности Python пришелся на период 2012—2014 гг. и свя­
зан в первую очередь с интересом разработчиков к сферам анали­
тики данных, нейронных сетей, машинного обучения и компьютер­
ного зрения. Так, например, библиотека для анализа и обработки
данных Pandas довольно быстро набирала популярность: в 2011 г.
на нее практически не приходилось никаких запросов из общего
трафика сервиса, однако на момент публикации статьи количе­
ство запросов возросло до 1 %. Такая же динамика наблюдается
по библиотекам numpy и matplotlib. Приведенная статистика сви­
детельствует о том, что развитие Python больше связано с областью
Computer и Data Science, а не с веб-разработкой.

0,75 %

views per month

% of Stack Overflow questions

Based on visits to Stack Overflow questions
from World Bank high-income countries
1,00 %

0,50 %

0,25 %

0,00 %

2012

2014

2016

2018

Time

Рис. 13. Процент от общего трафика запросов сервиса Stack Overflow по библиотекам
Python в период с 2012 по 2018 год

— Pandas; — Django; — Numpy; — Matlotlib; — Flask
14

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Такой рост популярности Python связан прежде всего с его про­
стотой, большим количеством различных библиотек, удобными
средствами развертывания и тестирования приложений. Это по­
зволяет в более короткие сроки проверить возникающие у разра­
ботчика идеи или разработать MVP (Minimum Viable Product) — ми­
нимально жизнеспособный продукт, который можно представить
потенциальным инвесторам или заказчикам стартапа.
Сама сборка проекта, написанного на Python, осуществляется
следующим образом. В момент запуска исходного кода осуществля­
ется его компиляция в «байт-код» (низкоуровневое и независимое
от платформы представление исходного кода), после чего он переда­
ется на исполнение «виртуальной машине» [6]. Это позволяет разра­
батываемой программе (написанному сценарию) выполняться бы­
стрее, особенно при последующих запусках. При компиляции кода
Python сохраняет его в промежуточные файлы с расширением «.рус»
(скомпилированный исходный файл .ру). До версии 3.2 эти проме­
жуточные файлы хранились в одном каталоге с проектом. Теперь
же для них в каталоге проекта создается подкаталог «__ pycache__ ».
При следующем запуске программы Python загрузит файлы «.рус»
и пропустит шаг компиляции при условии, что исходный код про­
екта не изменялся после того, как байт-код был сохранен, и запуска­
ется той же версией Python, которая создавала байт-код.
Виртуальная машина Python (Python Virtual Machine, PVM) пред­
ставляет собой большой закодированный цикл, проходящий по ин­
струкциям поступающего в нее байт-кода. PVM по своей сути — ис­
полняющий механизм, который всегда присутствует в виде части
системы Python и представляет собой компонент, который понастоящему выполняет подаваемые на вход сценарии. Именно PVM
и является последним звеном того, что называется «интерпретатор
Python».
На данный момент времени можно выделить следующие реали­
зации языка Python.
1. CPython — первоначальная и стандартная реализация Python,
поэтому букву «С», которая указывает на то, что CPython написан
на переносимом языке ANSI С, зачастую не опускают.
2. Jython — альтернативная реализация языка Python. Jython
компилирует исходный код Python в байт-код Java (бесшовная инте­
грация, позволяющая использовать сценарии Python в приложени­
ях, написанных на Java), после чего подает его на вход виртуальной
машине Java (Java Virtual Machine, JVM).
3. IronPython (Python для .NET) — реализация Python, схожая
по своей концепции c Jython, только для платформы .NET.
4. Stackless — реализация стандартного языка CPython, ориен­
тированная на параллелизм и предлагающая эффективные вариан­
ты многопроцессорной обработки выполняемого кода.
15

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

5. РуРу — реализация стандартной системы CPython, которая
ориентирована на производительность. РуРу предлагает быструю
реализацию Python с JIT-компилятором (just-in-time), способную вы­
полнять ненадежный код в защищенной среде. Помимо этого, дан­
ная реализация Python по умолчанию включает поддержку Stackless
Python.
В следующем разделе будут рассматриваться некоторые особен­
ности реализации интерпретатора CPython (3.7). Часть из них мож­
но встретить в других реализациях, а некоторые специфичны толь­
ко для данной реализации языка программирования.

1.2. В Python все является объектом
Как уже обсуждалось ранее, в Python все является объектом! Да­
вайте проверим это, используя следующий код:
i = 5
print(isinstance(i, object))
True
print(isinstance('Oo', object))
True
print(isinstance( [2, 4, 't’], object))
True
print(isinstance({'a': 3, 'b': 5}, object))
True
print(isinstance((2, 4, 'w'), object))
True
def test_func():
pass
print(isinstance(test_func, object))
True

Каждый объект Python хранит как минимум три вида данных:
— счетчик ссылок, который управляет временем жизни объекта
в памяти;
— тип: изменяемый или неизменяемый;
— фактическое значение, к которому можно обратиться по име­
ни объекта.

1.3. В Python нет переменных
Переменные в Python полностью отличаются от того, чем они
являются в C+ + или С. По факту их нет! Вместо переменных ис­
пользуются имена. Чаще всего, говоря о переменных, подразуме­
ваются именно имена, поэтому важно понимать разницу [7, 8]. Для
начала разберем, что такое переменные в рамках языка программи­
рования С и как они работают.
16

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

При выполнении кода типа:
int val = 15;

программа отрабатывает следующие этапы:
1) выделение памяти;
2) запись в выделенную память значения;
3) отображение того, что переменная с именем val указывает
на это значение.
В памяти это может выглядеть так, как показано на рис. 1.4.
val

Адрес
Значение

0х5а1
15

Рис 1.4. Хранение значения переменной в памяти в С++

Если в последующем переменной val присваивается новое зна­
чение:
val = 255;

то оно перепишет хранящееся по адресу 0х5а1 старое значение 15.
Теперь область памяти будет выглядеть так, как показано на рис. 1.5.
val

Адрес
Значение

0х5а1
255

Рис 1.5. Изменение значения переменной в С++

Это означает, что переменная val — изменяемая, и фактически
происходит работа с областью памяти, на которую она ссылается.
Если объявить новую переменную и инициализировать ее значе­
нием, хранящимся в переменной val, то будет выделено место под
новое число и по адресу выделенной памяти запишется значение,
хранящееся в val (рис. 1.6):
int new_val = val;
val

Адрес
Значение

0x5 al
255

new val
Адрес
0x5 а5
Значение
255

Рис 1.6. Объявление новой переменной в C++

Обратите внимание на тот факт, что произошло выделение па­
мяти под новую переменную, и она не ссылается на значение, хра­
17

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

нящееся по адресу переменной val, а сама хранит его. И перезапись
значения переменной new_val (рис. 1.7) можно производить без опа­
сения того, что изменится значение, хранящееся в переменной val:
new_val = 47;
val

Адрес
Значение

0x5 al
255

new val
Адрес
0x5 а5
Значение
47

Рис 1.7. Изменение значения новой переменной в С++

На текущем этапе запомните, что при изменении значения пере­
менной адрес, на который она ссылается, не поменялся. Это важно
для понимания того, как дела обстоят в Python (CPython).
Теперь на тех же самых примерах рассмотрим, что произойдет
при написании аналогичного кода на Python. При выполнении кода:
val = 15

интерпретатор CPython отработает следующие этапы:
1) создаст PyObject;
2) присвоит PyObject-y typecode;
3) в PyObject записывается значение 15;
4) создается имя val;
5) val указывает на новый PyObject;
6) счетчик ссылок PyObject-a увеличивается на 1.
PyObject характерен только для CPython и является базовой
структурой для всех объектов:
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;

Часть объектов расширяют данную структуру, добавляя необхо­
димые поля, но ob_refcnt (счетчик ссылок) и *ob_type (тип) должны
обязательно присутствовать. Тип объекта (и только он) определя­
ет, что можно делать с самим объектом, и не меняется в течение
его жизни (может поменяться только в крайне редких обстоятель­
ствах). Принцип работы счетчика ссылок и его место в Python будут
рассмотрены немного позже.
На рис. 1.8 и далее приводится общий вид, а не подробное описа­
ние расположения объектов и их атрибутов в области памяти.
18

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Python Name
val

PyObject
ob type
int
1
ob refcnt
value
15

Рис. 1.8. Хранение значения «переменной» в памяти в Python
Как можно заметить, имя переменной ссылается не на участок
памяти, в котором хранится значение, а на создаваемый объект
Python, который уже хранит присваиваемое ей в коде значение.
Если val присвоить новое значение:
val = 255

интерпретатор CPython отработает следующие этапы:
1) создаст PyObject;
2) присвоит PyObject-y typecode;
3) в PyObject записывается значение 255;
4) val указывает на новый PyObject;
5) счетчик ссылок нового PyObject-a увеличивается на 1;
6) счетчик ссылок старого PyObject-a уменьшается на 1.
Область памяти после этого будет выглядеть так, как показано
на рис. 1.9.

Python Name
val

PyObject
int
ob type
ob refcnt
1
value
15
PyObject
ob type
int
ob refcnt
1
value
255

Рис. 1.9. Изменение значения переменной в Python
Теперь val указывает на новый PyObject, который хранит значе­
ние 255. Это явно указывает, что «=» является не присваиванием,
а «привязкой» имени val к ссылке. Поскольку счетчик ссылок пре­
дыдущего объекта теперь равен нулю, он будет удален сборщиком
мусора.
Теперь посмотрим, что происходит при добавлении нового име­
ни new_val, которому присвоим val:
new_val = val

19

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В данном случае не будет создаваться новый объект, а имя пеш_
val будет ссылаться на уже существующий объект, на который ссы­
лается сам val, что увеличит счетчик ссылок этого объекта на 1:
Python Name
val

■>

Python Name
new val

PyObject
ob type
int
ob refcnt
2
value
255

Puc. 1.10. Объявление новой «переменной» в Python
и ее инициализация значением из старой «переменной»

Проверка имен на эквивалентность new_val is val вернет значе­
ние True, что явно указывает на то, что они ссылаются на один и тот
же объект.
Теперь прибавим к new_val число 3:
new_val += 3

Этот код уже вызовет цепочку с созданием нового объек­
та и теперь область памяти будет выглядеть так, как показано
на рис. 1.11.

Python Name
val
Python Name
new val

—>

PyObject
ob type
int
ob refcnt
1
value
255

—>

PyObject
ob type
int
1
ob refcnt
value
258

Рис. 1.11. Изменение значения новой «переменной» в Python

Данное поведение характерно для работы с неизменяемыми
типами данных. Для примера работы с изменяемым типом возь­
мем список, который инициализируется следующим набором
элементов:
my_list = [1, 3, 6]

Интерпретатор использует PyVarObject вместо PyObject для соз­
дания объектов переменной длины. От предыдущего он отличается
20

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

тем, что хранит еще и число вложенных в него элементов в пере­
менной ob_size. Структура PyVarObject представлена ниже:
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; // Количество элементов в объекте
} PyVarObject;

А с учетом того, что для каждого типа создаваемого объекта су­
ществует своя структура, список на уровне кода интерпретатора
на языке С описывается следующей структурой:
typedef struct {
PyVarObject ob_base;
PyObject **ob_item; // указатель на реальные данные
Py_ssize_t allocated;
} PyListObject;

Поле ob_item — массив указателей на PyObject, a allocated — зна­
чение, под какое количество элементов зарезервирован размер мас­
сива ob_item до его увеличения.
Отсюда можно проследить закономерность, что PyListObject явля­
ется расширением PyVarObject, который в свою очередь расширяет
функционал PyObject. Условный вид области памяти в случае работы
со списком представлен на рис. 1.12.
Python Name
PyListObject
list
ob type
ob refcnt
1
**ob item
ob size
3
allocated
4

PyObject
1
ob type
ob refcnt
1
value
1
PyObject
ob type
int
ob refcnt
1
value
3
PyObject
int
ob type
ob refcnt
1
value
6

Рис 1.12. Объявление и инициализация списка в Python
Теперь добавим в созданный список еще один элемент (рис. 1.13):
my_list.append('Оо')
21

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Python Name
my list

■>

PyListObject
ob type
list
ob refcnt
1
**ob item
ob size
4
allocated
4

■>

PyObject
ob type
int
ob refcnt
1
value
1

■>

PyOIbject
ob type
int
ob refcnt
0
value
3

>

PyObject
int
ob type
ob refcnt
1
value
6

■>

PyObject
int
ob type
ob refcnt
1
value
■Oo‘

Puc. 1.13. Добавление элемента в список

Или изменим значение элемента списка (рис. 1.14):
my_list[l] ='''
Python Name
my list

PyListObject
list
ob type
1
ob refcnt
" "ob item
ob size
4
allocated
4

PyObject
int
ob type
ob refcnt
1
value
1
PyObject
ob type
str
ob refcnt
1
1 1
value

PyObject
objype
int
ob refcnt 0
value
3

PyObject
int
ob type
1
ob refcnt
value
6
PyObject
ob type
str
ob refcnt
1
value
'Oo'

Рис. 1.14. Изменение значения элемента списка

При проведении данных операций новый объект не создавался
(только на верхнем уровне, на уровне элементов списка создава­
лись новые объекты), и работа производилась с первоначальным
PyObject на который ссылается имя val. Далее посмотрим, как пове­
22

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

дет себя программа при добавлении нового имени и присваивании
ему my_list (рис. 1.15):
bad_list = my_list

Python Name
my list

■>

Python Name —I
bad list

PyListObject
ob type
list
2
ob refcnt
**ob item
ob size
4
allocated
4

PyObject
int
ob type
ob refcnt
1
value
1
PyObject
ob type
str
ob refcnt
1
1
1
value
PyObject
ob type
int
ob refcnt
1
value
6
PyObject
str
ob type
ob refcnt
1
value
'Oo'

Рис 1.15. Связывание нового имени с существующим списком

Теперь рассмотрим, как изменится область памяти при изменении
первого объекта во вновь объявленном списке bad_list (рис. 1.16):
bad_list[0] = 5

или добавлении еще одного элемента (рис. 1.17):
bad_list.append(3.6)
Python Name
my list

-r^

Python Name bad list

PyListObject
ob type
list
ob refcnt
2
**ob_ite m
ob size
4
allocated
4

PyObject
int
ob type
ob refcnt
1
value
5

PyObject
int
ob type
ob refcnt
0
1
value

PyObject
ob type
str
ob refcnt
1
1
1
value
PyObject
int
ob type
1
ob refcnt
value
6
PyObject
ob type
str
ob refcnt
1
value
'Oo'

Рис 1.16. Изменение значения элемента списка
23

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Python Name
my list

■>

Python Name —1
bad list

PyListObject
ob type
list
ob refcnt
2
**ob item
ob size
5
allocated
8

PyObject
ob type
int
ob refcnt
1
value
5
->

PyObject
str
ob type
1
ob refcnt
1
1
value

->

PyObject
ob type
int
ob refcnt
1
value
'Oo'

■>

PyObject
ob type
float
ob refcnt
1
value
3.6

Puc. 1.17. Добавление элемента в список

В связи с этой особенностью изменяемых типов данных необ­
ходимо с особой осторожностью подходить к операции присваива­
ния, так как имени присваивается ссылка насуществующий объект
и при изменении элементов это не отображается на верхнем уров­
не, с которым происходит «связывание» имени. Если есть необхо­
димость работать с копией изменяемого типа данных, то операцию
копирования необходимо вызывать явно:
# поверхностное копирование
bad_list = my_list.сору()

При этом следует учитывать тот факт, что когда объект изменя­
емого типа хранит (как один из своих элементов) другой объект
изменяемого типа, необходимо использовать глубокое, а не поверх­
ностное копирование. Это связано с тем, что при поверхностном ко­
пировании сохранится ссылка на вложенный объект, а не его копия:
# поверхностное копирование
my_list = [2, 3, 4, [1, 4]]
print(my_list) # [2, 3, 4, [1, 4]]
bad_list = my_list.copy()
bad_list.append(10)
print(bad_list) # [2, 3, 4, [1, 4], 10]
print(my_list) # [2, 3, 4, [1, 4]]
bad_list[3].append('Oo')
print(bad_list) # [2, 3, 4, [1, 4, 'Oo'], 10]
print(my_list) # [2, 3, 4, [1, 4, 'Oo']]
# глубокое копирование
import copy
24

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

my_list = [2; 3, 4, [1, 4]]
print(my_list) # [2, 3, 4, [1, 4]]
bad_list = copy.deepcopy(my_list)
bad_list.append(10)
print(bad_list) # [2, 3, 4, [1, 4], 10]
print(my_list) # [2, 3, 4, [1, 4]]
bad_list[3].append('Oo')
print(bad_list) # [2, 3, 4, [1, 4, 'Oo'], 10]
print(my_list) # [2, 3, 4, [1, 4]]

1.4. Интернированные (intern) объекты в Python
Интернированный объект — это объект, копия которого хранит­
ся в единичном экземпляре в памяти программы. Так, например,
при старте интерпретатора Python в памяти предварительно созда­
ется подмножество таких объектов, доступ к которым можно полу­
чить через глобальное пространство имен.
В CPython к интернированным объектам относят:
— целые числа в диапазоне от -5 до 256;
— строки (< 20 символов), которые содержат только: ASCIIбуквы, цифры или знаки подчеркивания.
Это сделано из-за того, что переменные с такими значениями
часто используются при разработке программ. А путем их интерни­
рования Python исключает выделение дополнительной памяти под
объекты, которые хранят аналогичное значение. Вместо этого име­
ни переменной присваивается ссылка на уже имеющийся в памяти
объект с этим значением.
Давайте разберемся, для чего осуществляется интернирование
объектов с некоторыми значениями. Как вам известно, переменная
типа int (int х = 15;) в C+ + занимает в памяти 4 байта (32 бита).
А сколько тогда в байтах будет занимать в памяти следующая за­
пись на CPython (3.7)?
import sys
х = 15 # объем в байтах?
print(sys.getsizeof(x)) # 28
у = 15
print(x is у)
# True # х и у ссылаются на один и тот же объект
# объем в байтах пустого объекта?
print(sys.getsizeof(object())) # 16

Даже пустой объект в CPython занимает в 4 раза больше объема
в памяти, нежели переменная типа int в языке программирования
C+ + . А разница записи intx= 15; (С++)сх = 15 (CPython) 28/4 = 7раз
(размер объекта может варьироваться от реализации Python)!!!
25

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Теперь приведем ряд примеров для более подробного погруже­
ния в принципы работы интернирования в Python:
х = 320
у = 320
print(f'Xid = {id(x)}; Yid =
print(x is y)
# Xid = 1754587687760; Yid =
# False
X = 25
у = 24
print(f'Xid = {id(x)}; Yid =
print(x is y)
# Xid = 140721606730896; Yid
# False
у += 1
print(f'Xid = {id(x)}; Yid =
print(x is y)
it xid = 140721606730896; Yid
# True
x = 540
у = 539
у += 1
print(f'Xid = {id(x)}; Yid =
print(x is y)
# Xid = 1754587686992; Yid =
# False

{id(y)}')

1754587687856

(id(y)}')

= 140721606730864
{id(y)}')

= 140721606730896

{id(y)}')

1754587687408

Если приведенный выше пример исполняется в IDE Pycharm,
а не в Jupyter, то х = 320 и у = 320 при их проверке на эквивалент­
ность х is у вернут True. Это связано с тем, что компилятор CPython
выполняет оптимизацию, которая помогает по мере возможности
экономить память и шаги исполнения кода.
Далее рассмотрим пример интернирования строк:
strl = "Itsfantastic"
str2 = "Itsfantastic"
print(f'Slid = (id(strl)}; S2 = {id(str2)}')
# Slid = 1754574647408; S2 = 1754574647408
print(strl is str2)# True

Но если в строку вставить пробел, одиночные кавычки (' ') или
вопросительный знак, то получится другой результат:
strl = "Its fantastic"
str2 = "Its fantastic"
print(f'Slid = (id(strl)}; S2 = {id(str2)}')
# Slid = 1754583817968; S2 = 1754582600688
print(strl is str2) # False

Это различие следует запомнить! Итерируются только строки,
которые удовлетворяют следующим условиям: строка состоит толь­
ко из ASCII-букв, цифр или знаков подчеркивания, а ее длина мень­
ше 20 символов.
26

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Для того, чтобы strl и str2 ссылались на один и тот же интерни­
рованный объект, используется функция intern() модуля sys:
import sys
strl = sys.intern("It's fantastic!")
str2 = sys.intern("It's fantastic!")
print(f'Slid = (id(strl)}; S2 = {id(str2)}')
# Slid = 1754558281968; S2 = 1754558281968
print(strl is str2) # True

В таком случае при интернировании строки обязательно исполь­
зовать sys.intern() для каждого имени переменной, иначе будет соз­
дан новый объект, и проверка выдаст следующий результат:
import sys
strl = sys.intern("It's fantastic!")
str2 = "It's fantastic!"
print(f'Slid = (id(strl)}; S2 = {id(str2)}')
# Slid = 1754558281968; S2 = 1754580540080
print(strl is str2) # False

Интернирование строк позволяет слегка повысить производи­
тельность при поиске по словарю. Когда ключ в словаре и искомый
ключ интернированы, то поиск (сравнение хешированных ключей)
осуществляется с помощью сравнения указателей, а не строк.

1.5. Глобальная блокировка интерпретатора
Глобальная блокировка интерпретатора (Global Interpreter Lock,
GIL) — глобальный механизм интерпретатора CPython, который на­
правлен на решение проблемы, возникающей при работе с разделяе­
мыми ресурсами (например, памятью) [9]. Эта проблема может воз­
никать, когда два потока пытаются одновременно изменить данные
в одном и том же ресурсе (файле, объекте и т. д.). Это чревато тем,
что в результате ни один из потоков не выполнит как подобает своей
работы, а данные в разделяемом ресурсе будут пребывать в «хаосе».
В качестве аналогии можно привести пример с двумя писателями
и одной записной книгой. Каждый из писателей делает запись когда
захочется, но книга-то у них одна, и заранее не обговорен порядок,
в котором каждый будет производить в нее запись. Пока запись произ­
водится по очереди, нет ничего страшного. Когда же писатели захотят
сделать запись в книгу одновременно, произойдет коллапс! В том, что
получится, не удастся разобраться даже самому привередливому кри­
тику! Что уже говорить об обычных читателях или самих писателях.
GIL же в приведенном примере выступит в качестве арбитра, ко­
торый будет решать, какой из писателей получит возможность про­
извести запись в книгу. Да так, что другой писатель никак не смо­
жет этому помешать или же начать запись в книгу в тот же момент
27

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

времени и получит доступ к книге только тогда, когда первый писа­
тель завершит свою запись.
В CPython для достижения этой цели механизм GIL блокирует
весь интерпретатор. Таким образом, ничто не в силах помешать ра­
боте текущего потока, то есть GIL позволяет работать только одно­
му потоку даже в многопоточном приложении. Это обстоятельство
порождает неутихающие споры в Python-сообществе. По своей сути
GIL представляет собой мьютекс. А сам расчет блокировки произ­
водится в основном цикле построения байткода CPython, для того
чтобы установить, какой поток в текущий момент времени должен
выполнять инструкции.
Первая попытка улучшения GIL была предпринята в Python 3.2
путем добавления механизма подсчета потоков, которые нужда­
ются в GIL. Поскольку обычно приоритет отдавался процессорным
потокам, это отражалось на работе потоков ввода-вывода, которые
не могли получить доступ к GIL. После добавления данного улучше­
ния раз в 5 миллисекунд GIL останавливает текущий поток и дает
отработать следующему в очереди потоку (даже если его нет).
Вторая попытка заключается в том, что процессы CPython мо­
гут иметь несколько интерпретаторов, а, следовательно, несколько
блокировок (у каждого своя). Это позволяет использовать несколь­
ко интерпретаторов кода в рамках одного процесса, которые будут
иметь свои собственные GIL. На текущий момент времени эта функ­
циональность пребывает в черновом варианте и проходит обсужде­
ния (РЕР 554) [10]. Если судить из описания документа, то предва­
рительный анонс данного улучшения намечен в Python 3.9.
А пока приходится использовать следующие обходные пути при
работе с GIL, когда он вызывает проблемы:
1) использование процессов вместо потоков. Несмотря на то,
что происходит повышение производительности по сравнению
с многопоточной версией, на управление процессами также расхо­
дуется процессорное время. Это связано с тем, что работа с несколь­
кими процессами более сложна, чем с несколькими потоками;
2) использование альтернативных интерпретаторов Python.
Для демонстрации всего вышесказанного приведем несколько
примеров. Сначала сделаем замер по времени выполнения следую­
щего кода в однопоточном исполнении:
import time
def countup(N):
n = 0
while n < N:
n += 1
if __ name__ == '__ main__ ':
st_time = time.time()
countup(30000000)
28

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

end_time = time.time()

print(f'Время выполнения: {end_time-st_time}')
# Время выполнения: 1.5710163116455078

Далее «разобьем» его на два потока:
import time
from threading import Thread
def countup(N):
n = 0
while n < N:
n += 1
if __ name__ == '__ main__ ':
max_for_thread = 30000000//2
first_thread = Thread(target=countup,
args=(max_for_thread,))
second_thread = Thread(target=countup,
args=(max_for_thread,))
st_time = time.time()
first_thread.start()
second_thread.start()
first_thread. join()
second_thread.join()
end_time = time.time()
print(f‘Время выполнения: {end_time-st_time}')
# Время выполнения: 1.5899989604949951

Как видно из времени выполнения, между ними практически нет
никакой разницы! Теперь попробуем использовать механизм рабо­
ты с потоками из PySide2 и PyQt5:
import time
VER_LIB = 'PyQt'
if VER_LIB == 'PyQt':
from PyQt5.QtCore import QThread
else:
from PySide2.QtCore import QThread
class TestQtThread(QThread):
def__ init__ (self, n, parent=None):
super(TestQtThread, self).__ init__ (parent)
self.N = n

def run(self):
n = 0
while n < self.N:
n += 1
if __ name__ == '__ main__ ':
max_for_thread = 30000000//2
st_time = time.time()
first_thread = TestQtThread(max_for_thread)
second_thread = TestQtThread(max_for_thread)
first_thread. start ()
second_thread.start()
29

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

first_thread.wait()
second_thread.wait()
end_time = time.time()
print(f'Время выполнения: {end_time - st_time}')
# PyQt5
Время выполнения: 6.59580135345459
# PySide2
Время выполнения: 2.4407031536102295

Поскольку и PySide2, и PyQt5 являются сторонними библиотека­
ми, использование их механизма работы с потоками накладывает
на программу дополнительные вычислительные затраты. Но сама
разница между выходными данными программы поражает. Офици­
альный порт Qt на Python (PySide2) дает куда лучший результат.
А теперь реализуем данный код на основе многопроцессного,
а не многопоточного подхода:
from multiprocessing import Pool
import time
def countup(N):
n = 0
while n < N:
n += 1
if __ name__ == '__ main__ ':
max_for_process = 30000000//2
process_pool = Pool(processes=2)
st_time = time.time()
first_process = process_pool.apply_async(countup,
[max_for_process])
second_process = process_pool.apply_async(countup,
[max_for_process])
process_pool.close()
process_pool.join()
end_time = time.time()
print(f‘Время выполнения: {end_time-st_time}')
# Время выполнения: 0.9137446880340576

Все плюсы и минусы каждого из подходов будут более подробно
рассматриваться позже. На текущий момент времени просто необ­
ходимо запомнить наличие GIL в CPython и то, на какой аспект ра­
боты программы он влияет.

1.6. Подходы к сборке мусора в Python
При написании кода на Python, как и в ряде других языков про­
граммирования (Java, C# и т. д.), нет необходимости волноваться
о сборке мусора (например, неиспользуемых объектов) и самой ра­
боте с памятью. Когда объекты становятся более не нужными, Python
автоматически начинает освобождение памяти из-под них [11—13].
При этом для работы с объектами, размер которых не превышает
512 байт, в Python используется дополнительный менеджер памя­
ти. Он выделяет, резервирует и очищает блоки памяти, в которых
30

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

может храниться множество таких объектов. В связи с этим, когда
происходит удаление объекта, объем памяти, которую он ранее за­
нимал, не переходит операционной системе, а остается зарезерви­
рованным для новых объектов. Если в одном из выделенных блоков
памяти менеджера больше нет объектов, то Python может вернуть
этот блок памяти под управление операционной системы. В основ­
ном такое происходит при наличии большого числа временных
объектов в локальной области видимости какой-либо функции или
в скрипте.
Из-за такого механизма управления памятью ситуация, когда
программа начинает использовать больше памяти, чем обычно,
не всегда означает наличие проблем с утечкой памяти.
Интерпретатор CPython использует сразу два подхода к управле­
нию высвобождением памяти:
1) подсчет ссылок;
2) Garbage Collector (GC).

1.6.1. Алгоритм подсчета ссылок
В Python существует два типа ссылок: сильные и слабые. Алго­
ритм подсчета ссылок работает только с сильным типом ссылок.
При данном подходе объект удаляется, как только счетчик ссылок
на него становится равным нулю. Если удаляемый объект содержит
ссылки на другие объекты, эти ссылки также удаляются, что может
повлечь за собой удаление других объектов. Так как на объект мо­
жет ссылаться множество имен, то увеличение значения счетчика
ссылок происходит при следующих операциях:
1) присваивание «=»;
2) передача аргументов (в метод, функцию и т. д.);
3) вставка нового элемента в последовательность;
Объекты, которые объявляются вне функций, классов и блоков,
имеют глобальную область видимости. Их жизненный цикл равен
времени жизни программы (процесса), а количество ссылок на та­
кие объекты в процессе работы никогда не падает до нуля.
Объекты, которые объявляются внутри блока (функции, класса),
имеют локальную видимость (т. е. они видны только внутри блока).
Их жизненный цикл не столь долог. Как только интерпретатор вы­
ходит из блока, он уничтожает все ссылки на локальные объекты.
Рассмотрим пример подсчета ссылок на объект в ходе выполне­
ния программы:
import sys
one = two = three = object()
print(sys.getrefcount(one)) # 4

В данном случае инициализируется пустой объект и связывается
с тремя разными именами: one, two и three. Каждое имя связывается
с объявленным объектом путем копирования ссылки на него. При
31

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

вызове функции, выводящей количество ссылок на данный объект,
вместо значения «3» выводится «4». Это связано со вторым прави­
лом увеличения счетчика ссылок, так как одно из имен выступило
в качестве аргумента для sys.getrefcountOТеперь имени three присвоим объект с другим значением:
three = 4
print(sys.getrefcount(one)) # 3

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

1.6.2. Garbage Collector (GC)

Основная задача GC — бороться с наличием циклических ссы­
лок, которые приводят к тому, что объекты продолжают оставаться
в памяти, даже когда становятся недоступны в коде.
Чтобы более подробно разобраться в том, как получаются цикли­
ческие ссылки, приведем следующий пример:
import ctypes
import gc
if __ name__ == '__ main__ ':
# используется для доступа к объектам по их адресу в памяти
class PyObject(ctypes.Structure):
_fields_ = [("refcnt", ctypes.c_long)]
gc.disable() # выключаем GC
my_list = []
my_list.append(my_list)
my_list_address = id(my_list)
del my_list
first_dict = {}
second_dict = {}
first_dict['second'] = second_dict
second_dict['first'] = first_dict
my_dict_address = id(first_dict)
del first_dict, second_dict
# gc.collect() # ручной запуск сборки мусора
# проверка счетчика ссылок
print(f‘Кол-во ссылок на список = '
f'{PyObject.from_address(my_list_address).refcnt}')
# Кол-во ссылок на список = 1
print(f'Кол-во ссылок на словарь = '
f'{PyObject.from_address(my_dict_address).refcnt}')
# Кол-во ссылок на словарь = 1

32

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

При включении GC количество ссылок, выводимых программой,
станет равно нулю. Важно понимать, что циклические ссылки по­
лучаются только в «контейнерных» структурах данных, таких как
класс, список, словарь, кортеж и т. д. Ввиду этого GC не следит за на­
личием циклических ссылок у неизменяемых объектов, за исключе­
нием кортежей (при выполнении ряда условий). Со всем остальным
спокойно справляется алгоритм подсчета ссылок.
Garbage Collector разделяет все объекты на 3 поколения (в зави­
симости от длины их времени жизни). Новые объекты сразу же клас­
сифицируются как первое поколение, и если один из них «выжива­
ет» в процессе сборки мусора, то он перемещается во второе. Чем
старше поколение, тем реже GC выполняет обход хранящихся в нем
объектов. Это связано с тем, что у новых объектов более короткий
жизненный цикл (временные объекты), чем у тех, которые уже
прошли через несколько этапов сборки мусора.
В каждом поколении ведется свой специальный счетчик и имеет­
ся свой порог срабатывания, при достижении которого начинается
процесс сборки мусора. Если сразу несколько поколений преодоле­
ли порог срабатывания, то из них выбирается наиболее старшее.
Это сделано из-за того, что старшие поколения также сканируют все
предыдущие.

1.6.3. Слабые ссылки

Как уже говорилось ранее, как только счетчик ссылок на объект
обращается в нуль, сборщик мусора уничтожает этот объект. Но бы­
вают случаи, когда полезно иметь такую ссылку на объект, которая
не удерживает его в памяти дольше, чем необходимо. Примером
ее использования может служить кэш.
Таким инструментом в Python являются слабые ссылки на объект
(за эту функциональность отвечает модуль weakref), которые не уве­
личивают его счетчик ссылок, то есть не препятствуют уничтоже­
нию объекта сборщиком мусора:
import weakref
class TestWeakRef:
def say(self, name="NoName"):
print(f'Hi, {name}!')
strong_ref = TestWeakRef()
weak_ref = weakref.ref(strong_ref)
weak_ref().say() # Hi, NoName!
weak_ref().say('Alex') # Hi, Alex!
print(weak_ref() is strong_ref)
# True
del strong_ref # удаляем сильную ссылку
print(weak_ref()) # None
33

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Как видно из последних строчек кода, после того, как произо­
шло удаление единственной сильной ссылки на экземпляр класса,
он уничтожился, а слабой ссылке присвоилось значение None!
В большинстве случаев при разработке программ хватает кол­
лекций из модуля wreakref (WeakKeyDictionary — словарь со слабы­
ми ссылками на ключи, WeakValueDictionary — словарь со слабыми
ссылками на значения, WeakSet) и функции finalize, избавляющих
программиста от необходимости вручную создавать и обрабаты­
вать экземпляры weakref.ref [14].
Не всякий объект в Python может быть объектом слабой ссыл­
ки. Например, объектом слабой ссылки могут быть подклассы,
но не сами экземпляры классов list и diet:
class MyList(list):
pass
my_list = MyList(range(10))
print(my_list)
# [0, 1, 2, 3} 4, 5, 6, 7, 8, 9]
ref_list = weakref.ref(my_list)
print(ref_list())
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

new_list = list(range(10))
ref_new_list = weakref.ref(new_list)
I

I

I

TypeError Traceback (most recent call last) in
8
9 new_list = list(range(10))
—> 10 ref_new_list = weakref.ref(new_list)

TypeError: cannot create weak reference to 'list' object
I

I

I

Встроенные неизменяемые типы данных также не поддержива­
ют слабые ссылки. Большинство из этих ограничений связаны с ре­
ализацией CPython, и к другим интерпретаторам Python, возможно,
и не относятся.
Также при объявлении объекта слабой ссылки вторым аргумен­
том weakref.ref() можно передать функцию, которая будет вызвана
при его финализации:
import weakref
class TestWeakRef(object):
def _del_(self):
print('Class Deleted')
34

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

def say(self, name="NoName"):
print(f'Hi, {name}!')
def callback(ref):
"""Invoked when referenced object is deleted"""
print("I'am freee!!!")
strong_ref = TestWeakRef()
weak_ref = weakref.ref(strong_ref, callback)
weak_ref().say() # Hi, NoName!
weak_ref().say('Alex') # Hi, Alex!
del strong ref
# Class Deleted
# I'am freee!!!
print(' weak_ref ():', weak_ref())
# weak_ref(): None

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

1.7. Встроенные типы данных Python
Поскольку в Python все является объектом, то не удивительно,
что, в отличие от ряда других языков программирования, помимо
пользовательских типов (классов) существуют такие типы данных,
как функция, файл, модуль, метод и скомпилированный код.
К стандартным (встроенным) типам Python относят [15]:
1) попе (аналог nullptr из C++);
2) булевы значения;
3) числа (Numeric Туре):
— int — целое число,
— float — число с плавающей точкой,
— complex — комплексное число;
4) списки (Sequence Туре):
— list — список,
— tuple — кортеж,
— range — диапазон;
5) строки (Text Sequence Type ) — str;
6) бинарные списки (Binary Sequence Types):
— bytes — байты,
— bytearray — массивы байтов,
— memoryview — специальные объекты для доступа к внутрен­
ним данным объекта через protocol buffer;
35

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

7) множества (Set Types):
— set — множество,
— frozenset — неизменяемое множество;
8) словари (Mapping Types) — diet.
Все типы данных в Python делятся на две категории: изменяемые
(mutable) и неизменяемые (immutable).
К неизменяемым типам данных относят: символьные строки,
числа, булевы значения, неизменяемые множества и кортежи.
Остальные типы данных относятся к изменяемым.
Разница заключается в том, что в случае объявления имени пере­
менной неизменяемого типа ей задается идентификатор, который
невозможно изменить. Приведем следующий пример:
п = 10
b = п
print('id n =',id(n))
print('id b =',id(b))

В данном случае id переменных будет одинаковый —
140719951553200, и если изменить значение имени переменной п,
то будет создан новый объект с новым значением, на который будет
ссылаться переменная п. Соответственно изменится и ее идентифи­
катор:
П = 5
print('id n =',id(n))
print('id b =',id(b))

Вывод данного сценария представлен ниже:
id п = 140719951553040
id Ь = 140719951553200

В случае работы с изменяемыми типами данных не будет созда­
ваться новый объект, а идентификатор переменной останется без
изменения. Рассмотрим данный аспект на примере работы со спи­
ском:
п = [1, 2]
b = п
print('id n
print('id b
b[0]=5
n[l]=4
print('id n
print('id b

=',id(n))
=',id(b))
=',id(n))
=',id(b))

Вывод данного сценария представлен ниже:
id n = 2216118937864
id b = 2216118937864
id n = 2216118937864
id b = 2216118937864
36

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Поскольку в Python динамическая типизация, можно не указы­
вать тип объявляемой переменной. Он выведется в процессе компи­
ляции программы в байт-код, и далее будет выполняться проверка,
чтобы операции производились над объектами одного типа (силь­
ная типизация). Начиная с версии 3.6 в Python появилась возмож­
ность использовать аннотацию типов. Таким образом, у программи­
стов появилась возможность задавать тип объявляемой переменной
(т. е. использовать статическую типизацию, но для проверки соот­
ветствия типа переменной с используемой аннотацией необходимо
использовать дополнительную библиотеку, например, туру [16]):
from typing import List, Diet
n : int = 4
b : float = 3.5
myList : Listfint] = [1,3,4]
myDict : Dictfstr, float] = {'sd':3.4,'rdl' :4.76}

Далее будут более подробно рассмотрены такие типы данных,
как: строки, список, словарь, кортежи и множества.

1.7.1. Строки
Строковый тип данных применяется для хранения текстовой
информации и произвольных значений байтов. Строки относятся
к так называемым последовательностям — позиционно упорядо­
ченным коллекциям объектов, в которых объекты располагаются
в порядке слева направо, сохраняют свои позиции и могут извле­
каться по их относительному номеру.
Таким образом строки представляют собой последовательность
из односимвольных строк (также к последовательностям в Python
относятся списки и кортежи) и поддерживают операции, которые
предполагают наличие позиционного порядка среди элементов: ин­
дексация, срез, конкатенация и повторение.
Для примера используем переменную строкового типа my_str,
инициализировав ее значением «Test»:
my_str = 'Test'
# также можно инициализировать переменную через
# двойные кавычки my_str = " Test"
print(len(my_str)) # Длина строки = 4
print(my_str[0]) # Т
# Выводим первый элемент my_str, так как индексация
# начинается с нуля

Индексация в Python представляет собой смещение относительно
первого (нулевого) элемента последовательности и может быть как
положительной (от 0 до п - 1), так и отрицательной (от -1 до -п):
print(my_str[-l]) # Первый элемент с конца = t
print(my_str[-3]) # Третий элемент с конца = е
37

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Помимо простой позиционной индексации, последовательности
поддерживают ее более общую форму — срез. Срез представляет
собой способ извлечения целой части за один шаг. В общем виде
концепцию среза можно представить как
Такая форма записи
обозначает, что будут извлечены значения из последовательности X,
начиная с позиции i до позиции;, не включая ее (т. е. j - 1):
print(my_str[l:3]) # es
# Срез my_str со смещения 1 до 2 включительно (не 3)

При использовании среза его левая граница по умолчанию при­
нимается равной нулю, а правая — длине нарезаемой последова­
тельности (то есть длине строки). Это приводит к нескольким рас­
пространенным вариантам применения:
# Вся строка my_str
print(my_str[:]) # Test
# Всё после первого элемента
print(my_str[l:]) # est
# Кроме последнего элемента
print(my_str[0:3]) # Tes
# То же, что и my_str[0:3]
print(my_str[:3]) # Tes
# Аналогична предыдущей записи
print(my_str[:-1]) # Tes

Помимо приведенного формата записи среза, можно указывать
еще и его шаг: X[v.j:nJ. Такая форма записи обозначает, что будут
извлечены значения из последовательности X, начиная с позиции
i до позиции;, не включая ее (т. е.; - 1), с шагом и:
# От первого до последнего элемента с шагом 1
print(my_str[::1]) # Test
# От первого до последнего элемента с шагом 2
print(my_str[::2]) # Ts
# Аналогична предыдущей записи
print(my_str[0:4:2]) # Ts
# От второго до последнего элемента с шагом 2
print(my_str[l::2]) # et
# Перевернуть последовательность
print(my_str[::-1]) tseT

Конкатенация и повторение последовательностей производятся
следующим образом:
#Конкатенация (значение my_str не изменяется)
print(my_str+'Ко') # TestKo
# Повторение (значение my_str не изменяется)
print(my_str * 3) # TestTestTest
# Конкатенация
print([1,2,3] + [4,5]) # [1, 2, 3, 4, 5]
# Повторение
print([l, 2, 3] * 3) # [1, 2, 3, 1, 2, 3, 1, 2, 3]
38

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Так как строки относятся к неизменяемому типу данных, их нель­
зя модифицировать, обращаясь по индексу элемента (на месте):
my_str[l] = 'Е'
I

I

I

...текст сообщения об ошибке...
File "F:/code/python/myProjRep/main.py", line 27, in
my_str[l] = E
TypeError: 'str' object does not support item assignment
I

I

I

Но можно создавать новые объекты:
my_str = my_str[l:] + 'E'
print(my_str)
estE

Имеется возможность модифицировать текстовые данные на ме­
сте. Для этого необходимо развернуть их в список индивидуальных
символов и объединить вместе с пустым разделителем или исполь­
зовать для этого тип bytearray:
my_str = 'Test'
# Развернуть в список: [. . . ]
my_list = list(my_str)
print(my_list) # ['Т', 'е', 's', 't'J
my_list[0] = 'F'
# Изменить на месте
# Объединить с пустым разделителем
print(’’ .join(my_list))
# Fest
# Гибрид байтов/списка
my_array = bytearray(b'Test')
my_array.extend(b'Ko')
print(my_array) # bytearray(b‘TestKo')
# Преобразовать в обычную строку
print(my_array.decode())
TestKo

Далее рассмотрим методы, которые специфичны для строкового
типа данных и не распространяются на последовательности. Дан­
ные методы возвращают измененную строку, но не модифицируют
ту, над которой производится операция. К ним относят:
— find;
— replace;
— split;
— upper;
— rstrip и m. d.
Рассмотрим их принцип работы:
my_str = 'Test'
# Поиск смещения подстроки в my_str
print(my_str.find('es'))
# 1
# Замена вхождения подстроки в my_str
# другой подстрокой
39

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

print(my_str.replace('es', ’ XFQ')) # T XFQt
print(my_str) # Test
line = 'ffff,ttt,nn,d'
# Разбить строку по разделителю ', ' в список подстрок
print(line.split(','))
[’ffff’, 'ttt', 'nn', 'd']
# Преобразовать символы строки в верхний регистр
print(my_str.upper()) # TEST
line = 'ffff,ttt,nn,d\n'
# Удалить пробельные символы с правой стороны
print(line.rstrip())# ffff ,ttt ,nn ,d

Методы можно чередовать:
# Замена подстроки в строке с последующим её
#разделением в список
print(line.replace('tt', 'XFQ').split(','))
['ffff
'XFQt
'nn
'd\n']

Более подробный список методов для работы с встроенными ти­
пами данных приведен в [17].

1.7.2. Списки
Списки — позиционно упорядоченные коллекции объектов про­
извольных типов, не имеющие фиксированного размера. В отличие
от строк списки можно модифицировать на месте путем присваива­
ния по индексу или вызовом некоторых списковых методов. Это по­
зволяет использовать списки как довольно гибкий инструмент для
представления произвольных коллекций, таких как: перечня про­
дуктов в магазине, учащихся в школе и т. д.
К основным характеристикам списков можно отнести то, что
они:
— являются упорядоченными коллекциями произвольных объ­
ектов;
— поддерживают доступ по смещению;
— имеют переменную длину, разнородны и допускают произ­
вольно глубокое вложение;
— относятся к категории «изменяемая последовательность»;
— представляют собой массивы ссылок на объекты.
Существует несколько способов создания пустого списка:
typel = []
type2 = list()

Если список необходимо инициализировать переменными в мо­
мент его объявления, используют следующие конструкции:
typel = ['one', 'two']
type2 = list(['one', 'two'])

40

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Добавление в список осуществляется через вызов у него метода
.append(vaZ), где val — добавляемое в конец списка значение:
my_list = list(['one', 'two'])
print(my_list) # ['one', 'two']
my_list.append('three')
print(my_list) # ['one', 'two', 'three']

При этом не обязательно, чтобы в списке содержались или в него
добавлялись данные одного типа:
my_list.append(4)
my_list.append(5)
print(my_list) # ['one', 'two', 'three', 4, 5]

Казалось бы, при объявлении типа посредством механизма анно­
тации проблема будет решена, и в случае добавления объекта дру­
гого типа произойдет ошибка:
my_new_list: List[int] = [4, 5]

Но нет:
my_new_list.append('add')
print(my_new_list) # [4, 5, 'add']

В отличие от C++ или Java ошибка будет выдана не на этапе
компиляции, а на этапе выполнения программы, если у элемента
списка нет вызываемого разработчиком метода. Например, выпол­
ним вычитание из каждого элемента списка по единице:
for it in my_new_list:
it = it-1

В таком случае будет выдана ошибка типа:
III
File "F:/code/python/myProjRep/main.py", line 12, in
it = it-1
TypeError: unsupported operand type(s) for
'str' and 'int'

III

Это связано с тем, что типы в Python определяются автоматически
во время выполнения, а не при их объявлении в коде. Получается,
что разработчик никогда не объявляет переменные заблаговремен­
но (эту концепцию проще уловить, если постоянно помнить о том,
что все сводится к переменным, объектам и связям между ними).
Если бы строковый тип поддерживал операцию вычитания, раз­
работчик мог бы так и не узнать о допущенной им ошибке. Давай­
те реализуем файл со сценарием Python «main.py», предварительно
скачав анализатор кода Python (линтер) для статической проверки
типов (pip install туру):
from typing import List

def foo(a: str, b: int):
return len(a) - b

41

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

my_new_list: List[int] = [4, 5]
my_new_list.append('add')
print(my_new_list)
for it in my_new_list:
it = it-1
print(foo(False, 'sdasd'))

Перед запуском кода сценария в терминале IDE (Integrated
Development Environment) выполним следующую команду:
(venv) F:\code\python\myProjRep>mypy main.py

Муру выполнит проверку подаваемого ему на вход файла с кодом
на соответствие типов объектов и выдаст следующие замечания, ко­
торые явно указывают на допущенные ошибки:
III
main.py:7: error: Argument 1 to "append" of "list" has
incompatible type "str"; expected "int"
main.py:11: error: Argument 1 to "foo" has incompatible type
"bool"; expected "str"
main.py:11: error: Argument 2 to "foo" has incompatible type
"str"; expected "int"
Found 3 errors in 1 file (checked 1 source file) '''

Конечно, такой подход имеет свои недостатки, но позволяет из­
бежать утомительных поисков в случае несоответствия типов объ­
ектов, которые хранятся в списках, словарях, подаются на вход
функции или возвращаются ею и т. д.
Помимо метода append(), который осуществляет добавление объ­
екта в конец списка, можно использовать метод .insert(i,val). Он осу­
ществляет добавление объекта в список на указанную позицию, где
все элементы, следующие за индексом i, сдвигаются вправо [18]:
names = ['Ivan', 'Maxim', ’Alex']
print(names) # ['Ivan', 'Maxim', ’Alex']
names.insert(1,'Ion')
print(names) # ['Ivan', 'Jon', 'Maxim', 'Alex']

Метод extend(L) добавляет существующие элементы из списка
L в правую часть текущего списка:
one = [1, 2]
two = [3, 4]
one.extend(b)
print(one) # [1, 2, 3, 4]

Удаление элемента списка можно осуществлять по номеру его
индекса, а также используя методы removeO или .pop([i]), где
i — индекс извлекаемого из списка элемента (если его не задавать,
то произойдет извлечение последнего элемента в списке). А метод
.с1еаг() (равносильно del my_list[:]) полностью очищает список:
# равносильно names.remove('Ivan') или names.pop(0)
del names[0]
print(names) # ['Jon', 'Maxim', 'Alex']
42

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Сортировка элементов списка осуществляется следующим обра­
зом:
names.sort()
print(names) # ['Alex', 'Ion', 'Maxim']

При этом необходимо запомнить, что при сортировке списка
с разнотипными элементами может произойти ошибка:
new_array = [2, 'abc', 'ttt', '10', 3.6]
new_array.sort()
File "F:/code/python/myProjRep/main.py", line 13, in
new_array.sort()
TypeError: '
my_tuple = (2, 3)
print(type(my_tuple)) #

К основным характеристикам кортежей можно отнести то, что
они:
— являются упорядоченными коллекциями произвольных объ­
ектов;
— поддерживают доступ по смещению;
— относятся к категории «неизменяемая последовательность»;
— имеют фиксированную длину, разнородны и допускают про­
извольно глубокое вложение;
— представляют собой массивы ссылок на объекты.
Как и рассмотренные ранее списки, кортежи (как последователь­
ности) поддерживают ряд общих методов работы с ними:
— индекс, индекс индекса, срез, длина (my_tuple[i], my_tuple[i]
[j], my_tuple[O:n], len(my_tuple));
— конкатенация, повторение (my_tuple + new_my_tuple, my_
tuple*3);
— итерация (for it in my_tuple: print (x));
— членство ('ff in my_tuple);
— подсчет (my_tuple.count(x)) и т. д.
В тех случаях, когда необходимо изменить значения объектов,
находящихся в кортеже, применяют следующий подход. На основе
существующего кортежа создается список, содержащий в себе объ­
екты, хранимые в кортеже. Далее производится работа со списком
48

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

и на последнем шаге из данного списка создается новый кортеж,
и ссылка на него присваивается переменной, через чье имя осу­
ществляется работа с исходным кортежем:
my_tuple = 2, 3, 4, 5
print(my_tuple) # (2, 3, 4, 5)
my_list = list(my_tuple)
print(my_list) # [2, 3, 4, 5]
my_list = list(map(lambda x: x*2, my_list))
print(my_list) # [4, 6, 8, 10]
my_tuple = tuple(my_list)
print(my_tuple) # (4, 6, 8, 10)

Если попытаться изменить значение элемента кортежа, то в про­
цессе работы программы будет выведена следующая ошибка:
my_tuple[0] = 3
1 " File "F:/code/python/myProjRep/main.py", line 10,
in
my_tuple[0] = 3
TypeError: 'tuple' object does not support item assignment ' "

Зачем же применять кортежи, если есть списки? Дело в том, что
неизменяемость кортежей обеспечивает определенную степень
целостности. То есть разработчик может быть уверен, что кортеж
не изменится через ссылку где-то в другом месте программы. Для
списков же такая гарантия отсутствует.
Поскольку номера полей в кортеже несут меньше осмысленной
информации, чем имена ключей в словаре, то можно использовать
именованные кортежи, лишенные данного недостатка. Для этого
необходимо из библиотечного модуля collections импортировать
класс namedtuple:
from collections import namedtuple
ItemRec = namedtuple(’ItemRec', ['name',
'age', 'jobs'])

my_tuple = ItemRec('Alex', age=21,
jobs=['student', 'developer'])
print(my_tuple)
# ItemRec(name='Alex' , age=21,
# jobs=['student', 'developer'])
print(my_tuple[0], my_tuple[2])
# Alex ['student', 'developer']
print(my_tuple.name, my_tuple.jobs)
# Alex ['student', 'developer']

my_dict = my_tuple._asdict()
print(my_dict['name'], my_dict['jobs'])
# Alex ['student', 'developer']
print(my_dict)
#OrderedDict([('name', 'Alex'), ('age', 21),
#
('jobs', ['student', 'developer'])])
49

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

1.7.5. Файлы

Файлы считаются основным типом, потому что создаются встро­
енной функцией ореп(), но отличаются от чисел и последователь­
ностей, а также не реагируют на операции выражений. Файлы пре­
доставляют только методы для решения распространенных задач
их обработки. Большинство файловых методов занимаются выпол­
нением ввода из внешнего файла и вывода в файл, ассоциирован­
ный с файловым объектом. Остальные методы позволяют перехо­
дить в новую позицию внутри файла, сбрасывать буферы вывода
ит. д.
Перечислим некоторые особенности работы с файлами в Python:
— файловые итераторы лучше всего подходят для чтения строк;
— содержимое является строками, а не объектами;
— файлы буферизируются и поддерживают позиционирование;
— вызов метода close() часто необязателен: файл автоматиче­
ски закрывается при сборке мусора.
Для открытия файла необходимо использовать встроенную функ­
цию ореп() с указанием пути и имени внешнего файла, а также ре­
жима обработки. Вызов возвращает файловый объект, имеющий
методы для передачи данных:
my_file = open (file_path_and_name, access_mode)

В качестве входного параметра access_mode можно указать один
из следующих режимов работы [19], приведенных в табл. 1.1.
Таблица 1.1
Список режимов открытия файла

Режим
доступа
Г

W

Описание

Только для чтения
Только для записи, или создается новый файл в случае его отсут­
ствия в каталоге

rb

Только для чтения (бинарный)

wb

Только для записи (бинарный или создается новый файл в случае
его отсутствия в каталоге)

г+

Для чтения и записи, которые производятся с начала файла

rb+

Для чтения и записи (бинарный)

W+

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

wb+

Для чтения и записи (бинарный), или создается новый файл
в случае его отсутствия в каталоге

50

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Окончание табл. 1.1

Режим
доступа

Описание

а

Откроет для добавления нового содержимого или создаст новый
файл в случае его отсутствия в каталоге. Добавление будет осу­
ществляться в конец открываемого файла

а+

Откроет файл для чтения или добавления нового содержимого
или создаст новый файл в случае его отсутствия в каталоге. По­
ток работы с файлом будет начинаться с конца открываемого
файла

ab

Откроет для добавления нового содержимого (бинарный) или
создаст новый файл в случае его отсутствия в каталоге. Добавле­
ние будет осуществляться в конец открываемого файла

ab+

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

В табл. 1.2 приведены основные методы для работы с файлами.
Таблица 1.2
Распространенные файловые операции

Метод

Описание

close ()

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

fileno()

Возвращает целочисленный дескриптор файла

flush ()

Сбрасывает буфер вывода на диск, не закрывая файл

isattyO

Возвращает True, если файл привязан к терминалу

next()

Возвращает следующую строку файла

read()

Читает целый файл в одиночную строку

read(n)

Чтение первые п символов файла

readline ()

Читает следующую строку файла (включая символ новой
строки \п) в строку

readlines ()

Читает целый файл в список строк (с символами \п)

seek(N)

Изменяет позицию на N для следующей операции

tell()

Возвращает текущую позицию в файле относительно его
начала

write (str)

Записывает строку str в файл

writelines (lines)

Записывает все строки из списка в файл

51

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Рассмотрим простой пример, демонстрирующий основные прин­
ципы работы с файлами:
myfile = open('myfile.txt', 'w')
myfile.write('Пятнадцать человек на сундук
мертвеца.\п')
myfile.write('Йо-хо-хо, и бутылка рому!\п')
myfile.close()
myfile = open('myfile.txt', 'r')
print(myfile.readline())
# Пятнадцать человек на сундук мертвеца.
print(myfile.readline())
# Йо-хо-хо, и бутылка рому!
print(myfile.readline())
# Пустая строка: конец файла
# Чтение сразу всего файла
print(open('myfile.txt', 'г').read())
# Пятнадцать человек на сундук мертвеца.
# Йо-хо-хо, и бутылка рому!

Дня построчной работы с файлом применяется подход, в котором
временный файловый объект, созданный функцией open, будет автома­
тически читать и возвращать одну строку на каждой итерации цикла:
myfile = open('myfile.txt', 'г')
for line in myfile:
print(line, end='')
# Пятнадцать человек на сундук мертвеца.
# Йо-хо-хо, и бутылка рому!

Теперь разберем процесс записи разнообразных объектов Python
в текстовый файл. Здесь следует обращать внимание, что объекты
должны быть преобразованы в строки с применением инструментов
преобразования. Зачастую данные файла являются строками, и мето­
ды записи не делают никакого их автоматического форматирования:
а, Ь, с = 43, 23, 45
mytext = 'Test'
mydict = { 'а' : 1, 'b' : 2}
mylist = [1, 2, 3]
myfile = open('datafile.txt' , 'w')
myfile.write(mytext + '\n')
# Преобразование чисел в строки
myfile.write('%s,%s,%s\n' % (a, b, c))
# Преобразование и разделение посредством $
myfile.write(str(mylist )+'$'+ str(mydict) + '\n')
myfile.close()

После того, как файл создан, мы можем просмотреть его содер­
жимое, открыв и прочитав в строковый объект:
print(open('datafile.txt', 'г').read())
# Test
# 43,23,45
# [1, 2, 3]${'a': 1, 'b': 2}
52

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Для полноценного считывания данных из файла нужно выпол­
нить следующие шаги:
myfile = open('datafile.txt', 'г')
mytext = myfile.readline()
# Удаление символа конца строки
mytext = mytext.rstrip()
print(mytext) # Test
my_numbers_str = myfile.readline()
# Разбиение (разбор) по запятым
my_numbers_str = my_numbers_str.split(',')
# Преобразование всего списка за раз
my_numbers = [int(it) for it in my_numbers_str]
#int(P) - приведение элемента списка к типу int
print(my_numbers) # [43, 23, 45]

Чтобы преобразовать список и словарь, сохраненные в третьей
строке файла, можно прогнать их через eval() — встроенную функ­
цию, которая трактует строку как порцию исполняемого программ­
ного кода (формально как строку, содержащую выражение Python):
mydict_and_list = myfile.readline().split('$')
print(mydict_and_list)
# [*[1, 2, 3]', "{'a': 1, 'b': 2}\n"]
print(eval(mydict_and_list[l])) # {'a': 1, 'b': 2}
objects = [eval(P) for P in mydict_and_list]
print(objects) # [[1, 2, 3], {'a': 1, 'b': 2}]

Поскольку конечным результатом всего разбора и преобразова­
ния является список объектов Python, а не строк, теперь к ним мож­
но применять списковые и словарные операции.
Применение метода eval() к строкам файла не является без­
опасным, так как он выполнит любое выражение Python, подавае­
мое на вход. Например, это может быть код, удаляющий все файлы
в каталоге. К еще одному небезопасному способу загрузки объек­
тов Python из файла относится стандартный библиотечный модуль
pickle. Этот модуль позволяет сохранять почти любой объект Python
в файл напрямую, не требуя каких-либо преобразований в строку.
В то же самое времянужно быть внимательным при извлечении
данных из файла, так как там может содержаться вредоносный код,
который выполнится на вашем компьютере:
import pickle
mydict = { 'а' : 1, 'b' : 2}
myfile = open('datafile.pkl', 'wb')
pickle.dump(mydict, myfile)
myfile.close()

Ниже приведен код для считывания словаря из файла:
import pickle
myfile = open('datafile.pkl', 'rb')
mydict = pickle.load(myfile)
print(mydict) # {'a': 1, 'b': 2}
53

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Модуль pickle выполняет сериализацию объектов (преобразова­
ние объектов в строки байтов и обратно) и требует совсем незначи­
тельного объема работы со стороны разработчика. Более подробно
про данный модуль и работу с ним можно прочитать в [20].
В случае работы с файлом из ненадежных источников рекомен­
дуется использовать более безопасные форматы сериализации, та­
кие как JSON [21, 22]:
import json
name = dict(first='Ivan', last='Ivanov')
my_dict = dict(name=name, job=['developer', 'student'],
age=21.5)
print(my_dict)
#{'name': {'first': 'Ivan', 'last': 'Ivanov'},
# 'job': ['developer', 'student'], 'age': 21.5}
json.dump(my_dict, fp=open('testjson.txt', 'w'),
indent=4)
"""print(open ('testjson.txt').read())
{
"name": {
"first": "Ivan",
"last": "Ivanov"
Ъ
"job": [
"developer",
"student"
Ъ
"age": 21.5
у

II II II

my_json_obj = json.load(open('testjson.txt'))
print(my_json_obj)
# {'name': {'first': 'Ivan', 'last': 'Ivanov'},
# 'job': ['developer', 'student'], 'age': 21.5}

Кроме того, в инструментальном наборе Python доступны допол­
нительные средства для работы с внешними файлами, это:
— стандартные потоки данных;
— дескрипторные файлы в модуле os;
— сокеты, конвейеры и очереди FIFO;
— файлы с доступом по ключу, поддерживаемые модулем shelve;
— потоки данных командной оболочки.
1.7.6. Множества
Множество — неупорядоченная совокупность объектов, в кото­
рой не может быть дубликатов. Как и кортеж, множество можно
создать на базе списка или других последовательностей, элементы
которых можно перебирать. Однако, в отличие от списков и корте­
жей, для множеств неважен порядок элементов. Множества часто
используются для двух целей: для удаления дубликатов и для про­
верки принадлежности. Так как механизм поиска основан на опти­
мизированной функции хеширования, реализованной для слова­
54

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

рей, операция поиска занимает очень мало времени даже для очень
больших множеств.
Объявить множество можно следующим способом:
mylist = [0, 1, 1, 2, 3, 9, 4, 5, б, б, 7, 8, 9]
my_set = set() # пустое множество
my_set = set(mylist)
my_set = {0, 1, 1, 2, 3, 9, 4, 5, б, б, 7, 8, 9}
print(my_set) # {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

При создании множества все дубликаты удаляются, что видно
из приведенного выше кода. Для добавления элементов используют
следующие методы:
my_set.add(102)
print(my_set) # {0, 1, 2, 3, 4, 5, б, 7, 8, 9, 102}
my_set.update([2, 100, 99, 4, 5, б])
# или my_set.update({2, 100, 99, 4, 5, б})
print(my_set)
# {0, 1, 2, 3, 4, 5, б, 7, 8, 9, 102, 100, 99}

Метод updateO принимает на вход последовательность (список
итерируемых неизменяемых объектов) и добавляет все элементы
к исходному множеству. Как можно заметить из результата выпол­
нения кода, повторяющиеся значения, добавляемые в множество,
игнорируются.
Удаление элементов из множества производится следующим об­
разом:
my_set.remove(2)
print(my_set)
# {0, 1, 3, 4, 5, б, 7, 8, 9, 102, 100, 99}
my_set.discard(100)
print(my_set)
# {0, 1, 3, 4, 5, 6, 7, 8, 9, 102, 99}

При этом следует учитывать следующие моменты поведения про­
граммы при удалении элементов из множества: в случае удаления
элемента, которого нет в множестве, метод .discardO, в отличие
от .remove (), не выдаст ошибки, и программа продолжит работать
дальше. Для примера повторим те же самые операции удаления
элементов из множества:
print(my_set)# {0, 1, 3, 4, 5, 6, 7, 8, 9, 102, 99}
my_set.discard(100)
print(my_set) # {0, 1, 3, 4, 5, б, 7, 8, 9, 102, 99}
my_set.remove(2)
print(my_set)
File "F:/code/python/myProjRep/main.py", line 13, in
my_set.remove(2)
KeyError: 2

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

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Python поддерживает обычные математические операции над
множествами [23, 24]:
Таблица 1.3
Операции над множествами

Операция

Описание

А | В
A.union(B)

Возвращает объединение множеств
АиВ

А |= В
A.update(B)

Добавляет в множество А все элемен­
ты из множества В

А&В
A. intersection(В)

Возвращает пересечение множеств
АиВ

А &= В
A.intersection_update(B)

Оставляет в множестве А только те
элементы, которые есть в множестве В

А - В
A.difference(B)

Возвращает разность множеств А и В

А-=В
A.difference_update(B)

Удаляет из множества А все элемен­
ты, не входящие в множество В

АЛВ
A. symmetric_difference(B)

Возвращает симметрическую раз­
ность множеств А и В

Записывает в А симметрическую раз­
АЛ=В
A.symmetric_difference_update(B) ность множеств А и В
А = В
A.issuperset(B)

Возвращает True, если А является над­
множеством В

А < В

Эквивалентно А < = В and А ! = В

А > В

Эквивалентно А > = В and А ! = В

На рис. 1.18 приведены наглядные примеры операций над мно­
жествами:
Пересечение, объединение и разница множеств
Пересечение А&В
Объединение А | В
Симметричная разница А^В

Разница A-В

Разница В-А

Рис 1.18. Операции над множествами
56

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Объявим несколько множеств и произведем над ними приведен­
ные на рис. 1.18 операции:
А = {0, 1, 1, 2, 3, 9, 4, 5, 6, 6, 7, 8, 9}
В = {1, 3, 6, 10, 15, 21, 28, 36, 45}
# объединение
new_set = А | В # A.union(B)
print(new_set)
# {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 21,
# 28, 36, 45}
# пересечение
new_set = А & В # A.intersection(B)
print(new_set) # {1, 3, 6}
# симметрическая разность
new_set = А Л В # A.symmetric_difference(B)
print(new_set)
# {0, 2, 36, 4, 5, 7, 8, 10, 9, 45, 15, 21, 28}
# разность А - В
new_set = А - В # A.difference(B)
print(new_set) # {0, 2, 4, 5, 7, 8, 9}
# разность В - А
new_set = В - А # B.difference(A)
print(new_set) # {36, 10, 45, 15, 21, 28}

При этом три из приведенных операций симметричны:
# объединение
check = (А | В) == (В | А)
print(check) # True
# пересечение
check = (А & В) == (В & А)
print(check) # True
# симметрическая разность
check = (А л В) == (В л А)
print(check) # True
# разность
check = (А - В) == (В - А)
print(check) # False

Проверка на то, является ли множество А подмножеством мно­
жества В, и наоборот (множество В является надмножеством А), вы­
полняется следующим способом:
А = {1, 2, 3}
В = {1, 2, 3, 4}
# А является подмножеством В?
print(A.issubset(B)) # True
# В является надмножеством А?
print(B.issuperset(A)) # True
A.add(5)
print(A.issubset(B)) # False
print(B.issuperset(A)) # False
57

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Проверка на то, входит ли элемент в множество, осуществляется
посредством использования оператора irt (not in):
в = {1,
print(0
print(l
print(0
print(l

2, з, 4}
in В) # False
in B) # True
not in B) # True
not in B) # False

Так как множества должны вычислять хеш-код для каждого эле­
мента, в них могут храниться только хешируемые элементы. В язы­
ке Python изменяемые элементы не являются хешируемыми. Это
означает, что хешировать список или словарь невозможно. А для
хеширования пользовательских классов необходимо реализовывать
методы__hash__ и__ eq__ .
Единственным отличием типа set (множество) от frozenset (не­
изменяемое множество) является то, что frozenset — неизменяемый
тип данных.

Резюме
В первой теме учебного пособия, помимо краткой истории язы­
ка программирования Python, были рассмотрены его основные осо­
бенности на примере интерпретатора CPython (3.7), типы данных
и способы работы с ними. Этого должно хватить для базового пред­
ставления о возможностях Python.
Приведенные способы работы со списками, словарями и т. д. по­
стоянно будут встречаться в ходе работы над реальными проекта­
ми, так что лучше их изучить в самом начале пути и не заглядывать
каждый раз в справочник для уточнения по методам и способам ра­
боты с ними.

Вопросы и задания для самопроверки
1. Почему в Python нет переменных?
2. Для чего используется счетчик ссылок?
3. В чем разница между сильными и слабыми ссылками?
4. Что такое интернированные объекты?
5. Как можно интернировать строку, содержащую не ASCII-символы?
6. Что такое GIL? В чем его особенность?
7. Как работает алгоритм подсчета ссылок?
8. Опишите принцип работы Garbage Collector.
9. Зачем используется подход с поколениями при работе Garbage
Collector?
10. Для чего можно использовать слабые ссылки?
11. Назовите четыре основных типа данных Python.
12. Что означает «неизменяемость» и какие три основных типа Python
считаются неизменяемыми?

58

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

13. Что означает «последовательность» и какие три типа входят в эту
категорию?
14. Каким будет значение выражения 2 * (3 + 4) в Python?
15. Каким будет значение выражения 2 * 3 + 4 в Python?
16. Каким будет значение выражения 2 + 3 * 4 в Python?
17. Назовите два способа построения списка, содержащего пять цело­
численных нулей.
18. Назовите два способа построения словаря с двумя ключами, 'а' и Ъ',
с каждым из которых ассоциировано значение 0.
19. Назовите четыре операции, которые изменяют списковый объект
на месте.
20. Назовите четыре операции, которые изменяют словарный объект
на месте.
21. Почему вы можете использовать словарь вместо списка?
22. Как определить размер кортежа? Почему этот инструмент находится
там, где он есть?
23. Напишите выражение, которое изменяет первый элемент в кортеже.
В процессе кортеж (4,5,6) должен стать (1,5,6).
24. Какой модуль вы бы использовали для сохранения объектов Python
в файле без предварительного преобразования их в строки?
25. Как бы вы копировали сразу все части вложенной структуры (словарь
в словаре, список в словаре и т. д.)?
26. Можно ли использовать строковый метод find для поиска в списке?
27. Можно ли применять выражение среза строки для списка?
28. Как бы вы могли изменить строку в Python?
29. Для заданной строки S со значением «s,pa,in» назовите два способа
извлечения двух символов из середины строки.

Упражнения
Перед тем, как приступать к выполнению упражнений — обратите
внимание на то, что они сформулированы таким образом, что материал
по части из них либо не в полной мере рассматривался в этом разделе, либо
не рассматривался вовсе. Это сделано специально! Не забывайте о том, что
один из навыков, которыми вы должны обладать, — поиск информации
в интернете и ее корректное применение в своем проекте. Как уже обсуж­
далось во введении, не следует довольствоваться просто найденным ответом
и бездумно копировать найденный код. Лучше перепечатайте его вручную
и более подробно разберитесь в том, как работает один из вариантов (или
все варианты) ответов.
1. Что будет результатом следующих выражений?
"agility"[2:5] + "taxonomy"[3:6]
[115,202,192,334,257][:4]
len("crazy"[3:3+4])
[9,8,7,6,5,4,3,2,1] [-3: ]
type([False,True,False,True][2:3])
"—".join( "this is important".split() )
int( ".join( "7/7/07".split('/') ) )
"too soon to tell".replace('o','*').replace('* ','')
2. Напишите скрипт, вычисляющий двумя способами длину строки.

59

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

3. Напишите скрипт, который позволяет из строки собрать другую
по следующим правилам: новая строка должна состоять из двух первых
и двух последних элементов исходной строки. Если длина исходной строки
меньше двух, то результатом будет пустая строка.
4. Напишите скрипт, который заменяет произвольный символ/букву
в строке на «$».
5. Напишите скрипт, который позволяет инвертировать последователь­
ность элементов в строке.
6. Напишите скрипт, который считает количество вхождений символа
в строку. Например: «google.com» — {'о': 3, 'g': 2,1, 'е': 1, Т: 1, 'm': 1, 'с': 1}
7. Напишите скрипт, позволяющий из исходной строки собрать две
новые. Первая строка должна состоять только из элементов с нечетными
индексами исходной строки, а вторая — с четными.
8. Напишите скрипт, который удаляет задаваемый произвольный символ
в строке.
9. Напишите скрипт, который позволяет переводить все символы ис­
ходной строки в верхний и нижний регистры.
10. Напишите скрипт, выводящий все элементы строки с их номерами
индексов.
11. Напишите скрипт, проверяющий, содержится ли произвольный символ
(элемент) или слово в строке.
12. Выведите символ, который встречается в строке чаще, чем остальные.
13. Поменяйте регистр элементов строки.
14. Вычислите сумму элементов (чисел) в списке двумя разными спосо­
бами.
15. Умножьте каждый элемент списка на произвольное число.
16. Найдите максимальное и минимальное числа, хранящиеся в списке.
17. Напишите скрипт, удаляющий все повторяющиеся элементы из спи­
ска.
18. Скопируйте список двумя различными способами.
19. Напишите скрипт для слияния (конкатенации) двух списков различ­
ными способами.
20. Напишите скрипт, меняющий позициями элементы списка с индек­
сами и и и + 1.
21. Напишите скрипт, переводящий список из различного количества
числовых целочисленных элементов в одно число. Пример списка: [15, 23,
150], результат: 1523150
22. Объявите и инициализируйте словарь различными способами.
23. Добавьте еще несколько пар «ключ: значение» в следующий словарь:
{0: 10, 1: 20}.
24. Напишите скрипт, который из трех словарей создаст один новый.
25. Напишите скрипт, проверяющий, существует ли заданный ключ
в словаре.
26. Напишите скрипт для удаления элемента словаря.
27. Напишите скрипт, который выводит максимальное и минимальное
числа среди значений словаря.
28. Объявите и инициализируйте кортеж различными способами, после
чего распакуйте его.
29. Напишите скрипт для добавления элементов в кортеж.
30. Напишите скрипт, конвертирующий список в кортеж.
31. Конвертируйте кортеж в словарь.

60

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

32. Напишите скрипт, подсчитывающий количество элементов типа
кортеж в списке.
33. Объявите и инициализируйте множество различными способами.
34. Добавьте элемент в множество.
35. Удалите элемент из множества.
36. Удалите повторяющиеся элементы из списка.
37. Напишите скрипт для объединения двух множеств.
38. Напишите скрипт, находящий длину множества двумя различными
способами.
39. Напишите скрипт для проверки, входит ли элемент в множество.
40. Напишите скрипт для записи текста в файл.
41. Напишите скрипт для чтения текста из файла.
42. Напишите скрипт для добавления текста в файл и отображения со­
держимого файла.
43. Напишите скрипт для чтения последних п строк файла.
44. Напишите скрипт, подсчитывающий количество строк в файле.
45. Напишите скрипт, позволяющий найти самое встречаемое слово
в файле.
46. Напишите скрипт, копирующий содержимое одного файла в другой.
47. Запишите словарь в файл посредством модуля pickle и прочитайте
его.
48. Запишите список в файл посредством модуля pickle и прочитайте его.
49. Запишите словарь в файл посредством модуля json и прочитайте его.

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 2
СИНТАКСИС, ОПЕРАТОРЫ
И УПРАВЛЯЮЩИЕ КОНСТРУКЦИИ
В данной теме рассматривается синтаксис языка программиро­
вания Python, а именно: что такое отступы в парадигме Python и за­
чем они нужны, основные операторы, циклы, вложения, условные
конструкции и т. д.
Отдельно рассмотрим, как происходит итерация по объектам
Python на примере использования циклов и какие инструменты,
невидимые разработчику, при этом используются.
Также коснемся вопроса документации кода и тех стандартных
инструментов документирования, которые изначально входят в со­
став Python.
В результате изучения данной темы обучающиеся должны:

знать
• синтаксис и основные операторы языка программирования Python,
• механизм итерации последовательностей,
• как создаются списки посредством механизма списковых включений;

уметь
• применять условные конструкции и циклы,
• применять циклы для перебора элементов последовательностей;

владеть
• навыками документирования кода,
• навыками использования существующего механизма документирова­
ния в Python,
• навыками написания кода с использованием условных конструкций,
циклов и списковых включений.

2.1. Основные операторы в Python
Каждый оператор Python имеет собственное специфическое
назначение и собственный специфический синтаксис — правила,
определяющие его структуру.
Операторы в Python классифицируются следующим образом:
— процедурные операторы;
— операторы функций;
62

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

— операторы классов;
— операторы исключений;
— операторы модулей;
— арифметические операторы;
— операторы сравнения;
— операторы присваивания;
— побитовые операторы;
— логические операторы;
— операторы членства;
— операторы тождественности.
В табл. 2.1 приведен перечень операторов, относящихся к клю­
чевым словам Python:
Таблица 2.1
Операторы (ключевые слова) Python

Оператор

Роль

Присваивания

Создание ссылок

a, b = 'good', 'bad'

Вызовы и дру­
гие выраже­
ния

Выполнение
функций

log.write("spam, ham")

Вызовы print

Вывод объектов

print ('test', val)

if/elif/else

Выбор действий

if

for/else

Итерация

for x in mylist: print(x)

while/else

Универсальные
циклы

while X > Y:
print('hello! ')

pass

Пустой заполни­
тель

while True:
pass

break

Выход из цикла

while True:
if exittestQ:
break

continue

Продолжение
цикла

while True:
if skiptest () :
continue

def

Функции и ме­
тоды

def f(a, b, c=l, *d):
print(a + b + c + d[0])

return

Результаты функ­
ций

def f (a, bj c=lj *d) :
return a+b+c+d[0]

yield

Генераторные
функции

def gen (n) :
for i in n: yield i*2

global

Пространства
имен

x = 'old'
def function():
global x,y; x = 'new'

Пример

"python" in text:
print(text)

63

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Окончание табл. 2.1

Оператор

Роль

Пример

nonlocal

Пространства
имен

def outer():
х = 'old'
def function():
nonlocal x; x = 'new'

import

Доступ к модулям

import sys

from

Доступ к атрибу­
там

from sys import stdin

class

Построение объ­
ектов

class Subclass(Superclass):
staticData = [ ]
def method (self) :
pass

try/except/
finally

Перехват исклю­
чений

try:

raise

Генерация исклю­ raise EndSearch(location)
чений

assert

Отладочные про­
верки

assert X > Y, 'X too small'

with/as

Диспетчеры кон­
текста

with open ('data ') as myfile:
process(myfile)

del

Удаление ссылок

del
del
del
del

action()
except:
print('action error')

datafk]
data[i:j]
obj.attr
variable

Формально print, начиная с Python3, не является ни зарезерви­
рованным словом, ни оператором, а вызовом встроенной функции;
но поскольку print почти всегда будет выполняться в виде операто­
ра с выражением (и часто занимать отдельную строку), такой вызов
в общем случае трактуется как разновидность оператора.
Аналогично и с yield. Начиная с Python 2.5 оно считается выра­
жением, а не оператором, но аналогично print оно обычно исполь­
зуется как оператор с выражением и было включено в таблицу.
Арифметические операторы представлены в табл. 2.2, операторы
сравнения — в табл. 2.3.
Таблица 2.2
Арифметические операторы

Оператор

+

64

Описание
Сложение

Примеры

10 + 5 = 15
10 + -3 = 7

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Окончание табл. 2.2

Примеры

Описание

Оператор

Вычитание

15 - 5 = 10
25 - -3 =28
11.98 - 7 = 4.98

*

Умножение

2*2 = 4
7 * 3.2 = 22.4
-2 * 4 = -8

/

Деление

12 / 4 = 3
7 / 3 = 2.334

%

Деление по модулю

4 % 2 = 0
9 % 2 = 1
13.2 % 4 = 1.199

**

Возведение в степень

5 ** 2 = 25
4 ** 3 = 64
9**(1/2) = 3.0

//

Целочисленное деление

17 // 5 = 3
10 // 3 = 3

Таблица 2.3
Операторы сравнения

Оператор

Описание

Примеры

==

Проверка на равенство

1 == 1 (True)
True == False (False)
"test" == "test" (True)

!=

Проверка на неравенство

1 != 2 (True)
False != False (False)
"test" != "Test" (True)

>

Проверка на то, что значе­
ние левого операнда больше
правого

3 > 2 (True)
2 > 3 (False)

<

Проверка на то, что значе­
ние левого операнда меньше
правого

3 < 2 (False)
2 < 3 (True)

>=

Проверка на то, что значение 3 >= 1 (True)
левого операнда больше или 3 >= 3 (True)
"C" >= "D" (False)
равно правому

y")
print(x - y)
else:
print("y >= x")
print(y - x)

68

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Python понимает, какие строки кода относятся к блоку if на осно­
ве отступов. Выполнение блока if а > b заканчивается, когда встре­
чается строка с тем же отступом (в данном случае else:).
В качестве отступов могут использоваться табуляция или про­
белы (их смешивания необходимо избегать! Начиная с версии
Python 3.x оно запрещено.). Их количество в одном блоке должно
быть одинаковым. Также для форматирования кода можно исполь­
зовать пустые строки. Это делается для упрощения чтения.
С более подробными рекомендациями по оформлению кода в про­
цессе его написания можно ознакомиться в PEP (Python Enhanced
Proposal — заявки на улучшение языка Python) [25 — 34]. Наиболее
известный из них — РЕР8, руководство по стилю написания кода
на Python (The Style Guide for Руthon Code). Данный документ создан
на основе рекомендаций Гвидо ван Россума с добавлениями идей
от других разработчиков, входящих в программный комитет.

23. Комментарии
В том случае, когда написанный вами код будет поддерживаться
еще и сторонними разработчиками, необходимо его комментиро­
вать. Это позволяет описать особенности его работы, что сэкономит
как ваше время при последующем к нему обращении, так и время
того человека, который должен разобраться в том, как код работает.
Комментарии в Python могут быть однострочными и много­
строчными. В первом случае используется ключевой символ «#»,
после которого идет комментарий, который не переносится на сле­
дующую строку:
# комментарий
а = 10 # еще один

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

Сверх
длинный комментарий
II II II

х = "text"

Комментарии можно использовать не только для того, чтобы
комментировать происходящее в коде. Например, посредством ком­
ментариев можно исключить выполнение определенной строки или
блока кода (то есть закомментировать их).
69

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2.4. Правила именования переменных (имен)
В Python переменные начинают свое существование, когда
им присваиваются значения, но есть несколько правил, которым
необходимо следовать при выборе имен для объектов программы.
1. Синтаксис: (подчеркивание или буква) + (любое количество
букв, цифр или подчеркиваний). Такая запись подразумевает, что име­
на должны начинаться с подчеркивания или буквы, за которой может
следовать любое количество букв, цифр или подчеркиваний. Напри­
мер, _test, test и Test_ 1 — допустимые имена, но l_Test, test$ — нет.
2. Регистр символов имеет значение. TEST — не то же самое,
что и test.
3. Зарезервированные слова не разрешены. Например, если
выбрать для переменной имя class, то Python сообщит об ошибке,
но имена klass и Class будут допустимыми.
Помимо этих правил существует еще набор соглашений по име­
нованию. Это правила, которые считаются не обязательными, но со­
блюдаются разработчиками в повседневной практике:
— имена, которые начинаются с одиночного подчеркивания
(_Х), не импортируются оператором from module import *;
— имена с двумя подчеркиваниями в начале и конце (_Х_)
являются системными именами, которые имеют особый смысл для
интерпретатора;
— имена, начинающиеся с двух подчеркиваний, но не оканчи­
вающиеся ими (__X), локализованы («искажены») для включения
в себя классов, то есть являются псевдо-приватными;
— имя, состоящее из одиночного подчеркивания (_), хранит ре­
зультат последнего выражения при работе в интерактивном сеансе.

2.5. Оператор if
Оператор if в Python выбирает действия, которые будут выпол­
няться в процессе работы программы в зависимости от условий.
Он является основным инструментом выбора в Python и представ­
ляет большой объем логики.
Самая общая форма конструкции if-then-else в Python выглядит так:
if условие].:
блок1
elif условие2:
блок 2
elif условие(п-1):
блок (п-1)
else:
блок (п)
70

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Если результат проверки условия1 равен True, то выполняется
блок1. В противном случае, если результат проверки условия^ равен
True, выполняется блок2, и так далее, пока не будет найдено усло­
вие, которое возвращает True, или не будет достигнута секция else
(тогда выполняется блок(п)). Если else отсутствует и все проверки
условий вернули False, то ни один из блоков ветвления не выпол­
нится.
Блок кода после команды if является обязательным. А когда необ­
ходимо указать, что в данном блоке ничего не выполняется, то ис­
пользуется команда pass, которая размещается там, где должна на­
ходиться команда, но никаких действий она не выполняет:
if а < 25:
pass
else:
а = 25

К сожалению, в Python отсутствует оператор вроде switch или
case. Разработчики Python обычно обходятся цепочкой if... elif...
elif... else, а если она становится слишком громоздкой, то использу­
ется следующий способ со словарем функций:
def first_function():pass
#Обработка а
def second_function():pass
#Обработка b
def default():pass
#Обработка default
func_dict = {'a': first_function,
'b': second_function,
'c': lambda: ...}
#выполнить функцию из словаря
func_dict.get(var, default)()

Также в Python нет тернарного оператора «?:» как такового. Вме­
сто него используется конструкция if-else. Для примера преобразуем
следующий код:
if А:
Z = в
else:
Z = С

посредством тернарной записи в следующую строку:
Z = В if A else С

Приведенное выражение дает в точности такой же результат, что
и предшествующий четырехстрочный оператор if, но проще в напи­
сании.
71

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2.6. Цикл while
Оператор (цикл) while используется для итераций в языке Python.
Он многократно выполняет блок операторов до тех пор, пока про­
верка условия в заголовочной части возвращает True. Когда резуль­
тат проверки становится False, управление переходит на оператор,
следующий после блока while. Если с самого начала результат усло­
вия выполнения цикла вернет False, то тело цикла никогда не вы­
полнится (оператор while пропускается).
В общем виде оператор while состоит из строки заголовка с усло­
вием выполнения цикла, тела цикла с одним или большим количе­
ством операторов с отступами и необязательной части else, которая
выполняется, если цикл не прерывается оператором break:
while условие: # Проверка цикла
операторы # Тело цикла
else: # Необязательная часть else
операторы
# Выполняются, если не произведен
# выход из цикла с помощью break

При объявлении цикла необходимо удостовериться, что из него
предусмотрен выход. Иначе он получится бесконечным:
while True:
print('Я вечен!!!')

Такой цикл будет выполняться до тех пор, пока в терминале
не будет нажато сочетание клавиш Ctrl+С, прекращающее выпол­
нение программы.
В Python нет оператора цикла «do while», имеющегося в дру­
гих языках. Однако его можно эмулировать посредством проверки
и оператора break в конце тела цикла, так что тело цикла всегда
выполняется хотя бы раз:
while True:
#тело цикла.
if isExitLoop(): break

В следующем примере производится подсчет значений от х до у:
х= 0
У= 5
while х < у:
print(x, end=' ')
х += 1 # Или х = х + 1
#01234

Цикл с конструкцией else. Рассмотрим работу цикла с конструк­
цией else на основе предыдущего примера:
х = 0
У = 5
72

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

while x < у:
print(x, end=' ')
x += 1 # Или x = x + 1
else:
print('Happy End!')
#01234 Happy End!

Конструкция else выполняется после окончания цикла в том слу­
чае, если выход из него не был выполнен посредством оператора
break:
х= 0
У= 5
while х < у:
print(x, end=' ')
х += 1
if х > у-2: break
else:
print('Happy End!')
#0123

2.6.1. Работа цикла с операторами break, continue, pass
Теперь рассмотрим два простых оператора, которые достига­
ют своих целей, только когда вложены внутри циклов — break
и continue, а также пустой оператор-заполнитель pass (который
не привязан к циклам как таковой, но относится к общей категории
простых однословных операторов). Описание указанных операто­
ров Python приведено ниже:
— break — переходит за пределы ближайшего заключающего
его цикла;
— continue — переходит в начало ближайшего заключающего
его цикла (на строку заголовка цикла);
— pass — ничего не делает (это пустой оператор-заполнитель).
С учетом операторов break и continue расширенный формат цик­
ла while выглядит следующим образом:
while проверка:
# тело цикла
if проверка: break
# выход из цикла с пропуском else (если есть)
if проверка: continue
# переход на проверку в начале цикла
else: # выполняется, если не было break
#блок else

Операторы break и continue могут появляться где угодно внутри
тела цикла while (или/от). Обычно их записывают с дополнитель­
ным отступом в операторе if.
Далее будут рассмотрены несколько простых примеров примене­
ния этих операторов на практике.
73

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Оператор pass. Оператор pass — заполнитель, который исполь­
зуется в ситуациях, когда синтаксис требует оператора, но нет воз­
можности выполнить что-либо полезное.
Для примера его работы перепишем ранее приведенный беско­
нечный цикл, чтобы он ничего не выводил:
while True: pass

Иногда оператор pass обозначает место, подлежащее заполнению
в будущем, что служит временной заглушкой для тела функции. Это
связано с тем, что нельзя оставить тело функции пустым, не полу­
чив синтаксической ошибки:
def test():
pass
def test():
# ошибка
File
line 1
def test():
A

SyntaxError: unexpected EOF while parsing

Начиная c python3 вместо pass можно указать многоточие, кото­
рое записывается как «. . .» (просто три следующие друг за другом
точки):
def test():

Оператор continue. Оператор continue используется для немед­
ленного перехода в начало цикла. В некоторых случаях он позволя­
ет избежать вложения операторов:
а = 15
while а:
х = а-1 # а -= 1
if а % 2 != 0: continue
print(a, end=' ')

Из-за того, что continue инициирует переход в начало цикла, нет
необходимости вкладывать print внутрь проверки if, так как он вы­
полнится только в том случае, если не будет выполнен continue.
Приведенный выше пример можно записать и без использования
оператора continue:
а = 15
while а:
х = а-1 # а -= 1
if а % 2 == 0:
print(a, end=' ')

Оператор break. Оператор break используется для немедленного
выхода из цикла. Поскольку при его достижении код, который нахо­
74

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

дится за ним в теле цикла, не выполняется, за счет включения break
иногда можно избежать вложения:
my_sum = 0
while True:
val = int(input('Введите значение: '))
if val == 0: break
my_sum += val
print('Текущая сумма:
my_sum)
print('Конечная сумма:
my_sum)
""" Введите значение: б
Текущая сумма: 6
Введите значение: 10
Текущая сумма: 16
Введите значение: 20
Текущая сумма: 36
Введите значение: 0
Конечная сумма: 36 """

В текущем примере функция input используется для считывания
вводимого значения с клавиатуры. Поскольку данные всегда считы­
ваются в строковом формате str, введенное значение необходимо
преобразовать в числовой (целочисленный) формат данных:
int(input('Введите значение: '))

2.7. Цикл for
Цикл (оператор) for перебирает значения, возвращаемые любым
итерируемым объектом, то есть любым объектом, который может
сгенерировать последовательность значений. Например, цикл for
может перебрать элементы списка, кортежа или строки. Итериру­
емый объект может быть создан функцией range или специальной
разновидностью функций — генератором:
for элемент in последовательность:
... #тело цикла
else: # необязательная часть else

# если для выхода из цикла не использовался break

Тело цикла выполнится по одному разу для каждого элемента по­
следовательности. Необязательная часть else используется достаточ­
но редко и работает аналогично циклу while. То же самое относится
к командам break и continue. Таким образом, более полный формат
цикла for может быть записан следующим образом:
for элемент in последовательность:
# тело цикла
if проверка: break
if проверка: continue
else: #блок else

75

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Приведем пример поэлементного перебора списка с выводом
значений текущего итерируемого элемента в терминал:
for it in ['first', 'second', 4, 5.9, 'finish']:
print(it, end=' ')
# first second 4 5.9 finish

Цикл/or позволяет проводить поэлементный перебор строк:
my_str = 'в далёкой-далёкой галактике...'
for it in my_str:
print(it, end='-')
# в- -д-а-л-ё-к-о-й—д-а-л-ё-к-о-й# -г-а-л-а-к-т-и-к-е-.-.-.-

Итерация по элементам кортежей осуществляется следующим
образом:
my_str = ('в', 'далёкой-','далёкой', 'галактике...')
for it in my_str:
print(it, end=' ')
# в далёкой- далёкой галактике...

Для итерации по списку кортежей используют следующий под­
ход:
my_tuple_list = [(3,6), (0,1), (4,5), ('О',3.9)]
for (a,b) in my_tuple_list:
print(a, b)
#36
#01
#45
# О 3.9

Кортежи в циклах/or также становятся полезными при итерации
сразу по ключам и значениям в словарях с применением метода
items вместо прохода в цикле по ключам и индексации для извле­
чения значений вручную:
# извлечение значений через итерируемый ключ
my_dict = {'а':1, 'Ь': 10, 'с': 3.8}
for key in my_dict:
print(key, '=>', my_dict[key])
# a => 1
# b => 10
# c => 3.8
# итерация по паре "ключ:значение"
for key, value in my_dict.items():
print(key, '=>', value)
# a => 1
# b => 10
# c => 3.8
76

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Каждый цикл может содержать в себе еще один цикл и т. д. Такие
циклы называются вложенными:
items =
tests =
for key
for

("ааа", 111, (4, 5), 2.01)
[(4, 5) , 3.14]
in tests:
item in items:
if item == key:
print (key, 'найден')
break

else:
print(key, "не найден!")
# (4, 5) найден
# 3.14 не найден!

2.8. Различные способы написания циклов
Существуют ситуации, когда требуется выполнить проход по по­
следовательности более специализированным способом. Допустим,
нужно посетить каждый второй или каждый третий элемент в спи­
ске, либо попутно изменять список, или произвести обход более
одной последовательности параллельно в том же самом цикле for
ит. д.
Такие специфичные итерации всегда можно реализовать с помо­
щью цикла while и ручной индексации, но Python предлагает набор
встроенных функций, которые позволяют специализировать итера­
цию в цикле for:
— встроенная функция range (доступная начиная с Python 0.x)
производит серию последовательных целых чисел с задаваемым на­
правлением (возрастает или убывает) и шагом, которые могут ис­
пользоваться в качестве индексов в цикле/от;
— встроенная функция zip (доступная начиная с версии
Python 2.0) возвращает серию кортежей из параллельных элемен­
тов, которые могут применяться для обхода множества последова­
тельностей в цикле for;
— встроенная функция enumerate (доступная начиная с версии
Python 2.3) генерирует значения и индексы элементов в итерируе­
мом объекте.

2.8.1. Использование встроенной функции range
Встроенная функция range является универсальным инструмен­
том, который может использоваться в разнообразных контекстах.
Она наиболее часто применяется для генерации индексов в цикле
for, но ее можно использовать где угодно, когда требуется серия
целых чисел. Начиная с python3 range является итерируемым объ­
77

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

ектом, который генерирует элементы по запросу, и ее базовый син­
таксис может быть записан следующим образом:
range(from [, to, step])

где элементы в [] являются необязательными.
Приведем пример работы с различными значениями, подавае­
мыми на вход функции range:
print(range(10)) # range(0, 10)
print(list(range(10)))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(range(0,6)) # range(0, 6)
print(list(range(0, 6))) # [0, 1, 2, 3, 4, 5]

print(range(0, 6, 2)) # range(0, 6, 2)
print(list(range(0, 6, 2))) # [0, 2, 4]

С одним аргументом range генерирует список целых чисел, на­
чиная с нуля и заканчивая (но не включая) значением аргумента.
В случае передачи двух аргументов первый считается нижней гра­
ницей. Необязательный третий аргумент может задавать «шаг»,
когда он указан. Python добавляет выбранный шаг к каждому после­
дующему целому числу в результате (по умолчанию шаг равен +1).
При желании диапазоны могут быть неположительными и невоз­
растающими:
print(list(range(-5, 5)))

# [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
print(list(range(5, -5, -1)))
# [5, 4, 3, 2, 1, 0, -1, -2, -3, -4]

Таким образом, чтобы вывести три строки в цикле for, с помо­
щью range можно сгенерировать соответствующее количество це­
лых чисел:
for i in range (3) :
print(i, 'Test')
# 0 Test
# 1 Test
# 2 Test

Встроенная функция range также может использоваться для не­
прямого прохода по последовательности:
my_str = 'Test'
for it in my_str:
print(it, end=' ') # Простая итерация
# T e s t
print(len(my_str)) # 4
print(list(range(len(my_str)))) # [0, 1, 2, 3]
78

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

#Ручная итерация посредством range/len
for it in range(len(my_str)):
print(my_str[it], end=' ')
# T e s t

Во второй части примера производится проход по списку сме­
щений в строке my_str, а не по ее действительным элементам, вви­
ду чего для извлечения каждого элемента необходима индексация
в my_str.
При работе с циклами в Python необходимо ориентироваться
на следующие правила:
— во всех случаях, когда это возможно, вместо цикла while сто­
ит использовать for;
— использовать вызовы range в циклах/ог только в крайних или
специализированных случаях, то есть для обхода последовательно­
стей предпочтительнее использовать запись вида/от it in list.

2.8.2. Использование встроенной функции zip
Встроенная функция zip, в отличие от range, дает возможность
применять циклы for для просмотра множества последовательно­
стей параллельно — без совмещения во времени, но в течение того
же самого цикла. В базовом виде функция zip принимает одну или
большее количество последовательностей в качестве аргументов
и возвращает серию кортежей, объединяющих в пары параллель­
ные элементы из указанных последовательностей:
first_list = [1,2,3,4,5]
second_list = [6,7,8,9,10]
print(list(zip(first_list, second_list)))
# [(1, 6), (2, 7), (3, 8), (4, 9), (5, 10)]

Таким образом, встроенная функция zip в сочетании с циклом
for предоставляет возможность поддержки параллельных итераций:
for a, b in zip(first_list, second_list):
print(a, ' + ', b, '=' , a+b)
#1 + 6 = 7
#2 + 7 = 9
# 3 + 8 = 11
# 4 + 9 = 13
# 5 + 10 = 15

Эффект от такого подхода заключается в том, что в цикле про­
сматриваются оба списка: firstjist и second_list. Похожего результата
можно добиться используя цикл while, который поддерживает ин­
дексацию вручную. Но этот способ потребовал бы большего объема
кода и выполнялся несколько медленней, чем использование связки
for-zip.
Ранее было сказано, что функция zip может принимать на вход
любое количество последовательностей. Главное, чтобы объекты,
79

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

подаваемые ей на вход, представляли собой итерируемый объект.
Приведем пример с тремя аргументами. В этом случае функция zip
построит список трехэлементных кортежей с элементами из каждой
последовательности, по существу проецируя по столбцам (формаль­
но для N аргументов мы получаем N-арный кортеж):
one, two, three = (1,2,3) , (4,5,6), (7,8,9)
print(list(zip(one, two, three)))
# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
for a, b, c in zip(one, two, three):
print('({} - {}) * {} = {}'.format(b, a, c,
(b-a)*c))
# (4 - 1) * 7 = 21
# (5 - 2) * 8 = 24
# (6 - 3) * 9 = 27

Если на вход подаются последовательности различной длины,
результирующие кортежи нормируются по длине самой короткой
из них:
one, two, three = (1,2,3,5,9), (4,5,6), (7,8,9,23)
print(list(zip(one, two, three)))
# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]

2.8.3. Использование встроенной функции enumerate
В ряде случаев бывает необходимо производить итерацию
по элементам последовательности и знать, какой индекс у текуще­
го итерируемого элемента. Традиционно такая задача решается по­
средством простого цикла/or с дополнительным счетчиком индекса
итерируемого элемента:
my_str = 'Test'
index = 0
for it in my_str:
print('Индекс элемента ',it,' = 1' , index)
index +=1
# Индекс элемента т = 0
# Индекс элемента е = 1
# Индекс элемента S = 2
# Индекс элемента t = 3

Функция enumerate возвращает генераторный объект — разно­
видность объекта, поддерживающая протокол итерации. Такой объ­
ект имеет метод, вызываемый встроенной функцией next, который
на каждом проходе цикла возвращает кортеж {индекс, значение}:
my_str = 'Test'
for index, it in enumerate(my_str):
print('Индекс элемента ',it,' = 1' , index)
# Индекс элемента т = 0
# Индекс элемента е = 1
# Индекс элемента S = 2
# Индекс элемента t = 3
80

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Цикл/or автоматически проходит по всем кортежам, возвращае­
мым функцией enumerate, что позволяет распаковывать их значе­
ния почти так же, как это делается для zip:
test = enumerate(my_str)
print(next(test)) # (0, T)
print(next(test)) # (1, 'e')
print(next(test)) # (2, ’s')
print(next(test)) # (3, ’f)

2.9. Итерации и включения
2.9.1. Итератор и итерируемый объект
Как уже отмечалось ранее, цикл for работает с любым итери­
руемым объектом. Это же касается и всех итерационных инстру­
ментов Python, которые просматривают объекты слева направо:
списковых включений, проверок членства in, встроенной функции
тар и т. д.
Концепция «итерируемых объектов» в Python буквально про­
низывает всю языковую модель и представляет собой обобщение
понятия «последовательность». Большинство контейнеров и мно­
жество других типов данных (файлы, сокеты и т. д.) являются ите­
рируемыми объектами. В то время как контейнеры обычно содер­
жат конечное количество элементов, просто итерируемый объект
может представлять источник бесконечных данных [35].
В действительности итерация основана на двух объектах, кото­
рые используются на двух отдельных шагах:
— итерируемый объект, для которого запрашивается итерация,
чей метод__iter__ запускается методом iter;
— объект итератора, возвращенный итерируемым объектом,
который фактически производит значения во время итерации, чей
метод__ next__ запускается методом next, и генерирует исключение
Stopiteration, когда завершает выдачу результатов.
На рис. 2.1 приведен в упрощенном виде пример итерации по ите­
рируемому объекту или генератору, используемый циклом for.
iterator

next ()
generator

следующее
значение

iter()

iterable
итерируемый объект

Рис. 2.1. Пример итерации по итерируемому объекту, используемый циклом for
81

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Говоря простыми словами, итерируемый объект — это любой
объект, который может предоставить итератор, возвращающий от­
дельные элементы этого объекта:
my_list = [1, 2, 3]
first_iter = iter(my_list)
second_iter = iter(my_list)
print (next (first_iter)) # 1
print(next(first_iter)) # 2
print(next(second_iter)) # 1
print(next(first_iter)) # 3
print(next(first_iter))

Stopiteration
Traceback (most recent call last)
in
- — > 8 print(next(y))
Stopiteration:

myjist — это итерируемый объект (список), afirstjter и second_
iter — два отдельных экземпляра итератора, производящего зна­
чения из итерируемого объекта myjist.Из примера видно, что
first_iter и secondJter сохраняют состояние между вызовами next(),
и когда итерация по списку посредством firstJter становится невоз­
можной, генерируется исключение Stopiteration.
Из описанного выше следует, что код:
my_list = [1, 2, 3]
for it in my_list:
pass

выполняет набор действий, показанный на рис. 2.2.
next ()

iter()

1

my_list = [1, 2, 3]

iterator

2

3

x
Рис. 2.2. Итерация в цикле for по списку myjist

Также при работе с итератором можно напрямую вызывать ме­
тод __next__ :
my_list = [1, 2, 3]
first_iter = iter(my_list)
print(first_iter.__ next__ ()) # 1

2.9.2. Списковые включения (list comprehension)
В дополнение к операциям над последовательностями и списко­
вым методам в Python имеется более сложная операция, известная
как выражение спискового включения (list comprehension) и являю82

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

щаяся мощным способом обработки структур. Вместе с циклами for
списковые включения входят в число наиболее известных контек­
стов, в которых применяется итерирование.
Списковые включения происходят от системы обозначения мно­
жеств и предоставляют способ построения нового списка за счет
выполнения выражения на каждом элементе в последовательности,
по одному за раз (слева направо) [36]:
new_list = [expression for member in iterable]

Под expression понимается вычисление, вызов метода или лю­
бое другое допустимое выражение, которое возвращает значение;
member является объектом или значением в списке или итерируе­
мым объекте (iterable').
Все это позволяет следующий код:
my_list = [1, 2, 3, 4, 5]
for i in range (len(my_list)) :
my_list[i] += 10
print(my_list) # [11, 12, 13, 14, 15]

записать следующим образом:
my_list = [1, 2, 3, 4, 5]
my_list = [it+10 for it in my_list]
print(my_list) # [11, 12, 13, 14, 15]

Следует отметить, что списковые включения не являются обяза­
тельными, так как всегда можно построить список результатов вы­
полнения выражения вручную, используя цикл/от:
my_list = [1, 2, 3, 4, 5]
new_list = []
for it in my_list:
new_list.append(it + 10)
print(new_list) # [11, 12, 13, 14, 15]

Их часто называют «синтаксическим сахаром» для цикла for,
но по времени выполнения они даже выигрывают из-за того,
то не вызывают метод append() у списка [37].
Наиболее распространенный способ использовать условную ло­
гику при работе со списковыми включениями — добавить условное
выражение в конец выражения:
new_list = [expression for member in iterable (if conditional)]

Например:
new_list = [x for x in range(10) if x % 2 != 0]
print(new_list) # [1, 3, 5, 7, 9]

В случае, когда необходимо использовать полноценный блок вет­
вления if-else, используется следующий вид спискового включения:
new_list = [expression (if conditional) for member in iterable]
83

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Например, заменим отрицательные значения на 0, а положи­
тельные оставим без изменения:
my_list = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
new_list = [i if i > 0 else 0 for i in my_list]
print(new_list) # [1.25, 0, 10.22, 3.78, 0, 1.16]

Если необходимо использовать более сложную логику, то вместо
выражения можно использовать функцию:
def test_func(value):
return value if value > 0 else 0
my_list = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
new_list = [test_func(i) for i in my_list]
print(new_list) # [1.25, 0, 10.22, 3.78, 0, 1.16]

Начиная с версии 3.8 в Python введен моржовый оператор walrus.
В отличие от операции присваивания (=), результат моржового
оператора также является выражением и может быть использован,
например, для сравнения:
import random
def get_weather_data():
return random.randrange(90, 110)
too_hot = [temp for _ in range(20) if (temp :=
get_weather_data()) >= 100]
print(too_hot)
# [102, 106, 108, 103, 106, 103, 100, 108,
# 100, 102, 100]

Как и циклы, включения могут содержать вложенные включения.
Рассмотрим пример такого вложения для формирования матрицы:
matrix = [[i for i in range(4)] for _ in range(4)]
print(matrix)
tt [[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3],
# [0, 1, 2, 3]]

Включения можно использовать также для словарей (dictionary
comprehensions) и множеств (set comprehensions):
my_list = [10, 34, 56 ,1 ,1, 2, 4, 3, 102, 102]
my_set = {i for i in my_list if i%2==0}
print(my_set) # {2, 34, 4, 102, 10, 56}
my_dict = {i: i * i for i in range(10)}
print(my_dict)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25,
# 6: 36, 7: 49, 8: 64, 9: 81}

84

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2.10. Источники документации Python
Часто бывает необходимо найти информацию по той или иной
функции, методу объекта и т. д. Именно из-за этого документация
и является настолько важным инструментом при практическом
программировании. В табл. 2.9 приведены источники документа­
ции Python.
Таблица 2.9
Источники документации Python

Формат источника

Описание

Комментарии «#»

Внутрифайловая документация

Функция dir

Списки атрибутов, доступных в объектах

Строки документации:_ doc_

Внутрифайловая документация, присоеди­
няемая к объектам

PyDoc: функция help

Интерактивная справка для объектов

PyDoc: отчеты HTML

Документация по модулям, отображающа­
яся в браузере

Стандартный набор руководств Официальные описания языка, библиотек,
и Web-ресурсы
онлайновые руководства, примеры и т. д.

Печатные издания

Коммерческие справочники

Комментарии «#». Комментарии «#» являются наиболее базо­
вым способом документирования кода. Python просто игнорирует
весь текст, следующий за символом # (до тех пор, пока он не нахо­
дится внутри строкового литерала), поэтому после # можно поме­
щать любые слова и описания. Однако такие комментарии доступ­
ны только в файлах исходного кода, а для написания более широко
доступных комментариев понадобится применять строки докумен­
тации ('"Файл делает то-то'").
Функция dir. Встроенная функция dir предоставляет легкий способ
получения перечня всех атрибутов, доступных внутри объекта (т. е.
его методов и более простых элементов данных). Ее можно вызывать
без аргументов, чтобы получить список переменных в области види­
мости вызывающего кода. Но более удобно то, что dir можно вызывать
на любом объекте, имеющем атрибуты, в числе которых импортиро­
ванные модули и встроенные типы, а также имена типов данных:
print(dir())
['In','Out*,
'__ builtin__ ', '__ builtins__
'__ doc__ '_____ loader__ '_____ name__ '_____ package__ '_____ spec__
■_dh', *_i', *_il*, '_ih', '_ii', '_iii', '_oh', 'exit', 'get_
ipython', 'os', 'quit', 'sys']
print(dir(dict))

85

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

['__ class__ ', '__ contains__ ', '__ delattr__ ', '__ delitem__ ',
'_dir_', '_doc_', '_eq_', '—format—', ' ge '.
'_ getattribute__ ', '_ getitem_ ', '___ gt__ ', '_ hash__ ', '_
init_'_init_subclass_', '_iter_*, '_le_*,
len_
'_ It__ ', '__ ne__', '_ new__ ', '__ reduce__ ', '_ reduce_ex__ ',
'__ repr__ ', '__ setattr__ ', '__ setitem__ ', '__ sizeof__ ', '__
str__ '_____ subclasshook__ ', 'clear', 'copy', 'fromkeys', 'get',
'items', 'keys', 'pop', 'popitem', 'setdefault', 'update',
'values']
#- количество атрибутов в словаре
print(len(dir({}))) # 40

Чтобы отфильтровать элементы, например, с двумя подчерки­
ваниями в выводимом списке атрибутов, которые обычно не пред­
ставляют интереса для разработчика, можно воспользоваться вклю­
чениями:
print([it for it in dir(dict) if not it.startswith
С — ')])
['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop',
'popitem', 'setdefault', 'update', 'values']

Строки документации__ doc__ . Строки документации в Python
автоматически присоединяются к объектам и сохраняются во вре­
мя выполнения программы. Синтаксически такие комментарии
записываются как строки ('"Файл делает то-то'", """Метод делает
то-то""") в начале файлов модулей, а также операторов функций
и классов, их методов и т. д., перед любым другим исполняемым
кодом.
Python автоматически помещает текст таких строк в атрибуты
__doc__ соответствующих объектов (файл testdoc.py):
Документация модуля
Бла-бла-бла
I

I

I

spam =40
def square(x):
I

I

I

Документация функции
Дебажить? Или заявить, что это фича?
I

I

I

return х**1 #квадрат

class Employee:
"Документация класса"
pass

print(square (4)) # 4
print(square.__ doc__ )
# Документация функции
# Дебажить? Или заявить, что это фича?

86

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Весь смысл соблюдения такого подхода к документации в том,
что после импортирования файла комментарии хранятся в атрибу­
тах __doc__. Таким образом, для отображения строк документации,
ассоциированных с модулем и его объектами, необходимо импор­
тировать файл и вывести его атрибуты__doc__ , где Python сохранил
текст:
import testdoc

print(testdoc.__ doc__ )
# Документация модуля
# Бла-бла-бла
print(testdoc.square.__ doc__ )
# Документация функции
# Дебажить? Или заявить, что это фича?
print(testdoc.Employee.__ doc__ )
# Документация класса

Встроенные модули и объекты в Python применяют похожие ме­
тоды для присоединения к ним документации. Например, чтобы
увидеть фактическое описание встроенного модуля, предназначен­
ное для человека, необходимо его импортировать и вывести его
строку__doc__ :
import copy
print(copy.deepcopy.__ doc__ )
"""Deep copy operation on arbitrary Python objects.
See the module's __ doc__ string for more info. """
print(list.remove.__ doc__ )
# Remove first occurrence of value.
print(diet.__ doc__ )
""" dict() -> new empty dictionary
dict(mapping) -> new dictionary initialized from a
mapping object's
(key, value) pairs
dict(iterable) -> new dictionary initialized as
if via:
d = {}
for k, v in iterable:
d[k] = v
dict(**kwargs) -> new dictionary initialized with
the name=value pairs
in the keyword argument list. For example:
dict(one=l, two=2)
II II II

PyDoc: функция help. Стандартный инструмент PyDoc представ­
ляет собой код Python, которому известно, как извлекать строки до­
кументации вместе с ассоциированной структурной информацией
и форматировать их в аккуратно организованные отчеты различ­
ных видов.
87

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Существует несколько способов запуска PyDoc, включая параме­
тры сценария командной строки, которые могут сохранять резуль­
тирующую документацию для просмотра в будущем. Самыми вос­
требованными интерфейсами PyDoc обычно являются встроенная
функция help, а также средства отображения отчетов в формате
HTML с графическим пользовательским интерфейсом:
import testdoc
print(help(list.remove))
"""Help on method_descriptor:
remove(self, value, /)
Remove first occurrence of value.

Raises ValueError if the value is not present.
None """
print(help(diet.update))
"""Help on method_descriptor:

update(...)
D.update([E, ]**F) -> None. Update D from
dict/iterable E and F.
If E is present and has a .keys() method, then
does: for k in E: D[k] = E[k]
If E is present and lacks a .keys() method, then
does: for k, v in E: D[k] = v
In either case, this is followed by: for k in F:
D[k] = F[k]
None """
print(help(testdoc))
"""Help on module testdoc:

NAME
testdoc

DESCRIPTION
Документация модуля
Бла-бла-бла
CLASSES
builtins.object
Employee

class Employee(builtins.object)
| Документация класса
I
| Data descriptors defined here:
I
| __ diet__
88

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

I
I
|
|

dictionary for instance variables (if
defined)

weakref
list of weak references to the object
(if defined)

FUNCTIONS
square(x)
Документация функции
Дебажить? Или заявить, что это фича?
DATA

spam = 40

FILE
f:\code\python\myprojrep\testdoc.py """

PyDoc: отчеты HTML. Для запуска данного режима работы доку­
ментации необходимо в терминале (из каталога проекта) запустить
следующую команду:
(venv) F:\code\python\myProjRep>python -m pydoc -b
Server ready at http://localhost:58003/
Server commands: [bjrowser, [q]uit
server>

Результатом выполнения приведенной команды будет запуск
браузера с вкладкой документации, как показано на рис. 2.3.

Рис. 2.3. Сформированная HTML-документация

Если нажать на написанный ранее модуль testdoc.ру, то про­
изойдет переход на страницу с отображением его документации
(рис. 2.4).
89

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Рис 2.4. Сформированная HTML-документация модуля testdoc
Стандартный набор руководств и Web-ресурсы. На официаль­
ном веб-сайте Python (http://www.python.org ) помимо докумен­
тации [38] по языку программирования (имеются онлайн- и офлайн-версии) можно найти ссылки на разнообразные ресурсы, где
раскрываются специальные темы, тонкости применения и т. д.
Помимо этого, в наши дни значительно развит онлайн-сегмент
образовательных блогов, курсов, вебинаров и прочих ресурсов.
Надо только уметь пользоваться поисковиком и не пренебрегать
им в случае возникновения вопросов или затруднений при напи­
сании кода.
Печатные издания. Также можно выбрать из коллекции прошед­
ших профессиональную редактуру и опубликованных справочников
(самоучителей) по Python. При таком подходе стоит обратить вни­
мание на то, что книги имеют тенденцию отставать от самых совре­
менных изменений Python. Это связано отчасти с объемом работы
по созданию таких книг и частично с естественными задержками,
присущими издательскому циклу. Как правило, к моменту выхода ан­
глийская версия книги отстает на три и больше месяцев от текущего
состояния Python, что уж говорить о ее издании на русском языке.

Резюме
Данная часть была посвящена больше синтаксису Python и во­
просам документации кода. Были рассмотрены циклы, вложения,
условные конструкции и т. д. Отдельно рассматривался процесс
итерации по циклу и какие инструменты, невидимые разработчику,
при этом используются.
90

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Чтобы в дальнейшем избежать проблем, связанных с синтакси­
сом языка и стилем написания кода, приведем список подсказок,
к которому можно обратиться в любой момент времени.
1. Не забывайте о двоеточиях. Постоянно помните о необходи­
мости набора символа «:» в конце заголовка составного операто­
ра — первой строки if, while, for и т. д.
2. Начинайте свой код без отступов. Убедитесь, что начинаете
код верхнего уровня (не вложенный) без отступов.
3. Делайте отступы согласованно. Избегайте смешивания та­
буляций и пробелов в отступах. В противном случае то, что вы ви­
дите в своем редакторе, может быть не тем, что видит Python, когда
он пересчитывает табуляции в пробелы.
4. Не пишите код Python в стиле С. Напоминание программи­
стам на C/C+ + : нет необходимости помещать в круглые скобки
выражения проверок в заголовках if и while (например, if (Х= = 1)
:). При желании поступать так можно (заключать в круглые скобки
разрешено любое выражение), но в данном контексте они совер­
шенно излишни.
5. Используйте простые циклы for вместо while или range. Про­
стой цикл for (скажем, for х in seq:) почти всегда проще в написа­
нии и часто быстрее в выполнении, чем цикл с подсчетом на основе
while или range.
6. Будьте осторожны с изменяемыми объектами в присваива­
ниях. Нужно проявлять осмотрительность при использовании изме­
няемых объектов в групповом присваивании (а = Ь= [ ]), а также
в дополненном присваивании (а += [ 1, 2 ]). В обоих случаях из­
менения на месте могут повлиять на другие переменные.
7. Не ожидайте получить результаты от функций, которые из­
меняют объекты на месте.
8. Всегда применяйте круглые скобки для вызова функции. Что­
бы вызвать функцию, после ее имени потребуется указать круглые
скобки независимо от того, принимает она аргументы или нет.
9. Не используйте расширения или пути при импортировании
и перезагрузке. Опускайте пути по каталогам и файловые расшире­
ния в операторах import. Используйте import mod, а не import mod.py.

Вопросы для самопроверки
1. Почему необходимо соблюдать осторожность в случае присваивания
трем переменным изменяемого объекта?
2. Как вы могли бы закодировать множественное ветвление в Python?
3. Как вы могли бы закодировать оператор if/else в форме выражения
в Python?
4. Как вы могли бы распространить одиночный оператор на несколько
строк?

91

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

5. Что означают слова True и False?
6. Каковы главные функциональные отличия между while и for?
7. В чем разница между break и continue?
8. Когда выполняется конструкция else цикла?
9. Как можно записать цикл с подсчетом в Python?
10. Для чего может применяться range в цикле for?
11. Как связаны циклы for и итерируемые объекты?
12. Как связаны циклы for и списковые включения?
13. Когда должны использоваться строки документации вместо коммен­
тариев #?
14. Назовите три способа, которыми можно просматривать строки до­
кументации.
15. Как можно получить список доступных атрибутов в объекте?
16. Как можно получить список доступных модулей на компьютере?
17. Какую книгу, посвященную Python, вы должны приобрести после
этой?

Упражнения
1. Выведите все символы из строки «Данная часть была посвящена боль­
ше синтаксису Python и вопросам документации кода», значения индексов
которых делятся на 2.
2. Выведите все символы из строки «Данная часть была посвящена боль­
ше синтаксису Python и вопросам документации кода», значения индексов
которых без остатка делятся на 3, но не делятся на 4.
3. Выведите все символы из строки (Данная часть была посвящена боль­
ше синтаксису Python и вопросам документации кода», значения индексов
которых при делении на 6 дают остаток 2, 4, и 5.
4. Выведите числа из диапазона от 1 до 10, используя цикл for и while.
5. Выведите числа из диапазона от -20 до 20 с шагом 3, используя цикл
for и while.
6. Посчитайте количество вхождений элемента со значением «3» в сле­
дующем списке: [3 01304334 56 613], используя цикл for, while и метод
count.
7. Сформируйте список из элементов строки «список доступных атри­
бутов», используя механизм списковых включений и цикл for.
8. Сформируйте единичную матрицу N х N, используя механизм списко­
вых включений.
9. Напишите программу, выводящую элементы списка
[3 01304334 56 6 1 3] в обратной последовательности.
10. Напишите программу, которая выводит числе в диапазоне от 1 до 9,
кроме 5 и 7.
11. Напишите программу, выводящую сумму элементов списка
[3 01 304334 56 61 3], используя цикл for, while и метод sum.
12. Напишите программу, выводящую сумму элементов списка
[3 01304334 56 613], значения индексов которых делятся на без остатка
на 3, используя цикл for и while.
13. Сформируйте список, значения элементов которого находятся в диа­
пазоне от 23 до 35.

92

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

14. Сформируйте список, значения элементов которого находятся в диа­
пазоне от 3 до 15 с шагом 4.
15. Сформируйте список, значения элементов которого находятся в диа­
пазоне от 3 до 25 и без остатка делятся на 3.
16. Сформируйте словарь из двух списков [3 01 3 0 4 3 3 4 56 6 1 3] и [2,
4,7,26,33], используя встроенную функцию zip. Выведите словарь в консоль
и объясните, почему он получился такого вида.
17. Выведите различными способами в консоль элементы списка
[3 01304334 56 6 1 3] с их индексами.
18. Напишите программу, которая считывает целое число (месяц), после
чего выводит сезон к которому этот месяц относится.
19. Напишите программу, выводящую среднее из трех значений.
20. Напишите программу, выводящую таблицу умножения для задавае­
мого пользователем числа от 1 до 9 (включительно).

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

ТемаЗ
ФУНКЦИИ В PYTHON
В результате изучения данной темы обучающиеся должны:

знать





принципы объявления и использования функций,
механизм замыканий и правило LEGB,
принцип работы с рекурсивными функциями,
чем отличаются генераторы от списковых включений;

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

for,
• передавать аргументы в функцию и получать возвращаемый ею ре­
зультат;

владеть
• навыками написания приложений с использованием функций,
• навыками использования декораторов и генераторов в процессе на­
писания приложений.

Функции в Python относятся к объектам первого класса. Их мож­
но присваивать переменным, хранить в структурах данных, пере­
давать в качестве аргументов другим функциям и даже возвращать
в качестве значений из других функций [33]. Они могут принимать
произвольное количество аргументов или не принимать их вовсе.
Функции позволяют использовать код, заключенный в них,
сколько угодно раз в процессе выполнения программы и являют­
ся альтернативой любимого метода начинающих программистов:
«copy&paste». Они также позволяют разбивать сложные системы
на составные части, что упрощает процесс проектирования систем.
В табл. 3.1 приведены примеры всех операторов и выражений,
которые тем или иным образом связаны с функциями.
Таблица 3.1
Операторы и выражения, связанные с функциями

Оператор или выражение

Пример

Выражения вызовов

my test func('text', meat=ham, *rest)

def

def print_text(message) :
print('Hi, ' + message)

94

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Окончание табл. 3.1

Оператор или выражение

Пример

return

def add_test(a, b=l, *c):
return a + b + c[0]

global

x = 'old'
def changer():
global x; x = 'new'

nonlocal

def outer():
x = 'old'
def changer ():
nonlocal x; x = 'new'

yield

def test(x):
for i in range (x): yield i ** 2

lambda

funcs = [lambda x: x**2, lambda x: x**3]

Функции и методы класса в Python начинаются с оператора def.
В отличие от функций в компилируемых языках, таких как С, def
представляет собой исполняемый оператор. Это означает, что напи­
санная в коде функция не существует до тех пор, пока интерпрета­
тор Python не встретит и не выполнит ее. Функции можно вклады­
вать внутрь операторов if циклов while и даже других функций. При
типовом применении операторы def помещаются в файлы модулей
и естественным образом выполняются для генерации функций, ког­
да файл модуля, где они находятся, импортируется в первый раз.
Когда интерпретатор Python достигает оператора def и выполня­
ет его, генерируется новый объект, который присваивается имени
функции. Как и со всеми присваиваниями, имя функции становится
ссылкой на объект функции. Объекты функций также могут иметь
произвольные определяемые пользователем атрибуты, присоединя­
емые к ним для регистрации данных.
Помимо этого, функции можно также создавать с помощью
Zambda-выражений — средства, которое позволяет встраивать опре­
деления функций в места, где оператор def синтаксически не допу­
скается.
Когда функция вызывается, вызывающий код приостанавливает­
ся до тех пор, пока функция не завершит свою работу и не возвра­
тит управление обратно. Функции, которые вычисляют значение,
возвращают его вызывающему коду посредством оператора return
(return без значения просто возвращает управление в вызывающий
код и отправляет стандартный результат None).
yield — выражение, но оно обычно используется как оператор
и отправляет результирующий объект вызывающему коду, при этом
запоминая место, где он остановился. В функциях, известных как
генераторы, можно также использовать yield, чтобы посылать об95

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

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

3.1. Области видимости
Помимо упаковки кода, для многократного использования функ­
ции добавляют к программам дополнительный уровень, чтобы све­
сти к минимуму возможность возникновения конфликтов между
переменными с одним и тем же именем — по умолчанию все име­
на, которым выполнено присваивание внутри функции, ассоцииру­
ются с пространством имен данной функции и никаким другим:
— имена, присвоенные внутри def, могут быть видны только
внутри этого оператора def. Ссылаться на такие имена извне функ­
ции нельзя;
— имена, присвоенные внутри def, не конфликтуют с перемен­
ными за пределами def, даже если те же самые имена применяются
где-то в другом месте.
Переменные могут присваиваться в трех разных местах, соответ­
ствующих трем разным областям видимости:
— если переменная присваивается внутри def, то она будет ло­
кальной в этой функции;
— если переменная присваивается в объемлющем def, тогда она
будет нелокальной в отношении вложенных функций;
— если переменная присваивается за пределами всех def, то она
будет глобальной в целом файле.
Приведем описание схемы распознавания имен Python, которая
иногда называется правилом LEGB, согласно названиям областей
видимости.
1. Когда внутри функции указывается неуточненное имя, Python
ищет его максимум в четырех местах — в локальной (L, local) обла­
сти видимости, затем в локальных областях видимости любых объ­
емлющих (Е, enclosing) операторов def и lambda, далее в глобальной
(G, global) области видимости и, наконец, во встроенной (В, built-in)
области видимости. Поиск останавливается на первом же месте, где
96

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

обнаруживается имя. Если в результате такого поиска имя найти
не удалось, Python сообщит об ошибке.
2. Когда внутри функции выполняется присваивание имени,
Python всегда создает либо изменяет имя в локальной области ви­
димости, если только оно не было объявлено в этой функции как
глобальное или нелокальное.
3. Когда производится присваивание имени за пределами всех
функций (т. е. на верхнем уровне файла модуля или в интерактив­
ной подсказке), локальная область видимости совпадает с глобаль­
ной — пространством имен модуля.
Данные области видимости проиллюстрированы на рис. 3.1.
Встроенная область видимости (Python)
Имена, предварительно присвоенные в модуле builtins

Глобальная область видимости (модуль)
Имена, присвоенные на верхнем уровне файла модуля или
объявленные глобальными в операторе def внутри файла
Локальные области видимости объемлющих функций
Имена в локальной области видимости любых объем­
лющих функций (de/или lambda), от самой внутренней
до наружной
Локальная область видимости (функция)
Имена, так или иначе присвоенные внутри функции
и не объявленные глобальными в этой функции

Рис 3.7. Области видимости Python

В дополнение к приведенным выше областям видимости суще­
ствует еще три: временные переменные циклов в ряде включений,
переменные ссылок на исключения в некоторых обработчиках try
и локальные области видимости в операторах class.
3.1.1. Области видимости и вложенные функции
Каждая вложенная функция имеет свою локальную (вложенную)
область видимости, и с их добавлением слегка усложняются прави­
ла поиска переменных.
1. Ссылка (X) ищет имя X сначала в текущей локальной обла­
сти видимости (функция); затем в локальных областях видимости
любых лексически объемлющих функций в исходном коде, от вну­
тренней до наружной; далее в текущей глобальной области видимо­
сти (файл модуля); и, наконец, во встроенной области видимости
(модуль builtins). Объявления global заставляют поиск начинаться
в глобальной области видимости (файл модуля).
97

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2. Присваивание (X = значение) по умолчанию создает либо
изменяет имя X в текущей локальной области видимости. Если
имя X объявлено глобальным внутри функции, тогда присваивание
создает или изменяет имя X в области видимости включающего
модуля. Если же имя X объявлено нелокальным внутри функции,
то присваивание изменяет имя X в локальной области видимости
ближайшей объемлющей функции.

3.1.2. Пример работы с областями видимости
Для начала приведем простой пример, где X — глобальная пере­
менная и функция производит суммирование передаваемого ей зна­
чения со значением этой переменной:
х = 4
def test_func(y):
print(X + у)
test_func(4) # 8

Как видим из кода, все работает по описанным выше принципам
и, так как переменная X не была объявлена в области видимости
функции, Python обращается при ее поиске к области видимости
более высокого порядка.
Теперь добавим в функцию локальную переменную X = 3 и по­
смотрим, что изменится:
х = 4
def test_func(y):
X = 3
print(X + у)
test_func(4) # 7
print(X) # 4

Так как переменная X была объявлена локально и нет указаний
рассматривать ее как глобальную переменную (оператор global),
то Python будет считать, что нашел необходимую для выражения
переменную с именем X.
Рассмотрим, как работает оператор global:
х = 4
def test_func(y):
global X
X = 3
print(X + у)
test_func(4) # 7
print(X) # 3

Оператор global дает указание интерпретатору Python начинать
поиск переменной X с глобальной области видимости. Потом про­
исходит изменение значения этой переменной, что можно заметить
из результирующего вывода программы.
98

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

3.1.3. Замыкания
Суть данного подхода заключается в том, что указанный объект
функции помнит значения из объемлющих областей видимости, не­
взирая на то, присутствуют ли еще эти области видимости в памяти.
Фактически объект имеет присоединенные пакеты данных из памя­
ти (известные как сохранение состояния), которые локальны для
каждой созданной копии вложенной функции и часто выступают
в качестве простой альтернативы классам в такой роли.
Замыкания иногда применяются в программах, которым необ­
ходимо генерировать обработчики событий на лету в ответ на ус­
ловия, сложившиеся во время выполнения. Например, представим
себе графический пользовательский интерфейс, где нужно опреде­
лять действия согласно данным, введенным пользователем, которые
невозможно предугадать на этапе построения интерфейса. В таких
ситуациях нам требуется функция, создающая и возвращающая дру­
гую функцию с информацией, которая может варьироваться в за­
висимости от создаваемой функции.
Ниже представлен пример работы с замыканиями:
def maker(N):
def action(X): # Создание и возвращение функции action
return X ** N
return action
my_func = maker(3)
print(my_func(2)) # 8
print(my_func(4)) # 64
print(my_func(5)) # 125

В приведенном примере функция action сохраняет значение
N из объемлющей области видимости (т. е. области видимости
функции maker).
Также замыкание можно сделать с использованием lambdaфункции, которую будет возвращать основная (оборачивающая)
функция:
def maker(N):
return lambda X: X ** N
my_func = maker(3)
print(my_func(2)) # 8
print(my_func(4)) # 64
print(my_func(5)) # 125

Рассмотрим принцип работы оператора nonlocal на следующем
примере кода:
def counter(start):
state = start
def adder(X):
print(X + state)
return adder
99

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

F = counter(5)
F(3) # 8
F(5) # 10

В тех случаях, когда мы захотим в процессе выполнения замы­
кания изменить значение переменной state, при выполнении кода
будет сгенерировано следующее исключение:
def counter(start):
state = start
def adder(X):
print(X + state)
state += 1
return adder
F = counter(0)
F(3)
"""Traceback (most recent call last):
File "F:/code/python/myProjRep/main.py", line 9, in
F(3)
File "F:/code/python/myProjRep/main.py", line 4, in adder
print(X + state)
UnboundLocalError: local variable 'state' referenced before
assignment"""

Это связано с тем, что изменение значения из области видимо­
сти объемлющей функции по умолчанию запрещено. Для того, что­
бы обойти этот запрет, и используется оператор nonlocal:
def counter(start):
state = start
def adder(X):
nonlocal state
print(X + state)
state += 1
return adder
F = counter(5)
F(3) # 8
F(5) # 11

Следует отметить тот факт, что нелокальные (nonlocal) переменные
уже должны существовать в области видимости объемлющей функ­
ции (def), иначе произойдет ошибка во время работы программы:
def counter(start):
def adder(X):
nonlocal state
state = start
print(X + state)
state += 1
return adder
F = counter(5)
File "F:/code/python/myProjRep/main.py", line 3
nonlocal state
A

SyntaxError: no binding for nonlocal 'state' found
100

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Кроме того, оператор nonlocal ограничивает область поиска пере­
менной только областями видимостей объемлющих функций (def):
state = 10
def counter():
def adder(X):
nonlocal state
print(X + state)
state += 1
return adder
F = counter()
File "F:/code/python/myProjRep/main.py", line 4
nonlocal state
A

SyntaxError: no binding for nonlocal 'state' found

3.2. Аргументы функции
При передаче аргументов в функции необходимо запомнить сле­
дующие ключевые моменты.
1. Аргументы передаются в функцию путем автоматического
присваивания объектов именам локальных переменных. Аргумен­
ты функций, т. е. ссылки на (возможно) разделяемые объекты, от­
правленные вызывающим кодом, представляют собой еще один
пример присваивания в Python. Поскольку ссылки реализованы
в виде указателей, все аргументы в действительности передаются
через указатели, в связи с чем объекты, передаваемые как аргумен­
ты никогда автоматически не копируются.
2. Когда функция выполняется, имена аргументов в заголовке
функции становятся новыми локальными именами в области ви­
димости функции. Никакого совмещения имен аргументов функ­
ции и имен переменных в области видимости вызывающего кода
не происходит.
3. При модификации внутри функции передаваемого ей аргу­
мента, являющегося изменяемым объектом, его значение изменит­
ся и в вызывающем коде.
Разница в передаче изменяемых и неизменяемых объектов в ка­
честве аргументов в функции заключается в том, что:
— неизменяемые аргументы фактически передаются «по зна­
чению». Объекты, подобные целым числам и строкам, передаются
по ссылке на объекты, а не путем копирования, но из-за того, что
модифицировать на месте неизменяемые объекты невозможно, эф­
фект во многом похож на создание копий;
— изменяемые аргументы фактически передаются «по указате­
лю». Объекты вроде списков и словарей также передаются по ссыл­
101

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

ке на объекты, что аналогично способу передачи массивов как ука­
зателей в языке С, и их можно модифицировать на месте.
Для рассмотрения особенностей передачи объектов в функцию
используем следующий код:
def test(a, b, с):
print('a = {}; b = {}; с = {}'.format(a, b, c))
x, y, z = (1, [2, 10], 'Hi!')
test(x, y, z) # a = 1; b = [2, 10]; c = Hi!
print('x = {}; у = {}; z = {}'.format(x, y, z))
# x = 1; у = [2, 10]; z = Hi!

Как уже обозначили в предыдущем разделе, аргументам функции
присваиваются ссылки на объекты передаваемых на ее вход имен
переменных. Более наглядно это показано на рис. 3.2.
Имена переменных

Объекты

Вызывающий
код

1

[2, 10]
Функция

'Hi!'

Рис. 3.2. Присваивание ссылок на объекты в функции

Из рис. 3.2 видно, что независимо от того, изменяемый или
неизменяемый тип данных используется в качестве аргументов
функции, в случае ее вызова аргументам присваиваются ссылки
на объекты из вызывающей области. Чтобы убедиться в этом окон­
чательно, используем оператор id(), который возвращает уникаль­
ный идентификатор объекта, присваиваемый в момент выделения
под него область памяти:
def test(a, b, с):
print('a = {}; b = {}; с = {}'.format(id(a),
id(b), id(c)))
x, y, z = (1, [2, 10],
test(x, y, z)
# a = 140721789706640;
# C = 2740486981488
print('x = {}; у = {};
id(y), id(z)))
# X = 140721789706640;
# z = 2740486981488

102

'Hi!')
b = 2740459779272;

z = {}'.format(id(x),

у = 2740459779272;

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Теперь попробуем изменить значения объектов в функции и по­
смотрим, что из этого получится:
def test(a, b, с):
а = 5
b[0] = '0о'
с =
print('a = {}; b = {}; с = {}'.format(a, b, с))
print('a = {}; b = {}; с = {}'.format(id(a),
id(b), id(c)))
х, у, z = (1, [2, 10], 'Hi!')
test(x, у, z) # a = 5; b = ['Oo', 10]; c =
# a = 140721789706768; b = 2740487405256;
# C = 2740482508848
print('x = {}; у = {}; z = {}'.format(x, y, z))
#x=l;y=['Oo', 10]; z = Hi!
print('x = {}; у = {}; z = {}'.format(id(x), id(y), id(z)))
# x = 140721789706640; у = 2740487405256;
# Z = 2740479988528

В случае присваивания нового значения переменной, которая
хранит ссылку на объект неизменяемого типа, создается новый объ­
ект, на который теперь и будет ссылаться данная переменная. Это
видно по работе с аргументами функции, такими как «а» и «с». При
изменении значения объекта изменяемого типа новый объект соз­
даваться не будет (аргумент функции «Ь»).
На рис. 3.3 приведен пример описанного выше принципа работы
Python.
Имена переменных

Объекты

Вызывающий
код

1

['Оо', 10]
Функция
5

'Hi!'

Рис. 3.3. Изменение значений аргументов функции
Такой же принцип работы используется и при присваивании пе­
ременным уже существующих значений:

х, у, z = 1, [2, 10], 'Hi!'
а, b, с = х, у, z
print('a = {}; b = {}; с = {}'.format(id(a), id(b), id(c)))
# а = 140721789706640; b = 2740487423880;
# С = 2740485781232
print('x = {}; у = {}; z = {}'.format(id(x)> id(y), id(z)))
103

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

# х = 140721789706640; у
# z = 2740485781232
а = 5
b[0] = 'Oo'
с =
print('a = {}; b = {}; c
# a = 5; b = ['Oo', 10];
print('x = {}; у = {}; z
# x = 1; у = ['Oo', 10];
print('a = {}; b = {}; c
# a = 140721789706768; b
# C = 2740481511792
print('x = {}; у = {}; z
# x = 140721789706640; у
# z = 2740485781232

= 2740487423880;

=
c
=
z
=
=

{}'.format(a, b, c))
=
{}'.format(x, y, z))
= Hi!
{}'.format(id(a), id(b), id(c)))
2740487423880;

= {}'.format(id(x), id(y), id(z)))
= 2740487423880;

При присваивании переменным маленьких целочисленных
значений (от -5 до 256) оператор id() будет возвращать одно
и то же значение, даже в том случае, когда новая переменная не ссы­
лается на уже объявленный ранее объект. Это связано с оптимиза­
цией CPython, где для улучшения производительности небольшие
числа представляются одним объектом:
one = 4
two = one
three = 4
print('one = {}; two = {}; three = {}'.format(
id(one), id(two), id(three)))
# one = 140721789706736; two = 140721789706736;
# three = 140721789706736
print(id(one) == id(two) == id(three)) # True

3.2.1. Значения аргументов по умолчанию
Python, как и ряд других языков программирования, поддержи­
вает вызов функций и методов класса с их аргументами, значения
которых заданы по умолчанию. Для этого при объявлении функции
необходимо ее аргументу явно присвоить значение:
def test_default(a=10):
print(a)
test_default('Hi!') # Hi!
test_default() # 10

Наличие в функции аргументов, которые имеют значения
по умолчанию, позволяет производить ее вызов с меньшим числом
параметров:
def test_default2(a=10, b=12):
print(a+b)
test_default2() # 22
test_default2(4) # 16
test_default2(4,2) # 6

104

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Из приведенных выше примеров видно, что данные аргументы
не являются обязательными при вызове функции. Но если среди ар­
гументов должны быть обязательные, которые необходимо переда­
вать ей на вход при каждом вызове, их необходимо указывать в на­
чале списка аргументов:
def test_default2(a, b=12):
print(a+b)
test_default2(4) # 16
test_default2(4,2) # 6

Если первыми аргументами поставить необязательные (значе­
ния которых заданы по умолчанию), а следом за ними обязатель­
ные аргументы, то интерпретатор Python сообщит об ошибке:
def test_default2(b=12, а):
print(a+b)

test_default2(4,2)
File
line 1
def test_default2(b=12, a):
A

SyntaxError: non-default argument follows default argument

Правила расстановки обязательных и необязательных аргумен­
тов в функции рассматриваются в следующем параграфе.

3.2.2. Режимы сопоставления аргументов функции
Помимо уже рассмотренных ранее способов передачи аргумен­
тов в функции, Python предлагает дополнительные инструменты,
которые позволяют функции принимать необязательные аргумен­
ты, благодаря чему можно создавать гибкие API в модулях и классах.
Базовый вид такого объявления функции можно записать следу­
ющим образом:
def func_test([argl, ..., argN, *args, kwl, ...,
kwN, **kwargs]):

где в квадратные скобки заключены необязательные элементы,
argl — argN — позиционные аргументы, которые должны следовать
друг за другом; *args — итерируемая последовательность позици­
онных аргументов; kwl — kwN — ключевые аргументы (аргумен­
ты, значения которых задаются по умолчанию); **kwargs — сло­
варь ключевых элементов.
Далее рассмотрены примеры объявления функций с различными
способами сопоставления их аргументов и объектов вызывающего
кода.
1. Позиционная передача аргументов (сопоставляются слева на­
право) .
def custom_function(a, b, с):
print(a + b + с)
custom_function(10,4,8) # 22
105

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

При таком подходе выполняется сопоставление переданных
значений аргументов с именами аргументов в заголовке функции
по позиции, слева направо.
2. Передача по имени аргумента (сопоставляются по имени ар­
гумента).
def custom_function(a, b, с=2):
print(a + b + с)
custom_function(b=2, c=l, a=4) # 7

В качестве альтернативы позиционному способу сопоставления
в вызывающем коде можно указывать, какой аргумент в функции
получает значение посредством синтаксиса «имя=значение».
3. Переменное количество позиционных аргументов (сначала
сопоставляются единичные позиционные элементы, потом итери­
руемый список позиционных элементов, а далее аргументы сопо­
ставляются по имени).
def custom_function(a, b, с, *args):
print(a + b + с + sum(*args))
custom_function(1, 1, 1, [10, 20, 30])
# 63
custom_function(a=l, c=4, b=ll, [10, 20, 30])
..... File "", line 5
custom_function(a=l, c=4, b=ll, [10, 20, 30])
A

SyntaxError: positional argument follows keyword argument"""
def custom_function(a, b, c, *args, d):
print(a + b + c + sum(*args) - d)
custom_function(l, 1, 1, [10, 20, 30], d=4) # 59
custom_function(l, 1, 1, [10, 20, 30], 4)
II II II
TypeError
Traceback (most
recent call last)
in
2
print(a + b + c + sum(*args)-d)
3
- — > 4 custom_function(l, 1, 1, [10, 20, 30], 4)

TypeError: custom_function() missing 1 required keyword-only
argument: 'd' """

4. Передача произвольного количества позиционных и ключе­
вых элементов.
def custom_function4(*args, **kwargs):
if args:
print(args)
if kwargs:
print(kwargs)
custom_function4(10, 20, key='Hi', Oo = 100)
# (10, 20)
# {'key': 'Hi', 'Oo': 100}

106

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

При этом стоит учитывать, что имена аргументов *args и **kwargs
не является обязательным. Например:
def custom_function4(*a, **b):
if a:
print(a)
if b:
print(b)
custom_function4(10, 20, key='Hi', Oo = 100)
# (10, 20)
# {'key': 'Hi', 'Oo': 100}

5. Передача произвольного числа ключевых аргументов.
def custom_function5(**kwargs):
print(kwargs)
custom_function5(key='Hi', Oo = 100)
# {'key': 'Hi', 'Oo': 100}

Шаги, выполняемые Python для сопоставления аргументов перед
присваиванием, могут быть грубо описаны следующим образом:
1) присваивание не ключевых аргументов по позиции;
2) присваивание ключевых аргументов по совпадающим име­
нам;
3) присваивание добавочных не ключевых аргументов кортежу
*паше;
4) присваивание добавочных ключевых аргументов словарю
**паше;
5) присваивание стандартных значений не присвоенным аргу­
ментам в заголовке.
Затем Python проверяет, передается ли каждому аргументу толь­
ко одно значение, и если нет, возникает ошибка. Когда сопостав­
ление завершено, Python присваивает именам аргументов передан­
ные для них объекты.

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

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Для возвращения результатов работы функции в вызывающий
ее код используется оператор return. При его отсутствии в теле
функции по умолчанию возвращается None:
def test_return(a, b):
с = a + b

print(test_return(2, 4)) # None

Следующий пример возвращает в вызывающий код результат
суммы аргументов функции:
def test_return(a, b):
return a + b

print(test_return(2, 4)) # 6

Бывают случаи, когданужно вернуть не одно, а несколько значе­
ний. Здесь на помощь приходят кортежи:
def test_return(a, b):
с = а + b
m = а * b
е = а ** b
d = а / b
s = а - b
return c, m, e, d, s

print(test_return(4, 2)) # (6, 8, 16, 2.0, 2)

Чтобы присвоить значения из возвращаемого функцией кортежа
переменным, можно применить следующие методы распаковки по­
следовательностей:
x, у, q, w, z = test_return(4, 2)
print(x, у, q, w, z) # 6 8 16 2.0 2 #1
_, *my_list, _ = test_return(4, 2)
print(my_list) # [8, 16, 2.0] #2
y, *my_list, q = test_return(4, 2)
print(y, my_list, q) # 6 [8, 16, 2.0] 2 #3
w, _,
*my_list = test_return(4, 2)
print(w, my_list) # 6 [2.0, 2] #4

Символ «_» в данном случае используется как одноразовое имя
переменной и действует в качестве заполнителя для тех элементов
последовательности (кортежа), которые нам безразличны.
Когда нет уверенности, сколько элементов будет содержать воз­
вращаемый функцией кортеж, то при его распаковке можно исполь­
зовать переменную, которая соберет список значений. Это делается
путем размещения звездочки перед ее именем.
В примере «#1» все элементы кортежа распаковались и присвоились переменным именно в той последовательности, в которой рас­
полагались в нем. В примере «#2» отбрасываются крайние элементы
кортежа и распаковываются в список элементы, располагающиеся
108

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

между ними. В примере «#3» крайние значения крайних элемен­
тов записываются в переменные с именами «у» и «q», а в остальном
распаковка ведет себя, как и в предыдущем примере. Четвертый
пример тем интересен, что отбрасываются при распаковке второй
и третий элемент кортежа.
Если при распаковке не используется подход сбора части эле­
ментов последовательности в список, то главное — придерживать­
ся правила, что количество переменных, которым присваиваются
значения распаковываемой последовательности, должны равняться
количеству элементов этой самой последовательности:
w, _ = test_return(4, 2)
print(w, my_list)
II II II

ValueError
recent call last)
in
- — > 1 w, _ = test_return(4, 2)
2 print(w, my_list)

Traceback (most

ValueError: too many values to unpack (expected 2) """

Также запрещается использовать несколько имен переменных
с приставкой в виде символа «*»:
*first, *second = test_return(4, 2)
File
line 4
SyntaxError: two starred expressions in assignment

Вместо кортежей можно также использовать словарь.

3.4. Рекурсия
Рекурсивные функции — функции, которые вызывают самих
себя либо прямо, либо косвенно с целью организации цикла. Рекур­
сия относительно редко используется при написании кода в Python,
отчасти из-за того, что процедурные операторы включают более
простые циклические структуры.
Рекурсия позволяет программам обходить структуры, которые
имеют произвольную непредсказуемую форму и глубину, например,
при планировании маршрутов в путешествии, анализе языка и т. д.
Рассмотрим различные варианты написания рекурсивной функ­
ции на примере задачи суммирования элементов последовательно­
сти:
def my_sum (L) :
print (L)
if not L:
return 0
else:
return L[0] + my_sum(L[l:])
109

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

print(my_sum([1, 2, 3, 4, 5, 20, 30]))
II II II

[1, 2, 3, 4, 5, 20, 30]
[2, 3, 4, 5, 20, 30]
[3, 4, 5, 20, 30]
[4, 5, 20, 30]
[5, 20, 30]
[20, 30]
[30]
[]
65

Этот же код можно записать с использованием тернарного вы­
ражения if/else:
def my_suml(L):
return 0 if not L else L[0] + my_suml(L[l:])
print(my_suml([l, 2, 3, 4, 5, 20, 30])) # 65
def my_sum2(L):
return L[0] if len(L) == 1 else L[0] +
my_sum2(L[l:])
print(my_sum2([1, 2, 3, 4, 5, 20, 30])) # 65
def my_sum3(L):
first, *rest = L
return first if not rest else first +
my_sum3(rest)
print(my_sum3([1, 2, 3, 4, 5, 20, 30])) # 65

Как уже отмечалось, рекурсия может быть прямой или косвен­
ной. Ранее рассматривался прямой вызов функцией самой себя.
Косвенная рекурсия организуется следующим образом:
def my_sum4(L) :
if not L: return 0
return nonempty(L)
def nonempty(L) :
return L[0] + my_sum4(L[l:])
print(my_sum4([l, 2, 3, 4, 5, 20, 30])) # 65

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

3.5. Аннотация функций
Начиная с версии 3.6 функциям можно задавать типы аргу­
ментов и тип возвращаемого значения. Синтаксически аннотация
функции записывается в строке ее заголовка. Для аргументов анно­
110

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

тация указывается после двоеточия, следующего непосредственно
за его именем, а для возвращаемых значений после символа «->»,
следующих за списком аргументов:
def sum_ab(a: int, b: float) -> float:
return a+b

Вызывается аннотированная функция, как и обычная, но при
наличии аннотаций Python накапливает их в словаре и присо­
единяет его к самому объекту функции посредством атрибута__
annotations__:
print(sum_ab.__ annotations__ )
# {'a': cclass 'int’>, 'b': cclass 'float'>,
# 'return': }

Как видно из приведенного примера, имена аргументов и анно­
тация возвращаемого значения, если имеется (сохраняется под клю­
чом return), записываются в качестве ключа словаря, а в качестве
значений записываются выражения аннотации.
Поскольку аннотации являются всего лишь объектами Python
(словарем), присоединенными к другому объекту (функции), прой­
тись по их элементам можно следующим способом:
for arg in sum_ab.__ annotations__ :
print(arg, '=>', sum_ab.__ annotations__ [arg])
# a => cclass 'int'>
# b => cclass 'float'>
# return => cclass 'float'>

Но аннотации функций могут содержать не только типы аргумен­
тов и возвращаемого значения, ведь это только один из способов
их использования. Согласно РЕР8, проверка типов в Python не обя­
зательна и является отдельным инструментом. По умолчанию интер­
претаторы Python не должны выдавать никаких сообщений проверки
типов и не должны изменять свое поведение на основе аннотаций.
Перепишем аннотацию объявленной ранее функции следующим
образом:
def sum_ab(a: 'what?', b: 99, с) -> 'holy python!!!':
return a+b
print(sum_ab.__ annotations__ )
# {'a': 'what?', 'b': 99, 'return': 'holy python!!!'}
for arg in sum_ab.__ annotations__ :
print(arg, '=>', sum_ab.__ annotations__ [arg])
# a => what?
# b => 99
# return => holy python!!!

Или так:
def sum_ab(a:'what?'=2, b:(4, 5)=99, c:float=3.5) -> 'holy
python!!!':
return a+b+c
print(sum_ab.__ annotations__ )

111

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

# {'a': 'what?', 1b': (4, 5), 'c': ,
# 'return': 'holy python!!!' }
for arg in sum_ab.__ annotations__ :
print(arg, '=>', sum_ab.__ annotations__ [arg])
# a => what?
# b => (4, 5)
# c =>
# return => holy python!!!

В показанном выше коде запись аргумента и его аннотация
«a:'what?'=2» означают, что аргумент «а» имеет стандартное значе­
ние 4 и аннотирован строкой 'what?'. Аргумент «Ь» аннотирован
кортежем с элементами (4, 5) и т. д.
Таким образом, эту функцию можно спокойно вызывать со зна­
чениями по умолчанию или передавать на ее вход значения аргу­
ментов:
print(sum_ab()) # 104.5
print(sum_ab(l,3)) # 7.5
print(sum_ab(c=-5)) # 96

3.6. Лямбда-функции (выражения)
Ключевое слово lambda в Python предоставляет краткую форму
для объявления небольших анонимных функций. Лямбда-функции
ведут себя точно так же, как обычные функции, объявляемые клю­
чевым словом def, и могут использоваться всякий раз, когда требу­
ются объекты-функции.
Например, следующий код:
my_add = lambda х, у: х+у
print(my_add(l, 2)) # 3

эквивалентен:
def my_add(x, у):
return х+у
print(my_add(l, 2)) # 3

Концептуально лямбда-выражение
lambda х, у: х+у

аналогично объявлению функции при помощи ключевого слова
def, только записывается в одну строку. Основное отличие здесь
в том, что перед использованием не производится связывание объ­
екта-функции с ее именем.
Ввиду этого приведенное выше лямбда-выражение можно вы­
полнить сразу же при объявлении:
print((lambda х, у: х+у)(1, 2)) # 3
112

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Следующее различие между определениями лямбд и обычных
функций заключается в том, что лямбда-функция ограничена одним-единственным выражением. То есть в них не могут применять­
ся инструкции или аннотации (даже инструкция return). При этом
всегда существует неявная инструкция return, которая автоматиче­
ски возвращает результат выражения.
Этот обеспечивает удобную и краткую форму для определения
функции в Python там, где одним из аргументов функции или мето­
да является объект-функция. Примером может служить написание
кратких и сжатых функций для сортировки итерируемых объектов
по альтернативному ключу, где к каждому элементу последователь­
ности применяется лямбда-функция:
list_tuples = [Cd', 4), Cb-, 2), ('а', 1),
('с', 3), ('а', 0)]
print(sorted(list_tuples))
0), ('а', 1), ('Ь', 2), ('с', 3), ('d', 4)]
print(sorted(list_tuples, key=lambda x: x[l]))
0)j ('a', 1), ('b', 2), ('с', 3), ('d', 4)]
print(sorted(list_tuples, key=lambda x: x[0]))
1), ('a', 0), ('b', 2), ('c', 3), (’d', 4)]

Или можно использовать так:
print(sorted(range(-5, 6), key=lambda
# [0, -1, 1, -2, 2, -3} 3, -4, 4, -5,
print(sorted(range(-5, 6), key=lambda
# [-1, -3, -5, -4, -2, 0, 1, 2, 3, 4,

x: x * x))
5]
x: x ** x))
5]

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

3.7. Декораторы
Декораторы в Python позволяют расширять и изменять поведе­
ние вызываемых объектов (функций, методов и классов) без необ­
ратимой модификации самих вызываемых объектов. Поэтому лю­
бая достаточно универсальная функциональность, которую можно
присоединить к разрабатываемому классу или функции, является
отличным кандидатом для декорирования:
— ведение протокола операций (логирование);
— контроль за доступом и аутентификацией;
— хронометраж;
— ограничение частоты вызова API;
— кэширование и др.
113

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В основе декораторов лежит нескольких продвинутых концепций
Python, включая свойства функций первого класса:
1) функции являются объектами — их можно присваивать пе­
ременным, передавать в другие функции и возвращать из других
функций;
2) функции могут быть определены внутри других функций —
и дочерняя функция может захватывать локальное состояние роди­
тельской функции (лексические замыкания).
Самый простой декоратор можно записать следующим образом:
def free_decorator(function):
return function

Реализованный выше декоратор — это по сути вызываемый объ­
ект (функция), который на входе принимает еще один вызываемый
объект и возвращает тот же самый вызываемый объект без его изме­
нения. Чтобы показать, как происходит декорирование, объявим еще
одну функцию и обернем ее в уже реализованный ранее декоратор:
def my_func():
return "It's work!!!"
my_func = free_decorator(my_func)
my_func() # "It's work!!!"

Python предоставляет дополнительный синтаксис «@» для деко­
рирования функции. Благодаря ему нет надобности явным образом
вызывать декоратор с передаваемой ему с функцией и затем снова
присваивать его переменной:
@free_decorator
def my_func():
return "It's work!!!"
my_func() # "It's work!!!"

«@» декорирует функцию непосредственно во время ее опреде­
ления, что делает очень трудным получение доступа к недекорированному оригиналу без различных «фокусов». Поэтому для сохра­
нения способности вызывать как декорированную, так и исходную
функцию используют «ручной» режим декорирования.
Далее рассмотрим пример того, как декоратор может менять по­
ведение оборачиваемой в него функции. Для этого реализуем сле­
дующий декоратор:
def up_register(func):
def wrapper():
original_result = func()
modified_result = original_result.upper()
return modified_result
return wrapper

@up_register
def my_func():
return "It's work!!!"
my_func() # "IT'S WORK!!!"

114

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Замыкание wrapper имеет доступ к недекорированной входной
функции, и оно свободно может выполнить дополнительный про­
граммный код до и после ее вызова. Также, в отличие от предыду­
щего примера декоратора, текущий при декорировании функции
возвращает не исходный, а другой объект-функцию. Таким образом,
единственный способ повлиять на «будущее поведение» входной
функции, которую декорируем, состоит в том, чтобы подменить
(или обернуть) входную функцию замыканием.
Теперь рассмотрим применение нескольких декораторов к функ­
ции на примере HTML-разметки:
def hl_html(function):
def wrapper():
return '' + function() + ''
return wrapper
def body(function):
def wrapper():
return '' + function() + ''
return wrapper

@body
@hl_html
def my_func():
return "It's work!!!"
print(my_func())
# It's work!! !

Результат этой функции ясно показывает, что декораторы были
применены снизу вверх. То есть сначала входная функция была
обернута декоратором @hl_html, и затем результирующая (декори­
рованная) функция была обернута декоратором @body.
В «ручном» режиме декорирования такой подход выглядит сле­
дующим образом:
my_decfunc = body(hl_html(my_func))
print(my_decfunc ())
# It's work!! !

На практике в глубоких уровнях декорирования функций нет
проблем, но это может сказаться на производительности при работе
над вычислительно емким программным кодом, в котором декори­
рование применяется часто.
Но такой подход к декорированию не сработает, когда начальная
функция имеет аргументы:
def body(function):
def wrapper():
return '' + function() + ''
return wrapper

@body
def my_func(name):
115

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

return name+", It's work!!!"
my_func('Alex')
II II II

TypeError
recent call last)
in
7 def my_func(name):
8
return name+", It's work!!!"
- — > 9 my_func('Alex')

Traceback (most

TypeError: wrapper() takes 0 positional arguments but 1 was
given
— • -

II II II

Чтобы избежать этой ошибки, замыкающая функция также
должна принимать аналогичный перечень аргументов:
def body(function):
def wrapper(name):
return '' + function(name) + ''
return wrapper

@body
def my_func(name):
return name+", It's work!!!"
my_func('Alex')
# Alex, It's work!!!

Далее рассмотрим, как декорировать функцию с произвольным
количеством аргументов. В данном случае на помощь приходят та­
кие функциональные средства языка Python как "args и ""kwargs для
работы с неизвестными количествами аргументов:
def proxy(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper

Теперь перепишем функцию my_func следующим образом:
def my_func(name = "Alex", line="It's work!!!"):
return f'{name}, {line}'

и функцию wrapper так:
def proxy(func):
def wrapper(*args, **kwargs):
print(f'ТРАССИРОВКА: вызвана'
f' {func.__ name__ }() '
f'c {args}, {kwargs}')
original_result = func(*args, **kwargs)
print(f'ТРАССИРОВКА: {func.__ name__ }() '
f'вернула {original_result!r}')
return original_result
return wrapper

116

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

При декорировании функции и последующем ее вызове с различ­
ным количеством аргументов и их заданием будут выведены следу­
ющие значения:
print(my_func())
# ТРАССИРОВКА: вызвана my_func() с (), {}
# ТРАССИРОВКА: my_func() вернула "Alex,It's work!!!"
# Alex, It's work!!!
print(my_func('Jon'))
# ТРАССИРОВКА: вызвана my_func() c ('Jon',), {}
# ТРАССИРОВКА: my_func() вернула "Jon, It's work!!!"
# Jon, It's work!!!
print(my_func(line="A_A", name = 'Jon'))
# ТРАССИРОВКА: вызвана my_func() c (),
# {'line': 'A_A', 'name': 'Jon'}
# ТРАССИРОВКА: my_func() вернула 'Jon, A_A'
# Jon, A_A
print(my_func("Fine", "you win!!!"))
# ТРАССИРОВКА: вызвана my_func() c ('Fine',
# 'you win!!!'), {}
# ТРАССИРОВКА: my_func() вернула 'Fine, you win!!!'
# Fine, you win!!!

3.8. Генераторы
По своей сути генераторные функции (функции с оператором
yield.) или выражения (похожи на списковые включения) в Python
являются синтаксическим сахаром для простого написания итера­
тора и позволяют откладывать создание результатов всякий раз,
когда это возможно. Из-за того, что ни та, ни другая конструкция
не создает сразу весь результирующий список, они экономят про­
странство памяти и позволяют распределить время вычислений
по запросам результатов.

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

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

круг точки генерации значений. По этой причине они часто будут
служить удобной альтернативой заблаговременному вычислению
полной серии значений с последующим сохранением и восста­
новлением вручную состояния посредством классов. Состояние,
которое генераторные функции сохраняют, когда приостанавли­
ваются, содержит местоположение в коде и полную локальную
область видимости. Следовательно, их локальные переменные
помнят информацию между выпуском результатов и делают ее до­
ступной при возобновлении выполнения функциями. В генератор­
ных функциях вместо инструкции возврата return используется
инструкция yield.
Рассмотрим принцип работы генераторной функции на примере
генерации возрастающей последовательности чисел от нуля до 5:
def gen_test():
N = 0
while N < 5:
yield N
N += 1
for x in gen_test():
print(x, end=' ')
#01234

Чтобы лучше понять принцип работы генераторов в Python, реа­
лизуем функцию, тело которой состоит только из yield, и результат
работы не отличается от того, что приведен в примере выше:
def gen_surprise_test():
yield 0
yield 1
yield 2
yield 3
yield 4
for x in gen_surprise_test():
print(x, end=' ')
#01234

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

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

исключение Stopiteration, сигнализируя о том, что у нее больше нет
значений, которые она могла бы предоставить:
iterator = gen_surprise_test()
print(iterator)

print(next(iterator))
0
print(iterator.__ next__ ()) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
print(next(iterator)) # 4
print(next(iterator)) # Stopiteration

Таким образом, генераторные функции позволяют избежать вы­
полнения всей работы заранее, что особенно полезно, когда резуль­
тирующие списки большие или получение каждого значения требу­
ет длительных вычислений.
Отдельно при работе с генераторными функциями следует уде­
лить внимание методу send. Он осуществляет переход на следую­
щий элемент в серии результатов, в точности как__next__ , но также
снабжает вызывающий код возможностью взаимодействия с гене­
ратором для влияния на его работу:
def gen_send_test(N):
for х in range(N):
Y = yield x
print(Y)

iterator = gen_send_test(2)
print(next(iterator)) # 0
print(iterator.send('-_-'))

# -_# 1
print(iterator.send('o_o'))
# 00

# Stopiteration

Генераторная функция также позволяет перехватить прерывание
итерации по генерируемой последовательности:
def exept_gen_test(N):
try:
for x in range(N):
yield x
except GeneratorExit:
print('Exception!!!')
print('happy end')
for it in exept gen test(30):
if it >= 10:
break
print(it, end=' ')

119

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

#0123456789 Exception!!!
# happy end
# при (exept_gen_test(9))
#012345678 happy end

3.8.2. Генераторные выражения

Поскольку функциональность генераторных функций оказалась
удобной, со временем понятия итерируемых объектов и списковых
включений были объединены в новый инструмент — генератор­
ные выражения. Синтаксически генераторные выражения похожи
на нормальные списковые включения и поддерживают весь их син­
таксис, в том числе фильтры if и вложение циклов, но они помеща­
ются в круглые скобки, а не в квадратные:
print([x+2 for х in range(5)]) # [2, 3, 4, 5, 6]
print((x+2 for x in range(5)))
#

Для более наглядного примера перепишем следующий код, ис­
пользующий генераторную функцию:
def gen_func(N):
for it in range(N):
yield

func_iter = gen_func(4)
for it in func_iter:
print(it)
# -_#
#
#

на код, использующий генераторное выражение:
new_func_iter =
for _ in range(4))
for it in new_func_iter:
print(it)
#или
for it in
print(it)

for _ in range(4)):

Добавим в данный пример фильтрацию на четность:
for it in
print(it)
# -_#

120

for x in range(4) if x % 2 == 0):

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

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

Резюме
В данном разделе были рассмотрены функции и все, что с ними
связано в Python. При их объявлении необходимо обращать внима­
ние на следующие моменты:
— применяйте глобальные переменные, только когда они понастоящему нужны;
— не модифицируйте изменяемые аргументы, если только та­
кое изменение не ожидается вызывающим кодом;
— каждая функция должна иметь единственное назначение.
Декораторы определяют структурные блоки многократного ис­
пользования, которые можно применять к вызываемому объекту
с целью модификации его поведения без необратимого изменения
самого вызываемого объекта. Синтаксис с использованием «@» —
сокращенная запись для вызова декоратора с входной функцией.
Многочисленные декораторы, размещенные над одной-единственной функцией, применяются снизу вверх.
Генераторные функции (функции-генераторы) — «синтаксиче­
ский сахар» для написания объектов, которые поддерживают про­
токол итератора. Инструкция yield позволяет временно приоста­
навливать исполнение генераторной функции и возвращать из нее
значения. Исключения Stopiteration вызываются после того, как по­
ток управления покидает генераторную функцию каким-либо иным
способом, кроме инструкции yield.
Генераторные выражения (выражения-генераторы) похожи
на включения в список, но не конструируют объекты-списки. Вме­
сто этого они генерируют значения «точно в срок» подобно тому,
как это делают итераторы на основе класса или генераторные функ­
ции. Как только генераторное выражение было использовано, оно
не может быть перезапущено или использовано заново.

Вопросы и задания для самопроверки
1. В чем смысл написания функций?
2. В какой момент Python создает функцию?
3. Что функция возвращает, если в ней нет ни одного оператора return?

121

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

4. Когда выполняется код, вложенный внутрь оператора определения
функции?
5. Что выведет следующий код и почему?

X = 'Spam'
def func () :
print(X)
func()
6. Что выведет следующий код и почему?

X = 'Spam'
def func () :
X = 'N1!'
func ()
print (X)

7. Что выведет следующий код и почему?
X = 'Spam'
def func () :
X = 'N1'
print(X)
func()
print(X)
8. Что выведет следующий код и почему?

X = 'Spam'
def func () :
global X
X = 'N1'
func()
print(X)
9. Что выведет следующий код в Python 3.x и почему?

def func () :
X = 'N1'
def nested() :
nonlocal X
X = 'Spam'
nested()
print(X)
func()

10. Каким будет вывод следующего кода и почему?
def func(a, b=4, с=5) :
print (а, Ь, с)
func(l, 2)

11. Каким будет вывод такого кода и почему?
def func(a, b, с=5) :
print(a, b, с)
func(l, c=3, b=2)

122

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

12. Что насчет следующего кода: каким будет вывод и почему?

def func(a, *pargs) :
print(a, pargs)
func(l, 2, 3)

13. Что выведет этот код и почему?
def func(a, **kargs) :
print(a, kargs)
func(a=l, c=3, b=2)
14. Каким будет вывод следующего кода и почему?

def func(a, b, с=3, d=4) : print (a, b, c, d)
func(l, *(5, 6))

15. Каким образом связаны друг с другом лямбда-выражения и операторы
def?
16. Какой смысл применять лямбда-выражения?
17. Что такое аннотации функций и как их использовать?
18. Что такое рекурсивные функции и как их применять?
19. Назовите несколько универсальных принципов проектирования
функций.
20. Назовите три или больше способов передачи результатов функций
вызывающему коду.
21. В чем разница между помещением спискового включения в квадрат­
ные скобки и в круглые скобки?
22. Как связаны между собой генераторы и итераторы?
23. Как определить, является ли функция генераторной?
24. Что делает оператор yield?

Упражнения
1. Напишите функцию, вычисляющую максимальное из трех чисел.
2. Напишите функцию, которая возвращает сумму элементов списка.
3. Напишите функцию, которая возвращает произведение элементов
списка.
4. Напишите функцию, которая возвращает инвертированную строку,
подаваемую ей на вход.
5. Напишите функцию для вычисления факториала задаваемого числа.
6. Напишите функцию, которая проверяет, входит ли задаваемое зна­
чение в определенный диапазон или нет.
7. Напишите функцию, которая подсчитывает количество элементов
в нижнем и верхнем регистрах у входной строки.
8. Напишите функцию, удаляющую повторяющиеся элементы в списке.
9. Напишите функцию, выводящую элементы списка с четным индексом.
10. Напишите функцию, которая проверяет, является ли строка палин­
дромом (читается одинаково как слева направо, так и наоборот).
11. Напишите декоратор, обертывающий возвращаемое строковое зна­
чение функции в тег « ».
12. Декорируйте функцию из первого упражнения таким образом, чтобы
возвращаемое ей значение возводилось в квадрат.

123

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

13. Декорируйте функцию из четвертого упражнения таким образом,
чтобы возвращалась строка в верхнем регистре.
14. Напишите декоратор, выводящий значения аргументов, подаваемых
на вход декорируемой функции.
15. Напишите генераторную функцию, позволяющую проводить итера­
цию по значениям в диапазоне от 23 до 37.
16. Напишите генераторную функцию, позволяющую проводить итера­
цию по значениям в диапазоне от 5 до 37 с шагом 4.
17. Напишите генераторное выражение, позволяющее итерироваться
по последовательности чисел от 0 до 15.
18. Напишите генераторное выражение, позволяющее итерироваться
элементам списка [3 0130433456613] возводя их при этом в квадрат.
19. Напишите генераторную функцию, позволяющую проводить итера­
цию по значениям в диапазоне от 0 до 100 с шагом, регулируемым в процессе
ее работы.
20. Сформируйте список, используя для этого реализованные ранее ге­
нераторные функции из упражнений 15 и 16.

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 4
МОДУЛИ И ПАКЕТЫ
В результате изучения данной темы обучающиеся должны:

знать
• принципы организации модулей и пакетов,
• существующие механизмы импортирования модулей и пакетов, а также
их отдельных атрибутов;

уметь
• создавать и импортировать собственные модули,
• создавать и импортировать собственные пакеты;

владеть
• навыками написания приложений с использованием модулей и паке­
тов.

Модуль в Python — это единица организации программ наи­
высшего уровня, которая упаковывает программный код и данные
для многократного использования и предоставляет изолированные
пространства имен, сводящие к минимуму конфликты имен пере­
менных внутри программ. В Python каждый файл является модулем,
а модули импортируют другие модули, чтобы задействовать опреде­
ляемые в них имена. Для работы с ними используются два операто­
ра и одна функция:
1) import — позволяет импортирующему коду извлечь модуль
как единое целое;
2) from — позволяет клиентам извлекать отдельные имена
из модуля;
3) imp.reload — предоставляет способ перезагрузки кода модуля
без остановки интерпретатора Python.
Модули позволяют довольно простым способом организовывать
отдельные компоненты в единую систему, выступая в качестве изо­
лированных пакетов переменных, которые известны как простран­
ства имен. Все имена, определенные на верхнем уровне файла моду­
ля, становятся атрибутами объекта импортированного модуля.
В основном модули используют для следующего:
1) многократное использование кода;
2) разбиение пространства имен системы. В основном модули
можно рассматривать как изолированные пакеты имен, которые
125

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

запрещают доступ к имени в другом файле, пока этот файл явно
не импортируется. Такое решение помогает избежать конфликтов
имен в разрабатываемых программах. В данном случае необходимо
запомнить следующую особенность Python — абсолютно все «суще­
ствует» в модуле: запускаемый код и создаваемые объекты всегда
неявно заключаются в модули;
3) реализация совместно используемых служб или данных. Мо­
дули удобны для реализации компонентов, которые совместно ис­
пользуются в рамках системы и потому требуют только одной ко­
пии. Например, если необходимо предоставить глобальный объект,
применяемый в нескольких функциях или файлах, тогда его можно
реализовать в модуле, который затем будет импортироваться мно­
гими клиентами.

4.1. Принцип работы импортирования
Импортирование в Python — не просто вставка одного текста
внутрь другого, а операция со своим временем выполнения, кото­
рая протекает за три отдельных шага, при условии, что модуль им­
портируется впервые:
1) поиск файла модуля;
2) компиляция импортируемого модуля в байт-код (при необхо­
димости);
3) выполнение кода модуля для создания объектов, которые
в нем определены.
Следует запомнить, что все три описанных выше шага выпол­
няются только при первом импортировании модуля во время вы­
полнения программы. При последующем их импортировании (того
же самого модуля) во время выполнения программы интерпрета­
тор просто обходит эти шаги стороной, так как результат их выпол­
нения уже загружен в память. Python хранит информацию по за­
груженным модулям в словаре по имени sys.modules. Если данные
по загружаемому модулю отсутствуют в этом словаре, тогда запу­
скается описанный выше процесс.

4.1.1. Поиск файла подключаемого модуля
В большинстве случаев конфигурирование пути поиска файлов
не требуется. Однако если есть необходимость импортировать фай­
лы из сторонних каталогов, то необходимо знать, как его настраи­
вать. Грубо говоря, путь поиска модулей Python формируется объ­
единением следующих главных компонентов, часть которых задана
заранее, а другие можно конфигурировать для указания, где прово­
дить поиск.
126

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

1. Домашний каталог программы. Поиск всегда начинается
с домашнего каталога. В этом случае, если все модули программы
расположены в единственном каталоге, не требуется какая-либо
конфигурация путей. Все будет работать автоматически.
2. Каталоги PYTHONPATH (если установлены). PYTHONPATH —
список определяемых пользователем имен каталогов, которые
содержат файлы с кодом, являющийся системной переменной
операционной системы с таким же названием (PYTHONPATH).
Устанавливать PYTHONPATH нужно только при импорте файлов
из-за границ домашнего каталога программы, либо при написании
программ значительного размера.
3. Каталоги стандартной библиотеки. Следом за каталогами,
указанными в PYTHONPATH, Python автоматически выполняет по­
иск в каталогах, где располагаются стандартные библиотечные мо­
дули. Поиск в них происходит всегда, что не требует добавления
данных каталогов в переменную PYTHONPATH или их включения
в файлы конфигурации путей (см. далее).
4. Содержимое любых файлов .pth (при их наличии). Это еще
один инструмент, который позволяет пользователям добавлять
каталоги в путь поиска модулей, просто перечисляя их по одному
в строке внутри текстового файла с расширением .pth (от path —
путь). Такие файлы конфигурации путей являются несколько более
развитым средством и предлагают альтернативу настройкам пере­
менной PYTHONPATH.
5. Каталог сторонних расширений «Lib\site-packages» (устанав­
ливается автоматически). Самым последним Python проверяет на­
личие искомого файла в подкаталоге модулей (site-packages) своей
стандартной библиотеки. Сюда автоматически устанавливается
большинство сторонних расширений. Именно из-за того, что этот
каталог всегда является частью пути поиска модулей, разработчик
спокойно может импортировать модули устанавливаемых расшире­
ний безо всяких настроек путей.
Все приведенные пути поиска хранятся в sys.path. Это изменяе­
мый список строк с именами каталогов:
import sys
if __ name__ == '__ main__ ':
print(sys.path)
"""['F:\\code\\python\\myProjRep ', 'F:\\code\\python\\
myProjRep1, 'C:\\Users\\madcomrade\\Anaconda3\\python37.
zip', 'C:\\Users\\madcomrade\\Anaconda3\\DLLs 1, 'C:\\Users\\
madcomrade\\Anaconda3\\lib', 'C:\\Users\\madcomrade\\
Anaconda3', 'F:\\code\\python\\myProjRep\\venv', 'F:\\code\\
python\\myProjRep\\venv\\lib\\site-packages', 'F:\\code\\
python\\myProjRep\\venv\\lib\\site-packages\\setuptools-40.8.0py3.7.egg', 'F:\\code\\python\\myProjRep\\venv\\lib\\sitepackages\\pip-19.0.3-py3.7.egg'] """
127

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В большинстве случаев sys.path не нуждается в изменениях,
но всегда есть исключение. Например, сценарии, запускаемые
на веб-серверах, зачастую запускаются от имени пользователя
nobody (никто). Это делается для того, чтобы ограничить доступ.
При таком подходе до выполнения любых операторов import sys.
path устанавливается вручную. Для этого вполне хватает метода
sys.path.append() или sys.path.insert() (действует в течение только
одиночного запуска программы).
При наличии файлов с одинаковыми именами в разных катало­
гах загрузится тот, который встречается первым при проходе путей
поиска, прописанных в sys.path (слева направо). Такое же правило
действует и в случае, когда файлы находятся в одном каталоге. Од­
нако не факт, что такой подход гарантированно останется одинако­
вым с течением времени либо не будет отличаться в разных реали­
зациях Python. В связи с этим назначайте модулям несовпадающие
имена или конфигурируйте путь поиска модулей так, чтобы сделать
предпочтения выбора модулей явными.
4.1.2. Компиляция импортируемого модуля в байт-код

После того, как файл был найден, Python при необходимости
компилирует его в байт-код. Во время операции импорта осущест­
вляется проверка времени модификации подключаемых файлов ис­
ходного кода и уже существующего байт-кода, а также номер вер­
сии Python байт-кода. В первом случае используются временные
отметки файлов, а во втором — имя файла, которое содержит со­
держат версию Python, которой он компилировался. На основе этих
проверок принимается одно из следующих решений:
1) компилировать (сгенерировать байт-код заново), если файл
байт-кода старше файла исходного кода (т. е. исходный код был мо­
дифицирован) или был создан другой версией Python;
2) не компилировать, если файл байт-кода .рус не старше файла
исходного кода и создан текущей версией интерпретатора Python.
Если при поиске файла импортируемого модуля находится его
файл байт-кода, но не сам файл исходного кода, то Python загружает
байт-код. Такой механизм делает возможной поставку программы
в виде только файлов байт-кода (без передачи исходного) и уско­
ряет начальный запуск программы, так как этап компиляции про­
пускается.
Обратите внимание, что компиляция происходит во время им­
портирования файла. Из-за этого вы обычно не будете видеть файл
байт-кода .рус для файла верхнего уровня своей программы, если
только он также не импортируется где-то в другом месте — лишь
импортированные файлы оставляют после себя файлы . рус на ком­
пьютере. Байт-код файлов верхнего уровня применяется внутренне
128

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

и отбрасывается; байт-код импортированных файлов сохраняется
в файлах для ускорения будущих операций импорта.

4.1.3. Выполнение кода модуля
Все операции в файле выполняются по очереди, от начала до кон­
ца, и любые присваивания именам на данном шаге генерируют
атрибуты результирующего объекта модуля. Например, операторы
def в файле запускаются на стадии импортирования для создания
объектов функций и их присваивания атрибутам внутри объекта
модуля. Функции затем вызываются в файлах, импортирующих этот
модуль.

4.2. Создание и использование модулей
Создать модуль достаточно просто, так как каждый файл с рас­
ширением .ру Python автоматически воспринимает как модуль.
Все имена, которые объявлены на верхнем уровне модуля, будут
восприниматься как его атрибуты. Например, создадим файл «шу_
modulel.py», содержащий следующий код:
а = 15
strl = "test_string"
def printer(name):
print(f'Hello, {name}!')

Поскольку имена модулей становятся именами переменных
внутри программы Python, при их создании необходимо следовать
обычным правилам именования переменных (особенно в случае
ключевых слов и операторов). Так, например, если создать следу­
ющий файл «if.py», то его будет невозможно импортировать, а по­
пытка это сделать (import if) приведет к синтаксической ошибке.
То же правило распространяется и на имена каталогов (они могут
содержать только буквы, цифры и подчеркивания).
Использовать только что написанный файл модуля в модуле бо­
лее верхнего уровня можно за счет оператора import или from. Что
в первом, что во втором случае осуществляются поиск, компиляция
и запуск кода файла модуля, если он еще не был загружен. Основное
отличие заключается в том, что оператор import загружает модуль
как объект с атрибутами, поэтому обращение к его атрибутам не­
обходимо производить через имя самого модуля. А при использова­
нии оператора from извлекаются (или копируются) специфические
имена из модуля (его атрибуты).
В случае импортирования написанного ранее модуля посред­
ством оператора import его имя «my_modulel» служит двум разным
целям: 1) с его помощью идентифицируется внешний файл, под­
129

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

лежащий загрузке; 2) имя модуля становится переменной, которая
ссылается на объект модуля после того, как файл загружен. Ниже
приведен пример импортирования модуля оператором import:
import my_modulel

if __ name__ == "__ main__ ":
my_modulel.printer('Alex') # Hello, Alex!
print(my_modulel.strl) # test_string

Оператор from является расширением оператора import. Он им­
портирует файл модуля обычным образом, после чего копирует
одно или несколько имен из файла. Таким образом осуществляется
более прямой доступ к именам импортируемого модуля:
from my_modulel import printer

if __ name__ == "__ main__ " :
printer('Alex') # Hello, Alex!
print(strl)
"""Traceback (most recent call last):
File "F:/code/python/part4_module/main.py", line 5,
in
print(strl)
NameError: name 'strl' is not defined"""

Запись вида from my_modulel import * копирует все имена
на верхнем уровня импортируемого модуля:
from my_modulel import *

if __ name__ == "__ main__ " :
printer('Alex') # Hello, Alex!
print(strl) # test_string

Импорт является довольно затратной операцией, из-за чего
Python производит его только один раз на файл модуля (однократно
на процесс). При последующих операциях импортирования происхо­
дит извлечение имен (объекта) из уже загруженного ранее модуля.
Import и from, как и оператор def, выступают в роли явного при­
сваивания:
— import связывает имя (название модуля) с объектом целого
модуля;
— from связывает одно или несколько имен с объектами с таки­
ми же именами из импортируемого модуля.
Таким образом, результатом работы оператора from становятся
имена в локальной области видимости импортирующего модуля,
ссылающиеся на совместно используемые объекты. Повторное при­
сваивание скопированному имени какого-либо значения не оказы­
вает влияния на сам модуль, из которого оно было скопировано.
В то же самое время модификация совместно используемого объ­
130

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

екта изменяемого типа данных через скопированное имя изменит
его в импортируемом модуле. Изменение значения объекта неизме­
няемого типа может произойти при непосредственном обращении
к нему через его глобальное имя в другом файле, для чего необхо­
димо использовать оператор import.
Также операторы import и from могут:
— вкладываться внутрь проверок if для выбора среди несколь­
ких вариантов;
— находиться внутри операторов def функций, чтобы загружать
только по вызову;
— использоваться в операторах try.
По своей сути запись типа:
from import a, strl

эквивалентна следующей последовательности операторов:
import my_modulel
а = my_modulel.a
strl = my_modulel.strl
del my_modulel

Оператор import необходимо всегда применять в случае, когда
в различных импортируемых модулях на верхнем уровне использу­
ется одно и то же имя (если необходимо работать с обоими):
# first.ру
def function():
print('first')
# second.ру
def function():
print('second')
# main.py
import first
import second

if __ name__ == "__ main__ ":
first.function() # first
second.function() # second

Так, например, при использовании оператора from при импорти­
ровании имен произойдет следующее:
# main.py
from first import function
from second import function
# from second переписывает имя function,
# извлеченное из first

if __ name__ == "__ main__ " :
function() # second
131

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Выйти из такой ситуации можно при помощи расширения as:
# main.py
from first import function as ffunction
from second import function as sfunction

if __ name__ == "__ main__ ":
ffunction() # first
sfunction() # second

4.2.1. Пространства имен модулей
В рамках Python модули можно рассматривать как пространства
имен, а существующие в них на верхнем уровне имена — как атри­
буты. Доступ к пространству имен модуля можно получить через
его атрибут__ diet__или используя встроенную функцию dir(). По­
скольку модули представляют собой локальную область видимости,
то они следуют правилу поиска LEGB, но без двух первых уровней
(ЬиЕ).
Самое важное, что следует запомнить: когда модуль загружен,
его глобальная область видимости становится словарем атрибутов
объектов модуля. В качестве примера рассмотрим подключение мо­
дуля следующего вида:
print('start load')

import sys
name_a = 33
def function():
print('func use')
class Testclass:
class_name = 30
print('finish load')

Как уже говорилось ранее, при первом импортировании модуля
Python выполнит все операторы в нем. Часть из этих операторов
создаст имена в пространстве имен модуля, а другие будут выпол­
няться, пока не завершится операция импорта:
# main.py
import namespases_ex

if __ name__ == "__ main__ ":
pass
# start load
# finish load

Далее рассмотрим созданный в процессе импортирования сло­
варь атрибутов модуля:
# main.py
import namespases_ex

132

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ " :
attr_keys = namespases_ex.__ diet__ .keys()
print(list(attr_keys))
II II II

start load
finish load
['__ name__ ', '__ doc__ ', '__ package__ ', '__ loader__ ', '__
spec_', '_file_', '_cached_', '_builtins_', 'sys',
'name_a', 'function', 'Testclass'] """

Имена, присвоенные в файле модуля, становятся ключами слова­
ря. В процессе операции импорта Python автоматически добавляет
в пространство имен модуля несколько дополнительных имен (имена
с двумя нижними подчеркиваниями с разных сторон имени в Python
принято называть дандерами). Давайте выведем только те ключи
словаря, которые присваиваются только в написанном ранее коде:
# main.py
import namespases_ex

if __ name__ == "__ main__ " :
attr_keys = namespases_ex.__ diet__ .keys()
attr_keys = [name for name in attr_keys if notname.
startswith('__ ')]
print(attr_keys)
# start load
# finish load
# ['sys', 'name_a', 'function', 'Testclass']

Теперь попробуем обратиться к одному из имен модуля через его
атрибут и словарь__ diet__:
# main.py
import namespases_ex

if __ name__ == "__ main__ " :
print(namespases_ex.name_a) # 33
print(namespases_ex.__ diet__ ['name_a']) # 33

В Python доступ к атрибутам любого объекта, имеющего атрибу­
ты, производится с применением синтаксиса уточнения вида «объ­
ект-атрибут» (извлечение атрибутов). Уточнение представляет со­
бой выражение, которое возвращает значение, присвоенное имени
атрибута и ассоциированное с объектом.
Важно помнить о том, что уточнение атрибутов не имеет ничего
общего с правилами поиска в областях видимости и является не­
зависимой концепцией. Когда уточнение используется для доступа
к именам, Python получает явный объект, из которого необходимо
извлечь указанные имена. Ниже приведены основные правила, ко­
торые распространяются на уточнения.
1. X означает поиск имени X в текущих областях видимости.
2. Уточнение X. Y означает поиск X в текущих областях видимо­
сти, затем поиск атрибута Y в объекте X (не в областях видимости).
133

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

3. Путь уточнения X.Y.Z означает поиск имени Y в объекте X,
затем поиск Z в объекте X.Y.
4. Уточнение работает для всех объектов с атрибутами: моду­
лей, классов, типов расширений и т. д.
Помимо описанного выше, операции импорта никогда не обеспе­
чивают восходящую видимость коду в импортируемых файлах. Это
значит, что импортируемый файл не может видеть имена из файла,
в котором он импортируется, то есть:
— функции не могут видеть имена в других функциях, если
только они физически не вкладываются в них;
— код модуля не может видеть имена в других модулях, если
только он явно не импортирует их.
4.2.2. Перезагрузка модулей
Код модуля выполняется только один раз при запуске программы
(или процесса). Для его принудительной перезагрузки и повторно­
го выполнения используется встроенная функция reload. Механизм
перезагрузок позволяет делать разрабатываемые системы более ди­
намичными.
Таким образом, функция reload делает возможным изменение
частей программы без ее остановки, позволяя незамедлительно на­
блюдать эффекты от изменений в компонентах. Естественно, пере­
загрузка не может использоваться абсолютно в любой ситуации,
но там, где это возможно, это значительно сокращает цикл разра­
ботки.
Существуют следующие отличия reload от import и from:
— это функция, а не оператор Python;
— в reload в качестве аргумента передается существующий объ­
ект модуля, а не новое имя;
— функция reload находится внутри модуля в Python 3.x и долж­
на быть импортирована.
Из перечисленного выше следует, что прежде чем осуществить
перезагрузку модуля, он должен быть предварительно успешно за­
гружен. Синтаксис операторов import и from отличается от reload:
функция перезагрузки требует наличия круглых скобок. Обобщен­
ный вид перезагрузки модулей представлен ниже:
import module
# Первоначальная операция импорта
# ... использовать атрибуты module ...
# Внести изменения в файл модуля
from imp import reload
# Получить саму функцию reload, (в Python 3. X)
reload(module)
# Получить обновленные атрибуты
... использовать атрибуты module ...

134

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

К ключевым моментам работы с механикой перезагрузки модуля
можно отнести то, что:
— функция reload запускает новый код файла модуля в текущем
пространстве имен модуля. Повторное выполнение кода файла мо­
дуля переписывает его существующее пространство имен, а не уда­
ляет и воссоздает его;
— присваивания верхнего уровня в файле заменяют имена но­
выми значениями;
— перезагрузка воздействует на всех клиентов, которые приме­
няют import для извлечения модулей;
— перезагрузка воздействует только на клиентов, которые при­
менят from в будущем. То есть она не окажет влияние на клиентов,
которые использовали from для извлечения атрибутов в прошлом,
и они по-прежнему будут иметь ссылки на старые объекты, извле­
ченные перед перезагрузкой;
— перезагрузка применяется только к одиночному модулю.
Из этого следует, что если перезагружается модуль А, в котором
импортируются модули В и С, то перезагрузка применяется только
к модулю А (к В и С не применяется). Операторы внутри А, которые
импортируют В и С, в течение перезагрузки выполняются повторно,
но они всего лишь извлекают объекты уже загруженных модулей
В и С (предполагая, что они были импортированы раньше).

4.2.3. Запуск модуля как автономной программы
Каждый модуль имеет встроенный атрибут__пате__ , который
в процессе запуска программы или импортирования модуля автомати­
чески создается интерпретатором Python, после чего ему присваивает­
ся значение. При этом, если файл модуля запускается как программа
верхнего уровня, тогда во время старта атрибут пате устанавливает­
ся в строку «__main__ ». В ином случае, когда модуль импортируется,
атрибут_ пате__ устанавливается в имя файла модуля.
Это позволяет модулю проверять собственный атрибут__ пате__
для выяснения, запущен он или импортирован. Например, пусть
создан следующий файл модуля по имени «test_run.py», экспортиру­
ющий единственную функцию run:
# test_run.py
def run():
print('Let's go!')

if __ name__ == '__ main__ ': # если запущен как модуль верхнего
уровня
run()

Если запустить написанный модуль посредством команды,
то функция выполнится автоматически:
python test_run.py
# Let's go!
135

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В сущности, встроенный атрибут модуля__ пате__ служит фла­
гом режима его использования и позволяет коду быть задейство­
ванным как в качестве импортируемой библиотеки, так и в каче­
стве сценария верхнего уровня.
Самый распространенный вариант применения проверки атри­
бута __пате__ предназначен для самотестирования кода. Это позво­
ляет использовать файл модуля как в клиентах — импортируя его,
так и запускать его в командной оболочке или посредством другой
схемы запуска для тестирования его логики работы. Такой подход
является, вероятно, наиболее часто применяемым и простым спо­
собом модульного тестирования в Python. Это в разы проще и удоб­
нее, чем повторно набирать все тесты.
Помимо этого, такой подход часто используется при создании
файлов модулей, которые могут применяться как утилиты команд­
ной строки или как библиотеки инструментов.
4.2.4. Внутренние имена в модулях
Имена, которые начинаются с символа нижнего подчеркивания
«_», не импортируются командой from module import. Это позволяет
разработчикам писать модули, предназначенные для импортиро­
вания этой командой, и при этом ограничивать импортирование
некоторых функций или переменных. Отсюда следует, что внутрен­
ние имена (те имена, которые должны быть доступны только
в пределах модуля) должны начинаться с символа нижнего под­
черкивания «_». Такой подход гарантирует, что команда/rom module
import * импортирует только те имена, которые должны быть до­
ступны пользователю.
Для примера создадим модуль с названием «private_names.py»:
# private_names.py
def function(x):
print(x)

def _internal_func(x):
print(x)
first = 4
_second = 2

Далее импортируем его и попробуем обратиться к различным
именам модуля:
from private_names import *

if __ name__ == "__ main__ ":
function(20) # 20
_internal_func(20)
""" Traceback (most recent call last):
File "F:/code/python/part4_module/main.py", line 5,
136

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

in
_internal_func(20)
NameError: name '_internal_func' is not defined """

print(first)# 4
print(_second)
"""Traceback (most recent call last):
File "F:/code/python/part4_module/main.py", line 7,
in
print(_second)
NameError: name '_second' is not defined """

Из приведенного выше примера видно, что такие имена, как
function и first, спокойно импортируются, a _internaljunc и _second
остаются скрытыми за пределами модуля private_names.
Важно запомнить, что такое поведение при импортировании
возможно только с командой вида from ... import *. При обычном
импортировании, через оператор import, пользователь получает до­
ступ и к внутренним именам:
import private_names

if __ name__ == "__ main__ ":
private_names._internal_func(20) # 20
print(private_names._second) # 2

Такого же эффекта можно добиться, если использовать встроен­
ный атрибут модуля (дандер модульного уровня)__ all__ , который
является списком и идентифицирует имена, подлежащие копиро­
ванию, в то время как запись вида _Х обозначает имена, которые
копироваться не должны. Python сначала ищет список__ all__в мо­
дуле и копирует его имена вне зависимости от наличия в них под­
черкиваний:
# priv_names_v2.py
__ all__ = ['-internal-func', 'first', '_second']

def function(x):
print(x)
def _internal_func(x):
print(x)
first = 4
_second = 2

Далее импортируем написанный выше модуль и обратимся к раз­
личным именам модуля:
from priv_names_v2 import *

if __ name__ == "__ main__ ":
_internal_func(20) # 20
137

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

print(_second) # 2
function(20)
"""Traceback (most recent call last):
File "F:/code/python/part4_module/main.py", line 6,
in
function(20)
NameError: name 'function' is not defined"""

Подобно соглашению _X использование списка__ all__ имеет
смысл только при использовании оператора from * при импортиро­
вании имен модуля.

4.3. Создание и использование пакетов модулей
При импортировании в дополнение к имени модуля можно ука­
зывать путь к каталогу с кодом, который в Python называется паке­
том. При этом само импортирование пакетов превращает каталог
в еще одно пространство имен Python с атрибутами, соответствую­
щими подкаталогам и файлам модулей, которые он содержит.
Пакеты являются естественным расширением концепции модуля
и предназначены для очень больших проектов. По аналогии с тем,
как модули группируют взаимосвязанные функции, классы и пере­
менные, пакеты группируют взаимосвязанные модули. Там, где
в операторах import находится имя простого файла модуля, взамен
него можно указать путь с именами, разделенными точками:
import myproj.dirl.dir2.example

или
from myproj.dirl.dir2.example import x

Такое указание пути соответствует иерархии каталогов на диске
к файлу example.py. Если каталог не является домашним или не на­
ходится в подкаталоге site-packages с установленными сторонни­
ми расширениями, его необходимо вручную добавить в список sys.
path.
До версии Python 3.3 каждый каталог, указанный внутри пути
в операторе импортирования пакета, должен был содержать файл
по имени__init__ .ру. Несмотря на то, что теперь наличие этих фай­
лов в каталогах пакетов является не обязательным, их использова­
ние обеспечивает выигрыш в производительности.
Файлы__init__ .ру могут содержать код Python подобно нормаль­
ным файлам модулей, и их код выполняется автоматически при
первом импортировании каталога. При этом__ init__ .ру объявляет
каталог пакетом Python, генерирует пространство имен для катало­
га и реализует поведение операторов from * (т. e.from ... import *)
138

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

в случае применения с операциями импорта каталогов. Также эти
файлы могут быть пустыми.
Рассмотрим пакет следующей структуры:
myproj/
__ init__ .ру
dirl/
__ init__ .ру
value.ру
dir2/
__ init__ .py
examplel.py
example2.py

В Python-файлах приведенного пакета записан следующий код:
# myproj/__ init__ .ру
print("myproj init")
_all_ = [ 'dirl' ]
version = 1.03

#myproj/dirl/__ init__ .py
_all— = ['value']
print("myproj.dirl init")
#myproj/dirl/value.py
val = 3.99

#myproj/dirl/dir2/__ init__ .py
print("myproj.dirl.dir2 init")
#myproj/dirl/dir2/examplel.py
from myproj import version
from myproj.dirl import value
from myproj.dirl.dir2.example2 import function
def exl_finction():
print("Package version: ", version)
print(function())

#myproj/dirl/dir2/example2.py
def function():
return "Function in example2"

При импортировании пакета произойдет только выполнение
файла myproj/ init__.ру:
import myproj
if __ name__ == "__ main__ " :
print(myproj.version)
# myproj init
# 0.1

Если вызвать функцию exl_finction, определенную в файле
myproj/dirl/dir2/examplel.py, то по наличию ошибки станет очевид­
139

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

но, что импортирование пакета не осуществляет импортирование
его субпакетов:
import myproj
if __ name__ == "__ main__ ":
print(myproj.dirl.dir2.examplel)
"""Traceback (most recent call last):
File "F:/code/python/part4_module/main.py", line 4,
in
print(myproj.dirl.dir2.examplel)
AttributeError: module 'myproj' has no attribute 'dirl' """

Из приведенного выше примера следует, что загрузки модуля
верхнего уровня пакета недостаточно для загрузки всех его субмо­
дулей. Это соответствует философии Python, согласно которой ни­
чего не должно происходить у вас за спиной. Ясность важнее лако­
ничности.
Вызвать функцию пакета можно следующим образом:
import myproj.dirl.dir2.examplel

if __ name__ == "__ main__ ":
myproj.dirl.dir2.examplel.exl_finction()
# myproj init
# myproj.dirl init
# myproj.dirl.dir2 init
# Package version: 0.1
# Function in example2

Получается, что прежде чем Python сможет импортировать
myproj.dirl.dir2.examplel, он должен импортировать myproj.dirl,
а затем myproj.dirl.dir2. Из чего следует, что файлы в пакете не по­
лучают автоматического доступа к объектам, которые были опреде­
лены в других файлах пакета, и для явного обращения к объектам
из других файлов пакетов их необходимо импортировать.
Если атрибут__ all__ присутствует в файле__ init_ .ру, он возвра­
щает список строк, определяющий те имена, которые должны им­
портироваться при использовании записи вида jrom ... import * для
данного пакета:
from myproj import *

if __ name__ == "__ main__ ":
pass
# myproj init
# myproj.dirl init

Так как список__all__в файле myproj/ init__ .ру содержит запись
«dirl», то осуществляется импортирование субпакета «dirl». В том
случае, когда словарь__ all__отсутствует, ничего не импортируется.
140

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Для проектирования пакетов необходимо следовать следующим
рекомендациям.
Пакеты не должны использовать структуры каталогов с большой
глубиной вложенности. То есть для большинства пакетов достаточ­
но одного-двух каталогов верхнего уровня. Исключением может яв­
ляться проектирование действительно огромных библиотек.
Несмотря на то, что использование механизма с включением
в модуль списка__all__ может использоваться для сокрытия имен
при их импортировании посредством from ... import *, лучше ис­
пользовать нижний префикс «_» для этих целей.

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

Вопросы и задания для самопроверки
1. Как оператор from связан с оператором import?
2. Как функция reload связана с операциями импортирования?
3. Когда вы обязаны использовать import вместо from?
4. Назовите три потенциальных затруднения, связанных с оператором
from.
5. Каким образом файл исходного кода модуля становится объектом
модуля?
6. Затем вам может понадобиться установка переменной среды
PYTHONPATH?
7. Назовите пять главных компонентов в пути поиска импортируемых
модулей.
8. Назовите четыре типа файлов, которые Python может загрузить в от­
вет на операцию импорта.

141

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

9. Что такое пространство имен и что содержит пространство имен
модуля?
10. Что важно знать о переменных на верхнем уровне модуля, чьи имена
начинаются с одиночного подчеркивания?
11. Что означает наличие у переменной__пате__ модуля строки «__
main_ »?
12. Чем изменение sys.path отличается от установки PYTHONPATH для
модификации пути поиска модулей?
13. Для чего предназначен файл__init__.ру в каталоге пакета модуля?
14. Какие каталоги требуют наличия файлов__init__.ру?
15. Когда с пакетами обязательно использовать оператор import, а не from?

Упражнения
1. Напишите модуль, содержащий функции, которые выполняют следу­
ющие арифметические операции: сложение, вычитание, умножение.
2. Напишите модуль, содержащий функции, которые выполняют сле­
дующие операции: проверка наличия элемента в списке, подсчет частоты
вхождения элемента в список.
3. Напишите модуль, содержащий функции, которые выполняют следу­
ющие операции: проверку, является ли строка палиндромом, подсчет длины
строки, перевод всех символов в нижний регистр.
4. Напишите модуль, содержащий функции, которые выполняют следу­
ющие операции: подсчет площади круга, прямоугольника и треугольника.
5. Напишите модуль, содержащий функции, которые выполняют следу­
ющие операции: подсчет количества элементов в словаре, проверку на на­
личие ключа в словаре.
6. Реализуйте пакет, объединяющий модули из первого и второго упраж­
нений.
7. Реализуйте пакет, объединяющий модули из третьего и четвертого
упражнений.
8. Реализуйте пакет, объединяющий пакеты из шестого и седьмого
упражнений.
9. Напишите модуль, содержащий внутренние имена, значения которых
можно получить через функции верхнего уровня модуля.
10. Импортируйте имена из написанных модулей (пакетов) и проверьте
их работу.

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 5
КЛАССЫ И ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАНИЕ
В данной теме внимание будет уделено таким пользовательским
типам данных, как классы, механизму их реализации в Python, а так­
же насколько сам язык программирования поддерживает принципы
объектно-ориентированного программирования (ООП).
Отдельно коснемся таких понятий, как: экземпляр класса, пере­
менная или метод класса, переменная или метод экземпляра класса
ит. д.
В результате изучения данной темы обучающиеся должны:
знать

• принципы объектно-ориентированного программирования,
• механизм реализации классов в Python и работы с ними,
• способы работы с перечислениями (Enum),
• существующие отличия между переменными класса и переменными
экземпляра класса;
уметь

• создавать пользовательские классы;
• объявлять (абстрактные) базовые классы и их производные классы,
• осуществлять перегрузку методов класса;
владеть

• навыками написания кода на языке программирования Python в объ­
ектно-ориентированной парадигме.

5.1. Определение класса
Классы в Python являются пользовательским типом данных
и представляют собой описание объекта предметной области с его
состояниями и поведением. Они также используются для декомпо­
зиции кода на составные части с целью минимизации избыточности.
За создание классов отвечает оператор class:
class Testclass:
pass

Тело класса может состоять лишь из оператора-заполнителя pass
или из необязательной последовательности команд и определений
функций.
143

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

По соглашениям Python имя классов записывается в «верблю­
жьей нотации», и их можно рассматривать как пакеты функций,
которые используют и обрабатывают объекты встроенных типов.
Несмотря на это, классы предназначены для создания новых объ­
ектов, реализации их логики работы и поддерживают наследова­
ние — механизм настройки и многократного применения кода.
Экземпляр объекта типа объявленного ранее класса Testclass
создается следующим образом:
instance = TestClass()

Отличительной чертой экземпляров классов Python от струк­
тур С или классов Java является то, что их атрибуты (поля данных)
не обязательно объявлять заранее. Они могут добавляться «в про­
цессе работы». Давайте рассмотрим пример, в котором определяет­
ся класс с именем TestClass, создается его экземпляр и полю magic
экземпляра присваивается значение:
class TestClass:
pass

if __ name__ == "__ main__ " :
my_test = TestClass()
my_test.magic = 10
print(my_test.magic) # 10

Для автоматической инициализации атрибутов (полей) класса
поля в его тело включают метод инициализации__init__ . Этот ме­
тод выполняется при каждом создании экземпляра класса и отно­
сительно похож на конструктор в языке Java/C+ + . При этом, в от­
личие от классов Java/C+ +, метод__init__ ничего не конструирует
(только инициализирует поля класса) и не может перегружаться.
То есть классы в Python могут иметь только один метод__init__ :
class TestClass:
def __ init__ (self):
self.magic = 10

if __ name__ == "__ main__ ":
my_test = TestClass()
print(my_test.magic) # 10

По правилам Python первый аргумент метода__init__ — self, ко­
торому присваивается создаваемый экземпляр при выполнении__
init__ . Помимо метода инициализации__ init__ в Python существует
еще один аналог конструктора — метод__new__ . Он вызывается при
создании объекта и возвращает неинициализированный объект.
Классы не всегда необходимы в Python, в ряде случаев может хва­
тить и функции (или группы функций). Класс следует использовать
для программного представления физических или концептуальных
144

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

объектов и при его написании рекомендуется задуматься и найти
ответы на следующие вопросы:
1) Какое имя носит описываемый объект?
2) Какими свойствами обладает описываемый объект?
3) Присущи ли эти свойства всем экземплярам класса? А именно:
— Какие свойства являются общими для класса в целом?
— Какие из этих свойств уникальны для каждого экземпляра?
4) Какие операции выполняет описываемый объект?

5.2. Имена (переменные) экземпляров класса
В рассмотренном ранее примере имя (переменная) magic являет­
ся элементом (атрибутом) экземпляра класса Testclass. Это значит,
что каждый создаваемый экземпляр класса будет иметь собствен­
ную копию этой переменной, и значение, с которым осуществляет­
ся работа через нее, будет отличаться от значений аналогичных пе­
ременных других экземпляров класса. Как уже показывалось ранее,
Python позволяет создавать атрибуты экземпляров класса в процес­
се работы кода по мере необходимости путем присваивания значе­
ния имени атрибута экземпляра класса [39]:
my_test.new_ magic = value

Дня обращения к имени атрибута экземпляра класса для начала не­
обходимо явно указать имя экземпляра созданного класса, после чего
обратиться к его атрибуту (11мя_экземпляра.имя_атрибута>). Если
же обращение к имени атрибута экземпляра класса будет произво­
диться без указания имени экземпляра, то поиск имени будет произ­
водиться в локальной области выполняемого метода, функции и т. д.

5.3. Методы экземпляра класса
Метод класса — функция, которая связана с конкретным клас­
сом:
class Testclass:
def__ init__ (self, magic=10):
self.magic = magic

def square_magic(self):
return self.magic ** 2
if __ name__ == "__ main__ ":
my_testl = TestClass()
print(my_testl.square_magic()) # 100
my_test2 = TestClass(5)
print(my_test2.square_magic()) # 25
145

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Существует несколько способов вызова метода класса: связный
или несвязный. Приведенный выше способ вызова является связ­
ным. Несвязный способ вызова метода менее удобен и реже приме­
няется на практике, так как при таком вызове метода в его первом
аргументе должен передаваться экземпляр класса, что делает код
менее понятным:
print(TestClass.square_magic(my_test2)) # 25

В первом аргументе любого метода передается экземпляр, для
которого он вызывается — self, который является аналогом this
во многих языках программирования.
Python преобразует вызов метода класса в обычный вызов функ­
ции по следующим правилам:
— поиск имени метода в пространстве имен экземпляра (шу_
test2);
— если метод не обнаружен в пространстве имен экземпляра,
поиск осуществляется в типе класса экземпляра (TestClass);
— если метод не найден, то поиск продолжается в суперклассе;
— вызов найденного метода как обычной функции Python.
При вызове такого метода экземпляр передается в его первом ар­
гументе, а все остальные аргументы сдвигаются на одну позицию
вправо. Получается, что запись вида instance.method(argl, arg2, ...)
превращается в class, method (instance, argl, arg2,...).

5.4. Имена (переменные) класса
Переменная класса связана непосредственно с классом, а не с его
конкретным экземпляром, и доступна всем его экземплярам. Она
может использоваться для отслеживания информации на уровне
класса или хранить константные значения описываемого классом
объекта и создается присваиванием в теле класса, а не в методе__
init__. При их использовании необходимо следить за возможными
конфликтами имен между переменными классов и переменными
экземпляров.
После создания переменной класса она становится видимой для
всех его экземпляров:
class TestClass:
pow_val = 3 # переменная класса
def__ init__ (self, magic=10):
self.magic = magic

def square_magic(self):
return self.magic ** TestClass.pow_val
if __ name__ == "__ main__ " :
my_testl = TestClass()
print(my_testl.square_magic()) # 1000
146

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

my_test2 = TestClass(5)
print(my_test2.square_magic()) # 125

Имя переменной класса жестко фиксируется в методах это­
го класса. Но также к нему можно обратиться через специальный
атрибут экземпляра класса —__ class__. Этот атрибут вернет класс,
к которому принадлежит экземпляр:
print(TestClass) # cclass '__ main__ .Testclass'>
print(my_test2.__ class__ )
#

Таким образом, посредством атрибута__ class__ можно полу­
чить значение TestClass.pow_val без явного указания имени класса
Testclass:
print(my_test2.__ class__ .pow_val) # 3

Этот подход позволяет избавиться от внутреннего упоминания
класса TestClass путем замены ссылки: TestClass.pow_val заменяется
на self.__ class__ .pow_val:
def new_square_magic(self):
return self.magic ** self.__ class__ .pow_val

print(my_test2.new_square_magic()) # 125

При этом, если изменить значение переменной pow_val посред­
ством одного из экземпляров класса (my_test3) следующим обра­
зом:
my_test3 = TestClass()
my_test4 = TestClass()
my_test3.pow_val = 10
print(my_test3.pow_val) # 10
print(my_test4.pow_val) # 3

то данный экземпляр класса будет содержать собственную копию
pow_val, отличную от копии TestClass.pow_val, к которой обращает­
ся my_test4. Это связано с тем, что присваивание my_test3.pow_val
создает переменную экземпляра в my_test3.
При последующем обращения к my_test3.pow_val возвращает­
ся значение переменной из экземпляра класса, а при обращении
к my_test4.pow_val Python ищет переменную экземпляра pow_val
в my_test4, не находит ее и переходит к поиску значения перемен­
ной класса в TestClass. Если имеется необходимость изменить зна­
чение переменной класса, необходимо обратиться к ней по имени
класса, а не через переменную экземпляра self:
my_test5 = TestClass()
TestClass.pow_val = 40
print(my_test5.pow_val) # 40
147

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

5.5. Статические методы
Как и в C++ или Java, в Python статические методы можно вы­
зывать, даже если ни один экземпляр класса не создан. Чтобы объя­
вить метод как статический, используется декоратор @staticmethod:
class TestClass:
"""Класс пыщ-пыщ.....
all_created_clases = []
def__ init__ (self, magic=10):
"""Создание экземпляра класса"""
self.magic = magic
TestClass.all_created_clases.append(self)
#self.__ class__ .all_created_clases.append(self)

def square_magic(self):
"""Возведение в квадрат"""
return self.magic ** 2
@staticmethod
def sum_all_square_magic():
"""Статистический метод подсчета квадратов всех
TestClass.....
rezult = 0
for it in TestClass.all_created_clases:
rezult += it.square_magic()
return rezult

if __ name__ == "__ main__ ":
my_testl = TestClass()
my_test2 = TestClass(5)
my_test4 = TestClass(15)
print(TestClass.sum_all_square_magic()) # 350
my_test5 = TestClass(l)
print(TestClass.sum_all_square_magic()) # 351

5.6. Методы класса
Методы классов, так же как и статические методы, могут вызы­
ваться до того, как будет создан объект класса. Также они могут ис­
пользоваться с указанием экземпляра класса. Отличие заключается
в том, что у методов класса первым аргументом неявно идет класс
(cis), к которому они принадлежат. В связи с этим у них более про­
стое написание. Для этого используется декоратор @classmethod:
class TestClass:
"""Класс пыщ-пыщ"""
all_created_clases = []
def__ init__ (self, magic=10):
"""Создание экземпляра класса"""
self.magic = magic

148

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

TestClass.all_created_clases.append(self)
#self.__ class__ .all_created_clases.append(self)

def square_magic(self):
"""Возведение в квадрат"""
return self.magic ** 2
@classmethod
def sum_all_square_magic(cls):
"""Статистический метод подсчета квадратов всех
TestClass.....
rezult = 0
for it in cls.all_created_clases:
rezult += it.square_magic()
return rezult
if __ name__ == "__ main__ " :
my_testl = TestClass()
my_test2 = TestClass(5)
my_test4 = TestClass(15)
print(TestClass.sum_all_square_magic()) # 350
my_test5 = TestClass(l)
print(TestClass.sum_all_square_magic()) # 351

Использование метода класса вместо статического метода по­
зволяет всем производным классам класса TestClass вызвать ме­
тод sum_all_square_magic и обращаться к своим полям и методам
(а не к полям и методам TestClass).

5.7. Приватные методы и переменные
В C++, Java и других языках программирования приватные пе­
ременные или методы не видны за пределами методов класса, в ко­
тором они определяются. То есть к ним нельзя обратиться через
экземпляр класса. Наличие модификаторов доступа позволяет ис­
пользовать инкапсуляцию при проектировании программы, а сами
приватные переменные и методы при этом повышают уровень без­
опасности и надежности за счет ограничения доступа к важным или
критичным частям реализации объекта.
Во многих языках программирования для объявления приватной
переменной или метода используется модификатор доступа private.
В Python же к приватным относятся только переменные или мето­
ды, которые начинаются с двойного подчеркивания «__х».
В то же самое время необходимо понимать, что в Python инкап­
суляция присутствует только на словах, так как у пользователя все
равно есть возможность получить доступ к приватным переменным
или методам класса. Такое положение дел накладывает дополни­
тельные обязательства на разработчика, который должен помнить
149

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

об этом и всегда придерживаться условности, что переменные, на­
чинающиеся с двойного подчеркивания и не заканчивающиеся
им, — приватные:
class PrivateTest:
def __ init__ (self):
self.public_x = 2
self.__ private_y = "I'm invisible"
self._private_z = "I'm not invisible"

def print_y(self):
print(self.__ private_y)
def __ get_y(self):
return self.__ private_y
if __ name__ == "__ main__ " :
private_test = PrivateTest()
print(private_test.public_x) # I'm public
print(private_test._private_z)
# I'm not invisible
print(private_test.__ private_y)
"""Traceback (most recent call last):
File "F:/code/python/module5/private_val_and_method.py", line
14, in
print(private_test.__ private_y)
AttributeError: 'PrivateTest' object has no attribute '__
private_y'"""

Казалось бы, все ожидаемо, и напечатать значение, хранящее­
ся в приватной переменной_ privately, можно только посредством
метода print_y():
private_test.print_y() # I'm invisible

Однако вспомните, о чем говорилось ранее. Когда компилятор фор­
мирует байт-код запускаемого скрипта и встречается с переменной или
методом, которые начинаются с двойного подчеркивания, он создает
атрибут класса не с именем самого метода или переменной, а с име­
нем «_ИмяКласса_ИмяАтрибута», как показано в следующем коде:
print(private_test._PrivateTest__ private_y)
# I'm invisible
print(private_test._PrivateTest__ get_y())
# I'm invisible

5.8. Наследование
Поскольку Python — язык программирования с неявной динами­
ческой типизацией, в нем механизм наследования проще и гибче,
чем в компилируемых языках (например, Java или C++).
150

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В качестве примера того, как используется наследование
в Python, рассмотрим классический пример обобщения геометриче­
ских фигур. Для этого определим по классу для квадратов и окруж­
ностей:
class Square:
def __ init__ (self, side=l, x=0, y=0):
self.side = side
class Circle:
def__ init__ (self, radius=l, x=0, y=0):
self.radius = radius

Допустим, что эти классы планируется использовать при написа­
нии графического редактора. Исходя из этого, добавим координаты
в виде переменных х и у для хранения информации о том, в каком
месте «виртуальной поверхности» изображения располагается каж­
дый экземпляр:
class Square:
def __ init__ (self, side=l, x=0, y=0):
self.side = side
self.x = x
self.у = у
class Circle:
def__ init__ (self, radius=l, x=0, y=0):
self.radius = radius
self.x = x
self.у = у

К сожалению, такой подход приводит к появлению большого
количества дублирующего кода, если придется увеличивать коли­
чество классов геометрических фигур. Это связано с тем, что для
каждой такой фигуры придется хранить информацию о ее текущей
позиции. Такие проблемы являются стандартной ситуацией для
применения наследования.
Вместо того чтобы определять переменные х и у в каждом клас­
се геометрической фигуры, их можно абстрагировать в некоторый
обобщенный класс Shape, от которого будет производится наследо­
вание остальных классов геометрических фигур:
class Shape:
def __ init__ (self, x, y):
self.x = x
self.у = у
class Square(Shape):
def __ init__ (self, side=l, x=0, y=0):
super().__ init__ (x, y)
self.side = side

151

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class Circle(Shape):
def__ init__ (self, radius=l, x=0, y=0):
super().__ init__ (x, y)
self.radius = radius

В большинстве случаев для использования наследования необхо­
димо выполнить (обычно) два требования:
1) определить иерархию наследования, то есть в круглых скоб­
ках непосредственно за именем определяемого класса указать имя
класса, от которого производится наследование;
2) явно вызвать метод__ init__ классов, используемых при на­
следовании. Это связано с тем, что Python не делает этого автомати­
чески, и приходится использовать функцию super. В приведенном
коде за это отвечает строка вызова superQ.__ init__ (х,у), которая
из производного класса вызывает функцию инициализации роди­
тельского класса Shape с инициализируемым экземпляром и пра­
вильными аргументами.
Вместо использования функции super можно вызвать метод
__ init__ класса Shape с явным указанием родительского класса:
Shape.__ init__ (self, х, у). Однако этот способ будет менее гибким
в долгосрочной перспективе, потому что он жестко фиксирует имя
родительского класса, что может создавать проблемы в будущем
при изменении структуры и иерархии наследования.
Далее определим в классе Shape еще один метод с именем move,
который сдвигает фигуру, изменяя ее координаты х и у на величи­
ну, определяемую аргументами метода, а также по методу__ str__
для вывода состояния значений переменных каждого из класов по­
средством функции print:
class Shape:
def __ init__ (self, x, y):
self.x = x
self.у = у

def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.у = self.у + delta_y
def __ str__ (self):
return f'x: {self.x}, y: {self.y}'
class Square(Shape):
def __ init__ (self, side=l, x=0, y=0):
super().__ init__ (x, y)
self.side = side

def __ str__ (self):
return super().__ str__ () +
f', side: {self.side}'
152

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class Circle(Shape):
def__ init__ (self? radius=l, x=0, y=0):
super().__ init__ (x, y)
self.radius = radius

def __ str__ (self):
return super().__ str__ () +
f', side: {self.radius}'

Теперь создадим по экземпляру классов Square и Circle и изме­
ним их стартовые координаты:
if __ name__ == "__ main__ " :
my_circle = Circle(x=10, y=5)
my_square = Square(5, 25, 40)
print(my_circle) # x: 10, y: 5, side: 1
print(my_square) # x: 25, y: 40, side: 5
my_square.move(35, 50)
my_circle.move(10, 10)
print(my_circle) # x: 20, y: 15, side: 1
print(my_square) # x: 60, y: 90, side: 5

Классы Circle и Square изначально не содержали собственного
определения метода move. С другой стороны, так как они насле­
дуются от класса Shape, где этот метод реализован, то все экзем­
пляры Circle и Square могут использовать метод move. Это связано
с тем, что все методы Python являются виртуальными. Таким обра­
зом, если метод не существует в текущем классе, то Python ищет
его по списку атрибутов родительского класса и использует первый
найденный метод.
Давайте рассмотрим, как при наследовании обстоит дело с пере­
менными экземпляров и классов:
class BaseTextTest:
name = "Test"

def set_text(self):
self.text = "Class BaseTextTest"
def print_base(self):
return self.text
def __ str__ (self):
return self.text
class NewTextTest(BaseTextTest):
def set_new_text(self):
self.text = "Class NewTextTest"

def print_new(self):
return self.text
def __ str__ (self):
return self.text
153

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ ":
new_class = NewTextTest()
new_class.set_text()
print(new_class) #Class BaseTextTest
print(new_class.print_base())
#Class BaseTextTest
print(new_class.print_new())
#Class BaseTextTest
new_class.set_new_text()
print(new_class.print_new()) #Class NewTextTest
print(new_class) #Class NewTextTest

Из приведенного примера видно, что обращения к переменным
экземпляров класса с одинаковыми именами будут производиться
к одной переменной. Именно поэтому после объявления перемен­
ной text посредством метода родительского класса попытка выве­
сти ее значения посредством метода print_new() выводит «Class
BaseTextTest», а после переопределения ее значения методом дочер­
него класса set_new_text() выводится «Class NewTextTest», и наобо­
рот.
Так как переменные классов наследуются, необходимо помнить
об их обобщенном поведении. Например, переменную класса пате
определили для родительского класса BaseTextTest, и к ней мож­
но обратиться несколькими способами: через экземпляр new_class,
через дочерний класс NewTextTest или через родительский класс
BaseTextTest:
print(f'new_class = {new_class.name},
f' NewTextTest = {NewTextTest.name}, '
f'BaseTextTest = {BaseTextTest.name}')
# new_class = Test, NewTextTest = Test,
# BaseTextTest = Test

При попытке задать значение переменной класса пате через
класс NewTextTest для него создается новая переменная класса
с таким именем. Это не влияет на значение переменной класса
BaseTextTest. В тоже время при последующих обращениях через эк­
земпляр или сам класс NewTextTest пользователю будет доступна
только эта новая переменная, а не оригинал из BaseTextTest:
NewTextTest.name = "Not bad"
print(f'new_class = {new_class.name},
f'NewTextTest = {NewTextTest.name}, '
f'BaseTextTest = {BaseTextTest.name}')
# new_class = Not bad, NewTextTest = Not bad,
# BaseTextTest = Test

Тот же самый эффект будет наблюдаться, если задать новое
значение переменной пате через экземпляр класса NewTextTest.
154

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

То есть создастся новая переменная экземпляра, и уже будет не две
разные переменные, а три:
BaseTextTest.name = "Not good"
print(f'new_class = {new_class.name},
f' NewTextTest = {NewTextTest.name}, '
f'BaseTextTest = {BaseTextTest.name}')
# new_class = Not bad, NewTextTest = Not bad,
# BaseTextTest = Not good

5.9. Множественное наследование
В Python, как и C + + , разрешается множественное наследование,
а в таких языках программирования как Java оно запрещено, хотя
имеется возможность расширять функциональность класса различ­
ным количеством интерфейсов.
Что значит «множественное наследование»? Это когда произво­
дный класс может быть унаследован от любого количества базовых
классов. По умолчанию, если не происходит явного указания базо­
вого класса при наследовании, то класс наследуется от object.
Приведем простейший пример множественного наследования
с использованием «пустых» классов, которые не содержат перемен­
ных или методов с одинаковыми именами:
class FirstBaseClass:
class SecondBaseClass:
class ThirdBaseClass:
class FirstTestClass(FirstBaseClass,
ThirdBaseClass):
class SecondTestClass(SecondBaseClass):
class TopTestclass(FirstTestClass,
SecondTestClass):

Так как в приведенных классах нет одноименных методов, экзем­
пляр класса TopTestClass может использоваться, как если бы он был
экземпляром любого из классов. В то же самое время, если некото­
рые из приведенных классов будут содержать одноименные методы,
трудно даже предположить, не зная способов обхода базовых клас­
сов при поиске метода, какой из них должен считаться правильным:
class FirstBaseClass:
class SecondBaseClass:
155

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class ThirdBaseClass:
def test_method(self):
print("ThirdBaseClass")
class FirstTestClass(FirstBaseClass,
ThirdBaseClass):
class SecondTestClass(SecondBaseClass):
def test_method(self):
print("SecondTestClass")
class TopTestClass(FirstTestClass,
SecondTestClass):

if __ name__ == "__ main__ " :
my_class = TopTestClass()
my_class.test_method() # ??

В ходе попытки вызова метода test_method() для экземпляра
класса TopTestClass Python начнет перебирать его базовые классы
слева направо. Также он всегда просматривает всех предков одного
базового класса перед тем, как переходить к следующему базовому
классу. Давайте восстановим последовательность поиска интерпре­
татором выполняемого метода:
1) сначала поиск метода будет выполняться в классе объекта,
для которого он вызывается, то есть в TopTestClass;
2) поскольку в TopTestClass не определен метод test_method(),
поиск начнет осуществляться в его базовых классах. Первым ба­
зовым классом для TopTestClass является FirstTestClass, поэтому
именно в нем Python продолжит поиск;
3) так как в FirstTestClass аналогично предыдущему случаю
тоже не определен метод test_method(), поиск продолжится уже
в его базовых классах, и первым из них будет FirstBaseClass;
4) поскольку в FirstBaseClass не найден определенный ме­
тод test_method(), после проверки на отсутствие у него ба­
зовых классов поиск прерывается. Python переходит к классу
FirstTestClass и обращается к его следующему базовому классу —
ThirdBaseClass;
5) в классе ThirdBaseClass Python находит вызываемый ме­
тод test_method(), и поскольку это первый найденный метод
с заданным именем, именно он будет использован. Аналогичный
метод, определенный в классе SecondTestClass, будет проигно­
рирован.
Считается, что множественное наследование — плохой способ
организации архитектуры приложения. Но оно иногда способству­
ет предотвращению слишком глубоких иерархий, которые могут
встречаться при использовании обычного наследования или ис­
156

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

пользуются для создания классов примесей (mixin). Примеси пред­
ставляют собой класс, в котором у части или всех методов нет реа­
лизации, а обязанность по обеспечению недостающей реализации
ложится на их производный класс.

5.10. Абстрактные классы и переопределение методов
Абстрактный (интерфейсный) класс — класс, описывающий
(предоставляющий) интерфейс взаимодействия с объектом, реали­
зация части методов которого описывается в классах, наследующих­
ся от него, и который имеет хотя бы один чисто виртуальный ме­
тод, т. е. у объявленного метода нет реализации. Основной чертой
абстрактного класса является то, что его экземпляр нельзя создать.
Хотя к Python это утверждение в ряде случаев может быть неприме­
нимо, лучше придерживаться данного подхода при проектировании
архитектуры приложения, библиотеки и т. д.
Посредством абстрактного класса реализуется один из основных
принципов ООП — полиморфизм. Без использования модуля abc
абстрактный класс в Python можно реализовать двумя способами.
1. С использованием assert:
class AbstractBaseClass:
def print_name(self):
print("AbstractBaseClass")

def action(self):
assert False, "method not define"
if __ name__ == "__ main__ ":
my_class = AbstractBaseClass()
my_class.print_name() # AbstractBaseClass
my_class.action()
# AssertionError: method not define

2. С использованием исключения NotlmplementedError:
class AbstractBaseClassEx:
def print_name(self):
print("AbstractBaseClass")

def action(self):
raise NotlmplementedError("method not define")
if __ name__ == "__ main__ ":
#my_class = AbstractBaseClassQ
my_class = AbstractBaseClassExQ
my_class.print_name() # AbstractBaseClassEx
my_class.action()
# NotlmplementedError: method notdefine
157

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Как видно из приведенных примеров, реализованные абстракт­
ные классы не удовлетворяют одной из главных особенностей —
их экземпляры можно создать! Чтобы интерпретатор не сообщал
об ошибках в производном классе, по отношению к которому аб­
страктный класс выступает базовым, необходимо переопределить
реализацию метода action():
class SubClass(AbstractBaseClassEx):
def action(self):
print("method was defined")

if __ name__ == "__ main__ ":
my_class = SubClass()
my_class.print_name() # AbstractBaseClassEx
my_class.action() # method was defined

В следующем примере для написания абстрактного класса ис­
пользуем возможности модуля abc:
from abc import ABCMeta, abstractmethod

class AbstractBaseClass(metaclass=ABCMeta) :
def print_name(self):
print("AbstractBaseClass with abc
module help")

@abstractmethod
def action(self):
if __ name__ == "__ main__ ":
my_class = AbstractBaseClass()
'''TypeError: Can't instantiate abstract class
AbstractBaseClass with abstract methods action'1'

Реализация данного абстрактного класса уже удовлетворяет ос­
новному требованию — его экземпляр нельзя создать. Далее попро­
буем создать его производный класс без переопределения метода
action():
class FirstSubClass(AbstractBaseClass) :

if __ name__ == "__ main__ " :
my_class = FirstSubClass()
'''TypeError: Can't instantiate abstract class FirstSubClass
with abstract methods action'1'

Все верно! До тех пор, пока пользователь не напишет реализа­
цию данного метода, должна выводиться ошибка:
class SecondSubClass(AbstractBaseClass):
def action(self):
print("method was defined")

158

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ " :
my_class = SecondSubClass()
my_class.print_name()
# AbstractBaseClass with abc module help
my_class.action()
# method was defined

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

5.11. Перегрузка операций
Под перегруженной операцией подразумевается автоматиче­
ский вызов методов экземпляра класса, когда он встречается в коде
со встроенными операциями (+,-,* и т. д.) и возвращаемое значе­
ние перегруженного метода становится результатом этой операции.
Таким образом, когда класс перегружает особым образом именован­
ные методы, Python автоматически вызывает их в случае появления
экземпляров этого класса в ассоциированных с ними выражениях.
В табл. 5.1 приведены наиболее распространенные методы для
перегрузки операций:
Таблица 5.1
Наиболее распространенные методы перегрузки операций

Метод

Описание

Для чего используется

_ init_

Конструктор

Создание экземпляра объекта
my class = MyClass(args)

_del_

Деструктор

Удаление экземпляра объекта

_add—

Операция сложения

А + В, А + = В, если отсутствует
_ iadd_

_ or_

Операция побитовое
«ИЛИ»

А | В, АI = В, если отсутствует
_ ior_

_ sub_

Операция вычитания

А-В, А-= В

_ repr____ str_

Вывод, преобразования

print(X), repr(X), str(X)

—call—

Вызовы функций

function(*args, **kargs)
159

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Окончание табл. 5.1

Метод

Описание

Для чего используется

_ getattr_

Извлечение атрибута

_ setattr_

Присваивание атрибута X.any = value

_ delattr_

Удаление атрибута

del X.any

Извлечение атрибута

X.any

_ getitem_

Индексирование, срез,
итерация

X[key], X [ i: j ], циклы for и дру­
гие итерационные конструкции,
если отсутствует_ iter_

_ setitem_

Присваивание по ин­
дексу и срезу

X[key] = value,
X[i: j] - iterable

_ delitem_

Удаление по индексу
и срезу

del X[key], del X[i:j]

_ len_

Длина

len (X), проверки истинности,
если отсутствует bool

_ bool_

Булевские проверки

bool (X), проверки истинности

Сравнения

А < В, А > В, А < = В, А > = В,
А == В, А ! = В

_ radd_

Правосторонние опе­
рации

Other + X

_ iadd_

Дополненные на месте
операции

А + = В (либо иначе_ add_ )

_ iter_
_ next_

Итерационные контек­
сты

1=iter(X), next (I); циклы for, in,
если отсутствует_ contains_ ,
все включения, map (F,X),
остальные

_ contains_

Проверка членства

item in X (любой итерируемый
объект)

_ index_

Целочисленное значе­
ние

hex(X), bin(X), oct(X), O[X], O[
x: ]

_ enter_
_ exit_

Диспетчер контекста

with obj as var:

_get_
_ set_
_ delete_

Атрибуты дескриптора

X.attr, X.attr = value,
del X.attr

_ new_

Создание

Создание объекта, перед init

getattribute

X. undefined

1
1

1

J J

1

1

1

1

&

1

1

Z

1

£
1

1

Даже в том случае, когда методы перегрузки операций не опре­
деляются, они могут быть унаследованы от базового класса, как
160

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

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

5.11.1. Перегрузка__ add__ ,__ or__ ,__ sub__
В примере ниже рассмотрим перегрузку таких операций, как:
сумма, вычитание и исключающее «ИЛИ», реализовав класс Number
с последующей перегрузкой соответствующих методов:
class Number:
def __ init__ (self, val):
self.value = val
def__ sub__ (self, other):
return Number(self.value - other)
def__ add__ (self, other):
return Number(self.value + other)

def __ or__ (self, other):
return Number(self.value | other)
def __ str__ (self):
return f'{self.value}'
if __ name__ == "__ main__ " :
my_number = Number(4)
addValue = my_number + 10
print(addValue) # 14
subValue = addValue - 13
print(subValue) # 1
orValue = subValue | 4
print(orValue) # 5

5.11.2. Перегрузка__ getitem__ и___ setitem__
Когда метод__ getitem__ определен в классе, он автоматиче­
ски вызывается для операций индексирования экземпляров этого
класса. Например, в случае обращения = self.my_
range.stop:
raise Stopiteration
self.count_value += self.my_range.step
return self.count_value
if __ name__ == "__ main__ " :
print("MyRange")
my_range = MyRange(0, 4)
for it in my_range:
print(it, end=' ') # 0 1 2 3
print()
for it in MyRange(0, 12, 4):
print(it, end=' ') #048
print()
test_range = MyRange(0, 3)
for firtst_it in test_range:
print (f'firtst_it = {firtst_it}')
for second_it in test_range:
print(f'second_it = {second_it}, '
f'firtst_it*second_it = '
f' {firtst_it*second_it}')
I

I

I

firtst_it = 0

166

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

second_it = 0,
second_it = 1,
second_it = 2,
firtst_it = 1
second_it = 0,
second_it = 1,
second_it = 2,
firtst_it = 2
second_it = 0,
second_it = 1,
second_it = 2,

firtst_it*second_it = 0
firtst_it*second_it = 0
firtst_it*second_it = 0
firtst_it*second_it = 0
firtst_it*second_it = 1
firtst_it*second_it = 2
firtst_it*second_it = 0
firtst_it*second_it = 2
firtst_it*second_it = 4

5.11.4. Перегрузка__ contains__
В области итераций классы реализуют операцию проверки
членства in в виде итерации с применением методов__ iter__ или
__ getitem__ . Но для поддержки более специфической операции про­
верки членства предусмотрен метод__ contains__ . Если он присут­
ствует в классе, то ему всегда отдается предпочтение перед рассмо­
тренными ранее методами:
class TestContains:
def __ init__ (self, value):
self.data = value

def __ contains__ (self, x):
return x in self.data
if __ name__ == '__ main__ ' :
X = TestContains([1, 2, 3, 4, 5])
print(3 in X) # True
print(10 in X) # False
print('d' in X) # False

5.11.5. Перегрузка__ getattr__ и___ setattr__
Классы Python имеют возможность перехватывать обращение
к их атрибутам. Например, для объекта X, являющегося экзем­
пляром класса, может быть реализована следующая операция —
Х.атрибут, которая участвует в контекстах ссылки, присваивания
и удаления.
Метод__getattr__ перехватывает обращение к несуществующе­
му атрибуту класса, то есть он не вызывается в тех случаях, ког­
да Python может найти атрибут с применением процедуры поиска
в дереве наследования. Данный метод удобен в качестве привязки,
которая обеспечивает реагирование на запросы атрибутов в обоб­
щенной манере и используется при делегировании вызовов вло­
женным объектам из промежуточного объекта контроллера. Кроме
того, он может применяться для адаптации классов к интерфейсу.
167

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Если метод__setattr__ определен или унаследован, тогда он всег­
да будет перехватывать все обращения к атрибуту self, атрибут =
значение. Он позволяет классу выполнять желаемые проверки до­
стоверности или преобразования. При его использовании нужно
быть предельно внимательным, так как любое присваивание атри­
буту класса в стиле self, атрибут = значение внутри метода снова
вызовет его и тем самым станет причиной бесконечной рекурсии,
которая остановится после генерации исключения, связанного
с переполнением стека. Избежать такого поведения можно исполь­
зуя присваивания значений атрибутам экземпляра в виде присва­
иваний ключам словаря атрибутов. То есть внутри данного метода
вместо записи self, атрибут = значение необходимо использовать
self.__diet__["атрибут"] = значение.
Запомните: метод__ setattr__ без исключений перехватывает все
присваивания значений атрибутам!

Ниже приведен пример перегрузки методов__ getattr__ и
setattr__ :
class TestAttribute:
def__ init__ (self, name):
self.name = name
def__ getattr__ (self, item):
print("in __ getattr__ ")
if item == "age":
self.__ diet__ [item] = 3
return self.__ diet__ [item]
else:
raise AttributeError(item)
def__ setattr__ (self, key, value):
print(f"in __ setattr__ , key = {key},
f" value = {value}")
if key == "age":
self.__ diet__ [key] = value + 1
elif key == "name":
self.__ diet__ [key] = value
else:
raise AttributeError(value + "not allowed")

if __ name__ == "__ main__ ":
my_test = TestAttribute("Alex")
print(my_test.name) # Alex
print(my_test.age) # in __ getattr__ , 3
print(my_test.age) # 3
#print(my_test.country)
# in __ getattr__ , AttributeError: country
168

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

my_test.name = "Jon"
# in __ setattr__ , key = name, value = Jon
my_test.age = 10
# in __ setattr__ , key = age, value = 10
print(my_test.name) # Jon
print(my_test.age) # 11
my_test.country = "Germany"
# AttributeError: Germanynot allowed

Таким образом, используя метод__ setattr__ , можно реализовать
механизм, который позволяет каждому производному классу иметь
собственный список закрытых имен, которым экземпляры не могут
присваивать значения:
class PrivateExc(Exception): pass
class PrivatAttribute:
def__ setattr__ (self, key, value):
print(f"check key = {key}, value = {value}")
if key in self.privates:
raise PrivateExc(key, self)
else:
self.__ diet__ [key] = value
class FirstTestAttribute(PrivatAttribute):
privates = ["name", "city"]
class SecondTestAttribute(PrivatAttribute):
privates = ["age"]
def__ init__ (self, name):
self.name = name

if __ name__ == "__ main__ " :
first_test = FirstTestAttribute()
second_test = SecondTestAttribute("Alex")
# check key = name, value = Alex
second_test.city = "SPb"
# check key = city, value = SPb
first_test.country = "Germany"
# check key = country, value = Germany
#first_test.name = "Jon"
# check key = name, value = Jon
''' __ main__ .PrivateExc: ('name', )
second_test.age = 20 # check key = age, value = 20
__ main__ .PrivateExc: ('age', ) '''

5.11.6. Перегрузка__ repr__ и___ str__
Если у класса перегружены сразу два этих метода, то ме­
тод __ str___будет вызываться первым при передаче экземпляра
класса в функцию print или при его приведении к строковому типу
169

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

данных посредством встроенной функции str. Метод__ герг__ ис­
пользуется во всех остальных контекстах и даже в том случае, когда
метод__ str__ отсутствует. Эти методы должны возвращать строку,
которую можно было бы применять для воссоздания объекта или
для детального отображения значений атрибутов экземпляра класса
при отладке.
Без перегрузки этих методов стандартное отображение не при­
носит особой пользы:
class Number:
def __ init__ (self, val):
self.value = val
def__ sub__ (self, other):
return self.value - other

if __ name__ == "__ main__ ":
number = Number(10)
print(number)
#

Согласитесь, хотелось бы увидеть значение, которое хранит объ­
ект, а не то, что получили в итоге. Для этой цели и используется пе­
регрузка методов__герг__ и__ str__ . Приведем пример с__ герг__ :
class NewNumber(Number):
def __ герг__ (self):
return f'{self.value}'

if __ name__ == "__ main__ " :
newNumber = NewNumber(15)
print(newNumber) # 15
newNumber = newNumber - 3
print(newNumber) # 12

5.11.7. Перегрузка__ call__
Метод__ call__ вызывается каждый раз при вызове экземпляра
класса, в котором он был перегружен. Python выполняет его для вы­
ражений вызова функций, применяемых к экземпляру класса, пере­
давая ему любые позиционные или ключевые аргументы, которые
были отправлены:
class CallTest:
def__ call__ (self, *args, **kwargs):
print ("_call_")
print(f"args = {args} \nkwargs = {kwargs}")

if __ name__ == "__ main__
test = CallTest()
test(10, 'Oo', 32.1)
#
call
170

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

# args = (10, 'Oo', 32.1)
# kwargs = {}
test(10, 'Oo', 32.1, a=2, b='-_-')
# _call_
# args = (10, 'Oo', 32.1)
# kwargs = {'a': 2, 'b':

При этом метод__ call__ поддерживает все режимы передачи ар­
гументов (см. тему 3):
class CallTest2:
def __ call__ (self, a, b, c=3):
print ("_call_")
print(f"a = {a}, b = {b}, c = {c}")

if __ name__ == "__ main__ " :
test2 = CallTest2()
test2(4, ,л_л') # a = 4, b = л_л, c = 3
test2(b=4, c=45.3, а='л_л')
# a = л_л, b = 4, c = 45.3
test2(8)
# TypeError: __ call__ () missing 1
# required positional argument: *b'

Такой перехват выражений вызовов позволяет экземплярам
класса эмулировать внешний вид и поведение сущностей вроде
функций, используя их в качестве аргументов для различных API
или библиотек, которые ожидают на вход функцию. А для сокрытия
информации о состоянии или наследовании используется перегруз­
ка метода __са11__ :
class Persone:
def__ init__ (self, name="Ivan"):
self.name = name
def__ call__ (self, profession):
print(f"{self.name} is a {profession}")

class ProfessionName:
def__ init__ (self, persone, prof="unemployed"):
self.persone = persone
self.profession = prof
def__ setattr__ (self, key, value):
self.__ diet__ [key] = value
if key == "profession":
self.persone(self.__ diet__ [key])

if __ name__ == "__ main__ ":
persone = Persone("Alex")
prof = ProfessionName(persone)
# Alex is a unemployed
prof.profession = "doctor" # Alex is a doctor
prof.profession = "student" # Alex is a student
171

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

5.11.8. Перегрузка методов сравнения
Классы могут перегружать все шесть операций сравнения: ,
=, = =и!=. Ниже приведен пример перегрузки операторов
сравнения:
class Number:
def __ init__ (self, val):
self.value = val

def

It (self, other):
return self.value < other

def

gt (self, other):
return self.value > other

def

le (self, other):
return self.value = other

def

eq (self, other):
return self.value == other

def

ne (self, other):
return self.value != other

if __ name__ == "__ main__ " :
first = Number(10)
second = Number(ll)
print(first < second) # True
print(first > second) # False
print(first = second) # False
print(first == second) # False
print(first != second) # True

5.11.9. Перегрузка__ len__ и___ bool__
Каждый объект в Python может являться истинным или ложным.
Это означает, что объекты, у которых перегружены или реализова­
ны методы__ len__ или__ bool__ , могут возвращать значения True
или False экземпляра класса по запросу.
Python сначала пробует получить булево значение с помощью
метода__ bool__ , в ином случае он делает это посредством метода
__ len__ , пытаясь вывести значение истинности из длины объекта.
class MyTruth:
def __ bool__ (self):
return True
class MyTruth2:
def __ len__ (self):
return 0

172

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class MyTruth3:
def __ bool__ (self):
return True

def _len_(self):
return 0
if __ name__ == "__ main__ ":
my_truth = MyTruth()
if my_truth: print("True") # True
my_truth2 = MyTruth2()
if not my_truth2: print("True") # True
my_truth3 = MyTruth3()
if my_truth3: print("True") # True

Если ни один из этих методов не определен, объект возвращает
истину:
class TestTruth: ...

if __ name__ == "__ main__ ":
test = TestTruth()
print(bool(test)) # True

5.11.10. Декоратор @property или свойства
Вместо дескрипторов__ get___,___set__ и___delete___, которые
предлагают свой способ перехвата доступа к атрибутам, рассмотрим
использование встроенной функции property (свойства) в виде де­
коратора. Она представляет собой упрощенный способ создания
особого типа дескриптора, который запускает функции методов при
доступе к атрибуту, а также позволяет вычислять или изменять при­
сваиваемые значения «на ходу»:
class Persone:
def__ init__ (self, name="Alex", age=22):
self.__ persone_name = name
self.__ persone_age = age

@property
def name(self):
print("get name", end=' ')
return self.__ persone_name
@property
def age(self):
print("get age", end=' ')
return self.__ persone_age
@name.setter
def name(self, new_name):
print(f"set new name - {new_name}")
self.__ persone_name = new_name

173

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

@age.setter
def age(self, new_age):
self.__ persone_age = new_age + 1
print(f"set new age - {new_age}")

if __ name__ == "__ main__ " :
my_persone = Persone()
print(my_persone.name) # get name Alex
print(my_persone.age) # get age 22
my_persone.name = "Maxim" # set new name - Maxim
my_persone.age = 10 # set new age - 11
my_persone.name = 20 # set new name - 21

Если не добавить один из декорированных методов для установ­
ки возраста, то Python при попытке присвоить данному атрибуту
новое значение выдаст ошибку: AttributeError: can't set attribute.
Главное преимущество использования свойств заключается
в том, что в ходе разработки могут использоваться обычные пере­
менные экземпляров, которые затем будут легко преобразованы
в свойства там, где это понадобится, без изменения клиентского
кода и способа обращения к атрибутам экземпляра класса.

5.12. Вложенные классы и пространство имен
Несмотря на то, что классы обычно определяются на верхнем
уровне модуля, иногда их вкладывают в генерирующие их функции
(фабричный метод) с похожими ролями сохранения состояния, как
и в случае с замыканиями у функций. Сам по себе оператор class
вводит новую локальную области видимости подобно операторам
de/функций и следует правилу LEGB.
После выполнения оператора class, как и в случае с модулями,
локальная область видимости класса превращается в пространство
имен атрибутов. Это позволяет классу иметь доступ к областям
видимости объемлющих функций, но при этом сами объемлющие
функции не действуют в качестве объемлющей области видимости
для кода, вложенного внутрь класса. С этим связано то, что Python
ищет имена, на которые произведена ссылка, в объемлющих функ­
циях, но никогда не выполняет их поиск в объемлющих классах.
То есть класс является локальной областью видимости, кото­
рая имеет доступ к объемлющим локальным областям видимости,
но в то же самое время не служит объемлющей локальной областью
видимости для дальнейшего вложенного кода:
TestVal = 5
def test_function():
print(TestVal) # 5

174

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class NestedClass:
print(TestVal) # 5
def first_method(self):
print(TestVal) # 5
def second_method(self):
TestVal = 3 # скрывает глобальное имя
print(TestVal) # 3

my_class = NestedClass()
my_class.first_method()
my_class.second_method()

if __ name__ == "__ main__ " :
print(TestVal) # 5
test_function()

Теперь попробуем изменить значение X в теле объемлющей
функции сразу после ее объявления:
TestVal = 5
def test_function():
TestVal =1
# скрывает глобальное имя
print(TestVal) # 1

class NestedClass:
print(TestVal) # 1
def first_method(self):
print(TestVal) # 1
def second_method(self):
TestVal = 3
# скрывает имя из объемлющей функции
print(TestVal) # 3

my_class = NestedClass()
my_class.first_method()
my_class.second_method()

if __ name__ == "__ main__ ":
print(TestVal) # 5
test_function()

Остается еще рассмотреть случай, когда изменяется значение
X в теле вложенного класса NestedClass сразу после его объявления:
TestVal = 5
def test_function():
TestVal = 1 # скрывает глобальное имя
print(TestVal) # 1
175

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class NestedClass:
TestVal = 4
# скрывает имя из объемлющей функции
print(TestVal) # 4
def first_method(self):
print(TestVal) # 1
print(self.TestVal) # 4
def second_method(self):
TestVal = 3
# Скрывает имя из объемлющей области видимости, а не класса
print(TestVal) # 3
self.TestVal = 9
print(self.TestVal) # 9

my_class = NestedClass()
my_class.first_method()
my_class.second_method()

if __ name__ == "__ main__ " :
print(TestVal) # 5
test_function()

Основное правило, которое следует запомнить из рассмотренных
примеров — поиск простых имен вроде TestVal никогда не про­
изводится во включающих операторах class, а только в операто­
рах def, модулях и встроенной области видимости.

5.13. Перечисления (Enum)
Перечисление — набор символьных имен (членов), привязанных
к уникальным постоянным значениям. Так как Python не поддержи­
вает специального синтаксиса для работы с перечислениями, не­
обходимо использовать его библиотечный модуль епит [40]. Этот
модуль определяет четыре класса перечисления:
1) класс enum.Enum — базовый класс для создания перечисля­
емых констант;
2) класс enum.IntEnum — базовый класс для создания перечис­
ляемых констант, которые также являются подклассами int;
3) класс enum.IntFlag — базовый класс для создания перечис­
ляемых констант, которые можно комбинировать с помощью поби­
товых операторов без потери их членства в перечислении. Члены
IntFlag также являются подклассами int;
4) класс enum.Flag — базовый класс для создания перечисляе­
мых констант, которые можно комбинировать, используя побито­
вые операции, не теряя членства в перечислении.
176

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Помимо классов перечислений модуль определяет декоратор
unique() и один помощник auto. Декоратор обеспечивает привязку
только одного имени к какому-либо одному значению. Помощник
заменяет экземпляры соответствующими значениями для членов
Епит. Начальное значение начинается с 1.
Создать перечисление можно следующим способом:
from enum import Enum

class Color(Enum):
BLACK = 0
WHITE = 1
RED = 2
BLUE = 3

if __ name__ == "__ main__ " :
print(Color.BLACK) # Color.BLACK
print(Color.BLACK.name) # BLACK
print(Color.BLACK.value) # 0

По перечислениям можно итерироваться в порядке определения
их атрибутов:

#
#
#
#

for it in Color:
print(it)
Color.BLACK
Color.WHITE
Color.RED
Color.BLUE

В случае дублирования имени атрибута перечисления Python со­
общит об ошибке:
class TestEnum(Enum):
ONE = 0
ONE = 1
# TypeError: Attempted to reuse key: 'ONE'

Однако надо быть внимательным при присваивании значений
атрибутам перечисления. Так, если значения должны быть неповто­
ряемыми, то приведенный ниже код может при его использовании
вызвать ряд проблем:
class TestNewEnum(Enum):
ZERO = 0
ONE = 1
TWO = 0

if __ name__ == "__ main__ ":
print(TestNewEnum.ZERO.value) # 0
print(TestNewEnum.ONE.value) # 1
print(TestNewEnum.TWO.value) # 0
177

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Чтобы найти эту ошибку на этапе компиляции, необходимо ис­
пользовать декоратор unique:
from enum import Enum, unique

@unique
class TestNewEnum(Enum):
ZERO = 0
ONE = 1
TWO = 0
# ValueError: duplicate values found in :
# TWO -> ZERO

либо использовать помощник auto:
from enum import Enum, auto
class TestNewEnum(Enum):
ZERO = auto()
ONE = auto()
TWO = auto()

if _ name__ == "_ main_ ":
for it in TestNewEnum:
print(repr(it))
#
#
#

Если перечисление имеет уже описанные атрибуты, то оно не мо­
жет выступать в качестве базового класса при наследовании:
from enum import Enum
class Color(Enum):
BLACK = 0
WHITE = 1
class NewColor(Color):
GREEN = 99
# TypeError: Cannot extend enumerations

Для использования класса Color в качестве базового можно раз­
делить некоторое общее поведение между ним и его производным
классом:
from enum import Enum
class Color(Enum):
def magic(self): ...
class NewColor(Color):
BLACK = 0
WHITE = 1
GREEN = 99
178

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Еще один способ создания перечисления — создание экземпляра
класса Enum:

#
#
#
#

myColor = Enum('MyColor', 'BLACK WHITE RED BLUE')
for it in myColor:
print(repr(it))





Сравнивать члены перечисления можно следующим образом:
print(myColor.BLACK
print(myColor.BLACK
print(myColor.BLACK
print(myColor.BLACK
print(myColor.BLACK
print(myColor.BLACK
print(myColor.BLACK

is
is
is
==
!=
==
==

myColor.BLACK) # True
myColor.WHITE) # False
not myColor.WHITE) # True
myColor.BLACK) # True
myColor.WHITE) # True
myColor.WHITE) # False
1) # False

Для сравнения посредством операций: >, >— < и т. д. необхо­
димо для класса перегружать рассматриваемые ранее методы срав­
нения.
Еще важно запомнить, что члены перечисления хэшируемые, по­
этому их можно использовать в словарях и множествах.
5.13.1. IntEnum
Члены IntEnum можно сравнить с целыми числами, а также це­
лочисленные перечисления разных типов можно сравнивать друг
с другом:
from enum import IntEnum

class FirstEnum(IntEnum):
GO = 1
STOP = 2
class SecondEnum(IntEnum):
TURN = 1
DOWN = 2

if __ name__ == "__ main__ ":
print(FirstEnum == 1) # False
print(FirstEnum.GO == 1) # True
print(FirstEnum.GO == SecondEnum.TURN) # True

Однако их нельзя сравнивать со стандартными перечислениями
Епит:
from enum import IntEnum, Enum

class SecondEnum(IntEnum):
TURN = 1
DOWN = 2

179

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

class Color(Enum):
RED = 1
BLACK = 2

if __ name__ == "__ main__ ":
print(Color.RED == SecondEnum.TURN) # Flase

5.13.2. IntFlag

Текущий вариант Enum, как и предыдущий, основан на int. Раз­
личие состоит в том, что члены IntFlag могут быть объединены с ис­
пользованием побитовых операторов (&, |, 74,
from enum import IntFlag

class
A
В
C
D

TestFlag(IntFlag):
= 1
= 2
= 3
= 4

if __ name__ == "__ main__ " :
print(repr(TestFlag.A | TestFlag.B))
#
RW = TestFlag.A | TestFlag.D
print(repr(RW)) #
print(TestFlag.A in RW) # True
print(repr(TestFlag.D & TestFlag.C))
#
print(repr(TestFlag.D Л TestFlag.C))
#

Другое важное различие между IntFlag и Enum заключается
в том, что если флаги не установлены (значение равно 0), приве­
дение экземпляра класса IntFlag к логическому типу вернет False:
print(bool(TestFlag.D & TestFlag.C))

# False

Поскольку члены IntFlag также являются подклассами int,
их можно комбинировать с ними:
print(repr(TestFlag.D Л 12)) #
print(repr(TestFlag.А | 6)) #

5.13.3. Flag

Как и IntFlag, члены Flag можно комбинировать с помощью по­
битовых операторов (&, |, , —), но в отличие от IntFlag, члены
не могут ни комбинироваться, ни сравниваться ни с другими пере­
числениями Flag, ни с int.
180

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

В случае работы с Flag рекомендуется использовать auto для при­
своения значения, а не назначать его вручную:
from enum import Flag, auto

class
A
В
C
D

BoolFlag(Flag):
= auto()
= auto()
= auto()
= auto()

if __ name__ == "__ main__ " :
print(repr(BoolFlag.A | BoolFlag.B))
#
print(repr(BoolFlag.D & BoolFlag.C))
#
print(repr(BoolFlag.D Л BoolFlag.C))
#

Отдельные флаги должны иметь значения степеней двойки (1, 2,
4, 8,...), в то время как комбинации флагов — нет:
from enum import Flag, auto

class
A
В
C
D
E

NewBoolFlag(Flag):
= auto()
= auto()
= auto()
= auto()
= (A|B|C)AD

if __ name__ == "__ main__ " :
print(repr(NewBoolFlag.E)) #

5.13.4. Расширенные возможности перечислений
Так как перечисления реализуются посредством классов, их мож­
но расширять собственными методами или атрибутами:
from enum import Enum

class Color(Enum):
BLACK = 0
WHITE = 1
RED = 2
BLUE = 3
class CarBrand(Enum):
LADA = 0
FORD = 1
KAMAZ = 2
class Car(Enum):
MY_WORK_CAR = ("e006xxl27", Color.BLUE,
CarBrand.KAMAZ)

181

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

MY_HOME_CAR = ("H126yxl72", Color.WHITE,
CarBrand.LADA)
MY_WIFE_CAR = ("H125yx001", Color.BLACK,
CarBrand.FORD)

def__ init__ (self, number, color, brand):
self.number = number
self.color = color
self.brand = brand

if __ name__ == "__ main__ ":
for it in Car:
print(repr(it))
I

I

I

, )>


I

I

I

print(Car.MY_HOME_CAR.number) # H126yxl72
print(Car.MY_HOME_CAR.brand.name) # LADA
print(Car.MY_HOME_CAR.brand.value) # 0
print(Car.MY_HOME_CAR.color.name) # WHITE
print(Car.MY_HOME_CAR.color.value) # 1
print(Car.MY_WORK_CAR.color is Color.BLUE)
# True
print(Car.MY_WORK_CAR.color is Color.RED)
# False

Такое поведение перечислений близко по духу к тому, что имеет­
ся в языке программирования Java.

Резюме
В текущем разделе были рассмотрены классы и степень поддерж­
ки принципов ООП языком программирования Python. Фактически
определение класса равносильно созданию нового типа данных.
Отдельно стоит выделить тот факт, что у классов нет конструк­
тора с возможностью его перегрузки, как в ряде других языков про­
граммирования, а для инициализации атрибутов (переменных)
класса используется метод__ init__ , вызываемый при создании но­
вого экземпляра класса.
Инкапсуляция поддерживается условно. Ее соблюдение лежит
полностью на плечах разработчика и зависит от того, придержива­
ется он соглашений для обеспечения инкапсуляции в коде или нет.
Такой параметр класса, как self, ссылается на его текущий экзем­
пляр и передается в первом параметре методов класса. Также вме­
сто него методам класса может передаваться параметр cis, который
является ссылкой на класс, а статические методы могут вызываться
182

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

без создания экземпляра класса и не нуждаются в передаче им в ка­
честве аргумента параметра self.
Следует запомнить, что все методы классов в Python являются
виртуальными. Таким образом, если метод не переопределяется
в производном классе и не является приватным для базового класса,
он будет доступен для всех производных классов.
Методы и атрибуты (переменные) классов можно объявить «ус­
ловно» приватными путем добавления в начало их имени двойного
нижнего подчеркивания. Это вызывает механизм искажения, кото­
рый по-другому формирует такой атрибут класса, но не делает его
полностью недоступным.

Вопросы для самопроверки
1. Как в Python реализуются основные принципы ООП?
2. Где процедура поиска в иерархии наследования ищет атрибуты?
3. В чем отличие между объектом класса и объектом экземпляра?
4. Почему первый аргумент в функции метода класса является особым?
5. Каким образом создаются экземпляры и классы?
6. Где и как создаются атрибуты класса?
7. Где и как создаются атрибуты экземпляра?
8. Что self означает в классе Python?
9. Каким образом реализовывать перегрузку операций в классе Python?
Для чего применяется метод__init__?
10. Когда может понадобиться поддержка перегрузки операций в классах?
11. Что такое абстрактный суперкласс?
12. Что происходит, когда простое присваивание появляется на верхнем
уровне оператора class?
13. Почему в классе может возникнуть потребность в ручном вызове
метода__init__базового класса (суперкласса)?
14. Как можно дополнить унаследованный метод, не замещая его полно­
стью?
15. Чем локальная область видимости класса отличается от локальной
области видимости функции?
16. Какие два метода перегрузки операций можно использовать для под­
держки итерации в классах?
17. Какие два метода перегрузки операций обрабатывают вывод и в каких
контекстах?
18. Как можно перехватывать операции нарезания в классе?
19. Как можно перехватывать сложение на месте в классе?
20. Когда должна предоставляться перегрузка операций?
21. Что такое множественное наследование?
22. Как происходит поиск метода при множественном наследовании?

Упражнения
1. Напишите класс, позволяющий сформировать все уникальные под­
множества из списка целых чисел, например: [2, 4, 10, 1].

183

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

2. Напишите класс, реализующий все арифметические операции над
двумя значениями (а и b).
3. Напишите класс, описывающий такой объект, как автомобиль. Про­
думайте, какие методы и переменные он должен иметь.
4. Напишите класс, описывающий такой объект, как прямоугольник.
Перегрузите у реализованного класса методы сравнения (сравнивать
по площади), после чего создайте два экземпляра класса и проверьте, как
работают перегруженные методы.
5. Напишите класс, описывающий такой объект, как автомобиль. У него
может быть различное количество состояний, реализуемых посредством
перечислений. Добавьте методы, позволяющие экземпляру класса менять
свое текущее состояние (например: остановка, движение, поворот налево
ит. д.).
6. Напишите класс, который подсчитывает текущее количество его
экземпляров в приложении. Для корректного отображения этого числа
перегрузите у класса метод__del__ и напишите необходимую логику.
7. Напишите базовый класс, задающий интерфейс и часть характери­
стик (если надо) таких объектов, как геометрические фигуры, автомобиль
и магазин, животное.
8. Напишите несколько производных классов от базового класса гео­
метрических фигур (например: прямоугольник и квадрат).
9. Напишите несколько производных классов от базового класса авто­
мобилей (например: легковой и грузовой автомобиль).
10. Напишите несколько производных классов от базового класса мага­
зинов (например: ларек и супермаркет).
11. Напишите несколько производных классов от базового класса авто­
мобилей (например: лошадь и тигр).
12. Напишите класс, доступ к атрибутам (переменным) которого осу­
ществляется с помощью декоратора @property.
13. Напишите класс, хранящий целое число. Перегрузите у него методы
арифметических операций. Объявите два экземпляра класса и проверьте,
как работают перегруженные методы.
14. Напишите класс, который позволяет работать с json-файлом, осущест­
вляя его чтение, запись, добавление, удаление и изменение значений.
15. Напишите класс, который находит прямоугольник с максимальной
площадью из списка. Список можете формировать из экземпляров класса,
реализованного в упражнении 4.
16. Напишите класс, осуществляющий преобразование целого числа
из десятичной системы счисления в двоичную и наоборот.
17. Напишите класс, вычисляющий наименьший общий делитель (НОД).
Значения, участвующие в поиске НОД должны устанавливаться отдельными
методами или посредством декоратора @property до вызова метода расчета.
18. Напишите класс, вычисляющий корни квадратного уравнения.
19. Напишите класс, хранящий данные сотрудника фирмы и имеющий
метод, возвращающий характеристики текущего сотрудника в виде словаря.
20. Напишите класс, представляющий собой записную книжку. Каждый
элемент записной книжки должен содержать следующие поля: ФИО, но­
мер телефона, e-mail, день рождения. Записная книжка может сохраняться
на диск в виде json-файла, а также должна иметь метод загрузки данных
из файла.

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 6
ИСКЛЮЧЕНИЯ (EXCEPTION)
В результате изучения материалов данной темы обучающиеся должны:
знать

• как устроен механизм исключений в Python,
• основные способы генерации и обработки исключений;
уметь

• создавать и генерировать пользовательские исключения,
• использовать операторы try/except/finally и with/as;
владеть

• навыками перехвата и обработки исключений, возникающих в про­
цессе работы приложений,
• основными принципами работы с исключениями.

Исключения позволяют в случае ошибки в вычислениях или ло­
гике работы программы сразу же перейти к ее обработке, при этом
отменяя все вызовы функций, которые начались до того, как был
совершен вход в данный обработчик. Их можно рассматривать как
некоторый структурированный «безусловный переход». Исходя
из этого, оператор try представляет собой некоторую метку, к ко­
торой при генерации исключения перейдет интерпретатор Python,
прекратив выполнение любых функций, вызванных после объявле­
ния метки (то есть в теле оператора try).
Такое поведение обеспечивает согласованный способ реагиро­
вания на необычные события в процессе работы программы и по­
зволяет не вводить дополнительные проверки на коды отработки
вызываемых функций.
Перечислим основные способы применения исключения.
1. Обработка ошибок. Всякий раз, когда интерпретатор Python
обнаруживает ошибки в процессе выполнения программы, он гене­
рирует исключение. У пользователя имеется возможность перехва­
тить и обработать это исключение, либо проигнорировать. В по­
следнем случае интерпретатор остановит выполнение программы
и выведет соответствующее сообщение об ошибке.
2. Уведомление о событиях. В этом случае исключения ис­
пользуются вместо возвращаемых результирующих кодов, напри­
мер, у функций, что избавляет от необходимости вводить дополни­
тельные проверки.
185

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

3. Обработка особых случаев. Могут возникать ситуации, ког­
да условие возникновения исключения наступает настолько редко,
что его проще обрабатывать на более высоких уровнях, чем допол­
нительно усложнять код, внося обработку этой ситуации в различ­
ные места программы. Также для проверки того, что условия соот­
ветствуют ожидаемым, в процессе разработки может применяться
оператор assert.
4. Действия при завершении. Позволяет быть уверенным, что,
например, операции закрытия будут выполнены в любом случае,
независимо от того, сгенерировалось исключение в процессе рабо­
ты или нет.
5. Редкие потоки управления. Хотя в Python не существует
оператора «безусловного перехода», для этих задач может приме­
няться механизм исключений. Как уже рассматривалось ранее,
оператор break позволяет выйти только из тела цикла, в котором
он встречается, а при наличии блока с большим числом вложенных
циклов придется вводить множество дополнительных условий, что­
бы выйти из него. Самым простым и элегантным решением в дан­
ной ситуации будет сгенерировать исключение при достижении
условия выхода из блока вложенных циклов.

6.1. Пользовательские исключения
Такие исключения реализуются с помощью классов, унаследо­
ванных от встроенного класса исключения. Обычно им выступает
класс по имени Exception:
class TestException(Exception): pass

Пользовательские исключения в основном используются при
оповещениях о нарушении логики работы программы, отсутствии
каких-либо пользовательских данных и т. д.
Генерация пользовательского исключения производится при по­
мощи оператора raise, основы работы с которым будут рассмотрены
в следующем параграфе:
class TestException(Exception): pass

def test_raise(text):
if text == "exept":
raise TestException("My Exception")
print("No Exception")
def test(text):
try:
test_raise(text)
except TestException as my_ex:
print(my_ex)

186

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ " :
my_str = "notExept"
test(my_str) # No Exception
my_str = "exept"
test(my_str) # My Exception

Пользовательские исключения позволяют хранить добавочную
информацию в своих экземплярах и определять собственные мето­
ды, что расширяет их возможности в процессе их обработки. На­
пример, код ниже позволяет записать данные исключения в файл
журнала (лог):
class TestException(Exception):
logfile = 'dataerror.txt'

def __ init__ (self, line, file) :
self.line = line
self.file = file
def logerror(self):
log = open(self.logfile, 'a')
print('Error at:', self.file, self.line, file=log)
def test():
try:
raise TestException(22, 'test.log')
except TestException as my_ex:
my_ex.logerror()
print(my_ex)

if __ name__ == "__ main__ " :
test()

6.2. Основы обработки и генерации исключений
Для начала напишем следующую функцию:
def intro_function(text, index):
print(text[index])

При ее вызове с допустимым значением аргумента index не будет
генерироваться никаких исключений:
if __ name__ == "__ main__ ":
text_text = "Test"
intro_function(text_text, 2) # s

Но стоит передать значение, которое выходит за пределы дли­
ны последовательности, сгенерируется встроенное исключение
IndexError и интерпретатор прекратит свою работу:
intro_function(text_text, 10)
IndexError: string index out of range
187

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Такое поведение имеет смысл для простых сценариев. Так как
ошибки часто могут носить критический характер, то этот сцена­
рий позволяет изучить сообщение об ошибке и внести исправления
в код. Но нередко могут возникать ситуации, когда программа долж­
на продолжать свою работу даже после возникновения внутренних
ошибок. Рассмотрим, что для этого нужно сделать, на примере на­
писанной ранее функции. Для того, чтобы программа продолжила
свою работу, необходимо ее вызов поместить внутрь оператора try,
чтобы самостоятельно перехватывать исключения и не вызывать
стандартный механизм отработки исключений интерпретатора
Python, который приводит к остановке программы:
try:

intro_function(text_text, 10)
except IndexError:
print("Перехват исключения IndexError")
print("Продолжаем работу!")
Перехват исключения IndexError
Продолжаем работу!

Конечно, и следующий код отработает аналогично предыдущему:
try:
intro_function(text_text, 10)
except:
print("Перехват исключения IndexError")
print("Продолжаем работу!")

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

6.2.1. Унифицированный оператор try/except/finally
Ниже приведена структура унифицированного оператора try/
except/finally:
try:

...#блок try
except ИмяИсключения:
... # перехват исключения ИмяИсключения
except (ИмяИсключения2, ИмяИсключенияЗ):

# перехват любого из перечисленных исключений
except ИмяИсключения4 as переменная:
# перехват ИмяИсключения4 и присвоение переменной
# экземпляра класса исключения
except:

188

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

it если были сгенерированы все остальные исключения

else: # обработчик для случая отсутствия исключений
... #блок else
finally: # охватывает все остальное
... it блок finally

Код, который подлежит выполнению, помещается в блок try.
Если при его выполнении сгенерируется исключение, оно будет
перехвачено блоками except. При этом except может перехватывать
как определенное исключение, так и любое из перечисляемых при
его объявлении. Ссылку на экземпляр класса сгенерированного ис­
ключения можно присвоить любой переменной посредством опе­
ратора as. В том случае, если сгенерировалось исключение, которое
не было задано, оно будет перехвачено блоком except, у которого
не объявлено ни одного параметра.
Если в процессе выполнения кода в блоке try не генерируется
ни одного исключения, отработает блок else. Блок finally срабатывает
в любом случае, независимо от того, будет ли сгенерировано исключе­
ние. А сама конструкция finally предназначена для указания действий
очистки, которые всегда должны совершаться при выходе из try.
В приведенном примере структуры блок else можно опустить,
если код из него перенести в конец блока try. Его использование
дает возможность убедиться, что код блока try выполнился коррек­
тно и без генерации исключений.
Унифицированный оператор try/except/finally должен записы­
ваться полностью и в своей короткой форме, где он обязан иметь
блок except, либо finally:
def example_l(text):
try:
text[99]
except IndexError:
print('except') # выполняется except
finally:
print('finally') # выполняется finally
print('after try')
def example_2(text):
try:
text[3]
except IndexError:
print('except') it выполняется except
else:
print('else') it выполняется else
finally:
print('finally') # выполняется finally
print('after try')
def example_3(text):
try:
text[3]
189

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

except IndexError:
print('except') # выполняется except
finally:
print('finally') # выполняется finally
print('after try')
def example_4():
try:
my_error =1/0
except IndexError:
print('except') # выполняется except
finally:
print('finally') # выполняется finally
print('after run') # после выполнения
def example_5():
file = open('data', 'w')
try:
raise FileNotFoundError # Генерирует исключение
finally:
print('finally') # выполняется finally
file.close()
# Всегда закрывать файл, чтобы сбросить буферы
print('after try')

if __ name__ == "__ main__ " :
my_text = "Test"
print("run example_l") # run example_l
example_l(my_text)
# except
# finally
# after try
print("run example_2")# run example_2
example_2(my_text)
# else
# finally
# after try
print("run example_3")# run example_3
example_3(my_text)
# finally
# after try
# print("run example_4")
# example_4()
# print("run example_5")
# example_5()
print("Продолжаем работу!") # Продолжаем работу!

В приведенном примере показаны способы обработки исклю­
чений. При запуске функций example_4 или ехатр1е_5 после от­
работки блока finally интерпретатор прекратит выполнение про­
граммы, так как не предусмотрена обработка тех исключений, что
генерируются в процессе выполнения программы:
190

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

print("run example_4") # run example_4
example_4()
# finally
# ZeroDivisionError: division byzero
print("run example_5") # run example_5
example_5()
# finally
# FileNotFoundError

6.2.2. Оператор raise
Данный оператор используется для явного генерирования ис­
ключения. Оператор raise состоит из слова raise, за которым допол­
нительно указывается класс или экземпляр класса генерируемого
исключения:
raise экземпляр # Генерирует экземпляр класса
raise класс # Создает и генерирует экземпляр класса
raise
# Повторно генерирует самое последнее исключение

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

try:
raise IndexError("my error")
except IndexError:
print("Обработка исключения")
# Обработка исключения
raise
'''Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 3, in
raise IndexError("my error")
IndexError: my error'''

В некоторых случаях исключения могут генерироваться в ответ
на другие исключения. Для этого, начиная с Python 3.x, оператору
raise добавили конструкцию from:
class TestException(Exception): pass

if __ name__ == "__ main__ " :
try:
raise IndexError("my error")
except IndexError as ex:
print("Обработка исключения") # Обработка исключения
raise TestException("Cцeплeниe") from ex
print("Работаем дальше!")
' ' 'Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 12, in
raise IndexError("my error")
191

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

IndexError: my error

The above exception was the direct cause of the following
exception:

Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 15, in
raise TestException("CuenneHMe") from ex
__ main__ .TestException: Сцепление '''

Когда конструкция from используется в явном запросе raise, сле­
дующее за from выражение указывает еще один класс или экземпляр
для присоединения к атрибуту__ cause__нового генерируемого ис­
ключения. Если сгенерированное исключение не перехвачено, тог­
да интерпретатор Python выводит оба исключения как часть стан­
дартного сообщения об ошибке.
При таком подходе сцепленные исключения могут иметь произ­
вольную длину:
class TestException(Exception): pass
class NewTestException(Exception): pass
if __ name__ == "__ main__ " :
try:
try:
raise IndexError("my error")
except IndexError as ex:
print("Обработка исключения")
raise TestException("Cцeплeниe") from ex
except TestException as ex:
raise NewTestException("Euie одно сцепление")from ex
print("Работаем дальше!")
'''Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 12, in
raise IndexError("my error")
IndexError: my error

The above exception was the direct cause of the following
exception:

Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 15, in
raise TestException("C4enneHne") from ex
__ main__ .TestException: Сцепление
The above exception was the direct cause of the following
exception:

Traceback (most recent call last):
File "F:/code/python/module6/part3.py", line 17, in
raise NewTestException("Euie одно сцепление") from ex
__ main__ .NewTestException: Еще одно сцепление
I

192

I

I

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

6.2.3. Оператор assert
В процессе разработки приложения для целей отладки можно ис­
пользовать оператор assert, представляющий собой синтаксическое
сокращение для оператора raise и имеющий следующую форму за­
писи:
assert test, data
# Часть data является необязательной

В случае, когда результатом вычисления test будет False, интер­
претатор Python сгенерирует исключение, в котором элемент data
(если предоставлен) используется как аргумент для конструктора
класса исключения.
Так как операторы assert посредством флага компилятора удаля­
ются из сгенерированного байт-кода, то не рекомендуется в блоках
вычисления test использовать методы, меняющие внутренние состо­
яния объектов.
Далее приводится код, демонстрирующий принцип использова­
ния оператора assert:
def test_assert(value):
assert value > 0, 'value должен быть > 0'
return 30/value

if __ name__ == "__ main__ " :
print(test_assert(10)) # 3.0
print(test_assert(0))
'''Traceback (most recent call last):
File "F:/code/python/module6/part4.py" } line 7, in
print(test_assert(0))
File "F:/code/python/module6/part4.py" , line 2, in test_
assert
assert value > 0, 'value должен быть > 0'
AssertionError: value должен быть > 0'''

Самое важное, о чем необходимо помнить при использовании
оператора assert, — он служит для проверок ограничений (логики),
которые определяет разработчик в приложении, а не для перехвата
подлинных программных ошибок.
6.2.4. Оператор with/as и протокол
управления контекстами
Оператор with вместе с его необязательной конструкцией
as предназначен для работы с объектами диспетчеров контекстов.
Он используется как альтернатива оператору try/finally и, подобно
ему, предназначен для указания действий стадии завершения или
«очистки», которые должны выполняться независимо от того, воз­
никало исключение в процессе выполнения кода или нет.
193

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Диспетчер контекста расширяет возможности ряда встроенных
инструментов, таких как файлы (автоматически закрываются)
и потоки (автоматически блокируются и деблокируются), а также
позволяет разработчикам реализовывать собственные диспетчеры
контекстов с применением классов.
Базовый формат оператора with/as приведен ниже:
with выражение [as переменная]:
блок-with

В данном случае выражение должно возвращать объект, поддер­
живающий протокол управления контекстами, а также может воз­
вращать значение, которое будет присвоено имени переменная при
наличии необязательной конструкции as. Следом за этим, объект
может выполнить код запуска перед началом блока-with, а также
код завершения после окончания блока-with независимо от того,
генерировалось ли исключение.
Приведем пример работы с файлом посредством рассматривае­
мой конструкции:
with open('dataFile', 'г') as myfile:
for line in myfile:
print(line)
...дополнительный код...

После завершения блока-with механизм управления контекстами
гарантирует автоматическое закрытие файлового объекта, на кото­
рый ссылается myfile (даже если во время обработки файла в цикле
for возникло исключение).
Рассмотренный пример работы с файлом полностью идентичен
следующему коду с использованием оператора try/finally:
myfile = open('dataFile', 'г')
try:
for line in myfile:
print(line)
...дополнительный код...
finally:
myfile.close()

Теперь реализуем собственный объект, поддерживающий работу
с диспетчером контекста. Для этого у класса необходимо перегру­
зить следующие методы:
1) __ enter__ , возвращаемое значение, которое присваивается
переменной в конструкции as при ее наличии либо попросту отбра­
сывается;
2) __ exit_ , который отрабатывается при завершении блока with
или в случае возникновения исключения при выполнении кода
в блоке with. Когда генерируется исключение, в атрибуты данного
метода передается детальная информация по нему. После отработки
194

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

исключения рекомендуется, чтобы данный метод возвращал False,
тем самым исключение сгенерируется повторно и распространится
за пределы блока with. Если метод вызывается по завершении блока
with, в качестве атрибутов передается None.
Ниже приведен пример работы с классом, у которого перегруже­
ны методы__exit__и__ enter__:
class MyError(Exception):pass
class TestWith:
def print_value(self, value):
print(f"Входное значение: {value}")

def __ enter__ (self):
print("Начало блока with")
return self
def__ exit__ (self, exc_type, exc_val, exc_tb):
if exc_type is None:
print("Успешное завершение!")
else:
print("Было сгенерировано исключение: "+str(exc_
type))
return False

if __ name__ == "__ main__ ":
with TestWith() as test_object:
# Начало блока with
test_object.print_value("Test 1")
# Входное значение: Test 1
print("Конец блока with") # Конец блока with
with TestWith() as test_object:
# Начало блока with
test_object.print_value("Test 2")
# Входное значение: Test 2
raise MyError("Ошибка!")
# Было сгенерировано исключение: cclass '__ main__ .MyError’>
print("Конец блока with")
' ' 'Traceback (most recent call last):
File "F:/code/python/module6/part5.py"., line 24, in
raise MyError("Ошибка!")
__ main__ .MyError: Ошибка! '''

6.З. Встроенные классы исключений
Все встроенные исключения организованы в неглубокую иерар­
хию с универсальными категориями базовых классов и уточняющи­
ми типами производных классов, которые в свою очередь являют­
ся предварительно определенными классами, доступными в виде
195

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

встроенных имен в модуле builtins. Ниже приводится упрощенная
иерархия исключений:
BaseException
SystemExit
Keyboardinterrupt
GeneratorExit
Exception
Stopiteration
ArithmeticError
AssertionError
AttributeError
BufferError
EnvironmentError
EOFError
ImportError
LookupError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError
Warning

BaseException — базовый класс верхнего уровня, который пред­
назначен для прямого наследования классами, определяемыми
пользователем.
Exception — базовый класс для определяемых пользователем ис­
ключений, связанных с приложением. Его рекомендуется использо­
вать вместо BaseException, так как тогда в обработчике оператора
try гарантируется, что программа будет перехватывать все исключе­
ния, кроме событий выхода в систему (то есть не перехватываются:
SystemExit, Keyboardinterrupt и GeneratorExit).
Поскольку исключения подвержены самому частому изменению,
то модуль exceptions был удален в Python 3.x, что не позволяет полу­
чить по ним документацию посредством встроенной функции help.
С подробным их описанием можно ознакомиться в [41].

Резюме
Исключения Python являются высокоуровневым механизмом
управления потоком выполнения и могут генерироваться ин­
терпретатором или самим программистом «вручную». При этом
их можно игнорировать либо перехватывать посредством различ­
ных конструкций оператора try. Такие операторы, как raise и assert,
196

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

инициируют различные исключения: встроенные или определяе­
мые пользователем.
Использование диспетчеров контекста посредством оператора
with/as позволяет гарантировать то, что действия на стадии завер­
шения или «очистки» будут выполняться всегда, независимо от того,
возникало исключение в процессе выполнения кода или нет.
Поскольку исключения реализуются в виде объектов экземпля­
ров классов, то они поддерживают концепцию иерархии исключе­
ний. Что в свою очередь позволяет облегчить сопровождение кода
путем добавления дополнительной информации или методов в виде
атрибутов определяемого класса исключения. А также разрешает
исключениям наследовать данные и поведение от базовых классов.
Это способствует тому, что указания базового класса иерархии ис­
ключений в операторе try обеспечивает перехват исключения этого
класса и всех его производных классов.

Вопросы и задания для самопроверки
1. Назовите три случая, для обработки которых хорошо подходят ис­
ключения.
2. Что произойдет с исключением, если вы не предпримете ничего
специального для его обработки?
3. Как сценарий может восстанавливаться после исключения?
4. Назовите два способа генерации исключений в сценарии.
5. Назовите два способа указания действий, подлежащих выполнению
на стадии завершения вне зависимости от того, возникало исключение или
нет.
6. Для чего предназначен оператор try?
7. Для чего предназначен оператор raise?
8. Для чего предназначен оператор assert и на какой другой оператор
он похож?
9. Для чего предназначен оператор with/as и на какой другой оператор
он похож?
10. Назовите два способа присоединения информации контекста к объ­
ектам исключений.

Упражнения
1. Напишите класс, реализующий все арифметические операции над
двумя значениями (а и b). В случае, если одно из значений при вызове
операции равно нулю, генерируется исключение.
2. Напишите класс, реализующий такие арифметические действия, как
деление и умножение. Если одно из значений при вызове операции равно
нулю, генерируется исключение, значение ноль меняется на 1 и вычисление
операции продолжается.
3. Напишите функцию, возводящую строку в верхний регистр. Добавьте
проверку на то, что на вход функции подается не пустая строка.

197

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

4. Напишите функцию, проверяющую вхождение задаваемого элемента
в список. Добавьте проверку на то, что список не пустой.
5. Реализуйте собственный класс исключения, которое будет генери­
роваться каждый раз, когда в строке, которая является аргументом для
функции, присутствует символ «п».

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Тема 7
ПОТОКИ, ПРОЦЕССЫ И АСИНХРОННОЕ
ПРОГРАММИРОВАНИЕ
В первой теме мы познакомились с такой особенностью Python,
как глобальная блокировка интерпретатора (GIL). Именно GIL от­
ветственен за то, что в Python единовременно может выполняться
только один поток, из-за чего многопоточные программы не могут
использовать преимущества многоядерных процессоров при нали­
чии в них вычислений, работающих на CPU.
При этом, если разрабатываемая программа ориентирована
на операции ввода-вывода, что типично для сетевых коммуника­
ций, потоки или асинхронное программирование являются самым
оптимальным выбором, так как в этом случае GIL не задействуется.
В результате изучения данной темы обучающиеся должны:
знать

• особенности работы с модулями threading, multiprocessing и asyncio,
• разницу между многопоточным, асинхронным и многопроцессорным
программированием в Python, а также когда и какой из этих подходов к раз­
работке программного обеспечения необходимо применять,
уметь

• создавать классы, которые могут запускаться в отдельном потоке или
процессе,
• правильно организовать доступ к разделяемым ресурсам,
• организовывать межпроцессорное и многопоточное взаимодействие;
владеть

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

7.1. Многопоточное программирование
Поток — отдельно выполняемый набор инструкций, использую­
щий глобальное состояние (память) совместно с другими потоками.
Из приведенного определения может сложиться впечатление, что
потоки выполняются одновременно, но обычно они выполняются
поочередно на одном процессоре (ядре). В то же самое время кор­
ректное использование многопоточности позволяет повысить бы­
199

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

стродействие разрабатываемой программы по сравнению с однопо­
точным подходом.

7.1.1. Модуль threading и класс Thread

Применяемый в данном модуле подход к многопоточному про­
граммированию аналогичен используемому в Java. Отличия заклю­
чаются в том, что объекты блокировок и состояний моделируются
как отдельные объекты, а также нет возможности извне управлять
потоками.
Для работы с потоками в модуле threading содержатся следующие
классы: Thread, Condition, Semaphore, Event, Barrier, Lock, RLock,
Timer и BoundedSemaphore. При этом все объекты, которые предо­
ставляет модуль, атомарные.
Для использования потоков необходимо импортировать класс
Thread модуля threading, с которым можно работать несколькими
способами:
1) в процессе создания экземпляра класса Thread именованно­
му аргументу target можно передать функцию, которая будет ис­
пользоваться в качестве основной для данного экземпляра;
2) реализовать производный класс от класса Thread и переопре­
делить метод run.
Для того, чтобы запустить поток, необходимо у созданного эк­
земпляра класса вызвать метод start. Его работа завершится после
выполнения основной функции. Каждый объявляемый поток может
работать в двух режимах: основной режим и режим демона (аргу­
мент daemon — True). В первом случае потоки не дадут программе
завершиться, пока все они не выполнятся все их основные функции.
Во втором случае поток-демон позволяет завершать выполнение
программы, даже если потоки остаются активными.
Ниже приведен пример выполнения функции в различных пото­
ках:
from threading import Thread

def thread_work(name):
print(name)

if __ name__ =="__ main__
for it in range(5):
new_thread = Thread(target=thread_work,args=(f"Thread
#{it}",))
new_thread.start()
# Thread #0
# Thread #1
# Thread #2
# Thread #3
# Thread #4

200

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

Теперь рассмотрим аналогичный вариант, с тем отличием, что
в нем пользовательский класс наследуется от класса Thread. Сле­
дует отметить, что для отладки программ не следует использовать
функцию print, а лучше воспользоваться функциональностью, кото­
рую предоставляет модуль logging. Так, например, включение имен
потоков в протоколируемые сообщения позволяет отслеживать ис­
точники этих сообщений:
from threading import Thread
import logging

class MyThread(Thread):
def __ init__ (self, group=None, target=None,
name=None, args=(), kwargs=None, *,
daemon=None):
super().__ init__ (group=group,
target=target, name=name,
daemon=daemon)
self.args = args
self.kwargs = kwargs

def run(self) -> None:
logging.debug(f'Thread args = {self.args} '
f'and kwargs = {self.kwargs}')
if __ name__ =="__ main__
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
for it in range(5):
new_thread = MyThread(args=(it,),
kwargs={it: it+2, f"{it}": str(it)*2})
new_thread.start()
’’• (Thread-1 ) Thread args = (0,) and kwargs = {0:
'00'}
(Thread-2 ) Thread args = (1,) and kwargs = {1: 3,
(Thread-3 ) Thread args = (2,) and kwargs = {2: 4,
(Thread-4 ) Thread args = (3>) and kwargs = {3: 5,
(Thread-5 ) Thread args = (4,) and kwargs = {4: 6,
I

I

2} '0':

'1':
'2':
'3':
'4':

'11'}
'22'}
'33'}
'44'}

I

7.1.2. Потоки Timer
Экземпляры класса Timer начинают работать с некоторой задерж­
кой, определяемой пользователем. Кроме того, их выполнение мож­
но отменить (или завершить) в любой момент периода задержки:
from threading import Timer
import logging
import time
201

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

def thread_work():
logging.debug("Поехали!")

if __ name__ =="__ main__
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
my_timerl = Timer(0.3, thread_work)
my_timerl.setName("MyTreadTimer-1")
my_timer2 = Timer(0.3, thread_work)
my_timer2.setName("MyTreadTimer-2")
logging.debug("Запуск таймеров!")
# (MainThread) Запуск таймеров!
my_timerl.start()
my_timer2.start()

#

#

#
#

logging.debug(f"Задержка перед отменой
f'выполнения {my_timer2.getName()}")
(MainThread) Задержка перед отменой выполнения MyTreadTimer-2
time.sleep(0.2)
logging.debug(f"Отмена потока - "
f" {my_timer2.getName()}")
(MainThread) Отмена потока - MyTreadTimer-2
my_timer2.cancel()
logging.debug("Завершение")
(MainThread) Завершение
(MyTreadTimer-1) Поехали!

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

7.1.3. Класс RLock. Управление доступом к ресурсам
При работе со встроенными структурами данных используются
атомарные операции, в то время как простые типы данных (числа,
целые или с плавающей точкой) не имеют такой защиты. В этом
случае для контроля доступа к разделяемым ресурсам, чтобы избе­
жать потери данных или их повреждения, используется так называ­
емый замок — классы Lock или RLock.
При работе с Lock может возникнуть ситуация самоблокировки
из-за обращения к экземпляру класса Lock из потока, в котором уже
осуществляется блокировка доступа к ресурсам.
Чтобы избежать таких проблем, рекомендуется использовать
RLock. Данная реализация замка позволяет выполнить блокиров­
202

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

ку только в том случае, если замок удерживает другой поток, что
предотвращает нежелательную блокировку:
from threading import Thread, RLock
import logging
import time

class DoubleCounter:
def __ init__ (self):
self .first_counter = 1
self.second_counter = 5
self.lock = RLock()

def increment_first_counter(self):
with self.lock:
# для блокировки используем диспетчер контекста
self.first_counter += 1
logging.debug(f"New first counter value is "
f"{self.first_counter }")
def increment_second_counter(self ):
with self.lock:
self.second_counter += 1
logging.debug(f"New second counter value is "
f"{self.second_counter}")
def increment(self):
with self.lock:
self.increment_first_counter()
self.increment_second_counter()
def thread_work(counter):
counter.increment()
time.sleep(0.2)
counter.increment_second_counter()
time.sleep(0.3)
counter.increment_first_counter()

if __ name__ == "__ main__ ":
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
my_counter = DoubleCounter()
logging.debug("Start")
for it in range(3):
new_thread = Thread(target=thread_work, args=(my_
counter,))
new_thread.start()
logging.debug("Finish")
I

I

I

(MainThread) Start
(Thread-1 ) New first counter value is 2

203

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

(Thread-1 )
(Thread-2 )
(Thread-2 )
(Thread-3 )
(MainThread)
(Thread-3 )
(Thread-1 )
(Thread-2 )
(Thread-3 )
(Thread-1 )
(Thread-2 )
(Thread-3 )

New second counter value is 6
New first counter value is 3
New second counter value is 7
New first counter value is 4
Finish
New second counter value is 8
New second counter value is 9
New second counter value is 10
New second counter value is 11
New first counter value is 5
New first counter value is 6
New first counter value is 7

Для того, чтобы понять разницу между Lock и RLock, замените
в примере выше вариант блокировки и запустите скрипт.
7.1.4. Синхронизация потоков
Бывают случаи, когда необходимо синхронизировать операции,
выполняемые в двух или более потоках. Для этого модуль threading
предоставляет механизм событий, который обеспечивает простой
способ организации безопасного взаимодействия потоков. Класс
Event имеет внутренний флаг, который могут устанавливать или
сбрасывать другие потоки посредством методов set и clear. Метод
wait класса Event позволяет приостановить работу потока до тех
пор, пока другой поток не установит указанный флаг, или спустя
задаваемый период времени (в секундах):
from threading import Thread, Event
import logging
import time

def event_work(event):
logging.debug("run event_work")
event_wait = event.wait()
logging.debug("Флаг установлен")
def event_with_timeout(event, time):
while not event.is_set(): # флаг установлен?
logging.debug(f"Ожидание установки флага
" или истечение "
"времени в event_with_timeout")
event_wait = event.wait(time)
logging.debug("Флаг установлен")
if event_wait:
logging.debug("Обработка события")
else:
logging.debug("Флаг не был установлен")

if __ name__ =="__ main__ ":
logging. basicConfig(
level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s', )
204

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

event = Event()
my_threadl = Thread(name='event_thread',
target=event_work,
args=(event,))
my_threadl.start()
my_thread2 = Thread(name='event_timeout',
target=event_with_timeout,
args=(event, 3))
my_thread2.start()
logging.debug('Задержка перед установкой флага')
time.sleep(0.3)
event.set()
logging.debug("Флаг установлен")
I

I

I

(event_thread) run event_work
(event_timeout) Ожидание установки флага или истечение времени
в event_with_timeout
(MainThread) Задержка перед установкой флага
(MainThread) Флаг установлен
(event_timeout) Флаг установлен
(event_timeout) Обработка события
(event_thread) Флаг установлен
I

I

I

Помимо класса Event, для синхронизации потоков еще исполь­
зуются Condition и Barrier. Класс Condition является некоторой
комбинацией классов Event и Lock, что позволяет нескольким по­
токам ожидать, пока ресурс, с которым производится работа, не бу­
дет обновлен. Класс Barrier позволяет создавать так называемую
«контрольную точку» для задаваемого количества потоков. Каждый
из этих потоков, достигая контрольной точки, блокируется до тех
пор, пока ее не достигнут все потоки, участвующие в этом механиз­
ме блокировки.
Ниже приводится пример использования Condition:
from threading import Thread, Condition
import logging
import time

def slave(condition):
logging.debug('Запуск потока ведомого')
with condition:
condition.wait()
# ожидаем разблокировки ресурса
logging.debug('Блокировка с ведомого снята')
def master(condition):
logging.debug('Запуск потока ведущего')
with condition:
logging.debug('Разблокировка ресурса')
condition.notifyAll()

205

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ ":
logging.basicConfig(
level=logging.DEBUG,
format='(%(threadName)-3s) %(message)s',
)

condition = Condition()
slavel = Thread(name='slavel', target=slave,
args=(condition,))
slave2 = Thread(name='slave2', target=slave,
args=(condition,))
master = Thread(name='master', target=master,
args=(condition,))
slavel.start()
time.sleep(0.1)
slave2.start()
time.sleep(O.l)
master.start()
(slavel)
(slave2)
(master)
(master)
(slavel)
(slave2)

Запуск потока ведомого
Запуск потока ведомого
Запуск потока ведущего
Разблокировка ресурса
Блокировка с ведомого снята
Блокировка с ведомого снята

7.1.5. Семафоры
Порой может случиться, что необходимо разрешить одновре­
менный доступ к ресурсу сразу нескольким потокам или иметь воз­
можность ограничить общее количество потоков, которые имеют
доступ к ресурсу в один момент времени. Примером такой ситуации
может выступать пул потоков, задача которого — поддерживать
фиксированное количество соединений, загрузок и т. д.
Для решения таких задач используются семафоры (класс
Semaphore). Ниже приведен пример работы класса TestPool, кото­
рый обеспечивает удобный способ отслеживания работы потоков.
Естественно, в реальных системах на данном пуле лежала бы еще
задача распределения соединений и ряда значений между всеми
активными потоками и возврат их в пул после завершения потока:
from threading import Thread, Semaphore, Lock, current_thread
import logging
import time

class TestPool:

def __ init__ (self):
super(TestPool, self).__ init__ ()
self.active = []
self.lock = Lock()

206

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

def makeActive(self, name):
with self.lock:
self.active.append(name)
logging.debug(f'Потоков в пуле'
f' {self.active}')
def makelnactive(self, name):
with self.lock:
self.active.remove(name)
logging.debug(f'Потоков в пуле'
f' {self.active}')

def worker(semaphore, pool):
logging.debug('Ожидание очереди при '
'подключении к пулу')
with semaphore:
name = current_thread().getName()
pool.makeActive(name)
time.sleep(0.1)
pool.makelnactive(name)

if __ name__ =="__ main__ ":
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s (%(threadName)-Is) '
'%(message)s',
datefmt='%I:%M:%S %p'
)

pool = TestPool()
semaphore = Semaphored)
for it in range(6):
new_thread = Thread(target=worker,
name=str(it),
args=(semaphore, pool),
)
new_thread.start()

12:14:04
12:14:04
12:14:04
12:14:04
12:14:04
12:14:04
12:14:04
12:14:04
12:14:05
12:14:05
12:14:05
12:14:05

PM
РМ
PM
PM
РМ
РМ
РМ
РМ
РМ
РМ
РМ
РМ

(0) Ожидание очереди при подключении
(0)Потоков в пуле ['0']
(1) Ожидание очереди при подключении
(1) Потоков в пуле ['0', '1']
(2) Ожидание очереди при подключении
(3) Ожидание очереди при подключении
(4) Ожидание очереди при подключении
(5) Ожидание очереди при подключении
(0)Потоков в пуле ['1']
(2) Потоков в пуле ['1', '2']
(1)Потоков в пуле ['2']
(3) Потоков в пуле ['2', '3']

к пулу
к пулу
к пулу
к пулу
к пулу
к пулу

207

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

12:14:05
12:14:05
12:14:05
12:14:05
12:14:05
12:14:05
I

I

PM
РМ
РМ
РМ
РМ
РМ

(2)
(4)
(3)
(5)
(5)
(4)

Потоков
Потоков
Потоков
Потоков
Потоков
Потоков

в
в
в
в
в
в

пуле ['3']
пуле ['3', '4']
пуле ['4']
пуле ['4', '5']
пуле ['4']
пуле []

I

7.2. Multiprocessing
Процесс — это экземпляр выполняющейся программы. Процес­
сы, которым необходимо участвовать в обмене данными с други­
ми процессами, должны явно организовать такой обмен посред­
ством механизмов межпроцессного взаимодействия (IPC). Они
могут передавать данные между друг другом посредством файлов,
баз данных, встроенных структур данных, сетевого взаимодей­
ствия и т. д.
Модуль multiprocessing предоставляет пользователю схожую
функциональность в части синхронизации (Event, Condition и т. д.)
и блокировки общих ресурсов (Lock, Semaphore и т. д.) с модулем
threading. Поэтому в данном разделе сфокусируем свое внимание
на различных вариантах создания процессов и межпроцессорном
взаимодействии.

7.2.1. Модуль multiprocessing и класс Process
Для использования процессов необходимо импортировать класс
Process модуля multiprocessing, с которым можно работать, как
и с потоками, несколькими способами:
1) в процессе создания экземпляра класса Process именованно­
му аргументу target можно передать основную функцию, которая
будет выполняться в отдельном процессе;
2) реализовать производный класс от класса Process и пере­
определить метод run.
Чтобы запустить процесс, необходимо у созданного экземпляра
класса вызвать метод start. Его работа завершится после выполне­
ния основной функции. Каждый объявляемый процесс может рабо­
тать в двух режимах: основной режим и режим демона (аргумент
daemon = True).
Ниже приведен пример выполнения функции различными про­
цессами.
from multiprocessing import Process

def process_run(num):
print(f"Процесс № {num}")

208

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ " :
for it in range(5):
my_process = Process(target=process_run,
args=(it,))
my_process.start()
# Процесс № 0
# Процесс № 1
# Процесс № 2
# Процесс № 3
# Процесс № 4

Теперь рассмотрим вариант с производным пользовательским
классом, от класса Process:
from multiprocessing import Process

class MyProcess(Process):

def __ init__ (self, group=None, target=None,
name=None, args=(), kwargs=None,
*, daemon=None):
super().__ init__ (group=group,target=target,
name=name, daemon=daemon)
self.args = args
self.kwargs = kwargs
def run(self) -> None:
print(f'Process #{self.args[0]} run with '
f' args = {self.args} '
f'and kwargs = {self.kwargs}')

if __ name__ == "__ main__ ":
for it in range(5):
my_process = MyProcess(args=(itj),
kwargs={it: it+2,
f"{it}": str(it)*2}
)
my_process.start()
I

I

I

Process
Process
Process
Process
Process
I

#0
#1
#2
#3
#4

run
run
run
run
run

with
with
with
with
with

args
args
args
args
args

=
=
=
=
=

(0,)
(1,)
(2,)
(3,)
(4^)

and
and
and
and
and

kwargs
kwargs
kwargs
kwargs
kwargs

=
=
=
=
=

{0:
{1:
{2:
{3:
{4:

2,
3,
4,
5,
6,

'0':
'1':
'2':
'3':
'4':

'00'}
'll'}
'22'}
'33'}
'44'}

I

7.2.2. Взаимодействие между процессами
Самый простой способ взаимодействия между процессами —
использование встроенной структуры данных Queue из модуля
multiprocessing:
209

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

from multiprocessing import Process, Queue
def pow_square(value, queue):
for i in value:
queue.put(i*i)
def pow_cube(value, queue):
for i in value:
queue.put(i*i*i)

if __ name__ == "__ main__ " :
my_numbers = range(3)
queue = Queue()
process_pow_square = Process(target=pow_square,
args=(my_numbers,
queue) )
process_pow_cube = Process(target=pow_cube,
args=(my_numbers,
queue) )
process_pow_square.start() # стартуем процесс
process_pow_cube.start()

process_pow_square.join()
# ожидаем завершение процесса
process_pow_cube.join()
while not queue.empty():
print(queue.get(), end="
#014018

")

Еще одним способом взаимодействия между процессами являет­
ся добавление общего пространства имен. Для этого используется
класс Manager, который также предоставляет доступ к встроенным
типам данных модуля multiprocessing:
from multiprocessing import Process, Manager, Event

def slave(ns, event):
ns.my_list.append (1)
ns.my_list.append (2)
ns.my_list.append (3)
ns.my_list.append("new value")
ns.my_value = 3.14
event.set()
def master(ns, event):
print(f'my_list до установки флага события: '
f'{ns.my_list}')
print(f'my_value до установки флага события: '
f'{ns.my_value}')
event.wait()

210

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

print(f'my_list после установки флага события: '
f'{ns.my_list}')
print(f'my_value после установки флага события:'
f'{ns.my_value}')

if __ name__ == '__ main__ ':
mgr = Manager()
namespace = mgr.Namespace()
# общее пространство имен
namespace.my_list = mgr.list() # создаем список
# объявляем переменную
namespace.my_value = mgr.Value('d'> 0.0)

event = Event()
slave_process = Process(
target=slave,
args=(namespace, event),
)
master_process = Process(
target=master,
args=(namespace, event),
)
master_process.start()
slave_process.start()

master_process.join()
slave_process.join()
I

I

I

my_list до установки флага события: [1]
my_value до установки флага события: Value('d', 0.0)
my_list после установки флага события: [1, 2, 3, 'new value']
my_value после установки флага события: 3.14
I

I

I

Также имеется возможность использовать сетевое взаимодей­
ствие для организации передачи данных между потоками. Для это­
го из multiprocessing .connection необходимо импортировать классы
Listener и Client:
from multiprocessing.connection import (Listener,
Client)
from multiprocessing import Process
from array import array

def server():
address = ('localhost', 6000)
# family is deduced to be 'AF_INET'
with Listener(address,
authkey=b'secret password') as listener:
with listener.accept() as conn:
print(f'connection accepted
f'from {listener.last_accepted}')
211

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

conn.send([2.25, None, 'junk', float])
conn.send_bytes(b'hello')
conn.send_bytes(array('i', [42, 1729]))

def client():
address = ('localhost', 6000)
with Client(address,
authkey=b'secret password') as conn:
print(conn.recv())
# => [2.25, None, 'junk', float]
print(conn.recv_bytes()) # => b'hello'
arr = array('i', [0, 0, 0, 0, 0])
print(conn.recv_bytes_into(arr)) #=> 8
print(arr)
# => array('i', [42, 1729, 0, 0, 0])

if __ name__ == "__ main__ " :
my_server = Process(target=server)
my_client = Process(target=client)
my_server.start()
my_client.start()

7.2.3. Пул процессов
Для управления фиксированным количеством процессов, когда
можно разделить работу на независимые части, распределяемые
между процессами, используется класс Pool. В этом случае возвра­
щаемые значения отдельных процессов объединяются и возвраща­
ются в виде списка.
Конструктор класса имеет такие аргументы, как количество про­
цессов и функция, подлежащая выполнению при запуске процесса
отдельной задачи. В качестве примера рассмотрим параллельную
обработку списка, элементы которого будут возводиться в квадрат.
Для этого используем функцию шар модуля multiprocessing. Синхро­
низацию основного процесса с процессами, выполняющими работу,
можно производить с помощью методов close и join. Такой подход
гарантирует выполнение завершающих операций по освобожде­
нию неиспользуемых ресурсов:
from multiprocessing import Process, \
current_process, cpu_count, Pool

def square(data):
return data * data
def start_process():
print(f'Старт процесса - '
f'{current_process().name}')

if __ name__ == '__ main__ ':
inputs = list(range(10))
print(f'Начальный список: {inputs}')

212

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

builtin_map = map(square, inputs)
print(f'Встроенная функция map: '
f' {list(builtin_map)}')

pool_size = cpu_count() * 2
print(f'Количество ядер у процессора: '
f' {cpu_count()}')
print(f'Количество запускаемых процессов: '
f' {pool_size}')
pool = Pool(
processes=pool_size,
initializer=start_process,
)
pool_map = pool.map(square, inputs)
pool.close()
pool.join()
print(f'Результат работы пула процессов:
f'{pool_map}')
I

I

I

Начальный список: [0, 1, 2,
4, 5, 6, 7, 8, 9]
Встроенная функция тар: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Количество ядер у процессора: 12
Количество запускаемых процессов: 24
Старт процесса - SpawnPoolWorker-1
Старт процесса - SpawnPoolWorker-2
Старт процесса - SpawnPoolWorker-5
• • •

Старт процесса - SpawnPoolWorker-24
Результат работы пула процессов: [0, 1, 4, 9, 16, 25, 36, 49,
64, 81]
I

I

I

7.3. Асинхронное программирование
Модуль асинхронного программирования asyncio предоставля­
ет инструменты для создания приложений, основанных на выпол­
нении параллельных вычислений с использованием сопрограмм.
То есть он использует подход на основе единственного потока
и единственного процесса, в котором отдельные части приложения
кооперируются для явного переключения задач в наиболее подходя­
щие для этого моменты времени. Аналогично предыдущим рассмо­
тренным модулям, модуль asyncio имеет конструкции для синхро­
низации выполняемого кода и блокировок ресурсов, а также свой
набор встроенных типов данных.
Центральное место в модуле asyncio занимает «цикл событий» —
объект первого класса, ответственный за эффективную обработку
событий ввода-вывода и изменение контекста приложения. Суще­
ствует несколько вариантов реализации цикла событий. В обычной
213

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

ситуации выбирается цикл событий по умолчанию, но у пользова­
теля имеется возможность выбора. Это особенно актуально при за­
пуске программы на различных операционных системах.
Взаимодействие приложения и цикла событий происходит сле­
дующим образом. Сначала выполняется регистрация кода, подле­
жащего асинхронному выполнению, после чего цикл событий вы­
полняет необходимые вызовы в коде приложения, если доступны
ресурсы.
Возврат управления циклу событий реализован с помощью со­
программ — конструкции языка, предназначенной для параллель­
ного выполнения операций [42]. При вызове функции сопрограммы
создается ее объект, что позволяет вызывающему коду выполнить
код этой функции, используя метод send. Приостановить выпол­
нение сопрограммы можно с помощью ключевого слова await
совместно с именем другой сопрограммы. На протяжении такой
паузы состояние сопрограммы сохраняется, что позволяет ей при
пробуждении продолжить выполнение с той точки, в которой оно
было прервано.
Чтобы функция выполнялась в асинхронном режиме и выступа­
ла в роли сопрограммы, перед ее определением необходимо доба­
вить ключевое слово async. При этом цикл событий модуля asyncio
может запустить сопрограмму различными способами. Самый про­
стой вызов у цикла события метода run_until_complete с непосред­
ственной передачей ему объекта сопрограммы:
import asyncio
async def test_async():
print(await func()) # Hello, async world!
async def func():
return "Hello, async world!"
if __ name__ == "__ main__ " :
loop = asyncio.get_event_loop()
loop.run_until_complete(test_async())

Еще один способ объявления сопрограммы — использование
функции-генератора, обернутой декоратором @asyncio.coroutine:
import asyncio
@asyncio.coroutine
def test_async():
print((yield from func())) # Hello, async world!
@asyncio.coroutine
def func():
return "Hello, async world!"
214

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

if __ name__ == "__ main__ " :
loop = asyncio.get_event_loop()
loop.run_until_complete(test_async())

Ниже приведен пример асинхронной работы двух функций:
import asyncio
async def first_function():
print("first_function start")
for it in range(7):
await asyncio.sleep(l.2)
print(f"first_function {it}")
async def second_function():
print("second_function start")
for it in range(10):
await asyncio.sleep(0.9)
print(f"second_function {it}")
if __ name__ == "__ main__ ":
loop = asyncio.get_event_loop()
my_functions = asyncio. wait( [first_function(),
second_function()])
loop.run_until_complete(my_functions)

7.3.1. Планирование времени вызова функций
Цикл событий модуля asyncio позволяет планировать вызовы
функций, основываясь на значении внутреннего таймера цикла.
Существует несколько способов планирования вызова функции.
— «На ближайшее время». Используется метод call_soon, ко­
торый позволяет запланировать вызов callback-функции на следу­
ющую итерацию цикла. В качестве первого аргумента функции
выступает ссылка на callback-функцию, а после следуют любые до­
полнительные позиционные аргументы.
— С задержкой во времени. В данном случае используется метод
call_later. В качестве первого аргумента метода выступает длитель­
ность задержки в секундах, следом идет ссылка на callback-функцию
и позиционные аргументы.
— На определенное время. Такой способ позволяет запланиро­
вать вызов callback-функции на определенное время (метод call_at).
Чтобы выбрать время для запланированного обратного вызова,
необходимо вести отсчет от внутреннего состояния монотонных
часов, то есть от момента «сейчас», это позволяет сделать метод
time цикла событий. Последовательность подаваемых аргументов
на вход метода call_at аналогична предыдущему способу.
Если необходимо в callback-функцию передать ключевые аргу­
менты, для этого можно использовать функцию partial из модуля
functools:
215

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

import asyncio
import functools
def callback(arg, kwarg='default'):
print(f'Вызов callback-функции arg= {arg}
f'kwarg = {kwarg}')

async def main(loop):
print('Регистрируем callback-функцию')
loop.call_soon(callback, 1)
wrapped = functools.partial(callback,
kwarg='A_A')
loop.call_soon(wrapped, "-_-")
await asyncio.sleep(0.2)
if __ name__ == "__ main__ " :
event_loop = asyncio.get_event_loop()
try:
print('Запуск цикла событий')
event_loop.run_until_complete(main(event_loop))
finally:
print('Остановка цикла событий')
event_loop.close()
I

I

I

Запуск цикла событий
Регистрируем callback-функцию
Вызов callback-функции arg= 1 kwarg = default
Вызов callback-функции arg= -_- kwarg = Л_Л
Остановка цикла событий
I

I

I

7.3.2. Асинхронное получение результатов

Для асинхронного извлечения результатов из сопрограмм мож­
но использовать класс Future (фьючерс), который сам действует
подобно сопрограмме. Именно поэтому к фьючерсам применимы
любые приемы, используемые для ожидания завершения сопро­
граммы. То есть экземпляр класса Future можно использовать вме­
сте с ключевым словом await.
При этом экземпляры класса Future могут не только работать
как сопрограммы, но и запускать callback-функции при завершении
работы. Перед извлечением результата из экземпляра класса Future
у него должен вызываться метод set_result с передачей ему резуль­
тирующего значения:
import asyncio
import functools
def callback(future, arg):
print(f'{arg}: результат future = '
f' {future.result()}')

216

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

async def register_callbacks(all_done):
print('Регистрируем callback-функцию для '
' future')
all_done.add_done_callback(functools.partial(callback,
arg=l))
all_done.add_done_callback(functools.partial(callback,
arg=2))
async def main(all_done):
await register_callbacks(all_done)
print('Установка результирующего '
'значения future')
all_done.set_result({"-_-": 10, "Л_Л": 20})
if __ name__ == "__ main__ ":
event_loop = asyncio.get_event_loop()
try:
all_done = asyncio.Future()
event_loop.run_until_complete(main(all_done))
finally:
print(f"Основной процесс: "
f" {all_done.result()}")
event_loop.close()
# 1: результат future =
10, ,л_л': 20}
# 2: результат future =
10, 'л_л': 20}
# Основной процесс: {': 10, 'л_л': 20}

7.3.3. Параллельное выполнение задач
Задачи (Task) — один из основных способов взаимодействия
с циклом событий. Они являются обертками над сопрограммами
и отслеживают момент их завершения. Так как задачи являются
производным классом от Future, то наследуют часть его функцио­
нальности, а именно:
— другие сопрограммы могут ожидать завершения задач;
— каждая задача имеет результат, который может быть извле­
чен после ее завершения.
Чтобы создать экземпляр класса Task, необходимо у объекта
цикла события вызвать метод create_task. Результирующая задача
будет выполняться как часть параллельных операций, управляемых
циклом событий, до тех пор, пока выполняется цикл и сопрограмма
не возвращает управление.
До момента завершения задачи ее можно отменить, вызвав у эк­
земпляра класса метод cancel и тем самым сгенерировав исключе­
ние CancelledError, которое будет необходимо обработать.
Давайте приведем пример работы с задачами:
import asyncio
async def task_test():
print('Выполняем задачу')
return (30,
[4.6, "o_0"])
217

Больше книг по языку Python по ссылке https://coderbooks.ru/books/python/

async def main(loop):
print('Объявляем задачу')
task = loop.create_task(task_test())
print(f'Ожидание выполнения задачи: {task}')
return_value = await task
print(f'Завершение задачи: {task}')
print(f‘Результат работы задачи: '
f'{return_value}')
if __ name__ == "__ main__ " :
event_loop = asyncio.get_event_loop()
try:
event_loop.run_until_complete(main(event_loop))
finally:
event_loop.close()
I

I

I

Объявляем задачу
Ожидание выполнения задачи: