/
Автор: Ильин Е.В.
Теги: языки программирования компьютерные технологии компьютерное моделирование язык программирования python инженерные задачи
ISBN: 978-5-9775-2118-5
Год: 2026
Текст
Евгений Ильин
самоучитель
ДЛЯ ИНЖЕНЕРНЫХ ЗАДАЧ
Санкт-Петербург
« БХВ-Петербург»
2026
УДК
ББК
004.43
32.973.26-018.1
И46
Ильин Е. В.
И46
Python
672
для инженерных задач.
-
СПб.: БХВ-Петербург,
2026. -
с.: ил.
ISBN 978-5-9775-2118-5
Книга предназначена для изучения языка
Python
с ориентацией на использова
ние его в инженерных и научных вычислениях, начиная с установки интерпрета
тора и изучения основ языка и до применения специализированных библиотек.
Основные темы касаются встроенных типов языка, функций и аннотации типов,
особенностей динамической типизации, форматирования и обработки текста, в том
числе с использованием регулярных выражений, работы с файлами. Подробно рас
сматриваются основные идеи объектно-ориентированного программирования и его
особенности в
Python.
Также затрагиваются темы обработки исключений, тестиро
вания приложений, описываются некоторые модули из стандартной библиотеки, а
также множество сторонних библиотек, в частности,
вычислений,
Pandas
NumPy для математических
Matplotlib для построения
для обработки табличных данных,
различных видов графиков, библиотеки для работы с различными форматами фай
лов. Рассматриваются такие инструменты, как
IPython
и
JupyterLab,
применяемые
в научных и инженерных областях.
Д1я студентов инженерных специа1ьностей.
а также для начинающих изучать язык
ББК
УДК 004.43
32.973.26-018.1
Группа подготовки издания:
Руководитель проекта
Олег Сивченко
Зав. редакцией
Люд.1111.1а Гау.1ь
Редактор
Григоршi Добин
Компьютерная верстка
Ната1ьи Смирновой
Корректор
Свет.ина Крутоярова
Дизайн обложки
Зои Канторович
Подписано в печать
05.11.25.
Формат 70х100 1 / 16 . Печать офсетная. Усл. печ. л. 54, 18.
Тираж 1300 экз. Заказ № 16035.
"БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул.,
20.
Отпечатано с готового оригинал-макета
ООО "Принт-М",
ISBN 978-5-9775-2118-5
142300,
М.О., г. Чехов, ул. Полиграфистов, д.
© ООО
1
"БХВ",
2026
©Оформление.ООО "БХВ-Петербург",
2026
Оглавление
Предисловие ...................................................................................................................
13
13
Структура книги .............................................................................................................. 13
Благодарности ................................................................................................................. 16
Для кого эта книга? .........................................................................................................
Введение ..........................................................................................................................
17
Python ............................................................................................... 17
Области применения Python .......................................................................................... 18
Зачем Python инженеру? ................................................................................................. 20
Типы языков программирования ................................................................................... 22
Компилируемые языки программирования ........................................................... 22
Интерпретируемые языки программирования ...................................................... 24
Языки программирования, компилируемые в байт-код ....................................... 25
Исходные коды к книге .................................................................................................. 27
Общие сведения о
ЧАСТЬ
1. БАЗОВЫЕ
-ГЛАВА
ПОНЯТИЯ И ВСТРОЕННЫЕ ТИПЫ .............................................. 29
1-
Первое знакомство с
Python ........................................................................................ 31
под
Windows ......................................................... 31
37
39
......
...............................................................................................
переменных
Создание
Заключение ...................................................................................................................... 43
У станов ка интерпретатора
He\lo, world!
Python
Работаем в интерактивном режиме .........................................................
-ГЛАВА2Простейшие типы и математика в
Python ............................................................... 45
................................................................................................ 45
46
Числа с плавающей точкой ............................................................................................ 4 7
Комплексные числа ......................................................................................................... 49
Логический (булев) тип переменных ............................................................................ 52
Объект None ..................................................................................................................... 53
Коротко о терминологии
Целые числа .....................................................................................................................
Оглавление
4
........................................................................................... 54
57
Инструкции присваивания ...................................................................................... 5 8
Математические функции и модуль math .. ............................................................ 59
Модуль cmath ........................................................................................................... 65
Заключение ...................................................................................................................... 66
Математические операторы
Приоритет операторов .............................................................................................
-ГЛАВА3Пишем скрипты на
Python .......................................................................................... 68
Создание скриптов ..........................................................................................................
68
Выполнение скриптов ..................................................................................................... 70
Комментарии и указание кодировки файла скрипта
................................................... 72
if ... elif ... else ........................................................................... 74
Переносы строк ............................................................................................................... 79
Выражение if ... else ........................................................................................................ 80
Цикл while ........................................................................................................................ 81
Оператор := ...................................................................................................................... 84
Инструкция assert ........................................................................................................... 86
Python Enhancement Proposals (РЕР) .............................................................................. 87
Заключение ...................................................................................................................... 88
Инструкция ветвления
-ГЛАВА4-
Списки, кортежи и массивы
....................................................................................... 90
Способы хранения данных .............................................................................................
90
Массивы .................................................................................................................... 90
Списки ....................................................................................................................... 92
Кортежи .................................................................................................................... 93
Создание списков ............................................................................................................ 94
Создание кортежей ......................................................................................................... 95
Создание массивов .......................................................................................................... 96
Преобразование списков, кортежей и массивов друг в друга .................................... 98
Доступ к элементам по индексу .................................................................................... 99
Срезы .............................................................................................................................. 101
Выполнение присваивания для сложных объектов. Операторы is и is not .. ............ 104
Операторы in и not in .................................................................................................... 109
Распаковка элементов коллекций ................................................................................ 109
Основные методы классов list, tuple и array ............................................................... 111
Заключение .................................................................................................................... 118
-ГЛАВА5-
Перебор элементов коллекций
... in ...................................................................................................... 119
списков с помощью инструкцииfоr ... in .................................................. 123
Инструкция for
Создание
................................................................................. 119
Оглавление
5
Создание последовательности целых чисел. Класс
range ......................................... 125
enumerate .................................................. 127
Параллельный перебор элементов из нескольких коллекций. Класс zip ................. 128
Заключение .................................................................................................................... 132
Перебор элементов с нумерацией. Класс
-ГЛАВА6-
Словари .........................................................................................................................
133
Что такое «словари» и зачем они нужны?
.................................................................. 133
........................................................................................................ 134
Основные операции со словарями ............................................................................... 136
Ограничения на типы ключей ...................................................................................... 140
Обход элементов словаря с помощью цикла/оr ........................................................ 141
Заключение .................................................................................................................... 143
Создание словарей
-ГЛАВА
7-
Множества ....................................................................................................................
144
Что такое множества и зачем они нужны?
................................................................. 144
144
Создание неизменяемых множеств ............................................................................. 146
Основные операции над множествами ....................................................................... 146
Методы и операторы классов set иfrozenset ............................................................... 147
Заключение .................................................................................................................... 152
Создание множеств .......................................................................................................
-ГЛАВА8-
Строки ...........................................................................................................................
153
Создание строк ..............................................................................................................
154
Многострочные литералы ............................................................................................ 154
Вставка символов Unicode ........................................................................................... 158
«Сырые» строки ............................................................................................................ 159
Создание строкового представления чисел и других объектов ................................ 160
Базовые операции над строками .................................................................................. 160
Некоторые методы класса str ....................................................................................... 162
Заключение .................................................................................................................... 169
-ГЛАВА
9-
Форматирование строк ..............................................................................................
Использование оператора
170
% ........................................................................................ 171
Использование метода format() .................................................................................... 177
f-строки .......................................................................................................................... 181
Заключение .................................................................................................................... 187
Оглавление
6
ЧАСТЬ
11. ОСНОВНЫЕ подходы ................................................................................ 189
-ГЛАВА lО-
Функции ........................................................................................................................
191
Создание функций
........................................................................................................ 191
19 5
Именованные параметры функций ............................................................................. 197
Параметры со значениями по умолчанию .................................................................. 198
Функции с переменным числом позиционных параметров ...................................... 201
Функции с переменным числом именованных параметров ...................................... 202
Разделители параметров / и * ....................................................................................... 205
Функции и глобальные переменные ........................................................................... 208
Заключение .................................................................................................................... 211
«Утиная» типизация......................................................................................................
- ГЛАВА 11 Функции как объекты ................................................................................................
Функция
-
212
это тоже объект ........................................................................................
212
.................................................................................................... 214
Строки документации ................................................................................................... 218
Декораторы .................................................................................................................... 221
Заключение .................................................................................................................... 228
Анонимные функции
-ГЛАВА
12-
Модули и пакеты модулей .........................................................................................
229
Создание и импорт модулей
........................................................................................ 229
_file_ .......... 232
Пакеты модулей ............................................................................................................ 236
Заключение .................................................................................................................... 240
Выполнение кода модулей при импорте. Переменные _пате_ и
- ГЛАВА 13 Объектно-ориентированное проrраммирование. Создание классов ................
Что такое объектно-ориентированное программирование?
241
..................................... 241
Создание классов .......................................................................................................... 244
Видимость полей и методов классов ........................................................................... 248
Свойства ......................................................................................................................... 251
Поля класса .................................................................................................................... 252
Методы класса ............................................................................................................... 255
Статические методы ..................................................................................................... 257
Заключение .................................................................................................................... 259
Оглавление
-ГЛАВА
7
14-
Объектно-ориентированное программирование.
Наследование и полиморфизм .................................................................................. 260
Что такое наследование классов?
................................................................................ 260
260
Абстрактные базовые классы ....................................................................................... 267
Что такое полиморфизм? .............................................................................................. 273
Множественное наследование ..................................................................................... 273
Функции для определения родительских отношений классов. Класс object .......... 280
Заключение .................................................................................................................... 282
Наследование классов ...................................................................................................
-ГЛАВА
15-
«Магические» методы классов и перегрузка операторов ................................... 284
«Магические» методы классов
.................................................................................... 284
................................................................................ 286
Заключение .................................................................................................................... 296
Примеры перегрузки операторов
-ГЛАВА
16-
Сторонние библиотеки и инструменты для работы с ними ............................... 298
У станов ка пакетов с помощью
pip .............................................................................. 298
requirements.txt ............................................................................ 304
Обновление и удаление пакетов .................................................................................. 305
Заключение .................................................................................................................... 307
Файл зависимостей
-ГЛАВА
17-
Виртуальные окружения ........................................................................................... 308
Программа
venv ............................................................................................................. 308
........................................................................ 311
Программа Poetry ................................................................................................... 311
Создание проекта с помощью Poetry. Файл pyproject. toml ........................ 312
Создание виртуального окружения для проекта с помощью Poetry ......... 315
Менеджер пакетов и проектов uv ......................................................................... 319
Создание проекта с помощью uv .................................................................. 320
Создание виртуального окружения для проекта с помощью uv ............... 322
Заключение .................................................................................................................... 326
Работа с виртуальными окружениями
- ГЛАВА 18 Аннотации типов ......................................................................................................... 328
Проблемы динамической типизации ...........................................................................
328
329
Муру ................................................................................................ 331
Что такое «аннотации типов» и зачем они нужны? ...................................................
Знакомство с
Оглавление
8
.................................................................. 332
........................................................................................................ 338
Заключение .................................................................................................................... 342
Указание простейших типов и коллекций
Обобщенные типы
-ГЛАВА
19-
Обработка исключений
............................................................................................. 344
344
.................................................. 345
Перехват исключений ................................................................................................... 349
Пользовательские исключения. Наследование исключений .................................... 352
Конструкция try ... except ... else ... finally ................................................................ 358
Заключение .................................................................................................................... 363
Обработка ошибок без использования исключений ..................................................
Что такое исключения, как и зачем их ловить?
- ГЛАВА 20Запись и чтение файлов ............................................................................................. 364
364
with ............................................................................. 369
Чтение текстовых данных ............................................................................................ 3 71
Двоичные строки ........................................................................................................... 373
Запись и чтение двоичных данных .............................................................................. 378
Коротко о сериализации и десериализации ................................................................ 383
Заключение .................................................................................................................... 386
Открытие файла и запись текстовых данных .............................................................
Закрытие файлов. Инструкция
-ГЛАВА21Работа с файловой системой
..................................................................................... 387
Проблема формирования путей до файлов ................................................................. 387
os.path ...................................................... 388
pathlib ...................................................... 395
Создание, копирование, перемещение и удаление файлов и каталогов .................. 399
Создание пустых файлов ....................................................................................... 399
Создание каталогов ................................................................................................ 399
Копирование файлов ............................................................................................. 401
Копирование каталогов ......................................................................................... 403
Удаление файлов и каталогов ............................................................................... 404
Переименование и перемещение файлов и каталогов ........................................ 405
Заключение .................................................................................................................... 408
Формирование путей до файлов. Модуль
Формирование путей до файлов. Модуль
-ГЛАВА22Передача параметров через командную строку .................................................... 41 О
Зачем это надо?
............................................................................................................. 41 О
Разбор параметров командной строки без использования библиотек ..................... 412
Оглавление
9
Разбор командной строки с помощью модуля
Заключение
argparse ........................................... .415
.................................................................................................................... 425
-ГЛАВА23-
Регулярные выражения ............................................................................................. 427
Что такое «регулярные выражения» и когда их используют? .................................. 427
Символы подстановки
.................................................................................................. 428
............................................................................ 433
Инструкции группировки ............................................................................................. 434
Поиск и замена с помощью регулярных выражений ................................................ .440
Коротко про функции из модуля re ............................................................................. 444
Заключение .................................................................................................................... 444
Параметры регулярных выражений
-ГЛАВА
24-
Тестирование приложений ........................................................................................ 446
Зачем нужны тесты, и какие они бывают?
................................................................. 446
unittest .............................................................. 447
Добавим еще тесты ....................................................................................................... 453
Подготовка данных для тестов .................................................................................... 456
Способы запуска тестов ............................................................................................... 458
Тесты в строках документации .................................................................................... 460
Заключение .................................................................................................................... 464
Создание тестов с помощью модуля
ЧАСТЬ
111. PYTHON ДЛЯ НАУЧНЫХ ВЫЧИСЛЕНИЙ ................................................. 467
-ГЛАВА
25-
Массивы из библиотеки
N umPy ............................................................................... 469
Массивы
NumPy :........................................................................................................... 469
........................................................................................ 472
Основные операции над массивами ............................................................................ 4 77
Индексация, срезы и виды ............................................................................................ 480
Формы массивов ............................................................................................................ 484
Транслирование (broadcasting) ..................................................................................... 490
Булевы массивы и фильтрация элементов по условию ............................................. 492
Использование целочисленных массивов в качестве индексов ............................... 495
Заключение .................................................................................................................... 496
Способы создания массивов
-ГЛАВА
26-
Форматы файлов для хранения числовых данных .............................................. 498
Текстовые файлы, хранящие данные в столбцах ....................................................... 498
Работа с данными в формате
CSV ............................................................................... 504
Оглавление
10
Файлы форматов
NPY и NPZ ....................................................................................... 506
HDF5 ................................................................................................... 508
Создание файлов в формате HDF5 ....................................................................... 509
Сторонние приложения для работы с файлами формата HDF5 ........................ 511
Чтение файлов в формате HDF5 ........................................................................... 513
Другие форматы данных .............................................................................................. 514
Заключение .................................................................................................................... 515
Файлы формата
-ГЛАВА27-
Основы построения графиков с помощью библиотеки
Matplotlib ................... 517
Установка библиотеки и первые примеры графиков
................................................ 517
......................................................... 521
Способы задания цвета .......................................................................................... 522
Стили линий ........................................................................................................... 524
Маркеры .................................................................................................................. 525
Краткий способ задания внешнего вида кривых ................................................ 528
Несколько графиков в одних осях ............................................................................... 528
Добавление легенды ..................................................................................................... 530
Создание нескольких графиков в одном окне на разных осях ................................. 532
Настройка осей графика ............................................................................................... 535
Объектно-ориентированный подход к построению графиков ................................. 540
Заключение .................................................................................................................... 545
Настройка внешнего вида кривых на графиках
-ГЛАВА28Построение с помощью библиотеки
Matplotlib
более сложных графиков
...... 547
Диаграммы рассеяния ...................................................................................................
54 7
.................................................................... 550
Столбчатые диаграммы ................................................................................................ 553
Круговые диаграммы .................................................................................................... 558
Построение трехмерных графиков .............................................................................. 562
Линии уровня ................................................................................................................. 571
Отображение векторов ................................................................................................. 575
Заключение .................................................................................................................... 579
Графики в полярной системе координат
- ГЛАВА 29Знакомство с
Pandas ................................................................................................... 581
Установка библиотеки
Pandas ...................................................................................... 581
CSV .................................................................................... 582
Создание экземпляров класса DataFrame ................................................................... 589
Выбор элементов и фильтрация данных из DataFrame ............................................ 591
Обработка данных с помощью DataFrame .. ............................................................... 598
Группировка .................................................................................................................. 607
Заключение .................................................................................................................... 611
Чтение файлов в формате
Оглавление
-ГЛАВА
11
30-
Библиотека
SciPy:
решение сложных научных и инженерных задач
.............. 613
Физические константы и специальные математические функции ...........................
613
................................................................................................. 619
Заключение .................................................................................................................... 640
Преобразование Фурье
- ГЛАВА 31 Интерактивные среды
IPython
и
JupyterLab ........................................................ 642
IPython - более удобный REPL .................................................................................. 642
От IPython к JupyterLab ................................................................................................ 648
Заключение .................................................................................................................... 659
Заключение ко всей книге .........................................................................................
Литература
661
................................................................................................................... 662
Предметный указатель
.............................................................................................. 663
Предисловие
Сейчас
это один из самых популярных языков программирования. Благо
Python -
даря своей простоте, лаконичности и плавной кривой обучения
Python
получил рас
пространение во многих областях, включая разработку настольных приложений,
веб-сервисов, вспомогательных инструментов, а в последнее время закрепил свои
позиции в области обработки данных и искусственного интеллекта. Помимо этого
для
Python
написано огромное количество библиотек, позволяющих использовать
его в инженерной и научной деятельности.
Для кого эта книга?
Эта книга предназначена в первую очередь для тех, кто только начинает изучать
язык программирования
Python
и планирует на нем писать программы для инже
нерных и научных расчетов. Это могут быть студенты младших курсов, которые
уже имеют представление о программировании и, может быть, даже знают другие
языки, такие как С++ или
Java,
но с
Python
еще не работали. Книга также может
быть полезна инженерам, которые в своей работе хотят начать использовать язык
Python
для расчетов, анализа и обработки данных. Для чтения книги не требуется
предварительного знания языка
Python.
Структура книги
Книгу можно условно разделить на три части. Сначала (в главах
мимся с основами языка
(в главах
10-24)
Python,
1-9)
мы познако
его синтаксисом и основными типами. Затем
начнем использовать более сложные конструкции и модули из
стандартной библиотеки, поставляемой вместе с интерпретатором. Это базовые
знания, которыми должен обладать каждый программист на
области его интересов. В последней части книги (главы
тематические библиотеки, такие как
графиков
Matplotlib,
♦
1.
Часть
•
Глава
NumPy, SciPy,
Python, независимо от
25-31) мы рассмотрим ма
библиотеку для построения
библиотеку для обработки табличных данных
Pandas.
Базовые понятия и встроенные типы
1
посвящена первому взгляду на язык
Python -
процессу его установ
ки и выполнению простых команд, на которых уже можно показать особен
ности языка.
•
В главе
2
рассказывается о встроенных числовых типах и простейшей мате
матике. В ней описаны целочисленный тип, числа с плавающей точкой и
Предисловие
14
комплексные числа, булев тип. Коротко говорится о модулях math и cmath с
математическими функциями для работы с действительными и комплексны
ми числами. Изучив материал этой главы, вы уже сможете использовать
•
Python как
мощный калькулятор.
В главе
описываются способы оформления программы на языке
3
Python
в
виде файла скрипта, а также показан синтаксис некоторых базовых операто
ров языка (условный оператор и один из операторов для организации циклов).
•
Главы
4
и
5
посвящены контейнерам
спискам, кортежам (неизменяемым
-
спискам) и массивам. Это типы (особенно первые два из них), которые встре
чаются в большинстве скриптов на языке
Python.
К этим типам можно при
менять множество одинаковых операций, поэтому мы их рассматриваем вместе.
•
В главах
и
6
7 рассказывается
еще о двух видах контейнеров
словарях (ас
-
социативных массивах) и множествах.
•
В главах
8 и 9 описываются
строки, а также методы их обработки и формати
рования. Работа со строками
-
это одна из тех областей, где
Python
очень
удобно использовать. Эти главы завершают часть книги, посвященную стан
дартным базовым типам.
♦
Часть
•
II.
Главы
Основные подходы
1О
и
11
посвящены функциям. Мы рассмотрим способы объявления
функций с различными наборами параметров: позиционными и именованны
ми параметрами, значениями по умолчанию, а также функции с неограни
ченным числом параметров. Благодаря тому, что функции в
Python
являются
полноценными объектами, у нас появляется множество интересных возмож
ностей для взаимодействия с ними.
•
Глава
12 расскажет об
организации кода в модули, которые используются для
того чтобы сделать структуру приложения более логичной.
•
Главы с
13
по
15
граммирования в
посвящены особенностям объектно-ориентированного про
Python.
Это важные главы, поскольку
Python
является пол
ностью объектно-ориентированным языком. Стандартные классы мы будем
использовать с самых первых примеров, а эти главы описывают особенности
создания классов и работы с ними.
•
В главах
16
и
17
рассматриваются вопросы установки сторонних библиотек.
В них речь сначала пойдет про стандартный инструмент для установки, обнов
ления и удаления пакетов
-
pip.
Затем мы поговорим о проблеме, связанной с
тем, что для разных проектов могут требоваться библиотеки разных версий, и
узнаем, как эта проблема решается с помощью виртуальных окружений.
•
Глава
18
возвращает повествование к синтаксису языка
В ней речь пойдет об аннотации типов
-
Python
и функциям.
возможности указывать типы пе
ременных, в том числе в аргументах функций, а также об инструментах, ко
торые, опираясь на эту особенность языка, проверяют написанный код на на
личие потенциальных ошибок.
Предисловие
•
Глава
15
19
посвящена исключениям
языковому инструменту для обработки
-
нестандартных ситуаций.
•
В главах
20
и
21
речь пойдет о работе с файлами и файловой системой. Здесь
среди прочего мы рассмотрим не только текстовые, но и двоичные строки и
файлы. Затем познакомимся со способами формирования путей в файловой
системе, а также методами высокоуровневой работы с файлами: копированием,
переименованием и удалением файлов.
•
Г?ава
22
расскажет об обработке параметров командной строки. Это полезно,
когда требуется указать скрипту дополнительные параметры без изменения
текста программы. В этой главе речь также пойдет о стандартном модуле
argparse, который значительно облегчает разбор параметров командной строки.
•
В главе
23
мы снова вернемся к работе со строками. Но здесь мы сосредото
чимся на мощном инструменте, который используется для сопоставления
строк заданному шаблону и поиску определенного шаблона в строке,
регу
-
лярных выражениях.
•
Глава
24
завершает группу глав, посвященных стандартной библиотеке
В этой главе мы рассмотрим модуль
unittest,
Python.
предназначенный для написания ав
томатических тестов для функций и классов. Здесь также будет представлен
способ написания тестов в строках документации к функциям и классам.
♦
Часть
•
111. Python
Гтюва
25
для научных вычислений
открывает серию глав, посвященную научным вычислениям. В этой
главе мы познакомимся с библиотекой
NumPy,
стандартным инструментом при использовании
изучим класс массивов, который предоставляет
которая является де-факто
Python
NumPy.
для вычислений, и
Эти массивы позво
ляют писать компактный и сравнительно быстрый код для вычисления мате
матических выражений.
•
Гrюва
26
посвящена форматам файлов, используемых в научном сообществе
для обмена данными между приложениями. Здесь рассматриваются как про
стые текстовые форматы файлов, так и сложный двоичный формат
также коротко упоминаются форматы
•
NetCDF
и
HDF5,
а
Zarr.
Г?авы
27 и 28 расскажут о построении графиков с помощью библиотеки
Matplotlib. В этих главах будут рассмотрены способы построения как про
стых графиков вида у = J(x), так и трехмерных поверхностей, линий уровня,
гистограмм, графиков в полярной системе координат.
•
В главе
29
описана библиотека
Pandas,
предназначенная для работы с таблич
ными данными. Мы рассмотрим основные классы из этой библиотеки и спо
собы обработки и группировки данных на основе таблиц.
•
В главе
ний
-
30 мы познакомимся с еще одной библиотекой для научных вычисле
SciPy. Нас в этой библиотеке будут интересовать модули, предостав
ляющие большую базу физических констант, специальные функции (напри
мер, функции Бесселя) и модуль с функциями для преобразования Фурье.
16
Предисловие
•
Глава
31 -
заключительная. В ней сначала рассматривается
мощная консоль для выполнения команд
Python,
IPython -
более
а затем инструменты, разра
ботанные в рамках проекта
Jupyter, - в частности, блокноты Jupyter и среда
JupyterLab. Блокноты позволяют перемежать код на
только) с результатами расчетов - таблицами, графиками
для работы с блокнотами
языке
Python
(и не
и формулами. Эти инструменты активно используются в научном сообществе
при разработке алгоритмов и анализе данных.
Благодарности
Книга получилась большая, значительно превышающая тот объем, что планировал
ся изначально. Такой большой текст просто невозможно написать без ошибок. Од
нако несколько человек согласились прочитать ее «сырой)) вариант, чтобы указать
автору на допущенные ошибки: фактические, пунктуационные и орфографические,
неточные формулировки, а заодно предложить идеи по улучшению текста. Без них
эта книга была бы заметно хуже. Поэтому здесь хочется поблагодарить Михаила
Галузу, который присылал комментарии и исправления практически сразу, как
только получал очередную версию текста, Павла Серкова
сандра Шевченко, Кирилла Балунова
та
(https://t.me/pankrat_kazell),
держивал меня и в меру троллил.
(http://serkov.su), Алек
(https://github.com/godaygo) и Казла Панкра
который хоть и не читал книгу, но морально под
Введение
Общие сведения о
Язык программирования
_ван Россумом. В конце
Python
Python был создан нидерландским
1989 года он начал разрабатывать
программистом Гвидо
этот язык, будучи со
трудником Национального исследовательского института математики и компью
терных наук
(Centrum Wiskunde & Infonnatica, CWI) в
Амстердаме. Разработка язы
ка велась с оглядкой на язык АВС, созданный в том же институте. Кроме того, на
Python
повлияли языки программирования
версия языка
Python вышла
в
1991
Modula-2
и
Modula-3.
году.
Свое название язык получил в честь британской комик-группы
рая была образована в
до
2014
хотя
1969
программисты
Python
кото
произносится как «пайтон», и
его называют «питоном»,
сленг, несмотря на то, что на логотипе
Python
Monty Python,
году и просуществовала, хоть и с долгими перерывами,
года. Поэтому правильно название
многие
Первая публичная
надо иметь в виду, что это
Python нарисовано явно что-то змееподобное.
представляет собой высокоуровневый язык со строгой динамической типи
зацией. Высокоуровневость языка означает, что для решения задач вам не потре
буются глубокие знания устройства вашего компьютера и операционной системы
на уровне вызовов системных функций. Благодаря динамической типизации при
написании кода вы не обязаны (хотя и можете) указывать типы переменных, а сами
эти типы могут меняться в процессе выполнения программы. Строгая типизация
означает, что интерпретатор
Python
при несовместимости типов не станет за вас
пытаться угадать, что вы имели в виду, преобразовывая переменные одного типа к
другому, а будет генерировать ошибку.
Python
относится к интерпретируемым языкам программирования. Это значит, что
для выполнения программы у вас должен быть установлен интерпретатор
(скоро
Python
мы более подробно рассмотрим деление языков программирования по сте
пени их «компилируемости» ). Хотя существуют сторонние инструменты, которые
можно было бы назвать «компилятором
Python»,
мы не станем изучать эти инстру
менты, а будем говорить только об эталонной реализации языка
Python.
Преимуще
ством интерпретируемых языков, как правило, является тот факт, что текст про
граммы на них получается более компактным, а его написание происходит доста
точно быстро по сравнению с компилируемыми языками.
18
Введение
Язык
Python
Python
является
кроссплатформенным
языком,
то есть
интерпретаторы
существуют под различное аппаратное обеспечение (различные типы про
цессора) и под различные операционные системы. Это значит, что если вы не ис
пользуете в своей программе какие-то возможности, специфичные для конкретной
операционной системы, то ваша программа без каких-либо изменений должна за
пуститься и в другой операционной системе, для которой существует интерпрета
тор
Python требуемой
версии.
В последнее время обновленные версии интерпретатора
ностями выходят регулярно
Python
с новыми возмож
раз в год, обычно в октябре. В интервале между ре
-
лизами разработчики выпускают версии, содержащие только исправления ошибок.
Каждая версия имеет срок поддержки пять лет
-
в течение этого времени могут
выходить обновления с исправлениями ошибок.
Но, к сожалению, за быстроту написания программ на интерпретируемых языках
приходится расплачиваться скоростью их выполнения. Когда речь идет об эффек
тивности кода (времени его выполнения), приходится говорить очень аккуратно,
поскольку для каждого случая нужно измерять эффективность кода индивидуаль
но, а кроме того, для
Python
созданы библиотеки, позволяющие значительно уско
рить выполнение кода. Но если мы говорим только о классическом интерпретаторе
Python,
то «в среднем по больнице» код, написанный на
Python,
может выполняться
в разы и даже десятки раз медленнее аналогичного кода, написанного на компили
руемом языке. Возможно, для ваших задач скорость выполнения не окажется кри
тична. Например, заметит ли пользователь разницу, если ваша программа на
отработает за
300
мс вместо условных
1О
Python
мс, будь она написана на языке С? Но для
некоторых задач производительность может играть решающую роль, и тогда, воз
можно, будет иметь смысл использовать другой язык программирования или попы
таться повысить производительность с помощью сторонних библиотек и инстру
ментов. Например, в
программ на
Python:
[ 1]
описано множество способов повышения скорости работы
параллельные или распределенные вычисления, написание
части кода на компилируемом языке или использование вычислений на видеокар
тах. Например, самая часто задействуемая разработчиками на
для математических расчетов
Fortran,
библиотека
«под капотом» написана на языках С
и
что обеспечивает ей высокую скорость работы.
Области применения
Python
NumPy
Python
Python
является универсальным языком программирования, который можно ис
пользовать в самых разнообразных областях. По описанным ранее причинам, по
жалуй, не стоит его использовать в системном программировании и для написания
драйверов устройств, хотя существует проект
сать программы на
1
Python для
См. https://micropython.org.
MicroPython 1,
микроконтроллеров.
который позволяет пи
Введение
Python
19
хорошо зарекомендовал себя в качестве языка для разработки серверной
части веб-приложений. Для его применения в этой области существует огромное
количество библиотек и фреймворков различной степени сложности. Грань между
библиотекой и фреймворком достаточно размыта, но обычно под словом «фрейм
ворю> понимают более высокоуровневые библиотеки, позволяющие сравнительно
быстро создать веб-приложение из предоставляемых компонентов. Как правило,
фреймворки содержат готовые решения для регистрации и авторизации пользова
телей, работы с базами данных, генерации страниц сайтов, проверки на безопас
ность, рассылки
Flask4, FastAPI5
Python, удается
email
и т. д. Наиболее известные из них
-
это
Django2, Litestar3,
и др. Благодаря асинхронным операциям, реализованным в языке
достигать неплохой производительности при использовании этого
языка для создания веб-сервисов.
Хотя в последние годы разработчики скорее предпочитают создавать веб-сервисы,
которые взаимодействуют с посетителем через браузер,
Python
можно применять и
для подготовки классических настольных приложений, работающих на стороне
пользователя, а не где-то на сервере. В частности,
Python
может помочь при созда
нии оконного графического интерфейса. Среди стандартных библиотек, которые
прилагаются к интерпретатору
Python,
есть модуль tkinter, который позволяет соз
давать интерфейс на основе библиотеки
Tcl/Tk.
Для разработки более сложных
графических интерфейсов можно задействовать сторонние библиотеки, представ
ляющие собой обертки над крупными библиотеками, написанными на других язы
ках программирования.
pySide
Наиболее известные из них
-
это библиотеки
pyQt и
Qt), wxPython (про
библиотеки GTK) и др.
(обе являются прослойкой для работы с библиотекой
слойка для работы с
wxWidgets), PyGTK
И хотя, как было сказано ранее,
Python
(работает поверх
является интерпретируемым языком, и для
выполнения кода на нем требуется интерпретатор, которого может не оказаться у
обычного пользователя, имеется возможность с помощью сторонних инструментов
(например,
cx_Freeze, Pylnstaller) создавать для конечных пользователей исполняе
Windows это будут ехе-файлы). Тогда наличия у пользователя ин
терпретатора Python уже не потребуется. К сожалению, магии не произойдет, пол
мый файл (под
ноценной компиляции не будет, и скорость выполнения кода это не повысит, но, с
точки зрения пользователя, ему будет предоставлен обычный запускаемый файл с
дополнительными файлами, и он даже может не узнать, что на самом деле выпол
няется программа на языке
Python.
Интерпретатор в этом случае будет либо встроен
в ехе-файл, либо располагаться рядом в виде динамически загружаемой библиотеки.
Благодаря большому количеству библиотек, которые входят в состав стандартной
установки
Python
(принцип «батарейки прилагаются»),
2 См. https://www.djangoproject.com.
3
См. https://litestar.dev.
4
См. https://flask.palletsprojects.com/en/staЬ\e.
5
См. https://fastapi.tiangolo.com.
Python удобно
использовать
20
Введение
для написания небольших скриптов, предназначенных для обработки текстовых
данных или выполнения каких-либо рутинных задач. Это могут быть задачи, свя
занные с преобразованием форматов файлов, подсчета статистики на основе дан
ных из файлов, работа с изображениями и множество других применений.
Зачем
Python
Python
инженеру?
отлично подходит в качестве языка для написания скриптов, решающих
многие научные или инженерные задачи. Разумеется, не существует одного иде
ального языка программирования на все случаи жизни, и разработчику постоянно
приходится идти на компромиссы и выбирать между различными инструментами, с
помощью которых он может решить проблему. Во многом на выбор инструмента
влияет знание и опыт работы с тем или иным языком программирования. Именно
поэтому желательно
иметь
в своем
арсенале
знания о
языках
программирования
различных типов (компилируемых и интерпретируемых).
Иногда приходится выбирать между скоростью разработки и скоростью выполне
ния программы.
Python -
это в первую очередь язык для относительно быстрой
разработки, он позволяет писать меньше кода по сравнению с компилируемыми
языками. Во многих случаях, там, где в языках вроде С и С++ нужно писать цикл
или даже вложенные циклы, в языке
Python
можно использовать конструкции, ко
торые сведутся к одной или нескольким командам. Разумеется, магии не произой
дет и здесь, и циклы будут организованы «под капотом», но с точки зрения про
граммиста код окажется более компактным и лучше читаемым.
Разработчики языка
Python
очень аккуратно подходят к добавлению новых конст
рукций в язык, стараясь не вводить новые сущности без крайней на то необходимо
сти. Именно поэтому код на
Python
Python
хорошо читается, и даже те, кто глубоко с
не знакомы, глядя на код, могут примерно понять, что там происходит. Бо
лее того, если вы изучили какой-то один аспект языка, например, синтаксис для
доступа к элементам в списках, то с большой вероятностью вы эти знания сможете
применить и в других похожих ситуациях, так как тот же синтаксис будет исполь
зоваться для доступа к элементам массивов, кортежей и даже классов из сторонних
библиотек,
-
например, массивов
NumPy.
Если же скорость выполнения программы не будет вас устраивать, то
Python
пре
доставляет огромный выбор путей для улучшения производительности. В этом
случае вам уже требуется более внимательный анализ скорости работы программы
(профилирование) и оптимизация, но во многих случаях всё равно можно оставать
ся в рамках языка
На
Python.
основе уже упоминавшейся библиотеки
NumPy
построено множество других
библиотек, которые предполагают большие объемы вычислений. Поскольку для
таких задач часто требуется визуализация результатов, для
Python
были разработа
ны библиотеки, позволяющие строить двумерные и трехмерные графики различно
го вида. Наиболее известная и зрелая из них
-
это библиотека
Matplotlib,
о кото-
Введение
21
рой пойдет речь в главах
27
и
28,
но также есть и другие библиотеки, работающие
поверх
Matplotlib
или независимо от нее.
Python
science
получил популярность среди исследователей, в том числе в области
data-
(науке о данных) и машинного обучения, где требуется обрабатывать ог
ромные массивы данных. Для научных расчетов на
такое интересное приложение как
(в терминах
Jupyter
Jupyter,
Python
активно развивается
которое позволяет на одной странице
они называются блокнотами) смешивать блоки кода на
Python
(или других языках программирования) с последующим выводом результата вы
полнения этих блоков,
это может быть как текстовый вывод, так и графика,
-
формулы, таблицы. Благодаря этому такие блокноты можно использовать для де
монстрации работы алгоритмов на различных презентациях. На рис. В.1 показан
пример блокнота
~ - ~ -00.lpyr,,b
il + )( tl С1 ►
Х
8
Jupyter.
..;.
• - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ~ - - ~. .
0 ,+ Cod8
• OpilC'lln... 8 Python3(1pykemlll) Q ■
slaмl)
ilX_dfUl.pllrt(ti8e / i..t,
••-•it.._l .иt_tltl•(•c.,,_,.• )
P_s l11111J.иt_xt..l("~,1111:• )
1
u._slpl.иt_yi-,.,Ц•s{t)")
••-•lp1,нt_x1181(8,
19)
••-•i1ul,1r-1tl()
[]
·НЛШ J~
~
u
м
'
= = = = =
u
Врн4•. мс
Спе ктр последовательности видеоимпульсов
N- 1
S• (k) = "L., в. (n)e -;l•• , k = О, 1, ... , N - 1
•~•
1
ЦIК{r• • fftМlift(tft.(slp.,1))
• 111) ,«li( spкt"-8)
freq • fftJhHt(frtf~(slpl_len, dt))
f.pкtl'III_Uf
!J J:
freq_a111_1iIO • .,
,,..,._. . . _CiRI: • 1
fit_•p«tt'w" plt.f1CU"'t(fipl1~(f, :Щ
п_~t,__._,. • fi&_spкtn8.~_N!tiplot()
ax_s,кi,-_щ.,tot(f-i
/ lef,
1
spкtn._... )
••_.,кtn._... иt_tltl•{".tи1м1y...,_ cnкrp")
••-чж:t,__.,.иt _ xlDJ("'Чкror1, rrц")
••_IPtctNI_Щ.иtJl.Ь.l{"IS(f)I")
••_qiкt,,._. . . мt_alt.(fr.q_aJ.11_QQ.,
freq___&tl:)
n:_.,кt,,__.,.,vtd()
-~D.J.:E]ii;JJ.]
-6
-4
-2
О
2
4
б
ЧК'пlп., ГГц
Рис. В.1. Внешний вид блокнота
Jupyter
Кроме того, некоторые системы автоматизированного проектирования и моделиро
вания дают возможность использования языка
написания макросов, а также предоставляют
ных с помощью
Python.
Python для автоматизации задач API для обработки рассчитанных дан
Введение
22
В настоящий момент основными конкурентами языка
Python
в научных вычисле
ниях можно назвать язык и среду разработки МА TLAB компании
также языки
Julia и R (в
Math Works,
а
области статистики).
Типы языков программирования
Ранее уже было сказано, что
Python
относится к интерпретируемым языкам про
граммирования, и он был противопоставлен компилируемым языкам, таким как С,
С++ и др. В этом разделе мы чуть более подробно рассмотрим разделение языков
программирования по этому признаку, чтобы лучше понимать преимущества и ог
раничения каждой группы языков.
Компилируемые языки программирования
В конечном итоге, любая программа должна инициировать выполнение каких-то
команд центрального процессора
(Central Processing Unit, CPU)
на материнской
плате. Процессор ничего не знает о языках программирования, он выполняет толь
ко команды в формате двоичного кода. Все языки программирования созданы ис
ключительно для удобства разработчиков, которым предпочтительнее писать код в
виде текста, а не вводить двоичные коды, понятные процессору. К тому же, между
запускаемой программой и центральным процессором в качестве программной
прослойки находится операционная система.
Исходная программа
txt
Входные данные
----t~
1001
0011
Выходные данные
i-----+
Целевая программа
Рис. В.2. Принцип работы компилируемых языков программирования
Для перевода текста программы в код, понятный центральному процессору, пред
назначен компилятор (рис. В.2). Поскольку эта книга не является учебником по
построению компиляторов, здесь мы всё в этом плане сильно упрощаем. В боль-
23
Введение
шинстве случаев компилятор состоит из нескольких функциональных блоков (пре
процессор, компилятор, линковщик), но в рассматриваемом случае это не принци
пиально, а важно лишь то, что в результате компилятор создает двоичный файл, в
котором закодированы команды, которые могут быть выполнены на конкретном
процессоре и в конкретной операционной системе. На рис. В.2 это показано в виде
целевой программы.
Именно эту целевую программу разработчик передает пользователям, и именно с
ней пользователь взаимодействует. У такого подхода есть как преимущества, так и
недостатки.
Одно из главных преимуществ
-
это возможность компилятора оптимизировать
двоичный код под конкретное семейство процессоров или даже под конкретный
тип процессора. К тому же, современные компиляторы умеют анализировать ис
ходный текст программы и создавать, благодаря этому, еще более эффективную це
левую программу. Поэтому такие скомпилированные программы имеют наиболь
шую производительность.
Второе преимущество относится к процессу разработки. В процессе компиляции
можно выявлять большое количество потенциальных ошибок в коде программы и
сообщать об этом программисту. Некоторые языки, например,
Rust,
являются очень
требовательными к написанному коду и отказываются создавать двоичный файл,
если компилятор не уверен, что, например, работа с памятью осуществляется кор
ректно. Это позволяет на самом раннем этапе выявлять ошибки в исходном коде.
Третье преимущество компилируемых языков состоит в том, что пользователю
предоставляется только двоичный файл, и нет необходимости передавать ему ис
ходный код программы. Для коммерческих приложений этот факт часто является
обязательным условием.
Теперь поговорим о недостатках такого подхода. Если нам необходимо поддержи
вать работу приложения в разных не совместимых между собой операционных сис
темах, таких как
Windows, Linux, macOS
и других, то для каждой из них нам при
дется создавать свои двоичные файлы, а для этого понадобится иметь на компью
тере несколько установленных операционных систем и осуществлять компиляцию
в каждой из них. Помимо поддержки разных операционных систем, нужно будет
поддерживать разные архитектуры процессоров (х86, х86-64, АRМ и др.). Со вре
менем это становится достаточно обременительным процессом, поскольку для ка
ждой операционной системы и для каждой архитектуры процессора потребуется
иметь свою систему сборки.
Другим недостатком можно назвать скорость компиляции. Если для небольших
программ она не будет играть существенной роли, то для более крупных проектов
процесс компиляции может занимать минуты и даже часы, что существенно замед
ляет разработку и отладку приложений. Конечно, есть подходы для ускорения ком
пиляции за счет того, что программа разбивается на модули, и компиляция выпол
няется только для тех модулей, которые изменились с момента прошлой компиля-
Введение
24
ции, но всё равно этот процесс может быть достаточно медленным или требовать
специально выделенных серверов.
К компилируемым языкам относятся такие языки как С, С++,
Rust, Fortran, Go,
и множество других.
Swift, Delphi
Интерпретируемые языки программирования
Другой подход к выполнению программ предлагают интерпретируемые языки.
Идея их заключается в том, что для них нет компилятора в явном виде, который бы
создавал двоичный файл, который может выполняться непосредственно в операци
онной системе на конкретном центральном процессоре. Для выполнения приложе
ния, написанного на интерпретируемом языке, у пользователя должен быть уста
новлен интерпретатор для этого языка, и на вход такого интерпретатора пользо
ватель подает, помимо входных данных, еще и исходный текст программы (рис.
8.3).
Исходная программа
Выходные данные
Интерпретатор
Входные данные
Рис. В.З. Принцип работы интерпретируемых языков программирования
Интерпретатор разбивает текст программы на отдельные команды и выполняет их
последовательно. В большинстве случаев это намного менее эффективный подход с
точки зрения скорости выполнения приложения. Поэтому, как правило, такие язы
ки значительно медленнее их компилируемых собратьев. Понимая это, разработчи
ки интерпретируемых языков часто вводят в интерпретатор элемент компилируе
мости, что постепенно приближает их к языкам, компилируемым в байт-код, о ко
торых речь пойдет далее.
Главное преимущество интерпретируемых языков программирования
-
это ско
рость разработки приложений. Интерпретируемые языки часто являются динами
чески типизируемыми, что позволяет писать более компактный код. В процессе
работы с такими языками отсутствует полноценный этап компиляции (предвари
тельная проверка правильности синтаксиса кода все-таки имеется), благодаря чему
можно быстро пробовать различные алгоритмические идеи без ожидания сборки
приложения.
Другим преимуществом интерпретируемых языков можно назвать легкую перено
симость между разными системами. Один и тот же скрипт будет (в теории) одина
ково выполняться в разных операционных системах и на разном «железе», если в
программе не используются какие-то специфичные для конкретной операционной
системы или «железа» возможности.
Введение
25
Однако отсутствие этапа компиляции является одновременно и недостатком. По
мимо того, что это отрицательно сказывается на производительности,
мы теряем
возможность проверить код компилятором на наличие ошибок. Ошибки для интер
претируемых языков
всегда
выявляются уже
в процессе
выполнения
программы.
Именно поэтому для интерпретируемых языков особенно важно писать большое
количество тестов, пропускающих через программу различные комбинации вход
ных данных, чтобы проверить результат во всех особых случаях.
Еще один недостаток интерпретируемых языков
это обязательное требование,
-
чтобы у пользователя был установлен интерпретатор нужного языка. В некоторых
ситуациях это не является большой проблемой. Например, для пользователей
интерпретатор
Python
Linux
с очень большой вероятностью уже установлен в системе.
В то же время для пользователей
Windows
у разработчиков есть возможность соз
давать запускаемые файлы, которые включают в себя интерпретатор. Это, конечно,
увеличивает размер дистрибутива с программой и не сказывается положительно на
производительности,
но
пользователь
может даже
не догадываться,
что
какое-то
приложение на самом деле написано на интерпретируемом языке.
Дополнительным недостатком для коммерческого программного обеспечения явля
ется необходимость передавать пользователям исходный код программы на таком
языке. При этом программа становится беззащитной от пиратства. Кроме того, в
исходном тексте
могут содержаться алгоритмы, которые коммерческие компании
считают своим ноу-хау и не готовы их демонстрировать.
К интерпретируемым языкам обычно относят
Ruby, Perl, BASIC
Python,
МА TLAB,
R, JavaScript,
РНР,
и др.
Языки программирования, компилируемые в байт-код
Существует еще один подход к компилируем~сти, который занимает промежуточ
ную позицию между интерпретируемыми и компилируемыми языками (рис. В.4).
Более того, многие интерпретируемые языки для ускорения работы неявно исполь
зуют некоторые элементы описываемого далее подхода к выполнению программ.
Суть компиляции в байт-код заключается в том, что у нас есть компилятор, кото
рый преобразует исходный код в двоичный файл (на рис. В.4 обозначен как «про
межуточная программа»), который передается пользователю. Но этот двоичный
файл на самом деле не является полноценным запускаемым файлом с точки зрения
операционной системы. Такие файлы содержат не команды для операционной сис
темы и процессора, а команды для так называемой виртуальной машины.
У пользователя на компьютере должна быть установлена виртуальная машина, на
вход которой подается промежуточная программа и входные данные. Виртуальная
машина интерпретирует двоичную промежуточную программу и при необходимо
сти внутри себя может докомпилировать ее в команды процессора.
Такой подход объединят в себе преимущества двух предыдущих подходов. Во-первых,
у
нас есть этап компиляции,
на котором можно проверить код на потенциальные
Введение
26
ошибки. Код, выполняемый на виртуальной машине, намного быстрее, чем код в
чисто интерпретируемых языках, но в среднем на проценты или десятки процентов
может быть медленнее по сравнению с компилируемыми языками. Правда, при
этом первый запуск программы пользователем может быть сравнительно долгим,
потому что виртуальная машина должна проанализировать переданную ей проме
жуточную программу и собрать на основе ее полноценный двоичный код. Причем у
виртуальной машины есть всё необходимое, чтобы оптимизировать переданную
программу под конкретное аппаратное обеспечение, на котором производится за
пуск программы.
Исходная программа
txt
Промежуточная программа
1001
0011
Виртуальная
Входные даннь1е
Выходные данные
машина
Рис. В.4. Принцип работы языков программирования,
использующих промежуточный байт-код
Процесс компиляции в байт-код, как правило, происходит достаточно быстро, по
тому что здесь не проводятся сложные оптимизации, и исходный текст программы
достаточно прямолинейно транслируется в двоичный файл.
В конечном итоге пользователю не передается исходный код приложения, а только
двоичный файл. Но, как уже говорилось, для запуска такой программы у пользова
теля должна быть установлена виртуальная машина.
В качестве «классических» языков, которые используют такой подход с виртуаль
ной машиной, можно назвать семейство языков, созданных под виртуальную ма
шину
Java (Java Virtual Machine, JVM): Java, Kotlin, Scala, Clojure, Groovy. Другое
конкурируюшее семейство языков работает на виртуальной машине Microsoft
.NET. Сюда относятся языки С#, F#, VB.NET и др. Интересно, что есть альтерна
тивные реализации языка Python под эти виртуальные машины. Python, работаю
щий под JVM, называется Jython, а под платформу .NET IronPython. Но в этой
книге мы будем говорить только об эталонной реализации языка Python, написан
ной на языке С и называемой поэтому CPython.
Введение
27
Исходные коды к книге
Все исходные коды для этой книги доступны по ссылке
python-book.
https://github.com/Jenyay/
В этом git-репозитории примеры для каждой главы расположены в
отдельном каталоге. Когда в книге приводятся примеры программ, исходные коды
которых содержатся в репозитории, то в заголовке листинга указывается относи
тельный путь до соответствующего файла. В книге для удобства ссылок на тот или
иной пример из репозитория к именам файлов добавлено указание «Листинг» с его
последовательным номером в главе. Вот, например, так будет оформлен в книге
пример кода chapter_0З/example_01/hello.py:
Листинг
3.1. Chapter_OЗ/example_01/hello.py
text = "Привет,
print (text)
мир!"
-ЧАСТЬ
1-
БАЗОВЫЕ ПОНЯТИЯ
И ВСТРОЕННЫЕ ТИПЫ
- ГЛАВА
1-
Первое знакомство с
Установка интерпретатора
Python
Python
под
Windows
Как говорилось во введении, существуют разные реализации интерпретатора языка
Python. Например,
JPython - Python,
РуРу- это интерпретатор
Python,
написанный на языке
Python,
Java, IronPython, напи
написанный под виртуальную машину
санный под среду выполнения
в научном сообществе
популярен пакет программ
в себя интерпретатор
Python,
Microsoft.NET. Кроме того,
Anaconda, который включает
среды разработки и огромное количество библиотек, в основном научной
направленности. В этой книге будет подразумеваться, что используется эталонный
интерпретатор
Python,
который можно скачать с сайта
https://www.python.org.
Этот интерпретатор написан на языке С, поэтому среди разработчиков его называ
ют
CPython.
Предполагается, что примеры из этой книги будут выполняться имен
но в этой реализации языка
Мы
Python.
рассмотрим установку интерпретатора
Windows.
В операционных системах на основе
Python под операционную систему
Linux обычно Python уже установлен.
Будем считать, что вы уже скачали установщик
Python.
Скорее всего, файл уста
новщика будет называться python-3.13.2-amd64.exe или подобным образом, где циф
ры обозначают номер версии
Python,
а затем указана архитектура вашего процессо
ра. Далее подразумевается, что у пользователя в системе
Windows
есть права адми
нистратора для установки новых приложений.
Установка
Python
под
Windows
мало отличается от установки других программ,
когда можно последовательно нажимать кнопки
Next -
Next -
Next,
не задумы
ваясь о том, что происходит. Но мы все-таки об этом задумаемся, и рассмотрим,
какие опции установки предлагаются,
только начинаете осваивать
Python,
что лучше в установку включить, если вы
а что при этом лучше не устанавливать.
После запуска установщика откроется окно, показанное на рис.
1.1.
В заголовке
окна указана устанавливаемая версия. Здесь же предлагается выбор типа установки.
Можно запустить быструю установку, выбрав пункт
lnstall now,
но будет надежнее
перед установкой убедиться в том, что предлагаемые параметры установки нас уст
раивают, а для этого выбрать пункт
Customize installation.
Часть
32
1.
Базовые понятия и встроенные типы
Python 3.13.2 (64-Ьit) Setup
\;1-
х
lnstall Python 3.13.2
(64-Ьit)
Select lnstall Now t o install Python with default settings, or choose
Customize to еnаЫе or disaЫe features.
!nstall Now
C:\Users\jenyay\AppData\local\Programs\Python\Python.313
lncludes IDLE, pip and documentation
Creates shortcuts and file associations
➔ Cцstomize iпstallation
Choose location and features
python
for
windows
Рис
■ Use admin privi!eges when installing ру.ехе
◄•-----
■ A dd Q.Ython.exe to PATHi
1.1.
Начальное окно установщика
Python
под
Qзncel
Windows
В нижней части окна предлагаются две дополнительные опции:
♦
Use admin privileges when installing
ру.ехе
У вас может быть одновременно установлено несколько различных версий ин
терпретатора
Python.
Приложение ру . ехе позволяет через командную строку вы
брать, какую именно версию
Python
нужно запустить. Эта опция предусматри
вает использование прав администратора для установки приложения ру.ехе, что
бы это приложение было доступно всем пользователям системы. В противном
случае оно будет установлено только для того пользователя, под которым вы
в текущий момент вошли в систему. Выбор, устанавливать само приложение
ру.ехе или нет, будет вам предложен в следующем окне, если вы выберете вари
ант
♦
Customize installation.
Add python.exe to
Р А ТН
Эта опция нужна для добавления пути до установленного интерпретатора
Python
в переменную окружения РАТН. Это необходимо для того, чтобы при запуске ин
терпретатора
Python
через командную строку не нужно было указывать полный
путь до файла python. ехе, а достаточно было бы ввести команду python. Очень ре
комендуется установить этот флажок.
СОВЕТ
Если вы устанавливаете
указывают стрелки на рис.
После выбора варианта
Python впервые,
1.1-1.3.
то лучше включить те опции, на которые
Customize installation
откроется показанное на рис .
но, в котором можно выбрать, какие компоненты требуется установить.
1.2
ок
Глава
1.
r;i.
Первое знакомство с
Python
33
Python З.132 (64-bit) Setup
х
Optional Featu res
■ Qocumentation
lnstalls the Python documentation files.
■ oip
lnstalls pip, which can download and install other Python packages.
■ tcl/tk and lDLE
lnstalls tkinter and the IDLE development environment.
О Python !est suite
lnstalls the standard library test suite.
■ ру !auncher ■ for 011 users (requires admin privileges) ◄••---
Upgrades the global
'ру'
launcher from the previous version.
python
f )(
Рис
.!'Y_ext
~ack
windows
1.2.
9incel
Выбор дополнительных компонентов для установки
Коротко рассмотрим опции, предлагаемые в этом окне:
♦
Documentation
При выборе этого пункта на ваш компьютер будет установлена локальная вер
сия документации по языку
Python.
Обычно разработчики используют оnlinе
документацию, расположенную по адресу
https://docs.python.org,
но на всякий
случай при отсутствии доступа к Интернету иметь локальную версию докумен
тации полезно.
♦
pip
Это программа, с помощью которой устанавливаются дополнительные сторон
ние библиотеки для
Python.
Про
pip
мы более подробно поговорим в главе
16.
Устанавливать ее нужно обязательно.
♦
tcl/tk and 1D LE
tcl/tk -
это библиотека для построения графического пользовательского интер
фейса. К
Python
прилагается стандартный модуль tkinter, представляющий со
бой обертку над библиотекой
краской
Python.
синтаксиса
кода,
tcl/tk. IDLE -
позволяющий
это очень простой редактор с рас
запускать скрипты,
написанные
на
Для серьезной разработки этот редактор не очень подходит из-за своей
аскетичности, но при изучении языка он может стать неплохим выбором в каче
стве первого редактора, чтобы не пугать новичка обилием возможностей и на
строек, предлагающихся в более продвинутых редакторах.
♦
Python test suite
Эту опцию новичкам можно не выбирать, если вы хотите сэкономить место на
диске. При ее выборе в каталог LiЫtest/, расположенный внутри каталога уста-
Часть
34
новки
Python,
1.
Базовые понятия и встроенные типы
будут установлены тесты для стандартной библиотеки. Это необ
ходимо, если вы участвуете в разработке интерпретатора
Python
или его стан
дартной библиотеки. Тогда вам нужно будет периодически запускать тесты,
чтобы убедиться, что ваши изменения не сломали код, написанный до вас.
♦
ру
launcher
Эта опция предлагает установить приложение ру.ехе, о котором было сказано ранее.
♦
for all users (requires admin previleges)
Эта опция означает, что приложение ру.ехе нужно установить в систему таким
образом, чтобы оно было доступно всем пользователям системы. Обратите вни
мание, что в этом пункте речь идет только о приложении ру.ехе, а не об интер
претаторе
Python.
Определившись с опциями в окне, показанном на рис.
попадаем в следующее окно настроек (рис.
1.2,
нажимаем кнопку
Next
и
1.3 ).
~ Python З.132 (64-Ьit) Setup
х
Advanced Options
8 [1nstall
8
8
Python _ З.13 for all
user~
Associate files with Python (requires the 'ру' launcher)
Create shortcut.s for installed applications
◄-.(•----
■ Add Python to gnvironment variaЫe.s
◄-.(•----
■ frecompile standard library
◄-r.------
0
Download debugging .2}/mbols
О Download debug Ьinaries (requires VS 2017 ог later)
О Download free-threaded Ьinaries (experimental)
Customize install location
C:\Program
python
for
windows
Рис
1.3.
Files\PythonЗ 13
Browse
lnstall
~ack
Дополнительные настройки процесса установки интерпретатора
9ncel
Python
Здесь предлагаются следующие опции:
♦
Install Python 3.NN for all users
Если у вашего пользователя в системе имеются права администратора для уста
новки приложений, то рекомендуется установить этот флажок. Тогда интерпрета
тор
Python
будет установлен в каталог C:\Program Files\PythonNNN, где фрагмент
NNN определяет номер версии Python, и окажется доступен всем пользователям
операционной системы. Если убрать этот флажок, то вам будет предложено уста
новить интерпретатор в папку пользователя
-
в каталог С:\Usеrs\{имя_пользовате-
Глава
1.
Первое знакомство с
Python
35
ля}\AppData\Local\Programs\Python\PythonNNN. Согласитесь, что в таком случае най
ти интерпретатор при необходимости будет намного сложнее. Однако при уста
новке
Python только для
одного пользователя не требуются права администратора,
и каждый пользователь может устанавливать себе любое количество различных
версий
♦
Python.
Associate files with Python (requires the
'ру'
launcher)
♦ Если вы установите этот флажок, то в системный реестр
Windows
будет добав
лена ассоциация файлов *.ру и некоторых других с интерпретатором
Python,
что
бы при двойном щелчке на файле запускался выбранный скрипт. Тут уже дело
вкуса: кому-то удобно, чтобы по двойному щелчку файлы запускались с помо
щью интерпретатора
Python,
а кому-то хочется, чтобы по двойному щелчку
файл со скриптом открывался в его любимом текстовом редакторе. Для того,
чтобы эта опция стала доступна, нужно, чтобы в системе было установлено при
ложение ру.ехе, о котором речь шла ранее.
♦
Create shortcuts for installed application
Эта опция позволяет выбрать, нужно ли создавать ярлыки для установленных
приложений: интерпретатора,
IDLE,
документации. Вряд ли существуют веские
причины от этого отказываться.
♦
Add Python to environment variaЫes
Если выбрана эта опция, то инсталлятор обновит переменную окружения РАТН
(в эту переменную будет добавлен полный путь до каталога, содержащего файл
интерпретатора python.exe).
♦
Precompile standard library
Если выбрана эта опция, то в конце установки модули стандартной библиотеки
будут преобразованы в байт-код, что позволит быстрее их импортировать при
первом использовании. Полезная опция.
♦
Download debugging symbols
и
Download debug blnaries
Эти опции предназначены для разработчиков интерпретатора
ных библиотек. Если вы только изучаете
♦
Download free-threaded
Ьinaries
Python,
Python
и стандарт
вам эти файлы не понадобятся.
(experimental)
На момент подготовки этой книги в интерпретаторе
Python
есть ограничение,
не позволяющее полноценно использовать преимущества многопоточного про
граммирования, задействуя несколько ядер процессора одновременно. В
Python 3.13
начались работы по устранению этого ограничения, но пока еще они далеки от
завершения. Эта опция предназначена для тех, кто разрабатывает низкоуровне
вые библиотеки для
Python
и хочет проверить, как они будут вести себя с буду
щими наработками в этом направлении. Для остальных пользователей включать
эту опцию не рекомендуется.
♦
В расположенном в нижней части окна поле ввода
можете поменять путь до каталога установки
нию вас почему-то не устраивает.
Customize install location вы
Python, если значение по умолча
Часть
36
1.
Выбрав необходимые настройки, нажмите кнопку
установки (рис.
1.4)
Базовые понятия и встроенные типы
Install,
и будет запущен процесс
с выводом информации о текущем ее шаге.
~ Python 3.132 (64-Ыt) Setup
х
Setup Progress
lnstalling:
Python 3.13.2
Tcl/Тk
Support
(64-Ьit)
python
windows
fQr
~ncel
Рис
1.4.
Процесс установки интерпретатора
Python
~ Python З.132 (64-bit) Setup
х
Setup was successful
New to Python? Start with the online tutorial and
documentation. At your terminal, type " ру" to launch Python,
or search for Python in your Start menu.
See what's new in this release, or find more info
Python on Windows.
aЬout
using
python
for
windows
Рис
1.5.
Окно с сообщением об успешной установке интерпретатора
Python
Глава
1.
Первое знакомство с
Python
37
После успешного окончания установки откроется окно, показанное на рис.
1.5. Лю
бопытные пользователи могут перейти по предложенным в этом окне ссылкам и
прочесть предлагаемые страницы документации, а остальным ничего не остается,
кроме как нажать единственную кнопку
Close
и считать, что
Python установлен.
Hello, world!
Работаем в интерактивном режиме
Установив интерпретатор
Python,
давайте попробуем написать в нем какую-нибудь
очень простую программу. У программистов есть древняя традиция
-
первая про
грамма, написанная на новом языке программирования, должна выводить на экран
приветствие миру,
обычно это строка
-
Hello, world!
Для этого мы запустим
Python
в интерактивном режиме.
Интерпретатор
Python
может работать в двух режимах: интерактивном и в режиме
выполнения скриптов. Интерактивный режим подразумевает, что мы вводим по
одной строке кода, а интерпретатор их сразу выполняет и тут же выводит результат
на экран. Среду, которая позволяет работать в таком режиме, называют RЕРL
это сокращение от слов
Read-Eval-Print Loop,
то есть цикл «чтение-вычисление
вывод». Многие короткие примеры в этой книге мы будем выполнять в режиме
REPL.
Такой режим удобен, когда мы хотим попробовать на очень маленьком ку
сочке кода, как работает та или иная команда, и не хотим для этого создавать от
дельный файл скрипта.
Второй режим подразумевает, что мы сначала пишем полностью программу в виде
текстового файла (скрипта), который обычно имеет расширение ру, затем указыва
ем интерпретатору
Python
путь до этого файла, и интерпретатор начинает последо
вательно выполнять команды, записанные в этом файле. Обычно используют имен
но этот режим, но для небольших экспериментов удобнее будет использовать интер
активный.
Если вы работаете под
Windows, то можете запустить Python в интерактивном ре
(REPL) несколькими способами. Во-первых, установщик Python создает яр
лык Python 3.NN (64-Ьit) в папке Python 3.NN главного меню, где NN число,
зависящее от номера версии Python. Во-вторых, вы можете нажать комбинацию
клавиш <Win>+<R>, в открывшемся диалоговом окне ввести команду python и на
жать клавишу <Enter>. И третий способ- это запустить командную строку, в ней
набрать команду python и нажать клавишу <Enter>.
жиме
В результате должно появиться консольное окно
на рис.
1.6.
-
наподобие того, что показано
В этом окне вы можете увидеть номер версии
Python,
некоторую ин
формацию о версии интерпретатора, а затем приглашение к вводу команд:
>».
Введем после символов приглашения строку:
print("Hello, world!")
После нажатия клавиши
полнения этой команды
Python в качестве результата
выведет строку Hello, world!, как показано на рис. 1.7.
<Enter>
интерпретатор
вы
Часть
38
~ Pyt l10n 3.13 (6 4 -bit)
11
help 11
,
'copy1·igl1t 11
1
Рис.
Базовые понятия и встроенные типы
о
х
Ц
Python 3.13.2 (tags/v3.13.2:Цf8bb39, Feb
Туре
1.
,
~ Pyl11on 3-13 (6 4 - Ьit)
2025, 15:23:Ц8) [MSC v 19Ц2 бЦ Ыt (АМ(JбЦ)] 011 >111132
'credits 11 or "license 1' fo1· more
1
1.6. REPL:
интерпретатор
infor·н1,1t1011
Python
в интерактивном режиме
о
х
Ц
Pytho11 З 13 2 (tags/v3.1З.2:Цf8bb39, Feb
1. 7.
х
2025, 15:23:Ц8) [MSC v l'Щ2 ьц !Jit (АМ[JбЦ)J оп -,i11:,•
''help 1', "copyright", 11 credits" or "license" for more i11form, 1t
p1·i11t( "Не llo, woгld 1")
Не l lo, wor ld !
Туре
Рис.
у
io11
Результат выполнения первой программы на
Python
Таким образом мы поддержали «программерскую» традицию, но давайте посмотрим,
какую информацию о языке
Python
мы можем извлечь из этой единственной строки.
Во-первых, мы видим, что текстовые строки в
Python
обрамляются двойными ка
вычками. Мы могли бы использовать одинарные кавычки:
print ( 'Hello , world ! ')
и результат был бы тот же самый
-
строки в одинарных и двойных кавычках ни
чем не различаются. Когда мы будем подробно изучать строки в главе
8,
то увидим,
почему так сделано, и какие еще существуют способы создания строк.
Во-вторых, как можно догадаться, для вывода текста в консоль служит функция
print (). Она является встроенной
(builtin).
Это означает, что для использования та
кой функции ее не требуется импортировать из модуля какой-либо библиотеки
(модулям посвящена глава
енных функций в
Python
12,
но мы начнем с ними работать еще раньше). Встро
достаточно мало, большинство функций размещаются в
различных стандартных библиотеках, которые предварительно нужно импортиро
вать. Список встроенных функций можно найти на странице документации 1.
В-третьих, мы видим, что для вызова функций применяется нотация, характерная
для многих других языков программирования, когда после имени функции в круг
лых скобках указывают ее параметры.
1 См.
https://docs.python.org/3/library/functions.html.
Глава
1.
Первое знакомство с
Python
39
И, наконец, можно заметить, что, в отличие от многих других языков, в конце коман
ды не стоит точка с запятой. В нашем однострочном коде это не заметно, но идея в
том, что если языки типа С, С++,
в качестве разделителя команд ис
пользуют точку с запятой, то в
применяется перевод строки, и по
этому каждая команда в
Java и другие
Python для этого
Python должна располагаться на отдельной
Поздравляю вас, мы разобрали первую программу на языке
строке.
Python.
Можете смело
добавлять в свое резюме строчку о том, что вы имеете опыт программирования на
Python :-).
Но мы все-таки на этом не остановимся и продолжим изучать язык.
Создание переменных
Примеры из этого раздела мы по-прежнему станем запускать в интерактивном ре
жиме. Итак, создадим несколько переменных, которые будут хранить значения раз
ных типов.
Начнем с целых чисел. Создадим переменную foo, которая будет хранить целочис
ленное значение
42:
»> foo = 42
Здесь и далее, чтобы показать, что команда вводится в интерактивном режиме,
в начале строки будут добавляться символы
»>,
которые автоматически выводятся
в консоли, ожидая ввода очередной команды, так что самостоятельно их вводить не
надо. Эти символы отображаются, чтобы отличать вводимые команды от результа
та их выполнения.
Чтобы убедиться, что переменной foo действительно присвоено значение
42,
у нас
есть несколько способов. Мы можем воспользоваться уже знакомой нам функцией
print () и вывести значение этой переменной в консоль:
»> print(foo)
42
Кроме того, в интерактивном режиме для вывода значения переменной в консоль
мы можем обойтись без функции print () -
достаточно написать имя переменной
в качестве команды:
»> foo
42
Такой способ в режиме выполнения скриптов работать не будет
-
для вывода тек
ста в консоль в этом режиме нужно будет всегда использовать функцию print ().
На что еще следует обратить внимание в такой простой конструкции как присваи
вание? В первую очередь на то, что мы не объявляем тип переменных. В главе
18
будет сказано, что на самом деле тип указать можно, но он не будет влиять на вы
полнение программы, а может лишь выполнять роль подсказок. То, что мы не ука
зываем тип, не значит, что у переменных нет типа. Он очень даже есть, и, более того,
Python
является языком со строгой типизацией, о чем мы периодически будем
Часть
40
1.
Базовые понятия и встроенные типы
вспоминать на протяжении всей книги. Тип переменной определяется в момент
присваивания ей значения и зависит от того, какое значение присваивается. Для
того, чтобы узнать, какой тип у переменной foo, можно воспользоваться встроен
ной функцией t уре ( ) :
>>> print(type(foo))
В результате будет выведена следующая строка:
<class 'int'>
В интерактивном режиме мы можем не использовать функцию print ():
»> type(foo)
<class 'int'>
Функция type () возвращает некоторое значение (тип переменной), а поскольку это
значение не присваивается никакой переменной, то в интерактивном режиме (и толь
ко в нем) возвращаемое значение также будет выведено в консоль.
Полученная здесь строка говорит о том, что переменная
класса int. Язык
Python
foo является экземпляром
является объектно-ориентированным языком, где каждый
тип является классом. В дальнейшем слова «тиш> и «класс» мы будем использовать
как синонимы. Про объектно-ориентированное программирование и классы более
подробно речь пойдет в главах с
13
по
15,
а пока, если очень коротко, то класс
-
это некоторая сущность, которая содержит в себе описание каких-то данных и
функций (их еще называют мemoд(Ll\,fu). На основе классов создаются объекты, ко
торые хранятся в оперативной памяти. Класс определяет, какие действия можно
производить с объектами, созданными на основе этого класса.
Более подробно на встроенных числовых типах мы сосредоточимся в главе
ка посмотрим, какие еще числовые типы предоставляет язык
Python.
2,
а по
Следующий
пример показывает создание переменной, хранящей значение с плавающей точкой:
>>> pi = 3.14159265
»> type (pi)
<class 'float'>
Про другие форматы записи чисел с плавающей точкой будет сказано в следующей
главе, а сейчас важно отметить, что такие переменные имеют тип
float.
Для создания комплексных чисел предназначена нотация с использованием симво
ла мнимой единицы
-
j, перед которым без пробела записано число (целое или с
плавающей точкой). Такие переменные имеют тип complex:
>>> bar = 10.5+4j
»> type (bar)
<class 'complex'>
Теперь создадим переменные строкового типа. Как уже говорилось ранее, для соз
дания строк используются либо одинарные, либо двойные кавычки.
Глава
1.
Первое знакомство с
Python
41
Это показано в следующем примере:
>>> spam = 'Hello, world 1 '
»> type (spam)
<class 'str'>
>>> eggs = "Привет, мир!"
»> type (eggs)
<class 'str'>
Из этого примера видно, что строковые переменные относятся к классу str -
зависимо от того, с помощью каких кавычек они были созданы. В главе
8,
не
посвя
щенной строкам, будут рассмотрены другие способы создания строк, в том числе
многострочные
строки
«multiline strings» ),
(не
очень
удачный
перевод
с
английского
термина
которые позволяют вводить строки, содержащие символы пере
хода на новую строку.
Также к встроенным типам относятся списки (класс list), кортежи (класс tuple),
словари (класс dict) и множества (класс set). В этой главе они не рассматривают
ся, но каждому из этих классов будет посвящена отдельная глава.
К именам переменных предъявляются следующие требования: они могут содер
жать только буквы, цифры и знак подчеркивания
«_»,
при этом имя переменной
не может начинаться с цифры. Регистр букв в имени переменной важен, то есть пе
ременные
foo, Foo
и
FOO -
это разные переменные, они могут существовать одно
временно и хранить разные значения.
Таким образом, допустимыми являются имена: frequency, _ frequency, frequency _,
frequency_max, frequency2, frequency_2, Frequency, FREQUENCY. Но нельзя исполь
зовать имена наподобие таких: 2frequency, frequency'
и т. п.
Помимо этого, в качестве имен переменных нельзя испол~зовать имена, совпадаю
щие с ключевыми словами языка
Python.
Таких ключевых слов немного: and, as,
assert, async, await, break, class, continue, def, del, elif, else, except, False,
finally, for, from, global, if, import, in, is,
lamЬda,
None, nonlocal, not, or, pass,
raise, return,True, try,while,with,yield.
Строго говоря, в имени переменной допустимо использовать не только латинские
буквы, но и буквы из других языков. Например, с точки зрения
код является корректным:
>>>
длина волны=
>>> длина_волны
0.3
»> л = 0.15
>» [\
0.15
»> ; ~ = 0.2
»> ; ~
0.2
0.3
Python
следующий
Часть
42
1.
Базовые понятия и встроенные типы
Однако такие имена давать не рекомендуется, и это считается плохим стилем про
граммирования.
Согласно существующим рекомендациям, определяющим, как должны выглядеть
имена переменных и
классов,
в именах переменных
следует использовать только
латинские буквы в нижнем регистре, знаки подчеркивания и цифры.
Если имя переменной состоит из нескольких слов, то их предлагается разделять
символами подчеркивания,
-
например:
freq_max,
а не
FreqMax
или
freqMax.
Имена классов рекомендуется начинать с заглавной буквы и не использовать под
черкивания, а каждое слово в имени начинать с заглавной буквы,
-
например:
FieldCalculator.
Константы, которые не должны меняться в процессе выполнения программы, часто
пишут заглавными буквами,
например: FREQ_МАХ, MIN _ VALUE и т. д.
-
Указанные рекомендации можно нарушать, если в этом есть необходимость, и это
поможет лучшему
пониманию кода,
-
например,
когда в
коде используются
ка
кие-то общепринятые обозначения физических величин, которые всегда пишутся
заглавными или строчными буквами. Например, в библиотеке
будем
говорить
в
главе
30)
содержатся
константы,
SciPy
которые
(про нее мы
имеют
имена:
epsilon_O (электрическая постоянная), G (гравитационная постоянная) и т. д. Да и
стандартные классы, которые мы уже видели ранее (float, int, complex, list и пр.)
тоже не следуют этим рекомендациям, но тут скорее работают исторические при
чины, поскольку имена более новых классов следуют приведенным рекомендациям:
Path, Formatter, ValueError
И др.
Переменным нужно давать осмысленные имена, чтобы читатель вашего кода сразу
понимал, для чего предназначена та или иная переменная. При этом не рекоменду
ется использовать однобуквенные переменные вроде а, ь, х, у, если только такие
имена не являются общепринятыми обозначениями в той области, где вы работае
те. Например, для целочисленных счетчиков часто используют имена i, j, k, n, m.
Если вы решаете задачу, связанную с физикой, то, скорее всего, вам будет удобнее,
если переменная с станет обозначать скорость света. Но чаще всего лучше давать
переменным более развернутые имена.
Для демонстрации каких-либо аспектов языка программирования, когда нет при
вязки к конкретной задаче, часто используют следующие ничего не значащие име
на переменных (их иногда называют метапеременнымu): foo, ьаr, baz, bat, bam,
eggs, spam. Последние два имени характерны для примеров на языке
Python -
они
отсылают к одному известному скетчу про спам от британской комик-группы
Monty Python,
в честь которой и получил свое название язык
Как уже говорилось,
Python
Python.
является языком с динамической типизацией, а это
значит, что переменной с одним и тем же именем мы последовательно можем при
сваивать значения разных типов, и тип этой переменной будет меняться. Таким об-
Глава
1.
Первое знакомство с
разом, на
Python
Python
43
вполне корректен следующий код, который бы вьпвал ошибку при
компиляции в языках со статической типизацией:
>>> foo = "Hello, world'"
»> type (foo)
<class 'str'>
»> foo = 42
»> type (foo)
<class 'int'>
>>> foo = 73.1+42.5j
»> type (foo)
<class 'complex'>
Динамическая типизация часто позволяет писать меньше кода
-
это особенно бу
дет заметно при создании функций, но в то же время это потенциальный источник
ошибок. Во многих случаях тип переменных не изменяется, а изменение типа мо
жет свидетельствовать об ошибке в логике программы,
-
например, если при при
сваивании разработчик перепутал имя переменной. В таком случае либо будет соз
дана новая переменная с ошибочным именем
(если
такой переменной до этого
не существовало), либо изменится значение не той переменной, которую подразу
мевал разработчик. Оба эти случая приведут к ошибке выполнения программы где
то в последующих строках кода, и поиск таких ошибок может оказаться не самым
приятным занятием.
Компилируемые языки со статической типизацией подобные ошибки могут отлав
ливать на этапе компиляции, и программа, пытающаяся присвоить целочисленное
значение переменной и объявленная как строковая, даже не скомпилируется. От
логических ошибок, когда присваивание происходит в рамках одного типа, конеч
но, компилятор защитить не сможет.
Следствием динамической природы языка
нет такого понятия как «константа»,
-
Python
является тот факт, что в
Python
то есть не бывает переменных, значение
которых нельзя изменить. Но поскольку постоянные величины часто нужны по ло
гике программы, то для обозначения того, что ту или иную переменную не следует
изменять, ее имя часто пишут заглавными буквами:
>>> PI = 3.1415926535897932
Заключение
Если это было ваше первое взаимодействие с интерпретатором языка
Python,
то я
вас поздравляю. В этой главе мы только подготовились к изучению языка и тех
возможностей, которые предоставляет
Python
и библиотеки, созданные для него.
Но даже в этой главе мы уже увидели некоторые важные особенности.
Сначала мы разобрались с параметрами установщика интерпретатора
Windows
Python
под
и коротко обсудили, какие компоненты нам понадобятся в дальнейшем, а
что лучше пока не устанавливать.
44
Часть
После этого мы начали работать с
REPL -
1.
Базовые понятия и встроенные типы
средой для использования языка
в интерактивном режиме. Мы прошли посвящение в программисты
экран строку
Hello, world!
-
Python
вывели на
и на этом простом действии обсудили некоторые осо
бенности языка.
Затем мы научились создавать переменные и выводить их значения в консоль, уз
нали, что каждая переменная имеет свой тип (класс) и поняли, как его определять с
помощью встроенной функции type (). Научились создавать переменные целого
типа (класс int), числа с плавающей точкой (класс float) и комплексные числа
(класс complex).
В следующей главе мы более подробно рассмотрим числовые типы переменных.
- ГЛАВА 2-
ПрОСТеЙШИе типы
и математика в
Python
Из этой главы мы узнаем, как с помощью
Python
решать простые математические
задачи. Сначала мы более подробно поговорим о числовых типах, булевом типе и
объекте None, используемом в качестве «заглушки», чтобы показать, что перемен
ной не присвоено никакое осмысленное значение. А затем, в завершение главы,
рассмотрим два стандартных модуля с набором математических функций: math и
cmath.
Коротко о терминологии
При описании синтаксиса языка программирования используются такие термины
как «оператор», «выражение» и «инструкция». Эти термины часто путают или счи
тают синонимами, хотя между ними есть различия. Давайте договоримся о том, что
мы будем понимать под этими терминами.
♦ Литерал
(literal) -
это элемент кода, представляющий собой фиксированное
значение. Например, 42 и "hello" ♦
Оператор
(operator)-
это литералы.
это символ или набор символов, которые используются
для обозначения того, что нужно выполнить какую-то операцию
(operation).
Под
операцией поднимается действие, выполняемое над переменными и (или) значе
ниями. Например, для операции сложения используется оператор
выполнения операции
♦ Выражение
s
+ 1
(expression)-
«+».
Результат
можно применить в выражениях или инструкциях.
это код, который после выполнения имеет какое-то
значение, и поэтому результат вычисления выражения можно присвоить пере
менной
s
а
♦
или
задействовать
в
более
сложных
выражениях.
Например,
* 2 - это выражение, поэтому его можно использовать, например,
= s + з * 2 или применить в операции сравнения: s + з * 2 > о.
+
з
Инструкция
(statement)-
так:
это код, который указывает, что необходимо выпол
нить некоторые действия. При этом сама инструкция не имеет возвращаемого
значения, и поэтому результат инструкции нельзя присвоить переменной. На
пример, а
=
s-
это инструкция, поэтому мы не можем написать
(а
=
5)
>
о.
46
Часть
Объявление функции, циклы
-
1.
Базовые понятия и встроенные типы
это инструкции. По сути, программа
-
это на
бор инструкций.
В инструкциях могут использоваться ключевые слова
for, while, break, continue, return
(keywords) -
такие как
И др.
Целые числа
Если вы знакомы с такими языками программирования как С, С++ или им подоб
ными, то знаете, что там есть несколько типов целых чисел, которые отличаются
тем, что занимают в памяти разное количество байтов и при этом позволяют хра
нить различные интервалы значений. В
ный
-
Python
класс для целых чисел единствен
int, но он подстраивает свое внутреннее представление хранимого числа
под конкретное значение и поэтому дает возможность хранить очень большие по
модулю числа практически без ограничений. К сожалению, за это приходится рас
плачиваться скоростью работы. Это еще один пример того, как в
Python
ради
удобства для программистов пришлось пожертвовать эффективностью выполне
ния кода. Для демонстрации сказанного сохраним в переменной foo очень боль
шое значение:
>>> foo = 12345678998765432112345678
>>> foo
12345678998765432112345678
»> type (foo)
<class 'iпt'>
Большие числа для наглядности можно разделять знаком подчеркивания в произ
вольных местах. Так, если большие числа разбивать на группы по три цифры, на
чиная с младших разрядов, то получается весьма наглядно:
>>> foo = 12 345 678 998 765 432 112 345 678
>>> foo
12345678998765432112345678
Для записи целых чисел можно использовать не только десятичную систему счис
ления, но также двоичную, восьмеричную и шестнадцатеричную. Для указания то
го, что число записано в двоичной системе счисления, перед записью числа нужно
без пробела написать оь или ов, если в восьмеричной системе
шестнадцатеричной системе
>>> foo = ОЫО1010
>>> foo
42
>>> bar
0xAF102B
>>> bar
11472939
»> baz
007210
>>> baz
3720
-
Ох или ох. Например:
-
Оо или оо, если в
Глава
2.
Простейшие типы и математика в
Python
47
Числа с плавающей точкой
Кроме целочисленных значений мы можем создавать числа с плавающей точкой
(действительные числа). В
Python
ствует только один тип
-
float, который хранит значение с двойной точностью,
используя для этого
бита (аналог типа douЫe в языках С и С++). Следующие
64
для представления действительных чисел суще
примеры показывают, какие существуют способы для записи чисел в таком формате:
» >
Ьа r
=
1О . О
>>> bar
10.0
>>> baz
-25.
>>> baz
-25.0
>>> spam = .42
>>> spam
0.42
>>>с= 2.99792458е8
>>> с
299792458.0
>>> eps0 = 8.854187817е-12
>>> eps0
8.854187817е-12
»> type (eps0)
<class 'float' >
Как видно из этих примеров, для разделения целой и дробной частей используется
точка (не запятая). Обратите внимание, что если бы переменные bar и baz были
записаны без разделительной точки, то они получили бы тип int, а не float. Если
целая или дробная части равны нулю, то этот ноль можно не писать, но при этом
точку нужно поставить обязательно.
Запись видах. Ye±z называется :экспонс1111иа:1ьн011 (хотя символ е в такой записи не
имеет ничего общего с числом Эйлера е
чение, равное х. У • 1 о·
.
;::; 2.718281 ).
Эта запись представляет зна
При этом символ е может быть как строчной, так и заглав
ной буквой. Если при такой степенной записи дробная часть перед степенью равна
нулю, то ее можно не писать, и даже не ука'3ывать точку. При этом тип такой пере
менной по-прежнему останется
float. В следующих примерах все команды при
сваивания равнозначны:
»> freq = 4.Ое9
>>> freq = 4.ОЕ9
>>> freq = 4.е9
>>> freq = 4е9
»> type (freq)
<class 'float'>
Для типа float существуют три специальных значения: inf, -inf и nan. Первые два
обозначают оо и -оо, а третье обо:тачает
NaN, Not-a-Number -
не число. Эти зна-
Часть
48
1.
Базовые понятия и встроенные типы
чения могут получаться в процессе математических операций. Иногда это ожидае
мые значения, но часто такие значения говорят о том, что в вычислениях содержит
ся какая-то ошибка.
Для явного присваивания переменной значений inf, -inf или nan есть несколько
способов. Один из них заключается в импортировании переменных inf и nan из
модуля math из стандартной библиотеки, но про модуль math речь пойдет чуть поз
же, а сначала рассмотрим другой способ:
>>> foo = float("inf")
>>> foo
inf
»> type(foo)
<class 'float'>
>>> bar = float ("-inf")
>>> bar
-inf
>» type (bar)
<class 'float '>
>>> spam = float ("nan")
>>> spam
nan
»> type (spam)
<class 'float' >
Строго говоря, функция float (), которая здесь используется, выполняет преобра
зование переданных на ее вход типов (в нашем случае строк) в тип float. В этом
примере на ее вход передали строковое представление специальных значений inf,
-inf и nan, и на их основе были созданы соответствующие значения. Также на вход
функции f 1 оа t () можно передавать строковые представления чисел с плавающей
точкой, и она вернет значение типа floa t, если строку удастся преобразовать
в число:
>>> baz = float("l0.5")
>>> baz
10.5
»> type (baz)
<class 'float '>
>>> freq = float("3e9")
>» freq
3000000000.0
»> type(freq)
<class 'float '>
Такое преобразование из строки в число полезно, если строковое значение получе
но от
пользователя
в
процессе
ввода через
консоль или
прочитано
из текстового
файла. В случае ошибки, когда невозможно преобразовать строку в тип float, бу
дет возбуждено исключение valueError. Про исключения мы подробно будем го-
Глава
2.
Простейшие типы и математика в
ворить в главе
19,
Python
49
а пока их можно воспринимать как возникновение ошибки, и ес
ли эту ошибку не перехватить и не обработать, то программа экстренно завершится:
»> Ьоо = float ( "Ыа-Ыа-Ыа")
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
Ьоо = float ("Ыа-Ыа-Ыа")
ValueError: could not convert string to float: 'Ыа-Ыа-Ыа'
Для аналогичного преобразования строк в целые числа мы можем использовать
функцию int ():
>>> foo = int("42")
>>> foo
42
>» type (foo)
<class 'int'>
Комплексные числа
С комплексными числами мы уже встречались в главе
1
и знаем, что для создания
таких чисел нужно указать действительную и мнимую части. Для записи мнимой
части используется символ мнимой единицы j, а действительную часть можно не
указывать, если она равна О. Запись действительной и мнимой частей подчиняется
тем же правилам, что и числа с плавающей точкой. Корректными являются сле
дующие команды для их создания:
>» foo =
>>> foo
S+Зj
(S+Зj)
»> type(foo)
<class 'complex'>
>>> bar = 10.+.Sj
>>> baz = О.З+Зе-2j
>>> spam = -2.25e3j
»> eggs = lj
Обратите внимание на последнюю строчку этих примеров, где переменной eggs
присваивается мнимая единица. В такой записи обязательно нужно указать едини
цу перед j, иначе команда eggs = j будет воспринята как присваивание перемен
ной eggs переменной j, что в этом случае будет ошибкой, поскольку переменная j
не создана.
Комплексные
относятся
числа
к
классу
complex. Про особенности объектно
ориентированного программирования и классы в
рить в главах
13-15,
Python
мы будем подробно гово
но сейчас нам важно знать, что для классов в
понятия как «метод»
-
Python
есть такие
это функция, находящаяся внутри класса и имеющая дос
туп к внутренним переменным класса, а также «свойство»
-
это некоторая сущ
ность, которая внешне с точки зрения пользователя класса ведет себя как перемен-
50
Часть
1.
Базовые понятия и встроенные типы
ная внутри класса. Свойства могут быть только для чтения или для чтения и запи
си. Для доступа к любому атрибуту класса (переменной, методу, свойству) исполь
зуется символ
«. »,
который ставится между именем переменной и именем атрибу
та, к которому надо получить доступ. Возможно, такое строгое определение звучит
несколько сложно, но следующие примеры должны всё прояснить.
У класса complex есть два свойства только для чтения: real и imag (сокращение от
английского слова
imaginary),
которые предназначены для получения действитель
ной и мнимой частей комплексного числа. Имеется у него и метод con j ug а te () ,
возвращающий комплексно-сопряжеююе число (комплексное число, у которого
знак мнимой части изменен на противоположный). Следующие примеры демонст
рируют использование этих атрибутов класса complex:
>»с=
>>>
5.0
>>>
3.0
>>>
>>>
0.6
>>>
>>>
S+Зj
c.real
c.imag
angle_tan
angle_tan
c.imag / c.real
c_conj = c.conjugate()
c_conj
(5-Зj)
>>>
с
(S+Зj)
Здесь, в частности, показано, как получить действительную и мнимую части ком
плексного числа, вывести их в консоль и проделать с ними какую-либо математи
ческую операцию,
-
например, рассчитать тангенс аргумента комплексного числа,
после чего вызвать метод
са
con j ug а te () , который возвращает новый экземпляр клас
complex, являющийся комплексно-сопряженным к исходному значению. По
следняя в этих примерах команда показывает, что исходное значение переменной
при вызове метода
conjugate ()
не изменяется.
Обратите внимание на разницу в использовании свойств imag и real и метода
conjugate () -
для доступа к свойствам после имени свойства не ставятся круглые
скобки, в отличие от вызова метода (функции), где скобки обязательны.
Как уже отмечалось, свойства imag и real являются свойствами только для чтения,
поэтому изменить действительную или мнимую части комплексного числа нельзя,
можно только создать новую комплексную переменную с новыми значениями дей
ствительной и мнимой частей. При попытке присвоить свойствам real или imag
какого-то значения будет возбуждено исключение:
>» с = S+Зj
»> c.real = 10
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
Глава
2.
Простейшие типы и математика в
Python
51
c.real = 10
AttributeError: readonly attribute
Между прочим, свойства
real и imag, а также метод conjugate (), есть и у классов
int и float, но для них свойство real и метод conjugate () всегда возвращают са
мо значение числа, а значение свойства imag всегда равно нулю:
>» х = 10.5
>>> x.real
10.5
»> х. imag
о.о
>>> x.conjugate()
10.5
Благодаря этой особенности классов int и float мы можем писать код, который
одинаково работает и для действительных, и для комплексных чисел.
До сих пор для создания комплексных чисел использовались инструкция вида
с = 5 + з j , но есть еще один способ их создания с помощью функции
complex (). Существуют три варианта вызова этой функции, которые отличаются
параметрами, которые ей передают.
Один из вариантов использования функции complex () -
это передать ей на вход
два значения, которые будут являться действительной и мнимой частями создавае
мого комплексного числа:
>>>с=
complex(lO, 5)
>>> с
(10+5j)
Обычно такое использование этой функции удобно, когда действительная или
мнимая часть комплексного числа вычисляются на основе других переменных. Вот
как можно было бы изменить недавний пример про попытку изменения действи
тельной части комплексного числа:
»>
с
>>>с=
5+3j
complex(lO, c.imag)
>>> с
(10+3j)
Если действительная или мнимая часть комплексного числа равна нулю, то для вы
зова функции complex () можно воспользоваться сокращенной записью:
>>> х = complex(2.5)
>>> х
(2.5+0j)
>>>у= complex(imag=-0.5)
>>> у
-0.5j
»> z
complex ()
>>> z
Oj
52
Часть
1.
Базовые понятия и встроенные типы
Эти примеры работают за счет того, что у функций могут быть именованные пара
метры и значения параметров по умолчанию. Про всё это будет сказано в главе
1О,
посвященной функциям. Обратите внимание, что вызов функции complex () с пере
дачей ей целочисленного значения или числа с плавающей точкой может приме
няться для преобразования значений типов int или float в тип complex.
Функцию complex () можно использовать и для создания комплексного числа по
его строковому представлению, что может быть полезно, если комплексное число
вводит пользователь в консоли или оно читается из файла:
>>> х =
>>> х
(5+3j)
complex("S+Зj")
>>>у=
complex("-0.Sj")
>>> у
-0.Sj
Логический (булев) тип переменных
Еще одним простейшим типом в языке
Python
является логический, или булев тип,
названный в честь английского математика Джорджа Буля, который занимался ма
тематической логикой. В
Python
этот тип представлен классом ьооl. Переменные
этого типа могут принимать только два значения: True и False. Обратите внима
ние, что эти значения записаны с заглавных букв. Следующие примеры демонстри
руют тип
bool:
>>> foo = True
>>> bar = False
>>> foo
True
>>> bar
False
>» type (foo)
<class 'bool'>
Булевы значения часто используют в инструкциях ветвления if / elif / else, о ко
торых речь пойдет в главе
3,
а пока лишь приведем основные логические операторы
над булевыми переменными:
♦
or -
оператор дизъюнкции, другие названия: логическое сложение, логическое
ИЛИ. Выражение х or у равно тrue, если хотя бы один аргумент х или у равен
True. В противном случае это выражение равно False;
♦
and -
оператор конъюнкции, другие названия: логическое умножение, логиче
ское И. Выражение х and у равно True только в том случае, если оба аргумента:
х и у равны True. В противном случае это выражение равно False;
♦
not True
оператор отрицания, другое название
оператор отрицания возвращает
False,
-
логическое НЕ. Для значения
а для значения
False -
True;
Глава
♦
л
2.
Простейшие типы и математика в
исключающее ИЛИ, другое название
-
53
Python
-
логическое вычитание. Выражение
True только в том случае, когда один из аргументов равен True,
другой равен False. В противном случае это выражение равно False.
х
л
у равно
а
Следующие примеры демонстрируют использование логических операций:
>>> t
>» f
>» t
False
»> t
True
>>> t
True
>>> t
False
= True
= False
and f
or f
л
f
л
t
Значения булева типа получаются также и в результате применения операторов
сравнения:
♦
== -
отношение эквивалентности или сравнение на равенство;
♦
!= -
отношение неэквивалентности или сравнение на неравенство;
♦
<, <=, >, >= -
меньше, меньше или равно, больше, больше или равно.
Следующие примеры показывают использование таких операторов:
>» foo
10
>>> bar
10
>>> baz
20
>>> spam = foo == bar
»> spam
True
»> eggs
foo != baz
»> eggs
True
>>> not eggs
False
>>> foo > baz
False
>» baz >= bar
True
Объект
В
Python
None
есть специальный тип NoneType, представленный в виде единственного
глобального объекта None. Обратите внимание, что None пишется с заглавной бук
вы. Этот объект обозначает «отсутствующее значение». Он используется в таких
ситуациях, например, когда нужно создать переменную, но при этом показать, что
54
Часть
1.
Базовые понятия и встроенные типы
этой переменной еще не присвоено никакое осмысленное значение. Возможно, со
гласно логике программы этой переменной какое-то значение будет присвоено
позже, а может быть, эта переменная так и останется со значением
None. В после
дующем тексте программы в этом случае, скорее всего, где-то будет производиться
проверка на равенство None или какому-то другому значению. Забегая вперед, ска
жем, что значение None возвращают все функции, которые не используют инструк
цию
return для возврата какого-либо значения. Например, часто используемая на
возвращает объект None. Следующие примеры показывают
применение объекта None:
ми функция print ()
>>> foo = None
»> print(foo)
None
>>> print(type(foo))
<class 'NoneType'>
>>> bar = print("Hello, world!")
Hello, world!
>» print (bar)
None
В этих примерах использование функции print () для отображения значения пере
менных, равных
None, обязательно, поскольку в REPL при попытке вывести значе
None, ничего отображаться не будет:
ние переменной, равной
>>> foo = None
>>> foo
>>>
При этом мы не можем присвоить объекту
None никакое другое значение
-
если
мы попытаемся это сделать, то получим ошибку:
>>> None = 10
File "<unknown>", line 1
None = 10
SyntaxError: cannot assign to None
С объектом None мы будем неоднократно встречаться на протяжении всей книги.
Объект None всегда существует в единственном экземпляре, и создать второй объ
ект типа
NoneType
не удастся.
Математические операторы
Python
обладает стандартным набором математических операций, таких как вычи
тание и унарный минус
вида деления:
«/»
и
«-», сложение и унарный
«/ /», о различиях которых мы
плюс
«+»,
умножение
«*»,
два
скоро поговорим, возведение в
степень«**», получение остатка от деления«%». Для явного указания приоритета
операций используются круглые скобки.
Глава
Простейшие типы и математика в
2.
Python
55
Про операторы вычитания, сложения, умножения и возведения в степень нет смыс
ла что-то говорить, ограничимся лишь примерами:
»>x=l.5
>>>
у
>>>
у
- 3) ** 2 +
(х
=
(х
/ 2 + 4)
7.0
>»
ь
2
3.3
>>>
с
а
>>>
с
>>>
а
+ Ь ** 2
12.889999999999999
Обратите здесь внимание на последний результат. Вместо ожидаемого значения
12.89 Python
вывел
12.889999999999999.
Это связано с особенностями представле
ния чисел с плавающей точкой в двоичном виде. В общем случае невозможно
представить произвольное число с плавающей точкой без потери точности, особен
но после выполнения математических операций над числами.
минус
Унарный
на
-1,
>>>
а
»>
ь
>>>
ь
-
это оператор, который аналогичен умножению переменной
а унарный плюс
-
умножению на+ 1. Они показаны в следующих примерах:
7
=
-а
-7
>>>
с
>>>
с
+а
7
В
Python
есть два оператора деления: оператор
«/»
всегда возвращает результат в
виде числа с плавающей точкой, а оператор целочисленного деления
«! !»
всегда
возвращает целое значение (но, как мы скоро увидим, не обязательно тип int).
Сначала рассмотрим оператор«/»:
»>
а
= 10
»>
Ь
= 5
»>
с
= 2
»> d =
а
/
с
»> d
5.0
»> type (d)
<class 'float '>
»>
е
>>>
е
=
Ь
/ с
2.5
»> type
(е)
<class 'float' >
56
Часть
1.
Базовые понятия и встроенные типы
В этом примере созданы три целочисленные переменные: а, ь, с (имеющие тип
int). При вычислении значения, которое будет присвоено переменной d, можно
было бы ожидать, что результатом будет целое число
2.
5-
ведь
10
нацело делится на
Однако, как видно из примера, оператор«/» в обоих случаях возвращает значе
ние типа float. Разумеется, оператор«/» можно применять также к значениям ти
па
float
»>а=
»>
»>
и
complex:
7+5.25j
Ь =а/
3.5
ь
(2+1.Sj)
По-другому ведет себя оператор
«/ /»,
который, как уже упоминалось, называется
оператором целочисленного деления. Несмотря на свое название, этот оператор
может применяться как к переменным типа
int,
так и к переменным типа
float.
Для начала рассмотрим применение этого оператора к двум операндам типа int.
В этом случае оператор
«11»
вернет целочисленное значение, равное результату
деления первого операнда на второй и округлит это значение до целого числа в
меньшую сторону. Обратите внимание, что округление в меньшую сторону
-
это
не то же самое, что отбрасывание дробной части, разница проявляется при работе с
отрицательными числами:
»>а=
>>>
7 // 2
а
3
»> type(a)
<class 'int'>
»> Ь = -7 // 2
»> ь
-4
Если хотя бы один из операндов оператора«//» имеет тип float, то и результат
также получит тип float, но при этом рассчитанное значение будет иметь нулевую
дробную часть:
»>а= 7 // 2.0
>>> а
3.0
»> type(a)
<class 'float' >
>>> Ь = -7.0 // 2
»> ь
-4.0
»> type(b)
<class 'float'>
К комплексным числам оператор«//» применять нельзя:
»>а
= 7-7j
»> Ь =а// 2
Traceback (most recent call last):
Глава
2.
Простейшие типы и математика в
57
Python
File "<python-input- ... >", line 1, in <module>
Ь =а// 2
TypeError: unsupported operand type(s) for //: 'complex' and 'int'
Приоритет операторов
Как и в алгебре, математические операторы
Python имеют свои приоритеты.
В табл.
2.1
приведены приоритеты математических, логических и побитовых операторов, ко
торые нами пока не рассматривались. Побитовые операторы позволяют изменять
отдельные биты в целочисленных переменных. Важно не путать побитовые опера
ции с логическими. Чем ниже положение оператора в табл.
2.1,
тем ниже его при
оритет.
Таблица
2.1.
Приоритет математических операторов
Комментарий
Приоритет
Оператор
1
( )
Скобки
2
**
Возведение в степень
3
4
+, -
*
'
+, -
6
<<, >>
7
&
9
10
Умножение, деление, целочисленное деление,
%
' /' / /'
5
8
Унарные плюс, минус, побитовое отрицание
(инверсия)
~
взятие остатка от деления
Сложение, вычитание
Побитовые сдвиги влево и вправо
Побитовое И
Побитовое исключающее ИЛИ
л
Побитовое ИЛИ
1
<, <=, >, >=, !=
(XOR)
'
--
Операторы сравнения
11
not
Логическое НЕ
12
and
Логическое И
13
or
Логическое ИЛИ
Полную таблицу приоритетов, которая включает еще не рассмотренные нами опе
раторы, можно найти на странице документации'.
1 См.
https://docs.python.org/3/reference/expressions.html#operator-precedence.
Часть
58
1.
Базовые понятия и встроенные типы
Отдельно надо обратить внимание на оператор возведения в степень«**». В отли
чие от других математических операторов, если в одном выражении встречаются
несколько операторов«**», то они вычисляются справа налево,
-
т. е. сначала бу
дет вычислено значение правого оператора возведения в степень, а затем левого.
Это показано в следующих примерах:
>>> 2**2**3
256
>>> 2** (2**3)
256
>>> (2**2) **3
64
Первое выражение здесь равносильно следующей математической записи: 2 21 . При
написании кода, если есть хоть малейшие сомнения в приоритете операторов, луч
ше расставить лишние скобки, чтобы исключить трудно отлавливаемые математи
ческие ошибки.
Инструкции присваивания
Из предыдущих примеров уже должно быть ясно, что для присваивания значения
переменной используется знак
«=».
Однако у инструкции присваивания есть моди
фикации, которые были позаимствованы из языка программирования С. На практи
ке часто встречаются инструкции присваивания, используемые для модификации
исходной переменной:
»>
»>
>>>
14
»>
»>
»>
15
12
а
а
=
а
+ 2
а
Ь
= 3
Ь
=
Ь
* 5
ь
В языке
Python
для сокращения подобных записей существуют инструкции при
сваивания с модификацией переменных:
+=, -=, *=, /=, / /=, **=
и подобные им.
Следующие примеры показывают, как можно переписать предыдущий код с ис
пользованием инструкций+= и*=, а также работу других подобных инструкций:
»>а=
>>>а+=
>>>
14
>»
»>
>»
6
12
2
а
ь
ь
ь
=3
*= 2
Глава
2.
Простейшие типы и математика в
59
Python
»>с= 2
»> с -= 4
»> с
-2
»> d = 2
»> d **= 5
»> d
32
»>
е
7
»>е//=2
»>
е
3
Если вы знакомы с языками С, С++ и им подобными, то у вас может возникнуть
вопрос, есть ли в
Python
операторы«++» и«--»? Ответ: нет таких операторов. По
скольку при использовании операторов
«++»
и
«--))
часто возникали ошибки, свя
занные с тем, что программисты путали префиксную и постфиксную запись этих
операторов, в
Python
было решено от них отказаться. Вместо них нужно явно ис
пользовать инструкции
n
+=
1
и
n
-=
1,
хотя, строго говоря, они не являются пол
ными аналогами операторов«++)) и«--)) из языков С и С++.
Математические функции и модуль
В
Python
math
имеется встроенная функция abs () , предназначенная для получения абсо
лютного значения числа (его модуля). Эту функцию можно применять к целым,
действительным и комплексным числам. Например:
>» foo = -5
>>> foo abs = abs(foo)
»> foo abs
5
>>> type(foo_abs)
<class 'int'>
>» bar = -10.5
>>> bar abs = abs(bar)
»> bar abs
10.5
>>> type(bar_abs)
<class 'float' >
Применяя функцию abs () к целому числу, в результате мы получаем целое число, а
применяя эту функцию к действительному числу, получаем действительное число.
Если в функцию abs ()
передать комплексное число, мы получим модуль ком
плексного числа, равный квадратному корню из суммы квадратов действительной и
мнимой частей:
>»
>>>
х
х
= 2+3j
abs = abs(x)
Часть
60
1.
Базовые понятия и встроенные типы
>>> х abs
3.605551275463989
>» type (x_abs)
<class 'float' >
В
Python
имеются встроенные функции min () и max (), которые позволяют опреде
лять соответственно минимальное и максимальное значения среди параметров, ко
торые в них передаются:
>>> min(l0, 20, -5)
-5
>>> max(5, 3, 15, 20)
20
В эти функции также можно передавать списки, кортежи, массивы и другие кол
лекции для нахождения в них минимального и максимального значений. Про кол
лекции речь пойдет в главе
4.
До сих пор во всех примерах использовались только простейшие математические
операции, однако
Python
поставляется с обширной стандартной библиотекой, кото
рая включает в себя модуль math, внутри которого содержатся более сложные ма
тематические функции: тригонометрические (в том числе гиперболические), функ
ции для различных способов округления чисел, вычисления логарифмов, для рас
чета квадратного корня и возведения в степень, для перевода градусов в радианы и
обратно. Кроме того, в модуле math содержатся математические константы pi, е и
tau (некоторые математики считают, что константу т
= 2л
использовать удобнее,
чем л), а также специальные значения inf и nan, о которых говорилось в разделе
про тип
float.
Для работы с функциями или классами, которые содержатся в модуле, необходимо
импортировать либо модуль целиком, либо отдельные функции или классы из это
го модуля. Про модули более подробно будет рассказано в главе
12,
а здесь мы рас
смотрим лишь необходимый сейчас синтаксис для использования функций, распо
ложенных в каком-либо модуле. Начнем с примера, в котором импортируется мо
дуль
math:
>» import math
>>>а=
>>>
math.sin(math.pi / 6)
а
О.49999999999999994
>>> power = 16е-3
>>> power_dВm = 10 * math.logl0(power / le-3)
>>> power_dВm
12.041199826559248
Импорт модуля ma th дает возможность задействовать все функции, переменные и
классы (если они есть), объявленные в этом модуле. При таком способе импорта
доступ
к
содержимому
модуля
осуществляется
<имя_ модуля>. <имя_ сущности>, где <имя_ сущности>
с
помощью
синтаксиса
может быть именем функции,
Глава
2.
Простейшие типы и математика в
Python
61
класса или переменной. В приведенном примере участвуют функции math. sin () и
math. loglO () (расчет десятичного логарифма), а также переменная math .pi.
Когда часто используются функции из какого-то модуля, особенно, если имя модуля
достаточно длинное, то каждый раз набирать имя модуля может быть утомитель
ным, и это будет сказываться на читаемости кода. Поэтому в
Python
есть возмож
ность, импортируя модуль, давать ему псевдоним.
Для импорта модуля с установкой псевдонима используется синтаксис вида: import
<имя_ модуля> as
для модуля
<псевдоним>. Изменим предыдущий пример, добавив псевдоним
ma th:
>>> import math as m
>>>а= m.sin(m.pi / 6)
>>> а
О.49999999999999994
>>> power = 16е-3
>>> power_dВm = 10 * m.logl0(power / le-3)
>>> power_dВm
12.041199826559248
При установке псевдонима важно следить, чтобы в последующем коде не создава
лась сущность (переменная, функция, класс) с тем же именем, что и псевдоним мо
дуля. Для некоторых библиотек есть уже устоявшиеся имена псевдонимов, которые
им дают при импорте. Например, модуль numpy из библиотеки для математических
расчетов
NumPy часто импортируется под именем np, а модуль pandas из библио
Pandas, предназначенной для работы с табличными данными, обычно импор
тируется под псевдонимом pd. Библиотеку NumPy мы будем изучать в главе 25, а
библиотеку Pandas ~ в главе 29.
теки
Если вы используете из модуля лишь небольшое количество сущностей, то можно
импортировать только их, а не весь модуль целиком. В этом случае код будет еще
более компактным за счет того, что для явно импортированных сущностей не тре
буется указывать, из какого модуля они были импортированы. Изменим предыду
щий пример таким образом, чтобы импортировать из модуля ma th только исполь
зуемые функции s i n ( ) , 1 og 1 о
()
и переменную р i:
»> fram math ШFOrt sin, loglO, pi
>>>а= sin(pi / 6)
>>> а
О.49999999999999994
>>> power = 16е-3
>>> power_dBm = 10 * logl0(power / le-3)
>>> power_dВm
12.041199826559248
Как можно здесь видеть, для явного импорта сущностей из модуля применяется
синтаксис:
from
<имя_модуля>
import
<сущность_l>,
<сущность_2>,
... ,
<сущ
ность_ n>. После этого со всеми импортированными сущностями можно работать,
Часть
62
1.
Базовые понятия и встроенные типы
как будто они объявлены вне какого-либо модуля. Но с таким импортом надо быть
осторожным и следить за тем, чтобы имена импортируемых сущностей не совпада
ли с именами сущностей, которые будут созданы в нашей программе или импорти
рованы из другого модуля.
Есть еще один способ импорта сущностей из модуля, но к нему не рекомендуется
прибегать без крайней необходимости. Этот способ импортирует всё, что есть в
модуле, и осуществляется с помощью синтаксиса:
from
<имя_модуля>
import
*
Следующий пример показывает применение такого способа импорта:
>» from math import *
>>>а=
>>>
sin(pi / 6)
а
О.49999999999999994
>>> power = 16е-3
>>> power_dВm = 10 * logl0(power / le-3)
>>> power_dВm
12.041199826559248
Этот способ опасен из-за того, что мы можем заранее не знать, какие именно имена
сущностей импортируются. Более того, при обновлении библиотек набор этих
сущностей может меняться, и вполне может оказаться, что имя какой-то функции
из модуля совпадет с именем функции, которая уже объявлена в нашем коде. Это
может стать причиной неожиданного поведения программы и долгой неприятной
отладки кода. Поэтому использования такого способа импорта следует избегать.
Полный список функций из модуля math вы найдете в документации 2 . В табл.
2.2
приведены некоторые из функций, которые содержатся в модуле ma th.
Таблица
2.2. Некоторые мвтемвтические функции из модуля та th
Описание
Имя функции
Функции округления
ceil(x)
Округление в сторону меньшего целого числа
floor
Округление в сторону большего целого числа
(х)
trunc(x)
Округление путем отбрасывания дробной части
Тригонометрические функции
sin
(х),
cos
(х),
tan
(х)
Синус, косинус, тангенс угла х. Значение х должно быть
задано в радианах
2
См. https://docs.python.org/3/library/math.html.
Глава
Простейшие типы и математика в
2.
Python
63
Таблица
Имя функции
(продолжение)
2.2
Описание
Тригонометрические функции
asin (х), acos
(х),
atan
(х)
Арксинус, арккосинус, арктангенс от значения х. Результат
возвращается в радианах
atan2(y,
Арктангенс у/ х с учетом квадранта. Результат возвраща-
х)
ется в радианах
degrees
(х)
Преобразование угла х из радиан в градусы
radians(x)
Преобразование угла х из градусов в радианы
Гиперболические функции
sinh
(х),
cosh
(х),
asinh (х), acosh
tanh
(х),
(х)
atanh (х)
Гиперболические синус, косинус, тангенс от х
Обратные гиперболические синус, косинус, тангенс от х
Другие функции
fabs
(х)
Абсолютное значение числа х. Всегда возвращает значе-
ние с плавающей точкой
функции
sqrt(x)
pow(x,
float,
в отличие от встроенной
abs ()
Квадратный корень числа х
у)
Возведение х в степень у. В отличие от оператора«**»,
функция
pow ( )
всегда преобразует свои аргументы в тип
float
loglO(x)
Десятичный логарифм от х
log2(x)
Логарифм от х по основанию
log (х [, base])*
Логарифм от х по произвольному основанию. Второй па-
2
раметр ьаsе можно не указывать, тогда будет рассчитан
логарифм по основанию е, а именно натуральный логарифм
Функции сравнения
isclose(x, у, *, rel_tol=
le-09, abs_tol=0.0)**
Сравнение значений х и у с заданной погрешностью. Возвращает значение тrue, если разница между х и у уклады-
вается в относительный или абсолютный допуск, и возвращает
isfinite(x)
False
в противном случае
Возвращает тrue, если значение хне равно
nan,
и возвращает
False
inf, -inf
в противном случае
или
Часть
64
1.
Базовые понятия и встроенные типы
Таблица
Имя функции
2.2 (окончание)
Описание
Функции сравнения
isinf(x)
Возвращает тrue, если значение х
возвращает
isnan(x)
False
равно
Возвращает тrue, если значение х
щает
False
inf
или
-inf,
и
в противном случае
равно
nan,
и возвра-
в противном случае
В документации встречается такая запись параметров функции, когда какие-то пара
метры взяты в квадратные скобки. Это обозначает, что такой параметр является не
обязательным, и его можно не указывать. В документации обычно написано, какие
значения по умолчанию подразумеваются, если необязательные параметры не указаны.
Такое обозначение функций охватывает сразу несколько особенностей объявления
функций в языке Pythoп. О них будет рассказано в главе
10.
Сейчас же достаточно
понимать, что у функции isclose () обязательными являются два первых параметра,
а при задании параметров rel _ tol (относительная погрешность) или abs _ tol (абсо
лютная погрешность) необходимо указывать имена этих параметров.
Здесь необходимо привести несколько комментариев к функциям из последней
части табл.
2.2.
Функция isclose () применяется для сравнения чисел с плавающей
точкой. Как уже говорилось, не все такие числа можно представить точно в двоич
ной системе счисления, и может возникнуть следующая ситуация:
>»а=
»>
Ь
=
0.1
О
»>а+ Ь
.2
== 0.3
False
Поэтому для сравнения чисел с плавающей точкой нужно использовать функцию
isclose ():
>>> from math import isclose
»>а= 0.1
>» Ь = 0.2
>>> isclose(a +
Ь,
0.3)
True
С помощью именованных параметров rel_tol и abs_tol можно задать допустимую
относительную или абсолютную погрешность. Про именованные параметры функ
ций подробно написано в главе
10.
Функция isnan () требуется из-за того, что nan -
это особое значение, результат
его сравнения с любым значением (включая nan) равен False:
>>> from math import isnan
>>> foo = float ("nan")
>>> foo
Глава
2.
Простейшие типы и математика в
Python
65
nan
>>> bar = float("nan")
»> foo == bar
False
»> isnan(foo)
True
Модуль
cmath
Функции из модуля math работают с переменными типа float, однако в стандарт
ной библиотеке
Python
имеется еще модуль cmath, предназначенный для работы с
комплексными числами. Функции и константы из этого модуля во многом повто
ряют одноименные функции и константы из модуля ma th. В том числе:
♦ тригонометрические функции: sin (), cos (), tan (), asin (), acos (), atan ();
♦
гиперболические функции: sinh (), cosh (), tanh (), asinh (), acosh (), atanh ();
♦
степенные и логарифмические функции: sqrt (), ехр (), log (), loglO ();
♦
функции сравнения: isinf (), isnan (), isf ini te (), isclose ();
♦
константы и специальные значения: е, pi, tau, inf, nan и еще некоторые другие.
Отдельно стоит обратить внимание на несколько функций, связанных с представ
лением комплексных чисел в полярной системе координат:
♦
♦
phase (х)
-
возвращает аргумент комплексного числа;
polar (х)
-
возвращает два значения: r, phi, что является представлением ком
плексного числа в полярной системе координат;
♦
rect ( r,
phi) -
выполняет преобразование, обратное функции polar (), т. е. на
основе представления комплексного числа в полярной системе координат созда
ет переменную типа
complex.
Следующие примеры показывают совместное использование модулей math и cmath:
>>> import cmath
»> import math
»> foo = 5-5j
>>> foo_phase_deg = math.degrees(cmath.phase(foo))
>>> foo_phase_deg
-45.0
>>> foo_r, foo_phase rad = cmath.polar(foo)
»> foo r
7.0710678118654755
>>> foo_phase_rad
-0.7853981633974483
>>> bar = cmath.rect(2, math.radians(30))
>>> bar
(l.7320508075688774+0.9999999999999999j)
Часть
66
1.
Базовые понятия и встроенные типы
Здесь сначала из модуля math используются функции: degrees () угла из радиан в градусы и radiaпs
() -
для пересчета
для пересчета из градусов в радианы.
Также стоит обратить внимание на вызов функции cma th. polar (). Внешне это вы
глядит, как будто функция возвращает два значения. Однако, строго говоря, это не
так
-
на самом деле функция возвращает один объект кортежа (tuple), содержа
щий два значения, и эти значения распаковываются в две переменные. Если сейчас
вам это пояснение кажется непонятным набором слов, не пугайтесь, в главе
4
мы
подробно разберемся с тем, что такое «кортежи», и что такое «распаковка».
В следующем примере демонстрируются различия между одноименными функ
циями из модулей cmath и math. В школе нас учили, что синус не может принимать
значение больше
1.
чему равен арксинус
Потом оказалось, что это не совсем так. Попытаемся узнать,
3 ;-)
>>> import cmath
>>> import math
>>> Ьоо = cmath.asiп(З)
>>> Ьоо
(l.5707963267948966+1.762747174039086j)
>>> bar = cmath.sin(boo)
>>> bar
(3.0000000000000004+1.7319121124709863e-16j)
>>> baz = math.asin(З)
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
baz = math.asin(З)
ValueError: math domain error
Здесь хорошо видно, что если мы хотим остаться в рамках действительных чисел и
используем функции из модуля math, то asin (3) вычислить не удастся, но если нас
устроит
комплексный
результат,
то
мы
можем
воспользоваться
одноименной
функцией из модуля cmath.
Заключение
Эта глава посвящена простейшим типам, которые используются для математиче
ских расчетов.
Для хранения целых чисел предназначен класс int. При указании больших цело
численных значений мы можем использовать знак
«_»
для более наглядного разде
ления числа на группы цифр. С помощью префиксов целые числа можно задавать в
различных системах счисления: двоичной (префиксы оь или ов), восьмеричной
(префиксы оо или оо) и шестнадцатеричной (префиксы ох или ох).
Числа с плавающей точкой в
Python
представлены классом float. Существует не
сколько специальных значений: inf, -inf и пап, которые могут возникать в процес
се математических расчетов. Мы рассмотрели способы создания таких значений
Глава
2.
Простейшие типы и математика в
Python
67
путем преобразования из строки, а заодно познакомились со способом преобразо
вания строковых представлений чисел непосредственно в числа.
Python
имеет встроенную поддержку комплексных чисел (класс complex). Для по
лучения действительной и мнимой частей комплексных чисел используются свой
ства real и imag соответственно. Для получения комплексно-сопряженного значе
ния предназначен метод
Булев тип в
Python
conjugate ().
представлен классом bool, который может иметь два значения:
True и False. Помимо непосредственного присваивания значений тrue и False,
переменные булева типа могут быть созданы с помощью операторов сравнения: ==,
! =, <, <=, >, >=. Для выполнения логических операций над типами bool предусмот
рены операторы:
or, and, not
и л.
Мы также коротко упомянули такой важный объект как None, который часто ис
пользуется для того, чтобы показать, что переменной не присвоено никакое осмыс
ленное значение. Объект None всегда существует в единственном экземпляре и от
носится к классу
NoneType.
Закончили главу мы рассмотрением двух стандартных модулей, задействуемых при
математических расчетах: ma th и cma th. Многие одноименные функции содержатся
в обоих модулях, но их различие заключается в том, что функции из модуля ma th
предназначены для работы с действительными числами, а из модуля cmath с комплексными. На примере этих модулей мы увидели несколько способов импор
та функций из модулей.
Хотя мы только приступили к изучению языка
Python,
к этому моменту мы уже
вполне можем использовать его как многофункциональный калькулятор, позво
ляющий оперировать не только с действительными, но и комплексными числами.
Однако пока мы работали только в интерактивном режиме, так что пора присту
пить к созданию полноценных программ, которые для интерпретируемых языков
программирования обычно называют скриптами.
- ГЛАВА 3-
Пишем скрипты на
Python
Создание скриптов
До сих пор все примеры мы выполняли в предоставляемом
Python
интерактивном
режиме. Такой режим удобно использовать, чтобы попробовать какой-то неболь
шой кусок кода, проверить работу той или иной функции, но для реальной работы
интерактивный режим мало годится хотя бы потому, что в процессе работы у нас
не сохраняется текст программы, и при следующем запуске интерактивного режи
ма, если мы хотим повторить выполнение какого-то кода, его надо набирать заново.
Да, интерактивный режим хранит историю последних введенных команд, и вы мо
жете вернуться к предыдущим командам, нажимая клавишу «Вверх», но это явно
не то, что нам надо для полноценной работы.
Обычно мы пишем программу в виде текстового файла, который называют скрип
том. Затем в процессе выполнения интерпретатор последовательно читает коман
ды из этого файла и выполняет их. Под термином «скрипт» обычно понимают про
грамму,
которая
написана для
интерпретируемых
языков,
то
есть это такая
про
грамма, которую не надо предварительно компилировать, а можно сразу подавать
на вход интерпретатора.
Файлы
скриптов на
Python, как правило, имеют расширение ру - например,
Windows используется расширение pyw. Тогда по двой
myprogram.py. Но иногда под
ному щелчку на файле с расширением pyw вместо интерпретатора python.exe будет
запущен pythonw.exe, который не отображает черное окно консоли. Расширение pyw
обычно дают скриптам, которые создают графический интерфейс для взаимодейст
вия с пользователем. В этой книге в дальнейшем мы всегда будем использовать
расширение ру.
Для создания и редактирования скриптов подойдет любой текстовый редактор, ко
торый умеет сохранять текст в кодировке
кодировку, но на текущий момент
UTF-8
UTF-8.
Можно использовать и другую
является де-факто стандартом для напи
сания исходного кода, к тому же, с другой кодировкой у вас могут возникнуть про
блемы при выводе в консоль текста не на английском языке.
Желательно использовать текстовый редактор, который умеет раскрашивать раз
ными цветами синтаксические конструкции языка, подсвечивать парные скобки,
Глава
3.
Пишем скрипты на
69
Python
автоматически добавлять отступы для блоков кода. Это заметно повышает читае
мость кода. Кроме того, такие редакторы понимают некоторые конструкции языка,
правильно добавляя отступы и парные скобки.
Таких редакторов достаточно много, они отличаются интерфейсом и предлагаемы
ми возможностями, а кроме того, некоторые из них имеют свои идеологию, и что
бы ими эффективно пользоваться, нужно потратить некоторое время на обучение.
К числу наиболее популярных редакторов относятся:
IDLE. Это сокращение расшифровывается как Python's Integrated Development
and Learning Environment - интегрированная среда разработки и обучения
Python. Редактор IDLE, устанавливаемый вместе с интерпретатором Python, об
♦
ладает скромным набором возможностей для редактирования кода, но раскра
шивать код и помогать с его форматированием он умеет.
IDLE
позволяет рабо
тать и в интерактивном режиме, не запуская отдельно интерпретатор
При изучении языка
Python
Python.
это не такой уж плохой выбор, если до этого у вас не
было опыта работы с другими редакторами кода. Но для повседневной работы
чуть более опытного программиста этот редактор не подходит.
♦
Geany 1•
Неплохой и, главное, «легковесный» редактор для многих языков про
граммирования. Если вам кажется, что
IDLE
вы уже переросли, обратите внима
ние на этот редактор. Существует множество редакторов, обладающих схожим
функционалом, например, SciТe2, SuЬlime
Text3 и
пр.
Microsoft Visual Studio Code4. Сокращенно его называют VS Code. Не путайте
его с Microsoft Visual Studio, представляющим собой большую и сложную среду
разработки. VS Code хорошо подходит для разработки более крупных программ.
Он гибко настраивается с помощью огромного количества расширений. В VS
Code имеются автодополнение кода, подсветка ошибок, интеграция с системами
контроля версий вроде Git, интеграция с искусственным интеллектом для напи
сания кода и многое другое. Новички тоже часто выбирают VS Code как свой
♦
основной редактор, но иногда начинают путаться в возможцостях, которые он
предлагает.
♦
JetBrains PyCharm 5. Очень мощная среда разработки, существующая в двух вер
PyCharm Community Edition и коммерческой расширен
сиях: бесплатной PyCharm Professional Edition. Этот редактор уже не назовешь «легковес
ной ным». Опытным разработчикам PyCharm позволяет значительно повысить про
дуктивность работы, но новичкам обычно не рекомендуется начинать с этого
редактора.
1 См.
https://www.geany.org.
2
См. https://www.scintilla.org/SciTE.html.
3
См. https://www.suЫimetext.com.
4
См. https://code.visualstudio.com.
5
См. https://www.jetbrains.com/pycharm/.
Часть
70
♦
Emacs6, Vim 7
или его дальнейшее развитие
1.
-
Базовые понятия и встроенные типы
NeoVim8 •
Эти редакторы стоят
особняком и образуют вокруг себя целые сообщества фанатов. Каждый из этих
редакторов обладает своей идеологией работы с текстом, с которой надо озна
комиться, прежде чем начать в них работать. Они представляют собой конструк
торы, дающие широчайшие возможности для настройки редактора под себя.
Но для того, чтобы пользоваться этими редакторами эффективно, нужно потра
тить значительное время на их изучение и настройку под свои потребности. Для
этих редакторов написано огромное количество расширений, и вы можете ис
пользовать их как очень
«легковесные» редакторы,
а можете превратить
их в
полноценную среду разработки с возможностью отладки и рефакторинга кода.
Про каждый из этих редакторов пишут целые книги.
Разумеется, существует множество других текстовых редакторов, достойных того,
чтобы на них тоже обратить внимание, но здесь упомянуты только лишь наиболее
известные.
Допустим, мы определились с текстовым редактором, в котором хотим работать.
Напишем наш первый скрипт на языке
hello.py (листинг
Листинг 3.1.
Python,
для чего создадим файл с именем
3.1 ).
Chapter_03/example_01/hello.py
text = "Привет,
print (text)
мир!"
Убедитесь, что файл создан в кодировке
UTF-8, -
часто текстовые редакторы
отображают название кодировки в статусной панели.
Выполнение скриптов
Для запуска скрипта без использования среды разработки необходимо проделать
следующие операции:
1.
Запустить консоль.
2.
Перейти в каталог, где расположен созданный скрипт.
3.
Выполнить команду python имя_скрипта.
Рассмотрим эти шаги более подробно. Если вы пользуетесь операционной систе
мой Windows 10 и новее,
Windows PowerShell.
Консоль
cmd
то, скорее всего, у вас есть два варианта консоли:
cmd
и
можно запустить, либо найдя в главном меню пункт Командная
строка, либо нажав комбинацию клавиш
<Win>+<R>,
после чего в открывшемся
диалоговом окне Выполнить ввести cmd и нажать клавишу
6
См. https://www.gnu.org/software/emacs/.
7
См. https://www.vim.org.
8
См. https://neovim.io.
<Enter>.
Глава
3.
Пишем скрипты на
71
Python
Более современную консоль
Windows PowerShell можно запустить, либо найдя в
Windows PowerShell, либо через то же диалоговое окно Вы
вызываемое комбинацией клавиш <Win>+<R>, но ввести в нем надо ко
главном меню пункт
полнить,
манду
powershell.
Существуют также альтернативные консоли, которые нужно устанавливать само
стоятельно. Например, удобную консоль, работающую поверх
предоставляет программа с открытыми исходниками
Cmder
cmd
или
Если вы используете для написания кода среду разработки наподобие
Code, PyCharm или
PowerShell,
9.
Visual Studio
другой достаточно мощный инструмент, у вас есть возможность
открыть консоль непосредственно в этих программах.
Итак, консоль вы запустили. Теперь в консоли нужно перейти в каталог, в котором
расположен файл со скриптом. Для этого выполните в консоли команду:
cd
II
путь_ до_ каталога 11
например:
cd
"C:\Projects\python-book\chapter_OЗ\example_Ol 11
Название команды cd -
это сокращение от
change directory,
сменить каталог.
Если путь не содержит пробелы, то кавычки можно не писать.
Каталог, который выбран в консоли, называется текущим рабочим каталогом.
Если у вас исходники лежат на другом разделе жесткого диска (например, Е:), то,
прежде чем переходить в требуемый каталог, следует перейти на нужный диск, вы
полнив команду с указанием имени диска:
е:
-
ex1mple_01
1'
f-
0
о
Nam~
~Ноm<
helJo
~ G1llщ
8
Oe,ktop
!
1
Downlood, #
х
cm~
,..
Now •
о
+
х
'"1
'l',1,Sort•
Search example_01
ц
§V- ·
0-.te modif1~
,Ч! 1/2025 9'39 АМ
Sitt
Python F1~
1 КВ
#
Docum•nts #
1!!1 Picturos
#
8Muiic
#
а v;d..,
#
§ □
1-
Рис.
9
х
См. https://cmder.app.
3.1. Запуск консоли cmd из окна проводника Windows
Часть
72
В
Windows
1.
Базовые понятия и встроенные типы
есть возможность объединить шаги запуска консоли и переход в нуж
ный каталог. Для этого в проводнике
Windows
откройте каталог, где у вас распо
ложен скрипт, а затем в адресной строке вверху окна введите cmd или powershell
(рис.
3.1)- будет
запущена соответствующая консоль, а в качестве текущего рабо
чего каталога будет установлен тот, что был открыт в проводнике.
Теперь в консоли можно выполнить команду:
python hello.py
Записанная в файл hello.py программа выполнится (рис.
'ё'i.'
C:\Windows\ System32\cmd.e
Х
3.2).
о
+
х
Microsoft Windows [Version 10.O.22621.Q317]
(с) Microsoft Corporation. All rights reservea
С: \рго _ject
Пр1шет,
~; \pytl10n-book\ctыpte1·_03\e:.:,1111p le _01 >pytl101:
1~11р
lн,,
lо
ру
1
C:\projects\python-book\chapter_O3\exa111pte_Ol~
Рис.
3.2.
Консоль
cmd
с результатом выполнения скрипта
hello.py
Комментарии и указание кодировки файла скрипта
До сих пор мы создавали очень короткие примеры, не требующие особого поясне
ния. Однако программы, которые решают практические задачи, могут иметь размер
в тысячи строк, и разобраться в том, как они устроены, бывает непросто. Даже если
эту программу писали вы, то всё равно через некоторое время какие-то идеи, реали
зованные вами в коде, забываются, и вы будете пытаться вспомнить, почему код
написан именно так. Поэтому хорошей практикой считается добавлять в код ком
ментарии, которые игнорируются интерпретатором, но помогают ориентироваться
в коде тем, кто его читает.
Комментарии в языке
Python
начинаются с символа #, после которого может распо
лагаться текст комментария. Заканчивается комментарий в конце строки. Если вы
знакомы с языками вроде С++,
Python
Java
или
JavaScript,
то заметите, что комментарии в
подобны комментариям, которые начинаются с символов
Рекомендуется после символа
ментария,
-
#
//
в этих языках.
добавлять пробел, а затем уже писать текст ком
так комментарии выглядят аккуратнее.
В отличие от некоторых других языков программирования, в
Python
нет много
строчных комментариев. Если вам нужно написать пояснительный текст на не
сколько строк, то каждая из них должна начинаться с символа#. Комментарии так
же
часто
используют
для
временного
отключения
некоторых
строк
программы
Глава
3.
Пишем скрипты на
Python
73
в процессе отладки программы. Но не забывайте удалять закомментированные
строки кода, когда необходимость в них отпадает.
Следующий пример (листинг 3.2) решает квадратное уравнение вида ах2 + Ьх +с= О.
В этот скрипт добавлены комментарии с общей информацией о программе, а также
комментарии, поясняющие, что делают те или иные строки кода.
Листинг
#
#
#
3.2. Chapter_03/example_02/equation.py
Программа для решения квадратного уравнения.
Версия
1.0.
Дата изменения:
13.04.2025.
# Будем использовать функцию sqrt из модуля cmath,
# т.к. решения могут быть комплексные
from cmath import sqrt
#
Задаем коэффициенты квадратного уравнения
1.5
-2.0
= 5
а=
Ь
с
# Расчет дискриминанта
D = Ь**2 - 4 *а* с
# Расчет корней уравнения
xl
(-b+sqrt(D))/(2*a)
х2
(-Ь - sqrt (D)) / (2 * а)
рrint("ДИскриминант равен:",
D)
print("xl:", xl)
print("x2:", х2)
Если запустить этот скрипт, то в консоль будет выведен следующий результат:
-26.0
6666666666666666+ 1. 69967 317 ll 975948j)
(О. 6666666666666666-1. 6996731711975948j)
Дискриминант равен:
xl:
х2:
(О.
Часто в начале файла также указывают автора кода и лицензию, по которой рас
пространяется этот код, а дальнейшие комментарии используются для выделения
логических блоков программы.
Важно соблюдать баланс между количеством кода и количеством комментариев.
В комментариях не следует описывать то, что очевидно из самого кода, но коммен
тарии должны отвечать на вопрос, почему код написан именно так. При этом по
нятные имена переменных, функций и классов, поясняющие их назначение, позво
ляют писать меньше комментариев.
У комментариев в
Python
есть еще одна задача~ возможность указывать кодиров
ку, в которой создан файл скрипта. В начале главы мы договорились, что все файлы
74
Часть
1.
Базовые понятия и встроенные типы
примеров будем писать с использованием кодировки
UTF-8,
и в этом случае коди
ровку можно не указывать. Но если вы по какой-то причине используете другую
кодировку или хотите в явном виде указать, что используется кодировка
UTF-8,
то
нужно на первой строке файла скрипта написать один из следующих видов специального комментария :
# coding: utf-8
# coding=utf-8
# -*- coding: utf-8 -*Важно, чтобы такой комментарий стоял именно на первой строке, и перед ним не было
даже пустых строк. Предыдущий пример мы могли бы начать так (листинг
Листинг
3.3).
3.3. Chapter_03/example_03/equation.py
# coding: utf-8
#
#
#
Программа для решения квадратного уравнения.
Версия
1.0.1.
Дата изменения:
13.04.2025 .
Пустая строка после указания кодировки не является обязательной
-
такой отступ
в нашем случае сделан исключительно из эстетических соображений, чтобы визу
ально отделить последующие блоки текста программы.
Продолжая тему о договоренностях относительно структуры скриптов, надо отме
тить, что существуют рекомендации, согласно которым все команды импорта мо
дулей желательно располагать в начале программы до остальных команд. Наш
пример с решением квадратного уравнения следует этому правилу .
Инструкция ветвления
if ... elif ... else
До сих пор все написанные у нас программы представляли собой фиксированную
последовательность действий, не зависящую от входных данных . В реальных про
граммах многократно используются так называемые инструкции ветвления, когда,
в зависимости от определенных условий, выполняются различные блоки кода . Од
ной из таких инструкций является if ... elif .. . else, синтаксис которой можно
описать следующим образом:
if
Условие
1:
1
2:
Блок кода 2
elif Условие 3:
Блок кода 3
Блок кода
Условие
elif
else:
Блок кода
N
Глава
3.
Пишем скрипты на
75
Python
Здесь Условие 1, Условие 2, и т. д.
-
это выражения, значения которых представ
ляют собой логический тип bool (есть некоторые ситуации, когда в условиях мож
но использовать другие типы, которые неявно будут преобразованы в bool).
Когда в процессе выполнения кода интерпретатор встречает инструкцию if ... elif
... else, сначала проверяется Условие
1, идущее после ключевого слова if. Если
это условие истинно, то есть равно тrue (или это выражение может быть преобра
зовано в тrue), то выполняется Блок
кода
1. Завершив выполнение этого блока,
интерпретатор переходит на инструкцию, следующую после инструкции
i f ...
е1i
f
... else, и дальнейшие условия в ветвях elif не проверяются.
Если Условие 1 после ключевого слова if не выполняется (равно False), интерпре
татор начинает последовательно проверять все условия после ключевых слов е 1 i
f
(сокращение от else if). Если какое-нибудь из Условий, следующих после elif,
окажется равно True (или выражение может быть преобразовано в тrue), то будет
выполнен блок кода, соответствующий этой ветви. Ветвей elif может быть сколь
ко угодно или не быть вовсе.
Если же ни одно из условий в ветвях if и elif не выполняется, то выполняется
блок кода, следующий после ключевого слова else. Ветвь else в выражении if ...
elif ... else является необязательной, но если она присутствует, то такая ветвь
может быть только одна.
После каждого условного выражения, следующего за ключевыми словами if, elif,
а также после ключевого слова else, ставится двоеточие. Затем, начиная с новой
строки, начинаются блоки кода. В отличие от многих других языков, в
Python
нет
каких-либо специальных символов или ключевых слов для выделения блока кода
(например, в языках С, С++ и им подобных используются фигурные скобки). Блоки
кода в
Python
выделяются отступами.
В качестве отступа рекомендуется использовать четыре пробела. Синтаксис
Python
позволяет в качестве отступов задействовать любое количество пробелов или сим
волы табуляции,
-
главное, чтобы на протяжении всей программы вид отступа
оставался одинаковым, но общепринятым стандартом считается, что все отступы
должны состоять ровно из четырех пробелов. Редакторы кода, описанные в начале
главы, умеют добавлять отступы автоматически.
Рассмотрим пример (листинг
3.4),
в котором используется инструкция ветвления if
... elif ... else. Пусть пользователь вводит частоту электромагнитной волны в
диапазоне от
1
до
40
ГГц, а программа в ответ выводит название диапазона по
международной классификации, в который эта частота попадает.
Листинг 3.4. Chapter_OЗ/example_04/frequency_interval.py
freq_str; inрut("Введите
freq; float(freq_str)
частоту в ГГц:
if freq < 1.0:
рrint("Слишком низкая частота")
")
76
Часть
1.
Базовые понятия и встроенные типы
elif freq >= 1.0 and freq < 2.0:
print("Этo диапазон L")
elif freq >= 2.0 and freq < 4.0:
print("Этo диапазон S")
elif freq >= 4.0 and freq < 8.0:
print("Этo диапазон С")
elif freq >= 8.0 and freq < 12.0:
print("Этo диапазон Х")
elif freq >= 12.0 and freq < 18.0:
print ("Это диапазон Ku")
elif freq >= 18.0 and freq < 26.5:
print("Этo диапазон К")
elif freq >= 26.5 and freq <= 40.0:
pr in t ("Это диапазон Ка")
else:
рrint("Слишком высокая частота")
В этом примере задействована встроенная функция input (), которая ожидает от
пользователя ввода строки. В качестве параметра эта функция принимает строку,
которая будет выведена в консоль в качестве приглашения пользователю для ввода.
Когда
Python
доходит до вызова функции input (), программа приостанавливается
до того момента, пока пользователь не нажмет клавишу
строки. Результатом выполнения функции input ()
<Enter>,
завершая ввод
является строковое значение
(тип str).
Введенную пользователем строку нам нужно преобразовать в тип float. Для этого
служит функция float (), которая принимает на вход строку (тип str), возвращая
в качестве результата число (тип float), -
если, разумеется, эту строку возможно
преобразовать в число. Для успешного преобразования к типу float в качестве
разделителя дробной части нужно использовать точку (не запятую). Если введен
ное значение не удастся преобразовать к типу float, будет выведена ошибка (ис
ключение). Такое поведение функции float () было описано в главе
2.
После преобразования введенного значения к типу float осуществляется проверка,
в какой из радиочастотных диапазонов попадает введенная частота.
Далее показано несколько вызовов этого примера с различными вариантами ввода
пользователя:
> python frequency_interval.py
0.5
Введите частоту в ГГц:
Слишком низкая частота
> python frequency_interval.py
Введите частоту в ГГц: 5
Это диапазон С
> python frequency_interval.py
45
Введите частоту в ГГц:
Слишком высокая
частота
Глава
3. Пишем скрипты на Python
Приведенный в листинге
3.4
77
пример можно немного упростить, убрав из двойных
условий первые сравнения, то есть вместо
elif freq >= 1.0 and freq < 2.0:
написать
elif freq < 2.0:
Логика программы не изменится, потому что условия в е 1 i f проверяются последо
вательно сверху вниз, и если предыдущее условие не было выполнено, то в нашем
случае автоматически будет выполняться первое условие в последующем elif. Уп
рощенная версия примера из листинга
(листинг
3.4
может выглядеть следующим образом
3.5).
Листинг 3.5.
Chapter_03/example_05/frequency_interval.py
freq_ str = input ( "Введите
freq = float(freq_str)
частоту в
ГГц:
")
if freq < 1.0:
рrint("Слишком низкая частота")
elif freq < 2. О:
print("Этo диапазон
L")
elif freq < 4. О:
print("Этo диапазон
S")
elif freq < 8. О:
print("Этo диапазон С")
elif freq < 12. О:
print("Этo диапазон Х")
elif freq < 18. О:
print("Этo диапазон
Ku")
elif freq < 26. 5:
print("Этo диапазон К")
elif freq <~ 40. О:
рriпt("Это диапазон Ка")
else:
print ( "Слишком
высокая
частота")
Если при использовании инструкции if
... elif ... else какой-нибудь из блоков
кода в любой из ветвей представляет собой инструкцию, умещающуюся на одной
строке, то ее можно записать на той же строке, что и ключевые слова
else,
после двоеточия:
if Условие 1: Инструкция 1
elif Условие 2: Инструкция 2
elif Условие 3: Инструкция 3
else:
Инструкция
N
i f, е 1 i f,
78
Часть
Пример, приведенный в листинге
3.5,
1.
Базовые понятия и встроенные типы
вполне подходит для использования такой
формы записи, и его можно переписать более компактно (листинг
Листинг
3.6).
3.6. Chapter_03/example_06/frequency_interval.py
freq_str = inрut("Введите
freq = float(freq_str)
частоту в ГГц:
")
if freq < 1.0: рrint("Слишком низкая частота")
elif freq < 2.0: print("Этo диапазон L")
elif freq < 4.0: print("Этo диапазон S")
elif freq < 8.0: print("Этo диапазон С")
elif freq < 12.0: print("Этo диапазон Х'')
elif freq < 18.0: print("Этo диапазон Ku")
elif freq < 26.5: print("Этo диапазон К")
elif freq <= 40.0: print("Этo диапазон Ка")
else: рrint("Слишком высокая частота")
Впрочем, несмотря на то, что синтаксис допускает такую запись, обычно не реко
мендуют помещать несколько инструкций на одной строке.
Рассмотрим теперь пример, демонстрирующий более глубокий уровень вложения
условий и блоков кода, относящихся к ним. Вернемся к скрипту для решения квад
ратного уравнения (см. листинг
3.2).
В новой версии программы (листинг
3.7)
коэффициенты уравнения мы запрашиваем
у пользователя с помощью функции input (). Если дискриминант уравнения оказы
вается меньше нуля, а следовательно, у этого уравнения нет решений в действи
тельной области, программа спрашивает пользователя, подойдет ли ему решение в
комплексной области.
Листинг
3.7. Chapter_03/example_07/equation.py
import cmath
import math
print("Peшeниe уравнений вида
а*хл2
а
= float (input ("Введите
а:
Ь
float (input ("Введите
Ь:
с=
float(input("Bвeдитe с:"))
D=
Ь**2
- 4
*а*
+
Ь*х
+
с
О")
") )
"))
с
if D < О:
print ("У уравнения нет действительных корней.")
print ( 'Введите "д" или "у" для нахождения комплексных
answer = input(': ')
if answer == "д" or answer == "у":
корней.')
Глава
Пишем скрипты на
3.
xl =
79
Python
cmath.sq r t(D)) / (2 *
- cmath.sqrt(D)) / (2 *
print("xl:", xl)
print ("х2: ", х2)
=
х2
else:
xl
(-Ь +
а)
(-Ь
а)
+ math.sqrt(D)) / (2 *
- math.sqrt(D)) / (2 *
print("xl:", xl)
print("x2:", х2)
х2
=
(-Ь
а)
=
(-Ь
а)
Из этого примера видно, как оформляются блоки кода, если они имеют более глу
бокую вложенность. Обратите внимание, что здесь используются две одноименные
функции sqrt (), но одна из них содержится в модуле math, а другая
-
в модуле
cmath. Этот же пример демонстрирует необязательность ветвей elif и else.
Переносы строк
В языке
Python,
в отличие от многих других языков, нельзя разрывать инструкции
переносом строки в произвольном месте, поскольку перенос строки обозначает ко
нец инструкции (аналог точки с запятой в языках С, С++ и многих других). Тем не
менее, переносы строк можно добавлять в произвольном месте внутри скобок лю
бого вида
круглых, квадратных и фигурных (для чего используются квадратные
-
и фигурные скобки, мы узнаем в следующих главах). Длинные математические или
логические выражения желательно разбивать на несколько строк для улучшения
читаемости кода.
Есть еще одна рекомендация
-
желательно не делать строки длиннее
80
символов
(в последнее время некоторые разработчики говорят, что в связи с распространени
ем больших мониторов неплохо было бы это число увеличить до
В следующей версии программы (листинг
3.8)
120).
мы изменим условие, проверяющее
ввод пользователя в случае отрицательного дискриминанта, и будем вычислять ре
шение в комплексной области, если пользователь отвечает строками "да", "yes ", "д"
или " у ". Такое условие становится длинным, и часть его можно перенести на сле
дующую строку, для чего всё условие оборачивается в скобки. Для экономии места
в листинге
3.8
показана только часть кода, отличная от предыдущего примера.
Листинг 3.8. Chapter_OЗ/example_OS/equation.py
if D < О:
print("Y
уравнения нет действительных корней.")
print('Haйти корни в
комплексной области?')
answer = input (' : ')
if (answer == "д" or answer == "да" or
answer == "у" or answer == "yes "):
Часть
80
+ cmath.sqrt(D)) / (2 *
- cmath.sqrt(D)) / (2 *
print("xl:", xl)
print("x2:", х2)
xl =
(-Ь
а)
=
(-Ь
а)
х2
1.
Базовые понятия и встроенные типы
else:
Обратите внимание, что в этом примере для вторых двух условий (answer ==
"у"
or answer == "yes") добавлен еще один отступ, который не является обязатель
ным
-
он сделан исключительно для улучшения читаемости кода, чтобы условие
визуально не сливалось с последующим блоком кода. При этом внутри скобок
можно добавлять произвольное количество отступов.
Выражение
if ... else
Часто бывают ситуации, когда переменной присваиваются разные значения в зави
симости от некоторого условия, как показано, например, в листинге
3.9.
Листинг 3.9. Chapter_OЗ/example_09/if_assigment.py
foo = 6
if foo % 2:
bar = foo * 2
else:
bar = foo // 2
print("bar =", bar)
Если значение переменной foo нечетное, остаток от деления на
нулю (он окажется равным
\),
2
будет не равен
тогда условие после ключевого слова if будет счи
таться истиной, и переменной ьаr присвоено удвоенное значение переменной foo.
В противном случае (если значение foo четное) переменной bar будет присвоено
значение, равное целочисленному делению переменной foo на
В результате выполнения кода из листинга
3.9
2.
будет выведена строка:
bar = 3
Приведенные в коде четыре строки условия можно объединить в одно выражение
if ... else. Синтаксис такого выражения выглядит следующим образом:
Выражение
1 if
Условие
else
Выражение
2
Такое выражение будет равно значению, полученному в результате вычисления
Выражения
1, если Условие равно True (или может быть преобразовано к нему).
В противном случае выражение if ... else будет равно значению, полученному
в результате вычисления Выражения
2. Ветвь else в этом выражении является обя
зательной, а ветвей е 1 i f быть не может.
Глава
3.
Пишем скрипты на
81
Python
Перепишем пример из листинга
тинr
3.9
с использованием выражения
if ... else (лис
3.10).
Листинг 3.10. Chapter_OЗ/example_10/if_assigment.py
foo = 6
bar = foo * 2 if foo % 2 else foo // 2
print("bar =", bar)
Такой код полностью эквивалентен предыдущему. Если вы знакомы с языками С,
С++ и с подобными им языками, то можете заметить, что выражение if ... else в
Python
Цикл
по сути близко к оператору ? : в этих языках.
whi/e
Редкая программа обходится без циклов. Циклы позволяют выполнять одно и то же
действие несколько раз, пока будет выполняться какое-либо условие. Для органи
зации циклов в
Python
есть инструкции с использованием двух ключевых слов: for
и while. Цикл for мы рассмотрим в главе
5 после
того, как изучим работу со спи
сками, а сейчас рассмотрим цикл с использованием ключевого слова while. Син
таксис такого цикла выглядит следующим образом:
while
Условие:
Блок кода
1
else:
Блок кода
2
Когда во время выполнения кода интерпретатор доходит до инструкции while, он
проверяет Условие, и если оно равно тrue (или может быть к нему приведено), то
будет выполняться Блок
кода
1. Как только Блок кода 1 завершится, интерпрета
тор снова проверит Условие, и если оно по-прежнему равно тrue, еще раз выполнит
Блок кода 1, и так будет продолжаться до тех пор, пока Условие не станет равным
False, после чего будет выполнен Блок кода 2 из ветви else. Ветвь else с ее бло
ком кода является необязательной.
В этот момент должен возникнуть вопрос: зачем нужна «лишняя» ветвь кода после
ключевого слова
else,
если Блок
кода
2
можно поместить непосредственно после
всего цикла while, и этот блок кода также будет выполняться после завершения
цикла? Тут есть одна тонкость, заключающаяся в том, что в языке
вует инструкция
break,
Python
сущест
позволяющая прерывать цикл, независимо от того, выпол
няется Условие или нет. Блок кода 2 после ключевого слова else будет выполнять
ся только в том случае, если цикл завершился за счет того, что Условие стало рав
ным
False, но Блок
кода
2 не будет выполняться, если цикл был прерван
инструкцией break. Про инструкцию break мы поговорим уже совсем скоро.
Часть
82
Для демонстрации цикла
1.
Базовые понятия и встроенные типы
while напишем еще одну версию программы, которая ре
шает квадратные уравнения, но после каждого решения спрашивает пользователя,
нужно ли решить еще одно уравнение (листинг
3.11).
Если пользователь вводит "д"
или "у", то скрипт предлагает ввести коэффициенты для следующего уравнения.
Листинr
3.11. Chapter_03/example_11/whlle.py
import cmath
print("Peшeниe уравнений вида а*хл2
+
Ь*х
+
с
О")
process = True
while process:
а= float(input("Bвeдитe а:"))
Ь
float (input ("Введите
"))
Ь:
с= float(input("Bвeдитe с:"))
D=
xl
- 4
Ь**2
х2
*а*
с
(-Ь
+ cmath.sqrt(D)) / (2 *
а)
(-Ь
- cmath.sqrt(D)) / (2 *
а)
print("xl:", xl)
print("x2:", х2)
answer = input(
'Введите
"д" или "у" для решения еще одного уравнения')
process = (answer ==
"д"
or answer ==
"у")
Чтобы продемонстрировать работу инструкции break и ветви else цикла while,
напишем
программу,
которая
запрашивает у пользователя пароль
пытки на то, чтобы он был введен верно (листинг
Листинr 3.12.
3.12).
Chapter_03/example_12/password.py
correct_password
max_attempts = 3
attempts = О
=
"secret"
#
#
Правильный nароль
Максимальное количество попыток
while attempts < max_attempts:
password = inрut("Введите пароль: ")
if password == correct_password:
print("Дocтyп разрешен!")
break
рrint("Неправильный пароль,
попробуйте снова.")
attempts += 1
else:
рrint("Попытки исчерпаны.
Доступ запрешен.")
и дает три
по
Глава
3.
Пишем скрипты на
Python
83
В этом примере переменная attempts содержит количество неудачных попыток
ввода пароля. Цикл выполняется до тех пор, пока количество неудачных попыток
не сравняется со значением
пароль, то,
max_attempts. Если пользователь вводит правильный
помимо надписи о том, что доступ разрешен,
вызывается инструкция
break (она должна располагаться на отдельной строке), которая прерывает выпол
нение цикла while, и ветвь else также не будет выполнена. Если же цикл while
завершится,
потому что станет ложным условие цикла, это значит,
что пользова
тель исчерпал все свои попытки ввести пароль, о чем ему будет сообщено при вы
полнении ветви
else.
Далее показаны примеры запуска скрипта
password.py:
> python password.py
12345
Введите пароль:
Неправильный пароль,
Введите
пароль:
Неправильный пароль,
Введите пароль:
попробуйте снова.
qwerty
попробуйте
снова.
abyrvalg
Неправильный пароль,
Попытки исчерпаны.
попробуйте
Доступ
снова.
запрещен.
> python password.py
Введите пароль:
12345
Неправильный пароль,
Введите
пароль:
попробуйте
снова.
secret
Доступ разрешен'
Помимо инструкции break, в языке
Python
имеется инструкция continue, которая
прерывает не полностью цикл, а только лишь текущую итерацию, заставляя интер
претатор перейти к новой итерации того же цикла. Для демонстрации инструкции
continue напишем скрипт, который запрашивает у пользователя целое число, а за
тем выводит список простых множителей для этого числа (листинг
Листинг 3.13.
num
3.13).
Chapter_03/example_13/prime_numbers.py
= int(input("Bвeдитe целое число:
"))
print("Пpocтыe множители для введенного числа:")
d = 2
while num > 1:
if num % d
print (d)
num //= d
continue
О:
d += 1
В первой строке скрипта мы запрашиваем у пользователя целое число и сразу пре
образуем полученную строку к типу int.
Часть
84
1.
Базовые понятия и встроенные типы
Эта программа последовательно перебирает возможные множители в переменной d,
и если введенное число делится без остатка на очередное число, то это число выво
дится как множитель. Затем введенное число целочисленно делится на найденный
множитель, после чего требуется проверить, нет ли у введенного числа еще одного
такого же множителя. Именно для этого служит инструкция continue -
чтобы
прервать итерацию цикла до увеличения переменной d на единицу. Если бы у цик
ла while в этом примере была ветвь else, то она всегда бы выполнялась, т. к. здесь
не используется инструкция
break,
а инструкция
continue
не прерывает цикл це
ликом. Результат работы этой программы может выглядеть следующим образом:
> python while_continue.py
Введите целое число: 90
Простые множители для
введенного
числа:
2
3
3
5
Если вы имели опыт работы с языками С, С++ или им подобными, то инструкции
while (без ветви else), break и continue должны быть вам знакомы.
Оператор:=
Иногда бывают ситуации, когда хочется в одном выражении объединить расчет
какого-либо значения, присваивание этого значения переменной и сравнение полу
ченного значения с каким-нибудь значением. То есть хотелось бы иметь возмож
ность написать примерно такой код:
if
В
calculate()) >
(а=
Python
операция
О:
..
присваивание а
не
возвращает
ь является инструкцией, а не выражением, т. е. такая
никакого
выражениях (см. начало главы
В
Python 3.8
сваивания,
мому
появился новый оператор
но при
значению.
(walrus),
значения,
и
: =,
среде
ее
нельзя
использовать
в
который работает как инструкция при
этом результат выполнения
В
поэтому
2).
программистов
потому что если его развернуть на
этого
этот
90
оператора равен
оператор
присваивае
называют
«моржик»
градусов, то он будет напоминать
моржа с бивнями.
Рассмотрим простой пример, в котором можно использовать такой оператор для
сокращения количества строк кода. Сначала напишем скрипт без использования
«моржика» (листинг
Листинг
3.14).
3.14. Chapter_OЗ/example_14/no_walrus.py
foo = 5
bar = 2
spam = foo * bar
Глава
3.
Пишем скрипты на
if spam >=
85
Python
О:
рrint("Значение
foo * bar
неотрицательное:",
foo * bar
отрицательное:",
spam)
else:
рrint("Знач ение
spam)
Мы рассчитываем здесь значение для переменной spam, а затем это значение срав
нивается с нулем.
С помощью оператора
тинг
.-
мы можем объединить присваивание и сравнение (лис
3.15).
Листинг 3.15.
Chapter_03/example_15/walrus.py
foo = 5
bar = 2
if (spam := f oo * bar) >= О:
рrint("Значение foo * bar
else:
рrint("Значение foo * bar
неотрицательное:",
отрицательное:",
spam)
spam)
Результат работы этого скрипта не изменится по сравнению с версией, представ
ленной в листинге
3.14.
Обратите внимание на использование скобок вокруг выражения с оператором
: =.
Это важно из-за приоритета операторов. «Моржик» имеет низкий приоритет, по
этому, если скобки не указать, то в выражении:
spam := fo o * bar >=
О
сначала будет вычислено значение foo
* bar >= о, и именно это значение (равное
True) будет присвоено переменной spam.
В качестве менее абстрактного примера напишем новую версию скрипта, который
проверяет введенный пользователем пароль. Используя оператор
: =,
мы можем
объединить в цикле whi le сравнение количества попыток ввода пароля и увеличе
ние этого значения на
l
(листинг
3.16).
Листинг 3.16. Chapter_OЗ/example_16/password.py
correct_password = "secret"
max_attempts = З
attempts = О
#
#
Правильный пароль
Максимальное количество попыток
while (attempts := attempts + 1) <= max_attempts:
password = inрut("Введите пароль: ")
if password == correct_password:
print("Дocтyп разрешен!")
break
рrint("Неправильный пароль,
попробуйте снова.")
else:
рrint("Попытки исчерпаны.
Доступ
запрещен.")
Часть
86
1.
Базовые понятия и встроенные типы
В последующих главах мы столкнемся с другими ситуациями, когда оператор
:=
позволит немного сократить количество строк программы.
Инструкция
assert
В процессе написания или отладки скрипта иногда бывает полезно указать, что в
процессе его выполнения некоторые условия должны выполняться всегда. Если
хотя бы одно из таких условий становится ложным, это свидетельствует об ошибке
в программе, и скрипт должен экстренно прервать свое выполнение. Например, та
кими условиями могут быть предположения о том, что в процессе вычислений мы
получаем какие-то величины, которые должны быть положительными или не быть
равными нулю, или что полученная каким-то образом строка не должна быть пус
той, или файл обязательно должен существовать.
Указать такие предположения мы можем с помощью инструкции
assert, синтаксис
которой выглядит следующим образом:
assert
Условие[,
Комментарий]
Если Условие равно
True, эта инструкция ничего не делает. Если Условие равно
False, то будет возбуждено исключение AssertionError. Если в инструкции
assert указан необязательный комментарий, то он будет выведен вместе с инфор
мацией о возбужденном исключении в сообщении об ошибке.
Напишем простой скрипт, который рассчитывает скорость по заданному расстоя
нию и времени (листинг
Листинг 3.17.
3.17).
Chapter_03/example_17/speed.py
dist = 0.1
time = 250е-12
speed = dist / time
assert speed > О
assert speed <= 299792458,
print("Cкopocть равна:",
"Скорость не должна превЬШJать
speed,
скорость
света"
"м/с")
После расчета скорости в скрипте добавлена проверка полученного результата на
«физичность». Первая инструкция assert утверждает, что значение переменной
speed всегда должно быть положительным, а вторая подобная инструкция утвер
ждает, что значение переменной не должно превышать скорость света. Ко второму
утверждению добавлен комментарий.
В приведенном далее примере второе утверждение неверно, поэтому скрипт завер
шится со следующей ошибкой:
> python speed.py
Traceback (most recent call last):
Глава
3.
Пишем скрипты на
87
Python
File " ... /chapter_03/example_l7/speed.py", line 6, in <module>
assert speed <= 299792458, "Скорость не должна превЬШJать скорость
AssertionError:
не должна превьШJать
Скорость
скорость
света"
света
Инструкция assert работает только в режиме отладки, который используется ин
терпретатором
Python
по умолчанию. Если при запуске интерпретатора в команд
ной строке добавить параметр -о, то режим отладки будет отключен, и все инст
рукции
> python
assert будут игнорироваться:
-О
speed.py
400000000.0
Скорость равна:
м/с
Узнать, используется ли в текущий момент отладочный режим, можно с помощью
глобальной переменной
_ debug_,
щен с параметром -о, и равна
True
которая равна False, если интерпретатор запу
в противном случае.
Инструкции assert полезно расставлять в различных местах скрипта. Они не толь
ко позволяют выявить потенциальные ошибки в процессе работы, но и помогают
программисту лучше понять логику
программы,
получая представление
о том,
с
учетом каких предположений писался скрипт.
Python Enhancement Proposals (РЕР)
К этому моменту уже несколько раз упоминались рекомендации по оформлению
кода. Например, какие имена давать переменным, использовать ровно четыре про
бела для отступов или не допускать строки длиннее
80
символов. Эти и многие
другие рекомендации описаны в официальном документе, который называется
РЕР
8 10 .
РЕР расшифровывается
улучшению
Python.
как
Python Enhancement Proposals - предложения по
Python начинается с
Разработка каждой новой возможности
написания очередного РЕР, в котором автор предлагает внести ту или иную воз
можность, подробно описывает, как она должна работать и какие проблемы решает.
Если сообщество программистов посчитает, что описанное в РЕР поведение полез
но, то над этой возможностью начнут работать, и в очередной версии
Python
эта
возможность будет реализована.
В некоторых РЕР описаны рабочие моменты при работе над интерпретатором
Python.
Например, в РЕР
20 11
содержит «дзен
Python» -
идеологические установки,
которых надо придерживаться при разработке новых возможностей языка. Первые
фразы из этого «дзена» можно перевести так: «Красивое лучше, чем уродливое.
Явное лучше, чем неявное. Простое лучше, чем сложное. Сложное лучше, чем за
путанное».
10
См. https://peps.python.org/pep-0008/.
11
См. https://peps.python.org/pep-0020/.
Часть
88
Интерпретатор
Python
содержит «пасхалку»
-
1.
если выполнить команду import
this, то в консоль будет выведен этот самый «дзен
>>> import this
The Zen of Python,
Ьу
Базовые понятия и встроенные типы
Python»:
Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
РЕР
0 12 является
РЕР
8-
оглавлением по всем РЕР'ам.
это, пожалуй, наиболее известный и самый читаемый РЕР. Именно он
описывает рекомендации по оформлению кода. Помимо уже упомянутых ранее ре
комендаций, там можно найти такие, казалось бы, мелочи, как указания, как рас
ставлять пробелы около скобок и других операторов, как оформлять импорт моду
лей, как оформлять комментарии и документацию, какие давать имена перемен
ным, классам и модулям, и многое другое. Этот РЕР настолько важен, что в
интернете можно найти множество неофициальных русскоязычных его переводов.
Существуют инструменты, которые позволяют автоматически форматировать код
таким образом, чтобы он соответствовал РЕР
autopep8 13 •
8,
и наиболее известный из них
-
это
Кроме того, многие среды разработки (возможно, с помощью дополни
тельных расширений) предлагают возможности для форматирования исходного
текста таким образом, чтобы он соответствовал РЕР
8.
Заключение
Эта глава посвящена созданию скриптов
-
текстовых файлов с программой, кото
рые передаются интерпретатору для выполнения. При создании скрипта можно ис
пользовать различные кодировки файлов, но рекомендуется
UTF-8.
При написании кода полезно добавлять в текст программы комментарии, пояс
няющие не очевидные строки или блоки кода. Комментарии в
Python
начинаются с
символа#, после которого до конца строки текст игнорируется интерпретатором.
В этой главе мы приступили к изучению базовых инструкций языка
Python
и нача
ли с ключевых слов для организации ветвления: if, elif и else. Мы рассмотрели
также выражение if ... else, которое обычно служит для присвоения переменной
того или иного значения в зависимости от условия.
Для выделения блоков кода в языке
Python
применяются отступы. Каждый вложен
ный блок кода должен быть выделен дополнительными отступами. В качестве от
ступа рекомендуется использовать четыре пробела.
12
См. https://peps.python.org/pep-0000/.
13
См. https://pypi.org/project/autopep8/.
Глава
3.
Пишем скрипты на
89
Python
Мы изучили цикл while. Блок кода внутри этого цикла выполняется, пока условие,
записанное после ключевого слова
while,
истинно.
Мы обсудили две инструкции, влияющие на выполнение циклов. Инструкция break
немедленно прерывает цикл, а инструкция
continue
прерывает текущую итерацию
цикла и указывает интерпретатору, что нужно начать выполнять новую итерацию
цикла.
Коротко обсудили оператор
: =,
который позволяет объединить в одной команде
операцию присваивания и сравнение присвоенного значения с какой-то величиной.
И в завершение главы отметим, чего в языке
Python
нет, применительно к ветвле
ниям и циклам по сравнению с другими языками программирования. Во-первых,
нет инструкции goto (или ее аналога), позволяющей перепрыгивать в программе на
любую строчку кода. В сообществе программистов устоялся консенсус относи
тельно того, что такая инструкция является в большинстве случае вредной и делает
программу менее понятной. И, во-вторых, нет аналога цикла while с постусловием,
когда тело цикла выполняется хотя бы один раз, а условие проверяется в конце
итерации, а не в начале.
- ГЛАВА 4 -
Списки, кортежи и массивы
До сих пор мы использовали только простейшие типы данных: целые числа (int),
числа с плавающей точкой (float), строки (str) и булев тип (ьооl). Однако
Python
предоставляет большое количество более сложных классов, которые предназначе
ны для хранения элементов. Такие классы называют коллекция.ми. Перечислим не
которые из них:
♦
списки
класс list;
♦
кортежи
-
класс tuple;
♦
массивы
-
класс array;
♦
словари
♦
множества
♦
неизменяемые множества
-
-
класс dict;
-
класс set;
-
класс frozenset.
Для создания эффективных программ требуется понимание различий этих классов,
жешпельно также хотя бы в общих чертах представлять себе, каким образом каж
дый из этих классов хранит данные. Выбор правильной коллекции с учетом опера
ций, которые над ней будут выполняться, может сильно сказаться на производи
тельности программы и требовании к оперативной памяти.
Списки, кортежи и массивы во многом похожи, методы этих классов во многом пе
ресекаются, и, научившись работать с одной из этих коллекций, полученный опыт
вы сможете применять и к другим подобным классам.
Способы хранения данных
Списки, кортежи и массивы решают схожие задачи -
они позволяют хранить после
довательность данных, доступ к которым осуществляется по целочисленному индексу.
Массивы
Массив (array) -
это структура данных, предназначенная для хранения множества
элементов одного типа. В
Python
массивы могут хранить только целые числа или
Глава
4.
Списки, кортежи и массивы
91
числа с плавающей точкой и не позволяют хранить экземпляры произвольных
классов.
Все элементы массивов индексируются с помощью целых чисел, начиная с нуля.
При этом гарантируется, что все элементы в оперативной памяти располагаются
последовательно без пропусков. На рис.
4.1
схематично показано хранение данных
массива в оперативной памяти. Мы видим здесь массив а, содержащий шесть эле
ментов. Пустые ячейки символизируют свободные ячейки памяти, более светлые
ячейки с записями
ки
-
-
это ячейки памяти, занятые массивом а, темно-серые ячей
это ячейки памяти, занятые другими объектами, расположенными в памяти.
а[О]
Рис.
а[2]
а[1]
4.1.
а[4]
а[З]
а[5]
Схематичное изображение расположения элементов массива
в оперативной памяти
Отдельно стоящая ячейка а В
Python
это переменная, которая ссылается на сам массив.
работа со всеми объектами происходит через ссылки, но, в отличие от
языков С и С++, в
Python
нет арифметики указателей. Интерпретатор отвечает за
выделение памяти и ее освобождение, когда объекты становятся ненужными, а
также за корректность ссылок, и поэтому не позволяет обращаться к произвольной
ячейке оперативной памяти.
Показанная на рис.
4.1
светлая ячейка без индекса символизирует накладные рас
ходы в памяти, связанные с тем, что массив
-
объект
Python,
и он должен содер
жать дополнительные внутренние данные. При таком способе хранения данных
важно, чтобы все элементы в массиве имели одинаковые размеры, поэтому масси
вы могут хранить только однородные данные. Преимуществом такого способа хра
нения элементов является высокая скорость доступа к произвольному элементу по
его
индексу,
поскольку положение каждого
элемента в
памяти легко
вычислить,
зная размер каждого элемента.
Но у такого способа хранения данных имеются недостатки. Один из них заключа
ется в том, что добавление элементов в массив может быть достаточно долгой опе
рацией (массивы в
Python являются
динамическими, то есть в процессе выполнения
скрипта в них можно добавлять новые элементы или удалять имеющиеся). Слож
ность в том, что после последнего элемента массива может не быть непосредствен
но примыкающей свободной памяти, как и показано на рис.
4.1.
В этом случае ин
терпретатору нужно будет найти новую свободную область памяти с учетом добав
ляемых элементов, затем
скопировать
все имеющиеся данные
массива по
новым
адресам памяти, добавить новые данные, а затем освободить память, занимаемую
массивом по предыдущему адресу. Аналогичные действия придется выполнить,
если нужно вставить новый элемент в середину массива.
Часть
92
1.
Базовые понятия и встроенные типы
Массивы отлично подходят для хранения данных, над которыми производят мате
матические операции (например, векторы), а также в случае, когда нужно обра
щаться к элементам массива в произвольном порядке (не обязательно последова
тельно). В главе
25
мы рассмотрим массивы, предоставляемые библиотекой
NumPy,
и там эти преимущества будут особенно хорошо видны.
Списки
Когда говорят про списки (list), то обычно подразумевают структуру в виде це
почки данных, в которой каждый элемент списка ссылается на следующий, как по
казано на рис.
4.2.
Элементы таких списков, в отличие от элементов массива,
не обязаны располагаться в памяти последовательно, а сами данные (ячейки data
на рис.
4.2)
могут занимать в памяти разное количество байтов.
item[O]
ptr_next
item[1]
и
ptr_next
data
data
item[З]
item(2]
_f
ptr_next
и
ptr_next
data
data
Рис.
4.2.
Классическая схема построения списков
Добавление и удаление элементов из списка- сравнительно быстрые операции,
которые не требуют копирования большого количества данных.
Однако классические списки обладают одним существенным недостатком
-
для
доступа к произвольному элементу требуется пройти всю цепочку указателей, на
чиная с самого начала и до нужного элемента, что сильно сказывается на произво
дительности. Такого недостатка лишен список в реализации
Python,
в нем доступ к
произвольному элементу осуществляется не так быстро, как в массивах, но значи
тельно быстрее по сравнению с реализацией классического списка.
Несмотря на то, что в языке
Python
используется термин «списою>, реализация его,
в отличие от классических списков, выглядит совершенно по-другому и представ
ляет собой массив указателей на данные каждого элемента списка, как схематично
показано на рис.
4.3.
Сложности, связанные с добавлением и удалением элементов массива указателей
при такой реализации списка сохраняются, однако при выделении новой памяти
под элементы списка всегда происходит выделение памяти с запасом, чтобы как
можно реже приходилось перераспределять память.
Списки
-
более универсальные хранилища по сравнению с массивами, но при
этом требуют значительно больше оперативной памяти. Несмотря на это, списки в
Python
используются повсеместно.
Глава
4.
Списки, кортежи и массивы
а[О)
93
а[1)
а[2)
а[З)
а[4)
а[5)
data
data
data
data
data
data
Рис.
4.3.
Схема построения списков в
Python
Кортежи
Кортеж
( t up 1 е) -
это неизменяемый список, его элементы задаются в момент соз
дания кортежа и больше не могут измениться. Кортеж не может менять свою дли
ну, добавляя или удаляя элементы. Основное преимущество кортежей перед спи
сками заключается в том, что кортежи занимают меньше оперативной памяти.
Кортежи часто используют для возврата из функций нескольких значений или ко
гда нужно создать группу элементов, объединенных логически, но при этом хочет
ся избежать создания нового класса. Например, для хранения координат точек (х,
у,
z)
вполне можно использовать кортежи, поскольку количество элементов в та
кой структуре не будет меняться, а в зависимости от задач самих точек может быть
достаточно много, и таким образом оперативная память будет расходоваться более
экономно по сравнению со случаем, если бы для их хранения использовался список
или отдельный класс.
Хотя списки могут хранить данные разных типов, списки часто используют для
хранения однородных данных (одного типа), а кортежи
-
для неоднородных.
Главным преимуществом списков и кортежей перед массивами является возмож
ность хранить в них произвольные данные, причем один список или кортеж может
хранить данные разных типов. Однако массивы более компактно хранят данные, и
поэтому для хранения большого количества числовых данных предпочтительнее
использовать именно массивы.
Как видите, разница между коллекциями, представленными в этом разделе, суще
ственна, поэтому в каждом случае нужно задумываться о том, какая из коллекций
будет лучше подходить к вашей задаче по производительности и требованиям к
оперативной памяти.
Далее мы для начала рассмотрим создание упомянутых коллекций, а затем обсу
дим, какие действия мы можем с ними выполнять.
Часть
94
1.
Базовые понятия и встроенные типы
Создание списков
Начнем изучение работы с коллекциями со списков, как с наиболее гибких и часто
используемых классов. Как уже говорилось, список в
Python
представлен классом
list, который является встроенным классом, то есть для его использования не тре
буется импортировать никакой модуль.
Существует несколько способов создания списков, и со всеми мы будем знакомить
ся постепенно. Для создания списка и заполнения его элементами используются
квадратные скобки с перечислением элементов списка:
foo =
[элемент_l,
элемент_2,
... ]
Здесь элемент_ l, элемент_ 2 и так далее
-
это данные, которые нужно поместить в
список. Следующий код показывает создание нескольких списков:
>>> foo = [10, 20, 35, -5, 42, 16]
>>> foo
[10, 20, 35, -5, 42, 16]
»> type(foo)
<class 'list'>
>>> bar = [10, 1.5, 5+2j, 'Строка']
>>> bar
[10, 1.5, (5+2j), 'Строка']
»> baz
[]
>>> baz
[]
Из этого примера видно, что списки действительно имеют класс
list, что тип эле
ментов списка может быть произвольный, и что тип элементов не обязан быть оди
наковым в пределах одного списка. Также мы видим здесь один из способов созда
ния пустого списка. Другой способ создания пустого списка
-
это вызов функции
list () без параметров:
>>> baz = list()
Но для создания пустого списка чаще используют пустые квадратные скобки:
»> baz = []
В предыдущей главе говорилось, что
Python
позволяет вставлять переносы строк
в пределах любых скобок, в том числе и квадратных, что часто используется при
создании длинных списков. Например, сделаем список из строк на основе извест
ного текста
«Lorem ipsum ... »,
который традиционно задействуется, когда нужно
вставить какой-то абстрактный, ничего не значащий текст. В скрипте мы могли
бы написать:
foo = [
"Lorem ipsum",
"dolor sit amet",
"consectetur adipiscing elit",
Глава
4.
Списки, кортежи и массивы
95
"sed do eiusmod",
"tempor incididunt"
Внутри скобок мы можем добавлять отступы и переносы строк так, как нам удобно.
Более того,
Python
разрешает оставлять «лишнюю» запятую после последнего эле
мента списка:
foo = [
"Lorem ipsum",
"dolor sit amet",
"consectetur adipiscing elit",
"sed do eiusmod",
"tempor incididunt",
Разработчики
Python
в документе РЕР
8 (см.
главу
3)
рекомендуют добавлять запя
тую после последнего элемента списка, если каждый элемент списка расположен на
отдельной строке. Они обосновывают это тем, что такие списки часто дополняются
новыми элементами, и при добавлении нового элемента приходится модифициро
вать предыдущую строку, добавляя в нее запятую, и тогда системы управления
версиями
(git, svn
и др.) будут помечать предыдущую строку, где добавлена только
запятая, как измененную, что является избыточным с точки зрения логики внесен
ных изменений.
Если список умещается на одной строке, то запятую после последнего элемента
ставить не рекомендуется, хотя, если ее оставить, это не будет ошибкой с точки
зрения синтаксиса языка:
# Здесь последнюю запятую рекомендуется
foo = [10, 20, 35, -5, 42, 16,]
убрать
В следующей главе мы рассмотрим другие способы создания списка с использова
нием ключевого слова for, а пока перейдем к созданию кортежей.
Создание кортежей
Кортежи
-
это неизменяемые списки. В
Python
они являются экземпляром встро
енного класса tuple. Наиболее часто используемый способ создания кортежа вы
глядит точно так же, как и создание списка, только вместо квадратных скобок ис
пользуются круглые:
foo
= (элемент_!, элемент_2,
... )
Скобки в такой записи не являются обязательными, следующая запись также явля
ется корректной:
foo
= элемент_!, элемент_2,
...
В этих записях элемент_!, элемент_2 и так далее- это данные, которые нужно по
местить в кортеж. Элементы кортежа, как и элементы списка, могут быть различ
ных типов. Следующий пример, аналогичный примеру создания списка из преды-
Часть
96
1.
Базовые понятия и встроенные типы
дущего раздела, демонстрирует создание кортежей с различным содержанием, в
том числе пустой кортеж и кортеж из одного элемента:
>>> foo = (10, 20, 35, -5, 42, 16)
>>> foo
(10, 20, 35, -5, 42, 16)
»> type(foo)
<class 'tuple'>
>>> bar = 10, 1.5, 5+2j, 'Строка'
>>> bar
(10, 1. 5, (5+2j),
()
>>> baz
'Строка')
>>> baz
()
>>> bam = (42,)
>>> bam
(42,)
Обратите внимание на создание кортежа bam из одного элемента. Здесь запятая по
сле его единственного элемента обязательна: если бы ее не было, то интерпретатор
воспринял бы скобки в инструкции bam =
( 4 2) как скобки, используемые в мате
матических выражениях, и тогда переменной ьаm было бы присвоено целочислен
ное значение
42.
В этом выражении круглые скобки также можно было бы не пи
сать:
>>> bam = 42,
>>> bam
(42,)
Другой способ создания пустого кортежа
-
это вызов функции tuple () без пара
метров:
»> baz = tuple ()
>>> baz
()
Создание массивов
Массив в
Python
представлен классом array, но в отличие от list и tuple, он не
является встроенным классом, поэтому его нужно импортировать из модуля
array.
При создании массива также необходимо указать тип данных, который он будет
хранить. Формат конструктора массива описывается следующим синтаксисом:
array(typecode, [, initializer])
Здесь typecode -
это строка, указывающая тип хранимых данных, а необязатель
ный параметр initializer -
это значение, используемое для инициализации мас
сива при его создании (это может быть, например, список из целых чисел или чисел
с плавающей точкой).
Глава
4.
Списки, кортежи и массивы
97
Возможные значения параметра typecode показаны в табл.
Таблица
4.1.
4.1.
Возможные значения параметра typecode
Минимальный размер
typecode
Аналог из языка С
'Ь'
signed char
int
1
'В'
unsigned char
int
1
'h'
signed short
int
2
'Н'
unsigned short
int
2
i
signed int
int
2
, I,
unsigned int
int
2
, l'
signed long
int
4
'L'
unsigned long
int
4
'q'
signed long long
int
8
'Q'
unsigned long long
int
8
'f'
float
float
4
'd'
douЫe
float
8
1
1
Тип
Python
в байтах
Следующий пример показывает создание двух массивов: пустого
-
для хранения
чисел с плавающей точкой двойной точности и массива целых чисел. Массив це
лых чисел инициализируется на основе элементов списка, который передается в
качестве второго параметра:
>>> import array
>» foo
array. array ("d")
»> bar
array.array("i", [10, 20, 30, 40])
>» foo
array( 'd')
»> type(foo)
<class 'array.array'>
»> bar
array('i', [10, 20, 30, 40])
Обратите внимание на импорт модуля array и использование одноименного класса
на последующих строках.
Часть
98
1.
Базовые понятия и встроенные типы
Этот же пример мы можем переписать с использованием явного импорта класса из
модуля:
>>> from array import array
>>> foo
array ( "d")
»> bar = array ("i", [10, 20, 30, 40])
В следующих примерах, которые выполняются в интерактивном режиме, импорт
класса array из модуля array будет опускаться, подразумевая, что класс array уже
был импортирован ранее.
При попытке записать в массив значение, большее, чем может хранить элемент с
учетом
выделенного
ему
количества
байтов,
будет
возбуждено
исключение
OverflowError:
>>> foo = array("B",
»> foo[0] += 1
[255, 10, 20, 30])
Traceback (most recent call last):
File "< ... >", line 1, in <module>
foo[0] += 1
OverflowError: unsigned byte integer is greater than maximum
Преобразование списков, кортежей
и массивов друг в друга
Функции array (), list () и tuple () предназначены для создания массива, списка и
кортежа соответственно. Они могут принимать в качестве необязательного пара
метра экземпляр любого класса, который можно интерпретировать как последова
тельность элементов.
В предыдущем разделе мы уже видели преобразование списка в массив:
bar = array ("i", [10, 20, 30, 40])
Вместо списка в качестве второго параметра мог быть и кортеж:
bar = array("i",
(10, 20, 30, 40))
Однако при этом надо соблюдать осторожность и быть уверенным, что все элемен
ты списка или кортежа могут быть преобразованы к типу, который задается в пер
вом параметре функции array (). Если какой-нибудь из элементов не удастся пре
образовать к нужному типу, будет возбуждено исключение TypeError:
>» bar = array("i",
[10, 20, 30.5])
Traceback (most recent call last):
File "< ... >", line 1, in <module>
bar = array("i",
[10, 20, 30.5])
TypeError: 'float' object cannot
Ье
interpreted as an integer
Глава
4.
Списки, кортежи и массивы
99
Массивы и кортежи могут быть преобразованы в списки с помощью функции
list ():
>>> foo = (10, 20, 30.5)
>>> spam = list(foo)
>» spam
[10, 20, 30.5]
»> bar = array("i", [15, 25, 35])
>>> eggs = list(bar)
»> eggs
[15, 25, 35]
Аналогично работает функция tuple ():
>>> foo = [10, 20, 30.5]
>>> spam = tuple(foo)
»> spam
(10, 20, 30.5)
»> bar = array("i", [15, 25, 35])
>>> eggs = tuple(bar)
»> eggs
(15, 25, 35)
Теперь мы умеем создавать списки, кортежи и массивы несколькими способами.
В следующих разделах мы рассмотрим операции, которые можно применять для
этих классов. Несмотря на значительные различия в организации хранения данных,
внешний интерфейс у классов list, tuple и array имеет много общего, и таким
образом мы будем изучать эти классы параллельно.
Доступ к элементам по индексу
Прежде чем говорить об особенностях доступа к элементам коллекций, нужно ска
зать о функции, предназначенной для определения количества элементов в коллек
ции. Это встроенная функция len () (от английского слова
length- длина),
пользо
ваться ей очень просто:
>>> foo = [10, 20, 30, 42]
»> bar = array("i", [О, 2, 4, 6, 8])
»> рrint("Длина списка foo:", len(foo))
Длина списка
foo: 4
>» print ( "Длина
Длина массива
массива
bar: ", len (bar) )
bar: 5
Функция len () замечательна тем, что она универсальная и предназначена для оп
ределения количества элементов любой коллекции, в том числе словарей и мно
жеств (о них мы будем говорить в главах
6
и
7).
Также с помощью функции len ()
можно определять длину строк. Если говорить более корректно, функция len ()
может
работать
с
любым
классом,
который
реализует
специальный
метод
Часть
100
len
(),
но
здесь
мы
сильно
забегаем
1.
Базовые понятия и встроенные типы
вперед
в
ориентированного программирования, а это тема главы
особенности
объектно
15.
Для доступа к элементам списков, кортежей и массивов используется оператор
[J
(квадратные скобки), при этом нумерация элементов начинается с О. Индекс эле
ментов последовательности можно себе представить как смещение позиции эле
мента относительно начала последовательности. Индекс О определяет первый эле
мент
декс
1
у него нулевое смещение, он находится в начале последовательности. Ин
определяет второй элемент, который расположен за первым элементом.
Таким образом, если у нас в последовательности М элементов, то последний эле
мент имеет индекс М
- 1,
так как именно такое количество элементов расположено
до последнего элемента.
Дополним предыдущий пример следующими строками:
>>>
рrint("Первый элемент
Первый элемент
>>>
print("Tpeтий элемент
Третий элемент
>>>
foo:", foo[0])
foo: 10
foo:", foo[2])
foo: 30
рrint("Последний элемент
Последний элемент
foo:", foo[len(foo) - 1])
foo: 42
Обратите внимание, что индекс последнего элемента на единицу меньше количест
ва элементов в списке/массиве, т. к. нумерация элементов начинается с О.
В программах часто приходится отсчитывать элементы с конца коллекции, но пи
сать каждый раз выражения вида
len (foo) - N, где N - это номер элемента, от
считываемый с конца, громоздко. Поэтому в
Python есть
специальный вид индекса
ции для отсчетов элементов с конца. Для этого используются отрицательные ин
дексы:
элемент
с
индексом
это
-1 -
последний
элемент,
с
индексом
предпоследний и т. д. Для доступа к последним двум элементам списка
-2 -
foo мы мо
жем использовать следующую запись:
>>>
рrint("Последний элемент
Последний элемент
>>>
foo:", foo[-1])
foo: 42
рrint("Предпоследний элемент
Предпоследний элемент
Рис.
foo:", foo[-2])
foo: 30
4.4 иллюстрирует индексацию элементов
как с начала, так и с конца коллекции.
М элементов
__________А__________
с
Индексы
Рис.
4.4.
]
[О]
[1]
[2]
[М-3]
[М-2)
[М- 1)
[-М]
[-М - 1)
[-М-2)
[-3)
[-2)
[-1)
Индексация элементов с начала (верхний ряд индексов) и конца
(нижний ряд индексов) последовательностей
Глава
4.
Python
Списки, кортежи и массивы
101
всегда проверяет, существует ли в коллекции элемент с запрашиваемым ин
дексом, чтобы предотвратить выход за пределы коллекции. Если вы попытаетесь
получить элемент по не существующему индексу, то интерпретатор возбудит ис
ключение IndexError. В следующем примере у списка foo всего
симально возможный индекс
3),
4
элемента (мак
а мы запрашиваем 5-й элемент (с индексом
4):
>>> foo = [10, 20, 30, 40]
>» bar = foo[4]
Traceback (most recent call last):
File "< ... >", line 1, in <module>
bar = foo[4]
IndexError: list index out of range
Доступ к элементам массивов и кортежей работает точно так же.
По индексам элементы списков и массивов можно не только читать, но и присваи
вать им значения:
>>> foo = [10, 20, 30, 40]
>» foo[0] = -3
»> foo
[-3, 20, 30, 40]
Однако для кортежей такая операция недопустима, поскольку кортеж по определе
нию является неизменяемым. Давайте в этом убедимся:
>>> foo = (10, 20, 30, 40)
>» foo[0] = -3
Traceback (most recent call last):
File "< ... >", line 1, in <module>
foo[0] = -3
TypeError: 'tuple' object does not support item assignment
Срезы
Помимо того, что мы можем получать одно значение из списка, кортежа или мас
сива,
Python позволяет
с помощью одной команды извлечь из последовательностей
целую группу элементов. Для этого предназначен оператор
[J
с использованием
следующего формата записи:
foo[start:stop:step]
Такую операцию называют получением среза
(slice).
Здесь start и stop -
это ин
дексы в исходной коллекции. Если такой оператор стоит справа от знака присваи
вания, то он возвращает новый список, кортеж или массив, в который будут скопи
рованы элементы исходной коллекции, начиная с индекса
stop - l
включительно с шагом
step.
start и до индекса
Часть
102
1.
Базовые понятия и встроенные типы
При этом важно запомнить, что элемент с индексом s tart попадет в срез, а элемент
с индексом
stop -
не попадет, то есть такая операция выделяет срез на полуот
крытом интервале: [start, stop).
При этом в качестве индексов можно использовать как положительные числа
для индексации от начала последовательности, так и отрицательные
-
-
для индек
сации с конца.
В некоторых случаях можно использовать упрощенную запись оператора взятия
среза и не указывать следующие элементы:
♦
шаг s tep, если он равен
♦
первый индекс start, если он равен О;
♦
последний индекс stop, если он равен количеству элементов в коллекции.
1;
Для примера рассмотрим случай, когда шаг выборки среза равен
1:
»> foo
[10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = foo[l:6:1]
>>> bar
[20, 30, 40, 50, 60]
>>> bam = foo[l:6:]
>>> bam
[20, 30, 40, 50, 60]
>>> baz = foo[l:6]
>>> baz
[20, 30, 40, 50, 60]
Здесь с помощью срезов выделяются одни и те же элементы в три переменные.
Сначала используется полная запись: foo [ 1: 6: 1 J. Поскольку шаг равен
1,
то его
можно не писать, при этом двоеточие перед шагом можно оставить, а можно тоже
опустить.
В списки bar, baz и bam попадут элементы, начиная со второго элемента из списка
foo (с индексом
декс
5,
1),
а последний элемент, который попадет в эти списки, имеет ин
поскольку элемент с индексом
6 уже
не должен включаться в срез.
Следующий пример показывает работу выделения среза с шагом, отличным от
1:
»> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = foo[2:6:2]
>>> bar
[ 30, 50]
Первый элемент, который попадает в срез, имеет индекс
элемент в исходном списке имеет индекс
(число
70) уже
4
(число
50),
2
(число
30),
следующий
а вот элемент с индексом
6
не попадает в срез.
Следующие примеры показывают применение отрицательных индексов для отсчета
элементов с конца коллекции. Для разнообразия здесь будут использоваться кортежи:
»> foo = (10, 20, 30, 40, 50, 60, 70, 80)
>>> bar = foo[2:-l]
Глава
4.
Списки, кортежи и массивы
103
>» bar
(30, 40, 50, 60, 70)
>>> baz = foo[-4:-2]
>» baz
(50, 60)
Теперь рассмотрим случаи, когда можно не указывать начальный и конечный ин
дексы. В следующих примерах показывается, как можно копировать все элементы
списка в новый список:
»> foo
[10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = foo[O:len(foo) :1]
»> bar
[10, 20, 30, 40, 50, 60, 70, 80]
>>> baz = foo[O:len(foo)]
»> baz
[10, 20, 30, 40, 50, 60, 70, 80]
>>> bam = foo[O:]
»> bam
[10, 20, 30, 40, 50, 60, 70, 80]
»> baf = foo[:]
»> baf
[10, 20, 30, 40, 50, 60, 70, 80]
Все эти команды получения срезов эквивалентны. После выполнения этих команд
переменные bar, baz, bam и baf будут содержать копии исходного списка foo (по
чему для копирования списка недостаточно написать просто bar = foo, будет ска
зано далее). Разумеется, на практике для копирования списков обычно используют
самую короткую запись: baf = foo [:
J.
Синтаксис
Python
позволяет использовать
еще одну запись, которая не приведена в предыдущих примерах:
spam = foo [::
J.
Результат выполнения такой команды не будет отличаться от выполнения команды
spam = foo [:].
На рис.
4.5
графически показаны выделяемые элементы для срезов с использовани
ем разных форм индексации.
Если в операторе взятия среза указан отрицательный шаг, то элементы в результи
рующей коллекции будут располагаться в обратном направлении. Таким способом
мы можем перевернуть список или его часть. Рассмотрим несколько примеров:
>» foo = [10, 20, 30, 40, 50, 60, 70, 80]
»> bar = foo[::-1]
>» bar
[80, 70, 60, 50, 40, 30, 20, 10]
>>> baz = foo[-2:0:-1]
>» baz
[70, 60, 50, 40, 30, 20]
= foo[-2: :-1]
»> bam
[70, 60, 50, 40, 30, 20, 10]
>>> bam
Часть
104
1.
Базовые понятия и встроенные типы
>>> baf = foo[-2:2:-2]
»> baf
[70, 50]
foo[2:M-2]
[О]
[1]
г
А
[2]
1
[M-зil
[М-2]
[М-1]
[М-2]
[М-1]
[М-3)
[М-2]
[М-1]
[М-3]
[М-2]
foo[:M-2]
г [О]
[1]
1
[~
[м-з1l
foo[2:]
[О)
[1)
[2]
foo[:]
А
[2]
[1]
Рис.
4.5.
[М-1~
Получение срезов
Здесь с помощью операции f оо [ : : -1 J создается полная копия списка, но перевер
нутого задом наперед. Следующая операция: f оо [ -2 : о : -1
J-
выделяет все эле
менты списка, кроме первого и последнего. Обратите внимание, что элемент с ин
дексом О не будет включаться в результат, т. к. О
-
это правая граница интервала, и
если мы хотим, чтобы первый элемент был включен в результат, ноль нужно опус
тить, как это сделано в следующей операции: foo[-2: :-lJ. Последняя операция:
foo [-2: 2: -2 J -
показывает, что и при отрицательном шаге мы можем выбирать не
все элементы подряд.
Выполнение присваивания для сложных объектов.
Операторы
is и is not
В предыдущем разделе для копирования списков использовалась операция foo [:
J
и некоторые ее вариации. Рассмотрим ситуацию, когда эта команда необходима, и
почему не всегда достаточно обычной инструкции присваивания. Напишем про
стой пример:
>>> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = foo
>>> foo
[10, 20, 30, 40, 50, 60, 70, 80]
Глава
4.
Списки, кортежи и массивы
105
>>> bar
[10, 20, 30, 40, 50, 60, 70, 80]
На первый взгляд здесь всё хорошо. Так в чем же подвох? Изменим первый эле
мент в списке
bar:
>>> bar[0] = 1000
>>> bar
[1000, 20, 30, 40, 50, 60, 70, 80]
А теперь посмотрим, что стало со списком
foo:
>>> foo
[1000, 20, 30, 40, 50, 60, 70, 80]
Первый элемент изменился и в списке foo! Вполне возможно, это не то, чего мы
хотели добиться.
Почему же так произошло? Поскольку
Python-
объектно
ориентированный язык, то все переменные являются ссылками на какие-то объек
ты. Инструкция присваивания создает не новый объект списка, а лишь новую ссыл
ку на уже существующий объект, как показано на рис.
4.6.
foo}
10 .....____
60 _.___
80___,
30 ...___
40 _.___
70 __.__
20 ___.____
50 __...____
..____
Ьаг
Рис.
4.6.
Схематичное представление результата выполнения команды
bar = foo
Некоторые объекты, такие как целые, действительные и комплексные числа, строки
и кортежи, являются неизменяемыми, и при работе с ними такая особенность никак
себя не проявляет. Но большинство других классов могут изменять свое состояние,
и тогда при использовании инструкции присваивания надо быть осторожным. Из
менив состояние объекта посредством одной переменной, вы можете изменить со
стояние класса и для другой переменной, как в предыдущем примере. Состояние
класса list после команды bar [о]
foo}
= 1000 показано на рис
4.7.
___ __ ___ __ __ __ __ __
1000 _._ 20 ......._
30
..__ 40 _,_ 50 __,..__ 60 _,_ 70 ........
80__.
Ьаг
Рис.
4.7.
Одновременное изменение элемента списков
foo
и
bar
Такой подход повышает производительность программы, поскольку не требуется
копировать большие объекты в памяти, когда в этом нет необходимости, но требует
аккуратности и внимательности от программиста.
Если нужно создать новый список из элементов старого, то мы должны явно об
этом заявить, например, с использованием конструкции
>>> foo
=
[10, 20, 30, 40, 50, 60, 70, 80]
»> bar
=
foo[:]
[ : J:
Часть
106
>>> bar[0)
>>> bar
1.
Базовые понятия и встроенные типы
1000
=
[1000, 20, 30, 40, 50, 60, 70, 80)
>>> foo
[10, 20, 30, 40, 50, 60, 70, 80)
Здесь после инструкции присваивания
bar [0J = 1000 исходный список foo оста
нется в неизменном виде, поскольку для переменной bar будет создан новый спи
сок. Данные, содержащиеся в списках foo и bar после изменения элемента пере
менной bar, показаны на рис. 4.8.
foo ----,...._1о_........_2_0___3_о___._4_о_....__5_о_ _6_о_
_.___7_о_.___8о___,
bar ----,...._1_00_0__,__2_0_..___3_0__._4_о_....__5_о_'--6_о_
_.___7_о_.___8о___,
Рис.
4.8.
Изменение элемента списка
bar,
когда
bar -
копия списка
foo
После этого может возникнуть вопрос, каким образом работает оператор сравне
ния==, он сравнивает содержимое коллекций или эквивалентность ссылок на них?
Напишем несколько команд, которые формируют структуру, показанную на рис.
foo
bar
spam
~
10
20
30
40
50
60
70
80
4
10
20
30
40
50
60
70
80
Рис.
4.9.
4.9.
Два списка с одинаковым содержимым и три ссылки на них
У нас будут два отдельных объекта списка с одинаковым содержимым и три ссыл
ки, две из которых относятся к одному и тому же объекту:
»>
>>>
»>
>>>
foo = [10, 20, 30, 40, 50, 60, 70, 80)
bar = foo
spam = [10, 20, 30, 40, 50, 60, 70, 80)
foo
==
bar
>>> foo
==
spam
True
True
Из этого эксперимента видно, что при сравнении списков, помимо сравнения ссы
лок, также учитывается содержимое списков.
Но в
Python
есть возможность проверить, являются ли переменные ссылками на
один и тот же объект. Для этого предназначены оператор идентичности is и его
инверсия
-
переменная_l
переменная_l
is not. Синтаксис этих операторов можно описать следующим образом:
is переменная_2
is not переменная_2
Глава
4.
Списки, кортежи и массивы
107
Оператор is возвращает True, если оба его операнда (переменная_l и перемен
ная_2) ссылаются на один и тот же объект в памяти, иначе он возвращает False.
Оператор is not работает противоположным образом
-
он возвращает тrue, если
два его операнда (переменная_l и переменная_2) ссылаются на разные объекты, и
возвращает False, если оба операнда ссылаются на один и тот же объект.
Дополним предыдущий пример еще несколькими сравнениями:
»> foo is bar
True
>» foo is spam
False
»> foo is not bar
False
>» foo is not spam
True
В
Python
каждый объект имеет идентификатор, который представляет собой целое
число. Гарантируется, что у двух одновременно существующих объектов иденти
фикаторы не могут быть равны.
Python
предоставляет встроенную функцию id (),
которая в качестве параметра принимает объект и возвращает идентификатор этого
объекта. В реализации
CPython
функция id () возвращает адрес объекта в памяти.
Добавим в предыдущий пример еще несколько строчек, чтобы вывести идентифи
каторы объектов, на которые указывают переменные foo, bar и spam:
»> id(foo)
139848256819392
>» id(bar)
139848256819392
>» id(spam)
139848256817984
При каждом запуске интерпретатора выводимые идентификаторы окажутся разны
ми, но принцип будет сохраняться: идентификаторы у переменных foo и ьаr будут
совпадать, а у spam будет другой идентификатор. В процессе выполнения програм
мы объект может быть удален, и на его месте в оперативной памяти будет создан
другой объект, и тогда эти два объекта, не пересекающиеся по времени жизни, мо
гут иметь одинаковые идентификаторы.
Операторы is и is not сравнивают именно эти идентификаторы.
В главе 2 мы познакомились с объектом None, который имеет тип NoneType. Здесь
еще раз подчеркнем, что объект None является единственным глобальным объектом
этого типа, и поэтому, когда требуется сравнение переменной со значением None, в
РЕР
и
8 рекомендуется
is not,
»> foo = None
>>> bar = [1, 2, 3]
»> foo is None
True
использовать не операторы сравнения== и ! =, а операторы is
как показано в следующем примере:
108
Часть
1.
Базовые понятия и встроенные типы
>>> bar is not None
True
Использование операторов == и ! = не является ошибкой, но, благодаря применению
операторов is и is not, мы подчеркиваем особую роль объекта None.
Аналогично объекту None ведет себя пустой кортеж, поэтому, когда мы его создаем,
не создается новый объект, а используется уже существующий, и таким образом
хоть немного, но экономится память:
»> foo = ()
»> bar = ()
>>> foo is bar
True
»> id(foo)
139848273851880
»> id(bar)
139848273851880
Рассмотрим еще один пример, который, на первый взгляд, противоречит тому, что
кортеж
неизменяемый объект:
-
»> foo = (10, 20, [1, 2, 3])
>>> foo[-1]
>>> foo
(10, 20,
[О]
= 100
[100, 2, 3])
Как же так, ведь кортеж по определению не должен менять свое содержимое?
На самом деле кортеж его и не поменял. Кортеж содержит лишь ссылки на объек
ты, в том числе и на список. С помощью выражения f оо [ -1 J мы получаем ссылку
на список, а затем по этой ссылке можем изменить сам список, что мы и сделали.
С точки зрения кортежа, ссылка не изменилась, а за объект, на который указывает
ссылка, кортеж не отвечает,
-
он вполне может менять свое состояние.
В завершение разговора об операторах сравнения давайте проведем эксперимент и
посмотрим, будут ли равны между собой список и массив, если они содержат оди
наковые элементы:
»> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = array("i", foo)
>>> foo == bar
False
Здесь мы увидели, что хотя список и массив содержат одинаковые элементы, но
поскольку они относятся к разным классам, оператор сравнения вернет значение
False. Аналогичный эксперимент можно провести со списком и кортежем
зультат будет таким же:
»> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>» bar = (10, 20, 30, 40, 50, 60, 70, 80)
>>> foo == bar
False
-
ре
Глава
4.
Списки, кортежи и массивы
Операторы
109
in и not in
На практике часто требуется определить, содержится ли какое-либо значение в
списке или другой коллекции. Для проверки принадлежности или непринадлежно
сти элемента к коллекции предназначены операторы
in
и
not in,
которые имеют
следующий синтаксис:
Элемент
in
Элемент
not in
Коллекция
Коллекция
Оператор in возвращает значение тrue, если значение, хранимое в переменной
Элемент, содержится в Коллекции, и возвращает False в противном случае. Опера
тор not in является инверсией оператора in. Рассмотрим примеры:
>» foo = [1, 2, -3, 42, [10, 20]]
>» spam = [10, 20]
>» 42 in foo
True
>>> 50 not in foo
True
>» 60 in foo
False
>» spam in foo
True
Обратите внимание на последнее сравнение
-
с переменной spam. Этот пример
показывает, что при проверке принадлежности элемента коллекции выполняется не
проверка на идентичность (с использованием оператора
is),
а проверка на равенст
во (с использованием оператора==).
Распаковка элементов коллекций
Под распаковкой понимается операция над коллекцией, в результате которой от
дельным переменным присваиваются значения элементов коллекции. Синтаксис
распаковки выглядит следующим образом:
Переменная_ 1,
Переменная_ 2,
... ,
Переменная_n
=
Коллекция
Количество переменных Переменная_ 1, Переменная_ 2, .. , Переменная_ n
должно
быть ровно таким же, как и количество элементов в коллекции. В результате этой
операции переменным Переменная_ 1, Переменная_ 2, .. , Переменная_ n будут при
своены значения соответствующих элементов из Коллекции.
Операция распаковки показана в следующем примере:
>>> spam = (10, 20, 30)
>>> foo, bar, baz = spam
»> foo
10
Часть
110
1.
Базовые понятия и встроенные типы
>>> bar
20
>>> baz
30
Если же количество переменных, указанных слева от знака=, не будет равно коли
честву элементов в коллекции, то будет возбуждено исключение valueError:
>>> foo = (10, 20, 30)
>>> bar, baz = foo
Traceback (most recent call last):
File "< ... >", line 1, in <module>
bar, baz = foo
ValueError: too many values to unpack (expected 2)
Интерпретатор здесь сообщает нам, что слева от знака
= ожидаются
два значения
для распаковки, а коллекция содержит больше значений.
Распаковка часто используется для имитации поведения, как будто функция воз
вращает несколько значений, хотя на самом деле она возвращает одно значение
-
кортеж из нескольких элементов:
foo, bar = func()
Иногда распаковку можно увидеть в более неожиданных местах. Например, ее
можно использовать для обмена значений двух и более переменных:
>>>
10
20
>>> у, х = х, у
>>> print("x:",
х: 20 у: 10
х
>»у=
х,
"у:",
у)
Здесь нет ничего необычного, просто справа от знака равенства создается кортеж из
двух элементов со значениями х и у (мы же помним, что скобки при создании кор
тежа можно не указывать), а затем происходит распаковка этого кортежа в пере
менные у их.
Если распаковываемая коллекция содержит большое количество элементов, но из
нее
нам
лом
«*»,
нужны
только
некоторые
элементы,
то
можно
воспользоваться
симво
который указывает, что часть элементов коллекции при распаковке следу
ет поместить в одну переменную в виде списка. Рассмотрим несколько примеров
использования такой возможности:
>>> а, Ь, *с = (10, 20, 30, 40, 50, 60)
>>> а
10
»> ь
20
>>> с
(30, 40, 50, 60]
>>> *а, Ь, с = (10, 20, 30, 40, 50, 60)
Глава
4.
Списки, кортежи и массивы
111
>>> а
[10, 20, 30, 40]
»>
ь
50
»>
60
>>>
>>>
10
»>
с
а,
*Ь,
с
(10, 20, 30, 40, 50, 60)
а
ь
[20, 30, 40, 50]
>>> с
60
Переменная со звездочкой может находиться в любом месте списка переменных
для распаковки, но такая переменная может быть только одна. Интерпретатор при
своит этой переменной список значений, которые не попали в другие переменные
для распаковки.
Мы можем распаковывать вложенные коллекции, но для этого интерпретатору
нужно подсказать, какая ожидается структура вложений:
»> spam = (10, 20, (30, 40, 50))
»> а, Ь, (с, d, е) = spam
>» а
10
»>
ь
20
>>>
с
30
»> d
40
»>
е
50
Основные методы классов
list, tuple
и
array
Помимо рассмотренных к этому моменту общих операторов, которые можно при
менять ко многим коллекциям, у классов
list, tuple
и
array
имеются также внут
ренние функции (которые в терминах объектно-ориентированного программирова
ния называются ,wетодани). Прежде чем мы их изучим, рассмотрим еще одну
встроенную функцию
Python.
Это функция di r () , которая возвращает список всех
сущностей (переменных и методов), содержащихся в классе. Иногда, если вы забы
ли, как называется тот или иной метод класса, быстрее заглянуть внутрь класса с
помощью функции dir (), чем искать описание класса в документации. Но функция
di r ()
не заменяет документацию, поскольку с ее помощью вы можете узнать толь
ко имена функций, но не их параметры.
112
Часть
1.
Базовые понятия и встроенные типы
Применим функцию dir () к классу list:
»> dir(list)
['
add
class
class _getitem_
contains
delattr
1
1
delitem
dir
format 1
doc
ge
_eq_
_getattribute_',
getitem_ 1
_getstate- 1
hash
iadd
gt
imul
init 1
iter 1 1 le
init subclass
len 1
I new
lt '
mul
ne
reduce '
reduce ех
' repr
reversed
' rmul ',
setattr , ' seti tem
, ' sizeof ' ,
str ',
subclasshook .---; 'append' ,'clear' ,'сору.-; 'count~ 'extend', 'index'~'insert',
'рор',
'remove', 'reverse', 'sort']
Пока мы не станем обращать внимание на методы, которые начинаются и заканчи
ваются двумя символами подчеркивания. На жаргоне
скими
(magic),
Python
их называют магиче
или dunder-мemoдaмu (сокращение от слов douЫe
underscores-
двoйныe подчеркивания), и мы к ним вернемся, когда будем говорить про объектно
ориентированное программирование в главе
15.
Обратите внимание, что в конце
полученного списка указаны методы, описание которых можно найти в документа
ции к классу list 1. Чтобы не повторять дословно документацию, сведем имеющие
ся в этом классе методы в табл.
list, tuple
и
array,
4.2,
где заодно отметим методы, общие для классов
а затем рассмотрим примеры использования этих методов.
Таблица
Методы
Наличие анало-
Наличие анало-
класса
гичного метода
гичного метода
list
в классе
array
в классе
4.2.
Методы класса 1 i s
Описание
tuple
+
-
+
-
+
-
+
-
+
-
clear ()
-
-
Очистить коллекцию
index ()
+
+
Найти элемент в коллекции
+
+
-
-
Append ()
extend()
insert ()
remove()
рор()
count ()
sort ()
1
t
Добавить элемент в конец коллекции
Добавить несколько элементов
в конец коллекции
Добавить элемент в указанную
позицию коллекции
Удалить элемент из коллекции
Удалить элемент в заданной
позиции коллекции и вернуть его
Вернуть количество заданных элементов в коллекции
Сортировать коллекцию
См. https://docs.python.org/3/tutorial/datastructures.html#more-on-lists.
Глава
Списки, кортежи и массивы
4.
113
Таблица
Методы
Наличие анало-
Наличие анало-
класса
гичного метода
гичного метода
list
в классе
array
в классе
4.2
(окончание)
Описание
tuple
сору()
-
-
Сделать копию коллекции
reverse ()
+
-
Инвертировать коллекцию
Сначала рассмотрим работу методов, которые добавляют новые элементы в список
или массив. Разумеется, подобных методов нет у класса tuple, поскольку он явля
ется неизменяемым. Начнем с метода append (), который предназначен для добав
ления нового элемента в конец коллекции. Он довольно часто используется для за
полнения списка данными:
»> foo
=
(10, 20, 30, 40, 50, 60, 70, 80]
>>> foo.append(l00)
>» foo. append ( "text")
>» foo
(10, 20, 30, 40, 50, 60, 70, 80, 100, 'text']
Как можно здесь видеть, метод append () изменяет содержимое списка, добавляя в
конец один новый элемент. Этот же пример еще раз напоминает, что списки могут
хранить значения разных типов.
Следующий пример демонстрирует работу метода extend (), который напоминает
метод append (), но позволяет добавлять в конец коллекции сразу несколько эле
ментов. В качестве своего параметра метод extend ()
принимает коллекцию эле
ментов, которые надо добавить:
»> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>» foo.extend( (100, "text"))
>» foo
(10, 20, 30, 40, 50, 60, 70, 80, 100, 'text']
В этом примере новые элементы добавляются из кортежа.
Если элементы нужно вставить в произвольную позицию коллекции, то можно вос
пользоваться методом insert (), который принимает номер позиции, куда нужно
вставить новый элемент, и, собственно, сам добавляемый элемент:
»> foo
=
[10, 20, 30, 40, 50, 60, 70, 80]
>>> foo.insert(0, 100)
>» foo
(100, 10, 20, 30, 40, 50, 60, 70, 80]
>>> foo.insert(3, "text")
»> foo
(100, 10, 20, 'text', 30, 40, 50, 60, 70, 80]
Этот пример последовательно добавляет по одному элементу: в начало списка (по
зиция О), а затем в середину (позиция
3).
Часть
114
1.
Базовые понятия и встроенные типы
Следующая группа примеров показывает различные способы удаления элементов
из списка и массива. Если нам известно значение (не индекс), которое нужно уда
лить из списка или массива, то можно воспользоваться методом remove () . Если в
коллекции содержится несколько элементов с удаляемым значением, то будет уда
лен только один такой элемент с наименьшим индексом:
>>> foo = [10, 20, 35, -5, 42, 1, 42, 2, 42, 16]
>>> foo.remove(42)
>>> foo
[10, 20, 35, -5, 1, 42, 2, 42, 16]
Если нужно удалить все элементы с заданным значением, можно воспользоваться
циклом
while
и оператором
списке (листинг
in
для проверки существования нужного значения в
4.1 ).
Листинг 4.1 . Chapter_04/example_01/remove_all.py
foo = [10, 20, 35, -5, 42, 1, 42, 2, 42, 16]
removed val = 42
while removed val in foo:
foo.remove(removed_val)
print("foo:", foo)
Результатом работы этого скрипта будет следующая строка, выведенная в консоль:
foo: [10, 20, 35, -5, 1, 2, 16]
Такой способ удаления элементов решает еще одну проблему. Дело в том, что если
в списке нет элемента, который мы хотим удалить, то будет возбуждено исключе
ние:
>>> foo = [10, 20, 35, -5, 42, 1, 42, 2, 42, 16]
>>> foo.remove(l000)
Traceback (most recent call last):
File "< ... >", line 1, in <module>
foo.remove(l000)
ValueError: list.remove(x):
х
not in list
Поскольку в цикле while мы предварительно проверяем наличие удаляемого зна
чения, то, в случае отсутствия удаляемого значения, метод
remove ()
вызываться не
будет. Таким образом, при использовании метода remove () нужно предварительно
проверять, есть ли удаляемый элемент в списке, или перехватывать исключение,
как будет показано в главе
19.
Однако надо иметь в виду, что приведенный пример
при обработке больших списков может работать достаточно медленно из-за того,
что искомый элемент приходится искать дважды на каждой итерации, каждый раз
начиная поиск с начала коллекции.
Глава
4.
Списки, кортежи и массивы
115
Для удаления элемента по его индексу можно применить инструкцию с использо
ванием ключевого слова del, которая служит также для удаления объектов цели
ком, но сейчас нас интересует, как с ее помощью можно удалять элементы списка:
»> foo = (10, 20, 30, 40, 50, 60, 70, 80]
»> del foo[2]
>» foo
[10, 20, 40, 50, 60, 70, 80]
Она также поможет нам удалить сразу группу элементов, указанную с применени
ем оператора взятия среза:
»> del foo[-3:]
»> foo
[10, 20, 40, 50]
»> del foo[:2]
>» foo
[ 40, 50]
Здесь из оставшихся элементов списка выполнением команды del
ляются три элемента с конца, а затем с помощью команды
foo [-3:] уда
del foo 1: 2]
удаляются
два элемента с начала.
Иногда бывает полезно не только удалить элемент в заданной позиции, но и узнать,
что это был за элемент. Для этого предназначен метод рор (), способный принимать
индекс элемента, который нужно удалить из списка, а в качестве результата выпол
нения этого метода возвращается сам удаляемый элемент. Если метод рор () вы
звать без дополнительных параметров, он удалит элемент с конца списка или мас
сива и вернет этот элемент. Применение метода рор () показано в следующих при
мерах:
»> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>>> х
foo.pop(2)
>» х
30
»> foo
[10, 20, 40, 50, 60, 70, 80]
»>
»>
у
= foo .рор ()
у
80
>» foo
[10, 20, 40, 50, 60, 70]
Для удаления всех элементов списка предназначен метод clear (). Иногда в скрип
тах для очистки списка можно увидеть инструкции наподобие такой: foo =
нако надо понимать, что вызов метода
clear ()
[ J. Од
и присваивание пустого списка
-
это не одно и то же. При использовании метода clear () удаляются элементы из
уже созданного списка, а применением инструкции f оо =
1J
создается новый спи
сок, на который начинает ссылаться переменная foo. Такое различие важно, если у
Часть
116
1.
Базовые понятия и встроенные типы
вас имеется несколько переменных, ссылающихся на один и тот же объект списка.
Продемонстрируем это на двух примерах. Сначала воспользуемся методом clear ():
>>> foo = [10, 20, 30, 40, 50, 60, 70, 80]
>>> bar = foo
»> foo.clear()
>>> foo
[]
>>> bar
[]
В этом случае единственный созданный список будет очищен от элементов, и для
переменной Ьаr он также станет пустым. Если же мы будем использовать инструк
цию foo =
»> foo
>>> bar
>» foo
[ J, то получим другой результат:
[10, 20, 30, 40, 50, 60, 70, 80]
foo
[]
>>> foo
[]
>>> bar
[10, 20, 30, 40, 50, 60, 70, 80]
После выполнения команды foo
[ J переменные foo и bar станут ссылаться на
два разных списка.
Рассмотрим теперь работу метода index (), предназначенного для нахождения ин
декса заданного значения в коллекции. Этот метод присутствует в классах list,
tuple и array. Синтаксис этого метода выглядит следующим образом:
list.index(x[, start[, end]])
Здесь х -
это искомое значение, а необязательные параметры start и end ограни
чивают область поиска элемента внутри заданного интервала. Если параметры
start и end не указаны, тогда элемент ищется во всей коллекции. Если указано
только значение параметра
start,
то элемент ищется, начиная с индекса
start
до
конца коллекции. Если указаны значения обоих параметров: start и end, то иско
мое значение ищется на полуоткрытом интервале [start; end). Задать только конец
интервала без указания начала start нельзя. Если искомый элемент найден в спи
ске, то метод
index ()
вернет его индекс, если же такого элемента нет в списке или
в заданном интервале поиска, то метод index () возбудит исключение ValueError.
Следующий пример показывает пример поиска первых двух чисел
»> foo = [42, 20, 35, -5, 42, 16, 42, 20, 50, 42]
»> n = 42
>>> index 1 = foo.index(n)
>>> index 1
о
»> index 2
>» index 2
4
foo.index(n, index 1 + 1)
42
в списке:
Глава
4.
117
Списки, кортежи и массивы
В этом примере после первого вызова метода index () без указания начала поиска
будет найден элемент с индексом О, а затем ищется элемент, следующий за ним,
начиная поиск со следующего элемента в списке.
Давайте теперь напишем скрипт, который находит в списке все заданные элементы
(листинг
4.2).
1111стинr 4.2.
Chapter_041example_02/find_all.py
foo = [42, 20, 35, -5, 42, 16, 42, 20, 50, 42]
indexes = []
n = 42
next index = О
while n in foo[next_index:]:
position = foo.index(n, next index)
indexes.append(position)
next index = position + 1
print (indexes)
В этом скрипте все найденные индексы добавляются в список indexes. Переменная
next _ index задает начальную позицию для поиска очередного элемента. В цикле
while
с помощью оператора
in
и оператора взятия среза осуществляется проверка,
существует ли искомое значение в оставшейся части списка, и если зто условие
выполняется, находится положение очередного элемента. Его позиция добавляется
в список indexes с помощью метода append (), а затем переменной next _ index
присваивается значение индекса, следующего за найденным. Цикл прервется, когда
искомое значение не будет находиться в заданном интервале, возможно, по той
причине, что выражение foo [next_index:] вернет пустой список. В результате вы
полнения этого скрипта будет выведен следующий текст:
[О,·
4, 6, 9]
Если же нужно не только определить факт вхождения элемента в коллекцию, но и
узнать количество таких элементов, то для этого предназначен метод
count (),
ко
торый возвращает количество искомых элементов в коллекции. В отличие от мето
да index (), если метод count () не найдет искомое значение, то он вернет значение О,
а не будет возбуждать исключение:
>» foo = [42, 20, 35, -5, 42, 16, 42, 20, 50, 42]
>>> foo.count(42)
4
>>> foo.count(l00)
о
Нам осталось рассмотреть два простых метода:
зто
сору ()
из
класса
list и
reverse (), который присутствует в классах list и array:
♦
метод сору (), как можно догадаться по его названию, создает копию списка, и
по сути является полным аналогом оператора
[ : J, о
котором мы говорили ранее;
Часть
118
♦
1.
Базовые понятия и встроенные типы
метод reverse () переворачивает элементы таким образом, что первый элемент
списка или массива становится последним, второй- предпоследним и т. д., а
последний элемент становится первым. При этом не создается новый список
-
как говорится, инверсия списка происходит «по месту».
Без использования метода reverse () элементы можно было бы перевернуть с
помощью команды f оо
= f оо [ : : -1 J ,
но в этом случае был бы создан новый
объект списка, на который указывала бы переменная foo после присваивания.
Из табл.
главе
4.2
у нас остался не затронутым метод sort (), но про него мы поговорим в
11, когда научимся создавать функции и узнаем,
что такое анонимные функции.
Заключение
В этой главе мы познакомились с тремя коллекциями: списками, кортежами и мас
сивами, и рассмотрели их различия с точки зрения хранения данных.
Списки представлены в
tuple, а массивы
-
Python
встроенным классом list, кортежи
-
классом
классом array из модуля array. Кортежи, в отличие от списков
и массивов, являются неизменяемым типом, то есть не позволяют изменять свое
содержимое.
Мы рассмотрели способы создания списков, кортежей и массивов, а также способы
преобразования одной коллекции в другую.
Узнали о функции len (), предназначенной для определения количества элементов
в коллекциях. Научились извлекать из списков и массивов как отдельные элементы,
так и последовательность элементов с помощью срезов.
Обсудили особенности работы инструкции присваивания применительно к слож
ным объектам, связанные с тем, что все переменные в
Python
фактически являются
ссылками.
Познакомились с операторами is и is
not, предназначенными для определения,
являются ли две переменные ссылками на один и тот же объект. Узнали про встро
енную функцию id (), которая возвращает идентификатор объекта.
Получили информацию об операторах in и not
in, предназначенных для опреде
ления, существует ли заданное значение в списке или массиве.
Познакомились с понятием «распаковка»
-
процессом извлечения элементов из
коллекций и присваивания их значений отдельным переменным.
Разобрались с методами класса list, часть которых также имеются у классов tuple
И
array.
Научились удалять элементы из коллекций с помощью инструкции del.
В следующей главе мы продолжим работать с изученными в этой главе коллекция
ми, а также познакомимся с циклом for, который используется в инструкциях для
последовательного перебора элементов коллекций.
- ГЛАВА 5-
Перебор элементов коллекций
Инструкция
for ... in
В предыдущей главе мы выполняли над элементами коллекций много действий
-
выделение срезов, проверка наличия и поиск элементов и др. без явной органи
зации циклов. Однако часто для более специализированных действий без циклов не
обойтись. Эта глава посвящена организации циклов с использованием ключевого
слова for. Синтаксис инструкции для организации такого цикла выглядит следую
щим образом:
for
Переменная
Блок кода
in
Объект:
1
else:
Блок кода
2
При выполнении этой инструкции будут последовательно извлекаться по одному
элементу из Объекта, указанного после ключевого слова in, и эти значения будут
присваиваться переменной, указанной после ключевого слова for. Если удалось
извлечь из Объекта очередное значение, то выполняется Блок
1,
кода
в котором
можно задействовать значение Переменной. Объекты, которые можно использовать
после ключевого слова in, называются итерируемыми. Коллекции, которые мы к
этому моменту изучили (списки, кортежи, массивы), относятся к итерируемым объ
ектам.
Ветвь else работает точно так же, как и в цикле while (см. главу
3):
Блок кода 2
будет выполняться после того, как цикл полностью завершится (даже если не будет
выполнено ни одной итерации), но при условии, что цикл не будет прерван инст
рукцией break. Инструкции break и continue внутри Блока кода
же самые действия, что в цикле while, -
1 выполняют те
break полностью прерывает цикл, а
continue прерывает текущую итерацию цикла и принуждает перейти к следующей
итерации.
Рассмотрим простой пример, который перебирает элементы списка целых чисел,
умножает каждое число на
2и
выводит результат в консоль (листинг
5.1).
Часть
120
1.
Базовые понятия и встроенные типы
Листмнr 5.1. Chapter_05/example_01/for_douЫe.py
items = [5, 10, 15, 20, 25, 30]
for х in items:
print (х * 2, end=" ")
Здесь в процессе обхода списка i tems переменной х последовательно присваивают
ся значения элементов из этого списка.
В этом примере в функцию print () передан необязательный параметр end, указы
вающий, какие символы нужно добавлять в консоль после каждого значения. Если
параметр
end не указан, то по умолчанию используется перевод строки «\n». В этом
примере параметр end равен символу пробела, чтобы результат выводился на одной
строке, а числа были разделены пробелом. В результате в консоль будет выведен
следующий текст:
10 20 30 40 50 60
Чтобы продемонстрировать работу инструкции for ... in с ветвью else, напишем
скрипт (листинг
5.2),
который формирует список квадратных корней из элементов
заданного списка. Если в исходном списке присутствует отрицательное число, то в
качестве результата нужно вывести сообщение об ошибке.
Листинr 5.2.
Chapter_05/example_02/for_sqrt_break.py
from math import sqrt
items = [25, 100, 4,
result = []
for х in items:
if х < О:
О,
рrint("Ошибка!
-9, 36)
В списке есть отрицательные числа . ")
break
else:
result.append(sqrt(x))
else:
print("result:", result)
На каждой итерации цикла происходит проверка, не отрицательно ли очередное
число, и если оно отрицательное, то выводится сообщение об ошибке, а цикл пре
рывается с помощью инструкции
break, -
в этом случае ветвь
else
выполнена не
будет. Если число не отрицательное, в конец списка resul t добавляется новый
элемент, равный квадратному корню из х. Если все числа неотрицательные, то в
качестве результата в ветви else будет выведен список resul t.
Со значениями списка i tems, которые указаны в примере, будет выведено сообще
ние об ошибке. Однако если создать список без отрицательных чисел:
items = [25, 100, 4,
О,
9, 36]
Глава
5.
Перебор элементов коллекций
121
то в качестве результата будет выведена строка:
result: [5.0, 10.0, 2.0,
О.О,
3.0, 6.0]
Для демонстрации инструкции continue изменим условие предыдущей задачи: в
случае появления
цикл
-
в
списке
отрицательного числа оно должно
не прерываться (листинг
Листинг 5.3.
игнорироваться, а
5.3).
Chapter_05/example_03/for_sqrt_continue.py
frorn rnath irnport sqrt
iterns = [25, 100, 4, О, -9, 36]
result = []
for х in i terns:
if х < О:
continue
result.append(sqrt(x))
print("result:", result)
Теперь необходимость в ветви else отпала. Результат выполнения этого скрипта
будет выглядеть следующим образом:
result: [ 5. О, 1 О . О, 2 . О,
О
. О, 6. О]
Тут возникает вопрос, можно ли использовать значение переменной, созданной
в инструкции for ... in, после выхода из цикла. Вернемся к примеру из листинга
5.1
и попытаемся вывести переменную х после того, как цикл уже завершился (лис
тинг
5.4).
Листинг
5.4. Chapter_05/example_04/after_for.py
iterns = [5, 10, 15, 20, 25, 30]
х in i terns:
print (х * 2, end=" ")
print ("\nx: ", х)
for
Запустим скрипт на выполнение и увидим результат:
10 20 30 40 50 60
х:
30
В результате оказалось, что значение переменной х после завершения цикла равно
значению последнего элемента в списке. Если бы цикл был прерван с помощью ин
струкции break, то значение х было бы равно последнему присвоенному значению
этой переменной.
Однако в таком коде может скрываться потенциальная ошибка. Если список i tems
окажется пустым, то переменная х создана не будет, и мы получим исключение
NameError (листинг
5.5).
Часть
122
Листинг
1.
Базовые понятия и встроенные типы
5.5. Chapter_05/example_05/after_for_error.py
items = []
for х in items:
print(x * 2, end=" ")
print ()
print("x:",
х)
В результате выполнения этого скрипта будет выведен следующий текст:
Traceback (most recent call last):
File "< ... >", line 1, in <module>
print("x:", х)
NameError: name
'х'
is not defined
Существует несколько способов решения этой проблемы. Например, можно перед
циклом создать переменную х и присвоить ей значение None, а после цикла при не
обходимости проверить, чему равно значение этой переменной (листинг
Листинг
5.6).
5.6. Chapter_05/example_06/after_for_none.py
i tems = []
= None
for х in items:
print(x • 2, end=" ")
х
print ()
print("x:",
х)
В результате будет выведена строка:
х:
None
Однако такой способ не подойдет: если по логике программы список i tems может
содержать значения None, тогда нельзя будет однозначно сказать, х равен None, по
тому что цикл не выполнил ни одной итерации, или потому что последнее значение
в i tems равно None. Возможно, в этой ситуации лучше всего добавить новую буле
ву переменную с изначальным значением False, которая бы устанавливалась в True
внутри цикла. Но, как правило, следует избегать ситуации, когда переменная, пред
назначенная для использования в пределах цикла, используется после него.
Существуют способы узнать, создана ли нужная переменная. Например, если вы
звать функцию di r ( ) без параметров, то она вернет список, который включает в
себя в том числе имена созданных переменных. Но как правило, если требуется оп
реде.1ять существование переменной,
-
это повод подумать, как переписать про
грамму по-другому, чтобы избавиться от необходимости выполнения такой про
верки.
Глава
5.
Перебор элементов коллекций
123
Продолжим разбираться с ситуациями, которых лучше избегать. Посмотрим, что
произойдет, если в процессе итерации по элементам списка сам список изменится.
Например, если мы в каждой итерации цикла будем добавлять в список новый эле
мент, попадем ли мы в бесконечный цикл? А если в процессе обхода списка удалим
из списка все элементы, цикл прервется? Давайте проверим последний вопрос на
практике (листинг
Листинг 5.7.
5.7).
Chapter_05/example_07/for_clear.py
items = [5, 10, 15, 20, 25, 30]
х in i tems :
print("x + 2:",
i tems . clear ()
for
х
+ 2, end=" ")
print ()
print("items:", items)
В результате будет выведен следующий текст:
х
+ 2: 7
items: []
То есть после первой итерации список будет очищен, и цикл прервется. Аналогич
но, если бы мы в каждой итерации добавляли новые элементы, то оказались бы в
ситуации, когда цикл стал бы бесконечным. Поэтому это очень плохая идея
-
из
менять коллекцию в тот момент, когда происходит обход ее элементов.
Создание списков с помощью инструкции
for ... in
Часто возникает ситуация, когда требуется создать новый список на основе элемен
тов другой коллекции. Рассмотрим пример, который решает следующую задачу:
имеется исходный список foo с целочисленными элементами, и нам нужно создать
новый список bar, в котором каждый элемент равен соответствующему элементу
из
foo,
увеличенному на
Листинг 5.8.
100.
Мы могли бы написать следующий код (листинг
5.8).
Chapter_05/example_08/for_create_llst.py
foo = [5, 10, 15, 20, 25, 30]
bar = []
for
х in foo:
bar.append(x + 100)
print ("bar: ", bar)
Этот скрипт выполняет то, что от него требуется, и в качестве результата будет вы
ведена строка:
bar: [105, 110, 115, 120, 125, 130]
Часть
124
Однако в
Python
1.
Базовые понятия и встроенные типы
есть возможность значительно сократить код, объединив создание
нового списка и цикл в одно выражение, синтаксис которого выглядит следующим
образом:
Новый_список
=
[Выражение с Переменной
Здесь коллекция -
for
Переменная
in
Коллекция]
это объект, в котором последовательно перебираются все эле
менты, значения которых присваиваются Переменной. Соответствующий элемент
нового списка будет равен Выражению с Переменной, стоящему после открывающей
скобки и до ключевого слова for.
В англоязычной терминологии это выражение называется
list comprehension,
и для
него нет устоявшегося названия на русском языке. Его иногда называют «списоч
ное включение» или «списочное выражение».
Используя такое выражение, предыдущий пример мы можем переписать следую
щим образом (листинг
Листинг
5.9).
5.9. Chapter_05/example_09/list_comprehension.py
foo = [5, 10, 15, 20, 25, 30]
= [х + 100 for х in foo]
Ьаr
print ( "bar: ", bar)
print("type(bar)", type(bar))
Результат выполнения этого скрипта точно такой же, как и предыдущего, но до
полнительно мы выводим тип переменной bar, чтобы убедиться, что создается
именно список:
bar: [105, 110, 115, 120, 125, 130]
type(bar) <class 'list'>
Ранее мы написали скрипт, который создавал новый список, вычисляя квадрат
ный корень из элементов исходного списка, игнорируя отрицательные значения
(см. листинг
тинг
5.3).
Перепишем его, чтобы избавиться от инструкции coпtinue (лис
5.10).
Листинг
5.10. Chapter_OS/example_10/for_if_sqrt.py
from math import sqrt
foo
bar
for
[25, 100, 4,
О,
-9, 36]
[]
in foo:
if х >= О:
bar.append(sqrt(x))
print("bar:", bar)
х
Глава
5.
Перебор элементов коллекций
125
Такой код- с условием внутри цикла- тоже можно существенно сократить. Для
подобных случаев в
Python
есть расширенная версия выражения
[...
for ... in ... J с
условием, которое в общем виде выглядит следующим образом:
Новый_список
=
[Выражение с Переменной
for
Переменная
in
Коллекция
if
Условие]
В этом случае для формирования Нового списка используются только те элементы
из исходной Коллекции, для которых Условие будет равно True.
Используя такое выражение, мы можем значительно сократить предыдущий при
мер (листинг
Листинг 5.11.
5.11).
Chapter_05/example_11/list_comprehension_if.py
from math import sqrt
foo = [25, 100, 4, О, -9, 36]
= [sqrt(x) for х in foo if
print("bar: ", bar)
print("type(bar) ", type(bar))
Ьаr
х
>=
О]
Результатом выполнения этого скрипта будут следующие строки в консоли:
bar: [ 5. О, 1 О. О, 2. О, О. О, 6. О]
type(bar) <class 'list'>
Если для создания списка есть возможность использовать выражение
... ]
или
[...
for ... in ... if ... J,
[...
for ... in
то рекомендуется использовать именно их.
Создание последовательности целых чисел.
Класс
range
Если вы знакомы с языками программирования наподобие С и С++, то знаете, что
цикл for часто применяется для последовательного перебора целых чисел, которые
могут использоваться, например, как индексы для доступа к элементам массива или
в математических выражениях.
Для создания последовательности целых чисел предназначен встроенный класс
range, экземпляр которого можно создать, вызвав конструктор с разным набором
параметров:
range(stop)
range(start, stop[, step])
В
объектно-ориентированном
функции,
которые
создают
программировании
объекты
класса
конструкторами
(более
подробно
называются
про
ориентированное программирование мы будем говорить, начиная с главы
объектно
13).
Если мы используем вызов конструктора range с единственным параметром stop,
то такой экземпляр класса будет задавать последовательность целых чисел от о до
126
Часть
1.
Базовые понятия и встроенные типы
stop - 1 включительно с шагом 1. То есть экземпляр класса можно будет исполь
[о; s top) .
зовать для получения последовательности на полуоткрытом интервале
Экземпляр класса
range, созданный с использованием двух параметров: start и
stop, будет задавать последовательность целых чисел с шагом 1 на интервале
[start; stop), а добавление третьего параметра- step- позволяет задать шаг
последовательности.
Рассмотрим способы создания экземпляров класса range:
>>> foo = range(l0)
>>> foo
range(O, 10)
»> type(foo)
<class 'range'>
>>> foo list = list(foo)
»> foo list
[О,
1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> bar = range(2, 10)
>>> bar list = list(bar)
>>> bar list
[2, 3, 4, 5, 6, 7, 8, 9]
>>> baz = range(2, 10, 2)
>>> baz list = list(baz)
>>> baz list
[2, 4, 6, 8]
Здесь мы создаем три экземпляра класса range, убеждаемся в этом на примере пе
ременной foo, определяя ее тип с помощью функции type (). Затем каждый экзем
пляр класса range преобразуем в список. Вместо списков мы могли бы аналогич
ным образом создать кортеж или массив.
Важно, что объект класса range не создает одновременно все элементы, которые он
будет выдавать. Элементы последовательности создаются и возвращаются по за
просу, поэтому такую последовательность называют ленивой
(lazy sequence).
До
преобразования экземпляров класса range в список оперативная память для хране
ния всех элементов последовательности не выделяется. Во многих случаях не тре
буется создавать списки из последовательности, и поэтому мы можем создавать
очень большие последовательности, экономно расходуя оперативную память.
Для демонстрации более практического применения класса range напишем скрипт
(листинг
ла
5.12), рассчитывающий значение факториала положительного целого чис
n (если понадобится рассчитывать факториал при решении реальной задачи,
лучше воспользоваться функцией factorial () из модуля math).
Листинг
5.12. Chapter_OS/example_12/range_factorlal.py
n = 8
factorial = 1
for i in range(2, n + l):
Глава
Перебор элементов коллекций
5.
127
fact orial *= i
рrint("Факториал равен:",
f a ctorial)
В этом примере используется конструктор класса range с двумя параметрами. Об
ратите внимание, что в конструкторе в качестве правой границы последовательно
сти указывается значение
n + 1,
при этом последнее значение, которое выдаст та
кой объект range, будет равно n.
Ранее мы использовали цикл for для обхода элементов списка, а в этом примере
мы видим, что список создавать не обязательно,
-
главное, чтобы класс, экземпляр
которого задействован в цикле после ключевого слова in, удовлетворял некоторым
требованиям, и тогда его можно использовать как источник данных в цикле. Таким
требованиям удовлетворяют все коллекции, с которыми мы уже познакомились, а
также класс range. Строго говоря, эти требования заключаются в том, что класс
должен реализовать метод
_ i te r _
() .
Хотя последовательность в листинге
5.12
начинается с 2, скрипт будет выводить
правильные значения, даже если мы захотим вычислить факториал от о. Несмотря
на то, что в этом случае правый конец интервала будет меньше левого, к ошибке
это не приведет, но в последовательности не будет ни одного элемента. Мы можем
в этом убедиться на более простом примере:
»> foo
>» foo
=
range (2, 1)
range(2, 1)
>» l ist ( foo)
[]
Перебор элементов с нумерацией.
Класс
enumerate
Для решения многих задач требуется перебирать все элементы последовательности
и при этом знать их индексы. Допустим, нам нужно вывести на экран последова
тельно все элементы списка с указанием их номеров (начиная с
ем класса range мы могли бы написать следующий код (листинг
Jlистмнr 5.13.
Chapter_05/example_13/range_no_enumna.py
foo = [5, 10, 15, 20]
for n in range(len(foo)):
print(n + 1, ") ", foo [n ] , sep="")
В результате его выполнения в консоль будут выведены строки:
1)
2)
3)
4)
5
10
15
20
1). С использовани
5.13).
128
Часть
1.
Базовые понятия и встроенные типы
В этом примере для аккуратного вывода на экран мы воспользовались у функции
print () параметром sep (сокращение от английского слова
separator-
раздели
тель). Этот параметр указывает, какие символы нужно дополнительно выводить на
экран между выводимыми элементами в рамках одного вызова print (). По умол
чанию это пробел. Но в нашем примере в качестве параметра sep передана пустая
строка, чтобы пробелы не добавлялись.
Однако этот пример можно переписать более эффективно. Если есть возможность
избавиться от получения элемента списка по индексу, то лучше этой возможностью
воспользоваться. И такая возможность имеется, благодаря классу enumerate, кото
рый в качестве параметра конструктора принимает на вход последовательность, а
на каждой итерации цикла возвращает кортеж из двух элементов: номера элемента
в последовательности и сам элемент последовательности.
Изменим предыдущий пример с использованием класса enumerate (листинг
Листинr
5.14).
5.14. Chapter_05/example_14/enumerate.py
foo = [5, 10, 15, 20)
for n, item in enumerate(foo):
print(n + 1, ") ", item, sep="")
Обратите внимание, что после ключевого слова for используется распаковка зна
чений, которые получаются от объекта enumerate на каждой итерации. При ис
пользовании класса enumerate мы можем перебирать элементы последовательности
и одновременно получать их номера. Результат выполнения этого примера ничем
не отличается от предыдущего.
Этот пример можно еще немного упростить, воспользовавшись тем, что конструк
тор класса enumerate может принимать дополнительный параметр, указывающий, с
какого значения нужно начинать нумерацию элементов. Поскольку в наших при
мерах мы везде добавляли 1 к номеру элемента, код примера можно переписать
следующим образом (листинг
Листинг
5.15).
5.15. Chapter_05/example_15/enumerate_2.py
foo = [5, 10, 15, 20)
for n, item in enumerate(foo, 1):
print(n, ") ", item, sep="")
Результат выполнения этого скрипта будет точно таким же, что и предыдущего.
Параллельный перебор элементов
из нескольких коллекций. Класс
zip
В некоторых ситуациях требуется получать элементы, имеющие одинаковые ин
дексы, сразу из нескольких последовательностей. Например, если у нас есть три
Глава
Перебор элементов коллекций
5.
списка: х, у,
129
z, которые совместно описывают координаты точек, требуется полу
чить новый список, содержащий расстояния от каждой точки до начала системы
координат. Мы могли бы решить эту задачу следующим образом (листинг
nистинr 5.16.
5.16).
Chapter_05/example_16/dist_no_zip.py
from math import sqrt
=
[О.О,
у=
[О. О,
z =
[О . О,
х
1.0, 2 . о, -0.5]
-2. о, 1. 5, 2 . 5]
2 .0, -3. о, -1.5]
dist = []
for n in range(min(len(x), len(y), len(z))):
dist.append(sqrt(x[n]**2 + y[n]**2 + z[n]**2))
print ("Расстояния:", dist)
В этом примере все длины списков у нас равны, но выражение:
min(len(x), len(y), len(z))
используется для предотвращения ошибки в случае, если какой-то список окажется
короче остальных. Тогда последовательность индексов будет ограничена самым
коротким списком.
В цикле рассчитываются и добавляются в список di s t расстояния от точки до на
чала координат. Элементы списков здесь мы снова получаем по индексу.
Результат работы скрипта выглядит так:
Расстояния:
[О.О,
3.0, 3.905124837953327, 2.958039891549808]
Однако для решения этой задачи нам не нужно знать индексы каждой точки
-
достаточно было бы одновременно получать соответствующие элементы из каждо
го списка. Здесь нам поможет класс zip (несмотря на такое название, этот класс не
- имеет никакого отношения к формату
функция получила от слова
zipper -
ZIP
и сжатию данных,
-
свое название
застежка-молния).
Конструктор класса zip принимает на вход произвольное количество списков, за
тем на каждой итерации по созданному объекту возвращается кортеж, состоящий
из элементов переданных списков с одинаковыми индексами.
Чтобы лучше это понять, рассмотрим работу класса zip в интерактивном режиме:
>»
х =
[О.О,
»>у=
[О.О,
1.0, 2.0, -0.5]
-2.0, 1.5, 2 .5]
>» z = [О.О, 2.0, -3.0, -1.5]
»> foo = zip ( х , у, z)
»> type(foo)
<class 'zip' >
>>> foo list = list(foo )
»> foo lis t
[(О.О, О.О, О . О),
(1.0, - 2 . 0, 2.0),
(2. 0, 1.5, -3.0),
(-0.5, 2.5, -1.5) ]
Часть
130
1.
Базовые понятия и встроенные типы
Мы получили список из четырех кортежей, каждый из которых состоит из трех эле
ментов (по количеству переданных списков в конструктор класса zip).
Применим класс zip для нашей задачи, связанной с расчетом расстояний от точек
до начала координат (листинг
Листинг
5.17).
5.17. Chapter_05/example_17/dist_zip.py
from math import sqrt
х
=
[О.О,
у=
[О.О,
z =
[О.О,
1.0, 2.0, -0.5]
-2.0, 1.5, 2.5]
2.0, -3.0, -1.5]
dist = []
for x_val, y_val, z_val in zip(x, у, z):
dist.append(sqrt(x_val**2 + y_val**2 + z_val**2))
рrint("Расстояния:", dist)
Результат выполнения этого скрипта остается таким же, как и предыдущего.
Здесь, чтобы извлечь значения из кортежей, мы используем распаковку элементов.
Мы могли бы этого не делать, и тогда строки кода с циклом выглядели бы следую
щим образом:
for coord in zip(x, у, z):
dist.append(sqrt(coord[0]**2 + coord[1]**2 + coord[2]**2))
В этом случае переменная
coord была бы кортежем из трех элементов. Но, как пра
вило, использование распаковки делает код более понятным.
Записать этот пример можно более компактно, используя выражение создания спи
сков
for ... in ... ] (листинг
[...
Листинг
5.18).
5.18. Chapter_05/example_18/dist_zip_list_comprehenslon.py
from math import sqrt
[О. О,
1.0, 2. о, -0.5]
у
=
=
[О. О,
-2.0, 1.5, 2. 5]
z
=
[О. О,
2.0, -3.0, -1.5]
х
dist
=
[sqrt(x_val**2 + y_val**2 + z_val**2)
for x_val, y_val, z val
in zip (х,
print ("Расстояния:
у,
z)]
dist)
В нашем случае это более предпочтительный вариант. Обратите внимание, что в
выражении
[...
for ... in ... J
мы тоже используем распаковку.
Глава
Перебор элементов коллекций
5.
В примере, приведенном в листинге
131
5.16,
где еще не использовался класс
zip, мы
определяли минимальную длину списка на случай, если длины списков не совпа
дают. Проверим, как в этом случае поведет себя класс zip, и укоротим один из спи
сков на один элемент (листинг
5.19).
Листинг 5.19. Chapter_05/example_19/dist_zip_short.py
from math import sqrt
х
=
[О. О,
у =
[О.О,
z =
[О.О,
1.0, 2.0]
- 2 . 0, 1. 5, 2.5]
2.0, -3.0, - 1 .5]
dist = [ ]
for n i n range(min(len(x), len(y), len(z))):
dist.append(sqrt(x[n ] **2 + y [n] **2 + z[ n]**2))
print ("Расст о яния:", dist)
Результат выполнения этого скрипта будет выглядеть следующим образом:
Ра с стояни я :
[О .О,
3 . 0, 3 . 905124 83795332 7 ]
Перебор элементов закончился после окончания самого короткого списка, как мы это
делали самостоятельно без использования класса zip . Однако часто такая ситуация с
разными размерами исходных списков свидетельствует об ошибке (в нашем примере
это, скорее всего, именно так). В этом случае в конструктор класса
zip можно пере
дать необязательный именованный параметр strict (про именованные параметры
функций речь пойдет в главе
1О),
который должен принимать значение булева типа.
По умолчанию он равен
False, и поведение класса zip в случае входных списков с
разными длинами мы наблюдали. Однако если параметр strict равен тrue, при не
совпадении длин списков будет возбуждено исключение ValueErr o r .
Это показано в следующем примере (листинг
5.20). Но
для того, чтобы было ясно, в
какой момент возбуждается исключение, вместо сохранения расстояний в список,
мы будем рассчитанное расстояние выводить в консоль на каждой итерации цикла.
дистинг
5.20. Chapter_05/example_20/dlst_zlp_short_error.py
from math import sqrt
х
=
[ О.О,
у=
[О.О,
z =
[О .О,
1 . 0, 2 .0 ]
-2. 0, 1 .5, 2. 5]
2. 0, - 3.0 , - 1 .5 ]
for x_val, y_val, z_val in zip(x, у, z, strict=True):
dist = sqrt(x_val**2 + y_val**2 + z_val**2)
print ("(", x_val, ", ",
y_val, ", ", z_val, ") ->
dist, sep="")
Часть
132
1.
Базовые понятия и встроенные типы
В результате выполнения этого скрипта в консоль будет выведен следующий текст:
-> О.О
(1.0, -2.0, 2.0) -> 3.0
(2.0, 1.5, -3.0) -> 3.905124837953327
Traceback (most recent call last):
File " ... /dist_zip_short_error.py", line 7, in <module>
for x_val, y_val, z_val in zip(x, у, z, strict=True):
(О.О,
О.О,
О.О)
ValueError: zip() argument 2 is longer than argument 1
Таким образом, исключение возбуждается не в момент создания класса zip, а в тот
момент, когда на очередной итерации не хватает элементов из какого-то из спи
сков. И это логично, поскольку конструктор
zip может принимать не только зара
нее созданные списки, массивы и т. п., а также итерируемые объекты, которые рас
считывают свой очередной элемент динамически в процессе выполнения програм
мы.
Например,
экземпляр класса
одним
из
параметров
конструктора
класса
zip может быть
range.
Заключение
Эта глава посвящена инструкции
for ... in, которая позволяет организовывать цик
лы для последовательного обхода элементов коллекций. Примеры в этой главе ис
пользовали эту инструкцию для обхода элементов списков, но также ее можно
применять к другим коллекциям, таким как кортежи, массивы, и, как мы увидим в
следующих главах, к словарям и множествам.
Имеются специальные выражения с использованием ключевого слова
for, которые
[... for
позволяют создавать списки на основе другой входной последовательности:
... in ... ]
И
[ ... for ... in ... if ... ].
В этой главе мы также познакомились с некоторыми классами, порождающими по
следовательности:
♦
класс
range позволяет создавать последовательность целых чисел в заданном
[ start; stop);
полуоткрытом интервале
♦
класс enumerate позволяет обходить элементы последовательности и одновре
менно получать последовательные номера соответствующих элементов;
♦
класс
zip объединяет в одну последовательность несколько входных последова
тельностей, создавая последовательность из кортежей, состоящих из такого ко
личества элементов, сколько входных последовательностей будет передано в
конструктор класса
zip.
Далее мы продолжим знакомиться с коллекциями, которые предоставляет
следующей коллекцией, которую мы изучим, будет словарь.
Python,
и
-ГЛАВА6-
Словари
Что такое «словари)) и зачем они нужны?
В главе
4
мы рассмотрели три вида коллекций: списки, кортежи и массивы,
-
хранящие множество элементов, доступ к которым осуществляется с помощью це
лочисленных индексов. Однако часто возникают ситуации, когда порядок элемен
тов в коллекции не имеет значения, и находить нужный элемент нужно не по его
порядковому номеру, а по некоторому ключу.
Например, если вы разрабатываете многопользовательский веб-сервис, вам может
понадобиться находить пользователя по его уникальному идентификатору, кото
рый может быть строкой. Если вы пишете сервис для чатов или блогов, то, скорее
всего, вам придется по аналогичному идентификатору находить сохраненные поль
зователями сообщения или статьи. В инженерных расчетах возможна ситуация, ко
гда вы обрабатываете результаты измерений,
-
тогда в качестве ключа для доступа
к нужному результату у вас может быть дата измерения.
Во всех упомянутых случаях можно было бы хранить искомые данные в списке, а
затем перебором находить нужное значение, но это была бы слишком долгая опе
рация. Поэтому, когда для доступа к объекту удобно использовать какой-либо
идентификатор, обычно используют словари
(dictionary).
В других языках про
граммирования аналогичные структуры могут называться карта.ми
ратуре их еще называют ассоциативными массива.ми.
Словарь
1
~
Значение
1
Ключ2
~
Значение
2
Ключ
...
Ключ
Рис.
6.1.
N
Н ЗначениеN
Схематичное представление словаря
(map),
а в лите
Часть
134
Словарь (рис.
6.1) -
1.
Базовые понятия и встроенные типы
это структура данных, предназначенная для хранения элемен
тов (значений), для доступа к которым используется l(JIIOЧ. Поиск по ключу в слова
ре при большом количестве хранимых значений выполняется намного быстрее, чем
простой перебор всех элементов.
Создание словарей
Словари в
Python
представлены встроенным классом dict, экземпляры которого
можно создавать несколькими способами. Первый способ заключается в использо
вании фигурных скобок, внутри которых через запятую записаны пары ключ
значение, при этом ключ от значения отделен двоеточием:
>>>#Создание пустого словаря
»> foo = {}
>>> foo
{)
»> type (foo)
<class 'dict'>
>>>#Создание заполненного словаря
>>>
Ьаr
= { "key": 10, 42: "Hello, world", None: [10, 20, 30]}
>>> bar
{ 'key': 10, 42: 'Hello, world', None: (10, 20, 30))
»> type (bar)
<class 'dict'>
Как и при создании списков, после последнего элемента можно добавить лишнюю
запятую. Такой способ записи используется, когда создается словарь из большого
количества элементов, и эту запись разбивают на несколько строк:
bar = {
"key": 10,
42: "Hello, world",
None: (10, 20, 30),
Типы как ключей, так и значений не обязаны быть одинаковыми в рамках одного
словаря, хотя на практике ключи часто имеют одинаковый тип. Для ключей есть
ограничения на используемые типы, и про эти ограничения мы поговорим далее в
этой главе, а пока скажем, что в качестве ключей могут использоваться строки, це
лые и дробные числа, объект None и кортежи (в некоторых случаях).
Другой способ создания словарей заключается в вызове различных версий конст
руктора класса dict. Создать пустой словарь можно, вызвав конструктор класса
dict без параметров:
>>> foo = dict()
>>> foo
{)
6.
Глава
Словари
135
Еще один способ создания словаря с использованием конструктора класса dict за
ключается в том, что пары ключ-значение передаются в качестве именованных па
раметров:
»> bar = dict(key_l=lO, key_2="Hello, world", key_3=[10, 20, 30])
»> bar
{'key_l': 10, 'key_2': 'Hello, world', 'key_3': [10, 20, 30])
У этого способа есть два ограничения: ключи могут быть только строками, причем
эти строки должны удовлетворять требованиям, предъявляемым к именам пере
менных (см. главу
1).
Поэтому таким способом нельзя создать элемент с ключом 7Ь
или 42, т. к. имя переменной не может начинаться с цифры.
Следующий способ использования конструктора dict заключается в том, что ему в
качестве параметра передается последовательность из кортежей (или другой после
довательности), состоящих из двух элементов. Первый элемент каждого кортежа
будет использоваться в качестве ключа, а второй элемент
-
в качестве значения:
»> spam = dict ( [
("key", 10),
(42, "Hello, world"),
(None, [10, 20, 30])))
>» spam
{'key': 10, 42: 'Hello, world', None: [10, 20, 30])
К такому способу удобно прибегать, если у вас есть два списка: один из них хранит
ключи, а другой
-
значения. В этом случае мы можем воспользоваться классом
zip, о котором говорилось в главе
5,
и написать следующий код:
»> keys = ["key", 42, None)
»> values = [10, "Hello, world", [10, 20, 30))
>>> spam = dict(zip(keys, values))
»> spam
{'key': 10, 42: 'Hello, world', None: [10, 20, 30))
Еще одним способом создания словарей является выражение «словарное включе
ние»
(dictionary comprehension),
5:
напоминающее списковое включение, которое мы
использовали в главе
{Ключ:
Значение
for
Переменная
in
Коллекция}
Значение
for
Переменная
in
Коллекция
или
{Ключ:
if
Условие}
Здесь принцип работы такой же: необходимо сформировать Ключ и значение на ос
нове Переменной, которая будет последовательно равна каждому элементу Коллек
ции. Те значения Переменной, для которых Условие равно
False,
игнорируются.
Рассмотрим пример:
>>> foo = [10, 15, 42, 51, 35,
О)
>» bar = {"key_" + str (n) : n * 2 for n in foo}
>>> print("bar:", bar)
bar: {'key_lO': 20, 'key_15': 30, 'key_42': 84, 'key_51': 102, 'key_35': 70, 'key_O':
О)
Часть
136
1.
Базовые понятия и встроенные типы
Мы перебираем здесь элементы списка foo и на их основе формируем и ключ, и
значение. Для формирования ключа используется преобразование целого числа в
строку, а в качестве значений
-
удвоенные значения из списка.
Для демонстрации создания словаря с фильтрацией значений по условию изменим
предыдущий пример таким образом, чтобы словарь формировался только из нечет
ных элементов списка:
»> foo = [10, 15, 42, 51, 35, О]
>>> bar = {"key_ " + str(n): n * 2 for n in foo if n
>>> print("bar:", bar)
% 2 !=
О)
bar: { 'key_15': 30, 'key- 51': 102, 'key_35': 70)
Основные операции со словарями
Прежде чем мы продолжим изучение операций со словарями, приведем краткую
информацию о методах класса ctict (табл.
6.1 ).
На протяжении этой главы мы по
знакомимся с большинством из этих методов более подробно.
Таблица б.1. Методы класса dict
Краткое описание
Метод
get ()
Возвращает элемент по ключу или значение по умолчанию, если ключ
отсутствует
setdefault ()
Добавляет новый элемент в словарь, если указанный ключ отсутствует
update ()
Добавляет элементы из другого словаря
fromkeys ()
Создает новый словарь с ключами на основе списка и устанавливает
для них значения по умолчанию
сору()
Создает копию словаря
clear ()
Удаляет все элементы словаря
рор()
Удаляет элемент по заданному ключу и возвращает его значение
popitem()
Удаляет последний добавленный элемент и возвращает кортеж
(Ключ, Значение) удаляемого элемента
keys ()
Возвращает итерируемый объект, который можно использовать для обхода
ключей словаря
values ()
Возвращает итерируемый объект, который можно использовать для обхода
значений словаря
i tems ()
Возвращает итерируемый объект, который можно использовать для обхода
элементов словаря, получая кортежи вида (Ключ, Значение).
Глава
6.
Словари
137
Для добавления нового элемента в словарь используются квадратные скобки, внут
ри которых указывается ключ:
»>
»>
»>
»>
»>
»>
foo
=
()
foo["key_l"]
10
foo["key_2"]
"Hello, world"
foo["key_3"]
[10, 20, 30]
foo["key_l"J
=
200
foo
('key_l': 200, 'key_2': 'Hello, world', 'key_3': [10, 20, 30]}
В этом примере ключ "key _ 1" задействуется дважды: в первый раз создается новый
элемент со значением
10,
а затем он перезаписывается значением
200.
Если требуется избежать перезаписи существующих значений, то можно восполь
зоваться методом setdefaul t (), который добавляет в словарь новую пару ключ
значение (ключ передается в качестве первого параметра функции, значение
-
в
качестве второго ее параметра). Если заданный ключ в словаре уже существует, то
значение по указанному ключу не будет изменено.
Метод setdefault () возвращает значение, которое после вызова этой функции бу
дет храниться по заданному ключу. Работа с методом setdefault () показана в сле
дующем примере:
»> foo = ("key_l": 10, "key_2": 20)
>» foo.setdefault("new_key", 30)
30
>» foo.setdefault("key_l", 100)
10
>» foo
('key_l': 10, 'key_2': 20, 'new_key': 30)
Здесь первый вызов метода setdefaul t ()
добавляет новый элемент с ключом
"new_key" и значением 30. Второй вызов метода setdefault () с указанием ключа
"key _ 1 ", который уже присутствует в словаре, не изменяет словарь.
Еще один способ создания словарей
fromkeys
(iteraЫe
-
воспользоваться методом fromkeys ():
[, value])
Этот метод принимает на вход список ключей i teraЫe и необязательный параметр
value -
значение для создаваемых пар ключ-значение. В результате вызова этого
метода будет создан словарь с указанными в i teraЫe ключами, для которых все
значения будут равны value. Если параметр value не указан, то в качестве значе
ния по умолчанию он считается равным None. Следующий пример показывает соз
дание двух словарей:
>>> keys = ["key_l", "key_2", "key_3"]
>>> foo = dict.fraokeys(keys)
»> foo
{'key_l': None, 'key_2': None, 'key_3': None)
Часть
138
>>> bar
= dict.fromkeys(keys,
>>> bar
{ 'key_ 1' :
О,
'key_ 2' :
О,
1.
Базовые понятия и встроенные типы
О)
'key_ 3' :
О}
Обратите внимание, что метод fromkeys () вызывается непосредственно из класса
dict, а не из экземпляра класса. Такие методы называются методами класса (о том,
что такое «метод класса», более подробно рассказано в главе
Для
добавления
сразу
пар
нескольких
ключ-значение
13).
предназначен
метод
update (). Он может принимать в качестве параметра словарь или список кортежей
из двух элементов
(Ключ,
Значение), аналогично тому, как это делает конструктор
класса dict, описанный ранее. Метод update () добавляет элементы из переданного
ему параметра. Если среди добавляемых элементов присутствуют элементы с клю
чами, совпадающими с ключами первоначального словаря, то значения таких эле
ментов будут перезаписаны.
Работу метода upda te () демонстрирует следующий пример:
>>> foo = {"key_l": 10, "key_2": 20, "kеу_З": 30)
>>> bar = ("key_l": 100, "key_2": 200, "key_other": 1000}
>>> foo.update(bar)
>>> foo
('key_l': 100, 'key_2': 200, 'kеу_З': 30, 'key_other': 1000}
Результат этого примера не изменится, если переменная bar будет не словарем, а
списком кортежей:
bar = [ ("key_l", 100), ("key_2", 200),
("key_other", 1000)]
С помощью метода сору () можно создать новый словарь с теми же элементами,
что и изначальный словарь. Этот метод используется, когда нужно иметь две копии
словаря в виде независимых объектов, поскольку инструкция присваивания только
создает новую ссылку на имеющийся словарь.
Для того чтобы дальнейшие примеры использования словарей были менее абст
рактными, создадим словарь eps, у которого ключами будут являться названия ма
териалов, а значениями
-
величины относительных диэлектрических проницаемо
стей соответствующих материалов:
>>> eps =
("вакуум":
1.0,
"стекло":
6.4,
"тефлон":
2.1}
Для получения значения по ключу служит оператор
[]
(квадратные скобки):
>>> eps ["вакуум"]
1.0
>>> eps ["тефлон"]
2.1
Если мы попытаемся получить значение по отсутствующему ключу, то получим
исключение
KeyError:
>>> ерs["цемент"]
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
Глава
6.
Словари
139
ерs["цемент"]
KeyError:
'цемент'
Для проверки наличия или отсутствия ключа в словаре применяются уже знакомые
нам операторы
in
и
not in:
in Словарь
not in Словарь
Ключ
Ключ
Оператор
in возвращает значение тrue, если ключ содержится в словаре, и
в противном случае. Оператор not in является инверсией оператора in -
False -
он возвращает значение тrue, если ключ не содержится в словаре, и
False -
в про
тивном случае:
>>> eps =
»>
{"вакуум":
"вакуум"
in eps
"цемент"
in eps
1.0,
"стекло":
6.4,
"тефлон":
2.1 )
True
>>>
False
»>
"древесина"
not in eps
True
>>>
"стекло"
not in eps
False
Проверку с помощью операторов in и not
in можно осуществлять перед получе
нием значения по ключу, чтобы избежать исключения KeyError в случае отсутст
вия ключа. Избежать исключения в этом случае можно также при использовании
метода
get () :
get(key[, default])
Метод get ()
возвращает значение по ключу key, если этот ключ присутствует в
словаре. Если такого ключа нет, то возвращается значение по умолчанию. В каче
стве значения по умолчанию используется None, либо значение параметра default,
если он указан.
Работу метода get () демонстрирует следующий пример:
»> eps = {"вакуум": 1.0, "стекло": 6.4,
»> vacuum = eps. get ("вакуум")
>>> print(vacuum)
1.0
>» sand = eps. get ("песок")
»> print (sand)
None
>» oil = ерs.gеt("нефть", float("nan"))
»> print(oil)
nan
"тефлон":
2.1 )
Часть
140
1.
Базовые понятия и встроенные типы
Для удаления элемента из словаря можно использовать инструкцию del:
>>> eps = {"вакуум": 1.0,
>>> del eps ["стекло"]
>>> eps
"стекло":
6.4,
"тефлон":
2.1 }
{ ' вакуум ' : 1 . О , ' тефлон ' : 2 . 1 }
Если при использовании инструкции del указать несуществующий ключ, то будет
возбуждено исключение KeyError. Для удаления всех элементов словаря предна
значен метод
clear () :
>>> eps = {"вакуум": 1.0,
»> eps. clear ()
>>> eps
"стекло":
6.4,
"тефлон":
2.1)
{}
Ограничения на типы ключей
Ранее уже говорилось, что экземпляры не всех классов могут выступать в качестве
ключей. Например, в качестве ключа не могут использоваться списки. Для того,
чтобы класс мог выступать в роли ключа, необходимо, чтобы этот класс был хеши
руемым, то есть для этого класса должен быть реализован расчет хеш-функции.
Хеш-функцией называется такая функция, на вход которой подается некоторый на
бор данных, а она возвращает битовую последовательность заранее установленной
фиксированной длины, рассчитанную на основе входных данных. Значение, полу
ченное от хеш-функции, называется хешем (иногда хеш-суммой). Основное требо
вание к такой функции
-
для одинаковых входных данных хеш-функция должна
возвращать всегда одно и то же значение хеша. Помимо этого, хеш-функции строят
таким образом, чтобы минимизировать вероятность совпадения хешей для разных
входных данных. Случай совпадения хешей для разных входных данных называет
ся коллизией.
Применительно к
Python,
хеш-функция всегда должна возвращать целое число. Для
того, чтобы класс был хешируемым, он должен реализовать метод_ hash _
(). Сре
ди стандартных классов хешируемыми являются классы неизменяемых объектов:
int, float, str и др. Кортежи могут быть хешируемыми в том случае, если содер
жат только хешируемые объекты. Класс dict не является хешируемым объектом.
Требование хешируемости ключей исходит из особенностей представления данных
внутри словарей. Именно по хешу ключа осуществляется поиск элементов словаря.
Если для двух разных ключей случится коллизия хешей, ничего страшного не про
изойдет, но это немного замедлит поиск нужного ключа в словаре.
Получить значение хеша объекта можно с помощью встроенной функции hash ().
Давайте посмотрим на хеши разных объектов:
>>> foo = "Hello, world"
»> hash(foo)
-6114327725194465516
Глава
6.
141
Словари
>>> bar = (1, 2, 3)
>>> baz = (1, 2, 3)
>>> bam = (0, 2, 3)
»> hash (bar)
529344067295497451
»> hash (baz)
529344067295497451
»> hash (bam)
8477238830141211852
»> eggs = 13.5
>» hash(eggs)
1152921504606846989
»> spam = 150
»> hash (spam)
150
Для одинаковых кортежей bar и baz хеши ожидаемо совпали. Интересно, что для
не очень больших целых чисел хеш равен самому целому числу.
Если же для класса не реализована хеш-функция, то при попытке получить хеш бу
дет возбуждено исключение TypeError:
>>> foo = [10, 20, 30]
»> hash (foo)
Traceback (most recent call last):
File "< ... >", line 1, in <module>
hash(foo)
TypeError:
unhashaЫe
type: 'list'
Такое же исключение будет возбуждено, если попытаться получить хеш от корте
жа, который содержит нехешируемый объект:
»> foo = (10, 20, [100, 200])
>» hash (foo)
Traceback (most recent call last):
File "< ... >", line 1, in <module>
hash(foo)
TypeError:
unhashaЫe
type: 'list'
Обход элементов словаря с помощью цикла
for
Экземпляры класса dict являются итерируемыми объектами, то есть такие объек
ты можно использовать в инструкции for ... in ... после ключевого слова in. В этом
случае переменной цикла будут последовательно присваиваться значения ключей
словаря:
>>> eps = {"вакуум": 1.0,
>>> for key in eps:
"стекло":
6.4,
"тефлон":
2.1 }
Часть
142
1.
Базовые понятия и встроенные типы
print (key)
вакуум
стекло
тефлон
Начиная с
Python 3.6,
гарантируется, что элементы в словаре располагаются в том
порядке, в каком они были добавлены. Однако обычно на это не нужно рассчиты
вать.
Все ключи также можно получить, воспользовавшись методом keys () . Он возвра
щает экземпляр класса dict_keys -
объект, который можно преобразовать в спи
сок или использовать непосредственно в инструкции
>>> eps = {"вакуум": 1. О,
»> eps. keys ()
dict_keys(['вaкyyм',
"стекло":
6. 4,
'стекло',
'тефлон'])
'стекло',
'тефлон'])
"тефлон":
for ... in ... :
2 .1 }
>>> keys = eps.keys()
>>> keys
dict_keys(['вaкyyм',
>>> keys_list = list(keys)
>» keys_list
['вакуум',
'стекло',
'тефлон']
Подобным образом работает метод values (), только он возвращает экземпляр
класса
dict_values, на основе которого можно создать список значений или ис
for ... in ... :
пользовать его непосредственно в инструкции
>>> values = eps.values()
>>> values
dict_values([l.0, 6.4, 2.1])
>>> values list
list(values)
>>> values list
[1.0, 6.4, 2.1]
В классе dict имеется также еще один похожий метод i tems (), возвращающий эк
земпляр класса
пар
(Ключ,
dict_items, на основе которого можно создать список кортежей из
Значение):
>>> items = eps.items()
>>> items
dict _ i tems ( [ ( 'вакуум' , 1. О) , ( 'стекло' , 6. 4) , ( 'тефлон' , 2 .1) ] )
>>> items_list = list(items)
>» items list
[ ( 'вакуум' , 1 . О) , ( 'стекло' , 6. 4) , ( 'тефлон' , 2 . 1) ]
Часто этот метод используется в цикле for, чтобы одновременно перебирать и
ключи, и значения:
>>> eps = {"вакуум": 1.0, "стекло": 6.4,
>>> for key, value in eps.items():
print(key, "->", value)
"тефлон":
2.1}
Глава
6.
Словари
143
-> 1. О
6.4
тефлон-> 2.1
вакуум
стекло->
С помощью встроенной функции len () можно определить количество элементов в
словаре, то есть количество пар ключ-значение:
>>> eps = {"вакуум": 1.0,
>» len(eps)
"стекло":
6.4,
"тефлон":
2.1}
3
»> foo = {}
»> len(foo)
о
Заключение
В этой главе мы познакомились с еще одним важным и часто используемым встро
енным классом
-
dict, который представляет собой словарь или ассоциативный
массив, позволяющий хранить пары ключ-значение.
В качестве значений могут выступать экземпляры любых классов, а вот ключами
могут быть только экземпляры хешируемых классов, таких как int, float, str, но
не list или dict. Экземпляр класса tuple является хешируемым, если он содержит
только хешируемые объекты.
Мы рассмотрели также несколько способов создания экземпляров класса dict с
использованием фигурных скобок
-
как с указанием пар ключ-значение, так и со
вместно с ключевым словом for, а также разные способы непосредственно вызова
конструктора класса dict и создания словаря по списку ключей с помощью метода
fromkeys ().
Затем мы познакомились с основными операциями над словарями
получением
-
значения по ключу с помощью квадратных скобок и метода get () , а также добав
лением новых элементов, в том числе с помощью метода
Мы увидели, как работают операторы in и not
setdefaul t ().
in для определения наличия или
отсутствия ключа в словаре, научились удалять элементы с помощью инструкции
del
и полностью очищать словарь с помощью метода
clear ().
Мы изучили способы обхода словаря в цикле for, а также увидели, что с помощью
методов keys (), val ues () и i tems () можно получать объекты, которые возвраща
ют последовательность ключей, значений и кортежей вида (Ключ,
значение) соот
ветственно.
В следующей главе мы рассмотрим еще две встроенные коллекции
-
это классы
set и frozenset, представляющие собой множества, и увидим, что они имеют мно
го общего со словарями, которым была посвящена эта глава.
- ГЛАВА 7 -
Множества
Что такое множества и зачем они нужны?
Множество
-
это коллекция, предназначенная для неупорядочеююго хранения
уникальных элементов. Выделенные курсивом слова в этом определении являются
ключевыми, дающими представление о сути множеств. Если при хранении элемен
тов в списке нам был важен порядок их следования, то для множеств порядок сле
дования элементов не оговорен
-
они могут менять свое положение в множестве
при добавлении новых элементов. Кроме того, гарантируется, что множество не будет
содержать одинаковые элементы.
Если одной из основных операций над списком было получение элемента по ин
дексу, то главная операция над множеством
-
определение, присутствует ли эле
мент в множестве. Поиск элементов в множестве работает намного быстрее поиска
элемента в списке благодаря особой структуре хранения данных, которая, однако,
накладывает ограничения на возможный тип хранимых элементов.
В
Python
есть два встроенных класса, представляющих множества: это класс set, о
котором сейчас мы и поговорим, а также класс frozenset -
неизменяемое множе
ство, которое не позволяет удалять или добавлять элементы после его создания.
Создание множеств
Существует несколько способов создать множество. Самый простой из них
-
за
писать его элементы в фигурных скобках:
>» foo = {100, 10, 20, 30, 30, 10)
>>> foo
{10, 100, 20, 30}
»> type (foo)
<class 'set'>
Поскольку гарантируется, что множество
множество
f оо
не содержит одинаковых
элементов,
в этом примере после создания содержит только четыре элемента
вместо указанных шести. Кроме того, элементы множества foo в итоге оказались
расположены не в том порядке, в каком они были указаны при создании множества.
Глава
7.
Множества
145
Множество может состоять из элементов разных типов:
»> spam = (100, "hello", 42, 3.14)
»> spam
{'hello', 42, 3.14, 100)
Однако при создании множеств с помощью фигурных скобок нужно соблюдать ос
торожность. Дело в том, что если мы захотим создать пустое множество и напишем
Ьаz = { }, то создадим не пустое множество, а пустой словарь:
»> baz = {)
>» type (baz)
<class 'dict'>
Чтобы создать пустое множество, нужно использовать конструктор класса set без
параметров:
»> baz = set ()
>» baz
set ()
>» type (baz)
<class 'set'>
Множества также можно создавать из итерируемых объектов ( см. главу
5):
>» bar = set ( [100, 10, 20, 30, 30, 10])
»> bar
(10, 100, 20, 30)
>» baz
set ("abracadabra")
>» baz
{'а',
'r',
'с',
'Ь',
'd'}
Здесь множества Ьаr и Ьаz создаются на основе списка и строки- строки также
являются итерируемыми объектами, позволяющими последовательно получать по
одному символу. Повторные элементы не попадут в множество, поэтому объекты
Ьаr
и ьаz
содержат меньше элементов, чем последовательности,
из которых
они
создавались.
Существует еще один способ создания множеств с использованием фигурных ско
бок и
ключевого слова
comprehension,
for. Этот способ аналогичен созданию списков (list
5), только вместо квадратных скобок используются фигур
называют set comprehension. В этом случае также можно ис
см. главу
ные. Такое выражение
пользовать ключевое слово if для фильтрации входных данных:
>» foo
[100, 10, 20, 30, 30, 10]
>>> bar = {n + 5 for n in foo)
»> bar
(105, 35, 25, 15)
>>> baz = {n + 5 for n in foo if n < 50)
»> baz
(25, 35, 15}
Часть
146
1.
Базовые понятия и встроенные типы
На типы, которые могут быть помещены в множество, накладывается такое же ог
раничение, что и на ключи словарей,
главу
6).
-
эти типы должны быть хешируемыми (см.
По этой причине множество не может содержать в себе, например, списки
и словари, но может содержать хешируемые кортежи (кортежи, которые содержат в
себе только хешируемые элементы). Если мы попытаемся создать множество с не
хешируемым элементом, то будет возбуждено исключение TypeError:
>>> spam = {100, "hello", (1, 2, 3))
Traceback (most recent call last):
File "< ... >", line 1, in <module>
spam = {100, "hello", (1, 2, 3))
TypeError:
Класс
unhashaЫe
type: 'list'
set тоже не является хешируемым, и поэтому не может быть элементом дру
гого множества.
Создание неизменяемых множеств
В классе
frozenset отсутствуют методы, предназначенные для добавления и уда
ления элементов. При этом он является хешируемым в отличие от класса set.
Поэтому, если у вас стоит задача создать множество, включающее в себя другие
множества, то для этого можно использовать класс
frozenset.
Для создания неизменяемого множества существует лишь один способ
конструктор класса
вызвать
-
frozenset и передать в него итерируемый объект, из элементов
которого будет создано новое множество, или вызвать конструктор без параметров,
чтобы создать пустое неизменяемое множество:
>>> foo = frozenset()
>>> foo
frozenset ()
»> type(foo)
<class 'frozenset'>
>>> bar = frozenset([l0, 20, 30, 10))
>>> bar
frozenset({l0, 20, 30))
>>> baz = frozenset({20, 30, 42))
>>> baz
frozenset({42, 20, 30))
Основные операции над множествами
Одной из наиболее часто используемых операций над множествами является опре
деления наличия или отсутствия элементов с помощью операторов
in
Эти операторы здесь работают точно так же, как и для других коллекций:
>>> foo = {100, 10, 20, 30)
»> 100 in foo
и
not in.
Глава
7.
147
Множества
True
»> 110 in foo
False
>>> 20 not in foo
False
>>> 120 not in foo
True
Множества являются итерируемыми объектами, поэтому для обхода их элементов
можно использовать цикл
for:
»> foo = (100, 10, 20, 30, 30, 10)
>>> for item in foo:
print(item)
10
100
20
30
Множества можно преобразовывать в списки, кортежи и массивы:
>>> import array
»> foo = (100, 10, 20, 30, 30, 10)
>>> bar = list(foo)
»> bar
[10, 100, 20, 30]
>>> baz = tuple(foo)
»> baz
(10, 100, 20, 30)
»> spam = array.array("i", foo)
>» spam
array('i',
[10, 100, 20, 30])
Для определения количества элементов в множестве также используется функция
len ():
»> foo = (100, 10, 20, 30, 30, 10)
>» foo
(10, 100, 20, 30)
>» len (foo)
4
Все рассмотренные операции также применимы и для класса frozenset.
Методы и операторы классов
set и frozenset
Методы, которые содержатся в классах set и frozenset, приведены в табл.
7.1.
Не
которые из этих методов связаны с математическими операциями над множества
ми, и такие методы можно вызывать, используя специальные операторы.
Часть
148
Таблица
1.
Базовые понятия и встроенные типы
7.1. Методы и операторы классов set и frozenset
Наличие
Метод класса
set
Описание
в классе
Оператор
frozenset
add()
-
remove()
Добавляет элемент в множество
Удаляет элемент из множества.
-
Возбуждает исключение KeyError,
если элемент отсутствует
discard()
-
Удаляет элемент из множества
при его наличии
Удаляет случайный элемент из множе-
рор()
-
ства и возвращает его в качестве
значения функции. Возбуждает исключение кeyError, если множество пустое
clear ()
-
Удаляет все элементы из множества
сору()
+
Создает копию множества
union ()
update ()
difference ()
difference_update()
intersection ()
intersection_update()
+
+
+
-
1
Возвращает новое множество, равное
объединению нескольких множеств
Добавляет элементы из другого
1=
итерируемого объекта
-
Возвращает новое множество, равное
разности двух множеств
Получение разности двух множеств
-
~
с обновлением исходного множества
Возвращает новое множество, равное
&
пересечению двух множеств
Получение пересечения двух множеств
&=
с обновлением исходного множества
л
Возвращает новое множество,
symmetric_difference()
+
равное симметрической разности
двух множеств
л_
symmetric_differenceupdate ()
Получение симметрической разности
-
двух множеств с обновлением исходного множества
issubset ()
+
Проверка, является ли множество
подмножеством другого множества
<=
Глава
7.
Множества
149
Таблица
7.1 (окончание)
Наличие
Метод класса
set
Описание
в классе
Оператор
frozenset
issuperset ()
isdisjoint ()
+
+
Проверка, является ли множество
>=
надмножеством другого множества
Проверка, что множества не пересекаются
Рассмотрим примеры с использованием некоторых из упомянутых в табл.
7.1
мето
дов и начнем с методов, предназначенных для добавления элементов в множество:
»> foo
{10, 20, 30}
>>> bar = [10, 30, 40]
»> spam = {40, 50, 60}
>>> foo.add(l00)
»> foo
(100, 10, 20, 30}
>>> foo.update(bar)
»> foo
(100, 40, 10, 20, 30}
>>> foo.update(spam)
>» foo
(100, 40, 10, 50, 20, 60, 30}
Обратите внимание, что метод upda te () может принимать не только множество, но
и любой итерируемый объект. Метод update () также может принимать сразу не
сколько аргументов, поэтому строки кода с его использованием можно объединить
в одну:
{10, 20, 30}
»> foo
>>> bar = [10, 30, 40]
>>> spam = {40, 50, 60}
>>> foo.update{bar, spam)
»> foo
{50, 20, 40, 10, 60, 30)
Вместо метода upda te () , можно использовать оператор 1=, но только если в правой
части этого оператора указан экземпляр другого множества:
>>> foo = {10, 20, 30)
>>> bar = {10, 30, 40}
»> foo 1= bar
»> foo
(20, 40, 10, 30}
Часть
150
1.
Базовые понятия и встроенные типы
Оператор I возвращает новое множество, равное объединению двух множеств.
Применение этого оператора равносильно вызову метода
union () :
>>> foo = (10, 20, 30)
>>> bar = (10, 30, 40)
»> foo I bar
(20, 40, 10, 30)
>>> foo.union(bar)
(20, 40, 10, 30)
Функция
difference () и соответствующий ей оператор«-» возвращают разность
двух множеств
-
то есть новое множество, элементы которого состоят из элемен
тов первого множества за исключением элементов второго множества:
>>> foo = (10, 20, 30)
>>> bar = (10, 40)
>>> foo - bar
(20, 30)
>>> foo.difference(bar)
(20, 30)
Для присваивания результата разности первому множ_еству можно воспользоваться
методом
difference_ update ()
или оператором-=:
>>> foo = (10, 20, 30)
>>> bar = (10, 40)
>>> foo -= bar
>>> foo
(20, 30)
Метод intersection () и соответствующий ему оператор«&» создают новое мно
жество, элементы которого состоят из элементов, общих для двух множеств:
>>> foo = (10, 20, 30)
>>> bar = (10, 30, 40)
>>> foo & bar
{10, 30)
>>> foo.intersection(bar)
{10, 30)
Для присваивания результата пересечения двух множеств
предназначены оператор&= и метод
первому множеству
intersection_update ():
>>> foo = (10, 20, 30)
>>> bar = (10, 30, 40)
>>> foo.intersection_update(bar)
>>> foo
{10, 30)
Метод
symrnetric difference () и соответствующий ему оператор «л» предназна
чены для расчета симметрической разности
-
они создают новое множество, эле-
Глава
7.
менты
151
Множества
которого
состоят
из
элементов
тех
множеств,
двух
которые
содержатся
только в одном из двух множеств:
>>> foo = {10, 20, 30)
>>> bar = {10, 30, 40)
»> foo л bar
{40, 20)
>>> foo.symmetric_difference(bar)
{40, 20)
Для присваивания результата симметрической разности двух множеств первому
множеству предназначены метод
symmetric_difference_update () и оператор А=:
>>> foo = {10, 20, 30)
>>> bar = {10, 30, 40)
»> foo л= bar
»> foo
{40, 20)
Множества можно сравнивать между собой по правилам дискретной математики.
Как следует из данных табл.
7.1, для
некоторых операций сравнения существуют не
только операторы, но и методы. При этом в табл.
7 .1
-
нения, для которых нет соответствующих методов,
не включены операторы срав
это операторы <, > и ==. Сле
дующие примеры показывают использование операторов сравнения:
>>> foo = {10, 20, 30)
>>> bar = {10, 30, 40)
>>> spam = {30, 10)
>>> eggs = {30, 20, 10)
»> foo > bar
False
»> foo >= bar
False
»> foo < bar
False
»> foo <= bar
False
>» foo == bar
False
Для множеств foo и bar все пять операторов сравнения возвращают результат
False,
поскольку эти множества пересекаются, но ни одно из них не является под
множеством другого (в отличие от множеств foo и spam):
>» foo > spam
True
Все элементы множества
жит больше элементов:
»> foo
True
==
eggs
spam содержатся в множестве foo, но при этом foo содер
Часть
152
Базовые понятия и встроенные типы
1.
>>> foo > eggs
False
foo >= eggs
True
»>
Заключение
В этой главе мы познакомились с двумя классами, представляющими в языке
Python
множества:
set
и
frozenset.
Они различаются тем, что класс
frozenset
яв
ляется неизменяемым типом, то есть не содержит в себе методов, которые могли
бы изменить его содержимое после создания экземпляра класса.
Мы научились создавать экземпляры класса
зуя конструктор класса
выражения{fоr
set,
set
несколькими способами: исполь
выражение с применением фигурных скобок, а также
... in ... }ИЛИ{fоr ... in ... if ... }.
Существуют ограничения на типы элементов, хранимых в множествах,
должны быть хешируемыми. При этом сам класс
класс
frozenset -
set
-
они
не является хешируемым, а
является.
Познакомились мы здесь и с основными методами и операторами, которые можно
применять к множествам. В первую очередь, это операторы
in
и
not in,
предна
значенные для определения наличия или отсутствия элемента в множестве.
Рассмотрели различные операции над множествами: объединение, разность, пере
сечение и симметрическую разность, а также операции сравнения.
Большинство примеров в этой главе использовали класс
set,
но во многих из них за
исключением примеров, где множества изменяются, можно было бы задействовать
класс
frozenset.
В следующей главе мы продолжим изучать встроенные классы
про строки.
Python
и поговорим
- ГЛАВА 8-
СтрОКИ
При разработке программ часто приходится работать с текстом
читать и пре
-
образовывать входные данные из текстовых файлов различных форматов (напри
мер,
CSV, JSON, XML
и др.), проверять ввод пользователя на корректность, обра
батывать запросы от веб-сервисов, подготавливать результаты работы скрипта для
вывода в консоль и многое другое.
Python
предоставляет удобные инструменты для работы со строковыми данными,
начиная с простейших операций: получение подстроки, поиск символов в строке,
слияние и разделение строк и заканчивая более сложными, включая разбор текста с
помощью регулярных выражений, создание текста по шаблону, а также чтение и
создание текста в уже упомянутых форматах
Строка
-
CSV, JSON, XML.
это последовательность символов, которые могут быть представлены
различными способами. В
Python 3 для
Unicode.
представления символов в строках исполь
зуется стандарт кодирования
Стандарт
Unicode
описывает однозначное соответствие между целочисленными
номерами и огромным количеством символов. Таблица
Unicode
охватывает симво
лы из различных языков, диакритические знаки, пиктограммы, смайлики (эмодзи),
математические символы и многое другое. На момент подготовки книги актуаль
ной является версия
Unicode 17 .О,
которая включает в себя почти
160
тысяч симво
лов, и это число увеличивается с появлением новых версий стандарта.
Для представления всех этих символов применяются различные кодировки, кото
рые описывают способ преобразования номера символа в последовательность бай
тов. По этой причине интерпретатор
Python
при разборе текста программы должен
знать, в какой кодировке создан файл скрипта. В главе
3
говорилось о том, как ука
зать кодировку файла с исходным кодом, и что по умолчанию используется коди
ровка
UTF-8,
в которой символы могут иметь различный размер,
-
от одного до
четырех байтов.
Внутреннее представление строк в
Python
достаточно сложное, и способ хранения
зависит от набора символов, содержащихся в строке. Так, во внутреннем представ
лении строки могут представлять собой последовательности из одного, двух или
четырех байтов. К счастью, разработчику на
Python
в большинстве случаев не нуж
но задумываться о внутреннем представлении строк.
Часть
154
1.
Базовые понятия и встроенные типы
Создание строк
Для хранения и обработки строк в
Python
предназначен встроенный класс str. Со
строками мы уже работали и знаем, что для их создания используются одинарные
или двойные кавычки. Оба способа создают экземпляры класса str:
>>> foo = "Hello"
>>> bar = 'Привет'
»> type(foo)
<class 'str'>
>» type (bar)
<class 'str'>
Два способа создания строк нужны для того, чтобы было удобнее использовать ка
вычки внутри строковых литералов. Напомним, что литерал
-
это некоторое зна
чение, в рассматриваемом случае строка, записанное непосредственно в тексте про
граммы (см. главу
2).
То есть и "Hello", и 'Привет' в приведенном примере
-
это
строковые литералы. Если в строковом литерале нужно использовать двойные ка
вычки, то мы не можем написать:
text =
"Отмена""
"Нажмите кнопку "Сохранить" или
Интерпретатор посчитает, что строка закончилась второй двойной кавычкой ("На
жмите
"), а дальше идет непонятный текст программы, который вызовет
кнопку
ошибку. Зато внутри строкового литерала, который обрамляется одинарными ка
вычками, можно использовать двойные кавычки и наоборот:
>>> foo
>>> bar =
'Нажмите
кнопку
"Сохранить"
или
"Отмена"'
"Нажмите
кнопку
'Сохранить'
или
'Отмена'"
Если же все-таки необходимо внутри строкового литерала использовать тот же тип
кавычек, который обрамляет строку, то для этого применяется экранирование сим
волов с помощью обратного слеша
«\».
То есть, если в строковом литерале после
обратного слеша следует кавычка (одинарная или двойная), то
Python
не будет ин
терпретировать эту кавычку как символ завершения строки и включит ее в текст:
>>> foo =
>>> foo
"Нажмите кнопку \"Сохранить\" или \"Отмена\""
'Нажмите кнопку
>>> bar =
>>> bar
"Сохранить" или "Отмена"'
'Нажмите кнопку \'Сохранить\'
"Нажмите кнопку
Это свойство
'Сохранить'
Python
или
или \'Отмена\''
'Отмена'"
полезно, когда в строке нужно использовать кавычки разных
видов.
Многострочные литералы
Обратный слеш используется в строках не только для добавления кавычек
-
суще
ствуют еще несколько специальных символов, которые можно добавить в строки с
Глава
8.
Строки
155
его помощью. Это так называемые непечатаемые символы. Наиболее часто из них
применяются:
♦
♦
♦
\n \r \t -
перевод строки;
возврат каретки;
горизонтальная табуляция.
Есть еще несколько символов, которые сейчас практически не используются, по
этому мы и упоминать их не будем. Относительно горизонтальной табуляции, ско
рее всего, комментарии не требуются, а вот про перевод строки и возврат каретки
коротко надо сказать.
Символ, который записывается как
Feed
или, сокращенно,
LF
\n, в англоязычной литературе называется Line
и имеет номер Од в шестнадцатеричной системе счисле
ния. Он предписывает выполнить перевод строки, то есть создать новую строку и
перейти на нее. В следующем примере текст будет выведен в виде трех строк:
»>
print("Пepвaя строка\nВторая строка\nТретья строка")
Первая
строка
Вторая
строка
Третья
строка
Мы говорили ранее, что если функции
enct,
print () не передать в явном виде параметр
то по умолчанию производится перевод строки. Так вот, значение по умолча
нию для параметра
end
как раз и равно строке
"\n".
Другой специальный символ
туре используется термин
- \r, или возврат каретки. В англоязычной литера
Carriage Return или, сокращенно, CR. Этот символ пред
писывает перевести курсор на начало строки (без создания новой строки и перевода
на нее). Это можно продемонстрировать таким примером:
»> print ("Привет,
###вет,
мир\r###")
мир
Здесь после вывода символов "Привет,
следующие символы Н
#
мир" курсор перейдет на начало строки, и
продолжат выводиться с начала строки, перезаписывая
символы, которые там были до этого.
Исторически сложилось так, что в разных операционных системах для добавления
новой строки в файл или в консоль используются разные символы. В
Linux- это
символ \n, в
ко интерпретатор
Python
macOS- это \r,
а в
UNIX
и
Windows- комбинация \r\n. Одна
\ n под особенности
по умолчанию «подгоняет» символ
операционной системы таким образом, чтобы этот символ всегда интерпретировал
ся как перевод строки. На такое поведение нужно обращать внимание при записи и
чтении файлов, если предполагается, что эти файлы будут читаться в разных опе
рационных системах. При необходимости такое поведение можно отключать, но
мы этого делать не станем. В дальнейшем для нас символ
который переводит курсор на новую строку.
\n -
это всегда символ,
Часть
156
Мы уже увидели, как с помощью символа
1.
Базовые понятия и встроенные типы
\n можно разбивать строку на несколько
строк. Это удобно, когда у нас небольшое количество таких символов, однако когда
их становится много, читаемость строковых литералов заметно ухудшается.
В
Python
мы можем в качестве символов, обрамляющих строки, задействовать три
одинарные или двойные кавычки, и в этом случае вставлять в строку символы пе
ревода строк в явном виде, без использования последовательности
ния типов, это будет всё тот же класс str (листинг
\n. С точки зре
8.1).
Лмстинr 8.1. Chapter_08/example_01/multlline.py
foo = """Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua."""
bar = '' 'Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.'''
print ("type (foo) : ", type (foo))
print ( "type (bar) : ", type (bar))
print ()
print("foo:", foo)
print ()
print("bar:", bar)
Таким вот образом и создаются многострочные строки
(multiline strings).
Русскоя
зычный перевод этого термина выглядит как тавтология из-за того, что английские
слова
line
и
string
оба переводятся на русский язык как «строка». Поэтому мы бу
дем пользоваться термином «многострочные литералы». Более аккуратно было бы
их называть «многострочные строковые литералы», но это слишком длинно (и
опять возникает тавтология), да к тому же в
Python
нет никаких других литералов,
которые могут занимать несколько строк.
Если мы запустим скрипт из листинга
текст:
type(foo): <class 'str'>
type(bar): <class 'str'>
foo: Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
8.1,
то в консоль будет выведен следующий
Глава
8.
Строки
157
bar: Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut laЬore et dolore magna aliqua.
При создании многострочных литералов надо быть аккуратнее с отступами,
-
нужно учитывать, что все пробелы в начале каждой строки будут включены в лите
рал. Эту особенность демонстрирует следующий пример (листинг
8.2).
Л11СТ11нr 8.2. Chapter_08lexample_02/multlline_spaces.py
if True:
foo = """Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut laЬore et dolore magna aliqua."""
bar = """Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua."""
print("foo:", foo)
print ()
print("bar:", bar)
В результате пробелы в начале строк в переменной bar сохранятся:
foo: Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
bar: Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Еще одна приятная особенность многострочных литералов заключается в том, что
внутри них можно использовать любой вид кавычек, не экранируя их, если только
они не должны быть расположены три подряд:
>>> print(''
'Этот текст содержит
Этот текст содержит
>>>
'одинарные'
print("""Этoт текст содержит
Этот текст содержит "двойные"
'одинарные'
кавычки''')
кавычки
"двойные"
кавычки
кавычки""")
Часть
158
Вставка символов
1.
Базовые понятия и встроенные типы
Unicode
Иногда бывает нужно добавить в строку символы, которых нет на клавиатуре. Это
могут быть не самые популярные математические операторы, такие как
"
или
±,
эмодзи или просто символы из другого языка, для которого у вас не установлена рас
кладка клавиатуры. Такую задачу вы можете решить несколькими способами. Мож
но найти нужный символ в таблицах
Unicode (существует
множество сайтов, предос
тавляющих поиск символов по описанию) и вставить его непосредственно в код:
>>> print ( "Это символ Unicode - 15")
Это символ Unicode - 15
>>> print ( "Это символ Unicode - @
11 )
Это символ Unicode - @
Но, как правило, в таких ситуациях желательно вставлять символ по его коду или
строковому идентификатору. При использовании способа «скопировать подходя
щий символ»
-
«вставить нужный символ» можно ошибиться, когда визуально по
хожие знаки на самом деле разные. Применяя вставку по коду символа, вы можете
быть уверены, что добавляете именно тот символ, который нужен. Для этого вы
должны найти нужный символ в таблице
Unicode
и открыть его описание, где сре
ди прочих его свойств указываются название символа и его представления в коди
ровках
UTF-16
и
UTF-32.
Например, для показанных в предыдущем примере сим
волов это:
♦
название:
15 -
Ох03В4;
□
«Greek Small Letter Delta»
UTF-32: Ох000003В4;
(греческая малая буква «дельта»);
UTF-16:
@ - название: «Rolling On the Floor Laughing» (кататься по полу от смеха);
UTF-16: 0xD83E 0xDD23; UTF-32: 0x0001F923.
Здесь видно, что символ@, в отличие от символа 15, нельзя представить двумя бай
тами,
-
в кодировке
UTF-16
для него требуются
4
байта.
Если символ можно представить в виде двух байтов в кодировке
UTF-16
(как тот
же символ 15), то для его добавления в строковый литерал нужно вставить последо
вательность
\ u ## ##, где
UTF-16.
вместо ## # # должен стоять шестнадцатеричный код симво
ла в кодировке
Если двух байтов для представления символа недостаточно, то его можно добавить
в строковый литерал по шестнадцатеричному представлению в кодировке
UTF-32,
для чего вставить в строку последовательность \U######## (здесь используется за
главная буква
u),
где вместо ######## должен быть подставлен шестнадцатеричный
код символа в кодировке
UTF-32.
Предыдущий пример мы можем переписать следующим образом:
»>
print("Этo символ
Это символ
Unicode -
\u0ЗЬ4")
Unicode - 15
>» print ( "Это
символ
Unicode - \U000lf923")
Это символ Unicode - @
Глава
8.
Строки
159
Символ i5 также можно было бы представить в кодировке
UTF-32,
заменив
\u
на \U
и добавив в начале четыре нуля, но с практической точки зрения смысла в этом нет:
»> print ("Это символ Unicode - \U000003b4")
Unicode - i5
Символы Unicode также можно добавлять
Это символ
в строковые литералы с помощью ком
бинации \N { имя символа}:
»> print ("Это символ Unicode - \N{Greek Small Letter Delta}")
Unicode - i5
»> print ("Это символ Unicode - \N{rollinq on the floor lauqhinq}")
Это СИМВОЛ Unicode - @
Это символ
«Сырые» строки
В предыдущих разделах мы увидели, что обратный слеш используется для вставки
специальных символов и экранирования кавычек. А как быть, если нам нужно, что
бы в строковом литерале содержался непосредственно символ«\»
-
без интерпре
тации специальных символов после него? Для этого надо заэкранировать обратный
слеш другим обратным слешем, или, другими словами, удвоить его:
»>
рrint("Символ
Символ
\n -
\\n -
это перевод строки,
это перевод строки, а
\t -
а
\\t -
табуляция.")
табуляция.
Такой способ работает, однако он может сильно ухудшить читаемость текста, если
в нем содержится много обратных слешей:
>» foo = "Этот
»> print (foo)
текст\\tсодержит\\\\слишком много\\\\обратных слешей\\n"
Этот текст\tсодержит\\слишком много\\обратных слешей\n
В этом случае
Python
позволяет создавать так называемые «сырые»
(raw)
строки
(«сырые» строковые литералы). В «сырых» строковых литералах все символы ста
нут интерпретироваться непосредственно так, как они записаны, и слеши не будут
использоваться для задания специальных символов. Для того, чтобы создать «сы
рую» строку, нужно непосредственно перед открывающейся кавычкой (без пробе
ла) добавить символ
«r»
или
«R».
Тогда предыдущий пример можно переписать
следующим образом:
>>> foo = r'Этот
>» print (foo)
текст\tсодержит\\слишком много\\обратных слешей\n'
Этот текст\tсодержит\\слишком много\\обратных слешей\n
»> type (foo)
<class 'str'>
Как видно из этого примера, «сырые» строковые литералы создает всё тот же эк
земпляр класса
s t r, -
такая конструкция введена в
Python
исключительно для
удобства. «Сырые» строки особенно будут полезны при работе с регулярными вы
ражениями, о которых речь пойдет в главе
23.
Часть
160
1.
Базовые понятия и встроенные типы
Создание строкового представления чисел
и других объектов
Во многих ситуациях бывает необходимо преобразовать какой-либо объект в его
строковое представление. Например, из числа 4 2 создать строку
11
42 11 • Часто это
делается неявно, например, при выводе объектов в консоль, но иногда требуется
выполнить такое преобразование явно.
Для создания строкового представления объекта существуют два способа. Один из
них предназначен для представления объекта в том виде, как его должен видеть
конечный пользователь, а второй
-
для создания представления объекта таким об
разом, чтобы это представление как можно точнее описывало внутреннюю струк
туру объекта, и этот способ, в первую очередь, предназначен для программистов,
когда они отлаживают программу или выводят данные в лог работы. Однако ино
гда оба эти представления совпадают. В первом случае используется функция
а
str ( J,
во
втором
-
функция
repr ( J
(сокращение
от
английского
слова
representation).
Следующий пример показывает создание строковых представлений для класса
float (результаты вызовов функций str () и repr () совпадают) и класса datetime
из модуля ctatetime, предназначенного для работы с календарными данными. Для
класса datetime вызов функций str ( J и repr ( J приведет к разным результатам:
>>> import datetime
»> foo = 20.5
>» str(foo)
'20. 5'
»> repr (foo)
'20.5'
>>> date = datetime.datetime(2025, 4, 28, 18, 40, 2)
>» str(date)
'2025-04-28 18:40:02'
»> repr(date)
'datetime.datetime(2025, 4, 28, 18, 40, 2)'
Базовые операции над строками
Поскольку строки
-
это последовательности символов, то к ним применимы такие
же операции, как и к коллекциям, например, расчет длины с помощью функции
len ()
и получение одного символа или подстроки с помощью оператора квадрат
ных скобок.
Строки
-
это неизменяемые объекты, поэтому изменить символ внутри уже соз
данной строки не удастся, но при необходимости можно создать новую строку с
изменениями.
Начнем с определения длины строки. Как мы говорили ранее, каждый символ мо
жет быть представлен последовательностью от одного до четырех байтов. Может
Глава
8.
Строки
161
возникнуть вопрос, в чем тогда измеряется длина строк? Длина строки измеряется в
символах, а не в байтах, что очень удобно, поскольку программистам на
Python при
этом не приходится заниматься интерпретацией последовательностей байтов в за
висимости от кодировки, что является очень нетривиальной задачей. Продемонст
рируем эту особенность следующим примером:
»> len ("Hello")
5
»> lеn("Привет")
6
»> len ("ЙЧlт'')
2
»> len ("\U000lf923")
1
Все другие операторы и функции, работающие со строками, также подразумевают,
что любая индексация в строке работает именно с символами.
Для строк применимы операторы in и not
in, с помощью которых мы можем оп
ределять, является ли одна строка подстрокой другой строки:
»> spam = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
»> "ipsum" in spam
True
»> "hello" not in spam
True
»> "dolor" not in spam
False
С помощью оператора квадратных скобок из строки можно извлекать отдельные
символы и подстроки. В
Python
нет отдельного класса для представления одного
символа, поэтому оператор получения даже одного символа из подстроки возвра
щает экземпляр класса
»> foo =
»> foo
"Привет
str:
\U000lf923"
'Привет@,
>» foo[O]
'П'
»> type(foo[O])
<class 'str'>
»> fоо[З]
'в'
Поскольку строки
-
неизменяемые объекты, мы не можем изменить символ по
индексу. При попытке это сделать будет возбуждено исключение TypeError:
>>> foo[-1] = "!"
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
Часть
162
foo[-1]
1.
Базовые понятия и встроенные типы
11111
TypeError: 'str' object does not support item assignment
Так же, как и при работе со списками, мы можем получать интервал символов
-
подстроки. Задание интервала для выделения подстроки работает точно так же, как
и получение срезов в коллекциях (см. главу
4):
символ с начальным индексом
включается в результат, а символ с заключительным индексом
т. е. подстрока выделяется на полуоткрытом интервале
-
не включается,
[start; end):
>>> spam = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
»> spam[2:10]
'rem ipsu'
>>> spam[2:14:2]
'rmismd'
>>> spam[9:1:-l]
'uspi mer'
>>>#Выделить первые 5 символов
»> spam [: 5]
'Lorem'
>>>#Выделить последние 4 символа
»> spam[-4:]
'elit'
»> spam[:]
'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
>» spam[::-1]
'tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL'
В этом примере для выделения всех символов строки используется выражение
spam [ : J , однако на практике оно бессмысленно. Поскольку строки -
неизменяе
мые объекты, нет причин делать их копию, достаточно простого присваивания дру
гой переменной.
Некоторые методы класса
Класс
str
str содержит достаточно много методов. Мы не станем рассматривать их
все, однако вам было бы полезно почитать соответствующую страницу документа
ции 1, чтобы иметь представление об обширных возможностях, которые предостав
ляет класс
str для работы с текстом. Далее мы увидим, что делают некоторые из
этих методов.
Методы
startswi th ()
и
endswi th ()
предназначены для определения, является ли
переданная в качестве параметра строка началом или окончанием строки, для кото
рой вызывается метод:
>>> foo
"images/picture.png"
>>> bar = "data/application.exe"
1 См.
https://docs.python.org/3/library/stdtypes.html#string-methods.
Глава
8.
163
Строки
>» foo. startswi th ( 'images' )
True
»> bar. startswi th ( 'images')
False
»> foo.endswith (' .png')
True
»> bar.endswith(' .png')
False
В качестве параметра эти методы могут принимать не только строку, но и кортеж
строк (но не списки или другие коллекции), которые можно проверять в качестве
начала или окончания строки:
»> foo.endswith(('.jpeg', '.jpg', '.png'))
True
»> bar. startswi th ( ( 'data', 'etc'))
True
Методы startswi th () и endswi th () способны принимать необязательные парамет
ры, которые могут ограничивать интервал символов, где проверяется совпадение
начала
/ конца
строки.
Мы говорили, что для определения вхождения подстроки в строку используется
оператор in, однако, если нам недостаточно только факта вхождения, но еще хоте
лось бы узнать номер позиции, где искомая строка начинается, то мы можем вос
пользоваться методом find (). Список параметров этого метода выглядит следую
щим образом:
find(sub[, start[, end]])
Здесь sub -
это искомая подстрока, а необязательные параметры start и end могут
ограничивать интервал поиска. Метод f ind ()
возвращает наименьшую позицию
начала подстроки sub, если она входит в строку, для которой вызывается этот ме
тод. Если же подстрока не найдена, то метод find () возвращает значение -1 (минус
один). Следующий пример показывает несколько вариантов использования метода
find () -
с использованием ограничения интервала поиска и без него. Обратите
внимание, что так же, как и при задании срезов, мы можем использовать отрица
тельные индексы:
»> foo =
Lorem ipsum dolor sit amet,
... Lorem ipsum dolor sit amet,
... Lorem ipsum dolor si t amet. 111111
»> foo.find('ipsum')
6
111111
>>>#Поиск регистрозависимый
»> foo.find('Ipsum')
-1
»> foo.find('ipsum', 7)
34
>>> foo.find('ipsum', 35, 60)
Часть
164
1.
Базовые понятия и встроенные типы
-1
>>> foo.find('ipsum', 35, -5)
62
После этого примера может возникнуть вопрос, как нам найти все вхождения под
строки в строку? Для этого мы можем последовательно в цикле вызывать метод
find (), пока он не вернет значение -1, только при этом надо не забывать изменять
начальную позицию поиска при каждом удачном нахождении подстроки. Возмож
ная реализация такого алгоритма показана в листинге
8.3.
Листинг 8.3. Chapter_08/example_OЗ/find_all.py
foo = """Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet.
111111
# Искомая строка
sub = "ipsum"
result = []
pos = foo.find(suЬ)
while pos != -1:
result.append(pos)
pos = foo.find(suЬ, pos + 1)
print("result:", result)
В результате выполнения этого скрипта будет выведена строка:
result: [6, 34, 62)
Обратите внимание, что в коде листинга
8.3
позиция, с которой начинается поиск
следующего вхождения подстроки, на единицу больше, чем позиция последней
найденной подстроки.
И всё же не очень красиво, что в этом примере мы вызываем метод f ind () в двух
разных местах. Хотя эти вызовы не совсем одинаковые, но программисты всегда
стараются сократить количество дублированного кода. Было бы неплохо, если бы
мы могли совместить присвоение переменной pos значения положения очередной
найденной подстроки и сравнение со значением -1. Для этой цели отлично подхо
3.
8.4).
дит оператор := («моржик»), с которым мы познакомились в главе
ем этот пример можно переписать следующим образом (листинг
Листинг 8.4.
Chapter_08/example_04/find_all_walrus.py
foo = """Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet.
111111
С его участи
Глава
8.
Строки
165
# Искомая строка
sub = "ipsum"
result = []
pos = -1
while (pos := foo.find(suЬ, pos + 1)) != -1:
result.append(pos)
print("result:", result)
Перед каждой итерацией цикла осуществляется поиск подстроки с позиции, сле
дующей за позицией, найденной на предыдущей итерации. На первой итерации по
иск осуществляется с начала строки, то есть с позиции О. И сразу же происходит
проверка на неравенство результата вызова метода
find ()
значению
-1.
В последних примерах есть один недостаток. Дело в том, что наш поиск подстроки
регистрозависим
-
то есть в тексте не нашлись бы слова
«Ipsum»
или
«IPSUM»,
если бы они там присутствовали. Поэтому часто строки приходится приводить к
одному регистру, чтобы в строке все символы были либо заглавными, либо строч
ными.
Для создания новой строки с символами, у которых изменен регистр, предназначе
ны четыре метода класса
♦
lower () -
str:
возвращает новую строку, в которой все символы переведены в ниж
ний регистр;
♦ upper ( J
-
возвращает новую строку, в которой все символы переведены в верх
ний регистр;
♦
са р i t а 1 i z е ( ) -
возвращает новую строку, в которой первый символ будет в
верхнем регистре, а все остальные
♦ ti tle ( J -
-
в нижнем;
возвращает новую строку, в которой первый символ в каждом слове
будет в верхнем регистре, а все остальные- в нижнем.
♦ Например:
»> foo = "lorem IPSUМ
>» foo.lower ()
'lorem ipsum dolor sit
>» foo. upper ()
'LOREM IPSUМ DOLOR SIT
>>> foo.capitalize()
'Lorem ipsum dolor sit
»> foo.title()
'Lorem Ipsum Dolor Sit
dolor SIT amet"
amet'
АМЕТ'
amet'
Amet'
Теперь мы можем изменить пример поиска всех вхождений строки (листинг
8.5).
Часть
166
Листинг 8.5.
1.
Базовые понятия и встроенные типы
Chapter_08/example_05/find_all_case.py
foo = """Lorem ipsum dolor sit amet,
Lorem IPSUМ dolor sit amet,
Lorem IpSuМ dolor sit amet. 111111
#
Искомая строка
sub = "Ipsum"
#
Приводим обе строки к нижнему регистру
foo lower = foo.lower()
sub lower = sub.lower()
result = []
pos = -1
while (pos := foo_lower.find(suЬ_lower, pos + 1)) != -1:
result.append(pos)
print("result:", result)
Несмотря на то, что в переменной foo слово
«ipsum»
встречается в разных регист
рах, все они будут найдены.
Бывают ситуации, когда в строке, полученной от пользователя или прочитанной из
файла, перед основным содержанием текста или после него могут присутствовать
лишние пробелы, символы перевода строк или табуляции. Как правило, такие вход
ные строки нужно предварительно очистить от подобных «пробельных» символов.
В классе
♦
str для решения этой задачи есть несколько методов:
lstrip () -
возвращает копию строки с отброшенными «пробельными» симво
лами в начале строки;
♦
rstrip () -
возвращает копию строки с отброшенными «пробельными» симво
лами в конце строки;
♦
strip () -
возвращает копию строки с отброшенными «пробельными» симво-
лами в начале и конце строки.
Вот небольшой пример использования этих методов:
>>> foo ="
Lorem ipsum
>>> foo.lstrip()
'Lorem ipsum
>>> foo.rstrip()
Lorem ipsum'
»> foo. strip ()
'Lorem ipsum'
Глава
8.
Строки
167
Методы lstrip (), rstrip () и strip () могут принимать необязательный строковый
параметр, в котором должны быть указаны символы, которые нужно отбрасывать.
Например:
»> foo = " .... # Lorem ipsum ....
>>> foo.strip(" .#")
'Lorem ipsum'
Все символы, указанные здесь в строковом параметре метода strip (), были удале
ны из начала и конца строки.
Если же с начала или с конца строки нужно удалить не отдельные символы, а це
лые подстроки, то для этого предназначены следующие методы класса
♦
removeprefix (prefix) -
str:
возвращает новую строку, у которой в начале будет
отброшена строка prefix, если исходная строка начинается с этой строки.
В противном случае возвращается неизменная копия исходной строки;
♦ removesuffix (suffix) -
возвращает новую строку, у которой в конце будет
отброшена строка suffix, если исходная строка заканчивается этой строкой.
В противном случае возвращается неизменная копия исходной строки.
Вот примеры работы этих функций:
»> foo = "images/picture.png"
>>> foo.removeprefix('images/')
'picture.png'
»> foo.removesuffix(' .png')
'images/picture'
»> foo. removepref ix ( 'bin/' )
'images/picture.png'
>» foo. removesuffix (' . ехе')
'images/picture.png'
В классе str, помимо метода для поиска подстроки, имеется метод replace ( 1 -
для замены подстроки. Этот метод достаточно прост и имеет следующий синтаксис:
replace (pld, new [, count])
Здесь первый аргумент old- это текст, который надо найти в строке, для которой
метод вызывается, new -
это текст, на который следует заменить найденную стро
ку. Необязательный параметр count определяет, какое максимальное количество
замен нужно произвести. Если этот параметр не указан, то будут заменены все най
денные вхождения подстроки old. Метод replace () возвращает новый объект str,
который содержит строку после выполнения замен. Поиск в строке при использо
вании метода replace ( 1 регистрозависимый.
Работу метода replace () демонстрирует пример, приведенный в листинге
Листмнr 8.6.
Chapter_08/ex1mple_06/repl1ce.py
foo = """Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet."""
8.6.
Часть
168
1.
Базовые понятия и встроенные типы
old = "ipsum"
new
"*****"
bar
baz
foo.replace(old, new)
foo.replace(old, new, 1)
print("bar:
print("baz:
bar, end="\n\n")
baz)
Результат работы этого скрипта выглядит следующим образом:
bar: Lorem ***** dolor sit amet,
Lorem ***** dolor sit amet,
Lorem ***** dolor sit amet.
baz: Lorem ***** dolor sit amet,
Lorem ipsum dolor sit amet,
Lorem ipsum dolor sit amet.
Достаточно часто возникают задачи, когда требуется разбить строку на несколько
независимых строк по какому-либо разделителю (сепаратору). Например, при раз
боре файла формата
CSV (Comma-Separated Values)
таким разделителем может
быть запятая. Иногда данные записываются в текстовом файле в виде таблицы, ко
гда данные в каждой строке разделены несколькими пробелами или символами та
буляции. Для разделения строки по разделителю предназначены методы spli t () и
rspli t (). Отличие второго метода от первого мы покажем чуть позже, а сначала
spli t ():
рассмотрим синтаксис метода
split(sep=None, maxsplit=-1)
Метод
spl i t () возвращает список строк, разделенных по разделителю sep. Если
этот параметр не указан или равен None, разделение будет производиться по «про
бельным» символам. Второй необязательный параметр maxspli t указывает, какое
максимальное количество подстрок нужно выделить из строки. Если это значение
равно -1 или не указано, строка будет разделена по всем найденным разделителям.
Рассмотрим этот метод на следующих примерах:
>>> foo = "a=lO, Ь=20, с=ЗО, d=40, e=SO"
>» foo.split()
['a=lO,', 'Ь=20,', 'с=ЗО,', 'd=40,', 'e=SO']
>>> foo.split(", ")
['a=lO', 'Ь=20', 'с=ЗО', 'd=40', 'e=SO']
>>> foo.split(", ", maxsplit=2)
[ 'a=lO', 'Ь=20', 'с=ЗО, d=40, e=SO']
Обратите внимание на последний пример. Метод split () выделил две подстроки
согласно
параметру
maxspl i t=2,
а
третьим
элементом
результирующего
списка
стал остаток исходной строки. При этом две подстроки были «отрезаны» именно от
левой части строки. Если же нужно, чтобы разбор строки начинался не с начала, а с
Глава
8.
Строки
169
конца строки, нужно воспользоваться другим методом
-
rstrip (). Работает он
точно так же, только разбор строки начинает с ее правого края:
»> foo.rsplit(", ", maxsplit=2)
['a=lO,
В
Ь=20,
классе
с=ЗО',
st r
'd=40',
имеется
'е=50']
метод,
который
противоположен
методам
s р1i t ( )
и
rspli t (). Метод j oin () «склеивает» строку из списка строк. Он принимает в каче
стве аргумента список (или другую коллекцию) строк, а возвращает одну строку,
объединяющую переданный список строк. При этом разделителем между «склеи
ваемыми» строками будет выступать строка, для которой вызывается метод j oin ():
>>> bar = ["a=lO",
>» ", ".join(bar)
'a=lO,
Ь=20,
"Ь=20",
"с=ЗО"]
с=ЗО'
Мы рассмотрели далеко не все методы класса str -
их достаточно много, поэтому
для того, чтобы иметь более полное представление о возможностях этого класса,
полезно ознакомиться с остальными методами, описанными в документации.
Заключение
В этой главе мы начали знакомство с классом str, предназначенным для работы со
строками. Строки представляют собой последовательности Uniсоdе-символов, спо
собные занимать от
1 до 4
байтов. Но при использовании класса str в большинстве
случаев об этом можно не задумываться. Все операции, в которых вычисляются
длина строки, положение подстроки и т. д., в качестве параметров используют ин
дексы символов, а не байтов.
Мы познакомились со способами создания строк и поговорили о том, почему для
задания строк можно использовать два вида кавычек. Рассмотрели экранирование
символов с помощью символа обратного слеша
«\»,
а также узнали, каким образом
обратный слеш используется при добавлении в строку непечатаемых символов.
Научились вставлять в строковые литералы символы
Unicode
названию символа. Поговорили про многострочные литералы
нали про «сырые»
(raw)
по их номеру или по
(multiline strings).
Уз
строки, в которых обратный слеш не является экранирую
щим символом.
Строки имеют много общего со списками и кортежами. Для расчета длины строки
используется встроенная функция len (), а с помощью операторов in и not
in
можно определить, содержится ли заданная подстрока в строке. В завершение мы
рассмотрели некоторые наиболее часто используемые методы класса str.
Впереди у нас еще одна глава, связанная с классом str, -
в ней мы рассмотрим
несколько способов форматирования строк с использованием шаблонов.
- ГЛАВА 9-
Форматирование строк
В главе
8
мы познакомились с классом str, который предоставляет множество ме
тодов для работы с текстом. Однако одну часто используемую операцию мы до сих
пор не рассматривали. Имеется в виду создание текста по шаблону и форматирова
ние числовых данных. Чтобы стало понятно, о чем идет речь, рассмотрим пример.
Пусть у нас имеется несколько переменных разных типов:
foo
bar
baz
42
12.5
"hello"
Допустим, что мы хотим на основе этих переменных составить строку:
Целое число:
В
Python
42;
Число с плавающей точкой:
12.5;
Строка:
"hello".
для конкатенации (склеивания) строк используется оператор
«+».
Дейст
вуя слишком прямолинейно, мы можем написать следуюший код (на практике так
делать не рекомендуется):
spam = ( 'Целое число: ' + str ( foo) +
'; Число с плавающей точкой: ' + str(bar) +
' ; Строка: " ' + baz + '". ' )
print(spam)
В качестве результата выполнения этого скрипта мы получим в точности ту же
строку, которую хотели сформировать, однако в таком подходе имеется сразу не
сколько проблем.
Во-первых, такой код тяжело читать из-за обилия кавычек, операторов сложения и
преобразований чисел в строки с помощью вызова функции str (). Если в будущем
потребуется изменить шаблон строки, то придется потратить немало усилий, чтобы
разобраться, как должен выглядеть результат исходного выражения, и куда нужно
вносить изменения.
Вторая проблема заключается в том, что такой способ создания строки очень не
эффективный. Чтобы сформировать окончательную строку, интерпретатор будет
вынужден создавать множество промежуточных строк. Даже если не считать вызо
вы функции str (), то интерпретатору придется последовательно создавать фраг
менты результирующей строки, которые будут по очереди «склеиваться». Сначала
он создаст строки: "Целое
число:
",
затем "Целое
число:
42",
затем "Целое
чис-
Глава
ло:
9.
171
Форматирование строк
4 2; Число с плавающей точкой: " и т. д. И это мы еще не задаемся вопросом,
каким образом мы хотим представлять в виде строки число с плавающей точкой:
сколько цифр оставлять после запятой, может быть, его стоит представить в экспо
ненциальном формате, нужно ли писать знак
«+»
для положительных чисел и пр.
Для того, чтобы избежать множественного создания строк, повысить читаемость
шаблона текста, а также предоставить возможность задавать формат подставляе
мых числовых данных, предназначены подходы, о которых рассказывается в этой
главе. Всё это настолько часто используемые операции, что разработчики
Python
постоянно пытаются улучшить способы форматирования строк, иногда предлагая
кардинальные изменения. Поэтому в
Python
на сегодняшний день накопилось не
сколько способов работы с текстовыми шаблонами, и мы рассмотрим три из них.
Использование оператора
%
Начнем с самого старого из рассматриваемых способов. Сейчас его уже редко ис
пользуют, но мы про него скажем по нескольким причинам. Во-первых, при его
изучении становятся видны исторические корни других методов, во-вторых, нота
ция форматирования строк из этого метода применяется в некоторых стандартных
и сторонних библиотеках (например, в модуле logging, предназначенном для веде
ния логов работы программы, и в библиотеке
NumPy -
для указания формата чи
сел при записи их в текстовый файл). Заодно на примере такого способа формати
рования мы выявим некоторые проблемы, которые были решены в более новых
способах.
Если вы программировали на языке С, то увидите в этом способе форматирования
много знакомого. Примененный здесь формат записи строк форматирования был
позаимствован из функции printf () языка С, а затем дополнен.
Для форматирования строк с использованием оператора
%используется
следующий
синтаксис:
foo = Строка
форматирования
% (varl, var2, ... , varN)
Здесь Строка форматирования -
это строковая переменная (литерал), содержащая
специальные символы, обозначающие поля подстановки
(replacement fields),
на ме
сто которых будут подставлены переменные varl, var2 и т. д., указанные в кортеже
после оператора
%.
Символы подстановки в Строке
форматирования представляют
собой комбинации, начинающиеся со знака«%», после которого указывается способ
форматирования подставляемого значения.
Предыдущий пример мы можем переписать с использованием оператора
дующим образом (листинг
Листинг 9.1.
foo
=
9.1).
Chapter_09/example_01/percent.py
42
bar = 12.5
baz = "hello"
% сле
Часть
172
spam
=
( 'Цел о е числ о :
%d;
% (foo, bar,
Ьаz))
Числ о с плавающей т очкой:
1.
Базовые понятия и встроенные типы
%f;
Строка:
11
%s 11
• '
print(spam)
Выводимый в консоль результат будет немного отличаться от того, который мы
получили ранее:
Целое число:
42;
Число с плавающей точкой:
12.500000;
Строка:
11
hello".
В этом примере используются три набора символов подстановки: %d, %f
и %s. Буква
после знака « %» задает тип значения, которое будет подставлено на место символа
подстановки. Скоро мы увидим, что, помимо буквы, могут быть указаны и другие
параметры форматирования. Символы форматирования и их интерпретация пред
ставлены в следующем списке:
♦
d или i -
♦
о
♦
х или х
♦
f или F -
число с плавающей точкой;
♦
е или Е -
число с плавающей точкой в экспоненциальной форме ;
g или G -
целое число или число с плавающей точкой. Формат подбирается ав-
♦
-
целое число в десятичной форме;
целое число в восьмеричной форме;
-
целое число в шестнадцатеричной форме;
томатически;
♦
с
♦
s -
строка, для преобразования объекта в строку используется функция str ();
♦
r -
строка, для преобразования объекта в строку используется функция repr ();
♦
а -
-
одиночный символ ;
строка,
для
преобразования
объекта
в
строку
используется
функция
ascii ();
♦
%-
последовательность %% заменяется на знак процента.
Рассмотрим различия этих символов подстановки на примере целых чисел . Для
представления целых чисел имеются следующие символы: d (или, что то же самое,
i),
о, х и х. Кроме того, для целых чисел мы можем использовать все символы подста
новки, которые предназначены для чисел с плавающей точкой . Следующий пример
(листинг
9.2)
показывает форматирование целых чисел несколькими способами (а
заодно замену последовательности %% на символ %).
Листинг 9.2.
Chapter_09/example_02/percent_lntpy
f oo = 42
spam = """ %%d: %d
%% о:
%о
%% х:
%х
%%Х :
%Х
%% f: %f
%% е:
%е
Глава
%%Е:
9.
Форматирование строк
173
%Е
%%g: %g
111111
% (foo, foo,
foo, foo, foo, foo, foo, foo)
print(spam)
В результате выполнения этого скрипта в консоль будут выведены строки:
%d: 42
%о:
52
%х:
2а
%Х:
2А
%f: 42.000000
%е: 4.200000e+0l
%Е:
4.200000Е+01
%g: 42
Здесь видно, что в рассматриваемом случае совпадают результаты форматирования
чисел с помощью символов %d и %g, результат форматирования для %х и %Х отлича
ется регистром букв, используемых при выводе шестнадцатеричных чисел, анало
гично результат форматирования для %е и %Е отличается регистром символа «е»,
отделяющего мантиссу от экспоненты. Символ %f предназначен для вывода чисел с
фиксированным количеством цифр после запятой, и по умолчанию после запятой
бьто выведено
6 цифр.
Этот пример мы могли бы написать более красиво, избавившись от многократного
повторения переменной f оо в кортеже. Для этого можно заменить повторение всех
элементов кортежа на создание кортежа с помощью размножения одного элемента:
spam = """%%d: %d
%%о:
%о
%%х:
%х
%%Х:
%Х
%%f: %f
%%е:
%е
%%Е:
%Е
%%g: %g
111111
% ( (foo,) * 8)
Помимо приведенных ранее символов подстановки, между знаком процента и бук
вой могут стоять некоторые символы, позволяющие более тонко настраивать фор
матирование данных. Рассмотрим следующий пример, который показывает такие
возможности применительно к целым числам (листинг
Листинr 9.3. Chapter_09/example_03/percent_int_fonnat.py
foo = 42
spam="""1%%8dl:
1%%-Bdl:
1%-Bdl
1%%+-8dl:
1%+-Bdl
1%% -8d 1:
1% -Bdl
1%8dl
9.3).
Часть
174
1%%08dl:
1.
Базовые понятия и встроенные типы
1%08dl
'"'" % ( (foo,) * 5)
print(spam)
Результат выполнения этого скрипта выглядит следующим образом:
1%8dl :
1%-Bd 1:
1
142
1%+-Bd 1:
1 % -Bdl :
1+42
42
1
1%08dl:
1000000421
Здесь символы
421
1
1
« 1))
вокруг полей подстановки добавлены для того, чтобы были за
метны пробелы, которые добавляются около подставляемого значения при исполь
зовании разных настроек форматирования.
Когда между знаком«%)) и буквой указано число, оно обозначает, сколько символов
(знакомест) нужно зарезервировать для отображения числа. Если для отображения
числа требуется меньше символов, то оставшиеся символы дополняются пробела
ми, причем таким образом, что по умолчанию само число оказывается выровнен
ным по правому краю. Если же число нужно выровнять по левому краю, то перед
этим числом нужно поставить знак
Если после символа
«%)),
«-)).
независимо от остальных символов форматирования, мы
поставим символ«+)), это будет обозначать, что при подстановке числа всегда тре
буется указывать знак, даже если число положительное. Если же вместо символа
«+))
написать пробел, он будет обозначать, что для положительных чисел знак
«+))
выводить не надо, но все равно под него нужно оставить один пробел.
Если после знака
«%))
добавить символ «0)), то число будет дополнено до нужной
ширины незначащими нулями.
Такие способы форматирования применяются, например, для более аккуратного
вывода столбцов чисел. Чтобы продемонстрировать этот подход, напишем скрипт,
который выводит псевдослучайные числа в три столбца (листинг
Листинг
9.4).
9.4. Chapter_09/example_04/percent_random.py
from random import randint, seed
template = "%-Sd %-Sd %-8d"
seed(2)
for n in range(б):
vall
randint(-9999, 9999)
val2 = randint(-9999, 9999)
valЗ = randint(-9999, 9999)
print(template % (vall, val2,
valЗ))
В этом примере задействованы функция randint () из модуля random, выполняю
шая генерацию псевдослучайных целых чисел в заданном диапазоне, и функция
Глава
9.
seed (),
175
Форматирование строк
предназначенная для начальной инициализации генератора псевдослучай
ных чисел. Функция
используется в том случае, если мы хотим, чтобы при
seed ()
многократном запуске скрипта создавались одни и те же псевдослучайные чис
ла,
-
для этого функции
seed ()
нужно передавать одно и то же целочисленное
значение.
Запустив этот скрипт, мы получим следующий результат:
-8146
1832
-1756
9884
-4810
6682
-6998
-4459
9856
-8829
4113
2192
-7218
98
-3046
9045
2896
7832
Столбцы здесь выглядит не очень аккуратно вследствие смещения цифр отрица
тельных чисел из-за знака
«-».
Мы можем улучшить внешний вид отображаемых
столбцов, добавив в символы подстановки знаки пробела после символа
template =
"%
-8d
% -8d
%:
%-8d"
С такой строкой форматирования результат будет выглядеть менее хаотично:
-8146
1832
-1756
9884
-4810
6682
-6998
-4459
9856
-8829
4113
2192
-7218
98
-3046
9045
2896
7832
Или можно добавить после символа«%» символ«+»
-
чтобы знак числа добавлял
ся даже для положительных чисел:
template = "%+-8d %+-8d %+-8d"
В этом случае результат будет выглядеть так:
-8146
+1832
-1756
+9884
-4810
+6682
-6998
-4459
+9856
-8829
+4113
+2192
-7218
+98
-3046
+9045
+2896
+7832
Аналогично работает форматирование для чисел с плавающей точкой, когда ис
пользуется символ форматирования %f. При этом появляется возможность указать,
сколько цифр после запятой нужно вывести. Несколько вариантов такого формати
рования показывает пример, приведенный в листинге
Листинг 9.5.
Chapter_09/example_05/percent_float.py
foo = 42.5
spam = """ 1%%14f 1 :
1%%-14fl:
1%-14fl
1%14fl
9.5.
Часть
176
1%%14. Зf 1:
1%14.Зfl
1%%-14.Зfl:
1%-14.Зfl
1.
Базовые понятия и встроенные типы
1%%+-14. Зf 1: 1%+-14.Зfl
1%% -14.Зfl: 1% -14.Зfl
1%%014. Зf 1:
1%014.Зfl
111111 % ( (foo,)
* 7)
print (spam)
В результате выполнения этого скрипта в консоль будет выведен следующий текст:
1%14f 1 :
1%-14fl:
1%14.Зfl:
1%-14.Зfl:
1%+-14. Зf 1:
1%-14.Зfl:
1%014.Зfl:
42.5000001
142.500000
1
1
42.5001
142.500
1
1+42. 500
1
142.500
10000000042.5001
Символы форматирования
«+», «-»
и пробел после символа«%» при форматирова
нии чисел с плавающей точкой играют такую же роль, как и при форматировании
целых чисел. Выражение
ровано
14 знакомест,
%14. Зf
обозначает, что под число должно быть зарезерви
а после запятой нужно отобразить три цифры.
При показанном использовании строк форматирования важно, чтобы количество
полей подстановки в шаблонной строке было точно равно количеству элементов
кортежа, указанного после знака
«%»,
иначе интерпретатор возбудит исключение
TypeError:
>» foo = 42
»> bar = 12.5
>>> eggs = 11 hello 11
>>> 11 f = %d; Ь = %f; е = %s. 11 % (foo, bar)
Traceback (most recent call last):
File 11 <python-input- ... >11 , line 1, in <module>
11 f = %d; Ь = %f;
е = %s. 11 % (foo, bar)
TypeError: not enough arguments for format string
А если потребуется изменить порядок следования мест подстановок, нужно не за
быть поменять порядок следования переменных в кортеже после оператора%.
Для решения проблемы с порядком следования подставляемых значений в
Python
были добавлены именованные поля подстановки. В этом случае после знака про
цента в круглых скобках можно указать имя поля подстановки, а после оператора
%
передать не кортеж, а словарь, в котором ключами будут имена полей подстановки,
а значениями
»>
»>
>>>
»>
-
соответствующие подставляемые значения:
foo = 42
bar = 12.5
eggs = 11 hello"
spam = ("f = %(f)d;
Ь
= %(b)f;
е
%(e)s.
11
Глава
9.
Форматирование строк
177
% {"е": eggs, "f": foo,
"Ь":
bar}}
>» print(spam)
f
=
42;
Ь =
12.500000;
е =
hello.
Тогда можно не беспокоиться о порядке следования подставляемых переменных, а
также не станет ошибкой (по крайней мере, с точки зрения интерпретатора), если в
словаре будут присутствовать элементы с ключами, которые не используются в
шаблонной строке. При таком способе подстановки также можно применять все те
же способы форматирования чисел, о которых мы говорили ранее.
Более того, такой способ позволяет многократно задействовать одно и то же име
нованное значение в нескольких местах шаблона:
»> spam
("f = %(f)d; Ь = %(b)f; е = %(e)s; foo
% {"е": eggs, "f": foo, "Ь": bar})
>» print (spam)
f = 42; Ь = 12.500000; е = hello; foo = 42.
=
Использование метода
= %(f)d."
formatO
Другой, более современный, способ работы с шаблонными строками, заключается в
применении метода
format ()
из класса
str.
Этот способ по своим возможностям во
многом напоминает способ форматирования с участием оператора«%», но при этом
добавляет новые возможности.
Изменим один из наших предыдущих примеров таким образом, чтобы он задейст
вовал метод
format ():
»> foo = 42
>» bar = 12. 5
»> eggs = "hello"
»> spam = "f = {}; Ь = {}; е = {}." .format(foo,
»> print (spam)
f = 42; Ь = 12.5; е = hello.
При использовании метода
format ()
Ьаr,
eqqs)
в качестве символов подстановки применяют
ся фигурные скобки, внутри которых могут быть указаны дополнительные пара
метры форматирования подставляемых значений, которые передаются в качестве
параметров метода
format ().
В простейшем случае, как в предыдущем примере,
фигурные скобки можно оставлять пустыми, тогда они будут заполняться последо
вательно переданными методу
format ()
значениями.
Обратите также внимание, что поскольку при таком способе форматирования фи
гурные скобки используются как специальные символы, то для того, чтобы вста
вить в строковый литерал фигурную скобку как обычный
-
не специальный
-
символ, ее нужно удваивать.
Однако этот способ обладает теми же недостатками, что и присущи оператору
%, -
нужно следить за порядком следования значений, а их количество должно быть
равно количеству полей подстановок в шаблонной строке. Несмотря на зто, такой
способ форматирования удобен для коротких шаблонов.
Часть
178
1.
Базовые понятия и встроенные типы
Внутри фигурных скобок полям подстановки можно присваивать целочисленные
индексы или строковые имена. Сначала рассмотрим пример с использованием ин
дексов:
foo = 42
bar = 12.5
eggs = "hello"
spam = ("f = (О}; Ь = (1}; е = (2}; foo =
.format(foo, bar, eggs))
»> print (spam)
f = 42; Ь = 12.5; е = hello; foo = 42.
>»
»>
>>>
>»
(О}."
В таком способе работы с полями подстановки индексы соответствуют порядку
передачи значений в метод format (),ив шаблонной строке такие поля не обязаны
располагаться последовательно по возрастанию их номеров. Это решает проблему,
связанную с изменением порядка следования подстановок в шаблоне, а также дает
возможность задействовать в шаблоне одно и то же значение несколько раз.
Использование именованных полей подстановки выглядит следующим образом:
foo = 42
bar = 12.5
eggs = "hello"
spam = ("f = (val_f}; Ь = (val_b}; е = (val_e}."
. format (val_f=foo, val_e=eggs, val_Ь=bar))
»> print(spam)
f = 42; Ь = 12.5; е = hello.
»>
»>
>>>
»>
В фигурных скобках вместо индексов указываются имена, а затем в метод
format ()
передаются именованные параметры с такими же именами. Важно, чтобы для каж
дого именованного поля подстановки в методе
format () был указан соответствую
щий параметр.
После индекса или имени поля подстановки (если они присутствуют) можно доба
вить знак «: », после которого указать способ форматирования подставляемого зна
чения. Такая нотация описания форматирования напоминает нотацию, которая
применялась
при
использовании
оператора
%,
но
предоставляет дополнительные
возможности. Кроме того, каждый класс может добавлять свои символы для форма
тирования экземпляра этого класса при подстановке с помощью метода
f о rma t ( ) .
Нотация для форматирования подробно описывается в документации1, а здесь мы
ее рассмотрим на конкретных примерах. Сначала покажем форматирование целых
чисел (листинг
9.6).
Листинг 9.6. Chapter_09/example_06/format_lnt.py
foo = 42
spam = """{ {foo)): (foo}
{{foo:d)): (foo:d}
1 См.
https://docs.python.org/3/library/string.html#format-string-syntax.
Глава
9.
Форматирование строк
179
{ {foo:g)): {foo:q}
{ {foo:b)): {foo:b}
{ {foo:o)): {foo:o}
{ {foo:x)): {foo:x}
{ {foo:X)): {foo:X}
111111
.format(foo=foo)
print (spam)
Результат выполнения этого скрипта будет выглядеть следующим образом:
{foo): 42
{foo:d): 42
{foo:g): 42
101010
{fоо:Ь):
{foo:o): 52
{foo:x):
2а
{foo:X):
2А
В нашем случае представление целого числа без указания формата, а также с сим
волами форматирования
«d»
-
дит число в двоичной, «о»
и
«g»
совпадают. Символ форматирования «ь» выво
в восьмеричной, «х» и «х»
-
в шестнадцатеричной
системах счисления.
Напишем похожий скрипт для демонстрации форматирования чисел с плавающей
точкой (листинг
Листинг 9.7.
foo =
О.
9.7).
Chapter_09/example_07/format_float.py
425
spam = """{{foo)): {foo}
{{foo:g)): {foo:q}
{ {foo: f)): {foo:f}
{ {foo:e)): {foo:e}
{ {foo:E)): {foo:E}
{ {foo:%)): {foo:%}
'""'. format ( foo=foo)
print (spam)
После запуска этого скрипта в консоль будут выведены строки:
{foo): 0.425
{foo:g): 0.425
{foo:f): 0.425000
{foo:e):
4.250000е-01
{foo:E):
4.250000Е-01
{foo:%): 42.500000%
В показанном случае снова форматирование по умолчанию (без символа формати
рования) и с использованием символа «g)> совпало, подстановка с использованием
символа
«f»
вывела число с фиксированным количеством цифр после точки
(6
цифр),
180
Часть
1.
Базовые понятия и встроенные типы
«е» и «Е» вывели значение в экспоненциальной форме, а символ форматирования
«%»
обозначает, что число нужно представить в виде процентов, добавив знак
после числа, которое было умножено на
«%»
100.
Рассмотрим более подробно символ форматирования
«f»,
предназначенный для
форматирования чисел с фиксированным количеством цифр в дробной части. Так
же, как и для форматирования с помощью оператора %, мы можем задавать количе
ство знакомест, которое отводится под число, количество цифр после точки и спо
соб выравнивания числа (листинг
Листинг 9.8.
9.8).
Chapter_09/example_08/format_float.py
foo = 42.5
spam = """l{{foo:14f}}I :
l{foo:14f}I
{{foo:14.Зf}}I :
{foo:14.Зf}I
{ {foo: .Зf}} 1:
{ {foo:<14f}} 1:
{foo: .Зf} 1
{foo:<14f} 1
{ {foo:<14.Зf}} 1:
{{fоо:л14.Зf}} 1:
{foo:"14.Зf}I
{foo:<14.Зf}I
{ {foo:<+14.Зf}} 1:
{{foo:< 14.Зf}}I:
{foo:<+14.Зf} 1
{{foo:<014.Зf}}
{foo:<014 . Зf} 1
{{foo:014.Зf}}
{foo:<
1:
1:
14.Зf}I
{foo:014.Зf}I
{foo:_<14.Зf} 1
{ {foo:_ <14.Зf}} 1:
{{foo: .>14.Зf}} 1:
{foo:
{{foo:#л14.Зf}}I:
{foo:#"14.Зf}I
.>14 . Зf}
1
""'. format (foo=foo)
print (spam)
Результатом выполнения этого скрипта будет следующий текст:
{foo:14f} I :
{foo:14.Зf} 1:
{foo: .Зf} 1:
{foo:<14f} 1:
{foo:<14.Зf} 1:
{fоо:л14.Зf} 1:
{foo:<+14.Зf}
1:
1:
{foo:<014.Зf} 1:
{foo:014.Зf} 1:
{foo:_<14.Зf} 1:
{foo:<
14.Зf}
{foo:.>14.Зf}I:
{fоо:#л14.Зf}
1:
42.5000001
42.5001
42.5001
42.500000
42.500
42.500
+42.500
42.500
42.500000000001
0000000042.5001
42.500 _ _ _ 1
..... . .. 42.5001
H#Jt42. 500#### 1
Из этого примера видно, что:
♦ количество знакомест задается в виде числа перед символом форматирования
«f» ( { foo: 14f} );
Глава
9.
Форматирование строк
181
♦ количество чисел в дробной части задается после точки, следующей за количе
ством знакомест, если они указаны
( ( foo: 14. Зf J);
♦ по умолчанию выравнивание осуществляется по правой границе выделяемой
для числа области;
♦ для выравнивания по левому краю используется символ<«»;
♦ для выравнивания по центру используется символ «л»;
♦ для выравнивания по правому краю используется символ«>»;
♦ для указания, что даже для положительных чисел необходимо выводить его
знак, используется символ
«+»,
который должен следовать после символа вы
равнивания;
♦ если после знака выравнивания добавить пробел, то для положительных чисел
символ
«+»
выводиться не будет, но на его месте будет добавлен пробел;
♦ если перед количеством знакомест добавить «о», то число будет дополнено до
нужного количества символов незначащими нулями (в зависимости от выравни
вания
в начале или в конце числа);
-
♦ перед символом выравнивания можно указать произвольный символ, который
будет использоваться вместо пробела для заполнения знакомест. В этом случае
может понадобиться явное указание выравнивания по правому краю с помощью
символа«»>.
У метода format () есть еще одна интересная особенность. Если в качестве значе
ния передан список или другой итерируемый объект, то внутри строки форматиро
вания можно обращаться к его конкретным элементам по индексу:
>>>
>»
foo = [10.1, 20.2, 40.З, 60.4]
"foo[O] = {[О]:.Зf}; foo[l] =
'foo[O] = 10.100; foo[l] = 20.200'
>» "foo[O] = {0[0]:.Зf}; foo[l] =
{[1]:.Зf}".format(foo,
foo)
{O[l]:.Зf}".format(foo)
'foo[O] = 10.100; foo[l] = 20.200'
>» "foo[O] = {foo[O]:.Зf}; foo[l] =
{foo[l]:.Зf}".format(foo=foo)
'foo[O] = 10.100; foo[l] = 20.200'
Отрицательные индексы в строке форматирования использовать нельзя.
Мы рассмотрели здесь основные способы форматирования чисел с помощью мето
да format (), но более сложные классы (например, класс datetime из модуля
datetime) могут предлагать свои способы форматирования при подстановке.
f-строки
Следующий способ форматирования, который мы рассмотрим, является еще более
новым,
-
он появился в
Python 3.6.
Этот способ позволяет не только подставлять
значения в указанные места в шаблонной строке, но и непосредственно в шаблоне
производить вычисления с использованием переменных.
Часть
182
f-строки
-
1.
Базовые понятия и встроенные типы
это строковые литералы, у которых перед символом открывающей ка
вычки без пробела добавлен символ
«f».
В f-строках, так же, как и в методе
format (), для обозначения полей подстановки применяются фигурные скобки, но
при этом внутри них могут располагаться полноценные Руthоn-выражения (с неко
торыми ограничениями) и использоваться переменные.
Начнем с простого примера:
>>> foo = f"2
>>> foo
х
2 = {2 * 2}"
'2х2=4'
>» type(foo)
<class 'str'>
В этом примере в процессе создания строки по f-строковому литералу было выпол
нено вычисление выражения внутри фигурных скобок. Однако чаще f-строки ис
пользуют для подстановки значений переменных:
»> foo = 42
»> bar = 12.5
>>> eggs = "hello"
»> spam = f"foo = {foo}; bar = {Ьаr}; eggs = {egqa}."
>>> spam
'foo = 42; bar = 12.5; eggs = hello.'
>» type (spam)
<class 'str'>
Использование f-строк напоминает применение метода forma t (), но при этом они
позволяют существенно сократить код, поскольку для них не требуется вызывать
метод с указанием имен переменных и имен подстановки. Если бы мы переписали
предыдущий код с применением метода format (), он был бы значительно более
громоздким и с большим количеством дублирования имен:
»> spam = ("foo = {foo); bar = {bar); eggs = {eggs)."
.format(foo=foo, bar=bar, eggs=eggs))
Для
представления
чисел
f-строки
используют ту же
format () (листинг 9.9).
Листинг 9.9. Chapter_09/example_09/fstrlng_int.py
foo = 42
spam = f'""' {{foo)): {foo}
{{foo:d)): {foo:d}
{{foo:b}}: {foo:b}
{{foo:o}}: {foo:o}
{{foo:x}}: {foo:x}
{{foo:X}}: {foo:X}
print(spam)
нотацию,
что
и
метод
Глава
9.
Форматирование строк
Как показывает этот пример,
литералов.
183
f-строки могут использоваться и в
виде многострочных
В результате выполнения этого скрипта в консоль будет выведен следующий
текст:
{foo): 42
{foo:d): 42
{foo:b): 101010
{foo:o): 52
{foo:x):
2а
{foo:X):
2А
Нотация для форматирования и выравнивания чисел с фиксированной точкой так
же совпадает с нотацией при использовании метода format () (листинг
Листинr 9.10.
Chapter_09/example_10/fstrlng_float.py
foo = 42.5
l{foo:14f}I
spam = f"""l{{foo:14f))I
1{foo:<14f} 1
1{ {foo:<14f)) 1:
l{{foo:14.Зf})I:
l{foo:14.Зf}I
1{ {foo:<14.Зf)) 1:
1{foo:<14.Зf} 1
l{{foo:л14.Зf))I:
1{fоо:л14.Зf} 1
1{ { f 00 : <+14 . 3 f) ) 1:
1{foo :<+14. Зf} 1
14.Зf}
1{ { f 00 : < 14 . 3 f) ) 1:
1{foo:<
1{ { f 00 : < о 14 . 3 f) ) 1:
1{foo:<014.Зf} 1
1{ { f 00 : о 14 . 3 f ) ) 1:
1{foo:014.Зf} 1
l{{foo:_<14.Зf))
1
1:
1{foo:_<14.Зf} 1
1{ { foo: . >14. Зf)) 1:
1{foo: .>14.Зf} 1
l{{foo:#л14.3f))I:
1{fоо:#л14.Зf} 1
111111
print(spam)
Результат выполнения этого скрипта выглядит следующим образом:
l{foo:14f)I
:
42.5000001
l{foo:<14f)I:
142.500000
l{foo:14.Зf)I:
1
l{foo:<14.Зf)I:
142.500
l{foo:л14.Зf)I:
1
l{foo:<+14.Зf)
1:
1+42.500
1{foo:< 14. Зf) 1:
1 42.500
1{foo:<014.Зf) 1:
142.500000000001
1{foo: 014. Зf) 1:
10000000042.5001
1{foo:_<14.Зf) 1:
142.500_ _ _ 1
1{foo: .>14
.Зf)
1
42.5001
1
42.500
1:
1........ 42.5001
1{fоо:#л14.Зf) 1:
1####42.500####1
9.10).
Часть
184
1.
Базовые понятия и встроенные типы
Поскольку внутри полей подстановок f-строк вы можете задействовать полноцен
ные Руthоn-выражения, то в них можно обращаться к отдельным элементам спи
сков. Причем, в отличие от метода forrnat (), в f-строках допускается использовать
и отрицательные индексы:
»> foo = (10.1, 20.2, 40.3, 60.4]
»> f"foo(0] = {foo[O]: .Зf}; foo(-1]
'foo[0] = 10.100; foo(-1] = 60.400'
{foo[-1):
.Зf}"
Получать элементы словарей тоже можно:
>>> foo = {"keyl": 20.5, "key2": 42, "kеуЗ": None)
>>> bar = f"""foo('keyl'] = {foo['keyl']}
{foo['key2']}
... foo('key2']
... foo['keyЗ'] = {foo['key3']}"""
»> print (bar)
20. 5
foo ( 'keyl']
foo [ 'key2' ]
foo [ 'kеуЗ' ]
42
None
Следующий пример демонстрирует, как внутри f-строк можно совмещать расчет с
форматированием:
10.5е9
>>> freq =
>>>#Скорость света в вакууме
»>с= Зе8
>>> foo =
{freq / le9: .Зf} ГГц.
{с / freq * lеЗ : . Зf} мм'""'
f"""Частота:
Дпина волны:
»> print(foo)
10.500 ГГц.
Длина волны: 28.571 мм
Частота:
Внутри f-строк также можно вызывать функции:
>>> from math import logl0
»> gain = lОеЗ
»> f"Коэффициент усиления: {qain:
'Коэффициент усиления:
.2е} или
1.00е+04 или
40.000
(10 * loqlO(qain):
дБ'
Для экземпляров классов можно вызвать их методы:
>>> text = "Lorem Ipsurn"
>>> spam = f"""Lower: {text.lower()}
... Upper: {text.upper() )"""
»> print(spam)
Lower: lorem ipsurn
Upper: LOREM IPSUМ
>>> foo = ("hello", "world", "spam"]
>>> eggs = f"foo: {'; '.join(foo) )"
»> print(eggs)
foo: hello; world; spam
.Зf) дБ"
Глава
9.
Форматирование строк
185
И еще один пример, демонстрирующий, что внутри f-строк можно использовать
генератор списков:
>>> foo = [10, 20, 30]
»> f"foo: {'; '.join([str(item) for item in foo])}"
'foo: 10; 20; 30'
Несмотря на такие мощные возможности f-строк, ими стоит пользоваться осторожно.
Когда f-строки были анонсированы, то не все разработчики поддержали это ново
введение, считая, что таким образом код становится менее читаемым из-за смеши
вания логики программы и представления результатов в виде строк. Чтобы основ
ная логика работы программы не перешла в шаблоны, f-строки лучше применять
исключительно для оформления результатов расчета, а если требуются какие-то
громоздкие вычисления, то их лучше произвести заранее, а не внутри f-строк.
F-строки поддерживают еще один режим форматирования, которого не было в ме
тоде format (), -
это режим отладки. В таком режиме, помимо значения выводи
мой переменной, в строку будет добавлено еще имя переменной или целого выра
жения из поля подстановки. Для включения режима отладки достаточно в поле
подстановки после имени подставляемого значения добавить знак
«=»,
как показа
но в следующем примере:
>» foo = 10
»> bar = "hello"
»> baz = 20.5
>» spam = f"""{foo=}
{Ьаr=}
{Ьаz=:1O.Зf}
{foo
*
2
+
5=}
{type {Ьаr) =}"""
>» print (spam)
foo=l0
bar='hello'
20. 500
foo * 2 + 5=25
baz=
type(bar)=<class 'str'>
Как видно из этого примера, после символа
«=»
можно добавлять все те опции
форматирования, которые мы рассматривали. Режим отладки очень удобен для де
монстрации результатов работы небольших скриптов, поэтому в дальнейшем мы
будем активно пользоваться такой возможностью.
Напишем скрипт (листинг
9.11),
который будет выводить на экран два столбца с
данными. Пусть первый столбец представляет достаточно большие числа (их мож
но интерпретировать как частоты в диапазоне от
случайные величины в диапазоне от -1 до
+1.
1 до 2
ГГц), а второй
-
содержит
Часть
186
Листинг 9.11.
1.
Базовые понятия и встроенные типы
Chapter_09/example_11/fstring_random.py
from random import seed, uniform
seed (2)
freq = [le9 + О.2е9 * n for n in range(6))
in freq]
val = [uniform(-1, 1) for
for f, v in zip(freq, val):
print (f"{f:<15.Зe}(v:<
.Зf}")
В этом примере есть два момента, на которые надо обратить внимание:
♦
во-первых, здесь применяется ранее не встречавшаяся нам функция un i f о rm ( )
из модуля random. Эта функция предназначена для генерации псевдослучайных
чисел с плавающей точкой с равномерным распределением в заданном диапазо
не (в нашем случае от -1 до +1);
♦
во-вторых, в строке, где создается список val, применен типичный прием, когда
цикл выполняется такое количество раз, сколько элементов содержится
в кол
лекции, но при этом сами значения из списка не используются. Тогда в качестве
переменной в цикле задействуется переменная
«_>>.
По договоренности это имя
часто дают переменной, чтобы показать, что она не используется.
Далее в цикле перебираются пары значений из двух списков, и они выводятся на экран
с применением разных способов форматирования. В результате мы получим текст:
l.000e+09
1.200е+О9
1.400е+О9
1.600е+09
1.800е+09
2.000е+09
0.912
0.896
-0.887
-0.830
0.671
0.472
Если бы этот код относился к реальному приложению, то в нем можно было бы ус
мотреть один потенциальный недостаток, который заключается в том, что в полях
подстановки жестко прописан формат вывода. Для такого кода было бы сложно
добавить возможность, например, задавать количество цифр в дробной части и ко
личество знакомест, отводимых на первый столбец через файл настроек или другим
способом. И тут нам на помощь приходит еще одна возможность f-строк, которая
состоит в том, что внутри полей подстановки после знака
«: » тоже можно исполь
зовать переменные, только их надо еще раз взять в фигурные скобки. Поэтому зна
чения 1 s и з мы можем вынести в переменные (листинг
Листинг 9.12.
Chapter_09/example_12/fstring_random_params.py
from random import seed, uniform
width = 15
prec = 3
9.12).
Глава
9.
Форматирование строк
187
seed(2)
freq = [le9 + О.2е9 * n for n in range(б)]
val = [uniform(-1, 1) for
in freq]
for f, v in zip(freq, val):
print (f" {f: <{width}. {prec}e) {v: < . {prec} f) ")
Результат выполнения этого скрипта будет выглядеть так же, как у предыдущего,
но теперь у нас есть возможность менять ширину первого столбца и количество
чисел в дробной части.
Более того, внутри вложенных фигурных скобок мы можем использовать более
сложные выражения. Например, последнюю строку мы могли бы изменить на такую:
print(f"{f:<{width + 3}.{prec)e){v:< .{prec}f)")
увеличив ширину первого столбца на
3 символа.
Мы рассмотрели не все возможности, которые предоставляют нам f-строки, но уже
должно быть ясно, насколько это мощный инструмент, который надо использовать
аккуратно, чтобы написанный код оставался ясным для чтения.
Заключение
Язык
Python
отлично подходит для задач, связанных с обработкой текста. Помимо
множества методов в классе
вых задач, в
Python
str,
предназначенных для решения различных типо
существует несколько способов работы с шаблонным текстом,
в том числе создавать строки, содержащие поля подстановки, которые затем будут
заполнены данными из переменных. Помимо простой замены полей подстановки
на указанные значения, эти способы предлагают средства для форматирования
данных. В этой главе мы сосредоточились на форматировании числовых данных.
Первый рассмотренный нами способ использует оператор %, за которым следует
кортеж с подставляемыми значениями. Мы также увидели модификацию этого ме
тода, в которой задействованы именованные поля подстановки, при использовании
которых вместо кортежа после оператора
% передается
словарь.
Более современный способ работы с текстовыми шаблонами заключается в приме
нении метода format () из класса str. Этот способ предоставляет дополнительные
возможности форматирования текста, а также разрешает классам расширять встро
енные способы форматирования.
И, наконец, мы познакомились с еще более новым инструментом
-
f-строками,
которые позволяют задействовать в текстовых шаблонах полноценные Руthоn
выражения. Для форматирования f-строки применяется нотация, схожая с нотацией,
используемой методом
В
Python 3.14
format ().
должен
появиться
строк- t-строки (от слова
еще
template),
один
инструмент для
форматирования
которые продолжают развитие f-строк. Они
Часть
188
1.
Базовые понятия и встроенные типы
позволят проверить подставляемые значения перед формированием строки. Это
полезно,
если
подставляемые
значения получены от пользователя
либо ненадежного источника. Прочитать о t-строках можно в РЕР
или из
какого
750 2.
В завершение этой главы хочется упомянуть стороннюю библиотеку
Jinja3,
которая
является еще более мощным шаблонизатором по сравнению со встроенными сред
ствами
Python.
Ее часто используют для генерации больших текстов
-
например,
страниц сайтов или писем рассылки. В книге мы не станем рассматривать эту биб
лиотеку, но сказать о ее существовании в главе про текстовые шаблоны нужно.
На этом мы заканчиваем серию глав, посвященных изучению основных классов
языка
Python.
Мы поговорили в них про числовые классы (int и float), булев тип
bool, про коллекции list, tuple и array, про словари (dict) и множества (set), а
теперь и про строки (str).
В следующей главе мы продолжим изучать синтаксис
том, как создавать функции.
2
См. https://peps.python.org/pep-0750/.
3
См. https://jinja.palletsprojects.com.
Python
и будем говорить о
-ЧАСТЬ
11-
ОСНОВНЫЕ ПОДХОДЫ
-ГЛАВА
10-
ФуНКЦИИ
С самого начала изучения
Python
мы уже использовали функции. Это были встро
енные функции (такие как print (), len (), type () ), функции из модулей стандарт
ной библиотеки (например, математические функции из модулей math и cmath), а
также множество функций из классов, которые называют методами класса. В этой
главе мы научимся создавать свои функции, узнаем, какие бывают особенности у
способов передачи параметров, а в завершение поговорим о глобальных переменных.
В скриптах функции решают несколько задач. Во-первых, если в программе есть
повторяющиеся строки кода, то это повод оформить их в виде функции и таким
образом избавиться от дублирования кода и сократить его объем. Во-вторых, функ
ции используются для выделения логических единиц кода, тем самым подсказывая
тому, кто будет читать ваш код (в том числе и вам самим через некоторое время),
что делает та или иная часть программы. Даже если функции вызываются только
один раз, то есть написаны не ради уменьшения дублирования кода, они улучшают
понимание структуры программы.
Создание функций
Прежде чем разбираться с синтаксисом объявления функций, договоримся о тер
минологии.
Аргументами, или параметрами функции (мы эти термины будем использовать
как синонимы) называются переменные, которые указываются при объявлении
функции. Затем при вызове функции каждая такая переменная получает свое зна
чение. Например, если у нас есть функция sin (х), то х метр функции. При вызове функции sin (pi /
это аргумент, или пара
2), переменная х, которая использу
ется внутри функции sin (), получит значение pi /
2. У каждой функции может
быть произвольное количество параметров, в том числе и не быть вовсе.
Говорят, что функция возвращает значение, если она передает обратно в вызы
вающий код какой-либо объект. Этот объект называют значением функции. Напри
мер, если мы напишем код:
а=
sin(pi / 2)
то функция вернет объект типа float, который будет равен
ствительно считает синус угла в радианах).
1.0
(если функция дей
Часть
192
11.
Основные подходы
И это значение будет присвоено переменной а. Мы можем проигнорировать воз
вращаемое значение:
sin(pi / 2)
тогда оно просто будет потеряно, но функция его все равно вернет, независимо от
того, используем ли мы возвращаемое значение.
В
Python
функции всегда возвращают какое-то значение. Если внутри функции яв
но не указано, что функция что-то возвращает, то она возвращает значение None.
Например, мы использовали функцию print (), но результат вызова этой функции
ничему не присваивали, хотя могли бы написать код наподобие такого:
>>> foo = print("Hello, world!")
Hello, world 1
»> print(foo)
None
В этом случае переменной foo было присвоено значение None. Если функция всегда
возвращает None, то для краткости говорят, что такая функция не возвращает зна
чений, хотя, как мы только что видели, формально это не так.
Функция всегда возвращает строго одно значение, но, как мы скоро увидим, с по
мощью кортежей и оператора распаковки мы можем писать такой код, как будто из
функции было возвращено несколько значений, но это всего лишь «синтаксический
сахар», позволяющий сократить количество строк кода.
Остальные особенности функций в
Python
мы будем последовательно рассматри
вать в этой главе далее.
Для объявления новой функции используется ключевое слово cte f ( сокращение от
английского слова
define -
«определить»), вслед за которым нужно указать имя
функции, затем добавляются круглые скобки, внутри которых могут быть указаны
параметры функции (при их наличии). Даже если функция не принимает парамет
ры, скобки указывать обязательно. После круглых скобок ставится двоеточие, а да
лее идет вложенный блок кода
-
тело функции.
Для указания возвращаемого значения служит инструкция с использованием клю
чевого слова return, после которого указывается возвращаемое значение. После
выполнения этой инструкции выполнение функции завершается.
Рассмотрим несколько примеров объявления функций (листинг
Листинг
10.1. Chapter_10/example_01/functions.py
# Функция без параметров. Возвращает None.
def hello_world():
print("Hello, world!")
# Функция с одним параметром.
def hello(name):
print (f"Hello, (name} ! ")
Возвращает
None.
10.1 ).
Глава
#
10.
Функции
193
Функция с двумя параметрами.
def add(a,
Ь):
= а + Ь
return с
с
#
#
Функция с двумя параметрами.
Возвращает кортеж из двух значений.
def add_sub(a, Ь):
s = а + Ь
d = а - Ь
return (s, d)
# Вызов созданных функций
hello_ world ()
hello ( "Guido")
foo = add(4, 2)
bar, baz = add_sub(4, 2)
К именам функций предъявляются те же самые требования, что и к именам пере
менных: имя функции должно начинаться с буквы или символа подчеркивания
«_»,
далее в имени могут встречаться цифры. Регистр букв в имени функции имеет зна
чение, при этом, согласно РЕР
8,
использовать в именах функций заглавные буквы
не рекомендуется. Если имя функции состоит из нескольких слов, то их предлага
ется разделять символом подчеркивания. В имени функции желательно использо
вать буквы только из английского алфавита, хотя интерпретатор не возражает про
тив использования букв из других языков, включая русский. Таким образом, функ
цию,
которую
мы
ранее
назвали
hello_world()
не
рекомендуется
называть
HelloWorld(), helloWorld(), привет_мир() и т. п. Хотя это и не будет ошибкой с
точки зрения синтаксиса языка.
Имена аргументов функции записываются через запятую. В отличие от языков со
статической типизацией, в
Python указывать тип аргументов не требуется, хотя та
18 мы увидим, что на самом деле ожидаемые типы
кая возможность есть. В главе
параметров функции и возвращаемого значения указывать можно, но они будут
служить только лишь подсказками для программистов и для инструментов поиска
ошибок к коде, но влиять на работу скрипта не будут.
Если тело функции состоит из одной операции, то ее можно написать на той же
строке, что и объявление функции. Поэтому в предыдущем примере (см. листинг
10.1)
функции hello_world () и hello () можно было бы написать так:
def hello_world(): print("Hello, world!")
def hello(name): print(f"Hello, {name) 1")
После тела каждой функции для лучшей читаемости кода рекомендуется оставлять
две пустые строки, а если функция является методом класса, то одну пустую стро
ку. В примерах этой книги для экономии места после функций будет оставляться
только одна пустая строка.
Часть
194
Инструкция
тела
11.
Основные подходы
return для возврата значения из функции может встречаться внутри
функции
несколько
раз
(например,
в
разных
ветвях
инструкции
if ... elif ... else). Если функция не заканчивается оператором return, тогда под
разумевается, что функция возвращает значение None. Таким образом, функции
hello_world() и hello() из листинга 10.1 можно было бы записать следующим
образом, и поведение этих функций при этом не поменяется:
def hello_world():
print("Hello, world!")
return None
def hello (name) :
print (f"Hello, {name) ! ")
return None
Можно написать ключевое слово return без указания возвращаемого значения, и
тогда тоже будет считаться, что функция возвращает None.
Например, пусть у нас есть функция для решения квадратного уравнения вида
ах 2 + Ьх + с = О, которая принимает параметры а, ь, с и возвращает решение в виде
кортежа из двух значений, если решения существуют в действительной области,
или возвращает
None, если дискриминант меньше нуля и решений в действительной
области нет (листинг
Листинг 10.2.
10.2).
Chapter_10/example_02/equation.py
from math import sqrt
def equation(a, Ь, с):
D = Ь ** 2 - 4 *а* с
if D < О:
return
xl = (-Ь + sqrt(D)) / (2 *
х2 = (-Ь - sqrt(D)) / (2 *
return (xl, х2)
а)
а)
result 1 = equation(l.5, -2, 5.0)
result 2 = equation(l.5, -2, -5.0)
print(f"{result_l=)")
print(f"{result_2=)")
Обратите внимание, что для вывода результата скрипта используются f-строки с
форматированием в режиме отладки (см. главу
скрипта будет следующий текст:
result l=None
result_2=(2.610317298281767, -1.2769839649484336)
9).
Результатом выполнения этого
Глава
10.
Функции
195
При первом вызове функции equation () значение дискриминанта D будет отрица
тельным, и выполнится ветвь инструкции if с инструкцией return, которая тут же
прервет дальнейшее выполнение функции и возвратит значение None. Если значе
ние переменной D неотрицательное, интерпретатор будет выполнять последующие
команды, пока не дойдет до инструкции return
(xl,
х2), и тогда функция будет
тоже прервана с возвращением значения в виде кортежа.
Таким образом, выход из функции осуществляется или с помощью инструкции
return, или после выполнения последней команды внутри тела функции. Выполне
ние функции также может быть прервано исключением, но об этом мы будем гово
рить более подробно в главе
19.
Функцию equation () из листинга
10.2
можно было бы переписать следующим об
разом:
def equation(a, Ь, с):
D = Ь ** 2 - 4 *а* с
if D >= О:
xl = (-Ь + sqrt(D)) / (2 *
х2 = (-Ь - sqrt(D)) / (2 *
return (xl, х2)
а)
а)
Здесь говорится о том, что при неотрицательном значении переменной D будет воз
вращен кортеж, а в противном случае- значение None. Но вариант из листинга
10.2
может быть более предпочтителен, поскольку там явно подчеркнута ситуация, ко
гда именно функция возвращает None. Поэтому для улучшения понимания работы
функции, если все же хочется оставить последний вариант кода, можно добавить
явный возврат None:
def equation (а, Ь, с) :
D = Ь ** 2 - 4 *а* с
ifD>=O:
xl = (-Ь + sqrt (D)) / (2 *
х2 = (-Ь - sqrt (D)) / (2 *
return (xl, х2)
return None
а)
а)
«Утиная» типизация
Вспомним, что
Python -
это язык с динамической типизацией, то есть тип пере
менных может меняться по ходу выполнения программы. В работе функций это
особенно ярко проявляется. При объявлении функции мы не указываем ни тип ар
гументов, ни тип возвращаемого значения. Более того, мы уже увидели на примере
функции equation (), что тип возвращаемых значений у функции может быть раз
ным в зависимости от условий. В том случае это мог быть тип tuple или NoneType.
Давайте проведем несколько экспериментов над более простой функцией, которая
складывает два аргумента и возвращает их сумму (листинг
10.3).
Часть
196
Листинr
11.
Основные подходы
10.3. Chapter_10/example_03/add.py
def add (а,
return
Ь)
:
а+ Ь
foo add(l0, 20)
bar = add(3+2j, 20.5)
baz = add("hello ", "world")
spam = add( [10, 20, 30), ["hello", "world"))
print(f"{foo=}\n{bar=}\n{baz=}\n{spam=}")
Результат выполнения этого скрипта выглядит следующим образом:
foo=30
bar=(23.5+2j)
baz='hello world'
spam=[l0, 20, 30, 'hello', 'world']
Мы написали только одну функцию, но при этом она умеет работать с аргументами
разных типов,
-
лишь бы к объектам, которые передаются внутрь функции, можно
было применить оператор«+». Возвращаемый тип для разных типов выходных па
раметров тоже будет разным.
Функциям в
Python
не важно, какие типы аргументов ей будут переданы,
-
лишь
бы все действия, которые выполняются над объектами внутри функции, можно бы
ло бы выполнить. Такая особенность называется утиной типизацией, которая по
лучила свое название от выражения: «Если что-то выглядит как утка, плавает как
утка, и крякает как утка, значит, это и есть утка».
Это очень удобно и позволяет сократить количество написанных функций по срав
нению с языками со статической типизацией, где для каждого типа аргументов
нужно было бы создать отдельную функцию.
Попробуем передать в функцию
add ()
такие, которые нельзя сложить (листинг
Листинr 10.4.
def add(a,
10.4).
Chapter_10/example_041add_error.py
Ь):
print("Bнyтpи функции
return
аргументы, которые «не крякают», то есть
add()")
а+ Ь
foo = add("Hello", 42)
В результате будет возбуждено исключение
дующий текст:
Внутри функции add()
Traceback (most recent call last):
TypeError,
а в консоль выведен сле
Глава
10.
Функции
197
File " ... /add_error.py", line 6, in <module>
foo = add("Hello", 42)
File " ... /add_error.py", line 3, in add
return а+ Ь
TypeError:
сап
only concatenate str (not "int") to str
При попытке вызвать функцию add () мы получили ошибку, поскольку нельзя сло
жить строку и число. Но обратите внимание, что ошибка произошла только в мо
мент сложения двух объектов,
-
весь код, который был до этого внутри функции,
выполнился. Это является недостатком интерпретируемых языков с динамической
типизацией. В подобной ситуации компилируемые языки со статической типизаци
ей не позволили бы программе с такой ошибкой даже скомпилироваться. В реаль
ной программе с большим количеством классов и функций такого рода ошибки мо
гут быть обнаружены далеко не сразу. Именно поэтому в языках с динамической
типизацией особенно важно писать тесты, которые бы покрывали как можно боль
ше кода. Про тестирование кода мы поговорим в главе
24.
Именованные параметры функций
Рассмотрим теперь более подробно способы передачи аргументов в функцию и для
дальнейшей демонстрации напишем простую функцию с тремя параметрами:
def calc (а, Ь,
return а*
с)
Ь
:
+
с
Параметры в функцию могут передаваться двумя способами: как позиционные па
раметры или как именованные параметры.
Говорят, что параметр передается как позиционный, если для установки соответст
вия передаваемого значения и имени параметра функции используется порядок
указания объектов при вызове функции. Звучит это, может быть, сложно, но на са
мом деле мы этим способом постоянно пользовались. Следующий вызов функции
calc () использует позиционный способ передачи аргументов:
foo = calc(l0, 20, 30)
Способ передачи здесь является позиционным, потому что важен порядок передачи
параметров: внутри функции переменная а будет равна
1О,
ь
- 20,
с -
30.
Параметр может быть передан и как именованный, если при вызове функции мы с
помощью имени параметра указываем, какому параметру функции какое значение
соответствует. Имена параметров функции всегда описаны в документации к этой
функции. Например, предыдущую функцию, используя именованный способ пере
дачи параметров, мы можем вызвать следующим образом:
foo = calc(a=l0,
Ь=20,
с=30)
При именованном способе передачи параметров порядок их следования не играет
роли, и функцию calc () можно вызвать, например, так:
foo = calc(b=20,
с=30,
a=l0)
Часть
198
11.
Основные подходы
Более того, можно смешивать эти два способа передачи параметров, но с тем огра
ничением,
что
параметры,
переданные
как
позиционные,
должны
следовать
до
именованных:
foo = calc(lO,
Ь=20,
с=30)
или
foo = calc(lO, 20,
с=30)
А вот следующий вызов будет считаться нарушением синтаксиса:
foo = calc(a=lO,
Ь=20,
30)
При этом в функцию calc () нужно обязательно передавать ровно три параметра,
то есть следующие вызовы приведут к ошибкам:
foo
foo
foo
foo
=
=
=
=
calc (10, 20)
calc (a=lO, Ь=20)
calc (10, 20, 30, 40)
calc(a=lO, Ь=20, с=30, d=40)
Параметры со значениями по умолчанию
Параметры функции могут иметь значения по умолчанию. Это значит, что если при
вызове функции таким параметрам не присвоены какие-либо значения, то им при
сваиваются те значения, которые указаны при описании функции. Эта возможность
позволяет не передавать функциям каждый раз значения параметров, которые часто
бывают одинаковыми. Для того, чтобы параметру функции добавить значение по
умолчанию,
нужно указать
это
значение после
знака
«=»
в списке
параметров в
объявлении функции. При этом имеется ограничение, заключающееся в том, что
параметры со значениями по умолчанию должны располагаться после параметров
без значений по умолчанию. Например:
>>> def calculate(a, b=l,
return а* Ь + с
с=О):
>>>#Для параметров Ь и с будут использоваться значения по умолчанию
>>> calculate(3)
3
>>>#Значение по умолчанию будет использоваться только для параметра с
>>> calculate(3, 4)
12
>>>#Значения по умолчанию не используются
>>> calculate(3, 4, 5)
17
>>> calculate(3, Ь=4, с=5)
17
>>>#Значение по умолчанию будет использоваться только для параметра Ь
>>> calculate(3,
8
с=5)
Глава
10.
Функции
199
Как видно из этого примера, мы можем указывать параметры со значениями по
умолчанию и как позиционные, и как именованные.
Как мы говорили ранее, все параметры со значениями по умолчанию должны рас
полагаться после параметров без таких значений, поэтому объявить функцию сле
дующим образом мы не можем:
def calculate (а, b=l,
с)
:
У параметров со значениями по умолчанию есть одна особенность, которая может
привести к неожиданным результатам. Напишем функцию, которая принимает в
качестве параметра список, добавляет в него значение, равное количеству элемен
тов в списке, и возвращает этот же список из функции (листинг
10.5).
llистинr 10.5. Chapter_10/example_05/append_count.py
def append_count(lst):
lst.append(len(lst))
return 1st
foo = append_ count ( [] )
bar = append_count ( [])
spam = append_count([l0, 20, 30])
print (f 11 { foo=) 11 )
print(f 11 {bar=) 11 )
print (f 11 { spam=) 11 )
Здесь никаких неожиданностей нет, в консоль будет выведен результат:
foo= [ О]
bar=[0]
spam=[l0, 20, 30, 3)
Через некоторое время мы замечаем, что в программе эта функция чаще всего ис
пользуется, когда ей на вход передают пустой список, и логичным шагом кажется
добавить соответствующее значение по умолчанию, чтобы можно было не переда
вать пустой список в явном виде (листинг
nистинr
10.6).
10.6. Chapter_10/txamplt_06/append_count_error.py
def append_count(lst=[]):
lst.append(len(lst))
return 1st
foo = append_count()
bar = append_ count О
spam = append_count ( (10, 20, 30])
200
Часть
11.
Основные подходы
print(f"{foo=)")
print(f"{bar=)")
print(f"{spam=)")
И тут происходит неожиданное. Результат работы скрипта изменился:
foo=[O, 1]
1]
Ьаr=[О,
spam=[l0, 20, 30, 3]
Почему-то теперь в списках foo и bar оказались по два элемента. Причем, со спи
ском spam, который был получен с переопределением значения по умолчанию
функции, всё в порядке. Явно что-то не так со значением по умолчанию.
Произошло следующее. Когда мы объявляем функцию и указываем значение по
умолчанию, то объект для значения по умолчанию (у нас это пустой список) созда
ется лишь однажды
-
при объявлении функции, и затем этот объект по умолчанию
используется при каждом вызове функции. То есть, с точки зрения интерпретатора,
функцию append count () можно было бы записать так:
default
=
[]
def append_count(lst=default):
lst.append(len(lst))
return 1st
А поскольку, как мы знаем, операция присваивания не создает новый объект, а
только создает новую ссылку на него, то оказывается, что переменные
ссылаются
на
один
и
тот
же
объект.
И
когда
мы
вызываем
foo
и
bar
функцию
append_ count () второй раз, чтобы получить значение bar, не создается новый пус
той список, а используется тот, что уже был создан до этого при определении
функции. И в этот список добавляется еще один элемент. Чтобы подтвердить это
объяснение, добавим в пример промежуточный вывод значения foo до того, как
был создан объект bar. А в конце убедимся с помощью оператора is, что перемен
ные foo и ьаr указывают на один и тот же объект (листинг
Листинг
10.7. Chapter_10/example_07/append_count_error_debug.py
def append_count(lst=[]):
lst.append(len(lst))
return 1st
foo = append_count()
print(f"l) {foo=)")
bar = append_count()
print(f"2) {foo=)")
print(f"{bar=)")
print(f"{foo is
Ьаr=)")
10.7).
Глава
10.
201
Функции
Результат выполнения будет выглядеть так:
1) foo=[O]
2) foo=[0, 1]
bar=[0, 1]
foo is Ьar=Тrue
То есть действительно, сначала список
ния переменной bar -
foo
содержит один элемент, а после созда
уже два.
Отсюда следует вывод, что в качестве значений по умолчанию безопаснее исполь
зовать только неизменяемые объекты. Для решения нашей задачи мы можем по
ступить следующим образом (листинг
Листинг
10.8).
10.8. Chapter_10/example_OS/append_count_default.py
def append_ count (lst=None) :
if 1st is None:
1st = []
lst.append(len(lst))
return 1st
foo = append_ count ()
bar = append_ count ()
spam = append _ count ( [ 10, 20, 30])
print (f" ( foo=) ")
print (f" {bar=) ")
print(f"{foo is bar=)")
print ( f" ( spam=) ")
Результатом выполнения такого скрипта будет следующий текст в консоли:
foo=[0]
bar=[0]
foo is bar=False
spam=[l0, 20, 30, 3]
Всё работает именно так, как мы хотели. Если не задан явно параметр 1st, то есть
если он равен None, то в этом случае каждый раз внутри функции создается новый
пустой список, который затем дополняется новым элементом и возвращается из
функции, и переменные
foo
и ьаr указывают теперь на разные списки.
Функции с переменным числом
позиционных параметров
Python
позволяет создавать функции, для которых количество аргументов заранее
не определено. Например, мы часто использовали функцию print (), передавая ей в
качестве параметров для вывода на экран разное количество объектов.
202
Часть
11.
Основные подходы
Допустим, мы хотим создать функцию, которая принимает один обязательный чи
словой параметр и произвольное количество дополнительных числовых парамет
ров, а затем возвращает их произведение:
>>>
def mul(a, *arqs):
print(f"mul: {args=)")
result = а
for х in args:
result *= х
return result
Первым аргументом функции является обязательный аргумент а. После него следу
ет аргумент, перед именем которого указан символ«*» (звездочка). С точки зрения
функции
mul
аргумент (параметр)
args
будет представлять собой кортеж (возмож
но, пустой), в который попадут все позиционные параметры, переданные в функ
цию после параметра а. Чтобы в этом убедиться, внутри функции
параметра
Имя
args
args
mul ()
значение
выводится в консоль.
является общепринятым именем для переменной, куда попадают необя
зательные позиционные параметры, хотя,
с точки зрения интерпретатора,
можно
использовать любое другое имя. Важно, чтобы параметр со звездочкой был указан
после всех обязательных параметров.
Вызовем функцию
mul ()
с тремя дополнительными параметрами:
>>> foo = mul(2, 3, 4, 5)
mul: args=(3, 4, 5)
>>> foo
120
Параметры функции, обозначенные звездочкой, являются необязательными, по
этому функцию
mul ()
можно вызвать, указав только первый параметр:
>>> foo = mul(2)
mul : args= ()
>>> foo
2
Следующий вызов также является корректным:
>>>
foo = mul(a=2)
А вот передать параметр а как именованный, а затем добавить несколько дополни
тельных
позиционных
параметров
не
получится,
потому
что
именованные
пара
метры всегда должны быть указаны после позиционных:
>>>
foo = mul(a=2, 3, 4, 5)
Функции с переменным числом
именованных параметров
Помимо необязательных позиционных параметров,
функций необязательные именованные параметры.
Python
позволяет объявлять для
Глава
1О.
203
Функции
Для того, чтобы функция могла принимать произвольное количество именованных
параметров, в конце списка параметров должен быть указан параметр с двумя звез
дочками:
keywords -
«* *».
этому
Обычно
параметру
дают
имя
kwargs
(kw
обозначает
ключевые слова), хотя это не обязательно с точки зрения интерпрета
тора. В момент вызова функции этот параметр будет словарем, в котором ключами
станут необязательные именованные параметры (обязательные параметры туда не
попадают), а значениями
-
соответственно, значения этих параметров.
Чтобы продемонстрировать, зачем это нужно, и как это работает, напишем функ
цию-калькулятор
calculate (), которая будет принимать один обязательный чи
словой параметр х и произвольное количество именованных параметров. Если сре
ди именованных параметров есть параметр mul, то значение х будет умножено на
значение параметра
mul,
а если среди именованных параметров есть параметр
add,
то к результату будет добавлено значение параметра add, и функция вернет рас
считанное таким образом значение. Для лучшего понимания внутри функции
calculate () в консоль выводится значение параметра kwargs (листинг 10.9).
Листинг
10.9. Chapter_10/example_09/kwargs.py
def calculate(x, **kwargs) :
print(f"{kwargs=}")
result = х
if 'mul' in kwargs:
resul t *= kwargs [ 'mul' ]
if 'add' in kwargs :
result += kwargs [ ' add ' ]
return result
print(f ' {calculate(S) =}\ n\ n')
print(f'{calculate(lO, mu l=2)= }\ n\ n'}
print(f' {calculate(lO, add=S)=)\n\n')
print(f'{cal culate(lO, add=S, mul=2)=)\n\n')
print(f' {calculate(x=lO, mul=2 , add=S)=) ')
В результате выполнения этого скрипта будет выведен следующий результат:
kwargs={)
calculate(5)=5
kwargs={'mul': 2)
calculate(lO, mul=2)=20
kwargs={'add': 5)
calculate(lO, add=5 )=15
kwargs ={' add' : 5, ' mul ' : 2 )
calculate(lO, add=S , mul=2 )=2 5
204
Часть
11.
Основные подходы
kwargs={'mul': 2, 'add': 5)
calculate(x=lO, mul=2, add=5)=25
В этом примере показаны разные способы вызова функции
calculate (): без не
обязательных параметров, с одним из возможных дополнительных параметров и в
различных их комбинациях. Обратите внимание на последний вызов, где обяза
тельный параметр х указан тоже как именованный, но при этом он в словарь kwargs
не попадает.
В принципе, хоть в примере это и не показано, мы можем добавлять в вызов функ
ции и лишние именованные параметры, которые функция не ожидает получить,
-
например:
calculate(lO, add=S, div=2)
Если такая ситуация происходит, то, скорее всего, это значит, что мы допустили
какую-то логическую ошибку, но с точки зрения интерпретатора
кой не является,
-
Python
это ошиб
лишний параметр будет добавлен в словарь kwargs, но функция
использовать его не станет. Для аккуратности функция при вызове может прове
рить, не содержит ли словарь
kwargs
такие неожиданные ключи, и при их наличии,
например, возбудить исключение, сообщая об ошибке.
Пример, приведенный в листинге
10.9,
написан в том виде, чтобы быть максималь
но понятным, но мы можем сделать его более компактным, воспользовавшись ме
тодом get () из класса dict. Вспомним, что этот метод возвращает значение по ука
занному ключу или значение по умолчанию, если запрашиваемого ключа в словаре
нет. Поэтому код функции можно записать так:
def calculate(x, **kwargs):
print(f"{kwargs=)")
result = х
resul t *= kwargs. get ( 'mul' , 1)
result += kwargs.get('add', О)
return result
Или еще более компактно:
def calculate(x, **kwargs):
print(f"{kwargs=)")
return х * kwargs.get('mul', 1) + kwargs.get('add',
О)
Необязательные именованные параметры часто используются в ситуациях, когда
функция может принимать большое количество параметров, и если их все указы
вать в списке переменных, это будет слишком громоздкая запись. Например, в биб
лиотеке
Matplotlib,
предназначенной для построения различных видов графиков
(о ней речь пойдет в главах
27
и
28),
многие функции могут принимать большое
количество таких параметров для тонкой
настройки
внешнего вида графиков.
И поскольку возможных параметров очень много, и далеко не все из них требуются
одновременно, то они передаются через необязательные именованные параметры.
Глава
10.
Функции
205
Кроме того, такой способ передачи параметров позволяет расширять возможный
набор параметров, не нарушая обратную совместимость функции по отношению к
предыдущим версиям этой же функции.
Однако у такого способа есть очевидный недостаток
все возможные параметры
-
должны быть задокументированы, поскольку из заголовка функции невозможно
узнать, какие параметры она ожидает.
В одной функции можно совмещать необязательные позиционные и именованные
параметры. В этом случае параметр
параметр, а * * kwargs -
* args должен быть указан как предпоследний
как последний параметр функции.
Разделители параметров/ и*
Как мы видели до сих пор, обязательные параметры функции при вызове могут
быть переданы как позиционные или как именованные
на усмотрение того, кто
-
вызывает функцию. Однако у разработчика функции есть инструменты, ограничи
вающие такую свободу и указывающие, какие параметры должны быть переданы
только как позиционные, а какие
-
только как именованные.
Например, такая возможность активно используется в математической библиотеке
NumPy
(про нее будет рассказано в главе
ции вроде sin (), cos ()
25).
В частности, математические функ
и т. п. запрещают передавать им основной параметр как
именованный, то есть мы можем написать а = sin (Ь), но не можем написать
а = sin (х=Ь), хотя в описании функции sin () первый параметр имеет имя х. А до
полнительные, более редко используемые параметры функций, которых достаточно
много, мы, наоборот, обязаны передавать как именованные.
Для реализации таких ограничений в список параметров функции нужно добавить
параметры-разделители, которые имеют имена/ и* (эта возможность появилась в
Python 3.8).
Если такие параметры имеются в функции, то:
♦ все параметры, указанные слева от параметра
позиционные, а справа
-
/, должны
♦ все параметры, указанные справа от параметра
как именованные параметры, а слева
Схематично это показано на рис.
def func (pos 1,
... ,posN,
r
Позиционные
параметры
передаваться только как
произвольным образом;
-
*
должны передаваться только
произвольным образом.
10.1:
/,anyl,
anyN,
---~---
r
*,
kwdN):
kwdl,
r
Позиционные
Именованные
или
параметры
именованные
параметры
Рис.
10.1.
Разделение параметров функции с помощью разделителей
/
и
*
206
Часть
11.
При необходимости могут присутствовать или оба параметра:
/
один из них. Если присутствуют оба, то параметр
параметров левее параметра
/
Основные подходы
и
*,
или только
должен располагаться в списке
*.
В качестве примера напишем функцию calculate (), которая принимает три пара
метра. Первые два параметра:
action -
а и ь
-
будут числовыми, а третий параметр
строковый, он будет определять, какую операцию нужно выполнить над
значениями параметров а и ь. Если параметр ас t i on будет равен строке
значения параметров
11
mul 11 ,
а
и
ь
складываются,
а если
параметр
action
II
add 11 , то
равен
строке
то значения параметров а и ь перемножаются.
Если мы не будем использовать параметры
/
и
*,
то такая функция может выгля
деть так:
def calculate(a, Ь, action):
if action == "add 11 :
return а+ Ь
elif action == "mul":
return а* Ь
При ее вызове параметры можно передавать и как позиционные, и как именованные:
foo = calculate(2, 3, "add")
bar = calculate(2, 3, action= 11 add 11 )
spam = calculate(a=2, Ь=3, action="add")
Но если мы хотим, чтобы параметры а и ь передавались только как позиционные,
то после них можем указать разделительный параметр
def calculate(a,
Ь,
/:
/, action):
В этом случае корректными вызовами функции calculate () будут только такие:
foo
calculate (2, 3, "add")
bar = calculate(2, 3, action="add")
Если же мы попытаемся передать параметры а или ь как именованные:
foo = calculate(a=2,
Ь=3,
action="add")
то получим ошибку:
Traceback (most recent call last):
File " ... ", line 7, in <module>
foo = calculate(a=2, Ь=3, action="add")
TypeError: calculate() got some positional-only arguments passed as keyword arguments:
'а,
Ь'
Заменим разделительный параметр/ на*:
def calculate(a,
Ь,
*, action):
В этом случае мы должны будем параметр action передавать как именованный, а
параметры а и ь
-
произвольным образом.
Глава
10.
Функции
207
То есть корректными будут следующие вызовы:
foo = calculate(2, 3, action="add")
bar = calculate(a=2, Ь=3, action="add")
А вот такой вызов:
foo = calculate (2., 3, "add")
приведет к ошибке:
Traceback (most recent
File " ... ", line 14,
foo = calculate (2,
TypeError: calculate()
call last):
in <module>
3, "add")
takes 2 positional arguments but 3 were given
Разделительные параметры
/
и
* могут использоваться одновременно. Изменим
функцию calculate (), добавив в нее еще один булев параметр, который определя
ет, нужно ли применить функцию взятия модуля к рассчитываемому значению. Те
перь функция будет выглядеть так:
def calculate(a, Ь, /, action, *, use abs=False):
if action == "add":
result =а+ Ь
elif action == "mul":
result =а* Ь
else:
return None
if use abs:
result = abs(result)
return result
В этом случае параметры а и ь могут передаваться только как позиционные, пара
метр action может передаваться произвольным образом, а параметр use_abs
мо
жет быть передан только как именованный. То есть корректными будут следующие
вызовы:
foo
bar
baz
calculate(2, -5, "add")
calculate(2, -5, action="add")
calculate(2, -5, action="add", use abs=True)
А вызов:
spam = calculate(2, -5, "add", True)
приведет к ошибке:
Traceback (most recent call last):
File " ... ", line 16, in <module>
spam=calculate(2, -5, "add", True)
TypeError: calculate() takes 3 positional arguments but 4 were given
Такое разделение параметров может быть полезно в функциях с большим количе
ством параметров для их логической группировки.
Часть
208
11.
Основные подходы
Функции и глобальные переменные
Переменная называется глобалыюй, если она объявлена вне функций и классов, но
при этом доступна внутри них. Использование глобальных переменных считается
плохим стилем программирования, потому что в случае ошибок тяжело отлавли
вать ситуации, где именно изменяется глобальная переменная, и как это изменение
может повлиять на другие функции. Поэтому глобальных переменных следует по
возможности избегать, хотя иногда глобальные переменные создают для того, что
бы не передавать в большое количество функций одно и то же значение. Такие пе
ременные могут быть полезны, если они используются как константы, то есть их
значения не меняются после создания переменной.
Разберемся, как в
Python
работает доступ к глобальным переменным. Рассмотрим
для этого скрипт, приведенный в листинге
10.10.
Лмстинr 10.10. Chapter_10/example_10/gloЬal.py
foo =
"Вне
func () "
def func () :
print(f"l) {foo=}")
func ()
Здесь объявлена строковая глобальная переменная foo, которая доступна также
внутри функции func () . Результат выполнения этого скрипта очевиден и выглядит
следующим образом:
1)
fоо='Вне
func()'
Попытаемся изменить переменную foo внутри функции func () (листинг
Лмстинr
foo =
10.11).
10.11. Chapter_10/example_11/gloЬal.py
"Вне
func()"
def func():
foo = "Внутри func () "
print(f"l) {foo=}")
func ()
print(f"2) {foo=)")
И здесь уже мы увидим интересное поведение интерпретатора. Результат выполне
ния этого скрипта будет таким:
1)
2)
func()'
func()'
fоо='Внутри
fоо='Вне
Глава
10.
Функции
209
Глобальная переменная foo не изменилась, а внутри функции func () создалась но
вая локальная переменная с тем же именем.
Если мы изменим функцию func ()
таким образом, чтобы переменная foo сначала
использовалась для чтения значения, а потом создавалась такая же локальная пере
менная, то мы получим ошибку (листинг
Листинг
foo =
10.12 ).
10.12. Chapter_10/example_12/global_error.py
"Вне
func ()"
def func () :
print(f"l) {foo=)")
foo = "Внутри func()"
func ()
print(f"2) {foo=)")
Текст ошибки:
Traceback (most recent call last):
F'ile " ... ", 1 ine 7, in <module>
func ()
File " ... /global error.py", line 4, in func
print(f"l) {foo=}")
UnboundLocalError: cannot access local
а value
variaЫe
'foo' where it is not associated with
Интерпретатор увидел, что внутри функции будет создаваться переменная foo, и
поэтому глобальная переменная foo для такой функции стала невидимой.
Если же внутри функции нужно изменить глобальную переменную, то предвари
тельно хорошенько подумайте, нельзя ли без этого обойтись. Если вы все же реши
те, что это действительно нужно, то внутри функции нужно явно объявить, что
глобальная переменная foo будет изменяться. Это делается с помощью ключевого
слова global (листинг
Листинг
foo =
10.13).
10.13. Chapter_10/example_13/global_error.py
"Вне
func () "
def func () :
gloЬal
foo
foo = "Внутри func()"
print (f"l) {foo=) ")
func ()
print(f"2) (foo=)")
210
Часть
11.
Основные подходы
Результатом выполнения этого скрипта будет следующий текст:
1)
2)
fоо='Внутри
fоо='Внутри
func()'
func()'
Глобальная переменная foo изменилась. Код, который ранее вызывал ошибку, так
же можно исправить с помощью ключевого слова global (листинг
Листинг
foo =
10.14).
10.14. Chapter_10/example_14/global.py
"Вне
func()"
def func():
global foo
print(f"l) {foo=)")
foo = "Внутри func()"
func ()
print(f"2) {foo=}")
Результат будет ожидаемый
-
сначала будет выведено старое значение перемен
ной foo, а затем это значение изменится:
1)
2)
fоо='Вне
func()'
func()'
fоо='Внутри
То, что делает следующий пример (листинг
10.15),
лучше не применять на практи
ке, но мы можем сделать так, чтобы глобальная переменная создавалась внутри
функции.
Листинг
10.15. Chapter_10/example_15/global_bad.py
def func () :
global foo
foo = "Внутри func()"
print(f"l) {foo=)")
func ()
print(f"2) {foo=}")
В таком коде не очевидно, где создается глобальная переменная foo. Она не суще
ствует до вызова функции func () . Результат выполнения этого скрипта:
1)
2)
fоо='Внутри
fоо='Внутри
func()'
func()'
Если внутри функции нужно изменять несколько глобальных переменных, то мож
но их объявить с использованием одного ключевого слова global, указав имена
глобальных переменных через запятую:
def func():
global foo, bar
Глава
10.
Функции
211
Но, как уже говорилось, надо быть очень осторожным при использовании глобаль
ных переменных, особенно в случае, если они могут изменяться в процессе работы
программы. Это потенциальный источник ошибок.
Заключение
Функции
-
важнейший элемент в программировании. В этой главе мы разобра
лись с тем, как создавать функции, и убедились в том, что любая функция всегда
возвращает какое-то значение,
но, то это будет
-
даже если возвращаемое значение не указано яв
None.
Мы узнали, что такое «утиная типизация»,
-
когда не важен конкретный тип пере
даваемых переменных, главное, чтобы эти типы поддерживали требуемые опера
ции над ними.
Затем мы подробно разобрались со способами передачи параметров в функции, и
узнали, что параметры можно передавать как позиционные, так и как именованные,
а также, что параметрам можно задавать значения по умолчанию.
Мы рассмотрели способы создания функций, имеющих переменное число позицион
ных
и
именованных
параметров,
с
использованием
параметров
вида
*args
**kwargs соответственно. Затем познакомились с разделительными параметрами
и
*,
и
/
позволяющими указывать, какие параметры нужно передавать только как по
зиционные, какие только как именованные, а какие
-
произвольным образом.
И в завершение затронули тему глобальных переменных. Если вы хотите изменить
глобальную переменную внутри функции, нужно объявить ее как global -
Python
иначе
создаст новую локальную переменную.
В этой главе мы только начали изучать достаточно объемную тему функций.
Следующая глава будет посвящена особенностям, связанным с тем, что функции в
Python -
это тоже объекты.
- ГЛАВА 11
-
Функции как объекты
Функция
-
это тоже объект
Как мы уже знаем, в
Python
все сущности являются объектами, и функции
-
не
исключение. В этом можно убедиться на простом примере:
>>> def add(a, Ь): return
>» type (add)
<class 'function'>
>>> other calc = add
>>> other_calc(4, 2)
а+ Ь
6
>>> add is other calc
True
»> id(add)
140087780764672
Объявление функции с использованием ключевого слова de f -
это создание объ
екта класса function и переменной, которая ссылается на этот объект. С перемен
ной, ссылающейся на функцию, мы можем выполнять все допустимые операции,
применимые к переменным такого типа. Например, можем присваивать значение
переменной другой переменной, после чего они обе начинают указывать на один и
тот же объект функции. При этом каждый объект функции имеет свой идентифика
тор, который мы можем узнать с помощью функции id () . В таком случае говорят,
что функции относятся к объектам первого класса, противопоставляя их сущно
стям «второго класса», или «неполноценным», с которыми нельзя
проделать опи
санные операции.
На самом деле любой объект может вести себя как функция. Если в классе объекта
определен метод
оператор
().
>>> add.
call
6
_ cal 1 _ (),
значит, к экземплярам этого класса можно применять
При необходимости метод
(4, 2)
_ cal 1 _ ()
можно вызывать явным образом:
Глава
11.
Функции как объекты
Поскольку функция
-
213
это объект первого класса, то его можно передавать в каче
стве аргумента другой функции. И кроме того, функция может возвращать в каче
стве своего значения объект другой функции. Такие функции называют функциями
высшего порядка. Это элементы идеологии, которая называется «функциональное
программирование», а языки программирования, работающие в этой идеологии,
называются функциональными.
Python
нельзя назвать полноценным функциональ
ным языком, но элементы функционального программирования в нем присутствуют.
С помощью функций высшего порядка можно писать универсальный код, который
позволяет легко расширять возможности принимающей функции. Во многих си
туациях нам не придется изменять код такой функции
-
достаточно будет созда
вать новые функции, которые будут передаваться ей в качестве аргумента.
В предыдущей главе мы написали функцию calculate (), которая меняла свое по
ведение в зависимости от строкового параметра action, -
она либо складывала,
либо перемножала числа:
def calculate(a, Ь, action):
if action == "add":
return а+ Ь
elif action == "mul":
return а* Ь
В таком подходе есть два недостатка. Во-первых, программист, который вызывает
функцию calculate (), должен знать, какие конкретные строки ожидает эта функ
ция в качестве параметра action. И, во-вторых, если понадобится добавить новое
действие,
например, операцию деления, то придется изменять саму функцию
-
calculate () и добавлять новую строку для параметра action, что может быть не
всегда возможно, если, например, функция calcula te () является частью сто
ронней библиотеки.
Эти проблемы можно решить, если в качестве параметра action будем передавать
объект функции, который будет непосредственно выполнять нужные действия над
переменными а и ь. Для этого нужно изменить саму функцию calculate () и доба
вить к ней новые функции: add () и mul () . Чтобы сделать наш пример более полез
ным, изменим функцию calculate () с тем, чтобы она выводила в консоль имя вы
зываемой функции, параметры а и ь, а также результат расчета (листинг
Листинг
11.1. Chapter_11 /example_01/calculate.py
def calculate(a, Ь, action):
result = action(a, Ь)
action name = action. name
print(f"{action name)({a), {Ь))
return result
def add(a,
Ь):
return
а+ Ь
def mul(a,
Ь):
return
а* Ь
{result)")
11.1).
Часть
214
11.
Основные подходы
foo = calculate(2, 3, add)
print(f"{foo=)")
bar = calculate(2, 3, mul)
print(f"{bar=)")
В этом коде есть одна особенность, о которой мы еще не говорили. У функций име
ется строковое поле _name_ (в терминах объектно-ориентированного програм~и
рования полями называют переменные внутри класса), которое равно имени функ
ции. Позже мы узнаем, что эту переменную можно менять, и в каких случаях это
оправдано. Но пока мы эту переменную используем, чтобы узнать имя функции,
которую передали в функцию calculate (), и вывести лог работы в консоль.
После вызова этого скрипта в консоль будет выведен следующий результат:
add{2, 3)
foo=5
mul (2, 3)
5
6
Ьаr=б
Теперь, если мы хотим расширить набор действий, который может выполняться
внутри
функции
calculate (), -
calculate (),
нам
не
нужно
изменять
саму
функцию
достаточно написать новые функции, которые мы будем в нее пе
редавать. Например, мы можем добавить функции, которые после суммирования
или перемножения параметров еще вычисляют модуль:
def add_abs(a,
Ь):
return abs(a +
Ь)
def mul_abs(a,
Ь):
return abs(a *
Ь)
baz = calculate(2, -5, add_abs)
spam = calculate(2, -5, mul_abs)
Анонимные функции
В предыдущих примерах мы создали несколько очень простых функций, которые
выполняют всего лишь одно действие, и при этом используются в коде только один
раз. Это отличные кандидаты для того, чтобы заменить их анонимными функциями.
Анонюmая функция, или шLнбда-функция (лямбда-выражение)- это не имеющая
имени функция, которая обычно объявляется непосредственно в использующей ее
инструкции. Название «лямбда-функция» пришло из раздела математики «лямбда
исчисление», который начал формироваться в 1930-х годах благодаря трудам Алонзо
Чёрча.
Для создания анонимной функции используется следующий синтаксис:
lamЬda
argl, arg2, ... , argN:
Выражение
Глава
11.
215
Функции как объекты
Здесь lambda -это ключевое слово, затем приводятся параметры этой функции (их
может не быть). Обратите внимание, что скобки вокруг аргументов анонимной
функции не ставятся. Затем, после двоеточия, записывается выражение, значение
которого будет возвращено из анонимной функции. Ключевое слово return для
возврата значения не используется.
Перепишем предыдущий пример
функций (листинг
(см.
листинг
11.1)
с использованием анонимных
11.2).
def calculate (а, Ь, action) :
result = action(a, Ь)
action name = action. name
print(f"(action name}((a}, (Ь})
return result
(result}")
foo = calculate(2, З, lamЬda х, у: х + у)
bar = calculate(2, 3, lamЬda х, у: х * у)
baz = calculate (2, -5, lamЬda х, у: аЬs (х + у))
spam = calculate(2, -5, lamЬda х, у: аЬs(х * у))
Код стал заметно компактнее, а результат вызова такого скрипта теперь выглядит
следующим образом:
(2, 3) = 5
(2, 3) = 6
3
<lamЬda> (2, -5)
<lamЬda>(2, -5) = 10
<lamЬda>
<lamЬda>
Поскольку у лямбда-функций нет имени, то значения полей
_ name _
у них равно
<lambda>.
В
Python
анонимные функции имеют ограниченные возможности
-
они могут со
стоять лишь из одного выражения. Это принципиальная позиция автора языка Гвидо
ван Россума, состоящая в том, чтобы не давать разработчикам возможности созда
вать сложные функции внутри выражений, возможно, с большой глубиной вложен
ности. В других языках, (например,
Java
и
JavaScript)
считается обычной практикой
создавать длинные анонимные функции, которые в свою очередь вызывают другие
длинные анонимные функции и т. д., что обычно создает проблемы при чтении ко
да. В
Python,
даже если функция вам нужна только в одном месте кода, и если она
хоть сколько-нибудь длинная, ее нужно объявлять с помощью ключевого слова
def.
Но при этом анонимные функции
-
это по-прежнему полноценные объекты, по
этому, например, ламбда-выражение можно присвоить какой-нибудь переменной, и
в этом случае по поведению такая переменная будет практически неотличима от
обычной функции (листинг
11.3).
Часть
216
Листинг
11.
Основные подходы
11.3. Chapter_11/example_03/lambda.py
def calculate(a, Ь, action):
result = action(a, Ь)
action name = action. name
print(f"{action name)({a), {Ь))
return result
add = lamЬda х, у: х + у
mul = lamЬda х, у: х * у
add abs
lamЬda х, у: abs(x +
mul_abs = lamЬda х, у: abs(x *
{result)")
у)
у)
foo = calculate(2, 3, add)
bar = calculate(2, 3, mul)
baz = calculate(2, -5, add_abs)
spam = calculate(2, -5, mul_abs)
Результат выполнения этого скрипта будет такой же, как и у предыдущего. Значе
ние поля _name_ у таких объектов будет по-прежнему равно <lambda>. Иногда
можно увидеть использование лямбда-выражений с присваиванием, заменяющее
объявление функции, но, согласно РЕР
8, такой
код писать не рекомендуется.
Рассмотрим еще несколько типичных примеров, показывающих ситуации, в кото
рых используются анонимные функции.
Когда мы говорили про списки в главе
4,
то там намеренно не коснулись функций,
связанных с сортировкой. Пришло время поговорить и про них. Есть два основных
способа отсортировать список:
♦
использовать встроенную функцию sorted (), которая описывается следующим
образом:
sorted(iteraЫe,
♦
/, *, key=None, reverse=False)
или задействовать метод sort () класса list:
list.sort(*, key=None, reverse=False)
Различие между ними заключается в том, что функция sorted () принимает в каче
стве первого параметра сортируемый список и возвращает новый отсортированный
список, не изменяя исходный, а метод
sort () сортирует список, для которого этот
метод вызывается «по месту», изменяя первоначальный список.
Параметр reverse в обоих функциях обозначает, нужно ли сортировать список в
обратном порядке (в этом случае reverse должен быть равен True).
Но нас сейчас интересует параметр key, который в качестве значения может при
нимать функцию одного аргумента или значение None. Если параметр key не указан
или равен
None,
то при сортировке сравниваются непосредственно элементы спи
ска. Если параметр key указан и является функцией, то при сортировке для каждого
Глава
11.
Функции как объекты
217
элемента списка вызывается эта функция, и сравниваются не сами элементы, а зна
чения, которые она возвращает.
Такой прием используется, когда нужно отсортировать объекты в списке по како
му-то более сложному критерию, чем сравнение самих объектов.
Допустим, у нас есть строка из слов, и мы хотим отсортировать слова в алфавитном
порядке. При этом в строке часть слов начинается с заглавных букв, а часть
со
-
строчных. Без использования параметра key мы могли бы написать следующий
скрипт (листинг
Листинr
11.4).
11.4. Chapter_11/example_04/sort_no_key.py
text = "LOREM Ipsum dolor SIT amet Consectetur adipiscing Elit"
words = text.split(" ")
words. sort ()
print (f" {words=} ")
Результат будет выглядеть следующим образом:
words=['Consectetur', 'Elit', 'Ipsum', 'LOREM', 'SIT', 'adipiscing', 'amet', 'dolor'}
Слова действительно отсортированы по алфавиту, но только в начало списка попа
ли все слова, начинающиеся с заглавной буквы, а затем расположились слова, на
писанные строчными буквами. Так произошло, поскольку при сортировке строк их
сравнение происходит по кодам символов, а в таблице символов заглавные буквы
имеют номера меньше строчных. Скорее всего, это не то, что мы ожидали,
-
нам
по большей части бывает нужна сортировка без учета регистра букв. Для исправле
ния ситуации мы можем использовать параметр
key,
присвоив ему в качестве зна
чения функцию, которая бы переводила каждый элемент списка строк в верхний
или нижний регистр (листинг
Листинr 11.5.
11.5).
Chapter_11/example_OS/sort_lower.py
text = "LOREМ Ipsum dolor SIT amet Consectetur adipiscing Elit"
words = text. spli t (" ")
words.sort(key=str.lower)
print(f"{words=}")
Теперь результат выполнения этого скрипта таков:
words= [ 'adipiscing', 'amet', 'Consectetur', 'dolor', 'Eli t', 'Ipsum', 'LOREM', 'SIT']
Обратите внимание, что операция приведения слов к нижнему регистру использу
ется здесь только для сравнения элементов и не изменяет исходные элементы,
-
ведь можно было бы всю строку перевести в нижний регистр и не использовать па
раметр key, но тогда в отсортированном списке все слова были бы в нижнем регистре.
Также обратите внимание, что в качестве параметра key мы указали функцию из
класса
str.
Часть
218
11.
Основные подходы
И еще один пример, на этот раз не связанный со строками. Пусть у нас есть список
кортежей из двух числовых элементов, и мы хотим получить новый список, отсор
тированный по второму элементу кортежа. При этом исходный список должен ос
таться неизменным. Мы можем воспользоваться функцией sorted () (листинг
Листинг
11.6).
11.6. Chapter_11/example_06/sort_tuples.py
foo = [ (10, 10), (О, -2), (2, -3), (15, 12), (15,
foo_sorted = sorted(foo, key=lamЬda item: item[l])
print(f"{foo=)")
print(f"{foo_sorted=)")
О)]
Вот результат выполнения этого скрипта:
foo=[ (10, 10), (О, -2), (2, -3), (15, 12), (15, О)]
foo_sorted=[ (2, -3), (0, -2), (15, О), (10, 10), (15, 12)]
...
Подводя итог обсуждения в этом разделе, отметим, что в
Python
нередко использу
ется возможность передавать функции как параметр другой функции, и часто для
краткости кода в них можно задействовать анонимные функции.
Строки документации
Если для написания кода вы пользуетесь специализированным редактором наподо
бие
Visual Studio Code,
то могли заметить, что при наведении курсора мыши на ка
кую-либо функцию появляется всплывающая подсказка с описанием этой функции.
Пример такой подсказки показан на рис.
11.1.
После описания параметров функции (пока не обращайте внимание на его фор
мат
-
об этом мы поговорим в главе
18)
следует документация функции, где ука
зано, что она делает, что возвращает, и что обозначает тот или иной параметр.
Более того, такую документацию по функциям можно вывести в консоль, если вос
пользоваться встроенной функцией help (), передав ей функцию или класс. Вот что
будет выведено в консоль, если ее применить к функции pr int ():
»> help(print)
Help on built-in function print in module builtins:
print(*args, sep=' ', end='\n', file=None, flush=False)
Prints the values to а stream, or to sys.stdout Ьу default.
sep
string inserted between values, default а space.
end
string appended after the last value, default а newline.
file
а file-like object (stream); defaults to the current sys.stdout.
Глава
11.
219
Функции как объекты
flush
whether to
forciЫy
flush the stream.
Может возникнуть вопрос, откуда редактор и функция
help () берут эту информа
цию? Дело в том, что к каждой функции, классу и модулю (про модули разговор
пойдет в главе
может быть прикреплена строка документации
12)
(docstring),
именно ее и показывают при необходимости эти инструменты.
Рис.
11.1.
Всплывающая подсказка с описанием метода
find ()
класса
str
Чтобы добавить строку документации к функции, нужно сразу после ее объявления
на следующей строке написать строковый литерал с ее описанием. Для строк доку
ментации принято использовать многострочные литералы
(см.
главу
8),
даже если
описание функции умещается на одной строке, и обрамлять их тремя двойными
кавычками.
При создании функции строки документации сохраняются в поле
_ doc _
объекта
функции.
Вернемся к функции решения квадратного уравнения
ней строку документации (листинг
nистмнг
-
11. 7).
11.7. Chapter_11/example_07/equatlon_doc.py
from math import sqrt
def equation (а,
Ь,
с)
:
"""Функция для решения
квадратного уравнения.
Функция возвращает кортеж из двух корней уравнения,
если дискриминант неотрицательный,
возвращает
значение
None.
в противном случае
equation () -
и добавим к
Часть
220
Ь,
а,
D =
Ь
--
с
Основные подходы
коэффициенты квадратного уравнения.
** 2 - 4 *
а
*
с
О :
if D >=
(2 *
/ (2 *
xl =
(-Ь
+ sqrt (D)) /
а)
х2 =
(-Ь
- sqrt (D))
а)
return (xl,
pпnt
11.
( f" { equation.
х2)
doc
} ")
В результате выполнения этого скрипта в консоль будет выведена строка докумен
тации для функции equati o n ():
квадратн ого
уравнения.
Функция
дпя решения
Функция
возвращает кортеж из двух
если дискриминант неотрицательный ,
возвращает
а,
Ь,
с
--
значение
корней уравнения,
в противном случае
None.
к оэ ффициенты квадратного уравнения.
При наведении курсора мыши на упоминание в коде функции equation () редактор
Visual Studio Code
отобразит аналогичную всплывающую подсказку (рис.
11 .2).
Такие подсказки рекомендуется писать для всех нетривиальных функций.
Соглашения по оформлению строк документации приведены в РЕР
257 Docstring
Conventions 1.
Рис.
11.2.
Всплывающая подсказка в
Visual Studio Code
для функции equation ()
Строки документации также используются программами, предназначенными для
генерации документации по коду. Для языка
1 См.
https://peps.python.org/pep-0257/.
Python
наиболее известная из таких
Глава
11.
Функции как объекты
программ
-
это
Sphinx.2.
221
Она предлагает задействовать в строках документации
свои специфические команды для более наглядного выделения параметров функ
ции, возвращаемого значения и других особенностей описываемой функции. Одна
ко описание этой программы выходит за рамки этой книги.
Декораторы
Начнем издалека. Взгляните на функцию calculate () из листинга
11.1,
которая в
качестве параметра action принимает другую функцию, выполняет ее и выводит в
консоль параметры вызова этой функции и ее значение. У нас получилась полезная
функция для логирования вызовов других функций:
def calculate(a, Ь, action):
result = action(a, Ь)
print(f"{action. name )({а),
return result
{Ь))
{result)")
Сейчас функция calculate () представлена в таком варианте, что может работать
только с функциями, которые принимают ровно два параметра. Мы сделаем ее бо
лее универсальной.
Сначала изменим функцию calculate () так, чтобы она могла принимать любое
количество неименованных параметров и передавала бы их в функцию action. Это
достаточно легко сделать (листинг
Лмстинr 11.8. Фраrмент файла
11.8).
chapter_11/example_08/talculate_args.py
def calculate(action, *a.rgs):
result = action(*a.rgs)
params_text_items = [str(arg) for arg in args]
params_text = ", ".join(params_text_items)
print(f"{action. name ) ({params_text)) = {result)")
return result
Добавление параметра *args в функцию calculate () уже должно быть понятно по
сле прочтения главы
1О,
а вот на следующей строке используется новая для нас конст
рукция, позволяющая передать параметры из кортежа args в функцию action ():
result = action(*args)
Такая конструкция распаковывает кортеж args для передачи его содержимого в
виде аргументов функции. Продемонстрируем это поведение в интерактивном ре
жиме:
>>> def add(x, у, z): return
>>> args = (10, 20, 30)
2 См.
https://www.sphinx-doc.org.
х
+у+
z
Часть
222
11.
Основные подходы
>>> foo = add(*args)
»> foo
60
Если бы мы написали:
foo = add (args)
это означало бы, что в функцию
add () передается один параметр -
кортеж
args, и
это не то поведение, которое нам нужно.
После вызова функции action () происходит формирование строки params _ text,
предназначенной для вывода в консоль в качестве списка параметров функции.
Чтобы воспользоваться возможностями новой версии функции
ним функции
calculate (), изме
add () и mul (), чтобы они также могли принимать произвольное ко
личество параметров, но не меньше одного (листинг
Листинг 11.9. Фрагмент файла
11.9).
chapter_11/example_08/calculate_args.py
def add(a, *args):
return а+ sum(args)
def mul(a, *args):
result = а
for n in args:
result *= n
return result
Теперь мы можем вызывать функцию
ство параметров (листинг
calculate (), передавая в нее любое количе
11.1 О).
Листинг 11.10. Фрагмент файла
chapter_11/example_08/calculate_args.py
foo = calculate(add, 2, 3, 4, 5)
print(f"(foo=I")
bar = calculate(mul, 2, 3, 4)
print(f"(bar=)")
Результат выполнения полного варианта этого скрипта:
add(2, 3, 4, 5) = 14
foo=14
mul (2, 3, 4) = 24
bar=24
Следующим логичным шагом будет добавление в функцию calculate () возмож
ности передачи именованных параметров
-
вдруг для какой-то будущей функции
это понадобится. Добавим в список аргументов
calculate () еще и параметр
* * kwargs и дополним вывод параметров в консоль (листинг 11.11 ).
Глава
11.
Функции как объекты
Листинг 11.11. Фрагмент файла
223
chapter_11/example_09/calculate_kwargs.py
def calculate (action, *args , **kwargs):
result = act ion(*args , **kwargs)
act ion name = act i on. name
params_text_items = [s tr(arg) for arg in args]
params_text_items += [f"{ str(key) )={ str(value) )"
for key, value in kwargs . items()]
params_text = ", ". join( params_text _items)
print(f"{action_name ) ({params_text ) ) = {resul t)")
return result
Обратите внимание на вызов функции a ct i on () -
чтобы распаковать словарь
kwargs в именованные параметры (по аналогии с позиционными), используется
конструкция **kwargs.
Наши функции
add () и mul () не используют именованные параметры, но для де
**kwargs , хотя использовать их внут
монстрации работы добавим к их аргументам
ри функции не будем, оставив тела этих функций прежними (листинг
Листинг 11.12. Фрагмент файла
11.12).
chapter_11/example_09lcalculate_kwargs.py
def add (a, *a rgs , **kwargs ):
def mul (а, *args, **kwargs):
Теперь мы можем вызывать функцию calculate () и передавать еще именованные
параметры (листинг
11.13).
~нr 11.13. Фрагмент файла
chapter_11/examplt_ot/calculate_kwargs.py
foo = calculate (add, 2, 3, 4, 5, argl=lO, arg2=20)
print (f" {foo=) ")
bar = calcul ate (mul, 2, 3, 4, argl =l O, arg2=2 0)
print (f" {bar= ) " )
Результат выполнения полного варианта этого скрипта:
add(2, 3, 4, 5, argl=lO, arg2=20) = 14
foo=l4
mul(2, 3, 4, argl=l O, arg2=2 0) = 24
bar=24
224
Часть
11.
Основные подходы
У нас получилась достаточно полезная функция, которая выводит в консоль лог
вызовов других функций. Хотелось бы упростить использование этой возможности
для любых функций. Для этого надо сделать еще два шага.
До сих пор мы принимали функции как параметры. Но поскольку функции
-
это
полноценные объекты, то они могут возвращаться другими функциями в качестве
результата выполнения. А инструкция def для объявления функции может исполь
зоваться внутри другой функции. Сделаем новую функцию log_ func (), прини
мающую функцию действия (action) и возвращающую функцию calculate (),
которая функцию действия выполняет и выводит в консоль лог ее работы (лис
тинг
11.14).
Листинг
11.14. Фрагмент файла chapter_11/example_10/log.py
def log_func(action):
def calculate(*args, **kwargs):
result = action(*args, **kwargs)
action name = action. name
params_text items = [str(arg) for arg in args]
params_text_items += [f"{str(key) l={str(value)I"
for key, value in kwargs.items()]
params_text = ", ".join(params text_items)
((params_textl) = {resultl")
returп result
return calculate
print(f"(action_пamel
Теперь для того, чтобы добавить логирование к произвольным функциям, напри
мер, add () и mul (), нужно выполнить следующие операции (листинг
Листинг
11.15).
11.15. Фрагмент файла chapter_11/example_10/log.py
def add(a, *args, **kwargs):
def mul(a, *args, **kwargs):
add = log_func(add)
mul = log_func (mul)
После этого функции add () и mul () можно использовать как обычные функции, не
задумываясь о том, что они на самом деле вызываются через другую функцию
(calculate () ), но при этом у этих функций появилась способность выводить лог
своей работы (листинг
11.16).
Глава
11.
Листинг
Функции как объекты
225
11.16. Фрагмент файла chapter_11/example_10/log.py
foo = add(2, 3, 4, 5, argl=lO, arg2=20)
print(f" {foo= )")
bar = mul(2, 3, 4, argl=lO, arg2=20)
print (f" {bar= ) ")
Результат работы этого скрипта не отличается от результата предыдущего.
Рассмотрим функцию log_ func () более подробно. Она демонстрирует несколько
интересных моментов. Мы видим, во-первых, что можно объявлять функцию внут
ри другой функции, и во-вторых, что функцию можно возвращать из другой функ
ции (причем именно объект функции, а не результат ее выполнения). В-третьих,
теперь функция calculate () не принимает параметр action, а использует его из
объемлющего функцию calcula te () кода.
Процесс, когда к функциям «прицепляются» переменные, которые находятся вне
этих функций, чтобы функции могли эти переменные использовать, называется за
мыканием
(closure).
Также замыканием называют связку функции и внешних пере
менных.
Мы вызывали функцию log_func () дважды, передав в качестве параметров action
объекты
функций
add ()
и
mul (),
а
в
ответ
получили
две
новые
функции
calculate (), которые, благодаря замыканию, имеют доступ каждая к своему пара
метру action. Полученные из log_ func ()
функции мы присвоили переменным
add и mul, которые до этого указывали на первоначальные функции add () и mul (),
а теперь указывают на экземпляры функции calculate (), созданные внутри функ
ции
log_ func () .
После переопределения переменных add и mul мы можем их использовать как
обычные функции, которые, помимо вызова изначальных функций add ( J и mul (),
будут выводить в консоль лог работы.
На самом деле мы только что написали декоратор с именем log_func. Декорато
ром
(decorator)
в
Python
называется функция (или любой вызываемый объект), ко
торая принимает в качестве параметра функцию и возвращает другую функцию.
Для более простого применения декоратора к функции предусмотрен специальный
синтаксис @имя_декоратора, который ставится до имени функции, передаваемой
декоратору в качестве параметра. Скрипт, который у нас получился на этом этапе,
можно записать следующим образом (листинг
llмстмнг 11.17.
Chapter_11 /example_11 /decorator.py
def log_func (action) :
def calculate(*args, **kwargs):
result = action(*args, **kwargs)
action name = action.
name
11.17).
Часть
226
11.
Основные подходы
params_text_items = [str(arg) for arg in args]
params text items += [f"(str(key) )=(str(value) )"
for key, value in kwargs.items()]
params_text = ", ".join(params text_items)
print (f" ( action _ name) ( (params _ text)) = ( result} ")
return result
return calculate
@loq_func
def add(a, *args, **kwargs):
"""Функция возвращает сумму ее параметров."""
result = а
for n in args:
result += n
return result
@loq_func
def mul(a, *args, **kwargs):
"""Функция
возвращает произведение ее параметров."""
result = а
for n in args:
result *= n
return result
foo = add(2, 3, 4, 5, argl=lO, arg2=20)
print(f"{foo=)")
bar = mul(2, 3, 4, argl=lO, arg2=20)
print(f"{bar=)")
При этом говорят, что мы обернули декоратором функции add () и mul (). Но оста
лась одна маленькая проблема. В коде листинга
ки документации к функциям
11.1 7
не зря были добавлены стро
add () и mul () . Если в конце этого скрипта добавить
следующие команды:
print(f"{add._name_=)")
print(f"{add._doc_=)")
print(f"{mul. name_=)")
print (f" {mul ._doc_=) ")
то в консоль будет выведен текст:
add.
add.
mul.
mul.
name ='calculate'
doc =None
name ='calculate'
doc =None
Действительно, теперь переменные add и mul ссылаются на функции calcula te (),
и таким образом мы потеряли первоначальные значения полей
_ name _ и _ doc _.
11.18).
Но эта проблема легко решается исправлением декоратора (листинг
Глава
11.
Jlистинr
Функции как объекты
227
11.18. Chapter_11/example_12/decorator_flx.py
def log_ func (action) :
def calculate(*args, **kwargs):
result = action(*args, **kwargs)
action name = action. name
params_text_items = [str(arg) for arg in args]
params_text_items += [f"{str(key) )={str(value) )"
for key, value in kwargs.items()]
params_text = ", ".join(params text_items)
print (f" {action_ name) ( {params _ text)) = {result) ")
return result
calculate. name
calculate._doc_
return calc
= action. name
= action._doc_
Мы присваиваем здесь полям
_name_ и _doc_ из функций calculate () значе
ния одноименных полей из переданной функции action. И теперь вызов:
print (f" {add. _ name_=) ")
print (f" {add._doc_=) ")
print (f" {mul. _name _=) ")
print(f"{mul. doc_=)")
будет неотличим от вызова первоначальных функций add () и mul () :
add.
name
=' add'
аdd._dос_='Функция возвращает сумму ее параметров.'
mul.
name
= 'mul'
mul._dос_='Функция возвращает произведение ее параметров.'
Помимо полей
_ name _
и
_ doc _,
также имеются и другие поля, которые мы не
рассматривали, но которые также не мешало бы переопределить в декораторе. Это
достаточно частая операция при создании декораторов, поэтому стандартная биб
лиотека
Python
предоставляет специальный декоратор @wraps, расположенный в
модуле functools. Этот декоратор берет на себя рутинную задачу по подготовке
возвращаемой из декоратора функции. Функцию log_func () можно переписать с
использованием декоратора @wraps следующим образом (листинг
нr 11.19. Chapter_11/example_13/decorator_wraps.py
from functools import wraps
def log_ func (action) :
@wraps (action)
def calculate(*args, **kwargs):
11.19).
228
Часть
11.
Основные подходы
result = action(*args, **kwargs)
action name = action. name
params_text items = [str(arg) for arg in args]
params_text_items += [f"{str(key) )={str(value) )"
for key, value in kwargs.items()]
params_text = ", ".join(params text_items)
print(f"{action_name) ({params_text)) = {result)")
return result
return
calculate
Обратите внимание, что декоратор @wraps принимает в качестве параметра объект
функции action, который передается внутрь декоратора @log func. Строки, где мы
изменяли значения полей _name _
за нас сделает декоратор
Декораторы
-
и
_ doc _,
теперь можно удалить,
-
эту работу
@wraps.
это очень мощный инструмент, тема, связанная с ними, достаточно
обширна, и мы рассмотрели только лишь ее основы. Например, за рамками этой
книги остались такие вопросы, как декораторы в виде классов, передача в декора
торы дополнительных параметров (как это сделано в декораторе @wraps), а также
обработка ошибок в декорируемых функциях.
Заключение
Эта глава посвящена интересной и сложной теме «функции как объекты». Функции
в
Python являются
объектами, или, как говорят, объектами первого класса, поэтому
над ними можно производить все операции, которые допустимы с объектами, в том
числе операции присваивания, функции можно передавать в качестве параметра
другой функции, а также объект функции может быть возвращен из другой функции.
При этом внутри одной функции может быть объявлена другая функция, которую
можно вернуть. Такое поведение характерно для декораторов, которые, как мы
увидим позже, активно используются в
Python.
В этой главе мы рассмотрели лишь
основы создания декораторов.
Для многих функций, принимающих другую функцию в качестве параметра, может
оказаться избыточным создавать отдельную короткую функцию, которая будет ис
пользоваться лишь в одном месте. Для создания маленьких функций «по месту»
предназначены анонимные функции (лямбда-выражения), создаваемые с помощью
ключевого слова
lambda.
Функции также могут иметь строки документации
поле_ doc _
(docstring),
которые хранятся в
объекта каждой функции.
Следующая глава будет посвящена модулям, которые позволяют разбить длинный
скрипт на несколько файлов и сделать таким образом структуру программы более
понятной.
- ГЛАВА 12 -
Модули и пакеты модулей
Создание и импорт модулей
До этого момента мы активно использовали модули из стандартной библиотеки
Python -
такие как math, cmath, random и даже functools. В этой главе мы научим
ся создавать свои модули и пакеты модулей, а также рассмотрим, что происходит,
когда они импортируются.
Модули применяются не только как внешние библиотеки,
-
благодаря модулям
большую программу можно разделить на несколько файлов, чтобы было проще
находить нужный класс или функцию, кроме того, такая модульная архитектура
позволяет в дальнейшем более элегантно повторно использовать написанный ра
нее код.
В простейшем случае модули в
rут иметь и другой формат
-
Python -
это файлы с расширением ру. Модули мо
например, расширение рус принадлежит именам
файлов с байт-кодом, сгенерированным интерпретатором. Файлы в таком формате
создаются интерпретатором для ускорения загрузки модулей и обычно не предна
значены для передачи пользователю. Файл модуля также может иметь расширение
pyd (Python dynamic module)- это двоичный модуль, который может включать в
себя скомпилированный код, написанный на другом (компилируемом) языке про
rраммирования. Мы в этой книге такие модули рассматривать не будем.
Чтобы найти запрашиваемый при импорте модуль, интерпретатор
Python
просмат
ривает множество путей в файловой системе, по которым модули могут быть рас
положены. Эти пути можно узнать в процессе выполнения любого скрипта
-
они
содержатся в переменной path из модуля sys, представляющей собой список строк.
В процессе поиска импортируемого модуля каталоги просматриваются в том по
рядке, в котором они указаны в sys .path. При запуске скрипта с помощью коман
ды
python
<имя_скрипта.ру>
первым элементом списка
sys.path
станет каталог,
где расположен запускаемый скрипт.
Чтобы в этом убедиться, напишем короткий скрипт, который выводит список путей
для поиска модулей (листинг
12.1 ).
230
Часть
11.
Основные подходы
import sys
for path in sys.path:
print(path)
Результат выполнения этого скрипта зависит от настроек операционной системы и
каталога установки
Python.
Под
Windows
скрипт выведет в консоль примерно сле
дующее:
C:\projects\python-book\chapter_12\example_0l
C:\Program Files\Python313\python313.zip
C:\Program Files\Python313\DLLs
C:\Program Files\Python313\Lib
C:\Program Files\Python313
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32\lib
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\Pythonwin
C:\Program Files\Python313\Lib\site-packages
Список
sys. path можно изменять в процессе работы скрипта -
если требуется
добавить другие пути, где нужно искать модули. Кроме того, существует перемен
ная окружения
PYTHONPATH, которая может содержать дополнительный список пу
sys. path. Под Windows пути
в переменной окружения PYTHONPATH разделяются символом«;», а под Linux - «: )).
тей, которые при запуске скрипта будут добавлены в
Добавим в
PYTHONPATH несколько путей в операционной системе Windows. Для ус
тановки значения переменной окружения в консоли воспользуемся встроенной ко
мандой
set:
> set PYТHONPATH=C:\mypackaqes_l;C:\mypackaqes_2
> python print_path.py
C:\projects\python-book\chapter_12\example_0l
С: \mypackaqes_ 1
с: \mypackaqes_ 2
C:\Program Files\Python313\python313.zip
C:\Program Files\Python313\DLLs
C:\Program Files\Python313\Lib
C:\Program Files\Python313
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32\lib
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\Pythonwin
C:\Program Files\Python313\Lib\site-packages
Пути из переменной окружения PYTHONPATH были добавлены сразу после каталога
расположения запускаемого скрипта.
Глава
12.
231
Модули и пакеты модулей
В дальнейших примерах мы будем считать, что создаваемые нами модули распо
ложены в том же каталоге, что и выполняемый скрипт.
Создадим импортируемый модуль - файл с именем tools. ру, который будет со
держать несколько функций и переменных (листинг 12.2).
нr 12.2,
EPS
Chlpttr_12/tumplt_02/tooll.py
О= 8.854187817е-12
МU О= 1.256637061е-6
def add(a,
return
Ь):
def mul (а,
return
Ь) :
а+ Ь
а
*
Ь
Теперь нам нужно создать скрипт, который будет использовать функции из модуля
tools. Назовем его application.py. Этот файл должен располагаться в том же катало
ге, что и файл tools.py. У нас имеется несколько вариантов, как использовать функ
ции из модуля tools.py внутри скрипта application.py. В главе
импорта модулей на примере стандартных библиотек
2 мы
обсуждали способы
math и cmath, поэтому здесь
лишь коротко вспомним о них.
Первый способ импорта модулей заключается в использовании конструкции вида:
ilaport
Модуль
import мы импортируем целый модуль. После такого им
порта образуется новый объект класса module с именем модуля. Этот объект со
С помощью оператора
держит все сущности импортируемого модуля. Именно через этот объект мы полу
чаем доступ к переменным, функциям и классам модуля. Для того, чтобы в этом
убедиться, применим к объекту импортируемого модуля функции type () и dir ()
(листинг
12.3).
Аютинr 12.3. Chapter_12/examplt_02/appllclllon.py
import math
import tools
foo = tools.add(lO, 20)
bar = tools.mul(4, 2)
с= 1 / math.sqrt(tools.EPS_O *
tools.МU_O)
print(f"(type(tools)=}")
print(f"(dir(tools)=)")
Запустим скрипт application.py, и в консоль будет выведен следующий текст:
type(tools)=<cla11 'module'>
dir(tools)=['EPS_O', 'МU_О', ' builtins ' '_cached_', ' doc '
'__package_', '_spec_', 'add', 111111')
name
' loader '
1
'
file
1
,
Часть
232
11.
Основные подходы
Если использование полного имени модуля в коде оказывается слишком громозд
ким, для объекта модуля можно дать другое имя (псевдоним), импортировав мо
дуль с помощью инструкции:
i.mport
Модуль
as
Псевдоним
Когда из модуля нужно задействовать небольшое количество функций или классов,
то удобнее импортировать не весь модуль, а только требуемые сущности. Такой
импорт осуществляется с помощью инструкции:
fran
Модуль
i.mport
Сущность_l,
Сущность_2,
...
В этом случае при использовании импортированных сущностей не требуется пи
сать имя модуля, поскольку теперь мы будем обращаться к сущностям напрямую, а
не через объект модуля.
Однако при таком способе импорта могут возникнуть проблемы, связанные с тем,
что имена импортируемых объектов могут совпасть с именами других объектов в
вызывающем модуле, или разные модули могут предоставлять функции с одинако
выми именами (например, функция sqrt () имеется в модулях math и cmath, а если
установить библиотеку
NumPy,
то еще и в модуле numpy). Эту проблему можно ре
шить, если при импорте сущности дать ей другое имя (псевдоним) с помощью
ключевого слова
fran
Модуль
as:
i.mport
Сущность
as
Псевдоним
Например:
>>>
>>>
>>>
8.0
»>
from math import sqrt
from cmath import sqrt as csqrt
sqrt(60 + 4)
csqrt(l -
2)
lj
Есть еще один способ импорта, которым пользоваться не рекомендуется, но знать о
нем все-таки нужно. Этот способ импортирует из модуля всё, что он позволяет им
портировать:
fran
Модуль
i.mport
*
Такой способ импорта опасен тем, что заранее не известно, какие функции или
классы будут импортированы, и поэтому может создаться ситуация, когда имена
импортированных объектов будут совпадать с уже имеющимися объектами. По
этому такого способа импорта следует избегать.
Выполнение кода модулей при импорте.
Переменные _пате_ и
_fi/e_
Разберемся с тем, что происходит, когда мы используем инструкцию import. В этот
момент импортируемый модуль запускается на выполнение, в результате чего соз
даются все функции и классы, которые в нем объявлены. Далее либо создается объ-
Глава
12.
Модули и пакеты модулей
233
ект модуля со всеми классами и функциями, либо эти объекты передаются непо
средственно в тот модуль, который их импортирует. Но важно, что при импорте
происходит выполнение кода импортируемого модуля. Продемонстрируем это на
примере. Изменим импортируемый модуль tools.py, добавив в него строки для вы
вода текста в консоль (листинг
Листинг
12.4).
12.4. Chapter_12/example_OЗ/tools.py
print ( "ВИутри tools . ру - 1")
def add (а,
return
Ь)
а
:
+
tools.py - 2")
рrint("ВИутри
def mul (а,
return
Ь)
Ь
:
а*
Ь
В код скрипта application.py также добавим вывод в консоль (листинг
Листинг
12.5).
12.5. Chapter_12/example_OЗ/application. py
рrint("ВИутри
application.py - 1")
import tools
application.py - 2")
tools.add(lO, 20)
= tools.mul (4, 2)
рrint("ВИутри application.py - 3")
рrint("ВИутри
foo
bar
=
Обычно не рекомендуется писать код таким образом, чтобы до строк импорта были
какие-то еще команды, как мы это сделали в файле application.py, если в этом нет
особой необходимости. Если теперь запустить скрипт application.py, то в консоль бу
дет выведен следующий текст:
Внутри
Внутри
Внутри
Внутри
Внутри
application.py - 1
tools.py - 1
tools.py - 2
application.py - 2
application.py - 3
В тот момент, когда интерпретатор доходит до выполнения команды import tools,
он переключается на файл tools.py и выполняет код внутри него, в результате чего
не только создаются объекты для объявленных функций, но и выполняются строки,
которые выводят в консоль текст.
Это может вызвать проблемы, потому что если кто-то захочет импортировать ка
кие-то полезные функции из другого модуля, он может не знать о том, что импор-
Часть
234
11.
Основные подходы
тируемый модуль, помимо объявления функций или классов, содержит код, кото
рый был написан для непосредственного запуска, и он запустится на выполнение.
Python
предоставляет достаточно простое решение этой проблемы, но для его по
нимания нам надо познакомиться с переменной
ним файл tools.py (листинг
_name _. Для этого сначала изме
12.6).
Листинr 12.6. Chapter_12/example_lWtools.py
def add (а,
return
Ь) :
def mul (а,
return
Ь) :
а+ Ь
а* Ь
tools.py:
print("Bнyтpи
", f"{_name_=}")
Изменим также файл application.py (листинг
Листинг 12.7.
12.7).
Chapter_12/example_04/application.py
import tools
foo = tools.add(lO, 20)
bar = tools.mul(4, 2)
print ("Внутри application.py: ", f" {_name_=}")
Если теперь запустить на выполнение скрипт application.py, то будет выведен сле
дующий результат:
Внутри
Внутри
tools.py:
application.py:
Переменная
_ name _
name
name
='tools'
=' main
'
в импортируемом модуле равна имени модуля, а для скрипта,
который запускается через командную строку, переменная
строке
_name_
всегда равна
'_main_'.
Если мы запустим команду:
python tools.py
то получим в консоли следующий текст:
Внутри
tools.py:
name
='
main
'
Внутри скрипта tools.py в этом случае переменная
значение
'_main_'.
дующим образом (листинг
12.8).
Листинг 12.8. Chapter_12/example_05/tools.py
def add(a,
return
Ь):
а+ Ь
_name_ тоже стала содержать
И этим можно воспользоваться, изменив скрипт tools.py сле
Глава
12.
def mul (а,
return
if
235
Модули и пакеты модулей
Ы
:
а* Ь
= " main
- name-
print("ВнY'l'pи
"•
tools.py:
"
f"{_name_=}")
В скрипте tools.py мы будем выводить текст в консоль только в том случае, если
этот
выполняется
скрипт
непосредственно
через
передачу
его
интерпретатору
tools. ру. И результат при этом останется
прежним. Если теперь запустить скрипт application.py с помощью команды python
application. ру, то в консоли мы увидим только одну строку:
Python
Внутри
в командной строке: python
='
name
application.py:
main
Это очень удобно, поэтому во всех скриптах, которые по задумке разработчика
должны запускаться через консоль, желательно добавлять условие вида:
if
name
"
main
11 •
И выполняемый код помещать в блок кода этого условия. Даже если ваш скрипт не
предполагает, что его будут импортировать, хорошим стилем является добавление
такой проверки. Поэтому файл application.py тоже лучше переписать следующим об
разом (листинг
Листин
12.9).
lexample_
import tools
if
name
" main "·
foo = tools.add(lO, 20)
bar = tools.mul(4, 2)
print("Bнyтpи application.py:", f"{_name_=}")
Есть еще одна переменная, о которой полезно знать. Это переменная _file_. Она
содержит полный путь до файла модуля, внутри которого используется.
~--
Снова изменим файл tools.py (листинг
def add(a,
return
Ь):
def mul (а,
return
Ь)
12.10).
а+ Ь
:
а* Ь
print("Bнyтpи tools.py:
print ( "ВнУ'l'ри tools. ру:
"
f"{_name_=}")
f"{_file_=}")
236
Часть
А также дополним скрипт application.py (листинг
11.
Основные подходы
12.11).
Листинг 12.11. Chapter_12/example_07/application.py
import tools
if
name
" main ":
foo = tools.add(lO, 20)
bar = tools.mul(4, 2)
print("Bнyтpи application.py:", f"{ _ name_=}")
рrint("Виутри application.py: ", f"{_file
=}"}
print ( "Биутри application. ру: " , f" {tools. file_ =} ")
Здесь в файле application.py мы выводим значение переменной _file_ непосредст
венно из файла application .py и из модуля tools. В результате выполнения команды
python applicat ion. ру в консоль будет выведен текст:
Внутри
Внутри
Внутри
Внутри
Внутри
tools.py:
name ='tools'
tools.py:
file_=' ... /tools.py'
application.py:
name =' main
file_=' ... / application.py'
appl i cation.py:
application.py: tools. file_=' ... /tools.py'
Здесь под знаком
« ... »
скрывается полный путь до файлов tools.py и application.py.
Использование переменной
_ file_ может пригодиться, если необходимо знать
расположение какого-либо модуля и, например, прочитать некий файл, располо
женный в том же каталоге, что и модуль, или где-то в соседних каталогах.
Пакеты модулей
При создании больших приложений, а также библиотек, модули часто объединяют
в пакеты
(packages),
что позволяет лучше ориентироваться в архитектуре библио
теки или приложения.
В
Python
существуют два вида пакетов модулей: обычные пакеты
и пакеты пространств имен
(namespace packages).
(regular packages)
В дальнейшем мы будем говорить
только про обычные пакеты. Про пакеты пространств имен скажем лишь, что они
позволяют размещать вложенные пакеты (подпакеты,
subpackages)
и модули одного
пакета по разным путям файловой системы.
В простейшем случае обычный пакет
-
это каталог, в котором помимо файлов с
расширением ру, представляющих собой модули, расположен специальный файл с
именем _init_.py (обратите внимание, что до и после фрагмента init присутствуют
по два символа подчеркивания). По наличию этого файла интерпретатор
Python
определяет, что каталог нужно интерпретировать как пакет (для пакета пространст
ва имен файл _init_.py не требуется). В процессе импорта код из файла _init_.py
выполняется, но во многих случаях этот файл может оставаться пустым.
Глава
12.
Модули и пакеты модулей
237
В качестве примера создадим пакет с именем mymath. В этот пакет будут входить
модуль action, содержащий функции add ()
и mul (), а также модуль equations,
содержащий функцию equation () для решения квадратного уравнения. Для соз
дания указанного пакета myma th мы должны расположить файлы следующим об
разом:
f--- application.py
L
mymath
acti ons . ру
equa ti ons . py
init .ру
L_
f--f---
При этом внутри каталога myma th располагаются файлы со следующим содержи
мым (листинг
Листинг
и
12.13).
12.12. Chapter_12/example_08/mymath/actions.py
def add (а,
return
Ь) :
def mul (а,
return
Ь)
Листинг
12.12
а+ Ь
:
а*
Ь
12.13. Chapter_12/example_08/mymath/equations.py
from math import sqrt
def equation(a, Ь, с):
D = Ь ** 2 - 4 *а* с
if D >= О:
xl = (-Ь + sqrt(D)) / (2 *
х2 = (-Ь - sqrt(D)) / (2 *
return (xl, х2)
а)
а)
Файл _init_.py в этом каталоге останется пустым, а файл application.py, использую
щий пакет myma th, для начала будет содержать следующий код (листинг
Л истинг
12.14. Chapter_12/example_08/application.py
import mymath.actions
import mymath.equations
if
main "·
== "
name
foo = mymath.actions.add(l0, 20)
bar = mymath.actions.mul(4, 2)
result = mymath.equations.equation(S, 2, -10)
12.14).
Часть
238
11.
Основные подходы
print(f"{type(mymath)=}")
print(f"{type(mymath.actions)=)")
print(f"{type(mymath.equations)=)")
Если запустить на выполнение скрипт
application.py,
то в консоль будет выведен сле
дующий текст:
type(mymath)=<class 'module'>
type(mymath.actions)=<class 'module'>
type(mymath.equations)=<class 'module'>
Каждый раз писать полное название модуля, включая его родителей, слишком гро
моздко, поэтому при таком импорте тоже можно использовать оператор as, чтобы
дать импортируемому модулю псевдоним.
При импорте также можно указывать конкретную сущность, которую нужно им
портировать из модуля внутри пакета. В связи с этим файл
реписать следующим образом (листинг
application.py
можно пе
12.15).
Листинг 12.15. Chapter_12/example_09/appllcatlon.py
import mymath.actions as ma
from mymath.equations import equation
if
name
==" main ":
foo = ma.add(lO, 20)
bar = ma.mul(4, 2)
result = equation(S, 2, -10)
Впрочем, можно воспользоваться и промежуточным решением и импортировать
непосредственно вложенный модуль (листинг
Листинг
12.16).
12.16. Chapter_12/example_10/appllcatlon.py
from mymath import actions
from mymath import equations
if
name
==" main ":
foo = actions.add(lO, 20)
bar = actions.mul(4, 2)
result = equations.equation(S, 2, -10)
Внутри пакетов можно создавать вложенные пакеты
дать вложенную папку, содержащую еще один файл
- для этого достаточно соз
_init_.py и файлы модулей.
Иногда, особенно при разработке библиотек, с одной стороны, желательно для
лучшей организации кода разбить его на большее число модулей, но, с другой сто
роны, не хочется, чтобы пользователи библиотеки задумывались о внутренней
структуре модулей, а также есть намерение сделать путь до конкретных функций
Глава
12.
Модули и пакеты модулей
239
или классов более короткими. В этом может помочь файл
_init_py,
который у нас
пока еще всегда был пустым. Однако, если в этом файле объявлены или импорти
рованы какие-то объекты, то они будут доступны по имени пакета, благодаря чему
от пользователя можно скрыть наличие в пакете модулей. Изменим структуру ката
лога пакета
f--
myma th
на следующую:
application.py
L. mymath
f-f-L
_actions.py
_equations.py
init .ру
Мы переименовали
_equations.py,
здесь
файл
actions.py
в
_actions.py,
а файл
equations.py -
чтобы показать, что это внутренние модули, не предназначенные для
непосредственного импорта пользователем пакета. Добавим теперь в файл
следующий код (листинг
Листинг
в
_init_.py
12.17).
12.17. Chapter_12/example_11/mymath/_inlt_.py
from ._actions import add, mul
from ._equations import equation
Обратите внимание, что перед именами модулей стоят точки. Это означает, что
указанные модули нужно искать в пределах текущего пакета, где расположен файл
_init_.py, который вызывает эту инструкцию import. Можно еще использовать две
.. modulename - чтобы показать, что импортируемый модуль находится на
точки:
уровень выше в иерархии файловой системы.
Теперь в файл
(листинг
Листинг
application.py
12.18).
можно импортировать пакет mymath следующим образом
12.18. Chapter_12/example_11/application.py
import mymath
if
name
" main
foo = mymath.add(l0, 20)
bar = mymath.mul(4, 2)
result = mymath.equation(S, 2, -10)
11 :
Теперь функции, которые мы импортировали в файле
_init_py,
доступны непо
средственно через модуль (пакет) mymath без указания вложенных модулей. При
желании здесь также можно воспользоваться инструкцией импорта
В последнем примере мы переименовали файлы
начало
их
имен знак подчеркивания
-
actions.py
признак того,
что
и
from ... import
equations.py,
эти
модули
добавив в
не следует
240
Часть
11.
Основные подходы
импортировать напрямую вне пакета, но это всего лишь договоренность, и интер
претатор
не
запретит
вам
импортировать,
например,
модуль
mymath. _actions.
Но так лучше не делать.
Заключение
В предыдущих главах мы пользовались модулями, которые предоставляет стан
дартная библиотека
Python.
В этой главе мы разобрались с тем, как создавать соб
ственные модули, чтобы сделать архитектуру большой программы более понятной.
Мы увидели, где интерпретатор ищет модули в процессе импорта, узнали про пе
ременную sys. path, содержащую пути для поиска модулей, а также про перемен
ную окружения PYTHONPATH, которую можно использовать для добавления новых
путей, указывающих, где следует искать модули.
Мы еще раз вспомнили способы импорта сущностей из модулей, а также увидели,
что процесс импорта модуля или его содержимого запускает код импортируемого
модуля на выполнение.
С помощью переменной
_
name _
мы научились отделять строки кода, предназна
ченные для выполнения только во время запуска скрипта, от остальных строк кода,
которые должны выполняться всегда.
В последнем разделе главы мы рассмотрели создание пакетов, содержащих вло
женные модули, и узнали о разных способах импорта содержимого пакетов.
В этой книге мы не станем касаться особенностей создания пакетов с большим
уровнем вложенности, а также затрагивать тему сборки и распространения пакетов
через сервер
PyPi 1•
В следующих трех главах мы приступим к более подробному изучению объектно
ориентированного программирования в
Python
и поговорим про создание классов и
про многочисленные особенности, с этим связанные.
1
См. https://pypi.org.
- ГЛАВА
13-
QбъеКТНО•ОрИеНТИрОВаННОе
программирование.
Создание классов
Что такое объектно-ориентированное
программирование?
Python
первых
является объектно-ориентированным языком программирования. С самых
примеров
мы
использовали
объекты
и
постоянно
упоминали
термин
«класс» как синоним термина «тип» для переменных. В этой главе мы более под
робно поговорим про терминологию объектно-ориентированного программирова
ния, научимся создавать свои классы и обсудим особенности классов в
В главе
1О
Python.
про функции было сказано, что, благодаря функциям, мы можем выде
лить блоки кода для повторного использования, а кроме того, они позволяют сде
лать структуру программы более понятной за счет ее разделения на логические
блоки. Часто функции используются как «черный ящик», который выполняет ка
кие-то заранее известные действия, и пользователь функции может не задумывать
ся о том, как она устроена внутри, то есть функции позволяют абстрагироваться от
конкретного кода, который в них реализован. Объекты добавляют еще один уро
вень абстракции, объединяя в себя функции (их называют методами) и данные (их
называют полями).
Объектно-ориентированное программирование (ООП)- это парадигма програм
мирования, построенная вокруг понятия «объект». Объекты (экземпляры класса)
создаются
на основе классов, которые описывают, какие поля и методы должны
быть в объекте того или иного класса.
В литературе про ООП обычно называют три принципа, которые можно применять
к объектам (хотя это до сих пор тема многочисленных споров): инкапсуляция, на
следование и полиморфизм.
Инкапсуляция
(encapsulation, от латинского выражения in capsula, то есть помеще
- подразумевает, что объекты объединяют (инкапсулируют) в се-
ние в оболочку)
242
Часть
11.
Основные подходы
бе множество методов и полей. Концепция инкапсуляции предполагает, что взаи
модействие с объектом происходит только через специально созданные методы и
поля, предназначенные для использования вне объекта. В то же время, ряд методов
и полей предназначены лишь для внутреннего использования и не должны вызы
ваться (если речь идет о методах) или изменяться (если речь идет о полях) снаружи
объекта. Эта концепция позволяет повысить надежность программ.
Например, если мы создаем графический интерфейс, и у нас есть объект «кнопка»,
то у этого объекта должны быть такие поля как координаты расположения кнопки
на экране, цвет фона, текст на кнопке или картинка. Объект кнопки, кроме того,
должен иметь методы для ее скрытия/отображения. В качестве таких методов мо
жет быть реализована реакция кнопки на ее нажатие, и т. д. При этом пользователю
не обязательно знать, как кнопка реализована «под капотом»,
-
он оперирует
только теми методами, которые ему предоставляет объект. Мы уже работали с объ
ектами, принадлежащими
классам
int, float, str, list, dict, array
и другими,
и пользовались их методами, такими как list. append (), str. format () и пр. При
этом мы не задумывались о том, как именно эти методы работают, к каким внут
ренним полям обращаются, какие дополнительные внутренние функции вызывают.
Второе важное понятие ООП
-
наследование. Это возможность создавать новые
классы на основе уже существующих (их называют родительскими класса.ми, или
суперкласса.ми
-
от англ.
superclass).
Наследование позволяет использовать поля и
методы родительских классов и таким образом сократить дублирование кода по
сравнению с тем, если бы мы создавали новые классы или объекты заново. Про на
следование мы будем говорить в следующей главе.
Если возвращаться к нашему примеру с графическим интерфейсом, то, как прави
ло, в соответствующих библиотеках есть родительский класс, включающий в себя
поля и методы, общие для всех элементов управления,
-
например, координаты
объекта и методы для его скрытия и отображения. Остальные объекты для пред
ставления пользовательского интерфейса наследуют родительскому классу и полу
чают от него эти методы и поля, а также добавляют свои.
Третий принцип ООП
-
полиморфизм. Этот подход позволяет однотипно задейст
вовать методы разных классов, абстрагируясь от конкретных классов, но зная при
этом, что используемые классы реализуют нужную нам функциональность. В
Python
полиморфизм вытекает из идеологии «утиной типизации», поэтому его реализовать
намного легче по сравнению с полиморфизмом в компилируемых языках со стати
ческой типизацией. Применительно к примеру про элементы пользовательского
интерфейса, полиморфизм может проявляться в том, что окно способно содержать
объекты для разных элементов интерфейса, но, независимо от конкретного типа
элемента управления, их выравнивание и расположение в окне настраивается оди
наковым образом, вызывая одни и те же методы. В
Python
тот факт, что инструкция
for позволяет одинаково обходить коллекции разных типов, тоже можно назвать
проявлением полиморфизма.
Глава
13.
Объектно-ориентированное программирование. Соэдание классов
243
В реализации ООП существуют разные подходы, и иногда возникают споры отно
сительно того, какие языки более верны этой идеологии и что такое настоящее
ООП. Но мы не станем вдаваться в теоретические тонкости разных подходов, а рас
смотрим, что нам предлагает язык
Класс
-
Python.
это некое описание будущих объектов, где хранится информация о том,
как объект будет создаваться, какие он будет иметь внутри себя методы и поля.
Объект, или экземпляр класса
-
это некоторая сущность, которая создается на
основе информации, описанной в классе. Абстрагируясь от программирования, можно
сказать, что класс
-
это чертеж некой детали, а объект, или экземпляр класса
-
это
деталь, которая сделана по этому чертежу. Как на основе одного чертежа может
быть создано множество однотипных деталей, так и на основе одного класса может
быть создано множество независимых объектов.
Каждую более или менее большую программу можно разделить на классы различ
ными способами. С появлением ООП появилось множество теорий, предлагающих
свои видения того, как нужно выстраивать объектно-ориентированную архитектуру
приложения, то есть как организовывать взаимосвязи между объектами, как пра
вильно структурировать программы. Однако основной посыл этих теорий сводится
к тому, что нужно создавать такие классы, чтобы при последующей разработке
приложения приходилось бы как можно меньше вносить изменений в имеющийся
код, и каждое изменение затрагивало бы наименьшее количество классов, поэтому
каждый класс должен решать только одну задачу.
Например, мы многократно создавали и использовали экземпляры классов int,
float, str, list. К какому классу относится тот или иной объект, мы определяли с
помощью встроенной функции t уре () :
»> х = 10
»> type (х)
<class 'int'>
>» foo = [1, 2, 3]
>» type (foo)
<class 'list'>
Строго говоря, даже сами классы int, str и все остальные
-
это объекты класса
type, и мы можем в этом убедиться:
»> type(list)
<class 'type'>
>» type (int)
<class 'type'>
»> type (type)
<class 'type'>
Как видите, даже класс type относится к классу type, но это уже очень тонкие мо
менты, которые мы здесь обозначили, но углубляться далее в эту сторону не ста
нем. Перейдем, наконец, к созданию собственных классов.
Часть
244
11.
Основные подходы
Создание классов
Демонстрировать работу с классами мы будем на примерах. Предположим, что мы
разрабатываем сервис блогов. Разумеется, для создания настоящего веб-сервиса
нам пришлось бы предварительно изучить еще множество технологий вроде прото
кола НТТР, работы с базами данных, язык запросов
SQL
и многое другое, поэтому
классы, которые мы будем писать, станут притворяться, что они выполняют слож
ные взаимодействия с сервером, а на самом деле максимум, что они будут де
лать,
-
это выводить текст в консоль.
Для блогов мы сделаем класс для текстового поста, который назовем тextPost.
К каждому посту у нас должна быть привязана следующая информация: имя авто
ра, текст сообщения и дата создания.
Для лучшей структуризации кода класс TextPost мы поместим в отдельный модуль
textpost (листинг
Листинг
13.1).
13.1. Chapter_13/example_01/textpost.py
from datetime import datetime, timezone
class TextPost:
"""Класс текстового поста для блага"""
def
init (self, author, text):
print (f"TextPost. ini t () . self: ( self) ")
self.author = author
self. text = text
self.date = datetime.now(timezone.utc)
Скрипт
main.py
(листинг
13.2),
предназначенный для запуска через командную
строку, будет содержать код, использующий класс тextPost, и должен распола
гаться в том же каталоге, что и файл
Листинг
textpost.py.
13.2. Chapter_13/example_01/main.py
from textpost import TextPost
post =
ТехtРоst("Толстой Л.Н.",
"Очень длинный текст
... ")
Разберемся с объявлением класса тextPost (см. листинг
используется ключевое слово
класса, а затем символ «:
».
class,
13.1).
Для создания класса
после которого указывается имя создаваемого
Все содержимое класса располагается ниже объявления
класса и смещено на один отступ вправо (рекомендуются четыре пробела).
Требования интерпретатора к именам классов точно такие же, как и к именам пере
менных, но, согласно рекомендациям РЕР
8,
имя класса желательно начинать с за-
Глава
13.
Объектно-ориентированное программирование. Создание классов
245
главной буквы. Если название состоит из нескольких слов, каждое слово названия
так же следует начинать с заглавной буквы:
HelloWorld, ChatUser, settingsDialog
и т. п.
Так же, как и функции, классы могут иметь строки документации
(docstring) -
в
этом случае они помещаются в многострочный литерал на следующей строке после
использования ключевого слова
class.
Методы класса тоже могут иметь строки
документации.
Далее в классе тextPost располагается функция
_init_(),
которая в нашем слу
чае принимает три параметра. Эта функция (метод) представляет собой конструк
тор класса, она автоматически вызывается при создании каждого экземпляра класса
и предназначена для инициализации созданного объекта, в первую очередь
на
-
строек полей класса. Если объект после создания не требует инициализации, то ме
тод
_ ini t _ ()
можно не добавлять. Количество параметров метода
_ ini t _ ()
определяется логикой работы класса и зависит от того, сколько параметров требу
ется передавать при создании экземпляра класса.
При этом каждый метод (за исключением статических, про них мы поговорим от
дельно) должен иметь хотя бы один параметр, обычно называемый
self («self»
переводится с английского как «сам»). Это переменная, ссылающаяся на объект,
для которого вызывается метод, поэтому
self
имеет тип того объекта, для которого
этот метод объявлен. В нашем случае внутри класса
TextPost параметр self всегда
self в списке параметров ме
будет являться объектом класса тextPost. Параметр
тода обязательно указывается первым.
Если вы знакомы с языками С++, С# или
но только в этих языках
this
Java,
то
self -
это аналог указателя
передается в методы неявно, а в
Python
параметр
this,
self
нужно указывать явно.
С точки зрения синтаксиса языка
именно
self,
Python,
первый параметр не обязан называться
но это настолько общепринятая рекомендация, что редакторы кода
часто выделяют слово
self
как ключевое слово, хотя оно им не является.
При создании объекта интерпретатор выделяет память под объект, наполняет его
методами, а затем для созданного объекта вызывает конструктор
_ ini t _ ()
и в
качестве первого параметра передает ссылку на создаваемый объект, чтобы в кон
структоре этот объект можно было бы наполнить дополнительными полями.
Класс тextPost подразумевает, что при создании экземпляра этого класса будут
переданы два строковых параметра: имя автора поста
В скрипте
(author) и текст поста (text).
textpost создается эк
параметры. При этом параметр self
main.py после импорта класса тextPost из модуля
земпляр этого класса и передаются указанные
будет передан автоматически. Если бы в классе тextPost не был бы явно добавлен
конструктор
_ i n i t _ () , то
использовался бы конструктор по умолчанию, который
не принимает никакие параметры, кроме
Чтобы убедиться, что метод
дания объекта, а также, что
self.
_ ini t _ () действительно вызывается в процессе соз
self является экземпляром класса TextPost, первой
246
Часть
11.
Основные подходы
строкой в конструкторе вызывается функция print (), которая выводит в консоль
информацию о self. Выполнив в консоли команду:
> python main.py
мы получим вывод примерно такого текста:
TextPost._init_(). self: <textpost.TextPost object at Ox7f4a31135f70>
Из этой записи мы видим, что self является экземпляром класса тextPost из модуля
textpost.
Следующие три строки конструктора добавляют новые поля в объект класса. Инст
рукция присваивания вида:
sеlf.<имя поля>= <значение>
создает новое поле в экземпляре класса, если оно до этого не было создано, или
изменяет значение этого поля, если оно уже существует. Требования к именам по
лей те же самые, что и к именам переменных.
В
Python
объекты могут меняться в любом месте программы. Как инструкция при
сваивания
foo = 10
вне класса создает новую переменную
foo
при условии, что
она не была создана ранее, точно так же инструкция присваивания работает в вы
ражении вида
obj. foo = 10,
только в этом случае переменная
объекта obj. Теперь еще раз посмотрим на метод
_
ini t _
foo
() -
создается внутри
он содержит не
сколько таких присваиваний, где в качестве объекта выступает self. После вызова
print ()
следующие две строки конструктора сохраняют в полях класса переданные
параметры.
Строка:
self.date = datetime.now(timezone.utc)
сохраняет
в
поле
date значение текущих даты и времени (экземпляр класса
datetime). Параметр, переданный функции now (), обозначает, что мы хотим полу
чить текущее время в нулевом часовом поясе или Всемирное координированное
время, чтобы не зависеть от часового пояса, в котором запускается скрипт.
Метод
ра
_
ini t _
() должен возвращать None, что равносильно отсутствию операто
return.
Если вы знакомы с языком С++, то у вас может возникнуть вопрос: а что насчет
деструкторов (методов, которые вызываются перед уничтожением объекта класса)?
Деструкторов в явном виде в
Python
нет. Это связано с особенностями работы с па
мятью, поскольку мы не знаем, в какой момент сборщик мусора удалит тот или
иной объект. Мы можем реализовать метод _del_ (), вызываемый перед уничто
жением объекта, но нет гарантий, что этот метод в принципе будет вызван до за
вершения приложения. Кроме того, есть способы обеспечить автоматическое за
крытие объектов, таких как файлы, соединения с сокетами, базами данных и т. д.
Эту тему мы обсудим в главе
20,
когда речь пойдет про работу с файлами.
Теперь дополним скрипт main.py (см. листинг
13.2) несколькими
командами, которые
нам покажут информацию о созданном экземпляре класса тextPost (листинг
13.3).
Глава
13.
Объектно-ориентированное программирование. Создание классов
247
Jlистмнr 13.З. Chapter_13/examplt_01/maln.py
from textpost import TextPost
post =
ТехtРоst("Толстой Л.Н.",
"Очень дпинный текст
... ")
print (f" {type (post) =} ")
print(dir{post))
В результате выполнения этого скрипта в консоль будет выведен следующий текст:
TextPost. init (). self: <textpost.TextPost obJect at Ox7f8166335e80>
type(post)=<class 'textpost.TextPost'>
[' class
delattr
format
_eq_
doc
dict
' dir
, 1 init
_ ge_
' getattribute
'_getstate_' '_gt_'
hash
new
пе
' init subclass
le
' lt
' module '
str
' reduce
reduce ех
repr
, ' setattr ' ' sizeof
' subclasshook ' ' weakref ,-; 'author', 'date', 'text']
1
1
Мы здесь еще раз убеждаемся, что переменная post имеет тип (класс) тextPost из
модуля textpost, а затем с помощью функции dir () получаем список содержимого
экземпляра класса, где среди прочих полей и методов видим созданные поля
author, date
И
text.
То, что мы добавили поля именно в конструкторе класса
-
это хорошее правило,
однако поля можно добавлять в любом методе класса и даже вне класса, хотя
обычно это не приветствуется.
Следующие два примера относятся к серии «Вредные советы». Добавим метод,
создающий в классе тextPost новое поле id, которого не было после выполнения
конструктора (листинг
Листинг
13.4).
13.4. Chapter_13/example_02/textpost.py
from datetime import datetime, timezone
class TextPost:
"""Класс текстового поста дпя блога"""
def
init (self, author, text):
print (f"TextPost. ini t () . self: {self} ")
self.author = author
self.text
text
self.date = datetime.now(timezone.utc)
def set_id(self, value):
print ( "Присваивание значения
self.id value
=
Приведенная в листинге
13.5
полю
id")
версия скрипта
main.py
отображает содержимое объек
та класса тextPost сразу после его создания и после вызова метода
set _ id ().
Часть
248
Листинг
11.
Основные подходы
13.5. Chapter_13/example_02/main.py
from textpost import TextPost
post = ТехtРоst("Толстой
print (dir (post))
Л.Н.",
"Очень длинный текст
... ")
post.set_id(42)
print(dir(post))
В результате выполнения этого скрипта будет выведен следующий текст:
TextPost. init (). self: <textpost.TextPost object at Ox7f8683c31d30>
[ ... , 'author', 'date', 'set_id', 'text']
Присваивание значения полю id
[ ... , 'author', 'date', 'id', 'set_id', 'text']
Во втором выводе содержимого класса появилось поле id.
Теперь изменим скрипт main.py, добавив инструкцию создания нового поля вне
класса TextPost (листинг
Листинг
13.6).
13.6. Chapter_13/example_03/main.py
from textpost import TextPost
post = ТехtРоst("Толстой
post.new_field = 24
print(dir(post))
Л.Н.",
"Очень длинный текст
... ")
В результате выполнения этого скрипта будет выведен следующий текст:
TextPost. init (). self: <textpost.TextPost object at Ox7f6451139b50>
[ ... , 'author', 'date', 'new_field', 'set_id', 'text']
Часто создание нового поля вне конструктора является результатом ошибок. В при
веденном далее примере разработчик, скорее всего, хотел присвоить значение полю
author,
но в результате создал новое поле
post = ТехtРоst("Толстой Л.Н.",
post.avtor = "Пушкин А.С."
"Очень
avtor:
длинный текст
... ")
Это еще раз доказывает, что динамическая типизация является одновременно и
мощным, и опасным инструментом.
Видимость полей и методов классов
Если вы знакомы с другими объектно-ориентированными языками, такими как
С++, С# или
Java,
то знаете, что эти языки позволяют скрывать поля от доступа из
вне класса, предлагая для полей и методов несколько областей видимости. В
Python
Глава
13.
Объектно-ориентированное программирование. Создание классов
249
нет возможности скрыть поле, чтобы его нельзя было прочитать или изменить вне
класса. По аналогии с упомянутыми языками можно сказать, что в
Python все поля
13.4) класс
и методы являются публичными. В предыдущем примере (см. листинг
тextPost имеет несколько полей, и после создания экземпляра класса у нас есть
полный доступ к его содержимому:
post =
ТехtРоst("Толстой Л.Н.",
print("Aвтop:",
print("Дaтa:",
"Очень длинный текст
... ")
post.author)
post.date)
Мы можем даже менять значения полей:
post.author =
"Чехов А.П."
Это удобно для маленьких классов, единственное назначение которых
-
хранить
небольшой набор данных, однако для сложных классов такая вседозволенность
может стать проблемой. Во-первых, в больших классах часто создают предназна
ченные для внутреннего использования поля, возможность изменения которых вне
класса автор класса и не предполагал. Во-вторых, при изменении внутренних дан
ных нередко требуется также произвести какие-либо действия
-
например, сохра
нить измененное состояние объекта в базу данных, отправить сообщение об изме
нении объекта по сети и многое другое. Поэтому всегда полезно ограничивать
внешний доступ к полям. Интерпретатор нам в этом не помощник, поэтому разра
ботчики договорились, что поля, к которым не следует обращаться вне класса,
должны начинаться со знака
«_»,
а для получения или изменения значения поля вне
класса нужно предусматривать специальные методы.
Изменим класс тextPost согласно этим правилам (листинг
Листинг
13.7. Chapter_13/example_04/textpost.py
from datetime import datetime, timezone
class TextPost:
"""Класс текстового поста дпя блага"""
def
def
init (self, author, text):
self. author = author
self. text = text
self. date = datetime.now(timezone.utc)
self. save ()
save(self):
print("Пocт сохранен.")
def get_author(self): return self. author
def get_text(self): return self. text
13.7).
Часть
250
11.
Основные подходы
def set_text(self, value):
self. text = value
self ._save ()
def get_date(self): return self. date
Эта версия класса тextPost стала значительно длиннее. В конструкторе класса
тextPost все поля теперь начинаются с подчеркивания, предупреждая тем самым,
что их не следует использовать вне класса. Добавлен метод
_ s а ve ( ) ,
имитирующий
сохранение объекта в базу данных. Этот метод также начинается с символа
«_» -
то есть его тоже не следует вызывать вне класса. Обратите внимание, что все мето
ды класса в качестве первого параметра принимает объект self. И именно через
self
осуществляется доступ к полям экземпляра класса тextPost.
Кроме того, в этом примере добавлены методы для получения значений полей
так называемые геттеры
(getters),
так называемый сеттер
set_text () -
-
а для изменения поля _text добавлен метод
что устанавливает новое значение поля
(setter).
_ text,
Метод set_text (), помимо того,
следует использовать, как показано в листинге
_ save () - для ими
main.py класс TextPost
вызывает метод
тации сохранения объекта в базу данных. Теперь в скрипте
13.8.
Листинг 13.8. Chapter_13/example_04/main.py
from textpost import TextPost
post =
ТехtРоst("Толстой Л.Н.",
"Очень длинный текст
... ")
post.get_author())
print ("Дата:", post.get_date ())
print("Aвтop:",
print ( "Изменяем текст поста")
post, set _ text ("Еще более длинный
текст.
В результате выполнения скрипта
Пост
main.py в консоль
будет выведен следующий текст:
сохранен.
Автор:
Дата:
, , ")
Очень длинный текст
2024-10-11
...
18:23:49.859457+00:ОО
Изменяем текст поста
Пост
сохранен.
Мы можем получить доступ к полям класса тextPost напрямую, без использования
методов, но в этом случае работоспособность класса не гарантируется:
post._text =
"Еще более длинный текст.,."
С точки зрения языка этот код корректный, но не с точки зрения логики. Если вы
полнить такую версию скрипта
main.py,
то вывод будет точно такой же, как и ранее,
но в нем не появится вторая надпись о сохранении поста после изменения текста.
Глава
13.
Объектно-ориентированное программирование. Соэдание классов
251
Свойства
Геттеры и сеттеры часто встречаются в классах, но при этом явный доступ к полям
без вызова метода получается более компактным и интуитивно понятным. Чтобы
объединить удобство явного доступа к полям с безопасностью при использовании
методов,
Python
позволяет создавать в классе так называемые свойства
(properties),
которые, с точки зрения пользователя класса, выглядят как непосредственный дос
туп к полю класса, но на самом деле являются вызовом геттера
-
для получения
для его изменения. Свойства реализуются в виде де
значения поля или сеттера
-
кораторов (см. главу
Изменим класс тextPost таким образом, чтобы вместо
11).
геттеров и сеттера, которые в нем присутствовали, использовались свойства (лис
тинг
13.9).
Листинг 13.9.
Chapter_13/example_OS/textpost.py
from datetime import datetime, timezone
class TextPost:
"""Класс текстового поста для блага"""
def
init (self, author, text):
self. author = author
self. text = text
sel f ._da t e = da tet ime.now(time zone.ut c)
se lf . _save ()
def
save (self) :
print ("Пост сохранен.")
@property
def author(self): return self. author
@property
def text(self): return self. text
@text.setter
def text(self, value):
self. text = value
self. _ save ()
@property
def date(self): return self. date
Свойства класса тextPost теперь могут использоваться следующим образом (лис
тинг
13.10).
Часть
252
11.
Основные подходы
from textpost import TextPost
post =
print("Aвтop:",
print("Дaтa:",
... ")
"Очень длинный текст
ТехtРоst("Толстой Л.Н.",
post.author)
post.date)
print ("Изменяем текст поста")
post.text = "Еще более длинный
текст ...
"
Как можно здесь видеть, для создания свойства-геттера достаточно пометить ме
возвращающий
тод,
значение,
декоратором
@property.
Создание
свойства
сетгера- это чуть более сложная операция, и такой метод должен удовлетворять
следующим требованиям:
□
иметь то же имя, что и метод-геттер;
□
располагаться ниже метода-геттера в коде;
□
иметь два параметра: self и присваиваемое значение;
□
должен быть помечен декоратором с именем @<метод-геттер>. set ter.
Получается, что в классе не может быть свойства-сеттера без свойства-геттера.
Результат выполнения новой версии скрипта будет точно таким же, как при ис
пользовании геттеров и сеттеров в виде.обычных методов.
Поля класса
До сих пор все поля, созданные в классе, принадлежали экземплярам класса,
-
это
значит, что у каждого объекта класса TextPost были свои значения _author, _text
и
date (листинг
13.11).
'Листин
from textpost import TextPost
postl
post2
ТехtРоst("Толстой Л.Н.",
TextPost ( "Чехов
А. П.
",
"Очень длинный текст
"Текст покороче ...
")
print(f"{postl.author=)")
print(f"{post2.author=)")
В результате будут выведены авторы двух постов:
Пост
сохранен.
Пост
сохранен.
postl.author='Toлcтoй Л.Н.'
post2.author='Чexoв А.П.'
... ")
Глава
13.
Объектно-ориентированное программирование. Создание классов
253
Однако в некоторых случаях требуется, чтобы данные относились не к конкретным
объектам, а к классу в целом, и значение такого поля было бы одинаковым для всех
экземпляров класса. Часто это делается для экономии памяти, чтобы не создавать
поля с одинаковыми значениями во всех экземплярах класса. Для того, чтобы соз
дать поле, которое относится к классу, а не к объекту, это поле нужно объявить вне
какого-либо метода класса,
и обычно это делается до конструктора
-
_ i n i t _ () .
Добавим в класс тextPost строковое поле класса с именем db_name, которое будет
хранить имя базы данных, куда должны сохраняться объекты постов. Заодно допол
ним метод_ save
Листинг
(), чтобы он выводил информацию о базе данных (листинг 13.12).
13.12. Chapter_13/example_07/textpost.py
class TextPost:
"""Класс текстового поста для блога"""
dЬ_name
def
def
= "posts"
init (self, author, text):
self. author
author
self. text = text
self. date = datetime.now(timezone.utc)
self. save ()
save (self):
print(f"Пocт сохранен в базу данных
{TextFost.dЬ_name).")
Остальные свойства класса тextPost остались неизменными по сравнению с вари
антом, приведенным в листинге
13.9.
Обратите внимание, что для доступа к переменной db_name в методе _save ()
ис
пользуется имя класса: TextPost. db _ name, хотя в этом месте можно было бы ис
пользовать доступ к полю db_name и через объект класса: self .db_name. Результат
был бы точно таким же
-
это верно в случае, когда мы получаем значение поля
класса, но не изменяем его (скоро мы обсудим и такой случай).
Доступ к полю класса вне класса тоже можно получить через имя класса или через
экземпляр этого класса (листинг
Листинг
13.13).
13.13. Chapter_13/example_07/main.py
from textpost import TextPost
postl =
post2 =
#
ТехtРоst("Толстой Л.Н.",
TextPost("Чexoв А.П.",
Доступ к полю класса через имя
print(f"{TextPost.dЬ_name=)")
"Очень
"Текст
длинный текст
покороче
класса
... ")
... ")
Часть
254
#
11.
Основные подходы
Доступ к полю класса через экземпляры класса
print(f"{postl.dЬ_name=)")
print(f"{post2.dЬ_name=)")
В результате выполнения этого скрипта в консоль будет выведен следующий текст:
Пост сохранен в базу данных
Пост
сохранен
в базу данных
posts.
posts.
TextPost.dЬ_name='posts'
postl.dЬ_name='posts'
post2.dЬ_name='posts'
Главное отличие поля класса от поля экземпляра класса состоит в том, что изменение
значения такого поля приводит к изменению значения этого поля во всех экземпля
рах класса, в том числе уже созданных. Покажем это на примере (листинг
Листинг
13 .14 ).
13.14. Chapter_13/example_08/main.py
from textpost import TextPost
postl
post2
#
TextPost ( "Толстой Л. Н. ", "Очень длинный текст ... ")
TextPost ( "Чехов А. П. ", "Текст покороче ... ")
Значение dЬ_name изменяется через имя класса
name = "other
TextPost.dЬ
dЬ
name"
print(f"{TextPost.dЬ_name=)")
print (f ""{postl. dЬ_ name=} ")
print(f"{post2.dЬ_name=)")
postЗ
=
ТехtРоst("Пелевин В.О.",
"Текста нет.
Ничего нет.")
Результат выполнения этого скрипта выглядит следующим образом:
posts.
posts.
name='other- dЬ- name'
Пост
сохранен в базу данных
Пост
сохранен в
TextPost.dЬ
-
базу данных
postl.dЬ_name='other_dЬ_name'
post2.dЬ_name='other_dЬ_name'
Пост сохранен в базу данных other_dЬ_name.
Как можно здесь видеть, значение поля db name изменилось для всех объектов, в
том числе и для нового объекта postз.
Однако при изменении поля класса можно столкнуться с неочевидным поведением.
До сих пор мы видели, что доступ к полю
db _ name
через имя класса и экземпляр
класса приводил к одному и тому же результату, и можно было бы ожидать, что
если мы изменим значение поля
db_name
через экземпляр класса, например,
postl,
то изменения также затронут остальные экземпляры класса, однако это не так. По
смотрим на следующий пример (листинг
13 .15).
Глава
13.
Объектно-ориентированное программирование. Создание классов
255
from textpost import TextPost
postl
post2
#
TextPost ( "Толстой
Л. Н.
",
TextPost("Чexoв А.П.",
"Очень длинный текст
"Текст покороче
... ")
... ")
Значение c!Ь_name изменяется через экземпляр класса
postl.dЬ_name
=
"other_dЬ_name"
print(f"(TextPost.dЬ_name=}")
print(f"(postl.c!Ь_name=}")
print (f" (post2 .c!Ь_name=} ")
postЗ
=
ТехtРоst("Пелевин В.О.",
"Текста нет.
Ничего нет.")
Результат будет выглядеть по-другому:
Пост
сохранен в
базу данных
Пост сохранен в базу данных
posts.
posts.
TextPost.dЬ_name='posts'
postl.dЬ_name='other_dЬ_name'
post2.c!Ь_name='posts'
Пост
В
сохранен
в
базу данных
рассматриваемом
posts.
случае
мы
не
изменили
значение
поля
db _ name
класса
TextPost, а создали новое поле с таким же именем в объекте postl. И теперь, когда
мы используем доступ к полю db_name через экземпляр класса postl, то получаем
значение именно поля объекта. Таким образом, выходит, что теперь у объекта есть
два одноименных поля: одно
-
поле класса, а другое
-
поле объекта, и при ис
пользовании объекта для доступа к этому полю мы получим именно значение из
поля объекта. Но мы по-прежнему можем получить доступ к полю класса через имя
класса. Поэтому вызов метода
_save () для переменной postl по-прежнему в каче
стве имени базы данных выведет "posts". Однако, если бы мы в методе save ()
для доступа к полю db_ name использовали self. db_name, а не TextPost. db_name, то
тогда значение было бы взято из экземпляра класса, а не из самого класса.
Методы класса
Следующим логичным шагом для сокрытия данных была бы реализация желания
как-то ограничить доступ пользователям класса к полю db _ name. Для этого пере
именуем поле в
(листинг
db name и добавим метод для получения значения этого поля
13.16).
class TextPost:
'""'Класс текстового поста для блога"""
Часть
256
dЬ
def
def
Основные подходы
name = "posts"
init (self, author, text):
self. author
author
self. text = text
self. date = datetime.now(timezone.utc)
self ._save ()
save(self):
print(f"Пocт сохранен в базу данных
def
11.
{TextFost._dЬ_name}.")
qet_dЬ_name(self):
return TextFost.
dЬ
name
Остальные свойства класса тextPost остаются неизменными и для краткости не
приводятся. Указанные изменения отчасти решают задачу, но есть одна проблема
теперь для получения имени базы данных с помощью метода get _ db _ name ()
-
тре
буется предварительно создать какой-нибудь экземпляр класса тextPost, даже если
он нам не требуется (листинг
Листинг
13.17).
13.17. Chapter_13/example_10/main.py
from textpost import TextPost
postl = ТехtРоst("Толстой Л.Н.", "Очень длинный текст ... ")
post2 = TextPost ( "Чехов А. П. ", "Текст покороче ... ")
print(f"{postl.get dЬ name()=)")
Раньше у нас была возможность получить это значение без создания объекта, об
ращаясь к полю непосредственно через класс тextPost. Для решения этой пробле
мы предназначены методы класса
(class method).
Под методом класса понимается
метод, который привязан непосредственно к классу, а не к экземпляру класса,
аналогии с полем класса
ратор
-
по
db name. Для создания метода класса используется деко
@classmethod.
Изменим предыдущую версию класса тextPost так, чтобы метод
стал методом класса (листинг
Листинг
13.18).
13.18. Chapter_13/example_11/textpost.py
class TextPost:
"""Класс текстового поста для блага"""
dЬ
def
name
"posts"
init (self, author, text):
self. author
author
get db _ name ()
Глава
13.
Объектно-ориентированное программирование. Создание классов
257
self. text = text
self._date = datetime.now(timezone.utc)
self. _ save ()
def
save(self):
print(f"Пocт сохранен в базу данных
{TextPost._dЬ_name).")
@classmethod
def get_dЬ_name(cls):
return cls. dЬ name
Обратите внимание: помимо того, что метод get _ db _ name () был помечен декорато
ром
@classmethod,
у него изменено имя первого параметра,
указан параметр cls (сокращение от
class).
-
вместо
self
теперь
Это сделано с тем, чтобы подчеркнуть,
что такие методы в качестве первого параметра принимают не экземпляр класса, а
сам класс. В результате метод get _ db _ name () обращается к полю
имя класса тextPost, а через переменную
_ db _ name
не через
cls.
В скрипте main .ру теперь можно получить имя базы данных, не создавая экземпляр
класса (листинг
13.19).
from textpost import TextPost
print (f" {TextPost. get_ dЬ_ name () =) ")
Python до версии 3.11 можно было совместить декораторы @classmethod и
@property и таким образом создать свойства, привязанные к классу, а не к экземп
В
ляру класса. Начиная с
Python 3.11, такой
способ стал считаться устаревшим.
Статические методы
Мы уже знакомы с двумя видами методов: методами объектов, в качестве первого
неявного параметра получающими экземпляр класса
self,
и методами класса, ко
торые в качестве первого неявного параметра получают сам класс cls. Однако в
Python
есть еще статические методы, которые не получают в качестве неявного
параметра ни того, ни другого, а только те параметры, которые в метод были явно
переданы. Часто методы объявляют статическими, чтобы подчеркнуть, что этим
методам не требуется доступ к полям или методам экземпляра класса, внутри кото
рого статические методы расположены. Их можно воспринимать как независимые
функции, для удобства помещенные внутрь класса.
В качестве примера (листинг
13.20)
добавим в класс тextPost статический метод
prepare _ text (), который очищает текст поста от начальных и концевых пробелов,
а также делает первую букву текста заглавной. Мы применим этот метод в свойстве
сеттере text и в конструкторе класса для предварительной обработки текста. Что-
258
Часть
11.
Основные подходы
бы объявить функцию внутри класса статическим методом, ее нужно пометить де
коратором
Листинг
@staticmethod.
13.20. Chapter_13/example_12/textpost.py
class TextPost:
"""Класс текстового поста для блага"""
def
(self, author, text):
iпit
self. author = author
self._text = Textpost.prepare_text(text)
self. date = datetime.now(timezone.utc)
self. _ save ()
@static:method
def prepare_text(text):
result = text. strip ()
return result.capitalize()
@property
def text(self): return self. text
@text.setter
def text(self, value):
self. text = Textpost.prepare_text(value)
self. _save ()
Обратите внимание, что в статический метод не передаются параметры
self или
cls. Изнутри класса статический метод вызывается точно так же, как и метод клас
са с указанием имени класса. При этом вместо записи TextPost. prepare _ text (),
self .prepare_text (), если в методе, от
можно использовать вызов через self куда вызывается статический метод, параметр self доступен.
Статический метод вне класса тextPost также может быть вызван через имя класса
или через созданный объект (такой способ вызова обычно не рекомендуется). Что
бы убедиться, что статический метод prepare _ text () работает, изменим скрипт
main.py
(листинг
Листинг
13.21).
13.21 . Chapter_13/example_12/main.py
from textpost import TextPost
post = ТехtРоst("Толстой
print(f"{post.text=}"}
Л.Н.",
text = TextPost.prepare text("
print (f" {text=} ")
"
очень длинный текст
...
ещё более длинный текст
")
...
")
13. Объектно-ориентированное программирование. Соэдание классов
Глава
259
Результат выполнения этого скрипта будет выглядеть следующим образом:
Пост сохранен
в
базу данных.
... '
... '
post.text='Oчeнь длинный текст
tехt='Ещё более длинный текст
Заключение
В этой главе мы начали изучать особенности объектно-ориентированного програм
мирования в
Python,
и после обсуждения идеологии объектно-ориентированного
программирования научились создавать классы и добавлять в них поля и методы.
Конструктор класса
-
это метод с именем
_
i ni t _
( ) , который вызывается после
выделения памяти под созданный объект и предназначен для инициализации полей
объекта класса. Обычные методы объекта, как и конструктор, всегда принимают в
качестве первого аргумента ссылку на объект, для которого метод был вызван. Этот
параметр принято называть self. При вызове методов его задавать не требуется, но
его нужно указывать при объявлении методов.
Мы обсудили вопрос скрытия данных. На уровне языка у программиста нет воз
можности запретить доступ к полям или методам класса, чтобы их нельзя было ис
пользовать вне класса, но по договоренности поля и методы, начинающиеся с сим
вола
«_»,
подразумевают, что вне класса доступ к ним осуществляться не будет,
хотя интерпретатор гарантировать этого не может.
Для более компактного использования геттеров и сеттеров
-
методов, которые
предназначены для получения и изменения внутренних полей объектов класса
-
можно задействовать свойства, тогда вызов таких методов внешне будет выглядеть
как доступ к полям объекта класса.
Внутри класса можно объявить так называемые поля класса, эти поля привязаны к
классу, а не к конкретному экземпляру класса, и их значения являются общими для
всех экземпляров класса. Для доступа к таким полям не обязательно создавать эк
земпляры класса, доступ к ним можно осуществить через имя класса.
Аналогично полям класса можно создавать методы класса с помощью декоратора
@classmethod. Методы класса также не привязаны к конкретным объектам, а явля
ются общими для всех объектов данного класса. В отличие от обычных методов,
методы
класса
в
качестве
обычно обозначают cls, -
первого
параметра
принимают
переменную,
которую
это ссылка на сам класс, а не на его экземпляр (как
self).
Наконец, в классах с помощью декоратора @staticmethod могут быть созданы
статические методы. Статическим методам в качестве первого неявного парамет
ра не передают ни экземпляр класса
self,
ни сам класс
cls.
Темы, рассмотренные в этой главе, относятся к такому аспекту объектно-ориенти
ованного программирования как инкапсуляция, то есть включение сущностей внутрь
класса. В следующей главе мы поговорим про оставшиеся аспекты объектно
ориентированного программирования: наследование и полиморфизм.
- ГЛАВА
14 -
Объектно-ориентированное
программирование.
Наследование и полиморфизм
Что такое наследование классов?
В этой главе мы изучим следующий аспект объектно-ориентированного програм
мирования
классы
-
-
наследование. Благодаря наследованию мы можем создавать базовые
их, как отмечалось в предыдущей главе, еще называют родительскими
классами, или суперклассами
(superclasses), -
которые будут включать в себя
функциональность, общую для других классов. Затем на основе базовых классов
мы можем создавать производные класс
subclasses), -
-
дочерние классы,
или подклассы,
не реализуя повторно то, что уже сделано в базовых классах. Во мно
гих библиотеках и приложениях с помощью наследования образуются обширные
графы связей между классами. Это происходит постепенно, когда в процессе раз
работки создаются новые дочерние классы, в которых, благодаря наследованию,
нужно реализовать только тот минимум функциональности, которая требуется для
решения конкретной задачи.
В отличие от многих других языков программирования,
Python
поддерживает
множественное наследование, то есть порождение классов от двух и более базовых
классов. Это считается спорным приемом, которого по возможности следует избе
гать. Оборотная сторона сложного графа наследования классов заключается в том,
что в такой системе может быть трудно найти конкретный класс, в котором реали
зована та или иная возможность, а также могут существовать трудности с понима
нием порядка вызовов методов базовых классов из производных.
Наследование классов
Вернемся к примеру из предыдущей главы про посты для блога. У нас был класс
тextPost
для создания поста с текстом. Добавим в новую версию класса метод
format (), предназначенный для оформления поста в виде текстовой строки (лис-
Глава
тинг
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
14.1).
261
Пусть этот метод будет использоваться при отображении поста в ново
стной ленте пользователей нашей платформы блогов.
from datetime import datetime, timezone
class TextPost:
"""Класс текстового поста дпя блога"""
def
def
init (self, author, text):
self. author = author
self. text = text
self. date = datetime.now(timezone.utc)
self ._save ()
save(self):
рrint("Текстовый пост сохранен.")
@property
def author(self): return self. author
@property
def text(self): return self. text
@text.setter
def text(self, value):
self. text = value
self. save ()
@property
def date(self): return self. date
def format(self):
Автор: (self._author)
return f"""
Дата: {self._date:%d.%m.%Y %H:%M:%S)
{self._text)"""
Обратите внимание, как в методе format {) формируются данные о дате и времени
в f-строке. С помощью такой строки форматирования дата представляется в сле
дующем виде:
день.месяц.год
часы:минуты:секунды
Более подробно о представлении даты и времени в виде строки можно прочитать на
странице документации 1 .
1 См.
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior.
Часть
262
11.
Основные подходы
Сделаем еще один похожий класс, который предназначим для создания поста с
картинкой вместо текста, назовем его ImagePost и поместим в файл imagepost. ру
(листинг
Листинг
14.2).
14.2. Chapter_14/example_01/imagepost.py
from datetime import datetime, timezone
class ImagePost:
'"' "Класс поста
def
def
с картинкой для блага"""
init (self, author, image):
self. author = author
self._image = image
self. date = datetime.now(timezone.utc)
self. _ save ()
save (self) :
print("Пocт с картинкой сохранен.")
@property
def author(self): return self. author
@property
def image(self): return self._image
@image.setter
def image(self, value):
self._image
value
self. _ save ()
@property
def date(self): return self. date
def format(self):
Автор: {self._author)
return f
Дата: {self._date:%d.%m.%Y %H:%M:%S)
Картинка: {sel f. _ image} """
11 ' 11 '
Напишем скрипт, который будет создавать экземпляры этих классов и выводить
информацию о созданных постах (листинг
Листинг
14.3).
14.3. Chapter_14/example_01/main.py
from textpost import TextPost
from imagepost import ImagePost
postl
post2
ТехtРоst("Толстой Л.Н.",
ImagePost ("Малевич
К.С.",
"Очень
длинный текст
"Ыасk_ square.
jpg")
... ")
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
263
print ()
print(postl.format())
print ()
print(post2.format())
Результат выполнения этого скрипта выглядит следующим образом:
Текстовый пост сохранен.
Пост
с картинкой сохранен.
Автор:
Дата:
Толстой Л.Н.
08.05.2025 07:30:25
Очень длинный текст
Автор:
Дата:
...
Малевич К.С.
08.05.2025 07:30:25
Картинка:
Ыack_square.jpg
Для более наглядного представления классов и их взаимосвязей используется гра
фический язык
UML (Unified Modeling Language -
унифицированный язык моде
лирования). С помощью этого языка можно визуализировать стандартным образом
множество аспектов, связанных с архитектурой приложения и его поведением, но
мы из всего многообразия диаграмм
воспользуемся только диаграммами
UML
классов.
Классы тextPost и ImagePost в нотации
рис.
UML
можно изобразить, как показано на
14.1:
TextPost
Рис.
lmagePost
_author: str
_text: str
_date: datetime
_author: str
_image: str
_date: datetime
_save(): None
author(): str
date(): datetime
text(): str
text(str): None
format(): str
_save(): None
author(): str
date(): datetime
image(): str
image(str): None
format(): str
14.1.
UМL-диаграммы классов
TextPost
и
ImagePost
В верхней секции каждого прямоугольника, представляющего класс, записывается
имя класса, ниже
-
поля класса, еще ниже
-
методы класса. После двоеточия ука
зываются тип поля или тип возвращаемого методом значения.
Как можно видеть из кода, а еще лучше это заметно на UМL-диаграммах, классы
тextPost и
ImagePost
мало чем отличаются друг от друга,
-
в них присутствует
большая повторяемость кода, что является плохим признаком. Избавиться от дуб-
Часть
264
11.
Основные подходы
лирования нам поможет наследование. Но прежде чем писать код, нарисуем новую
UМL-диаграмму связей классов, которую мы затем реализуем (рис.
14.2).
BasePost
_author: str
_date: datetime
author(): str
date(): datetime
~
Рис.
14.2.
~
1
1
TextPost
lmagePost
_text: str
_image: str
_save()
text(): str
text(str): None
format(): str
_save()
image(): str
image(str): None
format(): str
UМL-диаграммы классов
TextPost
и
ImagePost
с общим базовым классом
В UМL-диаграммах наследование изображается в виде не закрашенной треуголь
ной стрелки от дочернего класса в сторону базового. Мы создадим родительский
класс
BasePost, куда поместим всё, что связано с автором и датой поста. А затем
классы TextPost и ImagePost объявим производными от BaseClass. В результате
наследования всё, что было в классе BasePost, окажется доступным в производных
классах.
Реализуем показанную на рис.
14.2
иерархию классов на
Python.
Сначала класс BasePost поместим в новый файл basepost.py (листинг
Листинг
14.4. Chapter_14/example_02/basepost.py
from datetime import datetime, timezone
class BasePost:
' 1 ""Базовь1J.1 класс для постов блога 111111
def
init
(self, author):
print("БasePost._init_()
")
self. author = author
self. date = datetime.now(timezone.utc)
@property
def author(self): return self. author
@property
def date(self): return self. date
14.4).
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
265
Конструктор базового класса принимает только один параметр, в отличие от клас
сов
TextPost
и
ImagePost.
В конструктор класса вasePost здесь добавлена строка,
выводящая в консоль информацию о том, что в текущий момент вызывается этот
самый конструктор.
Теперь сделаем так, чтобы класс тextPost стал производным от
тинг
BasePost
(лис
14.5).
Листинг
14.5. Chapter_14/example_02/textpost.py
from basepost import BasePost
class
TextPost(БasePost):
"""Класс текстового поста для блога"""
def
init (self, author, text):
print("TextPost. init ()")
super() ._init_(author)
self. text = text
self ._save ()
def save (self) :
рrint("Текстовый пост
сохранен.")
@property
def text(self): return self. text
@text.setter
def text(self, value):
self. text value
self. _save ()
def format(self):
Автор: {self._author}
{self._date:%d.%m.%Y %H:%M:%S}
return f" 1111
Дата:
{self._text}"""
Чтобы объявить класс производным от другого класса, нужно после имени созда
ваемого класса в скобках указать родительский класс. Второй интересный момент
связан
ini t
с
вызовом
( J.
конструктора.
Конструктор
-
это
просто
метод
с
именем
Этот метод был объявлен в базовом классе, а затем такой же метод
объявлен (переопределен) в производном классе. Получается, что внутри класса
тextPost существуют два метода
_ ini t _ (). Когда будет создаваться экземпляр
_init_ () именно из этого производного
класса тextPost, будет вызван метод
класса. Но нам нужно также вызвать конструктор базового класса, чтобы произошла
инициализация полей, описанная в методе
ini t
()
класса
BasePost.
По умол-
266
Часть
11.
Основные подходы
чанию этот метод вызываться не будет. Но с помощью функции super () мы можем
получить ссылку на объект базового класса (строго говоря, создается специальный
прокси-объект, через который можно получить доступ к полям и методам базового
класса) и вызвать метод
_
i ni t _
( ) , относящийся к этому базовому классу, пере
дав ему все необходимые параметры. Именно это мы в коде листинга
14.5
и сдела
ли.
Существует альтернативный способ вызвать функцию из базового класса:
def
init
(self, author, text):
BasePost.
init
(self, author)
В этом случае вместо использования функции super () для получения ссылки на
базовый класс мы явно вызываем метод класса вasePost. Обратите внимание, что
тогда в метод
_init_ () нужно явно передать объект self.
Такие приемы для вызова методов из базового класса работают не только для вызо
ва конструктора, но и с любыми методами, переопределяемыми в производном
классе. Если же нужно вызвать метод из базового класса, который не переопреде
лялся в производном, то использовать функцию super () не требуется,
-
в этом
случае метод базового класса можно вызывать как обычный метод через объект
self.
Аналогично изменим класс
Листинг
ImagePost (листинг 14.6).
14.6. Chapter_14/example_02/imagepost.py
from basepost import BasePost
class ImagePost(8asePost):
"""Класс
def
поста с картинкой для блога"""
(self, author, image):
init ()")
super() ._init_(author)
self._image = image
self._save()
iпit
priпt("ImagePost.
def
save (self) :
print("Пocт с картинкой сохранен.")
@property
def image(self): return self._image
@image.setter
def image(self, value):
self._image = value
self._save()
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
267
def format(self):
return f"""
Автор: {self._author)
Дата: (self._date:%d.%m.%Y %H:%M:%S)
Картинка: (self._image)"""
Никаких принципиальных отличий от класса тextPost здесь нет. Скрипт main.py в
изменениях не нуждается, создание экземпляров классов тextPost и
ImagePost
ос
талось прежним. В классах постов теперь выводится информация о том, что вызы
вается тот или иной конструктор, поэтому при выполнении этого скрипта в консоль
будет выведен следующий текст:
TextPost.
вasePost.
init
init
()
()
Текстовый пост сохранен.
ImagePos t.
вasePost.
Пост
с
ini t ()
ini t ()
картинкой сохранен.
Автор:
Дата:
Толстой Л.Н.
09.05.2025 08:04:19
Очень длинный текст
Автор:
Дата:
Малевич
...
К.С.
09.05.2025 08:04:19
Картинка:
Ыack_square.jpg
Выделенные полужирным шрифтом строки показывают, что действительно сначала
вызывается конструктор создаваемого класса, а из него уже
-
конструктор базово
го класса.
Абстрактные базовые классы
Внимательно посмотрев на запускаемый скрипт main.py, мы увидим, что в нем
предполагается
наличие метода format () у обоих классов и тextPost, и
ImagePost. В полноценном сервисе блогов при формировании ленты новостей
вполне может возникнуть ситуация, когда объекты постов разных типов хранятся в
одной коллекции,
-
они перебираются в цикле, и у них вызываются одни и те же
методы, и при этом не важно, к какому именно типу поста относится очередной
объект. Воспроизведем подобную ситуацию в нашем примере (листинг
from textpost import TextPost
from imagepost import ImagePost
feed ; []
feed.append(TextPost("Toлcтoй Л.Н.",
feed. append (ImagePost ("Малевич
К. С.",
... "))
Jpg"))
"Очень длинный текст
"Ыасk _ square.
14.7).
268
Часть
11.
Основные подходы
for post in feed:
print ()
print(post.format())
Представим себе ситуацию, когда в результате ошибки в классе rmagePost пропа
дет метод format () или его забудут реализовать. Проблема будет усугубляться тем,
что узнаем мы о ней только тогда, когда в цикле дело дойдет до вызова этого мето
да у объекта класса ImagePost, -
TextPost.
BasePost.
init
init
тогда в консоль будет выведен следующий текст:
()
()
Текстовый пост сохранен.
TextPost.
BasePost.
init
init
()
()
Пост с картинкой сохранен.
Автор:
Дата:
Толстой Л.Н.
09.05.2025 08:16:43
Очень длинный текст
...
Traceback (most recent call last):
File " ... /chapter_14/example 03/main.py", line 11, in <module>
print(post.format())
AttributeError: 'ImagePost' object has no attribute 'format'
Интерпретатор достаточно четко дает понять, в чем заключается ошибка, когда вы
полнение кода дойдет до вызова метода
format (), но это уже слишком поздно.
Кроме того, мы не застрахованы от случая, когда где-то в коде программист решит
создать экземпляр класса вasePost, который по логике никогда не должен созда
ваться, поскольку он служит лишь вспомогательным классом для производных от
него классов:
from basepost import BasePost
foo = BasePost ("Аноним")
Хотелось бы подобные ошибки обнаруживать не во время попытки вывода резуль
тата для пользователя, а в момент создания экземпляра сломанного класса. И такая
возможность есть благодаря абстрактным базовым классам.
Класс называется абстрактным, если он содержит методы, которые необходимо
переопределить в производных классах. Такие методы тоже называют абстракт
ными. При этом запрещено создавать экземпляры абстрактных классов. Если про
изводный класс переопределяет не все абстрактные методы, он тоже считается аб
страктным, и поэтому экземпляры такого класса также нельзя создавать.
Чтобы решить нашу проблему, достаточно изменить класс вasePost, объявив его абст
рактным и добавив в него абстрактный метод format (). Сделаем это (листинг
14.8).
Глава
14.
Листинг
Объектно-ориентированное программирование. Наследование и полиморфизм
269
14.8. Chapter_14/example_04/basepost.py
fran аЬс import АВСМеtа, aЬstract:method
from datetime import datetime, timezone
class BasePost (metaclass=AВCМeta) :
" 11
"Базовый
def
класс для постов
блага 1111
"
init (self, author):
print ("BasePost. ini t () ")
self. author = author
self. date = datetime.now(timezone.utc)
@property
def author(self): return self. author
@property
def date(self): return self. date
@aЬstract:method
def format(self):
В этом коде появились сразу несколько новых для нас элементов. Для начала мы
импортировали из стандартного модуля аьс (сокращение от
класс
ABCMeta
и декоратор
Abstract Base Class)
@abstractmethod.
Для того, чтобы объявить класс абстрактным, нужно указать, что его метаклассом
является класс ABCMeta, который мы только что импортировали. В этой книге мы не
станем углубляться в такие тонкости как метаклассы и метапрограммирование,
скажем лишь, что метакласс
-
это класс, который создает другие классы. Созда
ние абстрактных базовых классов
-
это единственное место, где мы встретимся с
метаклассами в рамках этой книги.
Затем мы объявили метод format () и пометили его декоратором @abstractmethod,
что сделало этот метод абстрактным.
14.8 есть еще одна новая для нас деталь. В метод format () мы по
« ... ». Здесь это многоточие не обозначает, что мы что-то не напе
экономии места. В Python многоточие - это полноценный объект класса
В коде листинга
местили объект
чатали для
ellipsis, в чем можно убедиться, например, в интерактивном режиме:
>» type ( ... )
<class 'ellipsis'>
У этого объекта нет единого назначения, и разные библиотеки его используют по
своему. Например, он нам еще встретится в главе
про
NumPy,
18
про указание типов и в главе
25
где он используется при индексации массивов.
В нашем случае объект
« ... »
используется в качестве заполнителя. При создании
метода мы не можем оставить тело метода пустым и должны туда что-то написать.
270
Часть
11.
Основные подходы
Что именно мы напишем в абстрактный метод, не имеет значения, поскольку тело
абстрактного метода никогда не будет выполняться,
его переопределят в произ
-
водных классах. Написание такого тела функции не имеет смысла, и мы могли бы
точно так же поставить упоминание любого другого объекта:
@abstractmethod
def format(self):
42
Так что объект
« ... »
придает телу функции осмысление визуально, но не с точки
зрения интерпретатора.
Часто в такой ситуации в метод помещают инструкцию pass, с которой до этого мы
также не встречались. Эта конструкция буквально ничего не делает
-
она тоже
используется как заполнитель или при отладке. В этом случае абстрактный метод
мог бы выглядеть так:
@abstractmethod
def format( se lf):
pass
Некоторые инструменты для проверки корректности кода ругаются, даже если
абстрактный метод возвращает объект не того типа, который будет возвращаться из
переопределенного метода. Тогда мы должны были бы вернуть какую-нибудь
строку
логично было бы возвращать пустую строку:
-
@abstractmethod
def format(self):
return " 11
Здесь нет каких-либо рекомендаций, в примерах РЕР
8
используется вариант с
pass, но в целом то, что именно писать в абстрактном методе, остается на усмотре
ние разработчика.
Итак, мы объявили класс вasePost абстрактным. Посмотрим, как это скажется на
результате выполнения скрипта
main.py
при условии, что метод
format ()
реализо
ван в обоих производных классах. На классах тextPost и ImagePost внесенные из
менения не скажутся никак, поскольку в них и так уже определен (а теперь переоп
ределен) метод format (), и они по-прежнему будут создаваться и работать, как и
раньше.
Однако теперь, если мы попытаемся создать экземпляр класса BasePost, то полу
чим исключение (листинг
Листинг
14.9).
14.9. Chapter_14/example_04/main.py
from textpost import TextPost
from imagepost import ImagePost
from basepost import BasePost
feed = []
feed.append(TextPost("Toл c тoй Л.Н.",
"Очень длинный
текст
... "))
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
feed. append (ImagePost ( "Малевич
К. С.",
"Ыасk _ square.
271
j pg") )
feed.append(BasePost("Aнoним"))
for post in feed:
print ()
print(post.format())
Результатом выполнения этого кода будет следующий вывод в консоль:
TextPost.
BasePost.
init
init
()
()
Текстовый гост сохранен.
ImagePost. init ()
BasePost. init ()
Пост
с картинкой сохранен.
Traceback (most recent call last):
File " ... main.py", line 9, in <module>
feed. append (BasePost ("Аноним") )
~~~~~~~~
лллллллллл
TypeError: Can't instantiate abstract class BasePost without an implementation for
abstract method 'format'
Здесь мы видим очень подробное описание ошибки, говорящее о том, что невоз
можно создать экземпляр абстрактного класса вasePost, так как в нем нет реализа
ции абстрактного метода
format ().
Аналогичная ошибка будет получена при попытке создать экземпляр производного
класса, если в нем мы забудем реализовать метод format (). Попробуем, например,
не реализовать этот метод в классе ImagePost (листинг
Листинг
14.10. Chapter_14/example_0S/imagepost.py
from basepost import BasePost
class ImagePost(BasePost):
"""Класс поста с картинкой для блога"""
def
def
init (self, author, image):
print ("TextPost. init () ")
super(). init (author)
self._image = image
self. _ save ()
save(self):
print("Пocт с картинкой сохранен.")
@property
def image (self) : return self. _ image
14.10).
Часть
272
11.
Основные подходы
@image.setter
def image(self, value):
self._image
value
self. save ()
При попытке создать экземпляр этого класса результат мы получим следующий:
>>> from imagepost import ImagePost
>>> post = ImagePost ("Малевич К.С.",
Traceback (most recent call last):
File "<python-input- ... >", line 1,
post = ImagePost("Maлeвич К.С.",
"Ыасk_
iп
square. jpg")
<module>
"Ьlack_square.jpg")
~~~~~~~~~лллллллллллллллллллллллллллллллллллл
TypeError: Сап't instantiate abstract class ImagePost without
abstract method 'format'
ап
implementation for
Сообщение об ошибке говорит, что класс ImagePost по-прежнему абстрактный, так
как в нем не определен метод forma t () . И самое главное в том, что эта ошибка воз
никает именно в момент создания класса с ошибкой, а не где-то дальше, где ее бу
дет сложнее отлаживать.
Переопределять методы в производных классах можно не только для абстрактных
классов и не только абстрактные методы. Например, если нас не устраивает формат
результата, возвращаемого методом
format ()
в классе
ImagePost,
и мы, например,
хотим, чтобы этот метод возвращал НТМL-код для форматирования поста, то мо
жем создать класс
HtmlimagePost, производный от ImagePost. Создадим такой
класс и поместим его в файл htmlimagepost.py (листинг
Листинr
14.11).
14.11. Chapter_14/example_06/htmlimagepost.py
from imagepost import ImagePost
class HtmlimagePost(ImagePost):
def format(self):
Автор: <b>{self._author}</b><br/>
return f 11 " "
Дата: <i>{self._date:%d.%m.%Y %H:%M:%S}</i><br/>
Картинка: <img src="{self._image}"/> 111111
Класс HtmlimagePost используется так же, как и ImagePost (листинг
Листинr
14.12. Chapter_14/example_06/main.py
from textpost import TextPost
fran htmlimagepost import HtmlimagePost
feed = []
feed.append(TextPost("Toлcтoй Л.Н.",
feed.append (HtmlimagePost("мaлeвич
"Очень длинный текст
К.С.",
"Ьlack_square.
... "))
jpg"))
14.12).
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
273
for post in feed:
print ()
print(post.format())
В классе Html ImagePost мы не объявляли конструктор, и поэтому при создании эк
земпляра
этого
класса
будет
вызываться
конструктор
родительского
класса
ImagePost. Если бы в родительском классе не было бы подходящего конструктора,
то
Python
проверил бы, есть ли у родительского класса свой родительский класс, и
есть ли в нем подходящий конструктор, и так далее, пока дело 1;1е дошло бы до са
мого верхнего предка в иерархии классов.
Точно так же дело обстоит и с другими методами
-
их поиск осуществляется, на
чиная с того экземпляра класса, который непосредственно был создан, и если у не
го нет вызываемого метода, интерпретатор начнет подниматься вверх по иерархии
классов и искать нужный метод в родительских классах, пока его, наконец, не най
дет. Если же метод найден не будет, то будет вызвано исключение AttributeError,
говорящее о том, что у проблемного класса нет вызываемого метода.
В больших программах обычно не рекомендуют делать производные классы от не
абстрактных классов, поскольку это может усложнить понимание порядка вызовов
методов классов, а также затруднит поиск потенциальных ошибок.
Что такое полиморфизм?
Термин «полиморфизм» можно перевести как «множественность форм». В про
граммировании под этим термином понимают такую особенность работы с объек
тами, когда функции или какому-то блоку кода не важен конкретный класс объек
та, с которым он работает,
функциональность. В
Python
главное, чтобы этот объект предоставлял нужную
полиморфизм получается автоматически, благодаря
«утиной» типизации. В предыдущих примерах мы с ним сталкивались, хотя не
упоминали этот термин, когда, например, создавали список объектов постов, а за
тем в цикле перебирали эти объекты и вызывали у них метод format (), ожидая, что
этот метод будет реализован во всех объектах из списка.
В отличие от языков со статической типизацией, в
Python
не требуется выстраивать
иерархию классов, чтобы убедить компилятор, что требуемый метод действительно
существует в каждом классе.
Множественное наследование
Множественное наследование
-
это создание класса, производного от нескольких
родительских классов. Это еще одна возможность, которая существует в
Python,
но
пользоваться ей нужно крайне аккуратно и только при необходимости. Множест
венное наследование может приводить к усложнению и
запутыванию кода, и по
этому некоторые языки программирования его запрещают. Но мы все равно рас
смотрим эту возможность, чтобы иметь представление о том, что это такое, и какие
могут быть сложности, с ним связанные.
274
Часть
К этому моменту у нас есть классы: тextPost
rmagePost -
-
11.
Основные подходы
для текстового поста в блоге и
для поста с картинкой. Допустим, теперь нам понадобилось сделать
новый тип поста, который должен содержать и текст, и картинку. Самое первое
решение, которое приходит на ум
rmagePost.
сделать класс, производный от тextPost и
-
В этом случае иерархия классов будет выглядеть, как показано на рис.
14.3.
Обратите внимание: для написания имен абстрактных классов и абстрактных мето
дов на UМL-диаграммах используют курсивный шрифт.
BasePost
- author: str
-
date: datetime
author(): str
date(): datetime
format(): str
_save()
~
1
1
1
TextPost
lmagePost
_text: str
_image: str
text(): str
text(str): None
format(): str
_save()
image(): str
image(str): None
format(): str
_save()
1
~
TextlmagePost
format(): str
_save()
Рис
14.3.
Ромбовидное наследование
Иерархия из этих четырех классов образовала ромб, поэтому такую структуру на
зывают ромбовидным наследованием (в англоязычной литературе используется
термин
«diamond inheritance» ),
и ее желательно по возможности избегать. В такого
рода иерархии классов очень много тонкостей, связанных с порядком вызовов ме
тодов и конструкторов. Часть проблем связаны с тем, как быть с общим базовым
классом
(в
ImagePost, -
нашем
случае
вasePost),
являющимся
родителем
для
тextPost
и
нужно ли вызвать конструктор этого класса дважды при вызове кон
структоров классов
TextPost
и
ImagePost?
Если в классах тextPost и
реализован один и тот же метод (в нашем случае
format
ImagePost
()),и если бы его не было в
производном классе, из какого базового класса его вызвать?
Глава
14.
Python
Объектно-ориентированное программирование. Наследование и полиморфизм
275
довольно аккуратно решает такие вопросы, но на текущем этапе изучения
языка нам пока не стоит углубляться в эти особенности. Лучше изменим архитек
туру классов, чтобы избежать ромбовидного наследования.
Заметим, что в классах TextPost и ImagePost есть поля и методы, обязательные для
постов
любого
BasePost, -
вида,
-
эти
поля
им
достались
от
родительского
класса
а есть поля и методы, предназначенные для хранения и редактирова
ния данных определенного типа (текста или картинки),
эти поля и методы объ
-
явлены непосредственно в классах тextPost и ImagePost. Но если мы эти поля и
методы вынесем в отдельные маленькие базовые классы, то получим возможность
создавать классы постов с любой комбинацией данных, и в будущем сможем доба
вить новые базовые классы, которые позволят создать посты, например, еще и с
прикрепленными музыкальными файлами или видеороликами. Такие маленькие
классы, предназначенные для добавления какой-либо небольшой функционально
сти в производные классы, иногда называют мuксuнамu
(mixins,
что можно при
мерно перевести как «примесь»). Изменим нашу иерархию классов таким образом,
чтобы часть полей и методов вынести в миксины, назовем которые TextMixin и
ImageMixin. Такая иерархия классов показана на рис.
14.4.
BasePost
-
TextMixin
author: str
date: datetime
lmageMixin
_text: str
author(): str
date(): datetime
_image: str
text(): str
text(str): None
_save()
format(): str
_save()
image(): str
image(str): None
_save()
~
г3
lmagePost
format(): str
_save()
Рис.
14.4.
~
~
TextPost
format(): str
_save()
Множественное наследование с использованием миксинов
В миксины вынесены поля и методы, которые могут отличаться у постов с разным
содержанием. Кроме того, эти классы будут тоже абстрактными и станут требовать
от производных классов наличия метода
_ save (), -
того же, что требует класс
BasePost. Этот метод используется в миксинах, чтобы сохранять данные после их
изменения с помощью свойств. Предлагаемое решение можно считать спорным, но
в рассматриваемом случае оно нам подходит.
Поместим классы миксинов в отдельный файл mixins.py (листинг
14. 13).
Часть
276
Листинг
from
аЬс
11.
Основные подходы
14.13. Chapter_14/example_07/mixins.py
import
АВСМеtа,
abstractmethod
class TextMixin(metaclass=AВCMeta):
def
init (self, text):
print ( "TextMixin. ini t () ")
self. text = text
@abstractmethod
def save(self):
@property
def text(self): return self. text
@text.setter
def text(self, value):
self. text = value
self. _ save ()
class ImageMixin(metaclass=AВCMeta):
def
init (self, image):
print ( "ImageMixin. ini t () ")
self. image = image
@abstractmethod
def save (self):
@property
def image(self): return self._image
@image.setter
def image(self, value):
self._image ~ value
self. _save ()
В этих классах для нас нет ничего нового
-
мы перенесли в них поля и методы из
классов постов. Конструкторы у них очень простые, в них происходит только вы
вод текста в консоль и сохранение данных, переданных в качестве параметра.
Теперь классы постов будут выглядеть следующим образом (листинги
Листинг
14.14. Chapter_14/example_07/textpost.py
from basepost import BasePost
from mixins import TextMixin
14.14
и
14.15).
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
class
TextPost(БasePost,
277
Text:Мixin):
"""Класс текстового поста дпя блога"""
def
def
init (self, author, text):
print("TextPost. init () ")
вasePost.
init (self, author)
init (self, text)
Text:Мixin.
self. _ save ()
save (self) :
рrint("Текстовый пост
сохранен.")
def format(self):
return f"""
Автор: (self._author)
Дата: (self._date:%d.%m.%Y %H:%M:%S)
(self. text)"""
from basepost import BasePost
from mixins import ImageMixin
class
ImagePost(БasePost,
ImageМixin):
"""Класс поста с картинкой для блога 111111
def
init (self, author, image):
("ImagePost. init () ")
вasePost. _ ini t_ (self, author)
ImaqeМixin._init_(self, imaqe)
self. _ save ()
pпnt
def
save (self):
print("Пocт
с
картинкой сохранен.")
def format(self):
return f"""
Автор: (self._author)
Дата: {self._date:%d.%m.%Y %H:%M:%S)
Картинка: ( self. _ image) """
Обратите здесь, во-первых, внимание на создание класса, у которого два родитель
ских класса. Они указываются после имени создаваемого класса в скобках через
запятую. Во-вторых, и это тут самое важное, посмотрите на вызов конструкторов
родительских классов. Мы вызываем конструкторы базовых классов по очереди,
при этом делаем это не с помощью функции super (), а явно указывая класс, из ко
торого мы вызываем метод_ ini t _
(). При таком способе вызова мы должны явно
Часть
278
11.
Основные подходы
передать в него объект self. Почему нам не подходит функция super ()? Поскольку
у нас теперь несколько базовых классов, то super () вернет ссылку только на пер
вый из них.
С точки зрения скрипта main.py, который использует классы тextPost и ImagePost,
ничего не поменялось, но мы должны убедиться, в каком порядке вызываются конст
рукторы, а также метод_ save
Листинг
(). Немного сократим скрипт main.py (листинг 14.16).
14.16. Chapter_14/example_07/main.py
from textpost import TextPost
from imagepost import ImagePost
postl
post2
TextPost ( "Толстой Л. Н. ",
ImagePost ( "Малевич К. С.",
"Очень дпинный текст
"Ьlack_ square.
... ")
jpg")
Результатом вызова этого скрипта будет следующий текст, выведенный в консоль:
TextPost. init ()
BasePost. init ()
TextMixin. init ()
Текстовый пост
сохранен.
ImagePost. init ()
BasePost. init ()
ImageMixin. init ()
Пост с картинкой сохранен.
BasePost
TextMixin
-
text: str
text(): str
text(str): None
_save()
_ author: str
_date: datetime
lmageMixin
author(): str
date(): datetime
_image: str
format(): str
_save()
image(): str
image(str): None
_save()
~
1
1
1
1
TextlmagePost
format(): str
_save()
Рис.
14.5.
UМL-диаграмма класса
TextimagePost
и его родителей
Никаких неожиданностей здесь нет. Сначала вызывается конструктор того или
иного класса поста, а внутри этого конструктора уже последовательно вызываются
Глава
Объектно-ориентированное программирование. Наследование и полиморфизм
14.
279
конструкторы класса вasePost, а затем того или иного миксина. Внутри конструк
торов миксинов вызывается метод
_save ().
Наконец, проделав всю эту работу, мы можем реализовать то, ради чего всё это за
думывалось,
-
сделать класс поста, который будет включать в себя и текст, и кар
тинку. Диаграмма наследования в этом случае будет выглядеть, как показано на
рис.
14.5.
Класс
TextimagePost имеет трех родителей: класс BasePost и два миксина, которые
мы создали до этого. Реализовать класс тextimagePost теперь достаточно просто.
Поместим его в файл textimagepost.py (листинг
Листинг
14.17).
14.17. chapter_14/example_08/textimagepost.py
from basepost import BasePost
from mixins import TextMixin, ImageMixin
class
TextimagePost(БasePost,
Text:Мi.xin,
ImaqeМixin):
'"'"Класс текстового поста для блога"""
def
def
init (self, author, text, image):
print("TextimagePost. init ()")
BasePost. init (self, author)
TextMixin. init (self, text)
ImageMixin. init (self, image)
self. save ()
save (self) :
print("Пocт с текстом и картинкой сохранен.")
def format(self):
Автор: {self._author)
return f"""
Дата: {self._date:%d.%m.%Y %H:%M:%S)
Картинка: {self._image)
{self._text)"""
В этом классе нет ничего нового, но в его конструкторе вызываются конструкторы
трех родительских классов. Использование этого класса мало отличается от ис
пользования классов тextPost и
ImagePost, только теперь в конструктор необхо
14.18).
димо передавать три параметра: автора, текст и картинку (листинг
Листинг
14.18. Chapter_14/example_08/maln.py
from textimagepost import TextimagePost
post = TextimagePost ("Стэн
print(post.format())
Ли",
"Описание комикса",
"spiderman. jpg")
280
Часть
11.
Основные подходы
Запустив этот скрипт, мы увидим, что из конструктора класса тextimagePost вызы
ваются конструкторы всех его родительских классов:
TextimagePost. init ()
BasePost. init ()
TextMixin. init ()
ImageMixin. init ()
Пост с текстом и картинкой сохранен.
Автор:
Стэн Ли
09.05.2025 13:01:09
Картинка: spiderman.jpg
Дата:
Описание
комикса
Функции для определения
родительских отношений классов. Класс
object
Иногда в программах с разветвленной системой классов требуется определить, к
какому именно классу относится объект. Например, когда у нас есть список постов
из предыдущих примеров, и нам нужно показать пользователю только посты с кар
тинками.
Для дальнейших экспериментов построим небольшую иерархию классов, кото
рые
-
для краткости
на рис.
не будут делать ничего полезного. Эта иерархия показана
-
14.6.
Foo
Spam
Рис.
14.6.
Иерархия классов для демонстрации способов
определения родительских отношений
Не станем мы также помещать эти классы в отдельные модули, и весь код для на
шего примера поместим в один скрипт main.py (листинг
Листинг
14.19. Chapter_14/example_09/main.py
class Foo:
class Bar (Foo) :
14.19).
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
281
class Baz:
class Spam(Bar, Baz):
spam = Spam ( )
Эти классы будут пустыми и иметь конструктор по умолчанию. В качестве запол
нителя тела классов используются объекты ellipsis. После объявления классов в
скрипте создается объект spam класса Spam. С этим объектом мы будем дальше ра
ботать.
Мы уже знаем, что для того, чтобы определить класс объекта, можно воспользо
ваться встроенной функцией t уре ( ) :
print (type (spam))
В результате выполнения этой команды в консоль будет выведена следующая строка:
<class '
main
.Spam'>
Однако с тем же успехом мы можем воспользоваться полем
class
,
которое
содержится в любом экземпляре класса:
print(spam.
class
Результат выполнения будет точно таким же. При этом часто нужно не только по
лучить класс объекта, но и определить, относится ли объект к тому или иному
классу. Мы могли бы сделать это следующим образом:
print(type(spam)==Spam)
В результате будет выведено булево значение тrue. Однако обычно нас интересует
более общий вопрос: является ли объект экземпляром указанного класса или класса,
производного
от него.
Именно
на этот вопрос отвечает встроенная
функция
isinstance (), которая принимает два параметра: объект и класс. Функция
isinstance () вернет True, если объект является экземпляром указанного класса
или класса, производного от указанного. Поэтому все три следующие команды вы
ведут значение
True:
print(isinstance(spam, Spam))
print(isinstance(spam, Foo))
print(isinstance(spam, Baz))
Кроме того, в
Python
есть еще встроенная функция issubclass (), которая работает
подобным образом, но в качестве первого параметра принимает не экземпляр класса,
а сам класс. Функция issubclass () возвращает тrue, если класс, переданный в ка
честве первого параметра, является производным от класса, переданного в качестве
второго параметра. Следующие команды выведут в консоль значение тrue:
print(issubclass(Spam, Foo))
print(issubclass(Spam, Bar))
print(issubclass(Spam, Baz))
282
Часть
А вот эта команда выведет
prinё(issubclass(Bar,
11.
Основные подходы
False:
Baz))
При этом стоит учесть, что если в функцию issubclass () в качестве обоих пара
метров передать один и тот же класс, то issubclass () возвращает True, т. к. любой
класс считается наследником самого себя. Поэтому следующая команда также вы
ведет значение тrue:
print(issubclass(Spam, Spam))
И в завершение главы скажем, что в
Python
все классы являются производными от
встроенного класса object. В старых версиях
Python
(начиная с версии
2.2)
при со
здании классов даже требовалось указывать наследование от этого объекта явным
образом (хотя до версии
3.0
также оставалась возможность не наследоваться от
класса object, но этот факт сейчас имеет лишь исторический интерес).
По этой причине следующие команды также выведут значение тrue:
print(isinstance(spam, object))
print(issubclass(Foo, object))
Класс object имеет конструктор по умолчанию, поэтому можно сделать экземпляр
этого класса:
>» foo = object()
>» type (foo)
<class 'object'>
Заключение
В этой главе мы изучили наследование классов. Благодаря механизму наследования
появляется возможность выносить общую функциональность нескольких классов в
базовый (родительский) класс, а затем порождать от него новые производные (или
дочерние) классы, реализуя в них остальную необходимую функциональность.
Когда создаются производные классы, то часто необходимо из их конструктора вы
зывать конструктор базового класса. Это можно сделать, используя встроенную
функцию super (), которая возвращает прокси-объект, через который можно полу
чить доступ к методам и полям базового класса. Кроме того, методы базового клас
са можно вызвать непосредственно через имя этого класса.
Чтобы показать, что не следует создавать экземпляры определенного класса, а так
же указать на необходимость переопределения методов в нем, используются абст
рактные базовые классы. Чтобы создать абстрактный класс, при его объявлении
необходимо объявить, что его .wетаклассом является класс ABCMeta из стандартно
го модуля аЬс. При этом методы, которые необходимо переопределить в производ
ных классах, помечаются декоратором
мо импортировать из модуля аЬс.
@abstractmethod, который также необходи
Глава
14.
Объектно-ориентированное программирование. Наследование и полиморфизм
283
В этой главе мы также затронули еще один аспект объектно-ориентированного
программирования
-
полиморфизм, который подразумевает, что объекты разных
классов можно использовать одинаковым образом, передавать их как параметры в
одни и те же функции или применять к ним одни и те же инструкции. Полимор
физм в
Python
получается естественным образом благодаря «утиной» типизации.
Python
поддерживает множественное наследование, то есть производный класс
может иметь несколько родительских классов. Это мощный инструмент, но он мо
жет заметно усложнить код. В некоторых ситуациях множественное наследование
удобно использовать при создании классов, производных от нескольких маленьких
классов, каждый из которых добавляет небольшую функциональность.
Для определения того, является ли объект экземпляром определенного класса или
его
потомка, предназначена встроенная функция
isinstance (). А с помощью
встроенной функции issubclass () можно определить, является ли один класс про
изводным (возможно, через одно или несколько поколений) от другого класса. При
этом считается, что каждый класс является производным от самого себя.
Все объекты в
Python
неявным образом восходят к общему родительскому классу
-
obj ect.
К этому моменту мы рассмотрели все три аспекта объектно-ориентированного про
граммирования: инкапсуляцию, наследование и полиморфизм. В следующей главе
мы более подробно поговорим о еще одном проявлении полиморфизма
-
пере
грузке операторов и использовании «магических» методов, которые позволяют до
биваться от объектов различных классов похожего поведения.
- ГЛАВА 15-
«МаГИЧеСКИе» методы классов
и перегрузка операторов
«Магические» методы классов
В предыдущих главах мы уже вскользь упоминали о «магических» методах, кото
рые еще называют dundеr-методами
кивания),
-
черкивания:
(от
англ. douЫe
underscores,
двойные подчер
методах, которые начинаются и оканчиваются двумя символами под
«_».
Конструктор классов
_
ini t _
() тоже относится к таким мето
дам. Dunder-мeтoды, как правило, добавляют новую возможность, помогающую
повысить удобство использования объекта. Например, после реализации метода
() к объек'Ьу можно будет применять оператор «н>, а если в классе реали
_add _
зован метод _sub_ (), -
то оператор
«-».
Если нужно, чтобы к объекту можно
было применять оператор квадратных скобок, то следует реализовать один или оба
метода:
_getitem_() -
для получения данных и
_setitem_() -
ния данных внутри класса. Использование «магических» методов
проявление полиморфизма в
15. l
В табл.
-
для измене
это еще одно
Python.
приведены некоторые «магические» методы с кратким пояснением то
го, какую функциональность они добавляют.
Таблица
«Магический»
15.1. Некоторые «магические» методы
Назначение
метод
Общие
-
init-
()
Конструктор класса
-
call -
()
Делает объект вызываемым, т. е. позволяет применять круглые скобки
hash
()
Позволяет сделать объект хешируемым или не хешируемым (см. главу
-
-
len-
-
()
Позволяет применять к объекту функцию len 1)
6)
Глава
«Магические» методы классов и перегрузка операторов
15.
285
Таблица
15.1
(продолжение)
Назначение
«Магический»
метод
Общие
-
-
getitem-
()
Позволяет применять квадратные скобки для получения значения
setitem
()
Позволяет применять квадратные скобки для изменения значения
Преобразование объектов
str -
-
Возвращает строковое представление объекта при использовании функ-
()
ЦИИ
repr
-
Возвращает строковое представление объекта при использовании функrepr (). Иногда это более подробный формат, используемый для от-
()
-
str ()
ции
ладки, по сравнению с
-
bool -
-
int float
-
str -
()
Позволяет преобразовывать объект в булево значение
()
Позволяет преобразовывать объект в целое число
()
Позволяет преобразовывать объект в дробное число
()
-
-
Математические операторы
add
.,
()
Позволяет применять к объекту оператор суммирования
sub
()
Позволяет применять к объекту оператор вычитания
-
mul -
()
Позволяет применять к объекту оператор умножения«*»
-
truediv-
-
floordiv
-
-
-
-
mod
-
()
()
«-»
Позволяет применять к объекту оператор деления«/»
Позволяет применять к объекту оператор целочисленного деления
«/ /»
Позволяет применять к объекту оператор вычисления остатка от деления
()
matmul -
«+»
()
Позволяет применять к объекту оператор матричного умножения«@»
_neq- ()
Позволяет применять к объекту оператор «унарный минус»
_pow-
Позволяет применять к объекту оператор возведения в степень«**»
()
-
lshift - ()
Позволяет применять к объекту оператор сдвига влево««»
-
rshift-
Позволяет применять к объекту оператор сдвига вправо«»»
-
abs -
()
()
Позволяет применять к объекту функцию
abs ()
«%»
286
Часть
11.
Основные подходы
Таблица
«Магический»
15.1
(окончание)
Назначение
метод
Операторы сравнения
-
lt-
()
Позволяет применять к объекту оператор<«»
-
le-
()
Позволяет применять к объекту оператор
-
gt-
()
Позволяет применять к объекту оператор«>»
_ge-
()
Позволяет применять к объекту оператор
«>=»
_eq_ ()
Позволяет применять к объекту оператор
«==»
Позволяет применять к объекту оператор
« ! =»
Позволяет применять к объекту оператор
in
-
ne-
-
contains -
()
()
<«=»
Примеры перегрузки операторов
Для демонстрации использования «магических» методов и перегрузки операторов
создадим класс вектора на плоскости. При этом вектор будет определяться как на
правленный отрезок, поэтому для его создания потребуется задать координаты то
чек начала и конца в декартовой системе координат. В дальнейших примерах этой
главы мы будем иметь дело с двумя файлами: vector.py -
представляющим собой
модуль
запускаемым скриптом,
vector
и содержащим класс
использующим класс
Vector2D,
и
main.py-
Vector2D.
Начнем с очень простого определения класса vector2D (листинг
15.1).
class Vector2D:
"""Класс
def
вектора на
плоскости"""
init (self, xl, yl,
self.pl
(xl, yl)
self.p2 = (х2, у2)
х2,
у2):
Пока единственное, что делает этот класс,
-
сохраняет точки начала и конца век
тора в виде двух кортежей. Для сокращения места мы не станем задумываться об
инкапсуляции полей класса, пусть они остаются открытыми. Постепенно этот класс
начнет обрастать «магическими» методами, и мы будем наблюдать, как они сказы
ваются на поведении класса.
Теперь напишем запускаемый
тинг
15.2).
скрипт
main.py, использующий этот класс (лис
Глава
15.
Листинг
«Магические» методы классов и перегрузка операторов
287
15.2. Chapter_15/example_01/main.py
frorn vector irnport Vector2D
= Vector2D(xl=l, yl=l,
CD = Vector2D(xl=2, yl=l,
АВ
х2=3,
у2=2)
х2=3,
у2=5)
print ("АВ: ", АВ)
print("CD:", CD)
Результат выполнения этого скрипта будет выглядеть примерно так:
<vector.Vector2D object at Ox7efed3b36030>
CD: <vector.Vector2D object at Ox7efed3b36060>
АВ:
Такое строковое представление векторов нам мало о чем говорит, хотелось бы сде
лать, чтобы класс vector2D автоматически преобразовывался в строку, когда это
Согласно
требуется.
данным
табл.
15 .1,
для
этого
нужно
реализовать
метод
_str_ (). Давайте сделаем это (листинг 15.3).
Листинг
15.3. Chapter_ 15/example_02/vector.py
class Vector2D:
вектора на плоскости"""
"""Класс
def
init (self, xl, yl,
(xl, yl)
self. pl
self.p2 = (х2, у2)
х2,
у2):
def _str_(self):
return f"{self.pl} -> {self.p2}"
После такого добавления тот же самый скрипт
main.py
будет выводить следующий
текст:
АВ:
(1, 1)
CD:
(2,
1)
->
->
(3, 2)
(3,
5)
Это намного понятнее для пользователя.
Теперь мы постепенно начнем добавлять функциональность, связанную с матема
_ abs _ () - чтобы можно было применять
abs () для объектов класса Vector2D и получать длину векто
тикой. Для начала реализуем метод
встроенную функцию
ра (листинг
15.4).
1Листинг 15.4. Chapter;-;1~/example_OЗ/vect?.~·PY
frorn math import hypot
class Vector2D:
"""Класс вектора на пло скос ти"""
288
Часть
def
init (self, xl, yl,
self.pl
(xl, yl)
self.p2 = (х2, у2)
def
str (self):
return f"(s,lf.pl) -> (self.p2)"
х2,
11.
Основные подходы
у2):
~
l
@property
def coord(self):
return (self.p2[0] - self.pl[O], self.p2[1] - self.pl[l])
def
аЬs
(self):
return hypot (*self. coord)
В этой версии файла
vector.py мы
добавили свойство coord, возвращающее кортеж с
координатами вектора. Вспомним, что координатами вектора называются коорди
наты точки, равные разности координат конца вектора и его начала. В геометриче
ской интерпретации координаты вектора
-
это координаты конца вектора при ус
ловии, что его начало совпадает с началом системы координат. В дальнейшем нам
неоднократно понадобятся координаты вектора в такой интерпретации. В нашей
версии класса
Vetor2D
мы используем координаты вектора для расчета его длины,
которая по теореме Пифагора равна корню квадратному из суммы квадратов его
координат. В методе _ abs _ () для расчета длины вектора используется функция
hypot () из модуля math. Эта функция предназначена для расчета евклидовой нор
мы, или длины вектора с заданными координатами. Для сокращения записи при
передаче параметров в функцию hypot ()
используется распаковка позиционных
параметров, о которой мы говорили в главе
11.
Изменим скрипт
тинг
main.py,
чтобы в нем использовался расчет модулей векторов (лис
15.5).
Листинг
15.5. Chapter_15/example_03/main.py
from vector import Vector2D
АВ = Vector2D(l, 1, 2, 2)
CD = Vector2D(2, 1, 3, 5)
print("AВ:",
АВ)
print("CD:", CD)
print (" IАВ 1: ", abs (АВ))
print (" ICDI: ", abs (CD))
В результате выполнения этого скрипта в консоль будет выведено:
(1, 1) -> (2, 2)
CD: (2, 1) -> (3, 5)
IAВI: 1.4142135623730951
ICDI: 4.123105625617661
АВ:
Глава
15.
«Магические» методы классов и перегрузка операторов
Следующее, что нам надо сделать,
-
289
это организовать проверку векторов на ра
венство. Вектора называются равными, если они имеют одинаковые длины и сона
правлены. По-другому можно сказать, что вектора равны, если они имеют одинако
вые координаты.
Если, не меняя класс vector2D, попытаться их сравнить в скрипте
сделано в листинге
15.6,
main.py,
как это
то все проверки вернут False, поскольку будут сравни
ваться ссылки на объекты, а в этом скрипте содержатся четыре разных объекта.
Листинг
15.6. Chapter_15/example_04fmain.py
from vector import Vector2D
АВ = Vector2D(l, 1, 2, 2)
CD = Vector2D(l, 1, 2, 2)
EF = Vector2D(3.0, 3.0, 4.0, 4.0)
GH = Vector2D(l, 2, 3, 4)
print("AВ --
CD: ",
АВ
--
print("AВ --
EF: ",
GH:",
АВ
--
print("AВ - -
АВ - -
CD)
EF)
GH)
Результат будет выглядеть следующим образом:
АВ
АВ
АВ
== CD: False
== EF: False
== GH: False
Чтобы для класса Vector2D перегрузить оператор
(листинг
Листинг
«==»,
реализуем метод_ eq_ ()
15.7).
15.7. Chapter_15/example_04fvector.py
from math import hypot, isclose
class Vector2D:
def _eq_(self, other):
if not isinstance(other, Vector2D):
return False
self coord = self.coord
other coord = other.coord
return (isclose(self_coord[O], other_coord[O]) and
isclose(self_coord[l], other_coord[l]))
Многоточие« ... » в этом коде не является объектом ellipsis (см. главу
лишь обозначает, что в листинге
код класса Vector2D (см. листинг
15.7 для
15.4).
14),
а всего
экономии места не приведен остальной
Часть
290
Метод
eq
()
11. Основные подходы
в качестве своего параметра принимает экземпляр класса, с кото
рым сравнивается наш объект. В самом начале этот метод проверяет, является ли
переданный объект экземпляром класса vector2D (или производным от него), и ес
ли происходит сравнение с каким-то другим типом, то сразу возвращается значение
False.
Если выполняется сравнение объектов класса vector2D, то сравниваются их коор
динаты. При этом используется функция isclose (), предназначенная для сравне
ния вещественных чисел с учетом возможной погрешности (см. главу 2У. Если ко
ординаты равны (или разница между ними мала), то метод вернет тrue, иначе
-
False.
В этом методе координаты объектов сохраняются в отдельных переменных, чтобы
не приходилось для каждого объекта дважды вычислять координаты, вызывая
СВОЙСТВО
coord.
После добавления такой реализации метода
сия скрипта
main.py
(см. листинг
15.6)
_eq_ () в vector2D предыдущая вер
будет выводить в консоль следующий ре
зультат:
АВ
АВ
АВ
== CD: True
== EF: True
== GH: False
Мы уже близки к цели, осталось добавить еще несколько методов, которые позво
лят применять к векторам операторы
«+»
и
«-».
Чтобы сложить два вектора, к ко
ординатам конца первого вектора нужно добавить координаты второго вектора.
При вычитании двух векторов координаты второго вектора требуется вычитать.
Для
перегрузки
операторов
«+»
Листинr
«-»
15.8).
и
_sub_() соответственно (листинг
надо реализовать
методы
_ add_ ()
и
15.8. Chapter_15/example_05/vector.py
class Vector2D:
def
def
1
add (self, other):
delta = other.coord
х2 = self.p2[0] + delta[O]
у2 = self.p2[1] + delta[l]
return Vector2D(self.pl[O], self.pl[l],
х2,
у2)
sub (self, other):
delta = other.coord
х2 = self.p2[0] - delta[O]
у2 = self.p2[1] - delta[l]
return Vector2D(self.pl[O], self.pl[l],
х2,
у2)
При использовании функции isclose () с параметрами по умолчанию относительная ошибка не должна
превышать 10-9 _
Глава
15. «Магические» методы классов и перегрузка операторов
Так же, как и метод
_ eq_ (),
методы
add
() и
291
() принимают объект, с
sub
которым производятся математические операции. Эти методы возвращают новый
объект Vector2D. В приведенной здесь реализации также можно было бы добавить
проверку того, что объект other относится к классу vector2D.
Мы это не сделали
по двум причинам. Первая заключается в том, что в случае ошибки следовало бы
возбуждать исключение ТуреЕттоr, но изучать исключения мы будем только в главе
С другой стороны, мы понадеемся на «утиную» типизацию
-
19.
если у переданного
объекта также будет свойство coord, то наша реализация метода сможет работать с
другими типами объектов.
Скрипт main.py, который покажет, что операторы
нять к объектам Vector2D, приведен в листинге
Листинге
«+»
15.9.
и
«-»
теперь можно приме
15.9. Chapter_15/example_05/maln.py
from vector import Vector2D
= Vector2D(l, 1, 3, 2)
CD = Vector2D(2, 2, 4, 4)
АВ
print("AВ
print ("АВ
+ CD =",
CD =",
-
АВ
+ CD)
CD)
АВ -
В результате выполнения этого скрипта в консоли будет выведено:
АВ
АВ
+ CD = (1, 1) -> (5, 4)
- CD = (1, 1) -> (1, 0)
Работает! А что у нас с операторами«+=» и«-=»? Достаточно ли перегрузить методы
_add_() и _sub_(), чтобы они заработали? Давайте проверим, и напишем для
этого следующий вариант скрипта main.py (листинг
Листинг
15.10).
15.10. Chapter_15/example_06/maln.py
from vector import Vector2D
= Vector2D(l, 1, 2, 2)
CD = Vector2D(2, 2, 4, 4)
АВ
print("AВ:",
АВ
АВ)
+= CD
print("AВ
+= CD")
print("AВ:",
АВ)
В результате выполнения этого скрипта в консоль будет выведен текст:
АВ:
АВ
АВ:
(1, 1)
-> (2, 2)
+= CD
(1, 1) -> (4, 4)
Часть
292
11.
Основные подходы
Вроде бы, всё хорошо. Но тут есть один нюанс, который, в зависимости от наших
ожиданий, станет либо проблемой, либо предпочтительным поведением. В теку
щий момент инструкция АВ
+=
CD воспринимается как АВ
=
АВ
+
CD, но мы знаем,
что оператор«+» создает новый объект класса vector2D. Получается, что после ин
струкции
«+=»
переменная АВ будет содержать ссылку на другой объект, а не на
тот, на который он ссылался до применения этого оператора. Давайте это проверим
(листинг
Листинг
15.11).
15.11. Chapter_15/example_06/main-2.py
from vector import Vector2D
АВ
= Vector2D(l, 1, 2, 2)
CD = Vector2D(2, 2, 4, 4)
print ( "АВ: ",
АВ)
print (f" {id(AВ) =} ")
АВ
+=
CD
print("AВ
+=
print("AВ:",
CD")
АВ)
print (f" {id(AВ) =} ")
Здесь мы добавили в код вывод идентификатора объекта АВ до и после применения
инструкции«+=». И вот что у нас получилось:
АВ:
(1, 1)
->
(2, 2)
id(AВ)=l40712376230176
АВ
АВ:
+=
CD
(1, 1)
->
(4, 4)
id(AВ)=140712376232576
Действительно, переменная АВ до и после применения инструкции
«+=»
ссылается
на два разных объекта. Если вы создаете класс объектов, которые не должны изме
няться (как кортеж), то это оправданное поведение. В остальных случаях, скорее
всего, было бы желательно, чтобы после инструкций
«+=», «-=»
и им подобных
исходный объект оставался бы тем же самым. Для этого нам нужно реализовать в
создаваемом классе методы
_
iadd _
()
и
_
isub _
()
или их аналоги для прочих
инструкций с присваиванием. Эти методы в качестве параметра принимают другой
объект (предполагаем, что также класса vector2D, но в реальном коде это надо про
верять), только внутри этих методов должно происходить изменение внутреннего
состояния исходного объекта self, и эти методы, как правило, возвращают self.
Добавим реализацию методов
тинг
15.12).
iadd
()
И
isub
()
в класс Vector2D (лис-
Глава
15.
Листинr
«Магические» методы классов и перегрузка операторов
293
15.12. Chapter_15/example_07/vector.py
class Vector2D:
def
iadd (self, other):
delta = other.coord
х2 = self.p2[0] + delta[O]
у2 = self.p2[1] + delta[l]
self.p2 = (х2, у2)
return self
def
isub (self, other):
delta = other.coord
х2 = self.p2[0] - delta[O]
у2 = self.p2[1] - delta[l]
self.p2 = (х2, у2)
return self
В этих методах мы добавляем координаты переданного вектора к координатам то
чек конца вектора self или вычитаем их из него, а затем возвращаем self. Если
теперь запустить предыдущий скрипт main.py ( см. листинг
15 .11 ),
то мы получим
следующий результат:
АВ:
(1, 1)
-> (2, 2)
id(AВ)=l40595246096672
АВ
+= CD
АВ:
(1, 1)
-> (4, 4)
id(AВ)=l40595246096672
Как можно видеть, теперь идентификатор объекта Ав не меняется.
Чтобы показать еще одну особенность, связанную с перегрузкой операторов, доба
вим возможность умножать вектор на число (скаляр). Для перегрузки оператора
«*»
нужно реализовать метод_mul _
() . Поскольку мы уже знаем про особенности
работы инструкции вида«*=», сразу реализуем еще и метод _imul_ () и добавим
оба метода в новую версию класса Vector2D (листинг
Лмстинr
15.13. Chapter_15/example_08/vector.py
class Vector2D:
def
def
mul (self, n):
coord = self.coord
х2 = self.pl[O] + n * coord[O]
у2 = self.pl[l] + n * coord[l]
return Vector2D(self.pl[O], self.pl[l],
imul (self, n):
coord = self.coord
х2,
у2)
15.13).
Часть
294
11.
Основные подходы
= self.pl[O] + n * coord[O]
= self.pl[l) + n * coord[l)
self.p2 = (х2, у2)
return self
х2
у2
При умножении вектора на число n на это число умножаются координаты векто
ра
чтобы длина вектора стала в n раз больше. Наши векторы имеют еще точку
-
начала, и эта точка при умножении не меняется. Проверим, как работает оператор
«*»
с новой версией класса
но в листинге
Vector2D, для чего изменим скрипт main.py, как показа
15.14.
f Ли~~инге 15.14. Chapter_15/example_08/main.py
from vector import Vector2D
АВ
= Vector2D(l, 1, 2, 2)
CD = АВ * 2
print ("СО:", CD)
print(f"(id(AB)=}")
*= 3
АВ
print("AВ
*= 3")
print("AВ:",
АВ)
print (f" {id (АВ) =} ")
В результате выполнения этого скрипта мы получим:
CD:
(1,
1)
-> (3, 3)
id(AВ)=l40439410925856
АВ
АВ:
*= 3
(1, 1)
-> (4, 4)
id(AВ)=l40439410925856
На первый взгляд, всё хорошо: умножение происходит верно, при использовании
оператора«*=» исходный объект остается тем же самым. Но мы не учли один мо
мент. Согласно положениям математики, мы можем писать не только CD
но и CD
тинг
= 2 *
15.15).
Листинг
АВ. Попробуем это сделать в новом варианте скрипта
15.15. Chapter_15/example_09/maln.py
from vector import Vector2D
АВ
= Vector2D(l, 1, 2, 2)
CD = АВ * 2
print("CD:", CD)
= АВ * 2,
main.py (лис
Глава
15.
295
«Магические» методы классов и перегрузка операторов
ЕF=2*АВ
print("EF:", EF)
Если мы запустим этот скрипт, то в консоль будет выведен следующий текст:
(1, 1) -> (3, 3)
Traceback (most recent call last):
File " ... /main.py", line 8, in <module>
EF = 2 * АВ
CD:
TypeError: unsupported operand type(s) for *: 'int' and 'Vector2D'
Действительно, в стандартном классе
int нет реализации метода _mul_ (), кото
рый бы работал с нашим классом vector2D, - откуда ему о нем знать? Но эта про
блема легко решается реализацией в классе vector2D метода _rmul_ (), который
вызывается в случае, если объект этого класса расположен справа от оператора«*»,
и в качестве параметра принимает объект, который расположен слева от оператора
«*».
Предположим сейчас, что этот объект- число, и дополним класс Vector2D
реализацией метода _rmul_ () (листинг
15.16).
1Листинг 15.16. Chapter_15/example_10/vector.py
class Vector2D:
def
def
mul (self, n) :
coord = self.coord
х2 = self.pl[O] + n * coord[O]
у2 = self.pl[l] + n * coord[l]
return Vector2D(self.pl[O], self.pl[l],
х2,
у2)
rmul (self, n):
return self. mul (n)
Проверим работу предыдущей версии скрипта
main.py
(см. листинг
15.15)- в
кон
соль будет выведен следующий текст:
CD:
(1, 1)
-> (3, 3)
EF: (1, 1) -> (3, 3)
Теперь
всё
работает.
Аналогичные
методы
есть
и
для
других
операторов:
_radd_ (), _rsub_ () и т. д., но в рассмотренных нами ранее примерах необхо
димости в них не было, поскольку мы предполагали, что операторы«+» и«-» будут
вызываться только с объектами vector2D.
И в завершение перегрузим еще один оператор
-
«@>>,
предназначенный для пере
множения матриц. Этим оператором мы воспользуемся для вычисления скалярного
произведения векторов. Для его перегрузки нужно реализовать метод
(сокращение от
раметра
matrix multiplication,
принимает
другой
объект
_matmul_ ()
умножение матриц), который в качестве па
(продолжим
предполагать,
что
это
тоже
vector2D). С этим оператором не возникнет никаких неожиданностей, подобных
Часть
296
тем, с какими мы сталкивались ранее,
-
11.
Основные подходы
просто покажем, что такой оператор тоже
существует и может использоваться в математических вычислениях. Напомним,
что скалярное произведение двух векторов равно сумме произведений соответст
вующих
координат векторов,
matmul
Листинг
()
(листинг
и дополним
vector2D
класс
реализацией
метода
15.17).
15.17. Chapter_15/example_11/vector.py
class Vector2D:
def
matmul (self, other):
coord self = self.coord
coord other = other.coord
return (coord_self[O] * coord_other[O]
+ coord_self[l] * coord_other[l])
Проверим работу оператора«@» с классом
Листинг
Vector2D
(листинг
15.18).
15.18. Chapter_15/example_11/main.py
from vector import Vector2D
АВ
CD
=
=
Vector2D(l, 1, 3, 2)
Vector2D(2, 2, 4, 4)
print("AВ@
CD
=",
АВ@
CD)
Результатом работы этого скрипта будет следующий текст в консоли:
АВ@
CD
=
6
...
На этом мы завершаем знакомство с перегрузкой операторов.
Python
предоставляет
еще множество «магических» методов для разных случаев. Например, мы ничего
не сказали про методы
_ i ter _ ()
и
_ next _ (),
которые преобразуют объекты в
итераторы, чтобы их можно было бы использовать для обхода элементов в цикле
for. За рамками нашего
_ seti tem _ (), позволяющие
внимания
остались
методы
getitem
()
и
перегружать оператор «квадратные скобки» и многое
другое.
Заключение
Мы в этой главе сосредоточились на перегрузке операторов, которая работает через
реализацию так называемых dunder-мeтoдoв (или «магических» методов). Имена
таких методов начинаются и оканчиваются двумя символами подчеркивания.
В начале мы коротко описали некоторые из таких методов, а затем начали созда
вать класс
вектора на плоскости для демонстрации перегрузки операторов и осо
бенностей, с ними связанных.
Глава
15.
Сначала мы реализовали метод
тем
-
297
«Магические» методы классов и перегрузка операторов
метод
_
abs _
_
() , преобразующий объект в строку, за
st r_
(), позволяющий вычислять длину вектора с помощью встро
енной функции abs () . Перегрузили оператор
_ eq_ (). Перегрузили операторы
sub
()
«+»
и
«==» с помощью реализации метода
add
() и
«-», реализовав методы
соответственно.
Познакомились с особенностями работы инструкций присваивания
«+=», «-=»
и
подобных им, и для более аккуратной работы нашего класса с такими операторами
реализовали методы
iadd
()
и
isub
().
Рассмотрели особенности операторов, для которых важно, слева или справа от зна
ка
оператора
_ mul _
расположен
() и _ rmul _
экземпляр
нашего
класса,
и
реализовали
методы
() для умножения вектора на число. И в завершение перегру
зили оператор«@», используемый для расчета скалярного произведения векторов.
На этом мы заканчиваем изучение трех глав, посвященных основам объектно
ориентированного программирования в
Python.
В следующей главе мы поговорим
про установку сторонних библиотек и про особенности, связанные с этим процессом.
- ГЛАВА 16-
СторОННИе библиотеки и инструменты
для работы с ними
Установка пакетов с помощью
pip
Помимо модулей и пакетов из стандартной библиотеки, для
Python
создано огром
ное количество сторонних библиотек. Многие из них разрабатываются в виде про
ектов с открытым исходным кодом сообществами программистов или являются
результатом труда отдельных разработчиков. Чтобы, с одной стороны, программи
стам
на
Python
было
бы
легче
искать
нужную
библиотеку,
а
с
другой
-
разработчикам библиотек проще делиться своими разработками, было создано еди
ное хранилище библиотек-
PyPi (The Python Package Index, индекс
пакетов
Python) 1.
Любой желающий может загрузить туда свой Руthоn-пакет, если он оформлен тре
буемым образом. На момент подготовки этой книги на серверах
лее
670
PyPi
хранится бо
тыс. проектов (библиотек). С помощью поиска по этому сайту можно найти
нужный пакет, прочитать его подробное описание, посмотреть, насколько он попу
лярен, как часто обновляется, какие версии
другую информацию. На рис.
найти пакет по слову
16. l показана
«optimization».
Каждый пакет из базы
PyPi
Python
поддерживает, а также увидеть
страница сайта
pypi.org
при попытке
можно скачать в виде архива и попытаться его собрать
и установить самостоятельно, однако это не самый простой путь. Многие библио
теки требуют, чтобы были установлены другие библиотеки (так называемые зави
симости). Для установки библиотеки из исходных кодов нужно предварительно
установить зависимости, которые, в свою очередь могут иметь свои зависимости.
Это очень кропотливый процесс, особенно, если надо учитывать требования к вер
сии
Python
или другим установленным библиотекам. Поэтому были созданы раз
личные инструменты (и постоянно появляются новые), которые берут на себя ре
шение многих проблем, связанных с установкой библиотек.
В этой главе мы рассмотрим самый распространенный инструмент для установки
библиотек,
Python
1 См.
который устанавливается на компьютер вместе с
(см. в главе
https://pypi.org.
1 про
установку
Python) - pip.
интерпретатором
Глава
16.
Сторонние библиотеки и инструменты для работы с ними
-
Q
Помощь
Фильтр по классиФ.икатоР.У.
•
•
•
•
•
•
•
•
•
•
Спонсоры
Сортировать по
10 ООО+ проектов по 1апросу «optlmlzaUon•
299
Воити
Зарегистрироваться
Соответствию
Fram~ork
Toplc
bandit-optimization
Bandlt optlmlzatlon a\gorithms for mlcroscopy
23 мар. 2024 г.
bayesian-optimization
23 дек. 2024 г
Oevetopment Status
llcense
Bayeslan Optimlzation package
Programmlng Language
Ope"tlng System
Environment
cai-optimization
Causal AI Optlmization
18 мая 2023 г .
Oasis-Optimization
29 дек . 2024 г.
lntended Audience
Natural languagto
oasls 1s а Harmony ~arch optimlzation at.gorlthm .
Typlng
30 мюн . 2021
optimiiation-alr;orithms
lt is а Python llbrary that contains useful algorithms for severat c.omplex proЫems such as partitlonln~
floor ptanning, scheduling,
optimiiation-~apstone
Python package for Bradley Unlversity • Optimlzation Capstone
Рис.
16.1.
Результаты поиска по слову
Чтобы убедиться, что
pip
«optimization»
г.
19 окт . 2023 г.
в базе библиотек на сайте
PyPi
установлен, запустим консоль и выполним команду pip.
На экран должна быть выведена справка про использование этого инструмента:
> pip
Usage :
pip <command> [options]
Commands :
install
download
uninstall
freeze
inspect
list
show
check
config
search
cache
index
wheel
Install packages.
Download packages.
Uninstall packages.
Output installed package s in requirements format.
Inspect the python environment.
List installed packages.
Show information about i ns talled packages.
Verify installed package s have comp atiЫe dependencies.
Manage local and global conf igurati on.
Search PyPI for packages.
I nspect and manage pip's wheel cache.
Inspect information availaЫe fr om package indexes.
Build wheels from your requirements.
300
Часть
hash
completion
debug
help
11. Основные подходы
Compute hashes of package archives.
helper command used for coпrnand completion.
Show information useful for debugging.
Show help for commands.
А
Здесь, как можно видеть, приведены команды
pip,
и некоторыми из них мы будем
пользоваться.
Однако
не только как приложение, но и как исполняемый мо
ду ль
текущий момент рекомендуется запускать
pip устанавливается
Python. Более того, на
pip
именно
как модуль. Для этого введите в консоли команду:
> python -m pip
Результат выполнения этой команды будет точно таким же, как и предыдущей. Так
что в дальнейшем мы будем пользоваться именно такой формой для вызова
Под операционной системой
Windows
pip.
вместо вызова команды python можно ис
пользовать команду ру, о которой мы говорили в главе
1.
установлено несколько версий интерпретатора
можно указывать, для какой
именно версии
Например, если
Python вы
pip нужно
Python,
В этом случае, если у вас
хотите установить, обновить или удалить библиотеку.
запустить для работы с
Python 3.13,
то команда в консо
ли должна выглядеть так:
>
ру
-3.13 -m pip
Прежде чем мы начнем устанавливать пакеты, нужно упомянуть некоторые осо
бенности их установки. Пакеты могут устанавливаться глобально
-
чтобы они бы
ли доступны для всех пользователей операционной системы, но в этом случае для
их установки требуются права администратора (при работе под
ка
pip
Windows для
запус
нужно использовать консоль с правами администратора). Поэтому обычно,
если нет каких-либо особых требований, лучше устанавливать библиотеки для те
кущего пользователя,
то есть без использования прав администратора. Однако
-
если у вас в операционной системе зарегистрировано несколько пользователей, то
каждому из них придется устанавливать требуемые ему библиотеки самостоятельно.
Для установки пакета из индекса
PyPi с
помощью
pip предназначена
команда:
pip install
В простейшем случае эта команда выглядит следующим образом:
> python -m pip install
имя_пакета_l имя_пакета_2
За один запуск команды
... имя_пакета_N
pip install можно установить произвольное количество
пакетов. Если мы запустим эту команду в
Windows,
то
pip
самостоятельно опреде
лит наличие или отсутствие прав администратора (с какими правами была запуще
на консоль) и, в зависимости от этого, установит пакеты глобально для всех поль
зователей или только для текущего пользователя. Однако, если мы собираемся ус
тановить
пакеты
только
для
текущего
пользователя,
лучше
это
указать
явно,
добавив к команде pip install параметр --user (обратите внимание на два симво
ла«-»):
> python -m pip install --user
имя_пакета_l имя_пакета_2
...
имя_пакета_N
Глава
16.
Сторонние библиотеки и инструменты для работы с ними
301
Итак, давайте установим библиотеки, которые нам понадобятся для работы в сле
дующих главах:
♦
NumPy-
математическая библиотека, предоставляющая очень удобные средст
ва для работы с массивами, и математические функции;
♦
Matplotlib -
♦
SciPy -
библиотека для построения различных видов графиков;
библиотека для научных вычислений с большим количеством специа-
лизированных функций;
♦
Pandas -
библиотека для работы с табличными данными.
Для этого выполним команду:
> python -m pip install --user numpy matplotlib scipy pandas
Если у нас до этого не были установлены эти библиотеки, то
их с сайта
PyPi
pip
начнет скачивать
и устанавливать. Как мы уже говорили, у каждой библиотеки могут
быть зависимости
-
другие библиотеки, которые требуются для ее работы. Такие
библиотеки также будут скачиваться и устанавливаться автоматически.
Pip
выво
дит лог своей работы, и если в процессе установки не возникнет никаких ошибок, в
консоли мы увидим примерно следующий текст (приводится с сокращениями):
Downloading numpy-2.2.5-cp313-cp313-win_amd64.whl (12.6 МВ)
---------------------------------------- 12.6/12.6 МВ 12.3 МВ/s eta 0:00:00
Downloading matplotlib-3.10.3-cp313-cp313-win_amd64.whl (8.1 МВ)
---------------------------------------- 8.1/8.1 МВ 64.1 МВ/s eta 0:00:00
Downloading scipy-l.15.3-cp313-cp313-win_amd64.whl (41.0 МВ)
---------------------------------------- 41.0/41.0 МВ 43.7 МВ/s eta 0:00:00
Downloading pandas-2.2.3-cp313-cp313-win_amd64.whl (11.5 МВ)
---------------------------------------- 11.5/11.5 МВ 42.6 МВ/s eta 0:00:00
Downloading pyparsing-3.2.3-py3-none-any.whl (111 kВ)
Downloading python_dateutil-2.9.0.post0-py2.py3-none-any.whl (229 kB)
Downloading pytz-2025.2-py2.py3-none-any.whl (509 kВ)
Downloading tzdata-2025.2-py2.py3-none-any.whl (347 kВ)
Downloading six-l.17.0-py2.py3-none-any.whl (11 kВ)
Installing collected packages: pytz, tzdata, six, pyparsing, pillow, packaging, numpy,
kiwisolver, fonttools, cycler, scipy, python-dateutil, contourpy, pandas, matplotlib
Successfully installed contourpy-1.3.2 cycler-0.12.1 fonttools-4.58.0 kiwisolver-1.4.8
matplotlib-3.10.3 numpy-2.2.5 packaging-25.0 pandas-2.2.3 pillow-11.2.1 pyparsing3.2.3 python-dateutil-2.9.0.post0 pytz-2025.2 scipy-1.15.3 six-1.17.0 tzdata-2025.2
В самом конце указываются установленные библиотеки и их версии, включая зави
симости. В нашем примере мы попросили установить четыре библиотеки, но мож
но видеть, что, кроме них, были установлены также несколько других библиотек.
При работе под
Windows
ближе к концу лога могут быть выведены предупреждения:
WARNING: The scripts f2py.exe and numpy-config.exe are installed in
'C:\Users\USERNAМE\AppData\Roaming\Python\Python313\Scripts' which is not on РАТН.
Consider adding this directory to РАТН or, if you prefer to suppress this warning,
use --no-warn-script-location.
302
Часть
11.
Основные подходы
WARNING: The scripts fonttools.exe, pyftmerge.exe, pyftsubset.exe and ttx.exe are
installed in 'C:\Users\USERNANE\AppData\Roaminq\Python\Python313\Scripts' which is not
on РАТН.
Consider adding this directory to РАТН or, if you prefer to suppress this warning,
use --no-warn-script-location.
Суть их заключается в том, что установленные библиотеки также создали в катало
ге Scripts (см. пути, выделенные в приведенном выводе полужирным шрифтом) не
сколько исполняемых файлов, но путь до этого каталога еще не был добавлен в пе
ременную окружения РАТН, и они не могут быть запущены без указания полного
пути до них. Поэтому рекомендуется самостоятельно добавить требуемый путь в
переменную окружения РАТН.
Для доступа к переменным окружения нужно в меню
Windows
найти пункт Изме
нение системных переменных среды, в открывшемся диалоговом окне Свойства
системы нажать кнопку Переменные среды, после чего в открывшемся диалого
вом окне Переменные среды выполнить на переменной среде РАТН двойной щел
чок мышью (редактировать надо эту переменную для текущего пользователя, а не
для системы) и в открывшийся список добавить путь, указанный в процессе уста
новки библиотек,
-
в нашем случае это:
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\Scripts
где USERNAМE- имя текущего пользователя системы.
Такую процедуру для текущей версии
Но при установке новой версии
повторить с новым путем до каталога
По умолчанию команда
Python
Python
pip install
нужно проделать только один раз.
(например,
3.14),
эту процедуру придется
Scripts.
устанавливает самую последнюю версию биб
лиотек из тех, что поддерживают текущую версию
Однако в некоторых си
Python.
туациях может понадобиться установить какую-то конкретную версию библиоте
ки,
-
если, например, возникнет конфликт с другими библиотеками, для своей ра
боты требующими определенную версию той или иной библиотеки, или в случае,
когда в последней версии библиотеки появилась проблема, которой не было в пре
дыдущих версиях. В этом случае при указании устанавливаемых пакетов после
имени пакета можно указать требуемую версию. Например, так:
> python -m pip install --user numpy==2.0.0
Помимо оператора
«==»,
matplotlib==З.8.2
который обозначает точное совпадение версий
занной версии не существует, то будет выведена ошибка},
pip
(если
ука
поддерживает и дру
гие операторы сравнения и подстановки версий. Так, при указании версии можно
использовать символ подстановки«*», обозначающий любую цифру. В этом случае
будет установлена самая последняя из имеющихся версий, удовлетворяющих мас
ке. Например, команда:
> python -m pip install --user numpy==2.0.*
установит библиотеку
NumPy 2.0.2
и
matplotlib==З.8.*
Matplotlib 3.8.4. Pip
также поддерживает и
другие операторы сравнения, такие как«~=» (совместимый релиз), «»>,«>=»,но мы
подробно на них останавливаться не будем. Эти операторы сравнения чаще исполь-
Глава
16.
303
Сторонние библиотеки и инструменты для работы с ними
зуются при создании собственных пакетов, когда требуется указать зависимости от
других библиотек.
Скажем несколько слов о путях, куда устанавливаются библиотеки, чтобы их уста
новка не казалась какой-то загадочной операцией. В главе
12
было отмечено что
все пути, где интерпретатор ищет импортируемые модули, указаны в переменной
sys .path. Там же мы написали небольшой скрипт, отображающий эти пути:
C:\Program
C:\Program
C:\Program
C:\Program
Files\Python313\python313.zip
Files\Python313\DLLs
Files\Python313\Lib
Files\Python313
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\win32\lib
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages\Pythonwin
C:\Program Files\Python313\Lib\site-packages
В рассматриваемом случае первые элементы списка являются путями, ведущими
туда, где расположена стандартная библиотека. Файла python313.zip
(Python
умеет
загружать пакеты из архивов) может и не быть.
Место, куда устанавливаются пакеты для текущего пользователя:
C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages
Можно открыть указанный каталог и увидеть там установленные пакеты. Послед
ний из приведенных в выводе путей
место, куда устанавливаются пакеты
глобально для текущей версии
- это то
Python, если
требуется обеспечить доступ к ним
всем пользователям.
Чтобы узнать, куда именно установлен пакет, можно воспользоваться командой pip
show с указанием имени пакета. Давайте посмотрим информацию о пакете
matplotlib:
> python -m pip show matplotlib
Name: matplotlib
Version: 3.10.3
Summary: Python plott1ng package
Home-page: https://matplotlib.org
Author: John D. Hunter, Michael Droettboom
Author-email: Unknown <matplotlib-users@python.org>
License: License agreement for matplotlib versions 1.3.0 and later
Location: C:\Users\USERNAМE\AppData\Roaming\Python\Python313\site-packages
Requires: contourpy, cycler, fonttools, kiwisolver, numpy, packaging, pillow,
pyparsing, python-dateutil
Required-by:
Здесь можно увидеть путь, куда установлена библиотека
Matplotlib,
а также список
других библиотек, требующихся для ее работы, и установленных автоматически
при установке самой библиотеки.
304
Часть
В разделе
11.
Основные подходы
Required-by указываются библиотеки, требующие в качестве зависимости
рассматриваемую библиотеку. В нашем случае никакая библиотека не зависит от
Matplotlib,
а вот, например, для библиотеки
NumPy
этот раздел может выглядеть
следующим образом:
Required-by: contourpy, matplotlib, pandas, scipy
Чтобы увидеть все возможные параметры для какой-либо команды
бавить к ней параметр
pip,
нужно до
-h или --help. Например:
> python -m pip install -h
В результате в консоль будет выведена справка об этой команде с указанием всех
ее возможных дополнительных параметров.
Файл зависимостей
requirements.txt
В исходных кодах многих проектов, написанных на
Python,
можно встретить файл
requirements.txt, содержащий список требуемых пакетов, необходимых для работы
приложения или библиотеки. Формат этого файла очень простой
-
это текстовый
файл, в котором требуемые пакеты записаны по одному на строку, возможно, с ука
занием номера версии, как при использовании команды pip install. Пример фай
ла requirements.txt приведен в листинге
Лисntнr
16.1.
16.1. Requirements.txt
numpy
pandas==2.2. 3
matplotlib==З.10.3
Для того, чтобы установить пакеты, указанные в таком текстовом файле, команде
pip install нужно добавить параметр -r и указать путь до файла с зависимостями:
> python -m pip install -r requirements.txt
В результате будут установлены указанные в этом файле библиотеки и все требуе
мые для них зависимости.
При распространении исходного кода своего проекта полезно указать конкретные
версии библиотек, на которых проводилось тестирование приложения, чтобы не
было неожиданностей, связанных с новыми версиями библиотек, когда при их об
новлении ломается обратная совместимость. Для этой цели
манду
pip freeze,
pip
предоставляет ко
которая выводит список всех установленных имен пакетов с их
версиями в том формате, который можно скопировать в файл requirements.txt. На
пример:
> python -m pip freeze
contourpy==l.3.2
cycler==D.12.1
fonttools==4.58.0
kiwisolver==l . 4.8
matplotlib== З .10.3
Глава
16.
Сторонние библиотеки и инструменты для работы с ними
305
numpy==2.2.5
packaging==25.0
pandas==2.2.3
pillow==ll.2.1
pyparsing==3.2.3
python-dateutil==2.9.0.post0
pytz==2025.2
scipy==l .15. 3
six==l.17.0
tzdata==2025.2
С помощью перенаправления вывода
requirements.txt
( оператор «>»)
можно сразу создать файл
с таким содержимым:
> python -m pip freeze > requirements.txt
Теперь мы можем распространять этот файл вместе с исходным кодом нашего про
екта, и другие пользователи после выполнения команды:
pip install -r requirements.txt
будут работать ровно с тем же набором библиотек, который был установлен у нас в
момент выполнения этой команды.
В последнее время файл requirements.txt уступает другому способу описания зависи
мостей
-
файлу pyproject.toml, с которым мы познакомимся в следующей главе.
Обновление и удаление пакетов
список
увидеть
freeze,
можно воспользоваться командой
результата выполнения этой команды:
> python -m pip
Package
--------------contourpy
cycler
fonttools
kiwisolver
matplotlib
numpy
packaging
pandas
pillow
pip
pyparsing
python-dateutil
pytz
scipy
six
tzdata
библиотек, помимо команды pip
pip list. Далее показан пример вывода
всех установленных
Чтобы
list
Version
-----------
1.3.2
0.12.1
4.58.О
1.4.8
3.10.3
2.2.5
25.0
2.2.3
11.2.1
24.3.1
3.2.3
2.9.0.postO
2025.2
1.15.3
1.17.0
2025.2
306
Часть
Также
pip
11.
Основные подходы
позволяет определить, для каких библиотек вышли более новые версии
по сравнению с установленными. Для этого к команде pip
list нужно добавить
--ou tda ted:
параметр
> python -m pip list --outdated
В результате выполнения такой команды, если на
PyPi
есть более новые версии ус
тановленных пакетов, будет выведена таблица с указанием имен пакетов, их уста
новленных версий и версий, доступных на
Package
---------matplotlib
numpy
pip
Version Latest
3. 8. 4
2.0.2
24.3.1
PyPi:
Туре
3.10.6 wheel
2.3.2 wheel
wheel
25.2
Последний столбец в выводимой таблице обозначает формат пакета, который ис
пользуется для установки соответствующей библиотеки. Вопрос о форматах паке
тов мы оставим за рамками книги.
У параметра
списка
--outdated есть сокращенный вариант -о, поэтому для получения
пакетов
с
новыми
версиями
можно
использовать
команду
в
следующем
формате:
> python -m pip list
-о
Если у установленного пакета появилась новая версия, то, возможно, пора его об
новить. Для установки новой версии пакета используется уже знакомая нам коман
да pip install с указанием имени пакета. Но если мы повторно запустим команду
pip install без указания номера версий пакетов, и указанные пакеты уже установ
лены, то
pip
ничего делать не будет, он только сообщит, что эти пакеты уже уста
новлены. Чтобы
pip
обновил пакеты, нужно добавить параметр --upgrade:
> python -m pip install --user --upgrade numpy
В этом случае будет установлена самая свежая версия библиотеки. У параметра
upgrade
есть сокращенная версия
-u,
--
поэтому предыдущую команду можно запи
сать так:
> python -m pip install --user -U numpy
Сам
pip
тоже достаточно часто обновляется, и обновить его до более современной
версии можно с помощью команды:
> python -m pip install --user -U pip
В этом случае при работе под
Windows
важно использовать вызов
команды python -m (или ру -m), а не просто запуская
pip
pip,
pip
с помощью
как приложение. Иначе
не сможет обновиться, поскольку исполняемый файл pip.exe в момент обновле
ния будет запущен и не сможет быть заменен на более новую версию.
Для удаления пакетов, которые уже не требуются, предназначена команда pip
uninstall. С ней всё достаточно просто -
после этой команды нужно указать
имена пакетов, которые требуется удалить,
например,
-
NumPy:
> python -m pip uninstall numpy
и, если у вашего пользователя достаточно прав, то эти пакеты будут удалены:
Глава
16.
Перед удалением каждого пакета
лить,
-
307
Сторонние библиотеки и инструменты для работы с ними
pip
спросит, действительно ли его нужно уда
для подтверждения достаточно будет нажать клавишу
<Enter>.
Для того,
чтобы автоматически удалить указанные пакеты без подтверждений, следует доба
вить параметр -у:
> python -m pip uninstall
Pip -
numpy
-у
достаточно мощный инструмент, он предоставляет множество других воз
можностей для работы с пакетами, в том числе и для их создания, и мы рассмотре
ли здесь лишь наиболее часто используемые из них.
Заключение
В этой главе мы познакомились с
инструментом, позволяющим легко уста
pip -
навливать дополнительные библиотеки для
ды pip
Python.
В процессе выполнения коман
install с серверов PyPi будут скачаны искомые пакеты, а также все тре
буемые дополнительные зависимости.
Пакеты можно устанавливать глобально
-
чтобы они были доступны для всех
пользователей операционной системы (для выполнения этой операции требуются
права администратора), но рекомендуется устанавливать пакеты в пользователь
ский каталог,
-
тогда каждый пользователь компьютера сможет самостоятельно
устанавливать, обновлять и удалять пакеты. Для установки пакета в каталог поль
зователя к команде pip install нужно добавить параметр --user.
В некоторых проектах требуемые библиотеки записывают в файл requirements.txt.
Для создания файла requirements.txt можно воспользоваться командой pip freeze, а
для установки библиотек из этого файла
-
выполнить команду:
pip install -r requirements.txt
С помощью команды pip list можно получить полный список установленных па
кетов, а выполнив команду:
pip list --outdated
список пакетов, для которых вышли более новые версии.
Для обновления уже установленных пакетов предназначена команда:
pip install --upgrade
А для удаления пакетов
-
команда pip uninstall.
В этой главе были рассмотрены лишь основные возможности для работы с пакета
ми. Хорошим стилем считается устанавливать пакеты отдельно для каждого проек
та, разрабатываемого на
Python, -
для этого предназначены виртуальные окруже
ния, о которых мы поговорим в следующей главе.
- ГЛАВА 17 Виртуальные окружения
В этой главе мы продолжим тему установки библиотек, начатую в главе
где мы
16,
уже научились устанавливать сторонние библиотеки. Со временем количество ус
тановленных пакетов растет, и в них становится трудно ориентироваться: какие из
них еще требуются, какие были установлены как зависимости, а какие можно без
болезненно удалить. Но могут возникнуть и более серьезные проблемы. При работе
над несколькими проектами на
Python,
каждый из которых требует определенного
набора библиотек, может возникнуть ситуация, когда для двух разных проектов
требуется одна и та же библиотека, но разных версий. Для решения этой проблемы
используются виртуш~ьные окружения. Именно про них здесь и пойдет речь.
Программа
venv
Суть виртуальных окружений заключается в том, что для каждого отдельного про
екта создается свой каталог, внутри которого располагается требуемая версия ин
терпретатора
Python
и свои наборы пакетов. Перед началом работы с проектом мы
активируем то или иное виртуальное окружение, после чего интерпретатор будет
видеть только те пакеты, которые установлены в этом окружении, а
pip
станет ра
ботать внутри каталога виртуального окружения, устанавливая, обновляя и удаляя
пакеты только из него.
Существует множество сторонних приложений, позволяющих очень гибко рабо
тать с виртуальными окружениями, и некоторые из них мы еще рассмотрим далее,
но для начала поработаем с тем инструментом, который предлагает
робки»
-
это программа
Python
«из ко
venv.
Для дальнейших экспериментов создадим пустой каталог, не очень глубоко распо
ложенный в дереве файловой системы, чтобы путь до нее был не слишком длин
ным. Все примеры этой главы будут показаны для операционной системы
в предположении, что для виртуальных окружений создан каталог
Venv
является модулем
Python,
Windows
C:\projects\venv.
поэтому его запуск производится командой:
python -m venv
Эта команда требует в качестве обязательного параметра путь до каталога, куда
будут скопированы файлы интерпретатора, требующиеся для использования в вир
туальном окружении.
Глава
309
Виртуальные окружения
17.
Запустим консоль и создадим виртуальное окружение с помощью команды:
> python -m venv c:\projects\venv\myproject
Внутри каталога
C:\projects\venv
будет создан подкаталог
со следующей
myproject
структурой:
myproject
~ Include
~ Lib
L_ site-packages
1
~ pip
1
L_ pip-24.3.1.dist-info
1
~ pyvenv.cfg
L_ Scripts
~ activate
~ activate.bat
~ activate.fish
~ Activate.psl
~ deactivate.bat
~ pip.exe
~ рiрЗ.13.ехе
~ рiрЗ.ехе
~ python.exe
L_
pythonw.exe
Каталог
Scripts,
скрипты,
помимо запускаемых файлов интерпретатора
активирующие
виртуальные
окружения
для
Python
разных
тем. Так, для активации виртуального окружения под
и
pip,
Активируем
созданное
виртуальное
окружение,
сис
Windows предназначены
Activate.ps1 (если вы поль
activate.bat (если вы пользуетесь консолью cmd) и
зуетесь PowerShell). Для других операционных систем предназначены
activate и activate.fish -- для терминалов bash и fish соответственно.
скрипты
содержит
операционных
выполнив
скрипты
соответствующий
скрипт для вашей операционной системы и терминала. Например:
> c:\projects\venv\myproject\Scripts\activate.bat
После этого в консоли
команд
-
cmd
можно увидеть, что изменилось приглашение для ввода
в самом начале в скобках добавилось имя виртуального окружения:
(myproj ect)
С:\>
Теперь, после активации виртуального окружения, пакеты будут устанавливаться в
подкаталог
Lib\site-packages внутри
каталога виртуального окружения.
Если сейчас запустить команду pip list, то мы увидим, что не установлено ника
ких пакетов, кроме самого
(myproject) C:\>pip list
Package Version
pip
24.3.1
pip:
Часть
310
Запустим интерпретатор
Python
11.
Основные подходы
в интерактивном режиме и убедимся, что он дейст
вительно настроен таким образом, чтобы устанавливать пакеты в каталог виртуаль
ного окружения:
> python
»> import sys
>>> for path in sys.path:
print(path)
C:\Program Files\Python313\python313.zip
C:\Program Files\Python313\DLLs
C:\Program Files\Python313\Lib
C:\Program Files\Python313
c:\projects\venv\myproject
c:\projects\venv\myproject\Lib\site-packages
Выйдем из интерактивного режима
под
Python 3.13
Windows
вызвав команду exi t ()
Python,
(начиная с
команду exit можно вызывать без скобок). Убедитесь,
что вы по-прежнему находитесь в виртуальном окружении.
Установим библиотеку
(myproject)
С:\>
NumPy,
выполнив команду:
python -m pip install numpy
Обратите внимание, что при работе с виртуальными окружениями для
pip
при ис
пользовании команды install не требуется указывать параметр --user. Более того,
если вы его укажете, то
pip
выдаст ошибку.
После установки пакета numpy выполним команду:
(myproject)
С:\>
python -m pip list
В консоль будет выведен примерно следующий текст:
Package Version
numpy
pip
2.2.5
24.3.1
Вы также сможете увидеть, что в каталоге
C:\projects\venv\myproject\Lib\site-packages\
появились файлы и подкаталоги, содержащие пакет numpy.
После активации виртуального окружения все рассмотренные в предыдущей главе
команды
pip
также будут работать внутри него.
Для выхода из виртуального окружения под
Windows
просто перезапустите кон
соль, а под другими операционными системами закрывать консоль не обязательно,
достаточно выполнить в ней команду
exi t.
Рекомендуется создавать свое виртуальное окружение для каждого отдельного про
екта, над которым вы работаете, чтобы не возникало проблем с версиями библио
тек. Кроме того, поскольку в системе допускается устанавливать несколько разных
версий интерпретатора
своей версией
Python, -
Python,
каждое виртуальное окружение можно создавать со
в зависимости от того, с помощью какой версии интерпре-
Глава
17.
Виртуальные окружения
311
татора выполнялась команда python -m venv ... Под
Windows
для этой цели удобно
использовать инструмент ру.
Работа с виртуальными окружениями
Если мы хотим, чтобы наше приложение, написанное на
Python,
могло выполняться
не только на нашем компьютере, нужно обеспечить идентичное окружение и на
других компьютерах. Под этим понимается, что на них, как минимум, должны быть
установлены требуемая версия
и набор библиотек, необходимых для работы
Python
приложения. Также желательно, чтобы на других компьютерах были установлены
именно те версии библиотек, с которыми мы тестировали работу нашего приложения.
Мы уже рассмотрели некоторые средства, которые позволяют этого добиться,
-
файл с описанием требований requirements.txt и виртуальные окружения. Однако в
крупных проектах возможностей requirements.txt не хватает, поскольку обычно для
их полноценной работы требуется иметь несколько разных наборов библиотек:
один
гой
для работы приложения на сервере или у конечного пользователя, а дру
-
-
для тестирования и отладки кода на компьютере разработчиков. Поэтому в
некоторых проектах еще можно встретить файл requirements-dev.txt (или с подобным
именем), содержащий список пакетов, предназначенных для разработки
(development).
В последнее время на смену файлу requirements.txt активно приходит новый формат
описания зависимостей
-
файл pyproject.toml. Мы не станем подробно рассматри
вать сам формат, но нужно отметить, что формат
который когда-то был популярен под
Windows,
TOML
напоминает формат
INI,
но имеет больше возможностей для
описания параметров сложных типов. Файл pyproject.toml, помимо зависимостей,
содержит
и
другие
аспекты
приложения:
имя
автора,
название
приложения,
его
описание, лицензию, способы сборки приложения. При этом любая утилита, ис
пользуемая при разработке проекта на
Python,
может добавлять свои разделы в этот
файл, не влияя на другие настройки.
Программа
Poetry -
Poetry
инструмент, о котором пойдет речь в этом разделе, объединяет в себе
множество возможностей, связанных с созданием приложения и подготовкой для
него файла pyproject.toml, а также более удобным управлением виртуальными окру
жениями. Здесь мы коснемся лишь базовых возможностей этой утилиты, а полную
документацию по
Poetry
Poetry
можно найти на ее официальном сайте 1 •
устанавливается как обычный Руthоn-пакет:
> python -m pip install --user poetry
После
установки,
если
вы
работаете
под
Windows,
то
в
каталоге
C:\Users\USERNAME\AppData\Roaming\Python\PythonЗ 13\Scripts\ или его аналоге появится
запускаемый файл poetry.exe.
1
См. https://python-poetry.org.
Часть
312
Чтобы убедиться, что инструмент
Poetry
11.
Основные подходы
установлен, можно выполнить команду:
> poetry
или
> python -m poetry
В результате будет выведена справка со списком основных его команд.
Создание проекта с помощью
Файл
Poetry.
pyproject.toml
Для демонстрации работы с
Poetry
создадим простой проект
«Hello, World!»
Для
этого сначала нужно создать новый пустой каталог, запустить консоль и перейти в
этот каталог с помощью команды:
> cd
путь_до_каталога
Затем, находясь в каталоге с будущим проектом, выполнить команду:
> poetry init
После выполнения этой команды
Poetry
начнет последовательно задавать вопросы
относительно настроек проекта. Для каждого вопроса в квадратных скобках
- если оно
клавишу <Enter>.
лагается значение по умолчанию
можно просто нажимать
Диалог с
Poetry
пред
нас устраивает, в ответ на этот вопрос
получается относительно длинным, поэтому разберемся с ним по
частям. В приведенных далее фрагментах этого диалога строки, введенные пользо
вателем, выделены полужирным шрифтом:
This command will guide you through creating your pyproject.toml config.
Package name [hello]: hello
Version [0.1.0]:
Description []: Hello world project
Author [None, n to skip]: Eugene Ilin
License []: GPL-3
CompatiЫe Python versions [>=3.13]:
лз.13
Сначала мы вводим имя проекта, а точнее, пакета, в который он будет собран, если
мы планируем его распространять через сервис
PyPi.
Затем указываем номер вер
сии (в нашем примере мы оставили его со значением по умолчанию
0.1.0),
описа
ние проекта, имя автора и название лицензии, под которым будет распространяться
этот пакет.
После этого указываем номер версии
Python,
которая требуется для работы нашего
приложения. Выражение л3 .13 означает, что требуется версия
Python
не ниже
3.13,
но совместимая с ним. Такая запись равносильна неравенствам >=3 .13 и <4. о. То
есть мы указали, что для нашего проекта подойдет
4.0
Python 3.13, 3.14
и выше, но не
(которого пока не существует еще даже в планах).
Иногда желательно еще более жестко ограничить разброс версий
-
зать, что требуется версия
Для этого можно
Python
не ниже
3.13.0,
но ниже
3.14.
например, ука
Глава
17.
Виртуальные окружения
313
было бы указать версию так: ~3 .13. В этом случае
Python 3.13.1
и
3.13.2
также по
дошли бы.
Если же мы хотели бы ограничиться только определенной версией
Python,
то могли
бы написать: ==3 .13. 2.
После того, как версия
Python
указана,
Poetry
спросит, хотим ли мы сразу добавить
в проект зависимости, или будем добавлять зависимости в файл pyproject.toml позд
нее самостоятельно:
Would you like to define your main dependencies interactively? (yes/no) [yes]
Poetry
позволяет разделить устанавливаемые пакеты на группы: пакеты, необходи
мые для работы приложения, и пакеты, используемые при разработке и отладке
(эта группа пакетов зависимостей не является обязательной, и у конечного пользо
вателя их можно не устанавливать).
Сначала
Poetry
спрашивает про обязательные пакеты. Выбираем
«yes»
(значение по
умолчанию)- это означает, что мы хотим добавить зависимости сейчас, в процес
се настройки параметров проекта.
Затем
Poetry
выводит подсказку, в каких форматах можно указывать требуемые
пакеты:
You can specify а package in the following forms:
- А single name (requests): this will search for matches on PyPI
- А name and а constraint (requests@л2.23.0)
- А git url (git+https://github.com/python-poetry/poetry.git)
- А git url with а revision (git+https://github.com/pythonpoetry/poetry.git#develop)
- А f ile path ( .. /my-package/my-package. whl)
- А directory ( .. /my-package/)
- А url (https://example.com/packages/my-package-0.1.0.tar.gz)
Мы воспользуемся первым показанным здесь способом
рый
Poetry
будет искать в
PyPi.
-
по имени пакета, кото
Укажем, что мы хотим добавить пакет numpy:
Package to add or search for (leave
Ыank
to skip):
nuшpy
Poetry сделает запрос к базе данных PyPi, выдаст информацию о
«numpy» встречается в именах многих пакетов (117 пакетов), но
том, что фрагмент
из них, которые имеют наилучшее совпадение. В нашем случае
-
ный пакет
покажет только те
это единствен
numpy. Нам нужно подтвердить, что нас интересует именно он, введя его
номер:
Package to add or search for (leave
Found 117 packages matching numpy
Showing the first 10 matches
Ыank
to skip): numpy
Enter package # to add, or the complete package name if it is not listed []:
[ О] numpy
[ 1]
>
о
Часть
314
Выбираем в списке номер О. Теперь
Poetry
11.
Основные подходы
просит указать номер версии пакета:
Enter the version constraint to require (or leave
Ыank
Если ничего не вводить, а просто нажать клавишу
to use the latest version):
<Enter>,
то будет использоваться
последняя имеющаяся в текущий момент версия. Так мы и поступим:
Using version
л2.2.5
for numpy
Мы больше не хотим добавлять пакеты, которые требуются для работы приложе
ния, поэтому на запрос о вводе следующего имени пакета:
Add
а
package (leave
Ыank
to skip):
просто нажимаем клавишу
После этого
Poetry
<Enter>.
спросит, хотим ли мы добавить пакеты, которые требуются
только при разработке и отладке проекта:
Would you like to define your development dependencies interactively? (yes / no) (yes)
Ответим на этот вопрос
«yes»
(значение по умолчанию). Далее процесс добавления
пакетов происходит точно так же, как при добавлении в основные зависимости.
Для примера добавим библиотеку
Pytest,
которая используется для написания тес
тов к приложению:
Found 178 packages matching pytest
Showing the first 10 matches
Enter package # to add, or the complete package name if it is not listed (]:
О) pytest
1)
>
о
о
Епtес the version constraint to require (or leave
Usi ng version лs.З.5 for pytest
Ьlank
to use the latest version):
Других пакетов мы добавлять не собираемся, поэтому в ответ на следующий во
прос:
Add
а
package (leave
Ыank
to skip):
просто нажимаем клавишу
<Enter>:
Generated file
Теперь, когда мы добавили все необходимые пакеты,
держимое будущего файла pyproject.toml (листинг
Листинг
17.1 . Файл pyproject.toml
name = "hell o"
version = "0.1.0"
description = "Hello world project"
autr.ors =
(name = "Eugene Ilin"}
license = {text = "GPL-3"}
17.1).
Poetry
выводит в консоль со
Глава
17.
Виртуальные окружения
315
readme = "READМE.md"
requires-python = "лЗ.13"
dependencies = [
"numpy (>=2.2.5,<З.О.0)"
[tool.poetry]
[tool.poetry.group.dev.dependencies]
pytest = "л8.З.5"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
Последнее, что требуется,
это подтвердить, что полученный результат можно
-
сохранить в файл pyproject.toml:
Do you confirm generation? (yes/no) [yes]
Нажимаем клавишу
<Enter>,
что равносильно выбору
«yes».
И на этом работа по созданию файла описания проекта завершена. В каталоге про
екта появится файл pyproject.toml с точно таким же содержимым, как мы только что
видели в консоли.
В разделе
[projectJ созданного файла содержится общее описание проекта. По
мимо того, что мы указывали в интерактивном режиме, там добавлен параметр
readme = "README.md", который означает, что еще должен быть файл README.md в
формате
Markdown
с более подробным описанием проекта (его требуется создавать
самостоятельно, но мы его создание проигнорируем).
В параметре dependencies указаны обязательные зависимости проекта, а в разделе
[ tool. poetry. group. dev. dependencies] ботке. Здесь dev -
зависимости, используемые при разра
название группы пакетов. Помимо этой группы, мы можем соз
давать еще свои дополнительные группы,
-
если нам нужно каким-то образом
упорядочивать зависимости и в разных случаях устанавливать разные наборы пакетов.
Раздел [build-system] связан со сборкой пакета для его распространения, мы эту
тему опустим.
Создание виртуального окружения для проекта с помощью
Пока всё, что мы сделали,
шаг
-
-
Poetry
это создали файл с описанием проекта. Следующий
создать для проекта виртуальное окружение и установить в него требуемые
библиотеки. Для этого, находясь в том же каталоге, где расположен файл pyproject.toml,
нужно выполнить команду:
> poetry update
Creating virtualenv hello-Va8GUGOZ-py3.13 in
C:\Users\USERNAМE\AppData\Local\pypoetry\Cache\virtualenvs
Часть
316
11.
Основные подходы
Updating dependencies
Resolving dependencies ... (1.8s)
Package operations: 6 installs,
-
Installing
Installing
Installing
Installing
Installing
Installing
О
updates,
О
removals
colorama (0.4.6)
iniconfig (2.1.0)
packaging (25.0)
pluggy (1.5.0)
numpy (2.2.5)
pytest (8.3.5)
Writing lock file
Команда poetry update создает новое виртуальное окружение, если оно не было
создано
ранее,
устанавливает
в
него
все
зависимости,
указанные
в
файле
pyproject.toml (включая все группы пакетов, в том числе и группу dev) и создает файл
poetry.lock, содержащий полную информацию обо всех установленных пакетах (в
том числе обо всех установленных зависимостях), а также контрольные суммы всех
пакетов. Этот файл нужен для воспроизводимой установки
-
когда мы перенесем
свой проект на другой компьютер или сервер, благодаря файлу poetry.lock будут ус
тановлены именно те версии библиотек, которые там указаны, а также проверены
их контрольные суммы на случай, если злоумышленник сумел изменить пакеты на
сервере, откуда они скачиваются.
Если не требуется устанавливать пакеты, предназначенные для разработки, то в
команду poetry
update можно добавить дополнительный параметр --without, с
помощью которого можно указать, какие группы пакетов устанавливать не следует.
Например:
> poetry update --without=dev
Такая команда полезна при установке приложения на компьютер пользователя или
на производственный сервер, куда желательно устанавливать минимально необхо
димый набор пакетов.
В процессе выполнения команды poetry update можно увидеть расположение ка
талога с виртуальными
окружениями
и узнать
путь до
созданного для нашего проекта. Например, под
виртуального
Windows
окружения,
по умолчанию путь до
виртуальных окружений будет таким:
C:\Users\USERNAMВAppData\Local\pypoetry\Cache\virtualenvs
Имя каталога для нового виртуального окружения, которое будет создано внутри
указанного каталога, зависит от пути расположения проекта на диске и используемой
версии
Python.
Этот каталог может называться примерно так: hello-Va8GUG0Z-py3.1 З,
то есть полный путь до созданного виртуального окружения будет иметь вид:
C:\Users\USERNAMВAppData\Local\pypoetry\Cache\virtualenvs\hello-Va8GUG0Z-py3.1 З
Глава
17.
Виртуальные окружения
317
В этом каталоге можно найти уже знакомые нам подкаталоги Scripts и Lib\sitepackages. Но, как мы скоро увидим, при использовании Poetry нам не надо помнить,
где именно расположено созданное виртуальное окружение.
poetry update, то Poetry проверит, изменились
ли зависимости, указанные в файле pyproject.toml, и, если изменения есть, установит
указанные там пакеты и обновит файл poetry.lock.
Если повторно запустить команду
Чтобы активировать созданное виртуальное окружение, нужно сначала выполнить
в консоли, находясь в каталоге с файлом pyproject.toml, команду:
> poetry env activate
При этом будет указан путь до скрипта активации виртуального окружения:
"C:\Users\USERNAMSAppData\Local\pypoetry\Cache\virtualenvs\hello-Va8GUG0Z-py3.13\Scripts\activate.bat"
Эту команду нужно скопировать и запустить на выполнение~ тогда и будет акти
вировано созданное виртуальное окружение.
Теперь при запуске скриптов на выполнение станут использоваться библиотеки,
установленные в это виртуальное окружение.
Напишем небольшой скрипт, использующий библиотеку
что виртуальное окружение работает (листинг
Листинг
NumPy,
чтобы убедиться,
17.2).
17.2. Chapter_17/example_01/hello.py
import numpy as np
print (f"NumPy version: {np. version
х = np.linspace(0, np.pi * 3, 100)
у= np.sin(x) * np.sin(З * х)
print(y)
Более подробно про библиотеку
}"}
NumPy
речь пойдет в главе
25,
а сейчас для нас
важно, что этот скрипт выполняется, выводит номер установленной версии
и
NumPy
массив чисел.
Pip
при этом также будет работать внутри виртуального окружения. Убедимся в
этом, отобразив список установленных пакетов:
> pip list
Version
Package
---------
-------
colorama
iniconfig
numpy
packaging
pip
pluggy
pytest
О. 4. 6
2.1.0
2.2.5
25.0
24.2
1. 5. О
8. 3. 5
Часть
318
11.
Основные подходы
Если потребуется добавить в список установки новый пакет, то не следует использо
вать
pip
непосредственно. Новый пакет нужно прописать в файле pyproject.toml, доба
вив его в параметр
dependencies,
раздел
[tool.poetry.group.dev.dependencies]
или в другую группу пакетов, если она существует. После этого надо еще раз вызвать
команду
poetry update, которая установит новые пакеты и обновит файл poetry.lock.
Можно также воспользоваться командой
poetry add с указанием требуемой биб
лиотеки. Давайте, например, добавим пакет библиотеки
Matplotlib,
предназначенной
для построения графиков, с которой мы познакомимся более подробно в главе
27:
> poetry add matplotlib
Если мы не указываем конкретную версию пакета (как в этом случае), то будет уста
новлена последняя на текущий момент версия и ее зависимости. Информацию обо
всем этом вы увидите в консоли. В нашем случае вывод может быть примерно таким:
>poetry add matplotlib
Using version л3.10.3 for matplotlib
Updating dependencies
Resolving dependencies ... (2.7s)
Package operations: 9 installs,
-
Installing
Installing
Installing
Installing
Installing
Installing
Installing
Installing
Installing
О
updates,
О
removals
six (1.17.0)
contourpy (1.3.2)
cycler (0.12.1)
fonttools (4.58.0)
kiwisolver (1.4.8)
pillow (11.2.1)
pyparsing (3.2.3)
python-dateutil (2.9.0.post0)
matplotlib (3.10.3)
Writing lock file
Если теперь открыть файл pyproject.toml, то можно увидеть, что в разделе [project]
в параметре
dependencies
прописан пакет
matplotlib:
dependencies = [
"numpy (>=2.2.5,<3.О.0)",
"matplotlib (>=3.10.3,<4.0.0)"
Файл poetry.lock также был обновлен.
Poetry
...
предоставляет еще множество возможностей для работы с зависимостями,
виртуальными окружениями, запуском скриптов, сборкой и публикацией пакетов.
Мы рассмотрели здесь лишь самые базовые возможности этого
Но кроме
Poetry
инструмента.
существуют и другие подобные инструменты. Один из них мы бо
лее подробно рассмотрим далее.
Глава
17.
Виртуальные окружения
319
Менеджер пакетов и проектов
uv
В этом разделе мы рассмотрим инструмент
uv2,
активно разрабатываемый в по
следнее время. Этот инструмент предоставляет множество возможностей, включая
установку пакетов и различных версий
Python,
управление виртуальными окруже
ниями, а также работу с зависимостями проекта с помощью файла pyproject.toml.
Однако, если для установки пакетов
Poetry
запускал
pip,
то разработчики
ли свой установщик пакетов, не использующий классический
щество
pip.
uv
созда
Главное преиму
перед другими подобными инструментами, на что особо обращают вни
uv
мание его разработчики,
санных на
-
это скорость работы. В отличие от
написан на компилируемом языке
Python, uv
pip
Rust.
и
Poetry,
напи
При установке
пакетов с большим количеством зависимостей разница между скоростью работы
и того же
Poetry
uv
заметна невооруженным глазом. Это может быть особенно полезно
при работе с большими проектами, когда зависимостей много, и они часто устанав
ливаются на удаленном сервере для проведения автоматического тестирования.
Для установки
uv
под
Windows
есть несколько способов. Так, его можно устано
вить с помощью р1р, выполнив команду:
> python -m pip install --user uv
или выполнив в консоли
PowerShell
> powershell -ExecutionPolicy ByPass
команду:
-с
"irm https://astral.sh/uv/install.psl I iex"
которую, чтобы не ошибиться, лучше скопировать с сайта
Если вы работаете в
Windows
и устанавливаете
uv
uv 3 .
через консоль
PowerShell,
то за
пускаемый файл uv.exe будет создан в каталоге C:\Users\USERNAME\.local\Ьin, о чем
будет сказано в процессе установки. Для дальнейшей работы этот путь нужно до
бавить в переменную окружения РАТН, чтобы не требовалось каждый раз указывать
полный путь до запускаемого файла.
Если установка прошла успешно, то после выполнения в консоли команды:
> uv
будет выведен список команд этой утилиты:
An extremely fast Python package manager.
Usage: uv [OPTIONS]
Commands:
run
init
add
remove
sync
2
<СОММАND>
Run а command or script
Create а new project
Add dependencies to the project
Remove dependencies from the project
Update the project's environment
См. https://docs.astral.sh/uv/.
3 См.
https ://docs.astral.sh/uv /getting-started/installation.
320
Часть
lock
export
tree
tool
python
pip
venv
build
puЫish
cache
self
version
help
11.
Основные подходы
Update the project's lockfile
Export the project's lockfile to an alternate format
Display the project's dependency tree
Run and install commands provided Ьу Python packages
Manage Python versions and installations
Manage Python packages with а pip-compatiЫe interface
Create а virtual environment
Build Python packages into source distributions and wheels
Upload distributions to an index
Manage uv's cache
Manage the uv executaЫe
Read or update the project's version
Display documentation for а command
Для вывода более подробной информации о каждой из приведенных в этом списке
команд с указанием всех возможных параметров, нужно вызывать интересующую
команду с дополнительным параметром
Создание проекта с помощью
-h
или
--help.
uv
Давайте создадим новый проект с использованием
uv.
Для этого сначала нужно со
здать каталог, где будет располагаться проект (назовем его, например:
hello-uv), и
перейти в него в консоли с помощью команды cd. Далее будет подразумеваться,
что все последующие консольные команды вызываются из этого каталога.
Для создания проекта предназначена команда uv
ini t, при этом дополнительно
допускается указать, какой тип будет у нового проекта. Это может быть приложе
ние (подразумевается по умолчанию, или можно явно это указать, добавив пара
метр --арр), библиотека (для этого нужно добавить параметр --liь) или одиноч
ный скрипт (для этого следует добавить параметр --script). Разница между этими
вариантами заключается в том, какие файлы будут созданы.
Например, при выполнении следующей команды (имя файла создаваемого скрипта
нужно обязательно указать):
> uv init --script hello.py
будет создан единственный файл hello.py с таким содержимым:
#
#
#
#
!// script
requires-python = ">=3.10"
dependencies = []
///
def main() -> None:
print ( "Hello from hello. ру ! ")
if
name
main ()
"
main
"·
Глава
17.
Виртуальные окружения
321
Если мы захотим создать библиотеку, которая, возможно, будет распространяться
через
PyPi,
то нужно выполнить команду:
> uv init --lib
Будет создана следующая структура файлов:
hello-uv
.git
.gitignore
.python-version
pyproject. toml
READМE. md
L src
L hello uv
~ py.typed
init .ру
L
fffff-
Мы не станем подробно разбираться с этим типом проекта, обратите лишь внима
uv, помимо файлов с исходным кодом, создает файл pyproject.toml, файл
README.md с текстовым описанием проекта в формате Markdown, файл .pythonversion с указанием версии интерпретатора Python, а также git-репозиторий и файл
.gitignore, содержащий маски файлов, которые не должны попадать в этот репозиторий.
ние, что
Для дальнейших экспериментов мы создадим проект-приложение
-
выполним для
этого команду:
> uv init
В результате будут созданы следующие файлы: main.py, pyproject.toml, README.md,
.python-version, .gitignore, а также git-репозиторий.
Из них нас сейчас интересуют два файла: main.py и pyproject.toml. Содержимое файла
main.py представляет собой типичный скрипт в стиле
def main ():
print ("Hello from hello-uv! ")
if
name
main ()
"
main
«Hello, world!»
(листинг
17.3).
------
":
Файл pyproject.toml пока тоже содержит не так много информации (листинг
[project]
name = "hello-uv"
version = "0.1.0"
description = "Add your description here"
readme = "READМE.md"
requires-python = ">=3.10"
dependencies = []
17.4).
Часть
322
11.
Основные подходы
Сравните содержимое этого файла с содержимым такого же файла, который был
создан ранее с помощью
Poetry (см.
листинг
17 .1 ).
Создание виртуального окружения для проекта с помощью
Следующий шаг после создания проекта
-
uv
это создание для него виртуального
окружения. Для этого достаточно выполнить команду:
> uv venv
Будет создано очень минималистичное окружение
-
например, в нем не будет
pip
и некоторых других библиотек, иногда используемых при установке и сборке паке
тов. Впрочем, во многих случаях без этого можно обойтись, учитывая, что
мится заменить собой
го
окружения
setuptools),
pip.
в него
стре
сразу
был
добавлен
pip
(а также
библиотеки
и
wheel
в команду uv venv нужно добавить параметр --seed. Тем не менее, при
использовании
uv
обычно следует избегать использования
лательно использовать команду
параметры
uv
Однако, если мы хотим, чтобы при создании виртуально
pip,
Инструмент
uv,
pip, -
вместо него же
uv pip, за которой следуют все те же команды и
которые мы изучили ранее.
в отличие от
Poetry,
по умолчанию создает каталог для виртуально
го окружения непосредственно в каталоге с проектом
-
в подкаталоге
каталог включается в список исключений в создаваемом файле
.gitignore).
.venv
(этот
При необ
ходимости каталог с виртуальным окружением можно создать и в другом месте
для этого надо после всех параметров команды
uv venv
-
указать ее желаемое распо
ложение. Сама структура виртуального окружения точно такая же, какую мы виде
ли ранее. Преимущество создания каталога виртуального окружения в папке проек
та заключается в том,
что
не надо помнить,
виртуального окружения под
Windows
где он расположен,
и для активации
достаточно выполнить команду:
> .venv\Scripts\activate
Впрочем, при работе с
вать вручную
-
uv
виртуальное окружение даже не обязательно активиро
скрипт (или команду) можно выполнить в виртуальном окружении
с помощью команды
uv run. При этом uv активирует виртуальное окружение само
стоятельно только для выполнения скрипта:
>uv run main.py
Hello from hello-uv!
Если нужно запустить
REPL Python
той версии интерпретатора, которая содержит
ся в виртуальном окружении, надо выполнить команду:
> uv run python
Для добавления в проект зависимостей предназначена команда uv
добавим в проект библиотеки
> uv add numpy matplotlib
Resolved 12 packages in 940ms
Prepared 11 packages in 14.21s
Installed 11 packages in 1.81s
+ contourpy==l.3.2
NumPy
и
Matplotlib:
add. Давайте
Глава
17.
Виртуальные окружения
323
+ cycler==0.1 2.1
+ fonttoo l s==4. 58 . 0
+ ki wisol ver ==l .4. 8
+ mat p l otlib==З . 10 . 3
+ numpy==2.2 . 5
+ packaging==25.0
+ pillow==ll.2.1
+ pyparsing==З.2.3
+ python-dateutil==2.9.0.post0
+ six==l.1 7. 0
Команда uv add дополняет файл pyproject.toml, добавляя в него требуемые зависимо
сти . Поскольку мы не указали версии библиотек,
uv
с помощью сервера
PyPi
опре
делил , какие версии являются самыми последними , и именно их в обновленном
файле pyproject.toml и прописал (листинг
17.5).
Лмстинr 17.5. Файn pyproject.toml с добавленными зависимостями
[project]
name = "hello-uv"
version = "0.1 . 0"
description = "Add your description here "
readme = "READМE.md"
requi res-python = ">=3.1 0"
dependenci es = [
"matplotlib>=З .1 0 . 3 ",
"numpy>=2 . 2 . 5",
В процессе выполнения команды
uv add добавляемые библиотеки и все их зависи
мости были установлены в виртуальное окружение. Если бы до этого мы его не
создали , то оно было бы автоматически создано на этом шаге . Кроме того, был
также создан файл uv.lock, являющийся аналогом файла poetry.lock, о котором мы
говорили в разделе «Создание виртуального окружения для проекта
Poetry» . Этот
файл содержит ссылки на текущие версии всех требуемых библиотек и их зависи
мостей, а также их контрольные суммы.
Так же, как и
Poetry, uv
позволяет добавлять зависимости, требуемые только для
разработки . Для этого перед именами устанавливаемых пакетов в команду
нужно добавить параметр
зом библиотеку
pytest для
--dev.
тестирования кода на
> uv add --dev pytest
Resolved 19 packages in 614ms
Prepared 7 packages in 1.03s
Insta lled 7 packages i n 145ms
+ colorama== 0. 4.6
+ exceptiongroup==l.3.0
+ iniconfig==2.1.0
uv add
Установим в виртуальное окружение таким обра
Python:
324
Часть
11.
Основные подходы
+ pluggy==l.5.0
+ pytest==8.3.5
+ tomli==2.2.l
+ typing-extensions==4.13.2
В результате выполнения этой команды в виртуальное окружение будет установле
на самая последняя версия библиотеки
pytest,
а в файле pyproject.toml появятся сле
дующие строки:
[dependency-groups]
dev = [
"pytest>=8.3.5",
Кроме того, команда uv add позволяет создавать свои группы необязательных за
висимостей.
Чтобы увидеть полное дерево зависимостей используемых
uv
библиотек, нужно
выполнить команду:
> uv tree
Для нашего проекта в консоль будет выведено такое дерево:
Resolved 19 packages in 4ms
hello-uv vO .1. О
~ matplotlib vЗ.10.3
~ contourpy vl. 3. 2
1
L numpy v2.2.5
1
1
1
~ cycler v0.12.1
1
~ fonttools v4. 58. О
~ kiwisolver vl. 4. 8
1
~ numpy v2.2.5
1
~ packaging v25. О
1
1
~ pillow vll.2.1
1
~ pyparsing vЗ.2.3
1
L python-dateutil v2. 9. О .postO
1
Lsixvl.17.0
~ numpy v2.2.5
L pytest v8.3.5 (group: dev)
~ colorama v0.4.6
~ exceptiongroup vl.3.0
L typing-extensions v4 .13. 2
1
~ iniconfig v2.l.O
~ packaging v25.0
~ pluggy vl.5.0
L tomli v2.2.l
Если внимательно посмотреть на результат вывода, то можно заметить, что пакет
библиотеки
NumPy (numpy)
указан непосредственно в зависимостях нашего проекта,
а также он требуется для библиотеки
Matplotlib и библиотеки
Matplotlib.
свою очередь, является зависимостью для
Contouгpy, которая, в
Глава
17.
325
Виртуальные окружения
До сих пор мы самостоятельно добавляли зависимости с помощью команды
add -
uv
они устанавливались и прописывались в файл pyproject.toml. Но часто требу
ется установить зависимости, которые уже записаны в этот файл, но еще не уста
новлены в виртуальное окружение. Такая ситуация возникает, когда мы получаем
исходный код приложения, созданного с помощью
uv,
но для которого мы еще не
создавали виртуальное окружение, или когда в файле pyproject.toml изменился спи
сок зависимостей. Все эти задачи решает команда
uv sync.
Чтобы увидеть, как она работает, удалим каталог
полним команду
.venv
из каталога проекта и вы
uv sync. В результате будет создано новое виртуальное окруже
ние и в него установлены все требуемые библиотеки, включая те, что требуются
только для разработки. Если библиотеки для разработки устанавливать не требует
ся, следует добавить параметр
--no-dev:
uv sync --no-dev
Бывает так, что в процессе разработки возникает необходимость установить какую
либо библиотеку в виртуальное окружение, но при этом не добавлять ее в зависи
мости, а также не выполнять какие-то другие действия над уже установленными в
виртуальное окружение пакетами. Для этой цели
uv
предоставляет команду
uv pip,
которая во многом реализует возможности р1р.
Выполнив команду
uv pip list, мы можем увидеть все пакеты, установленные в
виртуальном окружении. Обратите внимание, что для этого даже не обязательно это
виртуальное окружение активировать,
>uv pip list
Package
Version
----------------- ----------colorama
contourpy
cycler
exceptiongroup
fonttools
iniconfig
kiwisolver
matplotlib
numpy
packaging
pillow
pip
pluggy
pyparsing
pytest
python-dateutil
setuptools
six
tomli
typing-extensions
wheel
О. 4. 6
1. 3. 2
0.12.1
1.3.0
4.58.0
2.1.0
1. 4. 8
3.10.3
2.2.5
25.0
11.2 .1
25.1.1
1. 5. О
3.2.3
8.3.5
2.9.0.post0
80.4.0
1.17.0
2.2.1
4.13.2
0.45.1
-
достаточно находиться в каталоге проекта:
Часть
326
А с помощью команды
uv pip list --outdated
11.
Основные подходы
можно проверить появление но
вых версий используемых библиотек.
Чтобы установить новый пакет в виртуальное окружение, но при этом не добавлять
его в зависимости, предназначена команда:
uv pip install
имя_пакета
Аналогично, для удаления пакета из виртуального окружения следует выполнить
команду:
uv pip uninstall
Uv
имя_пакета
предоставляет еще много возможностей для управления пакетами и виртуаль
ными окружениями, и мы рассмотрели здесь лишь самые основные из них. Напри
мер,
uv
позволяет устанавливать разные версии интерпретатора
Python для
Python,
проекта. Чтобы при создании проекта указать требуемую версию
каждого
команде
uv ini t нужно добавить параметр --python:
> uv init
--python=З.14
Затем если при создании виртуального окружения с помощью команды uv
окажется, что требуемая версия интерпретатора
Python
venv
не установлена в системе,
то она будет автоматически скачана и установлена в виртуальное окружение.
В настоящее время инструмент
uv
активно разрабатывается, и в нем постоянно по
являются новые возможности и новые настройки для уже существующих команд.
Заключение
В этой главе мы продолжили обсуждать тему установки библиотек, начатую в главе
16.
Мы узнали, почему не следует устанавливать сторонние пакеты глобально, и какую
проблему решают виртуальные окружения, которые рекомендуется создавать для
каждого проекта.
К
Python
прилагается стандартный инструмент
venv,
позволяющий создавать вир
туальные окружения. После создания и активации виртуального окружения в него
можно устанавливать библиотеки, используя
pip,
как это было показано в главе
16.
Поскольку каждый проект требует своего набора библиотек, разработчикам нужен
способ указывать, какие библиотеки устанавливать. Раньше для этого использовал
ся файл requirements.txt, однако в настоящее время для работы с зависимостями и
виртуальными окружениями были созданы (и продолжают разрабатываться) новые
инструменты, два из которых мы рассмотрели в этой главе.
Инструмент
Poetry
дает возможность создавать виртуальные окружения и управ
лять зависимостями, описывая требования в файле pyproject.toml, ставшем в послед
нее время стандартом для описания свойств проекта.
Poetry
позволяет не задумы
ваться, где было создано виртуальное окружение для того или иного проекта, а
также фиксировать все версии установленных в текущий момент библиотек, чтобы
при повторном создании виртуального окружения, например, на сервере, использо
вались именно эти версии. Такая возможность позволяет исключить проблемы, свя
занные с несовместимыми версиями библиотек.
Глава
17.
Виртуальные окружения
Более молодой инструмент
ботчики
uv
uv
327
предназначен для тех же задач, но при этом разра
пошли дальше и создали на замену
рый работает намного быстрее. В отличие от
pip свой
Poetry, uv
установщик пакетов, кото
создает виртуальное окру
жение непосредственно в каталоге с проектом, а кроме того,
навливать нужную версию интерпретатора
Python.
При этом
позволяет еще уста
uv
также использует
файл pyproject.toml.
Тема установки пакетов в
Python
настолько актуальная, что многие разработчики
продолжают создавать новые инструменты. Эту главу можно было бы продолжить,
например, описывая
Pipfile -
аналог
Poetry, но использующий свой формат файла
(Python Development Master) - еще один
для описания зависимостей, или РОМ
мощный и активно развивающийся менеджер пакетов, который, кстати, может ис
пользовать
uv
для установки пакетов. В этом ряду еще можно упомянуть и
инструмент для управления проектами и упаковки пакетов для
Выбор между
Poetry, uv
Hatch -
Python.
или другими подобными инструментами часто носит субъ
ективный характер или зависит от особенностей проекта. Мы подробно рассмотре
ли
Poetry,
так как он является одним из наиболее популярных менеджеров пакетов
и виртуальных окружений. При этом популярность
uv
в последнее время также за
метно растет, и у него есть объективные преимущества в скорости работы по срав
нению с
Poetry
и другими менеджерами пакетов.
Отдельной главы, если не книги, заслуживает проект
Anaconda,
в рамках которого
пользователю предоставляется целый набор предустановленных не только библио
тек, но и редакторов кода со своей экосистемой и менеджером пакетов conda.
Anaconda
нашла свою нишу в научном сообществе, машинном обучении и обра
ботке данных, предоставляя разработчикам уже настроенную среду для работы с
Python.
- ГЛАВА 18-
АННОТ3ЦИИ типов
Проблемы динамической типизации
При программировании на
Python
нам не требуется объявлять тип переменных
-
они приобретают свой тип в момент присваивания им значений, к тому же тип пе
ременной может меняться в процессе выполнения скрипта. Кроме того, благодаря
«утиной» типизации, нам не важно, какой конкретный тип имеет переменная,
-
главное, чтобы с этой переменной можно было бы выполнить все те действия, ко
торые описаны в программе.
Динамическая типизация
-
это обоюдоострый меч. С одной стороны, благодаря
ей, мы можем писать меньше кода. Написать, например, одну функцию, которая
будет работать с разными типами входных параметров, и даже с такими, о которых
разработчик функции может не догадываться. При этом нет надобности как-либо
описывать требования к передаваемым переменным. Однако если мы передадим в
функцию значения таких типов, с которыми внутри функции невозможно будет
выполнить предусмотренные действия, то получим сообщение об ошибке лишь в
процессе выполнения скрипта, когда интерпретатор попытается выполнить это не
допустимое действие. А вот у разработчиков, которые пишут код на языках со ста
тической типизацией, таких как С, С++,
Java, Rust
и многих других, подобные
ошибки будут обнаружены еще на этапе компиляции.
Пусть у нас есть функция, которая принимает два параметра и возвращает резуль
тат их сложения. Мы можем передавать в эту функцию числа, строки или любые
другие объекты, для которых определен оператор сложения. Со всеми такими ти
пами функция будет работать корректно:
>>> def add(a,
Ь):
»> add(lO, 15)
25
>>> add ("Привет,
'Привет,
мир'
return
"мир")
а+ Ь
Глава
18.
329
Аннотации типов
Однако если для какого-то типа переменной не найдется подходящего оператора,
то в процессе выполнения будет возбуждено исключение:
>»аdd("Привет,
", 42)
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
аdd("Привет,
", 42)
File "<python-input- ... >", line 1, in add
def add(a, Ь): return а+ Ь
TypeError: can only concatenate str (not "int") to str
При этом нам не всегда требуется «утиная)) типизация. Часто мы пишем функции,
подразумевая, что им в качестве параметров будут передаваться экземпляры опре
деленных типов. В этом случае у пользователей языков со статической типизацией
есть еще одно преимущество
описание функций является одновременно доку
-
ментацией, подсказывающей программисту, какие типы переменных можно пере
давать ей в качестве параметров.
Что такое «аннотации типов))
и зачем они нужны?
В
Python 3.5
появилась возможность указывать, какой тип ожидается у перемен
ных, параметров функций, а также у возвращаемого функцией значения. Эта тех
нология называется
typing,
или
type hints,
что на русский язык переводят как «анно
тации типою>, или «подсказки типов». Ей и посвящена эта глава.
Аннотации типов не делают
Python
языком со статической типизацией
-
интер
претатор игнорирует эту информацию, и программист тоже может ее игнориро
вать,
-
но при этом он должен понимать, что нарушение требований к типам мо
жет приводить к проблемам во время выполнения кода.
Зато, благодаря аннотациям типов, сторонние утилиты и умные редакторы кода
получили возможность проверять корректность кода (хотя бы с точки зрения ти
пов) и указывать на потенциальные ошибки. Аннотации типов активно развивают
ся, и в последних версиях
Python
постоянно появляются новые возможности для
всё более точного описания типов, которые ожидаются у переменных.
Чтобы продемонстрировать аннотацию типов, возьмем простую функцию сложе
ния из предыдущего раздела и укажем, что эта функция ожидает в качестве вход
ных данных целые числа (тип int) и возвращает целое число (листинг
Пистинr
18.1. Chapter_18/example_01/add_lntpy
def add(a: int, Ь: int) -> int:
return а+ Ь
18.1).
Часть
330
f оо = add (1 О, 15)
bar = add ("Привет,
11.
Основные подходы
"мир")
print(foo)
print (bar)
В этом примере мы встречаем новые синтаксические конструкции. Во-первых, ан
нотация типов добавляется вслед за именем переменной после знака двоеточия.
Если мы хотим определить, какой тип возвращается из функции, то для этого в
строке объявления функции
чия,
-
-
после закрытия скобок, но перед знаком двоето
нужно поставить символы«->», а затем указать возвращаемый тип.
Если этот пример открыть в каком-нибудь сравнительно «продвинутом» редакторе
кода,
-
например,
Visual Studio Code,
18.1 ).
то редактор выделит последнюю строчку как
содержащую ошибку (рис.
Рис.
18.1. Visual Studio Code
сообщает о том, что в коде есть проблемы
с передаваемыми в функцию параметрами
Если теперь навести курсор на подчеркнутую красной волнистой линией строку,
появится подсказка о том, в чем заключается ошибка (рис.
Рис.
18.2.
18.2).
Более подробная подсказка относительно проблемы
с первым параметром функции
add ()
Среда разработки подсказывает, что у функции add () ожидается первый аргумент
типа int, но вместо него в функцию передают тип str. Аналогичную подсказку
можно увидеть и у второго аргумента.
Несмотря на это, если запустить выполнение скрипта add_int.py, он сработает без
ошибок, и в консоли можно будет увидеть результат работы.
Глава
18.
Аннотации типов
331
Обратите внимание, что в конце строки подсказки написано загадочное слово
«Муру» (читается как «майпай»)
рого
Visual Studio Code
так называется инструмент, с помощью кото
-
в нашем случае проверяет правильность исходного кода.
В других редакторах или при других настройках
Visual Studio Code
подсказка мо
жет выглядеть несколько по-другому, поскольку существуют разные инструменты
для проверки правильности кода. Но в любом случае текст сообщения об ошибке
будет примерно таким же.
Инструмент Муру, как и другие утилиты для проверки кода, могут работать и вне
редактора кода через командную строку. Давайте посмотрим, как им пользоваться.
Знакомство с Муру
Муру
-
один из наиболее известных инструментов для проверки кода (их еще на
зывают линтерами, от англ.
linter),
он написан на
мает участие сам Гвидо ван Россум, создатель
Python, и в его разработке
Python. В отличие от многих
прини
других
более универсальных линтеров, Муру нацелен на поиск ошибок, связанных именно
с типами.
Для установки Муру используется
> python -m
р1р
pip:
install --user mypy
Если установка прошла успешно, то при вызове команд
mypy -v или:
python -m mypy -V
будет выведена информация об установленной версии Муру:
> python -m mypy -V
mypy 1.16.0 (compiled: no)
Проверим предыдущий пример (см. листинг
18.1)
с использованием Муру. Сначала
надо с помощью команды cd перейти в каталог, содержащий проверяемый скрипт.
Здесь и далее при работе с Муру подразумевается, что вместо команды mypy можно
выполнять команду
python -m mypy.
Проверим файл с помощью команды:
> mypy add_int.py
Первый запуск Муру может быть сравнительно долгим (единицы секунд), и по за
вершении его работы в каталоге со скриптом появится каталог .mypy_cache, предна
значенный для внутреннего использования Муру и для ускорения его повторного
запуска.
Муру выведет те же самые ошибки, которые мы видели на рис.
18.2:
add_int.py:5: error: Argument 1 to "add" has incompatiЫe type "str"; expected "int"
[arg-type]
add_int.py:5: error: Argument 2 to "add" has incompatiЫe type "str"; expected "int"
[arg-type]
Found 2 errors in 1 file (checked 1 source file)
332
Часть
11.
Основные подходы
При работе над большим проектом не обязательно проверять каждый файл по от
дельности. Вы можете передать в Муру в качестве параметра имя каталога с исход
ным кодом, и тогда Муру проверит всё дерево исходных кодов. В нашем случае,
если мы находимся в каталоге с примером, можно выполнить команду:
> python -m mypy .
Результат выполнения будет таким же. Если бы у нас было больше файлов с ошиб
ками, все они были бы упомянуты в выводе инструмента.
Указание простейших типов и коллекций
Теперь, когда мы увидели, для чего используются аннотации типов, продолжим
разбираться с тем, какие возможности имеются для описания ожидаемых типов пе
ременных.
Чем более аккуратно мы будем описывать ожидаемые типы, тем больше потенци
альных ошибок нам поможет выявить Муру или другой подобный инструмент. Од
нако, как мы скоро увидим, слишком дотошное описание типов может привести к
тому, что код будет становиться менее читаемым. В реальном проекте придется
искать компромисс.
В предыдущих примерах для параметров функции и возвращаемого значения мы
указывали единственный простейший тип
int. Аналогично мы можем указывать и
другие классы: str, float, complex и пр. Со списками, словарями и прочими кол
лекциями дела обстоят несколько сложнее, потому что там желательно указывать
тип хранимых значений, и про них мы тоже скоро поговорим.
Указывать тип можно не только в объявлении функции, но и в блоках кода, как по
казано в листинге
Листинг
18.2.
18.2. Chapter_18/example_02/typing_var.py
foo: int = 100
bar: str = "Привет"
baz: float = О. О
baz = input ( "Введите
print(baz / 2)
число:
")
В этом скрипте имеется ошибка. Мы указали, что переменная ьаz будет иметь тип
float, но затем присваиваем этой переменной строковое значение -
результат вы
зова функции input (). Скорее всего, здесь разработчик забыл преобразовать вве
денную пользователем строку к типу
float.
Если этот скрипт проверить с помощью Муру, то он сообщит об ошибке:
typing_var.py:5: error: IncompatiЫe types in assignment (expression has type "str",
has type "float") [assignment]
variaЫe
Глава
18. Аннотации типов
333
Для исправления ошибки проблемную строку можно записать в виде:
baz
=
float(input("Bвeдитe число:
"))
Инструменты вроде Муру сами определяют тип переменной по присваиваемому
значению, поэтому в большинстве случаев указывать аннотацию типа, когда он
может быть таким образом определен (говорят, что тип может быть выведен), не
обязательно.
В подобных ситуациях даже без явного указания типов Муру сообщит об ошибке в
последней строке, где переменная baz делится на
Листинг
2 (листинг 18.3).
18.3. Chapter_18/example_03/typing_var.py
baz = input
")
("Введите число:
print (baz / 2)
Ошибка будет выглядеть следующим образом:
typing_var.py:2: error: Unsupported operand types for / ("str" and "int")
[operator]
При присваивании полезно указывать аннотацию типа в тех случаях, когда тип пе
ременной будет меняться,
-
например, если изначально переменная равна None, а
затем ей может быть присвоено какое-либо другое значение.
Рассмотрим функцию equation () для решения квадратного уравнения (листинг
Листинг
18.4).
18.4. Chapter_18/example_04/equation.py
from math import sqrt
def equation
result
D=
Ь
(а,
Ь,
:
None
=
** 2 - 4
if D >=
с)
*а*
с
О:
xl =
(-Ь
+ sqrt (D)) /
*
а)
=
(-Ь
- sqrt(D)) / (2 *
а)
х2
result = (xl,
(2
х2)
return result
result_l = equation(l, 2, 3)
print (f" {result_ 1} ")
result_2 = equation(l.l, 2.5, -3.0)
print(f"(result_2}")
Функция equation ()
ожидает, что на вход будут подаваться переменные типа
float, а вернуть она может либо кортеж из двух чисел типа float, либо None.
Часть
334
11.
Основные подходы
Результат выполнения этого скрипта выглядит следующим образом:
None
(0.8682797337454499, -3.1410070064727225)
Добавим аннотации типов для параметров этой функции и для возвращаемого зна
чения. С параметрами функции всё просто, а вот с возвращаемым значением ситуа
ция интереснее. Чтобы описать тип кортежа, который содержит ровно два элемента
типа float, нужно написать tuple[float,
нет таким, как показано в листинге
Листинг
floatJ, и тогда описание функции ста
18.5.
18.5. Chapter_18/example_OS/equation.py
def equation(a: float, Ь: float,
result = None
D = Ь ** 2 - 4 * а * с
if D >= О:
xl = (-Ь + sqrt (D)) / (2
х2 = (-Ь - sqrt (D)) / (2
result = (xl, х2)
с:
float) -> tuple[float, float]:
* а)
* а)
return result
Однако Муру заметит, что, помимо кортежа, функция может возвращать также
None, и выведет ошибку по этому поводу:
equation. ру: 11: error: IncompatiЫe return value type (got "tuple [ float, float]
[return-value)
None", expected "tuple [float, float) ")
Нам надо указать, что
возвращаемое
значение
может быть или
floa t J, или None. Как это сделать, можно увидеть в подсказке Муру,
tuple [ f loat,
-
для указа
ния нескольких возможных типов инструмент предлагает воспользоваться
тором«
1
».
опера
С учетом этого объявление функции может выглядеть следующим обра
зом (листинг
Листинг
1
18.6).
18.6. Chapter_18/example_06/equation.py
def equation(a: float,
Ь:
float,
с:
float) -> tuple[float, float] 1 None:
Теперь Муру не к чему будет придраться, и он сообщит, что этот код не содержит
потенциальных ошибок.
В рассмотренном примере мы использовали формат аннотаций типов, который
появился относительно недавно (в
Python 3.9
и
ции equation () с расчетом на старые версии
типов
-
был бы более длинным (листинг
18. 7).
3.1 О). Если бы мы
Python, то он - с
писали код функ
учетом описания
Глава
18.
Листинг
335
Аннотации типов
18. 7. Chapter_18/example_07/equation_old_python.py
fran typing
jщюrt ТUple,
def equation(a: float,
Ь:
Union
float,
с:
float) ->
Union[ТUple[float,
float],
None]:
В более старых версиях
Python
для указания встроенных контейнерных типов надо
было импортировать из стандартного модуля
typing нужный тип - в нашем слу
Tuple (или его аналогов List и
чае тuple. Необходимость в использовании класса
Dict для списков и словарей соответственно) отпала, начиная с Python 3.9.
Помимо этого, до версии
Python 3.10
не существовало оператора
указания типов приходилось импортировать из того же модуля
и задействовать его так, как показано в листинге
18.7.
поэтому для
« »,
1
typing
класс
Union
В дальнейшем мы будем ис
пользовать только новые способы описания типов.
В нашей функции
equa tion () мы можем немного изменить описание возвращаемо
I None встречаются довольно часто, и в
модуле typing имеется специальный тип для такого случая optional, который
го типа. Аннотации типов вида имя_типа
используется следующим образом (листинг
Листинг
18.8).
18.8. Chapter_18/example_08/equation_optional.py
from typing import Optional
def equation(a: float,
Ь:
float,
с:
float) -> Optional[tuple[float, float]]:
Аннотация типа Optional показывает, что ожидается либо тип, указанный в квад
ратных скобках, либо
зовать всё же
None. Но в последних версиях Python рекомендуется исполь
аннотацию имя типа I None.
Приведем еще несколько примеров, которые покажут, как мы можем описывать
контейнерные типы: списки, множества, словари и кортежи, а заодно
-
какие еще
потенциальные ошибки можно выявлять с помощью аннотации типов. Начнем со
списков (листинг
Листинг
18. 9).
18.9. Chapter_18/example_09/list_error.py
foo: list[str] = []
foo. append ("Привет")
foo. append ( "typing")
#
Муру укажет
foo.append(42)
на
потенциальную ошибку
Часть
336
11.
Основные подходы
Ошибка, которую Муру выведет применительно к последней строке, будет выгля
деть следующим образом:
list_error.py:6: error: Argument 1 to "append" of "list" has
expected "str" [arg-type]
incompatiЫe
type "int";
Мы обещали, что список будет содержать строки, а добавили целое число.
Помимо того, что, благодаря указанию подсказок о типах, мы практически в реаль
ном времени во время набора кода можем получать от редактора кода сообщения
об ошибках, при использовании методов классов во время набора будут появляться
еще и подсказки, которые также уже учитывают ожидаемые типы. Например, на
рис.
показана
18.3
всплывающая
подсказка в
Visual Studio Code
для
метода
append (). Обратите внимание, что в ней указан ожидаемый тип str.
Рис.
18.3.
Подсказка для метода
append ()
класса
1 ist
с учетом аннотации типов
Аналогично мы можем указывать типы для множеств (листинг
Листинг
18.1 О).
18.10. Chapter_18/example_10/set_error.py
foo: set[str] = set()
foo. add ("Привет")
foo.add ("typing")
#
Муру укажет на потенциальную ошибку
foo.add(42)
Для словарей нужно указывать два типа: первый
чения (листинг
Листинг
18.11 ).
18.11. Chapter_18/example_11/dict_error.py
foo: dict[str, float] = {)
foo["pi"] = 3.14159
foo["e"] = 2.72
#
Муру укажет на потенциальную ошибку
fоо[б.28]
= "tau"
-
для ключа, второй
-
для зна
Глава
18.
Аннотации типов
337
Ранее мы уже видели, что для кортежей, в отличие от списков, нужно указывать,
сколько элементов будет в кортеже (листинг
Листмнr
18.12).
18.12. Chapter_181example_12/tuple_error.py
foo: tuple[int, int] ;
foo ; (3, 4)
# Муру укажет на
foo ; (5, 6, 7)
(1, 2)
потенциальную оимбку
Однако, начиная с
Python 3.9,
с помощью объекта ellipsis (напомним, что этот
объект представляется в виде многоточия
« ... »)
можно указать, что количество
элементов кортежа может быть произвольным (листинг
18.13).
Листмнr 18.13. Chapter_18/example_13/tuple_ellipsis.py
foo: tuple[int, ... ] ;
foo ; (2, 3)
foo ; (4, 5, 6)
(1, 2)
# Муру укажет на потенциальную
foo ; (5, 6, "Привет")
оuмбку
В последней строке ошибка, по мнению Муру, заключается в том, что, согласно
аннотации типа, мы обещали, что кортеж будет содержать только целые числа, но
поместили в него еще и строку.
Если же мы хотим показать, что кортеж может содержать и целые числа, и строки,
то можем описать тип уже знакомым нам способом
-
с использованием оператора
« » (листинг 18.14).
1
Листмнr
18.14. Chapter_18/example_14/tuple_unlon.py
foo: tuple [int I str, ... ] ;
foo ; (2, 3, 4)
foo ; (5, 6, "Привет")
(1, 2)
В этом случае Муру ругаться не станет.
В модуле typing имеется также тип Any, который обозначает, что в этом месте мо
жет использоваться любой тип (листинг
Листинг
18.15).
18.15. Chapter_18/example_15/dict_any.py
from typing import Any
foo: dict[str, Any] ;
foo[ 'bar'] ; 1
()
338
Часть
foo [ 'baz']
foo[ 'bam']
#
11.
Основные подходы
'hello'
[ 1, 2, 3]
Муру укажет на потенциальную ОIШ1бку
foo[l00] = "error"
В этом примере мы указываем, что словарь foo в качестве ключей должен исполь
зовать строки, а значения могут быть произвольного типа.
Обобщенные типы
До сих пор для переменных мы указывали конкретные типы (или их объединение),
однако из-за «утиной» типизации этого может оказаться недостаточно. Рассмотрим,
например, функцию, которая принимает на вход контейнер, и нам не важно, какого
конкретного типа этот контейнер,
-
главное, чтобы мы могли перебирать его со
держимое с помощью инструкции for. Мы не в состоянии указать все возможные
классы контейнеров, с которыми работает инструкция for, но можем определить,
какое поведение ожидаем от переменной.
Б стандартной библиотеке
Python,
а точнее, в стандартном модуле collections. аЬс,
содержится множество типов, с помощью которых можно описать подобные требо
вания к переменным. Фрагмент аЬс в названии модуля обозначает
Class
Abstract Base
(абстрактный базовый класс). Например, следующая функция в качестве па
раметра ожидает объект, который можно использовать в цикле for и при этом по
лучать значения типа float (листинг
Листинг 18.16.
from
18.16).
Chapter_18/example_16/iteraЫe.py
collections.aЬc iJ!q:юrt IteraЬle
def sum_squares(items:
result =
IteraЬle[float])
-> float:
О. О
for item in items:
result += item
* item
return result
foo
sum_squares ( [1, 2, 3])
bar
sum_squares ( (3, 4, 5))
baz
sum_squares ( (2, 4, 6})
Мы не будем получать никаких предупреждений от Муру, если станем передавать
в эту функцию списки, кортежи, множества, а также другие классы, которые реали
зуют нужное нам поведение.
Глава
18.
339
Аннотации типов
Вернемся к примеру из главы
11 (см.
листинг
11.1 ),
ции в качестве параметра другой функции (листинг
Листинг
где речь шла о передаче функ
18.17).
18.17. Chapter_18/example_17/calculate.py
def calculate(a, Ь, action):
result = action(a, Ь)
action name = action. name
print(f"{action_name} ({а}, {Ь})
return result
def add(a,
Ь):
return
а+ Ь
def mul(a,
Ь):
return
а* Ь
{result}")
foo = calculate(2, 3, add)
bar = calculate(2, 3, mul)
Как мы можем описать типы для этих функций? Для упрощения будем исходить из
предположения, что функции, передаваемые в качестве параметров, принимают на
вход аргументы типа
float и возвращают такой же тип. Для описания вызываемых
типов (а вызываемые типы
-
это не только функции, но и все классы, реализую
щие метод _call_ ()) в стандартном модуле collections. аЬс предусмотрен тип
CallaЫe, который мы можем применить следующим образом (листинг
Листинг 18.18.
fran
18.18).
Chapter_18/example_18/calculate_callaЫe.py
collections.aЬc
iuport
C&llaЬle
def calculate(a: float,
Ь: float,
action: callaЬle[[float, float], float]) -> float:
result = action(a, Ь)
action name = action. name
print(f"{action_name) ({а}, {Ь))
{result)")
return result
def add(a: float,
Ь:
float) -> float: return
а
+
ь
def mul(a: float,
Ь:
float) -> float: return
а
*
ь
foo
bar
calculate(2, 3, add)
calculate(2, 3, mul)
При использовании типа callaЫe мы указали, что передаваемый в функцию объект
должен быть вызываемым, принимать два параметра типа float: CallaЫe[ [float,
Часть
340
11.
Основные подходы
float], ... ] и возвращать тоже тип float: callaЫe [ [ ... ], float]. Все остальные па
раметры функций у нас тоже типа float.
При таком описании типов Муру будет удовлетворен, но если мы добавим сле
дующую строку кода:
# Муру укажет на потенциальную ошибку
spam = calculate ("Hello, ", "typing", add)
то Муру укажет в ней ошибку (и будет прав), поскольку типы не удовлетворяют
аннотации.
В примере из листинга
18.18
для указания типа вызываемого объекта была приме
нена достаточно громоздкая конструкция
callaЫe
[ [float,
float],
float]. Ес
ли такая запись встречается однократно, в этом нет ничего страшного, но при по
стоянном использовании подобной аннотации типа может возникнуть желание до
бавить для нее псевдоним (листинг
Листинг
18.19. Chapter_18/example_19/calculate_typealias.py
from collections.abc import
Action
18.19).
= callaЬle[[float,
def calculate(a: float,
Ь:
CallaЫe
float], float]
float, action: Action) -> float:
В этом примере мы создали новый объект Action (рекомендуется имена типов на
чинать с заглавных букв), который является аннотацией типов, описывающей тип
CallaЫe[[float,
float], float].
Для большей наглядности желательно указать, что Action -
это псевдоним анно
тации типа. Это можно сделать с помощью типа TypeAlias из модуля typing (лис
тинг
18.20).
Листинг
18.20. Chapter_18/example_20/calculate_typealias.py
from collections.abc import CallaЫe
fran typing i.mport ТypeAlias
Action:
ТypeAlias
=
CallaЬle[
def calculate(a: float,
В
Python 3 .12
Ь:
[float, float], float]
float, action: Action) -> float:
для указания того, что мы создаем новую аннотацию типа, появилось
новое ключевое слово t уре (листинг
18.21 ).
Глава
18. Аннотации типов
Лмстмнr 18.21.
from
Chapter_18/example_21/calculate_type.py
collections.aЬc
type Action
341
import
CallaЬle[
=
def calculate(a: float,
CallaЫe
[float, float], float]
Ь:
float, action: Action) -> float:
Рассмотрим теперь такой пример (листинг
Лмстмнr
18.22).
18.22. Chapter_18/example_22/add.py
def add (а,
return
Ь)
:
а+ Ь
foo = add(l0, 20.5)
bar = add ("Привет, ", "typing")
spam = add ( "Привет, " , 42)
Первые два вызова функции add ( ) корректные, а в последней строке
-
явная
ошибка, которая приведет к исключению во время выполнения скрипта. Можем ли
мы так расставить аннотации типов, чтобы Муру обратил наше внимание на эту
ошибку? Первое, что приходит на ум,
-
указать, что параметры а и ь могут быть
числами с плавающей точкой или строками (листинг
Листинг
18.23).
18.23. Chapter_18/example_23/add.py
def add(a: float I str,
return а+ Ь
Ь:
float I str) -> float I str:
foo = add(l0, 20.5)
bar = аdd("Привет, ", "typing")
spam = аdd("Привет, ", 42)
В этом случае Муру будет ругаться, но совершенно не на ту строку, где бы нам хо
телось:
add.py:2: error: Unsupported operand types for + ("float" and "str")
add.py:2: error: Unsupported operand types for + ("str" and "float")
add.py:2: note: Both left and right operands are unions
Муру укажет на ошибку во второй строке: return а + ь
-
[operator]
[operator]
ведь мы не указали, что
переменные а и ь должны быть одновременно или float, или str.
Для решения этой проблемы предназначены переменные типа
(type
variaЫe). С их
помощью можно описывать достаточно сложные типы, в том числе и обобщенные
типы
(generics).
typing.
Для создания переменной типа используется тип TypeVar из модуля
Часть
342
В следующем примере (листинг
18.24)
11.
Основные подходы
показано, как можно создать переменную
типа, которая является или строкой, или числом с плавающей точкой, но не может
быть и тем и другим одновременно (в отличие от объединения, которое создается
оператором
« »).
1
[ Листинг 18.24. Chapter_18/example_24/add_typevar.py
from typing ilrport
Т
=
ТypeVar("T",
def add(a:
return
Т, Ь:
ТypeVar
float, str)
Т)
->
Т:
а+ Ь
foo = add(l0, 20.5)
bar = аdd("Привет, ", "typing")
spam = аdd("Привет, ", 42)
Обратите внимание, что первым параметром TypeVar должна быть строка с именем
типа, который мы создаем (какой переменной присваиваем результат). В этом слу
чае мы добиваемся желаемого
-
Муру будет удовлетворен объявлением функции,
но при этом выведет ошибку для последней строки скрипта:
add_typevar.py:10: error: Value of type
[type-var]
variaЫe "Т"
Ье
of "add" cannot
"object"
Если у нас такой тип т используется только в одной функции, то мы можем не соз
давать его отдельно от функции, а сделать функцию обобщенной и описать требо
вания к типу непосредственно в объявлении функции. Тогда пример из листинга
можно переписать следующим образом (листинг
Листинг
18.24
18.25).
18.25. Chapter_18/example_25/add_typevar.py
def add[T: (str, float)]
return а+ Ь
(а:
Т,
Ь:
Т)
->
Т:
foo = add(l0, 20.5)
bar = аdd("Привет, ", "typing")
spam = add ("Привет, ", 42)
Здесь тип т описан непосредственно внутри объявления функции
-
после ее имени
в квадратных скобках. В этом случае Муру или другой подобный инструмент для
проверки кода также укажет на ошибку в последней строке скрипта.
Заключение
В этой главе мы рассмотрели основы аннотации типов для указания ожидаемых
типов для переменных, аргументов функций и значений, которые функции возвра-
Глава
18.
Аннотации типов
343
щают. Аннотация типов не влияет на выполнение программы, но позволяет при
использовании специализированных редакторов кода или отдельных инструментов
проверки кода обнаруживать потенциальные ошибки без выполнения программы.
Однако при этом указание типов делает код более многословным, поэтому нужно
соблюдать баланс между точным указанием типов и читаемостью кода. Мы рас
смотрели далеко не все возможности для всё более точного указания типов, но
многие из этих возможностей требуются, скорее, при создании библиотек, а не для
прикладных программ.
В этой главе для выявления ошибок мы использовали инструмент Муру, однако
- на
pyright от компании Microsoft 1 или активно развиваемый ruff2, написанный
языке Rust. Все они также могут использоваться вместе с Visual Studio Code или
кроме него существуют и другие инструменты, выполняющие ту же задачу,
пример,
на
другими редакторами кода.
Мы рассмотрели вопросы, связанные с тем, как указываются простейшие типы и
коллекции, а также затронули тему обобщенных типов.
В следующей главе нам предстоит разобраться с тем, что такое исключения, о ко
торых мы периодически упоминали в предыдущих главах, как их обрабатывать и
создавать свои исключения.
1 См.
2
https://github.com/microsoft/pyright.
См. https://docs.astral.sh/ruff/.
- ГЛАВА 19-
Qбработка исключений
Обработка ошибок без использования исключений
Существует несколько подходов для обработки ошибок, возникающих в процессе
работы программы. Под ошибками мы будем понимать не ошибки в логике самой
программы (баги), а те ошибки, возникновение которых для нас не является неожи
данностью, и программа должна на них как-то предсказуемым образом отреагиро
вать. Например, если пользователь вводит имя файла, но при попытке его открыть
оказывается, что этого файла не существует. Или пользователя просят ввести ка
кое-то число, а он вводит строку, которая в число не преобразуется. Существуют
более сложные ошибки, например, когда мы обращаемся к неб-сервису или удален
ной базе данных для получения или изменения какой-то информации, но в этот мо
мент оказывается, что связь с сервером потеряна. Хорошая программа должна
не только предполагать, что все такие и многие подобные ситуации будут периоди
чески случаться, но при этом оповещать пользователя об этих ошибках и предла
гать варианты решения.
Проблема заключается в том, что ошибки часто возникают на достаточно глубоком
уровне вложенности
-
внутри функции, которую вызывает другая функция, кото
рую, в свою очередь, вызывает еще одна функция, и так далее еще несколько раз.
При таком уровне вложенности информацию об ошибке нужно передавать с самого
нижнего уровня, где приложение общается с удаленным сервером или читает файл,
на самый верхний, отвечающий за вывод результата работы для пользователя.
В языках программирования, где нет механизма перехвата и обработки исключе
ний, каждая вложенная функция должна, помимо основного результата работы,
возвращать дополнительное значение, сообщающее, что что-то пошло не так, а еще
лучше
-
какую-то дополнительную информацию о том, что конкретно случилось.
Функция, получившая информацию об ошибке от вызываемой функции, если она
не в состоянии обработать такую ошибку, должна сама вернуть информацию об
этой же ошибке своей вызывающей функции, а та, в свою очередь, своей. И так,
пока ошибка не дойдет до того уровня, где ее смогут адекватно обработать и пред
ложить решение.
Такой подход реализован в коде примера из листинга
19.1.
Глава
19.
Листинг
Обработка исключений
345
19.1. Chapter_19/example_01/equation_error.py
from math import sqrt
def equation(a, Ь, с)-> tuple[float, float ] 1 None:
D = Ь ** 2 - 4 *а* с
if D < О:
return None
xl = (-Ь + sqrt(D)) / (2 *
х2 = (-Ь - sqrt(D)) / (2 *
return (xl, х2)
а)
а)
def func_l(a, Ь, с) -> float I None:
result = func_2(a, Ь, с)
return result[O] + result[l] if result is not None else None
def func _ 2 (а, Ь, с) -> tuple [ float, floatJ I None:
result = equation(a, Ь, с)
return (result[O ] * 2, result[l ] * 2) if result is not None else None
if
name
" main
= 1
Ь = 2
с= 10
result = func_l(a,
if result is None:
11
•
а
Ь,
с)
рrint("Ошибка вычисления.")
else:
рrint("Результат равен",
result)
В этом примере ошибка может возникнуть в функции equation (), но отреагиро
вать на нее мы можем только в основном коде скрипта, отобразив сообщение об
ошибке или результат вычислений. При таком подходе каждая вышестоящая функ
ция вынуждена проверять результат, полученный от вызываемой функции, даже
если заведомо известно, что эта функция не сможет никак обработать ошибку и
будет вынуждена ее возвращать выше еще на один уровень вызовов. Именно эту
проблему решают исключения.
Что такое исключения, как и зачем их ловить?
Исключение (exception)-этo объект, который в случае ошибки, возникающей где-то
глубоко в иерархии вызовов функций, будет проброшен из места возникновения
ошибки в вызываемой функции в вызывающую, а затем, если там исключение не
будет обработано, оно будет проброшено еще выше в цепочке вызовов, и, если по
надобится, то еще и еще, и так до тех пор, пока на каком-то уровне приложения это
исключение не будет перехвачено и обработано.
Часть
346
11.
Основные подходы
Передачей исключения от вызываемой функции в вызывающую занимается интер
претатор, а не программист. Разработчику требуется где-нибудь в цепочке вызовов,
где становится понятно, как реагировать на ошибку, исключение перехватить и об
работать. Все промежуточные функции, если они не в состоянии обработать ис
ключение, могут делать вид,
что этого исключения
не существует,
а вызываемая
функция всегда возвращает ожидаемое значение.
Перепишем пример из листинга
19.1
таким образом, чтобы он использовал исклю
чение вместо возврата ошибки, и на нем разберемся, как работать с исключениями
(листинг
Листинг
19.2).
19.2. Chapter_19/example_02/equatlon_exceptlon.py
from math import sqrt
def equation(a, Ь, с) -> tuple[float, float]:
D = Ь ** 2 - 4 *а* с
if D < О:
raiae ValueError ( "ДксlСрИИИНU'l' N8И1о1118
xl = (-Ь + sqrt(D)) / (2 * а)
х2 = (-Ь - sqrt(D)) / (2 * а)
return (xl, х2)
кум.
")
def func_l(a, Ь, с) -> float:
result = func_2(a, Ь, с)
return result[O] + result[l]
def func_2(a, Ь, с) -> tuple[float, float]:
result = equation(a, Ь, с)
return (result[O] * 2, result[l] * 2)
if
main "·
name
=1
Ь = 2
с= 10
try:
result = func_l(a,
11
а
Ь,
с)
рrint("Результат равен",
result)
except ValueError as error:
рrint("Ошибка вычисления.",
error)
Результатом выполнения этого скрипта будет строка:
Ошибка вычисления.
Дискриминант меньше нуля
Благодаря использованию исключения, мы мы не только сумели сократить код
функций и корректно обработать ошибку, но еще и передали информацию о том,
что именно случилось.
Глава
19.
Обработка исключений
347
Рассмотрим, как этот скрипт работает, и начнем с конца. В основном теле скрипта
мы встречаем не знакомую до сих пор конструкцию:
try:
result = func_l(a, Ь, с)
print ( "Результат равен", resul t)
except ValueError as error:
print ( "Ошибка вычисления.", error)
Такая запись обозначает следующее: мы предполагаем, что внутри блока между
ключевыми словами try и except может быть возбуждено (иногда говорят «бро
шено») исключение. От встроенной функции pr in t ()
ошибки,
значит,
мы не ожидаем никакой
исключение может быть возбуждено где-то
внутри функции
func _ 1 () или внутри функций, которые она вызывает. Нам не важно, на каком
именно уровне вложенности функции исключение будет возбуждено на самом деле,
-
оно будет подниматься всё выше по уровню вложенности кода, пока где-нибудь не
будет перехвачено.
За блоком кода try следует блок кода except. После ключевого слова except мы
указали,
что ожидаем исключение класса
ValueError
(здесь наследование также важно учитывать),
-
или производного
от него
все остальные исключения пере
хвачены не будут. В инструкции except после ключевого слова as указано имя пе
ременной, которая будет ссылаться на объект исключения соответствующего типа,
если оно возникнет. Позже мы увидим, как можно перехватывать исключения раз
ных типов.
Если внутри функции func_l () или далее в цепочке вызовов функций будет возбу
ждено исключение ValueError, которое не будет обработано нигде внутри функции
func_l () и вырвется из нее наружу, то выполнение блока try прервется, следую
result) уже не будет вызвана, а управле
ние передано в блок except, который перехватывает исключение ValueError.
щая строка print ("Результат равен",
Внутри этого блока мы выводим информацию об ошибке, а дальше продолжает
выполняться код после блоков try
... except. В нашем случае скрипт просто за
вершается.
Теперь рассмотрим путь исключения с момента возбуждения. Ошибка возникает
внутри функции equation (), но вместо того, чтобы вернуть значение, которое обо
значало бы ошибку, внутри этой функции создается экземпляр класса ValueError и
с помощью инструкции raise этот объект «бросается» как исключение. В этом
месте интерпретатор прерывает выполнение кода функции equa tion ().
Если внутри функции строка с инструкцией raise не входит в блок try, за которым
следует except с ожиданием именно этого класса исключения (или производного
от него), функция прерывается (заметьте, что в этом случае она не возвращает ни
какого значения), и управление передается в функцию, вызвавшую функцию, кото
рая возбудила исключение. В нашем случае это функция
func_2 (). Она могла бы
ожидать возбужденное исключение, и тогда вызов функции equation () находился
бы внутри блока try, за которым следовал бы блок except с указанием, что здесь
Часть
348
ожидается исключение типа
11.
Основные подходы
ValueError. Однако в нашем случае функция func_2 ()
не перехватывает это исключение, поэтому ее вызов также прерывается
(result [О]
(строка
2) не выполняется, и функция ничего не
возвращает), а управление передается еще выше в функцию func _ 1 (), и там
return
2,
*
result [1]
*
происходит то же самое: эта функция не обрабатывает возникшее исключение, вы
полнение
ее
также
прерывается,
и
управление
передается,
наконец,
основному
скрипту, где исключение ловится и выводится информация об ошибке.
Таким образом, мы перехватываем исключение только там, где нам удобнее всего
его обрабатывать. Однако возникает вопрос, а что было бы, если бы мы не перехва
тили исключение и на уровне основного скr,:шта? На самом деле мы уже сталкива
лись с таким поведением в предыдущих главах, когда говорили, что в той или иной
ситуации будет возбуждено исключение. Тогда для нас это было равносильно тому,
что скрипт «падает» и выводит информацию о том, где и по какой причине про
изошла ошибка. Изменим основное тело скрипта, убрав всякое упоминание об об
работке исключений (листинг
19.3).
Листинг 19.3. Chapter_19/example_OЗ/equation_exceptlon.py
if
name
а =
Ь
=
11
main
":
1
2
10
result = func_l(a,
с=
Ь,
с)
рrint("Результат равен",
result)
Запустив этот скрипт, в консоли мы увидим следующий результат:
Traceback (most recent call last):
File " ... /equation_exception.py", line 23, in <module>
result = func_l(a, Ь, с)
File " ... /equation_exception.py", line 12, in func 1
result = func_2(a, Ь, с)
File " ... /equation_exception.py", line 16, in func 2
result = equation(a, Ь, с)
File " ... /equation_exception.py", line 6, in equation
raise VаluеЕrrоr("Дискриминант меньше нуля.")
ValueError: Дискриминант меньше нуля.
Выполнение скрипта прервалось на строке вызова func _ 1 (), а в консоль вывелось
то, что называется термином «стек вызовою)
(call stack).
Стек вызовов позволяет
отследить с точностью до строки, где было возбуждено исключение, и как оно «пу
тешествовалm) по вложенным функциям. В нашем случае мы видим, что исключе
ние
было
создано
на
6-й
строке
файла
equation_exception.py
внутри
функции
equa tion (), а в это место мы попали из функции func _ 2 (), вызов функции
equation () описан на 16-й строке, функцию func_2 () вызвали из функции
Глава
19.
Обработка исключений
349
func_l () на 12-й строке и так далее, пока дело не дошло до основного скрипта, где
на 23-й строке была вызвана функция func 1 () . Дальше поднимать исключение
уже некуда, его нигде не обработали, поэтому интерпретатору ничего не осталось,
как прервать выполнение скрипта и вывести показанное сообщение об ошибке.
Итак, мы рассмотрели основную идею возбуждения и обработки исключений, те
перь погрузимся в эту тему более глубоко.
Перехват исключений
В этом разделе мы рассмотрим различные варианты перехвата исключений. Для
последующих экспериментов мы не станем использовать столь громоздкий код,
показанный ранее, а напишем более компактные примеры.
Начнем с примера, демонстрирующего, что исключение может быть возбуждено и
обработано в пределах одной функции или блока кода (листинг
19.4).
[ ~и;rинг 19.4. C~apter_10/example_04/try_except.py
if
name
=="
main
":
try:
print("Дo возбуждения исключения.")
raise ValueError("Cooбщeниe для ис1ШЮЧения.")
print ("Эта строка никогда не будет вьmолняться. ")
except ValueError:
рrint("Возникло исключение ValueError.")
print("Пocлe обработки исключения.")
Здесь мы и возбуждаем исключение valueError, и тут же его перехватываем. Этот
же пример показывает, что мы можем не создавать переменную для объекта ис
ключения в инструкции перехвата
except,
если нас не интересуют данные, которые
можно получить от этого объекта, а важен только сам факт возникновения исклю
чения. Поскольку мы обработали возникшее исключение, то код продолжает бла
гополучно выполняться после конструкции
try . . . except,
и в результате в кон
соль будет выведен следующий текст:
До возбуждения исключения.
Возникло исключение
После
ValueError.
обработки исключения.
Теперь рассмотрим ситуацию, когда внутри блока могут возбуждаться различные
исключения, и эти исключения мы хотим обрабатывать по-разному. Напишем для
этого функцию, рассчитывающую длину волны по частоте сигнала. Внутри этой
функции будут осуществляться две проверки: частота должна быть передана в виде
числа, и это число должно быть положительным (листинг
19.5).
Часть
350
Листинг
11.
Основные подходы
19.5. Chapter_19/example_OS/wavelength.py
def wavelength(frequency, speed=Зe8):
if not isinstance (frequency, (int, float)):
raise ТypeError ("Частота должна быть числом")
if frequency <= О:
raise ValueError ( "Частота должна быть положительной")
return speed / frequency
if
name
==" main "·
frequency = "10 ГГц"
# frequency = -10е9
try:
wl = wavelength(frequency)
print (f"Д.пина волны: {wl} м")
except ТypeError аа err:
print("TypeError.", err}
except ValueError аа err:
print("ValueError.", err)
print("Пocлe обработки исключения.")
В этом примере мы использовали новую для нас форму вызова встроенной функ
ции
isinstance (), позволяющую проверить, является ли первый ее параметр эк
земпляром класса одного
из нескольких типов,
переданных в кортеже в качестве
второго параметра. Функция
чений:
TypeError
или
wavelength () может возбуждать одно из двух исклю
ValueError - в зависимости от параметра frequency.
В основном коде добавлены два блока
except, каждый из которых обрабатывает
одно или второе из упомянутых исключений. В зависимости от значения перемен
ной frequency, в результате работы этого скрипта в консоль будет выведен либо
такой текст:
TypeError.
Частота должна быть числом
После обработки исключения.
либо такой:
ValueError.
После
Частота должна быть положительной
обработки исключения.
Иногда мы хотим обрабатывать ожидаемые исключения одинаково, и тогда, чтобы
не писать один и тот же код в нескольких блоках except, мы можем указать в од
ном блоке
except сразу несколько типов исключений (листинг 19.6).
Листинr 19.6. Chapter_19/ex1mple_061wavtlength.py
if
name
main
"·
Глава
19.
Обработка исключений
frequency = "10
# frequency =
351
ГГц"
-10е9
try:
wl = wavelength(frequency)
print (f"Длина волны: {wl} м")
except (ТypeError, ValueError) as err:
print ("Что-то пошло не так.", err)
print("Пocлe обработки исключения.")
Как можно здесь видеть, при возникновении исключения типа TypeError или
ValueError переменная err будет ссылаться на объект одного из этих типов. При
необходимости можно добавить еще блоки except, которые будут перехватывать
другие исключения.
Случаются ситуации, когда требуется поймать исключение, выполнить какое-то
действие (например, добавить в лог работы информацию о том, что исключение
произошло), а затем повторно возбудить то же исключение, чтобы оно продолжило
распространяться дальше по цепочке вызовов функций. Для этого в блоке except
можно выполнить инструкцию
Листинг
raise без указания объекта исключения (листинг 19.7).
19.7. Chapter_19/example_07/raise_repeat.py
def wavelength(frequency, speed=ЗeB):
try:
i f not isinstance (frequency, (int, float)) :
raise ТуреЕrrоr("Частота должна быть числом")
if frequency <= О:
raise ValueError ( "Частота должна быть положительной")
except (TypeError, ValueError) as err:
рrint("Ошибка при вызове функции wavelength() .", err)
raise
return speed / frequency
if
name
== " main "·
frequency = "10 ГГц"
# frequency = -10е9
try:
wl = wavelength(frequency)
print (f"Длина волны: {wl} м")
except (TypeError, ValueError) as err:
print ("Что-то пошло не так.", err)
print("Пocлe обработки исключения.")
352
Часть
11.
Основные подходы
В процессе выполнения этого скрипта исключение ValueError сначала будет пере
хвачено внутри функции wavelength (), затем
ции raise -
-
в результате выполнения опера
это же исключение будет возбуждено повторно, после чего оно будет
еще раз перехвачено в основном коде скрипта. В результате в консоль будет выве
ден следующий текст:
Ошибка nри вызове функции
Что-то пошло не так.
wavelength().
Частота должна быть числом
Частота должна быть числом
После обработки исключения.
Пользовательские исключения.
Наследование исключений
До сих пор в качестве примеров мы возбуждали только стандартные исключения
-
их достаточно много, и в большинстве случаев можно задействовать именно их,
если они описывают нашу ошибку. Например, исключение ValueError использу
ется при получении неправильных значений (в частности, параметров функций),
для оповещения об ошибках, связанных с
ArithmeticError -
вычислениями,
FileNotFoundError возбуждается при попытке открыть несуществующий файл.
Однако мы можем создавать свои классы исключений, для чего нужно ознакомить
ся с правилами, по которым они создаются.
Все исключения должны быть производными от класса BaseException, но непо
средственно
ний,
-
этому
классу
наследуют
очень
ограниченное
количество
исключе
те, что имеют отношение к системным событиям или даже не являются
признаком ошибки, а используются для организации работы некоторых механиз
мов
Python.
Все остальные классы исключений являются производными от класса
Exception, который, в свою очередь, является производным от BaseException.
Исключения, которые вы будете создавать, с вероятностью
изводными от класса
Exception
99%
должны быть про
или его дочерних классов.
В простейшем случае класс исключения может выглядеть следующим образом
(листинг
19.8).
Листинг 19.8. Chapter_19/example_08/myexception.py
class
МyException(Exception):
def raise_error():
raise
if
МyException ( "Это МyException.
name
==" main
try:
raise_ error ()
except
":
МyException аз
print ( "Что-то
")
err:
поl!IПо не так.",
err)
Глава
19.
353
Обработка исключений
В классе MyException объект ellipsis
« ... »можно
заменить на инструкцию pass.
Если от класса исключений не требуется хранить никакую дополнительную ин
формацию, то подобного класса исключения вполне достаточно.
Во многих задачах новые исключения создаются только для того, чтобы более точ
но описать возникшую проблему. Однако часто, помимо сообщения об ошибке,
исключение должно хранить еще дополнительную информацию о том, что именно
привело к возникновению ошибки. Например, это может быть имя файла, который
не удалось открыть, или неправильное значение параметра.
Прежде всего, вы можете ничего не добавлять в классе исключения, но при его воз
буждении передать в конструктор несколько параметров. Все параметры, передан
ные в конструктор исключения, хранятся в виде кортежа в поле
использовать эти данные в блоке except (листинг
args,
и мы можем
19.9).
class MyException(Exception):
def raise_error(filename, value):
raise
if
filename, value)
МуЕхсерtiоn("Это МyException.",
name
main "·
try:
raise_error("invalid.txt", 42)
except MyException as err:
print ("Что-то поrwю не так.")
print(f"Cooбщeниe:
{err.args[O] }")
print ( f"Имя файла: {err. args [l] }")
print ( f"Значение: {err. ar9s [2] }")
print (err)
Результатом выполнения этого скрипта будет следующий текст в консоли:
Что-то
ПОl!!ЛО
Сообщение:
не
так.
Это
MyException.
Имя файла: invalid.txt
Значение: 42
('Это MyException. ', 'invalid. txt', 42)
Обратите внимание, что последний вызов функции print () применительно к объ
екту исключения вывел именно кортеж
args.
Однако такой способ не очень удобен тем, что нужно помнить, в каком порядке
хранятся данные в поле args. Чтобы упростить использование класса исключения,
в него можно добавить новые поля, а также определить конструктор таким обра
зом, чтобы он принимал строго определенное количество параметров. Предыдущий
пример можно переписать следующим образом (листинг
19.10).
354
Часть
Листинг 19.1 О.
11.
Основные подходы
Chapter_19/example_10/myexception_params.py
class MyException(Exception):
def
init (self, message:str, filename:str, value:int):
super(). init (message, filename, value)
self.message = message
self.filename = filename
self.value = value
def raise_error(filename, value):
raise MyException("Этo MyException.", filename, value)
if
name
main "·
try:
raise_error("invalid.txt", 42)
except MyException as err:
print ("Что-то пошпо не так.")
print ( f"Сообщение: {err .message} ")
print(f"Имя файла: (err.filename}")
рrint(f"Значение: (err.value}")
print(err)
Результат выполнения этого скрипта не изменится, но пользоваться таким классом
исключения стало проще.
Ранее вскользь упоминалось, что инструкция except перехватывает не только те
классы исключений, которые указаны после этого ключевого слова, но и исключе
ния, производные от указанного класса.
Такое поведение весьма удобно для тонкой настройки перехватываемых исключе
ний. Например, у нас может быть класс MyAppException -
базовый класс для всех
исключений, относящихся к нашему приложению, а от него мы можем создавать
производные классы:
EquationException -
которые станем возбуждать в случае
ошибки решения квадратного уравнения, или TrigonometryException -
на случай
тригонометрических ошибок (например, если мы пытаемся рассчитать арксинус
значения больше
1,
но решение должно быть действительным числом). Где-то в
коде мы можем ловить либо
EquationException, либо TrigonometryException, а
где-то можем перехватывать MyAppException, и в блок except будем попадать при
любом исключении, связанном с нашим приложением, но не при стандартных ис
ключениях.
Напишем абстрактный пример с использованием наследования исключений (лис
тинг
19.11).
Листинг
19.11. Chapter_19/example_11/myappexception.py
class MyAppException(Exception): ...
class FooException(MyAppException): ...
Глава
19.
355
Обработка исключений
class BarException(MyAppException):
if
name
exceptions
main
"·
[FooException("Этo
BarException("Этo
FooException."),
BarException.")]
for exception in exceptions:
try:
raise exception
except FooException as err:
рrint("Поймано FooException.", err)
except MyAppException as err:
рrint("Поймано MyAppException.", err)
В этом примере по очереди возбуждаются два исключения, и оба они производные
от класса MyAppException. У нас есть здесь два блока except -
первый перехваты
вает только исключения FooException (или производные от него, но у нас таких
классов нет), а второй
-
все исключения, производные от MyAppException. Поэто
му первое исключение будет обработано в первом блоке except, а второе
-
во
втором, и мы получим следующий результат выполнения:
Поймано
Поймано
FooException. Это FooException.
MyAppException. Это BarException.
Обратите внимание, что блок, обрабатывающий исключения MyAppException, в ко
де
этого
примера
расположен
после
блока,
обрабатывающего
исключения
FooException. Порядок следования блоков except важен, поскольку после возник
новения исключения интерпретатор начинает искать подходящий блок except
сверху вниз и останавливается на первом таком блоке. Поэтому, если бы блок, об
рабатывающий MyAppException, стоял первым, то он всегда обрабатывал бы ис
ключения, производные от этого класса, а до блока, обрабатывающего FooException,
выполнение никогда бы не дошло (листинг
19.12).
~стинг 19.12. Chapter_19/ex~mple~12/myappexception.py
class MyAppException(Exception): ...
class FooException(MyAppException):
class BarException(MyAppException):
if
name
exceptions
11
main
"·
[FooException ( "Это FooException. ") ,
BarException("Этo BarException.")]
for exception in exceptions:
try:
raise exception
356
Часть
11.
Основные подходы
except MyAppException as err:
рrint("Поймано MyAppException.", err)
except FooException as err:
рrint("Поймано FooException.", err)
Этот скрипт выведет следующий результат:
Поймано
Поймано
MyAppException.
MyAppException.
Это
Это
FooException.
BarException.
Поскольку большинство исключений являются производными от класса Exception
или его потомков, а сам он является производным от вaseException, то получается,
что, перехватывая исключения типа Exception, мы можем ловить большинство
ошибок. А перехватывая BaseException, -
отслеживать и более низкоуровневые
исключения. Однако перехват всех исключений
-
это не всегда хорошая идея.
Лучше обрабатывать исключения только того типа, для которого вы знаете, как по
ступать в случае возникновения этой ошибки, и у вас есть способ восстановления
после нее. В некоторых приложениях, работающих с критическими данными или
оборудованием, в случае непредвиденной ошибки лучше дать программе упасть,
чем продолжить работу с неопределенным состоянием. Иногда имеет смысл пере
хватывать все ошибки, чтобы занести их в журнал (лог) работы, а затем возбудить
исключение повторно, чтобы им занялся вышестоящий код.
Вернемся к нашему примеру с решением квадратного уравнения (см. листинг
19.2).
Изменим его таким образом, чтобы в случае отрицательного дискриминанта возбу
ждалось созданное нами исключение
EquationError,
дадим пользователю возмож
ность вводить коэффициенты квадратного уравнения через консоль, а затем будем
обрабатывать возможные ошибки (листинг
Листинr
19 .13 ).
19.13. Chapter_19/example_13/equation_errors.py
from math import sqrt
class EquationError(Exception):
def equation(a, Ь, с) -> tuple[float, float):
D = Ь ** 2 - 4 *а* с
if D < О:
raise EquationError("ДИcxpиминall'l' меньше
xl = (-Ь + sqrt(D)) / (2 * а)
х2 = (-Ь - sqrt(D)) / (2 * а)
return (xl, х2)
if
name
=="
main
нуля.")
"·
print("Peшeниe уравнения вида ахл2
+
Ьх +с=
0")
try:
input_str = inрut("Введите а, Ь и с через пробел: ")
values = [float(val) for val in input_str.split(" "))
Глава
19.
Обработка исключений
357
result = equation(values[O], values[l], values[2])
result)
except EquationError аз err:
print ("Ошибка решения уравнения.", err)
except Exception аз err:
print ("Ошибка вычисления.", err, type (err))
except БaseException аз err:
рriпt("\nЧто-то пошло не так.", type(err))
рrint("Результат вычисления:",
В этом примере последовательно обрабатываются ожидаемые исключения, начиная
с самого частного случая, когда дискриминант отрицательный, и возникает исклю
чение
EquationError. В ветку, связанную с классом Exception, мы попадем, на
пример, если пользователь введет меньше трех чисел или вместо чисел введет сим
волы,
которые нельзя
преобразовать в
число.
В
ветку,
связанную с
классом
BaseException, мы можем попасть, если вместо ввода коэффициентов уравнения
нажмем комбинацию клавиш
<Ctrl>+<C>, что
означает прерывание работы скрипта.
Далее показано несколько вариантов развития событий, связанных с различными
ошибками:
> python example_l9_13.py
+
Решение уравнения вида ахл2
Введите а,
Ь и с через
пробел:
Ошибка решения уравнения.
>
pythoп
example 19
+с= О
1 2 10
дискриминант меньше нуля.
13.ру
+
+ с
О
1 2
list index out of range <class 'IndexError'>
Решение уравнения вида ахл2
Введите а,
Ьх
Ьх
Ь и с через пробел:
Ошибка вычисления.
> python example_19_13.py
+
Решение уравнения вида ахл2
Введите а,
Ь и с через пробел:
Ошибка вычисления.
Ьх +с= О
привет
could not convert string to float:
'привет'
<class 'ValueError'>
> python example_19_13.py
+
Решение уравнения вида ахл2
Введите а,
Ь и с через пробел:
Что-то пошло не так.
Ьх +с= О
лс
<class 'Keyboardinterrupt'>
В качестве дальнейшего улучшения можно было бы добавить обработку исключе
ний
и
других
классов
-
например,
присутствующих
здесь
IndexError
и
ValueError, и при их возникновении сообщать пользователю больше информации
об ошибке: что именно случилось, и как это можно исправить.
Обратите внимание на последний случай с использованием прерывания нажатием
комбинации
клавиш
<Ctrl>+<C>.
Пользователь
хочет
немедленно
остановить
скрипт, но мы ему не даем это сделать, а выполняем вместо этого какие-то дейст-
Часть
358
11.
Основные подходы
вия. В нашем случае скрипт после этого все равно завершается, но мы могли бы
помешать пользователю выйти из скрипта. Так лучше не поступать.
В
Python
есть специальный синтаксис для ситуации, когда нужно перехватывать
все исключения. Для этого можно использовать ключевое слово
except без указа
ния типа исключения. Разумеется, такой блок должен располагаться после всех ос
тальных блоков обработки исключений. Часть кода, отвечающую за обработку ис
ключений в предыдущем примере (см. листинг
следующим образом (листинг
Листинг
if
19.13),
можно было бы написать
19 .14 ).
19.14. Chapter_19/example_14/equation_errors.py
name
=="
main
"·
print("Peшeниe уравнения вида ахл2
+
Ьх
+
с
О")
try:
except EquationError as err:
print ("Ошибка решения уравнения.", err)
except Exception as err:
рrint("Ошибка вычисления.", err, type(err))
except:
print("\nЧтo-тo пошло не так.")
Результат работы этого скрипта не отличается от предыдущего.
Конструкция
try ... except ... else ... final/y
Теперь, когда мы разобрались с тем, как работает перехват исключений с помощью
конструкции
try ... except, нужно сказать, что у этой конструкции есть еще два
необязательных блока: else и fiпally.
Блок else выполняется в том случае, если внутри блока try никакое исключение не
было возбуждено, то есть код внутри try выполнился успешно.
Мы можем использовать ветвь else, переписав предыдущий пример решения квад
ратного уравнения (см. листинг
краткости кода в новом
варианте
обработка
примера
(листинг
19.13). Для наглядности и
19.15) оставлена только
исключений
Exception, но мы помним, что лучше обрабатывать более конкретные исключения.
Листинг
if
19.15. Chapter_19/example_15/equation_try_else.py
name
=="
main
":
print("Peшeниe уравнения вида
result = None
try:
input str
input
ахл2
("Введите а,
+
Ьх
+
с
О")
Ь и с через пробел:
")
Глава
19.
Обработка исключений
359
values = [float(val) for val in input_str.split(" ")]
result = equation(values[O], values[l], values[2])
except Exception as err:
рrint("Ошибка вычисления.", err, type(err))
else:
рrint("Результат вычисления:", result)
В этом коде мы вынесли вывод результата удачного расчета корней уравнения в блок
else. Может возникнуть вопрос: зачем нужен блок else, когда код, который в него
try после строк, которые могут возбуж
попадает, можно разместить в конце блока
дать исключения? Ведь если исключение не возбудится, этот код будет выполнен. Но
здесь есть одна тонкая деталь. Желательно помещать в блок
try только те строки ко
да, от которых мы ожидаем исключения. Так мы показываем, что код, расположен
ный вне блока try, не должен возбуждать исключения, или, если оно будет возбуж
дено, то текущая функция не знает, как его обрабатывать, и это исключение будет
передано выше по цепочке вызовов. Исключения, которые возбуждаются внутри
ветви
else, не будут обработаны никаким из блоков except, которые относятся к вы
try ... except ... else.
полняемой в текущий момент конструкции
Еще один необязательный блок, связанный с обработкой исключений,
-
это блок
finally. Он вызывается всегда - независимо от того, было возбуждено исключе
ние или нет. Блок finally обычно используется для освобождения ресурсов, кото
рые были выделены в блоке try. Это может быть закрытие файлов или соединений
с базой данных, очистка графических ресурсов и т. д.
Важно понимать, в какой момент будет вызван блок finally в различных случаях:
если исключение не было возбуждено, если исключение было возбуждено, пере
хвачено и обработано в блоке except, а также если исключение было возбуждено,
но не было перехвачено.
Блок
finally должен располагаться в самом конце конструкции try ... except ...
else ... finally, - после блока else, если он присутствует.
Начнем с самого простого случая, когда исключение не возбуждается (листинг
Листинг
19.16. Chapter_19/example_16/flnally_по_exceptlon.py
try:
print ( "BolllЛи
в блок
try. ")
try.")
рrint("Выходим из блока
except ValueError:
print("BolllЛИ в
блок
except.")
else:
print("BolllЛи в блок
finally:
print ( "BolllЛИ
в блок
рrint("Вьшши из блока
else.")
finally. ")
try ... except ... else ... finally.")
19.16).
Часть
360
11.
Основные подходы
В этом примере мы попадем в блок else, поскольку никакое исключение не возбу
ждается. В блок finally мы тоже должны попасть (туда мы всегда должны попа
дать), но только после выполнения кода в блоке else. В результате выполнения
этого скрипта в консоль будет выведен текст:
Вошпи в блок
try.
Выходим из блока
try.
Вошпи в блок
else.
Вошпи в блок
finally.
Вышпи из блока
try ... except ... else ... finally.
Следующий пример показывает случай, когда мы возбуждаем исключение, но пе
рехватываем его в блоке except (листинг
Листинr
19.17).
19.17. Chapter_19/example_17/finally_except.py
try:
print("Boшпи в блок
try.")
raise ValueError()
print ( "Выходим
из блока
try. ")
except ValueError:
print("Boшпи в блок
except.")
else:
print("Boшпи в блок
else.")
finally:
print("Boшпи в блок
print ( "Вышпи
из блока
finally.")
try ... except ... else ... finally. ")
В этом случае в блок else мы не попадем, и строка «Выходим из блока
не будет напечатана.
try.»
тоже
Мы увидим, что блок finally выполняется после блока
except. Текст в консоли это подтвердит:
Вошпи в
блок
try.
Вошпи в блок
except.
Вошпи в блок
finally.
Вышпи из блока
try ... except ... else ... f inall у.
И, наконец, случай, когда исключение возбуждается, но не перехватывается (лис
тинг
19.18).
Листинг 19.18.
Chapter_19(example_18/finally_no_except.py
try:
pr int ( "Вошпи
в блок
try. ")
raise OSError ()
print ("Выходим
из блока
try. ")
Глава
19.
Обработка исключений
361
except ValueError:
рriпt("Вощпи в блок
except.")
else:
рriпt("Вощпи в блок
else. ")
finally:
рriпt("Вощпи в блок
print("Bыщ,,и из блока
finally. ")
try ... except ... else ...
fiпally.")
В консоль будет выведен такой текст:
Вощпи в
блок
Вощпи в блок
try.
finally.
Traceback (most
receпt
call last):
File " ... /fiпally_по_except .ру", line 3,
iп
<module>
raise OSError ()
OSError
После возбуждения исключения мы сразу попадаем в блок finally, где при необ
ходимости можем освободить занятые ресурсы, а затем исключение распространя
ется дальше. Поскольку оно больше нигде не перехватывается, то программа ава
рийно завершается.
Давайте усложним пример. Пусть теперь конструкция try ...
except ... else ...
fiпally расположена внутри функции, и при этом внутри блока try не происходит
возбуждение исключения, однако в нем происходит возврат из функции с помощью
инструкции return. Будет ли в этом случае выполняться код ветви else? А finally?
Давайте проверим (листинг
Листинг
19.19).
19.19. Chapter_19/example_19/lry_return.py
def func():
try:
print
("Вощпи в блок
try. ")
return
print ( "Выходим
из блока
try. ")
except ValueError:
print("Boшпи в блок
except.")
else:
print
("Вошпи в блок
else. ")
("Вощпи в блок
finally. ")
finally:
print
if
name
== "
print ( "Вызов
main
функции
"·
func () ")
func ()
рrint("Выщли из функции
func()")
Часть
362
11.
Основные подходы
Запустив этот скрипт, в качестве результата мы увидим:
func ()
try.
в блок finally.
из функции func ()
Вызов функции
Вошпи в блок
Вошпи
BbDllЛИ
Получается, что блок else не вызывается, если блок try не выполнился до конца,
даже если он прервался не исключением, а инструкцией return. Но блок finally
обязан выполниться в любом случае.
Аналогичное поведение мы увидим, если конструкция
try ... except ... else ...
finally будет располагаться внутри цикла, и по некоторому условию в блоке try
станет происходить прерывание цикла с помощью инструкции break (листинг 19.20).
Листинг
19.20. Chapter_19/example_20/try_break.py
def func ():
for n in range(S):
print (f" {n=} ")
try:
print ("Вошпи
в блок
try. ")
=
if n
1:
print ( "Прер.аааем
ЦЮСJI
for. ")
break
рrint("Выходим из блока
try.")
except ValueError:
print("Boшnи в блок
except.")
else:
print("Boшnи в блок
finally:
print
("Вошпи в блок
print("Bызoв функции
func ()
print ( "ВьDllПи
else.")
finally. ")
func()")
из функции
func () ")
В результате выполнения этого скрипта в консоль будет выведен текст:
func ()
Вызов функции
n=O
Вошпи
в
блок
Выходим из
try.
try.
else.
finally.
блока
Вошпи в блок
Вошпи в блок
n=l
try.
for.
в блок finally.
ИЗ функции func ()
Вошпи в блок
Прерываем цикл
Вошпи
BbDllЛИ
Глава
19.
Обработка исключений
363
Первая итерация цикла отработала полностью, а на второй произошел вызов инст
рукции break, после чего немедленно выполнился код внутри блока finally, а по
том завершились и цикл, и функция. Блок else на второй итерации не выполнялся.
Заключение
В этой главе мы изучили обработку ошибок с помощью механизма исключений.
Благодаря этой технологии, код, в котором требуется обрабатывать ошибки, стано
вится более коротким и наглядным по сравнению с многократным возвратом кода
ошибки из вложенных функций.
Мы рассмотрели способы перехвата нескольких исключений в случаях, когда их
нужно обрабатывать по-разному или одним и тем же способом.
Мы узнали, что все исключения являются производными от класса BaseException,
но непосредственно от этого класса создавать свой производный класс не рекомен
дуется,
-
вместо него лучше наследовать классу Exception, который является
производным от
BaseException.
Мы рассмотрели особенности перехвата нескольких исключений, которые связаны
наследованием, а также научились перехватывать все возможные исключения.
Мы
изучили расширенную версию конструкции
try
except
else
finally и узнали, что необязательный блок else вызывается только в том случае,
если блок t ry полностью завершился, в нем не возникло исключений, и он не был
прерван инструкциями
return
или
break.
Блок finally выполняется всегда перед выходом из конструкции try
... else ... finally -
... except
независимо от того, перехватили мы возбужденное исклю
чение или нет. Даже если в блоке try вызываются инструкции return или break,
код в блоке finally будет выполнен.
За рамками этой книги остались такие темы, связанные с исключениями, как це
почки исключений (когда мы перехватываем одно исключение, вместо него возбу
ждаем новое, но при этом сохраняем информацию о первоначальном исключении),
а также группы исключений. Мы почти ничего не сказали о стеке вызовов функ
ций, а также о том, что его можно вывести в любом месте программы.
На этом мы завершаем знакомство с синтаксисом языка Pythoп и переходим к прак
тическим вопросам. В следующих главах этой части книги мы рассмотрим работу с
файлами и продолжим изучать стандартную библиотеку
Python.
- ГЛАВА 20-
ЗаПИСЬ и чтение файлов
До сих пор для получения входных данных от пользователя и для вывода результа
тов работы скриптов мы использовали консоль. Однако часто скрипты читают ис
ходные данные из файлов. Например, это могут быть файлы настроек в форматах
JSON, XML, INI
или в каком-то ином, может быть, даже не в стандартном формате.
Это также могут файлы с числовыми данными
формат
CSV
-
в этом случае часто используется
или текстовый файл, в котором данные оформлены в виде столбцов
(про работу с такими файлами мы поговорим в главе
26).
Если вы занимаетесь об
работкой изображений, то программе придется читать двоичные файлы в форматах
PNG, JPEG, ВМР и др.
Python
Результат работы приложения также часто выводится в файлы.
позволяет удобно работать с файлами, предоставляя, помимо операций чте
ния и записи, также широкий набор инструментов для работы с файловой систе
мой, облегчающих формирование путей до нужных файлов, копирование, переиме
нование и удаление файлов, а также обход дерева каталогов для его отображения
или поиска нужного файла.
В этой главе мы сосредоточимся на базовых операциях для работы с файлами
-
их
создании, наполнении данными, а затем чтении данных из файлов. Работе с файло
вой системой будет посвящена следующая глава.
Открытие файла и запись текстовых данных
Когда мы хотим что-то записать в файл или прочитать данные из него, то должны
выполнить три действия:
1.
Открыть файл в одном из режимов: только чтение, только запись или и то и другое.
2.
Записать или прочитать данные.
3.
Закрыть файл.
На каждом из этих этапов что-то может пойти не так и возбудиться исключение.
Например, при попытке открытия файла на чтение может оказаться, что требуемого
файла не существует, при попытке записи данных в файл
диске, а при закрытии файла
-
-
закончиться место на
произойти ситуация, когда из-за сложной логики
мы где-то ранее уже закрьmи открытый файл. Но что бы ни случилось, наша задача
-
Глава
20.
365
Запись и чтение файлов
гарантировать, что открытый файл будет закрыт как только вы проделаете с ним
все необходимые действия, связанные с чтением или записью.
Чтобы открыть файл для чтения или записи, используется встроенная функция
open (), способная принимать до восьми параметров:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None,
closefd=True, opener=None)
Мы не станем рассматривать все параметры функции open () -
некоторые из них
используются редко и предназначены для работы с файлами на низком уровне,
ближе к уровню операционной системы, но с некоторыми параметрами мы порабо
таем.
Функция open () принимает в качестве первого и единственного обязательного па
раметра строку с именем файла, который нужно открыть. Строго говоря, в качестве
первого параметра может быть не только строка, но также «объект, напоминающий
путь»
(path-like object),
но про эти объекты мы поговорим в главе
21,
когда речь
пойдет про модуль pathlib. В качестве первого параметра также может быть при
нят целочисленный дескриптор файла, но этот момент мы обойдем стороной.
Второй параметр (mocte) функции open () определяет режим открытия файла. Этот
параметр представляет собой строку, состоящую из одного или нескольких симво
лов, обозначающих, что именно мы хотим делать с открываемым файлом (табл.
Таблица
20.1.
20.1 ).
Режимы открытия файла
Значение
Символ
Открыть файл для чтения. Если запрашиваемого файла не существует, будет
"r"
возбуждено исключение FileNotFoundError (является производным классом
от класса OSError}
,1w"
Открыть файл для записи. Если файл не существует, он будет создан, а если
существует, то его содержимое очистится
"х"
Создать файл. Если файл существует, будет возбуждено исключение
FileExistsError (является производным классом от класса OSError}
Открыть файл для записи в режиме добавления. Если файл не существует,
"а"
он будет создан, а если существует, то операции записи будут добавлять
данные в его конец
"t"
Открыть файл в текстовом режиме. Используется по умолчанию
"Ь"
Открыть файл в двоичном режиме
Открыть файл для чтения и записи. Используется в сочетании с режимом "r",
"+··
если открываемый файл не нужно очищать при открытии, или с "w", если
требуется, чтобы открываемый файл при открытии был очищен
Часть
366
Некоторые из этих символов можно объединять
-
11.
Основные подходы
например, добавлять символы
"t" или "Ь" ко всем остальным символам. Если ни "t", ни "Ь" не указаны, подра
зумевается, что файл будет открыт в текстовом режиме.
Когда мы открываем файл в текстовом режиме, предполагается, что он будет со
держать только такие комбинации байтов, которые можно интерпретировать как
корректные символы в указанной кодировке. Кодировку можно указывать с помо
щью параметра encoding функции open (). Если этот параметр не указан при от
крытии файла в текстовом режиме, то используемая кодировка по умолчанию зави
сит от настроек операционной системы. Узнать кодировку по умолчанию можно с
помощью функции getencoding () из модуля locale.
Если вы используете
по умолчанию будет ис
пользоваться
быть кодировка СР-1251
Linux, то с большой вероятностью
UTF-8, а под Microsoft Windows это может
или СР-1252, которые не предназначены для использования символов
Unicode,
и
тогда при попытке записи некоторых символов может возникнуть ошибка.
Во избежание проблем с чтением файлов из разных операционных систем реко
мендуется всегда явно указывать требуемую кодировку. В последнее время обычно
используют кодировку
UTF-8,
если нет причин задействовать какую-либо другую.
Если при открытии файла мы указали одну кодировку, а пытаемся записать или
прочитать данные в другой несовместимой кодировке, то по умолчанию будет воз
буждено исключение ValueError, однако с помощью параметра errors функции
open () мы можем изменить такое поведение. Более подробно об этом мы погово
рим чуть позже.
В случае удачного выполнения функции open () мы получим объект файла, а если
возникнет ошибка, то будет возбуждено исключение OSError (или производное от
него) с информацией о том, что именно произошло.
Для начала мы будем создавать и читать только текстовые файлы (про двоичные
файлы поговорим в этой главе позже).
Но сначала научимся записывать файлы, чтобы потом нам было откуда читать данные.
Напишем первый пример, который открывает файл в режиме записи текста, выво
дит информацию о полученном объекте и закрывает файл (листинг
20.1 ).
file = open ("example. txt", "wt")
print(f"{type(file)=)")
print(dir(file))
file. close ()
В результате выполнения этого скрипта в консоль будет выведен следующий текст:
type(file)=<class '_io.TextIOWrapper'>
[ ... , 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush',
'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readaЫe',
Глава
Запись и чтение файлов
20.
367
'readline', 'readlines', 'reconfigure', 'seek', 'seekaЫe', 'tel1', 'truncate',
'writaЫe', 'write', 'write_through', 'writelines')
Здесь можно увидеть, к какому классу относится возвращенный функцией open ()
объект, а также, какие методы он содержит (перечень методов здесь приведен в со
кращенном виде). Полужирным шрифтом выделены те методы, с которыми мы бу
дем работать далее.
Помимо вывода текста в консоль, у этого скрипта есть еще один побочный эф
фект
он создает файл example.txt. Чтобы понять, где именно будет создан этот
-
файл, нам нужно немного отвлечься и разобраться с таким понятием как «текущий
рабочий каталог» (мы уже затрагивали этот вопрос в главе
3).
При задании имени файла мы могли бы указать полный (или абсолютный) путь до
того места в файловой системе, где файл должен быть создан.
Windows
Например, под
использовать строку наподобие следующей:
"С:\ \projects\ \python\ \book\ \chapter_ 20\\example. txt"
Однако в примере из листинга
указать вложенные каталоги),
мы указали только имя файла (хотя могли бы
20.1
это относительный путь. Он относительный, по
-
тому что отсчитывается относительно текущего рабочего каталога.
С большой вероятностью файл example.txt будет создан в том же каталоге, где рас
положен файл скрипта file_open.py. Это значит, что текущий рабочий каталог в на
шем случае
-
это тот каталог, в котором расположен скрипт. Но это не обязатель
но всегда так и зависит от способа запуска интерпретатора. Например, можно за
пустить консоль и перейти в каталог C:\projects\python\ (подразумевается, что такой
путь должен существовать) с помощью команды:
cd C:\projects\python
а после этого выполнить команду:
python "C:\projects\python-book\chapter_20\example_Ol\file_open.py"
В
этом
случае текущим
рабочим
каталогом
станет
C:\projects\python\, и
файл
example.txt будет создан именно в этом каталоге, а не в каталоге со скриптом
file_open.py. Узнать текущий рабочий каталог можно с помощью функции getcwd ()
из модуля
os.
При завершении работы скрипта объект созданного файла будет закрыт с помощью
метода
close ().
Возвращаемся к нашему примеру и функции open () . Если мы откроем содержимое
созданного файла example.txt в текстовом редакторе, то увидим, что он пустой,
-
да
и размер этого файла равен нулю.
Откроем текстовый редактор и запишем что-нибудь в этот файл, а затем снова за
пустим скрипт file_open.py. Открыв файл example.txt в текстовом редакторе после за
вершения работы скрипта, мы увидим, что он опять стал пустым,
работает режим открытия файла
II
w
11
•
-
именно так
Часть
368
Если в скрипте из листинга
20.1
11.
Основные подходы
указать какой-нибудь путь, по которому файл не
может быть создан (например, в несуществующем каталоге
x:\invalid),
то будет воз
буждено исключение FileNotFoundError:
Traceback (most recent cal l last):
File " ... \ example_20_01 .ру", line 1, in <module>
file = open("x:\\invalid\ \example.txt", "wt")
FileNotFoundError: [Errno 2] No such file or directory:
'х:\ \invalid\ \example.
В этом случае не удалось создать файл с полным путем
txt'
"x:\invalid\example.txt".
Теперь запишем что-нибудь в файл. Для удачного выполнения следующего скрипта
(листинг
20.2)
обязательно, чтобы он был сохранен в кодировке
тии файла для записи мы также укажем кодировку
UTF-8
UTF-8.
При откры
с помощью параметра
encoding.
Листинr 20.2.
Chapter_20/example_02/file_write.py
file = open("example.txt", "wt", encoding="utf-8")
file.write("Hello, ")
file.write ("world! \ n")
lines = ["Привет, мир!\n",
file.writelines(lines)
11 i1!Фт'l:!t.Ji!.!\n"]
file.close ()
В этом примере файл снова открывается в режиме "wt"
(этот режим совпадает с
режимом "w"). Затем для записи строк в файл используются два метода: wri te () и
writelines (). Метод write (} принимает в качестве параметра строку и записывает
ее в файл. Метод wri telines () принимает в качестве параметра список строк и за
писывает их все. Ни write(), ни writelines() не добавляют в конец переданных
строк символы перевода строки. И если мы хотим, чтобы каждая переданная в эти
функции строка располагалась в файле на отдельной строке, надо не забывать до
бавлять символы перевода строки
" \ n" там, где мы считаем это нужным.
Если открыть теперь созданный файл
умеет читать кодировку
UTF-8,
example.txt
в текстовом редакторе, который
мы увидим следующий текст:
Hello, world'
Привет,
мир!
i1!Фт1:!t.Ji!.!
Методы write()
и writelines(}
также могут возбуждать
исключение класса
OSError или производные от него. Наиболее вероятная причина этого
-
закончи
лось место на диске, куда записывается созданный файл .
Если мы запустим сейчас скрипт примера из листинга
крыть измененное содержимое файла
example.txt,
20.1
еще раз
-
чтобы от
то уже знакомая нам ситуация по-
Глава
20.
369
Запись и чтение файлов
вторится,
-
окажется, что содержимое файла вернулось к первоначальному со
стоянию (до нашей правки).
Чтобы увидеть, как работает режим добавления, в предыдущих примерах доста
точно поменять в функции режим открытия файла "wt" на "а" или "at". После это
го скрипт станет работать следующим образом: если файл
example.txt
не существует,
он будет создан, и в него записано то же содержимое, которое мы видели. Если же
он существует, то записываемые строки будут дописываться в конец файла.
Закрытие файлов. Инструкция
with
Мы незаслуженно мало сказали о закрытии файла и методе close () при том, что
это очень важная операция. Если мы забудем закрыть файл или не закроем его из-за
возникшего исключения или по какой-либо другой причине, файл останется откры
тым до завершения работы скрипта, другая программа или даже наше приложение
в другом месте кода не смогут его использовать для записи или чтения, и возникнет
новая ошибка. Что произойдет в случае попытки открыть уже открытый файл, за
висит от операционной системы. Хуже того, в такой ситуации могут быть потеряны
записываемые данные . Основное правило при работе с файлами состоит в том, что
если мы открыли файл, то его нужно гарантированно закрыть, что бы ни случи
лось. Причем желательно закрыть сразу, как только мы перестанем с ним работать.
В примерах предыдущего раздела мы поступали наивно, полагая, что если файл
открылся без ошибок, то дальше всё пойдет хорошо, и интерпретатор все-таки дой
дет до выполнения строки, где файл закрывается. Но если мы решим написать на
дежное
приложение,
то должны ловить
исключения и
при
их
возникновении
за
крывать файл. В этом деле нам поможет конструкция try ... finally.
Перепишем пример из листинга
20.2
с использованием этой конструкции, а заодно
продемонстрируем, что файл будет закрыт в любом случае, даже если между опе
рациями записи будет возбуждено исключение (листинг 20.3).
Листинг
20.3. Chapter_20/example_0З/open_finally.py
file = open ( "example. txt", "w", encoding="utf-8")
try:
file.writ e ("Hello, world! \ n")
raise OSError ( "Что-то пош:по не тах")
lines = ["Привет, мир!\n", "-f11Фт1!!-:W-!\n"]
file.writelines(lines)
finally:
print ("Закрываем файл.")
f ile. close ()
При запуске этого скрипта в консоль будет выведен следующий текст:
Закрываем файл.
Traceback (most recent call l as t):
File " .. . /open_finally.py", line 7, in <module>
Часть
370
raise OSError("Чтo-тo пошло
OSError: Что-то пошло не так
11.
Основные подходы
не так")
Открыв в текстовом редакторе созданный файл, мы увидим, что в него успешно
записалась первая строка. И если закомментировать или удалить строку кода, воз
буждающую исключение, то сообщение о закрытии файла все равно появится в
консоли.
Такая конструкция безопасна, но очень уж громоздка. К тому же, легко забыть за
крыть файл, особенно, если после открытия файла выполняется сложная ветвистая
логика. Для решения этой проблемы в
Python
была добавлена конструкция wi th,
которая делает примерно то же, что мы сделали самостоятельно.
Напишем пример с записью в файл так, как это принято делать в
Python
(листинг
20.4),
и разберемся в его коде.
Листинг
20.4. Chapter_20/example_04/with_open.py
with open("example.txt", "w", encoding="utf-8") as file:
file. wri te ( "Hello, ")
file.write("world!\n")
lines = ["Привет, мир!\n", "'f~тtit~!\n"]
file.writelines(lines)
После ключевого слова wi th мы вызываем функцию open (), и она возвращает объ
ект, присваиваемый переменной, имя которой указано после ключевого слова as.
Интерпретатор гарантирует, что перед выходом из блока внутри w i th этот объект
будет закрыт, даже если внутри этого блока будет возбуждено исключение.
Конструкция with может работать не только с файлами, но и с любыми объектами,
которые реализуют два «магических» метода: _enter_()
и _exit_(). Метод
_enter_() вызывается при входе в блок with после создания объекта. Подразу
мевается, что именно тут станут создаваться ресурсы, которые надо будет потом
освободить (файлы, подключения к базе данных, графические объекты). Метод
_ exi t _
() должен содержать код, освобождающий созданные ресурсы, при этом
он вызывается как при удачном завершении блока wi th, так и в случае возникнове
ния исключения.
Если ссылка на создаваемый объект не требуется, конструкцию as имя_ переменной
можно не писать.
Когда нам надо создать более одного «закрываемого» объекта
-
например, не
сколько файлов, для создания нужного числа объектов мы можем использовать
один оператор wi th. Например, следующий код открывает два файла и записывает
в них строки (листинг
Листинг
20.5).
20.5. Chapter_20/example_05/with_open_many.py
with (open("hello_er..txt", "w", encoding="utf-8") as file_l,
open("hello_ru.txt", "w", encoding="utf-8") as file_2):
Глава
20.
Запись и чтение файлов
371
file_l.write ("Hello, world! ")
file_2.write ("Привет, мир'")
Здесь в инструкции wi th используются круглые скобки только для того, чтобы пе
ренести создание второго объекта (file 2) на новую строку. При выходе из блока
wi th оба файла будут гарантированно закрыты. В дальнейшем при работе с файла
ми мы всегда будем использовать конструкцию wi th.
Чтение текстовых данных
Теперь попробуем прочитать созданный файл. Для следующих примеров нам пона
добится файл example.txt, созданный ранее (см. например, листинг
20.4).
Впрочем,
подойдет любой текстовый файл, содержащий несколько строк в кодировке
UTF-8.
Этот файл нужно скопировать в текущий рабочий каталог или в скриптах примеров
указывать полный путь до него.
Начнем рассматривать методы, которые позволяют читать данные из файла, с ме
тода readlines (). Он читает строки текстового файла и возвращает список прочи
танных строк. Если методу readlines () не передавать никакие параметры, будут
прочитаны все строки. Однако, если файл слишком большой, результирующий
список может занимать слишком много оперативной памяти, и тогда лучше читать
файл частями. В этом случае методу readlines () нужно передать целочисленный
параметр, ограничивающий количество прочитанных строк указанным количест
вом символов (не строк!). Когда строки в файле закончатся, функция readlines ()
вернет пустой список.
Прочитаем построчно наш файл example.txt (листинг
Листинг
20.6).
20.6. Chapter_20/example_06/readlines.py
with open("example.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
for line in lines:
print(line.rstrip())
Здесь для краткости мы по-прежнему игнорируем тот факт, что в процессе откры
тия или чтения файла могут возникать исключения (тогда следовало бы вывести
сообщение об ошибке). Обратите внимание, что для каждой прочитанной строки
вызывается метод rstrip (), удаляющий символы перевода строки, которые при
чтении строк с помощью метода
Для
многих
задач,
в
том
readlines ()
числе
для
сохраняются в прочитанных строках.
этого
примера,
использование
метода
readlines () избыточно: мы всего лишь хотим последовательно прочитать строки и
вывести их в консоль, поэтому нет смысла хранить все прочитанные строки в спи
ске,
а
для
чтения
одной
строки
из
файла
можно
воспользоваться
методом
readline (), который возвращает прочитанную строку, причем, если внутренний
Часть
372
11.
Основные подходы
указатель на текущее положение в файле достиг конца файла, метод вернет пустую
строку.
Пример из листинга
20.6
можно переписать с использованием метода readl ine () и
цикла while, в котором проверяется, не достигли ли мы конца файла (листинг
20.7).
Листинг 20.7. Chapter_20/example_07/readline.py
with open("example.txt", "r", encoding="utf-8") as file:
while (line := file.readline())
!= ""·
print(line.rstrip())
Это еще один типичный пример для использования оператора
«: =» ( см.
главу
3).
Аналогичного результата можно добиться более компактным кодом, благодаря то
му, что объект файла является итерируемым, и его можно использовать непосред
ственно в цикле for (листинг
Листинг 20.8.
20.8).
Chapter_20/example_08/read_for.py
with open("example.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.rstrip())
Теперь посмотрим, как можно прочитать содержимое файла порциями, ограничи
вая максимальное количество прочитанных символов (листинг
Листинг
20.9).
20.9. Chapter_20/example_09/readlines_size.py
size = 15
with open("example.txt", "r", encoding="utf-8") as file:
while (lines := file.readlines(size))
print ( "Прочитано
строк:",
!= []:
len (lines))
for line in lines:
print(line.rstrip())
Параметр s i ze здесь работает следующим образом. Метод readl i nes ()
последова
тельно читает строки и добавляет их в результирующий список, который будет воз
вращен этим методом. Как только после прочтения очередной строки количество
прочитанных символов достигнет или превысит указанное значение, возвращается
то количество строк, которое было прочитано. При следующем обращении к мето
ду
readlines ()
или другим методам чтения последующие строки станут читаться,
начиная с того места, где чтение остановилось в прошлый раз. При этом гарантиру
ется, что будут прочитаны полные строки, даже если при этом придется прочитать
больше символов, чем того требует параметр size.
В реальных приложениях чаще всего имеет смысл читать данные более крупными
порциями, чем в нашем примере,
-
хотя бы размером в единицы килобайт.
Глава
20.
Запись и чтение файлов
373
Для файла, созданного в предыдущем разделе (см. листинг
20.4),
этот скрипт выве
дет такой результат:
2
Прочитано строк:
Hello, world!
Привет,
мир!
1
Прочитано строк:
-f~тtit-W-!
Если требуется читать файл не построчно, а сплошным потоком текста (посим
вольно), то следует использовать метод read () . Он принимает единственный пара
метр, который для текстовых файлов обозначает, сколько символов надо прочитать
из файла. Если этот параметр не указывать, или он будет равен -1, это будет озна
чать, что нужно прочитать все символы до конца файла. Если файл открыт в тек
стовом режиме, метод read ()
вернет строку с прочитанными символами. Если
окажется, что указатель в файле достиг конца файла, метод вернет пустую строку.
В листинге
20.1 О
показано, как использовать метод read () для чтения файла не
большими порциями по
5
символов. Впрочем, обычно размер порций делают зна
чительно больше.
Пистинr 20.10. Chapter_20/example_10/read_slze.py
size = 5
with open ("example. txt", "r", encoding="utf-8") as file:
while (chars := file.read(size)) != "":
print(chars, "1", sep="", end="")
Здесь после каждой прочитанной порции символов в консоль выводится символ
« 1 ».
Выполнив этот скрипт, мы увидим в консоли следующий текст:
Hello 1, wor I ld!
Пlриветl,
-f~тtit1
мирl
!
W!
Двоичные строки
До сих пор мы записывали в файлы и читали из файлов только текстовые данные.
В этих данных не могло быть произвольного набора байтов, а присутствовали
только те комбинации, которые можно интерпретировать как строки в указанной
кодировке. Однако многие файлы не являются текстовыми,
-
то есть последова
тельность байтов в них требуется интерпретировать не как символы, а как двоич
ные данные
-
последовательности целых или дробных чисел или просто как по
следовательности байтов, интерпретацией которых занимается разрабатываемое
приложение. Например, это могут быть изображения в форматах ВМР,
звуковые файлы
WA V,
МРЗ и многие другие.
JPEG, PNG,
374
Часть
Для работы с двоичными данными в
данных:
♦
bytes
и
Python
11.
Основные подходы
предусмотрены два встроенных типа
bytearray.
Класс bytes -
это неизменяемая последовательность байтов, очень напоми
нающая строки, однако способная хранить любую последовательность байтов.
Поэтому тип bytes еще называют двоичной строкой (бинарной строкой или
байтовой строкой).
♦
Класс bytearray это изменяемый массив байтов, который отличается от типа
bytes тем, что мы можем изменять значение каждого байта в последовательности.
Есть несколько способов создания двоичных строк. Первый способ
-
это их соз
дание по заранее известной последовательности байтов (как литерал). Для этого в
Python
предусмотрена специальная конструкция, напоминающая создание строк, но
перед кавычками нужно поставить символ «ь»:
»> foo =
>» foo
Ь"\х50\х4Ь\х03\х04"
Ь'РК\х03\х04'
>» type(foo)
<class 'bytes'>
В этом коде создается последовательность из четырех байтов. Такая последова
тельность взята не просто так
-
это первые байты любого файла в формате
ZIP.
В этой двоичной строке каждый байт представляется с помощью нотации \х##, где
## -
значение байта в шестнадцатеричной системе счисления.
Обратите внимание, что при выводе переменной foo в консоль последовательность
была преобразована в ь ' РК \хоз\ хо 4 ' . Байты, которые имеют символьное представ
ление в АSСП-таблице (с кодами до
127
в десятичной или
7F
в шестнадцатеричной
системах счисления), будут выводиться в виде символов. При создании двоичных
строк мы тоже можем использовать эти символы, вместо того, чтобы писать их коды:
>>> bar = b"hello"
>>> bar
b'hello'
»> type (bar)
<class 'bytes'>
Хотя в переменной bar присутствуют только байты, у которых имеется символьное
представление в АSСП-таблице, эта переменная относится к классу bytes, а не str.
Если мы попытаемся повторить ту же операцию с использованием кириллицы, то
мы получим ошибку, поскольку в АSСП-таблице русских букв нет:
>>> spam = Ь"привет"
File "<python-input- ... >", line 1
spam = Ь"привет"
SyntaxError: bytes
Иногда требуется
сап
only contain ASCII literal characters
преобразовывать строковые
последовательности
в двоичные
строки и наоборот. Преобразование из строки в последовательность байтов называ-
Глава
20.
375
Запись и чтение файлов
ется кодированием, а преобразование последовательности байтов в строку
-
деко
дированием. При этих операциях важно понимать, в какой кодировке представлены
символы.
Для преобразования строки в класс
bytes в классе str имеется метод encode (), па
раметры которого описываются следующим образом:
str.encode(encoding='utf-8', errors='strict')
Первый параметр
(encoding) определяет, какую кодировку нужно использовать,
чтобы представить строку в двоичном виде. Второй параметр (errors) влияет на
поведение кодировщика в случае, если какой-либо символ невозможно представить
в указанной кодировке. Параметр
errors мы рассмотрим чуть позже, а пока скажем
лишь, что значение по умолчанию 'strict' обозначает, что в случае ошибки коди
рования будет возбуждаться исключение
подклассом для класса
UnicodeEncodeError, которое является
UnicodeError.
В следующем примере показано кодирование слова «привет» с использованием
различных кодировок:
>>> bytes_utfB = "привет" .encode ()
>» bytes_utfB
b'\xd0\xbf\xdl\x80\xd0\xb8\xd0\xb2\xd0\xb5\xdl\x82'
>>> bytes_utflб = "привет" .encode ("utf-16")
>» bytes_utflб
b'\xff\xfe?\x04@\x048\x042\x045\x04B\x04'
>>> bytes_cpl251 = "привет".еnсоdе("ср1251")
>» bytes_cpl251
b'\xef\xf0\xe8\xe2\xe5\xf2'
>>> bytes_koiB_r = "привет" .encode ("koiB_r")
>>> bytes_koiB_r
b'\xd0\xd2\xc9\xd7\xc5\xd4'
А теперь попробуем преобразовать слово «привет» в кодировку, в которой не пре
дусмотрены символы русских букв:
>>> bytes_cpl252 = "привет".еnсоdе("ср1252")
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
bytes_cp1252 = "привет".еnсоdе("ср1252")
~~~~~~~~~~~~~~~
File " ... /python3.13/encodings/cpl252.py", line 12, in encode
return codecs.charmap_encode(input,errors,encoding_taЫe)
~~~~~~~~~~~~~~~~~~~~~
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-5: character
maps to <undefined> encoding with 'ср1252' codec failed
Л/\ЛЛЛЛЛЛЛЛ
ллллллллллллллллллллллллллллл
Пришло время разобраться со вторым параметром метода
encode () -
параметром
errors, который может принимать указанные далее значения:
♦
"strict" -
это значение по умолчанию. Как мы уже видели, при появлении
проблемных символов будет возбуждено исключение
UnicodeEncodeError;
376
♦
Часть
"ignore" -
11.
Основные подходы
проблемные символы будут исключены из полученной двоичной
строки;
♦
♦
"replace" -
проблемные символы будут заменены на символ«?»;
"xmlcharrefreplace" -
проблемные символы будут заменены на их коды в
формате, принятом в языке
♦
"backslashreplace" -
XML
и
HTML:
&#код;
проблемные символы будут заменены на их коды в
формате, который мы обсуждали в главе
8 про
строки:
\ и код.
Следующий пример демонстрирует использование параметра errors:
>>> text = "Hello Привет Ola"
>>> text.encode("cp1252", "ignore")
01 \xel'
Ь' Hello
>>> text.encode("cp1252", "replace")
b'Hello ?????? O1\xel'
>>> text.encode("cp1252", "xmlcharrefreplace")
b'Hello Привет O1\xel'
>>> text. encode ( "ср1252", "Ьackslashreplace")
b'Hello \\u041f\\u0440\\u0438\\u0432\\u0435\\u0442 Ol\xel'
Операция, обратная кодированию, то есть преобразование байтовой строки в стро
ку, как уже отмечено ранее, называется декодированием. Для декодирования ис
пользуется метод decode () класса bytes. Описание метода decode () сильно напо
минает описание метода
encode () :
bytes.decode(encoding='utf-8', errors='strict')
Различие здесь заключается в том, что параметр errors может принимать только
три
значения:
"strict", "ignore" и "replace", аналогичные
errors из метода str. encode ().
соответствующим
значениям параметра
Посмотрим, как работает метод decode () на примерах:
»> bytes_utf8 = b"\xd0\xЬf\xdl\x80\xd0\xЬ8\xd0\xЬ2\xd0\xb5\xdl\x82"
>>> bar = bytes_utf8.decode()
>>> bar
'привет'
»> type (bar)
<class 'str'>
>>> bytes_cp1251 = b"\xef\xf0\xe8\xe2\xe5\xf2"
»> baz = bytes_cp1251.decode("cpl251")
>>> baz
'привет'
Вместо метода decode (), можно использовать функцию str (). Мы об этом раньше
не говорили, но функция str () может принимать те же самые параметры, что и
decode () . Поэтому предыдущий пример можно было бы переписать следующим
образом:
>» bytes_utf8 = b"\xd0\xЬf\xdl\x80\xd0\xЬ8\xd0\xЬ2\xd0\xЬ5\xdl\x82"
>>> bar = str(bytes_utf8, "utf-8")
Глава
20.
377
Запись и чтение файлов
>>> bar
'привет'
>>> bytes ср1251 = b"\xef\xf0\xe8\xe2\xe5\xf2"
»> baz = str (bytes _ ер 1251, "ср1251")
>>> baz
'привет'
Если мы хотим декодировать двоичную строку с помощью функции str (), то нуж
но обязательно указать кодировку, а при необходимости, и параметр errors. Если
мы забудем указать кодировку, то получим не совсем то, чего ожидали:
»> bytes_utfB = b"\xd0\xЬf\xdl\x80\xd0\xb8\xd0\xb2\xd0\xb5\xdl\x82"
>>> bar = str(bytes_utfB)
>>> bar
"b'\\xd0\\xbl\\xdl\\x80\\xd0\\xЬ8\\xd0\\xЬ2\\xd0\\xЬS\\xdl\\x82'"
Как можно видеть, мы получили строковое представление байтовой строки.
Интересно, что в
Python
предусмотрены специальные параметры запуска интерпре
татора, позволяющие легче обнаруживать эту неожиданную проблему.
Если запустить интерпретатор с параметром -ь, то в результате преобразования
двоичной строки в текстовую строку без указания кодировки будет выведено пре
дупреждение:
> python -Ь
»> bytes_utfB = b"\xd0\xЬf\xdl\x80\xd0\xb8\xd0\xb2\xd0\xb5\xdl\x82"
>>> bar = str(bytes_utfB)
<python-input- ... >:1: BytesWarning: str() оп а bytes instance
bar = str(bytes_utfB)
»> bar
"b'\\xd0\\xbl\\xdl\\x80\\xd0\\xЬ8\\xd0\\xЬ2\\xd0\\xЬ5\\xdl\\x82'"
А если интерпретатор запустить с параметром -ьь, то вместо предупреждения бу
дет возбуждено исключение Byteswarning:
> python -ЬЬ
»> bytes_utfB = b"\xd0\xbl\xdl\x80\xd0\xb8\xd0\xb2\xd0\xЬ5\xdl\x82"
>>> bar = str(bytes_utfB)
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
bar = str(bytes_utfB)
BytesWarning: str() on а bytes instance
В двоичную строку можно также преобразовывать целые числа
се
int
предусмотрен метод
-
для этого в клас
to_bytes ():
int.to_bytes(length=l, byteorder='big', *, signed=False)
Этот метод принимает следующие параметры:
♦
length -
указывает, сколько байтов нужно выделить для представления числа.
Если указанного количества байтов не хватит, будет возбуждено исключение
Overf lowError;
Часть
378
♦
11.
Основные подходы
указывает на порядок следования байтов. Этот параметр может
byteorder -
принимать два строковых значения:
"Ьig" -
•
представление Ьig-endian (от старшего к младшему), когда старший
байт расположен слева;
•
"little" -
представление
little-endian
(от младшего к старшему), когда
старший байт расположен справа;
♦
именованный параметр
signed указывает на то, является ли целое число знако-
вым или беззнаковым.
Метод to bytes ( J возвращает двоичную строку. Вот как это работает:
»> foo = 1025
>>> foo.to_bytes(4,
"Ьig")
Ь'\х00\х00\х04\х01'
>>> foo. to _bytes (4, "little")
Ь'\х01\х04\х00\х00'
»> bar = -1025
>>> bar.to_bytes(4, "Ьig", signed=Тrue)
b'\xff\xff\xfb\xff'
>>> bar.to_bytes(4, "little", signed=Тrue)
b'\xff\xfb\xff\xff'
>>> bar.to_bytes(4, "little", signed=False)
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
bar.to_bytes(4, "little", signed=False)
~~~~~~~~~~~~
OverflowError: can't convert negative int to unsigned
ллллллллллллллллллллллллллл
Для обратного преобразования из двоичной строки в целое число в классе int пре
дусмотрен метод класса from_bytes () (о том, что такое «метод класса», пояснялось
в главе
13):
int.from_bytes(bytes,
byteorder='Ьig',
*, signed=False)
Параметры
Метод
byteorder и signed имеют тот же смысл, что и в методе to_bytes().
f rom_ bytes () возвращает целое число:
>>> int. from _ bytes (Ь"\х00\х00\х04 \х01", "Ьig")
1025
>>> int. from _ bytes (b"\xff\xfb\xff\xff", "li ttle", signed=True)
-1025
Запись и чтение двоичных данных
Познакомившись с байтовыми строками, возвращаемся к файлам. Теперь нам нуж
но научиться записывать и читать двоичные данные. Как было сказано в начале
главы, чтобы открыть файл в двоичном режиме, к строке описания режима откры-
Глава
20.
Запись и чтение файлов
379
тия файла в функции open () нужно добавить символ "Ь". Остальные принципы ра
боты с двоичными файлами остаются такими же, что и для текстовых файлов.
Чтобы записывать двоичные данные, у нас есть два метода: write () одной двоичной строки и
wri telines () -
для записи
для записи сразу нескольких двоичных
строк.
Напишем скрипт, который с помощью метода wr i te () последовательно записывает
несколько двоичных строк (листинг
Листинг 20.11.
20.11 ).
Chapter_20/example_11/write_bytes.py
foo = 1025
bar = -1025
with open("example.dat", "wb") as file:
file.write(b"\x50\x4b\x03\x04")
file.write(b"hello")
foo _bytes = foo. to_ bytes (4, "big")
bar_bytes = bar.to_bytes(4, "big", signed=True)
file.write(foo_bytes)
file.write(bar_bytes)
В этом примере двоичные строки получены разными способами. Сначала две дво
ичные строки заданы в явном виде. Еще раз вспомним, что в двоичной строке мы
можем использовать символы, коды которых меньше
127,
поэтому строка b"hello"
корректная, но при записи в двоичный файл передавать в метод wr i te () текстовую
строку "hello" мы не можем. Если бы мы хотели записать в двоичный файл текст
на русском или другом языке, который использует не латинский алфавит, нам нуж
но было бы предварительно закодировать строку с помощью метода str. encode ().
Следующие две двоичные строки получены на основе целых чисел.
Того же самого результата можно добиться, используя метод wri telines () (лис
тинг
20.12).
Листинr 20.12.
Chapter_20/example_12/wrltelines_bytes.py
foo = 1025
bar = -1025
foo_bytes = foo.to_bytes(4, "big")
bar_bytes = bar.to_bytes(4, "Ьig", signed=True)
data =
[Ь"\х50\х4Ь\х03\х04",
b"hello",
foo_bytes, bar_bytes]
with open ("example.dat", "wb") as file:
file.writelines(data)
380
Часть
11. Основные
подходы
Если мы теперь откроем файл example.dat в любом приложении, которое умеет ото
бражать файлы в шестнадцатеричном представлении (например, с помощью кон
сольного приложения НехуР), то увидим содержимое наподобие того, что показано
на рис. 20.1.
00000000
00000010
Рис.
50
ff
20.1.
4Ь
03 04 68 65
бс бс
Содержимое файла
бf
00 00 04 01 ff ff fb
example.dat
PK••helljooo••xxx
х
!
в шестнадцатеричном представлении
В первом столбце таблицы показаны смещения байтов, следующие два столбца
таблицы
столбца
-
это непосредственно шестнадцатеричные данные, а последние два
те же шестнадцатеричные данные в текстовом формате для тех симво
лов, для которых такое представление возможно, или в виде условных символов,
если у байта нет АSСП-представления.
Теперь прочитаем данные, записанные в файл example.dat (листинг 20.13). Не за
будьте скопировать его в текущий рабочий каталог, где будет выполняться этот
скрипт.
Листинr 20.13. Chapter_20/example_131read_Ьytes.py
with open("example.dat", "rb") as file:
header = file.read(4)
hello_bytes = file.read(S)
foo_bytes
file.read(4)
bar_bytes = file.read(4)
hello = hello_bytes.decode()
foo = int.from_bytes(foo_bytes, signed=True)
bar = int.from_bytes(bar_bytes, signed=True)
print(f"{header=}")
print(f"{hello=}")
print(f"{foo=}")
print(f"{bar=}")
Если все пройдет удачно, то в консоль будет выведен следующий текст:
header=b'PK\x03\x04'
hello= 'hello'
foo=l025
bar=-1025
Заголовок из четырех байтов мы оставили неизменным, двоичную строку "hello"
преобразовали в строку с помощью метода bytes. decode (), а оставшиеся данные
преобразовали в два целых числа.
1
См. https://github.com/sharkdp/hexyl.
Глава
20.
Запись и чтение файлов
381
При чтении или записи данных мы можем перемещать внутренний указатель по
файлу с помощью метода see k ( ) , позволяющего читать только интересующие нас
участки файла, и не читать те данные, которые нам не нужны. Метод seek () опи
сывается следующим образом:
seek(offset, whence=os.SEEK_SET, /)
Здесь offset -
смещение в байтах, на которое мы хотим переместиться. Это зна
чение может быть как положительным, так и отрицательным.
Параметр whence указывает, относительно чего задано смещение offset, -
он мо
жет принимать одно из трех значений:
♦
os. SEEK _ SET -
♦
os. SEEK cuR -
смещение задано относительно начала файла;
смещение задано относительно текущей позиции указателя в
файле;
♦
смещение задано относительно конца файла. В этом случае
os. SEEK END -
смещение offset должно быть равно нулю или отрицательным.
Изменим предыдущий пример (см. листинг
20 .13)
таким образом, чтобы данные,
записанные в файл example.dat, читались не последовательно, а с использованием
перемещения по файлу (листинг
Листинг
20.14).
20.14. Chapter_20/example_14/seek.py
import os
with open("example.dat", "rb") as file:
file.seek(4, os.SEEК_SET)
hello_bytes = file.read(S)
file.seek(4,
os.SEEК_CUR)
bar_bytes = file.read(4)
file.seek(-8,
os.SEEК_END)
foo_bytes = file.read(4)
file.seek(O, os.SEEК_SET)
header = file.read(4)
hello = hello_bytes.decode()
foo = int.from_bytes(foo_bytes, signed=True)
bar = int.from_bytes(bar_bytes, signed=True)
print(f"{header=)")
print(f"{hello=)")
print(f"(foo=)")
print(f"{bar=)")
382
Часть
Здесь мы сначала пропускаем
4
11.
байта от начала файла и читаем
лами "hello". Затем пропускаем еще
4
Основные подходы
5
байтов с симво
байта относительно текущей позиции и чи
таем целое число ьаr. После этого перемещаемся в позицию, отстоящую на
8
бай
тов от конца файла и читаем целое число foo. И, наконец, возвращаемся к началу
файла и читаем заголовок header, который пропустили в самом начале. Результат
работы этого скрипта выглядит точно так же, как и предыдущего.
Как говорилось в самом начале главы, при открытии файла мы можем использовать
дополнительный флаг режима "+", обозначающий, что мы хотим и читать данные
из файла, и писать в него данные. В этом случае для перемещения по файлу, скорее
всего, пригодится метод
seek ().
Изменим предыдущий пример (см. листинг
20.14)
таким образом, чтобы файл от
крывался в режиме для обновления (без очистки файла) "r+b", но перед тем, как
читать данные, мы запишем на позицию последнего целого числа значение
тинг
42
(лис
20.15).
import os
with open("example.dat", "r+b") as file:
file.seek(-4,
os.SEEК_END)
file.write((42)
.to_Ьytes(4,
"Ьiq"))
file.seek(4, os.SEEK_SET)
hello_bytes = file.read(S)
file.seek(4, os.SEEK_CUR)
bar_bytes = file.read(4)
file.seek(-8, os.SEEK_END)
foo_bytes = file.read(4)
file.seek(O, os.SEEK_SET)
header = file.read(4)
hello = hello_bytes.decode()
foo = int.from_bytes(foo_bytes, signed=True)
bar = int.from_bytes(bar_bytes, signed=True)
print(f"{header=)")
print(f"{hello=)")
print(f"{foo=)")
print(f"{bar=)")
Здесь сразу после открытия файла мы переместили файловый указатель на четвер
тый байт относительно конца файла и записали в это место число
42,
указав, что
Глава
20.
Запись и чтение файлов
оно займет те же
4
383
байта. Остальной код остался неизменным по сравнению с пре
дыдущим примером. В результате в консоль будет выведен следующий текст:
header=b'PK\x03\x04'
hello='hello'
foo=l025
bar=42
Коротко о сериализации и десериализации
Серuш1uзацuя
это преобразование объекта в последовательность байтов. Обрат
-
ная процедура, то есть создание объекта из последовательности байтов, называется
десерuалuзацuей.
Во всех предыдущих примерах мы записывали в файл только простейшие типы
данных, преобразовывая их в двоичную последовательность. Это тоже можно на
звать сериализацией. Затем мы читали из файла последовательность байтов и, зная,
по какому смещению какие типы данных должны быть записаны, преобразовывали
прочитанную двоичную строку к нужному классу. Это можно назвать простейшей
десериализацией.
Однако часто нужно сохранять не только простейшие типы данных, но и целые эк
земпляры классов, внутри которых могут содержаться ссылки на другие объекты.
При этом размер данных внутри класса может меняться, например, если он содер
жит списки или словари с заранее не известным количеством элементов. В этом
случае нужно хорошо продумать формат записи таких данных, чтобы можно было
определить, по какому смещению в файле записаны байты, относящиеся к одному
объекту, а по какому- к другому. Это кропотливая работа, а реализация такой
обобщенной сериализации и десериализации требует написания достаточно боль
шого количества кода.
К счастью, в
Python
для этой цели предусмотрено несколько модулей, позволяю
щих сохранять объекты в различных форматах. В первую очередь
формат и текстовый формат
JSON.
-
это двоичный
Для сериализации и десериализации объектов в
двоичном формате предназначен модуль pickle, а в формате
JSON -
модуль j son.
Напишем пример из области радиотехники, в котором будем сохранять в файл ин
формацию об антенной решетке
антенна (листинг
Листинг 20.16.
iиport
совокупности антенн, работающих как единая
-
20.16).
Chapter_20/example_16/antenna_array.py
pickle
class Point:
"""Класс для
def
представления
init (self,
self.x = х
self.y = у
self. z = z
х:
точки в
float,
у:
трехмерном пространстве."""
float, z: float):
Часть
384
def
11.
Основные подходы
str (self):
return f"({self.x), {self.y), {self.z))"
class Source:
def
init
(self, point: Point, frequency: float,
mag:float, phase_deg:float):
"'""' Класс
дпя представления источника
электромагнитного излучения."""
self.point = point
frequency
self.frequency
self .mag = mag
self.phase_deg
phase_deg
def
str (self):
return (f"""Координаты: {self.point),
Частота: {self.frequency) Гц,
Амплитуда: {self.mag) В,
Фаза: {self.phase_deg) град.""")
class AntennaArray:
def
init (self, sources: list[Source], comment: str):
"""Класс дпя представления антенной решетки."""
sources
self.sources
self.comment = comment
if
name
main
sources = [
Source(Point(O.O, О.О, О.О), le9, 1.0, О.О),
Source(Point(0.15, О.О, О.О), le9, 1.0, 30.0),
Source(Point(0.30, О.О, О.О), le9, 1.0, 60.0),
antenna array = AntennaArray(sources,
"Пример антенной решетки")
# Сериализация объекта AntennaArray в
file_name = "antenna_array.dat"
with open(file_name, "wb") as file:
pickle.dump(antenna_array, file)
файл с использованием
# Десериализация объекта AntennaArray из файла
with open(file_name, "rb") as file:
antenna_array_loaded = pickle.load(file)
# Вывод информации о загруженной антенной решетке
print(f"{antenna_array_loaded.comment)")
for source in antenna_array_loaded.sources:
print(source)
print ()
pickle
Глава
20.
385
Запись и чтение файлов
В этом примере мы создаем три класса. Класс Point описывает координаты источ
ника (антенного элемента в антенной решетке) и содержит три числа с плавающей
точкой. Класс Source описывает антенный элемент и содержит информацию о его
координатах (экземпляр класса Point), а также о частоте излучаемой электромаг
нитной волны, ее амплитуде и фазе (три числа с плавающей точкой). Класс
AntennaArray
ляров класса
описывает антенную решетку в целом, он содержит список экземп
Source,
а также строку с комментарием.
Такую вложенную структуру мы сериализуем с помощью модуля pickle и резуль~
тат записываем в файл
antenna_array.dat.
Затем открываем полученный файл, десе
риализуем записанные в нем данные в объект antenna_array_loaded и выводим в
консоль информацию о прочитанной антенной решетке.
Обратите внимание, что в этом примере для создания наглядного строкового пред
ставления объектов в классах Point и source определяется «магический» метод
_ st r_
( ) , про который мы говорили в главе
14.
Результат выполнения этого скрипта приведен далее:
Пример антенной решетки
Координаты:
(О.О,
О.О,
1000000000.0
Амплитуда: 1.0 В
Частота:
Фаза:
О.О)
Гц
О.О град.
(0.15, О.О, О.О)
1000000000.0 Гц
Амплитуда: 1.0 В
Фаза: 30.0 град.
Координаты:
Частота:
(0.3, О.О, О.О)
1000000000.0 Гц
Амплитуда: 1.0 В
Фаза: 60.0 град.
Координаты:
Частота:
Двоичный файл, создаваемый с помощью модуля pickle, удобен, если известно,
что этот файл будут читать из скрипта, написанного на
Python.
Если же такой га
рантии нет, и нужен более стандартизированный формат вывода, то можно исполь
зовать формат
JSON и
модуль j son для работы с этим форматом данных.
Читать данные с помощью модуля
pickle следует только из проверенных источни
ков, поскольку для десериализации этот модуль использует не безопасный с точки
зрения уязвимостей код. Модуль j son не подвержен таким уязвимостям, но размер
текстового файла
JSON
будет занимать намного больше места по сравнению с дво
ичным представлением, создаваемым pickle. Если это важно, можно обратиться к
сторонним модулям, предоставляющим другие сериализаторы. Например, те, что
сохраняют данные в стандартизированный формат
главе
26 мы
BSON
(двоичный
JSON).
А в
рассмотрим другие форматы данных, которые используются в том чис
ле для хранения больших данных.
Часть
386
11.
Основные подходы
Заключение
В этой главе мы научились записывать данные в файл и читать их из файла. Перед
записью или чтением файла его нужно открыть с помощью встроенной функции
open (), принимающей, кроме имени файла, параметр, обозначающий, в каком ре
жиме нужно открыть файл: для чтения, для записи или для того и другого. Файлы
также могут быть открыты в текстовом и двоичном режимах.
Важно не забывать закрывать файл сразу после завершения работы с ним. Для ав
томатического закрытия файлов и других объектов, которые требуют освобожде
ния ресурсов, в языке
Python
предусмотрена конструкция wi t h ... а s.
При работе с текстовыми данными нельзя забывать о кодировках, с учетом которых
символы будут представлены в виде последовательности байтов.
Для представления двоичных данных в
Python
предназначены классы bytes и
bytearray.
Преобразование объекта из класса str в двоичную строку bytes называется коди
рованием и осуществляется с помощью метода encode () класса str. Обратное пре
образование из двоичной строки bytes в строку str называется декодирование,1,1 и
осуществляется с помощью метода decode () класса bytes. Для декодирования так
же можно использовать встроенную функцию str () с указанием кодировки.
Старые кодировки, которые не поддерживают
Unicode,
не могут представлять мно
гие символы, и в этом случае либо возбудится исключение, либо проблемные сим
волы будут заменены на разрешенные АSСП-символы.
Преобразование объектов в последовательность байтов называется сериализацuей,
а обратная процедура
дят два модуля для
-
десерuализацией. В стандартную библиотеку
сериализации
и десериализации:
с
помощью
Python
модуля
можно преобразовать объект в bytes (и обратно), а с помощью модуля j son образовать сложный объект в текстовый формат
JSON.
вхо
pickle
пре
- ГЛАВА 21
-
Работа с файловой системой
Проблема формирования путей до файлов
В предыдущей главе при записи и чтении файлов мы предполагали, что файлы рас
положены в текущем рабочем каталоге. Такая ситуация бывает далеко не всегда
-
часто требуемые файлы располагаются где-то в подкаталогах, и полный путь до
файлов нужно правильно сформировать.
Предположим, вы используете
Python
во множестве файлов с именами вроде
для обработки данных, которые сохранены
data_001.dat, data_002.dat, ... , data_451.dat,
рас
положенных в подкаталогах, как показано далее:
L. data
f--
001
1
f-- data_001.dat
f-- data_002.dat
f-- ...
1
L
L
042
1
1
data 451.dat
f-- data_00l.dat
f-- data_002.dat
f-- ...
L
data 451.dat
Путь до каждого файла нужно сформировать программно, а затем с каждым фай
лом что-то сделать. Если мы работаем в операционной системе
мог бы выглядеть примерно так, как показано в листинге
Листинг 21 .1. Chapter_21/example_01/path_gen_slash.py
root = "data"
for n in range(l, 43):
for m in range(l, 452):
fllename = f"(root)\\(n:0Зg)\\data
# Что-то делаем с файлом ...
{m:0Зg).dat"
21. 1.
Windows,
то код
Часть
388
11.
Основные подходы
Но с этим кодом есть как минимум одна проблема. Если мы попытаемся запустить
этот скрипт под операционными системами
Linux
или
macOS,
то при попытке от
крыть файл по сформированному пути получим ошибку, сообщающую об отсутст
вии файла. Дело в том, что в большинстве операционных систем, отличных от
Windows,
в путях к файлам используются не обратные слеши
«\»,
а прямые
- «/>>.
В нашем примере мы могли бы пойти на хитрость, заключающуюся в том, что
Windows
нормально воспринимает также прямые слеши, и могли бы использовать
их в строке формирования пути:
filename = f"{root}/{n:03g}/data_{n:03g}_{m:03g}.dat"
Но все равно этот прием не очень красивый из-за наличия предположений о разде
лителях в операционных системах. Более аккуратный подход мог бы состоять в
том, чтобы узнать разделитель путей для текущей операционной системы с помо
щью переменной os. sep и использовать это значение вместо явно указанного еле
ша (листинг
21.2).
import os
sep = os.sep
root = "data"
for n in range(l, 43):
for m in range(l, 452):
fllename = f" {root }{sep} {n: ОЗg} {sep}data {m: ОЗg} . dat"
# Что-то делаем с файлом ...
Этот подход уже более строгий, но явно менее читаемый.
К счастью, у нас есть целых два инструмента для аккуратного и простого формиро
вания путей. Это модуль os .path и более новый модуль pathlib, предоставляющий
возможность использовать к работе с файловой системой объектно-ориентирован
ный подход. Сначала мы рассмотрим модуль os. ра th.
Формирование путей до файлов.
Модуль
os.path
Модуль os .path содержит достаточно много функций для работы с путями файло
вой системы. Некоторые из наиболее часто используемых приведены в табл.
Таблица
21.1.
21.1.
Наиболее часто используемые функции для работы
с путями файловой системы
Функция
join ()
Назначение
Формирование пути объединением переданных имен каталогов
и/или файлов с использованием правильного разделителя
Глава
21.
389
Работа с файловой системой
Таблица
Функция
21.1
(окончание)
Назначение
Преобразование (разделение) полного пути в кортеж вида («голова»,
«хвост»), где «хвост»
spli t ()
-
это самый правый компонент пути (имя файла
или каталога), а «голова»
-
остальная часть пути
dirname ()
Получение только «головы» из результата работы функции spli t ()
basename ()
Получение только «хвоста» из результата работы функции spli t ()
spli text ()
Разделение пути на расширение файла и всё остальное
abspath ( 1
Преобразование относительного пути в абсолютный
Возвращает тrue, если переданный в качестве параметра путь существует,
exists ()
и
False
Возвращает True, если переданный в качестве параметра путь существует
isfile ()
и указывает на файл (не каталог), и False в противном случае
Возвращает тrue, если переданный в качестве параметра путь существует
isdir()
и указывает на каталог (не файл), и False в противном случае
Пример из листинга
(листинг
в противном случае
21 .2
мы можем переписать с использованием функции j о i n ( )
21.3).
Листинr 21.3.
Chapter_21/example_03/pathjoln.py
i.uport os.path as
ор
root = "data"
for n in range(l, 43):
for m in range(l, 452):
filename = ор. join (root, f" {n:OЗq}", f"data_{m: ОЗq} .dat")
#
Что-то делаем с файлом ...
Здесь мы передаем в функцию join () части пути, а в качестве результата получаем
путь, сформированный из переданных частей с правильным разделителем между
ними. То, что формируемый путь может не существовать на диске, для функции
join ()
не важно.
Для следующих экспериментов с модулем os. ра th нам понадобится полный путь
до какого-нибудь существующего на диске файла. Пусть это будет путь до самого
запускаемого скрипта. Узнать этот путь не составляет труда, благодаря тому, что в
каждом Руthоn-модуле доступна переменная _file_, содержащая абсолютный
путь к файлу модуля, внутри которого эта переменная используется
(см.
главу
12).
Часть
390
11.
Основные подходы
Для начала напишем однострочный скрипт, который выводит в консоль перемен
ную
_file_
Листинг 21.4.
pпnt(f"{
Результат
(листинг
21.4).
Дальнейшие примеры будут дополнять этот скрипт.
Chapter_21/example_04/flle.py
file_=)")
выполнения
этого
скрипта
у
каждого
пользователя
будет
свой.
Например, он может быть таким:
_file_='C:\\python_book\\chapter_21\\example_04\\file.py'
Функция split (), в противоположность j oin (), разделяет путь на две части и воз
вращает кортеж из двух элементов. Создадим новый пример и посмотрим, что в
этот кортеж попадает (листинг
Листинг
21.5).
21.5. Chapter_21/example_05/1pllt.py
import os.path as
print(f"{
ор
file_=)", end="\n\n")
splitted_l = op.1plit(_file_)
print(f"{splitted_l=)", end="\n\n")
splitted_2 = op.1plit(splitted_l[O])
print(f"{splitted_2=)", end="\n\n")
Результат выполнения этого скрипта может выглядеть так:
_file_='C:\\python_book\\chapter_21\\example_05\\split.py'
splitted_l=('C:\\python_book\\chapter_21\\example_05', 'split.py')
splitted_2=('C:\\python_book\\chapter_21', 'example_05')
В этом примере мы дважды используем функцию
функцию переменную
_
spli t (). Если передать в эту
f i 1 е _, то в первый элемент кортежа («голову») попадет
путь до каталога, в котором расположен скрипт. Имя файла без пути попадет во
второй элемент кортежа ( «хвосп> ).
Затем мы еще раз применяем функцию spli t (), передав ей «голову», полученную
от предыдущего вызова. На этот раз в «голову» попал путь, за исключением имени
конечного каталога, а имя конечного каталога попало в «хвост».
Иногда для решения задачи не требуется одновременно знать и путь к каталогу, где
лежит файл, и имя самого файла, но требуется что-то одно из этого. В этом случае
могут пригодиться следующие две функции:
♦
функция dirname () возвращает путь до каталога, в котором расположен указан
ный файл или каталог;
Глава
21.
Работа с файловой системой
391
♦ функция basename () возвращает только имя указанного файла или каталога без
ПУТИ ДО НИХ.
Использование этих функций показано в листинге
21.6.
nистинrе 21.8. Chapter_21/example_08/dlrn1me_ЬaIename.py
import os.path as
ор
print(f"(_file_=}", end="\n\n")
dirname = op.dirname( file_)
basename = op.basename(_file_)
print(f"(dirname=}", end="\n\n")
print(f"(basename=}", end="\n\n")
В результате выполнения этого скрипта будет выведен следующий текст:
_file_='C:\\python_book\\chapter_21\\example_06\\dirname_basename.py'
dirname='C:\\python_book\\chapter_21\\example_06'
basename='dirname_basename.py'
Функции
split ()
или
dirname ()
часто используют, когда нужно узнать каталог, в
котором содержится указанный файл. Например, это может пригодиться, если в
этот же каталог нужно записать результат обработки данных или в этом же катало
ге найти другие файлы с данными.
Функция basename () полезна, когда требуется создать новый файл на основе имени
уже существующего файла. Например, если у нас имеется файл data_123.dat, и мы
хотим на основе данных из этого файла создать файл изображения с именем
data_ 123.рпg.
В последнем случае нам еще нужно будет разделить имя файла на непосредственно
имя и расширение. В этом нам поможет функция spl i text (), которая также воз
вращает кортеж из двух элементов: во второй элемент кортежа попадает расшире
ние файла (если оно есть), включая точку, а в первый элемент
Следующий скрипт (листинг
функции
Chapter_21/example_07/splltext.py
import os.path as
ор
file_=}", end="\n\n")
dirname = op.d1rname( file_)
basename = op.basename( file
всё остальное.
показывает несколько примеров использования
spli text ().
Листинг 21.7.
pпnt(f"(
21.7)
-
Часть
392
11.
Основные подходы
print (f" (op.splitext(_file_) =) ", end="\n\n")
print (f" (op.splitext (dirname) =) ", end="\n\n")
print(f"(op.splitext(Ьasename)=)", end="\n\n")
Здесь мы сначала применяем функцию spli text () к полному пути до файла, за
тем
-
к пути до каталога, который не имеет в своем имени точки, а затем только к
имени файла.
В результате выполнения этого скрипта мы получим следующий вывод:
file_='C:\\python_book\\chapter_21\\example_07\\splitext.py'
op.splitext(
file
)=('C:\\python_book\\chapter_21\\example_07\\splitext',
'.ру')
op.splitext(dirname)=('C:\\python_book\\chapter_21\\example_07', '')
op.splitext(basename)=('splitext',
'.ру')
Функция spli text () не проверяет, является ли переданный ей параметр путем до
файла или каталога, и существует ли он. Поэтому, если в эту функцию передать
путь до каталога, имя которого содержит точку, то
spli text ()
выделит из пере
данной строки это «расширение».
Еще одна полезная функция из модуля os. ра th -
это функция abspa th (), преоб
разующая относительный путь в абсолютный. Если в эту функцию передать абсо
лютный путь, то он же и будет возвращен в качестве результата. Относительный
путь в абсолютный преобразуется относительно текущего рабочего каталога.
В листинге
21.8
Листинг 21.8.
приведено несколько вариантов использования функции abspa th ().
Chapter_21/example_08/path_abspath.py
import os
import os.path as
ор
cwd = os. getcwd ()
print(f"(cwd=)", end="\n\n")
rel_paths = [
"example_l.txt",
op.join("subdir_l", "subdir_2", "example_2.txt"),
op.join(" .. ", " .. ", "example 3.txt"),
for rel_path in rel_paths:
abs_path = op.abspath(rel_path)
print(f"(rel_path=)")
print(f"(abs_path=)", end="\n\n")
Глава
21.
393
Работа с файловой системой
Здесь мы сначала с помощью функции os. cwd () получаем путь до текущего рабоче
го каталога и выводим его в консоль. Далее из относительных путей
(список
rel_path) формируются три абсолютных пути (строковая переменная abs_path).
Первый относительный путь
-
это просто имя файла. При получении абсолютного
пути перед ним добавляется путь до текущего рабочего каталога. Затем мы показы
ваем, что относительный путь может содержать также подкаталоги. И третий отно
сительный путь содержит каталоги с именем
« .. »,
что обозначает переход к роди
тельскому каталогу, и поэтому абсолютный путь содержит на два вложенных ката
лога меньше.
Результат выполнения этого скрипта может быть следующим:
cwd='C:\\python_book\\chapter_21\\example_08'
rel_path='example_l.txt'
abs_path='C:\\python_book\\chapter_21\\example_08\\example_l.txt'
rel_path='subdir_l\\subdir_2\\example_2.txt'
abs_path='C:\\python_book\\chapter_21\\example_08\\subdir_l\\subdir_2\\example_2.txt'
rel_path=' .. \\ .. \\example_З.txt'
abs_path='C:\\python_book\\example_З.txt'
Следующие три функции из табл.
21.l: exists (), isfile ()
и isdir () -
позволяют
определить существование указанного файла или каталога, а также определить, на
какую сущность указывает путь, если он существует.
В листинге
21.9
показано использование этих функций применительно к разным
видам путей.
import os.path as
ор
file_basename = op.basename(
cwd_dir = op.dirname( file
file
pr1nt(f"{op.ex1sts( file )=}")
pr1nt(f"{op.isf1le( file )=}")
print(f"{op.isdir( file )=}")
print ()
print(f"{op.exists(file_basename)=}")
print (f" (ор. isfile (file basename) =} ")
print(f"{op.isdir(file_basename)=)")
print ()
print(f"{op.exists(cwd_dir)=}")
print(f"{op.isfile(cwd dir)=}")
print(f"{op.isdir(cwd dir)=}")
Часть
394
11.
Основные подходы
print ()
print(f"{op.exists('. ')=)")
print (f"(op.isfile ('. ') =}")
print(f"(op.isdir('. ')=)")
print ()
print(f"(op.exists(' .. ')=)")
print(f"{op.isfile(' .. ')=)")
print (f"(op.isdir (' .. ')=)")
print ()
print (f"{op.exists ( 'invalid. txt' )=) ")
print(f"{op.isfile('invalid.txt')=)")
print(f"{op.isdir('invalid.txt')=)")
Здесь функции exists (), isfile () и isdir () применяются к полному пути до за
пускаемого скрипта, к его относительному пути file_basename, к текущему рабо
чему каталогу, к двум специальным каталогам:
лог, и
11 • • 11 ,
11 • 11 ,
что обозначает текущий ката
что обозначает родительский каталог, а также к файлу с именем
invalid.txt, который должен отсутствовать. В результате выполнения этого скрипта в
консоль будет выведен следующий текст:
op.exists( file )=True
op.isfile( file )=True
op.isdir( file )=False
op.exists(file_basename)=True
op.isfile(file_basename)=True
op.isdir(file_basename)=False
op.exists(cwd_dir)=True
op.isfile(cwd_dir)=False
op.isdir(cwd_dir)=True
op.exists('. ')=True
ор. isfile ('. ') =False
ор. isdir ('. ') =True
op.exists(' .. ')=True
op.isfile(' .. ')=False
ор. isdir (' .. ') =True
op.exists('invalid.txt')=False
op.isfile('invalid.txt')=False
op.isdir('invalid.txt')=False
Обратите внимание, что функции isfile () и isdir () возвращают False, если в
качестве параметра им передать путь до отсутствующего файла или отсутствующе
го каталога. Результаты остальных вызовов исследуемых здесь функций очевидны.
Глава
21.
Работа с файловой системой
395
Формирование путей до файлов.
Модуль
pathlib
Модуль os. path удобен и часто используется, однако в стандартную библиотеку
Python
включен также модуль pathlib, позволяющий решать похожие задачи в бо
лее объектно-ориентированном стиле с помощью класса Ра th.
Изучение класса Path начнем с конструктора, выполняя первые примеры в инте
рактивном режиме. Если конструктору не передавать никакие параметры, то это
будет равносильно тому, что мы указали текущий рабочий каталог, но не по абсо
лютному пути, а по относительному
-
с помощью точки
" . ":
>>> from pathlib import Path
»> foo = Path ()
»> foo
WindowsPath (' . ')
>» print(foo)
»> str (foo)
Обратите внимание, что если мы выполняем этот пример под
Windows,
ве результата вызова конструктора получим экземпляр класса
выполнении этого же кода под
Linux
или
macOS -
то в качест
WindowsPath,
а при
PosixPath. Оба эти класса
производные от класса Ра th, который, в свою очередь, является производным от
PurePath.
Экземпляры класса Path преобразуются в строку с помощью встроенной функции
str (). Это часто приходится делать, когда мы имеем дело с библиотекой, не под
держивающей работу с классами из модуля pathlib, а ожидающей в качестве па
раметров строку с путем до файла или каталога. Однако многие функции и методы
классов из стандартной библиотеки, если они ожидают в качестве параметра путь
до файла или каталога, могут принимать не только строку, но и экземпляры класса
Path. В частности, это относится и к функциям из модуля os .path, которые мы
изучали в предыдущем разделе.
В качестве параметра конструктору класса Path можно передавать строку с абсо
лютным или относительным путем (не важно, существует указанный путь или нет):
>>> bar = Path ("example. txt")
>>> bar
WindowsPath('example.txt')
»> spam = Path ("с:\ \projects \ \python \ \example .ру")
»> spam
WindowsPath('c:/projects/python/example.py')
»> baz = Path("examples\\images\\42.png")
»> baz
WindowsPath('examples/images/42.png')
396
Часть
Впрочем, последние примеры
-
с созданием переменных
11.
Основные подходы
spam и baz -
не очень
хороши по той же причине, которую мы обсуждали в самом начале этой главы: при
использовании обратных слешей в явном виде мы теряем кроссплатформенность.
Интересно, что при выводе в консоль объектов spam и baz даже под
Windows
в пу
тях мы увидим прямые слеши.
Прямые слеши в строковых литералах, конечно, решат проблему, но есть более ак
куратный способ создания путей
-
передавать в конструктор Path элементы пути,
которые требуется «склеить» в полный путь. Это аналог функции os. ра th. j oin ():
>>> baz = Path("examples", "images", "42.png")
>>> baz
WindowsPath('examples/images/42.png')
Если у нас уже есть экземпляр класса Path, а нам нужно создать путь, вложенный в
него, то есть очень красивый способ сделать это, благодаря перегрузке оператора
"/":
>>> foo = Path ("examples")
>>> bar = foo / "images" / "42.png"
>>> bar
WindowsPath('examples/images/42.png')
Этот же пример можно было бы написать, используя конструктор Path, который в
качестве параметров может принимать не только строки, но и экземпляры класса
Path:
>>> foo = Path("examples")
>>> bar = Path(foo, "images", "42.png")
>>> bar
WindowsPath('examples/images/42.png')
Класс Path содержит два метода класса, предназначенных для создания экземпля
ров класса Path, содержащих пути до особых каталогов: метод home () -
создает
экземпляр Path, содержащий абсолютный путь до каталога профиля пользователя,
а метод cwd ( ) -
до текущего рабочего каталога:
»> Path.home ()
WindowsPath ('С: /Users/username')
>» Path. cwd ()
WindowsPath('C:/python_book/chapter_2l')
Кроме того, класс Path содержит множество свойств и функций, по своим действиям
аналогичных функциям из модуля os .path. Коротко рассмотрим их работу на при
мерах (листинг
Листинr
21.1 О).
21.10. Chapter_21/example_10/path_methods.py
from pathlib import Path
foo = Path( file
print(f"{foo=)")
Глава
21.
397
Работа с файловой системой
print (f" ( foo .exists () =) ")
print (f" {foo .is_file () =) ")
print ( f" {foo. is_dir () =) ")
print ()
bar = Path("path_methods.py")
print(f"{bar=)")
print ( f" (bar. aЬsolute О=)")
print(f"{foo.samefile(bar)=)")
print ()
spam = Path("examples", "images", "42.png")
print(f"{spam=)")
print (f" {spam .aЬsolute () =) ")
Результат выполнения этого скрипта будет выглядеть примерно так:
foo=WindowsPath('C:/python_book/chapter_21/example_10/path_methods.py')
foo.exists()=True
foo.is_file()=True
foo.is_dir()=False
bar=WindowsPath('path_methods.py')
bar.absolute()=WindowsPath('C:/python_book/chapter 21/example_lD/path_methods.py')
foo.samefile(bar)=True
spam=WindowsPath('examples/images/42.png')
spam.aЬsolute()=WindowsPath('C:/python_book/chapter_21/example_10/examples/images/42.png')
Методы exists (), is_file () и is_dir () аналогичны одноименным функциям из
модуля os. path. Метод absolute () аналогичен функции abspath () из того же мо
дуля.
В коде листинга
21.10
стоит обратить внимание на метод samefile (), принимаю
щий в качестве параметра экземпляр класса
Path
или строку с путем, а возвра
щающий тrue, если путь в переданном параметре указывает на тот же файл или
каталог, что и объект, для которого метод samefile () вызван. При этом учитывает
ся, являются ли пути относительными или абсолютными, что и показано в примере,
касающемся этого метода.
Рассмотрим теперь некоторые свойства, которые содержатся в классе Path (лис
тинг
21.11 ).
from pathlib import Path
foo = Path( file
print(f"{foo=)")
398
Часть
11.
Основные подходы
print(f"{foo.parent=}"I
print(f"{foo.name=)"I
print (f" {foo .stem=) ")
print(f"{foo.suffix=}")
print(f"{foo.drive=}")
Этот скрипт выведет следующий текст:
foo=WindowsPath('C:/python_book/chapter_21/example 11/path_properties.py')
foo.parent=WindowsPath('C:/python_book/chapter_21/example_ll')
foo.name='path_properties.py'
foo.stem='path_properties'
foo. suffix=' . ру'
foo.drive='C:'
Скорее всего, тут особых комментариев не требуется, кроме как для свойства
drive. Под Windows это свойство возвращает имя диска, на котором расположен
файл, а для тех файловых систем, где нет такого понятия как диск (например, в
Linux
и
macOS),
это свойство равно пустой строке.
И в отдельном примере рассмотрим еще два свойства класса Path, которые разде
ляют путь на его составляющие (листинг
Листинг
21.12).
21 .12. Chapter_21/example_12/path_parts.py
from pathlib import Path
foo = Path( file
print(f"{foo=)", end="\n\n")
print(f"{foo.parts=}", end="\n\n")
print(f"{list(foo.parents)=)")
Свойство parts возвращает кортеж, который содержит все составляющие пути, на
чиная с имени диска (под
Windows)
и до имени файла. Свойство parents возвраща
ет итерируемый объект (в примере он сразу преобразуется в список) из объектов
Ра th, которые для исходного пути являются родителем, затем родителем родителя
и так далее до корня файловой системы.
В результате скрипт примера должен вывести в консоль примерно такой текст:
foo=WindowsPath('C:/python_book/chapter_21/example_l2/path_parts.py')
foo.parts={'C:\\', 'python_book', 'chapter_21', 'example_l2', 'path_parts.py')
list(foo.parents)=(WindowsPath('C:/python_book/chapter_21/example_l2'),
WindowsPath('C:/python_book/chapter_21'), WindowsPath('C:/python_book'),
WindowsPath ('С:/') ]
Глава
21.
Работа с файловой системой
399
Создание,копирование,перемещение
и удаление файлов и каталогов
Для выполнения описанных в заголовке раздела действий мы воспользуемся сразу
несколькими стандартными модулями: os, shutil и pathlib. Модуль os содержит
функции для базовых действий с файлами и каталогами, с помощью функций из
модуля shutil можно выполнять более высокоуровневые действия, затрагивающие
целые ветви файловой системы, а методы класса Path из модуля pathlib дублиру
ют действия функций обоих этих модулей.
Создание пустых файлов
Начнем с простого
-
создания файла, который мы потом будем удалять или куда
нибудь копировать. Нам не важно, что будет в нем записано,
-
он будет оставаться
пустым. Чтобы создать пустой файл, достаточно открыть его в режиме для записи
с помощью функции open () и тут же закрыть,
это заняло бы две строчки кода.
-
А можно воспользоваться методом touch () из класса Ра th -
той
файл.
Метод
touch ()
может
принимать
он тоже создает пус
необязательный
булев
параметр
exist_ok, определяющий, что делать, если файл уже существует: если exist_ok
равен True (это значение по умолчанию), то ничего не происходит, а если он равен
False, то будет возбуждено исключение FileExistsError.
Следующий пример (листинг
21.13)
создает пустой файл и подтверждает его нали
чие с помощью инструкции assert. Мы давно эту инструкцию не использовали
(с
ней мы познакомились еще в главе
исключение
AssertionError,
3),
поэтому вспомним, что она возбуждает
если выражение после ключевого слова
assert
равно
False.
from pathlib import Path
filename = "myfile.txt"
file_path = Path(filename)
file_path.touch()
assert file_path.exists()
Файл с именем
myfile.txt
будет создан в текущем рабочем каталоге.
Создание каталогов
Теперь нам надо научиться создавать каталоги. Для этого у нас есть несколько ин
струментов. Начнем с самого низкоуровневого
-
Эта функция
нее
создает пустой
каталог,
но у
функции mkdir () из модуля os.
есть
несколько ограничений.
Во-первых, создаваемый каталог не должен существовать на момент вызова функ-
Часть
400
ции, иначе будет возбуждено исключение
11.
Основные подходы
FileExistsError. И, во-вторых, должен
существовать родительский каталог, в котором создается новый каталог, иначе бу
дет возбуждено исключение F'ileNotFoundError. Это значит, что если мы хотим
создать дерево каталогов,
-
например,
examples/data/example_001, examples/data/example_002 и
examples, затем examptes/data, и только потом
examples/data/example_001 и examples/data/example_002. При этом на каждом шаге нам придется
т. д., то должны сначала создать каталог
проверять, что создаваемый каталог еще не существует. Покажем это на примере
(листинг
21.14).
Листинг 21.14.
Chapter_21 /example_14/mkdir.py
import os
import os.path as
ор
dir_l = "examples"
if not op.exists(dir 1):
os.mkdir(dir_l)
dir_2 = op.join(dir_l, "data")
if not op.exists(dir_2):
os.mkdir(dir_2)
dir 3 = op.join(dir_2, "example_OOl")
if not op.exists(dir_З):
os.mkdir(dir_З)
В результате выполнения этого скрипта в текущем рабочем каталоге будет создан
подкаталог
examples/data/example_001. Повторный запуск скрипта не приведет к
ошибке . Такой способ создания большого количества каталогов не очень удобный,
он больше подходит для создания каталогов без сложной вложенности.
Для более удобного создания дерева каталогов в модуле os существует функция
makedirs (), рекурсивно создающая сразу дерево каталогов. Кроме того, у функции
makedirs () имеется именованный параметр exist_ok, определяющий поведение
функции в случае, когда конечный создаваемый каталог уже существует. Если па
раметр exist_ok равен False (это значение по умолчанию), то в случае, когда ко
нечный каталог существует, возбуждается исключение FileExistsError, а если он
равен True, то функция makedirs () успешно завершается.
С помощью функции makedirs () пример из листинга
кратить (листинг
Листинг 21.15.
21.15).
Chapter_21/example_15/makedirs.py
import os
import os.path as
ор
21.14
мы можем сильно со
Глава
21.
Работа с файловой системой
401
dir_full = op.join("examples", "data", "example_OOl")
os.makedirs(dir_full, exist_ok=True)
assert op.isdir(dir_full)
Если мы используем класс Path, то можем использовать его метод mkdir (), спо
собный работать и как os .mkdir (), и как os .makedirs (). Метод mkdir () имеет сле
дующий синтаксис:
Path.mkdir(mode=Oo777, parents=False, exist_ok=False)
♦
параметр mode устанавливает права на создаваемый каталог для тех файловых
систем, где есть такая поддержка. Мы этим параметром пользоваться не будем;
♦
параметр parents определяет, нужно ли создавать промежуточные родительские
каталоги. Если parents равен False (значение по умолчанию), то родительские
каталоги не создаются, и, если их не существует, то возбуждается исключение
FileNotFoundError. Если же параметр parents равен True, то отсутствующие
родительские каталоги будут созданы;
♦
параметр
exist ok
os .makedirs ().
Пример из листинга
аналогичен
21.15
дующим образом (листинг
одноименному
параметру
из
функции
мы можем с использованием класса Ра th переписать сле
21.16).
from pathlib import Path
dir_full = Path("examples", "data", "example_OOl")
dir_full.mkdir(parents=True, exist_ok=True)
assert dir_full.is_dir()
Копирование файлов
Для копирования файлов в модуле shutil существуют две функции: copyfile () и
сору () . Их объявление выглядит похожим образом:
shutil.copyfile(src, dst, *, follow_symlinks=True)
shutil.copy(src, dst, *, follow_symlinks=True)
Здесь параметр src определяет исходный файл, который нужно скопировать, а па
раметр dst задает назначение, куда файл должен быть скопирован. Разница между
этими функциями заключается в интерпретации параметра ds t:
♦
функция сору f i 1 е () подразумевает, что
ds t -
это полный путь до конечного
файла, которым должен стать исходный файл;
♦
функция сору () более гибкая: если dst содержит имя файла, файл sгс копирует
ся в файл dst, если dst содержит имя существующего каталога, файл sгс будет
скопирован в этот каталог.
402
Часть
В качестве параметров
11. Основные подходы
src и dst можно указывать как строки, так и объекты Path.
Параметр follow_symlinks определяет, как поступать, если в качестве src указана
символическая ссылка на файл. Мы не станем рассматривать этот случай.
Если файл, указанный в параметре dst, уже существует, он будет перезаписан. При
возникновении ошибок будут возбуждаться исключения, производные от класса
OSError.
Для демонстрации процесса копирования файлов объединим примеры из предыду
щих разделов
-
пустой каталог
в следующем скрипте (листинг
examples/data/example_001,
21.17)
мы создадим пустой файл и
после чего созданный файл будет скопи
рован в этот каталог разными способами.
Листинг 21.17.
Chapter_21/example_17/shutil_copy .ру
from pathlib import Path
import shutil
src_file = "myfile.txt"
Path(src_file) .touch()
dir_full = Path("examples", "data", "example_00l")
dir_full.mkdir(parents=True, exist ok=True)
dst file 1
dst file 2
"myfile l.txt"
"myfile 2.txt"
shutil.copyfile(src_file, dir_full / dst_file_l)
shutil.copy(src_file, dir_full / dst_file_2)
shutil.copy(src_file, dir_full)
assert (dir full / src_file) .exists()
assert (dir_full / dst_file_l) .exists()
assert (dir_full / dst_file_2) .exists()
рrint("Файлы скопированы")
В
результате
выполнения
ples/data/example_001
этого
появятся файлы
ждается инструкциями
скрипта
в
создаваемом
myfile.txt, myfile_ 1.txt
и
каталоге
myfile_2.txt,
exam-
что подтвер
assert.
Этот пример показывает, что, применяя функции из модуля shutil, мы можем од
новременно использовать в качестве параметров как строки, так и объекты Ра th.
В
Python 3.14
в класс Path были добавлены методы сору() и copy_into(), также
предназначенные для копирования файлов и каталогов. Пример из листинга
21 . 17
с использованием этих методов может быть переписан следующим образом (лис
тинг
21.18).
Глава
21.
Работа с файловой системой
Листинг 21.18.
403
Chapter_21/example_18/path_copy.py
from pathlib import Path
src_file = Path ("myfile. txt")
src_file.touch()
dir_full = Path("examples", "data", "example_00l")
dir_full.mkdir(parents=True, exist_ok=True)
dst file 1
dst file 2
"myfile l.txt"
"myfile 2.txt"
src_file.copy(dir_full / dst_file_l)
src_file.copy(dir_full / dst_file_2)
src_file.copy_into(dir_full)
pr int ( "Файлы
скопированы")
Копирование каталогов
Для копирования каталогов со всем их содержимым, включая подкаталоги, предна
значена функция copytree ()
Python 3.14
сору
()
из модуля shutil. Кроме нее, если вы используете
или более новую его версию, то также можете применять методы
и сору
into ()
из класса
Path.
В следующем примере (листинг
21.19)
создается каталог src/data_001, а в нем
-
пустой файл myfile.txt. После этого создается каталог dst, в который с помощью
функции copytree () копируется каталог data_001 с файлом внутри него.
Листинг
21.19. Chapter_21/example_19/shutil_copytree.py
from pathlib import Path
import shutil
dir src = Path ("src")
# Создаем каталог src/data_00l/
dir - src - data = dir - src / "data- 001"
dir_src_data.mkdir(parents=True, exist_ok=True)
# Создаем файл src/data_00l/myfile.txt
file_name = "myfile.txt"
src - file = dir - src- data / file name
src _ file. touch ()
Часть
404
11.
Основные подходы
# Создаем каталог dst/
dir_dst = Path ("dst")
dir_dst.mkdir(parents=True, exist_ok=True)
# Копируем каталог src/data_00l/ в каталог dst
dir- dst - data = dir- dst / "data- 001"
shutil.copytree(dir_src_data, dir_dst_data, dirs_exist_ok=True)
assert dir_dst_data.exists()
assert (dir_dst_data / file_name) .exists()
print ("Файлы
скопированы")
Удаление файлов и каталогов
С удалением файлов всё просто. Чтобы удалить один файл, в модуле os имеются
remove () и функция с менее очевидным названием unlink (). Они делают одно и то же. Имя unlink - это дань истории UNIX, где для
удаления файла служила одноименная команда. При использовании класса Path
для удаления файла предназначен метод unlink ().
две функции: функция
В следующем скрипте (листинг
21.20)
создаются два файла, и процесс останавли
вается, пока пользователь не нажмет клавишу
<Enter>.
До ее нажатия он может
убедиться, что файлы созданы, а после ее нажатия созданные файлы будут удалены.
Листинг 21.20.
Chapter_21/example_20/remove_files.py
import os
from pathlib import Path
file_l = Path("myfile_l.txt")
file_l. touch()
file_2 = Path("myfile_2.txt")
file 2. touch ()
inрut("Файлы созданы.
Нажмите
Enter,
чтобы их удалить.")
os.remove(file_l)
file _ 2. unlink ()
assert not file 1.exists()
assert not file_2.exists()
рrint("Файлы удалены.")
В этом примере в функцию os. remove () передается экземпляр класса Path, но так
же в нее можно передавать и строки, содержащие путь до файла. В случае возник-
Глава
21.
Работа с файловой системой
405
новения ошибок во время удаления файла будет возбуждаться исключение OSError
или производное от него. В частности, при попытке удалить несуществующий файл
будет возбуждено исключение FileNotFoundError.
С удалением каталогов всё немного сложнее. В модуле os имеется соответствую
щая функция rmdir (), есть также одноименный метод и в классе Path, но они рабо
тают только для пустых каталогов. Если в удаляемом каталоге содержатся файлы
или вложенные каталоги, будет возбуждено исключение osError.
Для удаления каталога со всем его содержимым предназначена функция rmtree ()
из модуля shutil. Аналога этой функции в классе Path на момент подготовки кни
ги нет.
Следующие пример (листинг
21.21) демонстрирует работу
функции rmtree ().
Листинг 21.21. Chapter_21/example_21/rmtree.py
from pathlib import Path
import shutil
dir_full = Path("examples", "data", "example_00l")
dir_full.mkdir(parents=True, exist_ok=True)
file = dir full / "myfile.txt"
file. touch ()
inрut("Каталоги созданы.
Нажмите
Enter
для их удаления.")
examples_dir = Path("examples")
shutil.пntree(examples_dir)
assert not examples_dir.exists()
print ("Каталоги удалены.")
Переименование и перемещение файлов и каталогов
С точки зрения файловой системы, переименование и перемещение файлов и ката
логов
-
это всё одна и та же операция, если она происходит в пределах одной фай
ловой системы (под
Windows -
В стандартной библиотеке
в пределах одного диска).
Python
содержатся несколько функций для переимено
вания файлов и каталогов. Так, в модуле os -
это функции rename () и replace (),
различающиеся тем, что в случае, если мы переименовываем файл и присваиваем
ему
имя,
которое
уже
существует,
OSError, а функция replace () -
функция
rename ()
возбудит
исключение
перезапишет имеющийся файл. Если же у нас
случится аналогичная ситуация при работе с каталогами, то обе эти функции воз
будят исключение OSError. К тому же, эти функции не работают, если нужно пере
нести файл или каталог в другую файловую систему.
Часть
406
11.
Основные подходы
Однако в модуле shutil содержится более высокоуровневая функция move (), не
имеющая ограничения, связанного с переносом файлов и каталогов в другую фай
ловую систему.
У класса Ра th также имеются методы rename () и replace (), аналогичные одно
именным функциям из модуля os, но без ограничения относительно переноса в
другую файловую систему. С методом rename () надо быть осторожнее, поскольку
он работает по-разному в разных операционных системах. Например, под
Windows
попытка переименовать файл, указав имя файла, который уже существует, приве
дет к возбуждению исключения FileExistsError, а в других операционных систе
мах существующий файл будет перезаписан.
Кроме того, в
Python 3.14
в классе Path появились методы move () и move_into (),
которые работают так же, как и функция move () из модуля shutil.
Для демонстрации этих методов рассмотрим два примера. Первый пример (лис
тинг
21.22)
простой
-
в нем создаются шесть пустых файлов, а затем каждый из
них переименовывается своим способом.
Листинг
21.22. Chapter_21/example_22/rename.py
from pathlib import Path
import os
import shutil
for n in range(l, 7):
Path ( f"myfile _ ( n). txt") . touch ()
inрut("Файлы созданы.
Нажмите
Enter
для их переименования.")
os.rename("myfile_l. txt", "new_myfile_l. txt")
os.replace ("myfile_2. txt", "new_myfile_2. txt")
shutil .move ( "myf ile _ 3. txt", "new_ myf ile _ 3. txt")
Path ("myf ile_ 4. txt") .rename ( "new _ myfile _ 4. txt")
Path ( "myfile_ 5. txt") .replace ("new_ myfile _5. txt")
# Path("myfile_б.txt") .move("new_myfile_б.txt")
рrint("Файлы переименованы.")
При запуске этого скрипта до нажатия клавиши
<Enter>
можно убедиться, что соз
даны файлы с именами myfile_ 1.txt ... myfile_б.txt, а после ее нажатия
-
что они пере
именовались. Обратите внимание, что в коде скрипта закомментирована строка с ис
пользованием метода Ра th. move (), так как она будет работать только в
Python 3.14
и выше.
Следующий пример (листинг
21.23)
более сложный
-
в нем показан перенос ката
лога из одного родительского каталога в другой. Скрипт предварительно удаляет с
помошью функции shutil. rmtree () каталоги, которые мы станем потом создавать.
Обратите внимание, что в эту функцию передается дополнительный параметр
Глава
21.
Работа с файловой системой
ignore_errors=True -
407
чтобы не возникало исключений, если каталоги еще не су
ществуют. Затем создаются два каталога: src/data/example_001 с пустым файлом по
имени
myfile.txt и dst. По завершении этой подготовительной части скрипт останав
ливается до тех пор, пока пользователь не нажмет клавишу
им этой клавиши каталог
src/data
<Enter>.
После нажатия
со всем его содержимым переносится в каталог
dst.
Указанная операция осуществляется одним из пяти способов, причем используе
мый для переноса способ при каждом запуске скрипта определяется одной из по
следних пяти строк кода, четыре не задействанные из которых при запуске должны
быть закомментированы.
Листинг 21.23.
Chapter_21/example_23/move_dlr.py
import os
import shutil
from pathlib import Path
shutil.rmtree("src", ignore_errors=True)
src_dir_full = Path ("src", "data", "example_OOl")
src_dir_full.mkdir(parents=True, exist_ok=True)
file = src dir full / "myfile.txt"
file. touch ()
dst_dir = Path("dst")
shutil.rmtree(dst_dir, ignore_errors=True)
dst_dir.mkdir(parents=True, exist_ok=True)
inрut("Каталоги созданы.
Нажмите
Enter
для их переноса.")
os.rename (Path ("src", "data"), Path ("dst", "data"))
# os. replace (Path ("src", "data"), Path ("dst", "data") )
# shutil.move(Path("src", "data"), Path("dst", "data"))
# Path("src", "data") .rename(Path("dst", "data"))
# Path("src", "data") .replace(Path("dst", "data"))
Аналогичный пример можно написать, если использовать
move () или move into () из класса Path (листинг
Листинг 21.24.
Chapter_21/example_24/path_move.py
from pathlib import Path
import shutil
shutil.rmtree("src", ignore_errors=True)
src_dir_full = Path("src", "data", "example_OOl")
src_dir_full.mkdir(parents=True, exist_ok=True)
21.24).
Python 3.14
и метод
Часть
408
11.
Основные подходы
file = src dir full / "myfile.txt"
file. touch ()
dst_dir = Path("dst")
shutil.rmtree(dst_dir, ignore_errors=True)
dst_dir.mkdir(parents=True, exist_ok=True)
inрut("Каталоги созданы.
Нажмите
Enter
для их переноса.")
src_data = Path("src", "data")
src_data.move(Path("dst", "data"))
# src_data.move_into("dst")
В этом примере также при каждом запуске должна быть закомментирована одна из
двух последних строчек.
Заключение
В этом главе мы познакомились со множеством функций, предназначенных для
работы с файловой системой.
Мы начали с рассказа о том, что описание пути до файла или каталога
-
это не
такая тривиальная задача, как может показаться, если требуется, чтобы скрипт ра
ботал в разных операционных системах.
Мы рассмотрели моду ль os . ра th, содержащий ряд функций, позволяющих форми
ровать пути из отдельных его частей, преобразовывать относительные пути в абсо
лютные, разбивать путь до файла на части, а также разделять имя и расширение
файла.
Затем мы изучили класс Path из модуля pathlib, позволяющий делать те же дейст
вия, что и функции из модуля os. ра th, но в объектно-ориентированном стиле.
После этого мы научились создавать новые каталоги, используя функции mkdir () и
makedirs ()
makedirs ()
из модуля os и метод mkdir ()
из класса Path. Узнали, что метод
при этом позволяет создавать сразу вложенные каталоги.
Рассмотрели мы также тему копирования файлов, познакомившись с используемы
ми для этого функциями сору () и copyfile () из модуля shutil, а также методы
сору() и copy_into () из класса Path, которые появились в
Python 3.14.
Научились копировать целые каталоги со всем их содержимым с помощью функ
ции
copytree ()
ИЗ модуля
shutil.
Разобрались с вопросами удаления файлов и каталогов. Для удаления файлов ис
пользовали функцию remove () из модуля os и метод unlink () из класса Path. Уз
нали, что для удаления пустого каталога можно использовать функцию rmdir () из
Глава
21.
409
Работа с файловой системой
модуля os, а для удаления каталога со всем его содержимым
из модуля
shutil
или метод
rmdir ()
из класса
-
функцию rmtree ()
Path.
В завершение главы мы изучили способы переименования и перемещения файлов и
каталогов, рассмотрев используемые для этого функции rename () и replace () из
модуля os, одноименные методы из класса Path, а также функцию move () из моду
ля shutil. Узнали также, что в
Python 3.14
у класса Path появились новые методы
переименования и перемещения файлов и каталогов: move () и move _ into ().
В следующей главе мы поговорим о том, каким образом можно через командную
строку передавать скрипту дополнительные параметры.
- ГЛАВА 22-
Передача параметров
через командную строку
Зачем это надо?
Допустим, мы пишем приложение для обработки данных. Скорее всего, нам потре
буется получать от пользователя какие-то параметры. Это могут быть имена вход
ных файлов с данными, условия, влияющие на работу алгоритма, может быть, имя
файла, куда нужно сохранить результат расчета. У нас есть разные способы полу
чить от пользователя такие параметры. Упомянем некоторые из них:
♦
все параметры записываются в скрипте в виде значений переменных, а пользо
ватель при необходимости их изменяет в исходном коде.
Такой способ работает для маленьких скриптов, когда значения меняются редко,
и нет необходимости вызывать наш скрипт из другого скрипта, передавая при
этом тому дополнительные данные. К тому же, в этом случае подразумевается,
что пользователь скрипта имеет хотя бы минимальное знание языка
♦
Python;
создать графический интерфейс.
Мы можем сделать это на
Python с
помощью стандартной библиотеки
используя сторонние библиотеки, такие как
PyQt, PySide, wxPython
tkinter или
и многие
другие. Однако создание графического интерфейса, если приложение поддержи
вает большое количество параметров, потребует значительного времени. Кроме
того, подобные приложения будет трудно использовать при автоматизации, ко
гда требуется многократно. выполнять какие-то действия с разным набором па
раметров;
♦
запрашивать данные у пользователя при запуске скрипта с помощью функции
input ().
Этот способ подходит для скриптов, которые запускаются редко, и пользователя
не затруднит при каждом запуске вводить ответы на задаваемые скриптом во
просы. Например, так работают приложения, создающие проекты с исходным
кодом по шаблону, как, например,
Poetry или uv,
о которых мы говорили в главе
17.
Такой способ снова не подойдет, если наш скрипт должен многократно запус
каться из других скриптов;
Глава
22.
Передача параметров через командную строку
411
♦ получать параметры из командной строки.
Этот вариант избавлен от проблем предыдущих способов, но при редком ис
пользовании скрипта пользователь вынужден будет вспоминать, какие парамет
ры от него ожидает скрипт. На этот случай должна быть предусмотрена возмож
ность показать пользователю документацию по использованию нашего скрипта;
♦ другие способы, включая передачу данных с помощью протокола НТТР, сокеты
и прочие более сложные способы, превращающие скрипт в полноценный сервер.
Этот способ мощный, но его реализация более кропотливая, и, скорее всего,
пользователь
не
захочет
писать
запросы
к
такому
серверу
через
командную
строку, и понадобится создать еще веб-интерфейс.
В этой главе мы поговорим о том, какие возможности предоставляет
Python
для
реализации подхода с использованием командной строки.
В роли примера у нас будет выступать скрипт, рассчитывающий длину электромаг
нитной волны л при заданных частоте сигнала f, относительной диэлектрической
проницаемости в и относительной магнитной проницаемости
µ.
Формула для рас
чета выглядит просто:
-
где с
скорость света в вакууме.
Пусть у нас уже есть вариант скрипта, в котором все параметры указаны в виде зна
чений переменных (листинг
22 .1).
from math import sqrt
if
name
#
=="
main
"·
Частота в Гц
freq = le9
#
Относительная диэлектрическая проницаемость
среды
eps = 4.0
#
Относительная магнитная проницаемость среды
mu = 1. О
# Скорость света
с= 299792458
в вакууме в м/с
wavelength =с/ (freq * sqrt(eps * mu))
print (f"Длина волны: {wavelength) м. ")
В дальнейшем мы будем изменять этот скрипт так, чтобы параметры freq, eps и mu
передавались через командную строку. Сначала мы научимся работать с парамет
рами командной строки без использования библиотек, а затем посмотрим, как мо
дуль argparse стандартной библиотеки
разборе переданных параметров.
Python
берет на себя рутинные проверки при
Часть
412
11. Основные подходы
Разбор параметров командной строки
без использования библиотек
Рассматриваемая в этом разделе версия скрипта wavelength.py должна уметь прини
мать параметры через командную строку, ее вызов должен будет выглядеть сле
дующим образом:
> python wavelength.py freq eps mu
Здесь вместо параметров
freq, eps и mu пользователю нужно будет указывать числа
с плавающей точкой. Если пользователь не передает достаточное количество пара
метров, следует выводить сообщение об ошибке с информацией о том, как пра
вильно вызывать скрипт.
Для получения параметров командной строки предназначена переменная argv из
модуля sys. Эта переменная содержит список, в котором нулевой элемент
запускаемого скрипта, а последующие элементы
-
-
имя
остальные параметры, передан
ные через командную строку. При этом каждый элемент списка sys. argv -
это
строка, и если нам нужно получать параметры в виде чисел, то придется самостоя
тельно преобразовать строковые значения к типу float или int.
Кроме того, нам надо убедиться, что в списке
sys. argv имеется хотя бы четыре
элемента. Если пользователь передаст больше параметров, лишние параметры мы
проигнорируем. Изменим предыдущий скрипт так, чтобы он получал параметры из
командной строки (листинг
Листинr 22.2.
22.2).
Chapter_22/example_02/wavelength.py
inport sys
from math import sqrt
if
name
main "·
print ("sys .argv: ", sys .argv)
if len(sys.arqv) < 4:
print("He указаны обязательные параметры.")
print ("Формат вызова: python wavelength.py freq eps mu")
sys.exit(l)
freq = float(sys.arqv[l])
eps = float(sys.arqv[2])
mu = float(sys.arqv[З])
#
Скорость света в вакууме в м/с
с=
299792458
wavelength =с/ (freq * sqrt(eps * mu))
print (f"Длина волны: {wavelength) м. ")
Глава
22.
Передача параметров через командную строку
413
В самом начале выполнения мы выводим в консоль значение списка sys. argv и
проверяем, есть ли в нем хотя бы четыре элемента. Если нет, выводим сообщение
об ошибке и показываем информацию о том, в каком формате и в каком количестве
скрипт ожидает получить параметры.
В случае неправильного количества переданных параметров мы используем функ
цию exi t () из модуля sys, -
она завершит работу скрипта. Мы можем передать в
эту функцию целое значение (рекомендуемый интервал от О до
127),
которое будет
возвращено скриптом в операционную систему. Значение О обозначает, что скрипт
(или приложение) завершился успешно, а остальные значения свидетельствуют о
произошедшей в процессе выполнения ошибке. Смысл конкретных значений кодов
ошибок оставляется на усмотрение разработчика.
Если количество переданных параметров достаточное, то преобразуем строковое
значение каждого параметра в тип
eps
И
float
и сохраняем значения в переменные
freq,
mu.
В этом примере мы игнорируем возможные ошибки, связанные с тем, что пользователь
может передать значения, которые невозможно преобразовать к действительным
числам. В этом случае скрипт упадет с исключением ValueError, но в настоящих
приложениях такие ошибки надо обрабатывать и выводить более понятные пользо
вателю сообщения об ошибках. Кроме того, для краткости мы не проверяем пара
метры на физический смысл,
-
например, что значение относительной диэлектри
ческой проницаемости должно быть не меньше
1.0,
а значение частоты
-
положи
тельное.
Пример вызова нашего скрипта может выглядеть так:
> python wavelength.py le9 4 1
sys.argv: ['wavelength.py', 'le9', '4', '1']
Длина волны: 0.149896229 м.
Здесь для передачи данных мы использовали позиционные параметры, когда каж
дый параметр должен находиться на строго определенной позиции. Такой способ
передачи параметров удобен для скриптов, которые принимают небольшое число
параметров, и все они являются обязательными.
Большинство консольных приложений используют именованные параметры, когда
перед
значением каждого параметра указывается
имя, определяющее,
для
какого
именно параметра далее будет указано значение. При использовании именованных
параметров нет надобности запоминать последовательность, в которой они должны
передаваться, то
есть
именованные
параметры
можно
указывать в
произвольном
порядке.
Часто параметры командной строки имеют два имени: короткое и длинное. Корот
кое имя состоит из знака«-» и одной буквы латинского алфавита. Длинное имя на
чинается с последовательности символов«--», после которых без пробела следует
имя параметра.
Создадим для нашего скрипта wavelength.py такие параметры:
♦
-f или --freq- частота в Гц;
Часть
414
♦
-е или
♦
-m или --mu -
--eps -
11.
Основные подходы
относительная диэлектрическая проницаемость;
относительная магнитная проницаемость.
При этом мы сразу изменим логику работы скрипта таким образом, чтобы парамет
ры -е (или --eps) и -m (или --mu) были необязательными. То есть, если их не за
дать, то значения по умолчанию для них будут приняты равными
1.0.
Возможный код скрипта wavelength.py для этого случая приведен в листинге
Листинг 22.3. Chapter_22/example_OЗ/wavelength.py
import sys
from math import sqrt
if
main "·
name
print("sys.argv:", sys.argv)
freq = None
eps = 1 .0
mu = 1.0
n = 1
while n < len(sys.argv) - 1:
current = sys.argv[ n]
if current == "-f" or current == "--freq":
freq = float(sys.argv[n + 1] )
n += 2
continue
if current == "-е" or current == "--eps" :
eps = float(sys.argv [n + 1])
n += 2
continue
if current == "-m" or current
mu = float(sys.argv[n + 1])
"--mu":
n += 2
continue
1
n +=
if freq is None:
print("He указаны
обязательные параметры.")
pri n t("Фopмaт выз о ва:")
print ("pyt hon wavelength . р у --freq freq [--eps eps] [--mu mu] ")
sys.exit(l)
22.3.
Глава
#
22.
Передача параметров через командную строку
Скорость
с=
415
света в вакууме в м/с
299792458
wavelength =с/ (freq * sqrt(eps * mu))
print (f"Длина волны: {wavelength) м. ")
В этом скрипте мы последовательно перебираем элементы списка sys. argv, и, если
встречается ожидаемое значение имени параметра, предполагаем, что следующий
элемент списка
-
это значение соответствующего параметра.
Теперь мы можем запускать скрипт, указывая именованные параметры, причем
обязательным из них является только -f
(или --freq). Вот несколько примеров
запуска:
> python wavelength.py --freq le9 --eps 4 --mu 4
sys.argv: ['wavelength.py', '--freq', 'le9', '--eps', '4', '--mu', '4']
0.0749481145 м.
Длина волны:
> python wavelength.py --freq le9 --eps 4
sys.argv: ['wavelength.py', '--freq', 'le9', '--eps', '4']
0.149896229 м.
Длина волны:
> python wavelength.py --freq le9
sys.argv: [ 'wavelength.py', '--freq', 'le9']
Длина волны:
О.299792458 м.
> python wavelength.py --eps 9
sys.argv: ['wavelength.py', '--eps', '9']
Не
указаны обязательные
Формат
параметры.
вызова:
python wavelength. ру --freq freq [--eps eps] [ --mu mu]
Разбор командной строки
с помощью модуля
argparse
Как видите, даже для такого небольшого количества параметров нам пришлось пи
сать достаточно длинный код, а многие консольные приложения имеют десятки
возможных параметров. Кроме того, мы могли бы предпочесть одновременно ис
пользовать и позиционные, и именованные параметры. Например, в нашем скрипте
обязательный параметр, задающий частоту, можно было бы оставить позиционным
без необходимости указывать перед ним -f или --freq. Логика работы с парамет
рами может значительно усложняться. Для того, чтобы каждый раз при написании
скрипта, принимающего параметры через командную строку, не приходилось пи
сать
длинный
однотипный
код,
в
стандартной
библиотеке
имеется
модуль
argparse, содержащий класс ArgumeпtParser, который берет всю рутинную обра
ботку параметров командной строки на себя.
Часть
416
Перепишем сейчас предыдущую версию скрипта (см. листинг
11 . Основные
22.3)
подходы
так, чтобы в нем
использовался класс ArgumentParser, но пока не на полную мощность (листинг 22.4), а
в последующих его версиях всё больше проверок и преобразований станем пере
кладывать на плечи этого класса.
Листинг 22.4.
Chapter_22/example_04/wavelength.py
import sys
fran argparse :inport Argumentparser
from math import sqrt
if
name
=="
parser
main
= Argumentparser ()
parser.add_argument("-f", "--freq")
parser.add_argument("-e", "--eps")
parser.add_argument("-m", "--mu")
args = parser.parse_args()
print(f"{args=)")
freq
(float(args.freq)if args.freq is not None
else None)
eps = (float(args.eps) if args.eps is not None
else 1. О)
mu
(float(args.mu) if args .mu is not None
else 1. О)
=
if freq is None:
print("He указаны
обяз ательные параметры.")
parser. print_help ()
sys.exit(l)
#
Скорость света в вакууме в м/с
с=
299792458
wavelength =с / (freq * sqrt(eps * mu))
print (f"Дпина волны: {wavelength) м. ")
При использовании класса
ния его экземпляра,
-
ArgumentParser первое, что нужно сделать после созда
указать, какие аргументы в командной строке мы ожидаем
от пользователя. Это делается с помощью метода
add_ argument (), который прини
мает строки с короткими и/или длинными именами параметров. Этот метод имеет
множество дополнительных параметров, позволяющих гибко настраивать поведе
ние каждого параметра, чем мы вскоре и займемся.
Настроив аргументы, можно вызвать метод ра r s е _ а rg s ( ) , который займется разбо
ром параметров командной строки и вернет экземпляр класса
Namespace, содержа-
Глава
22.
Передача параметров через командную строку
417
щий свойства, совпадающие с именами аргументов. Если у аргумента указано и
длинное, и короткое имя, то в качестве имени свойства указывается длинное. Для
понимания работы в скрипте выводится значение переменной args, являющейся
экземпляром класса
Namespace.
Если в метод parse _args () не передаются никакие параметры, то он будет извле
кать параметры из переменной sys. argv, что нам сейчас и нужно. Но, при необхо
димости, мы можем передавать в этот метод список строк, и тогда параметры будут
извлекаться не из переменной
s ys. argv, а из переданного списка.
Пока мы не настроили параметры аргументов, все они считаются необязательными,
и если какой-то из них не указан, значение соответствующего свойства объекта
args принимается равным None. Поэтому далее мы проверяем все три ожидаемых
параметра, и, если не указаны параметры
вующим переменным значения
--eps
или
--mu,
присваиваем соответст
1.0.
Затем мы проверяем, введено ли значение частоты, и если оказывается, что нет, то
мы не просто завершаем выполнение скрипта с ошибкой, но еще с помощью метода
print_help () выводим справку про использование нашего скрипта. Текст справки
класс ArgumentParser формирует на основе ожидаемых аргументов. Если же всё
хорошо, выводим пользователю результат расчета.
Если запустить скрипт, не забыв указать параметр --freq, то результат его выпол
нения не будет отличается от запуска его предыдущей версии за тем исключением,
что скрипт выводит значение переменной args вместо списка аргументов:
> python wavelength.py --freq le9 --eps 4
arqs=Namespace(freq='le9', eps='4', mu=None)
Длина волны:
0.149896229
м.
В случае, если мы не укажем параметр --freq, в консоль будет выведен следую
щий текст:
> python wavelength.py
args=Namespace(freq=None, eps=None, mu=None)
Не указаны обязательные параметры.
usage:
waveleпgth.py
[-h] [-f FREQ]
[-е
EPS] [-m
МU]
optioпs:
-h, --help
-f FREQ, --freq FREQ
-е EPS, --eps EPS
-m МU, --mu MU
show this help message and exit
Обратите внимание, что класс ArgumentParser без нашего участия добавил пара
метр --help (или -h), который обычно используется для получения справки о при
ложении. Если мы запустим скрипт с этим параметром, то будет выведена та же
самая справка, и выполнение скрипта на этом завершится. Внешний вид справки
мы можем настраивать на свое усмотрение, чем и займемся ближе к концу главы.
Часть
418
11.
Основные подходы
В рассматриваемой версии скрипта мы переложили задачу разбора параметров на
класс ArgumentParser, но все равно слишком много рутинной работы делаем сами.
Во-первых, по умолчанию всё аргументы считаются необязательными, а мы уст
раиваем проверку на обязательное наличие параметра --freq, хотя такую проверку
можно переложить на класс ArgumetParser. Во-вторых, мы вручную преобразуем
float, а это тоже можно делать автоматически с помо
щью параметров метода add_argument (). И, наконец, для каждого параметра мы
строковые значения к типу
можем
указывать значение
по умолчанию,
которое присваивается
соответствую
щему свойству, если значение аргумента не указано.
Всё это реализовано в следующей версии скрипта (листинг
Листинг 22.5.
22.5).
Chapter_22/example_05/wavelength.py
from argparse import ArgumentParser
from math import sqrt
if
name
==" main "·
parser = ArgumentParser()
parser.add_argument("-f", "--freq", required=Тrue, type=float)
parser.add_argument("-e", "--eps", type=float, default=l.O)
parser.add_argument("-m", "--mu", type=float, default=l.O)
args = parser.parse_args()
print(f"(args=}")
freq = args.freq
eps = args.eps
mu = args.mu
#
Скорость света в вакууме в м/с
с=
299792458
wavelength =с/ (freq * sqrt(eps * mu))
print (f"Длина волны: (wavelength} м. ")
Метод
add_ argumen t () имеет достаточно много именованных параметров, все они
хорошо описаны в документации, и рассматривать их здесь не имеет смысла. Пояс
ним далее только те из них, что использованы в нашем скрипте:
♦
required -
булево значение. Если оно равно тrue, параметр является обяза
тельным, при его отсутствии скрипт выводит справку о параметрах командной
строки и завершает работу;
♦
type -
позволяет преобразовать значение параметра из строки к нужному типу.
Этот параметр может принимать в качестве значения функцию, на вход прини
мающую строку и возвращающую преобразованное значение. В нашем примере
Глава
22.
Передача параметров через командную строку
419
мы ожидаем, что все значения будут типа
float, и поэтому именно функция
float () будет вызываться для преобразования параметра. С такой настройкой
соответствующее свойство объекта args получит значение именно этого типа;
♦
задает значение по умолчанию для необязательных параметров. Ра
default -
нее для всех необязательных параметров в качестве значения по умолчанию
принималось значение
None, и мы вынуждены были для переменных eps и mu
выполнять дополнительные проверки с присваиванием значения
чае. В новой версии скрипта всё это происходит внутри класса
1.0
в этом слу
ArgumentParser.
Теперь, если мы выполним скрипт wavelength.py с указанием обязательного пара
метра --freq и без него, результат будет выглядеть следующим образом (обратите
внимание на значение объекта
args):
> python wavelength.py --freq le9 --eps 4
args=Namespace(freq=lOOOOOOOOO.O, eps=4.0, mu=l.0)
Длина волны: 0.149896229 м.
> python wavelength.py
1Jsage: wavelength.py [-h] -f FREQ [-е EPS] [-m MU]
wavelength.py: error: the following arguments are required: -f/--freq
У нашего скрипта один обязательный именованный параметр:
--freq, и пользова
тель каждый раз должен указывать его имя. Мы облегчим работу пользователю,
если этот параметр можно будет указывать без указания имени. То есть переделаем
его из именованного в позиционный. Для этого достаточно при добавлении пара
метра с помощью метода add _ argumen t () в качестве имени указать значение без
символов«-».
Изменим в коде листинга
ной код неизменным
Листинг
22.5 строку
(листинг 22.6).
добавления параметра freq, оставив осталь
22.6. Chapter_22/example_06/wavelength.py
parser. add_argument (" freq", type=float)
Позиционные параметры всегда являются обязательными, поэтому мы также убра
ли из метода
add _ argumen t ()
параметр
requi red.
Теперь наш скрипт можно запускать любым из следующих вариантов:
>
>
>
>
python
python
python
python
wavelength.py
wavelength.py
wavelength.py
wavelength.py
le9
le9 --eps 4
--eps 4 le9
--eps 4 le9 --mu 2
Позиционные параметры можно указывать как до, так и после именованных, а так
же перемежать с ними, что не очень наглядно.
Часть
420
11.
Основные подходы
Если позиционный параметр не указать, то по-прежнему будет выведена справка.
Обратите внимание на описание позиционного параметра:
> python wavelength.py
usage: wavelength.py [-h] [-е EPS] [-m МU] freq
wavelength.py: error: the following arguments are required: freq
Или более подробно- после вызова скрипта с параметром -h:
> python wavelength.py -h
usage: wavelength.py [-h]
[-е
EPS] [-m
МU]
freq
positional arguments:
freq
options:
-h, --help
-е EPS, --eps EPS
-m МU, --mu МU
show this help message and exit
Для некоторых приложений иногда требуется добавить параметры, которые рабо
тают как флаги,
-
после них не указываются значения, а важен сам факт указания
или не указания параметра. Подобным образом работает параметр -h (или --help).
Многие приложения добавляют флаг
-v ( сокращение
от англ.
verbose -
много
словный), обозначающий, что в процессе работы нужно выводить более подробную
информацию. Добавим такой же параметр и в наш скрипт (листинг
22.7).
будет указан, то выведем пользователю информацию об исходных данных.
Листинr 22.7.
Chapter_22/example_07/wavelength.py
from argparse import ArgumentParser
from math import sqrt
if
==" main "·
name
parser = ArgumentParser()
parser.add_argument("freq", type=float)
parser.add_argument("-e", "--eps", type=float, default=l.0)
parser.add_argument("-m", "--mu", type=float, default=l.0)
parser.add_argument("-v", action="store_const",
const=Тrue, default=False)
args = parser.parse args()
print(f"(args=}")
freq = args.freq
eps = args.eps
mu = args.mu
verbose = args.v
Если он
Глава
22.
Передача параметров через командную строку
#
Скорость света в вакууме в м/с
с
= 299792458
421
i f verbose:
print(f"Чacтoтa:
print(f"Eps:
print(f"Mu:
wavelength
print
{freq)
Гц")
{eps)")
{mu)")
=с/
(freq
(f"Дпина волны:
*
sqrt(eps
{wavelength)
*
mu))
м.
")
Для создания параметра-флага мы использовали еще несколько новых для нас па
раметров метода add_argument 1). Строковый именованный параметр action опи
сывает поведение парсера при разборе соответствующего аргумента. Мы не станем
рассказывать обо всех возможных значениях параметра action, а ограничимся
лишь некоторыми из них. По умолчанию он имеет значение "store", обозначаю
щее, что нужно хранить значение переданного параметра.
В коде листинга
22.7
мы использовали значение "store_const", означающее, что
парсер не ожидает после имени аргумента какого-либо значения, и свойство будет
хранить заранее заданное значение. Если этот аргумент указан, то свойство станет
хранить значение, которое нужно указать в именованном параметре const (в нашем
случае это значение тrue -
флаг установлен), в противном случае значение будет
взято из именованного параметра default (в нашем случае False -
флаг не уста
новлен).
Далее в скрипте мы проверяем значение переменной verbose, которой присвоили
значение args. v, и если оно равно True, то выводим информацию об исходных
данных.
Использование параметра -v показано далее:
> python wavelength.py le9 --eps 4 -v
args=Namespace(freq=lOOOOOOOOO.O, eps=4.0, mu=l.O, v=True)
Частота:
1000000000.0
Гц
Eps: 4.0
Mu: 1. О
Дnина волны:
0.149896229
м.
Хранение булевых значений при использовании аргумента как флага настолько часто
используется, что для именованного параметра
action
метода
add_ argument ()
раз
работчики библиотеки добавили следующие возможные значения:
♦
"store_true" False
♦
в противном случае;
"store_false" и
True
значение аргумента будет равно тrue, если аргумент указан, и
значение аргумента будет равно False, если аргумент указан,
в противном случае.
Поэтому в примере из листинга
как показано в листинге
22.8.
22.7
можно упростить добавление аргумента -v,
Часть
422
Листинг 22.8.
11.
Основные подходы
Chapter_22/example_08/wavelength.py
parser.add_argument("-v", action="store true")
Результат работы скрипта не изменится.
Последнее, что нам осталось обсудить в этом разделе
выводимой при использовании параметра
-
это оформление справки,
-h или --help. Пока еще она выглядит
довольно аскетично:
> python wavelength.py -h
usage: wavelength. ру [-h] [ -е EPS] [-m
МU]
[-v] freq
positional arguments:
freq
options:
-h, --help
-е EPS, --eps EPS
-m МU, --mu МU
-v
show this help message and exit
Добавим в скрипт описание параметров и общую информацию о программе. Для
этого нам достаточно изменить часть кода, отвечающую за создание парсера и до
бавление аргументов (листинг
Листинr
parser
22.9).
22.9. Chapter_22/example_09/wavelength.py
ArgumentParser(proq="python wavelength.py",
clescription="Cкpипт для расчета длины волны в среде",
epiloq=" (с) Василий
parser.add_argument("freq", type=float,
hеlр="Частота,
Пупкин,
2025")
Гц")
parser.add_argument("-e", "--eps", type=float, default=l.O,
hеlр="Относительная диэлектрическая проницаемость")
parser.add_argument("-m", "--mu", type=float, default=l.O,
hеlр="Относительная магнитная проницаемость")
parser.add_argument("-v", action="store_true",
hеlр="Подробный вывод")
В этом коде:
♦
параметр prog, переданный в конструктор класса ArgumentParser, задает имя
программы. Ранее мы его не указывали, и тогда по умолчанию в качестве этого
значения использовался первый элемент из списка
sys. argv, а это, как мы зна-
Глава
423
22. Передача параметров через командную строку
ем, имя скрипта. В измененном коде мы подчеркиваем, что скрипт нужно запус
кать с помощью интерпретатора
♦
python;
параметр description там же задает более подробное описание приложения.
Здесь обычно описывается назначение программы;
♦
параметр epilog задает текст, который будет выведен в конце справки;
♦
к каждому аргументу при вызове метода add_argument () мы добавили краткое
описание с помощью параметра
help.
Если теперь запустить скрипт с параметром -h, то справка будет выглядеть сле
дующим образом:
> python wavelength.py -h
EPS] [-m MU] [-v] freq
usage: python wavelength.py [-h]
[-е
Скрип т для расчета длины волны в
среде
positional arguments:
Частота,
freq
options:
-h, --he lp
-е EPS, --eps EPS
-m МU, --mu MU
-v
(с)
Василий Пупкин,
Гц
show this help message and exit
Относительная диэлектрическая
проницаемость
Относительная магнитная проницаемость
Подробный вывод
2025
Параметры в справке разделены на группы. По умолчанию создаются две группы:
positional arguments и options. Мы можем создать свои группы и назвать их так, как
нам будет угодно. Для перевода названий групп на русский язык сделаем две груп
пы с названиями: Позиционные
Для
создания
группы
параметры И Именованные параметры.
параметров
нужно
воспользоваться
методом
add_argument_group () класса ArgumentParser. Этот метод вернет объект группы,
после чего аргументы командной строки нужно добавлять не с помощью метода
add_argument () класса ArgumentParser, а с помощью одноименного метода объек
та группы (листинг
Листинг 22.10.
parser
22.1 О).
Chapter_22/example_10/wavelength.py
ArgumentParser(prog="python wavelength.py",
description="Cкpипт для расчета длины волны в среде",
epilog=" (с) Василий Пупкин, 2025")
groupl = parser. add_ argument_9roup ( "Позиционные параметры",
"Это обязательные параметры")
group2 =
parser.add_arqument_9roup("Имeнoвaнныe параметры",
"Это
необязательные параметры")
Часть
424
11.
Основные подходы
groupl. add_argument ( "freq", type=float,
hеlр="Частота,
Гц")
group2.add_argument("-e", "--eps", type=float, default=l.0,
hеlр="Относительная диэлектрическая проницаемость")
group2.add_argument("-m", "--mu", type=float, default=l.0,
hеlр="Относительная магнитная проницаемость")
group2.add_argument("-v", action="store_true",
hеlр="Подробный вывод")
После создания групп справка будет выглядеть следующим образом:
> python wavelength.py -h
usage: python wavelength.py [-h]
[-е
Скрипт для расчета длины волны в
среде
options:
-h, --help
EPS] [-m
МU]
[-v] freq
show this help message and exit
Позиционные параметры:
Это
обязательные параметры
freq
Частота,
Гц
Именованные параметры:
Это
EPS, --eps EPS
МU, --mu МU
-е
-m
-v
(с)
необязательные
параметры
Относительная диэлектрическая проницаемость
Относительная магнитная проницаемость
Подробный вывод
Василий Пупкин,
2025
Теперь почти всё хорошо. Единственный момент, который остался неисправлен
ным,
-
это параметр
-h
для вызова справки. Поскольку этот аргумент добавлялся
автоматически парсером, он не попал ни в одну из наших групп. Но этот момент
мы можем достаточно легко исправить так, как показано в листинге
parser
ArgumentParser(prog="pythoп
wavelength.py",
description="Cкpипт для расчета длины волны в
groupl
epilog="(c) Василий Пупкин, 2025",
add_help=False)
parser. add_argument _group ( "Позиционные параметры",
"Это обязательные
22.11.
параметры")
среде",
Глава
22.
425
Передача параметров через командную строку
group2 = parser. add_argument_group ("Именованные
"Это необязательные
параметры",
параметры")
group2.add_argument("-h", "--help", action="help",
hеlр="Вывод справки и завершение работы")
Мы отключили здесь автоматическое добавление аргумента для вызова справки,
добавив в конструктор класса ArgumentParser параметр add_help=False. Затем до
бавили новый аргумент в группу
зав значение параметра
group2 с именами "-h" и "--help", при этом ука
action="help", чтобы сказать парсеру, что этот аргумент
обозначает вызов справки.
Теперь справка будет выглядеть так:
> python wavelength.py -h
usage: python wavelength.py
Скрипт
[-е
EPS] [-m
МU]
[-v] [-h] freq
для расчета длины волны в среде
Позиционные
параметры:
Это обязательные
параметры
freq
Частота,
Именованные
Гц
параметры:
Это необязательные
параметры
-е EPS, --eps EPS
-m МU, --mu МU
-v
-h, --help
Относительная диэлектрическая
(с)
Василий Пупкин,
Относительная
магнитная
проницаемость
проницаемость
Подробный вывод
Вывод справки и завершение работы
2025
Что ж, теперь всё выглядит очень аккуратно. На этом мы и закончим знакомство с
классом
ArgumentParser,
хотя мы рассмотрели далеко не все его возможности.
Заключение
Во многие скрипты удобно передавать параметры через аргументы командной
строки. Чтобы узнать, какие параметры были переданы, можно воспользоваться
переменной sys. argv, содержащей список строк
вой элемент этого списка
-
-
переданных параметров. Нуле
это всегда имя запускаемого скрипта.
Часть
426
11.
Основные подходы
Мы рассмотрели два способа работы с параметрами командной строки. Сначала мы
вручную разбирали переданные параметры и увидели, сколько однотипного кода
приходится писать в этом случае. Затем мы начали изучать класс ArgumentParser
из стандартного модуля argparse, который берет на себя всю рутину по разбору
параметров.
Мы не только научились создавать разные типы параметров, но и оформили справ
ку про ожидаемые параметры, которая выводится, если
метром
-h
или
скрипт запустить с
пара
--help.
В следующей главе мы в некотором смысле вернемся к теме обработки строк в
Python
и поговорим про регулярные выражения, которые позволяют искать в тексте
достаточно сложный шаблонный текст.
- ГЛАВА 23-
Регулярные выражения
Что такое «регулярные выражения»
и когда их используют?
В процессе обработки текстовых данных часто приходится разбирать на состав
ляющие строки, имеющие определенную заранее известную структуру. Допустим,
мы прочитали из файла следующий текст:
1. foo = 10 ;
236.
31.
bar
bar = 0.01;
bar=0.01;
42. foo= 0.5;
= 0.01; baz = 40
baz= 30.1; foo =20.5
foo = 30 ; baz =25.5
bar=l0;
baz=900
Данные здесь составлены небрежно: где-то есть пробелы до или после знака
«=»,
где-то их нет, переменные расположены в произвольном порядке, да еще и в начале
строк могут попадаться пробелы. Наша задача состоит в том, чтобы из каждой
строки извлечь значения указанных переменных (цифры в начале строки нас не ин
тересуют). Мы могли бы реализовать следующий алгоритм:
1.
Разбить текст по символу перевода строки на отдельные строки. Затем в каждой
строке выполнить действия, описанные далее.
2.
Найти положение первой точки и удалить из строки весь текст слева до положе
ния точки, включая ее саму, и таким образом избавиться от не интересующего
нас числа в начале строки.
3.
Удалить все пробелы в начале (а можно и в конце) строки.
4.
Разделить строку на три подстроки по символу«;».
5.
Каждую подстроку снова разделить на две подстроки по символу
6.
Первая полученная подстрока
-
«=».
это имя переменной (не забыть удалить пробе
лы в начале и в конце этой подстроки).
7.
Полученная вторая подстрока
-
это значение (не забыть удалить пробелы в на
чале и в конце этой подстроки, а потом преобразовать к типу float).
428
Часть
11.
Основные подходы
Этот алгоритм, конечно, не очень сложный, но реализовывать его было бы утоми
тельно. В подобных задачах нам помогут регулярные выражения
(regular expressions,
regex
в англоязычных текстах для них часто применяются сокращенные выражения
или
regexp).
Регулярные выражения представляют собой строки, записанные по
определенным правилам с использованием своего мини-языка. Эти строки описы
вают формат, в котором ожидается получить входные данные. Так, они дают воз
можность указывать, какие элементы во входных данных должны обязательно при
сутствовать (в нашем примере это числа в разных форматах, имена переменных,
знаки
«=»
и
«;»),
а какие элементы могут как присутствовать, так и отсутствовать
(например, пробелы).
Регулярные выражения используют не только для разделения текста на составляю
щие, но и для проверки того, что входной текст удовлетворяет определенным усло
виям. С помощью регулярных выражений можно описывать достаточно сложные
конструкции. Классический пример
-
регулярное выражение, позволяющее про
верить, что пользователь ввел корректный с точки зрения стандарта
RFC 5322
адрес
электронной почты с учетом всех тонкостей возможных записей доменных имен:
((?:[а-zО-9!#$%&'*+/=?л '(I)~-]+(?:\.[а-zО-9!#$%&'*+/=?л '(l)~-]+)*l\"(?:[\x01\x08\x0b\x0c\x0e-\xlf\x21\x23-\x5b\x5d-\x7f] l\\[\x01-\x09\x0b\x0c\x0e\x7f])*\")@(?: (?: [a-z0-9] (?: [a-z0-9-]*[a-z0-9])?\.)+[a-z0-9] (?: [a-z0-9-]*[a-z09])? 1\ [ (?: (?:25 [0-5] 12 [0-4] [0-9] 1 [01]? [0-9] [0-9] ?) \.) (3) (?:25 [0-5] 12 [0-4] [09] 1 [01]? [0-9] [0-9]? 1 [a-z0-9-] * [a-z0-9]: (?: [\x01-\x08\x0b\x0c\x0e-\xlf\x21-\x5a\x53\x7f] 1\ \ [\x01-\x09\x0b\x0c\x0e-\x7f]) +) \]))
Если вы не понимаете, что значит вся эта абракадабра, и при чем тут адрес элек
тронной почты, то не расстраивайтесь,
просто взял его с сайта
-
stackoverflow.com.
автор книги тоже этого не понимает, а
Но к концу этой главы вы начнете по
нимать отдельные части этой записи и сможете расшифровать, что здесь написано.
К счастью, на практике регулярные выражения обычно выглядят не настолько
страшно.
Итак, начнем постепенное погружение в язык регулярных выражений.
Символы подстановки
Регулярные выражения можно использовать для нескольких задач:
♦
проверки того, удовлетворяет ли заданный текст требуемому формату записи;
♦
извлечения из текста требуемых данных;
♦
поиска в тексте подстрок, удовлетворяющих регулярному выражению.
Мы начнем решать первую задачу, связанную с проверкой соответствия входных
данных требуемому формату, а затем постепенно перейдем к остальным двум.
В качестве исходных данных мы воспользуемся текстом, приведенным в начале
главы.
Сначала составим регулярное выражение (листинг
23.1),
которое проверяет, что
строка начинается с целого числа, после которого следует точка. Возможно, перед
целым числом есть пробелы.
Глава
23.
Регулярные выражения
429
Пмстинr 23.1. Chapter_23/example_01/find_numЬers.py
inport re
text correct = """1. foo = 10; bar = 0.01; baz
236.
bar = 0.01;
baz= 30.1; foo =20.5
31. bar=0.01; foo = 30; baz =25.5
42. foo= 0.5; bar=l0; baz=900"""
40
text_invalid = """bar=0.01; baz=30; foo=l0.2
5В. bar=0.01; baz=30; foo=l0"""
lines_correct = text_correct.split("\n")
lines_invalid = text_invalid.split("\n")
regex = re.caipile(r" *[0-9)+\ . . +")
for line in lines correct:
match = reqex.ma.tch(line)
assert match is not None
for line in lines invalid:
match = reqex.ma.tch(line)
assert match is None
Теперь разберемся, что делает приведенный здесь код. Все функции и классы, не
обходимые для работы с регулярными выражениями, находятся в стандартном мо
дуле re ( сокращение от
«regular expressions» ),
поэтому первое, что мы делаем
-
импортируем этот модуль.
Затем идут два набора данных. Многострочный литерал text_correct содержит
строки, для которых регулярное выражение должно срабатывать положительно (все
строки будут соответствовать регулярному выражению), а в многострочном лите
рале text_invalid содержатся строки, на которых мы будем проверять, что регу
лярное выражение не срабатывает на текст, написанный в неправильном формате.
Затем на основе этих литералов, разделяя их по символу переноса строки, мы соз
даем списки строк
lines_correct
И
lines_invalid.
Далее начинается самое интересное. С помощью функции compile () мы получаем
экземпляр класса re.Pattern (переменная regex) для работы с регулярным выра
жением. В эту функцию мы передаем строку с пока еще загадочными символами.
Это и есть наше первое регулярное выражение. Обратите внимание, что в функцию
compile () передается «сыраю>
(raw)
строка, поскольку в синтаксисе регулярных
выражений активно используются обратные слеши, и если бы мы записывали
обычные строки, то эти слеши пришлось бы слишком часто удваивать, что значи
тельно ухудшило бы читаемость и без того не простого для восприятия выражения.
Часть
430
Разберемся с регулярным выражением
«
*».
" * [ О-9 J +\ .. +".
11.
Основные подходы
Оно начинается с символов
Здесь пробел не является управляющей инструкцией, а вот«*» информирует,
что символ или группа символов (о них скажем позже), расположенные перед ним,
могут повторяться О или более раз. То есть запись «
*»
означает, что в этом месте
может быть любое количество пробелов, в том числе их может и не быть.
Выражение
скобки
-
[ 0-9 J
обозначает, что далее должен идти символ цифры. Квадратные
это инструкция перечисления. В нашем случае мы указываем интервал
символов от О до
9,
[ о 12 з 4 s6 7 8 9 J.
что равносильно выражению
Помимо цифр, мы
могли бы при необходимости указать и другие символы,- например:
[0-9a-zA-ZJ -
такая запись означает, что в этом месте может быть либо цифра, либо английская
буква в нижнем или верхнем регистре. В перечисление мы можем добавлять и дру
гие символы:
[0-9a-zA-Z_*+J -
помимо букв и цифр в этом месте могут быть еще
символы подчеркивания, звездочка и плюс. Учтите, что в квадратных скобках сим
вол«*» не является управляющим символом. А если нужно использовать символ«
» внутри
конструкции
[... J не
для указания интервала, а в качестве возможного сим
вола перечисления, то его следует указать первым:
После выражения
[ 0-9 J
[-0-9a-zA-z _ *+ J.
в нашем регулярном выражении стоит знак«+». По дейст
вию он напоминает звездочку, но указывает на то, что предыдущий символ (в на
шем случае
-
цифра) должен встретиться
1 или
более раз. То есть он обозначает,
что после возможных пробелов должна следовать хотя бы одна цифра.
Затем следует сочетание«\.», обозначающее просто символ точки. Дело в том, что
сама точка является управляющим символом, который представляет «любой сим
вол», а если мы хотим указать, что в какой-то позиции должен располагаться сим
вол «точка», то нам следует экранировать ее обратным слешем.
Конец регулярного выражения у нас заканчивается символами
«. +»,
что обознача
ет, как мы уже знаем, «любой символ» один или более раз.
Таким образом, записанное нами регулярное выражение удовлетворяет любому
выражению, которое начинается с номера, после которого идет точка, а затем что
то еще. Перед номером может быть любое количество пробелов. Скоро мы изме
ним и дополним это регулярное выражение, чтобы оно проверяло и последующие
символы.
После компиляции регулярного выражения и получения экземпляра класса Ра t
мы в цикле вызываем метод
тех,
match ()
tern
из этого класса, передавая в него строки из
что должны удовлетворять регулярному выражению, а затем строки, которые
ему удовлетворять не должны. Метод
match ()
сверяет переданную строку с регу
лярным выражением и возвращает экземпляр класса
летворяет регулярному выражению, и
None -
re .Match (),
если строка удов
в противном случае. Именно этот
результат мы и проверяем далее с помощью инструкции
assert.
Если всё прошло
успешно, то скрипт завершается, а если бы мы где-либо ошиблись, скрипт завер
шился бы с исключением
AssertionError.
Глава
Регулярные выражения
23.
431
Язык регулярных выражений включает в себя достаточно много видов конструк-
~-
ции
все они описаны в документации к стандартному модулю
re 1.
д алее
приве-
ден список некоторых управляющих инструкций, содержащих символы подстановки:
♦
.-
любой символ;
♦
* -
предшествующее выражение должно встречаться О или более раз;
♦
+-
предшествующее выражение должно встречаться
♦
? -
предшествующее выражение должно встречаться О или
♦
♦
{m J - предшествующее выражение должно встречаться ровно m раз;
{m, n J - предшествующее выражение должно встречаться от m до n раз;
♦
1-
1 или
более раз;
1 раз;
оператор «илю>. Должно встретиться или выражение слева от символа
« 1»,
или выражение справа от него;
♦
♦
[... J [л J ...
перечисление символов, которые могут встретиться в этой позиции;
перечисление символов, которые не должны встречаться в этой позиции
(не перечисленные символы могут располагаться в этой позиции);
♦
\s \s -
♦
\ d - цифра;
♦
\о
♦
\w -
буквенный символ;
♦
\ w-
любой символ, кроме буквенного;
♦
л
♦
$ -
♦
-
любой пробельный символ;
любой символ, кроме пробельного;
любой символ, кроме цифры;
начало строки;
-
конец строки.
Дадим некоторые пояснения к этим инструкциям. Здесь написано, что инструкции
*,
+, ?, {m} и {m,n} применяются к предшествующим выражениям. Такое выраже
ние может состоять либо из одного символа, как в нашем примере, либо из не
скольких символов, часто объединенных инструкциями группировки, о которых
будет сказано далее. Эти инструкции также могут стоять после выражений
[.. J
и
[ л_.] •
Управляющая инструкция«
1
» применяется
не только к ближайшему символу, но и
к группе символов. Например, регулярному выражению foo I bar удовлетворяют
строки foo и bar, но не fobar, как было бы, если бы символ« 1» применялся только
к отдельным символам. Но чтобы исключить подобную неоднозначную трактовку,
на практике лучше использовать группировку, о которой мы еще будем говорить.
Инструкцию
[... ]
мы уже упоминали, теперь рассмотрим инструкцию
[ л ... J,
которая
обозначает, что в этом месте подойдет любой символ, кроме записанных в квадрат-
1 См.
https://docs.python.org/3/library/re.html.
Часть
432
11.
Основные подходы
ных скобках. Например, регулярному выражению
строка
такая
foo,
но не
[ла-d] + будет удовлетворять
bar. Если в инструкции [... ] нужно указать символ «л», то, чтобы
инструкция
не
превратилась
не первым символом:
[ а-dл J +.
в
инверсию,
достаточно
символ
«л»
указать
Такому регулярному выражению удовлетворяет
строка аЬс л.
Далее в приведенном списке мы видим несколько инструкций, представляющих
сокращенные записи для указания символов разного рода и их инверсий.
Инструкция
«\s» обозначает пробел, символ табуляции, перевод строки и некото
рые другие символы, добавляющие отступы, включая пробельные символы из таб
лицы
Unicode.
В нашем примере в регулярном выражении использовались пробе
лы, но обычно лучше в подобных случаях
применять «\s» -
кто знает, вдруг
пользователь вместо пробелов вставит символ табуляции. К тому же, это более на
глядно.
Инструкция
«\d» на первый взгляд аналогична записи [О-9], но на самом деле еще
более умная, так как включает в себя не только цифры от О до
тается цифрами в таблице
вать
«\d»
Unicode.
9,
но и всё, что счи
В нашем примере мы тоже могли бы использо
вместо [О-9].
Инструкция «\w» обозначает любой буквенный символ, причем не только символ
из таблицы
Unicode,
ASCII,
но также все символы, которые считаются буквами в таблице
если в настройках регулярного выражения не указано обратное (про пара
метры регулярных выражений мы поговорим чуть позже). Благодаря этой особен
ности, нам не нужно беспокоиться о том, что
Python
может не понять, что русские
буквы относятся именно к буквам.
Комбинации «\s», «\d» и «\w» можно использовать внутри инструкции квадратных
скобок. Тогда регулярное выражение
[ \ w\d _J +
будет обозначать один или более
символов, представляющих собой букву, цифру или знак подчеркивания.
Символы для обозначения начала или конца строки («л» и
«$» соответственно)
служат для обработки многострочного текста или для того, чтобы указать, что по
сле определенной инструкции уже не должны следовать никакие символы.
Используя всё здесь сказанное, регулярное выражение из предыдущего примера мы
можем обобщить и записать более компактно: "\s*\d+\ .. +"
(листинг
23.2).
Ос
тальной код останется неизменным.
Листинг
23.2. Chapter_23/example_02/find_numЬers.py
regex = re.compile(r"\s*\d+\ .. +")
Прежде, чем мы продолжим дополнять это регулярное выражение, поговорим о
параметрах, с помощью которых можно настраивать поведение регулярных выра
жений.
Глава
23.
Регулярные выражения
433
Параметры регулярных выражений
Вызывая функцию compile (), мы в нее передавали только строку с регулярным
выражением, однако эта функция в качестве второго параметра может принимать
наборы целочисленных флагов для настройки поведения регулярных выражений.
Флаги можно объединять с помощью битовой операции ИЛИ
-
« ».
1
Далее приве
дены некоторые из возможных флагов:
♦
re. NOFLAG.
Значение равно О. Равносильно тому, что никакой флаг не установлен или вто
рой параметр в функции compi le () не передан. Может использоваться для более
наглядной демонстрации того факта, что никакие дополнительные параметры не
установлены;
♦ re .А ИЛИ re .ASCII.
«\s)), «\d)), «\w))
и прочие учитывают не только символы из базовой таблицы ASCII, но и из всей
таблицы Unicode. Этот флаг отключает поиск в таблице Unicode будут ис
пользоваться только символы из таблицы ASCII. Поэтому, например, русские
буквы не будут удовлетворять выражению «\w»;
В предыдущем разделе говорилось, что выражения подстановки
♦
re. DEBUG.
После компиляции будет выведена отладочная информация про регулярное вы
ражение, по которому можно попытаться понять, как будет происходить сравне
ние строки с регулярным выражением;
♦
re. I ИЛИ re. IGNORECASE.
При поиске не будет учитываться регистр букв- т. е. выражению [A-ZJ будут
соответствовать также буквы в нижнем регистре;
♦
re.M ИЛИ re.MULTILINE.
Используется для работы с текстом, который содержит символы перевода строк.
При установке этого флага символ «л)) будет обозначать не только начало тек
ста, но и начало строки
(line)
после символа
«\n)).
Аналогично, символ«$)) будет
обозначать не только конец всего текста, но и окончание каждой строки;
♦
re.
s
или re. DOTALL.
При установке этого флага символу подстановки
также символ перевода строки
«\n)).
«. ))
будет соответствовать
Без установки этого флага символ
«. )>
ис
ключает символ перевода строки при сравнении;
♦
re. х или re. VERBOSE.
При установке этого флага регулярное выражение можно разбивать на несколь
ко строк и писать в нем комментарии после символа
«#».
При этом пробелы и
переводы строк в таком регулярном выражении игнорируются.
Используя флаги, строку компиляции нашего примера из листинга
бы записать следующим образом (листинг
23.3).
23.2
мы могли
Часть
434
Листинг 23.3.
regex
11.
Основные подходы
Chapter_23/example_03/find_numbers.py
re.compile(r"""\s*\d+\.
.+
re.VERВOSE
# Пробел, число и точка
# Пока любой символ""",
re.IGNORECASE)
Инструкции группировки
Теперь нам нужно научиться с помощью регулярных выражений извлекать данные
из строк. Сначала изменим регулярное выражение таким образом, чтобы проверя
лась остальная часть строки, следующая после номера с точкой. В этом примере
для простоты мы сделаем предположение, что числа могут иметь формат либо NNN,
либо NNN. ммм, где NNN и ммм
То есть часть
. ммм
-
последовательности десятичных цифр любой длины.
не является обязательной. Предположим также, что все числа
положительные. В дальнейшем при необходимости можно будет расширить регу
лярное выражение, чтобы ему удовлетворяли отрицательные числа, а также числа
вида 123е-4,
-56. 7е+8
и т. п.
Изменим регулярное выражение из примера, приведенного в листинге
дующим образом (листинг
Листинг
regex
23.3,
сле
23.4).
23.4. Chapter_23/example_04/find_variaЫes.py
re.compile(r"""
#
Число в начале каждой строки
л\s*\d+\.\s*
# Формат даннь~: ХХХ = NNМ.МММ;
(\w+\s*=\s*\d+(\.\d+)?)\s*;\s*
(\w+\s*=\s*\d+(\.\d+)?)\s*;\s*
(\w+ \s *=\s*\d+ ( \. \d+) ?) \s*$""", re. VERВOSE)
Первое небольшое косметическое дополнение, которое здесь сделано,
-
это до
бавление символа «л» в самом начале регулярного выражения, чтобы явным обра
зом подчеркнуть, что число с точкой мы ожидаем в начале строки. На нашем при
мере это изменение не скажется.
Более заметны следующие дополнения. Среди новых строк в этом регулярном вы
ражении выделяются три почти одинаковые конструкции. Разберемся с одной из
них: (\w+\s*=\s*\d+ (\. \d+) ?) \s*; \s* -
поподробнее.
Большая часть этого выражения взята в скобки. В нашем случае внешние скобки не
будут влиять на сопоставление регулярного выражения со строкой, однако в сле
дующем примере, когда мы начнем обсуждать группы внутри сопоставления, они
нам понадобятся.
Глава
23.
435
Регулярные выражения
Внутри внешних скобок выражение описывает образец вида
ххх
-
это набор букв длины
1и
более
ххх
= NNM. ммм, где
(«\w+»). Это у нас имя переменной в задан
ном тексте (foo, bar, baz), затем могут идти произвольное (в том числе нулевое)
количество пробельных символов
торого
еще раз
могут
затем следует знак равенства, после ко
( « \ s * » ),
встретиться
пробельные символы.
\d+ ( \. \d+) ? описывает формат NNM. ммм, где
. ммм
А
затем
выражение
может отсутствовать. Здесь уже
круглые скобки являются обязательными, так как инструкция
«?» относится ко
всему содержимому внутри них. Напомним, что эта инструкция обозначает О или
1
совпадение (или есть, или нет).
После круглых скобок следует выражение
зывать вопросы,
-
«\s*; \s*», которое уже не должно вы
оно обозначает, что должен присутствовать знак«;», до и после
которого может быть любое количество пробельных символов.
В последней строке регулярного выражения вместо«; \s*» указано «\s*$». Это оз
начает, что после значения последней переменной нет точки с запятой, но вместо
нее могут быть пробельные символы, а затем строка должна завершиться. Такая
запись
-
это своеобразная защита от ошибочных строк, в которых может быть за
писано еще что-то после значения третьей переменной.
Программа с новым регулярным выражением должна отработать с тем же резуль
татом, что и предыдущая ее версия. Для проверки в список ошибочных строк мож
но добавить еще одну, в которой пропущены точки с запятой:
text invalid = """bar=0.01;
52. fmin=0.01
fmax=ЗO
Ьаz=ЗО;
foo=l0.2
D=l0"""
Эта строка не должна удовлетворять новому регулярному выражению.
Группировка элементов в регулярных выражениях играет важную роль. Объект
класса мatch, который мы получаем в случае удачного сопоставления строки регу
лярному выражению, сохраняет внутри себя все подстроки, попавшие в группы
(если не указано обратное
-
к этому вопросу мы скоро вернемся). Чтобы получить
все подстроки, попавшие в ту или иную группу, нужно вызвать
метод
groups (),
который возвращает кортеж подстрок, попавших в группы. Подстроки отсортиро
ваны в том порядке, в котором группы определены в шаблоне.
Класс ма tch также содержит метод group (), который принимает целое число, соот
ветствующее номеру группы, начиная с
группу. Индексация начинается с
1,
1,
и возвращает подстроку, входящую в эту
поскольку группа с индексом О соответствует
всей строке, удовлетворяющей всему регулярному выражению.
Использование методов group () и groups () показано в листинге
import re
text = '""'1. foo = 10 ; bar = 0.01; baz = 40
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= 0.5; bar=lO; baz=900"""
23.5.
Часть
436
lines
text.split("\n")
regex = re.compile(r"'"'
# Число
11.
Основные подходы
в начале каждой строки
л\s*\d+\.\s*
# Формат данных: ХХХ = NNМ.МММ;
(\w+\s*=\s*\d+(\.\d+)?)\s*;\s*
(\w+\s*=\s*\d+(\.\d+)?)\s*;\s*
(\w+ \s*=\s*\d+ (\. \d+)?) \s*$""", re. VERВOSE)
for line in lines:
match = regex.match(line)
assert match is not None
print (match.group(O))
print(match.groups())
print (match.group(l), match.group(З), match.group(S),
sep=", ", end="\n\n")
Этот скрипт выведет следующий результат:
1. foo = 10; bar = 0.01; baz = 40
('foo = 10', None, 'bar = 0.01', '.01', 'baz = 40', None)
foo = 10, bar = 0.01, baz = 40
236.
bar = 0.01;
baz= 30.1; foo =20.5
('bar = 0.01', '.01', 'baz= 30.1', '.1', 'foo =20.5', '.5')
bar = 0.01, baz= 30.1, foo =20.5
42. foo= 0.5; bar=lO; baz=900
('foo= 0.5', '.5', 'bar=lO', None, 'baz=900', None)
foo= 0.5, bar=lO, baz=900
Обратите внимание, что некоторые группы имеют значения None. Так получается,
когда конструкция
(\. \d+J ?J
срабатывает при отсутствии дробной части у числа (в
противном случае эта группа содержит значение дробной части с точкой).
Скорее всего, нас не будет интересовать значение этой группы,
-
она только заму
соривает список групп, однако в приведенном выражении круглые скобки нам не
обходимы. На этот случай в регулярных выражениях есть конструкция, которая
называется «не захватывающие скобкю)
так:
« (?: ... ) )).
(non-capturing parentheses),
записываемая
При ее использовании подстроки, удовлетворяющие содержимому
этой конструкции, не попадают ни в какую группу. При этом инструкции
«+))
«?)), «*)),
и другие работают с такими группами как обычно.
Мы можем переписать пример, приведенный в листинге
конструкции «не захватывающие скобкю) (листинг
23.6).
23.5,
с использованием
Глава
23.
437
Регулярные выражения
Листинг 23.6.
Chapter_23/example_06/groups.py
import re
text = """1. foo = 10 ; bar = 0.01; baz = 40
1
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= О. 5; bar=l0; baz=900'""'
lines
regex
text.split("\n")
re.compile(r"""
# Число
в начале каждой строки
л\s*\d+\.\s*
# Формат данных: ХХХ = NNМ.МММ;
(\w+\s*=\s*\d ? \.\d+)?)\s*;\s*
(\w+\s*=\s*\d ?. \.\d+)?)\s*;\s*
(\w+\s*=\s*\d+J ? \.\d+)?)\s*$""",
for line in lines:
match = regex.match(line)
assert match is not None
print(match.group(0))
print(match.groups())
print(match.group(l), match.group(2),
sep=", ", end="\n\n")
re.VERВOSE)
match.group(З),
Изменения, произведенные в регулярном выражении, для разборчивости подсвече
ны серым фоном. Обратите внимание, что в этом примере нужно поменять индек
сацию групп в последней строке кода, поскольку теперь групп у нас стало меньше.
Результат работы этого скрипта выглядит так:
1. foo = 10 ; bar = 0.01; baz = 40
('foo = 10', 'bar = 0.01', 'baz = 40')
foo = 10, bar = 0.01, baz = 40
bar = 0.01;
baz= 30.1; foo =20.5
236.
('bar
0.01', 'baz= 30.1', 'foo =20.5')
bar = 0.01, baz= 30.1, foo =20.5
42. foo= 0.5; bar=l0; baz=900
('foo= 0.5', 'bar=l0', 'baz=900')
foo= 0.5, bar=lO, baz=900
Как можно видеть, мы здесь выделили из текста все выражения в формате ххх
= NNN. ммм. При дальнейшей обработке результатов действия регулярного выраже
ния нам, скорее всего, понадобилось бы разделить такие выражения на два: имя
переменной и ее значение. Но мы можем слегка дополнить регулярное выражение с
тем, чтобы имена и значения переменных попадали в отдельные группы (листинг 23.7).
Часть
438
Листинг 23.7.
11.
Основные подходы
Chapter_23/example_07/groups.py
import re
text = '"'"1. foo = 10 ;
236.
bar = 0.01;
42. foo= 0.5;
bar
= 0.01; baz = 40
baz= 30.1; foo =20.5
bar=l0;
lines
text.split("\n")
regex
re.compile(r"""
#
baz=900"""
Число в начале каждой строки
л\s*\d+\.\s*
#
Формат данных:
ХХХ
=
NNМ.МММ;
(?: ( \ w+) \s *= \s * ( \d+ (?: \. \d+) ? ) ) \s *; \s *
(?: ( \ w+) \s *= \s * (\d+ (?: \. \d+) ? ) ) \s *; \s *
(?: ( \ w+) \s *=\s* (\d+ (?: \. \d+) ? ) ) \s *$""",
re.VERВOSE)
for line in lines:
match = regex.match(line)
assert match is not None
print(match.group(0))
print(match.groups(), "\n")
В этой версии скрипта группа, объединяющая всё выражение ххх = NNN. ммм, сде
лана не захватывающей, а конструкции, описывающие имена и значения, обернуты
обычными скобками, чтобы эти подстроки попали в группы. В общем-то, в этом
примере внешние не захватывающие скобки не нужны, и их можно убрать, по
скольку к ним не применяются никакие операции. Результат выполнения этого
скрипта следующий:
1. foo = 10 ;
bar
= 0.01; baz = 40
('foo', '10', 'bar', '0.01', 'baz', '40')
236.
bar = 0.01;
baz= 30.1; foo =20.5
('bar', '0.01', 'baz', '30.1', 'foo', '20.5')
42. foo= 0.5;
bar=l0;
baz=900
('foo', '0.5', 'bar', '10', 'baz', '900')
С полученными кортежами из подстрок легко работать. Четные элементы кортежа
имена переменных, а нечетные
-
-
их значения. Главное, не запутаться в индексах.
Для удобства работы с группами регулярные выражения позволяют создавать имено
ванные группы, при использовании которых не требуется высчитывать номер нужной
группы, где легко ошибиться, а можно находить нужную подстроку по ее имени. Для
создания именованной группы используется конструкция
(? P<groupname> ... ),
где
Глава
23.
439
Регулярные выражения
вместо groupname нужно указать имя группы. Имена групп должны быть уникаль
ными в пределах регулярного выражения.
Дополним предыдущий пример, добавив к интересующим нас группам имена (лис
тинг
23.8).
Листинг 23.8.
Chapter_23/example_08/group_names.py
import re
text = """1. foo = 10; bar = 0.01; baz = 40
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= 0.5; bar=l0; baz=900'"'"
lines
regex
text.split("\n")
re.compile(r"""
# Число в
начале каждой строки
л\s*\d+\.\s*
#
Формат данных:
ХХХ
=
NNМ.МММ;
(?: (?P<namel>\w+)\s*=\s*(?P<vall>\d+(?:\.\d+)?))\s*;\s*
(?: (?P<name2>\w+)\s*=\s* (?P<val2>\d+(?:\.\d+)?) )\s*;\s*
(?: (?P<nameЗ>\w+) \s *=\s* (?P<valЗ>\d+ (?: \. \d+)?)) \s * $""",
re. VERВOSE)
for line in lines:
match = regex.match(line)
assert match is not None
print(line)
print(match.groupdict{))
print (f" {match.group( 'namel')}
print (f" {match.group( 'name2')}
print (f" {match.group ( 'nameЗ')}
print ()
Для
работы
с
именованными
{match.group( 'vall')} ")
{match. group ( 'val2') }")
{match.group ( 'valЗ')} ")
группами
класс
мatch
предоставляет
метод
groupdict ( J, возвращающий словарь, где ключами служат имена групп, а значе
ниями
-
соответствующие подстроки, которые в эти группы попали. При исполь
зовании метода
group ()
в качестве параметра метода также можно указывать стро
ку с именем именованной группы.
Результат работы этого примера выглядит следующим образом:
1. foo = 10 ; bar = 0.01; baz = 40
{'namel': 'foo', 'vall': '10', 'name2': 'bar', 'val2': '0.01', 'name3': 'baz', 'val3':
1 40 1}
foo
bar
baz
10
0.01
40
Часть
440
11.
Основные подходы
236.
bar = 0.01;
baz= 30.1; foo =20.5
{'namel': 'bar', 'vall': '0.01', 'name2': 'baz', 'val2': '30.1', 'name3': 'foo',
'val3': '20.5')
bar = 0.01
baz
30.l
foo = 20.5
42. foo= 0.5;
bar=lO;
baz=900
{'namel': 'foo', 'vall': '0.5', 'name2': 'bar', 'val2': '10', 'name3': 'baz', 'val3':
'900 1 )
foo = 0.5
bar = 10
baz = 900
Язык регулярных выражений в
Python
предоставляет еще множество интересных
типов групп, но мы ограничимся лишь теми, что уже рассмотрели. Далее мы узна
ем, в каких еще ситуациях могут пригодиться регулярные выражения.
Поиск и замена с помощью
регулярных выражений
Еще одно применение регулярных выражений
-
это поиск подстрок, которые удо
влетворяют регулярному выражению. При этом мы можем выполнять замену найденной подстроки на другой текст, который даже может использовать текст, по
павший в группы регулярного выражения. Начнем с простого поиска.
1
В предыдущих примерах мы предварительно разбивали многострочный текст на
отдельные строки, к которым применяли регулярное выражение, и проверяли с по
мощью инструкции assert, что все они удовлетворяют нашему шаблону. Если нам
не нужна такая строгость, и мы готовы проигнорировать строки, которые не подхо
дят под наш шаблон, то можем упростить код, осуществляя поиск подстрок, кото
рые
подходят под наше регулярное выражение,
игнорируя неправильные
строки.
Для поиска имеется несколько методов:
♦
search () -
поиск первого совпадения. Можно указывать интервал поиска в
строке. Возвращает объект Match, если совпадение найдено, или None в против
ном случае;
♦
findall () -
возвращает список найденных подстрок или список кортежей из
подстрок, если регулярное выражение содержит группы;
♦
f indi ter () -
возвращает итерируемый объект, последовательно возвращаю
щий объекты класса мatch, описывающие найденные подстроки.
♦
Последний метод самый гибкий, им мы и воспользуемся. Перепишем наш код
следующим образом (листинг
23.9).
Глава
23.
Регулярные выражения
Лмстинr 23.9.
441
Chapter_23/example_09/finditer.py
import re
text = """1. foo = 10 ; bar = 0.01; baz = 40
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= 0.5;
bar=lO;
baz=900"""
regex = re.compile(r"""
#
Число в начале каждой строки
л\s*\d+\.\s*
#
Формат данных:
ХХХ
=
NNМ.МММ;
(?: ( ?P<namel>\w+) \s*=\s* (?P<vall>\d+ (?: \. \d+)?)) \s*; \s*
(?: (?P<name2>\w+)\s*=\s* (?P<val2>\d+(?:\.\d+)?))\s*;\s*
(?: ( ?P<name3>\w+) \s*=\s* ( ?P<val3>\d+ (?: \. \d+)?)) \s*$""",
re.VERВOSE
I
re.МULTILINE)
for match in reqex.finditer(text):
print(match.group(O))
print(match.groupdict())
print (f" {match. group ( 'namel') }
{match.group('vall') }")
print(f"{match.group('name2')}
{match. group ( 'val2') }")
print (f" {match.group( 'name3'))
{match.group('val3') }")
print ()
В этом
коде
мы
поменяли
настройки регулярного
выражения, добавив
флаг
te.MULTILINE, который влияет на поведение символов <С» и«$». Без этого флага
указанные символы в регулярных выражениях обозначают начало и конец всего
текста соответственно, а с флагом re .MULTILINE им также соответствуют начало и
конец каждой строки.
Затем мы использовали метод findi ter (), чтобы найти все совпадения и получить
для каждого из них объект Match. Остальная часть осталась прежней за исключени
ем того, что теперь отпала необходимость в инструкции assert.
Однако поведение этого кода отличается от поведения его предыдущей версии, по
казанной в листинге
23.8.
Если в тексте будут строки, которые не удовлетворяют
шаблону, например:
bar
text = """l. foo = 10
236.
bar = 0.01;
= 0.01; baz = 40
baz= 30.l; foo =20.5
,Ыа-Ыа-Ыа
42. foo= 0.5;
bar=lO;
baz=900"""
ТQ предыдущая версия скрипта упала бы с ошибкой, а новая версия неправильную
строку просто проигнорирует. В остальном результат работы этого скрипта будет
таким же, как и у его предыдущей версии.
Часть
442
11.
Основные подходы
Теперь предположим, что нас не интересует строгий формат каждой строки, а мы
просто хотим найти все вхождения вида ххх = NNN. ммм. Тогда мы можем оставить
только часть нашего регулярного выражения, которая отвечает за этот шаблон, и
снова воспользоваться методом f indi ter () (листинг
Листинг
23.1 О).
23.1 О. Chapter_23/example_10/find_variaЫes.py
import re
text = """1. foo = 10 ; bar = 0.01; baz = 40
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= О. 5; bar=l0; baz=900"""
regex
re. compile (r" "" (?: ( ?P<name>\w+) \s*=\s* ( ?P<val>\d+ (?: \. \d+) ?) ) "'"',
re .№JLTILINE)
for match 1n regex.finditer(text):
print(match.groupdict())
В рассматриваемом случае не играет роли, установлен ли флаг re.MULTILINE, по
скольку в регулярном выражении не используются символы«"» и«$». Результатом
работы этого скрипта будет следующий текст в консоли:
{ 'name': 'foo', 'val': '10'}
{ 'name': 'bar', 'val': 1о. 01 1}
{ 'name': 'baz', 'val': '4 о 1}
{ 'name': 'bar', 'val': 1о. 01 1}
{ 'name': 'baz', 'val': 130 .1')
{ 'na:ne': 'foo', 'val': 120.5 1}
{ 'name': 'foo', 'val': 1о.51}
{ 'name': 'bar-', 'val': 110 1 }
{ 'name': 'baz', 'val': 1900 1}
В наших примерах текст, по которому происходит поиск, намеренно сделан очень
неряшливым, пробелы расставлены хаотично, где-то их нет, где-то их больше од
ного. Чтобы сделать текст более аккуратным, а заодно разобраться, как работает
замена с помощью регулярных выражений, поставим себе задачу заменить выра
жения вида ххх = NNN. ммм на xxx=NNN. ммм, то есть без пробелов до и после знака
равенства.
Для такой замены мы воспользуемся регулярным выражением из предыдущего
примера (см. листинг
23 .1 О)
и методом sub () . Этот метод в качестве первого пара
метра принимает строку для замены, а в качестве второго
-
текст, в котором осу
ществляется поиск и замена. Самое важное, что в строке для замены мы можем ис
пользовать специальные инструкции подстановки:
♦
\g<groupname> - для подстановки подстроки, попавшей в именованную группу
с именем groupname;
Глава
♦
23.
Регулярные выражения
\ 1 ... \ 9 -
443
для подстановки подстрок, попавших в неименованные группы. Если
групп больше
9,
то нужно использовать запись «\g<NN»> -
например: «\g<lO>».
Также для подстановки полной найденной подстроки можно применить запись
«\g<O>».
Таким
образом,
для
решения
нашей
задачи
строка для
замены
будет равна:
\g<name>=\g<val>, а сам скрипт может выглядеть, как показано в листинге
Листинг 23.11.
23.11.
Chapter_23/example_11/replace.py
import re
text = "'"'1. foo = 10 ;
bar
=
О.
01; baz
40
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= 0.5; bar=lO; baz=900"""
regex = re. compile (r""" (?: ( ?P<name>\w+) \s*=\s* ( ?P<val>\d+ (?: \. \d+) ?) ) """,
re.MULTILINE)
result = reqex. suЬ (r"\q<name>=\q<val>", text)
print (result)
Результатом работы этого скрипта будет текст:
1. foo=lO ;
236.
bar=0.01; baz=40
bar=0.01;
42. foo=0.5;
Ьаz=ЗО.1;
bar=lO;
foo=20.5
baz=900
В нашем простом регулярном выражении мы можем использовать неименованные
группы, и тогда код, отвечающий за поиск и замену, выглядел бы так (листинг
Листинг
23.12).
23.12. Chapter_23/example_12/replace.py
regex = re.compile(r""" (?: (\w+)\s*=\s*(\d+(?:\.\d+)?))""",
re .МULTILINE)
result = regex. suЬ(r"\l=\2", text)
Результат на выводе будет точно таким же.
Метод
sub ()
также
может
принимать
именованный
целочисленный
параметр
count, который обозначает, сколько замен нужно сделать в строке. Если count=O,
то в строке заменяются все подстроки, удовлетворяющие шаблону.
Кроме того, класс Pattern содержит метод subn (), который отличается от метода
sub ()
тем, что возвращает кортеж из двух элементов:
равное количеству сделанных замен.
результат замены и число,
Часть
444
Коротко про функции из модуля
11.
Основные подходы
re
Во всех предыдущих примерах мы предварительно компилировали регулярное вы
ражение с помощью функции re. compi le () и получали экземпляр класса Ра t tern.
После этого применяли методы этого класса: ma tch (), f indi ter (), sub () и др. Од
нако модуль re позволяет использовать менее объектно-ориентированный подход,
предоставляя аналогичные собственные функции. У таких функций имена совпа
дают с аналогичными методами из класса
Pattern,
но только они в качестве перво
го параметра принимают строку с регулярным выражением, а в качестве последне
го
-
именованный параметр flags, с помощью которого можно задавать настрой
ки для регулярного выражения.
В таком стиле программирования наш пример будет выглядеть следующим обра
зом (листинг
Листинг 23.13.
23.13).
Chapter_23/example_13/replace_function.py
import re
text = """1. foo = 10; bar = 0.01; baz
236.
bar = 0.01;
baz= 30.1; foo =20.5
42. foo= 0.5; bar=lO; baz=900"""
40
regex = r""" (?: (?P<name>\w+) \s*=\s* (?P<val>\d+ (?:\. \d+) ?) ) "'"'
result = re.sub(regex, r"\g<name>=\g<val>", text, flags=re.MULTILINE)
print(result)
Принципиальных различий между этими двумя подходами нет.
Заключение
Регулярные выражения предназначены для сопоставления текста с образцом, кото
рый кодируется с помощью специального языка, позволяющего описывать доста
точно сложные конструкции и множество вариантов совпадения. Это обширная
тема, про которую пишут целые книги, например
здесь рассмотреть,
-
[3 ],
поэтому то, что мы успели
это самые базовые операции.
Для создания экземпляра класса Ра t tern по регулярному выражению используется
функция
re. compi le ().
Для
сопоставления
текста с
образцом
служит
метод
match () класса Pattern или одноименная функция из модуля re.
Для поиска текста, совпадающего с регулярным выражением, предназначены мето
ды search (), findall () и finditer () из класса Pattern или одноименные функции
ИЗ модуля
re.
Для поиска и замены предназначены методы sub () и subn () класса Ра t tern или
одноименные функции из модуля re.
Глава
23.
Регулярные выражения
445
Мы изучили основные инструкции этого языка для составления регулярных выра
жений, но далеко не все его возможности. Мы ничего не сказали про «жадные» и не
«жадные» варианты инструкций
«+»
и
«*»,
про то, что настройки регулярных вы
ражений можно применять не только ко всему выражению в целом, но и менять их
в пределах групп, про ссылки на уже найденный текст, на группы, которые позво
ляют посмотреть, какие символы идут после заданной позиции, но не захватывать
их, а также другие типы групп. Мы ничего не сказали про функцию re. spli t () и
одноименный метод в классе Pattern, применяемые для разделения строки по ре
гулярному выражению.
В Интернете можно найти множество онлайн-сервисов, предоставляющих возмож
ность быстро проверить регулярные выражения на введенном тексте и убедиться,
что в группы попадают нужные символы, а также при необходимости скорректиро
вать регулярное выражение
В первых примерах этой главы мы написали примеры, которые с помощью инст
рукции assert сверяют полученный результат с ожидаемым. По сути, мы написали
unit-тecты для тестирования регулярного выражения. В следующей главе мы рас
смотрим тему тестирования приложений более подробно и узнаем, какие инстру
менты предоставляет для этого стандартная библиотека
Python.
- ГЛАВА 24-
Тестирование приложений
Зачем нужны тесты, и какие они бывают?
Тестирование
-
это важный этап разработки любого приложения, независимо от
его размера. Мы должны убедиться, что написанная программа работает, причем
работает так, как было задумано. Если мы пишем небольшой скрипт для обработки
данных, который будем использовать один-два раза, то, скорее всего, будет доста
точно ручного тестирования, когда мы запустим этот скрипт и увидим на несколь
ких примерах, что получаем ожидаемые результаты.
Однако для более крупных проектов, а также в случае, когда приложение постоян
но развивается с добавлением новых возможностей и периодическим переписыва
нием кода, ручное тестирование станет очень неэффективным, поскольку начнет
занимать слишком много времени и сил, а это значит, что в коде приложения мож
но будет легко пропустить ошибки, внесенные во время программирования. В этом
случае
используют автоматическое тестирование
-
пишут тесты,
вызывающие
ту или иную функцию приложения и проверяющие, что приложение ожидаемо реа
гирует на различные входные данные.
Автоматические тесты делят на несколько уровней.
♦
На самом нижнем уровне располагаются модульные тесты
(unit tests),
предна
значенные для тестирования отдельных функций и классов. Такие тесты должны
выполняться быстро и, по возможности, не взаимодействовать с внешней сре
дой, то есть с сетью, базами данных, файловой системой. Если при разработке
архитектуры приложения мы заранее ориентируемся на тестирование, то классы
желательно делать как можно более независимыми,
-
то есть при создании
класса не создавать множество других классов, от которых он зависит, чтобы не
усложнять последующее написание тестов.
Некоторые методологии разработки приложений рекомендуют писать модуль
ные тесты даже раньше основного кода. Это называется разработкой через
тестирование (от англ.
TDD).
test-driven development,
часто используется аббревиатура
В этом случае мы сначала пишем тест, но он не будет пройден (говорят,
«провален»), и только потом пишем код, чтобы тест стал успешно проходить,
Глава
Тестирование приложений
24.
447
после этого пишем новый тест, который опять сначала будет провален, затем
пишем новый код, чтобы тест проходил, и т. д.
♦ Второй уровень тестов
-
интеграционные тесты. Они проверяют взаимодей
ствие нескольких классов и модулей между собой. Такие тесты уже более мед
ленные, хотя, по возможности, в них лучше тоже избегать взаимодействия с
внешней средой, которое, как правило, всегда происходит медленно.
♦ Третий уровень тестов
-
системное тестирование. На этом уровне приложе
ние тестируется как единое целое, проверяются интерфейсы, взаимодействие с
сетью, с базами данных, в том числе в ситуациях, когда, например, пропадает
сетевое соединение. На этом уровне важно убедиться, что приложение коррект
но отреагирует на ввод неожиданных значений
где
ожидается
ввод
чисел,
или
ввод
-
например, букв в тех полях,
отрицательных
чисел
там,
где
по
логике
должны вводиться только положительные, и т. д.
♦ И, наконец, четвертый уровень тестирования
которых
-
-
это приемочные тесты, цель
продемонстрировать заказчику, что приложение работает так, как
было написано в техническом задании.
В этой главе мы будем говорить о модульном тестировании, хотя эти же инстру
менты можно использовать и для интеграционных тестов.
Поскольку
Python -
интерпретируемый язык с динамической типизацией, при раз
работке очень легко внести какую-нибудь глупую ошибку, связанную с неожидан
ным типом переменной, неправильным указанием имени переменной и т. п. Если в
l(Оде встречаются подобные ошибки, то узнаем мы о них только в процессе работы
программы, когда попытаемся выполнить участок кода, содержащий ошибку. По
этому программы на
Python
Python следует тестировать
особенно тщательно.
предоставляет несколько стандартных инструментов для модульного тести
рования
это модуль unittest, а также модуль doctest, позволяющий писать
-
простые тесты непосредственно в строках документации к функциям, классам или
модулям. Сначала мы рассмотрим создание тестов с помощью модуля unittest.
Создание тестов с помощью модуля
unittest
Для демонстрации работы с тестами мы напишем небольшой класс, предназначен
ный для
выполнения
геометрических расчетов,
связанных
с треугольником
на
плоскости. Сначала создадим конструктор класса Triangle и поместим его в файл
triangle.py. Конструктор будет принимать три кортежа из двух чисел с плавающей
точкой, соответствующие координатам вершин треугольника на плоскости (лис
тинг
24.1 ).
cl_ass Triangle:
def
init (self,
pl: tuple[float, float],
Часть
448
tuple[float, float],
tuple[float, float]):
self._vertices: list[tuple[float, float]] = [pl,
11.
Основные подходы
р2:
рЗ:
р2,
р2]
Пока этот класс ничего не делает, кроме того, что сохраняет переданные координа
ты вершин во внутренний список. Внимательный читатель заметит, что в этом коде
есть ошибка, но мы сделаем вид, что о ней еще не знаем.
Начнем наполнять этот класс функциональностью, придерживаясь принципа разра
ботки через тестирование, о котором говорилось ранее. Для начала сделаем так,
чтобы мы могли получать координаты вершин с помощью индексации, используя
квадратные скобки. Для этого нам понадобится определить «магический» метод
_getitem_() (про такие методы речь шла в главе 15). Но перед этим напишем
тест, который должен показать, как мы хотим использовать новую возможность
класса. Этот тест, разумеется, сначала не будет проходить.
Для тестов мы создадим отдельный файл с именем
tests.py.
При использовании
стандартного модуля uni t test все тесты должны входить в какой-нибудь тесто
вый набор
(test case),
которых может быть много. Тестовые наборы обычно объеди
няют тесты, связанные с одной и той же тестируемой областью,
-
например, тес
тирующие один и тот же класс. Тестовый набор представляет собой класс, произ
водный от класса
сами тесты
-
unittest.TestCase. Внутри этого класса должны располагаться
методы, названия которых начинаются с префикса test. Эти методы
не принимают никаких параметров (кроме self) и не возвращают никаких значений.
Наш первый тест приведен в листинге
24.2.
unittest
from triangle import Triangle
з..q:юrt
class TriangleTests(unittest.TestCase):
def testinit(self):
(О.О, О.О)
pl
р2
=
(О.О, 1. О)
= (2.0, О.О)
triangle = Triangle(pl,
рЗ
р2,
рЗ)
self.assertEqual(triangle[D], pl)
self.assertEqual(triangle[l], р2)
self.assertEqual(triangle[2], рЗ)
if
== " main
name
unittest.main ()
"·
Здесь мы создали класс TriangleTests, который будет включать в себя различные
тестовые методы. Как говорилось ранее, этот класс должен наследовать классу
uni t test. TestCase. Первый наш тест -
метод testlni t (). В этом тесте мы прове-
Глава
24.
Тестирование приложений
449
ряем, можно ли с помощью оператора индексации получить координаты вершин,
переданных
Triangle,
в
конструктор
класса.
Внутри
а затем три раза вызывается метод
теста
создается
assertEqual ()
экземпляр
класса
класса
Testcase,
пред
назначенный для проверки того, что первые два переданных ему параметра равны
между собой. Если это так, выполнение теста продолжается дальше. Если условие
равенства не выполняется, тест помечается как проваленный, а тестовый метод
прерывается. Также тест не считается пройденным, если в тестовом методе про
изойдет возбуждение исключения, которое не будет перехвачено.
Помимо метода assertEqual (), класс Testcase содержит еще множество других
методов, проверяющих различные условия. Все такие методы начинаются с пре
фикса assert. В табл.
24.1
приведен ряд методов assert*, применяемых для прове
рок различных условий. Это не полный список, но он содержит наиболее часто ис
пользуемые методы. Для многих методов assert * существует «инверсный» метод,
проверяющий противоположное условие. Например, для метода assertEqual ()
таким методом является assertNotEqual (), который успешно проходит, только
если два его аргумента не равны между собой.
Таблица
Метод класса
24.1. Методы assert *, применяемые для проверок различных условий
«Инверсный» метод
Проверяемое условие
TestCase
assertEqual ()
Два аргумента равны
assertNotEqual
assertAlmostEqual ()
Два аргумента равны с указанной точ-
assertNotAlmostEqual
ностью
assertGreater ()
assertGreaterEqual()
Первый аргумент больше второго
-
Первый аргумент больше второго
-
либо равен ему
assertLess ()
assertLessEqual()
Первый аргумент меньше второго
-
Первый аргумент меньше второго
-
либо равен ему
assertis ()
Два аргумента указывают на один
и тот же объект
assertisNot
asser-tisNone ()
Аргумент равен None
assertisNotNone
assertTrue ()
Аргумент равен True
-
assertFalse ()
Аргумент равен False
-
assertin ()
Первый аргумент входит во второй
assertNotin
аргумент {контейнер). Сравнение осуществляется с помощью оператора
in
Часть
450
11.
Основные подходы
Таблица
Метод класса
assertisinstance()
«Инверсный» метод
Проверяемое условие
TestCase
Первый аргумент является экэемпля-
24.1 (окончание)
assertNotisinstance
ром класса, переданного в качестве
второго аргумента. Проверка осуществляется с помощью функции
isinstance()
assertCountEqual()
Два аргумента, являющихся контейне-
-
рами, содержат одинаковое количество
элементов
assertRegex ()
Текст, переданный в качестве первого
assertNotRegex
аргумента, содержит подстроку, удавлетворяющую регулярному выражению,
переданному в качестве второго аргу-
мента (регулярные выражения мы рассмотрели в главе
assertRaises ()
23)
При вызове функции, переданной в
качестве второго аргумента, воэбужда-
-
ется исключение, класс которого передан в качестве первого аргумента
Кроме того, класс тestcase содержит метод
fail (), который делает текущий тест
проваленным.
Все эти методы также могут принимать дополнительный параметр
msg, представ
ляющий собой строку с комментарием к тесту. Эта строка помогает легче находить
проблемные тесты с помощью лога работы. В наши тесты такую строку мы доба
вим чуть позже.
В самом конце файла
tests.py
в случае, если этот скрипт выполняется непосредст
венно
с
помощью
команды
вызывается
функция
python
tests. ру,
un i t te s t. ma i n () , запускающая все тесты этого модуля. Далее мы увидим, что это
не единственный способ запуска тестов.
Давайте, наконец, запустим наш тест:
> python tests.py
Е
ERROR: testinit ( main
.TriangleTests.testinit)
Traceback (most recent call last):
File " ... /Chapter_24/example_Ol/tests.py", line 10, in testinit
self.assertEqual(triangle[0], pl)
Глава
24.
Тестирование приложений
TypeError: 'Triangle' object is not
451
subscriptaЫe
Ran 1 test in 0.001s
FAILED (errors=l)
Разумеется, наш тест не прошел проверку. Разберемся с тем, что здесь написано. На
«Error».
теста - один
самой первой строке стоит буква «Е», что означает
Именно на этой строчке
выводятся статусы всех тестов. Для каждого
символ. Вариантов сим
волов может быть несколько:
♦
Е
-
ошибка, обозначающая, что в теле теста произошло необработанное исклю
чение;
♦
F -
ошибка, обозначающая, что хотя бы одно из условий теста, проверяемых с
помощью методов assert *, не выполнено
♦
.-
(Failed);
тест успешно пройден.
В нашем случае тест пока только один. После краткой сводки о всех тестах выво
дятся ошибки, произошедшие во всех тестах, если они есть. В нашем случае мы
видим, что тест провалился из-за возникшего исключения, т. к. в методе
еще
не
реализован
метод
_geti tem_ (),
и
поэтому
к
экземпляру
Triangle
этого
класса
нельзя применять оператор «квадратные скобки».
В самом конце, после описания всех ошибок, выводится результат тестирования.
В нашем случае он неутешительный
-
мы запустили один тест и получили одну
ошибку. С этим надо что-то делать.
Добавим в класс
Листинг
Triangle реализацию метода _getitem_ () (листинг 24.3).
24.3. Chapter_24/example_02/triangle.py
class Triangle:
def _getitem_(self, index: int) -> tuple[float, float]:
return self._vertices[index]
Еще раз запустим тест:
> python tests.py
F
FAIL: testinit ( main
.TriangleTests.testinit)
Traceback (most recent call last):
File " ... /Chapter_24/example_02/tests.py", line 12, in testinit
self.assertEqual(triangle[2], рЗ)
~~~~~~~~~~~~~~~~
AssertionError: Tuples differ: (О.О, 1.0) != (2.0, О.О)
ллллллллллллллллл
452
Часть
First differing element
11 . Основные
подходы
О :
о.о
2.0
- (О.О, 1.0)
+ (2.0, О.О)
Ran 1 test in 0.001s
FAILED (failures=l)
Буква «Е» в первой строке сменилась на букву
«F».
Это значит, что необработанных
исключений не возникло, но в тесте не выполнено какое-то условие. Из текста со
общения об ошибке видно, что проблема проявилась на 12-й строке файла
Далее, после описания исключения
tests.py.
AssertionError, написано, почему условие не
выполнено: в кортежах не совпадают первые элементы.
В самом конце видно, что мы запустили один тест, ошибок больше нет, но есть
один проваленный
(failed)
тест.
Чтобы было проще ориентироваться в проваленных тестах, добавим к вызовам ме
тода assertEqual () третий параметр с информацией о тесте (листинг
24.4).
Листинr 24.4. Chapter_241example_OЗ/tests.py
class TriangleTests(unittest.TestCase):
def testinit(self):
pl = (О.О, О.О)
р2 =
( 2 . О,
1 . О)
рЗ
= (-o:s, 1.s)
triangle = Triangle(pl,
р2,
рЗ)
self.assertEqual(triangle[O], pl,
self .assertEqual (triangle [1], р2,
self.assertEqual(triangle[2], рЗ,
"Координата первой вeplllIOGI")
"Координата второй вeplllIOGI")
"Координата третьей вершииw")
Если теперь запустить тесты, то информация об ошибке будет выглядеть так:
Traceback (most recent call last):
File " ... /Chapter_24 / example_03/tests.py", line 12, in testinit
self.assertEqual(triangle[2], рЗ, "Координата третьей вершины")
~~~~~~~~ ~ ~~~~~~~ллллллллллллллллллллллллллллллллллллллллллллллл
AssertionError: Tuples differ:
First differing element
о.о
2.0
О:
(О.О,
1.0) != (2.0,
О.О)
Глава
24.
Тестирование приложений
- (О.О, 1.0)
+ (2 . О, О . О) :
453
Коор.цина.та третьей вep!IDOIЫ
После размышлений о том, почему последнее условие не выполняется, мы замеча
ем ошибку в конструкторе класса Triangle и исправляем ее (листинг
Листинг 24.5.
24.5).
Chapter_24/example_04/triangle.py
class Triangle:
def
init (self,
pl: tuple[float, float],
р2: tuple[float, float],
рЗ: tuple[float, float]):
self. vertices: list[tuple[float, float]]
[pl,
р2,
рЗ]
Теперь выполнение теста будет выглядеть следующим образом:
> python tests.py
Ran 1 test in 0.000s
ок
Лаконично. Ошибок нет, один пройденный тест (одна точка в первой строке).
Добавим еще тесты
Продолжим развивать наш класс Triangle и добавим метод с именем side_len (),
который будет принимать два индекса вершин и возвращать длину стороны между
ними.
При тестировании нам понадобится сравнивать числа с плавающей точкой, причем
в тестах мы сможем ввести ожидаемое значение только с ограниченной точностью.
На этот случай в классе
Testcase есть метод assertAlmostEqual:
assertAlmostEqual(first, second, places=7, msg=None, delta=None)
Первые два параметра:
first
и
second -
аналогичны первым двум параметрам из
метода assertEqual (). Это значения, которые мы сравниваем друг с другом. Пара
метр msg мы тоже использовали в assertEqual () для добавления комментария к
тесту. В методе
♦
параметр
assertAlmostEqual () интересны оставшиеся параметры:
places задает, с точностью до какого знака после запятой должны сов
падать сравниваемые значения;
♦
если задан параметр del ta, то значение этого параметра будет определять до
пустимую погрешность сравнения первых двух параметров функции. В нашем
случае параметр
places
игнорируется.
Добавим тест на проверку правильности расчета длин сторон (листинг
24.6).
Часть
454
Листинг 24.6.
11.
Основные подходы
Chapter_24/example_05/tests.py
class TriangleTests(unittest.TestCase):
def testSides (self) :
(О.О, О.О)
pl
р2 = ( О . О, 1 . О )
рЗ = ( 2. О, О. О)
triangle = Triangle(pl, р2, рЗ)
self.assertAlmostEqual(trianqle.side_len(O, 1), 1, delta=le-5)
self.assertAlmostEqual(trianqle.side_len(O, 2), 2, delta=le-5)
self.assertAlmostEqual(trianqle.side_len(l, 2), 2.236, places=З)
И добавим в класс
Листинг
Triangle метод side_len () (листинг 24.7).
24.7. Chapter_24/example_05/trlangle.py
import math
class Triangle:
def side_len(self, indexl: int, index2: int) -> float:
pl = self._vertices[indexl]
р2 = self._vertices[index2]
return math.hypot(pl[O] - р2[O], pl[l] - р2[1])
В тесте
testSides используется метод assertAlmostEqual () с разными способами
проверки близости двух значений. Если запустить файл с тестами, то результат бу
дет следующий:
> python tests.py
Ran 2 tests in O.OOOs
ок
Два теста прошли успешно. Теперь посмотрим, как мы можем проверить, что вы
зываемый метод в случае ошибки возбуждает исключение. Добавим новое требова
ние к методу
side_len () -
индексы вершин во входных параметрах функции
должны принимать значения О,
1
значение или значение больше
должно возбуждаться исключение ValueError.
2,
или
2.
Если пользователь вводит отрицательное
Для проверки того, что код возбуждает нужное исключение, предназначен метод
assertRaises (), который может работать в двух режимах. В одном режиме в этот
метод нужно передать
класс ожидаемого
исключения,
указатель на тестируемую
функцию, которая должна возбудить исключение, а также параметры для вызова
этой функции. В другом режиме метод assertRaises () можно использовать вместе
Глава
24.
Тестирование приложений
с оператором wi th -
455
в этом случае код внутри блока wi th должен возбудить ожи
даемое исключение, которое указывается в качестве единственного параметра ме
тода
assertRaises ().
Добавим пару тестов на еще не реализованную возможность класса Triangle (лис
тинг
24.8).
Листинг 24.8.
Chapter_241example_06/tests.py
class TriangleTests(unittest.TestCase):
def testSidesindexErrorl(self):
(О.О, О.О)
pl
р2
р3
= ( О . О, 1 . О)
= (2. О, О. О)
triangle = Triangle(pl, р2, р3)
aelf.assert.Raises(ValueError, trianqle.side_len, -1,
О)
def testSidesindexError2(self):
(О.О, О.О)
pl
р2 = (О . О, 1 . О 1
р3
= (2. О,
О. О)
triangle = Triangle(pl, р2, р3)
with self.assert:Raises{ValueError):
triangle.side_len(2, 3)
Эти тесты показывают оба способа использования метода assertRaises (). Запус
тим тесты и посмотрим на результат:
> python tests.py
"FE
ERROR: testSidesindexError2 ( main
.TriangleTests.testSidesindexError2)
Traceback (most recent call last):
File " ... /tests.py", line 36, in testSidesindexError2
triangle.side_len(2, 3)
File " ... /triangle.py", line 15, in side len
р2 = self._vertices[index2]
~~~~~~~~~~~~~~
IndexError: list index out of range
лллллллл
FAIL: testSidesindexErrorl ( main
.Tr1angleTests.testS1desindexErrorl)
Traceback (most recent call last):
File " ... /tests.py", line 28, in testSidesindexErrorl
456
Часть
self.assertRaises(ValueError, triangle.side_len, -1,
AssertionError: ValueError not raised Ьу side_len
11.
Основные подходы
О)
Ran 4 tests in 0.001s
FAILED (failures=l, errors=l)
Тест testSidesindexErrorl () провалился, потому что метод side_len () не возбу
дил никакого исключения, а тест testSidesindexError2 () провалился, так как бы
ло возбуждено не то исключение
тод side len () (листинг
Листинг
-
IndexError вместо ValueError. Исправим ме
24.9).
24.9. Chapter_24/example_07/triangle.py
class Triangle:
def side_len(self, indexl: int, index2: int) -> float:
if indexl < О or indexl > 2 or index2 < О or index2 > 2:
raise ValueError("Heпpuиnьиыe индексы верш,~и")
pl = self._vertices[indexl]
р2 = self._vertices[index2]
return math.hypot(pl[O] - р2[0], pl[l] -
р2[1])
Теперь все четыре теста выполнятся успешно:
> python tests.py
Ran 4 tests in 0.000s
ок
Подготовка данных для тестов
Вы могли обратить внимание, что все написанные нами тесты начинались одинако
во,
-
с создания экземпляра класса
Triangle, который в каждом тесте имеет одни
и те же координаты вершин. От дублирования кода желательно избавляться. По
скольку для тестов часто нужно подготавливать одни и те же данные, а затем, после
завершения теста, освобождать занятые им ресурсы, в базовом классе тestcase
предусмотрены четыре метода, которые можно переопределить:
♦
setUpClass () -
вызывается сразу после создания класса с тестами;
♦
setup () -
♦
tearDown () -
♦
tearDownclass () -
вызывается перед каждым тестом;
вызывается после каждого теста;
вызывается после завершения последнего теста.
Глава
24.
Тестирование приложений
457
Методы
setUp () и tearDown () являются обычными методами, а setUpClass () и
tearDownClass () -методами класса (о том, что это такое, рассказано в главе 13).
Метод setup ()
обычно используется с целью создания для тестов каких-либо
вспомогательных объектов. При этом для него могут быть выделены те или иные
ресурсы, которые потребуется освободить после завершения теста, независимо от
его результата. Это может быть, например, создание временных файлов, баз дан
ных или соединений с удаленным сервером. В модульных тестах для ускорения их
работы использования таких ресурсов желательно избегать, но если вы задействуе
те модуль
unittest
для написания интеграционных тестов, то это вполне оправ
данно. Освобождать созданные ресурсы (удалять файлы, очищать базу данных)
можно в методе
tearDown ().
Исправим класс с тестами, перенеся создание экземпляра класса тriangle в метод
setUp () (листинг 24.10).
Листинг 24.10.
Chapter_24/example_08/tests.py
class TriangleTests(unittest.TestCase):
def setUp(self):
self.pl = (О.О, О.О)
self.p2 = (О.О, 1.0)
self.p3 = (2.0, О.О)
self.triangle = Triangle(self.pl, self.p2, self.p3)
def testinit(self):
self.assertEqual(self.triangle[O], self.pl,
"Координата первой вершины")
self.assertEqual(self.triangle[l], self.p2,
"Координата второй вершины")
self.assertEqual(self.triangle[2], self.p3,
"Координата третьей вершины")
def testSides(self):
self.assertAlmostEqual(self.triangle.side_len(O, 1), 1,
delta=le-5)
self.assertAlmostEqual(self.triangle.side_len(O, 2), 2,
delta=le-5)
self.assertAlmostEqual(self.triangle.side_len(l, 2), 2.236,
places=3)
def testSidesindexErrorl(self):
self.assertRaises(ValueError, self.triangle.side_len, -1,
def testSidesindexError2(self):
with self.assertRaises(ValueError):
self.triangle.side_len(2, 3)
О)
Часть
458
11.
Основные подходы
Теперь переменные triangle, pl, р2 и рЗ являются полями класса TriangleTests, и
поэтому обращаться к ним нужно через self. В остальном код тестов стал заметно
короче. На результате работы это изменение не скажется.
В нашем простом случае, поскольку объект triangle не меняет своего состояния в
процессе тестирования, мы могли бы создавать его не перед каждым тестом в ме
тоде
setUp (),
setUpClass () Листинг
а
только
в
самом
начале
-
до
запуска
как, например, показано в листинге
первого
теста
в
методе
24.11.
24.11. Chapter_24/example_09/tests.py
class TriangleTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.pl
(О.О, О.О)
cls.p2 = (О.О, 1.0)
cls.pЗ = (2.0, О.О)
cls.triangle = Triangle(cls.pl, cls.p2,
cls.pЗ)
Остальной код при этом можно не изменять. Результат выполнения тестов тоже не
изменится.
Способы запуска тестов
До сих пор для запуска тестов использовалась функция unittest.main(). Такой
способ подходит, когда тестов не много, и все они расположены в одном модуле.
В более или менее крупных проектах количество тестов исчисляется сотнями и ты
сячами, и они разнесены по разным модулям. В этих случаях такой способ запуска
тестов становится затруднительным, хотя к нему, конечно, можно прибегнуть, если
внутри модуля, где вызывается функция unittest.main(), импортируются осталь
ные тестовые модули, но это кропотливая работа, и в процессе ее выполнения мож
но легко забыть импортировать какой-нибудь модуль с тестами.
Задачу поиска всех классов с тестами, производных от класса Testcase, можно пе
реложить на модуль
unittest. При этом для поиска классов с тестами и их запуска
достаточно в командной строке перейти в каталог, где расположены тесты, и вы
полнить одну из следующих двух команд (они эквивалентны):
> python -m unittest discover
или
> python -m unittest
Здесь важно, чтобы файлы с тестами соответствовали маске
tests.py
test*.py.
Наш файл
этой маске удовлетворяет. У команды discover есть дополнительный пара
метр -р (или --pattern), позволяющий изменять маску файлов, в которых модуль
Глава
24.
Тестирование приложений
459
unittest пытается обнаружить тесты, но подробно на этом параметре мы останав
ливаться не будем.
Важно также и то, что модуль unittest будет искать тесты не только в текущем
каталоге, но и рекурсивно во вложенных пакетах
ini t
Если
каталогах, содержащих файл
-
. ру (модули и пакеты мы подробно обсуждали в главе 12).
нужно запустить тесты,
находясь в другом каталоге, то после команды
discover (в этом случае ее уже обязательно указывать) следует добавить параметр
-s
и имя каталога с тестами:
> python -m unittest discover -s
"путь_до_каталога"
До сих пор все тесты мы запускали в обычном режиме, однако есть еще «много
словный»
(verbose)
режим, когда в процессе тестирования выводится полное имя
каждого теста и результат его выполнения. Для перехода к такому режиму нужно
добавить параметр -v или --verbose. Вот пример запуска наших тестов в «много
словном» режиме:
> python -m unittest -v
testinit (tests. TпangleTests. testinit) ... ok
testSides (tests. TriangleTests. testSides) ... ok
testSidesindexErrorl (tests.TriangleTests.testSidesindexErrorl)
testSidesindexError2 (tests.TriangleTests.testSidesindexError2)
ok
ok
Ran 4 tests in 0.000s
ок
Кроме того, мы можем запускать не все тесты, а только те, что расположены в оп
ределенном файле, модуле, классе, а также запускать отдельные тестовые методы.
Для нашего случая эти варианты будут выглядеть так:
python
python
python
python
python
-m unittest tests.py
-m unittest tests
-m unittest tests.TriangleTests
-m unittest tests.TriangleTests.testinit
-m unittest tests.TriangleTests.testSidesindexErrorl \
tests.TriangleTests.testSidesindexError2
Первые два варианта запуска для наших тестов не очень показательны, поскольку у
нас имеется только один файл с тестами, и он же
-
единственный модуль с теста
ми, но при разработке приложений среднего и крупного размера такие способы за
пуска часто используются при отладке, чтобы не приходилось ждать выполнения
тестов, результат которых не интересует в текущий момент. Из последнего примера
видно, что мы можем для запуска указать несколько тестов, это же относится и к
остальным сущностям: модулям, файлам, классам.
460
Часть
11.
Основные подходы
Тесты в строках документации
В главе
11
мы говорили о том, что функции, классы и модули могут включать в себя
строки документации, используемые для отображения информации о них в автома
тически создаваемой документации и подсказках в среде разработки. Помимо это
го, строки документации могут включать в себя небольшие блоки кода, демонстри
рующие работу функции, класса или метода, к которым они относятся. Но самое
главное, что эти примеры кода могут приводиться с результатом работы, а с помо
щью стандартного модуля doctest такие примеры можно запускать и убеждаться,
что результат работы, приведенный в строке документации, соответствует действи
тельности, иначе будет выводиться ошибка.
Добавим строки документации к классу Triangle (листинг
Листинг 24.12.
24.12).
Chapter_24/example_10/triangle.py
class Triangle:
"'"'Класс,
трех
описывающий треугольник с помощью координат
вершин
на
плоскости.
>»triangle=Triangle((0, 0), (0, 1), (2, 0))
»> triangle[0]
(0,
О)
»> triangle[l]
(О,
1)
>» triangle[2]
(2, О)
def
init
(self,
pl: tuple [ float, float],
р2: tuple [ float, float],
рЗ: tuple[float, float]) :
self. vertices: list[tuple[float, float]]
[pl,
р2,
рЗ]
В документации строки описания и строки кода могут перемешиваться. Код в стро
ках документации выглядит так, как будто его скопировали из интерактивного ре
жима
(REPL), -
они начинаются с символов«»>
» (пробел
после них обязателен).
Следующая строка после такого кода может быть снова кодом, и тогда подразуме
вается, что после выполнения предыдущей строки в консоль ничего не выводится, а
может быть и результатом выполнения, выводимым в
REPL.
При запуске таких
тестов происходит сравнение указанных результатов выполнения команд с тем, что
будет выведено в действительности. Если эти результаты не совпадают, тест будет
считаться проваленным.
Глава
24.
Тестирование приложений
461
Чтобы запустить тесты из строк документации, нужно выполнить команду:
python -m doctest triangle.py
Если всё сделано правильно, то эта команда завершится, не выведя ничего.
Чтобы убедиться, что она действительно нашла тесты и что-то выполнила, тестиро
вание можно запустить в «многословном» режиме, добавив параметр
-v:
> python -m doctest triangle.py -v
Trying:
triangle = Triangle ( (0, О), (0, l), (2, 0))
Expecting nothing
ok
Trying:
triangle[D]
Expecting:
(0,
О)
ok
Trying:
triangle[l]
Expecting:
(О,
1)
ok
Trying:
triangle[2]
Expecting:
(2,
О)
ok
4 items had no tests:
triangle
triangle.Triangle._getitem_
triangle.Triangle. init
triangle.Triangle.side_len
1 items passed all tests:
4 tests in triangle.Triangle
4 tests in 5 items.
4 passed and О failed.
Test passed.
Здесь для каждой строки кода выводится сам код, ожидаемое значение (или факт
его отсутствия) и результат тестирования. В завершающей части вывода отобража
ется статистика, сообщающая о том, что отсутствуют тесты для всего модуля
triangle в целом, а также указываются методы без тестов. Приводится и статисти
ка с цифрами:
сколько сущностей (функций, классов,
модулей) имеют тесты,
сколько не имеют, сколько тестов было запущено, сколько из них выполнились
удачно, а сколько нет. И итоговый результат: тest
passed -
всё хорошо.
Часть
462
11.
Основные подходы
Чтобы увидеть, что будет, если тест не выполнится, добавим еще один метод с именем
area (), рассчитывающий площадь треугольника, «случайно» допустив ошибку в
наборе формулы для расчета (листинг
Листинг
24.13).
24.13. Chapter_24/example_11/triangle.py
class Triangle:
def area(self) -> float:
"""Расчет площади треугольника.
Этот метод содержит
ошибку.
»> triangle ~ Triangle ( (0,
>>> triangle.area()
1.0
О),
(0, 1), (2,
О))
xl, yl
self. vertices[O]
self. _vertices[l]
хЗ, уЗ
self. - vertices[2]
return 0.5 * abs(xl * (у2 - уЗ) +
х2 * (уЗ - yl) +
хЗ * (yl - уЗ))
х2,
у2
Если запустить тесты для такого класса, то результат будет выглядеть следующим
образом:
> python -m doctest triangle.py
**********************************************************************
File " ... /triangle.py", line 38, in triangle.Triangle.area
Failed example:
triangle. area ()
Expected:
1.0
Got:
о.о
**********************************************************************
l items had failures:
1 of
2 in triangle.Triangle.area
***Test Failed*** 1 failures.
Ошибка содержится в последней строке. Правильный код приведен в листинге
Листинг
24.14. Chapter_24/example_12/triangle.py
class Triangle:
def area(self) -> float:
24.14.
Глава
24.
Тестирование приложений
return 0.5
*
abs(xl
х2
хЗ
* (у2
463
-
+
уЗ)
* (уЗ - yl) +
* (yl - у2))
После исправления этой ошибки запуск тестов без параметра -v снова ничего не
будет выводить в качестве результата работы.
А как быть с тестированием исключений? Чтобы подготовить тесты, проще всего
сначала выполнить код, вызывающий исключения, в интерактивном режиме, а за
тем в сокращенном виде вставить результат выполнения в тест . Что именно важно
оставить, а что можно пропустить, сейчас увидим. Запустим интерпретатор
Python
в том каталоге, где расположен файл triangle.py, и выполним следующий код:
>>> from triangle import Triangle
»> triangle = Triangle ((О, О) , (О, 1) , (2,
>>> triangle.side_len(2, 3)
TraceЬack (most recent call last):
О))
File "<stdin>", line l, in <module>
File " ... /triangle.py", line 27, in side len
raise VаluеЕrrоr("Неправильные индек сы вершин")
ValueError: Неnрави.пьНi!е индехсы вершин
Строки, которые должны попасть в тесты, выделены здесь полужирным шрифтом.
Нужно сохранить строку Traceback
(most recent call lastl :, а также последнюю
строку с описанием исключения. Строки между ними можно пропустить ~ обычно
вместо
них
ставят многоточие.
В результате строки документации для метода
side_len () могут выглядеть так (листинг 24.15) .
Листинг
24.15. Chapter_24/example_13/triangle.py
class Triangle:
def side_ le n(self, indexl: int, index2: int) - > float:
"""Р асче т
длины стороны по
номера м вершин.
»>triangle=Triangle((0, 0), (0, 1), (2, 0))
>>> triangle.side_len(2, 3)
TraceЬack (most recent call last):
ValueError:
НеnравиnьНilе индехсы вершин
if indexl < О or indexl > 2 or index2 < О or i nde x2 > 2 :
raise VаluеЕrrоr("Неправильные индексы вершин")
pl = se lf._vertice s[index l]
р2 = se l f. _vertices[index2]
re tu rn math.hypot(pl [O] - р2[0 ]
,
pl[l ] -
р2 [ 1 ) )
464
Часть
11.
Основные подходы
Если теперь запустить выполнение тестов в «многословном» режиме, можно будет
увидеть следующие строки:
> python -m doctest triangle.py -v
Trying:
triangle.side len(2, 3)
Expecting:
Traceback (most recent call last):
ValueError:
Неправильные индексы вершин
ok
3 items had no tests:
triangle
triangle.Triangle._getitem_
triangle.Triangle. init
3 items passed all tests:
4 tests in triangle.Triangle
2 tests in triangle.Triangle.side len
2 tests in triangle.Triangle.area
8 tests in 6 items.
8 passed and О failed.
Test passed.
Все тесты пройдены успешно.
Заключение
В этой главе мы рассмотрели основы тестирования приложений на
Python.
Сначала
мы обсудили, какие виды тестов существуют (модульные, интеграционные, сис
темные и приемочные), а в дальнейшем сосредоточились на модульных тестах.
Мы научились создавать тесты с помощью стандартного модуля
ли основные методы
assert * ()
из класса
TestCase,
uni t test и приве
проверяющие различные типы
условий. Обсудили тему инициализации и очистки данных для тестов с помощью
методов
setUpClass (), setUp (), tearDown (), tearDownClass ().
Разобрались со способами запуска всех тестов, а также отдельных наборов тестов.
В последнем разделе главы научились писать в строках документации тесты, кото
рые запускаются с помощью стандартного модуля
Тестирование
-
ctoctest.
это обширная тема, и много интересных вопросов осталось за рам
ками этой главы. Отдельно хочется упомянуть библиотеку
1
См. https://docs.pytest.org.
Pytest 1,
часто исполь-
Глава
24.
465
Тестирование приложений
зуемую вместо стандартного модуля unittest.
Pytest
предлагает несколько иную
идеологию организации тестов и подготовки данных для них. Так, вместо множест
ва методов
При этом
assert * ()
Pytest
для проверок используется стандартная инструкция
assert.
умеет запускать тесты, написанные с использованием модуля
uni ttest, что позволяет постепенно переводить тестирование на
сывая все ранее написанные с использованием модуля
uni ttest
Pytest,
не перепи
тесты.
На этом мы завершаем изучение материала, касающегося стандартной библиотеки
Python,
и переходим к знакомству со сторонними библиотеками, используемыми в
научных вычислениях. В следующей главе мы рассмотрим библиотеку
NumPy,
ко
торая де-факто является стандартом при написании скриптов, занимающихся вы
числениями.
- ЧАСТЬ 111-
PYTHON
ДЛЯ НАУЧНЫХ ВЫЧИСЛЕНИЙ
- ГЛАВА 25-
МаССИВЫ из библиотеки
До сих пор мы изучали базовый синтаксис
NumPy
Python
и при написании скриптов ис
пользовали только стандартную библиотеку. Однако одно из преимуществ экоси
стемы
Python
заключается в наличии огромного количества сторонних библиотек
практически на все случаи жизни. Одной из важнейших библиотек, которая позво
лила
Python
занять нишу в области вычислений, несмотря на невысокую скорость
интерпретатора, является
NumPy.
На основе
NumPy
работает множество других
библиотек для математики, обработки данных, статистики, работы с изображения
ми, искусственного интеллекта и других областей. Благодаря тому, что «под капо
том»
NumPy
написана на компилируемых языках
Fortran
и С, эта библиотека по
зволяет повысить скорость выполнения расчетов на порядок, что сделало
де-факто стандартом при использовании
Python
NumPy
для вычислительных задач. В ос
тавшихся главах книги мы в той или иной мере будем обращаться к
NumPy
или в
явном виде, или как к необходимой зависимости для других библиотек.
Устанавливается
NumPy
стандартным образом с помощью
pip:
python -m pip install --user numpy
Приступим к изучению этой замечательной библиотеки.
Массивы
NumPy
Класс массивов является одним из наиболее важных объектов, который предостав
ляет
библиотеки.
В
два важных
NurnPy, и поверх которого реализованы все остальные возможности
главе 4 мы разбирались с отличием массивов от списков. Напомним
свойства массивов, благодаря которым при их использовании можно добиться
большей производительности и меньшего потребления оперативной памяти:
♦
массивы хранят только однородные данные, то есть все их элементы имеют
один и тот же тип;
♦
элементы массива располагаются в оперативной памяти последовательно без
промежутков.
Этими же свойствами обладают и массивы из стандартного модуля array, однако
массивы
NumPy
обладают еще одним свойством, благодаря которому код, напи
санный с их использованием, не только быстрый, но еще и намного более компакт-
Часть
470
111. Python для
научных вычислений
ный, а значит, проще и легче читаемый. Это векторизация, то есть применение не
которых операций и функций сразу ко всему массиву или выбранному интервалу
элементов. Благодаря векторизации мы часто можем избавиться в коде от циклов,
которые негативно сказываются на производительности.
Чтобы понять отличие в использовании стандартных массивов от массивов
NumPy,
напишем небольшой скрипт, который рассчитывает значение некоторой функции
на интервале значений переменной х:
у = sin ( х) · cos ( Зх + 7t / 3)
Без использования
Листинг
NumPy
код может выглядеть следующим образом (листинг
25.1 ).
25.1. Chapter_25/example_01/std_array_func.py
import array
import math
min
max
count
х
х
-4.0
4.0
21
dx
= (x_max - x_min) / (count - 1)
all
array.array("d")
y_all = array.array("d")
х
for n in range(count):
х = х min + dx * n
у= math.sin(x) * math.cos(З *
x_ all. append (х)
y_all. append (у)
х
+ math.pi / 3)
print(x_all)
print (y_all)
В этом примере мы сначала задаем минимальное и максимальное значение х (пере
менные х _ min, х _ max), а также количество точек на этом интервале. Исходя из ука
занных данных, рассчитываем шаг изменения по х (переменная dx). После этого
создаем два пустых массива: x_all и y_all, которые затем будем заполнять дан
ными.
В цикле последовательно рассчитываем очередное значение х и соответствующее
ему текущее значение функции у и добавляем их в массивы x_all и y_all соответ
ственно. И хотя мы здесь применили метод append () , желательно избегать его ис
пользования в массиве, поскольку этот метод может приводить к перераспределе
нию памяти при добавлении новых элементов.
Заполненные таким образом массивы х _ а 11 и у_ а 11 можно в дальнейшем исполь
зовать, например, для построения графика функции с помощью библиотеки
Matplotlib,
Глава
25. Массивы из библиотеки NumPy
о которой мы будем говорить в главе
471
27.
В нашем примере эти значения выводятся
в консоль.
Если же мы воспользуемся библиотекой
деть так (листинг
Листинг 25.2.
NumPy,
то аналогичный код будет выгля
25.2).
Chapter_25/example_02/numpy_array_func.py
import numpy as np
xmin = -4.0
xmax = 4.0
count = 21
х = np.linspace(xmin, xmax, count)
у= np.sin(x) * np.cos(З * х + np.pi / 3)
print ( f" {type (х) =) ")
print(f"{type(y)=)")
print{x)
print (у)
Как можно видеть, расчетная часть этого скрипта уместилась в две строчки вместо
восьми, как в листинге
25.1.
В начале скрипта мы импортируем модуль
numpy и даем ему псевдоним np. Такой
псевдоним является общепринятым для библиотеки
NumPy.
Затем используем функцию linspace (), которая возвращает массив, заполненный
числами в интервале от заданного минимального до заданного максимального зна
чения. При этом шаг будет рассчитан автоматически таким образом, чтобы в мас
сиве содержалось заданное количество элементов. По умолчанию правый конец
интервала включается в массив. Функция
linspace () имеет еще несколько допол
нительных параметров, о которых мы поговорим в следующем разделе.
Функция linspace () возвращает объект типа numpy. ndarray
(N-dimensional array,
N-мерный массив)- в общем случае это многомерный массив, но в нашем приме
ре он одномерный. Это и есть тот самый массив из
NumPy,
которому посвящена эта
глава.
Благодаря векторизации, нам не нужно писать цикл для расчета каждого отдельно
го значения функции. Используя векторные функции из модуля numpy -
такие как
sin (), cos () и другие функции и операторы- мы можем работать с массивами как
с единым целым.
Пример, показанный в листинге
25.2,
использует несколько векторных операций:
умножение массива на число, добавление к массиву числа, а также векторизован
sin () и cos (). В рассматриваемом случае функции sin () и
cos () из модуля math нам не подойдут - они могут работать только с одним зна
чением, а не массивом. Значение pi мы тоже берем из модуля numpy, в результате
чего необходимость в стандартном модуле math в нашем примере отпадает.
ные версии функций
Часть
472
111. Python
для научных вычислений
Результат выполнения этого примера выглядит следующим образом:
type(x)=<class 'numpy.ndarray'>
type(y)=<class 'numpy.ndarray'>
[-4. -3.6 -3.2 -2.8 -2.4 -2. -1.6 -1.2 -0.8 -0.4 о.
0.4 0.8 1.2
1.6 2.
2.4 2.8 3.2 3.6 4. ]
[-0.03235997 -0.41892554 -0.03755298 -0.16094842 -0.66972998 -0.21650756
0.81860517 0.77509596 -0.15514362 -0.384881
-0.24377224
о.
-0.68411749 -0.06071793 0.90606684 0.65657281 -0.25881106 -0.33490396
0.01992733 -0.33293058 -0.67099075]
Способы создания массивов
NumPy
содержит множество функций, предназначенных для создания массивов с од
новременным заполнением их значениями. Вот краткий перечень некоторых из них:
♦
создание массива на основе других данных (списка или другого ите-
array () -
рируемого объекта);
♦
empty () -
♦
zeros () -
♦
ones () -
создание массива заданного размера и заполнение его единицами;
full () -
создание массива заданного размера и заполнение его указанным зна
♦
создание массива заданного размера без инициализации элементов;
создание массива заданного размера и заполнение его нулями;
чением;
♦ arange () -
создание массива и заполнение его последовательностью чисел.
В качестве входных параметров используются минимальное и максимальное
значения, а также шаг между элементами;
♦
linspace () -
создание массива и заполнение его последовательностью чисел.
В качестве входных параметров используются минимальное и максимальное
значения, а также количество элементов в создаваемом массиве.
Рассмотрим эти функции более подробно, и начнем с функции empty (), которая
создает массив, но не инициализирует элементы никакими значениями, из-за чего
после создания он содержит мусорные значения, зато создание его происходит не
много быстрее. Объявление функции empty () выглядит следующим образом:
numpy.empty(shape, dtype=float, order='C', *, device=None, like=None)
Обратите внимание, что здесь используется разделительный параметр«*», который
обозначает, что параметры правее него мы обязаны передавать только как имено
ванные (такие разделительные параметры мы обсуждали в главе
ции из библиотеки
NumPy
10).
Многие функ
используют разделительные параметры.
Последние два параметра являются слишком низкоуровневыми для нашей книги,
поэтому мы их рассматривать не станем, а вот про первые три параметра нужно
поговорить, поскольку они будут нам встречаться также в других функциях:
♦
shape -
определяет размерность создаваемого массива. Значение этого пара
метра может быть целым числом или кортежем (или другой последовательно
стью) целых чисел.
Глава
25.
Массивы из библиотеки
473
NumPy
Если значение параметра shape -
число, то создается одномерный массив с ко
личеством элементов, равным этому значению. Если передается кортеж ·чисел,
то размерность создаваемого массива будет соответствовать количеству элемен
тов кортежа, и по каждому направлению количество элементов будет равно со
ответствующему значению из кортежа. Для двумерного случая это количество
строк и столбцов;
♦
определяет тип элементов массива.
dtype -
NumPy
предоставляет множество
типов:
•
для знаковых целых чисел: intB (он же byte), intl6 (short), int32 (intc),
int64, int_ (занимает
- long);
32
бита на 32-битных системах и
64
бита на 64-битных,
псевдоним
•
для беззнаковых целых чисел: uint8 (ubyte), uintl6 (ushort), uint32 (uintc),
uint64, uint;
•
для чисел с плавающей точкой: floatl6 (half), float32 (single), float64
(douЫe), float96, floatl28;
•
для комплексных чисел: complex64 (csingle), complexl28 (cdouЫe), com-
plexl92, complex256;
♦
строковый параметр, который используется для многомерных массивов
order -
и определяет, каким образом многомерный массив располагается в оперативной
памяти, являющейся линейной. Значение по умолчанию «с» обозначает по
строчное хранение, как это принято в языке С. Другое возможное значение па
раметра
-
«F» -
обозначает хранение по столбцам, как это принято в языке
Fortran.
Аналогичный набор параметров имеют функции zeros ()
и ones (). У функции
full () параметры такие же, но, кроме них, у нее имеется второй обязательный па
раметр, который показывает, каким значением нужно заполнить созданный массив.
Рассмотрим далее несколько примеров использования этих функций с разными
комбинациями параметров. Вот первый из них:
>>> import numpy as пр
»> np.empty( (2, 3), dtype=np.intlб)
array( [ [-18040, 23401, 21911],
О,
О,
О]], dtype=intlб)
[
При передаче в качестве первого параметра кортежа с двумя элементами, значение
первого элемента кортежа обозначает количество строк, а второго
-
количество
столбцов. Таким образом, мы создали здесь массив, состоящих из двух строк и трех
столбцов. Поскольку при выполнении функции empty () элементы не инициализи
руются никакими значениями, сразу после создания массива они содержат мусор.
Теперь создадим массив, заполненный нулями:
»> np.zeros((З, 4))
array( [[О., о., о.,
о.],
[о.,
о.,
о.,
о.],
[О.,
о.,
о.,
о.]])
Часть
474
111. Python для
научных вычислений
Если мы не указываем тип элементов массива, то по умолчанию будет использо
ваться
numpy. float64.
Теперь создадим массив, все элементы которого будут равны заданному значению
(в показанном случае
>>> np.full( (3, 5),
array( [ [3.14159265,
[3.14159265,
[3.14159265,
-
числу
1t):
np.pi, dtype=np.float64)
3.14159265, 3.14159265, 3.14159265, 3.14159265),
3.14159265, 3.14159265, 3.14159265, 3.14159265),
3.14159265, 3.14159265, 3.14159265, 3.14159265)))
Если же в качестве первого параметра указано целое число, то будет создан одно
мерный вектор:
>>> np.zeros(5, dtype=np.cdouЫe)
array ( [О. +О. j, О. +О. j, О. +О. j, О. +О. j,
О. +О.
j))
Поскольку элементы массива не могут менять свой тип в процессе работы скрипта,
то если подразумевается, что какие-то элементы могут быть комплексными, надо
не забывать сразу объявлять весь массив как содержащий комплексные числа.
Еще одной важной функцией для создания массивов является функция array ().
Она имеет достаточно много параметров, которые можно использовать для опти
мизации, но они нам сейчас не важны. Главное, что она принимает в качестве пер
вого параметра итерируемый объект, на основе которого создается массив. В каче
стве второго параметра можно указать тип элементов, иначе функция попытается
его определить сама:
»> np.array( [1, 2, 3, 4, 5))
array ( [1, 2, 3, 4, 5])
Для создания двумерного (или еще более многомерного) массива можно использо
вать вложенные списки:
»> np.array([[l, 2, 3), [4, 5, 6]), dtype=np.float64)
array([[l., 2., 3.],
[ 4., 5. 1 6.]])
В следующем примере показано использование именованного параметра ndmin,
который задает минимальную размерность массива. И хотя данные мы передаем
изначально одномерные, но массив создается двумерный, при этом по второму из
мерению он будет содержать только один элемент:
>>> np.array([l, 2, 3, 4, 5.1), ndmin=2)
array([[l., 2., 3., 4., 5.1)))
Обратите внимание, что тип всех элементов массива
-
float, поскольку в пере
данном списке имеется одно число с плавающей точкой. Аналогично, если бы спи
сок содержал хотя бы одно комплексное число, все элементы массива были бы соз
даны комплексными:
>» np.array ( [1, 2, 3, 4, 5+3j))
array([l.+O.j, 2.+0.j, 3.+0.j, 4.+0.j, 5.+3.j])
Глава
25.
Массивы из библиотеки
475
NumPy
Мы часто использовали стандартную функцию range (), с помощью которой можно
создавать итерируемые объекты с последовательностями целых чисел. В
NumPy
имеется похожая функция arange (), которая возвращает массив, заполненный по
следовательными числами с заданным шагом, но при этом она работает и с числа
ми с плавающей точкой. Рассмотрим примеры использования функции arange ():
>>> np.arange(l0)
array( [О, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Если передан только один параметр, то подразумевается, что первый элемент равен О,
а шаг равен
1:
>>> np.arange(l0.0, 20.0)
array([l0., 11., 12., 13., 14., 15., 16., 17., 18., 19.])
Так же, как и с функцией range (), правая граница не включается в создаваемый
массив. Шаг между элементами, разумеется, тоже можно передавать:
>>> np.arange(5, 10, 0.5)
array([5., 5.5, 6., 6.5, 7., 7.5, 8., 8.5, 9., 9.5])
Функция arange ( ) также пытается определить подходящий тип элементов, но при
необходимости его можно указать явно с помощью именованного параметра dtype,
как мы это делали ранее для других функций.
И завершим мы этот раздел функцией, с которой начали изучение
NumPy, -
linspace (), которая напоминает функцию arange (), но вместо шага принимает
количество элементов в создаваемом массиве, причем правый конец заданного ин
тервала по умолчанию включается в массив. Впрочем, как мы сейчас увидим, это
поведение настраивается. Описание функции
linspace () выглядит следующим
образом:
numpy.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, axis=0,
*, device=None)
Параметр device, как и для предыдущих функций, мы проигнорируем, а что обо
значают остальные параметры, поясним:
♦
start -
начальная точка для создания последовательности. Может быть числом
или последовательностью, тогда linspace () создаст многомерный массив;
♦
stop -
конечная точка последовательности. Может быть числом или последо
вательностью;
♦
num -
количество элементов в создаваемой последовательности;
♦
endpoint -
определяет, нужно ли включать в создаваемую последовательность
конечную точку;
♦
retstep (сокращение от англ.
retum step -
«вернуть шаг»). Если этот параметр
равен тrue, то, вместо массива, функция вернет кортеж из двух элементов: соз
данный массив и рассчитанный шаг между соседними элементами;
♦
dtype -
определяет тип элементов. Работает аналогично одноименному пара
метру в рассматриваемых ранее функциях;
Часть
476
♦
axis -
111. Python
для научных вычислений
используется, если start и (или) stop являются последовательностями.
Этот параметр определяет, вдоль какой оси будет создаваться последователь
ность: вдоль строк или столбцов (для двумерного массива).
Рассмотрим далее примеры использования этой функции.
Если не указан шаг, то по умолчанию создается массив из
>>> np.linspace(0, 9.8)
array([0. 1 0.2, 0.4, О. 6,
2. 6, 2. 8, 3. 1 3.2,
5.2, 5. 4, 5. 6, 5. 8,
7.8, 8. 1 8.2, 8.4,
0.8,
3.4,
6. 1
8.6,
1. 1
3. 6,
6.2,
8.8,
1.2,
3. 8,
6.4,
9. 1
1.4,
4. 1
6. 6,
9.2,
1. 6,
4. 2,
6.8,
9.4,
1.8,
4.4,
7. 1
9. 6,
50 элементов:
2. 1 2.2, 2.4,
4. 6, 4.8, 5. 1
7.2, 7.4, 7. 6,
9. 8])
Наиболее часто используемый способ вызова функции linspace () даем
первую
и
последнюю
точки,
а также
количество
элементов
когда мы за
в
создаваемом
массиве:
>>> np.linspace(0, 0.9, 10)
array([0., 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Если параметр endpoint равен False, то конечная точка не включается в последо
вательность, как при использовании функции arange () :
>>> np.linspace(0, 0.9, 10, endpoint=False)
array([0. , 0.09, 0.18, 0.27, 0.36, 0.45, 0.54, 0.63, 0.72, 0.81])
Иногда требуется узнать, какой получился шаг между элементами при заданном
количестве элементов массива. Конечно, его можно рассчитать, исходя из разности
между соседними элементами, но удобно, когда функция сразу вернет это значе
ние. Для этого параметр retstep должен быть равен True:
>>> foo, step = np.linspace(l0, 19, 10, retstep=Тrue)
>>> foo
array([l0., 11., 12., 13., 14., 15., 16., 17., 18., 19.])
>>> step
np. float64 (1. О)
Теперь рассмотрим примеры, когда в качестве первых двух параметров передаются
последовательности:
»> np.linspace([0, 10, 20], [9, 19, 29], 5)
array([[ О. ,
[ 2.25,
4.5 1
6. 75,
9. 1
10. ,
12.25,
14.5 1
16. 75,
19. 1
20. ],
22.25],
24.5 ],
26. 75] 1
29. ]])
Когда в качестве начальной и конечной точки мы передаем, например, список или
массив
(они
должны иметь одинаковый размер), то по умолчанию создается дву
мерный массив, первая строка которого совпадает с параметром start, последняя
строка определяется параметром stop, а по второму направлению (по умолча
нию
-
это размер столбцов) массив имеет заданное с помощью параметра num ко
личество элементов.
Глава
25.
Массивы из библиотеки
477
NumPy
С помощью параметра axis можно изменить направление создания последователь
ностей. Таким путем мы получим транспонированную матрицу относительно пре
дыдущего примера:
>» np. linspace ( [О, 10, 20), [9, 19, 29), 5, axis=l)
array( [ [ о.
2.25, 4.5 , 6.75, 9. ) ,
[10. , 12 .25, 14.5 , 16.75, 19. ) ,
[20.
, 22.25, 24.5, 26.75, 29.
Если значение параметра start -
)))
это число, а значение параметра stop -
после
довательность, это означает, что начальное значение у всех создаваемых последо
вательностей будет одинаковым, равным значению start:
>>> np.linspace(0, [ 9, 19, 29], 5)
array([[ о.
),
о.
о.
4.75, 7 .25),
9.5, 14. 5 ] ,
6.75, 14.25, 21. 75),
9. , 19. , 29. ) ] )
[ 2.25,
[ 4. 5 ,
Аналогично функция работает в случае, когда параметр start тельность, а stop -
это последова
число. Тогда последняя точка последовательностей будет сов
падать.
Помимо функции linspace (), создающей последовательность с равномерным ша
гом, в библиотеке
NumPy
имеется также функция logspace (), которая работает
аналогично, но создает последовательность с логарифмическим шагом. Мы ее рас
сматривать не будем, но полезно иметь в виду, что она существует.
Основные операции над массивами
Мы научились несколькими способами массивы ndarray создавать. Теперь по
смотрим, какие мы можем выполнять с ними операции. В начале главы мы уже ви
дели, что к массивам благодаря векторизации можно применять математические
операции и функции из модуля numpy. Многие функции, которые мы начнем сейчас
рассматривать, существуют в двух вариантах: в виде методов класса
ndarray
и в
виде отдельных функций в модуле numpy. Они делают одно и то же, и в каком виде
их использовать, зависит исключительно от ваших предпочтений. Начнем с про
стых функций.
Транспонирование массива
-
перемена местами строк и столбцов. Эта операция
часто используется не только в матричных вычислениях, но, например, и при запи
си данных в текстовый файл в виде столбцов (про такие файлы мы поговорим в
главе
26).
Для транспонирования можно использовать функцию transpose (), од
ноименный метод из класса ndarray или воспользоваться свойством массивов т.
Забегая вперед, хочу предупредить, что с транспонированием нужно работать осто
рожно, поскольку все эти функции возвращают не копию массива, а так называе
мый «вид» (мы рассмотрим виды в следующем разделе),
-
а это значит, что при
Часть
478
111. Python
для научных вычислений
изменении элементов транспонированного массива будут изменяться элементы ис
ходного массива. В остальном транспонирование работает очевидным образом:
>» foo = np.array([[l, 2, 3, 4, 5], [6, 7, 8, 9, 10]])
>>> foo.transpose()
array([[ 1, 6],
[ 2, 7],
3, 8],
4, 9],
5, 10]])
Аналогичный результат получится, если выполнить foo. т или np. transpose ( foo).
С одномерными массивами немного сложнее. Если мы попытаемся транспонировать такой массив, то он не изменится:
>» bar = np.array( [1, 2, 3, 4, 5])
>>> bar
array([l, 2, 3, 4, 5])
>>> bar.T
array ( [1, 2, 3, 4, 5])
Если нужно, чтобы одномерный массив действительно транспонировался, то его
надо изначально сделать двумерным
-
с единственным значением по второму на
правлению. Как мы говорили ранее, для этого можно воспользоваться параметром
ndmin В функции array ():
>>> bar = np.array([l, 2, 3, 4, 5], ndmin=2)
>» bar
array([[l, 2, 3, 4, 5)))
>>> bar.T
array([[l],
[2],
[3],
[ 4],
[5]])
Если массив у нас должен быть обязательно одномерным, мы можем на его основе
создать двумерный массив с помощью функции atleast_2d (), и уже полученный
таким образом массив транспонировать:
»> bar = np.array( [1, 2, 3, 4, 5])
>>> np.atleast_2d(bar).transpose()
array([[l],
[2],
[3],
[ 4],
[5]])
Если массив содержит комплексные числа, то для создания матрицы с комплексно
сопряженными элементами есть целых четыре эквивалентных способа: функции
conj ()
и
conjugate (),
а также два одноименных метода в классе
ndarray.
Глава
25.
Массивы из библиотеки
479
NumPy
Все они делают одно и то же:
»> baz = np.array([[l+2j, 3+4j], [5+6j, 7+8j]])
»> baz. conj ()
array([[l.-2.j, 3.-4.J],
(5.-6.j, 7 .-8.j]])
Следующая группа функций, которую мы рассмотрим, работает примерно одина
ковым образом:
♦
sum () / prod () -расчет поэлементной суммы/ произведения элементов массива;
♦
min () / max () -
нахождение в массиве минимального
/
максимального значе
ния;
♦
argmin () / argmax () -
нахождение в массиве номера минимального
/
макси-
мального элемента;
♦
нахождение в массиве среднего значения.
mean () -
У этих функций много общего, начиная с того, что их можно вызывать как функ
ции из модуля numpy или как одноименные методы класса ndarray. По умолчанию
эти функции работают целиком со всем многомерным массивом, то есть результа
том их работы будет число, однако им можно передать дополнительный параметр
axis, который задает ось, вдоль которой нужно выполнять соответствующие дейст
вия, и тогда эти функции вернут массив со значениями, рассчитанными вдоль ука
занной оси. Например, если у нас двумерный массив, то при значении параметра
axis=0 соответствующие значения будут рассчитываться по столбцам, и количест
во элементов в итоговом массиве будет равно количеству элементов в строках. Ес
ли же axis=l, то наоборот, количество элементов в итоговом массиве будет равно
количеству строк. Покажем это на примерах:
>» foo = np.array([[l, 2, 3, 4, 5], (4,
>» print(foo)
([ 1
2
(4
3
4
5]
0-2
9
5]]
>>> print(foo.sum())
31
>>> print(foo.sum(axis=0))
[ 5
2 1 13 10]
>>> print(foo.sum(axis=l))
(15 16]
>>> print(foo.min())
-2
>>> print(foo.min(axis=0))
[ 1
О
-2
4
5]
>>> print(foo.min(axis=l))
[ 1 -2]
О,
-2, 9, 5]])
Часть
480
111. Python
для научных вычислений
>>> print(foo.argmin())
7
>>> print(foo.argmin(axis=0))
[О
О
1 1
О]
>>> print(foo.argmin(axis=l))
[О
2]
>>> print(foo.mean())
3.1
>>> print(foo.mean(axis=0))
[2.5 1.
0.5 6.5 5. ]
>>> print(foo.mean(axis=l))
[3.
3.2]
Раз уж мы упомянули функции для нахождения минимального и максимального
значения, то надо сказать еще про две похожие функции из модуля numpy -
это
функции fmin () и fmax () . Эти функции принимают в качестве параметров два мас
сива одинакового размера и возвращают массив такого же размера, заполненный
результатом поэлементного сравнения двух массивов. То есть в соответствующий
элемент результирующего массива попадет то значение из двух массивов, которое
окажется меньше или больше,
-
в зависимости от используемой функции:
»> foo = np.array( [1, 2, 3, 4, 5])
>>> bar = np.array([4,
>>> np.fmin(foo, bar)
array( [ 1, О, -2, 4,
>>> np.fmax(foo, bar)
array([4, 2, 3, 9, 5])
О,
-2, 9, 5])
5])
Индексация, срезы и виды
Доступ к элементам массива, на первый взгляд, осуществляется практически так
же, как и к элементам списков, с поправкой на то, что список
-
это одномерный
контейнер (хотя может содержать другие списки), а массив может быть много
мерным. Но если копнуть глубже, то мы увидим, что различия достаточно су
щественные.
Создадим два массива: одномерный и двумерный, на которых далее будем экспе
риментировать:
>>> foo = np.array([l, 2, 3, 4, 5, 6, 7, 8])
»> bar = np.array([[l, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
>» print(foo)
[1 2 3 4 5 6 7 8]
»> print (bar)
[[ 1
2
3
4]
5
6
7
8]
9 10 11 12]]
Глава
25.
Массивы из библиотеки
NumPy
481
Для начала получим значение одного элемента:
>>> print(foo[S])
6
>>> print(bar[2, 1])
10
Здесь особенность проявляется в многомерном массиве
-
индексы по двум осям
указываются в общих квадратных скобках и разделяются запятой. Если бы у нас
был более многомерный массив, то мы должны были бы указать больше индек
сов,
-
например,
spam [ 2, 3, 4 J.
В индексации можно использовать символ «: » для получения среза
нительно к спискам мы подробно обсудили в главе
(срезы
приме
4):
>>> print(foo[2:5])
[ 3 4 5]
>>> print(foo[::2])
[1 3 5 7]
С многомерными массивами интереснее. Если мы хотим выделить срез только по
одному направлению, то для того, чтобы указать, что по другому направлению нам
нужны все элементы, используется символ
«: »:
»> print(bar[l:3, :])
[[ 5
6
7
В]
[9101112]]
>>> print(bar[:, 2:4])
[[ 3
4)
[ 7
В]
[11 12]]
Выделение блока элементов сразу по нескольким осям выполняется так:
>>> print(bar[l:3, 1:3])
[[ 6
7]
[ 10 11]]
В предыдущих примерах мы только получали значения элементов массива, однако
с помощью срезов мы можем их также изменять:
>>> foo[l:5] = [-10, -20, -30, -40]
»> print(foo)
1 -10 -20 -30 -40
[
6
7
8)
В этом примере важно, чтобы количество выделенных элементов совпадало с коли
чеством элементов в списке (или массиве), который стоит справа от знака равенства.
Это работает и с многомерным массивом:
>» bar[l, :] =
»> print(bar)
[[ 1
[
О
2
3
4]
О
О
О]
9101112)]
[О,
О,
О,
О]
Часть
482
111. Python
для научных вычислений
Однако в здесь, поскольку мы всем элементам присваиваем одно и то же значение,
мы могли бы написать:
>>> bar[l, :] =
О
Такое поведение называется транслированием (от англ.
broadcasting),
о нем мы по
говорим чуть позже в этой главе, а пока просто запомним, что так делать можно.
Более того, мы можем использовать эту возможность для присваивания значений
многомерному блоку элементов (в следующем примере заново создается массив
bar, поскольку в предыдущем примере мы его испортили присваиванием):
»> bar = np.array([[l, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
>>> bar[l:3, 1:3] = О
»> print (bar)
[[ 1
2
3
4]
[ 5
О
О
8]
90012]]
Последние примеры с присваиванием на самом деле намного сложнее и интерес
нее, чем кажутся на первый взгляд. Здесь мы переходим к термину «вид»
(view).
Чтобы лучше понять, что это такое, рассмотрим одно принципиальное отличие
массивов
В главе
4,
NumPy
от списков.
когда речь шла о создании копии списков, был приведен пример, наподо
бие такого:
>>>
>>>
>>>
>>>
foo_list = [1, 2, 3, 4, 5]
bar_list = foo_list[:]
bar_list[O] = 50
print(bar_list)
[50, 2, 3, 4, 5]
>>> print(foo_list)
[1, 2, 3, 4, 5]
Оператор получения среза
[: J
применительно к списку создает его копию, чем
часто пользуются. Однако с массивами
>>>
>>>
>>>
>>>
[50
>>>
[50
ndarray ситуация иная:
foo_array = np.array([l, 2, 3, 4, 5])
bar_array = foo_array[:]
bar_array[O] = 50
print(bar_array)
2
3
4
5]
print(foo_array)
2
3
Оператор
4
[: J
5]
возвращает здесь не копию, а вид, изменения в котором затрагивают
изменения в базовом объекте. Это можно определить по свойству base. Если оно
равно None, значит, это полноценный массив, который хранит данные. Если же зна
чение свойства ьаsе не равно None, значит, массив использует данные из другого
объекта, на который ссылается свойство base:
>>> print(foo_array.base)
None
Глава
25.
Массивы из библиотеки
483
NumPy
>>> print(bar_array.base)
[ 50
5]
4
3
2
>>> bar_array.base is foo array
True
Здесь мы видим, что свойство base в объекте bar array ссылается на foo array.
А что если мы применим оператор
[:
J к bar array?
>>> spam_array = bar_array[:]
>>> spam_array.base is bar_array
False
>>> spam_array.base is foo_array
True
создан
Будет
еще
один
вид,
который
тоже
ссылается
на
исходный
массив
foo_array (не на bar_array). Таким образом, изменения в spam_array тоже будут
влиять на данные исходного массива.
Если же мы хотим сделать копию массива, нужно воспользоваться методом сору () :
>>> baz_array = foo_array.copy()
>>> baz_array[0] = О
>>> print(baz_array)
[02345]
>>> print(foo_array)
[ 50
5]
4
3
2
>>> print(baz_array.base)
None
Библиотека
NumPy
старается создавать виды вместо копий массивов везде, где это
возможно. Например, виды не обязаны ссылаться только на элементы, идущие под
ряд. Если мы выделяем срез с шагом, отличным от
1,
также будет создан вид:
»> foo = np.array([l, 2, 3, 4, 5, 6, 7, 8])
>>> foo_slice = foo[l:6:2]
>>> print(foo_slice)
[2 4 6]
>>> foo_slice[:] =
»> print (foo)
[1 О 3 О 5 О 7 8]
О
Аналогично работает и выделение блокового среза:
>» bar = np.array([[l, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
»> print (bar)
[[ 1
[ 5
2
6
3
7
4]
8]
[9101112]]
>>> bar_slice = bar[l:3, 1:3]
>>> print(bar_slice)
[[ 6
7]
[1011]]
Часть
484
>>> bar slice[:, :]
111. Python
для научных вычислений
о
>» print(bar)
[[ 1
2
3
4]
5
О
О
8]
9
О
О
12]]
Но в документации ко многим функциям указано, что в некоторых случаях функ
ция создаст копию вместо вида. Поэтому нужно понимать, когда будет создан вид,
а когда новый массив.
И еще небольшое дополнение. При индексации массивов
NumPy
вместо инструк
ции «: », который выделяет все элементы по оси, можно использовать объект
« ... »:
ellipsis -
>>> foo = np.array([l, 2, 3, 4, 5])
>>> bar = foo[ ... ]
>>> bar.base is foo
True
»> baz = np.array([[l, 2, 3, 4], (5, 6, 7, 8], [9, 10, 11, 12]])
»> baz[0, ... ]
array([l, 2, 3, 4])
»> baz[ ... , О]
array([l, 5, 9])
Формы массивов
Когда массив тем или иным способом создан,
мы можем узнать его форму
то есть количество измерений (размерность) и количество элементов по
(shape) -
каждой оси. Более того, как мы скоро увидим, форму массива можно менять после
его создания.
Создадим двумерный массив, на котором будем показывать дальнейшие примеры:
>» foo = np.array([[l, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
»> print(foo)
[[ 1
2
3
4]
5 6 7 8]
9101112]]
Узнать размерность массива можно с помощью свойства ndim класса ndarray:
>>> foo.ndim
2
С помощью свойства shape можно узнать форму массива
-
оно вернет кортеж с
количеством элементов по каждой оси:
>>> foo.shape
(3,
4)
Свойство size возвращает общее количество элементов в массиве:
>>> foo.size
12
Глава
25.
Массивы из библиотеки
485
NumPy
Также существует функция size () в модуле numpy -
она позволяет узнать общее
количество элементов (как свойство size) или количество элементов по каждой оси
массива:
>>> np.size(foo)
12
>>> np.size(foo, axis=0)
3
>>> np.size(foo, axis=l)
4
Важной особенностью массивов
NumPy
является возможность изменять форму по
сле создания. Для изменения формы массива предусмотрено несколько способов:
с сохранением общего количества элементов и с изменением их количества. Снача
ла рассмотрим функции, изменяющие форму массива с сохранением количества
элементов. Для этого предназначена функция numpy. reshape () :
numpy.reshape(a, /, shape=None, order="C", *, newshape=None, copy=None)
Параметры этой функции обозначают следующее:
♦
а-
массив, на основе которого будет создаваться новый массив, форма исход
ного массива не изменится;
♦
shape -
кортеж из целых чисел, определяющий форму нового массива. Может
быть и целым числом, если нужно создать одномерный массив;
♦
order -
аналог одноименного параметра в функции array (), определяющий
порядок расположения элементов в памяти: в стиле С
"с"), в стиле
элементов
♦
-
newshape -
Fortran
(значение
сору -
по строкам (значение
столбцам, сохранить исходный порядок
значение "д";
устаревший параметр, использовать его не рекомендуется. Ему на
замену пришел параметр
♦
"F")- по
-
shape;
параметр, определяющий, должен ли новый массив содержать копию
данных или будет создан вид на основе исходных данных. Если значение этого
параметра равно тrue, то всегда станет создаваться копия, если
False -
копия
никогда не будет создаваться, но если вид нельзя создать (в случае, если изменя
ется порядок хранения данных, который задается параметром order), то про
изойдет возбуждение исключения ValueError. Если же параметр сору равен
None, будет создан вид, если это позволяет значение параметра order, и копия
в противном случае.
Класс ndarray содержит метод reshape () с теми же параметрами.
Давайте посмотрим, как работает функция numpy. reshape ():
>» foo = np.array([[l, 2, 3, 4], (5, 6, 7,
»> print(foo)
[[ 1
2 3 4]
[ 5 6 7 В]
9101112]]
В],
[9, 10, 11, 12]])
-
Часть
486
111. Python
для научных вычислений
>» bar = np.reshape(foo, (2, 6))
»> print (bar)
[[ 1
2
[7
8
3
4 5 6]
9101112]]
>» baz = np.reshape(foo,
»> print (baz)
[[l 9 6 311 8]
[ 5 2 10 7 4 12] ]
(2, 6), order='F')
При использовании функции reshape () важно, чтобы запрашиваемое количество
элементов в новом массиве совпадало с количеством элементов в исходном. Обра
тите внимание на порядок элементов
ле языка С, а во втором случае
-
по умолчанию используется порядок в сти
в стиле
-
Fortran.
Вместо функции np. reshape (), можно было бы использовать одноименный метод:
»> bar
foo.reshape((2, 6))
»> baz = foo.reshape((2, 6), order='F')
При использовании метода reshape ()
как отдельные параметры
-
размеры нового массива можно указывать
без объединения их в кортеж:
>>> bar
foo.reshape(2, 6)
>>> baz = foo.reshape(2, 6, order='F')
В нашем случае массив bar -
это вид на исходный массив foo, а массив baz со
держит копию данных, поскольку мы изменили порядок следования элементов (по
умолчанию функция array () создает массив с порядком следования элементов в
стиле языка С). Убедимся в этом:
>>> bar.base is None
False
>>> bar.base is foo
True
>>> baz.base is None
False
>>> baz.base is foo
False
»> bar[0, О] = 100
>>> baz[l, 1] = 200
»> print(foo)
[[100
2
3
4]
[ 5
6
7
8]
9 1 О 11 12] ]
Особенностью реализации функции reshape () является тот факт, что в создавае
мом ею массиве значение свойства base не будет равно None, но, как мы видим, это
свойство (ьаz .base) не указывает на исходный массив (foo). И, как видно из при
мера, изменение элементов ьаr влияет на исходный массив foo, а изменение эле
ментов ьаz -
нет. Если бы нам нужно было, чтобы массив ьаr создавался не как
вид, а как копия данных, мы могли бы добавить параметр copy=True.
Глава
25.
Массивы из библиотеки
487
NumPy
Может возникнуть вопрос, зачем кому-нибудь понадобится изменять форму масси
ва, да еще и с таким не самым очевидным изменением порядка элементов? Часто
эту функцию используют, чтобы «выпрямить» массив в одномерную последова
тельность для последующей обработки или сохранения в файл, или наоборот, когда
из элементов одномерного массива (например, прочитанного из файла) требуется
создать многомерный массив. Так, исходный массив foo мы могли бы создать,
не указывая все его элементы:
>>> foo = np.arange(l, 13)
»> print (foo)
[ [ 1 2 3 4]
[ 5 6 7 8]
9 10 11 12]]
.reshape(З,
4)
Если у нас уже есть многомерный массив, с помощью функции reshape () мы мо
жем на его основе создать вектор-строку:
>>> print(foo.reshape(l, 12))
[[ 1 2 3 4 5 6 7 8 9 10 11 12]]
Если бы нам нужен был вектор-столбец, то мы могли бы написать:
>>> print(foo.reshape(l2, 1))
или
>>> print(foo.reshape(l, 12)
.Т)
Обратите внимание, что хотя все элементы расположены вдоль одного измерения
массива, сам массив двумерный и имеет одну строку. Если нужно создать действи
тельно одномерный массив, то вместо кортежа
(1, 12) можно передать просто число 12:
>>> print(foo.reshape(l2))
[ 1 2 3 4 5 6 7 8 9 10 11 12]
Теперь массив получился одномерным. Поскольку функция reshape () часто ис
пользуется для «выпрямления» массива, и есть вероятность ошибиться с количест
вом элементов, в качестве первого параметра можно передать значение
-1,
и тогда
будет создан одномерный массив с правильным количеством элементов:
>>> print(foo.reshape(-1))
[ 1
2
3
4
5
6
7
8
9 1 О 11 12]
Для «выпрямления» массивов в классе ndarray предусмотрен отдельный метод
Па t ten (), но, в отличие от функции reshape (), он всегда создает копию данных
для нового массива.
Этот метод может принимать единственный необязательный параметр order, ана
логичный одноименному параметру из функции reshape (). Строго говоря, у пара
метра order функции f la t ten () есть еще одно возможное значение, но мы не бу
дем на этом останавливаться.
Часть
488
Рассмотрим пример использования метода
>>>
>»
[ 1
>>>
>»
[ 1
111. Python для научных вычислений
f la t ten () :
bar = foo.flatten()
print(bar)
2 3 4 5 6 7 8 9 1 О 11 12)
baz = foo.flatten(order='F')
print (baz)
5 9 2 6 10 3 7 11 4 8 12]
Иногда массивы «выпрямляют» для того, чтобы последовательно обойти его эле
менты и выполнить над ними какую-либо операцию. Если это наш случай, то нет
надобности создавать новый массив,
-
можно воспользоваться свойством flat,
которое возвращает итерируемый объект по элементам «выпрямляемого» массива.
Для примера найдем в двумерном массиве
(листинг
foo максимальный нечетный элемент
25.3).
Листинг 25.3.
Chapter_25/example_03/flat.py
import numpy as np
foo = np.array([[l, 2, 3, 4), [5, 6, 7, 8], [9, 10, 11, 12]])
result = None
for item in foo.flat:
if item % 2 != О:
if result is None:
result = item
else:
result = max(result, item)
print(result)
В результате будет выведено число
11.
До сих пор мы рассматривали случаи, когда на основе массива создавался новый
массив, содержащий такое же количество элементов, что и исходный. Однако так
же есть возможность изменить не только форму, но и размер массива. Для этого
предназначены метод resize () класса ndarray и одноименная функция из модуля
numpy. Несмотря на одинаковое имя, они работают по-разному:
♦
метод resize () изменяет количество элементов в исходном массиве, не создавая
новый. Если размер массива увеличивается, новые элементы заполняются нуле
выми значениями;
♦
функция numpy. resize () создает новый массив, не изменяя исходный. Данные
при этом копируются в новый массив. Если размер массива увеличивается, но
вые элементы заполняются повторяющимися значениями из исходного массива.
Сначала посмотрим, как работает метод resize (). В качестве единственного обяза
тельного параметра он принимает новый размер массива
-
кортеж из целых чисел,
Глава
Массивы из библиотеки
25.
489
NumPy
но, как и в случаях с методом
reshape (),
вместо кортежа новые размеры можно
передавать в виде отдельных параметров:
>» foo = np.array( [ [1, 2, 3, 4], [5, 6, 7,
>» print(foo)
[[ 1
2
3
4]
[ 5
6
7
В]
В],
[9, 10, 11, 12]])
[9101112]]
>>> foo.resize(2, 6)
>» print(foo)
[ [ 1 2 3 4 5 6]
[ 7 в 9 10 11 12]]
Еше раз обратим внимание, что, в отличие от метода reshape (), метод resize ()
изменяет исходный массив.
Если после вызова метода resize () количество элементов уменьшается, то их зна
чения теряются безвозвратно:
>>> foo.resize(2, 4)
»> print(foo)
[ [1 2 3 4]
[5 6 7
В]]
Если количество элементов увеличивается, то новые элементы будут заполнены
нулями:
>>> foo.resize(2, 6)
»> print(foo)
[ [1 2 3 4 5 6]
[780000]]
А вот как работает функция numpy. resize ():
>» foo = np.array([[l, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
»> print(foo)
[[ 1
2
3
4]
[ 5
6
7
В]
[ 9 10 11 12]]
»> bar = np.resize(foo,
»> print (bar)
[ [l 2 3 4]
[5 6 7
В]]
»> bar[0,
О]
100
»> print (bar)
[[100
2
3
4]
5
6
7
В]]
[
»> print(foo)
[[ 1
2
3
4]
[ 5
6
7
В]
9101112]]
(2, 4))
490
Часть
111. Python
для научных вычислений
Из этого примера видно, что ьаr и foo используют разные участки памяти с дан
ными:
»> baz = np.resize(bar, (2, 6))
»> print (baz)
[ [100
[
2
7
З
4
5
6]
8 100
2
З
4] ]
Как было сказано ранее, функция numpy. resize () заполняет новые элементы по
вторяющимися данными из исходного массива.
Транслирование
(broadcasting)
С темой формы массивов связана еще одна важная особенность массивов
NumPy,
для которой в русском языке нет устоявшегося перевода. В оригинале она назы
вается
broadcasting,
что можно перевести как транслирование (в смысле «транс
лирование радиопередачи»), или вещание. Мы далее будем пользоваться первым
вариантом.
Под транслированием понимается набор правил, которым следует
NumPy
при ма
тематических операциях над двумя массивами, когда размерности массивов разли
чаются.
В самом начале этой главы мы уже использовали транслирование (см. листинг
25.2),
когда умножали двумерный массив на число, которое можно интерпретировать как
массив размера
1х 1.
Основная идея транслирования заключается в том, что если производятся матема
тические операции над двумя массивами А и В, и у А в каком-то измерении (или
нескольких измерениях) размерность равна
1,
а у В она отлична от
1,
то будет счи
таться, что у массива А в этом измерении элементы «размножаются» до той раз
мерности, чтобы она совпала с размерностью у массива В. Звучит запутанно, но
после примеров всё должно стать более понятно.
Начнем с простого примера сложения двумерного массива и числа:
»> foo = np.arange(l,
»> print(foo)
[[ 1
2
З
4]
[ 5
6
7
8]
13)
.reshape(З,
4)
[ 9 10 11 12]]
>>> print(foo + 100)
[[101 102 103 104]
[105 106 107 108]
[109 110 111 112]]
Последнее выражение в этом примере можно было бы написать в виде:
>>> print(foo + np.array([l00]))
При попытке сложения
3 х4,
а второе слагаемое
NumPy увидит, что массив foo имеет форму
- 1х 1, поэтому это единственное значение
(foo. shape)
будет «раз-
Глава
25.
Массивы из библиотеки
491
NumPy
множено» по двум направлениям до достижения размера Зх4. Разумеется, внутри
библиотеки никакого настоящего «размножения» элементов и создания многомер
ного массива не происходит, но для удобства объяснения принципа работы можно
себе такую операцию представить.
Следующий пример складывает двумерный массив и вектор-строку:
>» bar ; np.array( [100, 200, 300, 400])
>>> print(foo + bar)
[ [101 202 303 404]
[105 206 307 408]
[109 210 311 412]]
Здесь массив ьаr, который имеет размерность
3 х4
1х4,
«размножается» до размерности
вдоль одного направления.
Если нам нужно сложить двумерный массив с вектором-столбцом, то мы можем
поступить следующим образом:
>>> baz; np.array([l0, 20, 30]) .reshape(3, 1)
»> print (baz)
[ [10]
[20]
[30]]
>>> print(foo + baz)
[ [11 12 13 14]
[25 26 27 28]
[39 40 41 42]]
С помощью уже знакомого нам метода reshape () мы из вектора-строки с размер
ностью
1хЗ
создаем вектор-столбец с размерностью
кого массива с массивом размером
3 х4
3 х 1,
и тогда при сложении та
«размножение» элементов происходит
вдоль второго направления.
Такой же вектор-столбец можно было бы создать и следующим способом:
»> baz; np.array([[l0], [20], [30]])
И, наконец, самое интересное (и не очевидное):
>>> print(bar + baz)
[[110 210 310 410]
[120 220 320 420]
[130 230 330 430]]
Массив bar имеет размерность
1х4,
а baz -
множаются» вдоль первого направления, а
Зх
baz -
1,
и поэтому элементы bar «раз
вдоль второго.
Ранее для создания вектора-столбца мы использовали метод reshape (), делая при
этом из одномерного массива двумерный.
возможность
повышения
размерности
NumPy
массива,
предоставляет еще одну удобную
используя
хитрую
индексацию
и
константу numpy. newaxis (которая на самом деле равна None). С помощью этой
константы мы можем увеличивать размерность массива, не изменяя общего коли
чества элементов.
Часть
492
111. Python для
научных вычислений
Рассмотрим несколько примеров:
»>
» >
>»
foo = np.array([l0, 20, 30])
bar = foo [ : , np. newaxis]
print(bar)
[ [10]
[20]
[30]]
Так мы создали вектор-столбец.
Если поместить np. newaxis на первую позицию индекса, то вектор останется стро
кой, но размер массива станет двумерным:
>>> baz
=
foo[np.newaxis,
:]
»> print (baz)
[[10 20 30]]
С помощью numpy. newaxis можно добавить сразу несколько измерений массива:
>>> spam = foo[np.newaxis, np.newaxis, :]
»> print (spam)
[[[10 20 30]]]
>>> eggs = foo[np.newaxis,
>» print(eggs)
np.newaxis]
[ [ [10]
[20]
[30]]]
И так далее.
Индексацию с использованием numpy. newaxis часто используют в операциях, где
требуется транслирование, чтобы создать вектор-столбец из вектора-строки.
Как отмечалось ранее, numpy. newaxis равен None, поэтому, если в приведенных
примерах заменить
np. newaxis
на
None,
результат не изменится, но использование
numpy. newaxis является более предпочтительным, поскольку такое выражение явно
показывает намерение программиста.
Булевы массивы и фильтрация элементов
по условию
До сих пор мы применяли к массиву только арифметические операции и функции, а
что будет, если к массиву применить какой-нибудь оператор сравнения? Давайте
попробуем.
Сначала создадим двумерный массив:
>>> foo = np.array([[0, -5, 2, 4],
[1, 3, -4, 7],
[2, -1, 8, -7]])
Глава
25.
Массивы из библиотеки
NumPy
493
Сравним такой массив с о:
>>> positive_filter = foo >=
>>> print(positive_filter)
[ [ True False True True]
True True False True]
True False True False]]
О
Мы получили булев массив, где каждый элемент показывает, соответствует ли ка
ждый из элементов записанному условию. Теперь этот массив мы можем использо
вать для фильтрации элементов, выделив из исходного массива только те элементы,
где на соответствующих позициях расположено значение тrue:
>>> foo_positive = foo[positive_filter]
>>> print(foo_positive)
[О 2 4 1 3 7 2 8]
Таким способом мы выделили из многомерного массива только неотрицательные
числа. Часто для подобных операций не сохраняют промежуточный булев массив, а
пишут так:
>>> print(foo[foo >=
[О 2 4 1 3 7 2 8]
О])
Однако такого рода фильтрацию можно использовать не только для получения эле
ментов, но также и для их изменения. Например, следующая строка умножает на -1
все неотрицательные числа:
>>> foo[foo >=
»> print (foo)
[[ О -5 -2 -4]
[-1 -3 -4 -7]
О]
*= -1
[-2 -1 -8 -7]]
Булевы массивы и операции с их использованием позволяют записывать выраже
ния для фильтрации элементов и их преобразования более компактно, избегая соз
дания явных циклов.
Операции сравнения можно применять и к двум массивам, но при этом важно, что
бы они имели одинаковую форму;
>>> foo = np.array( [ [О, -5, 2, 4],
[1, 3, -4, 7],
[2, -1, 8, -7]])
>>> bar = np.array([[2, 3, 7, -4],
[О,
-2, 10, 13],
[-5, 1, 3, 3]])
Узнаем, на каких позициях элементы в массиве foo больше или равны соответст
вующему элементу в массиве
»> print(foo >= bar)
[ [False False False True]
[ True True False False]
[ True False True False]]
bar:
494
Часть
111. Python
для научных вычислений
И выведем значения этих элементов:
>>> print(foo[foo >= bar))
[41328]
В массиве foo заменим элементы, которые больше или равны элементам в массиве
bar
с теми же индексами, на о:
>>> foo[foo >= bar) =
>» print(foo)
[[ О -5 2 О)
О
О -4
7)
О
-1
О
О
-7))
Приведем еще один практический пример. Пусть у нас есть массив points с коор
динатами точек на плоскости. Координаты каждой точки
»> points
-
это строка в массиве:
np.array([[0.l, 0.2),
О,
1. О),
[О. О,
О. О],
[ 1.
Наша задача
-
[О.
3,
О.
2],
[О.
8,
О.
9)))
вывести только те точки, которые попадают в окружность единич
ного радиуса, то есть для которых расстояние от центра системы координат не пре
вышает
1.
С использованием массивов
NumPy
мы можем записать решение этой
задачи очень компактно без явного применения циклов.
Сначала подсчитаем расстояния от каждой дочки до центра:
»> distances = np. sqrt ( (points * * 2) . sum (axis=l) )
>>> print(distances)
[0.2236068 1.41421356 о.
0.36055513 1.20415946]
Расстояние до начала координат вычисляется по формуле ✓ х 2 + у2 . Сначала мы
возвели все
координаты
в квадрат,
элементы построчно (вдоль оси
1),
затем в полученном массиве просуммировали
получили новый массив и вычислили квадрат
ный корень из всех элементов этого массива.
Затем воспользуемся булевым массивом, полученным сравнением рассчитанных
расстояний с 1:
>>> print(distances <= 1.0)
[ True False True True False]
Непосредственно результат мы получим из такого выражения:
>>> print(points[distances <= 1.0])
[ [О .1
О.
2)
[О.
О.
)
2]]
[О.
3
О.
Чтобы этот прием работал (ведь размеры массивом points и distances не совпа
дают), нужно, чтобы массив distances был одномерным (тогда будет работать
транслирование) и размер обоих массивов по первому направлению был одинаковым.
Глава
25.
Массивы из библиотеки
495
NumPy
Булевы массивы используют не только для выделения элементов, но также в слу
чае, когда нужно узнать, все ли элементы массива удовлетворяют какому-то усло
вию, или есть ли хотя бы один элемент, который удовлетворяет условию. Для этого
в модуле numpy предусмотрены две функции:
♦
all () возвращает тrue, если все элементы булева массива равны True, и воз
вращает
♦
False
в противном случае;
any () возвращает True, если хотя бы один элемент булева массива равен True, и
возвращает
False
в противном случае.
Строго говоря, эти функции в качестве параметра могут принимать не только булев
массив, но и массив, элементы которого могут быть преобразованы в булев тип.
Кроме того, проверки можно осуществлять не по всему массиву, а вдоль заданной
оси, если указать дополнительный параметр axis.
Мы можем проверить, все ли точки из предыдущего примера попали в окружность
единичного радиуса:
>>> np.all(distances <= 1.0)
False
Или выполнить проверку, есть ли точки за пределами этой окружности:
>>> np.any(distances > 1.0)
True
Использование целочисленных массивов
в качестве индексов
В завершение главы мы рассмотрим еще одну полезную возможность массивов
NumPy.
Они позволяют выделять одной командой сразу несколько элементов мас
сива по их конкретным индексам,
-
в отличие от применения инструкции
«: »,
но
мера элементов не обязаны располагаться друг за другом с определенным шагом и
даже могут повторяться. Чтобы использовать эту возможность, для одномерного
массива в качестве индекса надо передать массив или список индексов элементов,
которые нам нужны:
>>> foo = np.arange(lO, 21)
>» print (foo)
[10 11 12 13 14 15 16 17 18 19 20]
»> print(foo[[l, 3, 5, 1]])
[11 13 15 11]
Используя такую индексацию, мы можем изменять элементы массива:
»> foo[ [1, 3, 5]] *= -1
»> print(foo)
[ 10 -11 12 -13 14 -15
16
17
18
19
20]
С многомерными массивами такой прием тоже работает, только в качестве индек
сов нужно указывать такое количество массивов (или списков), чему равна размер-
Часть
496
ность массива,
-
111. Python
для научных вычислений
при этом каждый переданный массив (или список) должен со
держать индексы элементов вдоль одного направления. Более наглядно это видно
на примере:
>>> bar = np.arange(20) .reshape(4, 5)
»> print(bar)
[[01234]
[ 5
6
7
8
9]
[10 11 12 13 14]
[15 16 17 18 19]]
>» print(bar[[O, 2, 3],
[О,
3, 4]])
[01319]
Здесь выделяются элементы с индексами (О, О),
(2, 3)
и
(3, 4).
Изменение элементов с такой индексацией тоже работает:
»> bar[ [О, 2, 3],
»> print (bar)
[ [100
[
[О,
1
2
3
5
6
7
8
9]
10
11
12 100
14]
15
16
17
3, 4]] = 100
4]
18 100]]
Заключение
В этой главе мы подробно изучили массивы из библиотеки
NumPy
(класс ndarray),
которые во многих случаях позволяют не только более компактно реализовать тре
буемую обработку данных, но и одновременно с этим повысить скорость выполне
ния скрипта и сократить потребление оперативной памяти по сравнению с явным
применением циклов в
Python.
Сначала мы ознакомились с несколькими функциями, предназначенными для соз
дания массивов, и увидели, что массивы можно создавать на основе уже имеющих
ся последовательностей с использованием функции array (), заполняя их одинако
выми значениями (функции zeros (), ones (), full ()) или последовательностями
чисел (функции arange (), linspace () и logspace ()),а также, только выделяя ме
сто для них
в оперативной памяти, но не инициализируя элементы
( функция
empty () ).
Мы рассмотрели некоторые функции, выполняющие простейшие операции над
массивами: нахождение минимального и максимального значения, суммы и произ
ведения элементов, среднего значения, транспонирование.
Мы подробно разобрались с выделением элементов массива и увидели, что во мно
гих операциях
не
происходит копирования данных,
а создаются так
называемые
виды, которые работают с данными исходного массива. Создание видов экономит
оперативную память и позволяет записывать вычисления более компактно.
Глава
25.
Массивы из библиотеки
NumPy
497
Мы обсудили весьма важную тему, связанную с формами массивов и транслирова
нием, а также обратились к теме, связанной с булевыми массивами и выделением
элементов из массива, которые должны удовлетворять определенному условию.
В завершение главы мы научились выбирать множество элементов массива, ис
пользуя целочисленные массивы в качестве индексов.
Это была объемная и важная глава, описывающая основы работы с библиотекой
NumPy. В дальнейшем мы будем активно использовать библиотеку NumPy и мас
сивы из неё.
Следующая глава книги посвящена способам хранения данных (в том числе боль
ших) в файлах различных форматов, которые используются в научной среде.
- ГЛАВА 26 -
Форматы файлов для хранения
числовых данных
В главе
20
мы уже говорили о работе с файлами и о том, как в них записывать и
читать данные. Там же мы коротко затронули тему сериализации, когда данные за
писываются в бинарный файл, например, с помощью модуля pickle, или в формат
JSON
с помощью модуля j son. Однако во многих инженерных и научных областях
часто используются другие форматы файлов, которые более удобны для просмотра
человеком или специально предназначены для хранения больших данных. В этой
главе мы рассмотрим несколько таких форматов, применяемых инженерами и уче
ными для хранения данных.
Текстовые файлы, хранящие данные в столбцах
Широкое распространение получили текстовые файлы, данные в которых записаны
в виде столбцов чисел, разделенных в каждой строке знаком табуляции или не
сколькими пробелами. Так, например, может быть записан временной сигнал, когда
первый столбец содержит отсчеты по времени, а второй
-
мгновенные значения
напряжения на выходе приемника. Если в одни и те же моменты времени регистри
руются несколько сигналов, то столбцов с данными может быть больше. Еще один
пример из радиотехники
-
зависимость значения коэффициента усиления антенны
от направления в сферической системе координат. Тогда первый столбец может
содержать значения направления по азимуту, второй
-
по углу места, а третий
-
значение коэффициента усиления антенны.
Часто такие файлы, помимо данных, содержат еще и 3аголовки, поясняющие, какие
данные записаны в каждом из столбцов. Содержимое подобного файла может вы
глядеть следующим образом:
# theta
0.00000
0.50000
1.00000
1.50000
phi
F[дБ]
Fnorm[дБ]
о.оооос
28. 94316063
28.87450306
28.66716719
28.31693002
0.00000000
-0.06865757
-0. 27599344
-0. 62623061
0.00000
о.оооог:
0.00000
Глава
26.
499
Форматы файлов для хранения числовых данных
16.00000
16.50000
17.00000
17.50000
18.00000
45.00000
45.00000
45.00000
45.00000
45.00000
9.30397595
8.71266173
8.00012489
7.19378970
6.32818980
-19.63918468
-20.23049889
-20.94303573
-21.74937093
-22. 61497082
Для записи данных в таком формате в модуле numpy предусмотрена функция
savetxt (), имеющая следующий синтаксис:
savetxt(fname, Х, fmt=' .18е', delimiter=' ', newline='\n',
header=' ', footer=' ', comments='# ', encoding=None)
Коротко опишем ее параметры:
♦
fname -
♦
х
-
имя файла для сохранения данных;
одномерный или двумерный массив, на основе которого будут формиро
ваться столбцы в файле;
♦
fmt -
определяет формат числовых данных в каждом столбце.
Этот формат может быть одинаковым для всех столбцов (тогда значение пара
метра
fmt должно быть записано как строка) или настраиваться для каждого
столбца индивидуально (тогда значение параметра fmt
должно быть списком
строк). Способ форматирования здесь такой же, как и при использовании опера
тора«%» (см. главу
♦
delimiter -
♦
newline -
♦
header -
9);
разделитель между данными в строке;
разделитель между строками;
строка заголовка, которая может выводиться перед числовыми дан
ными;
♦
footer -
♦
comments -
строка подвала, которая может выводиться после числовых данных;
символ, обозначающий комментарий (выводится в начале заголовка
и подвала);
♦
encoding -
определяет кодировку текста для создаваемого файла.
В этом разделе для демонстрации работы функций записи в качестве данных мы
воспользуемся значениями простой функции, имитирующей временной сигнал.
Пример, приведенный в листинге
26.1,
показывает, как просто мы можем сохра
нить в текстовый файл одномерный массив в виде столбца цифр.
f Листинг 26.1. Chapter_26/example_01/columns_t:xt.py
import numpy as np
= np.linspace(O.O, 8.Ое-9, 200)
np.sin(2e9 * х) * np.cos(5e9 *
np.savetxt("example 26 01.txt", у)
х
у=
х)
500
Часть
111 . Python
для научных вычислений
В результате выполнения этого скрипта в текущем рабочем каталоге будет создан
файл с именем example_26_01 .txt (листинг
Листинг 26.2.
26.2).
Chapter_26/example_01/example_26_01.txt
О.ОООООООООО О ОООООООе+ОО
7.869837465 333 279214е-02
1.473472054 3 850 3 3536е-01
1 .96 7 435865 56 66 3 6022е- 0 1
2.19 310 4 70108738 0 8 15е- 0 1
Перед рассмотрением
следующих примеров нам нужно
немного
отвлечься от
функции s avetxt () и разобраться с тем, какими способами мы можем подготовить
данные для сохранения их в виде столбцов. Для этого мы должны взять имеющиеся
одномерные массивы и объединить их таким образом, чтобы данные из исходных
одномерных массивов образовывали столбцы двумерной матрицы.
Есть несколько способов это сделать, и мы рассмотрим их на более компактных
данных в командном режиме
Python.
Сначала создадим данные, которые должны
образовывать столбцы:
>» bar = np.array( [1, 2, 3, 4, 5])
>>> baz = np . array([-1, - 2, - 3, -4, -5])
»> bam = np . arra y ( [1 0, 20 , 30 , 40 , 50 ])
Далее приведены четыре способа достижения того , что нам нужно. В результате
каждого из них мы получим одинаковые массивы:
>>> matrix 1
np. col umn stac k ( (bar, baz, bam))
>>> matrix 2
np.stac k ( (ba r, baz, bam), axis=l)
>>> ma tri x 3
np. s ta c k ( (bar, baz, bam))
.Т
>» matrix 4
np.array( (bar, baz, bam))
.Т
>>> print(matrix_ l)
[ [ 1 -1 10]
2 -2 20 ]
3 - 3 30 ]
4 -4 40]
5 -5 50]]
♦
Функция c o lumn_st ac k () делает непосредственно то , что нам нужно, и в даль
нейшем мы будем использовать именно ее .
♦
Функция s tack () более универсальная, и можем «склеивать» данные как в виде
строк, так и в виде столбцов. Чтобы она объединяла данные в виде столбцов, мы
передаем ей параметр ax is=l. Такого же результата мы добьемся, если с помо
щью функции sta c k () «склеим» данные в виде строк, а потом транспонируем
полученный массив.
Глава
♦
26.
Форматы файлов для хранения числовых данных
501
Пример с функцией array () кажется самым очевидным на первый взгляд
-
мы
создаем новый массив, передав ему список.из имеющихся массивов, но важно не
забыть его транспонировать, иначе переданные массивы будут образовывать
строки, а не столбцы.
Теперь возвращаемся к функции savetxt (). Чтобы сохранить данные из двух
столбцов, нам достаточно вызвать ее, как показано в листинге
Лмстинr 26.3.
26.3.
Chapter_26/example_02/columns_txt.py
np. savetxt ("example_26 _ 02. txt", np .column_stack ( (х,
у)))
Новый файл с данными example_26_02.txt получит следующее содержимое (лис
тинг
26.4).
Лмстинr 26А.
Chapter_261example_02/example_26_02.txt
О.ООООООООООООООООООе+ОО
О.ООООООООООООООООООе+ОО
4.020100502512562980е-11
7.869837465333279214е-02
8.040201005025125960е-11
1.473472054385033536е-01
1.206030150753769023е-10
1.967435865566636022е-01
1.608040201005025192е-10 2.193104701087380815е-01
2.010050251256281361е-10 2.097463649839725053е-01
2.412060301507538047е-10
1.654936248369187901е-01
Нам остается только навести красоту
-
настроить более наглядное форматирова
ние (возможно, с потерей точности, если это допустимо) и добавить заголовок.
Чтобы это показать, мы дополним вызов функции savetxt () некоторыми парамет
рами (листинг
Лмстинr 26.5.
26.5).
Chapter_26/example_03/columns_txt.py
import numpy as np
= np.linspace(0.0, 8.Ое-9, 200)
yl = np.sin(2e9 * х) * np.cos(5e9 *
у2 = np.sin(Зe9 * х) * np.cos(7e9 *
х
х)
х)
np.savetxt("example_26_03.txt", np.column_stack((x, yl,
fmt=("%.3e", "%10.Sf", "%10.Sf"),
U2 [В]",
Ul [В]
header="t [с]
encoding="utf-8")
у2)),
Обратите внимание на использование здесь параметра
нашем случае
-
fmt, значение которого в
это кортеж (можно и список) из строк. Количество элементов это-
Часть
502
111. Python
для научных вычислений
го кортежа должно совпадать с количеством столбцов данных, а каждая строка
описывает способ форматирования чисел для каждого столбца. Причем способ
форматирования здесь задействован тот, который мы в главе
шим,
-
с применением оператора
«%».
9
назвали устарев
В рассматриваемом случае для первого
столбца используется экспоненциальная запись чисел с тремя цифрами после запя
той, а в остальных столбцах числам отводится
1О
символов и
5
символов после за
пятой. Оставшееся место заполняется пробелами таким образом, чтобы числа были
выровнены по правому краю, что позволит лучше визуально отделить столбцы друг
от друга.
Параметр header описывает заголовок, добавляемый перед данными. В начале
строки заголовка будет добавлен символ комментария. Поскольку мы явно не зада
comments,
ли параметр
то используется значение по умолчанию
-
символ«#».
И, наконец, указывается, что файл должен быть сохранен в кодировке
UTF-8.
Это
важно, поскольку заголовок содержит символы из русского алфавита.
Выполнив
этот
example_26_03.txt
# t
[с]
Ul
скрипт,
4.020е-11
8. 040е-11
1.206е-10
1.608е-10
2.0l0e-10
2.412е-10
текущем
рабочем
каталоге
мы
получим
файл
следуюшего содержания:
[В]
U2
0.00000
0.07870
0.14735
0.19674
0.21931
0.20975
0.16549
О.ОООе+ОО
в
[В]
0.00000
О .11558
0.20203
0.23514
О .19977
0.09246
-0.07771
Как можно видеть, мы обрезали здесь количество цифр после запятой, а насколько
это приемлемо
-
зависит от задачи.
Создав такой файл, надо научиться его читать. Для этого предназначена функция
loadtxt () из модуля numpy. Она содержит достаточно много параметров:
loadtxt(fname, dtype=float, comments='#', delimiter=None, converters=None, skiprows=0,
usecols=None, unpack=False, ndmin=0, encoding=None, max_rows=None, *, quotechar=None,
like=None)
Рассмотрим их подробнее:
♦
fname -
♦
d t уре · - тип данных, в который нужно преобразовать прочитанные данные;
♦
имя файла, откуда нужно прочитать данные;
comments -
символ комментария. Текст, начиная с этого символа и до конца
строки, игнорируется;
♦
delimi ter -
разделитель между столбцами. По умолчанию используется про
бел. Если несколько пробелов расположены подряд, они считаются за один раз
делитель;
Глава
♦
26.
Форматы файлов для хранения числовых данных
503
функция, которую можно задать для более сложного разбора и
converters -
преобразования данных;
♦
skiprows -
♦
определяет, сколько строк нужно пропустить при чтении файла;
используется, если из файла нужно прочитать не все столбцы, а
usecols -
только указанные. Должен содержать кортеж (или список) целых чисел. Нуме
рация столбцов начинается с О;
♦
unpack. По умолчанию функция loadtxt () возвращает двумерный массив. Если
значение unpack = тrue, будет возвращен кортеж из одномерных массивов для
каждого прочитанного столбца. Полученный от функции кортеж можно распа
ковать в несколько переменных, которые будут содержать данные по каждому
прочитанному столбцу;
♦
задает минимальную размерность массива, который будет возвращен
ndmin -
из функции. Возможные значения: о (используется по умолчанию), 1 и 2;
♦
encoding -
♦
max_rows -
кодировка файла;
максимальное количество строк, которые будут прочитаны. Ис
пользуется, если не требуется читать весь файл до конца;
♦
quotechar -
применяется для указания того, что каждое число в файле оберну
то какими-то символами (например, кавычками);
♦
like -
используется, если на выходе функции нужно получить объект, отлич-
ный ОТ ndarray.
В последующих примерах будем читать файл example_26_03.txt, который мы созда
ли, выполнив скрипт, приведенный в листинге
26.5,
поэтому далее подразумевает
ся, что копия этого файла расположена в текущем рабочем каталоге.
Простейший способ использования функции loadtxt () показан в листинге
26.6.
import numpy as np
data = np.loadtxt("example 26 03.txt", encoding="utf-8")
print(data)
В результате выполнения этого скрипта будет выведен длинный двумерный массив:
11
[
О.ООООе+ОО
О.ООООе+ОО
4.0200е-11
7.8700е-02
О.ООООе+ОО]
1.1558е-01]
8.0400е-11
1.4735е-01
2.0203е-01]
7. 9600е-09
1.ОбОЗе-01
-6.4110е-01]
8.ООООе-09
1.9201е-01
-7.7266е-01]]
Обратите внимание, что по умолчанию символом комментария считается символ
«#»,
поэтому заголовок, который был в файле, функция loadtxt () проигнорировала.
Часть
504
111. Python
для научных вычислений
Чтобы продемонстрировать частичную загрузку, а заодно и распаковку столбцов, в
вызов
функции
loadtxt (), приведенный в листинге
usecols и unpack (листинг
Листинг
добавим
параметры
26.7).
26.7. Chapter_26/example_05/load_txt.py
import numpy as
х, у=
2.6,
пр
np.loadtxt("example_26_03.txt", usecols=(O, 2),
unpack=Т:r:ue,
encoding="utf-8")
print(x)
print (у)
Здесь мы говорим, что хотим загрузить только первый и последний столбцы, на основе
которых будут созданы одномерные массивы, присваиваемые переменным х и у.
В
заключение раздела отметим еще
одну особенность функций
savetxt ()
и
loadtxt (). Если для функции savetxt () в имени сохраняемого файла указать рас
ширение
gz, то будет создан текстовый файл, заархивированный с помощью алго
ритма gzip. Функция loadtxt () также подразумевает, что файл сжат, если он имеет
такое расширение. При этом архивация и извлечение текстового файла из архива
происходит автоматически.
Работа с данными в формате
Формат
CSV
(сокращение от
CSV
Comma-Separated Values)-
пользуемый текстовый формат. Строгое описание формата
ции
это еще один часто ис
CSV
дано в специфика
RFC 4180 1.
ПОЯСНЕНИЕ
RFC (Request for Comments)системы Интернета.
{lnternet Engineering Task Force,
Формат
CSV
серия документов, на основе которых работают многие
Публикацией таких документов занимается организация
IETF
Инженерный совет Интернета).
имеет широкое распространение, потому что он часто используется
как промежуточный формат, в который сохраняются и из которого читаются дан
ные, создаваемые в электронных таблицах
Отличие формата
CSV
Microsoft Excel
и его аналогов.
от описанного в предыдущем разделе столбцового формата
заключается в том, что данные в строках разделены не пробелами, а, как правило,
запятыми (есть вариант формата, когда данные разделяются точками с запятой).
В общем случае формат
CSV
более сложный, поскольку подразумевает, что данные
могут быть не только числовые, но и строковые или другого типа (например, даты),
1
См. https://datatracker.ietf.org/doc/html/rfc4180.
Глава
26.
Форматы файлов для хранения числовых данных
505
а строковые данные часто требуется оборачивать кавычками, поскольку они внутри
себя могут содержать знак запятой.
Поскольку в этой главе речь идет о данных, которые содержат только числа, то та
кие особенности формата нас пока не интересуют, и для записи и чтения числовых
данных можно использовать знакомые нам функции savetxt () и loadtxt (). Если
же вам нужно работать с файлами
CSV,
которые могут содержать данные другого
типа, лучше воспользоваться возможностями библиотеки
речь в главе
В листинге
Pandas,
о которой пойдет
29.
26.8
показано, как можно создать простой файл с формате
CSV
и тут же
его прочитать.
Листинг
26.8. Chapter_26/example_06/csv_flle.py
import numpy as np
= np.linspace(O.O, 8.Ое-9, 200)
np.sin(2e9 * х) * np.cos(Se9 *
у2 = np.sin(Зe9 * х) * np.cos(7e9 *
х
yl
х)
х)
np.savetxt("example 26 06.csv", np.column_stack((x, yl,
у2)),
fmt=("%.3e", "%.5f", "%.Sf"),
delimiter=", ", encoding="utf-8")
readed_x, readed_y = np.loadtxt("example_26_05.csv", delimiter=",",
usecols=(O, 2), unpack=True,
encoding="utf-8")
Единственная особенность, которая здесь присутствует по сравнению с предыду
щими
примерами,
это
-
использование
параметров
delimiter
в
функциях
savetxt () и loadtxt (). Созданный файл будет выглядеть так:
О.ОООе+ОО,О.ООООО,0.00000
4.020е-11,О.07870,О.11558
8.040е-11,О.14735,О.20203
1.206е-10,О.19674,О.23514
1.608е-10,О.21931,О.19977
Если файл в формате
CSV
был получен из электронных таблиц, надо внимательно
смотреть на его формат. Часто эти программы при работе в русской локализации в
качестве разделителя дробной части чисел используют запятую, а не точку, и тогда
все числа также оборачиваются кавычками. Впрочем, для функции loadtxt () это
не является проблемой. Предположим, что у нас есть файл со следующим содер
жимым (листинг
26.9).
506
Часть
Листинг 26.9.
11
111. Python
для научных вычислений
Chapter_26/example_07/example_26_07.csv
1, 1 ", 11 2,2 11
"1, 22
11 ,
"2, 3"
"1, 14", "2, 4"
"3, 45", "2, 5"
"4, 75", "2, 6"
"О,
36 11 , 11 2, 7 11
"1, 9", "2,8 11
Чтобы прочитать такой файл, в функцию loadtxt () могут быть переданы парамет
ры, показанные в листинге
Листинг
26.1 О.
26.10. Chapter_26/example_07/load_csv.py
import numpy as np
data = np.loadtxt('example 26 07.csv',
delimiter=', ', quotechar='"',
converters=lamЬda х:
float(x.replace(",", ".")))
print(data)
Здесь с помощью параметра quotechar указывается, что все значения обернуты в
кавычки, а в качестве значения параметра converters передана лямбда-функция,
которая заменяет в каждом значении запятую на точку и преобразует полученную
строку в тип float. Если замена запятой на точку требуется не во всех столбцах, то
этот параметр может принимать словарь, в котором ключ
столбца, начиная с о, а значение
-
-
целочисленный номер
вызываемая функция для преобразования зна
чения.
Файлы форматов
NPY и NPZ
До сих пор мы работали с текстовыми файлами, которые часто применяются для
обмена данными между разными приложениями. Однако при использовании таких
файлов нужно согласовывать форматы столбцов и аккуратно настраивать парамет
ры функций для записи и чтения. Другим недостатком текстовых файлов является
их размер. Двоичные файлы из-за более компактного представления чисел в двоич
ном формате по сравнению с текстовым занимают на порядок меньше места на же
стком диске.
Библиотека
NumPy
предоставляет функции, которые умеют записывать и читать
данные в специально разработанных для хранения массивов двоичных форматах:
♦ формат
NPY
предназначен для хранения одного массива
NumPy.
Создание тако
го файла выполняется с помощью функции numpy. save (), а чтения
щью функции numpy. load ();
-
с помо
Глава
♦
26.
Форматы файлов для хранения числовых данных
формат
NPZ
507
предназначен для хранения нескольких массивов
NumPy.
Создание
такого файла выполняется с помощью функции numpy. savez (), а чтение- с
помощью всё той же функции numpy. load ().
Преимущество использования файлов форматов
NPY
и
NPZ -
в простоте записи и
чтения. Однако, поскольку эти форматы не являются общеупотребительными, при
менять их стоит в основном только в тех случаях, когда работа с ними всегда будет
осуществляться с помощью библиотеки
NumPy.
Напишем простой пример, который сначала сохраняет массив в файл
читает из файла записанные данные (листинг
Листинг 26.11.
NPY,
а потом
26.11 ).
Chapter_26/example_OS/npy_flle.py
import numpy as np
= np.linspace(0.0, 8.Ое-9, 200)
yl = np.sin(2e9 * х) * np.cos(Se9 *
у2 = np.sin(Зe9 * х) * np.cos(7e9 *
х
filename = "example 26 08.npy"
np.save (filename, np.column_stack(
data = np.load(filename)
print(data)
х)
х)
yl,
(х,
у2)))
После запуска этого скрипта будет выведен двумерный массив с тремя столбцами.
Функция save () очень простая, из обязательных параметров она принимает только
имя файла и сохраняемый массив. Она имеет также еще один необязательный па
раметр allow_pickle, который мы, впрочем, использовать не станем. Это булево
значение, которое может быть по соображениям безопасности задействовано для
запрета сериализации с помощью модуля pickle (о сериализации с применением
этого модуля речь шла в главе
20).
Однако в этом случае не удастся сохранить мас
сив из сложных объектов (тема массивов из сложных объектов также выходит за
рамки этой книги).
Функция load (), помимо обязательного параметра с именем файла, может прини
мать
несколько
дополнительных
параметров,
но
они
настолько
низкоуровневые,
что в этой книге мы их даже не будем упоминать.
Теперь посмотрим, как создавать и читать файлы в формате
Листинг 26.12.
Chapter_26/example_09/npzJlle.py
import numpy as np
= np.linspace(0.0, 8.Ое-9, 200)
yl = np.sin(2e9 * х) * np.cos(Se9 *
у2 = np.sin(Зe9 * х) * np.cos(7e9 *
х
х)
х)
NPZ
(листинг
26.12).
Часть
508
111. Python
для научных вычислений
filename = "example_26_09.npz"
np.savez(filename, х=х, yl=yl, у2=у2)
with np.load(filename) as data:
print(type(data))
х = data["x"]
yl = data["yl"]
у2 = data["y2"]
При записи в файл формата
NPZ
каждому массиву присваивается уникальное имя,
соответствующее имени параметра функции savez (). Функция savez () разрешает
передавать сохраняемые массивы, в том числе и через неименованные параметры,
но тогда внутри файла
NPZ
им будут присвоены имена arr _ о, arr _ 1 и т. д. По
скольку это не очевидно без чтения документации, лучше всегда передавать масси
вы через именованные параметры с явным присвоением имен.
Если в функцию load () в качестве имени загружаемого файла передается файл с
расширением
npz, то функция вернет не объект ndarray, а объект
numpy. lib. npyio. NpzFile (объект такого типа и будет выведен в консоль при вы
полнении примера их листинга
26.12),
который нужно не забыть закрыть после ис
пользования. Именно поэтому в примере используется конструкция wi th. Каждый
сохраненный массив доступен с помощью оператора квадратных скобок, в которых
указывается имя массива.
Файл формата
NPY -
NPZ
представляет собой ZIР-архив с несколькими файлами формата
в этом легко убедиться, если у созданного NРZ-файла изменить расшире
ние на
zip и заглянуть внутрь этого файла. Для нашего примера мы увидим там
файлы x.npy, у1 .пру и y2.npy. Впрочем, генерируемый с помощью функции savez ()
файл
NPZ
хоть и является архивом, но при его создании для ускорения работы сжа
тие не используется.
Однако,
помимо
numpy имеется функция
savez_compressed (), работающая аналогично функции savez (), но при создании
файла NPZ применяющая алгоритмы сжатия. Функция load () может читать также
и сжатые файлы
функции
savez (),
в
модуле
NPZ.
Файлы формата
HDF5
Описанные в предыдущем разделе двоичные файлы форматов
NPY
и
NPZ
имеют
очень простую и удобную для использования в ряде случаев структуру, но в то же
время плохо подходят для хранения больших и разнородных данных. В этом разде
ле мы рассмотрим формат
HDF5
(сокращение от
Hierarchical Data Format),
разрабо
танный американским Национальным центром суперкомпьютерных приложений
(National Center for Supercomputing Applications, NCSA),
держиваемый компанией The HDF Group.
в настоящее время под
Это достаточно сложный формат файла, в котором одновременно могут храниться
данные разных типов: массивы, таблицы, изображения, двоичные данные и др.
Данные внутри этого файла организованы в иерархическую структуру, напоми-
Глава
26.
509
Форматы файлов для хранения числовых данных
нающую файловую систему. К каждой порции данных можно добавлять атрибуты,
эти данные описывающие. Это может быть полезно, например, при хранении ре
зультатов измерений
тогда в атрибутах можно указать дату и условия, при кото
-
рых были получены те или иные сохраненные в файле результаты. Формат
HDF5
оптимизирован для хранения очень больших данных, а также для их параллельной
обработки. При этом существуют приложения, которые позволяют просматривать
содержимое файлов
HDF5.
Файлы в этом формате могут иметь разные расширения,
но чаще всего используются h5, hdf и hdf5. Можно также всё еще встретить файлы
формата предыдущей версии
- HDF4.
Согласно его спецификации, в формат
HDF5
заложено большое количество воз
можностей, однако из-за сложности реализации разные библиотеки поддерживают
различные их наборы. Разработчики формата официально осуществляют поддерж
ку библиотеки только для четырех языков программирования: С, С++,
Java.
Fortran и
HDF5 на Python чаще всего используются две наибо
h5py и РуТаЫеs, также поддерживающие разные наборы
Для работы с форматом
лее известные библиотеки:
возможностей:
♦
библиотека
h5py 2
ориентирована в первую очередь на хранение массивов, в ко
торых все элементы имеют один тип. Целью этой библиотеки является органи
зация как можно более бесшовной связи с
Установка
h5py
с помощью
pip
NurnPy.
выполняется следующей командой:
> python -m pip install hSpy
♦
библиотека РуТаЫеs3, в отличие от
h5py,
поддерживает также таблицы
-
струк
туру данных, в которых тип данных одинаковый в пределах одного столбца.
Библиотека
Pandas,
о которой речь пойдет в главе
29,
для чтения файлов
HDF5
использует РуТаЫеs.
Установка РуТаЫеs с помощью
> python -m pip install
pip
выполняется следующей командой:
taЫes
В этом разделе мы рассмотрим работу с файлами
теки
HDF5
с использованием библио
h5py.
Создание файлов в формате
HDF5
Сначала напишем пример, который сохраняет в файл
(листинг
26.13).
Лмстинr 26.13.
Chapter_261example_10/save_hdf5.py
import numpy as np
import hSpy
2
См. https://docs.hSpy.org.
3
См. https://www.pytaЫes.org.
HDF5
некоторые данные
Часть
510
= np.linspace(0.0, 8.Ое-9, 200)
yl = np.sin(2e9 * х) * np.cos(5e9 *
у2 = np.sin(Зe9 * х) * np.cos(7e9 *
111. Python
для научных вычислений
х
х)
х)
filename = "example 26 10.h5"
with h5py.File (filename, "w") as f:
print (f" {type (f) =} ")
f.attrs["description"] = "Пример данных,
f.attrs["date_created"] = "2025-05-15"
записанных в
HDF5"
examples_group = f.create_group("examples")
print(f"{type(examples_group)=}")
signals_group = examples_group.create group("signals")
signals_group.attrs["description"] = "Примеры сигналов"
datasetl = signals_group.create_dataset("signall",
data=np.column_stack{ [х, yl]))
print(f"{type(datasetl)=}")
datasetl.attrs["description"] = "sin(2e9 * х) * cos(5e9 * х)"
dataset2 = signals_group.create_dataset("signal2",
data=np.column_stack([x, у2]))
dataset2.attrs["description"] = "sin(Зe9 * х) * cos(7e9 * х)"
other_group = examples_group.create_group("other")
other_group["foo"] = np.arange(35) .reshape(S, 7)
other_group["foo"] .attrs["description"] = "Пример массива
Все классы, используемые для работы с библиотекой
данных"
h5py,
расположены в одно
именном модуле, поэтому в начале скрипта мы этот модуль импортируем. Подго
товив данные, создаем экземпляр класса File, который нужно не забыть закрыть,
поэтому используем конструкцию with. После открытия файла выводим в консоль
полное имя класса с помощью встроенной функции type (). В этом месте будет вы
ведена строка:
type(f)=<class 'h5py._hl.f1les.File'>
К каждому объекту внутри файла
HDF5
с помощью атрибутов можно добавлять
произвольные строки и даже массивы. Имена атрибутов никак не регламентируют
ся. Не рекомендуется хранить в атрибутах большие объемы информации (больше
64
Кбайт). В этом примере к файлу мы добавили два атрибута: "description" -
общим описанием содержимого и "date_created" Для хранения данных в формате
тасеты
(datasets).
HDF5
с
с датой создания.
предусмотрены две сущности: группы и да
Группы по сути напоминают каталоги файловой системы
-
с их
Глава
26.
Форматы файлов для хранения числовых данных
511
помощью реализуется иерархическая структура данных. Внутри групп могут рас
полагаться другие группы и датасеты.
С помощью метода create _group ()
класса File мы создаем группу с именем
"examples". Последующий вывод типа полученного объекта покажет имя создан
ного экземпляра класса:
type(examples_group)=<class 'hSpy._hl.group.Group'>
Затем мы создаем еще одну вложенную группу с именем "signals". Мы могли бы
создать группу "signals" и одной командой:
signals_group = f.create_group("examples/signals")
но этот пример показывает последовательный способ создания иерархии групп.
К группе "signals" мы добавляем атрибут "description" с текстовым описанием
того, что в этой группе предполагается хранить.
После этого с помощью метода create_dataset () класса Group создаются два да
тасета с именами "signall" и "signal2". К ним также добавляются атрибуты
"description" с описанием вида сигнала. С помощью функции type () мы опреде
ляем класс объектов, отвечающих за датасеты:
type(datasetl)=<class 'hSpy._hl.dataset.Dataset'>
Датасеты мы можем создавать не только внутри групп, но и непосредственно в
корне файла, вызвав метод create_dataset () из объекта File (переменная f в на
шем случае).
Последние три строки примера показывают еще один способ создания датасета без
явного создания объекта Dataset.
Библиотека
h5py
и формат
HDF5
предоставляют еще много интересных возможно
стей для хранения данных (в том числе создание ссылок между объектами внутри
файла), однако поскольку цель этого раздела
-
показать только основные возмож
ности формата, ограничимся этим примером, но, прежде чем прочитать получен
ные данные с помощью той же библиотеки
h5py,
посмотрим на полученный файл,
воспользовавшись сторонними приложениями.
Сторонние приложения для работы
с файлами формата
HDFS
Для просмотра файлов
существует множество приложений, и наиболее из
вестные из них
(разработано компанией
-
HDF5
HDFView
The HDF Group),
а также
ViТaЫes, созданное теми же разработчиками, что и упомянутая ранее библиотека
РуТаЫеs.
Чтобы скачать дистрибутив
HDFView,
нужно зарегистрироваться на сайте прило
жения4, и тогда станут доступны ссылки на сборки под
4
См. https://www.hdfgroup.org.
Windows, macOS
и
Linux.
Часть
512
111. Python
для научных вычислений
Энтузиасты могут собрать это приложение самостоятельно из исходных кодов, по
этому во многих репозиториях приложений под
На
рис.
26.1
показано
окно
приложения
example_26_ 10.h5, созданный в листинге
Linux HDFView также доступен.
HDFView,
в
котором
открыт
файл
26.13.
,,
.... '1{1f\l-~J2
~dt
Тооk
Wll'llk,w
Не1р
4' fjJ
l2S d
~
Recenl FOes ~IC\py!hon
--_~- - ,.,- ~ - - - _,-,,._--..,26-_,o-h5---------------------------..r
OearTeмl
• l!il O><aЩ)0>..,26_ 1 0 h5
-•~·
.... other
AttriЬute Qeatюn Order QeaЬon Orde_r _
NO_T_
Trзd<ed
'--'-----
Ni.mЬer of attnЬutes • 1
llfoo
... signa/5
sщna11
II SМ)l1al2
Name
Т)!>О
descnptюn
Stnng leng1l1 • vanaЬle. padd<><) •
H5Т_ST R_ N U LLTERM
cset •
- ~ l l t /~ 1 ~_26_1 0 . h S i n C . ~ ~-~ I O I
-
H5Т_CS EТ_U T F8
□
-
Array S<ze Valuel50K )
S<a/ar
"1(2е9 " х) • ооs(Бе9 " х)
Х
1Ы]
00
О 07869
О 14734
О 19674
02193 1
О 20974
О 16649
User p<operty № • C\Use~e"f<')\ММOwЗ 3 2
slgnall at l<»oзmples/S,gnalsl lexampte_26_10h5" C\pyltюt\_Ьool(\chapler_26\eJ<ample_10JldtmsOxt slat10><0.CO<X>t200.2.stnde tx! )
Рис.
26.1.
Файл example_26_10.h5, открытый в приложении
HDFView
-
1r ,Р Vi~Ьlel. 1.0
•-~
..
Nou,
~e-!•;nqs
Oat.1s.e-t
, wi
t!i.i', (Q Г X - -
•
"Ъ"~оf.S.UЬиа
11
V
tt D .......26. 10.h>
"'8eiample5
'у'
-
··-
S,~rtwlts
.
З 1.2060Э01~10
0.19674359
4 1.6080«!2r-10
G.2 1931()(7
sz.o,~10
2.А120603е-10
7 2.11.t0101Se-10
lll(i(lfl(№- 10
--
х
""""
sm(ld • 111) • cos(Se9 • 1)
о....,..
.....,
V
..,.,,,_
0.16S49362
.......,.,,
-d022Ш67
-
1
1
......
descriplюn
Add
°""'
WNt'1thts
~
Log
1
х
о
1
SystemattriЬutб
.,,,..,.,,
0.1-t734721
0
х
о.
z 1.04020101е,-11
6
о
1
I 4.0201~11
. . . . other
~----~
, 81(1
,ign,11
о
о о.
v8~
Н(>!р
W'1dow
Vi1fbla З, 1JJ
""""
х
,,_,
Copyl'tght (t) 2008-202А V1e1Мt Ми.
AКngntsf'tИrY'td.
C:/P"Jtho,\.Ьoot/c~pt~_'l6/e:иnplre_10/в~_26_10.~> /ts~~sign.t!1
Рис.
26.2.
Файл example_26_10.h5, открытый в приложении ViTaЫes
,.
Глава
26.
513
Форматы файлов для хранения числовых данных
В левой части окна видна структура файла; буква «А» на массивах и некоторых
группах обозначает, что к этим объектам добавлены атрибуты. В правой части окна
открыт список атрибутов для массива signall, а в плавающем окне
-
содержимое
этого массива.
Приложение ViTaЬles устанавливается как пакет
> python -m pip install
Python с
помощью команды:
ViTaЬles[PyQtб]
Это приложение написано с использованием библиотеки для построения графиче
ского
интерфейса
PyQt,
поэтому
в
строке
установки
присутствует
фрагмент
[ PyQt 6 J , означающий, что нужно установить также и эту дополнительную библио
теку.
Для запуска ViTaЬles надо после установки выполнить команду:
> python -m
vitaЫes
Внешний вид окна приложения ViTaЫes после открытия того же файла показан на
рис.
26.2.
Чтение файлов в формате
HDFS
Теперь, когда мы увидели структуру созданного файла, используя графический ин
терфейс, посмотрим, как с этим файлом можно работать программно с помощью
все той же библиотеки
h5py.
В следующем примере (листинг
26.14)
мы читаем соз
данный ранее файл example_26_09.h5, предполагая, что он находится в текущем ра
бочем каталоге. При этом выводится информация, добавленная в атрибуты, размер
записанных массивов, первые
10
строк массивов signall и signal2, а массив foo
выводится полностью.
~
import hSpy
filename
=
"example 26 11.hS"
with hSpy.File(filename, "r") as f:
print ("Описание:", f. attrs [ "description"])
print("Дaтa создания:", f.attrs["date_created"])
signall = f [ "examples/ signals/ signall"]
print ( "Тип signall: ", type (signall))
рrint("Описание сигнала 1:", signall.attrs["description"])
print ( "Размер массива данных:", signall. shape)
print(signall[:10, :], "\n")
signal2 = f [ "examples/ signals/ signal2"]
print ( "Описание сигнала 2: ", signal2. attrs [ "description"])
print("Paзмep массива данных:", signal2.shape)
print(signall(:10, :], "\n")
111. Python
Часть
514
для научных вычислений
foo = f["examples/other/foo"]
рrint("Описание массива данных:",
foo.attrs["description"])
foo.shape)
print("Paзмep массива данных:",
print(foo[:, :])
Как можно здесь видеть, чтение данных происходит согласно тем же принципам,
что и запись. С помощью оператора квадратных скобок мы получаем датасет, кото
рый, в свою очередь, позволяет задействовать квадратные скобки так, как они ис
пользуются в библиотеке
NumPy
применительно к массивам. Впрочем, на самом
деле, там имеются некоторые особенности, о которых мы здесь умолчим.
*
Поскольку цель этого раздела
-
*
*
дать лишь общее представление о формате
HDF5,
мы не обсудили многие интересные вещи, такие как:
♦
структурированные данные, в которых столбцы имеют имена;
♦
одновременное чтение и запись файлов формата
♦
изменение размеров датасетов;
♦
способы оптимизации доступа к большим данным;
♦
параллельная обработка файлов
♦
хранение сжатых данных;
♦
ограничение размеров хранимых данных;
♦
создание ссылок на данные и группы внутри файла;
♦
виртуальные датасеты.
Формат
HDF5 -
HDF5;
HDF5;
очень мощный инструмент для хранения больших данных, кото
рые нужно структурировать внутри одного файла. Многие научные и инженерные
приложения позволяют экспортировать в этот формат результаты моделирования.
Этот раздел является лишь введением в использование формата
данных, его цель
-
HDF5
для хранения
показать, что существует такой интересный формат, который
стоит изучить более подробно.
Другие форматы данных
В завершение главы коротко упомянем еще несколько известных форматов, кото
рые используются для хранения массивов данных.
Формат
NetCDF (Network Common Data Form),
на момент подготовки этой книги
имеющий 4-ю версию. Он разработан Объединением университетов в области ис
следования атмосферы
(University Corporation for Atmospheric Research, UCAR),
и
поэтому в основном получил распространение в климатологии, метеорологии, океа
нографии и, в меньшей степени, в геоинформационных системах.
Главная особенность этого формата
-
он построен поверх
кация намного проще. Можно сказать, что
NetCDF
HDF5,
но его специфи
использует подмножество воз-
Глава
26. Форматы файлов для хранения числовых данных
можностей
515
Благодаря этому файлы в формате
HDF5.
программах, предназначенных для чтения файлов
Для работы с форматом NetCDF
ncview, ncBrowse, Panoply.
NetCDF
HDF5.
можно открывать в
разработано множество приложений, в том числе
Еще одним способом хранения больших данных является довольно новый формат
который был создан в рамках активно развивающегося проекта
Zarr,
Zarr-Python5 .
Хотя, как видно из названия проекта, разработчики этого формата в первую оче
редь ориентируются на
существуют библиотеки для работы с этим форма
Python,
том и на других языках (на сайте проекта упоминаются языки
Rust
и
Формат
Zarr
С, С++,
также позволяет хранить большие многомерные однородные массивы
в иерархической структуре. Главная особенность библиотеки
Zarr
Java, Julia,
JavaScript).
Python Zarr
и формата
заключается в том, что они разрабатываются с учетом возможности парал
лельной обработки очень больших данных, которые могут быть расположены как
локально (в том числе внутри ZIР-файла) у пользователя, так и в распределенной
системе, в частности, в облаке. Мы не называем
Zarr
форматом файла, потому что
при создании хранилища в этом формате создается каталог, содержащий файлы с
данными, которые делятся на кусочки
(chunks)
заданного пользователем размера, и
каждый из них хранится в отдельном файле. Это позволяет во многих случаях ор
ганизовать параллельную обработку данных из разных потоков
В настоящее время формат
Zarr
Zarr-Python
и процессов.
используется различными университетами в науч
ных исследованиях, а также в некоторых проектах
сайте
(threads)
NASA, Google
и
Microsoft.
На
можно найти более подробный список научных организаций, ко
торые с форматом
Zarr
работают. В книге
[1]
приводятся примеры использования
этого формата.
Заключение
Эта глава посвящена способам хранения данных, которые используются в научных
или инженерных расчетах. Мы говорили только о хранении однородных данных,
когда
все
элементы
массивов
имеют
один
тип,
однако
многие
из
описываемых
здесь форматов позволяют хранить и таблицы, где данные имеют один тип в преде
лах одного столбца, а разные столбцы могут хранить разные типы данных.
Сначала мы рассмотрели самый простой с точки зрения пользователя способ хра
нения данных
-
текстовый файл, в котором данные записаны в виде столбцов, а
столбцы разделены символом табуляции или несколькими пробелами. Для записи и
чтения таких данных в библиотеке
NumPy
предусмотрены функции savetxt ( J и
loadtxt () соответственно. Это достаточно гибкие функции, позволяющие настраи
вать внешний вид записываемых и считываемых данных, а также добавлять ком
ментарии в файл.
5
См. https://zarr.readthedocs.io.
Часть
516
111. Python
для научных вычислений
Другим рассмотренным нами распространенным форматом является формат
CSV,
в
котором данные в строках разделены запятой. Для чтения данных в таком формате
лучше всего использовать возможности библиотеки
в главе
Pandas,
речь о которой пойдет
но в некоторых случаях для записи и чтения таких файлов можно при
29,
менить всё те же функции savetxt () и loadtxt (). Кроме того, в стандартную биб
лиотеку
Python
входит модуль csv, предназначенный для работы с данными в та
ком формате.
Текстовые форматы файлов удобны тем, что их легко просматривать и редактиро
вать в текстовом редакторе, однако для хранения очень больших данных такие
форматы мало подходят, поскольку занимают много места и требуют сложного
разбора данных при их чтении, поэтому были разработаны специальные форматы,
предназначенные для хранения больших массивов и таблиц.
Библиотека
NumPy
предоставляет функции save () и savez () для сохранения одно
го массива или нескольких массивов в файлы форматов
NPY
и
NPZ
соответствен
но. Для чтения таких файлов используется функция load () из модуля numpy.
Познакомились мы в этой главе и с одним из наиболее известных форматов хране
ния больших данных
HDF5, а также научились создавать файлы этого формата с
h5py, позволяющей работать с массивами внутри файлов
принципам, что и с массивами NumPy.
-
помощью библиотеки
HDF5
по тем же
Затем мы коротко обсудили имеющиеся приложения с графическим интерфейсом
для чтения и изменения данных в файлах
HDF5,
а также написали скрипт, читаю
щий созданный ранее файл в этом формате. Кроме библиотеки
форматом
HDF5
h5py,
для работы с
можно применять библиотеку РуТаЫеs, которая также использу
ется в библиотеке
Pandas.
В завершении главы мы упомянули еще два других формата, предназначенных для
хранения больших данных
HDF5,
-
это формат
NetCDF,
построенный поверх формата
а также Zатт, ориентированный, в том числе, на распределенное хранение
данных.
Работать с библиотекой
NumPy
мы будем и далее, но пришло время научиться
строить графики различного вида. Для этого мы воспользуемся
Matplotlib,
речь о которой пойдет в следующих двух главах.
библиотекой
- ГЛАВА 27 -
Основы построения графиков
с помощью библиотеки
Построение графиков
-
Matplotlib
это важный этап в научных исследованиях и при инже
нерных расчетах. Часто графики являются результатом моделирования какого-либо
физического процесса или измерения какой-то величины, полученной от измери
тельного прибора. Отображение данных в виде графиков, гистограмм, линий уров
ня или другим способом является более наглядным представлением выведенной
закономерности или результата расчета по сравнению с таблицей чисел. Иногда
результатом выполнения скрипта с расчетами могут стать десятки графиков того
или иного вида. Различные виды данных наиболее наглядно представляются раз
ными типами графиков, и даже одни и те же данные могут быть представлены
по-разному, например, в разных системах координат.
В этой главе мы начнем изучать библиотеку
Matplotlib,
которая является практиче
ски стандартом для создания графиков в различных научных областях, где исполь
зуется
Python.
Эта библиотека позволяет создавать огромное количество видов
графиков и гибко настраивать каждый их элемент: стиль и цвет линий, масштаб
осей и расположение рисок, добавлять текст и формулы, воспроизводить анима
цию, есть даже возможность добавлять элементы управления и встраивать графики
в приложения, написанные с использованием различных библиотек для построения
графического интерфейса. Библиотека
Pandas, о которой пойдет речь в главе 29,
Matplotlib для отображения данных, кроме того, графики, созданные с
Matplotlib, встраиваются в блокноты среды JupyterLab (см. главу 31).
использует
помощью
Установка библиотеки и первые примеры графиков
Установка библиотеки
помощью
pip
Matplotlib
не отличается от установки других библиотек. С
она устанавливается следующей командой:
> python -m pip install -user matplotlib
Как мы уже видели в главе
16,
навливает также библиотеку
библиотека
NumPy
Matplotlib
В этой главе мы научимся строить графики вида у
вид.
в качестве зависимости уста
и некоторые другие.
= j(x)
и настраивать их внешний
Часть
518
Matplotlib
♦
111. Python
для научных вычислений
предоставляет возможность строить графики на основе двух подходов:
при использовании первого подхода для создания графика или настройки его внеш
него вида нужно вызывать ту или иную функцию из модуля matplotlib.pyplot, и
такой вызов повлияет на текущий график. При этом одновременно могут ото
бражаться несколько графиков, среди которых один является активным (автома
тически становится активным последний созданный график).
Эти функции
очень напоминают функции из языка и среды разработки МА TLAB, и если вы
работали с МА TLAB, то многие функции вам покажутся знакомыми (библиоте
ка
Matplotlib
даже содержит модуль pylab, который сейчас считается устарев
шим);
♦
второй подход к построению и настройке графиков более объектно-ориентиро
ванный. При его использовании вы получаете или создаете экземпляры классов,
отвечающих за ту или иную часть графика (окно, оси, кривая), и настраиваете
его внешний вид с помощью методов этих классов. Этот подход позволяет более
гибко формировать внешний вид графиков, а также с ним удобнее работать, ко
гда нужно одновременно изменять параметры множества графиков, хотя код
при таком подходе будет чуть более длинным.
Сначала мы в основном будем ориентироваться на первый подход, а про объектно
ориентированный подход поговорим в конце этой главы.
Сразу начнем с примера (листинг
27 .1 ).
В качестве функции, график которой мы
будем строить, возьмем функцию sinc () -
она имеется в библиотеке
NumPy.
На
помним, что:
.
sшс
( )
х
sin(7tX)
=---,
7tX
при этом в точке х
Листинг 27.1.
= О значение этой
функции равно
1.
Chapter_27/example_01/plot_sinc.py
import numpy as np
import matplotlib.pyplot as plt
х
= np.linspace(-np.pi * 2, np.pi * 2 , 201)
у=
np.sinc(x)
plt. plot
(х,
у)
plt. show ()
После запуска этого скрипта откроется окно, показанное на рис.
27.1.
График функции отображается в основной части окна, а в верхней его части (воз
можно, и в нижней
-
зто зависит от настроек по умолчанию, которые в разных
операционных системах различаются) расположена панель инструментов, с помо-
Глава
27. Основы построения графиков с помощью библиотеки Matplotlib
519
щью которой можно включать режим перемещения графика (кнопка + ), изменение
его масштаба (кнопка Q. ), отмену и возврат изменений внешнего вида (кнопки +
и+), возврат к внешнему виду по умолчанию (кнопка А). Правее расположены
=
кнопки
и ~, обеспечивающие изменение некоторых параметров внешнего вида
графика (далеко не всех из тех, что доступны программно), а также кнопка ig) для сохранения графика в различные графические форматы.
о
Figun, 1
х
1.0
о. в
0.6
0.4
0.2
о.о
- 0 .2
-б
Рис.
27.1.
-4
-2
о
2
4
Результат выполнения примера из листинга
Рассмотрим подробно исходный код приведенного в листинге
шинство функций библиотеки
жены в модуле
доним
Matplotlib,
matplotlib. pyplot,
б
27.1
2 7 .1
скрипта. Боль
которые мы будем вызывать, располо
которому при импорте принято давать псев
plt.
График функции строится по точкам, которые соединяются отрезками прямых ли
ний. Чем ближе друг к другу расположены точки, тем более гладкими выглядит
график. В нашем примере мы подготавливаем два массива, которые содержат ко
ординаты точек по осям Х и У соответственно. Затем вызываем функцию plot (),
предназначенную для создания графика такого типа, что приведен на рис.
27.1.
У этой функции огромное количество необязательных дополнительных парамет
ров, и некоторые из них мы вскоре рассмотрим.
Если бы мы закончили наш скрипт вызовом функции plot (), убрав вызов функции
show (), то скрипт завершился бы без видимого результата. Функция plot () не соз-
Часть
520
111. Python
дает окно с графиком непосредственно в момент вызова
-
для научных вычислений
оно будет создано толь
ко после вызова функции show () (окно с графиками в терминах
ется фигурой,
figure).
Matplotlib
называ
Кроме того, в момент вызова функции show () выполнение
скрипта приостанавливается до тех пор, пока пользователь не закроет окно с гра
фиком. То есть, если бы после вызова функции show () были бы записаны другие
команды, то они начали бы выполняться только после закрытия окна с графиком.
В
Matplotlib
есть возможность изменения такого поведения с помощью так назы
ваемого интерактивного режима, который включается и отключается с помощью
функций ion () и ioff () соответственно. Интерактивный режим может быть поле
зен при создании анимированных графиков, но мы его рассматривать не будем.
В приведенном в листинге
27. l примере в качестве входных данных использова
NumPy, но это не обязательно. В качестве входных
последовательности например, списки, как показано
лись массивы из библиотеки
данных могут быть любые
в листинге
Листинг
27 .2.
27.2. Chapter_27/example_02/plot_list.py
import matplotlib.pyplot as plt
х
= [-6, -5, -4, -3, -2, -1, о, 1, 2, 3, 4, 5, 6]
[-2, -1, 1, 4, 3.5, 3, 5, 5, 4.5, 5.5, 6.2, 6.0, 7.0]
у=
plt.plot (х,
plt. show ()
у)
В результате выполнения этого скрипта мы увидим ломаную линию, показанную
на рис.
27.2.
Для экономии места в иллюстрациях для этого и последующего при
меров будет показываться только та часть окна, где отображаются графики, без за
головка окна и панелей инструментов.
б
4
2
о
-2
-6
Рис.
27.2.
-4
-2
о
2
4
б
Результат выполнения примера из листинга
27.2
Глава
Основы построения графиков с помощью библиотеки
27.
521
Matplotlib
График проходит через указанные точки, и интерполяция для сглаживания не при
меняется.
Функцию plot () можно вызывать с различным набором параметров. Например, мы
можем не передавать ей значения координат по оси Х,
-
и в этом случае в качестве
отсчетов по этой оси будет выводиться последовательность чисел О,
N-
1, ... N - 1,
где
количество элементов в единственном переданном параметре. То есть, если в
листинге
27.2
вызов функции plot () изменить на следующий:
plt.plot(y)
то мы получим график, показанный на рис.
Обратите внимание на изменив
27.3.
шиеся отсчеты по оси Х. Мы можем вызывать функцию plot () совсем без пара
метров, тогда откроется окно с пустыми осями без графика, но в сейчас нам такой
способ создания графика не интересен.
б
4
о
-2
о
Рис.
27.3.
4
Результат вызова функции
б
plot ()
8
10
12
с единственным параметром
Настройка внешнего вида кривых на графиках
Графики даже с настройками по умолчанию смотрятся неплохо, но часто требуется
изменить какие-то параметры их отображения. Для изменения свойств создаваемых
кривых в функции plot () предусмотрены дополнительные необязательные пара
метры, с помощью которых мы можем менять следующие свойства:
♦
цвет
♦
стиль (сплошная линия, штриховая, пунктирная, штрихпунктирная)- параметр
-
параметр color (или с);
linestyle (или 1s);
-
♦
маркеры
параметр ma r ke r;
♦
толщина линии
-
параметр linewidth (или lw).
Часть
522
111. Python
для научных вычислений
Способы задания цвета
Рассмотрение параметров отображения графиков мы начнем с параметра color,
предназначенного для задания цвета кривых. Это достаточно важный момент, по
скольку цвета используются во всех видах графиков, и мы его поясним достаточно
подробно.
В
Matplotlib
существуют как минимум восемь способов указания цвета:
1.
С помощью однобуквенной строки. Например,
2.
С помощью словесного описания цвета. Например,
3.
С помощью словесного описания цвета из таблицы
11
g 11
для зеленого цвета
-
11
(green).
goldenrod 11 •
xkcd.
Например,
11
xkcd:moss
green 11 •
4.
С
11
5.
помощью указания компонентов цвета в
С помощью указания компонентов цвета в формате
что равносильно
6.
11
11
#RGB". Например, "#AFS 11 ,
С помощью указания красного, зеленого и синего компонентов цвета в виде
0.0-1.0.
Например, (о.
s,
о. 2,
о. з).
С помощью указания компонентов цвета и альфа-канала в виде кортежа или
списка четырех чисел в диапазоне
8.
"#RRGGBB". Например,
# AAFF s s 11 •
кортежа или списка трех чисел в диапазоне
7.
формате
#310115 11 •
0.0-1.0.
Например, (о.
s,
о. 2,
о. з,
о. в)
.
Для задания серого цвета можно использовать строку с числом с плавающей
точкой в диапазоне
0.0-1.0.
Например, "о. з 11 •
Рассмотрим эти способы более подробно.
♦
Первый способ самый компактный, особенно, если в функции plot ()
параметра
plt.plot(x,
color
у,
использовать его аналог
c= g
11
-
вместо
параметр с:
11 )
Но таким способом мы можем выбрать цвета из очень небольшого набора:
•
11 Ь 11
-
синий цвет (Ыuе);
•
11
g 11
-
зеленый цвет
(green);
•
11
r
-
красный цвет
(red);
•
"с" -
цвет морской волны
•
"m 11
пурпурный цвет
•
"у" -
•
11
•
"w" -
11
-
k" -
желтый цвет
(magenta);
(yel\ow);
черный цвет (Ыасk);
белый цвет
(cyan);
(white).
Глава
♦
27.
Основы построения графиков с помощью библиотеки
523
Matplotlib
Второй способ подразумевает написание полного названия цвета из приведенно
го на странице документации библиотеки набора, включающего около
150
цве
тов'. Сюда, например, входят такие названия как "orange", "lime", "navy",
"gray" и "grey", а также множество других. Эти названия и сами цвета взяты из
спецификации
CSS 2 .
Таким образом, для указания оранжевого цвета мы можем
написать:
plt.plot(x,
♦
color="orange")
у,
Третий способ предусматривает, что, кроме цветов из спецификации
но задавать цвета из расширенного набора палитры
в себя названия почти
1ООО
xkcd 3 .
CSS,
мож
Этот набор включает
цветов. Чтобы использовать такие названия, перед
именем цвета в команде его выбора нужно добавить префикс xkcd:. Например,
если вы хотите окрасить график в цвет зеленого горошка, то можете написать:
plt.plot(x,
♦
color="xkcd:pea green")
у,
Следующие два способа давно используются в веб-разработке и пришли в
Matplotlib
(красный),
из языка разметки
HTML.
Мы можем указывать компоненты цвета
R
(зеленый) и В (синий) в виде шестнадцатеричных чисел в форматах
G
вида "#RRGGBB" (или "#RGB" -
если каждый компонент цвета описывается дву
мя одинаковыми шестнадцатеричными числами). Например, "# з з oos s" или, что
равносильно,
plt.plot
♦
(х,
"# з os":
у,
color="#3D5")
Согласно шестому способу, компоненты цвета
RGB
тежа из трех дробных чисел в интервале от О.О до
можно задавать в виде кор
1.0.
Например, следующая
строка окрасит линию в красный цвет:
plt.plot(x,
♦
у,
color=(l.O,
О.О,
О.О))
Если мы, как предлагает седьмой способ, добавим в кортеж четвертый элемент,
то он будет обозначать степень непрозрачности (О
1-
-
полностью прозрачный,
полностью непрозрачный). Для линий графика, с которым мы сейчас рабо
таем, прозрачность не особо полезна, но в других ситуациях, когда, например,
нужно нарисовать прямоугольник, выделяющий какую-то область на графике,
это можес uj.JИГОДИТЬСЯ.
♦
И, наконец, последний, восьмой способ используется для задания серого цвета.
Серый цвет
-
это такой цвет, у которого все три компонента:
R, G
и В
-
рав
ны. Для задания серого цвета можно указать строку с дробным числом винтер
вале от О до
plt.plot(x,
1,
у,
где О
-
это белый цвет, а
1-
черный. Например:
color~"O.3")
1
См. https://matplotlib.org/staЫe/gallery/color/named _ colors.html.
2
См. https://www.wЗ.org/TR/css-color-4/#named-colors.
3
См. https://xkcd.com/color/rgb.
Часть
524
111. Python
для научных вычислений
Стили линий
Для изменения стиля линии графика предназначен параметр linestyle, или в более
коротком варианте
-
1 s.
Эти параметры могут принимать два вида значений:
♦
строку с названием или обозначением именованного предустановленного стиля;
♦
кортеж в формате (смещение,
ха,
длина
пропуска,
(длина штриха,
длина пропуска,
длина штри
... ) ) .
Небольшое количество предустановленных в библиотеке стилей приведено в
табл.
27.1.
Таблица
Обозначение стиля
11 - 11
ИЛИ
11
"- . "
ИЛИ
"dashdot"
"" ' " " '
Внешний вид
--
dashed"
ИЛИ
ИЛИ
стили линии
"solid 11
" - - 11
":"
27.1. Bcmpoeh чые
'dotted 11
none 11
-
- -
-. - . ---. -·-···•··-·····
1
11
-
ИЛИ
Нет линии
"None"
Например, при использовании стиля "dashdot":
plt. plot
(х,
у,
linestyle="dashdot")
график из примера, приведенного в листинге
так, как показано на рис.
27.1
(см. рис.
27.1),
будет выглядеть
27.4.
Такого же результата можно было бы добиться с помощью следующего выражения:
plt.plot(x,
у,
linestyle="-.")
Второй, более гибкий способ, позволяет описывать последовательность штрихов и
промежутков в линии с помощью кортежа. Например, стиль (о,
( s,
3) ) обозначает следующее: длина штриха
длина штриха
длина пропуска
3,
длина штриха
2,
5,
длина пропуска
длина пропуска
3,
3,
з,
2,
3,
2,
2,
а затем всё повторяется. Пер
вый ноль задает смещение начала рисунка линии. Все длины заданы в условных
единицахроiпt (1 point равен 1/ 72 дюйма или примерно 0,35 мм).
Применив указанный стиль:
plt.plot
(х,
у,
linestyle= (0,
(5, 3, 2, 3, 2, 3)))
мы получим на графике результат, показанный на рис.
27.5.
Глава
27.
Основы построения графиков с помощью библиотеки
1.0
Matplotlib
525
·\
1.
\
.
1 \
1
0.8
1
1
1
1
1
О.б
i
1
1
1
1
1
1
\
1
1
1
1
0.4
,
1
0.2
i
о.о
r.
1
1
i \
1
1
/
/.
1
\
\
·~
/
I
\
\, __,,.1
•
/
-0.2
\ .
./
·.;
-4
-б
Рис.
27.4.
-2
о
2
б
4
Здесь применен стиль линии "dashdot"
1.0
/" 1
1
0.8
1
О.б
0.4
0.2
:\
о.о
'
f
'
\
/
\
/
\
1
1
/
/
/
-0.2
-б
Рис.
27 .5.
-4
-2
о
Здесь применен стиль линии (о,
2
б
4
( 5,
з,
2,
з,
2,
з)
)
Маркеры
Маркеры
-
это значки, которые дополнительно накладываются на отображаемую
линию. Это могут быть кружочки, квадратики, крестики и другие символы.
Для добавления маркеров в функции plot () используется параметр marker, в каче
стве значения принимающий строку из одного символа, обозначающего тип марке
ра. Возможные обозначения маркеров и их внешний вид показаны в табл.
27 .2.
Часть
526
Таблица
Символ
Внешний вид
Символ
•••••
•••••
•••••
о
s
р
♦
d
♦
♦
♦
V
•••••
•••••
D
h
*
Символ
•••••
Внешний вид
1
"(
)"
А
А
А
А
2
:,..
:,..
<
◄
◄
◄
◄
◄
3
---:
---:
>
►
►
►
►
►
4
.).
+
+ + + + +
-
* * * * *
Обозначение маркеров и их внешний вид
Внешний вид
>::
х
•••••
н
для научных вычислений
А
л
♦
27.2.
111. Python
х
1
Таким образом, если мы в листинге
1
27.1
1
:,..
-:
-~
"(
:,..
---:
.).
"(
:,..
---:
.).
х
х
- - - -
1
).
·r
1
1
заменим вызов функции plot () на сле
дующий:
plt.plot(x,
у,
marker='x')
то получим график, показанный на рис.
27.6.
1.0
о.в
0.6
0.4
0.2
о.о
-0.2
-6
Рис.
27 .6.
-4
-2
С помощью параметра
о
ma r ke r= 'х'
4
б
к линии добавлены маркеры
По умолчанию маркеры отображаются строго в тех точках, координаты которых
заданы в первых двух параметрах функции plot (), поэтому, если график строится
по большому количеству точек, маркеры могут располагаться слишком близко друг
к другу и пересекаться, что не очень эстетично. Для прореживания маркеров слу
жит параметр markevery. Он достаточно универсальный, и может принимать раз
ные типы значений, два из которых мы здесь рассмотрим.
Глава
27.
Основы построения графиков с помощью библиотеки
Если значение параметра markevery -
целое число
N,
527
Matplotlib
это означает, что нужно ото
бражать каждый N-й символ. Например, если мы хотим сделать так, чтобы отобра
жался только каждый третий символ, то нужно вызвать функцию plot () следую
щим образом:
plt.plot(x,
у,
marker='x',
markevery=З)
Результат такого вызова показан на рис.
27.7.
1.0
0.8
0.6
0.4
0.2
О.О
-0.2
-6
Рис.
27.7.
-4
-2
о
4
6
Маркеры разрежены с помощью параметра markevery=З
Это уже лучше, но выглядит так, будто маркеры расположены неравномерно, хотя
на самом деле они расположены равномерно вдоль оси Х, но не по расстоянию ме
жду ними. Однако если параметру markevery в качестве значения указать дробное
число, то библиотека
стояния
между
Matplotlib
ними
были
постарается так расположить маркеры, чтобы рас
одинаковыми.
Чем
больше
значение
параметра
markevery, тем более разреженными будут маркеры. Например, если в нашем слу
чае мы укажем
plt.plot(x,
у,
markevery=0. 07:
marker='s', markevery=0.07)
то в качестве результата увидим график, выглядящий более аккуратно (рис.
27.8).
Упомянем еще некоторые параметры, которые влияют на внешний вид маркеров:
размер маркеров;
♦
markersize -
♦
markeredgecolor -
цвет обводки маркеров;
♦
markerfacecolor -
цвет заливки маркеров;
♦
markeredgewidth -
толщина обводки маркеров.
Как видите, маркеры, как и все остальные части графика, настраиваются достаточ
но гибко.
Часть
528
111. Python
для научных вычислений
1.0
о.в
0.6
0.4
0.2
О.О
-0.2
-6
Рис.
27.8.
-4
-2
о
2
Маркеры разрежены с помощью параметра
4
6
markevery=O. 07
Краткий способ задания внешнего вида кривых
Мы познакомились с достаточно большим количеством параметров, влияющих на
стиль линии, а также на вид и расположение маркеров. Однако функция plot ()
предоставляет возможность более компактной записи стиля, если используются
только встроенный стиль линии и цвет, задаваемый одной буквой. В этом случае в
качестве третьего (или второго, если не указывать координаты по оси Х) параметра
функции plot () нужно передать строку, содержащую описание стиля линии, цвета
и, если необходимо, маркеров. Например, следующая команда отображает график
штриховой линией:
plt.plot(x,
у,
"--")
Если в качестве третьего параметра передать строку "g--" (или "--g"), то график
будет нарисован штриховой зеленой линией, а если "go--", то будут добавлены
еще и маркеры-кружочки.
Такой способ задания внешнего вида кривых часто используется на практике.
Несколько графиков в одних осях
До сих пор мы строили графики, отображающие кривые на основе одного набора
данных, но часто бывает нужно показать сразу несколько графиков. И здесь могут
быть два варианта: либо построение несколько графиков в одних осях, либо созда
ние для каждого графика отдельных осей (возможно, даже в разных окнах).
Сначала
мы
(см. листинг
рассмотрим
27.1)
первую
ситуацию.
Изменим
самый
первый
пример
так, чтобы рассчитать значения еще одной функции. Для по-
Глава
27.
Основы построения графиков с помощью библиотеки
529
Matplotlib
строения двух графиков в одних осях у нас есть два способа. Первый
-
в одном
вызове функции plot () последовательно передать не именованные параметры, не
обходимые для построения первого графика, затем те же параметры для второго
графика, и так далее, если нужно отобразить более двух кривых (листинг
Листинг
27.3).
27.3. Chapter_27/example_OЗ/plot_several.py
import numpy as np
import matplotlib.pyplot as plt
= np.linspace(-np.pi * 2, np.pi * 2 , 201)
np.sinc(x)
у2
np.sinc(x / 2.5)
х
yl
plt.plot(x, yl, "-k",
plt. show ()
х,
у2,
"--r")
В этом примере для первого графика устанавливается сплошной стиль линии и
черный цвет
("-k"),
а для второго
штриховой стиль и красный цвет
-
зультат такого вызова показан на рис.
("--r").
Ре
27.9.
,
1.0
/
/
' ''
/
о.а
О.б
0.4
0.2
\
\
\
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
о.о
-0.2
-4
-б
Рис.
27 .9.
-2
о
2
4
б
Отображение двух кривых в одних осях графика
Того же результата можно добиться, если последовательно вызывать функцию
plot () для каждого графика:
plt.plot (х, yl, "-k")
plt.plot (х, у2, "--r")
plt. show ()
Первый способ более компактный, но позволяет применять только ограниченные
возможности по настройке внешнего вида кривых. Второй способ более универ-
530
Часть
111. Python
для научных вычислений
сальный, поскольку при его применении мы можем использовать все те параметры,
о которых говорили ранее. К тому же второй способ удобен, когда заранее не из
вестно количество графиков, и они добавляются последовательно в цикле.
Добавление легенды
Представляя в одних осях несколько графиков, нужно показать пользователю, ка
кая кривая каким данным соответствует, или, по-другому, что обозначает каждая
кривая .
Эту
проблему
решает легенда
-
небольшой
прямоугольник,
который
обычно располагается в углу графика и содержит поясняющий текст рядом с каж
дым типом линии. Для создания легенды предназначена функция legend () из мо
дуля
matpl ot l ib. pyplot.
Этот поясняющий текст для каждого графика функция legend () должна откуда-то
взять. В терминах
Matplotlib
такой текст называется меткой
(label).
Способов зада
ния меток для графиков существует несколько, и мы рассмотрим здесь два из них.
Один из способов заключается в том, чтобы передать в функцию legend () метки в
виде списка строк (или другой последовательности, например, кортежа). При этом
порядок следования меток в последовательности должен соответствовать порядку
вызова функции plot (). Дополним пример из листинга
legend () (листинг
Листинг
27.3
вызовом функции
27.4).
27.4. Chapter_27/example_04/plot_legend.py
import numpy as np
import matpl otlib .pypl ot as plt
х
yl
у2
= np.linspace (-np.pi * 2, np .p i * 2 , 201)
np.sinc (x)
np.sinc(x / 2.5)
plt.plot(x, yl, "-k")
plt.plot(x, у2, "--r")
plt.legend( ["sinc(x)", "sinc(x/2.5) "])
plt. show ()
В результате будет создано окно с графиком, показанное на рис.
27. 1О, -
в правом
верхнем его углу появилась легенда.
По умолчанию библиотека
Matplotlib
старается разместить легенду в том углу, где
бы она не перекрывала кривые графика, но у функции legend () есть дополнитель
ный параметр
loc, с помощью которого можно указывать, куда следует поместить
легенду.
Такой способ добавления легенды
-
когда метки передаются непосредственно в
функцию legend (), не очень удобен тем, что кривые создаются в одном месте, а
Глава
27.
Основы построения графиков с помощью библиотеки
531
Matplotlib
метки задаются в другом, и, если мы поменяем местами вызовы функции plot (), то
надо не забыть при этом поменять порядок меток в функции 1 egend () . Поэтому
лучше задавать метки непосредственно при создании графиков, передавая в функ
цию plot () дополнительный именованный параметр label:
plt.plot(x, yl, "-k", label="sinc(x)")
plt.plot(x,
у2,
"--r", label="sinc(x/2.5)")
pl t. legend ()
1.0
,'
1
/
'\
'
1
0.8
1
\
\
1
\
1
1
1
1
1
1
1
1
1
'
1
1
'
1
1
'
0.4
'
--- sinclx/2.5)
\
''
1'
О.б
1
1
1
1
1
1
1
'
,,
1
1
1
1
0.2
SIПC(X)
-
1
1
1
1
О.О
-0.2
-6
-4
Рис.
-2
27.10.
о
4
б
График с легендой
В этом случае функцию legend () можно вызывать без параметров, или передавать
ей только дополнительные параметры, влияющие на внешний вид легенды. Таких
дополнительных параметров достаточно много, и далее указаны некоторые из них:
заголовок легенды;
♦
t i t 1е -
♦
loc -
♦
labelcolor -
♦
fontsize -
♦
markerscale -
♦
facecolor -
цвет заливки легенды;
♦
edgecolor -
цвет рамки легенды;
♦
ncols -
положение легенды;
цвет шрифта надписей в легенде;
размер шрифта надписей в легенде;
масштаб отображения меток кривых в легенде;
в случае большого количества меток их можно выводить в несколько
столбцов (этот параметр задает количество столбцов);
♦
draggaЫe -
♦
shadow -
разрешает пользователям перемещать легенду с помощью мыши;
добавляет тень около легенды.
Часть
532
Для примера с помощью параметра
111. Python
для научных вычислений
l oc переместим легенду в левый верхний угол,
зададим ее заголовок с помощью параметра ti tl e , покрасим фон легенды серым
цветом с помощью параметра
раметра
facec o l o r , ее рамку сделаем черной с помощью па
edge c ol or и , наконец, добавим тень с помощью параметра s hadow:
plt.legend( loc= "uppe r le f t ",
ti t l e= "sinx(x) vs s inc(x/2. 5)",
facecolor="0. 9", edgecolor="0.0",
shadow=True)
В результате мы получим график, показанный на рис.
1,0
sinx(x) vs sinc(x/2 .5)
sinc(x)
sinc(X/2 .5)
---
0.8
0,6
0,4
0.2
,
27 .11.
''
I
I
''
I
I
I
1
1
1
1
I
1
1
1
1
I
1
1
I
1
I
1
1
1
1
I
I
\
\
\
\
\
\
\
\
\
\
1
\
1
1
1
\
1
1
1
1
1
\
О.О
-0.2
-4
-6
Рис.
27 .11.
-2
о
2
4
б
Влияние на легенду дополнительных параметров
Создание нескольких графиков
в одном окне на разных осях
В предыдущем разделе мы строили несколько графиков в одних осях, но часто тре
буется создать несколько отдельных графиков в одном окне, разместив их каждый
в своих осях в виде таблицы ,
например, так, как показано на рис.
-
27 .12.
Для этого предназначена функция s ubp l o t () из модуля ma tpl o tlib. p ypl o t -
она
создает новые оси в текущем окне. Функция subpl ot (), как и многие другие функ
ции из библиотеки
Matplotlib,
может принимать большое количество необязатель
ных параметров, но мы рассмотрим здесь только основные.
Чтобы получить результат, показанный на рис.
щее содержание (листинг
27 .5).
27 .12,
скрипт может иметь следую
Глава
27.
Основы построения графиков с помощью библиотеки
1.00
1.00
0.75
0.75
,,
\
1
1
1
1
0.50
0.25
0.25
0.00
0.00
-0.25
-0.25
-5
1
1
1
1
1
1
1
1
1
1
1
\
\
/
/
/
/
/
/
1
1
1
\
\
~''
-5
5
о
-, \
\
1
/
0.50
533
Matplotlib
о
,_,I
,
,,
,
5
1.0
0.5
О.О
-0.5
-1 .0
-6
Рис.
Листинг
27.12.
-4
-2
о
2
4
б
Расположение нескольких графиков в одном окне
27.5. Chapter_27/example_05/plot_subplot.py
import numpy as np
i mport matplotlib . pyplot as plt
= np.linspace (-np.pi * 2, np.pi * 2 , 201)
np.s i nc (x)
np.sinc (x / 2.5)
у2
np.sin(x) * np.cos(x * 2 )
уЗ
х
yl
2, 1)
pl t. pl ot(x, yl, "-k")
plt.suЬplot(2,
plt.suЬplot(2,
pl t .plot (х,
у2,
plt.suЬplot(2,
plt.plot(x,
plt.show()
уЗ ,
2, 2)
"--Ь")
1, 2)
"-r")
Здесь функция s ubpl o t ( J вызывается три раза, поскольку надо создать три графи
ка, и принимает три параметра:
♦
количество строк в виртуальной таблице с графиками;
♦
количество столбцов в виртуальной таблице с графиками;
♦
номер ячейки, к которой будут применяться последующие функции рисования.
Нумерация ячеек начинается с единицы.
Часть
534
111. Python для
научных вычислений
Для создания левого верхнего графика мы с помощью первого вызова функции
subplot () условно делим окно на четыре части (две строки и два столбца) и указы
ваем, что начнем рисовать график в ячейке номер
1.
В результате выполнения этой
команды будут созданы пустые оси для левого верхнего графика, и функция plot ()
будет применяться именно к этим осям. Затем мы тем же способом создаем второй
график, который тоже вписывается в виртуальную таблицу 2х2, но в ячейку номер
2.
Поскольку третий график должен занимать нижнюю половину окна, для создания
осей под него мы передаем в функцию subplot () параметры, сообщающие, что нам
нужна таблица из двух строк и одного столбца, а оси надо создать для второй ячей
ки, то есть нижней. Затем мы создаем новый график, вызвав функцию plot (), по
сле чего вызываем функцию show () для отображения окна со всеми графиками,
которые мы создали. Результат выполнения этого скрипта на рис.
27 .12
и показан.
С помощью функции subplot () мы можем в любой момент переключаться между
созданными осями, чтобы добавлять новые графики или вызывать другие функции,
настраивая их внешний вид. Например, создав все три графика, можно добавить
легенды к первым двум (листинг
Листинг 27.6.
27.6).
Chapter_27/example_06/plot_subplot_repeat.py
plt.subplot(2, 2, 1)
plt.plot(x, yl, "-k")
plt.subplot(2, 2, 2)
plt.plot(x, у2, "--Ь")
plt.subplot(2, 1, 2)
plt.plot(x, уЗ, "-r")
plt.suЬplot(2,
2, 1)
plt.legend( ["sinc(x) "])
plt.suЬplot(2,
2, 2)
plt. legend ( [ "sinc (х/2. 5) "] , loc = "upper left")
plt. show ()
В результате графики приобретут вид, показанный на рис.
27.13.
В этом случае было бы уместнее вызывать функции legend () непосредственно по
сле соответствующих функций plot (), чтобы избежать лишнего переключения
между графиками, но в некоторых ситуациях такое повторное переключение на
уже имеющиеся графики бывает оправданно.
Функция subplot () имеет также сокращенную форму передачи параметров, когда
количество графиков в окне не превышает девяти. В этом случае в subplot () мож
но передать не три целых числа, а одно трехзначное. Например, вместо subplot (2,
з,
4) написать subplot ( 234). Использовать ли такой способ
-
дело вкуса.
Глава
27.
Основы построения графиков с помощью библиотеки
1.00
-
sinc(x)
0.75
1.00
Matplotlib
sinc(x/2.5)
1
1
1
i
/
0,75
1
1
1
/
0.50
0,50
0,25
0.25
0.00
0.00
-0.25
-0.25
-5
/
1
1
1
1
1
1
1
''
', ... ,
1
1
1
1
1
1
1
1
1
I
,,
,, ,
,_,
I
-5
5
о
-,
535
5
о
1,0
0.5
о.о
-0.5
-1,0
~
Рис.
27.13.
-4
-2
о
4
2
б
Результат выполнения скрипта из листинга
27.6
Настройка осей графика
После создания графика с помощью функции plot ()
интервал отображения по
осям Х и У устанавливается автоматически таким образом, чтобы были видны все
точки на графике, и график занимал бы максимальную отведенную ему площадь.
Однако с помощью функций xlim() и ylim()
из модуля matplotlib.pyplot мы
можем менять пределы, установленные для осей Х и У соответственно. В качестве
параметров эти функции принимают минимальное и максимальное значения, кото
рые нужно установить по соответствующей оси.
Изменим самый первый наш пример (см. листинг
функций xlim () и ylim () (листинг
Листинг
27.1),
добавив в него вызов
27.9).
27.9. Chapter_27/example_09/plot_xylim.py
import numpy as np
import matplotlib.pyplot as plt
х
= np.linspace(-np.pi
у=
* 2, np.pi * 2, 201)
np.sinc(x)
plt.plot (х, у)
plt.xlim(-4, 4)
plt.ylim(-1, 2)
plt. show ()
Здесь с помощью функции xlim () мы «обрезали» график по оси Х, а с помощью
функции ylim (), наоборот, добавили свободного пространства по оси У. В резуль
тате выполнения этого скрипта график приобретет вид, показанный на рис.
27.14.
Часть
536
111. Python
для научных вычислений
1.5
1.0
0.5
о.о
--0.5
-1.0 + - - - - - - - - - - - - - - - - - - - - - - - - - - 1
-з
-4
Рис.
27.14.
-2
-1
о
з
Наш график после применения функций
xlim ()
4
и
ylim ()
Если нам нужно изменить границу только для одного конца шкалы, то вместо пе
редачи двух неименованных параметров можно передавать именованные:
♦
параметры left и right (левая и правая границы соответственно) для функции
xlim ();
♦
параметры bot tom и top (нижняя и верхняя границы соответственно) для функции
ylim().
Например, если в примере из листинга
ylim ()
27.9
изменить вызовы функций xlim ()
и
на такие:
plt.xlim(left=-4)
plt.ylim(top=2)
то по оси Х изменится левый предел, по оси У показанный на рис.
верхний, и график примет вид,
27.15.
Все наши предыдущие графики не были информативны, поскольку было не ясно,
какие величины откладываются по осям. На практике у осей обязательно должны
быть подписи (в терминах библиотеки
Matplotlib -
метки,
labels).
Для добавления
меток к осям предназначены функции xlabel () и ylabel (). В простейшем случае
им достаточно передать строку с меткой
-
и она появится рядом с соответствую
щей осью. Кроме того, эти функции могут принимать множество дополнительных
параметров, влияющих на внешний вид и положение меток. Вот некоторые из них:
♦
параметр loc -
предназначен для перемещения метки к началу, середине или
концу оси. Этот строковый параметр может принимать следующие значения:
•
для функции xlabel() -
значения "left", "center", "right" определяют
расположение метки слева, по центру и справа соответственно;
Глава
•
27.
Основы построения графиков с помощью библиотеки
для функции
ylabel () -
значения
Matplotlib
537
"bottom", "ceпter", "top" определяют
расположение метки снизу, по центру и сверху соответственно.
-4
-2
Рис.
27.15.
о
4
б
График после изменения левой границы
по оси Х и верхней границы по оси У
♦
параметр color -
♦
параметр
предназначен для изменения цвета текста метки;
labelpad -
предназначен для изменения расстояния от оси графика
до метки. Расстояние задается в условных единицах в виде числа с плавающей
точкой. Чем больше это значение, тем дальше метка будет отстоять от оси. Па
раметр может принимать и отрицательные значения. Значение по умолчанию
равно
♦
4.0;
параметр
rotatioп -
предназначен для поворота текста. Значение задается в
градусах. О градусов соответствует горизонтальному расположению текста.
В следующем примере (листинг
ylabel ()
27.10)
показано применение функций xlabel () и
с дополнительными параметрами, а также еще некоторых новых для нас
функций, влияющих на внешний вид графика.
import пumpy as np
import matplotlib.pyplot as plt
х
=
пp.liпspace(0.0,
у= np.siп(2e9
plt.plot(x,
*
х)
8.Ое-9, 200)
* np.cos(Se9 *
х)
у)
рlt.хlаЬеl("Время,
с",
laЬelpad=lS,
loc="right",
color="Ьlue")
Часть
538
111. Python для
научных вычислений
loc= 11 top 11 , rotation=O,
plt.ylaЬel( 11 U, В 11 ,
laЬelpad=-8,
color= 11 Ьlue 11 )
plt.xlim(left=0.0)
plt.ylim(-1.0, 1.0)
pl t. ti tle ( 11 у = sin (2е9 *
* cos (5е9 *
х)
х)
11 ,
pad=8)
plt.grid()
plt.tight_layout()
plt.show()
В результате выполнения кода из этого листинга появится окно с графиком, пока
занное на рис.
27.16.
у
U, 6 1.00
= sin(2e9 * х)
• cos(5e9 • х)
0.75
0.50
0.25
0.00
-0.25
-0.50
-0.75
-1.00
о
1
з
2
4
5
б
8
le-9
Время, с
Рис.
В
этом
примере
loc=" right
11
27.16.
Результат выполнения примера из листинга
метка
около
оси Х сдвинута вправо
с
27.1 О
помощью параметра
и немного отодвинута вниз с помощью параметра
labelpad=l5,
иначе
она налезала бы на надпись le-9. Метка около оси У передвинута вверх с помощью
параметра loc="top" и приближена к оси с помощью параметра labelpad=-8, а
кроме того,
повернута с
помощью
параметра
rotation=O,
иначе
по
умолчанию
текст располагался бы вдоль вертикальной оси. Для обеих меток с помощью пара
метра color="Ыue" установлен синий цвет.
Изменены также пределы по осям с помощью функций xlim () и ylim (), после чего
вызваны несколько функций, с которыми мы еще не встречались:
♦ с помощью функции title () добавлена надпись над графиком. По используе
мым параметрам эта функция во многом напоминает функции xlabel ()
и
ylabel (). В нашем примере задействован именованный параметр pad, позво
ливший немного отодвинуть надпись от графика вверх;
Глава
♦
27.
Основы построения графиков с помощью библиотеки
539
Matplotlib
с помощью функции grid () включено на графике отображение сетки, а с помо
щью функции tight_layout () график отмасштабирован в окне таким образом,
чтобы он занимал максимальную площадь, но при этом чтобы все метки около
осей умещались в окне;
♦
библиотека
Matplotlib
позволяет также выводить произвольный текст в любом
месте графика. Для этого предназначена функция text () , принимающая в каче
стве основных параметров координаты, где должен быть выведен текст (коор
динаты задаются в системе координат осей графика), и строку, которую необхо
димо вывести. Кроме того, функция text () может принимать множество допол
нительных
функций
параметров,
напоминающие
те,
что
мы
уже
использовали
для
xlabel () И ylabel ().
У
функций, предназначенных для отображения текста, таких как xlabel (),
ylabel (), ti tle (), text () и других, есть интересная особенность. Если передавае
мый текст окружить символами "$ ", то этот текст будет интерпретироваться как
формула в формате
LaTeX -
популярного в научной среде языка разметки для
подготовки публикаций. Описание команд
в следующем примере (листинг
27 .11)
LaTeX
выходит за рамки этой книги, но
эти команды применены для вывода грече
ских букв около осей, а кроме того, здесь с помощью функции
text () выводится
формула, по которой рассчитываются значения функции для графика.
Листинг
27.11. Chapter_27/example_11/plot_latex.py
import numpy as np
import matplotlib.pyplot as plt
theta = np.linspace(-np.pi / 2, np.pi / 2, 201)
f = np.abs(np.sinc(theta * 5))
plt.plot(np.rad2deg(theta), f)
plt.xlabel (r"$\theta, \deqree$")
pl t. ylabel (r"$f (\theta) $")
plt. text (40, О. 9,
r"$f(\theta) = \leftl\frac(sin(S\pi\theta)}(S\pi\theta}\riqhtl$",
fontsize=l5)
plt.xlim(-90, 90)
plt.ylim(0, 1.1)
plt.grid()
plt.tight_layout()
pl t. show ()
Для лучшего понимания поясним некоторые команды
пользуются:
♦
\ theta
♦
\pi -
- символ 0;
символ л;
LaTeX,
которые здесь ис
Часть
540
♦
♦
\degree \left
I
111. Python
для научных вычислений
знак градуса;
и \right
I
работают как масштабируемые (подстраивающиеся по разме
рам под содержимое) скобки в виде вертикальных линий;
♦
\frac -
команда для создания дроби, числитель и знаменатель которой указы
ваются последовательно в фигурных скобках.
Результат выполнения кода из листинга
-80
-60
-40
-20
27 .11
О
е
Рис.
27.17.
Использование функции
показан на рис.
20
40
бО
27 .17.
80
.•
text ()
и вывод формул в формате
LaTeX
Координаты, передаваемые в функцию text (), по умолчанию соответствуют левой
нижней границе текстового объекта. Это поведение можно изменить с помощью па
раметров horizontalalignment (выравнивание по горизонтали) и verticalalignment
(выравнивание по вертикали).
Объектно-ориентированный подход
к построению графиков
Те функции, которые мы использовали до сих пор, по своей сути очень напомина
ют программный интерфейс, который предоставляет пользователям среда МА TLAB.
Такой интерфейс представляет собой набор независимых функций, однако все час
ти графика, которые мы видим на экране, на самом деле являются экземплярами
какого-то класса, и мы можем настраивать их внешний вид через методы этих клас
сов. Например, класс Figure отвечает за внутреннюю часть окна, куда будут поме
щены оси для графиков, за оси отвечает класс Axes, кривые на графике
ляры классов Line2D, а легенды
-
-
экземп
экземпляры класса Legend и т. д. Мы об этом не
говорили, но многие функции, которые мы до этого использовали, возвращают эк
земпляры какого-либо класса. Так, функция plot () возвращает список экземпляров
Глава
27.
класса
Основы построения графиков с помощью библиотеки
Line2D,
Matplotlib
541
количество которых в этом списке соответствует количеству созда
ваемых функцией кривых. Функция subplot () возвращает экземпляр класса Axes, а
функция text () -
экземпляр класса техt, отвечающий за созданную надпись.
Экземпляр класса Figure можно создать с помощью функции figure (), которая
создает пустое окно (при этом можно указать размер окна). Таким образом, можно
совмещать использование описанных функций с объектно-ориентированным под
ходом. Например, создать оси с помощью функции subplot (), а настроить их
внешний вид с помощью методов класса Axes. Можно пойти еще дальше и каждый
элемент графика создавать самостоятельно, но, как правило, этого не требуется.
Подытожим всё, что мы изучили в этой главе, и напишем скрипт, использующий
более объектно-ориентированный подход (листинг
27.12).
Для ознакомления с
классами, получаемыми в процессе вызова различных функций, мы будем выво
дить в консоль их типы.
Листинг 27.12.
Chapter_27/example_12/plot_oop.py
import numpy as np
import matplotlib.pyplot as plt
х
= np.linspace(0.0,
yl
у2
=
200)
np.sin(2e9 * х) * np.cos(5e9 * х)
np.sin(0.5e9 * х) * np.cos(le9 * х)
8.Ое-9,
# Создаем окно с графиком
fig = plt.figure(figsize=(9, 7))
print(f"{type(fig)=}")
# Создаем оси для графика
axes = fig.add_subplot(l, 1, 1)
print(f"{type(axes)=}")
# Рисуем графики
curves = axes.plot(x, yl, "-k",
print (f" {type (curves [О])=}")
# Настраиваем внешний вид
axes . gr id ()
axes.set_xlim(left=0.0)
axes.set_ylim(-1.0, 1.0)
# Метки
xlabel
ylabel
х,
у2,
"--r")
графика
около осей
loc="right",
axes.set_xlabel("Bpeмя,
с",
labelpad=l5,
axes.set_ylabel("U,
labelpad=-6,
loc="top", rotation=0,
color="Ьlue")
В",
color="Ьlue")
Часть
542
111. Python
для научных вычислений
print(f"{type(xlabel)=)")
print(f"{type(ylabel)=)")
# Настраиваем заголовок гра.фика
title = axes.set_title("y = sin(2e9 *
print(f"{type(title)=)")
х)
* cos(5e9 *
х)",
pad=B)
# Настраиваем легенду
legend = axes.legend(["sin(2e9 * х) * cos(5e9 * х)",
"sin(0.5e9 * х) * cos(le9 * х)"],
loc="upper left", shadow=True,
facecolor="lightgray", edgecolor="Ыack",
titlе="Сигналы")
print(f"{type(legend)=)")
fig.tight_layout()
plt. show ()
Первое, что мы делаем в этом скрипте- с помощью функции figure () создаем ок
но с областью для будущих графиков. В эту функцию мы передаем именованный
параметр figsize, задающий размеры области в дюймах. Само окно с учетом панели
и заголовка будет немного больше указанного размера. Функция figure () возвраща
ет экземпляр класса Figure, в чем мы убедимся по следующей строке в консоли:
type(fig)=<class 'matplotlib.figure.Figure'>
Если в этот момент вызвать функцию show (), то мы увидим пустое окно, в котором
не будет даже осей. Если бы нам нужно было создать несколько окон с графиками,
то мы могли бы вызвать несколько раз функцию f igure () и получить несколько
экземпляров класса
Figure,
связанных со своим окном, а затем, используя их, рисо
вать разные графики в разных окнах.
Получив экземпляр класса
Figure, мы вызываем его метод add_subplot () и созда
ем оси для графика. Этот метод возвращает экземпляр класса Axes, что также видно
по следующей строке в консоли:
type(axes)=<class 'matplotlib.axes._axes.Axes'>
Получив экземпляр класса Axes, мы можем нарисовать график в этих осях, что и
делаем с помощью метода
plot (), который принимает все те же параметры, что
функция plot (), которую мы использовали до этого. За один вызов метода plot ()
мы создаем сразу два графика, поэтому функция вернет список из двух элементов
типа Line2D. Скрипт выводит тип одного из этих элементов, чтобы мы могли в этом
убедиться:
type(curves[O])=<class 'matplotlib.lines.Line2D'>
Затем мы возвращаемся к использованию экземпляра класса Axes в виде перемен
ной axes и настраиваем внешний вид осей. Как можно видеть, методы класса Axes
имеют или такие же имена, как у функций из модуля matplotlib. pyplot, или очень
Глава
27.
Основы построения графиков с помощью библиотеки
543
Matplotlib
похожие с префиксами set_ * (есть еще методы с префиксами get_*). Методы при
нимают те же самые параметры, что и их аналоги-функции.
Далее
мы добавляем метки около осей с помощью методов set_xlabel () и
se t _У l аЬе l () и сохраняем возвращаемые этими методами объекты - ими оказы
ваются экземпляры класса техt:
type(xlabel)=<class 'matplotlib.text.Text'>
type(ylabel)=<class 'matplotlib.text.Text'>
При необходимости мы можем использовать эти объекты для настройки внешнего
вида текста.
Экземпляр класса техt мы получаем и при создании заголовка графика, вызвав ме
тод
set_title ():
type(title)=<class 'matplotlib.text.Text'>
Ну и, наконец, создаем легенду через метод legeпd
() всё того же класса Axes и по
лучаем экземпляр класса Legeпd:
type(legeпd)=<class
'matplotlib.legeпd.Legeпd'>
у
U,
= sin(2e9 * х) * cos(Se9 * х)
В 1.00 r.:==========~-г---,---,----i----,---,--,
сигналы
0.75
s1n(2e9 * х) • cos(Se9 • х)
-- sin(0,5e9 * х) • cos(le9 * х)
~
0.50
1
,,f'r
0.25
,r
,I
~,.,. 1
1
'
,
I
0.00
\
\
\
\
\
1
,,
,,
/
/
' 1 ,,,L,
\
д,. .
-0.25
\
\
\
-0 .50
\
\
\
--0,75
\
\\
,'''
'
'
''
/
-1.00 + - - - - - + - - - - - + - - - - ' ..,._~'---+----t-----+------т------t-~
о
2
1
3
4
5
б
7
8
le-9
Время . с
Рис.
27.18.
Результат выполнения примера из листинга
В завершение мы вызываем метод
tight_layout ()
27.12
экземпляра класса
Figure,
рас
тягивающий график на всю доступную область, и, наконец, вызываем функцию
544
Часть
111. Python
для научных вычислений
show () для отображения всего того, что мы напрограммировали. В результате будет
создано окно, показанное на рис.
27.18.
На этом примере мы убедились, что за каждый элемент, который мы видим в окне с
графиком, отвечает свой класс. Однако, создавая здесь все элементы графика, для
настройки их внешнего вида мы передаем большое количество параметров в каж
дую функцию. Используя же объектно-ориентированный подход, мы можем разде
лить этапы создания объектов от настройки их внешнего вида. Это, конечно, по
требует большего количества строк кода, но зато позволит сделать код более струк
турированным,
что
может быть важно
в сложных скриптах.
Индивидуальная
настройка каждого параметра элементов графика показана в листинге
Листинг 27.13.
Chapter_27/example_13/plot_oop_params.py
import numpy as np
import matplotlib.pyplot as plt
= np.linspace(0.0, 8.Ое-9, 200)
np.sin(2e9 * х) * np.cos(5e9 * х)
у2 = np.sin(0.5e9 * х) * np.cos(le9 * х)
х
yl
# Создаем окно с графиком
fig = plt.figure()
fig.set_size_inches(9, 7)
# Создаем оси для графика
axes = fig.add_subplot(l, 1, 1)
# Рисуем графики
curvel, curve2 = axes.plot(x, yl,
curvel. set_ linestyle ( '-')
curvel. set _ color ( 'Ыасk')
curvel.set_label("sin(2e9 *
х)
curve2.set_linestyle('--')
curve2. set _ color ( 'red')
curve2.set_label("sin(0.5e9 *
# Настраиваем внешний вид
axes. grid ()
axes.set_xlim(left=0.0)
axes.set_ylim(-1.0, 1.0)
х,
у2)
* cos(5e9 *
х)
х)")
* cos(le9 *
х)")
графика
# Метки около осей
xlabel = axes.set_xlabel('Bpeмя,
xlabel. set _ color ( 'Ыuе')
с',
loc='right', labelpad=15)
2 7.13.
Глава
27.
Основы построения графиков с помощью библиотеки
ylabel = axes.set_ylabel('U,
В',
Matplotlib
545
loc='top', labelpad=-6)
ylabel.set_color('Ьlue')
ylabel.set_rotation(O)
# Настраиваем заголовок графика
title = axes.set_title("y = sin(2e9 *
х)
* cos(Se9 *
х)",
pad=8)
# Настраиваем легенду
legend = axes.legend()
legend.set_loc('upper left')
legend. set _ ti tle ('Сигналы' )
legend.shadow = True
# Настраиваем рамку легенды
legend_frarne = legend.get_frame()
legend_frame.set_facecolor('lightgray')
legend_frarne.set_edgecolor('Ьlack')
fig.tight_layout()
plt. show ()
Обратите внимание на настройку внешнего вида легенды. Для включения тени ис
пользуется свойство shadow, а не функция наподобие set _ shadow (), -
такой функ
ции у класса Legend нет. А для того, чтобы настроить рамку легенды, сначала мы
получаем экземпляр класса, отвечающий за нее, с помощью метода get _ frame ().
В этом скрипте тип возвращаемого значения не выводится, но это будет экземпляр
класса FancyBboxPatch. И уже используя методы этого класса, мы меняем заливку
и цвет рамки легенды.
Заключение
В этой главе мы начали знакомство с библиотекой для построения графиков
Matplotlib.
На примере простого и наиболее часто используемого вида графиков мы
изучили ее основные функции:
♦
plot () -
♦
legend () -
♦
для построения графика вида у= j(x);
для добавления легенды;
х 1 im () / у 1 im () -
для изменения отображаемой области по осям Х и У соответ-
ственно;
♦
ti tle () -
для добавления заголовка к графику;
♦
grid () -
♦
subplot () -
для включения сетки;
для создания нескольких графиков в одном окне;
Часть
546
111. Python
для научных вычислений
♦
tight layout ()-для растягивания графика на всю доступную область;
♦
text () -
для добавления текстовой надписи в произвольном месте графика;
♦
show () -
для отображения окна с графиком.
Мы также подробно рассмотрели способы задания цвета и стиля линий, которые
используются совместно с функцией plot ().
В последнем разделе
главы мы показали,
как можно
использовать объектно
ориентированный подход при программировании графиков.
На протяжении всей этой главы мы рисовали только простейший вид графика, ото
бражающий зависимость у= j(x), но
Matplotlib
позволяет создавать большое коли
чество других видов графиков, в том числе и трехмерных. Как это делается, мы уз
наем в следующей главе.
- ГЛАВА 28-
Построение с помощью библиотеки
Matplotlib более сложных
графиков
В предыдущей главе мы на примерах простых графиков вида у= f{x) рассмотрели
основные принципы работы с библиотекой
не ограничиваются такими графиками,
-
Matplotlib.
Но возможности
Matplotlib
с помощью этой библиотеки можно соз
давать диаграммы и графики для очень разнообразного представления данных,
включая наложение разных графиков друг на друга. В этой главе мы научимся соз
давать еще несколько видов графиков, но надо иметь в виду, что приведенные
здесь примеры также охватывают далеко не все возможности,
ляет
которые предостав
Matplotlib.
Диаграммы рассеяния
Мы уже знаем, что, используя функцию plot (), можем задавать маркеры, отме
чающие точки на графике, через которые он проходит, а также, что один из воз
можных стилей линии
-
это отсутствие линии. Два этих обстоятельства дают нам
возможность построения диаграммы рассеяния
(scatter plot),
которая отображает
точки, разбросанные на плоскости. Подобные графики часто строят для визуализа
ции каких-либо статистических величин.
При создании такого графика нужно иметь в виду, что точки, описываемые с по
мощью первых двух параметров функции
plot () (координаты по осям Х и У соот
ветственно), могут быть указаны в любом порядке и не обязаны быть упорядочены
по возрастанию координаты х, как это требовалось при построении графика в виде
соединенных точек. Такое использование функции plot () показано в листинге
import numpy as np
import matplotlib.pyplot as plt
count = 30
х = np.random.rand(count) * 10
у= np.random.rand(count) * 10
28.1.
Часть
548
plt.plot(x,
у,
111. Python
для научных вычислений
"Ьо")
plt.grid()
plt.xlirn(0, 10)
plt.ylirn(0, 10)
plt. xlabel ( 'Х')
plt. ylabel ('У', rotation=0)
plt.tight_layout()
plt. show ()
В этом примере в качестве данных создаются два массива по
дослучайными значениями в интервале от О до
применена функция rand ()
10.
30
элементов с псев
Для создания таких массивов
из модуля numpy. random, возвращающая массив ука
занного размера, содержащий равномерно распределенные псевдослучайные вели
чины в интервале от О до
1.
Чтобы отобразить график в виде голубых точек, устанавливается стиль графика
"Ьо" ("Ь" указывает на голубой цвет, "о" -
символ маркера в виде кружочков, а
поскольку не указан стиль линии, то линия, соединяющая точки, не отображается).
В результате выполнения этого скрипта получится график, показанный на рис.
10
•
•
8
•
28.1.
••
•
б
••
у
4
•
2
б
4
8
10
х
Рис.
28.1.
Использование функции
plot ()
для отображения рассеянных точек
Однако в некоторых задачах бывает нужно визуализировать какую-либо дополни
тельную числовую информацию применительно к каждой точке. В этих случаях
для представления дополнительных значений можно использовать цвет точек и их
размер. Если имеется только один дополнительный параметр, то он может быть
представлен одновременно и цветом, и размером.
Для построения таких графиков предназначена функция scatter (). Эта функция,
помимо первых двух параметров, аналогичных параметрам из функции plot (),
может принимать дополнительные параметры: s (задает размеры каждой точки в
Глава
28.
Построение с помощью библиотеки
Matplotlib
условных единицах, где одна единица равна
1/ 72
более сложных графиков
549
дюйма) и с (задает цвета каждой
точки). Параметры s и с могут быть последовательностями и содержать такое же
количество
элементов,
что
и
первые
два
параметра,
отвечающие
Кроме того, параметр s также может быть одним числом
-
за
координаты.
тогда все маркеры бу
дут иметь одинаковый размер. Аналогично, параметр с может являться строкой,
задающей цвет для всех маркеров, если они должны быть окрашены одним цветом.
Применение функции scatter () показано в листинге
этого скрипта
-
на рис.
28.2,
а результат выполнения
28.2.
import numpy as np
import matplotlib.pyplot as plt
count = 30
х = np.random.rand(count) * 10
у= np.random.rand(count) * 10
data = np.random.rand(count) * 30
plt.scatter(x, у, s=data * 5 + 1, c=data, marker="o", cmap="jet")
pl t. colorЬar ()
pl t. grid ()
plt.xlim(0, 10)
plt.ylim(0, 10)
plt.xlabel("X")
plt.ylabel("Y", rotation=0)
plt.tight_layout()
plt. show ()
Здесь массив data, который так же, как и массивы х и у, заполнен псевдослучай
ными числами, визуализируется с помощью размера и цвета. Значения коэффици
ентов для параметра s подобраны таким образом, чтобы ограничить максимальный
размер кружков, а также, чтобы при значении О они все-таки были видны (поэтому
к рассчитанным значениям еще добавляется единица).
Параметру с присваиваются непосредственно значения массива data. Масштабиро
вание цветового градиента осуществляется автоматически с учетом минимального
и максимального значений в указанных данных. Для расчета цвета применяется
цветовая схема, указанная с помощью параметра cmap (сокращение от
карта цветов). В библиотеку
Matplotlib
color map,
встроено большое количество градиентов,
названия и внешний вид которых можно найти на странице ее документации'.
В нашем примере используется довольно популярная цветовая схема "j et". Если
вас не устраивает ни одна из предложенных цветовых схем,
создавать свои.
1
См. https://matplotlib.org/stable/users/explain/colors/colormaps.html.
Matplotlib
позволяет
Часть
550
10
111. Python
для научных вычислений
•
25
+
8
•
•
б
•
•
20
у
i
4
••
2
15
•
•
•
о
о
2
10
-•
1
б
4
•
•
8
5
10
х
Рис.
28.2.
Использование функции
scat ter ()
для отображения рассеянных точек
С помощью параметра marker="o" мы указываем, что в качестве символов на плос
кости ХОУ собираемся использовать кружочки. Мы можем задействовать для этого
все те маркеры, что и при использовании функции plot () , описанные в предыду
щей главе. С помощью символов маркеров можно дополнительно визуализировать
еще какой-нибудь параметр- если разбить данные на несколько массивов по при
надлежности точек к какой-либо группе, и для каждого из них вызвать функцию
scatter ()
со своим видом маркера.
После вызова функции scatter () мы вызываем функцию colorbar (), чтобы вклю
чить отображение градиента, который в нашем случае размещается справа от осей.
Функция colorbar () в приведенном примере используется со значениями по умол
чанию, однако у нее есть множество дополнительных параметров, которые влияют
на внешний вид градиента и его расположение относительно графика.
Графики в полярной системе координат
Следующий вид графика, который мы рассмотрим,
-
это графики в полярной сис
теме координат. В такой системе координат каждая точка описывается координа
тами
0
(угол, откладываемый от положительного направления оси Х) и
r
(расстоя
ние от центра системы координат).
Полярная система координат используется, например, в радиотехнике, где с ее
помощью отображают диаграммы направленности антенн и микрофонов, а в ра
диолокации графики в этой системе применяются для отображения эффективной
площади рассеяния объектов.
Есть два способа построить график в полярной системе координат. Первый из них
заключается в использовании функции po l ar ()
из модуля ma tplotlib. pyplot, а
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
второй основан на применении уже знакомой нам функции pl ot ()
1юго метода класса
551
или одноимен-
Axes, но при создании осей тогда нужно указать, что система
координат будет полярная, а не декартова. Рассмотрим оба способа.
В следующем скрипте (листинг
28.3)
с помощью функции p o l ar ()
выполняется
построение графика функции:
r(0) = lsinc(l.5(0-n/3))1
на отрезке :
Листинг 28 . З . Chapter_28/example_OЗ/polar.py
import numpy as np
i mport matpl otlib .pyplot as pl t
theta = np.linspace(-np.pi, np.pi, 300) + np.pi / 3
r = np.abs(np.sinc(l.5 * (theta - np.pi / 3)))
plt .polar(theta, r)
plt.tight_layout ()
plt.show ()
В результате выполнения этого скрипта будет получен график, показанный на
рис.
28.3.
90•
о·
270'
Рис.
28.3.
Использование функции
p o lar ()
для построения графика в полярной системе координат
Часть
552
для научных вычислений
111. Python
Обратите внимание, что углы в функцию polar () передаются в радианах, хотя на
графике они отмечены в градусах.
Теперь рассмотрим второй, более объектно-ориентированный, способ создания та
кого же графика с предварительным созданием осей (листинг
Листинг
28.4).
28.4. Chapter_28/example_04/plot_polar.py
import numpy as пр
import matplotlib.pyplot as plt
theta = np.linspace(-np.pi, np.pi, 300) + np.pi / 3
r = np.abs(np.sinc(l.5 * (theta - np.pi / 3) ))
fig = plt.figure()
fig.add_subplot(l, 1, 1,
print(f"{type(ax)=)")
ах=
polar=Тrue)
ax.plot(theta, r)
ax.set_rmax{l.0)
ax.set_rticks(np.arange(0, 1.1, 0.2))
ax.set_thetagrids(np.arange(0, 360, 15))
plt.tight_layout()
plt. show ()
В этом примере мы изменили некоторые параметры по умолчанию, чтобы сделать
внешний вид полученного графика более наглядным (рис.
105°
90•
28.4).
75•
120·
135•
150°
зо·
1.0
165°
15°
180°
о·
195°
345•
210·
ззо·
225°
315°
240°
зоо·
255'
Рис.
28.4.
270°
285°
Результат выполнения примера из листинга
28.4
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
Рассмотрим подробнее приведенный в листинге
28.4
553
код. Сначала с помощью
функции figure () мы создали пустое окно и получили экземпляр класса Figure,
который был присвоен переменной fig. Затем для этого объекта вызывали уже зна
комый нам метод add _ subplot (), чтобы создать оси, но добавили именованный
параметр polar=True, благодаря чему создаются оси не для декартовой системы
координат, а для полярной. Для того, чтобы в этом убедиться, на следующей строч
ке в консоль выводится тип переменной ах:
type(ax)=<class 'matplotlib.projections.polar.PolarAxes'>
Класс PolarAxes также имеет метод plot (), которым мы и воспользовались. Затем
мы вызывали следующие методы класса PolarAxes для настройки внешнего вида
графика:
♦
set _ rmax () -
устанавливает предел по оси
r
(максимальное значение, которое
умещается на графике);
♦
set _ rticks () -
устанавливает риски вдоль оси
r
(расположение концентриче-
ских окружностей сетки);
♦
set_ thetagrids () -
настраивает внешний вид сетки по окружности.
Помимо методов, использованных в этом примере, класс PolarAxes содержит еще
множество
методов,
с
помощью
которых
можно
изменять
различные
элементы
графика. Вот только некоторые из них:
♦
♦
set_ theta_zero_location () set _ theta _ direction () -
позволяет изменять положение нуля угла
0;
позволяет изменять направление отсчета углов: про
тив часовой стрелки (это значение по умолчанию) или по часовой стрелке;
♦
set _ rmin () вдоль оси
позволяет ограничивать минимальное отображаемое значение
r.
В рассматриваемом примере мы сначала получили экземпляр класса Figure, а за
тем с его помощью создали экземпляр класса PolarAxes, но мы могли бы эти два
действия объединить в одно, вызвав функцию subplots ():
fig,
ах=
plt.subplots(subplot_kw={ 'projection': 'polar'})
С помощью этой функции мы одновременно можем создать фигуру и оси, а заодно,
воспользовавшись словарем, передаваемым ей в качестве параметра subplot_kw,
указать дополнительные параметры. Мы же передали ей здесь единственный до
полнительный параметр, указывающий, что система координат должна быть по
лярной.
Каким из рассмотренных способов создавать оси для построения графика в поляр
ной системе координат, зависит от предпочтений разработчика.
Столбчатые диаграммы
Под столбчатыми диаграмма.ми понимают такие графики, в которых значения не
которой величины визуализируются с помощью высоты столбиков. Для построения
столбчатых диаграмм в модуле matplotlib.pyplot предусмотрена функция bar(),
554
Часть
111. Python для
имеется одноименный метод и в классе Axes. В листинге
дания простейшей столбчатой диаграммы (рис.
Листинг
28.5
научных вычислений
приведен пример соз
28.5).
28.5. Chapter_28fexample_05/bar.py
import matplo t l ib.pyplot as plt
х =
1, 2, 4, 5, 8]
[О,
height
=
[0.1, 0.2, 0.4, 0 . 8, 0.6, 0.1]
plt.Ьar(x,
heiqht)
plt.tight_layout()
plt.show()
Здесь положения столбиков по оси Х задаются с помощью списка х, а их высоты
с помощью списка
-
height.
0.8
0.7
О.б
0.5
0.4
0.3
0.2
0.1
о. о
о
Рис.
2
4
28.5. Пример
б
использования функции
8
bar ()
Существует также очень похожая на функцию bar () функция barh (), отличие ко
торой заключается в том, что столбики рисуются не вертикально, а горизонтально
(листинг
28 .6). График,
28.6.
отображаемый в результате выполнения этого скрипта, по
казан на рис.
Листинг 28.6.
Chapter_28/example_06/barh.py
import matplotlib.pyplot as plt
у = [ О , 1 , 2 , 4, 5, 8]
height = [0. 1 , О .2, 0.4,
О
. 8,
О .
6,
О
.1]
Глава
Построение с помощью библиотеки
28.
Matplotlib
более сложных графиков
555
plt.barh(y, height)
plt.tight_layoutl)
plt. show ()
8
б
4
2
о
0.1
о. о
Рис.
28.6.
0.4
0.3
0.2
0.5
О.б
Пример использования функции
0.7
о. в
barh ()
bar () и barh () аналогичны по используемым параметрам, и поэтому в
дальнейших примерах этого раздела будет применяться только функция ьаr (), но
Функции
при необходимости их легко изменить и для вывода горизонтальных столбцов с
ПОМОЩЬЮ функции barh () .
При построении столбчатых диаграмм по горизонтальной оси могут располага
ются не только числовые данные. Например, если мы хотим построить график
зависимости посещаемости сайта от месяца в течение года, то по горизонтальной
оси вместо цифр можно написать названия месяцев. Функция bar () позволяет это
сделать, если вместо чисел в качестве первого параметра передать ей список
строк (листинг
28. 7).
Листинг 28.7. Chapter_28/example_07/bar_string_data.py
import matplotlib.pyplot as plt
dat.es =
["Янв .
11
,
"Июль",
visitors
=
1 'Фев. 11
,
1 'Март 11
,
11
,
11
Сент.
11
"Авг.
"Апр.
,
11
"Май",
,
11 Окт. 11
,
"Июнь",
11 Нояб.",
"Дек.")
[10_748, 12_610, 15_245, 14_468, 14_897, 10_441,
8_904, 9_301, 11_272, 14_083, 12_788, 11_748 )
plt.bar(dates, visitors)
pl t.grid()
plt.tight_layout()
plt.show()
Часть
556
111. Python
для научных вычислений
Обратите внимание, что в этом примере при заполнении массива
visi tors (посети
тели) для целых чисел используется разделение больших чисел знаком«_» (об этой
возможности было сказано в главе
как показано на рис.
Янв .
Рис.
2). В результате диаграмма будет выглядеть так,
28 .7.
Фев . Март Апр.
28.7.
май Июнь Июль Авr . Сент . Окт . нояб. дек .
Использование вдоль оси Х строковых данных
Строго говоря, если в качестве первого параметра передаются не числовые данные,
это равносильно тому, что столбики располагаются в точках
1, 2, 3
и т. д., но при
этом значения на оси заменены на текстовые метки. В этом можно убедиться, если
в коде листинга
28.7
заменить строку с вызовом функции bar () на следующие две
строки:
positions ~ np.arange(l, len(dates) + 1)
plt.bar(positions, visi to rs, tick_laЬel=dates)
Здесь для задания меток под рисками мы используем параметр tick_label. Сам
график внешне будет выглядеть точно так же, как показано на рис.
28.7.
Этой особенностью можно воспользоваться, чтобы для каждой метки по горизон
тали строить два и более столбика. Для этого надо вызвать функцию bar ( J не
сколько раз для каждого набора столбиков, но в качестве координат по оси Х каж
дому набору столбиков добавить небольшое смещение.
Покажем это на следующем примере (листинг
28.8),
где рядом с каждым столби
ком, обозначающим общее количество посетителей, добавляется количество посе
тителей, пришедших из поисковых систем (рис.
здесь использование метода
bar ()
из класса
Axes.
28.8).
Заодно продемонстрируем
Глава
28.
Листинr
Построение с помощью библиотеки
Matplotlib
более сложных графиков
28.8. Chapter_28/example_08/bar_visitors_search.py
import numpy as np
import matplotlib.pyplot as plt
dates =
["Янв.",
"Фев.",
11 Март",
"Июль",
' 1 Авг. 11
11
,
Сент.
11
"Алр.",
,
"Окт.",
"Май",
"Июнь",
"Нояб.",
"Дек."]
visitors = [10_748, 12_610, 15_245, 14_468, 14_897, 10_441,
8_904, 9_301, 11_272, 14_083, 12_788, 11_748]
search = [8 649, 10_431, 12_751, 12_375, 12_464, 8_610,
7_113, 6 652, 9_193, 11 689, 9 936, 9_497]
width = 0.3
positions = np.arange(l, len(dates) + 1)
fig = plt.figure()
fig.add_subplot(lll)
ах=
ax.bar(positions - width / 2, visitors, linewidth=l.0, edgecolor="k",
width=width)
ax.bar(positions + width / 2, search, linewidth=l.0, edgecolor="k",
width=width)
ax.set xticks(positions, dates, rotation=45)
ax.grid()
fig.tight layout()
pl t. show ()
11
14000
12000
10000
8000
6000
Рис.
28.8.
Использование нескольких вызовов функции
для отображения нескольких наборов данных
bar ()
557
Часть
558
111. Python
для научных вычислений
В этом примере следует обратить внимание на положение столбиков по оси Х. Для
его расчета нам надо знать ширину столбиков, которую мы задаем с помощью па
раметра width в методе bar (). Перед вызовами метода bar ()
positions со значениями от
I
до
12
создается массив
(по количеству месяцев в году), а затем в мето
де bar () для первой группы столбиков (посетители) из этих координат вычитается
половина ширины столбика width (половина, потому что по умолчанию значение
по оси Х задает положение центров столбиков), а для второго вызова метода bar ()
половина ширины прибавляется.
Кроме того, с помощью параметров linewidth=l. о и edgecolor="k" мы устанавли
ваем соответственно толщину линии обводки столбиков и ее цвет (черный). Цвета
заливки столбиков оставляем по умолчанию.
В завершение вызывается метод set_xticks (), который устанавливает положение
рисок по оси (по умолчанию
Matplotlib
может расставить риски не там, где бы нам
хотелось), присваиваем им метки, а заодно поворачиваем эти надписи на
45°,
чтобы
они не пересекались.
У подобных графиков есть еще множество настроек внешнего вида, однако нам
пора переходить к следующему виду графиков.
Круговые диаграммы
Круговые диагра.~о,ы используют, когда отображаемые данные все вместе образу
ют некую общность, в сумме составляющую
100%, -
чтобы показать долю ее час
тей по отношению к целому. Такие графики еще называют «пирогом»
(pie).
Возвращаясь к примеру с посетителями сайта, покажем на круговой диаграмме их
возрастной состав, разбив его на группы: младше
35-44
Для
года,
45-54
построения
года и старше
круговых
55
18
лет,
18-24
года,
25-34
года,
лет.
диаграмм
используется
функция
pie ()
из
модуля
matplotlib. pyplot или одноименный метод из класса Axes. Нарисуем сначала кру
говую диаграмму с настройками по умолчанию (листинг
Листинг
28.9).
28.9. Chapter_28/example_09/pie.py
import matpl ot li b.pyplot as plt
visitors = [2 _248, 34_260, 22_569, 9_778, 5_162, 3_373]
labels = ["<18", " 18-24", " 25 -34", "35-44", "45-54", "55+"]
plt.pie(visitors,
laЬels=laЬels)
plt.tight_ layout()
plt.show()
В результате выполнения этого скрипта будет выведен график, показанный на
рис.
28.9.
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
559
18-24
<18
55+
Рис.
28.9.
Пример использования функции
pie ()
для построения круговой диаграммы с настройками по умолчанию
Давайте подумаем, как можно придать этому графику большую наглядность:
1.
Сделаем так, чтобы первый сектор «пирога»:
<18 -
располагался сверху. Для
этого нужно добавить именованный параметр startangle=90 (угол, с которого
начинается отсчет секторов).
2.
Пусть при этом секторы располагаются по часовой стрелке, а не против нее. Для
этого надо добавить именованный параметр counterclock=False.
3.
Функция pie () позволяет автоматически рассчитывать процент каждого сектора
по отношению к сумме всех данных. Для этого нужно добавить именованный
параметр autopct (см. главу
4.
строку в формате, используемом вместе с оператором " %"
9).
Зададим с помощью параметра pctdistance расстояние от центра круга, на котором будут располагаться значения в процентах.
Применение перечисленных здесь параметров реализовано в следующем примере
(листинг
28.1 О).
Заодно этот пример показывает, что можно использовать метод
pie () из класса Axes вместо одноименной функции.
Листинг 28.10.
Chapter_28/example_10/pie_percent.py
import matplotlib.pyplot as plt
visitors = [2_248, 34_260, 22_ 569, 9_778, 5_162, 3_373]
labels = ["<18", "18-24", "25-34", "35-44", "45-54", "55+" ]
fig = plt.figure()
fig.add_subplot(lll)
ах=
560
Часть
111 . Python
для научных вычислений
ax.pie(visitors, labels=label s, startangle=90, countercl ock=False,
autopct=" %.lf%%", pctdistance=0. 85)
fig.tight_layout()
plt.show()
Обратите внимание на значение параметра
autopct=" %. lf%%".
Первый символ про
цента нужен, чтобы задать формат чисел. Следующий фрагмент:
«. 1 f» -
предпи
сывает вывести значение с точностью до одного знака после запятой. Два завер
шающих символа процента просто выводят знак
«%» -
здесь используется экрани
ровка с помощью символа процента, чтобы интерпретатор понял, что последний
символ процента не нужно воспринимать как символ форматирования.
В результате выполнения этого скрипта будет выведен график, показанный на
рис.
28.10.
55+
<18
18-24
Рис.
28.10.
Пример использования функции
pie () для
построения круговой диаграммы
с расчетом процентов и другими настройками внешнего вида согласно коду из листинга
18.1 О
Если нужно как-то выделить один или несколько секторов по сравнению с други
ми, их можно немного из «пирога» выдвинуть. Для этого используется параметр
explode,
который должен принимать последовательность из такого же количества
элементов, что и количество элементов в данных, и содержать величины, опреде
ляющие, на сколько должен быть выдвинут каждый сектор.
Чтобы продемонстрировать эту возможность, выдвинем на
0.2
единицы последний
сектор, отвечающий за самую пожилую часть аудитории сайта (рис.
го заменим вызов метода
pi e ( 1 в листинге 28. 1О на следующий:
ax.pie(visitors=labels, st artangle=90, countercl oc k=Fal se,
autopct=" %. lf %% ", pct di stance=0. 85 ,
explode=(O,
О,
О,
О,
О,
0.2))
28.11 ),
для че
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
561
55+
18-24
Рис.
28.11.
Пример использования функции
с применением параметра
explode
pie () для
построения круговой диаграммы
для выдвижения одного из секторов
Завершающий пример этого раздела (листинг
28.11)
показывает, как можно влиять
на внешний вид секторов «пирога». Для задания каждому сектору цвета использу
ется
параметр
colors, принимающий последовательность строк, описывающих
цвета. Если мы хотим, чтобы каждый сектор был окрашен своим цветом, то коли
чество элементов в этой последовательности должно равняться количеству секто
ров на графике.
Для изменения обводки каждого сектора нужно указать именованный параметр
wedgeprops (от англ.
wedge,
клин), который должен быть словарем, описывающим
внешний вид клиньев. Так, в коде листинга
28.11
ключ "edgecolor" со значением
"Ыасk" изменяет цвет обводки на черный, а ключ "linewidth" со значением 1 уве
личивает на единицу толщину линии обводки.
Листинг 28.11.
Chapter_28/example_11/pie_colors.py
import matplotlib.pyplot as plt
visitors = [2_248, 34_260, 22_569, 9_778, 5_162, 3_373]
["<18", "18-24", "25-34", "35-44", "45-54", "55+"]
labels
colors = ["ilff9999", "#66b3ff", "tl99ff99", "ilffcc99", "tlc2c2f0", "#ffb3e6"]
fig = plt.figure()
fig.add_subplot(lll)
ах =
ax.pie(visitors, labels=labels, startangle=90, counterclock=False,
autopct="%.lf%%", pctdistance=0.85,
Часть
562
111. Python
для научных вычислений
explode; (О, О, О, О, О, D. 2) ,
colors=colors,
wedqeprops = {"edgecolor": "Ьlack", "linewidth": 1})
fig.tight_layout()
plt.show()
Результат выполнения этого скрипта показан на рис.
28.12.
55+
18·24
Рис.
28.12.
Пример использования функции
с применением параметров
colors
pie () для
построения круговой диаграммы
для изменения цвета секторов и
wedgeprops
для изменения цвета и толщины их обводки
Построение трехмерных графиков
Пусть нам нужно отобразить зависимость, которая описывается следующей функ
цией от двух переменных:
z ( х, у) = sinc ( х / тт) •sinc (у/ тт)
на интервале:
х Е [-1 О; 1О], у Е [-1 О; 1О].
График такой функции будет трехмерным, и чтобы его построить, нам нужно соз
дать массив узлов сетки со всеми координатами х и у, в которых должны рассчиты
ваться значения
мы воспользуемся функцией meshgrid () из
библиотеки
z. Для создания сетки
NumPy. Чтобы понять, что
эта функция делает, попробуем сначала соз
дать сетку на небольшом массиве точек.
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
563
Представим, что наш график должен строиться на интервале:
а шаг сетки по каждой координате будет равен
1. Для задания сетки нам нужно ка
ким-то образом описать координаты (1, 4), (1, 5), (1, 6), (1, 7), (2, 4), (2, 5), и т. д. до
(3, 7), а затем в этих координатах рассчитать двумерную функцию z =f{x, у).
Для создания такой сетки воспользуемся функцией meshgrid () (листинг
28.12).
[л:~сти~г 28.12. fhapter_28/example_12/meshgrid.py
import numpy as np
xgrid, ygrid = np.meshgrid([l,2,3],
print ("xgrid: ")
print (xgrid)
print ("ygrid: ")
print(ygrid)
[4,5,6,7])
Функция meshgrid () принимает в качестве основных параметров несколько одно
мерных последовательностей (в нашем случае
-
два списка), а возвращает такое
же количество многомерных массивов с размерностью, равной количеству масси
вов на входе. В нашем случае функция вернет два двумерных массива.
Первый возвращаемый двумерный массив будет создан повторением элементов
первого переданного параметра в качестве строк, а второй возвращаемый массив
-
повторением элементов второго переданного параметра в качестве столбцов.
В результате выполнения приведенного в листинге
массивы
xgrid
и
ygrid,
28.12
примера мы получили
содержащие координаты х и у соответственно всех узлов
сетки, где будет рассчитана функция:
xgrid:
[ [ 1 2 3]
[1 2 3]
[ 1 2 3]
[ 1 2 3]]
ygrid:
[ [ 4 4 4]
[5 5 5]
[6 6 6]
[7 7 7]]
Применяя математические функции к массивам xgrid и ygrid, мы благодаря векто
ризации
NumPy
рассчитываем значения функции
z(x, у) сразу во всех узлах. На
пример, одно выражение:
z = np.sinc(xgrid / np.pi) * np.sinc(ygrid / np.pi)
рассчитает значения интересующей нас функции сразу во всех узлах двумерной
сетки. Если же мы хотим увидеть более плавный график в заданном нам диапазоне
Часть
564
111. Python
для научных вычислений
значений по осям Х и У, для расчета функции можно воспользоваться следующим
кодом:
х
= np.linspace(-10, 10, 100)
np.linspace(-10, 10, 100)
xgrid, ygrid = np.meshgrid(x, у)
z = np.sinc(xgrid / np.pi) * np.sinc(ygrid / np.pi)
у=
Теперь нам осталось разобраться, как по рассчитанным данным строить трехмер
ный график. В
Matplotlib
есть несколько видов трехмерных графиков, которые
строятся с помощью различных методов класса трехмерных осей Ахеsзо:
♦
метод plot_surface () -
строит трехмерную поверхность на основе
сетки.
Промежутки между узлами сетки будут заполнены цветом;
♦
метод plot_wireframe () -
строит «каркасный» график, представляющий собой
трехмерную сетку без заливки промежутков между узлами;
♦
с помощью методов bar () и barЗd () можно строить столбчатые диаграммы в
трехмерных осях;
♦
метод plot () -
строит линии в трехмерном пространстве;
♦ метод qui ver () -
♦
строит трехмерное векторное поле;
метод scatter () -
аналог одноименного двумерного метода для построения
диаграммы рассеяния, но в трехмерном пространстве.
Существует также еще множество других видов графиков, которые умеет строить
библиотека
Matplotlib.
Код, приведенный в листинге
28.13,
строит трехмерную поверхность с настройками
по умолчанию.
Листинге
28.1 З. Chapter_28/example_13/surface.py
import numpy as np
import matplotlib.pyplot as plt
х
= np.linspace(-10, 10, 100)
np.linspace(-10, 10, 100)
xgrid, ygrid = np.meshgrid(x, у)
z = np.sinc(xgrid / np.pi) * np.sinc(ygrid / np.pi)
у=
fig = plt.figure()
= fig.add_subplot (projection="Зd")
print(f"{type(ax)=)")
ах
ax.plot_surface(xgrid, yqrid, z)
fig.tight_layout()
plt. show ()
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
565
Разберемся более подробно, что делает этот код. Сначала с помощью функции
meshgrid () мы, как было показано ранее, подготовили сетку для расчета функции
двух переменных и рассчитали значение этой функции.
Следующий шаг
-
создание трехмерных осей. Мы сначала создали класс Figure,
вызвав функцию f igure (), а затем воспользовались методом add _ subplot (), указав
значение
именованного
параметра
projection="Зd",
чтобы
этот метод
создал
трехмерные оси. Для того, чтобы посмотреть, экземпляр какого именно класса мы
получили, мы вывели тип переменной ах в консоль:
type(ax)=<class 'mpl_toolkits.mplot3d.axes3d.Axes3D'>
Команды для создания фигуры и осей мы могли бы объединить в одну, вызвав
функцию subplots (), а чтобы эта функция создала трехмерные оси, ее нужно вы
зывать со следующим параметром:
fig,
ах=
plt.subplots(subplot_kw={"projection":
В завершение мы вызвали метод
"Зd"})
plot _ surf асе (), передав в него два массива, опи
сывающие сетку, и также рассчитанное значение функции на этой сетке. В резуль
тате был построен график, показанный на рис.
28.13.
1.0
о. в
0.6
0.4
0.2
о. о
1
-/4
-0.2
10.0
• ..,
7.5
5.0
2.5
7
о. о
-10 .07.5
- 5 -~2 5
• о. о
25
• 5.0
7 •5 10.0
Рис.
28.13.
-2.5
-5.0
-7.5
- 10.0
Пример использования метода
plot surface ()
для построения трехмерного графика с настройками по умолчанию
Этот график можно вращать мышкой, а также, если воспользоваться кнопками на
панели инструментов, которые здесь не показаны, его можно масштабировать и
перемещать.
Займемся теперь настройкой внешнего вида этого графика. Для этого мы рассмот
рим следующие параметры метода
♦
color и cmap -
plot_surface ():
позволяют менять цвет заливки поверхности;
Часть
566
♦
linewidth и edgecolor (или edgecolors) -
111. Python
для научных вычислений
позволяют менять толщину и цвет
отображаемой сетки на поверхности;
♦
rtcount и ccount или rstride и cstride -
позволяют менять шаг линий сетки,
по которой строится поверхность;
♦
shade и lightsource -
позволяют настраивать освещенность трехмерной по-
верхности.
Сначала попробуем поменять цвета заливки поверхности и обводки сетки, для чего
в коде листинга
28.13
в вызов метода plot_surface () добавим несколько пара
метров из числа только что упомянутых:
ax.plot_surface(xgrid, ygrid, z,
color="white",
edqecolor="Ьlack",
linewidth=0.5)
Теперь заливка поверхности должна стать белой, а линии сетки
(рис.
-
черными
28.14).
I
1.0
0.8
О.б
0.4
0.2
о.о
-0.2
10.0
7.5
5.0
2.5
о.о
-10,07 .5
-s.q__2 5
• о. о
25
• 5.0
7 •5 10.0
Рис.
28.14.
-2.5
-5.0
-7.5
-10.0
Трехмерная поверхность nосле изменения параметров заливки
и параметров сетки
Странно, вроде бы заливка должна быть белой, но почему-то график выглядит ско
рее серым. Это действительно так, потому что по умолчанию включено затенение
поверхности, чтобы поверхность казалась более объемной. Чтобы отключить зате
нение, нужно в метод plot_surface () добавить параметр shade=False:
ax.plot_surface(xgrid, ygrid, z,
color="whi te", edgщ:olor="Ьlack", linewidth=O. 5,
shade=False)
Вот теперь поверхность действительно белая, а объемность ей придают только из
гибы сетки (рис.
28.15).
Глава
28. Построение с помощью библиотеки Matplotlib более сложных графиков
567
1.0
0.8
О. б
0.4
0.2
о.о
-0.2
10 . О
7.5
5.0
2.5
о. о
-10.07.5
-5 .0_2 5
• о. о
25
'
5.0
7•5 10.0
Рис.
28.15.
-2 .5
-5.0
-7 .5
-10.0
Трехмерная поверхность после отключения затенения
Если мы все-таки оставляем затенение, то есть возможность влиять на освещение
поверхности с помощью параметра lightsource (источник света), но мы этот па
раметр рассматривать не будем.
Чтобы сделать график более информативным, мы можем задать цветовую карту с
помощью уже знакомого нам по функции
scatter () параметра стар, и тогда цвет
будет также обозначать значение функции.
Заменим в примере из листинга
28.13
вызов метода
plot_surface () на следующие
три команды:
surf = ax.plot_surface(xgrid, ygrid, z,
anap="hsv", edgecolor="Ьlack", linewidth=0.5)
print(f"{type(surf)=)")
plt.colorbar(surf)
Здесь мы добавили в вызов метода
plot_surface () параметр стар, передав в каче
"hsv". При использовании параметра стар зате
нение графика отключается, как будто мы передаем параметр shade=False.
стве значения имя цветовой карты
Метод
plot_surface () возвращает объект, отвечающий за поверхность, -
раньше
он нам был не нужен, поэтому мы его игнорировали. Теперь он нам понадобится,
чтобы добавить рядом с графиком градиент, показывающий соответствие цвета и
значения. Чтобы понять, какой класс возвращает этот метод, мы его выведем в кон
соль, в результате чего увидим следующую строку:
type(surf)=<class
'mpl_toolkits.mplotЗd.artЗd.PolyЗDCollection'>
Полученный объект нужно передать в функцию colorbar (). В результате будет
построен график, показанный на рис.
28.16.
Часть
568
111. Python
для научных вычислений
0.8
1.0
0.8
0.6
0.6
0.4
0.4
0.2
о. о
- 0 .2
0.2
10.0
7.5
5.0
2.5
- 5 ·q_2 5
•
- 2.5
- 5 .0
-7 .5
о. о 2 5
• 5.0 7.5
10.0
Рис.
28.16.
о. о
о. о
-10 •.07 .5
-10.0
-0.2
Применение градиентной заливки к трехмерной поверхности
Поговорим теперь о настройке сетки на поверхности. От чего зависит густота ли
ний на графике? Хочется сказать, что от количество точек в исходных массивах х и у.
На самом деле это не совсем так. Если мы захотим сделать график более плавным и
добавим в эти массивы больше точек:
х
= np.linspace(-10, 10, 500)
np.linspace(-10, 10, 500)
у=
то с удивлением увидим, что график не изменился. Дело в том, что по умолчанию
количество линий вдоль каждой оси ограничено значением
менить это значение, в методе
plot_surface ()
ры rcount и ccount (которые обозначают
50.
Для того, чтобы из
нужно указать при вызове парамет
row count
и
column count -
количество
строк и количество столбцов соответственно). Эти параметры определяют количе
ство линий сетки в каждом направлении. Чтобы заметить влияние этих параметров,
уменьшим количество линий сетки до
20
в каждом направлении:
ax.plot_surface(xgrid, ygrid, z,
color="white", edgecolor="Ыack", linewidth=0.5,
rcount=20, ccount=20,
shade=False)
В результате мы получим поверхность с более редкой сеткой (рис.
28.17).
Есть еще и второй способ повлиять на сетку. Вместо параметров rcount и ccount
можно передать параметры rstride и cstride, которые задают коэффициент про
реживания данных по каждой оси. Поскольку у нас изначально сетка данных соз-
Глава
28.
дана по
Построение с помощью библиотеки
100 точкам,
Matplotlib
более сложных графиков
то, задав параметры rstride и cstride равными
такой же график, что показан на рис.
28.17,
5,
569
мы получим
с двадцатью линиями по каждому на
правлению:
ax.plot_surface(xgrid, ygrid, z,
color="white", edgecolor="Ьlack", linewidth=0.5,
rstride=S, cstride=S,
shade=False)
1.0
0.8
0.6
0.4
0.2
о.о
-0.2
10.0
7.5
5.0
2 .5
о. о
-10 •.07 .5
-5 .0_2 5
-2 .5
• о.о
-5 . О
25
• 5.0
7•5 10.0
Рис.
28.17.
-7 .5
-10.0
Поверхность, построенная по двадцати линиям сетки
в каждом направлении
Трехмерный график пользователь может вращать с помощью мыши, но также есть
возможность программно установить поворот осей. Это полезно, когда заранее из
вестно, с какого ракурса трехмерный график смотрится наиболее наглядно.
Для поворота осей предназначен метод view_init () класса AxesЗD. Он принимает в
качестве двух параметров два угла: угол места и азимут, определяющие, с какого
направления мы будем смотреть на график,
места
-
-
то есть положение камеры. Угол
это угол подъема от горизонтальной плоскости ХОУ, а азимут
поворота относительно оси
Z.
-
это угол
Величины углов задаются в градусах.
Следующие две строки строят поверхность и устанавливают точку наблюдения,
задавая угол места (параметр elev), равный
5°,
и азимут (параметр azim), равный
45°:
ax.plot_surface(xgrid, ygrid, z,
color="white", edgecolor="Ыack", linewidth=0.5,
shade=False)
ax.view_init(elev=S, azim=45)
Поскольку угол места задан совсем небольшой, мы увидим поверхность с неболь
шого угла подъема (рис.
28.18).
Часть
570
111. Python
для научных вычислений
1.0
0.8
0.6
0.4
0.2
о. о
- 0 .2
5 1)~0 . О
-lCUJ .55~
-~ .02 .55_01.510 .0
Рис.
28.18.
502 ,50.0-2 .~ .
10.07,5 •
Поворот трехмерного графика с помощью метода
В завершение этого раздела посмотрим на работу метода
рый по сути очень напоминает метод
plot _ surface (),
view ini t ()
plot_wireframe (), кото
но строит не закрашенную
поверхность, а трехмерную каркасную модель.
Например, если в предыдущих примерах вместо вызова метода
plot_surface ()
написать следующие строки:
ax.plot_wireframe(xgrid, ygrid, z,
linewidth=l.O,
rstride=S, cstride=S)
edgecolor="Ьlack",
то будет нарисован график, показанный на рис.
28.19.
1.0
l
1
0.8
0.6
0.4
0.2
о.о
-0.2
10.0
7.5
5.0
2.5
-10 •.07 .5
- 5 ·°-2 5
• о. о
о. о
25
• 5.0
7•5 10.0
Рис.
28.19.
-2 .5
-5 .0
-7 .5
-10.0
Пример использования метода
plot_wireframe ()
для построения трехмерной каркасной поверхности
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
571
Линии уровня
Трехмерные поверхности смотрятся достаточно эффектно, но, к сожалению, они не
очень информативны из-за того, что по ним трудно определить значение функции в
конкретной точке или области. В случае, если по графику нужно определять кон
кретные значения функции, строят так называемые линии уровня, которые пред
ставляют собой линии пересечения трехмерной поверхности и плоскостей, парал
лельных плоскости ХОУ. Каждая линия уровня проходит через точки поверхности,
имеющие одинаковые значения по оси
Z.
По-другому линии уровня можно назвать
картами высот.
Принципы построения линий уровня такие же, что и для трехмерной поверхно
сти,
-
сначала нужно создать сетку, рассчитать значения функции на этой сетке, а
затем вызвать функцию contour () из модуля matplotlib. pyplot или одноименный
метод из класса
Axes.
Для более наглядной демонстрации этих принципов в качестве примера мы возь
мем десятичный логарифм от модуля функции двух переменных, которую исполь
зовали в предыдущем разделе, и теперь функция будет описываться следующим
выражением:
z(x,y) = 20-lg(Jsinc(x/ те)· sinc(y/ те)!)
Интервал построения функции останется прежним:
х Е [ -1 О; 1О], у Е [-1 О; 1О]
о
-20
-40
-80
-100
10.0
7.5
5.0
2.5
о.о
- 10.07 .5
- 5 ·~2 5
• о. о
-2 .5
-5 .0
-7 .5
25
• 5.0
7•5 10.0
Рис.
28.20.
-10 .О
Трехмерная поверхность, соответствующая функции,
для которой будут строиться линии уровня
Чтобы вы могли лучше представить себе, как выглядит эта функция, на рис.
28.20
показана соответствующая трехмерная поверхность, построенная с помощью мето-
Часть
572
111. Python
для научных вычислений
да plot surface (). Обратите внимание на недостаток этого графика
-
без его
вращения совершенно не очевидно, что максимальное значение на нем равно О.
Сначала построим линии уровня нашей функции с параметрами по умолчанию
(листинг
Листинг
28.14).
28.14. Chapter_28/example_14/contour.py
import numpy as пр
import matplotlib.pyplot as plt
= np.linspace(-10, 10, 100)
np.linspace(-10, 10, 100)
xgrid, ygrid = np.meshgrid(x, у)
z = 20 * np.logl0(np.abs(np.sinc(xgrid / np.pi) *
np.sinc(ygrid / np.pi)))
х
у=
fig, ах= plt.subplots()
ax.contour(xqrid, yqrid, z)
fig.tight_layout()
plt. show ()
В результате выполнения этого скрипта откроется окно с графиком, показанным на
рис.
28.21.
Общее впечатление о форме поверхности такое отображение дает, но
где же обещанное удобное представление, помогающее определить значения функ
ции? В графике, построенном с параметрами по умолчанию даже не понятно, каким
уровням соответствуют отображаемые линии.
10.0
.::,;
,с,;
7.5
5.0
2.5
О.О
-2.5
-10.0
-10.0
Рис.
28.21.
-7.5
-5.0
-2.5
о.о
2.5
5.0
7.5
Линии уровня, построенные с помощью метода
с настройками по умолчанию
10.0
contour ()
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
573
Чтобы сделать этот график более понятным, мы можем указать, на каких именно
уровнях нужно проводить линии, а также включить отображение значений на этих
линиях.
Для задания конкретных уровней мы должны передать их список в качестве допол
нительного именованного параметра levels. При этом важно, чтобы уровни, пере
данные в качестве параметра levels, были отсортированы по возрастанию. Пара
метр levels может принимать также и целочисленное значение
тогда
-
Matplotlib
автоматически подберет такие значения уровней, чтобы их количество было равно
указанному значению, но обычно лучше указывать конкретные характерные уров
ни, которые нас интересуют.
Для добавления меток
clabel ()
класса
к линиям
QuadContourSet,
уровня мы
можем
воспользоваться
экземпляр которого возвращает метод
методом
contour ().
У становим свои уровни и добавим метки:
levels = [-50.0, -30.0, -20.0, -13.3, -6.0, -3.0, -0.05]
cs = ax.contour(xgrid, ygrid, z, levels=levels)
cs.clabel(colors='Ьlack', fmt='%.lf')
В метод clabel () мы добавили два именованных параметра:
♦
с помощью параметра colors мы указали, что все метки должны быть черными
(по умолчанию они раскрашиваются в тот же цвет, что и линия, к которой отно
сятся);
♦
с помощью параметра fmt задан формат вывода чисел.
Полученный результат показан на рис.
28.22.
Такой график уже более информативен.
i:::г--:::~-!_"-t~i?o:3-s[Oo].0~5~~:::::ec;;:~~;;;_~~:::-s~o.:o:o:~-.~~c=~-O::j-:--.l
5.0
________1,,..-"::::=.... ~~Rii;;~~~
~i?.____
2 .5
о. о
-2.5
( ..,'!~
V
~==:.'?·
.. '
о
·о
'
~~~~~~~
~
~
(
i
\_,,
,
~
-5.0
'!>i?o
;С
-7.5
-10.0 .l-~~-~_,~O:O_~.C:,~3щJJ::::;:;:::~..L.~·~~.._;-~50~.Q_O~_J
- 10.0
- 7 .5
-5.0
-2.5
7.5
5.0
10.0
о. о
2.5
Рис.
28.22.
Линии уровня, построенные на заданных уровнях
и с добавлением меток
Часть
574
Для
изменения
градиента
цветов,
как
и для
111. Python для
предыдущих
научных вычислений
графиков,
в
метод
contour () можно добавить именованный параметр cmap, а если надо сделать линии
уровня одного цвета, то передать параметр colors, равный строке, описывающей
нужный цвет. Параметр colors также может быть списком цветов, и тогда линии
уровня будут окрашены в цвета из этого списка. Если список цветов будет содер
жать меньше элементов, чем количество уровней, то цвета станут циклически по
вторяться.
Если с помощью параметра colors установить для всех линий уровня одинаковый
цвет, то изменятся стили линий: для положительных значений уровня они будут
сплошными, а для отрицательных
-
штриховыми.
Изменить стили линий можно с помощью параметра linestyles, который может
быть либо строкой, либо списком строк, задающих стиль линии в том же формате,
что и для функции plot (), о которой речь шла в предыдущей главе.
Для изменения толщины линий предназначен параметр linewidths. Он также мо
жет быть одним значением типа float или последовательностью с элементами типа
float.
Для демонстрации этих параметров изменим вызов метода contour ():
levels = [-50.0, -30.0, -20.0, -13.3, -6.0, -3.0, -0.05]
cs = ax.contour(xgrid, ygrid, z, levels=levels,
colors='Ьlack', linestyles='-', linewidths=0.5)
cs.clabel(colors='Ьlack',
fmt='%.lf')
Результат такого вызова показан на рис.
28.23.
7.5
5.0
2 .5
о. о
-2.5
-5 .0
- 7.5
-7 .5
Рис.
28.23.
-5 .0
- 2 .5
о. о
2.5
5.0
7.5
10.0
Линии уровня после изменения их цвета и толщины
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
575
Отображение векторов
Графики, изображающие векторные поля, используются во многих областях физи
ки
-
например, при визуализации электрического или магнитного полей. Для по
строения
векторного
поля
предназначена
функция
qui ver ()
из
модуля
matplotlib. pyplot, а также одноименный метод из класса Axes. В этом разделе мы
рассмотрим только тот случай, когда векторное поле отображается на плоскости,
однако метод
qu i ve r () для класса осей Ахе s з о позволяет строить векторное поле в
трехмерном пространстве.
Для примеров построения векторного поля нам снова понадобится сетка, в узлах
которой должны быть рассчитаны значения векторной функции. Подразумевается,
что векторная функция рассчитывается в декартовой системе координат,
-
то есть
такую функцию можно представить в следующем виде:
](х,у) = и(х,у) ·Х0 + v(x,y) •Уо
где х 0 и
Функция
ji0
-
единичные векторы (орты).
qu i ve r () в качестве первых двух параметров принимает двумерные мас
сивы, описывающие сетку, а в качестве третьего и четвертого параметров ей нужно
передать двумерные массивы и(х,у) и
v(x,y),
описывающие проекции векторов на
орты в каждой точке сетки.
Для примера (листинг
28.15)
построим векторное поле, описываемое функцией:
f- ( х,у )
= х •е
-,·'!3
•х- 0
+у· е
-г'•IО
•у- 0 ,
где r = Jx 2 + у2 - расстояние от центра системы координат.
Листинг 28.15.
Chapter_28/example_15/quiver.py
import numpy as np
import matplotlib.pyplot as plt
np.linspace(-5, 5, 21)
np.linspace(-5, 5, 21)
xgrid, ygrid = np.meshgrid(x,
х =
у=
r2 = xgrid**2 + ygrid**2
u = np.exp(-r2 / 3) * xgrid
v = np.exp(-r2 / 10) * ygrid
fig, ах= plt.subplots()
ax.quiver(xqrid, yqrid, u, v)
ax.grid()
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
у)
Часть
576
для научных вычислений
111. Python
ax.set_xticks(np.arange(-5, 6, 1))
ax.set_yticks(np.arange(-5, 6, 1))
fig.tight_layout()
plt. show ()
Результат выполнения этого скрипта показан на рис.
5
!
\
!
4
l
l
.
•
.'
__..___LJ
з
1
2
~
;
1
1
о
♦
♦
•
-1
1
l
,
.r
1
1
!
't
1
-5
-5
-4
Рис.
,,
/
' I
I
1
't
'
'
."
-4
.,,
\
'
,,
/
~I
1
,
~ ,-- 1
'
//
,
t
-2
28.24.
\L
~
it
•
,_t
/-
♦
•
♦
1
'\.
........
'
~f
1
'
1
о
.
f t •
' "
\
\
1
;
r
.
♦
'
'
'
♦
•t
t
; ''
т
t
'
з
2
Пример использования метода
!~
\ ~
♦
-1
•
• •
• •♦
'/ '/ L _,,,I _LJ_,
,, ~
/ //
1i
-3
♦
t
_i '
;
t
t
{
' '
f
-з
♦
'
!
-1
-2
•
•
t
28.24.
4
5
qui ver ()
для отображения векторного поля
Въедливый читатель, глядя на этот график, может возмутиться или начать искать
ошибку в программе. Дело в том, что длины отображаемых стрелок не соответст
вуют значениям нашей функции. Например,/(1,1)=0.51.х0 +0.82ji0 ,нo проекции
стрелки, исходящей из точки
умолчанию функция quiver ()
( 1, 1),
на оси Х и У явно короче. Действительно, по
масштабирует длины векторов, чтобы они лучше
смотрелись в окне в ущерб точности отображения, сохраняя правильные соотноше
ния длин между векторами. Для многих задач это не является проблемой, если век
тора обозначают величины, отличные от длины,
-
например, вектор напряженно
сти электрического поля, единица измерения для которого В/м.
Угол поворота стрелок тоже не совсем корректный, если считать его относительно
координатной сетки. Чтобы в этом убедиться, нужно запустить скрипт и, изменяя
ширину окна, наблюдать за стрелками векторов. Мы увидим, что при изменении
единичной длины по оси Х стрелки сохраняют свой угол поворота относительно
окна, однако получается, что при таком масштабировании угол поворота стрелок
меняется относительно системы координат.
Глава
28.
Построение с помощью библиотеки
Matplotlib
577
более сложных графиков
Такое поведение можно исправить, добавив три дополнительных параметра в метод
quiver():
ax.quiver(xgrid, ygrid, u, v, scale=l, scale_units="xy", angles="xy")
Параметр scale задает коэффициент масштабирования векторов. Если мы хотим,
чтобы они соответствовали тем значениям, что у нас получаются из расчета, уста
новим коэффициент масштабирования равным
1.
Однако параметра scale не достаточно для корректного отображения. Нужно уста
новить, в каких единицах задаются длины векторов. По умолчанию длина
1 соот
ветствует длине оси Х. Чтобы указать, что единица длины должна совпадать с еди
ничным отрезком по каждой оси, надо параметру scale_units присвоить значение
"ху". Возможны и другие единицы измерения
-
они описаны в документации, и
мы их рассматривать не станем.
Чтобы исправить ситуацию с поворачивающимися при масштабировании вектора
ми, нам надо указать, что угол поворота векторов должен быть привязан к коорди
натам по осямХи У. Для этого мы передаем параметр angles со значением "ху".
Векторное поле, построенное с учетом сделанных исправлений, показано на рис.
5
28.25.
-г---r--г---,--,----.----.г-r--.----т---г---------,
~ш= i1
4
3 -
._
1
r;
/✓/,,,
; .
- --- - - - - ; - o ---+-- - r - - 1- -- - ~ .
о
-4
1
♦
'
•
-s+---+--~--+-......,__-+___,_,,__..L--+---'---+---г---r-----i
-5
Рис.
28.25.
-3
-4
-2
-1
о
1
2
3
4
5
Векторное поле после использования корректирующих параметров
scale uni ts
и
scale,
angles
Полученный график выглядит более точно с математической точки зрения, однако
такая математическая точность не всегда приводит к наглядному результату.
Функцию quiver () можно использовать и для отображения отдельных векторов,
если
в
качестве
первых
четырех
В следующем примере (листинг
параметров
28.16)
передавать
числа,
мы нарисуем три вектора:
А= 3.х0 + Уо, В= -Х0 + 2у0 И С= А+ В.
а
не
массивы.
Часть
578
Листинг 28.16.
111. Python для
научных вычислений
Chapter_28/example_16/vector.py
import numpy as np
import matplotlib.pyplot as plt
1.0])
np.array( [-1.0, 2.0])
А= np.array([З.0,
В=
С
=А +
fig,
В
ах=
plt.subplots()
ax.quiver(0, О, А[О], A[l],
color='r', scale=l, scale_units='xy', angles='xy')
ax.quiver(0, О, В[О], B[l],
color='g', scale=l, scale_units='xy', angles='xy')
ax.quiver(0, О, С[О], C[l],
color='b', scale=l, scale_units='xy', angles='xy')
ах. text (А[О], A[l],
ax.text(B[0], В [1],
ax.text(C[0], С [1],
ax.set - xlim(-4, 4)
ax.set - ylim(-1, 4)
'А''
'В''
'С',
fontsize=l4, color=' r')
fontsize=l4, color='g')
fontsize=l4, color='b')
ax.grid()
fig.tight_layout()
plt.show()
В этом примере важно не забыть установить правильный масштаб стрелок, как мы
это сделали в предыдушем примере. Результат выполнения этого скрипта показан
на рис.
28.26.
Есть еше один способ рисовать стрелки
са Axes. В примере из листинга
28.16
-
воспользоваться методом а r
row ( ) клас
строки с вызовом методов quiver () можно
заменить на следующий код:
ax.arrow(0, О, А[О], A[l],
head_width=0.l, head_length=0.2, length_includes_head=True,
color='r', linewidth=2)
ax.arrow(0, О, В[О], B[l],
head_width=0.l, head_length=0.2, length_includes_head=True,
color='g', linewidth=2)
ax.arrow(0, О, С[О], C[l],
head_width=0.l, head_length=0.2, length_includes_head=True,
color='b', linewidth=2)
Глава
28.
Построение с помощью библиотеки
Matplotlib
более сложных графиков
579
4-т-------т--------------------,
3
2
А
1
о
-1
+-----т-----т-----т-----,-----,-----т-----т-------1
--4
-3
-1
-2
Рис.
28.26.
о
2
Испольэова!-tие метода
3
4
qu i ve r ( )
для отображения отдельных векторов
Результат будет выглядеть примерно так же, поэтому для экономии места получае
мый график мы здесь приводить не будем.
В этом коде используются параметры head_width и head_length, которые задают
ширину и длину наконечников стрелок. Кстати, метод quiver () тоже имеет анало
гичные параметры, но называются они
Если параметр
headwidth
и
headlength.
length_includes_head равен True, то это означает, что длина нако
нечника стрелки включается в общую длину вектора, а если это значение равно
False (значение по умолчанию), то вектор окажется длиннее на длину наконечника.
Параметры color и linewidth задают цвет и толщину линии вектора соответственно.
Заключение
В этой достаточно объемной главе мы научились создавать еще несколько типов
графиков с помощью библиотеки
Matplotlib.
Сначала мы строили диаграммы рассеяния с помощью функции plot (), а затем
-
с помощью функции s са t te r () , позволяющей визуализировать больше информа
ции о каждой точке, изображенной на плоскости.
Мы научились строить график в полярной системе координат с помощью функции
polar (), а также с помощью метода plot () класса Axes, если при создании осей
указывается, что они должны быть созданы для полярной системы координат.
С помощью функции bar () мы строили столбчатые диаграммы и настраивали их
внешний вид, упомянув при этом, что с помощью функции barh () можно строить
Часть
580
111. Python
для научных вычислений
подобные диаграммы, когда столбики вытянуты не вертикально вдоль оси У, а го
ризонтально
-
вдоль оси Х.
С помощью функции pie () или одноименного метода класса Axes мы строили кру
говые диаграммы в виде «пирога» и также обсудили некоторые параметры, влияю
щие на их внешний вид.
Большой раздел этой главы посвящен построению трехмерных графиков. Для их соз
дания предварительно нужно создать сетку с помощью функции numpy .meshgrid (),
затем в узлах этой сетки рассчитать значения функции, после этого получить эк
земпляр
класса
трехмерных
осей
AxesЗD,
и,
наконец,
использовать
метод
plot surface () полученного объекта осей для построения поверхности или метод
plot_wireframe () для построения каркасной модели.
Часто для анализа функции от двух переменных лучше строить не трехмерную по
верхность, а линии уровня. Для этого предназначена функция contour () или одно
именный метод класса Axes. Однако, чтобы линии уровня были достаточно инфор
мативны, нужно внимательно подходить к настройке уровней для таких линий.
В завершающем разделе главы мы обсудили вопросы, связанные с рисованием век
торного поля и отдельных векторов. Мы сначала использовали метод quiver ()
класса Axes, после чего изучили методы решения проблемы адекватности отобра
жаемых стрелок рассчитанным значениям. В конце раздела мы научились строить
отдельные векторы с помощью того же метода
arrow ()
класса
qui ver (),
а затем с помощью метода
Axes.
Изучая материал этой главы, надо иметь в виду, что в ней рассмотрены далеко не
все виды графиков, которые можно строить с помощью библиотеки
Matplotlib.
На
ее официальном сайте 2 можно найти огромное количество впечатляющих примеров
построения самых разнообразных графиков. Однако общие принципы построения
других видов графиков остаются такими же, что и для тех графиков, которые мы
изучили в этой и предыдущей главах.
К этому моменту мы познакомились уже с двумя библиотеками, которые, скорее
всего пригодятся, если
Python
используется вами для научных вычислений. В сле
дующей главе мы поработаем с еще одной важной для научного сообщества биб
лиотекой
2
- Pandas,
которая позволяет работать с табличными данными.
См. https://matplotlib.org/staьte/gallery.
- ГЛАВА 29-
ЗнаКОМСТВО с
Pandas -
Pandas
это еще одна популярная библиотека, которая используется в научных
вычислениях. В отличие от библиотеки
сивами и однородными данными,
NumPy, ориентированной на работу с мас
Pandas предназначена для работы с таблицами и
разнородными данными, которые невозможно или не имеет смысла интерпретиро
вать как матрицы. При этом с помощью
Pandas
можно выполнять все те же опера
ции, что и в приложениях для работы с электронными таблицами,
Microsoft Excel
и
LibreOffice Calc.
-
такими как
Это может быть сбор статистических данных,
фильтрация и сортировка строк таблиц, а также объединение таблиц, как это часто
делают с базами данных
SQL.
Кстати,
Pandas
умеет загружать данные и из файлов
электронных таблиц.
Установка библиотеки
При установке
Pandas
Pandas
есть одна особенность. Можно выполнить стандартную ко
манду для установки библиотеки в
Python:
> python -m pip install pandas
В этом случае будет установлена сама библиотека с минимальным количеством
зависимостей (среди которых и
возможностей
Pandas
NumPy).
Однако для реализации некоторых своих
использует дополнительные сторонние библиотеки, которые
можно будет установить отдельно, когда они потребуются, но можно установить и
сразу при установке
Pandas.
Для этого нужно указать, какие дополнительные воз
можности нам нужны. На странице документации библиотеки I приводится полный
список таких возможностей, а здесь для примера упомянем лишь некоторые из них:
♦
plot вах
♦
будет установлена библиотека
performance числений:
♦
о которой мы говорили в гла
excel -
будут установлены библиотеки для высокопроизводительных вы
NumExpr, Numba, Bottleneck;
будут установлены пять дополнительных библиотек, предназначенных
для работы с файлами
1
Matplotlib,
27 и 28;
Microsoft Excel;
См. https://pandas.pydata.org/docs/getting_started/install.html.
Часть
582
♦
111. Python
для научных вычислений
будет установлена библиотека РуТаЫеs, предназначенная для работы с фай-
hdfS -
лами формата
HDF5,
и несколько других. Про файлы
мы говорили в главе
HDF5
26.
Таким образом, если мы знаем, что нам, например, понадобятся библиотеки для
визуализации данных, работы с файлами
Pandas
HDF5
и
то можем установить
Excel,
следующей командой:
> python -m pip install pandas[plot,hdfS,excel]
В квадратных скобках указываются группы зависимостей. Обратите внимание, что
в квадратных скобках не должно быть пробелов после запятых.
Если же у нас нет цели экономить место на диске, и мы готовы немного подождать
завершения процесса установки, можно установить
Pandas
со всеми дополнитель
ными библиотеками с помощью команды:
> python -m pip install pandas[all]
В этом случае процесс установки может занять несколько минут.
Чтение файлов в формате
26,
В главе
CSV
когда речь шла о файлах формата
CSV,
было сказано, что если нужно
работать с такими файлами, лучше использовать библиотеку
Pandas.
Именно этим
мы сейчас и займемся. Сразу начнем с интересных примеров. В Интернете можно
найти отличный набор данных, содержащих краткую информацию обо всех извест
ных запусках космических аппаратов с
и
будем
пользоваться
в
этой
главе.
1957
года по
2022
Единственный
год 2 . Этими данными мы
момент,
который
нужно
учесть,
-
UTF-8,
и для дальнейших примеров файл был в эту кодировку перекодирован.
исходный скачиваемый файл space_missions.csv сохранен не в кодировке
Открыв скачанный файл
CSV
в приложении электронных таблиц (рис.
29.1),
мы
увидим столбцы, содержащие следующие данные: организация, выполнявшая за
пуск, место запуска, дата и время запуска, тип ракеты, название миссии, использу
ется ли указанная ракета на момент составления файла, бюджет миссии
(если
из
вестен), статус миссии (успех или провал).
Соmрму
RVSNUSSR
RVSNUSSR
USN8Yy
......
us..,,
.....
USN&Y)'
.....
10 RVSN USSR
......
,
Slt• 115, 881Conur Cosmodtorne, КUakhlUln
sa, 1.15, 8al(onur Ca.modfome. каzмnмаn
LС· Щ C,ipt c.n.v.r11 .ЧS, ~ USA
LC·28A.. C,ipt ~ a l AFS, Aorlб&, USA
LC-18A. С11ре c.n.v.ra1 AFS, Aoflda. USA
LC·28A, Ctflt CanlVefal AFS. Ftonda. USA
LC-18A. Cllf» C8nawral AFS, Rol1da. USA
LC-5. Саре C8nlivef81AFS, Florlcla, USA
511• 1.15, Bdlonur Coamodromt, ~ W I
Рис.
29.1.
,,.,.D
....1957-10-0,18'2800
1951"11-0302"30 00
1957·12-о& 18 44 00
1'58,02-0103 А8 00
195&-02-о507;1Э 00
1~-0)-0518 77 00
1D!58-Q3.1712.1500
185&-ОЗ-U 17·38 00
Иt58,,О,4-27 ot 01 00
...... .....-·•...
-·•
.._,
..-·
"""'
_,
..,.,.,.
"""'
·-·
"""'
·-·
Spunk81<71PS
SpucnllllК71PS
•-тvэ
Explorмl
V8/llg!JllfdТV38U
s
s
•Э•l
-.................
.....
·- ·-G
Rodl8tl&8Ws Prlc8
......
......
......
......
......
......,
......
МlalNOn1!81u8
,
,
,....
Первые записи с данными в файле space_missions.csv
Напишем первый скрипт, который будет читать эти данные из этого файла (лис
тинг
29.1).
Здесь и далее мы будем считать, что файл space_missions.csv расположен
в текущем рабочем каталоге.
2
См. https://www.kaggle.com/datasets/mysarahmadbhat/space-missions.
Глава
29. Знакомство с Pandas
Лмстинr 29.1.
583
Chapter_29/example_01/read_csv.py
import pandas as pd
data = pd. read_csv ("space _missions. csv")
print(data)
print(f"(type(data)=)")
Обратите внимание: при импорте модуля
Для чтения данных из файла в формате
pandas ему принято давать псевдоним pd.
CSV
мы используем функцию read_csv ()
из этого модуля. Она имеет достаточно много дополнительных параметров, с неко
торыми из них мы скоро познакомимся, а пока применим эту функцию со значе
ниями по умолчанию.
Функция
read_csv () возвращает объект класса DataFrame, в чем можно убедиться,
взглянув на последнюю строчку результата выполнения скрипта, приведенного на
рис.
29.2.
Этот класс
-
краеугольный камень библиотеки
Pandas;
большинство
операций, которые мы будем здесь рассматривать, являются методами этого класса.
Количество отображаемых в выводе столбцов зависит от ширины окна консоли:
если ширины окна не будет хватать для отображения всех столбцов, то столбцы в
середине таблицы будут пропускаться, а вместо них выводиться знаки многоточия.
Co•penv
RVSN USSR
Rll'SN USSR
~
1
~
~
~
us Navy
Al'tBA
US Navy
~62!
~626
'i627
~628
~629
Spac,x
CASC
Spac,x
CAS Space
CASC
location
S1t1 1/'5, Baikonur cos■odr-o■t, ka11khstan
S1te 1/!1, Balkonur
LC-18A,
LC-26A,
LC-18A,
Саре
Саре
С1р1
Cos1odroн,
Т1 ■ 1
Dete
Canav1r1\ AFS, F\.orlda, USA 19!17-12-06
Canaverel AFS, Ftor1da, USA 1958-02-01
Canav1r1t AFS, Ftor1Cla, USA 1958-82-85
16:44:88
03:48:80
87:33:88
2822-87-22
2822-07-2ti
2822-87-2.ti
2822-87-27
2822-87-29
13:38:88
Bli:12:88
13:28:88
SLC-4E, Vendenberg
LC-101, Wtnchang Satett1te
LC-39A, кennedy Space
Jiuquan Satettite
LC-3, X1chang Satettlte
Rocket
Sputn1k 8K71PS
Sputnik BK71PS
van9uard
Juno I
V1ngu1rel
19!17-18-84 19:28:88
Kazakhstan 19'57-11-03 82:38:80
SF8, Cat1torn1a, USA
Launch Ctnt,r, Ch1na
center, Ftor1Cla, USA
Launch Ctnter, Chtna
Launch Center, Ch1na
17:39:88
GЬ:22:88
H1ss1on RocketStatus
Sputnlk-1
Ret1red
Rtt1rtd
Sputnlk-2
vanguard ТV3
Rttlrtd
Exptorer 1
Ret1red
Vanguarel T\138U
R1ttree1
F1tcon 9 Btock 5 Starttnk Group 3-2
Long ttarch 58
Wtntian
Fatcon 9 Btock !5 Start1nk Group 'i-25
Ое•о Ft1ght
Zhon9k1-1A
Long кarch 20
Yaogan 35 Group 83
Act1Yt
Act1ve
Act1vt
Act1Y8
Act1ve
Prlc1 "1ss1onStatus
Success
success
Fallurt
Success
Fatture
•••
•••
•••
•••
•••
67
•••
•••
67
29. 75
Success
Success
Success
Success
Success
{4638 rows х 9 cotu1n1}
typ1 (d1t1)нctass 'pandas .cort. tr11t .DataFra111 '>
Рис.
29.2.
Результат чтения файла space_missions.csv, выводимый в консоль
(при большом количестве строк
Pandas
показывает только первые
и последние строки прочитанной таблицы)
Обратите внимание, что библиотека
прочитанный из первой строки
Pandas присвоила каждому столбцу заголовок,
файла CSV. Помимо этого, в таблице появился
столбец с нумерацией строк (столбец индекса), которого не было в исходных дан
ных. Скоро мы увидим, что при обращении к столбцам мы можем использовать как
их индекс, так и указанный заголовок.
С помощью метода info () класса DataFrame мы можем получить полезную инфор
мацию о прочитанных данных (листинг
Листинr
29.2).
29.2. Chapter_29/example_02/data_lnfo.py
import pandas as pd
data = pd.read_csv("space_missions.csv")
print(data.info())
Часть
584
111. Python
для научных вычислений
В результате выполнения этого скрипта будет выведен следующий текст:
<class 'pandas.core.frame.DataFrame'>
Rangeindex: 4630 entries, О to 4 629
Data columns (total 9 columns):
Non-Null Count Dtype
# Column
-------------Company
4630
l
Location
4630
2 Date
4630
4503
3 Time
4 Rocket
4630
5 Mission
4630
6 RocketStatus
4630
7
Price
1265
MissionStatus 4630
8
dtypes: object(9)
memory usage: 325.7+ кв
None
о
non-null
non-null
non-null
non-null
non-null
non-null
non-null
non-null
non-null
object
object
object
object
object
object
object
object
object
Как можно здесь видеть, метод info () класса DataFrame выводит информацию об
общем количестве записей (строк) и о количестве столбцов таблицы. Помимо име
ни, для
каждого столбца
выводится
количество nоt-null-значений
строк, для которых в этом столбце указаны какие-то данные).
(количество
Pandas
умеет рабо
тать с неполными данными, и поскольку далеко не для всех космических запусков
известен их бюджет, то именно в этом столбце есть много пропущенных данных.
На рис.
29.2
они обозначены как
NaN (Not-a-Number).
Кроме того, данные в каж
дом столбце при необходимости могут быть преобразованы к какому-то типу. По
скольку мы еще не включили преобразование к типам, все столбцы имеют общий
ТИП
object.
Прежде чем приступить к более подробному изучению класса DataFrame, рассмот
рим некоторые дополнительные параметры функции read csv () :
♦
usecols -
♦
names -
♦
позволяет загружать данные только из выбранных столбцов;
позволяет задавать собственные имена заголовкам столбцов;
converters -
позволяет преобразовывать данные в выбранных столбцах к вы
бранному типу;
♦
parse_dates -
позволяет интерпретировать данные в выбранных столбцах как
даты.
Сначала посмотрим, как работает параметр usecols. Он используется, если нам не
нужны все столбцы, записанные в файле,
-
необходимо
столбцы
оставить,
а
все
остальные
параметр указывает, какие столбцы
будут отброшены.
usecols может принимать или список строк, где каждый элемент списка
вок столбца, или список целых чисел, где каждый элемент списка
ца, начиная с О.
-
Параметр
-
заголо
номер столб
Глава
29.
Знакомство с
585
Pandas
Допустим, мы ходим прочитать данные о космических запусках, но нас интересуют
только данные о предприятии, дата запуска, название ракеты, имя миссии, бюджет
и статус миссии. Тогда использование функции read _ csv () может выглядеть сле
дующим образом (листинг
Листинг 29.3.
29.3).
Chapter_29/example_03/read_csv_usecols.py
import pandas as pd
data
=
pd.read_csv("space_missions.csv",
usecols=["Caпpany",
"Мission",
"Date", "Rocket",
"Price",
"МissionStatus"])
print(data.info())
print ()
print(data)
На рис.
29.3
показан результат работы этого скрипта.
<class 'pandas.core.frame.DataFrame'>
Rangeindex: 4630 entries, 0 to 4629
Data columns (total 6 columns):
Non-Null Count Dtype
Column
#
4630
0 Company
4630
1 Date
4630
2 Rocket
4630
3 Hission
1265
4 Price
5 Hissionstatus 4638
dtypes: object(6)
memory usage: 217.2+ КВ
None
0
1
2
3
4
non-null
non-null
non-null
non-null
non-null
non-null
Company
Date
RVSN USSR 1957-10-04
RVSN USSR 1957-11-83
US Navy 1957-12-86
1958-82-81
АНВА
US Navy 1958-02-85
object
object
object
object
object
object
Rocket
Sputnik 8K71PS
Sputnik 8K71PS
Vanguard
Juno I
Vanguard
Hission Price Hissionstatus
Success
NaN
Sputnik-1
Success
NaN
Sputnik-2
Failure
NaN
Vanguard TV3
Success
NaN
Explorer 1
Failure
NaN
Vanguard TV3BU
67
Spacex 2022-07-22 Falcon 9 Block 5 Starlink Group 3-2
4625
NaN
Long Harch 5В
Wentian
CASC 2822-87-24
4626
67
SpaceX 2022-87-24 Falcon 9 Block 5 Starlink Group 4-25
4627
NaN
Demo Flight
Zhongke-lA
1<628 CAS Space 2822-07-27
Long Harch 2D Yaogan 35 Group 83 29.75
CASC 2822-07-29
4629
[4630 rows
Рис.
29.3.
х
Success
Success
success
Success
Success
6 columns]
Результат чтения файла
space_missions.csv только
с выбранными столбцами
Здесь видно, что остались только те столбцы, которые мы указали. Такого же ре
зультата можно было бы добиться при передаче в качестве параметра usecols спи
ска целых чисел:
data = pd.read_csv("space_missions.csv", usecols=[O, 2, 4, 5, 7, 8])
586
Часть 111. Python для научных вычислений
Следующий шаг
-
изменение заголовков данных. Для этого мы воспользуемся
параметром names, который принимает список строк, соответствующих заголовкам
каждого прочитанного столбца. Но при этом мы должны еще добавить параметр
это будет означать, что первая строка по-прежнему считается заголов
header=O -
ком данных и будет проигнорирована при чтении данных. Если мы этого не сдела
ем и просто добавим параметр names, то первая строка с заголовками будет интер
претироваться как первая строка данных. В следующем примере (листинг 29.4)
приводится только команда чтения данных, остальные строки скрипта остаются без
изменений по сравнению с примером из листинга 29.3.
Листинг 29.4.
da t a
Chapter_29/example_04/read_csv_names.py
pd.read_csv("space_missions.csv",
header= O,
nаmеs=["Орrанизаци,r",
"Миссия",
"Дата",
11 Бlод,кет 11
,
"Annapaт",
"Успех"],
usecols=[ O, 2, 4, 5, 7, 8],
Результат работы этого скрипта показан на рис.
29.4.
<class 'pandas.core.frame . DataFrame'>
Rangelndex : 4630 entries, 0 to 4629
Data columns (total 6 columns):
#
Column
Non-Nutt Count Dtype
0
Организация
4630 non-null
1 Дата
4630 non-null
2 Аппарат
4630 non-nutl
4630 non-nutl
3
Ниссия
4
Бюджет
1265 non-nutt
Успех
4630 non-nutl
5
dtypes : object(6)
memory usage: 217.2+ кв
None
object
object
object
object
object
object
Организация
Дата
Аппарат
Ниссия
Бюджет
Успех
0
1
2
3
4
RVSN USSR
RVSN USSR
us Navy
us Navy
1957-10-04
1957-11-03
1957-12-06
1958-02-01
1958-02-05
Sputn1k 8K71PS
Sputnik 8K71PS
Vanguard
Juno 1
Vanguard
Sputn1k-1
Sputn1k-2
Vanguard TV3
Explorer 1
Vanguard TV3BU
NaN
NaN
NaN
NaN
NaN
Success
Success
Failure
Success
Failure
4625
4626
4627
4628
4629
Spacex
CASC
SpaceX
CAS Space
CASC
2022-07-22
2022-07-24
2022-07-24
2022-07-27
2022-07-29
67
Success
Success
Success
Success
Success
АНВА
[4630 rows
Рис.
х
Falcon 9 Block 5 Starl1nk Group 3-2
Long Harch 5В
Wentian
Falcon 9 Block 5 Starlink Group 4-25
Zhongke-lA
Demo Flight
Long Harch 20
Yaogan 35 Group 03
NaN
67
NaN
29.75
6 columns]
29.4.
Изменение заголовков столбцов с помощью параметра
функции
r e ad csv ()
names
Глава
29.
Знакомство с
Pandas
587
И последнее, что мы сделаем с этими данными непосредственно при чтении
-
это
добавим два преобразования типов. Значения столбца "Бюджет" мы преобразуем к
типу numpy. float64, а столбца "Успех" -
к булеву значению: строку "Success" -
в True, остальные значения- в False. Кроме того, столбец "дата" преобразуем к
календарному типу.
Для преобразования данных столбцов используется параметр converters, который
принимает в качестве значения словарь, в котором ключи
-
это либо заголовок
столбца, либо его индекс, а в качестве значений должна быть функция или другой
вызываемый объект, преобразующий строку в необходимый тип данных.
Для преобразования данных столбца в календарный тип применяется параметр
parse_dates функции read_csv (), который в качестве значения может принимать
разные типы, в том числе список строк с заголовками столбцов или список номеров
столбцов, для которых нужно применить преобразование к дате.
Дополним наш пример преобразованиями данных (листинг
Листинr 29.5.
29.5).
Chapter_29/example_05/read_csv_converters.py
converters = (
"Бюджет":
"Успех":
(lamЬda price:
float (price. replace (", ", ""))
if price != "" else None),
lamЬda
status: status.strip() .lower()
"success"
data = pd.read_csv("space_missions.csv",
usecols=[O, 2, 4, 5, 7, 8],
header=O,
nаmеs=["Организация",
"Миссия 11 ,
"Дата",
"Бю,цжет",
"Аппарат",
"Успех"],
converters=converters,
parse_d&tes=["Дaтa"],
1
В качестве значения параметра converters в функцию read_csv ( 1 передается сло
варь, описывающий преобразования для двух столбцов:
♦
конвертер для столбца "Бюджет" -
это лямбда-выражение, возвращающее зна
чение None, если для переданного значения нет данных (строка "price" равна
пустой строке). Если же данные есть, то они преобразуются в тип float с пред
варительным удалением
из строки
запятых,
которые
в
некоторых
строках
ис
пользуются для отделения тысяч;
♦
для
столбца
"Успех"
конвертер
-
это
лямбда-выражение,
которое
строку
"success" независимо от регистра символов преобразует в True, а все остальные
значения в False.
Часть
588
111. Python
для научных вычислений
Этот словарь можно было бы сформировать также следующим образом:
converters = {
7: (lamЬda price:
float (price. replace (", ", "") )
if price != "" else None),
8:
lamЬda
status: status.strip() .lower()
Здесь 7 и 8 -
"success"
индексы столбцов "Бюджет" и "Успех" в первоначальных данных (без
учета отбрасывания столбцов из-за использования параметра usecols).
Помимо этого, в функцию read_csv() передается параметр parse_dates, значение
которого представляет собой список с единственным элементом
заголовком
-
столбца, который нужно преобразовать в календарный формат.
После запуска этого примера мы увидим результат, показанный на рис.
29.5.
<class 'pandas.core.frame.DataFrame'>
Rangelndex: 4630 entries, 0 to 4629
Data columns (total 6 columns):
#
Column
Non-Null Count Dtype
4630 non-null
object
Организация
0
1 Дата
4630 non-null
datetime64[ns]
2 Аппарат
4630 non-null
object
3 Миссия
4630 non-null
object
4 Бюджет
1265 non-null
float64
5 Успех
4630 non-null
bool
dtypes: bool(l), datetime64[ns](l), float64(1), object(3)
memory usage: 185.5+ КВ
None
Организация
Дата
Аппарат
Миссия
Бюджет
Успех
1
2
3
4
RVSN USSR 1957-10-04
RVSN USSR 1957-11-03
us Navy 1957-12-06
АМВА 1958-02-01
us Navy 1958-02-05
Sputnik 8K71PS
Sputnik 8K71PS
Vanguard
Juno 1
Vanguard
Sputnik-1
Sputnik-2
Vanguard TV3
Explorer 1
Vanguard TV3BU
NaN
NaN
NaN
NaN
NaN
True
True
False
True
False
4625
4626
4627
4628
4629
SpaceX
CASC
SpaceX
CAS Space
CASC
Falcon 9 Block 5 Starlink Group 3-2
Long March 58
Wentian
Falcon 9 Block 5 Starlink Group 4-25
Zhongke-lA
Demo Flight
Long March 20
Yaogan 35 Group 03
67.00
True
True
True
True
True
0
[4630 rows
Рис.
х
2022-07-22
2022-07-24
2022-07-24
2022-07-27
2022-07-29
NaN
67.00
NaN
29.75
6 columns]
29.5.
Данные после преобразования типов для некоторых столбцов
Обратите внимание на выведенный в списке столбцов вверху тип данных
(столбец
Dtype).
Помимо функции read_csv (), предназначенной для чтения данных в формате
в
Pandas
данных,
CSV,
имеются функции для чтения данных и в иных форматах: read _ tаЫе () представленных
в
виде
столбцов,
read_jsoп () -
в
формате
JSON,
Глава
29.
Знакомство с
read_hdf () лиц
в формате
Microsoft Excel.
589
Pandas
HDF5, read_ excel () -
данных файлов электронных таб
Есть также функции для чтения данных из файлов прочих
форматов.
Создание экземпляров класса
DataFrame
До сих пор мы получали экземпляр класса DataFrame, читая данные из файла с по
мощью функции read _ csv (). Теперь посмотрим, как создавать экземпляр этого
класса через конструктор, в документации описанный следующим образом:
class pandas.DataFrame(data=None, index=None, columns=None,
dtype=None, copy=None)
Параметр da ta может принимать в качестве значения различные типы. Например,
он может быть массивом ndarray из библиотеки
Листинг 29.6.
NumPy
(листинг
29.6).
Chapter_29/example_06/create_from_array.py
import pandas as pd
import numpy as np
data = np.array( [ [1, 10,
[2, 20,
[3, 30,
( 4, 40,
[5, 50,
pd.DataFrame(data)
=
df
100],
200],
300],
400],
500]])
print(df.info())
print ()
print (df)
В результате запуска этого скрипта в консоль будет выведен следующий текст:
<class 'pandas.core.frame.DataFrame'>
Rangeindex: 5 entries, О to 4
Data columns (total 3 columns):
Column Non-Null Count Dtype
#
-------------int64
5 non-null
о
о
int64
5 non-null
1
1
int64
2
5 non-null
2
(3)
int64
dtypes:
memory usage: 252.0 bytes
None
О
О
1
2
1
10
100
590
Часть
1 2 20
2 3 30
3 4 40
4 5 50
111. Python
для научных вычислений
200
300
400
500
Обратите внимание, что к столбцам добавлены заголовки в виде последовательно
сти целых чисел, добавлен также индекс по строкам.
Согласно описанию конструктора класса DataFrame, мы можем изменить индекс и
заголовки столбцов, передав в явном виде параметры index и columns (листинг
Листинг
29.7).
29.7. Chapter_29lexample_07lcreate_from_array_headers.py
import pandas as pd
import numpy as пр
data = np.array( [ [1, 10, 100],
[2, 20, 200],
[3, 30, 300),
[ 4, 40, 400],
[ 5, 50, 500)))
columns = ["Foo", Bar", "Baz"]
"d", "е"]
index = ["а"' "Ь"'
df = pd.DataFrame(data, index=index, columns=columns)
print (df)
11
ltcll f
Результат выполнения этого скрипта выглядит следующим образом:
а
ь
Foo
1
2
с
з
d
4
е
5
Ваr
Ваz
10
20
30
40
50
100
200
300
400
500
Как можно здесь видеть, индекс не обязан быть последовательностью целых чисел.
Часто используется и такой способ создания экземпляров класса DataFrame, когда в
качестве данных передается словарь, в котором ключи
значения
Листинг
-
29.8. Chapter_29lexample_08/create_from_dict.py
import pandas as pd
da ta = {"Foo": [ 1, 2, 3, 4, 5] ,
"Bar": [10, 20, 30, 40, 50),
"Ва z"
index
=
{
11
а 11 ,
-
это заголовки столбцов, а
массивы с данными для каждого столбца (листинг
: [ 1 О О, 2 О О, 3 О О, 4 О О, 5 О О) }
"Ь", "с", "d", "е 11 ]
df = pd.DataFrame(data, index=index)
print (df)
29.8).
Глава
Знакомство с
29.
591
Pandas
При этом будет создан экземпляр класса DataFrame с таким же содержимым, что и
в предыдущем примере.
Выбор элементов и фильтрация данных
из
DataFrame
Дальнейшие примеры этой главы для краткости мы будем выполнять в интерак
тивном
режиме
Python (REPL).
Предварительно
подготовим
экземпляр
класса
DataFrame, с которым будем экспериментировать:
>>> import pandas as pd
>>> foo = { 'Foo': [l, 2, 3, 4, 5],
'Bar': [10, 20, 30, 40, 50],
'Baz': [100, 200, 300, 400, 500],
'Spam': [-3, -4, -5, -6, -7],
>>> foo index = ['а', 'Ь', 1 С I 'd'' 'е']
>>> foo_df = pd.DataFrame{foo, index=foo index)
>>> foo df
Foo Bar Baz Spam
-3
1
10 100
а
-4
2
20 200
ь
-5
3
30 300
с
-6
400
40
4
d
-7
50 500
5
е
f
-
Чтобы узнать размер таблицы, можно воспользоваться свойством shape, по своей
NumPy. Для двумерной таб
с количеством строк и
элементов -
сути напоминающим аналогичное свойство массивов
лицы это свойство возвращает кортеж из двух
столбцов:
>>> foo_df.shape
(5,
4)
А с помощью свойства size можно узнать общее количество элементов в таблице
(количество строк, умноженное на количество столбцов):
>>> foo df.size
20
По аналогии с массивами
NumPy,
класс
DataFrame имеет и свойство т, возвращаю
щее транспонированную таблицу, в которой строки и столбцы меняются местами:
>>> foo df.T
Foo
Bar
Baz
Spam
а
ь
с
1
10
100
-3
2
20
200
-4
3
30
300
-5
d
4
40
400
-6
е
5
50
500
-7
592
Часть
111. Python
для научных вычислений
Класс DataFrame предоставляет широкие возможности для того, чтобы создавать
выборки данных, которые удовлетворяют различным критериям.
Начнем с получения объектов, отвечающих за индексацию строк и столбцов. Объ
ект индекса по строкам можно получить с помощью свойства index, а по столб
цам
-
с помощью свойства columns:
>>> foo df.index
Index(['a', 'Ь', 'с', 'd', 'е'), dtype='object')
>>> foo df.columns
Index(['Foo', 'Bar', 'Baz', 'Spam'), dtype='object')
В нашем случае оба этих объекта имеют тип Index. Однако, если бы мы не задали
индексацию по строкам в виде строковых значений, индексация была бы целочис
ленной, как мы это видели в примерах с данными о космических запусках, а индекс
имел бы тип Rangeindex.
Для доступа к отдельным строкам данных по индексу используется свойство loc
вместе с оператором квадратных скобок. Например, чтобы получить строку с ин
дексом "с", выполним команду:
>>> row
foo_df.loc["c"]
>>> row
Foo
3
Bar
30
Baz
300
Spam
-5
Name: с, dtype: int64
»> type(row)
<class 'pandas.core.series.Series'>
В результате такого действия мы получаем экземпляр класса Series. Этот класс
включает в себя данные строки, а также индекс по столбцам. Теперь для доступа к
содержимому каждого столбца мы снова можем использовать свойство loc, но уже
полученного экземпляра класса
Series:
>>> row. index
Index(['Foo', 'Bar', 'Baz', 'Spam'), dtype='object')
>>> print (row. loc [ "Foo"))
3
>>> print(row.loc["Bar"))
30
>» print (row.loc ["Spam"))
-5
Экземпляр класса series для строки мы также можем получить не по индексу, а по
номеру строки (нумерация начинается с О). Для этого используется свойство iloc
класса DataFrame и оператор квадратных скобок:
>>> row = foo_df.iloc[2)
>>> row
Foo
3
Глава
29.
Знакомство с
593
Pandas
30
Bar
300
Baz
-5
Spam
Name: с, dtype: int64
»> type(row)
<class 'pandas.core.series.Series'>
Класс Series также имеет свойство iloc, поэтому содержимое ячейки мы тоже
можем получать по целочисленному индексу:
>>> print(row.iloc[2])
300
При использовании свойств
получения среза
>>>
>>>
suЬdata
«: » -
loc и iloc есть возможность использовать операцию
как это делается для списков и массивов:
= foo_df.iloc[l:4]
suЬdata
Foo Bar Baz Spam
-4
20 200
2
-5
30 300
3
с
-6
40 400
4
d
>>> type(subdata)
<class 'pandas.core.frame.DataFrame'>
ь
Как и при получении среза из списка или массива, элемент, соответствующий но
меру правой границы, не включается в результат. Выполнив такую операцию, мы
получаем новый экземпляр класса DataFrame.
Свойство loc можно использовать и совместно с операцией взятия среза:
>>> foo_df.loc["b":"d"]
Foo Bar Baz Spam
-4
20 200
2
ь
-5
30 300
3
с
-6
40 400
4
d
В этом случае правая граница среза включается в результат.
К свойствам loc и iloc можно применять многомерную индексацию, чтобы одно
временно выделять блок элементов по строкам и столбцам. Например, мы можем
получить все значения из одного столбца:
foo_df.loc[:, "Bar"]
>>> col
>>> col
10
а
20
ь
30
с
40
d
50
е
Name: Bar, dtype: int64
»> type (col)
<class 'pandas.core.series.Series'>
594
Часть
111. Python
для научных вычислений
Если мы выделяем только один столбец, то получаем экземпляр класса Series. Од
нако такого же результата можно добиться более короткой записью, если приме
нить оператор квадратных скобок непосредственно к экземпляру класса Da taFrame:
>>> foo _df [ "Foo"]
а
1
2
ь
с
3
d
4
е
5
Name: Foo, dtype: int64
Для доступа к столбцам предусмотрен еще более короткий способ
-
через свойст
ва, имена которых совпадают с именами столбцов, если это не противоречит син
таксису языка
Python (если
имена столбцов подчиняются правилам именования пе
ременных):
>>> foo df.Bar
а
10
ь
20
с
30
40
d
е
50
Name: Bar, dtype: int64
Так можно получить все значения для интервала столбцов:
>>> cols = foo_df.loc[:, "Bar": "Baz"]
>>> cols
Bar Baz
а
10 100
20 200
ь
с
30 300
d
40 400
е
50 500
>>> type (cols)
<class 'pandas.core.frame.DataFrame'>
Когда мы выделяем несколько столбцов, то получаем экземпляр DataFrame.
Если нужно выделить столбцы, которые расположены не подряд, то можно исполь
зовать следующую запись с явным указанием имен требуемых столбцов:
»> foo_df.loc[:, ["Foo", "Baz"]]
Foo Baz
а
1 100
ь
2 200
с
3 300
d
4 400
е
5 500
Глава
595
29. Знакомство с Pandas
Можно одновременно выделить часть столбцов и часть строк:
>>> foo_df.loc["b": "d", ["Foo", "Baz"]]
Foo Baz
2 200
Ь
с
3 300
d
4 400
Если нужно получить единственное значение на пересечении одного столбца и од
ной строки, можно воспользоваться теми же свойствами
loc и lloc:
>>> print(foo_df.loc['c', 'Foo'])
3
>>> print(foo_df.iloc[2,
О])
3
однако это неэффективно с точки зрения производительности, и для того, чтобы
получать одно значение, рекомендуется, вместо loc и iloc, использовать свойства
at
И
iat:
>>> print (foo df .at ["с", "Foo"])
3
>>> print(foo_df.iat[2,
О])
3
Библиотека
Pandas
многое заимствует из библиотеки
NumPy.
Например, в главе
25
мы говорили про булевы массивы и выделение нужных элементов с их помощью.
Подобный подход работает и в
Pandas.
Следующая команда создает экземпляр
класса series, заполненный булевыми значениями, которые будут равны True в тех
строках, где значение в столбце "Bar" будет превышать значение 20:
>>>
Ьig_bar
>>>
Ьig_bar
foo_df["Bar"] > 20
False
False
True
с
True
d
True
е
Name: Bar, dtype: bool
>>> type(Ьig_bar)
<class 'pandas.core.series.Series'>
а
ь
Теперь этот объект мы можем использовать для фильтрации строк, если его пере
дать в качестве параметра в оператор квадратных скобок:
>>>
с
d
е
foo_df[Ьig_bar]
Foo Bar
30
3
4
40
50
5
Baz
300
400
500
Spam
-5
-6
-7
596
Часть
111. Python
для научных вычислений
Но часто в таких ситуациях не создают отдельный объект series (если только не
предстоит его использовать несколько раз), а пишут так:
»> foo_df[foo_df["Bar"] > 20]
Foo Bar Baz Spam
с
3 30 300
-5
d
4
40 400
-6
5 50 500
-7
е
Такого же результата мы добьемся, если применим свойство loc:
>>> foo_df.loc[foo_df["Bar"] > 20]
Foo Bar Baz Spam
с
3
30 300
-5
d
4
40 400
-6
5
50 500
-7
е
Причем такую запись мы можем совмещать с выделением нескольких столбцов, но
в этом случае нужно обязательно использовать свойство loc:
»> foo _ df. loc [ foo _df [ "Bar"] > 20, "Foo":"Baz"]
Foo Bar Baz
3
30 300
с
4
40 400
d
е
5
50 500
Если мы хотим построить более сложные условия
-
например, выделить строки,
где значение "Bar" > 20, а "Baz" < 450, то нужно учесть некоторые особенности. Пер
вое, что хочется сделать, это объединить два условия с помощью логического опе
ратора and, однако это приведет к ошибке:
»> foo_filter = (foo_df["Bar"] > 20) and (foo_df["Baz"] < 450)
Traceback (most recent call last):
File "<python-input- ... >", line 1, in <module>
foo_filter = (foo_df['Bar'] > 20) and (foo_df['Baz'] < 450)
File "/usr/liЬ/python3.13/site-packages/pandas/core/generic.py", line 1577, in
nonzero
raise ValueError(
... <2 lines> ...
ValueError: The truth value of
a.any() or a.all().
Проблема
здесь
в
том,
(foo_df["Baz"J < 450)
а
Series is
что
каждое
amЬiguous.
из
Use a.empty, a.bool(), a.item(),
выражений
создает свои экземпляры класса
(foo df["Bar"J > 20)
Series,
и
которые содержат
значения как True, так и False. А далее мы пытаемся применить булеву операцию к
этим двум объектам, но интерпретатор не знает, как ему считать такие объекты: как
истинные или как ложные. Для рассматриваемой задачи нужно использовать логи
ческие поэлементные операторы
&,
1
и~.
Глава
29.
Знакомство с
597
Pandas
При этом наше условие должно выглядеть так:
>» foo filter = (foo_df["Bar"] > 20)
>>> foo filter
а
False
&
(foo_df["Baz"] < 450)
False
с
True
True
d
False
е
dtype: bool
»> foo_df[foo - filter]
Foo Bar Baz Spam
-5
с
3
30 300
-6
d
4
40 400
ь
Здесь важно правильно расставить скобки, иначе возникнет ошибка, связанная с
приоритетом логических операторов.
Формирование условия в этом примере выглядит очень громоздко, но подобные
запросы можно выполнить более компактным способом, если воспользоваться ме
тодом
query () класса DataFrame. Этот метод в качестве параметра принимает стро
ку, описывающую запрос на языке
Python
или его напоминающем:
>>> foo_df.query("Bar > 20 and Baz < 450")
Foo Bar Baz Spam
-5
с
3
30 300
-6
4
40 400
d
Если в таком запросе нужно использовать внешние переменные, то перед их име
нами надо написать символ
@:
»> min bar = 20
>>> max baz = 450
>>> foo_df.query("Bar > @min bar and Baz < @max_baz")
Foo Bar Baz Spam
с
3
30 300
-5
-6
4
40 400
d
Если имена столбцов содержат кавычки, пробелы или другие символы, которые
мешают интерпретировать имена столбцов как переменные, то такие имена нужно
оборачивать символами«·»
-
обратная кавычка, не путайте с обычной! В нашем
случае такой проблемы нет, но для демонстрации мы все равно можем их исполь
зовать:
>>> foo_df.query('
с
d
·ваr·
Foo
Bar
Baz
Spam
3
4
30
40
300
400
-5
-6
> 20 and
·ваz'
< 450 1 )
Часть
598
111. Python для
научных вычислений
С помощью метода to_numpy () мы можем преобразовать данные из DataFrame в
экземпляр класса
numpy. ndarray:
>>> foo_array = foo_df.to_numpy()
>» foo_array
array( [ [ 1, 10, 100, -3] 1
[ 2, 20, 200, -4] 1
3, 30, 300, -5] 1
4, 40, 400, -6] 1
5, 50, 500, -7]])
>» type (foo_array)
<class 'numpy.ndarray'>
Обработка данных с помощью
DataFrame
Создадим еще один экземпляр класса DataFrame, который, в отличие от foo_df, бу
дет содержать значения
None
в качестве некоторых элементов:
>>> eggs_df = pd.DataFrame(
{"Foo": [None, 5, 2, 1, 5, 2],
"Bar": [40, None, 30, None, 50, 30],
"Baz": [200, 100, None, None, None, 300] 1
"Bam": [-3, -4, -5, -6, -7, -8] 1
"Spam": [None, None, None, None, None, None],
})
>>> eggs_df
Foo
Bar
о
NaN 40.0
1 5.0
NaN
2 2.0 30.0
3 1.0
NaN
4 5.0 50.0
5 2.0 30.0
Baz
200.0
100.0
NaN
NaN
NaN
300.0
Bam Spam
-3 None
-4 None
-5 None
-6 None
-, None
-8 None
~
Обратите внимание, что в столбцах, которые содержат числовые данные, значения
None были преобразованы в NaN, а в столбце Spam, для которого Pandas не смог вы
брать тип, значения None остались неизменными. Кроме того, если посмотреть ин
формацию, выводимую методом info (), можно увидеть, что первые три столбца
имеют тип f 1оаt64, четвертый столбец - тип i n t 6 4, а последний столбец остался с
ТИПОМ object:
>>> eggs_df.info()
<class 'pandas.core.frame.DataFrame'>
Rangeindex: 6 entries, О to 5
Data columns (total 5 columns):
# Column Non-Null Count Dtype
О
1
Foo
Bar
5 non-null
4 non-null
float64
float64
Глава
29.
Знакомство с
Pandas
599
2
Baz
float64
З non-null
Bam
6 non-null
int64
О non-null
4 Spam
object
dtypes: float64(3), int64(1), object(l)
memory usage: 372.0+ bytes
З
Класс
DataFrame предоставляет множество методов для сбора статистических дан
count () возвращает экземпляр класса Series,
ных по столбцам. Например, метод
который содержит информацию о количестве элементов, отличных от None или
NaN, в каждом столбце:
»> eggs __ df. count ()
Foo
5
Bar
4
Baz
3
Bam
6
О
Spam
dtype: int64
Метод
sum () позволяет просуммировать все элементы в каждом числовом столбце,
prod () - перемножить все элементы столбца (значения NaN по умолчанию
а метод
игнорируются, но это поведение можно отключить, если передать дополнительный
параметр
skipna=False):
eggs_df.sum()
15.0
150.0
600.0
-33
>»
Foo
Bar
Baz
Bam
О
Spam
dtype: object
Имеются методы и для нахождения минимального и максимального значений в ка
ждом столбце
- min ()
и
max () соответственно, среднего значения - mean (), сред
- std (), медианного значения - median (), кванти
неквадратичного отклонения
лей
-
quantile ()
>>> eggs _ df. median ()
2.0
Foo
35.0
Bar
Baz
200.0
-5.5
Bam
Spam
NaN
dtype: object
>>> eggs_df.min()
Foo
1.0
Bar
30.0
Baz
100.0
-8
Bam
None
Spam
dtype: object
и некоторых других статистических величин:
600
Часть
111. Python
для научных вычислений
Есть также методы idxmin () и idxmax (), которые возвращают индекс первого ми
нимального и максимального элемента в столбце соответственно:
>>> eggs_df.idxmin()
<python-input- ... >:1: FutureWarning: The behavior of DataFrame.idxmin with all-NA
values, or any-NA and skipna=False, is deprecated. In а future version this will raise
ValueError
eggs_df.idxmin()
Foo
3. О
Bar
2.0
Baz
1.0
Bam
5.0
Spam
NaN
dtype: float64
Обратите здесь внимание на два следующих момента. Во-первых, мы получили
предупреждение из-за столбца Spam, который содержит только значения None. Пре
дупреждение говорит о том, что сейчас для такого столбца
Pandas
возвращает зна
чение NaN, но в будущих версиях библиотеки поведение изменится и станет возбу
ждаться исключение
ValueError.
Во-вторых, номера индексов были преобразованы к типу float64, что может стать
проблемой: если впоследствии по этим индексам потребуется получить сами зна
чения, то надо будет не забыть преобразовать их к целым числам.
К объектам DataFrame можно применять математические операции:
>>> tmp_df = eggs_df *
>>> tmp_df
Foo
Bar
Baz
NaN 400.0 2000.0
о
l 50.0
NaN 1000.0
2 20.0 300.0
NaN
NaN
NaN
3 10.0
4 50.0 500.0
NaN
5 20.0 300.0 3000.0
10
Bam Spam
-30 NaN
-40 NaN
-50 NaN
-60 NaN
-70 NaN
-80 NaN
В результате умножения был создан новый экземпляр DataFrame, в котором все
элементы, не равные NaN, были умножены на
10.
Есть также возможность применять ко всей таблице или к отдельным ее столбцам
математические функции из библиотеки
>>> import numpy as np
>>> np.sin(tmp_df.Foo)
о
NaN
1 -0.262375
2
0.912945
3 -0.544021
4 -0.262375
0.912945
5
Name: Foo, dtype: float64
NumPy:
Глава
29.
Знакомство с
601
Pandas
В нашем случае мы не сможем применить функцию numpy. sin () или ей подобную
ко всему объекту oataFrame из-за последнего столбца, поскольку тип этого столбца
не относится к числовым типам.
К объектам oataFrame и к отдельным столбцам можно применять и сокращенные
операции присваивания
-
наподобие +=, * = и т. п:
>>> tmp_df.F'oo /= 1000
»> tmp_df
F'oo
Bar
Baz Bam Spam
о
NaN 400.0 2000.0 -30 NaN
1 0.05
NaN 1000.0 -40 NaN
NaN -50 NaN
2 0.02 300.0
3 0.01
NaN
NaN -60 NaN
4 0.05 500.0
NaN -70 NaN
5 0.02 300.0 3000.0 -80 NaN
Если над данными в таблице нужно выполнить болt;е сложные операции, то мож
но воспользоваться методом apply (), который принимает функцию для пересчета
данных:
♦
если этот метод вызывается для объекта oataFrame, то в указанную в качестве
параметра функцию последовательно передаются объекты Series, содержащие
данные по каждому столбцу. Эта функция должна вернуть экземпляр класса
Series;
♦
если метод apply ()
функцию
вызывается для объекта Series, то в принимаемую им
последовательно
передаются
его
элементы
(например,
элементы
столбца). В этом случае функция должна возвращать новое значение, которым
будет заменено переданное ей значение.
Если мы вернемся к переменной eggs _ ctf и на ее основе захотим сделать новый
объект oataFrame, но такой, чтобы вместо значений NaN были нули, то можем написать такой код:
>>> tmp_df = eggs_df.apply(lamЬda col:
col.apply(
lamЬda х:
х
if х is not None and not np.isnan(x)
else О)
>>> tmp df
F'oo
Bar
40.0
о
о.о
о.о
1 5.0
2 2.0 30.0
о.о
3 1. О
4 5.0 50.0
5 2.0 30.0
Baz
200.0
100.0
о.о
о.о
о.о
300.0
Bam
-3
-4
-5
-6
-7
-8
Spam
о
о
о
о
о
о
Часть
802
В этом примере мы вызываем метод
111. Python
для научных вычислений
apply () экземпляра класса DataFrame, и в ка
честве параметра передаем лямбда-выражение, на вход которому будут последова
тельно передаваться экземпляры series с содержимым каждого столбца. Внутри
лямбда-выражения для каждого полученного объекта Series снова вызывается ме
тод
apply (), который, в свою очередь, принимает последовательно каждый элемент
в столбце. Если полученное значение не равно None или NaN, то возвращается само
значение, иначе возвращается О.
Приведенный пример был показан здесь лишь для демонстрации работы метода
apply (), однако, если нам действительно нужно избавиться от значений NaN, то мы
можем
сделать это
более
компактным способом, воспользовавшись методами
mask () или where (). Оба этих метода могут принимать в качестве первого парамет
ра различные типы данных, в том числе и экземпляр DataFrame, заполненный буле
выми значениями (назовем его cond, как в документации). Методы mask () и
where () возвращают новый объект Da taFrame, копируя в него данные из исходного
DataFrame, делая при этом замену: метод mask () заменяет элементы на второй пе
реданный параметр, если соот.ветствующее значение из cond равно True, а метод
where () выполняет аналогичную замену для тех элементов, у которых соответст
вующие элементы из cond равны False. Звучит, может быть, запутанно, но приме
ры ДОЛЖНЫ всё это пояснить:
>>> mask na = eggs_df .isna ()
>>> mask na
Foo
Bar
Baz
Bam
True False False False
о
True False False
1 False
2 False False
True False
True
True False
3 False
True False
4 False False
5 False False False False
>>> eggs_df.mask(mask_na, О)
Foo
Bar
Baz Bam Spam
-3
о
о
40.0 200.0
о.о
-4
о.о
о
1 5.0
100.0
-5
2 2.0 30.0
о.о
о
о
-6
о.о
о.о
3 1. О
-7
о.о
о
4 5.0 50.0
о
-8
5 2.0 30.0 300.0
Spam
True
True
True
True
True
True
Сначала мы воспользовались методом isna (), который создал объект DataFrame из
булевых значений: элемент равен тrue в тех ячейках, где у исходного объекта
DataFrame
значения равны
None
или
NaN,
и равен
False
тов. Этот объект затем мы использовали в методе
mask () -
на месте остальных элемен
mask (). Второй параметр в
это значение для замены.
Для аналогичной задачи мы могли бы использовать метод where (), только булевы
значения в маске нужно инвертировать:
>>> eggs_df.where(eggs_df.notna(),
Foo Bar
Baz Bam Spam
О)
Глава
29.
Знакомство с
о
о.о
40.0
1
2
3
4
5
5.0
2.0
1. О
5.0
2.0
о.о
200.0
100.0
30.0
о.о
о.о
о.о
50.0
30.0
о.о
-3
-4
-5
-6
-7
-8
300.0
603
Pandas
о
о
о
о
о
о
Здесь для создания маски мы использовали метод
тивоположностью метода
которые не равны
None
isna ()
NaN.
и присваивает
notna () , который является про
значение True для тех элементов,
или
Методы mask () и where () используют и для более сложных условий, а поскольку
задача замены значений NaN часто встречается при обработке данных, в классы
DataFrame и Series был добавлен метод fillna (), предназначенный непосредст
венно для замены отсутствующих данных:
>>> eggs_df.fillna(O)
<python-input- ... >:1: FutureWarning: Downcasting object dtype arrays on .fillna,
.ffill, .bfill is deprecated and will change in а future version. Call
result.infer_objects(copy=False) instead. То opt-in to the future behavior, set
'pd.set_option('future.no_silent_downcasting', True)'
eggs_df.fillna(O)
Bar
Baz Ват Spam
Foo
-3
о
40.0 200.0
о
о.о
-4
100.0
о.о
о
l 5.0
-5
о
о.о
2 2.0 30.0
-6
о
о.о
о.о
1.0
3
-7
о
о.о
4 5.0 50.0
-8
о
5 2.0 30.0 300.0
Мы получили здесь правильный результат, но с предупреждением, снова связан
ным с последним столбцом, который содержит только значения None. Предупреж
дение говорит о том, что
Pandas
преобразует этот столбец к типу int64, но в буду
щих версиях библиотеки это поведение может измениться.
Метод
может принимать входные параметры разных типов, позволяющие
fillna ()
выборочно выполнять замену. Например, мы можем передать словарь, в котором в
качестве ключей будут имена столбцов, а в качестве значений
мены:
>» eggs_df. fillna ({
"Foo": о,
"Bar": -1,
Baz":
о,
"Ват":
о,
11
))
Foo
о
о.о
1 5.0
2 2.0
Bar
40.0
-1.0
30.0
Baz
200.0
100.0
о.о
Ват
-3
-4
-5
Spam
None
None
None
-
значения для за-
Часть
604
3 1.0
4 5.0
5 2.0
-1.0
50.0
30.0
-6
-7
-8
О.О
о.о
300.0
111. Python
для научных вычислений
None
None
None
В этом примере предупреждение не появилось, поскольку мы не делаем замену в
столбце Spam, а в столбце Bar значения NaN заменились на -1.
Методы mask (),
where ()
и
fillna ()
по умолчанию
создают новые
объекты
DataFrame, но если нам нужно сделать подобную замену в исходной таблице, то мы
можем использовать еще более компактную запись:
>>> eggs_df[eggs_df.isna()] =
>» eggs_df
Bar
Foo
Baz Bam Spam
40.0 200.0
-3
о
о.о
о
-4
1 5.0
о.о
100.0
о
-5
2 2.0 за.о
о.о
о
-6
о.о
о.о
3 1.0
о
-7
4 5.0 50.0
о.о
о
-8
о
5 2.0 за.о 300.0
О
Кроме того, методам
mask (), where () и fillna () можно передать дополнительный
параметр inplace=True - тогда они также будут изменять исходную таблицу, а не
создавать новую, и в таком случае эти методы будут возвращать значение None.
Класс DataFrame также имеет метод dropna (), удаляющий те строки, в столбцах
которых содержится NaN или None. На нашем объекте eggs_df этот метод демонст
рировать бессмысленно, поскольку он удалит все элементы, но полезно помнить,
что такой метод существует.
Для следующих примеров вернемся к данным о запусках космических аппаратов,
на которых мы рассмотрим более сложные способы обработки данных. Предвари
тельно загрузим эти данные:
>>> import pandas as pd
>>> converters =
"Price": (lamЬda price:
float(price.replace(",", ""))
if price != "" else None),
"MissionStatus": (lamЬda status:
status.strip() .lower()
"success")
>>> space_df = pd.read_csv("space_missions.csv",
usecols=["Company", "Location", "Date", "Rocket",
"Mission", "Price", "MissionStatus"],
converters=converters,
parse_dates=["Date"])
>>> space_df.info()
<class 'pandas.core.frame.DataFrame'>
Rangelndex: 4630 entries, О to 4629
Data columns (total 7 columns):
Глава
29.
Знакомство с
605
Pandas
Non-Null Count
-------------4630 non-null
Company
о
1
4630 non-null
Location
Date
2
4630 non-null
Rocket
4630 non-null
3
4 Mission
4630 non-null
Price
1265 non-null
5
6 MissionStatus 4630 non-null
dtypes: bool(l), datetime64 [ns] (1),
memory usage: 221.7+ кв
Column
#
Dtype
object
object
datetime64[ns]
obj ect
obj ect
float64
bool
float64 (1), object(4)
В предыдущих примерах мы в основном работали с числовыми данными, а теперь
поработаем со строковыми. Для работы со строками класс series предоставляет
свойство str, которое возвращает объект типа StringMethods:
>>> type(space_df["Company"].str)
<class 'pandas.core.strings.accessor.StringMethods'>
Внутри класса stringMethods имеется множество методов, связанных с обработкой
строковых данных. Воспользуемся сейчас методом contains (), с помощью которо
го можно выбрать только те строки таблицы, которые в определенном столбце со
держат данные, удовлетворяющие регулярному выражению (про регулярные вы
ражения мы говорили в главе
23). Метод contains () возвращает экземпляр класса
Series из булевых значений, которые будут равны True, если значение исходного
элемента удовлетворяет указанному регулярному выражению, или False в против
ном случае . Затем полученный объект можно будет использовать для фильтрации
строк таблицы .
Выберем из таблицы космических запусков только те запуски, которые осуществ
лялись с космодрома Байконур:
>>> space_df[space_df["Location"] .str.contains("Baikonur")]
Результат выполнения этой команды показан на рис.
Co11pany
USSR
USSR
USSR
USSR
USSR
18
22
RVSH
RVSN
RVSH
RVSH
RVSH
4522
4532
4551
4565
4598
Roscos11Os
Starsem
Roscos11Os
Roscosmos
RoSCOSIIIOS
Location
Kazakhstan
Kazakhstan
Kazakhstan
Kazakhstan
Kazakhstan
Oate
1957-lB-64
1957-11-В3
Rocket
Sputnik 81<71PS
Sputnik 8K71PS
1958-Bli-27
1958-05-15
1958-09-23
Sputn1k ВА91
Vostok
1/5,
1/5,
1/5,
1/5,
1/5,
Baikonur
Baikonur
Baikonur
Baikonur
Baikonur
Cos11odro11e ,
Cos11odro11e,
Cos11odro11e ,
Cos11odro11e,
Cos11odro"e,
Site 280/39,
Site 31/6,
S1te 31/6,
S1te 31/6,
Site 31/6,
Baikonur
Baikonur
Baikonur
Baikonur
Baikonur
Cosмodro11e, Kazakhstan 2821-12-13
Cos ■odro11e, Kazakhstan 2821-12-27
Site
Site
Site
Site
S1te
29.6.
Cos111odro11e, Kazakhstan 2822-82-15
Cos111odro11e, кazakhstan 2822-83-18
Cos111odro11e, Kazakhstan 2022-В6-Е13
Sputnik
8А91
tt1ssion
Sputnik-1
Sputnik-2
Sputnik-3 111
Sputnik - 3 #2
Price
нан
нан
нан
нан
Е-1 nвЪ В 1 (Luna - 1)
нан
Proton- H/Briz- H Ekspress-AHU3 & AHU7
OneWeb #12
Soyuz 2. lb/Fregat
Progress HS-19
Soyuz 2. la
Soyuz HS-21
Soyuz 2. la
Progress KS-28
soyuz 2.la
65.88
f11ss1onStatus
True
True
Fatse
True
False
25.ВВ
17 .42
17 . 42
17.42
Fatse
True
True
True
True
(719 rows х 7 cotumns]
Рис.
29.6.
Таблица космических запусков, выполненных с космодрома Байконур
Из этого примера явно не видно, что мы использовали регулярное выражение, а не
простой поиск строки (если нам не требуется применять регулярные выражения, а
достаточно обычного поиска по тексту, то в метод contains () можно передать до-
Часть
606
111. Python
для научных вычислений
полнительный параметр regex=False). Следующий пример с помощью регулярного
выражения выделяет только те запуски, которые осуществлялись Россией или
СССР. Для этого мы учтем тот факт, что космические запуски у нас осуществляло
очень небольшое количество организаций:
>» filter_rus = space_df["Company") .str.contains(
r" (?:RVSN) 1 (?:Roscosmos) (?:VКS RF) ")
>>> space rus
space_df[filter rus)
>>> space_rus
1
Результат такого поиска показан на рис.
Company
10
22
RVSN
RVSN
RVSN
RVSN
RVSN
USSR
USSR
USSR
USSR
USSR
4574
4582
4593
4598
4615
VKS RF
VKS RF
VKS RF
Roscosmos
VKS RF
Site 1/5, Baikonur Cosmodrome,
Site
Site
S1te
Site
1/5,
1/5,
1/5,
1/5,
29.7.
location
Date
кazakhstan
1957-10-84
Rocket
Sputn1k BK71PS
Baikonur Cosmodrome, Kazakhstan 1957-11-83
Baikonur Cosmodrome, Kazakhstan 1958-05-15
Baikonur Cosmodrome, Kazakhstan 1958-09-23
Sputn1k BK71PS
Sputnik 8А91
Sputn1k ВА91
Vostok
Site 43/3, Plesetsk Cos1nodrome, Russia 2822-04-07
S1te 35/1, Ptesetsk Cosmodrome, Russ1a 2022-04-29
S1te 43/4, Ptesetsk Cosmodrome, Russia 2822-85-19
Site 31/6, Baikonur Cosmodrome, Kazakhstan 2822-06-03
Site 43/4, Ptesetsk Cosmodrome, Russia 2е22-е1-е1
Soyuz 2. lb
Angara 1. 2
Soyuz 2.la
Soyuz 2. la
Soyuz 2. lb/Fregat -H
Ba1konur Cosmodrome, l<azakhstan 1958-84-27
Е-1
no~
111ss1on
Price
111ss1onstatus
Sputnik-1
Sputnik-2
Sputnik-3 #1
Sputnik-3 #2
в 1 (Luna-1)
NaN
NaN
NaN
NaN
NaN
True
Trut
False
True
28. ее
NaN
17.42
17.42
25.88
True
True
True
True
True
Cos ■os
2554
Cos•os 2555
Cos ■os 2556
Progress "S-28
Cos•os 2557
False
(2062 rows х 7 cotumns]
Рис.
29.7. Таблица с отфильтрованными данными по значениям столбца company
Здесь в регулярном выражении мы применили не захватывающие группировки
символов, поскольку с полученными группами мы ничего делать не собираемся.
Если бы мы использовали захватывающие группы, то получили бы предупрежде
ние о том, что регулярное выражение захватило группы, но мы их не используем, и
нам было бы предложено применить метод extract (), предназначенный для рабо
ты с группами из регулярных выражений.
Если нам понадобится добавить еще какое-либо условие
-
например, оставить
только удачные запуски, то мы можем объединить два объекта series с булевыми
значениями с помощью логических операторов
1,
&,
»> space_df[filter_rus & space_df["MissionStatus"])
Результат такого поиска показан на рис.
1
10
44
47
Company
RVSN USSR
RVSN USSR
RVSN USSR
RVSN USSR
RVSN USSR
4574
4582
4593
4598
4615
VKS RF
VKS RF
VKS RF
Roscosmos
VKS RF
29.8.
location
Kazakhstan
Kazakhstan
Kazakhstan
Kazakhstan
Kazakhstan
Oate
1957-lG-84
1957-11-83
1958-05-15
1959-09-12
1959-18-04
Rocket
Sputn1k 8K71PS
Sputn1k BK71PS
Sputnik ВА91
Vostok
Vostok
"ission
Sputnik-1
Sputnik-2
Sputnik-3 #2
Luna-2
Luna-3
Site 43/3, Ptesetsk Cosmodrome, Russia
Site 35/1, Ptesetsk Cosmodrome, Russia
Site 43/4, Ptesetsk Cosmodrome, Russia
Site 31/6 , Baikonur Cosmodrome, f<azakhstan
Site 43/4, Ptesetsk Cosmodrome, Russia
2822-84-07
2822-64-29
2822-65-19
2822-06-83
2022-07-87
Soyuz 2. lb
Angara 1. 2
Soyuz 2, la
Soyuz 2.la
Soyuz 2. lb/Fregat-H
Cosmos 2554
Cosmos 2555
Cosmos 2556
Progress HS-28
Cosmos 2557
Sito
S1te
S1te
S1te
Site
1/5 ,
1/5 ,
1/5,
1/5,
1/5,
Baikonur
Baikonur
Baikonur
Baikonur
Baikonur
Cosmodrome,
Cosmodrome,
Cosmodrome,
Cosmodrome,
Cosmodrome,
Price
NaN
NaN
NaN
NaN
NaN
HissionStatus
True
True
True
True
True
28.08
True
NaN
True
17 .42
17.42
True
25.0е
True
True
[1880 rows х 7 cotumns]
Рис.
29.8.
Таблица с отфильтрованными данными по значениям столбцов
и
MissionStatus
company
Глава
29.
Знакомство с
607
Pandas
Группировка
В этом разделе мы рассмотрим группировку и агрегацию данных. Если вы работали
с базами данных, то наверняка знакомы с этими операциями, которые используют
ся для того, чтобы получать какие-то статистические сведения о подмножествах
данных. Например, с помощью группировки мы можем получить таблицу, показы
вающую сколько было удачных и неудачных запусков для каждой организации,
общее количество запусков каждого типа космического аппарата и другие сведе
ния, способные помочь в анализе данных.
Обычно в процессе группировки выполняются три действия: разделение исходных
данных на группы по заданным значениям в одном или нескольких столбцах, затем
к каждой группе применяется функция агрегирования (вычисление какой-либо ста
тистической величины), после чего полученные данные объединяются в новую
таблицу, где индексом строк будут данные, на основе которых производилась
группировка, а значениями
-
рассчитанные статистические величины.
Под агрегацией (или агрегированием) понимают такие операции, которые из век
торной величины (или массива со многими элементами) создают скалярную
-
од
но число. Операцией агрегирования может быть, например, подсчет количества
элементов, нахождение среднего значения или суммирование всех элементов.
Первый этап описанного процесса
тода
-
группировка
-
выполняется с помощью ме
groupby (), который возвращает объект типа DataFrameGroupBy.
Для примера подсчитаем на основе полученного ранее объекта space_rus, какие
организации сколько космических запусков осуществили. Для этого сначала полу
чим объект DataFrameGroupBy, выполняющий группировку по данным столбца
Company:
>>> space_group_company = space_rus.groupby("Company")
>>> type(space_group_company)
<class 'pandas.core.groupby.generic.DataFrameGroupBy'>
Теперь нам нужно выполнить агрегацию, которая в нашем случае будет заключать
ся в подсчете количества элементов в каждой группе. Для выполнения агрегации в
классе
DataFrameGroupBy
предназначен
метод
agg (),
которому
нужно
передать
функцию агрегирования, и это можно сделать разными способами. В результате
выполнения метод agg () вернет созданный объект DataFrame.
Если в метод agg () передать функцию, то эта функция будет вызываться для каж
дого столбца в каждой группе, и каждый раз этой функции будет передаваться объ
ект series с данными столбца, а переданная в качестве параметра функция должна
вернуть число. Например, следующий код подсчитывает количество элементов в
каждом столбце каждой группы:
>>>
space_group_company.agg(lamЬda
col: col.size)
Результат выполнения этой команды показан на рис.
29.9.
Часть
608
111. Python
для научных вычислений
Location Date Rocket Mission Price MissionStatus
Company
RVSN USSR
Roscosmos
VKS RF
Рис.
1777 1777
69
69
216
216
29.9.
1777
69
216
1777
69
216
1777
69
216
1777
69
216
Результат подсчета количества элементов
в каждом столбце после группировки данных
Мы получили новый объект
званиями организаций
-
oataFrame, у которого индекс по строкам заполнен на
значениями, на основе которых производилась группи
ровка. Значение каждого столбца является результатом работы функции, передан
ной в метод agg () . Такого же результата мы могли бы добиться, если бы в метод
agg () передали функцию len () :
>>> space_group_company.agg(len)
Если же нам нужно было бы получить количество элементов, не равных
NaN или
None, то мы могли бы воспользоваться методом count () класса Series:
>>>
space_group_company.agg(lamЬda
col: col.count())
Поскольку бюджет запусков указан далеко не везде, то мы бы получили результат,
показанный на рис.
29. l О.
Location Date Rocket Hission Price HissionStatus
Company
RVSN USSR
Roscosmos
VKS RF
Рис.
29.10.
1777 1777
69
69
216 216
1777
69
216
1777
69
216
2
~0
52
1777
69
216
Результат подсчета количества в каждом столбце элементов,
не равных
NaN
или
None
после группировки данных
В этом примере содержится множество столбцов, заполненных одинаковыми чис
лами. Для нашей задачи
-
это избыточные данные, нам достаточно было бы оста
вить один столбец, и метод
agg () класса DataFrameGroupBy позволяет построить
DataFrame, содержащие только необходимые столбцы.
Для этого в метод
agg () можно передать словарь, где в качестве ключа будет вы
ступать имя столбца, а в качестве значения
>>> space_group_company.agg({"Location":
Location
Company
RVSN USSR
1777
Roscosmos
69
216
VКS RF
-
lamЬda
агрегирующая функция:
col: col.size))
В результате мы получили объект DataFrame с одним столбцом Location, запол
ненным количеством элементов в каждой группе.
Глава
29.
Знакомство с
Библиотека
Pandas
609
Pandas
предоставляет несколько встроенных функций для агрегирова
ния, в том числе size (), и если мы хотим воспользоваться такой встроенной функ
цией, то ее имя можно передать в качестве строки вместо нашей функции:
>>> space_group_company.agg({"Location": "size"))
Результат выполнения этой команды будет точно таким же, как и предыдущей. Та
кого же результата можно добиться при использовании более компактной формы
записи:
>>> space_group_company["Location"] .size()
Мы уже добились того, что хотели, но в полученном результате можно навести еще
некоторую красоту. Все-таки не очень хорошо, что в нем заголовок называется
Location, хотя по сути это количество запусков. Мы можем переименовать созда
ваемый с помощью агрегирования столбец, если воспользуемся следующим син
таксисом вызова метода
agg () :
>>> space_group_company.agg(Count=("Location", "size"))
Count
Company
RVSN USSR
1 777
Roscosmos
69
VKS RF
216
Здесь мы переименовали столбец, дав ему имя Count, а вместо строки "size" могли
бы передать обычную функцию, как мы это делали ранее:
>>> space_group_company.agg(Count=("Location",
lamЬda
col: col.size))
Результат будет тот же самый.
В предыдущих примерах мы использовали группировку по данным одного столбца,
однако группировку можно проводить и по нескольким столбцам. Например, если
нам требуется подсчитать отдельно удачные и неудачные запуски для разных орга
низаций, то мы можем сгруппировать данные по названию организации и по успеху
запуска:
>>> gr_company_status = space_rus.groupby(["Carpany", "МissionStatus"])
>>> missions df
gr company_status.agg(Count=("Location", "size"))
>>> missions df
Count
MissionStatus
Company
163
RVSN USSR False
1614
True
5
Roscosmos False
64
True
False
14
VКS RF
202
True
В результате выполнения такой операции данные предварительно были разбиты на
шесть групп: сначала
каждой группы
-
-
на три группы по названиям организаций, а потом внутри
по значению столбца Missionstatus. После агрегации мы полу-
Часть
610
чили экземпляр класса
DataFrame,
111. Python
для научных вычислений
который имеет составной индекс (мultiindex).
В этом можно убедиться, выполнив команду:
>>> missions df.index
Multiindex ( [ ( 'RVSN USSR', False),
( 'RVSN USSR', True),
( 'Roscosmos', False),
('Roscosmos', True),
'VКS RF', False),
True)],
'VКS RF',
names;['Company', 'MissionStatus'])
Мы можем обратиться к объекту DataFrame по первой части индекса (название ор
ганизации) с помощью свойства loc. В результате мы получим объект DataFrame,
который будет содержать две строки, а индексом станет вторая часть мультииндек
са со значениями
True
и
False:
»> missions _ df. loc [ "RVSN USSR"]
Count
MissionStatus
False
163
1614
True
Если мы хотим получить конкретную строку, то нам надо использовать двойную
индексацию (указать обе части мультииндекса):
»> missions_df.loc["RVSN USSR", True]
1614
Count
Name: (RVSN USSR, True), dtype: int64
Можно получить данные и для указанного значения второй части мультииндекса:
>» missions_df.loc[:, True, :]
Count
Company
1614
RVSN USSR
64
Roscosmos
202
VКS RF
В этой записи важно указать последний символ среза «: >>
-
иначе
Pandas
будет
считать, что значение True относится к индексу по столбцам и не найдет нужный
столбец.
В завершение раздела рассмотрим пример, который покажет десятку самых попу
лярных космических аппаратов в мире. Для этого мы сначала сгруппируем данные
по столбцу Rocket, подсчитаем количество элементов в каждой группе и это значе
ние поместим в новый столбец Count. Затем отсортируем таблицу по этому значе
нию по убыванию и оставим только первые десять записей:
>>> gr_loc_size
(space_df
. groupby ("Rocket")
.agg(Count;("Location", "size")))
Глава
29.
Знакомство с
611
Pandas
>>> (gr_loc_size
.sort_values("Count", ascendinq=False)
.head(lO))
Count
Rocket
446
Cosmos-ЗM (11К65М)
299
Voskhod
128
Molniya-M /Вlock ML
126
Cosmos-2I (63SM)
125
Soyuz U
122
Tsyklon-3
111
Falcon 9 Block 5
106
Tsyklon-2
93
Vostok-2M
87
Molniya-M /Block 2В1
В этом примере только вторая команда использует ранее не упоминавшиеся мето
ды, да и то их названия говорят сами за себя:
♦
для сортировки данных по значению определенного столбца предназначен ме
тод
♦
sort _ val ues () ;
для сортировки строк по значению индекса
(он
не обязан изначально быть от
сортированным) предназначен метод sort_index ();
♦
♦
с помощью метода head () мы оставили только первые
1О
строк;
а если бы мы хотели получить вместо первых последние строки, то могли бы
воспользоваться методом
tail ().
На то, что последняя команда в примере взята в скобки, особо обращать внимание
не стоит,
-
так сделано только для того, чтобы длинную команду можно было бы
разбить на несколько строк.
Заключение
В этой главе мы рассмотрели только базовые возможности библиотеки
Pandas,
но и
их часто достаточно для решения практических задач. На этом главу про основы
работы с этой библиотекой
Pandas
нужно завершить, пока она не переросла в от
дельную книгу. Кстати, хорошая книга про
автором библиотеки
Pandas
CSV,
[4],
причем самим
Pandas.
В этой главе мы изучили возможности библиотеки
мате
уже написана
Pandas для
чтения файлов в фор
включая переименование столбцов и преобразование типов данных.
После этого мы обсудили способы создания экземпляров класса DataFrame, воз
можности для выборки данных из таблиц, а также некоторые варианты обработки
содержимого таблиц, отдельно обращая внимание на особенности, связанные с си
туациями, когда значения могут отсутствовать.
612
Часть
111. Python
для научных вычислений
И в завершение главы кратко рассмотрели достаточно объемную тему, связанную с
группировкой данных.
К сожалению, многие интересные темы, связанные с
этой главы. Например, мы ничего не сказали о том,
графики с использованием библиотеки
Matplotlib,
Pandas, остались за рамками
что Pandas позволяет строить
не рассматривали особенности
класса series, когда данные представляют собой временнь1е ряды, не упомянули
методы для объединения таблиц, как это часто делается при запросах к базам дан
ных. И это лишь наиболее крупные темы. Но если вы уже знаете о том, что в
Pandas
есть такие возможности, то методы для их использования вы наверняка без про
блем найдете в документации.
В следующей главе мы рассмотрим несколько узкоспециализированных тем, которые
могут быть полезны для научных вычислений, и поработаем с библиотекой
SciPy.
- ГЛАВА 30-
БИбЛИОТеКа
SciPy:
решение сложных научных
и инженерных задач
К этому моменту мы уже поработали с библиотеками
NumPy, Matplotlib
и
Pandas,
используемыми при реализации ряда алгоритмов, связанных с вычислениями в на
учной и инженерной практике. Эта глава посвящена более узконаправленным те
мам, основная ее цель
-
показать примеры решения некоторых научных и инже
нерных задач, а заодно познакомиться с библиотекой
SciPy,
в которой собрано
множество математических функций и операций, включая преобразование Фурье,
алгоритмы численного интегрирования, специальные математические функции,
алгоритмы обработки сигналов и изображений, алгоритмы оптимизации, интерпо
ляции, а также другие алгоритмы из различных областей математики.
Библиотека
SciPy устанавливается
без каких-либо особенностей командой:
> python -m pip install scipy
В этой главе мы воспользуемся некоторыми ее специальными функциями, а также
более подробно поговорим о функциях, связанных с быстрым преобразованием
Фурье.
Физические константы
и специальные математические функции
В этом разделе мы обратим внимание на два модуля из библиотеки
SciPy,
которые
в конце применим для решения физической задачи.
Начнем с более простого модуля
-
scipy. constants, который включает огромную
базу физических констант. Ряд наиболее распространенных величин в этом модуле
содержатся в виде переменных, причем некоторые величины представлены разны
ми переменными с одинаковыми значениями (как, например, определение скорости
света):
>>> import scipy.constants as const
>>>#Скорость света в вакууме
Часть
614
111. Python
для научных вычислений
»> const.c
299792458.0
>>> const.speed_of_light
299792458.О
>>>#Электрическая постоянная
>>> const.epsilon_0
8.8541878188е-12
>>>#Магнитная постоянная
»> const.rnu
О
1.25663706127е-06
>>>#Гравитационная постоянная
>» const.G
6. 6743е-11
>>>#Ускорение свободного падения
»> const.g
9.80665
Все значения величин определены здесь в единицах системы СИ. Полный список
констант можно найти в документации библиотеки 1 •
Однако не все включенные в модуль
виде
отдельных
переменных.
В
scipy. coпstants величины представлены в
этот
модуль
также
включен
словарь
physical_constants, содержащий еще большее количество величин, взятых из ба
зы
CODATA2 .
На момент подготовки книги эта база содержит
Ключами в словаре
2022
константы.
physical_constants являются строковые описания требуемой
величины (все они также приведены в документации библиотеки), а значениями
кортежи
из
трех элементов:
значение,
строка с
описанием
единицы
измерения
и
погрешность. Вот несколько примеров.
>>> frorn scipy.constants irnport physical_constants
>>>#Число Авогадро
>>> physical_constants["Avogadro constant"]
(6.02214076е+23,
'rnolл-1',
О.О)
>>>#Масса электрона
>>> physical_constants["electron rnass"]
(9.1093837139е-31,
kg', 2.Ве-40)
1
Отдельные элементы этих кортежей мы можем получать с помощью функций
value () (значение), uni t () (строка с описанием единицы измерения) и
precision () (погрешность), также содержащихся в модуле scipy. constants. На
пример:
>>>#Постоянная Больцмана
>» const. value ("Bol tzrnann constant")
1.380649е-23
1 См.
2
https://docs.sclpy.org/doc/sclpy/reference/constants.html.
См. https://physlcs.nlst.gov/cuu/Constants/.
Глава
30.
Библиотека
SciPy:
>>>#Единица измерения
решение сложных научных и инженерных задач
615
(Дж/К)
>>> const.unit("Boltzmann constant")
'J кл-11
В качестве практического примера решим задачу, связанную с расчетом эффектив
ной площади рассеяния (ЭПР) идеально проводящего шара в зависимости от часто
ты падающей на него электромагнитной волны.
ЭПР
-
это используемая в радиолокации величина, которая описывает объект с
точки зрения его отражательной способности, характеризующей радиолокацион
ную заметность этого объекта. ЭПР определяет, какая часть энергии электромаг
нитной волны, падающей на объект, отразится в обратном направлении (так назы
ваемая моностатическая ЭПР) или в произвольном другом направлении (бuста
тическая ЭПР). ЭПР измеряется в м 2 , но эту величину нельзя ассоциировать с
площадью объекта. Такая размерность получилась из определения ЭПР, согласно
которому нужно поделить мощность, отраженную объектом в Вт, на плотность по
тока мощности падающей волны в Вт/м 2 .
Для идеально проводящего шара радиусом
помощью бесконечного сходящегося ряда
r
величину ЭПР
(30.1).
л2
cr=-;-II:)-l{(n+0.5){b"
где л
(30.5):
cr
можно рассчитать с
Эта формула взята из книги
-ап)I
2
(30.1)
длина волны, а коэффициенты ап и Ьп рассчитываются по формулам
(30.2)-
Jn ( kr)
а
(30.2)
=---'-----'-
"
в этих выражениях: r -
[5]:
h(2) (
kr)
Ь = krJп-i(kr)-nJп(kr)
" krhi:\ ( kr )- nh~ 2 ) ( kr)
(30.3)
hi 2) { Х) = Jn ( Х ) - iyn ( Х)
(30.4)
k = 21t!л
(30.5)
радиус идеально проводящего шара,
k-
волновое число,
}п(х)- сферическая функция Бесселя первого рода, Уп(х)- сферическая функция
Бесселя второго рода, hi 2)( х) -
сферическая функция Ханкеля второго рода.
У функцийjп(х), Уп(х) и hi2)( х) нижний индекс п обозначает порядок функции.
Пусть в качестве входных данных нам задан диапазон частот, тогда для пересчета
его в диапазон длин волн мы воспользуемся выражением л
= c/f,
где с
-
скорость
света в вакууме. Кроме того, в качестве входных данных задан также радиус шара
r
в метрах.
Построим с помощью библиотеки
Matplotlib
график покажет зависимость
а второй должен выглядеть аналогично, но по-
cr (j),
два графика в одном окне. Первый
Часть
616
111. Python
для научных вычислений
кажет нормированную обобщенную величину а/ ( rrr 2) от 2rrr/л.. График нормиро
ванной величины позволит убедиться в правильности расчетов, о чем будет сказано
позже.
Бесконечный ряд в выражении
(30.1)
сходится, каждый последующий элемент ряда
меньше предыдущего, и обычно для определения момента окончания суммирова
ния сравнивают каждый член ряда с заранее выбранным порогом, и если член ряда
меньше (в нашем случае по модулю) заданного порога, суммирование останавли
вают. Однако для упрощения кода мы заранее выберем количество элементов ряда.
Для проверки правильности выбора количества элементов ряда мы можем увели
чить это число и убедиться, что результат заметно не изменился.
Для расчета ЭПР согласно приведенным формулам нам понадобятся содержащиеся
в модуле scipy. special сферические функции Бесселя. Наряду с ними, этот мо
дуль также содержит и ряд других функций:
♦ обычные функции Бесселя: первого рода
рядка
-
j v () , второго рода для целого по
-
yn ( ) , второго рода для дробного порядка -
yv () ;
♦
функции Ханкеля: первого рода- hankell (), второго рода- hankel2 ();
♦
ускоренные функции Бесселя первого рода нулевого и первого порядков
и
♦
j 1 (),
-
уО
()
и
jо
()
yl () ;
сферические функции Бесселя: первого рода- spherical jn () и второго ро
да-
♦
а также второго рода нулевого и первого порядков
-
spherical yn ();
функции для вычисления нулей функций Бесселя, производные от функций Бесселя и множество других модификаций этих функций.
Помимо указанных функций, в этот модуль включены функции многих других ви
дов
-
например, гамма-функции и их модификации, многочлены Лежандра, функ
ции для вычисления полиномов Чебышева, функции, связанные с различными ста
тистическими распределениями: биномиальным распределением, бета-распределе
нием, гамма-распределением и другими.
Код, который решает описанную задачу для расчета ЭПР проводящего шара, при
веден в листинге
Листинг
30.1.
30.1. Chapter_ЗO/example_01/rcs.py
import numpy as np
from scipy.special import spherical_jn, spherical_yn
from scipy.constants import pi, с
from matplotlib import pyplot as plt
def spherical_hn2(n,
"""Функция
х):
Ханкеля
второго рода"""
return spherical_jn(n,
х)
- lj * spherical yn(n,
def an(n, х):
return spherical jn(n,
х)
/ spherical_hn2(n,
х)
х)
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
def bn (n, kr) :
return ((kr * spherical_jn(n - 1, kr) n * spherical_jn(n, kr)) /
(kr * spherical hn2(n - 1, kr) n * spherical_hn2(n, kr)))
def sphere rcs(r,
lmЬd,
nmax):
"'"'Расчет эффективной площади рассеяния
идеально проводящего шара"""
k = 2 * pi / lmЬd
kr = k * r
result = О.О
for n in range(l, птах+ 1):
result += (((-1) ** n) * (n + 0.5)
* (bn(n, kr) - an(n, kr)))
return
if
(lmЬd**2
/ pi) * np.abs(result) ** 2
name
main "·
# диапазон частот
freq = np.linspace(O.Ole9,
# Радиус
r = 0.15
300)
шара
# Количество
nmax = 20
#
4е9,
слагаемых в ряде
Дпина волны
lmЬd =с/
freq
rcs = sphere_rcs(r,
lmЬd,
nmax)
# Построение ненормированного графика
fig = plt.figure()
axl = fig.add_subplot(2, 1, 1)
axl.plot(freq / le9, rcs)
ЭПР от частоты
axl.set_xlabel(r"Чacтoтa,
ГГц")
axl.set_ylabel(r"$\sigma,
axl. grid ()
\tехt{м}л2$")
#
Построение нормированного графика
х
values
2 * pi * r / lmЬd
y_values = rcs / (pi * r ** 2)
= fig.add_subplot(2, 1, 2)
ax2.plot(x_values, y_values)
ax2.set_xlabel(r"$2\pi r/\lamЬda$")
ах2
617
Часть
618
111. Python
для научных вычислений
ax2.set_ylabel(r"$\sigma/(\pi rл2)$")
ax2.set_xticks(np.arange(0, 14, 1.0))
ах2. grid ()
fig.tight_layout()
plt. show ()
В этом примере мы импортируем из модуля scipy. special сферические функции
Бесселя spherical_jn() и spherical_yn(). Важно, что нам нужны именно сфери
ческие функции, а не обычные. В модуле scipy. special нет сферических функций
Ханкеля, но это не является проблемой, поскольку сферическая функция Ханкеля
второго рода легко вычисляется по формуле (30.4), и мы ее реализуем в виде функ
ции
spherical_hn2 ().
Константы
1t
и
с
(скорость
света
в
вакууме)
мы
импортируем
из
модуля
scipy. constants.
Функции an () и bn () рассчитывают коэффициенты ап и Ьп соответственно. В этом
примере используется векторизация с помощью массивов
NumPy:
сначала рассчи
тываются длины волн сразу для всех заданных частот, результат помещается в мас
сив
lmbct,
а затем для всех элементов из этого массива рассчитываются последую
щие требуемые величины и, в конечном итоге, значения ЭПР.
0.25
0.20
1
0.15
t:i 0.10
➔ ·-"---+-----½+-··.,.
0.05
0.00
t...::t:::,::.:.:...:.:.=..;::.:.:...:.:::.:.:...:.::....:j:::.:.:...:.:::.:.:...:.::t:.:;::.:.:...:.:~l::===t==::t..==t.==.....:t....:J
о.о
0.5
1.0
1.5
2.0
2.5
3.0
3.5
4.0
Частота.ГГц
3
12+++1++-~-~~-~-~----+·············l+··········· ··+I
'fi
1
О
1
2
3
4
S
б
7
8
9
10
11
12
13
2rrrf}.
Рис. 30.1. Ненормированный (вверху) и нормированный (внизу) графики,
показывающие зависимость ЭПР идеально проводящего шара от частоты
Полученные графики приведены на рис.
30.1.
Нижний нормированный график сви
детельствует, что расчет выполнен верно. Это видно из двух фактов: максимальное
значение графика приходится на значение 1 по горизонтальной оси, а затухающие
Глава
30.
Библиотека
SciPy:
619
решение сложных научных и инженерных задач
колебания сходятся к значению
1 по
вертикальной оси. Из этих графиков следует,
что при стремлении частоты к бесконечности ЭПР идеально проводящего шара
стремится к площади сечения этого шара.
Преобразование Фурье
Преобразование Фурье, названное так в честь французского математика и физика
Жана-Батиста Жозефа Фурье
(1768-1830)-
важнейший инструмент в различных
инженерных областях, в том числе при обработке сигналов, их фильтрации и сжа
тии. Например, формат сжатия музыки МР3 основан на преобразовании Фурье, а
формат сжатия изображений
Преобразование Фурье
-
JPEG использует двумерное
преобразование Фурье.
весьма объемная тема, и этот раздел не претендует на
полноту изложения. Приведенные здесь формулы нужны для базового понимания
сути преобразования, а основная цель этого раздела
рументы, предоставляемые библиотекой
SciPy
-
продемонстрировать инст
для различных видов преобразова
ния Фурье.
Жан-Батист Фурье показал, что любую периодическую функцию
s(t)
с периодом Т
можно разложить в бесконечный ряд, представляющий собой сумму гармониче
ских колебаний с различными частотами, амплитудами и фазами.
Позже математики нашли, что функции можно раскладывать не только на гармо
нические составляющие, но и по различным другим базисам, состоящим из функ
ций, которые должны обладать определенными свойствами (быть ортогональными
другим функциям своего базиса),
вание. Библиотека
SciPy
так появилось, например, вейвлет-преобразо
-
в предыдущих версиях также включала в себя функции
для вейвлет-преобразований, однако, начиная с версии
SciPy
1.15.
с рекомендациями использовать отдельную библиотеку
они были удалены из
PyWavelets.
Поэтому
здесь мы будем говорить только о разложении функций на гармонические колеба
ния с помощью преобразования Фурье.
В изначальной формулировке представление бесконечной по длительности перио
дической функции
формулами
s(t) в виде
(30.6)-(30.9):
s(t)=
где СОп -
разложения на гармонические колебания описывается
~ + I:)ancos(roп1)+bnsin(roп1)),
круговая частота, равная
ron = 21tf,,,f,, -
2 fT/2
а0 = -
Т
-Т/2
линейная частота. В этой формуле:
s ( t) dt ;
2 fT/2
а,,= Т -ms(t)cos(roi)dt;
f
2 Т/2
а =п
Т
-Т/2
(30.6)
s(t)sin(roi)dt.
(30.7)
(30.8)
(30.9)
620
Часть
111. Python
Если применить формулу Эйлера и заменить функции
sin
и
для научных вычислений
cos
на экспоненты, то
ряд Фурье можно записать через комплексные величины:
(30.10)
где i -
мнимая единица, для которой Р = -1,
•
1
S(ffiJ=-
f
Т
Т/2
•
-Т/2
(30.11)
s(t)e-"""'dt.
Точки над символами обозначают, что это комплексные величины. Формула
записана в общем виде, когда раскладываемая в ряд функция
s(t)
(30.10)
может быть также
комплексной, но мы для простоты далее будем работать только с действительными
функциями сигнала s(t). Величины
S(ffi") называют гармониками на круговой
тоте ffiп, а полный набор гармоник для сигнала
Для периодических функций
жения
s(t)
-
называют спектром сигншю.
спектр является дискретным, что видно из выра
(30.1 О).
Обратите внимание, что в выражении
п
s(t)
час
(30.1 О)
появились отрицательные значения
для таких значений ffiп можно интерпретировать как отрицательные частоты.
Эти частоты не физичные, то есть мы не можем их измерить с помощью анализато
ра спектра, но их нужно учитывать математически при учете энергии сигнала.
Если у нас не периодический сигнал, то мы говорим не о спектре сигнала, а о его
спектрш,ьной плотности
S(ffi),
потому что для не периодических сигналов эта
величина станет непрерывной.
При обработке сигналов на компьютере мы, как правило, имеем дело с дискретны
ми сигналами. Сигнал называется дискретным, если у нас имеются его значения
только в определенные (дискретные) моменты времени. Именно такие сигналы
хранятся в массивах. Поскольку массив конечен, то такой сигнал не является бес
конечным периодическим, даже если в массиве записано большое количество пе
риодов периодического сигнала. Таким образом получается, что первоначальное
разложение в ряд Фурье для таких сигналов не применимо. Однако математики
придумали, как выйти из этой ситуации,
-
они стали предполагать, что сигнал,
записанный в конечный массив, бесконечно повторяется.
Это позволяет нам перейти к дискретному преобразованию Фурье, с помощью ко
торого от дискретного сигнала
s(п),
хранящегося в массиве, мы можем перейти к
дискретным отсчетам спектральной плотности
ны в массив, где п и
S(k),
которые также будут записа
номера элементов массивов сигнала и спектральной плот
k-
ности соответственно.
Переход от сигнала
s(п)
к спектральной плотности
S(k)
называется пря.мым дис
кретным преобразованием Фурье и записывается в виде
•
N-1 •
S(k)=Lп~os(n)e
-i3_r<nk
N
,k=0,1, ... ,N-1.
(30.12)
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
Переход от спектральной плотности
S(k)
к сигналу
s(п)
621
называется обратным
дискретным преобразованием Фурье и записывается в виде:
(30.13)
В выражениях
(30.12)
и
(30.13)
подразумевается, что массивы с сигналом и спек
тральной плотностью имеют одинаковый размер, равный
Модуль величины
личины
-
S(k)
N
отсчетам.
называется амплитудным спектром, а аргумент этой ве
фазовым спектром.
С точки зрения программирования, записать функции для расчета по формулам
(30.12)
и
(30.13)
вать значения
не является проблемой, однако придется многократно рассчиты
экспонент, да еще и
с комплексными степенями, а это достаточно
медленная операция. К счастью, и тут нам на помощь пришли математики, которые
придумали алгоритм быстрого преобразования Фурье (БПФ), или по-английски
Fast Fourier Transfonn (FFT),
-
который позволяет значительно сократить количество
рассчитываемых экспонент. Особенно быстро этот алгоритм работает в случае, ес
ли длина массива
N
равняется степени двойки
( 128, 512, 1024
и т. д. ). Именно этим
алгоритмом мы и будем пользоваться.
Модули для расчета быстрого преобразования Фурье имеются как в
дуль numpy. fft), так и в
SciPy (scipy. fft).
Однако библиотека
NumPy (мо
SciPy содержит
больше модификаций этого алгоритма, и хотя для наших примеров вполне хватило
бы функций из
NumPy,
мы воспользуемся модулем scipy. fft. С другой стороны,
если в вашей программе не задействована библиотека
SciPy,
но применяется
NumPy,
то те же самые функции, с которыми мы будем работать далее, также имеются и в
модуле
numpy. fft.
В следующих примерах мы рассмотрим различные функции, полезные при анализе
спектров, на примере скрипта, который сначала создает последовательность прямо
угольных импульсов с заданным коэффициентом заполнения
-
величиной, равной
отношению длительности импульса к его периоду повторения (эта величина обрат
но пропорциональна скважности). После этого рассчитывается спектр сигнала с
помощью функции fft (). В результате в одном окне будут отображаться три гра
фика: сигнал, его амплитудный спектр и фазовый спектр.
Первая версия такого скрипта приведена в листинге
import numpy as пр
import numpy.typing as npt
import matplotlib.pyplot as plt
from scipy.fft import fft
def meander(time: npt.NDArray,
duty: float,
period: float) -> npt.NDArray:
30.2.
622
Часть
111. Python для
научных вычислений
"" "Функция генерации последовательности прям_оугольных импульсов."""
s = np.zeros like(time)
triangle = (
np.arcsin(np.sin(2.0 * np.pi / period * time + np.pi / 2)) /
np.pi + 0.5)
s [triangle < duty]
1
return s
if
name
#
dt =
#
main
"·
Шаг дискретизации сигнала по времени в секундах
2е-11
Дпина сигнала в отсчетах
signal len = 8 * 1024
# Временные отсчеты в секундах
time = np.arange(signal len) * dt
# Коэффициент заполнения
duty = 0.25
#
Период сигнала в секундах
period =
#
4е-9
Исследуемый сигнал
signal = meander(time, duty, period)
#
Спектр сигнала
(массив комплексных чисел)
spectrum = fft(signal)
# Амплитудный спектр
spectrum_mag = np.abs(spectrum)
#
Фазовый спектр
spectrum_phase = np.angle(spectrum)
# Вывод графиков
fig = plt.figure(figsize=(l0, 8))
#
Вывод сигнала
ax_signal = fig.add_subplot(З, 1, 1)
ax_signal.plot(time / le-9, signal)
ах_ signal. set_ ti tle ("Сигнал")
ax_signal.set_xlabel("Bpeмя,
нс")
ax_signal.set_ylabel("s(t)")
ax_signal.set_xlim(0, 20)
ах signal.grid()
#
Вывод амплитудного спектра
spectrum_mag = fig.add_subplot(З, 1, 2)
ax_spectrum_mag.plot(spectrum_mag)
ax_spectrum_mag.set titlе("Амплитудный спектр")
ах
ax_spectrum_mag.set_xlabel("Чacтoтa,
spectrum_mag. set ylabel (" 1 S (f)
ax_spectrum_mag.grid()
ах
1 ")
отсчеты")
Глава
30.
Библиотека
SciPy:
623
решение сложных научных и инженерных задач
# Вывод фазового спектра
ax_spectrum_phase = fig.add_subplot(З, 1, 3)
ax_spectrum_phase.plot(spectrum_phase)
ax_spectrum_phase.set_title("Фaзoвый спектр")
ax_spectrum_phase.set_xlabel("Чacтoтa,
отсчеты")
ax_spectrum_phase.set_ylabel("arg(S(f) )")
ax_spectrum_phase.grid()
fig.tight_layout()
plt.show ()
В этом примере нам понадобится функция быстрого преобразования Фурье f ft (),
которую мы импортируем из модуля
scipy. fft.
Функция meander () создает последовательность импульсов. На вход она принимает
массив time с временнь1ми отсчетами в секундах, коэффициент заполнения duty и
период повторения импульсов period в секундах. Один из способов создания по
следовательности импульсов
-
это сначала создать последовательность треуголь
ных сигналов, амплитуда которых линейно меняется от О до
следовательность показана на рис.
30.2
1и
обратно. Такая по
(в коде функции meander () -
это массив
triangle). Затем с помощью функции numpy.zeros_like() создается массив s, за
полненный нулями. Массив
функцию
s
будет такого же размера, что и массив, переданный в
zeros_like () как параметр. После этого тем элементам массива s, где
значение соответствующего элемента массива triangle меньше значения коэффи
циента заполнения, присваивается значение
1.
1.0
о.в
О. б
0.4
0.2
о. о
о. о
2.5
5.0
7.5
10.0
12.5
15.0
20 .0
17.5
Время. нс
Рис.
30.2.
Последовательность треугольных импульсов,
на основе которых создается последовательность прямоугольных импульсов
В основной части скрипта сначала задаются параметры исходного сигнала. Мы сде
лаем достаточно длинный периодический сигнал длиной 8х
l 024 = 8192
элемента,
который будет расположен в массиве signal. Размер выбран равным степени двойки,
чтобы функция fft () работала максимально быстро. Шаг дискретизации задали рав
ным 20 пс (2Ох 10· 12 с). Таким образом, общая длина сигнала равна 8 l 92x20x l 0· 12 с=
= 0.16384 мкс= 0. l 6384x 1о-<> с. Мы не станем отображать всю эту длинную после
довательность импульсов на графике, а ограничимся длительностью 20 нс (2Ох 10-9 с).
Зато такой длинный импульс позволит нам увидеть характерные особенности спек-
Часть
624
111. Python
для научных вычислений
тра сигнала, который стремится к бесконечному периодическому, и с достаточно
мелким шагом дискретизации по частоте.
После создания массива signal рассчитывается спектр сигнала, который будет хра
ниться в переменной spectrum. Это массив комплексных величин, он имеет такую
же длину, что и массив с сигналом. Модуль этих комплексных величин представля
ет собой амплитудный спектр (переменная spectrum mag), а аргумент в радианах
-
фазовый спектр (переменная spectrum_phase).
Остаток кода занимается оформлением графиков
в одном окне (рис.
-
дятся три графика: исходный сигнал (с ограничением по времени
20
30.3)
выво
нс), амплитуд
ный спектр и фазовый спектр.
-
1.0
Сигнал
-
1-
0.8
-
'
-
~
0.6
~ 0.4
1
.
0.2
f---
1
2.5
5.0
-
о.о
-
--
t
- -
-
-
·-+ .
о.о
7.5
10.0
12 .S
15.0
17.5
20.0
Время. нс
Амплитудный спектр
2000 1500
+-
~ 1000
500
о
о
r-
4000
2000
6000
8000
Частота, отсчеты
Фазовый спектр
t\
--
-
.
,-
1
-
-
-2
1
о
'
2000
4000
6000
8000
Частота, отсчеты
Рис.
30.3.
Исходный сигнал, амплитудный
и фазовый спектры последовательности импульсов
Обратите внимание, что для исходного сигнала по оси Х откладываются отсчеты по
времени в нс, а для амплитудного и фазового спектров
-
пока только номера от
счетов.
Поскольку у нас достаточно длинная последовательность импульсов в исходном
массиве, внешний вид спектра приближается к внешнему виду бесконечной после-
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
625
довательности импульсов, и на центральном графике мы наблюдаем характерный
линейчатый характер спектра такого сигнала.
Если вы ранее не имели дела с быстрым преобразованием Фурье, то вас может уди
вить распределение отсчетов спектра в массиве амплитудного спектра (по фазово
му спектру пока мало что можно понять). У нас образовалось два скопления наи
больших гармоник
-
в начале массива и в конце. Это связано с особенностями
расчета быстрого преобразования Фурье. В результате расчета частоты в массиве
длиной
N располагаются
♦
N-
если
•
•
следующим образом:
четное:
0-й элемент соответствует нулевой частоте (постоянной составляющей);
элементы с номерами от 1 по (N/2) - 1 включительно соответствуют возрас
тающим положительным частотам;
•
элементы с номерами от
N/ 2
и до конца массива соответствуют возрастающим
отрицательным частотам;
♦
если
•
N-
нечетное:
0-й элемент соответствует нулевой частоте (постоянной составляющей);
• элементы с номерами от 1 по
<N- l)/ 2
включительно соответствуют возрастаю
щим положительным частотам;
•
элементы с номерами от
>; и до конца массива соответствуют возрас-
<N+ 1 2
тающим отрицательным частотам.
Если нас интересуют только положительные частоты, то мы можем просто не пока
зывать вторую половину массива с отрицательными частотами. Есть теорема, пока
зывающая, что если мы рассчитываем преобразование Фурье от действительного
сигнала, то полученные в результате отсчеты на частотах
плексно-сопряженными: S(fп) =
f,,
и
-f,,
являются ком-
s· (-fп ), то есть амплитуды у ЭТИХ отсчетов спек
тра совпадают, а мнимые части и аргументы имеют противоположный знак.
Так что, если бы после расчета спектра мы добавили бы следующую проверку, то
она бы успешно выполнилась:
assert abs(np.abs(spectrum[S]) - np.abs(spectrum[-5])) < le-10
assert abs(np.angle(spectrum[S]) + np.angle(spectrum[-5])) < le-10
Однако для обработки и визуализации не всегда удобно, что отрицательные часто
ты расположены после положительных,
-
хотелось бы поменять две половинки
массива местами, чтобы сначала располагались отрицательные частоты, а затем
-
положительные. Для этого в модуле scipy. fft имеется функция fftshift (), кото
рая делает такую перестановку.
В предыдущем примере (см. листинг
функции fftshift ()
функцией (листинг
30.2)
изменим две строчки: добавим импорт
и результат вызова функции fft ()
30.3).
сразу обработаем этой
626
Часть
Листинг 30.3.
111. Python
для научных вычислений
Chapter_30/example_03/fft_meander_fftshlft.py
from scipy.fft import fft, fftshift
signal = meander(time, duty, period)
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.angle(spectrum)
В результате внешний вид спектра примет вид, показанный на рис.
30.4
(для со
кращения места график с исходным сигналом не показан). Теперь отсчеты отрица
тельных частот стали располагаться левее положительных, и основные гармоники
спектра расположены в центре массива. Про фазовый спектр мы по-прежнему
не можем сказать ничего интересного, но по нему тоже видно, что его половинки
также поменялись местами по сравнению с рис.
30.3.
Амплитудный спектр
2000
1
1500
[
1000
4
500
о
f
····-,
о
2000
4000
6000
8000
Частота, отсчеты
Фазовый спектр
-
-
~
....
~
--
rs
1
- ·"
-2 -·--о
--
-
....
1
2000
]
1
бООО
4000
8000
Частота, отсчеты
Рис.
30.4.
Результат расчета спектра последовательности прямоугольных импульсов
после применения функции
fftshift ()
После применения функции fftshift ( J отсчеты в массиве спектра длиной
N
будут
расположены в следующем порядке:
♦
если
N-
четное:
• элементы с О до (N/2) - l включительно соответствуют отрицательным часто
там. Нулевой элемент соответствует частоте -(N/2 )xЛJ;
Глава
30.
Библиотека
решение сложных научных и инженерных задач
SciPy:
• элемент с номером
N/2
627
соответствует нулевой частоте (постоянной состав
ляющей);
• элементы, начиная с (/2) + 1 и до конца массива соответствуют положитель
ным частотам;
♦
если
N-
нечетное:
• элементы с О до
(N-
'J; 2 - 1 включительно соответствуют отрицательным час
тотам. Нулевой элемент соответствует частоте -(<N- J)/2 )xЛJ;
• элемент с номером
(N
'J/2 соответствует нулевой частоте (постоянной состав
ляющей);
• элементы, начиная с <N- IJ;2 + 1 и до конца массива соответствуют положительным частотам.
Теперь, когда мы разобрались с расположением отсчетов спектра в массиве, на оси
Х графиков, вместо номеров отсчетов, можно нанести значения частот. Для этого
нужно знать величину дискрета по частоте Лf, который рассчитывается по следую
щей формуле:
лr-_1_
у
где
N-
-
N-Лt'
количество элементов в массиве, а Лt
-
(30.14)
шаг дискретизации сигнала.
Зная это, мы можем сформировать в нашем примере массив частот, который будем
использовать при построении графиков (листинг
30.4).
dt = 20е-12
signal_len = 8 * 1024
time = np.arange(signal_len) * dt
duty = 0.25
period = 4е-9
signal = meander(time, duty, period)
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.angle(spectrum)
df = 1 / (signal_len * dt)
freq = np.arange(-signal_len / 2, signal_len / 2) * df
ax_spectrum_mag = fig.add_subplot(З, 1, 2)
ax_spectrum_mag.plot(freq / le9, spectrum_mag)
ах_ spectrum_mag. set _ xlabel ( "Частота, ГГц")
ax_spectrum_phase = fig.add_subplot(З, 1, 3)
ax_spectrum_phase.plot(freq / le9, spectrum_phase)
ax_spectrum_phase.set_xlabel("Чacтoтa,
ГГц")
628
Часть
111. Python
для научных вычислений
После этих изменений графики со спектрами будут выглядеть, как показано на
рис.
30.5.
Амплитудный спектр
2000
1500
~
t
1000
500 - -
--+-
-20
-10
о
10
20
10
20
Частота . ГГц
Фазовый спектр
~
~
~
o ➔--Ч~l~ffl~Нlhill/Нl-lll~~~ННJll+HINll'Жll~l➔ll~l,l~lll~~~H➔l~llllll➔H
11
'
-2
- 10
-20
о
Частота. ГГц
Рис.
30.5.
Графики спектров nосле расчета массива частот
Для понимания принципа работы дискретного преобразования Фурье полезно са
мостоятельно уметь рассчитывать массив частот, как мы это только
что сделали.
Однако в модуле scipy. fft имеется функция fftfreq (), которая принимает в ка
честве параметров количество отсчетов в сигнале и величину дискрета по времени Лt,
а возвращает рассчитанный по описанным правилам массив частот. При этом
функция
возвращает массив
частот
без
применения
функции
fftfreq ()
fftshift (), то есть в полученном массиве сначала будут располагаться положи
тельные частоты, а затем отрицательные. Поэтому для нашего примера нам придет
ся применить функцию fftshi ft () также и для рассчитанного с помощью функции
fftfreq ()
массива частот.
В листинге
30.5
приведен обновленный с учетом сказанного код из листинга
расчетом массива частот (не забываем импортировать функцию fftfreq () ).
Листинr 30.5. Chapter_ЗO/example_OS/fft_meander_fftfreq.py
from scipy.fft import fft, fftshift, fftfreq
dt = 20е-12
signal_len = 8 * 1024
time = np.arange(signal len) * dt
duty = 0.25
period = 4е-9
40.4
с
Глава
30.
Библиотека
SciPy:
629
решение сложных научных и инженерных задач
signal = meander(time, duty, period)
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.angle(spectrum)
freq = fftshift(fftfreq(signal_len, dt))
В результате мы получим те же графики, что были показаны ранее на рис.
30.5.
Займемся теперь фазовым спектром. Для большей наглядности посмотрим на полу
ченные спектры в более узкой полосе частот
-
например, от
диапазоне графики выглядят, как показано на рис.
-7
до
ГГц. В этом
7
30.6.
Амплитудный спектр
2000
t
т
+
1500
r
~ 1000
-6
+
г
500
-7
i1
~
-5
-4
-3
-2
-1
о
1
2
з
4
5
6
4
5
6
7
Частота. ГГц
Фазовый спектр
·t
-2
-7
-6
-5
-4
-з
-2
-1
о
Частота, ГГц
Рис.
30.6.
Графики спектров в более узком диапазоне частот
Первое, что бросается в глаза при взгляде на фазовый спектр,
-
перескоки фаз на 7t
от гармоники к гармонике на амплитудном спектре. Но, кроме того, виден линей
ный набег фаз, выражающийся в наклоне графика фазового спектра. При этом, по
скольку по определению аргумент комплексного числа лежит в диапазоне от
при приближении к этим границам происходит коррекция фазы на
21t,
-7t
до
1t,
чтобы оста
ваться в указанном интервале. Если бы такой неявно производимой коррекции не
было, то фазовый спектр представлял бы собой прямую наклонную линию, на ко
торую накладывались бы скачки на 7t между гармониками.
Для некоторых задач удобнее иметь такой линейный фазовый спектр без коррекции
на
21t
и не ограниченный интервалом от -7t до 7t. Чтобы избавиться от перескоков
фаз на
2n,
в модуле numpy имеется функция unwrap (). Если ее применить в нашем
примере, то его код будет выглядеть следующим образом (листинг
30.6).
Часть
630
Лмстмнr 30.6.
111. Python для научных вычислений
Chapter_30/example_06/fft_meander_unwrap.py
signal = meander(time, duty, period)
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.unwrap(np.anqle(spectrum))
freq = fftshift(fftfreq(signal_len, dt))
После этого изменения спектры будут выглядеть, как показано на рис.
30.7
(чтобы
получить этот график, был дополнительно изменен интервал отображения для фа
зового спектра по оси У).
Амплитудный спектр
2000
1500
[
1000
-б
-4
-2
о
2
4
б
Частота. ГГц
Фа,овый спектр
275
,-----,-----r------,------,-----,--------т------,--~
250
175
-6
-4
-2
о
4
б
Частота, ГГц
Рис.
30.7.
Спектры nосле применения функции
unwrap ()
к фазовому спектру
Обратите внимание на величину углов по оси У на графике фазового спектра
-
имейте в виду, что это величина в радианах.
Для следующих примеров мы возьмем другой сигнал
-
одиночный прямоуголь
ный импульс. В этом случае спектр не будет иметь линейчатого характера, а станет
непрерывным, и правильнее его теперь называть спектральной плотностью. Для
начала ничего нового, с точки зрения программирования, в примере из листинга
нет
-
в нем используются те же функции, что в предыдущих примерах.
Лмстмнr 30.7. Chapter_З0/example_07/fft_rect.py
import numpy as np
import numpy.typing as npt
30.7
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
import matplotlib.pyplot as plt
from scipy.fft import fft, fftshift, fftfreq
def rect(time: npt.NDArray, Т: float, delay: float) -> npt.NDArray:
s = np.zeros_like(time)
s[ (time >= delay) & (time < (Т + delay))) = 1
return s
if
name
==" main "·
dt = 20е-12
signal_len = 8 * 1024
time = np.arange(signal_len) * dt
Т = le-9
delay = le-9
signal = rect(time, Т, delay)
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.unwrap(np.angle(spectrum))
freq = fftshift(fftfreq(signal_len, dt))
freq_min_GHz
freq_max_GHz
-7
7
fig = plt.figure(figsize=(l0, 8))
ax_signal = fig.add_subplot(З, 1, 1)
ax_signal.plot(time / le-9, signal)
ах_ signal. set_ ti tle ("Сигнал")
ax_signal.set_xlabel("Bpeмя,
нс")
ax_signal.set_ylabel("s(t)")
ax_signal.set_xlim(0, 10)
ах_signal. grid ()
ax_spectrum_mag = fig.add_subplot(З, 1, 2)
ax_spectrum_mag.plot(freq / le9, spectrum_mag)
ах_ spectrum_mag. set_ ti tle ( "Амплитудный спектр")
ax _spectrum_mag. set_xlabel ("Частота, ГГц")
ах_ spectrum_mag. set _ylabel (" 1 S (f) 1 " )
ax_spectrum_mag.set_xlim(freq_min_GHz, freq_max_GHz)
ax_spectrum_mag.grid()
ax_spectrum_phase = fig.add_subplot(З, 1, 3)
ax_spectrum_phase.plot(freq / le9, spectrum_phase)
ax_spectrum_phase.set_title("Фaзoвый спектр")
ax_spectrum_phase.set_xlabel("Чacтoтa,
ах_ spectrum_phase.
ГГц")
set_ylabel ("arg (S (f)),
рад.")
631
Часть
632
111. Python
для научных вычислений
ах spectrum_phase.set_xlim(freq_min_GHz, freq_max_GHz)
ax_spectrum_phase.set_ylim(-250, -100)
ax_spectrum_phase.grid()
fig.tight_layout()
plt. show ()
В этом примере созданием прямоугольного импульса занимается функция rect (),
принимающая на вход массив временнь1х отсчетов, длительность сигнала т в се
кундах и его задержку delay (тоже в секундах) относительно нулевого момента
времени. Внутри этой функции создается массив s, изначально заполненный нуля
ми, но для элементов, соответствующих интервалу времени от
значения устанавливаются равными
delay
до
delay +
т,
1.
Остальной код практически полностью повторяет предыдущие примеры, за исклю
чением небольших оформительских моментов. В результате выполнения этого
скрипта будут показаны следующие графики (рис.
Сигнал
+
1.0
--
0.8
•·~-
-+
······-····
О.б
17,
-
30.8).
0.4
1
0.2
-
1
-
- -
.
i
1
о.о
о
б
4
2
10
8
Время.нс
Амплитудный спектр
r'
40
_зо
+-
:i[ 20
+···
-6
Г-
-4
-2
о
2
4
б
Частота. ГГц
Фазовый спектр
-100 ~ - ~ - - - - ~ - - - - ~ - - - - ~ - - - - - - - - - ~ - - - - ~ - ~
t
~
-125
-150
+=-r--===----+------+------+------+
+----+------+---'--="'"f--===----+------+-----+-----t----1
-175 +-- - + - - - - - + - - · ·
ii[
+---+-----+-----+-----+-----+-----+--====-__,,"""- j
"§
-200
"'
-225
+-----+--
-250
~--+-----+-----+-----+-----+------+------+-----'
-б
-4
-2
о
Частота. ГГц
Рис.
30.8.
Одиночный прямоугольный импульс,
его амплитудный и фазовый спектры
4
б
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
633
Здесь можно обратить внимание на линейный наклон фазового спектра, который
будет тем больше, чем больше величина задержки. Спектр теперь представляет со
бой непрерывную характеристику (с точностью до дискрета по частоте), а переско
ки фаз на 1t на фазовом спектре соответствуют нулям амплитудного спектра.
Если мы работаем с действительными сигналами, то как уже говорилось, значения
спектральных
отсчетов
для
отрицательных
частот
являются
комплексно
сопряженными для соответствующих отсчетов в положительной области частот.
Для упрощения обработки таких сигналов в модуле scipy. fft имеется функция
быстрого преобразования Фурье
rfft (), которая возвращает массив, включающий
также функция irfft () для
обратного преобразования Фурье и функция rfftfreq () для генерации массива
только неотрицательные частоты. Имеются в нем
частот в этом случае.
Изменим пример, приведенный в листинге
функции rfft () и rfftfreq () (листинг
30.7,
30.8).
Листинг 30.8. Chapter_З0/example_08/rfft_rect.py
from scipy.fft import rfft, rfftfreq
if
name
main "·
dt = 20е-12
signal_len = 8 * 1024
time = np.arange(signal_len) * dt
Т = le-9
delay = le- 9
signal = rect(time, Т, delay)
spectrum = rfft(signal)
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.unwrap(np.angle(spectrum))
freq = rfftfreq(signal_len, dt)
freq_max_GHz = 7
fig = plt.figure(figsize=(lO, 8))
ax_spectrum_mag = fig.add_subpl o t(З, 1, 2)
ax_spectrum_mag.plot(freq / le9, spectrum_mag)
ax_spectrum_mag.set_title("Aмплитyдный спектр")
ax_spectrum_mag.set_xlabel("Чacтoтa,
ГГц")
spectrum_mag. set _ylabel (" 1S (f) 1")
ax_spectrum_mag.set _xlim(O, freq_max_GHz)
ax_spectrum_mag.grid()
ах_
чтобы показать, как работают
Часть
634
111. Python для
научных вычислений
ax_spectrum_phase = fig.add_subplot(З, 1, 3)
ax_spectrum_phase.plot(freq / le9, spectrum_phase)
ax_spectrum_phase.set_title("Фaэoвый спектр")
ax_spectrum_phase.set_xlabel("Чacтoтa,
ГГц")
ax_spectrum_phase.set_ylabel("arg(S(f) ), рад.")
ax_spectrum_phase.set_xlim(O, freq_max_GHz)
ax_spectrum_phase.set_ylim(-50, О)
ax_spectrum_phase.grid()
fig.tight_layout()
plt. show ()
Функция
rfft ()
возвращает комплексный массив со спектром в два раза короче
массива сигнала, поскольку значения для отрицательных частот мы не получаем.
При этом отпадает необходимость применения функции fftshift ().
Результат выполнения этого скрипта показан на рис.
30.9.
Сигнал
1.0
-
о. в
О.б
~
0.4
0.2
о.о
о
4
10
в
Время, нс
Амплитудный спектр
+
50
t
40
j..
_зо
с
\!!. 20
10
о
3
о
4
б
Частота . ГГц
Фа,овый спектр
о
ct
- 10
..
"' -20
":
с
i?i
е'
"'
-30
-40
-50
о
3
4
е
частота, ГГц
Рис.
30.9.
Прямоугольный импульс и его спектры при использовании функции
rfft ()
До сих пор для расчета спектра сигнала мы использовали только прямое преобра
зование Фурье. Последний пример, который мы рассмотрим в этой главе, покажет
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
635
работу обратного преобразования Фурье для перехода от спектра к сигналу. Суть
этого примера заключается в том, что мы вычислим спектр одиночного импульса,
потом обнулим значения спектра выше заданной частоты (как будто мы пропусти
ли сигнал через идеальный фильтр нижних частот), а затем преобразуем изменен
ный спектр обратно в сигнал.
Для выполнения обратного преобразования Фурье предназначены функции
и ее аналог для действительных сигналов
-
ifft ()
функция irfft (). Но сначала мы вос
пользуемся более общими функциями fft () и ifft (), которые подразумевают на
личие отрицательных частот (листинг
Листинr 30.9.
30.9).
Chapter_ЗOluatplt_OMlt.Jflt.py
import numpy as np
import numpy.typing as npt
import matplotlib.pyplot as plt
from scipy.fft import fft, ifft, fftfreq, fftshift
def rect(time: npt.NDArray, Т: float, delay: float) -> npt.NDArray:
s = np.zeros_like(time)
s[ (time >= delay) & (time < (Т + delay))] = 1
return s
if
name
#
main
"·
Исходные данные
dt = 20е-12
signal_len = 8 * 1024
time = np.arange(signal len) * dt
Т = le-9
delay = le-9
freq_filter = 4е9
# Расчет спектра исходного сигнала
signal = rect(time, Т, delay)
spectrum_src = fft(signal)
spectrum_src_mag = np.abs(spectrum_src)
freq = fftfreq(signal_len, dt)
# Фильтрация сигнала
spectrum_filter = spectrum_src.copy()
spectrum_filter[np.abs(freq) > freq_filter) = О
spectrum_filter_mag = np.abs(spectrum_filter)
signal_filter = np.real(ifft(spectrum_filter))
# Визуализация результатов
freq_max_GHz = 7
fig = plt.figure(figsize=(7, 5))
Часть
636
# Отображение исходного сигнала и сигнала
ax_signal = fig.add_subplot(2, 1, 1)
ax_signal.plot(time / le-9, signal, "-Ь",
111. Python
для научных вычислений
после фильтрации
lаЬеl="Исходный сигнал")
ax_signal.plot(time / le-9, signal filter, "-k",
lаЬеl="Сигнал после фильтрации")
ax_signal.set_title("Cигнaл")
ax_signal.set_xlabel("Bpeмя,
нс")
ax_signal.set_ylabel("s(t)")
ax_signal.set_xlim(O, 10)
ax_signal.grid()
ах_ signal. legend ()
# Отображение исходного спектра и спектра после фильтрации
ax_spectrum_mag = fig.add_subplot(2, 1, 2)
freq_shift = fftshift(freq)
spectrum_src_mag_shift = fftshift(spectrum_src_mag)
spectrum_filter_mag_shift = fftshift(spectrum_filter_mag)
ax_spectrum_mag.plot(freq_shift / le9, spectrum_src_mag_shift,
"-Ь",
lаЬеl="Исходный спектр")
ax_spectrum_mag.plot(freq_shift / le9, spectrum_filter_mag_shift,
"-k", lаЬеl="Спектр после фильтрации")
ax_ spectrum_ mag. set_ title ("Амплитудный спектр")
ax_spectrum_mag.set_xlabel("Чacтoтa,
ГГц")
ах_ spectrum_mag.
set _у label (" 1 S ( f) 1 ")
ax_spectrum_mag.set_xlim(-freq_max_GHz, freq_max_GHz)
ax_spectrum_mag.grid()
ах_ spectrum_mag. legend ()
fig.tight_layout()
plt. show ()
По части расчета спектра исходного сигнала здесь нет никаких особенностей, кото
рые бы мы не рассмотрели в предыдущих примерах. Обратите внимание на процесс
фильтрации по частоте. Сначала с помощью метода сору ()
ного
спектра
поскольку для
-
визуализации
нам
создается копия исход
понадобятся
как
исходный
спектр, так и спектр после фильтрации. Фильтрация спектра осуществляется с по
мощью команды:
spectrum_filter[np.abs(freq) > freq_filter] =
Здесь freq -
О
заранее созданный с помощью функции fftfreq () массив частот.
При фильтрации спектра мы должны не забыть про отрицательные частоты, спектр
должен остаться симметричным, поэтому в этом выражении для выделения фильт
руемых частот используется функция numpy. abs ().
Глава
30.
Библиотека
решение сложных научных и инженерных задач
SciPy:
637
Другая особенность этого примера, на которую надо обратить внимание,
-
это
расчет обратного преобразования Фурье:
signal_filter = np.real(ifft(spectrum_filter))
Функция ifft () возвращает массив комплексных чисел, но поскольку спектр после
фильтрации у нас остался симметричным (с точностью до комплексного сопряже
ния), то сигнал после обратного преобразования Фурье должен быть действитель
ным, а его мнимая часть
равна О (или близка к ней из-за погрешностей при рабо
-
те с действительными числами). Поэтому мы преобразуем массив комплексных
чисел к массиву действительных чисел, взяв от каждого элемента с помощью
функции numpy. real () только действительную его часть.
Пока в этом примере мы не использовали функцию fftshift () и работали с не пе
ремещенным массивом, содержащим спектр, поскольку функция ifft ()
мевает именно такое расположение частот. Функцию fftshift ()
подразу
мы применим
только на этапе визуализации спектров.
В результате выполнения скрипта из листинга
показанные на рис.
30.9
30.10.
Сигнал
,,
1.0
I
, r
, .,
i
'
-
-----~
.
..
Исходный сигнал
I ___
~ 0.5
о.о
мы должны увидеть графики,
Сигнал после фильтрации
··- -----·
---
·-··
А
"
о
б
4
2
10
в
Время.нс
Амплитудный спектр
-6
Исходный спектр
---
40 +-- + - - -
-4
-2
о
Спектр после фильтрации
2
4
б
Частота.ГГц
Рис.
30.10.
Сигналы и спектры до и после фильтрации идеальным
низкочастотным фильтром
Поскольку за резкие переходы в сигнале «отвечают» высокие частоты, которые мы
обнулили, вместо резкого скачка в отфильтрованном сигнале появился характер
ный «звон».
В этом примере мы снова работаем с действительными сигналами, поэтому можем
упростить код, применив вместо функций
fft ()
и
ifft ()
функции rfft ()
и
638
Часть
111. Python
для научных вычислений
irfft (). Тогда отпадет необходимость в использовании функции fftshift (), и к
irfft () будет сразу возвращать массив действительных чисел, и
тому же функция
нам не придется выполнять преобразование из комплексных чисел.
Для экономии места в листинге
30.10
показаны только измененные строки по срав
нению с предыдущим примером.
Листинг 30.1 О.
Chapter_30/example_10/rfft_lrfft.py
from scipy.fft import rfft, irfft, rfftfreq
if
name
#
main
"·
Расчет спектра исходного сигнала
signal = rect(time, Т, delay)
spectrum_src = rfft(signal)
spectrum_src_mag = np.abs(spectrum_src)
freq = rfftfreq(siqnal_len, dt)
#
Фильтрация сигнала
spectrum_filter = spectrum_src.copy()
spectrum_filter[freq > freч._filter] = О
spectrum_filter_mag = np.abs(spectrum_filter)
signal_filter = irfft(spectrum_filter)
#
Визуализация результатов
# Отображение исходного спектра и спектра после фильтрации
ax_spectrum_mag = fig.add_subplot(2, 1, 2)
ax_spectrum_mag.plot(freq / le9, spectrum_src_mag, "-Ь",
lаЬеl="Исходный спектр")
ax_spectrum_mag.plot(freq / le9, spectrum_filter_mag, "-k",
lаЬеl="Спектр после фильтрации")
ax_ spectrum_mag. set _ ti tle ( "Амплитудный спектр")
ах_ spectrum_mag. set _ xlabel ( "Частота, ГГц")
ax_spectrum_mag.set_ylabel (" IS (f) 1 ")
ax_spectrum_mag.set_xlim(O, freч._max_GИz)
ax_spectrum_mag.grid()
ax_spectrum_mag.legend()
fig.tight_layout()
plt. show ()
Приведенный здесь код стал менее загроможден преобразованиями и немного бо
лее наглядным, поскольку нам не нужно задумываться об отрицательных частотах.
Глава
30.
Библиотека
решение сложных научных и инженерных задач
SciPy:
Результат его выполнения показан на рис.
30.11
639
и по сути не отличается от резуль
тата из предыдущего примера, только спектр теперь отображается, начиная с нуле
вой частоты.
Сигнал
,1 ,. ,. ,,
J , ., 1
1.0
Исходный сигнал
---
сигнал после фильтрации
~ 0.5 ·----
.
..
о.о
А
2
о
"
б
4
10
8
Время, нс
Амплитудный спектр
---
40
Исходный спектр
Спектр после фильтрации
s
!!l. 20
о
о
1
2
з
4
5
б
7
Частота, ГГц
Рис.
30.11.
Сигналы и его спектры до и после фильтрации идеальным низкочастотным
фильтром с использованием функций rfft () и i r f f t ()
В последних примерах мы применили к спектру сигнала прямоугольное окно. Это
самое простое, но не самое лучшее окно с точки зрения теории обработки сигналов.
Библиотека
SciPy
включает в себя модуль scipy. signal. windows, который содер
жит множество других окон, в том числе и прямоугольное в виде функции
ьохсаr (). В этой книге мы ничего говорить про окна не будем, но полезно иметь
представление о том, что такие возможности библиотека
SciPy предоставляет.
Результаты работы функций fft () и i f f t () по умолчанию соответствуют расче
там, выполняемым по формулам
(30.12)
и
(30.13)
соответственно. Различия между
этими формулами заключаются только в знаке степени экспоненты под суммой, а
также в дополнительном коэффициенте
1/N
в обратном преобразовании Фурье. Та
кая формулировка соответствует физическому смыслу закона сохранения энергии и
теоремы Парсеваля, которая описывается следующим равенством:
(30.15)
Однако для некоторых математических задач прямое и обратное преобразования
Фурье иногда удобнее записывать таким образом, чтобы коэффициент
1/N
стоял в
прямом, а не в обратном преобразовании Фурье, а иногда для симметрии выраже-
Часть
640
111. Python
для научных вычислений
ний и в прямом, и в обратном преобразовании пишут коэффициент 1/ Гн, и тогда
оба преобразования будут отличаться только знаком в степени экспоненты под
суммой. Такие вариации не удовлетворяют теореме Парсеваля. Однако, если нас не
интересуют конкретные значения в спектре
(с
точностью до постоянного коэффи
циента), или задача состоит в том, чтобы после манипуляций со спектром обратно
перейти к временному сигналу с помощью обратного преобразования Фурье, такие
записи в некоторых случаях могут оказаться более удобными.
Функции fft(), ifft(), rfft() и irfft() могут работать и с такими модифика
циями преобразований Фурье. Для этого в них предусмотрен дополнительный па
раметр norm, который может принимать строковые значения:
♦
это значение по умолчанию, преобразования Фурье соответству
"backward" ют формулам
(30.12)
♦ "forward" -
и
(30.13);
коэффициент 1/N из обратного преобразования Фурье переносится
в прямое преобразование;
♦
"ortho" -
и в прямом, и в обратном преобразовании Фурье присутствует коэф-
фициент 1/ Гн.
Мы не будем подробно рассматривать эти модификации, но если для ваших задач
это актуально, полезно иметь в виду, что библиотеки
SciPy
и
NumPy
предоставля
ют и такие возможности.
Заключение
Возможно, это была самая тяжелая для чтения глава. Чтобы показать лишь некото
рые из возможностей, которые открываются с использованием библиотеки
SciPy,
пришлось написать в ней достаточно большие разделы с примерами задач, взятыми
из области радиотехники.
Сначала мы рассмотрели модуль scipy. constants, который содержит большой на
бор физических и математических констант (для некоторых из них в этом модуле
предусмотрены отдельные переменные), но большую часть узкоспециализирован
ных
констант
можно
получать
по
строковым
ключам
с
помощью
словаря
physical constants ИЛИ функции value ().
Мы использовали модуль scipy. special, который содержит множество специали
зированных функций, и в качестве задачи, где бы эти функции понадобились (в ча
стности, сферические функции Бесселя), построили графики, показывающие зави
симость эффективной площади рассеяния идеально проводящего шара от частоты.
Следующий большой раздел был посвящен преобразованию Фурье. Мы коротко
познакомились
с
особенностями
дискретного
преобразования
Фурье.
Модули
scipy. fft и numpy. f ft содержат функции для прямого и обратного преобразова
ний Фурье: fft () и ifft () соответственно. Мы изучили особенности расположе
ния гармоник в результирующем массиве, заключающиеся в том, что отрицатель-
Глава
30.
Библиотека
SciPy:
решение сложных научных и инженерных задач
641
ные частоты следуют после положительных. Для более наглядной визуализации
спектра мы применили функцию fftshift (), которая меняет половинки массива со
спектром местами, чтобы отрицательные частоты располагались до положительных.
Для создания массива частот на основе длины массива и шага дискретизации сиг
нала по времени предназначена функция fftfreq ().
Для устранения перескока фаз на 2тс на графиках фазового спектра мы использова
ли функцию numpy. unwrap ().
Если мы работаем только с действительными сигналами, то часто проще применять
функции r f f t () и i r f f t () для прямого и обратного преобразований Фурье. В этом
случае мы получим только положительные частоты, а функция i r f f t () возвращает
массив действительных чисел, а не массив комплексных чисел, как это делает
функция i f f t ( ) . При использовании функций r f f t ( ) и i r ff t ( ) также отпадает
необходимость в применении функции fftshift (), а для создания массива частот
нужно задействовать функцию rfftfreq ().
И, наконец, мы с вами переходим к последней главе, которая будет посвящена не
скольким инструментам, также получившим широкое распространение в научной
сфере,
-
это среды разработки
Jupyter и JupyterLab.
- ГЛАВА 31 -
Интерактивные среды
и
IPython
JupyterLab
На протяжении всех предыдущих глав для выполнения команд
Python
мы исполь
зовали два стандартных способа: вводили по одной команде в интерактивной кон
соли
или писали скрипты и запускали их, передавая имя скрипта в
Python (REPL)
качестве параметра командной строки интерпретатору
Python.
Однако в научной
среде в последние годы получил популярность еще один способ оформления и вы
полнения скриптов
-
в виде так называемых блокнотов, когда текст программы
перемежается отображением рассчитанных результатов, и при этом можно переза
пустить любой промежуточный блок кода без перезапуска всего скрипта. При от
крытии такого блокнота он уже содержит рассчитанные ранее результаты. Этот
способ написания скриптов для языка
JupyterLab,
Python
предоставляет среда разработки
и до нее мы постепенно дойдем в этой главе.
IPython -
более удобный
Среда разработки
REPL
объединяет в себе несколько относительно независи
JupyterLab
мых инструментов. Чтобы стало понятно, как они между собой взаимосвязаны,
кратко разберем историю их создания.
Всё началось с проекта
IPython, в рамках которого была создана альтернативная
REPL. Этот проект продолжает развиваться, и, по сравнению со
REPL, можно отметить следующие (далеко не все) удобства, которые
версия консольной
стандартным
он предоставляет: автодополнение кода, более удобные возможности работы с ис
торией предыдущих команд
(история
сохраняется между перезапусками
IPython,
а
с помощью отдельной команды можно повторно запускать сразу несколько команд
из истории), возможность загружать последовательности команд из внешних фай
лов, а также выполнять скрипты на
Python
непосредственно из интерактивного ре
жима, подсветка синтаксиса во вводимых командах (появится также в
выхода
Python 3.14),
REPL
после
подсветка парных скобок.
Еще более важной особенностью
ваемых «магических» команд
-
не являются элементами языка
IPython
является огромное количество так назы
это команды, начинающиеся со знака«%», которые
Python,
но предоставляют дополнительные возмож-
Глава
31.
Интерактивные среды
IPython
и
Jupyterlab
643
ности. Такими командами являются, например, изменение текущего рабочего ката
лога, добавление закладок на выбранные каталоги, чтобы к ним можно было быст
рее переходить, возможность создания макросов из нескольких команд
Python,
возможность запуска профайлера для измерения скорости выполнения кода и мно
гое другое.
IPython устанавливается как обычный Руthоn-пакет
> python -m pip install --user ipython
с помощью команды:
После этого в командной строке операционной системы выполним команду:
> ipython
в результате чего запустится оболочка, показанная на рис.
Рис.
Выполним несколько команд
31 .1. IPython
Python
31.1.
после запуска
в этой консоли:
In [1]: import numpy as np
In [2]: import pandas as pd
In [3]: import matplotlib.pyplot as plt
In [4]: х = np.arange(0, np.pi * 3, 0.01)
In [5]: у= np.sin(x) * np.cos(2 * х)
In [6]: %matplotli.Ь
Using matplotlib backend: qtagg
Если первые пять команд не должны вызывать вопросов, то последняя команда
%matlotlib - может показаться необычной, поскольку не является выражением на
языке Python. Это и есть одна из тех самых «магических» команд. Строго говоря,
IPython распознает «магические» команды и без знака «%», если не возникает кон
фликтов с именами объектов (переменных, функций и т. д.), но для аккуратности
мы всегда будем использовать такие команды вместе со знаком«%».
Примененная здесь «магическая команда»
режим для библиотеки
Matplotlib.
стой график:
tn [7]: plt.plot(x, у)
Mesa: SVGA3D; build: RELEASE;
%matplotlib
включает интерактивный
Чтобы понять, что это нам дает, нарисуем про
Часть
644
111. Python
для научных вычислений
Mesa: 24.0.2
Out [7]: [ <matplotlib. lines. Line2D at
Ох18е518Ь4е10>]
Если бы мы выполнили эту команду в обычном командном режиме
Python,
то, по
мимо возврата объекта Line2D из функции, ничего бы не произошло, поскольку
график появляется после выполнения функции pl t. show (), и продолжить работу в
IPython
мы могли бы только после закрытия окна с графиком. Но поскольку мы до
этого выполнили команду %matplotlib, график откроется сразу, и его не обяза
тельно закрывать для продолжения работы. Это удобно, поскольку далее мы можем
выполнять команды, влияющие на внешний вид графика, и сразу видеть результат
(рис.
31.2):
In [8]: plt.grid()
In [9]: plt.xlim(0, 8)
Out[9]: (О.О, 8.0)
Рис.
«Магические» команды
-
31.2.
Окна с
IPython
и
Matplotlib
это одно из важных преимуществ
со стандартным командным режимом
Python.
IPython
по сравнению
Таких команд много, и все они опи
саны на странице документации 1 . Вот лишь некоторые из них:
♦
%matplotlib -
♦
%hist -
показать историю выполненных команд;
♦
%save -
сохранить указанные строки истории в файл *.ру;
1
включить интерактивный режим библиотеки
См. https://ipython.readthedocs.io/en/stable/interactive/magics.html.
Matplotlib;
Глава
31.
Интерактивные среды
IPython
и
Jupyterlab
645
♦
%macro -
создать макрос на основе выбранных команд из истории;
♦
%rerun -
выполнить последнюю или выбранные команды из истории;
♦
%load -
загрузить команды
Python
из файла, из истории или даже из Интернета.
После выполнения %load загруженные команды можно будет перед запуском
отредактировать;
♦
%run
♦
%pwd - получить строку с текущим рабочим каталогом;
♦
загрузить команды из файла и сразу их выполнить;
-
добавить, удалить или показать список каталогов, добавленных в
%bookmark закладки;
♦
изменить текущий рабочий каталог (перейти в другой каталог). При этом
%cd -
можно использовать закладки, созданные с помощью команды
♦
выполнить команду
%pip -
pip
(про
pip
мы говорили в главе
%bookmark;
16),
не выходя из
REPL;
♦
♦
выполнить команду
%uv главе
17),
%timeit -
не выходя из
uv
REPL;
(про виртуальные окружения и
измерить время выполнения указанной команды
uv
мы говорили в
Python.
Заданная
команда выполняется многократно, чтобы уменьшить погрешность измерения
среднего времени ее выполнения;
♦
%t ime -
измерить время выполнения указанной команды
Python.
Заданная ко-
манда выполняется однократно.
Рассмотрим работу некоторых из этих «магических» команд, а начнем с команды
%hist, предназначенной для отображения истории выполненных команд:
In [10]: %hist
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
х = np.arange(O, np.pi * 3, 0.01)
у= np.sin(x) * np.cos(2 * х)
'tmatplotlib
plt. plot (х, у)
plt.grid()
plt.xlim(O, 8)
1,hist
В таком виде команда
%hist
тоже полезна, но главные ее преимущества в том, что
мы можем получить пронумерованную историю, если добавим параметр
In [11]: %hist -n
1: import numpy as np
2: import pandas as pd
3: import matplotlib.pyplot as plt
4: х = np.arange(O, np.pi * 3, 0.01)
5: у= np.sin(x) * np.cos(2 * х)
«-n»:
Часть
646
111. Python для
научных вычислений
6: %matplotlib
7: plt.plot(x, у)
8: plt.grid()
9: plt.xlim(0, 8)
10: %hist
11: %hist -n
Дело в том, что многие «магические» команды в качестве параметров могут при
нимать диапазоны номеров строк истории. Например, если вы уже закрыли окно с
графиком, то для того, чтобы повторить команды, необходимые для его создания,
можно выполнить команду:
In [12]: %load 7-9
После этого вам будут показаны выбранные команды, их можно отредактировать, а
затем нажать клавишу
<Enter> для
выполнения:
In [13]: # %load 7-9
.... plt.plot(x, у)
.... plt.grid()
.... plt .xlim(0, 8)
Out[l3]: (О. О, 8.0)
Теперь график вновь будет создан.
Команда
%hist в том виде, как мы ее использовали, показывает только команды,
введенные в текущем сеансе. Если нам понадобится посмотреть историю команд до
текущего запуска
из
IPython
IPython,
к команде %hist нужно добавить параметр
«-g». Выйдем
<Ctrl>+<D>, а затем
IPython), а затем запустим
(для этого можно применить комбинацию клавиш
подтвердить выход, или просто закрыть консоль с
IPython заново:
In [1]: %hist -q
1/1: import numpy as np
1/2: import pandas as pd
1/3: import matplotlib.pyplot as plt
1/4: х = np.arange(0, np.pi * 3, 0.01)
1/5: у= np.sin(x) * np.cos(2 * х)
1/6: %matplotlib
1/7: plt.plot(x, у)
1/8: plt.grid()
1/9: plt.xlim(0, 8)
1/10: %hist
1/11: %hist -n
1/12: %load 7-9
1/13:
# %load 7-9
plt.plot(x, у)
plt.grid()
plt. xlim (0, 8)
1: %hist -g
Глава
31.
Интерактивные среды
IPython
и
647
Jupyterlab
Здесь видно, что команды из предыдущей сессии работы с
префиксом
IPython
имеют номера с
1/ ...
Сохраним команды
1/1-1/9
в файл
plot.py
с помощью команды
мы это сделаем, посмотрим, в каком каталоге мы находимся,
%save, но прежде чем
- именно туда будет
сохранен файл:
In [2]: %pwd
Out[2]: 'C:\\projects\\python-book'
Разумеется, у вас будет выведена другой каталог. Изменить текущий каталог мож
но с помощью команды
%cd.
Теперь сохраним часть истории в файл с помощью команды
%save:
In [3]: %save plot.py 1/1-1/9
The following commands were written to file 'plot.py·:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
х = np.arange(0, np.pi * 3, 0.01)
у= np.sin(x) * np.cos(2 * х)
get_ipython() .run_line_magic('matplotlib', '')
plt.plot(x, у)
pl t. grid ()
plt.xlim(0, 8)
В созданный файл
%save.
plot.py попали строки, указанные после выполнения команды
Обратите внимание, что «магическая» команда
на вызов функции
run_line_magic ()
из
была заменена
%matplotlib
IPython.
Теперь можно выполнить созданный файл с помощью команды
%run:
In [4]: %run plot.py
И в завершение этого раздела посмотрим, как с помощью команды
жем измерять время выполнения кода на
In
In
In
In
[5):
[6):
[7]:
%timeit
мы мо
Python:
from array import array
foo = [)
bar = array("i")
[8): %timeit foo.append.(100)
57.1 ns ± 6.98 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
In [9): %ti.meit Ьar.append(lOO)
123 ns ± 6.74 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
В этом примере мы создаем пустые список и массив, а с помощью команды
%timeit
измеряем скорость добавления элемента в конец того и другого контейне
ра. Команда
Python, указанная после магической команды %t ime i t многократно
(7 раз по 1О млн раз, эти параметры можно настраивать с помощью
«-r» и «-n» соответственно), и получилось, что добавление элементов в
выполняется
параметров
список в среднем выполняется более чем в два раза быстрее по сравнению с добав
лением элементов в массив (почему это так, мы разбирались в главе
4).
Часть
648
Мы рассмотрели здесь далеко не все возможности
сказали про настройки
IPython,
111. Python
IPython.
для научных вычислений
Например, мы ничего не
про профили, благодаря которым под разные про
екты можно хранить разные истории команд и разные файлы с параметрами. И во
обще, «магических» команд в
IPython
столько, что только о них можно написать
небольшую книгу. Однако нам надо двигаться дальше, в сторону
От
JupyterLab.
IPython к Jupyterlab
Мы не зря задержались на рассмотрении возможностей
IPython. Дело в том, что в
IPython появился новый проект, основанный на IPython. Изна
чально он назывался IPython Notebook, но через некоторое время был переименован
в Jupyter. С помощью Jupyter можно создавать так называемые блоююты
(notebooks), представляющие собой перемежающиеся блоки кода и результаты их
процессе развития
выполнения, в том числе графики, таблицы, блоки документации, в них можно да
же отображать формулы в формате
LaTeX.
Формат блокнотов разработан таким
образом, что с их помощью можно делиться результатами вычислений с коллегами,
а также выкладывать их на веб-сайт с заранее рассчитанными результатами. При
мер такого блокнота показан на рис.
31.3.
,.
Fill!
B
Ed<t
VU/Nf
М
+Xf!t:i
1 "'
~
►
•
~t~
~Р
C"Ccxtr,
t,
iap(lrt "U80)' •~ np
:i--tl№ldo•~pd
ц,p,,,rt.,.tplotllli,P'IJl1.ot•~Plt
)[ ,. np.&~(t, вp.fli • J , fHH)
у"
np.~ill()[) •
plt.p}ot(x,
щ,.,w( 1
• 11;)
у)
d1t•,. pd,reild_<t~(•s~,e..,rissl.o,,~.cs~·-, 11secols,..[8, 7, 4, •;, ·1, 8))
d1t•
Company
О
ф
ШJ
D.ote
R\l";N\,~SR. 1951-10.(14
Spul.r.li.8I0"1PS
!,р...111•-1
NaN
Succ,w
1957-11-<О
Spvtм:I01PS
Sp.,1n;io:.2
№N
SuФ~s,
1 RVS.NI.ISSR
с
G)
Рис.
31.3.
0
<>
Пример блокнота с графиком и таблицей,
загруженной с помощью
Pandas
~ -()---901'
S-39РМ
Глава
31.
Интерактивные среды
Для работы с блокнотами
IPython
и
649
Jupyterlab
Jupyter по умолчанию
Visual Studio Code
пример, для среды разработки
используется браузер, хотя, на
существует расширение, позво
ляющее работать с блокнотами непосредственно из нее.
В рамках проекта
Jupyter
также развивается среда разработки, предназначенная не
посредственно для работы с блокнотами, она называется
JupyterLab,
и именно ей
мы будем пользоваться на протяжении этой главы.
Для установки
JupyterLab
и всех требуемых зависимостей (а их немало) достаточно
выполнить в консоли команду:
> python -m pip install --user jupyterlab
Но прежде чем мы перейдем к использованию
JupyterLab и созданию блокнотов,
надо рассказать про архитектурные особенности проекта Jupyter. Когда мы запус
тим JupyterLab, будет запущен локальный веб-сервер Jupyter, через веб-интерфейс к
которому
и станет осуществляться
всё взаимодействие с блокнотами.
Сервер
Jupyter реализован таким образом, что для создания блокнотов можно использовать
не только язык Python, - для любого языка программирования вы можете написать
свое ядро (kemel), которое будет взаимодействовать с сервером, и расчеты станут
вестись с помощью того языка, для которого написано ядро. Есть неофициальное
мнение, что название проекта
Jupyter -
это сокращение от трех языков програм
мирования, на которые изначально ориентировались разработчики:
В документации к
Jupyter
Julia, Python
и
R.
можно найти информацию, что это не совсем так, но и
недалеко от истины. По крайней мере, по умолчанию устанавливается ядро для
Python (оно
называется
а для языков
блокнотах
Julia
Jupyter.
Непосредственно
и
ipykernel
R
с отсылкой к
IPython,
на основе которого создано),
также есть свои ядра, и эти языки можно использовать в
JupyterLab -
это оболочка, позволяющая запускать и останавли
вать ядра для нужного языка программирования, а также предоставляющая интер
фейс для работы с блокнотами и взаимодействия с файловой системой. Помимо
этого, возможности
JupyterLab
можно наращивать с помощью дополнительных
расширений. Все эти особенности сделали
JupyterLab
популярным инструментом в
научном сообществе.
У становив
JupyterLab,
для его запуска нужно выполнить в консоли операционной
системы одну из двух команд:
> jupyter lab
или
> jupyter-lab
В результате будет запущен сервер
Jupyter
и открыт браузер со страницей по адре
су http:/Лocalhost:8888Лab 2 .
2
На самом деле полный адрес для подключения к серверу более сложный и выглядит так:
http://localhost:8888Лab ?token=fedde9cf671 dcb95c 18Ь 1e2982e40256575fd7306e7 d6076.
Часть
650
111. Python
для научных вычислений
Значение параметра token (см. длинный адрес в сноске) у вас будет свое. Такой
длинный адрес можно увидеть в консоли, где происходит запуск сервера
Если мы захотим открыть еще один
JupyterLab
Jupyter.
в другом браузере, то нужно будет
открывать именно этот длинный адрес с токеном.
Внешний вид веб-интерфейса
JupyterLab
показан на рис.
31.4.
В нашем случае в
левой части окна открыт браузер файлов, но поскольку мы еще не создали ни одно
го блокнота, там сейчас пусто. В правой части окна расположена панель запуска
на ней можно увидеть несколько кнопок, с помощью которых можно
(Launcher),
создать новый блокнот на языке
Python,
запустить консоль
IPython,
запустить тер
минал операционной системы, создать новый текстовый файл с расширением txt,
создать файл с разметкой в формате
md), создать скрипт на
Python
(будет создан файл с расширением
Markdown
или открыть дополнительную панель с контекстными
подсказками. Если установить ядра для других языков программирования, то кноп
ки для работы с ними также будут добавлены на панель запуска. В качестве приме
ра на рис.
•
-а.
t:
о
31.5
т
показана панель запуска после установки ядра для языка
Julia .
Q~
•
,.
"
,.
Jl c.,,юo
■ ""'"
...
м
11
.....,.... си. ·
,.
а:!
о . , .
Рис.
31.4.
Веб-интерфейс
Jupyterlab
Создадим новый блокнот. Для этого на панели запуска в разделе
Notebook
нажмем
кнопку
на
Python 3 (ipykernel). После этого в текущем рабочем каталоге, показанном
левой панели окна JupyterLab, будет создан новый файл блокнота с именем по
умолчанию Untitled.ipynb, кроме того, новый пустой блокнот будет открыт на основ
ной панели
JupyterLab.
Теперь открытая веб-страница станет выглядеть примерно
так, как показано на рис.
31.6.
Если нажать на кнопку сохранения В1 (или комбинацию клавиш <Ctrl>+<S>), по
ступит предложение переименовать этот файл
В созданном блокноте мы видим ячейку
-
(cell)
назовем его example_31.ipynb.
для ввода команд
-
она будет рас
ширяться по вертикали по мере добавления новых строк с командами. Теперь мы
можем группировать строки кода, используя такие ячейки. Чтобы выполнить код
Глава
31.
Интерактивные среды
IPython
и
651
Jupyterlab
ячейки и создать следующую, можно применить комбинацию клавиш
<Shift>+<Enter>
или нажать кнопку ► (Run this cell and advance). В процессе написания и отладки
кода это действие можно производить не только с новыми ячейками, но и с ячейка
ми, созданными ранее. Такой подход дает возможность выборочного запуска бло
ков кода. Но с этой возможностью надо обращаться аккуратно
-
если вы переза
пустили расчет в какой-то ячейке в середине кода, то, скорее всего, вам понадобит
ся перезапустить расчет и в последующих ячейках. С помощью пункта меню
Run
АН
Cell можно
,.
,,
~
Notebook
•••
Python3
(lpykornel}
11
Julla 1.11.3
Console
•••
Jullo 1.11.3
Python 3
(lpykornel}
Е1
1
Run
запустить на выполнение все ячейки в блокноте.
Other
11
--
1
Text Ftte
Termlnlll
м
....
Mort«iown flle
•••
Julill Flle
,.
Python Ale
~
Show
Conteкtual
Help
Рис.
..
о
,.
-
.,
_
31 .5.
•
t
•
о
Панель запуска
т
, . . ~*
........
а
1
• )(
11:1
с:,
• •
с
..
Jupyterlab
с установленными ядрами для языков
Python
и
Julia
.
~С • "'"°'~О • .1 .
~
11,1.___ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _..:.•_
• ....с·-=·'--'.._.с...
'
"
-
Рис.
31 .6.
Внешний вид окна
Jupyterlab
t,11
0
~ ltdl
после создания нового блокнота
.....,.._.,.. 0
1
Часть
652
111. Python
для научных вычислений
Оформим в виде блокнота пример из предыдущей главы про расчет спектра после
довательности прямоугольных видеоимпульсов. В первую ячейку мы поместим все
импорты из библиотек, а в следующую
-
функцию meander () для создания после
довательности видеоимпульсов. После заполнения и запуска этих двух ячеек блок
нот примет вид, показанный на рис.
[!!]
B
+~l[]L)
'--~--------------------------.
1+
х
example_З1.ipynb
►
31. 7.
■
C»Code
NoteЬook ~
Python з (ipykernel) О
[1 ]
import numpy as np
import numpy.typing as npt
import matplotlib.pyplot as plt
from scipy.fft impor1: fft, fftshift, fftfreq
[2].
def meander(time: npt.NDArray, duty: float, period: float) -> npt.NDArray:
""''"Функция
генерации поспедоватеnьностм вид~ммлульсов.
s = np.zeros_like(time)
newtime = (
np.arcsin(np.sin(2 .0 • np.pi / period • time
np.pi + 0.5)
s [ newtime < duty ] = 1
ret:urn s
11
4
+
_
-.
•·•• ··
np.pi / 2)) /
[ ]:
Рис.
31.7.
Блокнот
Jupyter с двумя
ячейками кода
Ячейки пронумерованы в том порядке, в каком они исполнялись. Как мы уже гово
рили, отдельные ячейки можно запускать независимо от остальных, поэтому эта
нумерация может и не быть последовательной. При повторном запуске одной и той
же ячейки ее номер увеличивается. Кроме того, в процессе выполнения или ожида
ния выполнения вместо номера у ячеек отображается звездочка:
[* J.
На это полез
но обращать внимание при выполнении длительных расчетов, чтобы видеть, обно
вилась ячейка после расчета или еще нет.
Обратите внимание на выпадающий список
Code
на панели инструментов блокно
та. С помощью этого списка мы можем менять формат ячеек. Вместо кода на
Python, указать, например, что ячейка содержит текст, оформленный в нотации
Markdown, и тогда после выполнения такой ячейки разметка Markdown преобразу
ется в код HTML, который будет показан в этой ячейке. Такую возможность удоб
но использовать, чтобы добавлять в блокнот выделяющиеся заголовки и поясняю
щие надписи к идущим далее блокам кода. Помимо нотации
Markdown,
ячейка мо
жет содержать обычный текст, который не будет никак интерпретироваться или
преобразовываться.
Переключим следующую ячейку в режим
Code
##
выберем пункт
Исходные данные
Markdown.
Markdown,
для чего в выпадающем списке
В качестве содержимого этой ячейки напишем:
Глава
31.
Интерактивные среды
IPython
и
Jupyterlab
653
Такая нотация будет преобразована в заголовок второго уровня. После этой ячейки
добавим переменные с исходными данными для расчета. После запуска этих ячеек
окно блокнота станет выглядеть, как показано на рис.
~
х
example_31.ipynb
B+X'El
-----------
1+
□►■
C"Code
31.8.
NoteЬook С:
О:
Python З (ipykerneQ О
=
import numpy as np
import numpy . typing as npt
in,port matplotlib.pyplot as plt
fr·om scipy. fft import fft, fftshift, fftfreq
[1
2]:
def meander(time: npt.tIOArray, duty: float, period: float) -> npt.NDArray:
.. ''"Функция
генерацми поспедовательности видеоимлульсов .
s = np.zeros_like(time)
newtime = (
np.arcsin(np.sin(2 . 0 * np.pi / period • time
np.pi + 0.5)
s [ newtime < duty ] = 1
return s
+
......
np.pi / 2)) /
Исходные данные
[3 ] :
1
dt = 20е-12
signal_len = 8 • 1024
duty = 0.25
period = 4е-9
Рис.
31.8.
Блокнот
Jupyter
с ячейками кода на
Python
и Магkdоwп-ячейкой
Если выполнить двойной щелчок мышью на заголовке Исходные данные, ячейка
переключится
в режим
жимое в формате
редактирования,
и
вы
сможете скорректировать
ее содер
Markdown.
Добавим в блокнот еще один раздел, который будет создавать массив с последова
тельностью видеоимпульсов и отображать их в виде графика (рис.
Когда мы работаем в блокноте
31.9).
Matplotlib будут встраиваться непо
средственно в блокнот. Помимо библиотеки Matplotlib, существуют и другие биб
лиотеки для построения графиков, например, Plotly1, которая создана с расчетом на
интеграцию графиков в веб-страницы (а блокнот Jupyter является веб-страницей) и
Jupyter,
графики
добавляет больше возможностей взаимодействия пользователей с графиком. Одна
ко рассмотрение этой и других подобных библиотек выходит за рамки этой книги.
Ячейки в формате
Markdown
могут содержать нотацию в формате
бражения формул. Для этого до и после кода
«$$» -
для ото
нужно добавить символы
если формула должна располагаться на новой отдельной строке, или сим
волы «$» -
3
LaTeX
LaTeX
если формула должна быть встроена в строку. Добавим еще одну
См. https://plotly.com/python/.
Часть
654
111. Python
для научных вычислений
Markdown-ячeйкy с заголовком и формулой, описывающей дискретное преобразо
вание Фурье. Для этого в новую ячейку нужно вставить код:
##
Спектр последовательности видеоимпульсов
$$
\Large \dot {S) \left (k \right) = \sum_{n
1) {\dot {s)) \left (n \right)
D)л{N
{е) л {- i {{2 \pi) \over {N)) n k) , k = О , 1 , ... , N - 1
$$
1iii1
+1
х
example_З 1.ipynb
------------------------"""'
NoteЬook ~
Python З (ipykernelJ О $
4
Последовательность видеоимпульсов
f4]:
time = np.aгange(signal_len) * dt
signal = meandeг(time, duty, period)
[,]:
fig = plt.figure(figsize=( б , 2) )
ax_signal = fig.add_subplot()
ax_signal.plot(time / le-9, signal)
ax_signal, set_title ("Сигнал")
ax_signal. set_xlabel ("Время, нс")
ax_signal. set_ylabel( "s(t)")
ax_signal. set_xlim(0, 20)
ax_signal. grid()
Сигнал
--
1.00
0.75 - -
*
---
~--
0.50
0.25
1- - · ·
-- ---
0.00
о. о
2.5
5.0
7.5
10 .0
12 .5
15.0
17 .5
20.0
Время. нс
r
[1
1Е]
[ ]:
31 .9.
сЬ
с:,
+
j
1
Блокнот с добавленным графиком
После выполнения эта ячейка примет вид, показанный на рис.
Мы можем отображать формулы не только в ячейках
Python.
1,-
1
Рис.
на
1'
31.1 О.
Markdown,
но также и из кода
Для этого можно воспользоваться модулем IPython. display -
он со
держит функцию display (), реализующую отображение специальных объектов,
способных создавать представление, встраиваемое в веб-страницу. Эту же функ
цию можно использовать и как замену стандартной функции print ().
Для отображения формулы нам также понадобится класс Math из того же модуля
IPython. display. Так, чтобы отобразить формулу для обратного дискретного пре
образования Фурье, мы должны создать экземпляр класса ма th, передав ему в каче-
Глава
31.
Интерактивные среды
IPython
и
655
Jupyterlab
стве конструктора строку с текстом в формате
LaTeX,
а затем отобразить этот объ
ект с помощью функции display ():
from IPython.display import display, Math
' \ Large \ dot {s} \ left (n \ right ) =
{1 \over N} \s um_{k = О}л{N - 1} {\dot {S}} \ left (k \ right)
{е} л {i { (2 \pi} \ over {N}} n k},
n=O,l, ... ,N-1'''))
display(Мath(r''
х l+~ • - - - - - - - - - - - - - - - - - - ,.1
~J @xampl@_31 .1pynb
В+:К
'О
С
■
•
□
"
Notюoolc С
Codo
О
Pythoo 3 (ipykem,f)
0
i:
Спектр последовательности видеоимпульсов
N- 1
L s(n)e- iink, k
S (k) =
=
О, 1, ... ,N - 1
n=O
/1 [ ]:
Рис.
31.10.
Отображение формулы в ячейке
Markdown
Результат выполнения ячейки с таким кодом показан на рис.
х
f!!I example_31 .ipynb
-
+)<l[:J~
B
(6) :
►
■
31.11.
1+
C>►
COde
NoteЬook ~
О
1ё
f r - IPython.disploy inopor-t disploy, Moth
Python З {1pykernel) О
1'
~
~
=
';" 1
• '\torge \dot {s} \left ( n \ri ght ) {1 \over N} \su•_{k • 0}'{N - 1} {\dot {S}} \l•f~ (k \right )
{•} ' {i {{2 \pi} \over {N}} n k},
displ ay(Мoth( r'
1
n • 0, 1, - , N - 1'"))
1
s• (n ) -_ N
1
N- 1
~ •
~
S (k) e•lnk , n -_
О , 1, ... , N
- 1
k=O
[ ]:
Рис.
31.11.
Отображение формул с помощью класса
ИЗ модуля
Math
IPython.display
и функции
display ()
Вот далеко не полный список классов из модуля IPython. display, которые можно
встраивать в блокнот с помощью функции display ():
♦
Image
♦
SVG
♦ Audio
♦
Video
♦
YouTubeVideo
♦
Code -
предназначен для раскраски синтаксиса для различных языков програм
мирования. Работает на основе библиотеки
Pygments.
656
Часть
♦
GeoJSON
♦
Math
♦
JavaScript
♦
HTML
♦
JSON
111. Python
для научных вычислений
Вернемся снова к нашему примеру с сигналом и с его спектром. Работа с ячейка
ми блокнота чем-то напоминает работу в
REPL -
если нужно отобразить объект,
не обязательно явно вызывать функцию print () или display (). Если ячейка за
канчивается именем объекта или на последней строке ячейки вызываемая функ
ция возвращает значение, которое ничему не присвоено, то это значение будет
выведено после ячейки в качестве ее результата работы. Чтобы это показать, перене
сем из примера предыдущей главы в блокнот еще одну часть кода (рис.
[7]:
freq_min_GHz = -7
freq_max_GHz = 7
spectrum = fftshift(fft(signal))
spectrum_mag = np.abs(spectrum)
spectrum_phase = np.unwrap(np.angle(spectrum))
freq = fftshift(fftfreq(signal_len, dt))
freq
[7]
array([-2.50000000e+10,
-2.49938965е+10,
2.49816895е+10,
2.49877930е+10,
Рис.
31.12.
-2.49877930е+10,
2.49938965е+10],
31.12).
... ,
shape=(8192,))
Отображение объекта в качестве результата выполнения ячейки
Все принципы работы по созданию графиков, о которых мы говорили в главах
28,
применимы и к графикам, встраиваемым в блокноты
Jupyter.
27
и
Так, мы по
прежнему можем использовать функцию или метод subplot (), чтобы на одном
изображении отобразить несколько графиков. Перенесем для примера оставшиеся
строки кода, в которых отображается амплитудный и фазовый спектры, в блокнот
Jupyter.
Результат выполнения такой ячейки показан на рис.
31.12.
Выполняя приведенные в этом разделе примеры, мы реализовали одну из задач
предыдущей главы в блокноте
Jupyter.
Теперь при необходимости мы можем ме
нять параметры, менять сигналы, а после пересчета ячеек сразу видеть результат.
Мы также можем поделиться файлом блокнота с коллегами. Важной особенностью
блокнотов является тот факт, что при открытии блокнота с результатами расчет не
будет автоматически запущен, и мы увидим те результаты, которые были рассчита
ны ранее. Это может быть важно для задач, которые требуют значительного време
ни или объема оперативной памяти для вычислений. Файлы блокнотов имеют рас
ширение ipynb, а их содержимое представляет собой текстовые данные в формате
JSON,
в которые изображения внедрены в виде двоичных данных.
Глава
31.
Интерактивные среды
IPython
Используя в блокноте нотацию
и
Jupyterlab
Markdown
657
и модуль IPython. display, мы получаем
возможность наглядно и красиво оформлять результаты расчетов, поэтому некото
рые исследователи и преподаватели используют блокноты
Jupyter
для демонстра
ции на лекциях и выступлениях. Но в этих случаях исходные файлы блокнотов
лучше преобразовать в какой-то более популярный формат, например,
PDF,
чтобы
не было эксцессов при открытии блокнота во время выступления.
fig = plt.figure(figsize = ( б , 4))
ax_spectrum_mag = fig.add_subplot(2, 1, 1)
ax_spectrum_mag. plot( freq / 1е9 , spectrum_mag)
ax_spectrum_mag. sеt_titlе("Амnлитудный спектр"')
ax_spectrum_mag.set_xlabel("Чa cтoтa,
ГГц")
ax_spectrum_mag. set_ylabel(" IS(f) 1")
ax_spectrum_mag.set_xlim(freq_min_6Hz, freq_max_6Hz)
ax_spectrum_mag.grid()
ax_spectrum_phase = fig.add_subplot( 2, 1, 2)
ax_spectrum_phase.plot(freq / le9, spectrum_phase)
ax_spectrum_phase.set_title("Фa3oвый спектр")
ax_spectrum_phase.set_xlabel("Чacтoтa,
ГГц")
ax_spectrum_phase. set_ylabel( "arg(S(f))")
ax_spectrum_phase.set_xlim(freq_min_GHz, freq_max_GHz)
ax_spectrum_phase.set_ylim(1S0, 275)
ax_spectrum_phase.grid ()
fig.tight_layout()
Амплитудный спектр
-6
-4
-2
о
2
4
б
4
б
Частота, ГГц
Фазовый спектр
~ 2SO
~ 200
~
.
1
-4
-2
150
-1---.
-
1
-
1
-б
о
2
Частота , ГГц
Рис.
31.13.
Использование метода
subplot ()
для отображения двух графиков в одном окне
Jupyter
позволяет преобразовывать блокноты во множество форматов. Если мы хотим
преобразовать блокнот в формат
PDF
и некоторые другие форматы, то нам понадобят
ся дополнительные сторонние инструменты, которые использует
преобразования.
Jupyter
в процессе
Часть
658
111. Python
для научных вычислений
Для этого под
Windows нужно установить следующие приложения:
♦
после его установки нужно добавить в переменную окружения РАТН
pandoc4 -
путь до каталога с запускаемым файлом pandoc.exe. По умолчанию это C:\Program
Files\Pandoc\;
♦ Руthоn-модуль
pandoc -
его легко установить с помощью команды pip install
pandoc;
♦
MiKTeX 5 -
это набор приложений для работы с форматом верстки
♦ для некоторых форматов (например,
Webpdf)
LaTeX;
могут понадобиться другие допол-
нительные библиотеки.
После установки дополнительных инструментов следует перезапустить сервер
Jupyter,
если он был запущен. Если все требуемые библиотеки установлены, для
преобразования блокнота в различные форматы можно воспользоваться интерфей
сом JupyterLab. Для этого надо в главном меню открыть пункт File I Save and
Export Notebook As, показанный на рис. 31.14, и выбрать нужный формат для экс
порта, вам будет предложено сохранить созданный файл на диск. Это может
быть либо непосредственно файл в экспортируемом формате например, PDF,
либо ZIР-архив с набором файлов, который будет включать в себя файл в экспор
тируемом формате и дополнительные файлы, в частности, картинки.
Download
-• 1
Save and Export NoteЬook As
Workspaces
'
Cttl+P
Print"
Asciidoc
HTML
l aTeX
1'
Markdown
Log OUt
PDF
Shut Down
Qtpdf
1,
1,
Qtpng
Restructured Тех!
ExecutaЫe Script
Reveal.js Slides
1,
WeЬpdf
Рис.
13.14.
Меню
Jupyterlab для
экспорта блокнота в различные форматы
На протяжении этой главы мы преобразовали скрипт на
Python
в блокнот
Jupyter,
но часто процесс разработки идет и в обратную сторону: мы сначала создаем блок
нот, отлаживаем в нем алгоритмы, выводя промежуточные данные (графики, таб
лицы, значения параметров), а затем, когда убедимся, что алгоритм работает так,
как мы ожидаем, на основе блокнота создаем скрипты, которые и будут использо
ваться в дальнейшем.
4
См. https://pandoc.org/installing.html.
5
См. https://miktex.org/download.
Jupyterlab
659
Для экспорта блокнота в Руthоn-скрипт в меню
File I Save and Export Notebook As
Глава
31.
Интерактивные среды
IPython
нужно выбрать пункт ExecutaЫe
и
Script -
тогда вам будет предложено сохранить
код в файл *.ру.
Надо иметь в виду, что после преобразования блокнота в скрипт в него, возможно,
понадобится внести какие-то исправления. Например, для отображения графиков в
блокноте мы не вызывали функцию
show ()
из модуля
функцию необходимо вызывать при работе с
matplotlib. pyplot,
Matplotlib в скриптах Python.
но эту
Не ис
ключено также, что понадобится удалить вывод объектов с помощью функции
display (),
поскольку в обычных скриптах эта функция будет выводить только
описание отображаемого объекта, что, скорее всего, не то, что мы задумывали из
начально.
Однако преобразовывать блокноты в другие форматы можно не только через ин
терфейс
JupyterLab, - вы можете воспользоваться командной строкой. После уста
Jupyter в консоли становится доступна команда j upyter, имеющая множест
во вложенных команд. Например, с помощью команды jupyter console можно
запустить IPython. Вызвав команду jupyter qtconsole, вы вызовете графическую
версию IPython, которая, как и блокноты Jupyter, позволяет отображать графики
новки
непосредственно в окне консоли, а не в отдельных окнах.
Но нас сейчас интересует команда
jupyter nbconvert.
Полный список параметров
этой команды можно узнать, вызвав ее с дополнительным параметром
«-h»:
> jupyter nbconvert -h
Мы же воспользуемся единственным дополнительным параметром
«--to»,
за кото
рым должна следовать строка, описывающая требуемый формат для экспорта.
«--to»: asciidoc, custom, html,
latex, markdown, notebook, pdf, python, qtpdf, qtpng, rst, script, slides, webpdf.
Чтобы выполнить экспорт в скрипт Python, надо выполнить команду:
> jupyter nbconvert --to script <имя_файла_блокнота.1руnЬ>
Поддерживаются следующие значения параметра
Например:
> jupyter
nЬconvert
--to script
example_Зl.ipynЬ
[NbConvertApp] Converting notebook example_31.ipynb to script
[NbConvertApp] Writing 2334 bytes to example_31.py
В результате будет создан файл example_31.py, содержащий команды из ячеек блок
нота.
Заключение
В этой главе мы познакомились с несколькимм популярными в научном сообщест
ве инструментами. Сначала мы рассмотрели консоль
лее удобной версией
новке.
IPython
REPL
IPython,
которая является бо
по сравнению с той, что предоставляет
Python
при уста
позволяет использовать множество нестандартных «магических»
команд, начинающихся с символа«%», значительно повышающих удобство работы
с консолью.
660
Часть
Затем мы перешли к рассмотрению блокнотов
тами
JupyterLab.
Блокноты
Jupyter- это
111. Python
Jupyter
для научных вычислений
и среды для работы с блокно
удобный инструмент, позволяющий одно
временно отлаживать алгоритмы обработки данных, строить графики на основе
промежуточных данных, наглядно отображать таблицы
Возможности
Jupyter
и
JupyterLab
Pandas
и многое другое.
можно значительно обогатить с помощью сто
ронних расширений.
Созданные блокноты используют не только в процессе отладки алгоритма, но и для
демонстрации их работы другим пользователям. Для этого блокноты можно кон
вертировать во множество форматов, в том числе преобразовывать их в обычные
скрипты на языке
Python,
которые запускаются без привлечения среды
JupyterLab.
Заключение ко всей книге
В этой книге мы прошли путь от установки интерпретатора для языка программи
рования
Python,
изучения его основ, синтаксиса, встроенных классов и стандартных
библиотек до использования математических библиотек
NumPy
и
SciPy
для расче
тов, библиотеки
для построения различного вида графиков, а также биб
лиотеки
с табличными данными.
Matplotlib
Pandas для работы
Помимо этого, мы успели рассмотреть некоторые дополнительные инструменты,
помогающие при разработке проектов на
Python.
Мы поговорили о виртуальных
окружениях и приложениях, которые делают работу с ними проще- это
А закончили книгу рассмотрением приложений
IPython
и
JupyterLab,
Poetry
и
uv.
которые по
зволяют оформлять код в виде блокнотов, включающих в себя и код, и результаты
расчета в виде таблиц, графиков или других объектов.
Но несмотря на значительный объем получившейся книги, много интересных и бо
лее глубоких тем в ней даже не были затронуты. Например, ничего не было сказано
про сборку мусора, параллельные вычисления и асинхронный код. Было бы полез
но научиться использова11ь библиотеку для тестирования
pytest,
но для понимания
работы с ней предварительно надо было бы изучить более подробно работу с ите
рируемыми объектами и генераторы. В научной среде также популярна библиотека
SymPy
для символьных вычислений, о ней мы тоже ничего не сказали. И даже у
рассмотренных библиотек имеются интересные конкуренты.
Как вы поняли, изучение
Python
и его экосистемы только начинается ...
Литература
1.
Антао Т. Сверхбыстрый
Python.
Эффективные техники для работы с большими
наборами данных/ пер. с англ. А.Ю.Гинько.
2.
Шоу Энтони. Внутри
2023. - 352 с.:
3.
5.
М.: ДМК Пресс,
гид по интерпретатору
2023. - 370 с.
Python. -
СПб.: Питер,
ил.
Фридл Дж. Регулярные выражения, 3-е издание.
Плюс,
4.
CPYTHON:
-
2008. - 608
-
Пер. с англ.
-
СПб.: Символ
с., ил.
Уэс Маккинни.
с приме
нением
Алматы:
Python и анализ данных: Первичная обработка данных
Pandas, NumPy и Jupiter / пер. с англ. А. А. Слинкина. 3-е изд. Books.kz, 2023. - 536 с.: ил.
Кnott, Eugene F. Radar cross section / Eugene Кnott, John Shaeffer, Michael Tuley. 2nd ed. ISBN 1-891121-25-1.
Предметный указатель
D
L
Dunder-мeтoды,
112, 284
LaTeX, 539
N
F
f-строки,
NumPy:
182, 194
краткий перечень функций,
472
р
Ipython -
интерактивная оболочка
для языка программирования
на
РЕР
РЕР
257 Docstring Conventions, 220
8, 87
Python, 642
т
t-строки,
Библиотека
А
♦
♦
Абсолютный путь,
367
268
Абстрактный класс, 268
Агрегация, 607
Аннотации типов, 329
Анонимная функция, 214
Аргументы функции, 191
•
Абстрактные методы,
Ассоциативные массивы,
♦
♦
♦
133
♦
♦
♦
Б
База
187
♦
h5py, 509
Jinja, 188
Matplotlib, 204, 301, 4 70, 517,
547,653
NumPy, 61,171,205,232,301,469,
517,562
Pandas, 61,301,505
Pygments, 655
РуТаЫеs, 509
Pytest, 314, 464
PyWavelets, 619
SciPy, 301,613,619
Блокноты,648
CODATA, 614
260
Базовые классы,
Булев тип,
52
Булевы значения,
52
Предметный указатель
664
Именованный параметр,
в
Вейвлет-преобразование,
Инкапсуляция,
619
Векторные поля,
♦
25
Виртуальные окружения,
♦
308
Воспроизводимая установка,
♦
316
♦
•
♦
г
♦
♦
250
♦
Глобальная переменная,
Графический язык
74
Инструкция
575
Виртуальная машина,
60
241
Инструкции ветвления,
Векторизация,470
Геттеры,
197
Импорт модуля,
208
UML, 263
♦
•
(statement), 45
assert, 86, 399, 430
break, 81, 82,119,362
continue, 83, 119
del, 140
for ... in ... , 141
import, 232
pass, 270
raise,347,351
try ... finally, 369
with, 370, 508, 51 О
Инструмент
♦
д
Датасеты,
•
♦
51 О
Двоичная строка,
•
•
374
Декодирование,375
Интеграционные тесты,
Декоратор,
225
♦ @abstractmethod, 269
♦ @wraps, 227
Декораторы, 251
Десериализация, 383
Деструкторы, 246
Дзен Python, 87
Диаграмма рассеяния, 547
Дизъюнкция, 52
Динамическая типизация, 42, 328
Дочерние классы, 260
Интерактивный режим,
Интерпретатор,
447
37, 68
24
Исключающее ИЛИ,
53
195, 345
♦ ArithmeticError, 352
• FileNotFoundError, 352
♦ TypeError, 350
• UnicodeEncodeError, 375
• ValueError, 48, 347, 352, 366
Итерируемые объекты, 119, 141, 145
Исключение,
к
Е
Евклидова норма,
Карты высот,
288
Единое хранИШ1ще библиотек
PyPi, 298
Класс,
♦
•
3
♦
♦
♦
Зависимости,
Замыкание,
Hatch, 327
Муру, 331
PDM, 327
Pipfile, 327
Poetry, 311
298
225
Значение функции,
♦
♦
191
♦
♦
и
Именованные группы,
♦
♦
♦
438
Именованные поля подстановки,
176
♦
571
40
ABCMeta, 269
Any, 337
ArgumentParser, 415
Axes, 540
Axes3D, 564
BaseException, 352
bool, 52
bytearray, 374
bytes, 374
CallaЫe, 339
complex, 40
complex, 49
DataFrame, 583, 591
Предметный указатель
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
DataFrameGroupBy, 607
datetime, 160, 181
dict, 134, 204
ellipsis, 269
enumerate, 128
Exception, 352
Figure, 540, 565
File, 510
float, 160
float, 40, 47, 48
frozenset, 144, 146, 147
function, 212
Group, 511
int, 40, 47
int, 46,295
Legend,540
Line2D, 540
list, 216
Match, 430, 435
Math, 654
module, 231
ndarray,471,477,479
NoneType, 53
numpy.ndarray, 598
object, 282
Optional, 335
Path, 395
Pattem, 429, 430, 443
PolarAxes, 553
PosixPath, 395
PurePath, 395
QuadContourSet, 573
range, 125
Series, 599, 605
set, 144, 146
str, 154, 156, 159, 162, 166, 170,
217,374
str, 41
StringMethods, 605
TestCase, 448, 449
TypeAlias, 340
TypeVar, 341
WindowsPath, 395
zip, 129, 135
Ключевое слово
♦
♦
♦
♦
♦
♦
♦
class, 244
def, 192,212
del, 115
except, 347, 358
for, 119, 145
global, 209
if, 145
665
♦
♦
♦
in, 119
retum, 192
type, 340
Ключевые слова,
46
Кодирование,375
Кодировка
UTF-8, 74, 153
Коллекции,60,90
Коллизия хешей,
140
Команда
♦
¾cd, 647
¾hist, 645
¾matplotlib, 647
¾run, 647
¾save, 647
¾timeit, 64 7
cd,320
exit, 310
jupyter, 659
jupyter console, 659
jupyter nbconvert, 659
jupyter qtconsole, 659
pip, 299
pip freeze, 304
pip install, 300
pip list, 305
pip show, 303
pip uninstall, 306
poetry add, 318
poetry update, 316
♦
ру,300
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
python -m, 306
uv add, 322, 325
♦
uv init, 320, 326
♦
uv pip, 322, 325
♦
uv run, 322
♦
uv sync, 325
♦
uv tree, 324
♦
uv venv, 326
Комментарии, 72
Компилятор, 22
♦
♦
Комплексно-сопряженное число,
Комплексные числа,
Конкатенация
49
строк, 170
Консоль,
70
cmd, 70
♦
Windows PowerShell, 71
Константа numpy.newaxis, 491
Конструкторы, 125
Конъюнкция, 52
♦
Координатывектора,288
Кортеж
(tuple), 41, 93
Круговые диаграммы,
558
50
666
Предметный указатель
♦
л
♦
Легенда,
•
530
Линии уровня,
♦
571
♦
Линтеры,
Литерал
331
(literal), 45, 154
Логическое вычитание,
Логическое И,
♦
♦
53
♦
52
♦
Логическое ИЛИ,
52
Логическое НЕ, 52
Лямбда-функция, 214
♦
♦
♦
♦
♦
м
♦
Магические команды: краткий список,
Магические методы,
Маркеры,
644
112, 448
♦
90
ndarray, 477
Математические константы
Метакласс,
Метка,
♦
pi,
е и
269
Метапеременные,
♦
♦
525
Массив (агrау),
Массивы
♦
42
530
Метод, 49
♦ _call_(), 212,339
♦ _del_(), 246
enter_(), 370
♦
♦ _exit_(), 370
♦ _getitem_(), 296,448
♦ _hash_(), 140
♦ _iadd_(), 292
♦ _imul_(), 293
♦ _init_(), 265
♦ _isub_(), 292
♦ _iter_(), 127, 296
♦ _matmul_(), 295
♦ _mul_(), 293
♦ _next_(), 296
♦ _radd_(), 295
♦ _rmul_(), 295
♦ _rsub _(), 295
♦ _str_(), 385
♦ _setitem_(), 296
♦ absolute(), 397
♦ add argument(), 416, 418
♦ ad(argument_group(), 423
♦ add_subplot(), 542, 553, 565
tau, 60
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
agg(), 607
append(), 113
apply(), 601
arrow(), 578
assertAlmostEqual, 453
assertEqual(), 449
assertRaises(), 454
bar(), 556
capitalize(), 165
clabel(), 573
clear(), 115, 140
close(), 367, 369
contains(), 605
contour(), 573
сору(), 117, 138, 402, 483, 636
сору_into(), 402
count(), 117,599,608
create _dataset(), 511
create_group(), 511
cwd(), 396
decode(), 376
difference_update(), 150
dropna(), 604
encode(), 375
endswith(), 162
exists(), 397
extend(), 113
extract(), 606
fail(), 450
fillna(), 603
find(), 163
finditer(), 441, 444
flatten(), 487
format(), 177, 178, 181
from_bytes(), 378
fromkeys(), 137
get(), 139, 204
group(), 435
groupby(), 607
groupdict(), 439
groups(), 435
head(), 611
home(), 396
idxmax(), 600
idxmin(), 600
index(), 116
info(), 583, 598
insert(), 113
intersection(), 150
Предметный указатель
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
intersection_ update(), 150
is_dir(), 397
is_file(), 397
isna(), 602
items(), 142
join(), 169
keys(), 142
lower(), 165
lstrip(), 166
mask(), 602
match(), 430, 444
rnkdir(), 401
move(), 406
move_into(), 406
notna{), 603
parse_args(), 416
plot{), 553
plot_surface{), 572
plot_wireframe{), 570
рор(), 115
print_help{), 417
prod(), 599
query(), 597
read{), 373
readline(), 371
readlines{), 371
remove(), 114
removeprefix{), 167
removesuffix(), 167
rename(), 406
replace(), 167, 406
reshape{), 485, 486
resize{), 488
reverse(), 1 18
rstrip{), 166, 169, 371
samefile(), 397
seek(), 381
set_xticks{), 558
setdefault{), 137
setUp(), 457
sort{), 118,216
sort_index{), 611
sort_values(), 611
split(), 168
startswith(), 162
str.encode(), 379
strip(), 166
sub(), 442, 444
subn(), 443
667
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
subplot(), 656
sum(), 599
symrnetric_ difference(), 150
symrnetric_ difference_update(), 151
tail(), 611
tearDown(), 457
tight_layout(), 543
title(), 165
to_bytes(), 377
to_numpy(), 598
touch(), 399
union(), 150
unlink{), 404
update{), 138, 149
upper(), 165
values{), 142
view_ init(), 569
where{), 602
write{), 368, 379
writelines{), 368, 379
Миксины, 275
Многострочные литералы, 41, 156
Множественное наследование, 260, 273
Множество 41, 144
Модуль
•
•
•
•
•
♦
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
♦
аЬс,269
argparse, 411, 415
96, 469
cmath, 65, 232
col\ections.abc, 338
datetime, 160
doctest, 44 7, 460
functools, 227
IPython.display, 654
json, 383, 498
locale, 366
logging, 171
math, 48, 60, 65, 126,232, 288, 471
matplotlib.pyplot, 518,519,532,542,553
numpy,232,471,477,499,629
numpy.fft, 621
numpy.random, 548
os, 367, 399, 405
os.path, 388
pandas, 583
pathlib, 365, 388, 395, 399
pickle, 383, 498
pylab, 518
random, 174, 186
агrау,
668
Предметный указатель
re, 429, 444
scipy.constants, 613,618
♦ scipy.fft, 621, 628, 633
♦ scipy.signal.windows, 639
♦ shutil, 399,401, 406
♦ sys, 229,412
♦ tkinter, 19, 33
♦ typing, 335
♦ unittest, 44 7
♦ venv,308
Модульные тесты, 446
♦
Параметр
♦
н
Наследование,242,260,264
Неnечатаемые символы,
Нотация
155
Markdown, 652
♦
Параметры функции,
191
Параметры-разделители,
205
Переменная
♦
file , 235, 389
name ,214,234
♦ argv, 412
♦ os.sep, 388
Побитовые операторы, 57
Подклассы, 260
Подстрока, 162
Позиционный параметр, 197
Поиск по ключу, 134
♦
Поле
♦
class , 281
_doc ,219
Полиморфизм, 242, 273
♦
Полярная система координат,
о
Преобразование Фурье,
Приемочные тесты,
Обобщенные тиnы,
Обратный слеш,
341
154, 159
♦
♦
ellipsis, 289,337,484
ndarray, 508
♦ None, 53, 107
♦ self, 292
Объекты, 40
♦
Оnератор
447
♦
♦
HDFView, 511
32
ViTaЫes, 513
ру.ехе,
Приоритет операторов,
Присваивание,
57
39
Присваивание значения,
Проект
58
Anaconda, 327
Производные классы,
♦
«моржик»,
♦
♦
as, 238
in, 139
notin,139
♦
целочисленного деления,
84, 164
in
260
61,232
числа, 174
Псевдоним модуля,
Псевдослучайные
56
not in, 109, 146
Операторы идентичности, 106
Операция (operation), 45
Основные функции Matplotlib, 545
Относительный путь, 367
Отрицание, 52
Отступы, 75
Операторы
550
619
Приложение
Объект
♦
**kwargs, 222
*args, 221
self, 245
♦
♦
р
и
Разделитель (сепаратор),
168
Разработка через тестирование,
Распаковка,
109,128,288
Реrистрозависимость, 165
Регулярные выражения, 428
Редактор IDLE, 33
Ромбовидное наследование,
п
с
Пакеты
♦
модулей,236
♦
пространств имен,
Свойства,
236
251
Сериализация,383,507
274
446
Предметный указатель
Сеттеры,
669
250
Установщик
♦
Python, 31
196, 273, 291, 328
Утиная типизация,
Символ
Caпiage
Retum, 155
Line Feed, 155
♦ возврата каретки, 155
♦ перевода строки, 155
Символы подстановки, 172
Системное тестирование, 447
Системы счисления, 46
Скрипт, 67, 68
Словари, 41, 133
Словарное включение, 135
Словарь physical _constants, 614
♦
ф
Файл
♦
pyproject.toml, 311
requirements.txt, 304
Факториал, 126
Форма массива, 484
♦
Формат
♦
BSON, 385
♦
csv, 504
♦
♦
•
♦
♦
HDF5, 508
NetCDF, 514
NPY, 506
NPZ, 507
•
Zап,515
Специальное значение
inf, 47
-inf, 47
♦ nan,47
Списки (list), 41, 92
Список sys.path, 229
♦
Списочное включение,
Списочное выражение,
Функциональное программирование,
124
124
Функция
•
•
•
•
•
Среда разработки
•
JupyterLab, 649
Срезы, 101, 162,481
Стандарт Unicode, 153
Статические методы, 257
Стек вызовов, 348
Столбчатые диаграммы, 553
Строки документации, 460
♦
•
♦
Строковое представление объекта,
Суперклассы,
160
242, 260
Сферические функции Бесселя,
Сырые строки,
616
159
•
♦
•
•
♦
♦
♦
т
•
Текстовые редакторы,
♦
69
Текущий рабочий каталог,
71,367
Тело функции,
Тестовый
192
набор, 448
Транслирование,482,490
562
477
•
•
•
♦
•
•
•
•
у
Условная единица
♦
♦
Транспонирование массива,
Трехмерные графики,
♦
point, 524
75
Условное выражение,
♦
•
ifft(), 635
abs(), 59
abspath(), 392
all(), 495
any(), 495
arange(), 475
argmax(), 479
argmin(), 479
апау(), 474, 501
atleast_2d(), 478
bar(), 553
barh(), 554
basename(), 391
boxcar(), 639
colorbar(), 567
column_stack(), 500
compile(), 429
complex(), 51
conj(), 478
conjugate(), 478
contour(), 571
сору(), 401
copyfile(), 401
copytree(), 403
difference(), 150
dir(), 111, 122, 231, 24 7
dimame(), 390
display(), 654
empty(), 472
exists(), 393
213
670
Предметный указатель
♦
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
•
♦
♦
♦
♦
♦
•
•
♦
♦
•
•
•
•
•
•
•
•
•
•
•
•
•
•
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
exit(), 413
factorial(), 126
fft(), 635
fftfreq(), 628, 636
fftshift(), 625, 638
figure(), 541, 553, 565
float(), 48, 76, 419
fmax(), 480
fmin(), 480
full(), 473
getcwd(), 367
getencoding(), 366
grid(), 539
hash(), 140
help(}, 218
hypot(}, 288
id(), 107,212
ifft(), 635, 637
input(), 76, 78
ioff(), 520
ion(), 520
irfft(), 633, 635, 638
isclose(), 64, 290
isdir(), 393
isfile(), 393
isinstance(), 281, 350
isnan(), 64
issubclass(), 281
legend(), 530
len(), 99, 143, 147, 160
linspace(), 471,475
list(), 99
loadtxt(), 502, 505
logspace(), 477
makedirs(), 400
max(), 60, 4 79
mean(), 479
meshgrid(), 562
min(), 60, 479
rnkdir(), 399
move(), 406
now(), 246
numpy.abs(), 636
numpy.load(), 506
numpy.real(), 637
numpy.reshape(), 485
numpy.resize(), 488
numpy.save(), 506
numpy.savez(), 507
numpy.zeros_like(), 623
ones(), 473
open(), 365, 399
•
♦
•
•
•
•
•
♦
♦
•
•
•
•
•
•
•
♦
♦
•
♦
•
•
♦
♦
♦
•
•
•
•
•
•
•
•
•
•
•
•
•
•
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
♦
pie(), 558
plot(), 519, 540, 54 7
polar(), 550,551
precision(), 614
print(), 38, 39, 120, 155,201,246,
347,654
prod(), 479
quiver(), 575
rand(), 548
randint(), 174
range(), 475
re.compile(), 444
re.split(), 445
read_csv(), 583,588
read_ excel(), 589
read_hdf(), 589
read json(), 588
read_taЬ!e(), 588
remove(), 404
rename(), 405
replace(), 405
repr(), 160
reshape(), 486
rfft(), 633
rfftfreq(), 633
rmdir(), 405
rmtree(), 405
run_Jine_magic(), 64 7
savetxt(), 499, 505
savez(), 508
savez_ compressed(), 508
scatter(), 548, 567
seed(), 175
show(), 519
sin(), 205
sinc(), 518
size(), 485, 609
sorted(), 216
splitext(), 391
sqrt(), 232
stack(), 500
str(), 160, 170, 376, 395
subplot(), 532, 541
subplots(), 553, 565
sum(), 479
super(), 266, 277
text(), 539
tight_layout(), 539
title(), 538
transpose(), 477
tuple(), 99
type(), 40,126,231,243,281,510
uniform(), 186
671
Предметный указатель
•
•
•
•
•
•
•
•
•
•
•
•
•
unit(), 614
unittest.main(), 450, 458
unlink(), 404
unwrap(), 629
value(), 614
xlabel(), 536
xlim(), 535
ylabel(), 536
ylim(), 535
zeros(), 473
высшего порядка, 213
Бесселя, 616
Ханкеля,616
Цикл
•
•
for, 81, 147, 296
while, 81,114,119,372
Циклы, 81
ч
Числа с плавающей точкой,
47
э
х
Хеш,
ц
Экземпляры класса,
140
Хеш-функция,
140
252
Экспоненциальная запись,
47
d'w®
ИНТЕРНЕТ-МАГАЗИН
BHV.RU
КНИГИ, РОБОТЫ,
ЭЛЕКТРОНИКА
Интернет-магазин нздатеАЬСТВа «БХВ•
• Более 30 лет на
• Книги
российском рынке
и наборы по электронике
_
и робототехнике по издательским.ц_~
• Электронные архивы
книг
и компаКТ-дИСКОВ
Интернет-маrазин 6ХВ-31ектрон11ка
Скоро открытие!
,,