Text
                    
The Quick Python Book, Fourth Edition NAOMI CEDER FOREWORD BY LUCIANO R AMALHO
Python. Экспресс-курс, четвертое издание Н АОМИ СЕДЕР ПРЕДИСЛОВИЕ Л УСИАНУ РАМА ЛЬО 2026
Наоми Седер Python. Экспресс-курс 4-е издание Перевел с английского С. Гусев Научный редактор Т. Тимирханов ББК 32.973.2-018.1 УДК 004.43 Седер Наоми С28 Python. Экспресс-курс. 4-е изд. — СПб.: Питер, 2026. — 608 с.: ил. — (Серия «Библиотека программиста»). ISBN 978-5-4461-4404-4 Автоматизация систем. Высокопроизводительные веб-приложения. Облачные и серверные службы. Передовые технологии искусственного интеллекта. Чем бы вы ни занимались, умение читать и писать на Python всегда пригодится! Эта книга уже помогла сотням тысяч разработчиков уверенно освоить этот популярный и широко востребованный язык программирования. Вы познакомитесь с новейшими функциями и библиотеками и научитесь писать умный и продуктивный код, используя современные ИИ-ассистенты, откроете основные возможности Python, предназначенные для решения ключевых задач скриптинга, разработки приложений и аналитики данных. Базовых знаний программирования достаточно, чтобы перейти от теории к практике. Интерактивные блокноты, контрольные вопросы и практические работы в конце глав помогут закрепить знания и претворить их в рабочие навыки. А рекомендации по составлению промптов и взаимодействию со средствами ИИ подскажут, как быстрее и эффективнее справляться с повседневными прикладными задачами. 16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.) Права на издание получены по соглашению с Manning Publications. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. В книге возможны упоминания организаций, деятельность которых запрещена на территории Российской Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были действующими. ISBN 978-1633436336 англ. ISBN 978-5-4461-4404-4 Authorized translation of the English edition © 2025 Manning Publications. This translation is published and sold by permission of Manning Publications, the owner of all rights to publish and sell the same. © Перевод на русский язык ООО «Прогресс книга», 2026 © Издание на русском языке, оформление ООО «Прогресс книга», 2026 © Серия «Библиотека программиста», 2026 Изготовлено в России. Изготовитель: ООО «Прогресс книга». Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург, Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373. Дата изготовления: 07.2026. Наименование: книжная продукция. Срок годности: не ограничен. Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные профессиональные, технические и научные. Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01. Подписано в печать 15.06.26. Формат 70×100/16. Бумага офсетная. Усл. п. л. 49,020. Тираж 800. Заказ 0000.
Краткое содержание Часть 1 Первые шаги Глава 1. Знакомство с Python............................................................................................ 40 Глава 2. Первые шаги........................................................................................................... 49 Глава 3. Краткий обзор Python......................................................................................... 64 Часть 2 Основы Глава 4. Основы основ......................................................................................................... 84 Глава 5. Списки, кортежи и множества....................................................................... 101 Глава 6. Строки..................................................................................................................... 130 Глава 7. Словари.................................................................................................................. 160 Глава 8. Управляющие конструкции............................................................................ 176 Глава 9. Функции................................................................................................................ 197 Глава 10. Модули и правила области видимости..................................................... 221 Глава 11. Программы на Python..................................................................................... 249 Глава 12. Работа с файловой системой........................................................................ 277 Глава 13. Чтение и запись файлов................................................................................. 307 Глава 14. Исключения........................................................................................................ 334
   6 Краткое содержание Часть 3 Расширенные возможности языка Глава 15. Классы и объектно-ориентированное программирование................ 362 Глава 16. Регулярные выражения.................................................................................. 394 Глава 17. Типы данных как объекты............................................................................. 412 Глава 18. Пакеты.................................................................................................................. 433 Глава 19. Работа с библиотеками Python.................................................................... 456 Часть 4 Работа с данными Глава 20. Общая подготовка файлов............................................................................ 468 Глава 21. Обработка файлов данных............................................................................ 478 Глава 22. Получение данных по сети............................................................................ 503 Глава 23. Сохранение данных.......................................................................................... 527 Глава 24. Анализ данных................................................................................................... 551 Практический пример...........................................................................568 Приложение. Руководство по документации Python............................................ 585
Оглавление От издательства...................................................................................................................... 25 Введение.................................................................................................................................... 26 Предисловие............................................................................................................................ 28 Благодарности......................................................................................................................... 30 Об этой книге.......................................................................................................................... 32 Об авторе................................................................................................................................... 37 Об иллюстрации на обложке............................................................................................. 38 Часть 1 Первые шаги Глава 1. Знакомство с Python............................................................................................ 40 1.1. Почему стоит использовать Python?................................................................ 41 1.2. С чем хорошо справляется Python..................................................................... 41 1.2.1. Python прост в использовании................................................................ 42 1.2.2. Выразительность Python........................................................................... 42 1.2.3. Удобочитаемость кода Python................................................................. 43 1.2.4. Полнота Python: «батарейки в комплекте»........................................ 44 1.2.5. Богатая экосистема сторонних библиотек.......................................... 44 1.2.6. Кросс-платформенность............................................................................ 44 1.2.7. Свободное распространение..................................................................... 45 1.3. Что в Python стоит улучшить.............................................................................. 46 1.3.1. Не самый быстрый язык............................................................................ 46
   8 Оглавление 1.3.2. Python не применяет принудительное определение типов переменных во время компиляции........................................................ 46 1.3.3. Слабая поддержка мобильных устройств........................................... 47 1.3.4. Слабая поддержка многоядерных систем........................................... 48 Итоги.................................................................................................................................... 48 Глава 2. Первые шаги........................................................................................................... 49 2.1. Парадокс выбора: вам какой Python?............................................................... 49 2.1.1. Версии Python............................................................................................... 50 2.1.2. Как скачать Python...................................................................................... 50 2.1.3. Устройства, платформы и операционные системы.......................... 51 2.1.4. Среды и инструменты................................................................................. 51 2.2. Colaboratory: Jupyter Notebooks в облаке........................................................ 52 2.2.1. Доступ к исходному коду блокнотов.................................................... 52 2.2.2. Начало работы с Colaboratory................................................................. 52 2.2.3. Версия Python для Colaboratory............................................................. 55 2.3. Написание и запуск программ в Colaboratory............................................... 56 2.3.1. Hello, World.................................................................................................... 56 2.3.2. Работа с ошибками в Jupyter.................................................................... 57 2.4. Применение функций help и dir для изучения Python............................... 58 2.5. Использование инструментов ИИ для написания кода на Python........ 61 2.5.1. Преимущества инструментов ИИ.......................................................... 61 2.5.2. Недостатки инструментов ИИ................................................................ 61 2.5.3. Варианты ИИ-инструментов................................................................... 62 Итоги.................................................................................................................................... 63 Глава 3. Краткий обзор Python......................................................................................... 64 3.1. Краткая характеристика Python......................................................................... 64 3.2. Встроенные типы данных...................................................................................... 65 3.2.1. Числа................................................................................................................ 65 3.2.2. Списки ............................................................................................................ 68 3.2.3. Кортежи........................................................................................................... 70 3.2.4. Строки.............................................................................................................. 70 3.2.5. Словари........................................................................................................... 72
    Оглавление 9 3.2.6. Множества...................................................................................................... 72 3.2.7. Файловые объекты...................................................................................... 73 3.3. Аннотации типов в Python................................................................................... 74 3.4. Управляющие конструкции................................................................................. 74 3.4.1. Логические значения и выражения....................................................... 74 3.4.2. Оператор if-elif-else...................................................................................... 75 3.4.3. Структурное сопоставление шаблонов при помощи оператора match............................................................................................ 75 3.4.4. Цикл while...................................................................................................... 76 3.4.5. Цикл for........................................................................................................... 76 3.4.6. Определение функции............................................................................... 77 3.4.7. Исключения................................................................................................... 78 3.4.8. Обработка контекста с использованием ключевого слова with..... 79 3.5. Создание модуля...................................................................................................... 79 3.6. Объектно-ориентированное программирование.......................................... 81 Итоги.................................................................................................................................... 82 Часть 2 Основы Глава 4. Основы основ......................................................................................................... 84 4.1. Отступы и блочная структура............................................................................. 84 4.2. Как определить комментарий.............................................................................. 86 4.3. Переменные и присваивания............................................................................... 86 4.4. Необязательные аннотации типов в Python................................................... 89 4.4.1. Когда использовать аннотации типов?................................................ 91 4.4.2. Когда не использовать аннотации типов?........................................... 91 4.4.3. Прогрессивная типизация........................................................................ 91 4.5. Выражения................................................................................................................. 92 4.6. Строки.......................................................................................................................... 92 4.7. Числа............................................................................................................................ 93 4.7.1. Встроенные числовые функции.............................................................. 95 4.7.2. Расширенные числовые функции.......................................................... 95 4.7.3. Числовые вычисления................................................................................ 96
   10 Оглавление 4.7.4. Комплексные числа..................................................................................... 96 4.7.5. Расширенные функции комплексных чисел...................................... 97 4.8. Значение None........................................................................................................... 98 4.9. Получение пользовательского ввода................................................................ 98 4.10. Встроенные операторы........................................................................................ 99 4.11. Основные элементы стиля Python.................................................................. 99 Итоги.................................................................................................................................. 100 Глава 5. Списки, кортежи и множества....................................................................... 101 5.1. Списки подобны массивам................................................................................. 102 5.2. Индексы списка...................................................................................................... 103 5.3. Модификация списков......................................................................................... 105 5.4. Сортировка списков.............................................................................................. 108 5.4.1. Пользовательская сортировка............................................................... 109 5.4.2. Функция sorted()....................................................................................... 111 5.5. Другие операции со списками........................................................................... 111 5.5.1. Проверка принадлежности элемента списку оператором in...... 111 5.5.2. Конкатенация списков оператором +................................................. 112 5.5.3. Инициализация списков оператором *.............................................. 112 5.5.4. Получение наименьшего или наибольшего элемента функциями min и max.............................................................................. 113 5.5.5. Поиск по списку при помощи метода index...................................... 113 5.5.6. Подсчет вхождений методом count..................................................... 114 5.5.7. Краткое описание операций со списками.......................................... 114 5.6. Вложенные списки и глубокие копии............................................................ 115 5.7. Кортежи..................................................................................................................... 117 5.7.1. Базовые сведения о кортежах................................................................ 118 5.7.2. Одноэлементные кортежи должны содержать запятую............... 120 5.7.3. Упаковка и распаковка кортежей......................................................... 120 5.7.4. Преобразования между списками и кортежами.............................. 122 5.8. Множества................................................................................................................ 123 5.8.1. Операции с множествами........................................................................ 123 5.8.2. Замороженные множества...................................................................... 124
    Оглавление 11 5.9. Практическая работа: изучение списка.......................................................... 125 5.9.1. Зачем решать эту задачу устаревшим способом?........................... 125 5.9.2. Решение задачи при помощи кода, сгенерированного ИИ.............................................................................. 126 5.9.3. Решения и обсуждение............................................................................. 127 Итоги.................................................................................................................................. 129 Глава 6. Строки..................................................................................................................... 130 6.1. Строки как последовательности символов................................................... 130 6.2. Основные операции со строками...................................................................... 131 6.3. Специальные символы и экранирующие последовательности............. 132 6.3.1. Основные экранирующие последовательности.............................. 132 6.3.2. Числовые (восьмеричные и шестнадцатеричные) и экранирующие последовательности Unicode............................... 133 6.3.3. Вывод и обработка строк со специальными символами.............. 134 6.4. Строковые методы................................................................................................. 135 6.4.1. Строковые методы split и join................................................................ 135 6.4.2. Преобразование строк в числа.............................................................. 137 6.4.3. Удаление лишних пробельных символов.......................................... 138 6.4.4. Поиск в строках.......................................................................................... 140 6.4.5. Изменение строк........................................................................................ 142 6.4.6. Изменение строк при помощи операций со списками.................. 144 6.4.7. Полезные методы и константы.............................................................. 145 6.5. Преобразование объектов в строки................................................................. 146 6.6. Использование метода format............................................................................ 148 6.6.1. Метод format и позиционные параметры.......................................... 148 6.6.2. Метод format и именованные параметры.......................................... 149 6.6.3. Спецификаторы формата........................................................................ 149 6.7. Интерполяция строк с помощью f-строк....................................................... 150 6.8. Форматирование строк с помощью %............................................................. 151 6.8.1. Применение форматирующих последовательностей.................... 152 6.8.2. Именованные параметры и форматирующие последовательности.................................................................................. 153 6.9. Байтовые объекты.................................................................................................. 154
   12 Оглавление 6.10. Практическая работа: предварительная обработка текста.................... 155 6.10.1. Решение задачи при помощи кода, сгенерированного ИИ....... 156 6.10.2. Решения и обсуждение.......................................................................... 157 Итоги . ............................................................................................................................... 159 Глава 7. Словари.................................................................................................................. 160 7.1. Что такое словарь?................................................................................................. 161 7.2. Другие операции со словарями......................................................................... 163 7.3. Подсчет слов............................................................................................................ 166 7.4. Что может служить ключом?............................................................................. 167 7.5. Разреженные матрицы......................................................................................... 168 7.6. Словари как кэши.................................................................................................. 170 7.7. Эффективность словарей.................................................................................... 171 7.8. Практическая работа: подсчет слов................................................................. 171 7.8.1. Решение задачи с помощью кода, сгенерированного ИИ............ 171 7.8.2. Решения и обсуждение............................................................................. 172 Итоги.................................................................................................................................. 175 Глава 8. Управляющие конструкции............................................................................ 176 8.1. Оператор if-elif-else................................................................................................ 176 8.2. Структурное сопоставление шаблонов с помощью оператора match.............................................................................................................. 178 8.3. Цикл while................................................................................................................ 179 8.4. Цикл for..................................................................................................................... 180 8.4.1. Функция range............................................................................................ 181 8.5. Управление диапазоном с помощью аргументов для начала и шага... 182 8.6. Цикл for и распаковка кортежа......................................................................... 183 8.7. Функция enumerate............................................................................................... 183 8.8. Функция zip............................................................................................................. 183 8.9. Генераторы списков, множеств и словарей................................................... 184 8.9.1. Генераторные выражения........................................................................ 185 8.10. Операторы, блоки и отступы........................................................................... 186
    Оглавление 13 8.11. Логические значения и выражения............................................................... 189 8.11.1. Использование объектов Python как логических значений..... 190 8.11.2. Операторы сравнения и логические операторы........................... 190 8.12. Простая программа для анализа текстового файла................................. 192 8.13. Практическая работа: рефакторинг word_count....................................... 193 8.13.1. Решение задачи при помощи кода, сгенерированного ИИ....... 193 8.13.2. Решения и обсуждение.......................................................................... 194 Итоги.................................................................................................................................. 196 Глава 9. Функции................................................................................................................ 197 9.1. Базовые определения функций......................................................................... 197 9.2. Параметры функций............................................................................................. 199 9.2.1. Позиционные параметры........................................................................ 199 9.2.2. Передача аргументов по имени параметра........................................ 200 9.2.3. Переменное количество аргументов................................................... 201 9.2.4. Совмещение способов передачи аргументов.................................... 203 9.3. Изменяемые объекты в качестве аргументов............................................... 203 9.3.1. Изменяемые объекты в качестве значений по умолчанию.............................................................................................. 204 9.4. Локальные, нелокальные и глобальные переменные................................ 206 9.5. Присваивание функций переменным............................................................. 208 9.6. Лямбда-выражения............................................................................................... 209 9.7. Функции-генераторы........................................................................................... 210 9.8. Декораторы............................................................................................................... 211 9.9. Практическая работа: полезные функции..................................................... 213 9.9.1. Решение задачи при помощи кода, сгенерированного ИИ......... 213 9.9.2. Решения и обсуждение............................................................................. 213 Итоги . ............................................................................................................................... 220 Глава 10. Модули и правила области видимости..................................................... 221 10.1. Что такое модуль?............................................................................................... 221 10.2. Первый модуль..................................................................................................... 222 10.3. Оператор import................................................................................................... 226
   14 Оглавление 10.4. Путь поиска модулей.......................................................................................... 227 10.4.1. Где следует размещать собственные модули.................................. 228 10.5. Приватные имена в модулях............................................................................ 230 10.6. Модули стандартной библиотеки Python и сторонние.......................... 231 10.7. Правила области видимости и пространств имен в языке Python..... 232 10.7.1. Встроенное пространство имен.......................................................... 236 10.8. Практическая работа: создание модуля....................................................... 239 10.8.1. Решение задачи при помощи кода, сгенерированного ИИ....... 240 10.8.2. Решения и обсуждение.......................................................................... 240 Итоги.................................................................................................................................. 248 Глава 11. Программы на Python..................................................................................... 249 11.1. Создание простейшей программы................................................................. 250 11.1.1. Запуск скрипта из командной строки.............................................. 250 11.1.2. Аргументы командной строки............................................................. 251 11.1.3. Выполнение кода только как основного скрипта......................... 252 11.1.4. Перенаправление ввода и вывода скрипта..................................... 253 11.1.5. Модуль argparse........................................................................................ 254 11.1.6. Применение модуля fileinput............................................................... 256 11.2. Запуск скриптов в разных операционных системах............................... 258 11.2.1. Создание скрипта, исполняемого непосредственно в UNIX..... 258 11.2.2. Скрипты на macOS.................................................................................. 259 11.2.3. Параметры выполнения скриптов в Windows............................... 259 11.3. Программы и модули.......................................................................................... 260 11.4. Распространение приложений на Python................................................... 266 11.4.1. Wheel-пакеты............................................................................................ 266 11.4.2. zipapp и pex................................................................................................. 266 11.4.3. py2exe и py2app......................................................................................... 267 11.4.4. Создание исполняемых программ с помощью freeze.................. 267 11.5. Практическая работа: создание программы............................................... 268 11.5.1. Решение задачи при помощи кода, сгенерированного ИИ....... 268 11.5.2. Решения и обсуждение.......................................................................... 269 Итоги . ............................................................................................................................... 276
    Оглавление 15 Глава 12. Работа с файловой системой........................................................................ 277 12.1. Модули os и os.path в сравнении с pathlib.................................................. 278 12.2. Пути и путевые имена........................................................................................ 278 12.2.1. Абсолютные и относительные пути.................................................. 279 12.2.2. Текущий рабочий каталог..................................................................... 280 12.2.3. Обращение к каталогам с помощью pathlib.................................... 281 12.2.4. Управление путями................................................................................. 282 12.2.5. Управление путями с помощью pathlib............................................ 284 12.2.6. Полезные константы и функции........................................................ 286 12.3. Получение информации о файлах................................................................. 287 12.3.1. Получение информации о файлах с помощью scandir............... 288 12.4. Другие операции с файловой системой....................................................... 289 12.4.1. Другие операции с файловой системой при помощи pathlib.... 292 12.5. Обработка всех файлов в поддереве каталога........................................... 294 12.6. Практическая работа: другие операции с файлами................................. 295 12.6.1. Решение задачи при помощи кода, сгенерированного ИИ....... 296 12.6.2. Решения и обсуждение.......................................................................... 296 Итоги.................................................................................................................................. 304 Глава 13. Чтение и запись файлов................................................................................. 307 13.1. Открытие файлов и файловые объекты...................................................... 308 13.2. Закрытие файлов................................................................................................. 309 13.3. Открытие файлов для записи и в других режимах.................................. 309 13.4. Функции для чтения и записи текстовых и двоичных данных........... 310 13.4.1. Двоичный режим..................................................................................... 312 13.5. Чтение и запись при помощи pathlib............................................................ 313 13.6. Консольный ввод-вывод и перенаправление............................................. 314 13.7. Обработка структурированных двоичных данных с помощью модуля struct........................................................................................................ 317 13.8. Пиклинг объектов в файлы.............................................................................. 320 13.8.1. Когда лучше обойтись без пиклинга................................................. 323 13.9. Сохранение объектов в модуле shelve.......................................................... 324 13.10. Практическая работа: последние доработки утилиты wc................... 326
   16 Оглавление 13.10.1. Решение задачи при помощи кода, сгенерированного ИИ..... 326 13.10.2. Решения и обсуждение........................................................................ 327 Итоги.................................................................................................................................. 333 Глава 14. Исключения........................................................................................................ 334 14.1. Введение в исключения..................................................................................... 335 14.1.1. Общие принципы обработки ошибок и исключений.................. 335 14.1.2. Более формальное определение исключений................................ 337 14.1.3. Обработка разных типов исключений............................................. 338 14.2. Исключения в Python........................................................................................ 338 14.2.1. Типы исключений в языке Python..................................................... 339 14.2.2. Возникновение исключений................................................................ 341 14.2.3. Перехват и обработка исключений.................................................... 342 14.2.4. Установление новых исключений...................................................... 344 14.2.5. Группы исключений................................................................................ 345 14.2.6. Отладка программ с помощью оператора assert........................... 346 14.2.7. Иерархия наследования исключений............................................... 347 14.2.8. Пример: программа Python для записи на диск............................ 347 14.2.9. Пример: исключения в обычных вычислениях............................. 348 14.2.10. Где можно использовать исключения............................................ 350 14.3. Контекстные менеджеры с ключевым словом with................................. 350 14.4. Практическая работа: добавление исключений........................................ 352 14.4.1. Решение задачи при помощи кода, сгенерированного ИИ....... 352 14.4.2. Решения и обсуждение.......................................................................... 352 Итоги . ............................................................................................................................... 360 Часть 3 Расширенные возможности языка Глава 15. Классы и объектно-ориентированное программирование................ 362 15.1. Определение классов.......................................................................................... 362 15.1.1. Использование экземпляра класса как структуры или записи.................................................................................................. 363 15.2. Переменные экземпляра................................................................................... 364 15.3. Методы.................................................................................................................... 365
    Оглавление 17 15.4. Классовые переменные...................................................................................... 367 15.4.1. Необычное поведение классовых переменных............................. 369 15.5. Статические методы и методы классов........................................................ 370 15.5.1. Статические методы............................................................................... 370 15.5.2. Методы класса.......................................................................................... 371 15.6. Наследование........................................................................................................ 372 15.7. Наследование с использованием классовых переменных и переменных экземпляра............................................................................... 375 15.8. Резюмируем: основные возможности классов Python........................... 376 15.9. Закрытые переменные и закрытые методы................................................ 379 15.10. Использование декоратора @property для гибкой работы с переменными экземпляра............................................................................. 380 15.11. Правила определения области видимости и пространств имен для экземпляров класса......................................................................... 382 15.12. Деструкторы и управление памятью.......................................................... 386 15.13. Множественное наследование...................................................................... 386 15.14. Практическая работа: HTML-классы......................................................... 389 15.14.1. Решение задачи при помощи кода, сгенерированного ИИ..... 389 15.14.2. Решения и обсуждение........................................................................ 390 Итоги.................................................................................................................................. 393 Глава 16. Регулярные выражения.................................................................................. 394 16.1. Что такое регулярное выражение?................................................................ 394 16.2. Регулярные выражения со специальными символами.......................... 395 16.3. Регулярные выражения и сырые (необработанные) строки................ 397 16.3.1. Сырые строки спешат на помощь...................................................... 398 16.4. Извлечение из строк текста, совпадающего с шаблоном....................... 399 16.5. Замена текста при помощи регулярных выражений............................... 403 16.5.1. Использование функции с методом sub.......................................... 403 16.6. Практическая работа: нормализация телефонных номеров................. 404 16.6.1. Решение задачи при помощи кода, сгенерированного ИИ....... 405 16.6.2. Решения и обсуждение.......................................................................... 405 Итоги.................................................................................................................................. 410
   18 Оглавление Глава 17. Типы данных как объекты............................................................................. 412 17.1. Типы данных тоже являются объектами..................................................... 412 17.2. Работа с типами данных.................................................................................... 413 17.3. Типы данных и пользовательские классы................................................... 414 17.4. Утиная (неявная) типизация........................................................................... 416 17.5. Что такое специальный метод?....................................................................... 417 17.6. Придание объекту свойств списка................................................................. 418 17.7. Специальный метод __getitem__................................................................... 419 17.7.1. Как это работает....................................................................................... 420 17.7.2. Реализация полной функциональности списка............................ 421 17.8. Наделение объекта полной функциональностью списка...................... 422 17.9. Наследование от встроенных типов.............................................................. 425 17.9.1. Наследование от list................................................................................ 425 17.9.2. Наследование от UserList..................................................................... 426 17.10. Когда следует применять специальные методы..................................... 427 17.11. Практическая работа: создание словаря со строковыми ключами...... 428 17.11.1. Решение задачи при помощи кода, сгенерированного ИИ..... 429 17.11.2. Решения и обсуждение........................................................................ 429 Итоги.................................................................................................................................. 432 Глава 18. Пакеты.................................................................................................................. 433 18.1. Что такое пакет?................................................................................................... 434 18.2. Первый пример: mathproj................................................................................. 434 18.3. Реализация пакета mathproj............................................................................. 435 18.3.1. Файлы __init__.py в пакетах............................................................... 437 18.3.2. Основы использования пакета mathproj......................................... 438 18.3.3. Загрузка подпакетов и подмодулей................................................... 438 18.3.4. Операторы import внутри пакетов..................................................... 439 18.4. Атрибут __all__.................................................................................................... 440 18.5. Правильное использование пакетов............................................................. 442 18.6. Практическая работа: создание пакета......................................................... 442 18.6.1. Решение задачи при помощи кода, сгенерированного ИИ....... 442
    Оглавление 19 18.6.2. Решения и обсуждение.......................................................................... 443 Итоги.................................................................................................................................. 455 Глава 19. Работа с библиотеками Python.................................................................... 456 19.1. «Все включено»: стандартная библиотека.................................................. 457 19.1.1. Управление различными типами данных....................................... 457 19.1.2. Файловые операции и хранение данных......................................... 459 19.1.3. Доступ к службам операционной системы..................................... 459 19.1.4. Работа с интернет-протоколами и форматами данных.............. 460 19.1.5. Средства разработки и отладки, службы времени выполнения............................................................................................... 461 19.2. За пределами стандартной библиотеки....................................................... 462 19.3. Установка дополнительных библиотек Python......................................... 462 19.4. Индекс пакетов Python (The Python Package Index).............................. 463 19.5. Установка библиотек Python при помощи pip и venv............................. 463 19.5.1. Установка с флагом --user..................................................................... 464 19.5.2. Виртуальные среды................................................................................. 465 19.5.3. Прочие варианты..................................................................................... 466 Итоги . ............................................................................................................................... 466 Часть 4 Работа с данными Глава 20. Общая подготовка файлов............................................................................ 468 20.1. Проблема: бесконечный поток файлов данных........................................ 468 20.2. Сценарий: кошмарный товарный фид......................................................... 469 20.3. Дальнейшее структурирование....................................................................... 472 20.4. Оптимизация хранения: сжатие и упорядочивание................................ 473 20.4.1. Сжатие файлов......................................................................................... 473 20.4.2. Упорядочивание файлов....................................................................... 475 Итоги.................................................................................................................................. 477 Глава 21. Обработка файлов данных............................................................................ 478 21.1. Знакомство с ETL................................................................................................ 479
   20 Оглавление 21.2. Чтение текстовых файлов................................................................................. 479 21.2.1. Текстовые кодировки: ASCII, Unicode и другие........................... 479 21.2.2. Неструктурированный текст............................................................... 481 21.2.3. Плоские (простые текстовые) файлы с разделителями............. 484 21.2.4. Модуль csv.................................................................................................. 486 21.2.5. Чтение csv-файла как списка словарей............................................ 489 21.3. Файлы Excel.......................................................................................................... 489 21.4. Очистка данных.................................................................................................... 492 21.4.1. Очистка....................................................................................................... 492 21.4.2. Сортировка................................................................................................. 493 21.4.3. Сложности и подводные камни очистки данных......................... 494 21.5. Запись файлов данных....................................................................................... 495 21.5.1. CSV и другие файлы с разделителями............................................. 495 21.5.2. Запись файлов Excel............................................................................... 496 21.5.3. Архивирование файлов данных.......................................................... 497 21.6. Практическая работа: метеорологические наблюдения......................... 497 21.6.1. Решение задачи при помощи кода, сгенерированного ИИ....... 498 21.6.2. Решения и обсуждение.......................................................................... 498 Итоги.................................................................................................................................. 502 Глава 22. Получение данных по сети............................................................................ 503 22.1. Загрузка файлов................................................................................................... 503 22.1.1. Получение файлов с FTP-сервера с помощью Python............... 504 22.1.2. Получение файлов с помощью SFTP............................................... 506 22.1.3. Получение файлов по протоколу HTTP/HTTPS........................ 506 22.2. Получение данных через API.......................................................................... 508 22.3. Структурированные форматы данных......................................................... 510 22.3.1. Данные в формате JSON....................................................................... 511 22.3.2. Данные в формате XML........................................................................ 515 22.4. Сбор данных с веб-страниц (скрейпинг)..................................................... 518 22.5. Практическая работа: сбор метеоданных.................................................... 522 22.5.1. Решение задачи при помощи кода, сгенерированного ИИ....... 523 22.5.2. Решения и обсуждение.......................................................................... 523 Итоги.................................................................................................................................. 526
    Оглавление 21 Глава 23. Сохранение данных.......................................................................................... 527 23.1. Реляционные базы данных............................................................................... 528 23.1.1. API баз данных (DB-API) в Python.................................................. 528 23.2. SQLite: работа с базой данных sqlite3........................................................... 528 23.3. MySQL, PostgreSQL и другие реляционные базы данных................... 531 23.4. Оптимизация работы с базами данных при помощи объектнореляционного отображения (ORM)............................................................ 532 23.4.1. SQLAlchemy............................................................................................... 532 23.4.2. Изменение схемы базы данных при помощи Alembic................ 537 23.5. Нереляционные базы данных (NoSQL)...................................................... 540 23.6. Хранилища «ключ–значение» на примере Redis..................................... 541 23.7. Документы в MongoDB..................................................................................... 545 23.8. Практическая работа: создание базы данных............................................ 550 Итоги.................................................................................................................................. 550 Глава 24. Анализ данных................................................................................................... 551 24.1. Средства Python для анализа данных.......................................................... 551 24.1.1. Преимущества Python для анализа данных................................... 551 24.1.2. Python может быть эффективнее электронных таблиц............. 552 24.2. Python и pandas..................................................................................................... 552 24.2.1. Почему стоит использовать pandas?................................................. 553 24.2.2. Установка pandas...................................................................................... 553 24.2.3. Датафреймы............................................................................................... 554 24.3. Очистка данных.................................................................................................... 556 24.3.1. Загрузка и сохранение данных с помощью pandas....................... 556 24.3.2. Очистка данных при помощи датафрейма..................................... 558 24.4. Агрегирование и обработка данных.............................................................. 561 24.4.1. Слияние датафреймов............................................................................ 561 24.4.2. Выборка данных....................................................................................... 562 24.4.3. Группировка и агрегирование............................................................. 563 24.5. Построение графика данных........................................................................... 565 24.6. Когда не стоит использовать pandas ............................................................ 566 Итоги.................................................................................................................................. 567
   22 Оглавление Практический пример....................................................................................................... 568 Загрузка данных............................................................................................................. 569 Парсинг данных по периодам наблюдений (инвентаризациям).......... 572 Выбор станции по широте и долготе.............................................................. 574 Выбор станции и получение ее метаданных................................................ 576 Получение и парсинг метеоданных......................................................................... 578 Получение данных................................................................................................ 578 Парсинг метеоданных.......................................................................................... 579 Сохранение метеоданных в базе данных (необязательно)..................... 582 Выборка данных и построение графиков..................................................... 583 Использование pandas для графического представления данных....... 584 Приложение. Руководство по документации Python............................................ 585 П.1. Доступ к онлайн-документации Python....................................................... 585 П.2. Лучшие практики: как стать питонистом..................................................... 586 П.2.1. Десять советов о том, как стать питонистом................................... 586 П.3. PEP 8: Стандарт стилевого оформления кода Python............................. 588 П.3.1. Введение....................................................................................................... 588 П.3.2. Оформление кода...................................................................................... 589 П.4. Комментарии.......................................................................................................... 594 П.4.1. Правила именования............................................................................... 596 П.4.2. Рекомендации по программированию.............................................. 602 П.4.3. Другие руководства по стилю Python............................................... 605 П.5. Дзен Python............................................................................................................. 605
Отзывы о третьем издании Книга Наоми воплощает в себе суть питонизма: красивое лучше уродливого, простое лучше сложного, и читабельность имеет значение. — Из предисловия Николаса Толлерви (Nicholas Tollervey), Python Software Foundation Книга проведет вас от базовых знаний Python к его наиболее интересным возможностям, всегда используя доступный язык. — Эрос Педрини (Eros Pedrini), Everis Раскройте свой Python-потенциал с этой книгой и начните быстро создавать реальные приложения. — Карлос Фернандес Мансано (Carlos Fernandez Manzano), Aguas de Murcia Полная и исчерпывающая книга для начала изучения Python. — Христос Пайсиос (Christos Paisios), E-Travel Отличное начало для новичков в Python. — Руслан Видерт (Ruslan Vidert), Yandex Когда время дорого, это ТА САМАЯ книга, чтобы быстро и эффективно изучить Python. — Аарон Дженсен (Aaron Jensen), Shoot the Moon Products Книга лаконичная, но в то же время всесторонняя, она предоставляет инструменты для быстрого решения реальных задач с использованием Python! — Негмат Муллоджанов (Negmat Mullodzhanov), Wavemaker Полное руководство для знакомства с кодом на Python! — Фелипе Э. Вильдосо-Кастильо (Felipe E. Vildoso-Castillo), Университет Чили
Моим друзьям — прошлым, настоящим и будущим — во всемирном сообществе Python, с благодарностью за вашу любовь, принятие и поддержку
От издательства Мы выражаем огромную благодарность клубу рецензентов ИТ-литературы ReadIT Club за помощь в работе над русскоязычным изданием книги и вклад в повышение качества переводной литературы. Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах. Обратите внимание: примеры в книге были полностью переписаны на 3.12+, но в процессе работы мы обнаружили некоторые артефакты в коде автора. Мы не стали переписывать авторский код, но оставили для вас сноски. О научном редакторе русского издания Тимур Тимирханов занимается подготовкой технической документации в ИТкомпании Dashboard System. Выполнял локализацию Mozilla Firefox, плагинов и руководств к нему.
Введение Мне довелось познакомиться с первым изданием книги The Quick Python Book за авторством Дэрила Хармса (Daryl Harms) и Кеннета Макдональда (Kenneth McDonald) еще в конце 1999 года. В те времена книг о Python было раз-два и обчелся, и, полагаю, я не пропустил ни одной из них. Но именно этот экспресскурс был лучшим на рынке учебником по программированию на этом языке. Он мастерски вел читателя от азов к вершинам, щедро делясь практической мудростью, и не ограничивался сухими примерами программного кода, а освещал куда более важные вопросы: как правильно структурировать полноценные модули и приложения на Python. Все эти достоинства были не только сохранены, но и приумножены благодаря новым примерам и глубоким наблюдениям Наоми Седер, принявшей авторскую эстафету со второго издания. Сильнейшей стороной книги «Python. Экспресс-курс» всегда было рассмотрение технических деталей в практическом контексте. Практические работы в конце глав и глава с конкретным примером фокусируются не на абстракциях, а на поэтапной разработке полноценных программ, применимых на практике. В этом, уже четвертом, издании Наоми дополнила описание возможностей Python двумя масштабными нововведениями. Первое — это использование интерактивных блокнотов на платформе Google Colaboratory. Такой подход позволяет читателям сразу же приступить к написанию и тестированию программного кода, минуя утомительную процедуру установки и настройки ПО на своем компьютере. Это также помогает им привыкнуть к самой популярной среде кодирования для data science и искусственного интеллекта: Jupyter Notebooks. Второе крупное новшество касается раздела практических работ. Теперь Наоми проводит детальное сравнение своих решений с программным кодом, сгенерированным искусственным интеллектом в Colaboratory и GitHub Copilot. Она не только взвешивает все за и против применения ИИ-ассистентов и анализирует, как они влияют на сам процесс кодирования, но и дает ценные советы по формулировке промптов для получения наилучших результатов.
   Введение 27 Совершенствование этого великолепного руководства на протяжении полутора десятилетий — лишь одна из составляющих многогранного вклада Наоми в развитие Python. Она также посвятила бесчисленные часы волонтерской работе в Python Software Foundation1 и обширному международному сообществу Python. И хотя за это время вышли тысячи книг о Python, это издание остается лучшим самоучителем по данному языку программирования из всех, что мне известны. Браво, Наоми! И огромное спасибо за то, что этот фундаментальный труд сохраняет свою актуальность и новизну в XXI веке! Лусиану Рамальо (Luciano Ramalho), питонист с 1998 года, автор книги «Fluent Python»2 1 2 Python Software Foundation (PSF) — это некоммерческая организация, основанная в 2001 году в США, целью которой являются продвижение и поддержка языка программирования Python и его сообщества. — Примеч. ред. На русском языке: Лусиану Рамальо. Python. К вершинам мастерства: Лаконичное и эффективное программирование. — Примеч. ред.
Предисловие С момента работы над третьим изданием минуло более пяти лет — целая эпоха! Мы все пережили за это время немало потрясений: и пандемию, и череду различных кризисов. Но были и мощные позитивные сдвиги: неуклонное развитие data science и машинного обучения, настоящий бум ИИ-инструментов для создания кода (кажется, сегодня искусственный интеллект проникает повсюду!) и, разумеется, триумфальное шествие Python, который прочно вошел в число самых популярных языков программирования в мире. Уже почти четверть века я программирую на Python — намного дольше, чем я работала с любым другим языком программирования. За эти годы я применяла Python в самых разных областях: от системного администрирования и разработки веб-приложений до управления базами данных и анализа данных, но что самое важное — Python стал для меня инструментом, помогающим прояснять сложные проблемы. Судя по моему прошлому опыту, за эти годы я могла бы уже увлечься какимнибудь другим, более быстрым, крутым и эффектным языком. Думаю, этого не произошло по двум причинам. Во-первых, какие бы альтернативы ни возникали на горизонте, ни одна не позволяла мне решать поставленные задачи с такой же эффективностью, как Python. Даже спустя все эти годы, чем больше я работаю с Python и чем лучше понимаю его, тем сильнее я чувствую, как растет моя квалификация программиста. Вторая причина, по которой я остаюсь приверженцем Python, — его сообщество. Это одно из самых гостеприимных, открытых, активных и дружелюбных объединений единомышленников, которые я когда-либо встречала; в его рядах есть ученые, аналитики, веб-разработчики, системные администраторы и специалисты по данным со всех уголков планеты. Для меня большая радость и честь работать в таком коллективе, и я призываю всех желающих присоединиться к нему. Работа над книгой лишний раз напомнила мне о том, как быстро течет время и как стремительно все меняется. И хотя мы по-прежнему живем в эпохе Python 3,
   Предисловие 29 его сегодняшняя версия существенно эволюционировала даже по сравнению с Python 3.6, который был актуален на момент выпуска предыдущего издания. Возможности, ранее казавшиеся немыслимыми, теперь встроены в саму ткань языка. И даже давний «заклятый враг» многопоточности — пресловутый GIL1 — стоит на пороге обретения рабочих решений. Менялся не только сам язык программирования, но и способы его применения. И хотя моей целью было сохранить все лучшее из предыдущего издания, в новую редакцию вошло немало дополнений, изменений и структурных правок, призванных повысить актуальность и полезность содержащейся в ней информации. Я также стремилась придерживаться ясного и лаконичного стиля, излагая материал доступным языком в расчете на Python-программистов всего мира, для многих из которых английский не является родным. Для меня эта книга — прежде всего возможность поделиться положительным опытом программирования на Python и познакомить вас с Python 3 — последней и, на мой взгляд, лучшей из современных версий языка. И пусть ваше путешествие в мир Python будет не менее увлекательным, чем мое. 1 GIL (Global Interpreter Lock) — глобальная блокировка интерпретатора Python, которая позволяет одновременно выполнять байт-код Python только одному потоку в рамках одного процесса, несмотря на наличие нескольких ядер процессора. Это необходимо, поскольку управление памятью в CPython не является потокобезопасным. Без GIL несколько потоков могли бы одновременно обращаться к объектам Python и изменять их, что могло бы привести к повреждению данных. — Примеч. ред.
Благодарности Выражаю глубокую признательность Дэвиду Фьюгейту (David Fugate) из LaunchBooks, который когда-то привлек меня к работе над этой книгой и все последующие годы неизменно поддерживал советами и участием. Не могу представить лучшего литературного агента и друга. Также хочу поблагодарить Джонатана Генника (Jonathan Gennick) из издательства Manning, выступившего с инициативой по созданию четвертого издания и поддержавшего меня в стремлении сделать его таким же качественным, как и первые три. Я также признательна всей команде Manning, работавшей над проектом, и особенно благодарю Марджана Бейса (Marjan Bace) за его поддержку, Дуга Раддера (Doug Rudder) за руководство на этапах разработки, Кэти Россленд (Kathy Rossland) за проведение книги (и меня) через весь процесс производства, Кари Лак (Kari Lucke) за ее терпение в ходе редактирования и Кэти Теннант (Katie Tennant) за корректуру. Сердечно благодарю и всех рецензентов, в особенности Кена Элджера (Ken Alger), технического редактора этого издания, — их точные замечания и отзывы оказали неоценимую помощь. Кен преподавал, писал и выступал с докладами на различные темы, связанные с Python, а в 2017 году входил в совет директоров Django Software Foundation. Он работал с Python с середины 1990-х годов, помогая различным отраслям автоматизировать задачи с помощью этого языка. Кроме того, я признательна всем тем, кто делился ценными замечаниями, включая рецензентов: Аарона С. Буша (Aaron S. Bush), Адвайта Пателя (Advait Patel), Альфонсо Хардинга (Alfonso Harding), Андерса Перссона (Anders Persson), Ануджа Тьяги (Anuj Tyagi), Ариана Ахмадинежада (Arian Ahmdinejad), Асаада Саада (Asaad Saad), Кая Хорстманна (Cay Horstmann), Чаламайю Батчу (Chalamayya Batchu), Кристиана Саттона (Сhristian Sutton), Дайвида Моргана (Daivid Morgan), Дэйна Хилларда (Dane Hillard), Даррина Бишопа (Darrin Bishop), Фелипе Провезано Кутиньо (Felipe Provezano Coutinho), Гаджендру Бабу Токала (Gajendra Babu Thokala), Ганеша Харке (Ganesh Harke), Глена Мьюлза (Glen Mules), Грега Вагнера (Greg Wagner), Харшиту Аснани (Harshita Asnani), Хизер Уорд (Heather Ward), Йена Брайерли (Ian Brierley), Джеймса Бишопа (James Bishop), Джеймса Бретта (James Brett), Джеймса Маккорри (James McCorrie), Джима Мейсона (Jim Mason), Хосе Апаблазу (Jose Apablaza),
   Благодарности 31 Хуана Гомеса (Juan Gomez), Джастина Райзера (Justin Reiser), Кена Юэнс-Кларка (Ken Youens-Clark), Кирана Кумара Гунтуру (Kiran Kumar Gunturu), Лоуренса Болдуина (Laurence Baldwin), Лава Кумара (Lav Kumar), Льва Вейде (Lev Veyde), Монишу Ати Кесаван Премалату (Monisha Athi Kesavan Premalatha), Николя Шартье (Nicolas Chartier), Питера Ван Каннегема (Peter Van Caeneghem), Рагхуната Майсура (Raghunath Mysore), Раза Павела (Raz Pavel), Рича Хиллиара (Rich Hilliar), Роберта Ф. Шейдера (Robert F. Scheyder), Стива Грей-Уилсона (Steve Grey-Wilson), Танвир Каур (Tanvir Kaur), Ватче Джабагчуряна (Vatche Jabagchourian), Венкатеша Раджагопала (Venkatesh Rajagopal) и Уильяма Джамира (William Jamir). Ваши советы и предложения помогли сделать эту книгу лучше. Я должна поблагодарить авторов первого издания, Дэрила Хармса (Daryl Harms) и Кеннета Макдоналда (Kenneth MacDonald), создавших настолько фундаментальный труд, что он остается в печати гораздо дольше большинства технических книг, будучи востребованным и по сей день. Именно это позволило мне работать над вторым, третьим и теперь уже четвертым изданием. Спасибо и всем читателям, кто приобретал предыдущие выпуски книги и давал о них положительные отзывы. Надеюсь, это издание достойно продолжит успешную и проверенную временем традицию своих предшественников. Особая благодарность — Лусиану Рамальо (Luciano Ramalho), не только за теплые слова в предисловии к четвертому изданию, но и за годы нашей дружбы, а также за то, что он много лет делится своими обширными знаниями о Python с бразильским и мировым сообществами программистов. Muito obrigada, meu amigo (Огромное спасибо, мой друг). Я также в неоплатном долгу перед всемирным Python-сообществом — неизменным источником поддержки, мудрости, дружбы и вдохновения на протяжении многих лет. Позволю себе еще раз процитировать слова Бретта Кэннона (Brett Cannon): «Я пришел из-за языка, но остался ради сообщества». Спасибо всем вам, друзья! И самое главное: я неизменно и бесконечно благодарна Бекки — той, кто не просто вдохновила меня на этот труд, но и была моей опорой на каждом этапе работы. По правде говоря, без нее я бы не справилась.
Об этой книге Книга «Python. Экспресс-курс. 4-е изд.» адресована тем, кто уже знаком с программированием на одном или нескольких языках и стремится овладеть основами Python 3 максимально быстро и без лишних отступлений. Несмотря на то что в ней затронут ряд основных концепций, она не ставит целью научить азам программирования. Предполагается, что читатель уже знаком с такими базовыми понятиями, как управление потоком, объектно-ориентированное программирование (ООП), доступ к файлам, обработка исключений и т. п. Издание также может пригодиться пользователям более ранних версий Python, которым необходим краткий справочник по Python 3. Для кого эта книга Эта книга — для тех, кто уже имеет представление о том, как пишется программный код, и разбирается в основах программирования. Неважно, переходите вы с другого языка, стремитесь углубить ваши знания о Python или просто хотите их актуализировать — здесь вы найдете компактный и понятный обзор возможностей Python, охватывающий те 90 % средств языка, что применяются в большинстве реальных задач. Структура книги В части 1 дается введение в Python, описывается методика работы с этим языком программирования при помощи сервиса Google Colaboratory, а также объясняется, как получить исходный код Jupyter Notebooks из GitHub-репозитория издания. Кроме того, первая часть книги содержит общий обзор языка, который особенно пригодится опытным программистам, интересующимся высокоуровневым представлением о Python. Глава 1 содержит описание сильных и слабых сторон Python и обоснование целесообразности выбора этого языка программирования для решения широкого спектра практических задач.
   Структура книги 33 Глава 2 посвящена методам работы с Python в среде Google Colaboratory и процедуре получения исходного кода в формате Jupyter Notebooks из GitHub-репозитория данного издания. Глава 3 представляет собой краткий обзор языка Python, содержащий общее описание его философии, синтаксиса, семантики и возможностей. Часть 2 является ядром книги. Здесь систематизированы элементы, необходимые для формирования практического владения Python в качестве языка программирования универсального назначения. Главы разработаны так, чтобы позволить читателям, начинающим изучение Python, последовательно продвигаться, усваивая знания о ключевых аспектах языка. Эти главы также содержат разделы повышенной сложности, к которым можно при необходимости вернуться и найти в одном месте всю нужную информацию о конструкции или теме. Глава 4 начинается с рассмотрения основ Python. В ней вводятся понятия Python-переменных, выражений, строк и чисел. Здесь же читатели знакомятся с блочной структурой синтаксиса Python. Главы 5, 6 и 7 содержат описание пяти мощных встроенных типов данных Python: списков, кортежей, множеств, строк и словарей. Глава 8 знакомит с синтаксисом и способами применения управляющих конструкций Python (циклы, условные операторы if-else и новый оператор структурного сопоставления шаблонов match-case). Глава 9 описывает определение функций в Python и гибкие возможности передачи параметров. Глава 10 посвящена модулям Python, которые предоставляют простой механизм для сегментирования пространства имен программы. Глава 11 охватывает создание автономных программ (скриптов) Python и их запуск на платформах Windows, macOS и Linux. Здесь также рассматривается поддержка параметров командной строки, аргументов и перенаправления ввода-вывода. Глава 12 описывает работу с файлами и каталогами файловой системы и навигацию по ним. В ней показано, как писать код, максимально независимый от конкретной операционной системы. Глава 13 знакомит с механизмами чтения и записи файлов в Python, включая базовые возможности для чтения и записи строк (или байтовых потоков), механизм чтения двоичных записей, а также возможность чтения и записи произвольных объектов Python. Глава 14 посвящена исключениям — механизму обработки ошибок, используемому в Python. Материал не требует предварительного опыта работы с исключениями, а для программистов на C++ или Java соответствующие концепции покажутся знакомыми.
   34 Об этой книге Часть 3 рассказывает о расширенных возможностях Python, а именно об элементах языка, которые не являются обязательными для базового применения, однако могут оказаться весьма полезными для профессиональных разработчиков. Глава 15 знакомит с поддержкой объектно-ориентированного программирования (ООП) в Python. Глава 16 описывает возможности регулярных выражений, доступные в Python. Глава 17 знакомит с более продвинутыми техниками ООП, включая применение специальных методов Python, метаклассов и абстрактных базовых классов. Глава 18 вводит понятие пакетной организации кода в Python для структурирования крупных программных проектов. Глава 19 содержит краткий обзор стандартной библиотеки Python. В ней также рассматриваются вопросы нахождения внешних модулей и процедуры их установки. Часть 4 освещает более продвинутые или специализированные темы, выходящие за рамки строгого синтаксиса языка. Изучение данных разделов является факультативным, в зависимости от конкретных потребностей читателя. Глава 20 посвящена углубленному изучению файловых операций в Python. Глава 21 описывает стратегии чтения, очистки и записи различных типов файлов данных. Глава 22 содержит обзор процессов, проблем и средств, связанных с получением данных по сети. Глава 23 охватывает методы взаимодействия Python с реляционными и нереляционными базами данных. Глава 24 представляет собой краткое введение в использование Python, библиотеки pandas и matplotlib для исследования наборов данных. Практический пример описывает поэтапный процесс применения Python для извлечения данных, их очистки и последующей визуализации. Этот проект задействует ряд языковых возможностей, рассмотренных в предыдущих главах, и демонстрирует полный цикл его реализации от начала до конца. Приложение включает рекомендации по доступу к полной документации Python, руководство по «питоническому» стилю оформления программного кода (PEP 8), а также «Дзен языка Python» — немного ироничное и емкое изложение философии, лежащей в основе Python. Если вы новичок в программировании на Python и желаете сразу перейти к его изучению, рекомендуется начать с главы 3 для получения общего представления о языке, а затем ознакомиться с соответствующими главами из части 2.
   О коде в книге 35 Выполнение интерактивных примеров по мере их представления немедленно закрепляет теоретические понятия, а выход за рамки примеров из текста для поиска ответов на то, что может быть неясным, способен значительно ускорить процесс вашего обучения и повысить уровень понимания. Если вы не знакомы с ООП и не собираетесь применять его в вашем приложении, можете пропустить большую часть главы 15. Тем из вас, кто уже знаком с Python, также рекомендуется начать с главы 3. В ней представлен емкий обзор, демонстрирующий различия между Python и другими языками программирования. Кроме того, этот материал служит критерием оценки вашей готовности к изучению углубленных разделов, представленных в частях 3 и 4 издания. Не исключено, что отдельные читатели без опыта работы с Python, но с достаточной квалификацией в других языках программирования смогут освоить основной объем необходимых практических знаний, прочитав главу 3 и ознакомившись с модулями стандартной библиотеки Python, представленными в главе 19, с опорой на обширную документацию по стандартной библиотеке. О коде в книге Примеры программного кода в книге предельно упрощены, поскольку их цель — не предоставление готовых к повторному использованию компонентов для интеграции в ваши проекты, а демонстрация ключевых принципов языка через максимально лаконичные решения. Книга ориентируется на Python 3.13, включая некоторые функции, которые еще не поддерживаются в Colaboratory (где на момент написания установлен Python 3.11). Все представленные примеры кода сохраняют совместимость с последующими версиями Python 3. За исключением новейших функций, примеры сохраняют работоспособность, начиная с Python 3.9 и более поздних версий, однако я настоятельно рекомендую использовать самую актуальную версию, поскольку более ранние версии не предоставляют каких-либо преимуществ, тогда как каждая новая версия содержит множество точечных оптимизаций и улучшений по сравнению с предыдущими. Для удобства использования примеры кода представлены в виде ячеек Jupyter Notebook, которые можно запустить в Google Colaboratory или на любом другом Jupyter-сервере. По возможности как можно больше экспериментируйте с этими примерами. Jupyter Notebooks, содержащие программный код примеров и решения большинства упражнений из рубрик «Быстрая проверка» и «Попробуйте сами» издания, доступны на веб-сайте издательства Manning по адресу www.manning. com/books/the-quick-python-book-fourth-edition. Кроме того, все материалы можно найти в репозитории GitHub по ссылке https://github.com/nceder/qpb4e.
   36 Об этой книге Исполняемые фрагменты кода из онлайн-версии этой книги (liveBook) можно получить по адресу https://livebook.manning.com/book/the-quick-python-book-fourthedition. «Python. Экспресс-курс. 4-е изд.» содержит множество примеров исходного кода как в нумерованных листингах, так и непосредственно в тексте. В обоих случаях исходный код форматируется моноширинным шрифтом, чтобы отделить его от обычного текста. Результат выполнения кода выделяется полужирным моноширинным начертанием. Во многих случаях исходный код был переформатирован: мы добавили переносы строк и изменили отступы, чтобы уместить его на странице книги. В некоторых случаях даже этого было недостаточно, и в листинги добавлялись маркеры продолжения строки (➥). Помимо этого, комментарии часто исключались из листингов в случаях, когда соответствующий код подробно разбирается в основном тексте. Многие листинги сопровождаются аннотациями к коду, акцентирующими внимание читателя на ключевых аспектах. В некоторых случаях приводятся расширенные примеры программного кода, которые обозначены в тексте издания как файловые листинги. Можно скачать соответствующие файлы из репозитория исходного кода или сохранить листинги в виде файлов с именами, указанными в тексте, и запустить их как автономные скрипты. Форум liveBook Приобретая книгу «Python. Экспресс-курс. 4-е изд.», вы получаете бесплатный доступ к liveBook — интерактивной платформе для чтения издательства Manning. При помощи уникальных дискуссионных возможностей liveBook читатели могут прикреплять комментарии как к книге в целом, так и к ее конкретным разделам и абзацам, оставлять личные заметки, обсуждать технические вопросы, а также консультироваться с автором и другими пользователями. Для доступа к форуму перейдите по адресу https://livebook.manning.com/book/the-quick-python-book-fourthedition/discussion. Подробнее о форумах Manning и правилах поведения можно узнать по ссылке https://livebook.manning.com/book/discussion. В рамках своих обязательств перед читателями издательство Manning предоставляет ресурс для содержательного общения читателей и авторов. Эти обязательства не подразумевают конкретную степень участия автора, которое остается добровольным (и неоплачиваемым). Задавайте авторам хорошие вопросы, чтобы им было интересно участвовать в диалоге! Форум и архивы обсуждений доступны на сайте Manning, пока книга продолжает издаваться.
Об авторе Наоми Седер занимается программированием на различных языках свыше 30 лет. Она является системным администратором Linux, преподавателем программирования, разработчиком и системным архитектором. С Python она познакомилась в 2001 году и с тех пор преподает его пользователям всех уровней — от двенадцатилетних школьников до опытных профессионалов. Она активно пропагандирует Python и ценности открытого Python-сообщества. Экс-председатель фонда Python Software Foundation и обладательница его высшей награды за выдающиеся заслуги, Наоми теперь наслаждается заслуженным отдыхом, посвящая свободные часы игре на классической гитаре.
Об иллюстрации на обложке Обложку «Python. Экспресс-курс. 4-е изд.» украшает рисунок под названием «Женщина из Баварии» (фр. «Bavaroise») из четырехтомной энциклопедии национальных костюмов «Costumes Civils Actuels de Tous les Peuples Connus», составленной при участии Пьера Сильвена Марешаля (Pierre Sylvain Maréchal) и опубликованной во Франции в конце XVIII века. Каждая иллюстрация была тщательно прорисована и раскрашена вручную. В ту эпоху одежда была визитной карточкой человека: с первого взгляда можно было понять, где живут люди, каков их род занятий или положение в обществе. Создавая обложки для книг о компьютерных технологиях, издательство Manning отдает дань уважения изобретательности и духу предпринимательства — качествам, присущим индустрии IT, — и черпает вдохновение в богатом наследии региональных культур прошлого, которые оживают благодаря подобным коллекциям.
Часть 1 Первые шаги В первых трех главах мы коротко расскажем о Python, его сильных и слабых сторонах, а также о том, почему вам стоит подумать о его изучении. В главе 2 вы узнаете, как запустить Python при помощи Google Colaboratory, как получить доступ к приведенным в книге примерам программного кода на GitHub и как написать несложную программу. Глава 3 содержит краткий высокоуровневый обзор синтаксиса и возможностей Python. Для максимально быстрого знакомства с Python рекомендуется начать с главы 3.
1 Знакомство с Python В этой главе: 3 Почему стоит использовать Python? 3 Сильные стороны Python 3 Что в Python стоит улучшить Эта книга призвана помочь читателям как можно скорее обрести прочное общее представление о языке Python, избегая при этом углубления в продвинутые темы, но охватывая все необходимое для написания и чтения кода. В частности, она предназначена для тех, кто изучал другие языки программирования или уже немного знаком с Python, однако стремится расширить свои знания и практические навыки, поскольку популярность Python продолжает расти, особенно в таких областях, как data science, машинное обучение и научные исследования. Хотя никакого предварительного знакомства с Python не требуется, для извлечения максимальной пользы из этой книги желательно обладать некоторыми познаниями и опытом в сфере программирования. После введения в Python и рекомендаций по началу работы в его среде приводится краткий обзор синтаксиса Python, а затем следуют главы, в которых рассматриваются встроенные типы данных, создание функций, классов и пакетов, а также некоторые расширенные возможности и практический пример работы с данными.
   1.2. С чем хорошо справляется Python 41 С развитием инструментов искусственного интеллекта (ИИ) на базе больших языковых моделей (LLM) появилась возможность автоматически создавать все бˆольшие объемы программного кода при условии (и это немаловажно), что разработчик обладает достаточными знаниями в области программирования, чтобы грамотно управлять этим процессом. И хотя издание не является учебником по ИИ и его применению для генерации кода, задачи по программированию, поставленные в конце каждой главы, начиная с главы 5, содержат примеры ответов ИИ на те же вопросы, а также краткое обсуждение того, что искусственный интеллект сделал правильно (и что неправильно). Это поможет вам понять, как при помощи инструментов ИИ создавать программный код, который действительно работает. 1.1. Почему стоит использовать Python? В современном мире существуют сотни языков программирования, от проверенных временем — таких, как C и С++, — до относительно новых — Rust, Go и C#, а также мощных корпоративных решений вроде Java и более вебориентированных, как JavaScript и Typescript. Такое изобилие затрудняет выбор языка программирования. Хотя ни один язык не может считаться идеальным вариантом для всех возможных ситуаций, я думаю, что Python хорошо подходит для решения многих задач программирования; кроме того, это отличный выбор для тех, кто только учится программировать. Миллионы программистов по всему миру используют Python, и их число растет год от года. Python продолжает привлекать новых пользователей по целому ряду причин. Это полноценный кросс-платформенный язык, который одинаково хорошо работает на платформах Windows, Linux/UNIX и iOS, а также многих других, от суперкомпьютеров до мобильных устройств. Он может применяться для создания небольших приложений и быстрых прототипов, но также прекрасно масштабируется для разработки крупных программ. В состав Python входит мощный и удобный инструментарий для создания графических пользовательских интерфейсов (GUI), библиотеки для веб-программирования и многое другое. Python также стал важнейшим инструментом для выполнения научных вычислений, для науки о данных, машинного обучения и работы с искусственным интеллектом. И все это бесплатно. 1.2. С чем хорошо справляется Python Python — современный язык программирования, созданный Гвидо ван Россумом (Guido van Rossum) в 1990-е годы (и получивший свое название в честь знаменитой комедийной труппы «Монти Пайтон» («Monty Python»), а точнее, в честь их популярного телешоу «Летающий цирк Монти Пайтона» («Monty Python’s Flying Circus»)). Хотя Python нельзя назвать идеальным средством для создания любых приложений, благодаря своим сильным сторонам он хорошо подходит для решения множества различных задач.
   42 Глава 1. Знакомство с Python 1.2.1. Python прост в использовании У программистов, знакомых с традиционными языками программирования, не возникнет особых трудностей с изучением Python. В нем содержатся все знакомые конструкции: циклы, условные операторы, массивы и т. п., однако многие из них проще в использовании. И вот почему. Типы связываются с объектами, а не с переменными. Переменная может указывать на объекты любого типа, а список может содержать объекты различных типов. Это также означает, что преобразования типов обычно не требуется, а ваш код не ограничен жесткими рамками заранее объявленных типов. И хотя для переменных типы не требуются, Python позволяет использовать аннотации типов, дающие разработчикам возможность отслеживать согласованность своего кода в отношении типов объектов, используемых для параметров, возвращаемых значений и т. п. Далее в книге мы уделим внимание аннотациям типов. Python обычно работает на более высоком уровне абстракции. Отчасти это обусловлено особенностями языка, а отчасти объясняется обширной стандартной библиотекой, включенной в дистрибутив Python. Программу для загрузки веб-страницы можно написать всего в две-три строки! Правила синтаксиса очень просты. Конечно, чтобы стать настоящим гуру, потребуется немало времени и усилий, однако даже новички способны освоить синтаксис Python в достаточной мере для того, чтобы свободно писать работающий код. Python великолепно подходит для быстрой разработки приложений. На Python приложение нередко создается в пять раз быстрее, чем на С или Java, и при этом занимает впятеро меньше строк, чем аналогичная программа на C. Безусловно, все зависит от конкретного приложения; для числовых алгоритмов, выполняющих в основном целочисленные операции в циклах for, прирост производительности будет куда менее заметным. Однако в среднем такой прирост может оказаться весьма существенным. 1.2.2. Выразительность Python Язык Python чрезвычайно выразителен. Под выразительностью в данном контексте понимается то, что одна строка кода Python может сделать намного больше, чем одна строка кода в большинстве других языков. Преимущества более выразительного языка очевидны: чем меньше строк кода вам придется написать, тем скорее вы сможете сделать проект. Чем меньше строк кода содержит программа, тем меньше проблем будет с ее поддержкой и отладкой. Чтобы оценить, как выразительность Python упрощает код, возьмем задачу перестановки значений двух переменных, var1 и var2. В таком языке, как Java, для этого потребуются три строки кода и дополнительная переменная:
   1.2. С чем хорошо справляется Python 43 int temp = var1; var1 = var2; var2 = temp; Переменная temp необходима для сохранения значения var1 перед тем, как ей будет присвоено значение var2, и последующего присвоения сохраненного значения var2. Процесс не особенно сложен, но, чтобы прочитать эти три строки и понять, что произошла перестановка, даже опытному программисту придется немного поразмыслить. В отличие от этого, Python позволяет выполнить ту же перестановку в одной строке, причем так, что читатель программного кода сразу понимает, что значения меняются местами: var2, var1 = var1, var2 Разумеется, это очень простой пример, однако язык Python просто изобилует такими преимуществами. 1.2.3. Удобочитаемость кода Python Следующее преимущество кода Python заключается в том, что он легко читается. Казалось бы, язык программирования предназначен исключительно для компьютерной обработки, однако людям тоже приходится читать программный код: тому, кто занимается отладкой вашей программы (возможно, это будете вы сами), тому, кто осуществляет ее поддержку (опять-таки им можете оказаться вы), тому, кто, возможно, захочет модифицировать ее код в будущем. Во всех этих ситуациях чем легче прочесть и понять код, тем лучше. Таким образом, чем понятнее код программы, тем его проще отлаживать, поддерживать и модифицировать. Главное преимущество Python в этом отношении — использование отступов. В отличие от большинства языков, Python требует, чтобы блоки кода снабжались отступами. Кому-то это требование может показаться странным, но оно гарантирует, что ваш код всегда будет отформатирован в удобочитаемом виде. Ниже приведены две короткие программы, одна из которых написана на Perl, а другая — на Python. Обе получают два списка чисел одинакового размера и возвращают попарную сумму этих списков. По-моему, код Python читается лучше, чем код Perl; он визуально чище и содержит меньше непонятных символов: # Версия Perl. sub pairwise_sum { my($arg1, $arg2) = @_; my @result; for(0 .. $#$arg1) { push(@result, $arg1->[$_] + $arg2->[$_]); } return(\@result); }
   44 Глава 1. Знакомство с Python # Версия Python. def pairwise_sum(list1, list2): result = [] for i in range(len(list1)): result.append(list1[i] + list2[i]) return result Оба фрагмента кода выполняют одно и то же действие, однако код Python выигрывает в отношении удобочитаемости. (Справедливости ради, существуют и другие способы сделать это на Perl, некоторые из них гораздо компактнее, но читаются они, на мой взгляд, еще хуже.) 1.2.4. Полнота Python: «батарейки в комплекте» Еще одно преимущество Python — его философия «батарейки в комплекте» в отношении модулей. Суть ее заключается в том, что при установке Python вы получаете весь рабочий инструментарий целиком, без необходимости подключать дополнительные модули. Вот почему стандартная библиотека Python поставляется с модулями для работы с электронной почтой, веб-страницами, базами данных, вызовами операционной системы, разработкой графического пользовательского интерфейса и т. п. К примеру, на языке Python вам потребуются лишь две строчки кода, чтобы написать веб-сервер для обеспечения совместного доступа к файлам в каталоге: import http.server http.server.test(HandlerClass=http.server.SimpleHTTPRequestHandler) Нет необходимости устанавливать модули для обработки сетевых подключений и HTTP, вся функциональность уже доступна в установочной версии Python. 1.2.5. Богатая экосистема сторонних библиотек Несмотря на то что Python «укомплектован под завязку», неизбежны ситуации, когда все же приходится выходить за рамки полностью оснащенной стандартной библиотеки — это может быть узкоспециальная задача, новый формат данных, более сложные приложения и т. п. В этом отношении Python вышел на передовые позиции за последнее десятилетие. Во множестве областей, от веб-приложений и API до обработки и визуализации данных, машинного обучения и науки о данных, Python обладает одной из богатейших экосистем пакетов, модулей и фреймворков среди всех существующих языков программирования. И потому вероятность оказаться в ситуации, когда не найдется ни одного пакета Python, отвечающего вашим потребностям, ничтожно мала. 1.2.6. Кросс-платформенность Python также является превосходным кросс-платформенным языком. Он работает на многих платформах: Windows, Mac, Linux, UNIX и др. Так как язык
   1.2. С чем хорошо справляется Python 45 является интерпретируемым, один и тот же код может выполняться на любой платформе с интерпретатором Python, а он сейчас реализован практически на всех современных платформах. Существуют даже версии Python, работающие на базе Java (Jython) и .NET (IronPython), а также на микроконтроллерах (MicroPython и CircuitPython), что дополнительно расширяет круг возможных платформ для работы с Python. 1.2.7. Свободное распространение Наконец, за Python не нужно платить. Python изначально разрабатывался и продолжает разрабатываться по модели открытого исходного кода (open source) и находится в свободном доступе. Вы можете скачать и установить практически любую версию Python, использовать ее для разработки программного обеспечения как для коммерческих, так и для личных приложений, и вам не придется заплатить за это ни копейки. Хотя отношение к бесплатному программному обеспечению постепенно меняется, некоторые по-прежнему относятся к нему настороженно из-за недостаточного уровня поддержки и веса в лице платных клиентов. Тем не менее многие авторитетные компании применяют Python в ключевых сферах своего бизнеса. Google, Bloomberg, NVIDIA, Capital One — вот лишь несколько примеров. Эти и многие другие компании справедливо считают Python весьма надежным, стабильным и хорошо поддерживаемым продуктом с активным и компетентным сообществом пользователей. На многочисленных интернет-форумах, посвященных Python, даже на самый трудный вопрос можно получить ответ быстрее, чем по большинству горячих линий технической поддержки, причем ответ будет правильным и бесплатным. Python и ПО с открытым исходным кодом Не только сам Python бесплатен, но и его исходный код находится в открытом доступе, и при желании вы вправе свободно модифицировать, улучшать и дополнять его. Вы можете заняться этим самостоятельно или нанять того, кто сделает это за вас. В случае с коммерческим программным обеспечением такая возможность редко предоставляется по разумной цене. Если вы впервые сталкиваетесь с миром ПО с открытым исходным кодом, то вам следует понять, что вы можете не только свободно использовать и модифицировать Python, но и вносить свой вклад в его развитие и совершенствование (и это даже приветствуется). В зависимости от ваших обстоятельств, интересов и квалификации это может быть финансовый вклад (в частности, пожертвование в фонд Python Software Foundation), участие в одной из специальных групп по интересам (SIG — special interest group), тестирование и предоставление отзывов о релизах ядра Python или одного из вспомогательных модулей, а также предоставление сообществу части того, что разрабатываете вы или ваша компания. Конечно, уровень вклада зависит только от вас, но, если вы можете сделать что-то для других, это определенно того стˆоит. Здесь общими усилиями создается нечто весьма ценное, и у вас есть возможность внести свой посильный вклад.
   46 Глава 1. Знакомство с Python Итак, у Python есть масса преимуществ: выразительность, удобочитаемость, богатый набор встроенных библиотек, кросс-платформенность, а также открытый исходный код. В чем же подвох? 1.3. Что в Python стоит улучшить Хотя Python обладает многочисленными плюсами и постоянно совершенствуется, он неидеален, так как ни один язык не может удовлетворить абсолютно все потребности. Чтобы определить, станет ли Python подходящим языком для решения ваших задач, нужно обозначить и те области, в которых Python пока отстает. 1.3.1. Не самый быстрый язык Один из возможных недостатков Python — скорость выполнения кода. Он не является полностью компилируемым языком. Вместо этого код сначала компилируется во внутренний байт-код, который затем выполняется интерпретатором Python. Есть ряд задач, в частности парсинг строк при помощи регулярных выражений, для которых Python имеет эффективную реализацию и работает так же быстро, а то и быстрее, чем любая программа на C, которую вы сможете написать. Тем не менее в большинстве случаев программы, написанные на Python, работают медленнее по сравнению с программами на C. Впрочем, на это следует взглянуть здраво: современные компьютеры обладают такой вычислительной мощностью, что для большинства приложений скорость разработки важнее скорости выполнения, а программы на Python, как правило, пишутся куда быстрее, чем на других языках. Кроме того, Python легко расширяется модулями, написанными на C или С++; они могут использоваться для выполнения ресурсоемких частей программы. Основные разработчики Python также упорно трудятся над созданием его новых версий, которые являются более эффективными, загружаются и работают быстрее, а также лучше используют преимущества многоядерных процессоров. Эти усилия уже привели к значительному повышению быстродействия, и работа продолжится в будущем, так что, если вы собираетесь создать приложение, для которого критична производительность, вам стоит хорошенько подумать, подойдет ли Python для этой задачи (однако не стоит сразу же списывать его со счетов). 1.3.2. Python не применяет принудительное определение типов переменных во время компиляции В отличие от некоторых других языков, переменные в Python не служат контейнерами для своих значений, скорее, они похожи на ссылки для указания на объекты различных типов: целых чисел, строк, экземпляров класса и т. д. Это
   1.3. Что в Python стоит улучшить 47 означает, что, хотя сами объекты обладают типами, ссылающиеся на них переменные не привязаны к этим конкретным типам. Можно (хотя это и не всегда желательно) использовать переменную x для ссылки на объект строкового типа в одной строке кода и на объект целочисленного типа в другой строке кода (примечание: вывод программы выделен жирным шрифтом): x = "2" x '2' Вывод переменной x — строковое значение "2" x = int(x) x 2 Теперь вывод переменной x — целочисленное значение 2 Поскольку в Python типы связаны с объектами, а не с переменными, интерпретатор не поможет вам выявить несоответствие типов объектов. Если вы включили в программу переменную count для хранения указания на целочисленный тип объекта, Python не станет возражать, если вы присвоите этому объекту строковый тип "2". Программисты традиционной школы считают это недостатком, поскольку тем самым вы лишаетесь дополнительной проверки программного кода. Приняв во внимание эти опасения, Python дополнил синтаксис и средства, позволяющие программистам указывать требуемый тип объекта, на который ссылается переменная, а также параметры функции, возвращаемые значения и т. п. С помощью этих аннотаций типов, как их называют, различные инструменты могут выявлять любые несоответствия в типах объектов еще до начала выполнения программы. В небольших программах ошибки несоответствия типов, как правило, несложно обнаружить и исправить даже без аннотаций типов, и в любом случае средства тестирования Python позволяют избегать таких ошибок. Многие программисты Python считают, что гибкость динамической типизации с лихвой перевешивает любые преимущества, которые может дать обязательная типизация самих переменных. 1.3.3. Слабая поддержка мобильных устройств За последнее десятилетие появилось великое множество всевозможных мобильных устройств: смартфоны, планшеты, фаблеты, хромбуки и др. Новые мобильные устройства повсюду, и они работают под управлением различных операционных систем. Python не принадлежит к числу сильных игроков в этой области, однако различные проекты работают над данной проблемой, разрабатывая наборы инструментов и фреймворки, позволяющие писать приложения для платформ iOS и Android. Ситуация улучшается, но на момент написания книги писать и распространять коммерческие мобильные приложения при помощи Python все еще довольно хлопотно.
   48 Глава 1. Знакомство с Python 1.3.4. Слабая поддержка многоядерных систем В наши дни многоядерные процессоры встречаются повсюду, и во многих случаях они обеспечивают значительный прирост производительности. Однако стандартная реализация Python не рассчитана на использование нескольких ядер из-за механизма глобальной блокировки интерпретатора (Global Interpreter Lock, GIL). Как уже упоминалось в связи с быстродействием, команда разработчиков Python в настоящее время работает над тем, чтобы Python более слаженно и эффективно справлялся с многоядерными процессорами, и развитие многоядерной поддержки Python будет продолжаться в течение последующих нескольких лет. Итоги Python — современный высокоуровневый язык программирования с динамической типизацией и простым, логически ясным, последовательным синтаксисом и семантикой. Python — кросс-платформенный язык с высокой модульностью, хорошо подходящий как для быстрой разработки, так и для крупномасштабного программирования. Он достаточно быстрый, и может легко расширяться модулями C или C++ для повышения скорости. Python имеет встроенные расширенные возможности, такие как персистентное хранение объектов, продвинутые хеш-таблицы, расширяемый синтаксис классов и универсальные функции сравнения. Python включает в себя широкий набор модулей, предназначенных для обработки чисел, изображений, создания пользовательских интерфейсов и веб-скриптинга. Он поддерживается динамично развивающимся сообществом.
2 Первые шаги В этой главе: 3 Доступные опции Python 3 Начало работы в Colaboratory 3 Доступ к репозиторию GitHub для этой книги 3 Написание простой программы и обработка ошибок 3 Применение функций help() и dir() 3 Инструменты ИИ для генерации кода на Python В этой главе представлены краткий обзор различных вариантов установки и использования Python, а также пошаговые инструкции по началу работы с Google Colaboratory, веб-средой для работы с Python. В сочетании с примерами кода для каждой главы, доступными в репозитории GitHub по адресу https://github.com/ nceder/qpb4e, Colaboratory является одним из самых быстрых и простых способов начать работу с Python для того, чтобы вы смогли протестировать примеры кода из текста и выполнить все практические работы, представленные в книге. 2.1. Парадокс выбора: вам какой Python? По мере развития языка Python, особенно за последние 10 лет, количество возможных вариантов его запуска значительно возросло. После краткого обзора
   50 Глава 2. Первые шаги версий, источников, устройств, сред и инструментов экосистемы Python мы рассмотрим рекомендуемое нами решение — Google Colaboratory, а также то, как его использовать для выполнения примеров кода из этой книги и написания кода для практических работ в большинстве глав. 2.1.1. Версии Python В течение многих лет цикл выпуска версий Python был не вполне регулярным: новые версии появлялись лишь тогда, когда, по мнению основных разработчиков, накапливалось уже достаточно новых функций, чтобы счесть такой релиз целесообразным. Начиная с версии 3.1 в 2009 году и заканчивая версией 3.8 в 2018 году, новые версии Python выходили примерно каждые 18 месяцев. По мере роста популярности Python обозначилась потребность подготавливать новые версии быстрее и более регулярно. Так, начиная с версии 3.9, было решено (и закреплено в PEP 602: Annual Release Cycle for Python) перейти на ежегодный цикл выпуска и публиковать новую версию каждый октябрь. Текущий график релизов Python подразумевает, что после выхода версии 3.13 в октябре 2024 года версия 3.14 появится в октябре 2025 года и т. д. На момент подготовки этого издания Python 3.13 — это самая актуальная версия, выпущенная в октябре 2024 года. Поскольку набор функций версии 3.13 уже окончательно определен, я включила ее новые возможности и протестировала примеры кода на версиях 3.12 и 3.13. Выбор версии Python Ежегодный выпуск новой версии Python — это, безусловно, полезно для развития его функциональных возможностей, однако это не всегда удобно для тех, кому необходимо тестировать и внедрять новую версию языка. В связи с этим версии Python официально поддерживаются в течение пяти лет: первые два года предоставляются полноценные релизы, а затем три года — только исправления безопасности в виде исходного кода. И хотя, как правило, предпочтительнее работать с самой последней версией, с этой книгой можно использовать любую поддерживаемую версию (3.9 или выше на момент подготовки данного издания). Если у вас более ранняя версия, то некоторые новые возможности вам будут недоступны, однако вы все равно сможете выполнить бˆольшую часть заданий без обновления. 2.1.2. Как скачать Python Классический способ начать работу с Python — скачать и установить версию с сайта Python Sofware Foundation по адресу https://www.python.org/downloads/. Там вы найдете установочные пакеты для различных операционных систем и платформ, а также версии, восходящие к самым истокам Python, которые появились более 30 лет назад. Однако вполне возможно, что именно той версии,
   2.1. Парадокс выбора: вам какой Python? 51 что вам потребовалась, на python.org не окажется. Вы также можете скачать дистрибутив Python от Anaconda, ориентированный на data science, по адресу https://www.anaconda.com/download/, еще можно воспользоваться магазином приложений или менеджером пакетов вашей операционной системы для установки Python. Если вам интересны технические детали, вы также можете создать форк репозитория исходного кода на GitHub, а затем скачать и скомпилировать его самостоятельно. Словом, выбор зависит от ваших предпочтений и конкретных задач, и поскольку решение, рекомендуемое мной, не требует локальной установки, вы можете отложить ваш выбор до тех пор, пока не познакомитесь с Python немного поближе. 2.1.3. Устройства, платформы и операционные системы Python можно запускать на самых разных устройствах и во многих операционных системах. Если раньше нормой был сервер или настольный компьютер, то сегодня Python работает на множестве процессоров: от встраиваемых устройств и одноплатных компьютеров до телефонов и планшетов, хромбуков, ноутбуков и настольных компьютеров, виртуальных машин и контейнеров, кластеров серверов и т. п. Хотя наиболее распространенная реализация Python основана на языке C, существуют версии, написанные и работающие на Java, в браузере на базе WASM или даже в таких приложениях, как Excel. 2.1.4. Среды и инструменты Существует также несколько способов выполнения и разработки кода на Python. Можно запустить интерпретатор Python из командной строки и взаимодействовать с ним при помощи встроенной или расширенной оболочки, такой как IPython, а также писать программный код в текстовом редакторе по вашему выбору. Можно также прибегнуть к помощи интегрированных сред разработки (IDE — Integrated Development Environment), ориентированных на обучение, таких как IDLE (поставляется вместе с Python), Mu или Thonny, или же выбрать более продвинутые IDE для Python: VSCode, Wingware или PyCharm. Разумеется, во всех этих категориях много других прекрасных вариантов. Еще одно средство программирования на Python под названием Jupyter сочетает в себе возможности оболочки IPython и веб-интерфейса. Jupyter работает на сервере и доступен через браузер, а его веб-интерфейс позволяет объединять текст (отформатированный посредством языка разметки текста Markdown) с ячейками для выполнения кода в блокноте. Такое сочетание оказалось настолько удачным, что блокноты Jupyter — Jupyter Notebooks — все чаще применяются для задач обучения, исследования данных, науки о данных, экспериментов с ИИ и множества других целей. Хотя можно запустить собственный сервер Jupyter локально, гораздо удобнее пользоваться сервером, размещенным в облаке, и это подводит нас к моей рекомендации в этой книге.
   52 Глава 2. Первые шаги 2.2. Colaboratory: Jupyter Notebooks в облаке Как уже упоминалось в начале этой главы, для наших занятий я рекомендую использовать Jupyter Notebooks, в частности, хостинг-версию Jupyter от Google — Colaboratory1. С ней не нужно беспокоиться об установке (или обновлении) Python, и вы сможете воспользоваться преимуществами удобного интерфейса Jupyter, который становится все более популярным, особенно в области data science. Также плюс Colaboratory заключается в том, что одним щелчком мыши можно запустить сессию с блокнотом кода из исходного репозитория этого издания на GitHub, и он станет доступным практически на любом устройстве, оснащенном современным веб-браузером. Вот почему в этой книге я представляю примеры кода и решения задач в Jupyter Notebooks. Конечно, этот код будет работать и в других средах, и я также предоставлю текстовые файлы листингов, однако если не указано иное, то предполагается, что код приводится в виде блокнота. 2.2.1. Доступ к исходному коду блокнотов Самый простой способ начать работу с Colaboratory и этой книгой — получить доступ к блокноту в репозитории GitHub. Для тех, кто не знаком с GitHub, — это очень популярный онлайн-сервис управления версиями, основанный на инструменте управления версиями Git. Чтобы получить доступ к блокнотам в репозитории, вам не нужно знать, как пользоваться Git, и даже не нужно иметь аккаунт на GitHub — достаточно перейти по приведенным здесь ссылкам, чтобы начать работу. Репозиторий GitHub находится по адресу https://github.com/nceder/qpb4e/tree/ main; блокноты для каждой главы — в отдельных каталогах внутри каталога code. Для начала откроем блокнот для главы 2 (этой главы). Заглянув в каталог Chapter 02, вы найдете там файл блокнота под названием Chapter_02.ipynb (прямая ссылка: https://mng.bz/gaDG). Откроется страница, похожая на ту, что представлена на рис. 2.1. 2.2.2. Начало работы с Colaboratory Кликнув по файлу Chapter_02.ipynb, вы сможете просмотреть его содержимое при помощи просмотрщика GitHub (как показано на рис. 2.2). В верхней части окна кода (прямо над заголовком 2.2 Getting Started with Python and Colaboratory) вы увидите синюю кнопку-ссылку Open in Colab (Открыть в Colab). При нажатии на эту ссылку откроется блокнот в сессии Colaboratory, где можно редактировать, выполнять программный код и экспериментировать с ним, как показано на рис. 2.3. 1 В качестве AI-помощника для написания кода автор рекомендует Copilot. Вы также можете использовать Cursor — редактор со встроенным AI, поддерживающий Python и Jupyter-ноутбуки.
   2.2. Colaboratory: Jupyter Notebooks в облаке 53 Рис. 2.1. Репозиторий GitHub с открытым блокнотом Chapter_02 Рис. 2.2. Просмотр блокнота Chapter_02.ipynb на GitHub Как и в случае с GitHub, для доступа к Colaboratory вам не нужен аккаунт Google, однако для выполнения кода и использования всех функций, включая возможность сохранения сессий и файлов, необходим вход в систему с учетной записью Google.
   54 Глава 2. Первые шаги Рис. 2.3. Блокнот Chapter_02.ipynb, открытый в Colaboratory Вход в Colaboratory Также можно получить доступ к Colaboratory напрямую, открыв в браузере ссылку https://colab.research.google.com/. Откроется Colaboratory с приветственным блокнотом «Welcome to Colab» («Добро пожаловать в Colab»), как показано на рис. 2.4, в котором описаны некоторые возможности платформы. Если вы не знакомы с Jupyter Notebooks, в нижней части этого блокнота есть несколько ссылок на другие блокноты, поясняющие, как работает версия Jupyter Notebooks в Colaboratory. Если поначалу вы немного растеряетесь, эти ресурсы помогут вам. Рис. 2.4. Страница с приветственным блокнотом в Colaboratory
   2.2. Colaboratory: Jupyter Notebooks в облаке 55 Чтобы открыть файл этой книги, выберите меню File, расположенное под заголовком страницы «Welcome to Colab» в верхней левой части окна браузера, и пункт Open notebook. Чтобы проиллюстрировать, как это работает, давайте снова взглянем на блокнот для этой главы. В поле для URL-адреса GitHub введите ссылку https://github.com/nceder/qpb4e и нажмите на значок поиска в конце строки, как показано на рис. 2.5. После этого ниже отобразятся пути ко всем блокнотам, и вам останется кликнуть по файлу code/Chapter02/Chapter_02.ipynb, чтобы открыть блокнот для главы 2. Рис. 2.5. Открытие блокнота из репозитория GitHub 2.2.3. Версия Python для Colaboratory Преимущество работы в Colaboratory заключается в том, что вам не нужно беспокоиться об установке и поддержке среды Python, а Jupyter Notebooks доступны практически с любого устройства со стандартным веб-браузером. Единственный небольшой недостаток Colaboratory состоит в том, что установленная в нем версия Python может отставать от последней на несколько месяцев, и, следовательно, некоторые из новейших функций Python могут быть недоступны. Однако простота доступа и использования Colaboratory существенно перевешивает этот незначительный минус.
   56 Глава 2. Первые шаги 2.3. Написание и запуск программ в Colaboratory Jupyter Notebook состоит из двух типов ячеек: текстовых и ячеек кода. Текстовые ячейки предназначены для размещения текста, который можно форматировать при помощи языка текстовой разметки Markdown (руководство по его особенностям в Colaboratory доступно по ссылке https://mng.bz/eyjq). Если дважды кликнуть по текстовой ячейке или щелкнуть правой кнопкой мыши и выбрать пункт Edit (Редактировать), ячейка откроется в режиме редактирования, позволяющем изменять текст и его форматирование. Для сохранения изменений и возврата в текстовый режим нажмите Ctrl+Enter или щелкните на ячейке правой кнопкой мыши и выберите команду Select (Выбрать). Любые изменения, внесенные вами в исходные блокноты, не будут сохранены ни в репозитории GitHub, ни в Colaboratory, если вы не вошли в систему под учетной записью Google. Ячейки кода предназначены для исполняемого программного кода и всегда находятся в режиме редактирования. Для выполнения кода можно нажать Ctrl+Enter или щелкнуть на ячейке правой кнопкой мыши и выбрать пункт Run the Focused Cell (Запустить выделенную ячейку). Кроме того, можно кликнуть по значку в виде маленького треугольника в круге, который появляется слева от ячейки при наведении на нее мыши. При первом запуске кода из блокнота, загруженного с GitHub, отобразится соответствующее уведомление. Это стандартное предупреждение, и можно смело нажать кнопку Run Anyway (Запустить в любом случае). Если в результате выполнения программы будет получен вывод, он отобразится в области непосредственно под ячейкой. Мы рассмотрим, как все это работает, когда запустим наш первый фрагмент кода. 2.3.1. Hello, World Начнем с традиционной программы «Hello, World», которая укладывается в одну строку на Python: print("Hello, World") Hello, World Программа, введенная в ячейку кода Вывод результатов выполнения кода, ниже ячейки кода Здесь код функции print() со строкой "Hello, World" в качестве параметра уже введен в первую ячейку кода в блокноте главы 2. Для его запуска, как отмечалось выше, можно воспользоваться комбинацией клавиш Ctrl+Enter, и вывод «Hello, World» будет напечатан под ячейкой. Если вы новичок в Jupyter Notebooks (или в Python), не поленитесь немного поэкспериментировать с функцией print или другими командами в этой ячейке кода. Обратите внимание, что на протяжении всей книги я буду показывать код, который вводится в ячейку кода Jupyter (или в командную строку Python в другой среде), моноширинным шрифтом обычного начертания, а вывод — жирным моноширинным шрифтом. Для сравнения: предыдущий код и вывод должны выглядеть примерно так, как показано на рис. 2.6 в Colaboratory.
   2.3. Написание и запуск программ в Colaboratory 57 Рис. 2.6. Текстовые ячейки, ячейки кода и вывод 2.3.2. Работа с ошибками в Jupyter Ошибки — это то, с чем приходится сталкиваться всем программистам практически в каждом фрагменте написанного кода. Если при выполнении кода в ячейке кода возникает ошибка, Jupyter выводит сообщение об ошибке Python под ячейкой — в том месте, где должен отображаться вывод. Сообщения об ошибках в программном коде всегда были трудны для понимания, однако в последних версиях Python сообщения об ошибках стали более внятными и содержательными: # эта ячейка вызовет синтаксическую ошибку print("Hello, World!" В коде отсутствует закрывающая скобка Сообщение с указанием строки, File "<ipython-input-3-0d1705def0fc>", line 2 где произошла ошибка print("Hello, World!" ^ Указатель местоположения ошибки в строке SyntaxError: incomplete input Название ошибки или ее объяснение В данном случае в коде намеренно допущена синтаксическая ошибка — отсутствует закрывающая скобка. Python выводит сообщение об ошибке, где пишет номер строки, в которой она возникла, также ставит знак указателя (^) под позицией ошибки в строке и, наконец, обозначает проблему как «incomplete input» («неполный ввод»). Замечательной функцией Colaboratory является то, что под сообщением об ошибке Python может быть предложена опция автоматического исправления
   58 Глава 2. Первые шаги ошибки, сопровождаемая разделом Next Steps: (Следующие шаги) с кнопкой Fix Error (Исправить ошибку), как показано на рис. 2.7. В этом случае нажатие кнопки Fix Error внесет исправление в предыдущую ячейку кода, и пользователю останется лишь принять или отклонить его. Рис. 2.7. Сообщение об ошибке в коде с опцией ее исправления Стоит внимательно изучить предлагаемое исправление. Хотя для такой простой ошибки решение вполне корректно, однако чем сложнее код, тем больше шансов, что исправление окажется неправильным. 2.4. Применение функций help и dir для изучения Python Есть пара удобных инструментов, которые помогут вам в изучении Python. Первый — это функция help(), работающая в двух режимах. Можно выполнить help() в ячейке кода, чтобы войти в справочную систему и получить информацию о модулях, ключевых словах или темах. В справочной системе вы увидите приглашение help>, куда можно ввести имя модуля, к примеру, print или другой темы, чтобы ознакомиться с документацией Python по этому вопросу. Чтобы выйти из справочной системы, достаточно нажать клавишу Enter, не вводя никаких других данных. На рис. 2.8 показан результат, когда сначала вводится help(), а затем пользователь набирает print в поле ввода. Отображается справка по функции print, а ниже появляются новое приглашение help> и дополнительное поле для ввода. Как правило, удобнее применять функцию help() более целенаправленно. Ввод типа объекта или имени переменной в качестве параметра для help() моментально отображает документацию по этому объекту и выходит из справки: x = 2 help(x) Help on int object:
   2.4. Применение функций help и dir для изучения Python 59 class int(object) | int(x=0) -> integer | int(x, base=10) -> integer | | Convert a number or string to an integer, or return 0 if no arguments | are given. If x is a number, return x.__int__(). For floating point | numbers, this truncates towards zero. | | If x is not a number or if base is given, then x must be a string, | bytes, or bytearray instance representing an integer literal in the... (continues with the documentation for an int...) Рис. 2.8. Применение help() для функции print Такой вариант использования функции help() удобен для проверки точного синтаксиса метода или поведения объекта. Функция help() входит в модуль pydoc, который предлагает несколько способов доступа к документации, встроенной в библиотеки Python. Поскольку каждый установочный пакет Python включает полную официальную документацию, она будет у вас под рукой даже без доступа в интернет. Подробнее о доступе к документации Python см. в приложении.
   60 Глава 2. Первые шаги Другая полезная функция — dir(), она выводит список объектов в конкретном пространстве имен. При вызове без параметров она отображает текущее глобальное пространство имен, но также может перечислять объекты для модуля или даже типа: dir() ['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i2', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'quit', 'x'] dir(int) ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', ... (continues with more items... ]
   2.5. Использование инструментов ИИ для написания кода на Python 61 При помощи функции dir() можно выяснить, какие заданы методы и данные, а также освежить в памяти все компоненты, принадлежащие объекту или модулю. Наконец, функция полезна в ходе отладки, поскольку она позволяет увидеть, что и где определено. 2.5. Использование инструментов ИИ для написания кода на Python Появление инструментов, основанных на больших языковых моделях, таких как ChatGPT, Copilot и многих других, уже начинает кардинально менять наш подход к написанию программ. Теперь при помощи этих средств можно генерировать готовый код в ответ на текстовый запрос (промпт). Однако следует отметить, что хотя это и возможно, но пока не всегда надежно. Я отношусь к числу тех, кто считает, что человеческое понимание — важнейший компонент для написания хорошей программы, но вместе с тем опыт применения возможностей ИИ для генерации программного кода будет все больше и больше востребован в будущем. Учитывая это, мы прибегнем к помощи некоторых из этих средств для создания кода в ходе решения практических работ, а также обсудим и оценим решения, сгенерированные ИИ. Это даст вам представление о том, чего можно достичь, как создавать промпты и каковы потенциальные минусы. 2.5.1. Преимущества инструментов ИИ Несомненно, создание программного кода с помощью ИИ достигло такого уровня, который обладает целым рядом неоспоримых преимуществ. Во-первых, это скорость. Даже довольно объемные фрагменты кода можно сгенерировать менее чем за минуту, так что, если вы и потратите некоторое время на составление хорошего промпта для чат-бота, общее время, которое уйдет на получение достаточного объема кода, будет значительно меньше, чем у человека, способного работать даже с молниеносной скоростью. Во-вторых, сгенерированный код, как правило, не содержит тех мелких погрешностей, с которыми мы, люди, сталкиваемся при наборе текста, — чат-бот, вероятнее всего, будет грамотно писать, корректно использовать пунктуацию и т. п. Но — и это очень большое «но» — следует учитывать и ряд недостатков. 2.5.2. Недостатки инструментов ИИ Во-первых, не следует забывать о том, что чат-боты на базе ИИ расходуют огромное количество ресурсов. В наш век заботы о климате и окружающей среде многих беспокоит столь нерачительное потребление ресурсов (в частности,
   62 Глава 2. Первые шаги энергии и воды) машинами, обеспечивающими работу этих инструментов программирования. Остается лишь надеяться, что будущие разработки позволят снизить такие затраты до приемлемого для экологии уровня. Во-вторых, применение ИИ-бота в программировании подразумевает, что вы, по существу, делитесь своим кодом и используете чужой, а наиболее распространенные механизмы такого рода обмена недостаточно надежны с точки зрения защиты конфиденциальности и прав интеллектуальной собственности. В-третьих, не все генераторы программного кода на базе ИИ бесплатны, и по мере того, как компании будут принимать решения о возмещении затрат на ресурсы, потребляемые их серверами, скорее всего, лишь немногие из них останутся бесплатными для рядовых пользователей. И наконец, хотя сгенерированный программный код на редкость хорош, он все равно далек от совершенства. Ошибки и неэффективность кода, созданного ИИ, не всегда очевидны, и для их выявления может потребоваться наметанный глаз. Отправлять код, написанный ИИ, в продакшен без тщательного ревью и тестов — это как минимум не менее опасно, чем полагаться на код неопытного джуниора. Несмотря на то что ни один из этих недостатков не относится к разряду критичных, все они должны приниматься во внимание ответственными организациями и разработчиками перед тем, как принять решение об использовании в работе сгенерированного ИИ программного кода. 2.5.3. Варианты ИИ-инструментов Генератор программного кода на базе ИИ, предлагаемый в Colaboratory, является наиболее удобным, и в настоящее время он бесплатный для большинства пользователей. Для сравнения мы также обсудим решения, сгенерированные GitHub Copilot, который лучше всего работает с интегрированной средой разработки VSCode от Microsoft и локальной версией Python. На момент написания книги Copilot требует ежемесячную абонентскую плату в размере 10 долларов США. Разборы примеров кода и задачи, изложенные в данном материале, могут и не стоить таких хлопот, учитывая, что придется установить Python и VSCode на свой компьютер и оплатить подписку. Существуют и другие варианты, например Codeium, который предоставляет бесплатный тарифный план и лучше работает в связке с VSCode, чем с Colaboratory. Для написания продакшен-кода лично я предпочла бы Copilot, однако для экспериментов подходит любое из этих средств, а использование ИИ-инструмента вообще является совершенно необязательным, так как все полученные мной результаты приведены в тексте книги и в исходных блокнотах.
   Итоги 63 Итоги Python доступен для широкого спектра операционных систем, аппаратных платформ и пользовательских интерфейсов. При работе с Jupyter Notebooks, содержащими примеры кода из этой книги, для удобства использования и обслуживания рекомендуется применять Google Colaboratory. Доступ к репозиторию GitHub и Colaboratory предоставляется без учетной запи­си, но при ее наличии могут быть доступны дополнительные возможности. Jupyter Notebooks состоят из двух типов ячеек: форматируемых текстовых ячеек и ячеек для исполняемого кода. Сообщения об ошибках обычно отображаются в той же области, что и программный вывод, — непосредственно под ячейками кода. Для более глубокого изучения языка Python могут быть полезны функции help() и dir(). Для генерации программного кода на Python можно воспользоваться инструментами ИИ, и мы рассмотрим несколько таких инструментов в ходе разбора решений практических работ.
3 Краткий обзор Python В этой главе: 3 Общее описание Python 3 Использование встроенных типов данных 3 Управляющие конструкции 3 Создание модулей 3 Использование объектно-ориентированного программирования Цель этой главы — дать базовые знания о синтаксисе, семантике, возможностях и философии языка Python. Она призвана предоставить читателям исходную точку зрения или концептуальную основу, к которой будут прибавляться новые детали по мере вашего знакомства с книгой. При первом прочтении вам не нужно досконально разбирать фрагменты программного кода. Достаточно получить хотя бы общее представление о предмете описания. В последующих главах многие аспекты языка будут рассмотрены более подробно, но сейчас от вас не потребуется никаких специальных знаний. Изучив последующие главы, вы всегда можете вернуться к этой главе и проработать примеры в соответствующих разделах для закрепления материала. 3.1. Краткая характеристика Python Python содержит ряд встроенных типов данных: целые числа, числа с плавающей точкой, комплексные числа, строки, списки, кортежи, словари и файловые объекты.
   3.2. Встроенные типы данных 65 Для работы с этими типами данных применяются операторы языка, встроенные функции, библиотечные функции или собственные методы типа данных. Программисты также могут определять свои классы и создавать экземпляры этих классов. Для работы с ними используются методы, заданные программистом, а также языковые операторы и встроенные функции, для которых программист установил соответствующие специальные атрибуты методов. ПРИМЕЧАНИЕ В документации Python и в этой книге термин «объект» применяется для обозначения экземпляра любого типа данных Python, а не только того, что во многих других языках называется «экземпляром класса». Дело в том, что любой объект Python является экземпляром того или иного класса. Python обеспечивает условные и циклические управляющие конструкции в форме команд if-elif-else, циклов while и for, а также новой возможности структурного сопоставления с образцом match-case. Он позволяет определять функции с гибкими способами передачи аргументов. Исключения (ошибки) генерируются с помощью оператора raise, а для их перехвата и обработки используется конструкция try-except-else-finally. Переменные (или идентификаторы) объявлять не нужно. Они могут ссылаться на любой встроенный тип данных, определяемый пользователем объект, функцию или модуль. 3.2. Встроенные типы данных В Python поддерживаются различные встроенные типы данных: от скалярных, таких как числа и логические значения, до более сложных структур, например списков, словарей и файлов. ПРИМЕЧАНИЕ В примерах программный код набран обычным моноширинным шрифтом, а вывод — жирным моноширинным шрифтом. 3.2.1. Числа Четыре числовых типа Python — целые числа (целочисленный тип), числа с плавающей точкой (вещественный тип), комплексные числа и логические (булевы) значения: целые числа (Integers): 1, –3, 42, 355, 888888888888888, –7777777777 (размер целых чисел ограничен лишь объемом доступной памяти); числа с плавающей точкой (Floats): 3.0, 31e12, –6e-4; комплексные числа (Complex numbers): 3 + 2j, –4– 2j, 4,2 + 6,3j; логические значения (Booleans): True, False. Для работы с этими типами используются следующие арифметические операторы: + (сложение), – (вычитание), * (умножение), / (деление), // (деление с усечением до целого числа), ** (возведение в степень) и % (остаток целочисленного деления).
   66 Глава 3. Краткий обзор Python В следующих примерах используются целые числа: x = 5 + 2 - 3 * 2 x 1 5 / 2 2.5 Результат с плавающей точкой с использованием / 5 // 2 2 Результат с усечением до целого числа с использованием // 5 % 2 1 2 ** 8 256 1000000001 ** 3 1000000003000000003000000001 Размер целого числа ограничен лишь объемом доступной памяти При делении целых чисел оператором / будет получен результат с плавающей точкой, а при делении целых чисел оператором // происходит усечение до целого числа (отсечение дробной части). Следует отметить, что размер целых чисел не ограничен; они увеличиваются настолько, насколько это нужно, будучи ограничены лишь объемом доступной памяти. В следующей группе примеров используются числа с плавающей точкой, основанные на вещественных числах двойной точности языка C: x = 4.3 ** 2.4 x 33.13784737771648 3.5e30 * 2.77e45 9.695e+75 1000000001.0 ** 3 1.000000003e+27 Примеры с комплексными числами: (3+2j) ** (2+3j) (0.6817665190890336-2.1207457766159625j) x = (3+2j) * (4+9j) x Переменной x присвоено комплексное число
   3.2. Встроенные типы данных 67 (-6+35j) x.real -6.0 Комплексные числа состоят из двух частей: действительной и мнимой (снабженной суффиксом j). В приведенном выше фрагменте кода переменной x присваивается ссылка на комплексное число. Для получения действительной части используется атрибут x.real, а для получения мнимой части — x.imag. Для работы с числовыми типами могут использоваться некоторые встроенные функции. Также в вашем распоряжении библиотечный модуль cmath (функции для работы с комплексными числами) и библиотечный модуль math (функции для трех других типов): round(3.49) Функция round, вызванная с аргументом 3.49 3 import math math.ceil(3.49) Импорт библиотечного модуля math, вызов его функции ceil 4 Встроенные функции всегда доступны, а для их вызова используется стандартный синтаксис вызова функций. В приведенном выше коде функция round вызывается с числом с плавающей точкой в качестве входного аргумента. Для получения доступа к функциям библиотечных модулей используется оператор import. В примере кода выше импортируется библиотечный модуль math, а его функция ceil вызывается посредством представления атрибута: модуль. функция(аргументы). В следующих примерах используются логические значения: x = False x False not x True y = True * 2 y Логическое значение рассматривается как целое число 1 2 Если не считать представления в виде True и False, логические значения обрабатываются как числа 1 (True) и 0 (False).
   68 Глава 3. Краткий обзор Python 3.2.2. Списки В Python реализован мощный встроенный тип списка (list): [] [1] [1, 2, 3, 4, 5, 6, 7, 8, 12] [1, "два", 3, 4.0, ["a", "b"], (5,6)] Список содержит целые числа, число с плавающей точкой, строку, список и кортеж Список может содержать элементы различных типов: строки, кортежи, списки, словари, функции, файловые объекты и любые числовые типы. Список можно индексировать как с начала, так и с конца. Вы также можете обращаться к подсегменту, или срезу, списка, используя синтаксис срезов: x = ["first", "second", "third", "fourth"] x[0] 'first' x[2] Индексирование с начала с использованием положительных индексов, начиная с 0 'third' x[-1] 'fourth' x[-2] Индексирование с конца с использованием отрицательного индекса (-1 — последний элемент) 'third' x[1:-1] ['second', 'third'] x[0:3] ['first', 'second', 'third'] x[-2:-1] Срез [m:n], где m — начальный индекс, а n — конечный индекс, не включающий элемент с этим индексом ['third'] x[:3] ['first', 'second', 'third'] x[-2:] Срез от m до конца списка ['third', 'fourth'] Доступ к элементам списка может осуществляться по индексу: с начала, используя положительные индексы (где 0 соответствует первому элементу), и с конца, используя отрицательные индексы (где –1 соответствует последнему элементу). Срез [m:n] позволяет получить подпоследовательность, где m — индекс начального элемента (включительно), а n — индекс конечного элемента (не включая
69    3.2. Встроенные типы данных его) — см. табл. 3.1. Срез [:n] охватывает элементы от начала списка до n (не включительно), а срез [m:] — от m (включительно) до конца списка. Таблица 3.1. Индексы списка x= [ "first", "second", "third", "fourth" Положительные индексы 0 1 2 3 Отрицательные индексы −4 −3 −2 −1 ] Эта форма записи может использоваться для добавления, удаления и замены элементов списка, а также для получения отдельных элементов или новых спис­ ков, которые представляют собой срезы существующих списков: x = [1, 2, 3, 4, 5, 6, 7, 8, 9] x[1] = "two" x[8:9] = [] x [1, 'two', 3, 4, 5, 6, 7, 8] x[5:7] = [6.0, 6.5, 7.0] x Размер x увеличивается на 1 [1, 'two', 3, 4, 5, 6.0, 6.5, 7.0, 8] x[5:] [6.0, 6.5, 7.0, 8] Если новый срез больше или меньше заменяемого, то размер списка соответственно увеличивается или уменьшается. Также для работы со списками используются некоторые встроенные функции (len, max и min), ряд операторов (in, + и *), ключевое слово del и методы списков (append, count, extend, index, insert, pop, remove, reverse и sort): x = [1, 2, 3, 4, 5, 6, 7, 8, 9] len(x) 9 [-1, 0] + x + и * каждый создают по новому списку [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] x.reverse() x [9, 8, 7, 6, 5, 4, 3, 2, 1] Метод reverse() вызывается при помощи x.reverse() Операторы + и * создают новый список, оставляя исходный список без изменений. Для вызова методов списка используется точечная нотация, применяемая к самому списку: x.метод(аргументы).
   70 Глава 3. Краткий обзор Python Некоторые из этих операций дублируют функциональность, которая может быть реализована при помощи синтаксиса срезов, однако они позволяют улучшить читаемость программного кода. 3.2.3. Кортежи Кортежи (tuples) похожи на списки, но они являются неизменяемыми — иными словами, эти объекты невозможно изменить после создания. Операторы (in, + и *) и встроенные функции (len, max и min) работают с кортежами так же, как и со списками, поскольку они не изменяют исходный объект. Синтаксис срезов и индексов работает аналогично для получения элементов или срезов, однако он не может использоваться для добавления, удаления или замены элементов. Кроме того, у кортежей всего два метода: count и index. Одной из важных областей применения кортежей является их использование в качестве ключей для словарей. Также кортежи более эффективны в тех случаях, когда не нужна модификация. () Кортеж из одного элемента с запятой (1,) (1, 2, 3, 4, 5, 6, 7, 8, 12) (1, "two", 3L, 4.0, ["a", "b"], (5, 6)) Разные типы в одном списке Кортеж из одного элемента должен содержать запятую. Кортеж, как и список, может включать элементы различных типов: строки, кортежи, списки, словари, функции, файловые объекты и любые числовые типы. Список можно преобразовать в кортеж при помощи встроенной функции tuple: x = [1, 2, 3, 4] tuple(x) (1, 2, 3, 4) И наоборот, кортеж преобразуется в список при помощи встроенной функции list: x = (1, 2, 3, 4) list(x) [1, 2, 3, 4] 3.2.4. Строки Работа со строками (strings) — одна из сильных сторон Python. Существует множество вариантов для разграничения строк: "Строка, заключенная в двойные кавычки, может содержать 'одинарные' кавычки." 'Строка, заключенная в одинарные кавычки, может содержать "двойные" кавычки.' '''\tСтрока может начинаться с символа табуляции, а заканчиваться символом новой строки.\n''' """Строка, заключенная в тройные двойные кавычки. Только строки в тройных кавычках (одинарных либо двойных) могут содержать переводы строк без управляющих последовательностей."""
   3.2. Встроенные типы данных 71 Строки могут ограничиваться одинарными (' '), двойными (" "), тройными одинарными (''' ''') или тройными двойными (""" """) кавычками; они могут содержать символы табуляции (\t) и символы новой строки (\n). Строки также являются неизменяемыми. Операторы и функции, которые работают с ними, возвращают новые строки, полученные из исходных. Операторы (in, + и *) и встроенные функции (len, max и min) работают со строками так же, как со списками и кортежами. Синтаксис срезов и индексов работает аналогичным образом для получения элементов или срезов, но не может использоваться для добавления, удаления или замены элементов. Строки поддерживают ряд методов для работы с их содержимым; библиотечный модуль re также содержит функции для работы со строками: x = "live and x.split() let\t \tlive" ['live', 'and', 'let', 'live'] x.replace(" let \t \tlive", "enjoy life") 'live and enjoy live' Модуль re используется для замены всех пробелов import re regexpr = re.compile(r"[\t ]+") regexpr.sub(" ", x) 'live and let live' Модуль re обеспечивает работу с регулярными выражениями. Он предоставляет более сложные возможности извлечения и замены шаблонов по сравнению с модулем string. Функция print выводит строки. Другие типы данных Python можно легко преобразовать в строки и отформатировать: e = 2.718 e = 2.718 x = [1, "two", 3, 4.0, ["a", "b"], (5, 6)] x = [1, "two", 3, 4.0, ["a", "b"], (5, 6)] print("The constant e is:", e, print("The constant e is:", e, "and the list x is:", x) "and the list x is:", x) The constant The constant ➥[1, 'two', ➥[1, 'two', e is: 2.718 and the e is: 2.718 and the 3, 4.0, ['a', 'b'], 3, 4.0, ['a', 'b'], print(f"the value of e is: {e}") print(f"the value of e is: {e}") Список автоматически преобразуется Список автоматически преобразуется в строку с помощью функции print() в строку с помощью функции print() list x is: list x is: (5, 6)] f перед строкой создает (5, 6)] f перед строкой создает «f-строку» со значениями в { } «f-строку» со значениями в { } the value of e is: 2.718 the value of e is: 2.718 Объекты автоматически преобразуются в строковые представления для вывода. Оператор % предоставляет возможность форматирования, аналогичную sprintf в языке C.
   72 Глава 3. Краткий обзор Python 3.2.5. Словари Встроенный тип данных словаря (dictionary) в языке Python обеспечивает функциональность ассоциативных массивов при помощи хеш-таблиц. Встроенная функция len возвращает количество пар «ключ-значение» в словаре. Ключевое слово del используется для удаления пары «ключ-значение». Как и в случае со списками, доступны различные методы работы со словарями (clear, copy, get, items, keys, update и values): Устанавливает значение нового x = {1: "one", 2: "two"} ключа «первый» равным «один» x["first"] = "one" x[("Delorme", "Ryan", 1995)] = (1, 2, 3) Ключами могут быть числа, строки, list(x.keys()) кортежи или другие неизменяемые типы ['first', 2, 1, ('Delorme', 'Ryan', 1995)] x[1] 'one' x.get(1, "not available") 'one' x.get(4, "not available") 'not available' get возвращает «отсутствует», если ключа нет в словаре Ключи должны относиться к неизменяемому типу, как, например, числа, строки, кортежи и т. д. Значениями могут быть объекты любого типа, включая такие изменяемые типы, как списки и словари. При попытке обратиться к значению ключа, отсутствующего в словаре, возникнет ошибка KeyError. Чтобы избежать этой ошибки, при отсутствии ключа в словаре метод словаря get может возвращать значение, определяемое пользователем. 3.2.6. Множества Множество (set) в Python представляет собой неупорядоченный набор объектов, который задействуется, когда вас интересует лишь факт принадлежности объекта к некой группе и его уникальность в ней. Множество ведет себя как коллекция ключей словаря без каких-либо связанных значений: x = set([1, 2, 3, 1, 3, 5]) x {1, 2, 3, 5} Множество, созданное с помощью set() в списке Дубликаты удалены 1 in x True 4 in x False Используется для проверки принадлежности множеству
   3.2. Встроенные типы данных 73 Множество создается применением set к последовательности, например к списку. При преобразовании последовательности в множество дубликаты удаляются. Ключевое слово in используется для проверки принадлежности объекта множеству. Замороженное множество (frozenset) — это множество, которое является неизменяемым. Это означает, что после создания множества с помощью кода типа x = frozenset([1, 2, 3, 1, 3, 5]) его нельзя изменить, то есть никакие элементы не могут быть добавлены или удалены. 3.2.7. Файловые объекты В Python для доступа к файлам используются файловые объекты (file objects): f = open("myfile", "w") f.write("First line with necessary newline character\n") 44 Оператор open создает файловый объект в режиме записи («w») f.write("Second line to write to the file\n") 33 f.close() f = open("myfile", "r") line1 = f.readline() line2 = f.readline() f.close() print(line1, line2) Открывает тот же файл снова, на этот раз в режиме чтения («r») First line with necessary newline character Second line to write to the file import os directory = os.getcwd() print(directory) Модуль os предоставляет функции для работы с файлами и путями Отображаемый каталог будет зависеть от вашей операционной среды /content filename = os.path.join(directory, "myfile") print(filename) Метод join() объединяет имя дериктории и имена файлов в абсолютный путь /content/myfile f = open(filename, "r") print(f.readline()) First line with necessary newline character f.close() Оператор open создает файловый объект. В данном случае файл myfile в текущем рабочем каталоге открывается в режиме записи ("w"). После записи в этот файл двух строк и закрытия он открывается снова, на этот раз в режиме для чтения ("r"). Модуль os предоставляет несколько функций для перемещения по
   74 Глава 3. Краткий обзор Python файловой системе и работы с путями к файлам и каталогам. В данном примере вы переходите в другой каталог. Однако, используя абсолютный путь к файлу, вы все равно можете получить к нему доступ. Существует несколько других возможностей ввода-вывода. Как мы увидим позднее, встроенную функцию ввода input можно использовать для запроса и получения строки от пользователя. Библиотечный модуль sys открывает доступ к stdin, stdout и stderr. Библиотечный модуль struct предоставляет поддержку чтения и записи файлов, созданных программами на языке C или предназначенных для их использования. Библиотечный модуль Pickle обеспечивает сохранение данных благодаря возможности легко считывать и записывать типы данных Python в файлы и из них. 3.3. Аннотации типов в Python В отличие от многих языков программирования, Python изначально не использует типизированные переменные и возвращаемые значения. И хотя это делает язык более гибким и читабельным, во многих случаях тип объекта, на который ссылается переменная, который требуется в качестве параметра или возвращается функцией либо методом, не всегда очевиден с первого взгляда. Хотя случайное смешивание несовместимых типов объектов вызовет исключение во время выполнения программы в Python, это не приведет к генерации ошибки во время компиляции. Особенно в крупных проектах нередко бывает полезно обозначить информацию о типах объектов более явным образом. Для этого в Python и добавлены аннотации типов. Синтаксис аннотаций типов, позволяющий сообщить об использовании несовместимого или неожиданного типа, может быть прочитан такими средствами проверки типов, как mypy, pyright, pyre или pytype, а также рядом популярных IDE. Хотя эти инструменты могут сообщить об ошибке, сам Python не сгенерирует ошибку времени выполнения, если аннотации типов не соблюдены. Примеры аннотаций типов и более полное обсуждение их применения см. в разделе 4.4 следующей главы. 3.4. Управляющие конструкции В языке Python имеется полный набор конструкций для управления выполнением кода и программным потоком, к числу которых относятся стандартные структуры ветвления и циклы. 3.4.1. Логические значения и выражения В Python предусмотрено несколько способов выражения логических значений. Логическая константа False, 0, специальный объект None и пустые значения (к примеру, пустой список [] или пустая строка "") воспринимаются как False
   3.4. Управляющие конструкции 75 (ложь). Логическая константа True и все остальные значения интерпретируются как True (истина). Выражения сравнения можно создавать при помощи операторов сравнения (<, <=, ==, >, >=, !=, is, is not, in, not in) и логических операторов (and, not, or) — все они возвращают значение True или False. 3.4.2. Оператор if-elif-else Блок кода выполняется после первого истинного условия (в if или elif.). Если ни одно из условий не равно True, то выполняется блок кода после else: x = 5 if x < 5: y = -1 z = 5 elif x > 5: y = 1 z = 11 else: y = 0 z = 10 print(x, y, z) Необязательные блоки elif и else Для разграничения блоков в Python используются отступы Блоки elif и else необязательны, и количество блоков elif не ограничено. Для разграничения блоков используются отступы. Включения явных разделителей (таких, как квадратные или фигурные скобки) не требуется. Каждый блок состоит из одного или нескольких операторов, разделенных символами новой строки. Все эти операторы должны снабжаться отступами одного уровня. В приведенном примере будет выведен результат 5 0 10. 3.4.3. Структурное сопоставление шаблонов при помощи оператора match В языке Python, начиная с версии 3.10, появился оператор match, который обеспечивает гибкое и мощное сопоставление с шаблоном, внешне похожее на операторы switch, знакомые программистам на C++ и Java, но с более широкими и гибкими возможностями: # point – это кортеж (x, y) match point: case (0, 0): print("Начало координат") case (0, y): print(f"Y={y}") case (x, 0): print(f"X={x}") case (x, y): print(f"X={x}, Y={y}") case _: raise ValueError("Не является точкой")
   76 Глава 3. Краткий обзор Python В следующих главах мы обсудим многочисленные возможности применения оператора match. 3.4.4. Цикл while Цикл while выполняется до тех пор, пока условие (в следующем примере x > y) остается истинным (True): u, v, x, y = 0, 0, 100, 30 while x > y: u = u + y x = x—y if x < y + 2: Это блок цикла v = v + x x = 0 else: v = v + y + 2 x = x - y—2 print(f"{u=} {v=}") print(u, v) u и v присваиваются значения 0, x — 100, а y — 30 Выводит значения u и v при каждом этапе цикла В первой строке используется сокращенный синтаксис присваивания. Здесь u и v присваивается значение 0, x устанавливается равным 100, а y — 30. В этом примере вывод будет 60 40. Цикл также может содержать оператор break, который завершает цикл и переходит к блоку else, если такой существует. Также может присутствовать оператор continue, который останавливает текущую итерацию цикла и возвращает программный поток к началу блока цикла. 3.4.5. Цикл for Цикл for — простая, но мощная конструкция для перебора любого итерируемого типа, будь то список или кортеж. В отличие от реализации во многих других языках программирования, цикл for в Python перебирает все элементы определенной последовательности, что сближает его с foreach. Следующий цикл находит в списке первое вхождение целого числа, кратного 7: Переменной x item_list = [3, "string1", 23, 14.0, "string2", 49, 64, 70] присваивается каждое значение в списке for x in item_list: if not isinstance(x, int): Если x не ссылается на целое число, итерация continue останавливается командой continue if not x % 7: Если x делится на 7, print(f"found an integer divisible by seven: {x}") остаток будет равен 0 break и, следовательно, False Оператор break завершает цикл Переменной x последовательно присваивается ссылка на каждое значение из списка. Если x ссылается не на целое число, то оставшаяся часть итерации отменяется командой continue. Выполнение цикла продолжается присваиванием x
   3.4. Управляющие конструкции 77 ссылки на следующий элемент списка. После того как будет найдено подходящее целое число, цикл завершается командой break. Программа выводит: found an integer divisible by seven: 49 Как и в циклах while, break приведет к завершению цикла, и выполнение пе­ рейдет к блоку else, если он имеется, или к следующей строке после цикла. 3.4.6. Определение функции Python предоставляет гибкий механизм передачи аргументов функциям: def def funct1(x, funct1(x, y, y, z): z): value = = x x + + 2*y 2*y + + z**2 z**2 value if if value value > > 0: 0: return x x + + 2*y 2*y + + z**2 z**2 return else: else: return return 0 0 Функции определяются с помощью оператора def, аргументы — по позиции Оператор return оценивает выражение и возвращает его значение u, u, v v = = 3, 3, 4 4 funct1(u, v, 2) funct1(u, v, 2) 15 15 funct1(u, funct1(u, z=v, z=v, y=2) y=2) Здесь z и y вводятся по именам 23 23 def funct2(x, y=1, z=1): def funct2(x, y=1, z=1): return return x x + + 2 2 * * y y + + z z ** ** 2 2 funct2(3, z=4) z=4) funct2(3, Параметр функции со значениями по умолчанию 21 21 def def funct3(x, funct3(x, y=1, y=1, z=1, z=1, *tup): *tup): print((x, y, y, z) z) + + tup) tup) print((x, funct3(2) funct3(2) С помощью * можно собрать все дополнительные позиционные аргументы в кортеж (2, (2, 1, 1, 1) 1) funct3(1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9) 9) funct3(1, (1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9) 9) (1, def funct4(x, y=1, z=1, z=1, **kwargs): **kwargs): def funct4(x, y=1, print(x, y, y, z, z, kwargs) kwargs) print(x, funct4(1, funct4(1, 2, 2, m=5, m=5, n=9, n=9, z=3) z=3) При помощи ** можно собрать все дополнительные именованные аргументы в словарь 1 1 2 2 3 3 {'m': {'m': 5, 5, 'n': 'n': 9} 9} Функции определяются при помощи оператора def, а оператор return используется для возврата значения. Это значение может относиться к любому типу. Если оператор return не был обнаружен, возвращается значение None.
   78 Глава 3. Краткий обзор Python Аргументы функции можно определять как по позиции, так и по имени (ключевому слову). Также при помощи символа * можно задать специальный параметр, который при вызове функции объединяет в кортеж все дополнительные позиционные аргументы. Аналогичным образом при помощи символов ** указывается специальный параметр, который при вызове функции объединяет все дополнительные именованные аргументы в словарь. 3.4.7. Исключения Исключения (ошибки) можно перехватывать и обрабатывать с помощью составного оператора try-except-else-finally. Он также может перехватывать и обрабатывать исключения, которые вы устанавливаете и генерируете самостоятельно. Любое неперехваченное исключение приводит к завершению работы программы. В следующем фрагменте кода показана базовая обработка исключений; более сложные примеры мы рассмотрим в главе, посвященной исключениям. Тип исключения определен на основе базового типа Exception (Исключение) class EmptyFileError(Exception): Тип исключения определен на основе базового pass типа Exception (Исключение) class EmptyFileError(Exception): file_names = ["myfile1.txt", "non_existent", "empty_file", "myfile2.txt"] pass for file_name in file_names: "non_existent", "empty_file", "myfile2.txt"] file_names = ["myfile1.txt", Блок проверен на наличие IOError for try: file_name in file_names: try:file = open(file_name, 'r') или БлокEmptyFileError проверен на наличие IOError line file = = file.readline() open(file_name, 'r') Возникло Здесь может IOError иливозникнуть EmptyFileError if line == "": line = file.readline() исключение Возникло file.close() Здесь может возникнуть IOError if line == "": EmptyFileError исключение raise EmptyFileError("{file_name}: is empty") file.close() EmptyFileError except IOError as error: raise EmptyFileError("{file_name}: is empty") print(f"{file_name}: except IOError as error: could not be opened: {error.strerror}") except EmptyFileError as error: print(f"{file_name}: could not be opened: {error.strerror}") Если исключение не возникло, выполняется print(error) except EmptyFileError as error: необязательный блок else Если исключение не возникло, выполняется else: print(error) необязательный блок else print(f"{file_name}: {line}") else: Необязательный блок finally всегда finally: print(f"{file_name}: {line}") выполняется в конце print("Done processing", file_name) Необязательный блокблока finally всегда finally: print("Done processing", file_name) выполняется в конце блока В этом примере создается собственный тип исключения, наследуемый от базового типа Exception (Исключение). Если во время выполнения операторов в блоке try возникают исключения IOError или EmptyFileError, выполняется соответствующий блок except. Именно здесь может возникнуть исключение IOError. Здесь вы вызываете EmptyFileError. Блок else не является обязательным; он выполняется в том случае, если в блоке try не возникает исключений. (Обратите внимание, что в этом примере в блоках except можно было бы использовать операторы continue.) Блок finally также необязателен; он выполняется в конце блока независимо от того, возникло исключение или нет.
   3.5. Создание модуля 79 3.4.8. Обработка контекста с использованием ключевого слова with Более простой способ инкапсуляции шаблона try-except-finally основан на использовании ключевого слова with и менеджера контекста. Python определяет менеджеры контекста для таких задач, как доступ к файлам, однако и разработчик имеет возможность определить собственные менеджеры контекста. Одно из их неоспоримых достоинств заключается в том, что они могут автоматически определять (и обычно так и делают) действия по высвобождению ресурсов, которые выполняются всегда, независимо от того, возникло исключение или нет. Пример ниже демонстрирует открытие и чтение файла с использованием with и менеджера контекста: filename = "myfile.txt" with open(filename, "r") as file: for line in file: print(line) Здесь ключевое слово with создает менеджер контекста, который оборачивает функцию open и следующий за ней блок. В данном случае заранее определенное действие менеджера контекста по высвобождению ресурсов закроет файл, даже если возникло исключение; поэтому до тех пор, пока выражение в первой строке выполняется, не создавая исключения, файл всегда закрывается. Этот код эквивалентен следующему: filename = "myfile1.txt" try: file = open(filename, "r") for line in file: print(line) except Exception as e: raise e finally: file.close() 3.5. Создание модуля Вы можете без труда создавать собственные модули, которые импортируются и работают в точности так же, как модули встроенной библиотеки Python. Пример в листинге 3.1 представляет собой намеренно упрощенный модуль с одной функцией, которая предлагает пользователю ввести имя файла и подсчитывает, сколько раз слова встречаются в этом файле. Листинг 3.1. Файл wo.py Докстринг для доку- """Модуль wo. Содержит функцию: words_occur()""" ментирования модуля # Функции интерфейса Комментарий для пояснения кода def words_occur(): """words_occur() - подсчет количества вхождений слов в файле.""" # Запросить у пользователя имя файла. read возвращает все file_name = input("Enter the name of the file: ") символы в файле; split # Открыть файл, прочесть его и сохранить слова в списке. разбивает строку на file = open(file_name, 'r') пробельные символы word_list = file.read().split()
Докстринг для доку-    """Модуль wo. Содержит функцию: words_occur()""" ментирования модуля # интерфейса 80Функции Глава 3. Краткий обзорКомментарий Python для пояснения кода def words_occur(): """words_occur() - подсчет количества вхождений слов в файле.""" # Запросить у пользователя имя файла. read возвращает все file_name = input("Enter the name of the file: ") символы в файле; split # Открыть файл, прочесть его и сохранить слова в списке. разбивает строку на file = open(file_name, 'r') пробельные символы word_list = file.read().split() file.close() # Подсчитать количество вхождений каждого слова в файле. occurs_dict = {} for word in word_list: # Увеличить счетчик вхождений для данного слова. occurs_dict[word] = occurs_dict.get(word, 0) + 1 # Вывести результаты. print(f"File {file_name} has {len(word_list)} words Общая форма для запуска модуля {CA]({len(occurs_dict)} are unique)") в качестве команды с помощью print(occurs_dict) python wo.py в командной строке if __name__ == '__main__': words_occur() Строки документации, или докстрингс (docstrings), — это стандартные способы документирования модулей, функций, методов и классов. Комментарии — это все, что начинается с символа # и предназначено для пояснения конкретных деталей программного кода. Функция read возвращает строку, содержащую все символы файла, а split возвращает список слов строки, разграниченных пробельными символами. Заключительный оператор if позволяет запустить программу как скрипт, если ввести в командной строке python wo.py. Если файл расположен в текущем каталоге или в одном из каталогов, включенных в путь поиска модулей, который можно найти в sys.path, его можно импортировать с помощью оператора import так же, как любой встроенный библиотечный модуль: import wo wo.words_occur() Функция, вызываемая с помощью module.function Эта функция вызывается с использованием того же синтаксиса, который используется для функций библиотечного модуля. Заметьте: если изменить файл wo.py на диске, import не применит эти изменения в той же интерактивной сессии. В таких случаях можно воспользоваться функцией reload из модуля imp1: import imp imp.reload(wo) <module 'wo'> Для крупных проектов существуют пакеты, которые представляют собой обобщенные модули. Эта концепция позволяет с легкостью группировать модули 1 imp в Python — устаревший модуль, который сейчас уже не является частью стандартной библиотеки языка. Вместо него в новых версиях Python используется модуль importlib. — Примеч. ред.
   3.6. Объектно-ориентированное программирование 81 в каталоге или поддереве каталога, а затем импортировать и использовать для обращения к ним иерархические ссылки вида package.subpackage.module. Для этого нужно немного больше кода, чем создание файла инициализации (возможно, пустого) для каждого пакета или подпакета. 3.6. Объектно-ориентированное программирование В Python реализована полноценная поддержка объектно-ориентированного программирования (ООП). В листинге 3.2 показана заготовка простого модуля форм для графического редактора. Листинг предназначен в основном для справки, если вы уже знакомы с ООП. Выноски поясняют связь синтаксиса и семантики Python со стандартными функциями других языков. Листинг 3.2. Файл shape.py Класс, заданный ключевым """Модуль sh. Содержит классы Shape, Square и Circle""" Класс, ключевым """Модуль sh. Содержит классы Shape, Square и Circle""" словомзаданный class class Shape: словом class class Shape: """Класс Shape: содержит метод move""" Инициализатор экземпляра __init__ """Класс Shape: содержит def __init__(self, x, y):метод move""" Инициализатор экземпляра __init__ def __init__(self, x, y): self.x = x Создание и инициализация x и y self.x = x self.y = y Создание и инициализация x и y self.y = y deltaX, deltaY): def move(self, Методы, заданные с помощью def move(self, deltaX, deltaY): self.x = self.x + deltaX Методы, заданные ключевого слова defс помощью self.x = = self.y self.x + + deltaY deltaX self.y ключевого слова def self.y = self.y + deltaY class Square(Shape): class Square(Shape): """Класс Square: наследует от Shape""" pi — классовая """Класс Square: наследует от Shape""" def __init__(self, side=1, x=0, y=0): pi — классовая Класс Circle переменная, def __init__(self, side=1, x=0, y=0): Класс Circle Shape.__init__(self, x, y) переменная, наследуется доступная для Shape.__init__(self, x, y) наследуется self.side = side доступная для от класса Shape чтения всем self.side = side class Circle(Shape): от класса Shape чтения всем экземплярам class Circle(Shape): """Класс Circle: наследует от Shape и содержит метод area""" экземплярам класса """Класс Circle: наследует от Shape и содержит метод area""" pi = 3.14159 класса pi = 3.14159 def __init__(self, r=1, x=0, y=0): def Shape.__init__(self, __init__(self, r=1, x=0, y=0): x, y) Подкласс вызывает инициализатор Shape.__init__(self, x, y) self.radius = r Подкласс вызывает инициализатор своего базового класса self.radius = r def area(self): своего базового класса def """Метод area(self): area класса Circle: возвращает площадь круга.""" """Метод area класса возвращает площадь круга.""" return self.radius * Circle: self.radius * self.pi return self.radius * self.radius * self.pi def __str__(self): def __str__(self): return f"Circle of radius {self.radius} at ({self.x}, {self.y})" return f"Circle of radius {self.radius} at ({self.x}, {self.y})" Метод __str__ (используемый функцией print) Метод __str__ (используемый функцией print) Классы определяются с помощью ключевого слова class. Метод инициализации экземпляра (выполняющий роль конструктора) класса всегда называется __init__, и здесь создаются и инициализируются переменные экземпляра x и y. Методы, как и функции, определяются с помощью ключевого слова def. Первый аргумент любого метода обычно называется self. При вызове метода self присваивается экземпляру, вызвавшему метод. Класс Circle наследуется от класса Shape и похож, хотя и не в полной мере, на стандартную классовую
   82 Глава 3. Краткий обзор Python переменную. В своем инициализаторе подкласс должен явно вызывать инициализатор его же базового класса. Метод __str__ возвращает строковую версию класса и используется функцией print. Прочие специальные атрибуты-методы позволяют перегружать операторы или используются встроенными методами, такими как функция вычисления длины (len). Импорт этого файла открывает доступ к следующим классам: import shape c1 = sh.Circle() c2 = sh.Circle(5, 15, 20) print(c1) Инициализатор вызывается при создании экземпляра Circle of radius 1 at coordinates (0, 0) print(c2) Circle of radius 5 at coordinates (15, 20) Функция print неявно использует __str__ c2.area() 78.539749999999998 c2.move(5,6) print(c2) Класс Circle использует метод move родительского класса Shape Circle of radius 5 at coordinates (20, 26) Инициализатор вызывается неявно, и создается экземпляр окружности. Функция print неявным образом использует специальный метод __str__. Обратите внимание на то, что доступен метод move класса Shape (родительского по отношению к Circle). Метод вызывается с использованием синтаксиса атрибута для экземпляра объекта: object.method(). Значение первого параметра (self) задается неявно. Итоги В языке Python имеется ряд встроенных типов данных, включая целые числа, числа с плавающей точкой и комплексные числа, списки и кортежи, строки, словари, множества и замороженные множества, а также файловые объекты. Python также позволяет программисту использовать нестрогие и необязательные аннотации типов для переменных, а также для параметров функций/ методов и возвращаемых значений. В арсенале Python имеется богатый набор управляющих конструкций, в числе которых циклы for и while, структуры if-elif-else, структурное сопоставление с шаблоном, функции, исключения и обработчики контекста. Модули — это обычные файлы с кодом Python, которые можно использовать, загрузив их с помощью команды import. Python — объектно-ориентированный язык: помимо того, что встроенные типы данных являются объектами, в нем также удобно создавать пользовательские классы и наследовать их от других классов.
Часть 2 Основы В последующих главах мы рассмотрим основы языка Python. Начнем с азов построения программ, а затем перейдем к встроенным типам данных и управляющим структурам, определению функций и использованию модулей. В последней главе этой части вы узнаете, как писать автономные программы на Python, работать с файлами, обрабатывать ошибки и пользоваться классами.
4 Основы основ В этой главе: 3 Отступы и блочная структура 3 Определение комментариев 3 Назначение переменных 3 Необязательные аннотации типов 3 Оценка выражений 3 Использование общих типов данных 3 Получение пользовательского ввода 3 Основные элементы стиля Python В этой главе излагаются основы языка Python: вы узнаете, как использовать присваивание и выражения, числа и строки, как обозначать комментарии в коде и т. д. А начнем мы с обсуждения блочной структуры кода в Python, что отличает его от всех других основных языков. 4.1. Отступы и блочная структура Python отличается от большинства других языков программирования тем, что он использует пробельные символы (whitespace)1 и отступы для образования 1 Пробельный символ (whitespace) — это любой непечатаемый символ, который используется для форматирования текста. Эти символы не отображаются непосредственно, но
   4.1. Отступы и блочная структура 85 блочной структуры (то есть для определения того, какой код образует тело цикла, блок else условного оператора и т. д.). В большинстве языков для этого применяются фигурные скобки. Приведенный ниже код на языке C вычисляет факториал числа 9 и сохраняет результат в переменной r: /* Код на языке C */ int n, r; n = 9; r = 1; while (n > 0) { r *= n; n--; } Фигурные скобки ограничивают тело цикла while, то есть код, выполняемый при каждом повторении цикла. Обычно программный код оформляется с отступами, более или менее последовательно в целях наглядности, однако его можно записать и следующим образом: /* А это код C с произвольными отступами */ int n, r; n = 9; r = 1; while (n > 0) { r *= n; n--; } Такой код будет работать правильно, хотя читать его будет намного труднее. Эквивалентный код на Python выглядит так: # Код n = 9 r = 1 while r n на Python. n > 0: = r * n = n - 1 Python также поддерживает запись вида r *= n в стиле языка С Python также поддерживает оператор n –= 1 В Python не используются фигурные скобки для обозначения структуры кода; вместо них применяются отступы. Последние две строки предыдущего кода являются телом цикла while, поскольку они следуют сразу же за оператором while и имеют отступ на один уровень больше, чем он. Если бы эти строки не имели отступов, то они не стали бы частью тела цикла. Обратите внимание, что, хотя яснее и проще написать r = r * n, в Python возможна и приветствуется более короткая форма: r *= n. Для языка Python подходят оба варианта. влияют на расположение других символов, создавая визуальные отступы или разделяя элементы текста. К наиболее распространенным пробельным символам относятся: пробел (space, ` `), табуляция (tab, `\t`), перевод строки (line feed, `\n`), возврат курсора (carriage return, `\r`), перевод страницы (form feed, `\f`), вертикальная табуляция (vertical tab, `\v`). — Примеч. ред.
   86 Глава 4. Основы основ Может быть, вы не сразу привыкнете к структурированию кода с использованием отступов вместо фигурных скобок, но у этого способа есть значительные преимущества. Невозможно пропустить или поставить лишнюю фигурную скобку. Вам не придется выискивать в тексте кода внизу закрывающую фигурную скобку, соответствующую открывающей скобке выше. Визуальная структура кода отражает его логическую структуру, что позволяет с первого взгляда составить представление о его строении. Стили кодирования на Python в основном единообразны. Иными словами, у вас вряд ли возникнут проблемы из-за чужих представлений об эстетике кода. Программы других разработчиков будут в значительной степени похожи на ваши. Вероятно, вы уже придерживаетесь последовательной схемы отступов при написании кода, поэтому это не покажется вам сложным. Если вы работаете в IDLE, или в одном из популярных редакторов кода, или в IDE (такой, как Emacs, VIM, VS Code, PyCharm и др.), то отступы в строках будут расставляться автоматически. Вам всего лишь нужно удалять уровни отступов, нажав клавишу Backspace, когда блок кода закончится. Одна из вещей, которая пару раз может сбить вас с толку, прежде чем вы привыкнете, заключается в том, что интерпретатор Python возвращает сообщение об ошибке, если перед командами, которые вы вводите в ячейке Jupyter Notebook или в командной строке Python (приглашении >>>), стоит один или несколько пробелов. 4.2. Как определить комментарий Как правило, все символы, следующие за знаком # в файле Python, образуют комментарий и игнорируются интерпретатором. Очевидное исключение составляет тот случай, когда символ # является частью строки: # x x x Присвоить переменной x значение 5. = 5 = 3 # Теперь переменной x присвоено значение 3. = "# This is not a comment" Комментарии достаточно часто встречаются в программном коде Python. 4.3. Переменные и присваивания Самая распространенная команда языка Python — присваивание — очень похожа на операторы присваивания, которыми вы пользуетесь в других языках программирования. Код Python для создания переменной с именем x и присваивания1 ей значения 5 выглядит так: 1 В контексте Python нужно всегда помнить, что переменная является ссылкой, а не контейнером, поэтому содержит не само значение, а лишь указание на местоположение
   4.3. Переменные и присваивания 87 x = 5 В Python, в отличие от многих других языков, не нужно ни объявлять тип переменной, ни обозначать конец строки особым знаком (наподобие ;). Строка программы завершается переводом строки. Переменные создаются автоматически при первом присваивании. Имена переменных чувствительны к регистру и могут содержать любые алфавитно-цифровые символы, а также символы подчеркивания, но должны начинаться с буквы или символа подчеркивания (см. раздел 4.11 для более подробных указаний по стилю Python при создании имен переменных). Переменные в Python: контейнеры или ссылки? Термин «переменная» в Python немного вводит в заблуждение; точнее было бы назвать переменную «именем» или «меткой». Однако похоже, что практически все программисты рано или поздно начинают называть переменные в Python именно так. Словом, как бы они ни назывались, следует знать, как переменные в действительности работают в Python. Распространенное, хотя и неверное в отношении Python объяснение — что переменная является контейнером для хранения значения, чем-то вроде коробки. Такая аналогия справедлива для многих языков программирования (к примеру, C). С другой стороны, переменные Python на коробки вовсе не похожи, так как они не являются контейнерами для хранения значений. Переменная в Python ближе к этикетке или метке, а точнее всего назвать ее ссылкой на объект из пространства имен интерпретатора Python. На один объект может ссылаться любое количество переменных Python (и тогда на практике их могут называть ярлыками, а не ссылками), и при изменении этого объекта значение, на которое ссылаются все эти переменные, соответственно, тоже изменяется. Чтобы понять, что это означает, рассмотрим следующий простой код: a = [1, 2, 3] b = a c = b b[1] = 5 print(a, b, c) [1, 5, 3] [1, 5, 3] [1, 5, 3] Если воспринимать переменные как контейнеры, такой результат обескураживает. Как изменение содержимого одного контейнера могло привести к одновременному изменению содержимого двух других? Однако если учесть, что переменные — всего лишь ярлыки, ссылающиеся на объекты, то вполне понятно, отчего изменение объекта сразу отразилось на всех трех ярлыках, на него указывающих. объекта определенного типа, который и содержит это значение. Но в речи на практике обычно опускают эту техническую сложность и говорят проще и короче, например, так: «Присвоить переменной x значение 5», хотя технически верной была бы фраза: «Переменная х ссылается на объект целочисленного типа со значением 5». Поэтому далее в этой книге также будет употребляться фраза «присвоить переменной значение», но читатель должен помнить, что именно под этим подразумевается. — Примеч. ред.
   88 Глава 4. Основы основ Когда переменные ссылаются на константы или неизменяемые значения, это различие уже не столь очевидно: a = 1 b = a c = b b = 5 print(a, b, c) 1 5 1 Поскольку объекты, на которые они указывают, изменяться не могут, поведение переменных в данном случае соответствует обеим аналогиям. Фактически после третьей строки a, b и c ссылаются на один и тот же неизменяемый целочисленный объект со значением 1. Следующая строка b = 5 заставляет b ссылаться на новый целочисленный объект 5, но не изменяет ссылок a или c. Переменным Python может быть присвоена ссылка на любой объект, тогда как в C и многих других языках переменная может хранить значения только того типа, с которым она была объявлена. Ниже приведен абсолютно корректный код Python: x = "Hello" print(x) Hello x = 5 print(x) 5 Переменная x сначала ссылается на строковый объект "Hello", а затем на целочисленный объект 5. Безусловно, использование такой возможности может быть излишним, поскольку произвольное присваивание одному имени переменной ссылок на разные типы данных усложняет понимание кода. Новое присваивание переопределяет все предыдущие. Также в случае, если ключевое слово del удаляет переменную (но не обязательно объект, с которым она была связана), при попытке вывести значение объекта, на который ссылалась переменная, после ее удаления возникает ошибка, как если бы переменная никогда не создавалась: x = 5 print(x) 5 del x print(x) -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-2-260d38800877> in <cell line: 5>() 3
   4.4. Необязательные аннотации типов в Python 4 del x --- 5 print(x) 89 Строка, в которой возникает ошибка NameError: name 'x' is not defined Название возникшего исключения Здесь вы впервые видите трассировку выполнения (traceback), которая выводится при обнаружении ошибки, называемой исключением. Обратите внимание, что, когда мы пытаемся получить доступ к переменной после ее удаления, появляется строка из тире, за которой следует имя исключения — NameError. В трассировке выполнения присутствует маленькая стрелка ---->, указывающая на строку с ошибкой. Последняя строка поясняет обнаруженное исключение, которое в данном случае является исключением NameError, поскольку переменная x больше не определена (или недействительна) после ее удаления. Как правило, возвращается полная динамическая структура вызова существующей функции на момент возникновения ошибки. Если вы работаете в другой среде Python, то можете получить ту же информацию с небольшими отличиями. Трассировка выполнения может выглядеть примерно так: Traceback (most recent call last): File "/home/naomi/Dropbox/QPB4/code/vscode/ch04.py", line 5, in <module> print(x) ^ NameError: name 'x' is not defined Место ошибки обозначено символом ^ под проблемным участком кода Этот формат несколько проще и отличается лишь знаком указателя ^ под ошибкой в следующей строке, а в целом содержит те же самые сведения. В главе 14 этот механизм описан более подробно. Полный список возможных исключений и их причин содержится в документации стандартной библиотеки Python. Для поиска конкретных исключений (таких, как NameError) воспользуйтесь справочным указателем (индексом). 4.4. Необязательные аннотации типов в Python Несмотря на то что Python не требует указывать типы для переменных (а также для параметров функций, возвращаемых значений и т. д.), текущие версии Python предоставляют такую возможность. Как уже сообщалось в предыдущей главе, в языке Python часто не всегда сразу очевиден тип объекта, на который ссылается переменная, а также который требуется в качестве параметра или возвращается функцией или методом. Смешивание несовместимых типов объектов будет причиной возникновения исключения времени выполнения в Python, но не сгенерирует ошибку во время компиляции. Во многих случаях, особенно в крупных проектах, был бы полезен более явный доступ к типам объектов. С этой целью в Python добавлены аннотации типов.
   90 Глава 4. Основы основ Синтаксис аннотаций типов может быть прочитан инструментами проверки типов, такими как mypy, pyright, pyre или pytype, а также несколькими распространенными IDE, позволяющими дать знать об использовании несовместимого или неожиданного типа. Эти инструменты могут сообщить об ошибке, тогда как сам Python не сгенерирует ошибку во время выполнения программы, если аннотации типов не соблюдены. В качестве примера создадим простую функцию для сложения двух чисел и с помощью аннотации типов укажем, что она должна принимать и возвращать только объекты типа int: def add_ints(x: int, y: int) -> int: return x + y p: int = 2.3 z = add_ints(1, 2) w = add_ints(1.5, 2.5) Параметры и возвращаемое значение помечены как целые числа Переменная p помечена как ссылающаяся на целое число, но ей присвоено значение в виде числа с плавающей точкой Функция вызвана для двух чисел с плавающей точкой В определении функции указано, что оба параметра и возвращаемое значение будут иметь целочисленный тип int. В первой строке кода после определения функции мы указываем, что переменная p должна ссылаться на тип объекта int, но затем пытаемся присвоить ей значение типа float. После этого вызывается функция add_ints, сначала с двумя аргументами типа int, а затем с двумя аргументами типа float. При запуске этого кода Python выполнит его без каких-либо предупреждений или замечаний. Однако если сохранить данный код в файл test_types.py и запустить проверку mypy, будут отмечены три ошибки: Помечает присвоение целочисленной переменной значения числа с плавающей точкой naomi@naomi-NUC:~$ mypy test_types.py test_types.py:4: error: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment] test_types.py:8: error: Argument 1 to "add_ints" has incompatible type "float"; expected "int" [arg-type] test_types.py:8: error: Argument 2 to "add_ints" has incompatible type "float"; expected "int" [arg-type] Помечает вызов функции Found 3 errors in 1 file (checked 1 source file) с двумя аргументами типа float вместо int При запуске инструмента проверки типов mypy для этого кода выдается сообщение об ошибке при установке переменной p в значение 2.3, но не потому, что 2.3 — неподходящее значение для p, а потому, что в аннотации указано, что она должна принимать тип int. Также выявляются две ошибки во втором вызове add_ints, поскольку оба параметра относятся к типу float, тогда как функция помечена как принимающая два числа типа int. В обоих случаях Python выполнит код без каких-либо замечаний, но элементы, помеченные mypy, укажут
   4.4. Необязательные аннотации типов в Python 91 на некоторую нестыковку между намерениями и реальным применением и на то, что эта нестыковка с большой вероятностью может привести к ошибкам и в дальнейшем. 4.4.1. Когда использовать аннотации типов? Одна из причин, по которым все чаще прибегают к аннотациям типов, заключается в том, что во многих случаях они помогают избежать путаницы в понимании того, какой тип объекта ожидается. В большой кодовой базе может быть особенно досадно выискивать ошибку при возврате объекта несовместимого типа. Аннотации типов позволяют выявлять и предотвращать подобные проблемы до начала выполнения программы. Информация о типах также позволяет многим редакторам и средам разработки предупреждать о подобных ошибках и предлагать соответствующие альтернативы в процессе написания программного кода. 4.4.2. Когда не использовать аннотации типов? Несмотря на то что аннотации типов во многих ситуациях могут быть крайне полезны, есть ряд случаев, когда не стоит тратить на них время. В небольших скриптах, написанных в свободной манере, добавление аннотаций типов может не стоить времени и усилий. Стремление к полной и явной аннотации типов всего, что есть в программе, может привести к тому, что определения функций или методов будет труднее прочесть. В таком случае необходимо найти компромисс между удобочитае­ мостью и полнотой типизации. Остерегайтесь слишком увлекаться аннотациями типов. Подробное аннотирование может отнять у вас уйму времени и оказаться непродуктивным. Пересматривать и переписывать рабочий код только ради добавления аннотаций типов также может оказаться нецелесообразным. Куда разумнее оставить его в покое, пока у вас не появится веских оснований его менять. Добавление аннотаций типов можно выполнить в ходе работ по поддержке или рефакторингу программы. 4.4.3. Прогрессивная типизация К счастью, поскольку аннотации типов не являются обязательными, вам не придется добавлять их для всего сразу. Коль скоро вы решили, что это хорошая идея, то можете добавлять аннотации постепенно, начиная с тех мест, где наличие информации о типе будет наиболее полезным. На страницах этой книги мы не станем постоянно использовать аннотации типов в коротких примерах кода, однако прибегнем к ним в некоторых более развернутых примерах и ответах в практических работах.
   92 Глава 4. Основы основ 4.5. Выражения Python поддерживает арифметические и другие математические выражения, которые покажутся знакомыми большинству читателей. Следующий код вычисляет среднее арифметическое чисел 3 и 5, сохраняя результат в переменной z: x = 3 y = 5 z = (x + y) / 2 Обратите внимание, что арифметические операторы, использующие только целые числа, не всегда возвращают целое число. Хотя все значения являются целыми числами, оператор деления (начиная с Python 3) возвращает число с плавающей точкой, так что дробная часть не усекается. Если же вы хотите выполнить традиционное целочисленное деление, возвращающее округленное целое число, используйте оператор //. В выражениях действуют стандартные правила арифметического приоритета операций. Если не включить круглые скобки в последнюю строку, то код будет вычисляться в порядке x + (y / 2). Выражения не обязательно должны включать только цифровые значения; в них также могут использоваться строки, логические значения и многие другие типы объектов. Мы подробно рассмотрим эти объекты по мере их появления. Попробуйте сами: переменные и выражения Создайте несколько переменных в Jupyter Notebook. Что произойдет, если вы попробуете добавить пробелы, дефисы или другие символы, не являющиеся буквами и цифрами, в имя переменной? Поэкспериментируйте с несколькими сложными выражениями, такими как x = 2 + 4 * 5 — 6 / 3. Используйте круглые скобки для группировки чисел разными способами и посмотрите, как меняется результат по сравнению с исходным выражением без группировки. 4.6. Строки Вы уже знаете, что в Python, как и в большинстве других языков программирования, строки заключаются в двойные кавычки. Следующий пример показывает, что переменной x присвоено значение строкового типа "Hello, World": x = "Hello, World" Символ \ (обратная косая черта) используется для экранирования символов, то есть для придания им особого значения. \n — символ новой строки, \t — символ табуляции, \\ — один обычный символ обратной косой черты, а \" — обычный символ двойной кавычки (такой символ не означает конец объявления строки): x = "\tThis string starts with a \"tab\"." x = "This string contains a single backslash(\\)."
   4.7. Числа 93 Вместо двойных кавычек можно использовать одинарные. Следующие две строки делают одно и то же: x = "Hello, World" x = 'Hello, World' Единственное различие заключается в том, что не нужно экранировать (использовать обратный слеш) двойные кавычки " в строках, заключенных в одинарные кавычки ', или одинарные кавычки в строках, заключенных в двойные: x x x x = = = = "Don't need a backslash" 'Can\'t get by without a backslash' "Backslash your \" character!" 'You can leave the " alone' Обычную строку нельзя разделить на несколько строчек. Следующий код работать не будет: # При выполнении этого кода Python произойдет ОШИБКА – # нельзя записать одну строку в две строчки. x = "This is a misguided attempt to put a newline into a string without using backslash-n" File "<ipython-input-3-dc07520d5086>", line 2 x = "This is a misguided attempt to ^ SyntaxError: unterminated string literal (detected at line 2) Вместе с тем в Python поддерживаются строки в тройных кавычках, позволяющие создавать многострочные строки и включать в них одинарные и двойные кавычки без экранирования символом \: x = """ Starting and ending a string with triple " or ' characters permits embedded newlines, and the use of " and ' without backslashes """ Теперь x содержит весь текст, заключенный между символами-разграничителями """. [Вместо тройных двойных кавычек можно с тем же успехом применить тройные одинарные кавычки '''.] Возможности работы со строками в Python настолько широки, что им посвящена отдельная глава 6. 4.7. Числа Поскольку вы, вероятно, знакомы со стандартными операциями над числами из других языков программирования, в книге нет отдельной главы, описывающей числовые возможности Python. В данном разделе рассматриваются особенности работы с числами в этом языке, а все доступные функции приведены в соответствующем разделе документации. В Python поддерживаются четыре разновидности чисел: целые числа, числа с плавающей точкой, комплексные числа и логические значения.
   94 Глава 4. Основы основ Целочисленные константы записываются в стандартном формате целых чисел: 0, –11, +33, 123456 и обладают бесконечным диапазоном значений, который ограничивается только ресурсами вашего компьютера. Число с плавающей точкой может записываться либо с десятичной точкой, либо в экспоненциальном (научном) формате: 3.14, –2E-8, 2.718281828. Точность этих значений определяется архитектурой машины, но обычно она соответствует точности 64-разрядного типа double в языке C. Комплексные числа, вероятно, представляют интерес лишь для узкого круга читателей; они рассматриваются ниже в этом разделе. Логические значения принимают значения True и False, а по своему поведению идентичны 1 и 0, за исключением их строковых представлений. Арифметика Python во многом такая же, как и в языке C. Операции с двумя целыми числами дают целое число — кроме операции деления (/), которая дает число с плавающей точкой. Если использовать символ деления //, то результатом будет целое число (дробная часть усекается). Операции с числом с плавающей точкой всегда дают результат с плавающей точкой. Вот несколько примеров: 5 + 2 - 3 * 2 1 5 / 2 2.5 5 / 2.0 2.5 5 // 2 2 30000000000 Результат с плавающей точкой при обычном делении Также результат с плавающей точкой Целочисленный результат при делении с использованием // Во многих языках такое значение слишком велико для типа int 30000000000 30000000000 * 3 90000000000 30000000000 * 3.0 90000000000.0 2.0e-8 Экспоненциальная запись возвращает число с плавающей точкой 2e-08 3000000 * 3000000 9000000000000 int(200.2) Преобразует число с плавающей точкой в целое число
90000000000.0 2.0e-8 Экспоненциальная запись возвращает число с плавающей точкой 2e-08    4.7. Числа 3000000 * 3000000 9000000000000 int(200.2) 95 Преобразует число с плавающей точкой в целое число 200 int(2e2) 200 float(200) Преобразует экспоненту в целое число Преобразует целое число в число с плавающей точкой 200.0 Последние три команды иллюстрируют явное преобразование типов. Обратите внимание, что при преобразовании из float в int значение усекается и отбрасывается десятичная часть. С другой стороны, при преобразовании int в float всегда добавляется .0. У чисел в Python есть два преимущества по сравнению с C или Java: целые числа могут быть сколь угодно большими, а при делении двух целых чисел получается число с плавающей точкой. 4.7.1. Встроенные числовые функции Язык Python располагает следующими функциями для работы с числами, входящими в его ядро: abs, divmod, float, hex, int, max, min, oct, pow, round Подробности работы каждой из них см. в документации. 4.7.2. Расширенные числовые функции Более сложные функции для работы с числами — например, тригонометрические и гиперболические тригонометрические функции, а также ряд полезных констант — не встроены в Python, а предоставляются в стандартном модуле math. Модули будут более подробно рассмотрены позднее, а пока достаточно знать, что для получения доступа к функциям модуля math, о которых говорится в этом разделе, следует запустить программу Python или интерактивную сессию с помощью оператора from math import * Модуль math предоставляет следующие функции и константы: acos, asin, atan, atan2, ceil, cos, cosh, e, exp, fabs, floor, fmod, frexp, hypot, ldexp, log, log10, mod, pi, pow, sin, sinh, sqrt, tan, tanh Подробности их работы изложены в документации.
   96 Глава 4. Основы основ 4.7.3. Числовые вычисления Базовая версия Python не очень хорошо подходит для интенсивных числовых вычислений из-за ограничений скорости. Однако мощное расширение Python NumPy предоставляет чрезвычайно эффективные реализации многих сложных числовых операций. Основное внимание уделяется операциям с массивами, включая многомерные матрицы и такие дополнительные функции, как быстрое преобразование Фурье. Пакет NumPy (или ссылки на него) можно найти на сайте www.scipy.org. 4.7.4. Комплексные числа Комплексные числа создаются автоматически каждый раз, когда в программе встречается выражение вида nj, где n записывается в форме целого числа или числа с плавающей точкой языка Python. Конечно, j здесь соответствует стандартному обозначению мнимого числа, равного квадратному корню из –1, например: (3+2j) (3+2j) Обратите внимание: Python выводит полученное комплексное число в круглых скобках, чтобы показать, что выводимые данные представляют значение одного объекта: 3 + 2j - (4+4j) (-1-2j) (1+2j) * (3+4j) (-5+10j) 1j * 1j (-1+0j) Вычисление j * j дает ожидаемый ответ –1, но результат остается объектом комплексного числа Python. Комплексные числа никогда не преобразуются автоматически в эквивалентные объекты вещественного или целочисленного типа. Тем не менее вы можете легко получить доступ к их вещественным и мнимым частям при помощи функций real и imag: z = (3+5j) z.real 3.0 z.imag 5.0
   4.7. Числа 97 Обратите внимание: вещественная и мнимая части комплексного числа всегда возвращаются в формате чисел с плавающей точкой. 4.7.5. Расширенные функции комплексных чисел Функции из модуля math не применяются к комплексным числам. Причина в том, что большинство пользователей предпочитают, чтобы при извлечении квадратного корня из –1 программа выдавала ошибку, а не ответ! Однако существует модуль cmath, предоставляющий аналогичные функции для работы с комплексными числами: acos, acosh, asin, asinh, atan, atanh, cos, cosh, e, exp, log, log10, pi, sin, sinh, sqrt, tan, tanh Чтобы наглядно показать в программе, что функции являются специализированными версиями для комплексных чисел, и избежать конфликтов имен с более привычными эквивалентами, лучше всего импортировать модуль cmath с помощью import cmath а затем явно указывать пакет cmath при использовании функции: import cmath cmath.sqrt(-1) 1j Минимизация использования from <module> import * Перед вами хороший пример того, почему лучше свести к минимуму использование формы from <module> import * оператора import. Если использовать команду для импортирования сначала модуля math, а затем модуля cmath, то функции cmath заменят одноименные функции math. Кроме того, читателю вашего кода будет сложнее определить источник конкретных задействованных функций. Некоторые модули специально разработаны для использования этой формы импортирования. За подробной информацией об использовании модулей и их имен обращайтесь к главе 10. Важно помнить, что, импортировав модуль cmath, вы сможете делать с комплексными числами практически все то же, что и с другими типами чисел. Попробуйте сами: работа со строками и числами В Jupyter Notebook создайте несколько строковых и числовых переменных (целые числа, числа с плавающей точкой и комплексные числа). Поэкспериментируйте с различными операциями над ними, в том числе и между разными типами. Можно ли, например, умножить строку на целое число? Или умножить ее на число с пла-
   98 Глава 4. Основы основ вающей точкой или комплексное число? Также загрузите модуль math и опробуйте некоторые из его функций; затем загрузите модуль cmath и сделайте то же самое. Что произойдет, если после загрузки модуля cmath попытаться вызвать одну из функций для целого числа или числа с плавающей точкой? Как снова получить доступ к функциям модуля math? 4.8. Значение None Кроме стандартных типов (таких, как строки и числа), в Python существует специальный базовый тип данных, определяющий один специальный объект данных с именем None. Как подсказывает его имя, None используется для представления пустого значения. В Python он неоднократно встречается в разных обличиях. К примеру, процедура в Python представляет собой функцию, которая не возвращает явного значения, — а это означает, что по умолчанию она возвращает None. None часто используется в повседневном программировании Python в качестве заполнителя, указывающего, что значение некоторого поля структуры данных будет получено со временем, хотя в настоящее время оно еще не вычислено. Проверка присутствия None выполняется легко, потому что во всей системе Python существует только один экземпляр None (все ссылки на None относятся к одному объекту), а значение None эквивалентно только самому себе. 4.9. Получение пользовательского ввода Функция input() предназначена для получения данных от пользователя. В ее параметре передается строка запроса, которая выводится для пользователя: name = input("Name? ") Name? Jane print(name) Jane age = int(input("Age? ")) Преобразует ввод из строки в целое число Age? 28 print(age) 28 Этот способ получения входных данных довольно прост. Подвох лишь в том, что ввод поступает в виде строки, так что, если вы захотите работать с ним как с числом, придется преобразовать данные вызовом функции int() или float().
   4.11. Основные элементы стиля Python 99 Попробуйте сами: получение входных данных Поэкспериментируйте с функцией input() для получения строковых и целочисленных входных данных. Если вы используете код вроде приведенного выше, что получится, если не применять int() к вызову input() для ввода целого числа? Сможете ли вы модифицировать этот код так, чтобы он принимал число с плавающей точкой — скажем, 28.5? Что произойдет, если намеренно ввести значение неправильного типа — например, число с плавающей точкой вместо целого, строку вместо числа и наоборот? 4.10. Встроенные операторы Python предоставляет разнообразные встроенные операторы, от стандартных (+, * и т. д.) до более экзотических (например, операторы для выполнения побитового сдвига, битовых логических операций и т. п.). Большинство этих операторов наверняка знакомо вам по другим языкам программирования, поэтому я не буду подробно объяснять их. Полный список встроенных операторов Python приведен в документации. 4.11. Основные элементы стиля Python В Python относительно немного ограничений на стиль оформления программного кода, если не считать очевидного исключения — обязательного применения отступов для организации кода в блоки. Даже в этом случае величина отступов и их тип (табуляция или пробелы) не задаются жестко. Тем не менее существуют рекомендации по стилю Python, которые сформулированы в предложении по улучшению Python, или PEP (Python Enhancement Proposal) 8. Сводка этих рекомендаций приведена в приложении А и доступна по адресу www.python.org/dev/peps/pep-0008/. Подборка рекомендаций Python приведена в табл. 4.1, но, чтобы полностью усвоить стиль программирования на Python, следует периодически перечитывать PEP 8. Таблица 4.1. Рекомендации по стилю программирования Python Ситуация Рекомендация Пример Имена модулей/ пакетов Короткие, в нижнем регистре, подчеркивания только при необходимости imp, sys Имена функций В нижнем регистре, подчеркивания_для_удобочитаемости foo(), my_func() Имена переменных В нижнем регистре, подчеркивания_для_удобочитаемости my_var Имена классов КаждоеСловоСПрописнойБуквы MyClass Имена констант ВЕРХНИЙ_РЕГИСТР_С_ПОДЧЕРКИВАНИЯМИ PI, TAX_RATE Отступы Четыре пробела на уровень, без табуляций Сравнения Избегать явных сравнений с True и False if my_var: if not my_var:
   100 Глава 4. Основы основ Я настоятельно советую следовать рекомендациям PEP 8. Они тщательно подобраны и прошли проверку временем; при их применении вам и другим программистам Python будет проще понять ваш код. Быстрая проверка: стиль программирования на Python Какие из следующих имен переменных и функций не соответствуют стилю программирования на Python? Почему? bar(), varName, VERYLONGVARNAME, foobar, longvarname, foo_bar(), really_very_long_var_name Итоги Программный код Python организован при помощи уровней отступов. Комментарии к коду в Python отмечены начальным символом #. Строка в тройных кавычках в начале модуля, функции или метода называется «докстринг» (строка документации); она объясняет, какую задачу выполняет объект и/или как он себя ведет. Предусмотренные типы объектов для переменных, параметров и возвращаемых значений функций можно помечать при помощи аннотаций типов Python. В Python реализованы классические типы данных для строк и чисел, а также такие структуры данных, как списки, кортежи, множества и словари. Для получения данных от пользователя в виде строки можно воспользоваться функцией input(). В языке Python имеется несколько встроенных операторов, включая +, -, /, //, %, * и др. Предпочтительный стиль оформления программного кода приведен в рекомендациях PEP 8, включенных в онлайн-документацию по языку Python.
5 Списки, кортежи и множества В этой главе: 3 Управление списками и индексами списков 3 Изменение списков 3 Сортировка 3 Простые операции со списками 3 Работа с вложенными списками и глубокими копиями 3 Использование кортежей 3 Создание и применение множеств В этой главе рассматриваются две важнейшие разновидности последовательностей в языке Python: списки и кортежи. Поначалу списки могут напомнить вам массивы во многих других языках программирования, однако не стоит заблуждаться — списки по гибкости и мощи намного превосходят обычные массивы. Кортежи похожи на списки, которые нельзя измененять; рассматривайте их как ограниченный тип списка или как базовый тип записи. Необходимость такого ограниченного типа данных мы обсудим далее в этой главе, а также рассмотрим еще один тип коллекций (групповых структур данных) Python — множества. Множества полезны тогда, когда факт принадлежности объекта к группе важнее его позиции в ней.
   102 Глава 5. Списки, кортежи и множества Большая часть главы посвящена спискам, поскольку, освоив их, вы в значительной мере освоите и кортежи. В заключительной части главы рассматриваются различия между списками и кортежами в отношении функциональности и строения. 5.1. Списки подобны массивам Списки в Python имеют много общего с массивами в Java, C или любом другом языке; это упорядоченная коллекция объектов. Для создания списка заключите перечень элементов, разделенных запятыми, в квадратные скобки, как показано ниже: # Переменной x присваивается список из трех элементов x = [1, 2, 3] Вам не нужно объявлять список или заранее фиксировать его размер. Эта строка программы создает список, а также присваивает его переменной, причем этот список автоматически увеличивается или сокращается по мере необходимости. Массивы в Python В Python доступен модуль типизированных массивов array, который предоставляет возможность работы с массивами, основанными на базе типов данных языка C. Информацию по использованию этого модуля можно найти в документации к стандартной библиотеке Python. Я рекомендую обращаться к этому модулю только в том случае, если вам действительно необходимо повышение быстродействия. Если же ситуация требует численных вычислений, рассмотрите возможность использования NumPy, упомянутого в главе 4 (он доступен на сайте www.scipy.org). В отличие от списков во многих языках программирования, списки Python могут содержать элементы различных типов; элементом списка может быть любой объект Python. Следующий список содержит разнородные элементы: # Первый элемент - число, второй - строка, третий - другой список. x = [2, "two", [1, 2, 3]] Пожалуй, основной встроенной функцией для работы со списками является функция len, которая возвращает количество элементов в списке: x = [2, "two", [1, 2, 3]] len(x) 3 Обратите внимание: функция len не подсчитывает элементы внутреннего, вложенного списка. Быстрая проверка: len() Что вернет функция len() для каждого из следующих списков: [0]; []; [[1, 3, [4, 5], 6], 7]?
   5.2. Индексы списка 103 5.2. Индексы списка Разобравшись с тем, как работают индексы списка, вы сможете извлечь намного больше пользы из программирования на языке Python. Прошу вас внимательно прочесть весь раздел! Для извлечения элементов из списка Python используется синтаксис, сходный с синтаксисом индексирования массивов C. Как в C и многих других языках, отсчет индексов в Python начинается с 0; при запросе элемента 0 вы получаете первый элемент списка, при запросе элемента 1 — второй элемент, и т. д. Проиллюстрируем на примерах: x = ["first", "second", "third", "fourth"] x[0] 'first' x[2] 'third' Однако механизм индексирования Python заметно превосходит индексирование C по гибкости. Отрицательные индексы обозначают позиции элементов от конца списка; –1 соответствует последней позиции списка; –2 — предпоследней и т. д. Продолжим пример для того же списка x: a = x[-1] a 'fourth' x[-2] 'third' Для операций, в которых задействован только один индекс списка, обычно можно рассматривать индекс как указатель на конкретный элемент списка. Для более сложных операций индексы правильнее воспринимать как обозначения позиций между элементами. Для списка ["first", "second", "third", "fourth"] индексы можно рассматривать так, как показано в табл. 5.1. Таблица 5.1. Индексы списка x= [ "first", "second", "third", "fourth" Положительные индексы 0 1 2 3 Отрицательные индексы –4 –3 –2 –1 ] Это не так важно при извлечении одного элемента, но Python может извлекать или выполнять присваивание сразу для целой части списка — такая операция
   104 Глава 5. Списки, кортежи и множества называется срезом (slicing). Вместо того чтобы вводить list[index] для извлечения элемента после позиции index, введите list[index1:index2] для извлечения всех элементов от index1 (включительно) до index2 (не включая) в новый список. Вот несколько примеров: x = ["first", "second", "third", "fourth"] x[1:-1] ['second', 'third'] x[0:3] ['first', 'second', 'third'] x[-2:-1] ['third'] Казалось бы, если второй индекс обозначает позицию в списке, предшествующую позиции первого индекса, программа должна вернуть элементы между этими индексами в обратном порядке, однако на практике этого не происходит. Вместо этого возвращается пустой список: x[-1:2] [] При срезе списка также можно не указывать index1 или index2. Если отсутствует index1, сегмент начинается от начала списка, а если отсутствует index2, сегмент продолжается до конца списка: x[:3] ['first', 'second', 'third'] x[2:] ['third', 'fourth'] Пропуск обоих индексов создает новый список от начала и до конца исходного списка — то есть копирует его. Этот прием может пригодиться для создания копии, которую можно изменять, не затрагивая исходный список: y = x[:] y[0] = '1 st' y ['1 st', 'second', 'third', 'fourth'] x ['first', 'second', 'third', 'fourth']
   5.3. Модификация списков 105 Попробуйте сами: срезы и индексы списков Используя то, что вы знаете о функции len() и срезах списков, как бы вы объединили их, чтобы получить вторую половину списка, не зная его размера? Поэкспериментируйте в оболочке Python и убедитесь в том, что ваше решение работает. 5.3. Модификация списков Синтаксис индексирования списков позволяет не только изменять их, но и извлекать из них отдельные элементы. Укажите индекс в левой части оператора присваивания: x = [1, 2, 3, 4] x[1] = "two" x [1, 'two', 3, 4] Синтаксис срезов может использоваться и в этом случае. Команда вида lista[index1:index2] = listb заменяет все элементы списка lista между index1 и index2 элементами из списка listb. Список listb может содержать больше или меньше элементов, чем удаляется из lista; в этом случае длина списка lista изменяется. Присваивание срезу может использоваться для выполнения нескольких задач, как показано ниже: x = [1, 2, 3, 4] x[len(x):] = [5, 6, 7] x Добавляет новый список в конец исходного списка [1, 2, 3, 4, 5, 6, 7] x[:0] = [-1, 0] Добавляет новый список к началу исходного списка x [-1, 0, 1, 2, 3, 4, 5, 6, 7] x[1:-1] = [] Удаляет элементы из списка x [-1, 7] Добавление одного элемента к списку — операция настолько распространенная, что для нее имеется специальный метод append: x = [1, 2, 3] x.append("four") x [1, 2, 3, 'four'] При попытке присоединения одного списка к другому может возникнуть проблема. Список будет добавлен как один элемент основного списка:
   106 Глава 5. Списки, кортежи и множества x = [1, 2, 3, 4] y = [5, 6, 7] x.append(y) x [1, 2, 3, 4, [5, 6, 7]] Метод extend похож на метод append за тем исключением, что он предназначен для добавления элементов одного списка к другому: x = [1, 2, 3, 4] y = [5, 6, 7] x.extend(y) x [1, 2, 3, 4, 5, 6, 7] Также существует специальный метод insert, который вставляет новые элементы списка между двумя существующими элементами или в начало списка. Метод insert применяется как метод объекта списка и принимает два дополнительных аргумента. Первый дополнительный аргумент определяет индекс позиции в списке, куда будет вставлен новый элемент, а второй — сам новый элемент: x = [1, 2, 3] x.insert(2, "hello") print(x) [1, 2, 'hello', 3] x.insert(0, "start") print(x) ['start', 1, 2, 'hello', 3] Метод insert интерпретирует индексы списков так, как описано в разделе 5.2, но в большинстве случаев проще всего рассматривать list.insert(n, elem) как insert elem перед n-м элементом списка list. Метод insert создан исключительно для удобства. Все, что можно сделать c помощью метода insert, также можно сделать посредством присваивания срезу. А именно конструкция list. insert(n,elem) эквивалентна list[n:n] = [elem], где n — неотрицательное число. Использование insert несколько упрощает чтение кода, к тому же метод insert поддерживает отрицательные индексы: x = [1, 2, 3] x.insert(-1, "hello") print(x) [1, 2, 'hello', 3] Для удаления элементов списков или срезов рекомендуется использовать оператор del. Он тоже не делает ничего такого, чего нельзя было бы сделать при помощи присваивания срезу, но лучше запоминается и упрощает чтение кода:
   5.3. Модификация списков 107 x = ['a', 2, 'c', 7, 9, 11] del x[1] x ['a', 'c', 7, 9, 11] del x[:2] x [7, 9, 11] В целом команда del list[n] эквивалентна list[n:n+1] = [], тогда как команда del list[m:n] выполняет то же самое, что list[m:n] = []. Метод remove не является обратным по отношению к методу insert. Если insert вставляет элемент в указанное место, remove ищет первый экземпляр заданного значения в списке и удаляет его из списка: x = [1, 2, 3, 4, 3, 5] x.remove(3) x [1, 2, 4, 3, 5] x.remove(3) x [1, 2, 4, 5] x.remove(3) -------------------------------------------------------------------------ValueError Traceback (most recent call last) <ipython-input-9-be7b9eddb459> in <cell line: 1>() ----> 1 x.remove(3) ValueError: list.remove(x): x not in list Если метод remove не находит ничего для удаления, он генерирует ошибку. Вы можете перехватить ошибку, используя средства обработки исключений Python, или же избежать ее, проверяя наличие элементов в списке оператором in перед тем, как пытаться их удалить (см. примеры использования in в разделе 5.5.1). Метод reverse — более специализированный метод модификации списка. Он переставляет элементы списка в обратном порядке, таким образом изменяя сам список: x = [1, 3, 5, 6, 7] x.reverse() x [7, 6, 5, 3, 1]
   108 Глава 5. Списки, кортежи и множества Попробуйте сами: модификация списков Предположим, что перед вами список из десяти элементов. Как переместить три последних элемента в том же порядке из конца в начало списка? 5.4. Сортировка списков Для сортировки списков используется встроенный в Python метод sort: x = [3, 8, 4, 0, 2, 1] x.sort() x [0, 1, 2, 3, 4, 8] Метод выполняет сортировку «на месте», то есть изменяет сортируемый список. Если вы хотите отсортировать список без изменения исходного списка, возможны два варианта: либо используйте встроенную функцию sorted() (она описана в разделе 5.4.2.), либо создайте копию списка и отсортируйте ее. x = [2, 4, 1, 3] y = x[:] y.sort() y [1, 2, 3, 4] Полный срез списка создает копию этого списка Применяет метод сортировки на копии, а не на исходном списке x [2, 4, 1, 3] Обратите внимание, что здесь мы использовали синтаксис [:] для образования среза всего списка. Поскольку срез формирует новый список, это фактически создает копию всего списка, которую затем можно отсортировать с помощью метода sort(). Сортировка также работает со строками: x = ["Life", "Is", "Enchanting"] x.sort() x ['Enchanting', 'Is', 'Life'] Метод sort может отсортировать едва ли не все что угодно, потому что Python умеет сравнивать практически любые значения. Но с одной оговоркой: метод ключа, используемый по умолчанию методом sort , требует, чтобы все элементы списка имели совместимые типы. Это означает, что при использовании метода sort для списка, содержащего как числа, так и строки, возникает исключение:
   5.4. Сортировка списков 109 x = [1, 2, 'hello', 3] x.sort() -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-8-9c6228d80c69> in <cell line: 2>() 1 x = [1, 2, 'hello', 3] ----> 2 x.sort() 3 TypeError: '<' not supported between instances of 'str' and 'int' С другой стороны, можно спокойно отсортировать список списков: x = [[3, 5], [2, 9], [2, 3], [4, 1], [3, 2]] x.sort() x [[2, 3], [2, 9], [3, 2], [3, 5], [4, 1]] В соответствии со встроенными в Python правилами сравнения сложных объектов подписки сортируются сначала по возрастанию первого элемента, а затем по возрастанию второго элемента. Метод sort обладает еще большей гибкостью; он имеет необязательный параметр reverse. При reverse=True сортировка производится в обратном порядке. Также возможно задать собственную функцию, определяющую способ сортировки элементов списка. 5.4.1. Пользовательская сортировка Чтобы применить пользовательский критерий сортировки, необходимо уметь определять функции, и эта тема пока еще не рассматривалась. В этом разделе мы затронем и тот факт, что len(string) возвращает количество символов в строке. Операции со строками более подробно рассмотрены в главе 6. По умолчанию метод sort использует для определения порядка встроенные функции сравнения Python; в большинстве случаев этого достаточно. Однако иногда требуется отсортировать список способом, отличным от порядка по умолчанию. Допустим, вы хотите отсортировать список слов по количеству символов в каждом слове, а не по лексикографическому принципу сортировки, который обычно применяет Python. Для этого следует написать функцию, возвращающую значение (или ключ) для проведения сортировки, и использовать ее с методом sort. В контексте sort эта функция принимает один аргумент и возвращает ключ или значение, используемое функцией sort. Чтобы строки упорядочивались по количеству символов, ключевая функция может иметь вид:
   110 Глава 5. Списки, кортежи и множества def num_of_chars(string1): return len(string1) Эта ключевая функция элементарна — она передает методу сортировки длину строк вместо самих строк. После того как вы определите ключевую функцию, остается передать ее методу sort при помощи ключевого слова key. Поскольку функции являются объектами Python, они могут передаваться, как любые другие объекты Python. Ниже приведена небольшая программа, демонстрирующая различия между сортировкой по умолчанию и пользовательской сортировкой: def comp_num_of_chars(string1): return len(string1) word_list = ['Python', 'is', 'better', 'than', 'C'] word_list.sort() print(word_list) ['C', 'Python', 'better', 'is', 'than'] word_list = ['Python', 'is', 'better', 'than', 'C'] word_list.sort(key=compare_num_of_chars) print(word_list) ['C', 'is', 'than', 'Python', 'better'] Первый список упорядочен в лексикографическом порядке (верхний регистр предшествует нижнему), а второй список упорядочен по возрастанию количества символов в слове. Также можно использовать анонимную функцию lambda в самом вызове sort(). Это может пригодиться, если функция сортировки очень короткая (как это обычно и бывает) и используется только для сортировки (опять же, как и должно быть). Строку кода с вызовом функции comp_num_of_chars можно переписать с функцией lambda следующим образом: word_list.sort(key=lambda x: len(x)) Функция lambda определяется с помощью трех элементов: переменной для параметра, в данном случае x, двоеточия и возвращаемого значения, в данном примере len(x). В случае простой функции с простым возвращаемым значением lambda избавляет от необходимости создавать дополнительные имена функций, и можно увидеть, как работает ключ сортировки, не прибегая к поиску функции где-либо еще. Пользовательская сортировка чрезвычайно полезна, однако она может выполняться медленнее сортировки по умолчанию. Обычно расхождение минимально, и все же с самыми сложными ключевыми функциями оно становится заметным — особенно если в сортировке задействованы сотни тысяч или миллионы элементов.
   5.5. Другие операции со списками 111 В частности, пользовательской сортировки стоит избегать при необходимости упорядочить список по убыванию, а не по возрастанию. В этом случае установите параметру reverse метода sort значение True. Если по какой-либо причине это решение оказывается неприемлемым, все равно будет лучше отсортировать список обычным способом, а затем воспользоваться методом reverse для перестановки элементов полученного списка в обратном порядке. Две эти операции — стандартная сортировка и обратная перестановка — будут выполняться намного быстрее, чем пользовательская сортировка. 5.4.2. Функция sorted() У списков имеется встроенный метод сортировки, но у других итерируемых объектов Python (например, у ключей словаря) метода sort нет. В языке Python также имеется встроенная функция sorted(), которая возвращает отсортированный список для любого итерируемого типа. Функция sorted() использует те же параметры key и reverse, что и метод sort: x = (4, 3, 1, 2) y = sorted(x) y [1, 2, 3, 4] z = sorted(x, reverse=True) z [4, 3, 2, 1] Попробуйте сами: сортировка списков Имеется список, каждый элемент которого также является списком: [[1, 2, 3], [2, 1, 3], [4, 0, 1]]. Допустим, вы хотите отсортировать этот список по второму элементу каждого списка так, чтобы получить результат [[4, 0, 1], [2, 1, 3], [1, 2, 3]]. Какую функцию вы бы написали для передачи ее в качестве значения key методу sort()? 5.5. Другие операции со списками Есть и другие полезные методы для работы со списками, не относящиеся ни к какой конкретной категории. 5.5.1. Проверка принадлежности элемента списку оператором in Можно легко установить, присутствует ли некоторое значение в списке, при помощи оператора in, возвращающего логическое значение. Так же легко установить обратное посредством оператора not in:
   112 Глава 5. Списки, кортежи и множества 3 in [1, 3, 4, 5] True 3 not in [1, 3, 4, 5] False 3 in ["one", "two", "three"] False 3 not in ["one", "two", "three"] True 5.5.2. Конкатенация списков оператором + Чтобы создать новый список путем конкатенации (объединения) двух существующих списков, используйте оператор + (оператор конкатенации списков), который оставляет исходные списки без изменений: z = [1, 2, 3] + [4, 5] z [1, 2, 3, 4, 5] 5.5.3. Инициализация списков оператором * Оператор * создает список указанного вами размера, инициализированный заданным значением. Эта операция типична для больших списков, размер которых известен заранее. И хотя можно использовать оператор append для добавления элементов и автоматического расширения списка по мере необходимости, эффективнее будет использовать *, чтобы правильно настроить размер списка в начале работы программы. Список, размер которого не меняется, позволяет избежать лишней нагрузки на память при ее перераспределении: z = [None] * 4 z [None, None, None, None] При таком использовании со списками оператор * (который в данном контексте называется оператором умножения списков) дублирует заданный список указанное количество раз, а затем объединяет все копии для получения нового списка. Это стандартный метод Python для предварительного формирования списка заданного размера. В умножении списков нередко используется список, включающий один экземпляр None, впрочем, содержимое списка может быть любым: z = [3, 1] * 2 z [3, 1, 3, 1]
   5.5. Другие операции со списками 113 5.5.4. Получение наименьшего или наибольшего элемента функциями min и max Функции min и max предназначены для нахождения наименьшего и наибольшего элемента в списке. Вероятно, чаще всего вы будете использовать функции min и max с числовыми списками, но они могут применяться к спискам, содержащим элементы любых типов. Попытка определить наименьший или наибольший объект в последовательности объектов разных типов приведет к ошибке, если сравнение этих типов не имеет смысла: min([3, 7, 0, -2, 11]) -2 max([4, "Hello", [1, 2]]) -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-7-15ab1869d5d5> in <cell line: 1>() ----> 1 max([4, "Hello", [1, 2]]) TypeError: '>' not supported between instances of 'str' and 'int' 5.5.5. Поиск по списку при помощи метода index Когда требуется выяснить, в какой позиции списка находится некоторое значение (то есть недостаточно просто знать, присутствует значение в списке или нет), воспользуйтесь методом index. Этот метод ищет в списке элемент, эквивалентный заданному значению, и возвращает его позицию: x = [1, 3, "five", 7, -2] x.index(7) 3 x.index(5) -------------------------------------------------------------------------ValueError Traceback (most recent call last) <ipython-input-6-96ad5df81983> in <cell line: 1>() ----> 1 x.index(5) ValueError: 5 is not in list Как видно из этого примера, при попытке определить позицию элемента, отсутствующего в списке, генерируется ошибка. Эту ошибку можно обработать точно так же, как и аналогичную ошибку, возникающую при использовании метода remove (то есть проверив список с помощью оператора in перед применением index).
   114 Глава 5. Списки, кортежи и множества 5.5.6. Подсчет вхождений методом count Метод count также выполняет поиск заданного значения по списку, однако вместо позиционной информации возвращает количество вхождений значения в список: x = [1, 2, 2, 3, 5, 2, 5] x.count(2) 3 x.count(5) 2 x.count(4) 0 5.5.7. Краткое описание операций со списками Как видите, списки представляют собой чрезвычайно мощные структуры данных, которые по своим возможностям далеко превосходят обычные массивы. Операции со списками играют настолько важную роль в программировании на Python, что стоит для удобства привести их краткое описание, см. табл. 5.2. Таблица 5.2. Операции со списками Операция со списком Пояснение Пример [] Создает пустой список x = [] len Возвращает длину списка len(x) append Добавляет один элемент в конец списка x.append('y') extend Добавляет другой список в конец списка x.extend(['a', 'b']) insert Вставляет новый элемент в заданную позицию списка x.insert(0, 'y') del Удаляет элемент или срез списка del(x[0]) remove Ищет и удаляет заданное значение из списка x.remove('y') reverse Переставляет элементы исходного списка в обратном порядке x.reverse() sort Сортирует исходный список x.sort() + Соединяет два списка x1 + x2 * Создает список, повторяя исходный элемент заданное количество раз x = ['y'] * 3 min Возвращает наименьший элемент списка min(x) max Возвращает наибольший элемент списка max(x)
   5.6. Вложенные списки и глубокие копии Операция со списком Пояснение Пример index Возвращает позицию значения в списке x.index['y'] count Подсчитывает число вхождений значения в список x.count('y') sum Суммирует элементы списка (если они поддерживают суммирование) sum(x) in Сообщает, присутствует ли элемент в списке 'y' in x 115 Знание основных операций со списками заметно упростит вашу жизнь как программиста на Python. Быстрая проверка: операции со списками Каким будет результат len([[1,2]] * 3)? Опишите два различия между оператором in и методом списка index(). Какой из фрагментов кода создаст исключение: min(["a", "b", "c"]); max([1, 2, "три"]); [1, 2, 3].count("один")? Попробуйте сами: операции со списками Имеется список x. Напишите код безопасного удаления элемента в том — и только в том! — случае, если значение присутствует в списке. Измените код так, чтобы элемент удалялся, только если он присутствует в списке более чем в одном экземпляре. 5.6. Вложенные списки и глубокие копии В этом разделе рассматривается еще одна тема повышенной сложности, которую можно пропустить, если вы только приступили к изучению языка Python. Списки могут быть вложенными. В частности, вложение списков используется для представления двумерных матриц. К элементам этих матриц можно обращаться с помощью двумерных индексов. Индексы матриц работают следующим образом: m = [[0, 1, 2], [10, 11, 12], [20, 21, 22]] m[0] [0, 1, 2] m[0][1] 1 m[2] [20, 21, 22] m[2][2] 22
   116 Глава 5. Списки, кортежи и множества Этот механизм ожидаемым образом масштабируется до более высоких размерностей. В большинстве случаев этого достаточно. Однако у вас могут возникнуть проблемы с вложенными списками — в частности, с тем, как переменные ссылаются на объекты и каким образом некоторые объекты (такие, как списки) могут изменяться. Лучше всего проиллюстрировать это на примере: nested = [0] original = [nested, 1] original [[0], 1] Рисунок 5.1 поясняет суть происходящего. Теперь значение во вложенном списке можно изменить, используя переменные nested и original: nested[0] = 'zero' original nested original Рис. 5.1. Список, первый элемент которого ссылается на вложенный список [['zero'], 1] original[0][0] = 0 nested [0] original nested [[0], 1] Однако если присвоить nested другой список, связь между списками нарушится: nested = [2] original [[0], 1] Эта ситуация отображена на рис. 5.2. original Рис. 5.2. Первый элемент исходного списка все еще остается вложенным списком, но переменная nested ссылается на другой список Мы уже знаем, что можно получить копию списка, взяв полный срез (то есть x[:]). Также копию списка можно получить при помощи оператора + или * (например, x + [] или x * 1). Эти способы несколько уступают по эффективности методу среза. Все три способа создают так называемую поверхностную копию списка — и вероятно, в большинстве случаев вам этого будет достаточно. Но если ваш список содержит другие вложенные списки, возможно, вы предпочтете создать глубокую копию. Для этого можно воспользоваться функцией deepcopy модуля copy: original = [[0], 1] shallow = original[:]
   5.7. Кортежи 117 import copy deep = copy.deepcopy(original) Рисунок 5.3 иллюстрирует процесс глубокого копирования. Списки, на которые указывают переменные original или shallow, связаны. Изменение значения в списке nested через одну из этих двух переменных повлияет и на другую переменную: original shallow[1] = 2 shallow [[0], 2] shallow original [[0], 1] shallow[0][0] = 'zero' original [['zero'], 1] deep Рис. 5.3. Глубокие копии включают вложенные списки Глубокая копия не зависит от оригинала, и изменения в ней не отражаются на исходном списке: deep[0][0] = 5 deep [[5], 1] original [['zero'], 1] Такое же поведение присуще всем остальным вложенным объектам в изменяемых списках (например, словарях). Теперь, когда вы ознакомились с возможностями списков, пора обратиться к кортежам. Попробуйте сами: копирование списков Имеется следующий список: x = [[1, 2,3], [4, 5, 6], [7, 8, 9]]. Какой код вы бы использовали для создания копии y этого списка, элементы которой можно было бы модифицировать, не меняя содержимого x? 5.7. Кортежи Кортежи — это структуры данных, очень похожие на списки, с той лишь разницей, что их нельзя изменять; кортежи можно только создавать. Кортежи
   118 Глава 5. Списки, кортежи и множества настолько похожи на списки, что у вас даже может возникнуть вопрос, для чего вообще они были включены в Python. Дело в том, что у кортежей есть важные функции, с которыми не могут эффективно справиться списки (к примеру, функция ключей в словарях). 5.7.1. Базовые сведения о кортежах Создание кортежа практически не отличается от создания списка: переменной присваивается последовательность значений. Список представляет собой последовательность, заключенную в квадратные скобки [ и ]; кортеж представляет собой последовательность, заключенную в круглые скобки ( и ): x = ('a', 'b', 'c') Эта строка создает кортеж из трех элементов. После того как кортеж создан, операции с ним очень похожи на операции со списками — настолько, что разработчику легко забыть, что кортежи и списки являются разными типами данных: x[2] 'c' x[1:] ('b', 'c') len(x) 3 max(x) 'c' min(x) 'a' 5 in x False 5 not in x True Напомню: главное различие между кортежами и списками заключается в том, что кортежи неизменяемы. Попытка изменить кортеж приводит к выдаче несколько туманного сообщения об ошибке. Так Python пытается объяснить, что не знает, как задать значение элемента кортежа:
   5.7. Кортежи 119 x[2] = 'd' -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-2-dcc4b983047e> in <cell line: 1>() ----> 1 x[2] = 'd' TypeError: 'tuple' object does not support item assignment Кортежи можно создавать на основе существующих кортежей при помощи операторов + и *: x + x ('a', 'b', 'c', 'a', 'b', 'c') 2 * x ('a', 'b', 'c', 'a', 'b', 'c') «Копию» (а в действительности псевдоним, указывающий на исходный объект) кортежа можно создать любым из способов, применимых для этой цели к спискам: x[:] ('a', 'b', 'c') x * 1 ('a', 'b', 'c') x + () ('a', 'b', 'c') Сами кортежи изменяться не могут, но если они содержат изменяемые объекты (например, списки или словари), то такие объекты могут изменяться, если они все еще присвоены своим собственным переменным. Кортежи, содержащие изменяемые объекты, не могут использоваться в качестве ключей в словарях. Копирование кортежей Поскольку кортежи неизменяемы, приведенные здесь методы поверхностного копирования возвращают не новые объекты, а псевдонимы, указывающие на тот же объект. Это справедливо не только для кортежей, но и для строковых и байтовых объектов, которые рассматриваются в следующей главе. На практике все равно можно считать эти псевдонимы реальными «копиями», поскольку нет таких операций, которые можно было бы выполнить с настоящей копией, но нельзя с псевдонимом (при этом псевдонимы работают куда быстрее и эффективнее используют память).
   120 Глава 5. Списки, кортежи и множества 5.7.2. Одноэлементные кортежи должны содержать запятую С использованием кортежей связана одна синтаксическая тонкость. Так как квадратные скобки, в которые заключаются списки, в Python больше нигде не применяются, понятно, что [] обозначает пустой список, а [1] — список из одного элемента. О круглых скобках, в которые заключаются кортежи, этого сказать нельзя. Круглые скобки также могут использоваться для группировки элементов выражений с целью обеспечения определенного порядка вычисления. Допустим, в программе Python встречается запись (x + y) — что это должно означать? Что x и y нужно сложить и поместить в кортеж из одного элемента или же что x и y следует сложить до обработки любых других частей выражения? Эта ситуация создает проблемы только для кортежей из одного элемента, поскольку кортежи с несколькими элементами всегда содержат запятые, которыми разделяются элементы. По наличию запятых Python понимает, что круглые скобки обозначают кортеж, а не способ группировки. Для устранения неоднозначности в случае одноэлементного кортежа Python требует, чтобы за элементом кортежа следовала запятая. В случае кортежа с нулем элементов (пустым кортежем) проблем нет. Пустая пара круглых скобок должна быть кортежем, потому что в противном случае она не имеет смысла: x = 3 y = 4 (x + y) # Эта строка суммирует x и y. 7 (x + y,) # Наличие запятой показывает, что круглые скобки обозначают кортеж. (7,) () # Пустая пара скобок создает пустой кортеж. () 5.7.3. Упаковка и распаковка кортежей Для удобства Python позволяет размещать кортежи переменных в левой части оператора присваивания. В этом случае переменным из кортежа присваиваются соответствующие значения из кортежа в правой части оператора. Простой пример: (one, two, three, four) = one 1 two 2 (1, 2, 3, 4)
   5.7. Кортежи 121 Этот пример можно записать еще проще, потому что Python распознает кортежи в контексте присваивания даже без круглых скобок. Значения в правой части упаковываются в кортеж, а затем распаковываются в переменные из левой части: one, two, three, four = 1, 2, 3, 4 Одна строка кода заменила целых четыре строки: one = 1 two = 2 three = 3 four = 4 Это удобный способ для того, чтобы поменять местами значения переменных. Вместо того чтобы использовать запись temp = var1 var1 = var2 var2 = temp достаточно написать var1, var2 = var2, var1 Для еще большего удобства в Python 3 появилась расширенная функция распаковки, которая позволяет элементу с пометкой * поглощать любое количество элементов, не соответствующих остальным (то есть которые не были распределены по другим переменным). И снова поясним на примерах: x = (1, 2, 3, 4) a, b, *c = x a, b, c (1, 2, [3, 4]) a, *b, c = x a, b, c (1, [2, 3], 4) *a, b, c = x a, b, c ([1, 2], 3, 4) a, b, c, d, *e = x a, b, c, d, e (1, 2, 3, 4, []) Обратите внимание: все лишние элементы сохраняются в элементе со звездочкой в виде списка, а при отсутствии таковых элементу со звездочкой присваивается пустой список.
   122 Глава 5. Списки, кортежи и множества Упаковка и распаковка также могут выполняться с помощью списков: [a, b] [c, d] [e, f] (g, h) i, j = k, l = a = [1, 2] = 3, 4 = (5, 6) = 7, 8 [9, 10] (11, 12) 1 [b, c, d] [2, 3, 4] (e, f, g) (5, 6, 7) h, i, j, k, l (8, 9, 10, 11, 12) 5.7.4. Преобразования между списками и кортежами Кортежи легко преобразуются в списки с помощью функции list, которая принимает в качестве аргумента любую последовательность и создает новый список с теми же элементами, что и в исходной последовательности. Аналогичным образом список можно преобразовать в кортеж с помощью функции tuple, которая делает то же самое, но создает новый кортеж вместо нового списка: list((1, 2, 3, 4)) [1, 2, 3, 4] tuple([1, 2, 3, 4]) (1, 2, 3, 4) Быстрая проверка: кортежи Объясните, почему следующие операции недопустимы для кортежа x = (1, 2, 3, 4): x.append(1) x[1] = "hello" del x[2] Если у вас имеется кортеж x = (3, 1, 4, 2), как можно получить отсортированную версию x? Попутно отметим, что функция list предоставляет удобный способ разбиения строк на символы:
   5.8. Множества 123 list("Hello") ['H', 'e', 'l', 'l', 'o'] Этот прием работает, поскольку функция list (и tuple) может применяться к любой последовательности Python, а строка является обычной последовательностью символов. (Строки подробно рассматриваются в главе 6.) 5.8. Множества Множество (set) в Python представляет собой неупорядоченный набор объектов, который задействуется, когда вас интересует лишь факт принадлежности объекта к некой группе и уникальность ее членов. Как и ключи словарей (которые мы рассмотрим в главе 7), элементы множества должны быть неизменяемыми и хешируемыми. Это означает, что целые числа, числа с плавающей точкой, строки и кортежи могут быть элементами множества, а списки, словари и сами множества — нет. 5.8.1. Операции с множествами Помимо действий, применимых ко всем коллекциям элементов в целом (к примеру, in, len и итерация в циклах for), множества поддерживают ряд специфических операций: x = set([1, 2, 3, 1, 3, 5]) x {1, 2, 3, 5} x.add(6) x Создает множество вызовом set для списка Множество создано, дубликаты удалены Метод add для добавления элементов {1, 2, 3, 5, 6} x.remove(5) x {1, 2, 3, 6} 1 in x Метод remove для удаления элементов Ключевое слово in проверяет наличие объекта во множестве True 4 in x False y = set([1, 7, 8, 9]) x | y {1, 2, 3, 6, 7, 8, 9} x & y {1} x ^ y Оператор | позволяет получить объединение, или комбинацию, двух множеств Оператор & позволяет получить их пересечение Оператор ^ ищет элементы, имеющиеся в одном из двух множеств, но не в обоих
4 in x False    Оператор | позволяет получить объединение, y = set([1, 7, 5. 8,Списки, 9]) 124 Глава кортежи и множества или комбинацию, двух множеств x | y {1, 2, 3, 6, 7, 8, 9} x & y {1} x ^ y Оператор & позволяет получить их пересечение Оператор ^ ищет элементы, имеющиеся в одном из двух множеств, но не в обоих {2, 3, 6, 7, 8, 9} Множество можно создать вызовом set для последовательности — например, для списка. При преобразовании последовательности во множество дубликаты удаляются. После создания множества функцией set для изменения элементов множества применимы методы add и remove. Ключевое слово in используется для проверки принадлежности объекта множеству. Оператор | позволяет получить объединение, или комбинацию, двух множеств, оператор & — их пересечение, а оператор ^ — их симметрическую разность (то есть элементы, входящие лишь в одно из двух множеств). Данные примеры не являются полным перечнем операций со множествами, однако они дают достаточное представление для работы с ними. Дополнительные сведения см. в официальной документации Python. 5.8.2. Замороженные множества Поскольку множества не являются неизменяемыми и хешируемыми, они не могут быть элементами других множеств. Для решения этой проблемы в Python реализована еще одна разновидность множества, замороженное множество (frozenset), с тем единственным отличием, что его нельзя изменить после создания. Поскольку замороженные множества неизменяемы и хешируемы, они могут быть элементами других множеств: x = set([1, 2, 3, 1, 3, 5]) z = frozenset(x) z frozenset({1, 2, 3, 5}) z.add(6) -------------------------------------------------------------------------AttributeError Traceback (most recent call last) <ipython-input-4-60917eecf5a3> in <cell line: 1>() ----> 1 z.add(6) AttributeError: 'frozenset' object has no attribute 'add' x.add(z) x {1, 2, 3, 5, frozenset({1, 2, 3, 5})}
   5.9. Практическая работа: изучение списка 125 Быстрая проверка: множества Если создать множество из следующего списка, то сколько элементов оно будет содержать? [1, 2, 5, 1, 0, 2, 3, 1, 1, (1, 2, 3)] 5.9. Практическая работа: изучение списка В этой практической работе ваша задача — считать из файла набор температурных данных (ежемесячные температурные максимумы аэропорта Хитроу с 1948 по 2016 год), а затем вычислить некоторые статистические показатели: максимальную и минимальную температуру, среднюю и медианную температуру (которая займет центральную позицию при сортировке температур). Температурные данные находятся в файле lab_05.txt в каталоге исходного кода для этой главы. Поскольку чтение файлов еще не рассматривалось, приведу готовый код чтения файлов в список: temperatures = [] with open('lab_05.txt') as infile: for row in infile: temperatures.append(int(row.strip()) Определите самое высокое, самое низкое, среднее и медианное значения температуры. Вероятно, для этого вам пригодятся функции/методы min(), max(), sum(), len() и sort(). Дополнительное задание: установите, сколько уникальных (неповторяющихся) температурных отметок содержится в списке. 5.9.1. Зачем решать эту задачу устаревшим способом? Попытайтесь решить эту задачу с опорой на ваши знания и материал, представленный в этой главе. Приведенный выше код открывает файл и считывает его содержимое в виде списка чисел с плавающей точкой. Затем вы можете применить упомянутые функции, чтобы получить необходимые ответы. А в качестве дополнительного задания нужно придумать, как преобразовать список таким образом, чтобы в нем остались лишь уникальные значения. Вы можете подумать: «К чему мне писать этот код? Разве нельзя применить ИИ, чтобы сгенерировать готовое решение?» Ответ: можно. И все же попробуйте сначала решить задачу самостоятельно — это позволит вам и лучше понять саму проблему, и ближе познакомиться с работой Python. И то и другое крайне важно для работы с ИИ-генератором кода — вам нужно хорошо уяснить задачу, чтобы создать эффективный запрос для искусственного интеллекта, тогда как четкое представление о работе Python необходимо для оценки сгенерированного кода. Если для этой главы вы воспользуетесь Jupyter Notebook, то обнаружите там ячейку с кодом для загрузки файла, куда можно вставить ваш код для нахождения
   126 Глава 5. Списки, кортежи и множества среднего значения, медианы и других температурных отметок. Далее мы обсудим пример решения, созданного человеком (мной), и сравним его с решением, сгенерированным ИИ. 5.9.2. Решение задачи при помощи кода, сгенерированного ИИ Как уже отмечалось в главе 2, существует ряд средств ИИ для создания программного кода, и они стремительно развиваются, так что не исключено, что на момент прочтения этой книги вы предпочтете другой вариант, отличный от предложенных здесь. Для наглядности я прибегну к помощи генератора кода в Google Colaboratory, который в настоящее время предоставляется бесплатно, и GitHub Copilot, доступный по подписке с бесплатной пробной версией и работающий в среде разработки VS Code от Microsoft. Я расскажу об одном из решений и отмечу, есть ли что-то кардинально отличающееся в другой системе. Создание промпта Для создания программного кода в Colaboratory нужно нажать на ссылку generate (сгенерировать), отображенную в пустой ячейке: Start coding or generate with AI (Начать кодирование или сгенерировать с помощью ИИ). После этого откроется диалоговое окно генерации кода, ожидающее ввода промпта с объяснением генератору кода, что мы хотим получить. После ввода запроса в поле после кнопки Using… (как показано на следующем рисунке) можно нажать кнопку Generate, чтобы сформировать наш код. Определи, сколько уникальных температур содержится в списке Промпт для генерации программного кода в Colaboratory Создание промпта — это развивающееся искусство, но, поскольку поле промпта ограничено, мы не можем просто скопировать и вставить полную формулировку задачи. У нас уже есть программа для считывания из файла в список набора чисел с плавающей точкой, и промпт должен фокусироваться лишь на том, какую задачу нужно выполнить с помощью кода. Опираясь на это руководство, попробуйте создать промпт и сгенерировать программный код. Попытайтесь самостоятельно оценить полученный код и, при необходимости, доработать ваш запрос.
   5.9. Практическая работа: изучение списка 127 5.9.3. Решения и обсуждение Для создания нашего промпта мы сосредоточились на том, какую именно задачу должен выполнять код. Вот перечень необходимых действий. 1. Использовать список температур. 2. Вычислить максимальную и минимальную температуру. 3. Найти среднее значение и медиану. 4. Установить, сколько уникальных температурных отметок содержится в списке (да, давайте выполним и дополнительное задание). Все это можно представить в виде следующего промпта: Используя список температур, нужно найти самую высокую, самую низкую, среднюю и медианную температуры, а также показать, сколько уникальных температурных значений содержится в списке. Такой промпт является полным, лаконичным и включает все, что нужно. Помните, что у нас уже есть код для загрузки файла, поэтому мы не включили его в запрос, но, разумеется, нам нужно его добавить в сгенерированную программу: temperatures = [] with open('lab_05.txt') as infile: for row in infile: temperatures.append(float(row.strip())) Программа, созданная человеком У меня получилось следующее: max_temp = max(temperatures) min_temp = min(temperatures) mean_temp = sum(temperatures)/len(temperatures) Сортирует список # Теперь нужно выполнить сортировку, чтобы получить медианное температур, изменяя значение температуры его напрямую temperatures.sort() median_temp = temperatures[len(temperatures)//2] print(f"max = {max_temp}") Получает значение из средней print(f"min = {min_temp}") точки списка или на одно выше print(f"mean = {mean_temp}") середины при четном числе print(f"median = {median_temp}") элементов # Дополнительное задание unique_temps = len(set(temperatures)) print(f"number of uniquie temps - {unique_temps}") Эта программа выполняет задачу и отражает мою любовь к простоте и минимальному количеству комментариев.
   128 Глава 5. Списки, кортежи и множества Программное решение, созданное ИИ Программный код, сгенерированный ИИ, удивительно похож на предложенный мной, возможно, в силу того, что задача не слишком сложная. Используя Copilot (код, созданный Colaboratory, практически идентичен), мы получаем: # Высокая температура high_temp = max(temperatures) # Низкая температура low_temp = min(temperatures) # Средняя температура mean_temp = sum(temperatures) / len(temperatures) # Медианная температура sorted_temps = sorted(temperatures) mid = len(sorted_temps) // 2 if len(sorted_temps) % 2 == 0: median_temp = (sorted_temps[mid - 1] + sorted_temps[mid]) / 2 else: median_temp = sorted_temps[mid] Создает отсортированную копию списка Проверяет число элементов списка на четность Экстраполирует медиану из двух значений # Уникальные температуры unique_temps = len(set(temperatures)) print("High temperature:", high_temp) print("Low temperature:", low_temp) print("Mean temperature:", mean_temp) print("Median temperature:", median_temp) print("Unique temperatures:", unique_temps) Этот код также справляется со своей задачей, но с более удобным форматированием и большим количеством комментариев. В варианте ИИ содержится больше строк, что облегчает чтение кода. Он также изобилует комментариями — мне кажется, их здесь даже больше, чем необходимо. В Python хорошим тоном считается комментировать только там, где это требуется, чтобы объяснить, почему что-то происходит именно таким образом, а не иначе, и, по-моему, комментарий # High temperature прямо перед high_temp явно лишний. С другой стороны, комментарий в варианте, созданном человеком, # Теперь нужно выполнить сортировку, чтобы получить медианное значение температуры, объясняет, зачем мы прибегли к сортировке, чего не хватает в версии, сгенерированной ИИ. Впрочем, считайте это делом вкуса. Помимо некоторой разницы в выводе результатов, основное различие между двумя вариантами заключается в нахождении медианы. Поскольку медиана — это значение, находящееся ровно посередине ряда отсортированных значений, версия программиста сортирует список, изменяя его напрямую, и просто выбирает значение по индексу len(temperatures) // 2. Версия ИИ
   Итоги 129 создает отсортированную копию списка при помощи функции sorted, а затем, если список содержит четное количество элементов и это означает, что точной середины списка нет, она экстраполирует значение медианы. Может показаться, что версия, сгенерированная ИИ, лучше: в ней больше кода, она сохраняет исходный список и применяет более сложный подход к определению медианы. Однако на практике все не так однозначно. Если бы список температур был длиной в несколько миллионов строк, то создание отсортированной копии привело бы к неэффективному расходу памяти и вполне могло оказаться, что нет необходимости сохранять исходную версию списка. Также возможно, что нам нужна не экстраполированная медиана, а фактическое значение в списке, или неважно, взято ли медианное значение из ячейки на одну позицию правее его середины. В таком случае более простой код был бы предпочтительнее. Главный вывод из данного примера заключается в том, что, хотя инструменты ИИ могут создавать эффектный на вид рабочий код, при его анализе важно помнить как о поставленной задаче, так и о том, какие операции выполняет программа для ее решения. Итоги Списки и кортежи — это структуры, воплощающие идею последовательности элементов, как и строки. Списки похожи на массивы в других языках программирования, но с поддержкой автоматического изменения размеров, синтаксиса срезов и множеством удобных функций. Кортежи подобны спискам, однако их нельзя изменить, поэтому они расходуют меньше памяти и могут быть задействованы в качестве ключей словаря (см. главу 7). Множества представляют собой итерируемые наборы элементов, но они не упорядочены и не могут содержать повторяющиеся элементы. Замороженные множества — это множества, которые нельзя изменить. Инструменты ИИ могут генерировать полезный программный код, однако важно оценивать его в свете поставленной задачи и того, как работает Python.
6 Строки В этой главе: 3 Понятие о строке как о последовательности символов 3 Основные операции со строками 3 Вставка специальных символов и экранирующих последовательностей 3 Преобразование объектов в строки 3 Форматирование строк 3 Байтовый тип Работа с текстом — от пользовательского ввода и имен файлов до обработки фрагментов текста — относится к числу повседневных задач программирования. В языке Python реализованы мощные инструменты обработки и форматирования текста. В этой главе рассматриваются основные и вспомогательные операции со строками в Python. 6.1. Строки как последовательности символов В контексте извлечения символов и подстрок строки могут рассматриваться как последовательности символов, это означает, что с ними можно использовать синтаксис срезов или индексов:
   6.2. Основные операции со строками 131 x = "Hello" x[0] 'H' x[-1] 'o' x[1:] 'ello' В частности, синтаксис срезов часто применяется для отсечения символа новой строки в конце строки (обычно только что прочитанной из файла): x = "Goodbye\n" x = x[:-1] x 'Goodbye' Это просто пример. В языке Python есть другие, более удобные методы отсечения нежелательных символов, но этот код иллюстрирует пользу срезов. Также следует отметить, что в Python отсутствует отдельный символьный тип. Независимо от того, используете ли вы индекс, срез или какой-либо иной метод, при извлечении одного «символа» из строки Python он все равно останется односимвольной строкой, с теми же методами и поведением, что и исходная строка. То же самое справедливо и для пустой строки "". Чтобы определить, сколько символов содержит строка, можно воспользоваться функцией len, которая применяется для определения количества элементов в списке: len("Goodbye") 7 Однако строки не являются списками символов. Самое принципиальное различие между строками и списками заключается в том, что, в отличие от списков, строки нельзя изменять. При попытке использовать выражение вида string. append('c') или string[0] = 'H' произойдет ошибка. Из примера выше видно, что для отсечения символа новой строки создается строка, которая является сегментом предыдущей, а не прямой ее модификацией. Это базовое ограничение Python, установленное из соображений эффективности. 6.2. Основные операции со строками Простейший (и, пожалуй, самый распространенный) способ объединения строк Python основан на использовании оператора конкатенации строк +:
   132 Глава 6. Строки x = "Hello " + "World" x 'Hello World' В Python также существует аналогичный оператор умножения строк, который иногда (но не очень часто) оказывается полезным: 8 * "x" 'xxxxxxxx' 6.3. Специальные символы и экранирующие последовательности Нам уже встречались некоторые последовательности символов, которые Python считает специальными при использовании их в строках: \n — символ новой строки, а \t — символ табуляции. Последовательности символов, начинающиеся с обратной косой черты и используемые для представления других символов, называются экранирующими последовательностями. Они обычно используются для представления специальных символов — то есть символов, не имеющих стандартного односимвольного печатного представления (к примеру, символы табуляции и новой строки). В этом разделе мы подробно рассмотрим экранирующие последовательности, специальные символы и связанные с ними вопросы. 6.3.1. Основные экранирующие последовательности Python предоставляет короткий перечень двухсимвольных экранирующих последовательностей для использования в строках (табл. 6.1). Эти последовательности также могут использоваться с объектами байтового типа, о которых пойдет речь в конце главы. Таблица 6.1. Экранирующие последовательности для строковых и байтовых литералов Экранирующая последовательность Представляемый символ \' Одинарная кавычка \" Двойная кавычка \\ Обратная косая черта \a Звуковой сигнал \b Возврат на один символ (Backspace) \f Перевод страницы \n Новая строка \r Возврат указателя в начало строки (отличается от \n) \t Табуляция \v Вертикальная табуляция
   6.3. Специальные символы и экранирующие последовательности 133 Набор символов ASCII устанавливает еще некоторое количество специальных символов. Для доступа к ним используются числовые экранирующие последовательности, описанные в следующем разделе. 6.3.2. Числовые (восьмеричные и шестнадцатеричные) и экранирующие последовательности Unicode Можно включить в строку любой символ в кодировке ASCII, указав восьмеричную (основание 8) или шестнадцатеричную (основание 16) экранирующую последовательность, соответствующую этому символу. Восьмеричная экранирующая последовательность состоит из обратной косой черты, за которой следуют три цифры, определяющие восьмеричное число; символ ASCII, соответствующий этому восьмеричному числу, заменяется на восьмеричную экранирующую последовательность. Шестнадцатеричная экранирующая последовательность начинается с \x вместо \ и может содержать произвольное количество шестнадцатеричных цифр. Экранирующая последовательность завершается, когда встречается символ, который не является шестнадцатеричной цифрой. Например, в таблице символов ASCII символ m имеет десятичное значение 109, которое соответствует восьмеричному значению 155 и шестнадцатеричному значению 6D, поэтому: 'm' 'm' '\155' 'm' '\x6D' 'm' В результате обработки всех трех выражений будет получена строка, содержащая один символ m. Также числовые экранирующие последовательности могут применяться для передачи символов, не имеющих печатного представления. Например, символу новой строки \n соответствует восьмеричное значение 012 и шестнадцатеричное 0A: '\n' '\n' '\012' '\n' '\x0A' '\n'
   134 Глава 6. Строки Поскольку все строки в Python 3 являются строками Unicode, они могут содержать практически любые символы из всех доступных языков. Несмотря на то что обсуждение системы Unicode выходит за рамки этой книги, следующие примеры показывают, что экранирование может применяться к любому символу Unicode — по номеру (как показано выше) или имени символа в Unicode: unicode_a ='\N{LATIN SMALL LETTER A}' unicode_a Экранирование по имени в Unicode Символы ASCII являются символами Unicode 'a' unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}' unicode_a_with_acute 'á' "\u00E1" Экранирование по номеру посредством \u 'á' Этот код показывает, как можно получить доступ к символам, включая обычные символы ASCII, при помощи их имен в Unicode через \N (имя в Unicode) или их номеров через \u. 6.3.3. Вывод и обработка строк со специальными символами Ранее я уже говорила о различиях между интерактивным вычислением результата выражения Python и выводом результата того же выражения посредством функции print. И хотя в обоих случаях используется одна и та же строка, две операции могут выдавать на экран результаты, которые выглядят по-разному. Строка, которая обрабатывается на верхнем уровне интерактивной сессии Python, выводится со всеми специальными символами в виде восьмеричных экранирующих последовательностей, ясно отображая содержимое строки. Тогда как функция print передает строку непосредственно программе терминала, которая может интерпретировать специальные символы особым образом. Вот что произойдет со строкой, состоящей из символов a, новой строки, табуляции и b: 'a\n\tb' 'a\n\tb' print('a\n\tb') a b В первом случае символы новой строки и табуляции явно выводятся в строке; во втором случае они интерпретируются как экранирующие последовательности новой строки и табуляции.
   6.4. Строковые методы 135 Обычная функция print также добавляет символ новой строки в конец выводимого текста. В некоторых случаях (в частности, при работе со строками из файла, которые уже содержат завершающие символы новой строки) такое поведение может оказаться нежелательным. Если передать функции print параметр end со значением "", символ новой строки присоединяться не будет: print("abc\n") abc print("abc\n", end="") abc 6.4. Строковые методы Большинство строковых методов Python встроены в стандартный строковый класс Python, поэтому они автоматически поддерживаются всеми строковыми объектами. Стандартный модуль string также содержит некоторые полезные константы. Модули более подробно рассматриваются в главе 10. Для понимания этого раздела достаточно помнить, что большинство строковых методов присоединяются к строковому объекту, с которым они работают, посредством точки (.), как в x.upper(). То есть перед ними ставится строковый объект, за которым следует точка. Поскольку строки являются неизменяемыми, строковые методы используются лишь для получения возвращаемого значения, они никак не изменяют сам строковый объект, к которому присоединены. Начнем с самых важных и распространенных строковых операций, затем обсудим некоторые более редкие, но не менее полезные. В конце данного раздела будет затронут ряд аспектов, связанных со строками. Здесь мы рассмотрим далеко не все строковые методы. Их полный перечень приведен в документации. 6.4.1. Строковые методы split и join Любой разработчик, которому доводилось работать со строками, наверняка посчитает методы split и join бесценными. Они являются обратными друг другу: split возвращает список подстрок из строки, а join берет список строк и объединяет их в одну строку, вставляя исходную строку между элементами. Обычно split задействует пробельные символы в качестве разделителя при разбиении строк, но такое поведение можно изменить при помощи необязательного аргумента. Конкатенация строк оператором + полезна, но она неэффективна для объединения большого количества строк в одну, поскольку при каждом применении + создается новый строковый объект. В предыдущем примере «Hello World» создаются три строковых объекта, два из которых сразу же отбрасываются. Лучше воспользоваться функцией join, которая создает только один новый строковый объект:
   136 Глава 6. Строки " ".join(["join", "puts", "spaces", "between", "elements"]) 'join puts spaces between elements' Изменив строку, используемую для вызова join, можно поместить между соединяемыми строками все что угодно: "::".join(["Separated", "with", "colons"]) 'Separated::with::colons' Для объединения элементов списка может использоваться и пустая строка "": "".join(["Separated", "by", "nothing"]) 'Separatedbynothing' Наиболее частым применением split, вероятно, является простой парсинг разделенных строкой записей, которые хранятся в текстовых файлах. По умолчанию split осуществляет разбивку по любому пробельному символу, а не только по одиночному пробелу, однако можно указать методу и любую последовательность символов для разделения строки, которая передается ему в необязательном аргументе: x = "You\t\t can have tabs\t\n \t and newlines и \n\n " \ "mixed in" x.split() ['You', 'can', 'have', 'tabs', 'and', 'newlines', 'mixed', 'in'] x = "Mississippi" x.split("ss") ['Mi', 'i', 'ippi'] Иногда удобно, чтобы последнее поле объединенной строки содержало произвольный текст, возможно, даже с подстроками, совпадающими с разделителями, которые метод split использует при обработке входных данных. Для этого при помощи второго необязательного аргумента можно указать, сколько разбиений должен выполнить метод split при генерации результата. Если указать в этом аргументе n разбиений, метод split будет обрабатывать входную строку, пока не проведет n разбиений (и не сгенерирует список с n + 1 подстроками в качестве элементов) или пока не закончится строка. Приведем несколько примеров: x = 'a b c d' x.split(' ', 1) ['a', 'b c d'] x.split(' ', 2) ['a', 'b', 'c d']
   6.4. Строковые методы 137 x.split(' ', 9) ['a', 'b', 'c', 'd'] При использовании split со вторым необязательным аргументом необходимо задать первый аргумент. Чтобы разделить строки по пробельным символам при использовании второго аргумента, передайте в первом аргументе None. Я активно использую методы split и join, обычно при работе с текстовыми файлами, сгенерированными другими программами. Если вы предпочитаете использовать в ваших программах более распространенные форматы файлового вывода, хорошим выбором станут модули csv и json из стандартной библиотеки Python. Быстрая проверка: split и join Как использовать методы split и join для замены всех пробельных символов в строке x дефисами — например, преобразовать "this is a test" в "this-is-a-test"? 6.4.2. Преобразование строк в числа Функции int и float преобразуют строки в целые числа или числа с плавающей точкой соответственно. Если этим функциям передается строка, которая не может интерпретироваться как число заданного типа, они генерируют исключение ValueError. Исключения рассматриваются в главе 14. Кроме того, int можно передать необязательный второй аргумент с основанием системы счисления, которая должна использоваться при интерпретации входной строки: float('123.456') 123.456 float('xxyy') -------------------------------------------------------------------------ValueError Traceback (most recent call last) <ipython-input-30-14b25c5b2052> in <cell line: 1>() ----> 1 float('xxyy') ValueError: could not convert string to float: 'xxyy' int('3333') 3333 int('123.456') Целое число не может содержать десятичную точку -------------------------------------------------------------------------ValueError Traceback (most recent call last) <ipython-input-32-ed4c46a302ea> in <cell line: 1>() ----> 1 int('123.456')
   138 Глава 6. Строки ValueError: invalid literal for int() with base 10: '123.45 10000 интерпретируется как восьмеричное число int('10000', 8) 4096 Двоичное число int('101', 2) 5 Шестнадцатеричное число int('ff', 16) 255 int('123456', 6) Невозможно интерпретировать 123,456 как число в шестеричной системе -------------------------------------------------------------------------ValueError Traceback (most recent call last) <ipython-input-36-bdc1281d81c5> in <cell line: 1>() ----> 1 int('123456', 6) ValueError: invalid literal for int() with base 6: '123456' Вы поняли причину последней ошибки? Я указала, чтобы строка интерпретировалась как число в шестеричной системе счисления, но цифры 6 в шестеричном числе быть не может. Хитро! Быстрая проверка: преобразование строк в числа Какая из следующих строк не будет преобразована в число и почему? int('a1') int('12G', 16) float("12345678901234567890") int("12*2") 6.4.3. Удаление лишних пробельных символов Три удивительно полезных простых метода — это функции strip, lstrip и rstrip. Метод strip возвращает новую строку, которая получается из исходной после удаления всех пробельных символов в начале и конце строки. Методы lstrip и rstrip действуют аналогично, но они удаляют пропуски лишь в начале (левом конце) или в конце (правом конце) исходной строки соответственно: x = " Hello, x.strip() 'Hello, World\t\t " World' x.lstrip() 'Hello, World\t\t ' x.rstrip() ' Hello, World'
   6.4. Строковые методы 139 В этом примере символы табуляции рассматриваются как пробельные символы. Точное значение этого термина может зависеть от операционной системы, но вы всегда можете узнать, какие символы Python относит к категории пробельных символов, обратившись к константе string.whitespace. В моей операционной системе Windows Python возвращает следующий результат: import string string.whitespace ' \t\n\r\x0b\x0c' " \t\n\r\v\f" ' \t\n\r\x0b\x0c' Символы в шестнадцатеричном формате (\xnn) представляют собой символы вертикальной табуляции и новой страницы. Пробел означает сам себя. Возможно, вам захочется изменить значение этой переменной, чтобы повлиять на работу strip и других методов, но делать этого не стоит. Нет никаких гарантий, что это приведет к желаемому результату. Однако вы можете изменить состав символов, удаляемых strip, rstrip и lstrip, передавая строку с удаляемыми символами в качестве дополнительного параметра: x = "www.python.org" x.strip("w") Удаляет все буквы w '.python.org' x.strip("gor") Удаляет все буквы g, o и r с правой стороны строки 'www.python.' x.strip(".gorw") Удаляет все точки, буквы g, o, r и w 'python' Заметьте: strip удаляет все символы, содержащиеся в дополнительном параметре, независимо от порядка их следования. Чаще всего эти функции используются для быстрой очистки только что считанных строк. Они особенно полезны при чтении строк из файлов, поскольку Python всегда считывает всю строку целиком, включая завершающий символ новой строки, если он имеется. Когда вы переходите к обработке считанной строки, завершающий символ новой строки, как правило, не требуется. Метод rstrip позволяет легко от него избавиться. Кроме того, появились два новых строковых метода для удаления префиксов и суффиксов:
   140 Глава 6. Строки x = "www.python.org" x.removeprefix("www.") Удаляет только префикс www 'python.org' x.removesuffix(".org") Удаляет только суффикс .org 'www.python' В отличие от strip, методы removesuffix и removeprefix удаляют только те суффиксы и префиксы, которые точно совпадают с их параметрами, что делает поведение этих методов более предсказуемым. Быстрая проверка: strip Если строка x равна "(name, date),\n", какой из следующих вызовов вернет строку "name, date"? x.rstrip("),") x.strip("),\n") x.strip("\n)(,") 6.4.4. Поиск в строках Строковые объекты предоставляют ряд методов для выполнения простого поиска. Однако прежде чем описывать их, мне бы хотелось сказать несколько слов о другом модуле Python: re (этот модуль подробно рассматривается в главе 16). Другой метод поиска в строках: модуль re Модуль re также позволяет выполнять поиск в строках, но делает это намного гибче с помощью регулярных выражений. Вместо того чтобы искать одну конкретную подстроку, модуль re проводит поиск по шаблону — например, можно искать подстроки, состоящие только из цифр. Почему я упоминаю об этом сейчас, хотя модуль re будет рассматриваться позднее? По моему опыту, многие случаи применения основных методов поиска в строках неоправданны. Разработчику стоило бы воспользоваться более мощными средствами поиска, но если он не знает об их существовании, то даже не ищет чего-то лучшего. Возможно, вы работаете над срочным проектом, в котором используются строки, и у вас нет времени читать всю книгу. Если базовые средства поиска в строках решают вашу задачу — отлично. Но знайте, что есть более мощная альтернатива. Существуют четыре базовых метода поиска в строках: find, rfind, index и rindex. Связанный с ними метод count подсчитывает, сколько раз подстрока встречается в другой строке. Я подробно опишу метод find, а затем объясню, чем отличаются от него другие методы. Метод find принимает один обязательный аргумент: искомую подстроку (substring). Он возвращает позицию первого символа в первом вхождении подстроки в строковом объекте (string) или –1, если подстрока не встречается в строке:
   6.4. Строковые методы 141 x = "Mississippi" x.find("ss") 2 x.find("zz") -1 Метод find также может принимать один или два дополнительных, необязательных аргумента. Первый из этих аргументов, если он присутствует, — это start (целое число); он определяет начальную позицию поиска, заставляя find игнорировать все символы до позиции start при поиске подстроки. Второй необязательный аргумент, если он есть, — это end (целое число); он определяет конечную позицию поиска, заставляя find игнорировать все символы, начиная с позиции end и далее в строке: x = "Mississippi" x.find("ss", 3) 5 x.find("ss", 0, 3) -1 Метод rfind очень похож на find, за исключением того, что он начинает поиск с конца строки и поэтому возвращает позицию первого символа последнего вхождения подстроки в строке: x = "Mississippi" x.rfind("ss") 5 Метод rfind также может получать один или два необязательных аргумента, смысл которых аналогичен аргументам find. Методы index и rindex идентичны методам find и rfind соответственно, кроме одного различия: если index или rindex не могут найти вхождение подстроки в строке, они не возвращают –1, а генерируют исключение ValueError. Что именно это означает, станет ясно после прочтения главы 14. Метод count применяется аналогично любому из предыдущих четырех, однако он возвращает количество неперекрывающихся вхождений заданной подстроки в данной строке: x = "Mississippi" x.count("ss") 2
   142 Глава 6. Строки Для поиска в строках также могут использоваться еще два метода: startswith и endswith. Они возвращают результат True или False в зависимости от того, начинается ли (или заканчивается) строка, для которой они применяются, одной из строк, указанных в качестве параметров: x = "Mississippi" x.startswith("Miss") True x.startswith("Mist") False x.endswith("pi") True x.endswith("p") False Методы startswith и endswith позволяют выполнять поиск более одной строки одновременно. Если параметр представляет собой кортеж строк, то оба метода проверяют все строки в кортеже и возвращают True, если будет найдена хотя бы одна из них: x.endswith(("i", "u")) True Методы startswith и endswith хорошо подходят для простого поиска, когда вы уверены в том, что искомое находится в начале или конце строки. Быстрая проверка: поиск в строках Допустим, вы хотите проверить, завершается ли строка подстрокой rejected. Каким строковым методом вы для этого воспользуетесь? Можно ли добиться того же результата иными способами? 6.4.5. Изменение строк Строки являются неизменяемыми, однако строковые объекты поддерживают ряд методов, которые работают со строкой и возвращают новую строку, являющуюся модифицированной версией исходной. Для большинства задач это дает практически тот же результат, что и прямое изменение. Более подробное описание этих методов содержится в документации. Метод replace может использоваться для замены вхождений подстроки substring (первого аргумента) другой строкой newstring (второй аргумент).
   6.4. Строковые методы 143 Этот метод также принимает необязательный третий аргумент (подробности см. в документации): x = "Mississippi" x.replace("ss", "+++") 'Mi+++i+++ippi' Как и в случае с функциями поиска в строках, модуль re предоставляет гораздо более мощные средства замены подстрок. Функции string.maketrans и string.translate могут применяться совместно для перевода символов в строках в другие символы. Хотя эти функции редко используются, они могут упростить вашу жизнь, когда в них возникает необходимость. Предположим, вы работаете над программой, которая переводит строковые выражения с одного языка программирования на другой. В первом языке для обозначения логического НЕ используется оператор ~, а в другом !; в первом языке для обозначения логического И используется оператор ^, а во втором &; в первом языке используются круглые скобки ( и ), а во втором — квадратные [ и ]. В заданном строковом выражении необходимо заменить все вхождения ~ на !, все вхождения ^ на &, все вхождения ( на [и все вхождения ) на ]. Это можно сделать многократными вызовами replace, но проще и эффективнее действовать так: x = "~x ^ (y % z)" table = x.maketrans("~^()", "!&[]") x.translate(table) '!x & [y % z]' Во второй строке метод maketrans создает таблицу преобразования по двум строковым аргументам. Оба аргумента должны содержать одинаковое количество символов, и таблица устроена таким образом, что поиск n-го символа первого аргумента возвращает n-й символ второго аргумента. Далее таблица, созданная вызовом maketrans, передается методу translate. Затем метод translate перебирает все символы своего строкового объекта и проверяет, присутствуют ли они в таблице, заданной в качестве второго аргумента. Если символ встречается в таблице преобразования, translate заменяет этот символ соответствующим символом, найденным в таблице, для получения преобразованной строки. При вызове translate также можно передать необязательный аргумент с символами, которые следует удалить из строки. Для удаления символов нужно оставить строки для первого и второго аргументов пустыми и включить третью строку с символами для удаления:
   144 Глава 6. Строки x = "~x ^ (y % z)" table = x.maketrans("", "", "()~^%") x.translate(table) 'x y z' Здесь все знаки препинания исходной строки находятся в третьем параметре — строке, содержащей символы, которые необходимо удалить. Другие функции модуля string решают более специализированные задачи. Метод string.lower приводит все алфавитные символы строки к нижнему регистру, а метод upper — наоборот, к верхнему. Метод capitalize делает заглавным первый символ строки, а title — первый символ каждого слова в строке. Метод swapcase преобразует в строке символы нижнего регистра в верхний, а символы верхнего регистра в нижний. Метод expandtabs удаляет символы табуляции в строке, заменяя каждый из них заданным количеством пробелов. Методы ljust, rjust и center дополняют строку пробелами, чтобы выровнять ее по определенной ширине поля. Метод zfill дополняет числовую строку нулями слева. За подробной информацией обо всех этих методах обращайтесь к документации. 6.4.6. Изменение строк при помощи операций со списками Поскольку строки являются неизменяемыми объектами, не получится манипулировать ими напрямую, как списками. И хотя операции, создающие новые строки (и оставляющие исходные строки без изменения), полезны во многих случаях, иногда бывает удобно работать со строкой так, словно она представляет собой список символов. В таких ситуациях следует преобразовать строку в список символов, выполнить все необходимые операции, а затем преобразовать полученный список обратно в строку: text = "Hello, World" wordList = list(text) Удаляет все после запятой wordList[6:] = [] wordList.reverse() Объединяет без пробелов text = "".join(wordList) print(text) ,olleH Также можно преобразовать строку в кортеж символов встроенной функцией tuple. Для преобразования списка обратно в строку применяйте "".join(). Впрочем, не слишком увлекайтесь этим методом, поскольку он требует создания и удаления новых строковых объектов, что приводит к достаточно большим затратам ресурсов. Обработка сотен и тысяч строк не окажет заметного воздействия на вашу программу, но с обработкой миллионов строк ситуация может измениться. Быстрая проверка: изменение строк Как быстро заменить все знаки препинания в строке пробелами?
   6.4. Строковые методы 145 6.4.7. Полезные методы и константы У строковых объектов также есть несколько полезных методов для получения разных характеристик строк — например, содержит строка только цифры либо только алфавитные символы или состоит целиком лишь из символов верхнего или нижнего регистра: x = "123" x.isdigit() True x.isalpha() False x = "M" x.islower() False x.isupper() True Перечень всех возможных строковых методов приведен в соответствующем разделе официальной документации по языку Python. Наконец, модуль string задает ряд полезных констант. Вам уже знакома константа string.whitespace — строка, состоящая из всех символов, которые Python относит к категории пробельных символов в вашей системе. Константа string. digits содержит строку '0123456789'. Константа string.hexdigits включает все символы string.digits, а также 'abcdefABCDEF' — дополнительные символы, используемые в шестнадцатеричных числах. Константа string.octdigits содержит '01234567' — только цифры, допустимые в восьмеричных числах. Константа string.ascii_lowercase содержит все алфавитные символы ASCII нижнего регистра; string. ascii_uppercase содержит все алфавитные символы ASCII верхнего регистра; string. ascii_letters содержит все символы string. ascii_lowercase и string. ascii_uppercase. Возможно, вам захочется присвоить новые значения этим константам, чтобы изменить поведение языка. Python позволит вам это сделать, но, возможно, это будет не лучшая идея. Помните, что строки являются последовательностями символов. Это позволяет использовать удобный оператор in языка Python для проверки принадлежности символа к любой из строк, хотя обычно существующие строковые методы проще и удобнее. Наиболее распространенные операции со строками перечислены в табл. 6.2. Помните, что эти методы не изменяют саму строку; они возвращают либо позицию в строке, либо новую строку.
   146 Глава 6. Строки Таблица 6.2. Распространенные операции со строками Операция Описание Пример + Объединяет две строки x = "hello " + "world" * Дублирует строку x = " " * 20 upper Приводит строку к верхнему регистру x.upper() lower Приводит строку к нижнему регистру x.lower() title Приводит первую букву каждого слова в строке к верхнему регистру x.title() find, index Ищет заданную подстроку в строке x.find(y) rfind, rindex Ищет заданную подстроку в строке, начиная с конца строки x.rfind(y) startswith, endswith Проверяет начало или конец строки на совпадение с заданной подстрокой x.startswith(y) replace Заменяет подстроку новой строкой x.replace(y, z) strip, rstrip, lstrip Удаляет пробельные или другие символы в начале и/или конце строки x.strip() encode Преобразует строку Unicode в объект байтового типа (bytes) x.encode("utf_8") x.index(y) x.rindex(y) x.endswith(y) Попробуйте сами: операции со строками Допустим, имеется список строк, в котором отдельные (но не обязательно все) строки начинаются и завершаются символом двойной кавычки: x = ['"abc"', 'def', '"ghi"', '"klm"', 'nop'] Какой код вы бы использовали для удаления только двойных кавычек из каждого элемента? Какой код вы бы использовали для нахождения позиции последней буквы p в слове Mississippi? А после того, как эта позиция будет найдена, какой код вы бы использовали для удаления только этой буквы? 6.5. Преобразование объектов в строки В языке Python почти любой тип объекта можно преобразовать в строковое представление вызовом встроенной функции repr. Списки — единственный сложный тип данных Python, который вам уже встречался по ходу книги, поэтому мы преобразуем несколько списков в их строковые представления: repr([1, 2, 3]) '[1, 2, 3]'
   6.5. Преобразование объектов в строки 147 x = [1] x.append(2) x.append([3, 4]) 'the list x is ' + repr(x) ' the list x is [1, 2, [3, 4]]' Этот пример использует функцию repr для преобразования списка x в строковое представление, которое затем объединяется с другой строкой для формирования итоговой строки. Без применения repr этот код бы не работал. Что именно суммируется в выражении вида "string" + [1, 2] + 3 — строки, списки или только числа? Python не знает ваших намерений в такой ситуации, поэтому он выбирает безопасный вариант (генерирует ошибку) вместо того, чтобы делать предположения. В предыдущем примере все элементы необходимо было преобразовать в строковые представления, чтобы сработала конкатенация строк. Функцией repr можно воспользоваться для получения строкового представления не только списков, но и практически любых объектов Python. Чтобы убедиться в этом, попробуйте применить repr к встроенному сложному объекту — функции Python: repr(len) '<built-in function len>' Python не отображает строку с кодом реализации функции len, но по крайней мере возвращает строку — <built-in function len> — с описанием этой функции. Если вы опробуете функцию repr на каждом упоминаемом в книге типе данных Python (словари, кортежи, классы и пр.), вы увидите, что независимо от типа объекта Python можно получить строку, содержащую некоторые сведения об объекте. Это отлично подходит для отладки программ. Если вы сомневаетесь в том, какие данные присвоены переменной в определенной точке программы, используйте функцию repr и выведите содержимое объекта, на который ссылается эта переменная. Итак, теперь вы знаете, как Python может преобразовать любой объект в строку с описанием этого объекта. По правде говоря, Python может сделать это одним из двух способов. Функция repr всегда возвращает то, что можно условно назвать формальным строковым представлением объекта Python. Точнее, для простых объектов repr возвращает строковое представление объекта Python, по которому можно восстановить исходный объект. Для больших и сложных объектов такое представление может быть избыточным, поэтому repr возвращает некий описательный текст. Python также предоставляет встроенную функцию str. В отличие от repr, str предназначена для создания выводимых строковых представлений и может применяться к любым объектам Python. Функция str возвращает то, что можно назвать неформальным строковым представлением объекта. Строка, возвращаемая
   148 Глава 6. Строки str, не обязана определять объект полностью; она предназначена для чтения человеком, а не кодом Python. Когда вы начнете пользоваться функциями repr и str, то не заметите между ними никакой разницы, поскольку ее не будет до тех пор, пока вы не воспользуетесь объектно-ориентированными возможностями Python. При применении к любому встроенному объекту Python функция str всегда вызывает repr для вычисления результата. Лишь когда вы станете определять собственные классы, различия между str и repr начинают играть важную роль, что мы обсудим в главе 15. Зачем же сейчас упоминать об этом? Для того чтобы вы знали, что repr имеет более широкое применение, чем простой вывод (print) для отладки программы. Возможно, для развития привычки программировать в хорошем стиле лучше использовать str вместо repr при создании строк для отображения информации. 6.6. Использование метода format Строки в Python 3 можно форматировать тремя способами. Первый заключается в применении метода format строкового класса. Метод format сочетает строку форматирования, содержащую поля замены, отмеченные фигурными скобками { }, со значениями замены, взятыми из параметров, переданных команде format. Если потребуется включить в строку настоящие фигурные скобки { или } , удвойте их ({{ или }}). Команда format — это фактически мощный мини-язык, предоставляющий почти безграничные возможности для форматирования строк. С другой стороны, он достаточно прост и удобен для большинства распространенных сценариев применения, поэтому в данном разделе мы рассмотрим несколько базовых паттернов. Если же вам потребуются расширенные опции, обратитесь к разделу, посвященному форматированию строк, в документации стандартной библиотеки. 6.6.1. Метод format и позиционные параметры Простой способ использования строкового метода format связан с применением нумерованных полей замены, которые соответствуют параметрам, переданным функции format: Метод format применяется к строке форматирования, которая также может быть строковой переменной "{0} is the {1} of {2}".format("Ambrosia", "food", "the gods") 'Ambrosia is the food of the gods' "{{Ambrosia}} is the {0} of {1}".format("food", "the gods") '{Ambrosia} is the food of the gods' Удвоение символов { } экранирует их, чтобы они не обозначали поле замены
   6.6. Использование метода format 149 Обратите внимание, что метод format применяется к строке форматирования, которая также может быть строковой переменной. Удвоение символов { } экранирует их, чтобы они не обозначали поле замены. Пример содержит три поля замены: {0}, {1} и {2}, которые последовательно заполняются первым, вторым и третьим параметрами. Где бы в строке форматирования ни размещалось поле {0}, оно всегда замещается первым параметром и т. д. Можно задействовать и именованные параметры. 6.6.2. Метод format и именованные параметры Метод format также распознает именованные параметры и поля замены: "{food} is the food of {user}".format(food="Ambrosia", user="the gods") 'Ambrosia is the food of the gods' В данном случае параметр замены выбирается по совпадению имени поля замены с именем параметра, переданного команде format. Можно использовать как позиционные, так и именованные параметры; также имеется доступ к атрибутам и элементам внутри этих параметров: "{0} is the food of {user[1]}".format("Ambrosia", user=["men", "the gods", "others"]) 'Ambrosia is the food of the gods' В данном случае первый параметр является позиционным, тогда как обозначение user[1] относится ко второму элементу именованного параметра user. 6.6.3. Спецификаторы формата Спецификаторы формата позволяют задать результат форматирования с еще большей эффективностью и возможностью контроля, чем последовательности форматирования старого стиля форматирования строк. Спецификатор формата позволяет задать символ-заполнитель, выравнивание, знак, ширину, точность и тип данных при подстановке на место поля замены. Как упоминалось ранее, синтаксис спецификаторов формата является отдельным мини-языком и слишком сложен, чтобы полностью описывать его здесь. Тем не менее несколько примеров дадут вам представление о том, насколько он может быть полезен: "{0:10} is the food of gods".format("Ambrosia") 'Ambrosia is the food of gods' "{0:{1}} is the food of gods".format("Ambrosia", 10) 'Ambrosia is the food of gods' :10 задает поле шириной 10 символов, заполняя его пробелами :{1} ширина определяется вторым параметром "{food:{width}} is the food of gods".format(food="Ambrosia", width=10) 'Ambrosia is the food of gods' "{0:>10} is the food of gods".format("Ambrosia") :>10 устанавливает
"{0:10} is the food of gods".format("Ambrosia") 'Ambrosia is the food of gods' "{0:{1}} is the food of gods".format("Ambrosia", 10) 150    Глава 6. Строки 'Ambrosia is the food of gods' :10 задает поле шириной 10 символов, заполняя его пробелами :{1} ширина определяется вторым параметром "{food:{width}} is the food of gods".format(food="Ambrosia", width=10) 'Ambrosia is the food of gods' "{0:>10} is the food of gods".format("Ambrosia") ' Ambrosia is the food of gods' "{0:&>10} is the food of gods".format("Ambrosia") '&&Ambrosia is the food of gods' :>10 устанавливает выравнивание по правому краю :&>10 устанавливает выравнивание по правому краю, заполненное символом & :10 — спецификатор формата, задающий поле шириной в 10 символов и заполняющий его пробелами. :{1} получает ширину из второго параметра. :>10 устанавливает выравнивание поля по правому краю с заполнением пробелами. :&>10 включает выравнивание по правому краю с заполнением символами & вместо пробелов. Быстрая проверка: метод format() Какое значение будет присвоено переменной x при выполнении следующих фрагментов кода? x x x x = = = = "{1:{0}}".format(3, 4) "{0:$>5}".format(3) "{a:{b}}".format(a=1, b=5) "{a:{b}}:{0:$>5}".format(3, 4, a=1, b=5, c=10) 6.7. Интерполяция строк с помощью f-строк Начиная с Python версии 3.6, новейший способ создания строк называется f-строками (f-strings). F-строки, которые так называются из-за префикса f, позволяют включить значения произвольных выражений Python в строковый литерал. В них применяются фигурные скобки { } для включения и обработки выражений Python. Синтаксис f-строк практически идентичен синтаксису метода format, но без явного вызова format() они компактнее и легче читаются. Благодаря своей простоте они получили широчайшее распространение. Чтобы воспользоваться f-строкой, достаточно поставить символ f прямо перед первой кавычкой, а затем заключить в фигурные скобки выражения Python, которые вы хотите поместить (интерполировать) в строку. Следующие примеры дадут вам общее представление о том, как работают f-строки: value = 42 message = f"The answer is {value}" print(message) The answer is 42
   6.8. Форматирование строк с помощью % 151 primes = [1, 2, 3, 5] f"sum of first 4 primes is {sum(primes)}" 'sum of first 4 primes is 11' Как и в случае с методом format, можно добавить спецификаторы формата: PI = 3.1415 print(f"PI is {PI:{10}.{2}}") PI is 3.1 Еще одна полезная функция f-строк заключается в том, что добавление символа = после выражения в фигурных скобках позволяет получить «отладочный» вывод, отображающий и само выражение, и его значение: print(f"{PI=:{10}.{2}}") PI= Знак = поставлен после выражения 3.1 primes = [1, 2, 3, 5] f"sum of first 4 primes is {sum(primes)=}" 'sum of first 4 primes sum(primes)=11' Результат = выводит исходное выражение и его значение Все спецификаторы формата, работающие с описанным выше методом format, должны работать и с f-строками. Хотя f-строки были несколько ограничены в ранних версиях, они оказались настолько удобными в качестве краткого и читабельного способа создания строк со значениями переменных и выражений, что их возможности были расширены и они массово используются в настоящее время. 6.8. Форматирование строк с помощью % В этом разделе рассматривается форматирование строк при помощи оператора форматирования строк %. Он используется для объединения значений Python в отформатированные строки для вывода или другого применения. Пользователи языка C отметят неожиданное сходство с семейством функций printf. Применение % для форматирования строк относится к старому стилю форматирования, но я рассматриваю его здесь, поскольку он считался стандартом в предыдущих версиях Python. Вы с большой вероятностью можете встретить его в коде, перенесенном из более ранних версий Python или написанном программистами, знакомыми с этими версиями. В новом коде этот стиль форматирования не стоит использовать, так как он объявлен устаревшим и в будущем будет удален из языка. Приведем пример: "%s is the %s of %s" % ("Ambrosia", "food", "the gods") 'Ambrosia is the food of the gods'
   152 Глава 6. Строки Оператор форматирования строк (последний знак % в строке, а не три предшествующих вхождения %s) принимает две части: строку слева от себя и кортеж справа. Оператор форматирования строк ищет в левой части специальные форматирующие последовательности и создает новую строку, по порядку заменяя эти последовательности значениями из правой части. В данном примере форматирующими последовательностями в левой части являются три вхождения %s, которые означают «Вставить сюда строку». Передача различных значений из правой части приводит к получению различных строк: "%s is the %s of %s" % ("Nectar", "drink", "gods") 'Nectar is the drink of gods' "%s is the %s of the %s" % ("Brussels Sprouts", "food", "foolish") 'Brussels Sprouts is the food of the foolish' К элементам кортежа в правой части автоматически применяется str (преобразование в строку) благодаря %s, поэтому они не обязательно должны быть строками изначально: x = [1, 2, " three"] "The %s contains: %s" % ("list", x) "The list contains: [1, 2, 'three']" 6.8.1. Применение форматирующих последовательностей Все форматирующие последовательности представляют собой подстроки, содержащиеся в строке слева от центрального оператора %. Каждая форматирующая последовательность начинается со знака %, за которым следует один или несколько символов, определяющих, что должно быть подставлено вместо форматирующей последовательности и как эта подстановка должна быть выполнена. Ранее использовавшаяся форматирующая последовательность %s является самой простой; она означает, что на место %s следует подставить соответствующую строку из кортежа справа от центрального символа %. Возможны и другие, более сложные форматирующие последовательности. Следующая последовательность задает ширину поля (общее количество символов) выводимого числа равной 6, задает количество символов после десятичной точки равным 2 и выравнивает число по левому краю в пределах поля. В следующем примере эта форматирующая последовательность заключена в угловые скобки, чтобы было видно, где в отформатированную строку вставляются дополнительные пробелы: "Pi is <%-6.2f>" % 3.14159 # Использование форматирующей последовательности: %–6.2f 'Pi is <3.14 >'
   6.8. Форматирование строк с помощью % 153 Все допустимые варианты символов в форматирующих последовательностях указаны в документации. Вариантов достаточно много, но все они относительно просты в использовании. Помните, что форматирующую последовательность всегда можно протестировать в интерактивном режиме Python; это поможет понять, работает ли она так, как вы ожидаете. 6.8.2. Именованные параметры и форматирующие последовательности Наконец, у оператора % есть еще одна дополнительная функция, которая в ряде случаев может оказаться полезной. К сожалению, чтобы описать ее, мне придется воспользоваться функцией Python, которая еще подробно не рассматривалась, — словарями (dictionaries), которые в других языках программирования часто называются хеш-таблицами (hash tables) или ассоциативными массивами (associative arrays). Словари рассматриваются в главе 7. Вы можете пропустить этот раздел, ознакомиться с главой 7 и вернуться сюда позднее или же читать дальше в расчете на то, что из примеров все станет ясно. Форматирующие последовательности могут указывать, что должно быть подставлено вместо них, по имени, а не по позиции. В этом случае имя каждой форматирующей последовательности указывается в круглых скобках сразу же после начального символа %, например: "%(pi).2f" Обратите внимание, что имя заключается в круглые скобки Кроме того, в аргументе справа от оператора % указывается уже не отдельное значение или кортеж значений для вывода, а словарь, в котором для каждой именованной форматирующей последовательности содержится ключ с соответствующим именем. Используя приведенную выше форматирующую последовательность с оператором форматирования строк, можно создать код, подобный следующему: num_dict = {'e': 2.718, 'pi': 3.14159} print("%(pi).2f - %(pi).4f - %(e).2f" % num_dict) 3.14 - 3.1416 - 2.72 Такой код особенно полезен при использовании строк форматирования с большим количеством замен, поскольку вам не приходится следить за позиционным соответствием элементов кортежа в правой части форматирующим последовательностям в строке форматирования. Порядок определения элементов в аргументе dict не важен, а строка шаблона может использовать значения из dict более одного раза (как это происходит с записью pi). Возможности функции print достаточны для простого вывода текста, но в более сложных случаях лучше воспользоваться методом format.
   154 Глава 6. Строки Управление выводом с помощью функции print Встроенная функция Python print также имеет ряд опций, упрощающих обработку простых строковых выводов. При использовании с одним параметром print выводит значение и символ новой строки, поэтому серия вызовов print выводит каждое значение в отдельной строке: print("a") print("b") a b Однако функция print способна на большее. Ей также можно передать несколько аргументов, и эти аргументы будут выведены в одной строке, разделены пробелами и завершены переводом строки: print("a", "b", "c") a b c Если это не совсем то, что вам нужно, передайте функции print дополнительные параметры, управляющие разделителями элементов и завершающим символом строки: print("a", "b", "c", sep="|") a|b|c print("a", "b", "c", end="\n\n") a b c Наконец, функция print может использоваться для вывода как в файлы, так и на консоль. print("a", "b", "c", file=open("testfile.txt", "w")) Быстрая проверка: форматирование строк с помощью % На какое значение будет ссылаться переменная x после выполнения следующих фрагментов кода? x = "%.2f" % 1.1111 x = "%(a).2f" % {'a':1.1111} x = "%(a).08f" % {'a':1.1111} 6.9. Байтовые объекты Байтовый объект bytes напоминает строковый объект string с одним важным различием: строка string является неизменяемой последовательностью символов Unicode, тогда как байтовый объект bytes — это последовательность целых чисел со значениями от 0 до 255. Байтовые объекты могут пригодиться при работе с данными в двоичном формате — например, при чтении из двоичного файла данных. Главное — запомнить, что, хотя объекты bytes внешне и похожи на строки, они не могут использоваться точно так же, как строки, или объединяться с ними: unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}' unicode_a_with_acute 'á' xb = unicode_a_with_acute.encode() xb b'\xc3\xa1' Метод encode преобразует строковый объект в байтовый Закодированный байтовый объект имеет значение 2 байта; он не выводится как символ
unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}' unicode_a_with_acute    6.10. Практическая работа: предварительная обработка текста 'á' 155 xb = unicode_a_with_acute.encode() xb b'\xc3\xa1' xb += 'A' Метод encode преобразует строковый объект в байтовый Закодированный байтовый объект имеет значение 2 байта; он не выводится как символ Байтовые и строковые объекты относятся к разным типам данных -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-93-2c4ecee004ef> in <cell line: 1>() ----> 1 xb += 'A' TypeError: can't concat str to bytes xb.decode() Метод decode преобразует объект обратно в строку 'á' Первое, что вы заметите, — для преобразования обычной строки (в Unicode) в байтовый объект bytes необходимо вызвать строковый метод encode. После того как строка будет закодирована в объект типа bytes, символ занимает 2 байта и выводится не так, как строка. Более того, при попытке объединить объект bytes со строковым объектом вы получите сообщение об ошибке несовместимости двух типов. Наконец, чтобы преобразовать объект bytes обратно в строку, необходимо вызвать метод decode этого объекта. В большинстве случаев вам вообще не придется задумываться о Unicode и байтах. Однако в тех случаях, когда вы работаете с наборами международных символов (а сейчас эта задача встречается все чаще), необходимо понимать различия между обычными строками и байтовыми объектами bytes. Быстрая проверка: байтовые объекты Для каких из следующих разновидностей данных вы бы использовали обычные строки? В каких случаях можно применять байтовые объекты? Файл данных с двоичными данными. Текст на языке, содержащем символы с диакритическими знаками. Текст, состоящий только из букв латинского алфавита в верхнем и нижнем регистрах. Ряд целых чисел, не превышающих 255. 6.10. Практическая работа: предварительная обработка текста При обработке первичного текста нередко возникает необходимость сначала очистить его от лишних знаков и привести в нормальный вид, прежде чем с ним работать. К примеру, если вы хотите определить частоту вхождений слов в тексте, вам будет проще справиться с задачей, если перед началом подсчета вы убедитесь в том, что все слова записаны в нижнем регистре (или в верхнем,
   156 Глава 6. Строки если вам так удобнее) и что все знаки препинания удалены. Вы также можете облегчить свою работу, разбив текст на серию слов. В этой практической работе необходимо прочитать первую часть первой главы романа «Моби Дик»1, удостовериться в том, что все слова приведены к единому регистру, удалить все знаки препинания и записать слова по одному в каждой строке во второй файл. Как уже упоминалось в предыдущей главе, перед тем как прибегнуть к средствам ИИ для генерации решения, вам следует попытаться разработать его самостоятельно. Для решения этой задачи ваша программа должна открыть исходный файл moby_01.txt. Если вы пользуетесь Jupyter Notebook, просто выполните ячейку прямо перед практической работой. Также можно скачать файл из папки репозитория GitHub для этой главы и сохранить его в рабочем каталоге. Вам также нужно будет выполнить операции по очистке текста: сделать все буквы строчными, удалить знаки препинания, разбить текст на отдельные слова и записать очищенный вывод в файл. Я рекомендую обрабатывать файл построчно, так как это сэкономит память и упростит построчное чтение и запись текста в Python. Поскольку мы еще не рассматривали тему чтения и записи файлов, ниже я привожу программный код для этих операций. Просто вставьте ваш код вместо комментариев в тело цикла for: with open("moby_01.txt") as infile: with open("moby_01_clean.txt", "w") as outfile: for line in infile: # привести весь текст к единому нижнему регистру # удалить знаки препинания # разделить текст на отдельные слова # записать каждое слово в отдельную строку outfile.write(cleaned_words) Подсказка: можно работать над каждым шагом очистки текста по отдельности. Сначала напишите код, который сделает все буквы строчными, и проверьте, чтобы весь текст в выходном файле был в нижнем регистре. Затем поработайте над удалением знаков препинания и убедитесь в достижении желаемого результата и т. д. 6.10.1. Решение задачи при помощи кода, сгенерированного ИИ Сгенерировать программный код для решения данной задачи при помощи ИИ достаточно несложно. В данном случае, вероятно, проще позволить самому ИИ позаботиться об открытии, чтении и записи файлов. При создании промпта необходимо включить следующие требования. 1. Открыть файл moby_01.txt. 2. Прочитать текст построчно. 1 Герман Мелвилл. Моби Дик, или Белый кит. — Примеч. ред.
   6.10. Практическая работа: предварительная обработка текста 157 3. Привести весь текст к нижнему регистру. 4. Удалить все знаки препинания. 5. Разбить строки на отдельные слова. 6. Записать слова построчно в файл moby_01_clean.text. Если вы хотите попытаться сгенерировать решение при помощи ИИ, создайте промпт и посмотрите, что получится, а также проверьте, насколько правильно работает предложенный вам код. 6.10.2. Решения и обсуждение Неудивительно, что мое решение и предложение ИИ оказались очень схожими. Однако, как мы увидим, в версии ИИ упущена пара приемов, которые могли бы несколько повысить эффективность программы. Программное решение, созданное человеком Выполнить это задание при помощи представленной здесь схемы довольно легко — просто двигайтесь шаг за шагом: Импортирует модуль string Python import string punct = str.maketrans("", "", string.punctuation) Создает таблицу преобразования из списка знаков препинания модуля string with open("moby_01.txt") as infile: with open("moby_01_clean.txt", "w") as outfile: for line in infile: # привести весь текст к единому нижнему регистру cleaned_line = line.lower() # удалить знаки препинания cleaned_line = cleaned_line.translate(punct) Использует таблицу преобразования для удаления знаков препинания Объединяет слова текущей строки # разделить текст на отдельные слова при помощи новой строки, то есть words = cleaned_line.split() по одному слову на строку cleaned_words = "\n".join(words) # записать каждое слово в отдельную строку outfile.write(cleaned_words) Это решение лаконично и вполне эффективно. В нем мы импортируем модуль string, чтобы создать таблицу преобразования для удаления знаков препинания, используя список знаков препинания Python, а не создавая свой. При обработке строки текста мы сначала используем для всей строки метод lower(), а затем так же применяем к ней метод translate. Затем мы разделяем строку на слова, получая список слов без пробельных символов, и, наконец, снова объединяем слова переводом строки, благодаря чему при записи в файл каждое слово будет на отдельной строке.
   158 Глава 6. Строки Программное решение, созданное ИИ Создание версии средствами ИИ немного проще, ведь нам не нужно поддерживать механизм открытия, чтения и записи файлов, поскольку генератор программного кода должен знать, как это делается. В меру краткий промпт для того, что нам требуется, мог бы выглядеть так: Для каждой строки файла moby_01.txt приведи весь текст к нижнему регистру, удали знаки препинания и раздели текст на отдельные слова. Запиши каждое слово в отдельную строку в файл moby_01_clean.txt. Решения нашей задачи, сгенерированные в Colaboratory и в Copilot, были очень близки. Наглядным примером является следующая программа, созданная Copilot: import string # Открыть входной файл with open('moby_01.txt', 'r') as input_file: # Открыть выходной файл with open('moby_01_clean.txt', 'w') as output_file: # Выполнить итерацию по каждой строке входного файла for line in input_file: # Привести строку к нижнему регистру line = line.lower() Создает новую таблицу преобразования для каждой строки # Удалить из строки знаки препинания line = line.translate(str.maketrans('', '', string.punctuation)) # Разделить строку на отдельные слова words = line.split() # Записать каждое слово в выходной файл for word in words: output_file.write(word + '\n') Решение хорошо отформатировано и снабжено комментариями, хотя я бы сказала, что комментарий типа # Выполнить итерацию по каждой строке входного файла прямо перед строкой for line in input_file: является очевидным и избыточным, но это не столь существенно. Куда более серьезная проблема кроется в том, как программа использует метод translate. Обратите внимание, что, вместо того чтобы создать таблицу преоб- разования для знаков препинания один раз в начале программы (как это делает версия, написанная человеком), ИИ создает новую таблицу преобразования непосредственно для каждой строки. Опытные программисты содрогнутся — неоправданное повторение операции внутри цикла может значительно замедлить работу кода, и это грубая ошибка новичков, которой все стремятся избежать. Одно из решений, предложенных Colaboratory, оказалось еще хуже. Вместо применения метода translate инструмент ИИ предлагает перебирать все символы в string.punctuation с помощью метода replace для их удаления из строки:
   Итоги 159 # Удалить из строки знаки препинания for char in string.punctuation: line = line.replace(char, '') Здесь мы снова имеем цикл с разовой операцией translate, при этом перебираем все знаки препинания в каждой строке и создаем новый строковый объект всякий раз, когда знак препинания обнаружен и удален. Такое решение не даст побочных эффектов при обработке относительно небольшого файла, однако оно моментально обернется огромной проблемой, если придется обрабатывать набор данных в миллион строк. Аналогичная проблема возникает в последнем разделе, где слова записываются в файл вывода. Хотя это и не влечет за собой существенной потери производительности, как в первом случае, но все же проблема имеется по двум причинам. Во-первых, программа перебирает в цикле список слов в строке, а во-вторых, она задействует конкатенацию строк (+) при переводе строк. Хотя циклы for в языке Python хорошо оптимизированы, использование цикла без необходимости все равно замедляет работу программы, а для конкатенации двух строк приходится создавать новую строку, что также отнимает некоторое время. В версии, созданной человеком, решение находится за счет применения метода join с символом новой строки. Вместо цикла здесь разовая операция, а вместо создания нового строкового объекта для каждого слова создается только один такой объект для всей строки. В этой практической работе код, сгенерированный средствами ИИ, справляется со своей задачей, но стоит вам запустить эту программу в продакшен под рабочей нагрузкой, и низкая производительность может стать проблемой. Итоги Для работы со строками в языке Python имеются мощные средства обработки текста, включая поиск и замену, очистку символов и изменение регистра. Строки являются неизменяемыми, то есть их нельзя изменить напрямую. Операции, которые, казалось бы, изменяют саму строку, в действительности возвращают ее измененную копию. Существует три способа объединения строк и значений выражений Python: метод format, f-строки и устаревший метод с применением %. Метод format имеет развитый язык форматирования для управления выводом. F-строки — это очень удобный и популярный способ создания строк, содержащих значения переменных и выражений. В Python имеется байтовый тип данных bytes, содержащий целые числа в диапазоне от 0 до 255. Такой тип объектов похож на строковый, но для преобразования в строки такие данные нужно декодировать.
7 Словари В этой главе: 3 Определение словаря 3 Операции со словарем 3 Определение того, что может служить ключом 3 Создание разреженных матриц 3 Использование словарей в качестве кэшей 3 Доверие к эффективности словарей В этой главе рассматриваются словари — этим термином в языке Python обозначаются ассоциативные массивы или карты, реализованные в нем с помощью хеш-таблиц. Словари чрезвычайно полезны даже в простых программах. Поскольку словари знакомы многим программистам в меньшей степени, чем другие базовые структуры данных (такие, как списки и строки), некоторые примеры, иллюстрирующие способы применения словарей, получились чуть сложнее соответствующих примеров для других встроенных структур данных. Возможно, для полного понимания ряда примеров этой главы вам стоит прочесть отдельные части главы 8.
   7.1. Что такое словарь? 161 7.1. Что такое словарь? Если вы никогда не работали с ассоциативными массивами или хеш-таблицами в других языках, неплохим ключом к пониманию словарей и способов их применения послужит сравнение словарей со списками. Для доступа к значениям в списках используются целые числа, называемые индексами. Они указывают позицию списка, в которой находится заданное значение. В словарях для доступа к значениям используются целые числа, строки или другие объекты Python, называемые ключами. Они указывают, где в словаре находится заданное значение. Иными словами, и списки, и словари предоставляют индексированный доступ к произвольным значениям, однако набор элементов, которые могут использоваться в качестве индексов словаря, намного больше набора значений, которые могут использоваться в качестве индексов списка (причем первый полностью включает в себя второй). Кроме того, механизм, посредством которого словари обеспечивают индексированный доступ, в корне отличается от аналогичного механизма списков. Как в списках, так и в словарях могут храниться объекты любого типа. Значения, хранящиеся в списке, неявным образом упорядочиваются по своей позиции, поскольку индексы, используемые для обращения к ним, представляют собой последовательные целые числа. Возможно, порядок элементов для вас не важен, но при желании им можно воспользоваться. У значений, хранящихся в словаре, неявное упорядочивание относительно друг друга отсутствует, поскольку ключи не ограничиваются числами. Если вы пользуетесь словарем, но вас также интересует порядок элементов (то есть последовательность, в которой они были добавлены), то имейте в виду, что, начиная с Python версии 3.6, словари хранят свои элементы в порядке их поступления. Кроме того, имеется упорядоченный словарь — подкласс словаря, импортируемый из модуля collections и обладающий рядом дополнительных функций, таких как метод move_to_end и возможность доступа к элементам в обратном порядке. Также для элементов словаря можно определить порядок при помощи другой структуры данных (часто это список), в которой порядок элементов хранится в явной форме. Несмотря на все различия между словарями и списками, способы их применения часто кажутся одинаковыми. Сначала программа создает пустой словарь почти так же, как пустой список, но вместо квадратных скобок используются фигурные: x_list = [] y_dic = {} Список Словарь Первая строка создает новый, пустой список и присваивает его переменной x_list. Вторая строка создает новый, пустой словарь и присваивает его переменной y_dic.
   162 Глава 7. Словари После создания словаря в нем можно сохранять значения так, как если бы это был список: y_dic [0] = 'Hello' y_dic [1] = 'Goodbye' Даже в этих командах присваивания проявляются существенные различия в работе со словарями и списками. Попытка проделать то же самое со списком приведет к ошибке, так как в Python присваивание значения несуществующей позиции списка недопустимо. Например, при попытке присвоить значение 0-му элементу списка x_list произойдет ошибка: x_list [0] = 'Hello' -------------------------------------------------------------------------IndexError Traceback (most recent call last) <ipython-input-1-ba4ef4c8f6ab> in <cell line: 3>() 1 x_list = [] 2 y_dic = {} ----> 3 x_list[0] = 'Hello' IndexError: list assignment index out of range Со словарями такой проблемы нет — в них новые позиции создаются по мере необходимости. К значениям, сохраненным в словаре, можно обращаться и работать с ними: print(y_dic[0]) print(y_dic[1] + ", Friend.") Hello Goodbye, Friend. В целом словари кажутся весьма похожими на списки. Теперь о главном отличии: в словаре можно хранить (и использовать) значения по ключам, которые не являются целыми числами: y_dic['greeting'] = "Bon jour" y_dic['farewell'] = "Adios" print(y_dic['greeting']) print(y_dic['farewell'] + ", Friend.") Bon jour Adios, Friend. Проделать такое со списками, безусловно, невозможно! Если индексы списков могут быть только целыми числами, ключи словарей куда менее ограниченны; это могут быть числа, строки или любой другой объект Python. Таким образом, словари становятся естественным инструментом для тех задач, которые не под силу спискам. К примеру, приложение телефонного справочника удобнее реализовать на базе словарей, чем на базе списков, поскольку телефонный номер может храниться и индексироваться по фамилии абонента.
   7.2. Другие операции со словарями 163 Словарь — это способ сопоставления одного произвольного набора объектов с другим, связанным с ним, но столь же произвольным набором объектов. Хорошим аналогом словарей Python в реальном мире служат толковые и переводные словари, а также тезаурусы. Чтобы увидеть, насколько естественно выглядит это соответствие, начнем создавать англо-французский словарь названий цветов: english_to_french = {} Создаем пустой словарь english_to_french['red'] = 'rouge' english_to_french['blue'] = 'bleu' Сохраняем в нем три слова english_to_french['green'] = 'vert' print("red is", english_to_french['red']) Получаем значение для ключа «red» red is rouge Попробуйте сами: создание словаря Напишите код, который запрашивает у пользователя три имени и три возраста. После ввода имен и возрастов запросите у пользователя одно из имен и выведите соответствующий возраст. 7.2. Другие операции со словарями Кроме простого присваивания и обращения к элементам, словари поддерживают и другие операции. Словарь можно явно определить как последовательность пар «ключ-значение», разделенных запятыми: english_to_french = {'red': 'rouge', 'blue': 'bleu', 'green': 'vert'} # Функция len возвращает количество элементов в словаре: len(english_to_french) 3 Для получения всех ключей в словаре применяется метод keys. В языке Python он часто используется для перебора содержимого словаря с помощью цикла for, который описан в главе 8: list(english_to_french.keys()) ['green', 'blue', 'red'] В Python 3.5 и более ранних версиях последовательность ключей в списке, возвращаемом методом keys, не имеет значения; ключи не обязательно отсортированы, и они не всегда следуют в порядке их создания. Ваш код Python может вывести ключи в очередности, отличной от указанной в моем примере. Для получения отсортированной последовательности ключей можно преобразовать ее в список, присвоить этот список переменной и отсортировать его. Однако, как уже упоминалось ранее, начиная с Python 3.6, словари сохраняют порядок создания ключей и возвращают их в той же последовательности. Кроме того, можно получить все значения, хранящиеся в словаре, методом values:
   164 Глава 7. Словари list(english_to_french.values()) ['vert', 'bleu', 'rouge'] Этот метод используется значительно реже, чем метод keys. Метод items возвращает все ключи и связанные с ними значения в виде последовательности кортежей: list(english_to_french.items()) [('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')] Как и метод keys, items часто задействуется в циклах for для перебора содержимого словаря. Ключевое слово del может использоваться для удаления записи (то есть пары «ключ-значение») из словаря: list(english_to_french.items()) [('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')] del english_to_french['green'] list(english_to_french.items()) [('blue', 'bleu'), ('red', 'rouge')] Объекты-представления словаря Методы keys, values и items возвращают не списки, а объекты-представления (view objects), которые ведут себя как последовательности, однако динамически обновляются при каждом изменении словаря. Вот почему в этих примерах приходится использовать функцию list для того, чтобы отобразить их как списки. В противном случае они ведут себя как последовательности, позволяя коду перебирать их в цикле for, проверять наличие элементов при помощи оператора in и т. п. Объект-представление, возвращаемый методом keys (а в некоторых случаях методом items), также ведет себя как множество, допуская операции объединения, разности и пересечения. В языке Python попытки обратиться к ключу, отсутствующему в словаре, приводят к ошибке. Чтобы ее избежать, можно проверить словарь на наличие ключа оператором in, который возвращает True, если в словаре есть значение, связанное с заданным ключом, и False в противном случае: 'red' in english_to_french True 'orange' in english_to_french False
   7.2. Другие операции со словарями 165 В качестве альтернативы можно воспользоваться функцией get. Эта функция возвращает значение, связанное с ключом, если этот ключ присутствует в словаре, или второй аргумент, если ключ не найден: print(english_to_french.get('blue', 'No translation')) Chartreuse не найдено, возвращается сообщение «Перевод отсутствует» bleu >>> print(english_to_french.get('chartreuse', 'No translation')) No translation Второй аргумент не является обязательным. При его отсутствии get возвращает None, если ключ в словаре не найден. Аналогичным образом, если вы хотите гарантированно получить значение ключа и убедиться в том, что оно установлено в словаре по умолчанию, можно воспользоваться методом setdefault: print(english_to_french.setdefault('chartreuse', 'No translation')) No translation Chartreuse не найдено и добавляется в качестве ключа, а возвращаемое «Перевод отсутствует» добавляется в качестве его значения Различие между get и setdefault заключается в том, что после вызова setdefault в словаре появляется ключ 'chartreuse' со значением 'No translation'. Также существует подкласс defaultdict класса dict, который можно импортировать из модуля collections. Экземплярам defaultdict можно задать значение по умолчанию, и они будут автоматически работать так же, как setdefault. Для получения копии словаря применим метод copy: x = {0: 'zero', 1: 'one'} y = x.copy() y {0: 'zero', 1: 'one'} Этот метод создает поверхностную копию словаря; скорее всего, в большинстве случаев вам будет этого достаточно. Для словарей, содержащих в качестве значений изменяемые объекты (например, списки или другие словари), может потребоваться создать глубокую (полную) копию функцией copy.deepcopy. С концепцией поверхностного и глубокого копирования мы познакомились в главе 5. Метод update обновляет первый словарь, к которому он применяется, всеми парами «ключ-значение» из второго словаря. Для ключей, общих для обоих словарей, значения из второго словаря переопределяют значения первого: z = {1: 'ONE', 2: 'Two'} x = {0: 'zero', 1: 'one'} x.update(z) x {0: 'zero', 1: 'ONE', 2: 'Two'}
   166 Глава 7. Словари Методы словарей предоставляют в ваше распоряжение полный набор инструментов для работы со словарями. В качестве краткой справки важнейшие функции словарей приведены в табл. 7.1. Таблица 7.1. Операции со словарями Операция Описание Пример {} Создает пустой словарь x = {} len Возвращает количество записей в словаре len(x) keys Возвращает объект-представление, содержащий все ключи в словаре x.keys() values Возвращает объект-представление, содержащий все значения в словаре x.values() items Возвращает объект-представление, содержащий все элементы в словаре x.items() del Удаляет запись из словаря del(x[key]) in Проверяет наличие ключа в словаре 'y' in x get Возвращает значение ключа или настраиваемое значение по умолчанию x.get('y', None) setdefault Возвращает значение, если ключ присутствует в словаре; в противном случае связывает с ключом значение по умолчанию и возвращает его x.setdefault('y', None) copy Создает поверхностную копию словаря y = x.copy() update Объединяет записи двух словарей x.update(z) Таблица 7.1 не содержит полный список операций со словарями, его можно найти в документации стандартной библиотеки Python. Быстрая проверка: операции со словарями Допустим, имеются словари x = {'a':1, 'b':2, 'c':3, 'd':4} и y = {'a':6, 'e':5, 'f':6}. Каково будет содержимое объекта-словаря, на который ссылается переменная x, при выполнении следующих фрагментов кода? del x['d'] z = x.setdefault('g', 7) x.update(y) 7.3. Подсчет слов Предположим, имеется файл со списком слов, по одному слову в каждой строке. Требуется узнать, сколько раз каждое слово встречается в файле. Словари позволяют легко решить эту задачу:
   7.4. Что может служить ключом? sample_string = "To be or not to be" occurrences = {} for word in sample_string.split(): occurrences[word] = occurrences.get(word, 0) + 1 for word in occurrences: print("The word", word, " occurs", occurrences[word], "times in the string") The The The The The word word word word word 167 Увеличивает счетчик вхождений слова To occurs 1 times in the string be occurs 2 times in the string or occurs 1 times in the string not occurs 1 times in the string to occurs 1 times in the string Этот пример прекрасно иллюстрирует возможности словарей, используя каждое слово в качестве ключа словаря и метод get() для подсчета вхождений этого слова. Код отличается не только простотой, но и быстродействием, поскольку в Python операции со словарями значительно оптимизированы. Этот паттерн настолько удобен, что его стандартизировали в виде класса Counter модуля collections стандартной библиотеки. 7.4. Что может служить ключом? В предыдущих примерах в качестве ключей выступали строки, но это вовсе не обязательно. Ключом к словарю может быть любой объект Python, который является неизменяемым и хешируемым. Как упоминалось ранее, в Python любой объект, который может быть изменен, называется изменяемым. Списки изменяемы, поскольку можно добавлять, изменять и удалять их элементы. Словари изменяемы по тем же причинам. Числа неизменяемы. Если переменная x ссылается на число 3, то после присваивания x значения 4 переменная будет ссылаться на другое число (4), но само число 3 при этом никак не изменится. Строки тоже неизменяемы. Оператор list[n] возвращает n-й элемент списка, string[n] возвращает n-й символ строки, list[n] = value изменяет n-й элемент списка. Однако выражение string[n] = character в Python недопустимо и вызывает ошибку, поскольку строки в Python неизменяемы. К сожалению, условие неизменяемости и хешируемости ключей означает, что списки не могут быть ключами словарей, хотя во многих случаях было бы удобно работать с ключами-списками. К примеру, для хранения данных о человеке целесообразно воспользоваться ключом, состоящим из его имени и фамилии, что можно было бы легко реализовать, используя в качестве ключа двухэлементный список. В Python эта проблема решается за счет кортежей, которые, по существу, являются неизменяемыми списками; кортежи создаются и функционируют так же, как списки, однако после создания их нельзя изменить. Существует и другое
   168 Глава 7. Словари ограничение: ключи словарей должны быть хешируемыми, и здесь мы сталкиваемся с еще одной трудностью. Чтобы быть хешируемым, объект должен иметь хеш-значение (предоставляемое методом __hash__), которое остается неизменным на протяжении всего срока существования значения. Это означает, что кортежи, содержащие изменяемые объекты, не являются хешируемыми, хотя сами по себе кортежи формально остаются неизменяемыми. Лишь кортежи, не содержащие вложенных изменяемых объектов, являются хешируемыми и, следовательно, могут выступать в качестве ключей в словарях. В табл. 7.2 показано, какие из встроенных типов объектов Python являются неизменяемыми, хешируемыми и способными выступать в качестве ключей словаря. Таблица 7.2. Типы объектов Python, способные выступать в качестве ключей словаря Тип Python Неизменяемый? Хешируемый? Ключ словаря? int: целое число Да Да Да Float: число с плавающей точкой Да Да Да Boolean: логическое значение Да Да Да Complex: комплексное число Да Да Да Str: строка Да Да Да Bytes: неизменяемый байтовый Да Да Да Bytearray: изменяемый байтовый Нет Нет Нет List: список Нет Нет Нет Tuple: кортеж Да Иногда Иногда Set: множество Нет Нет Нет Frozenset: неизменяемое множество Да Да Да Dictionary: словарь Нет Нет Нет объект объект В следующих разделах приводятся примеры, демонстрирующие взаимодействие кортежей и словарей. Быстрая проверка: что может быть ключом? Определите, какие из следующих выражений могут быть ключами словаря: 1; 'bob'; ('tom', [1, 2, 3]); ["filename"]; "filename"; ("filename", "extension"). 7.5. Разреженные матрицы В математике матрица представляет собой двумерную числовую сетку, которая в учебниках обычно заключается в квадратные скобки, как показано на рисунке:
   7.5. Разреженные матрицы 169 Достаточно стандартным способом представления таких матриц является список списков. В Python матрица представляется так: matrix = [[3, 0, -2, 11], [0, 9, 0, 0], [0, 7, 0, 0], [0, 0, 0, -5]] К элементам матрицы можно обращаться по номерам строк и столбцов: element = matrix[rownum][colnum] Однако в некоторых приложениях, к примеру в области прогнозирования погоды, матрицы бывают огромными. Число строк и столбцов исчисляется тысячами, и, следовательно, матрица может содержать миллионы элементов. Кроме того, такие матрицы обычно включают много нулевых элементов. В некоторых приложениях все элементы матрицы, кроме небольшого процента, равны нулю. Для экономии памяти такие матрицы обычно хранятся в форме, в которой фактически в памяти находятся только ненулевые элементы. Подобные представления называются разреженными матрицами. Разреженные матрицы легко реализовать, используя словари с индексами-кортежами. Например, предыдущая разреженная матрица может быть представлена следующим образом, где каждый ключ — это строка и столбец значения: matrix = {(0, 0): 3, (0, 2): -2, (0, 3): 11, (1, 1): 9, (2, 1): 7, (3, 3): -5} Теперь для обращения к отдельному элементу матрицы с заданными номерами столбца и строки можно воспользоваться следующим фрагментом кода: if (rownum, colnum) in matrix: element = matrix[(rownum, colnum)] else: element = 0 Чуть менее понятный (но более эффективный) способ решения этой задачи основан на методе словаря get, которому можно указать вернуть 0, если он не находит ключ в словаре; в противном случае возвращается значение, связанное с этим ключом. Это позволяет избежать одного лишнего обращения к словарю: element = matrix.get((rownum, colnum), 0) Если вы собираетесь много работать с матрицами, вам стоит ознакомиться с NumPy — пакетом для числовых вычислений.
   170 Глава 7. Словари 7.6. Словари как кэши В этом разделе показано, как словари могут использоваться в качестве кэшей — структур данных, которые хранят результаты, чтобы избежать их многократных пересчетов. Предположим, вам нужна функция под названием sole, которая принимает три целых числа в качестве аргументов и возвращает результат. Функция может выглядеть примерно так: def sole(m, n, t): # . . . выполнить некие сложные вычисления. . . return(result) Но если выполнение этой функции занимает очень много времени, а сама функция вызывается десятки тысяч раз, это может очень сильно замедлить работу программы. Теперь предположим, что функция sole может быть вызвана с 200 различными комбинациями аргументов во время любого запуска программы. То есть вызов sole(12, 20, 6) может произойти 50 и более раз за время выполнения программы — и аналогично для многих других комбинаций аргументов. Исключив пересчет sole для одинаковых аргументов, вы сэкономите массу времени. Можно использовать словарь с кортежами в качестве ключей, например: sole_cache = {} def sole(m, n, t): if (m, n, t) in sole_cache: return sole_cache[(m, n, t)] else: # . . . выполнить некие сложные вычисления . . . sole_cache[(m, n, t)] = result return result Измененная функция sole задействует глобальную переменную для хранения предыдущих результатов. Глобальная переменная представляет собой словарь, ключами которого являются кортежи, соответствующие комбинациям аргументов, ранее переданным функции sole. Таким образом, каждый раз, когда функция sole передает комбинацию аргументов, для которой результат уже был вычислен, она возвращает этот сохраненный результат вместо того, чтобы пересчитывать его заново. Попробуйте сами: работа со словарями Предположим, вы пишете программу, которая должна выполнять функции электронной таблицы. Как можно использовать словарь для хранения содержимого этой таблицы? Напишите код для сохранения значения в определенной ячейке и извлечения его из нее. Какими недостатками может обладать такой подход?
   7.8. Практическая работа: подсчет слов 171 7.7. Эффективность словарей Если раньше вы работали с традиционными компилируемыми языками программирования, возможно, вы с настороженностью относитесь к словарям, считая, что они уступают по эффективности спискам (массивам). Однако в действительности реализация словарей Python работает достаточно быстро. Многие внутренние функции языка зависят от словарей, поэтому была проведена серьезная работа по их оптимизации. Поскольку все структуры данных Python хорошо оптимизированы, вам не нужно ломать голову над тем, какой способ быстрее или эффективнее. Если задача проще и понятнее решается с помощью словаря, а не списка, выбирайте первое и рассматривайте альтернативы лишь в том случае, когда словари со всей очевидностью приводят к неприемлемому снижению быстродействия. 7.8. Практическая работа: подсчет слов Во время предыдущей практической работы мы взяли фрагмент первой главы «Моби Дика», привели его к единому регистру, удалили знаки препинания и пословно записали в файл. Сейчас мы используем файл, созданный в главе 6, чтобы подсчитать количество вхождений каждого слова. Для этого мы считаем файл и воспользуемся словарем, как описано выше, для подсчета вхождений, используя слова в качестве ключей, а число их повторов — в качестве значений. После обработки файла мы выведем по пять наиболее и наименее распространенных слов с указанием их количества. Чтобы открыть и прочитать файл, можно прибегнуть к коду, аналогичному тому, что мы использовали в прошлый раз, однако сейчас нам не понадобится файл вывода: with open("moby_01_clean.txt", "r") as infile: for word in infile: # использовать метод stip() для удаления лишних пробельных символов # увеличить счетчик для текущего слова Помимо словаря для подсчета вхождений вам понадобится метод items() для получения ключей и значений в виде последовательности кортежей, которые можно отсортировать. Возможно, потребуется освежить в памяти главу 5, посвященную спискам. Подсказка: прежде чем приступить к обработке слов, вам нужно создать пустой словарь, в котором будут храниться счетчики слов. 7.8.1. Решение задачи с помощью кода, сгенерированного ИИ И снова главным фактором в применении средств ИИ для генерации программного кода станет создание промпта. Как и в предыдущей главе, не стоит
   172 Глава 7. Словари тратить время на объяснение чат-боту, как почитать данные из файла, однако в данном случае, если мы хотим быть уверены в том, что он задействует словарь, а не класс Counter, нам нужно будет это уточнить. Поэтому в промпте должно быть указано следующее. 1. Открыть файл moby_01_clean.txt. 2. С помощью словаря подсчитать количество вхождений каждого слова. 3. Вывести пять наиболее часто встречающихся слов и число их вхождений. 4. Вывести пять наименее часто встречающихся слов и число их вхождений. Создайте промпт в диалоге с чат-ботом на основе искусственного интеллекта и посмотрите, какое решение вы получите. 7.8.2. Решения и обсуждение Хотя эта задача немного сложнее предыдущей, она все же достаточно проста, чтобы мое решение и решения инструментов ИИ были похожи. Последние, впрочем, по-прежнему отличаются выбором чуть менее эффективных способов. Программное решение, созданное человеком Решение этой задачи, по сравнению с предыдущей, требует более тщательного обдумывания, поскольку после считывания файла необходимо осуществить дополнительную обработку, а также не забыть создать словарь для хранения вхождений слов перед их обработкой: word_count = {} with open("moby_01_clean.txt", "r") as infile: for word in infile: # использовать метод stip() для удаления лишних пробельных символов word = word.strip() # увеличить счетчик для текущего слова word_count.setdefault(word, 0) word_count[word] += 1 word_list = list(word_count.items()) word_list.sort(key=lambda x: x[1]) print("Most common words:") for word in reversed(word_list[-5:]): print(word) print("\nLeast common words:") for word in word_list[:5]: print(word) Если слово отсутствует, добавляет счетчик и устанавливает его равным нулю Увеличивает счетчик на 1 Получает элементы в форме списка кортежей Сортирует элементы по частоте их вхождений Меняет порядок на обратный, более высокая частота идет первой Ключом к данному решению является применение метода словаря setdefault(), чтобы установить значение для слова равным нулю, если его еще нет в словаре. Затем мы добавляем 1 к значению слова в словаре.
   7.8. Практическая работа: подсчет слов 173 После подсчета всех слов мы задействуем метод словаря items(), чтобы получить список кортежей с количеством вхождений слов, который можно отсортировать по частоте. Далее мы можем перевернуть этот список, чтобы вывести наиболее часто встречающиеся слова, и вместе с тем использовать исходный порядок, чтобы вывести наименее часто встречающиеся. Программное решение, сгенерированное ИИ Как и в случае с предыдущей задачей, инструменты ИИ генерируют работающий код с хорошими комментариями. Напомню, в промпте должен быть явно указан запрет на использование класса Counter, поэтому я составила его следующим образом: Открой файл moby_01_clean.txt и с помощью словаря подсчитай количество вхождений каждого слова. Не используй объекты класса Counter. Выведи пять наиболее часто встречающихся слов и количество их вхождений. Выведи пять наименее часто встречающихся слов и количество их вхождений. Colaboratory и Copilot предложили очень похожие способы. На этот раз мы рассмотрим решение Colaboratory: # Открыть файл и считать текст в переменную. with open('moby_01_clean.txt', 'r') as f: text = f.read() Считывает файл # Разделить текст на слова. words = text.split() Разбивает файл на список слов # Создать словарь для подсчета вхождений каждого слова. word_counts = {} for word in words: if word not in word_counts: Использует оператор if для проверки word_counts[word] = 0 наличия слова в словаре word_counts[word] += 1 # Найти пять наиболее часто встречающихся слов и количество их вхождений. most_common = sorted(word_counts.items(), key=lambda item: item[1], reverse=True)[:5] Создает обратный отсортированный список для получения наиболее часто встречающихся слов # Найти пять наименее часто встречающихся слов и количество их вхождений. least_common = sorted(word_counts.items(), key=lambda item: item[1])[:5] # Вывести результаты. print("Most common words:") for word, count in most_common: print(f"{word}: {count}") print("\nLeast common words:") for word, count in least_common: print(f"{word}: {count}") Создает обычный отсортированный список для получения наименее часто встречающихся слов
   174 Глава 7. Словари Версия Copilot похожа; оба решения считывают весь файл в память, а затем разбивают его на один большой список слов. Это может создать проблему, если размер файла окажется слишком велик. Версия, написанная программистом, учитывает преимущество формата записи (по одному слову на строку) и обрабатывает файл построчно, что позволяет избежать этой проблемы. Любопытно, что, даже если бы чат-ботам сообщили в промпте, что в каждой строке файла записано лишь одно слово, они бы не воспользовались этим фактом. Впрочем, между двумя решениями ИИ есть ряд примечательных различий. Так, в версии Copilot применяется чуть более подробный способ проверки списка: for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 Обратите внимание, что в этом случае, если слова еще нет в словаре, оно добавляется со значением 1, поскольку проверка не проводилась бы, если бы слово не было найдено. Если же это слово уже внесено в словарь, его значение увеличивается на единицу. Теоретически такой алгоритм может работать чуть быстрее, однако ни то ни другое решение ИИ не сравнится с использованием метода setdefault (или get), как это сделано в версии программиста. Второе отличие заключается в способе определения наиболее и наименее часто встречающихся слов. Для этого список элементов необходимо отсортировать хотя бы раз. Проблема с решением Colaboratory заключается в том, что оно выполняет две сортировки: одну в обратном порядке и другую в обычном. Очевидно, что выполнение двойной работы может стать проблемой при обработке больших наборов данных. Версия Copilot выполняет только одну сортировку, в обратном порядке, а затем выбирает первые пять элементов как наиболее часто встречающиеся, а последние пять — как наименее: # Отсортировать количество слов в порядке убывания sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) Программа, написанная человеком, тоже выполняет всего одну сортировку в обычном порядке, а затем, чтобы получить наиболее часто встречающиеся элементы, задействует функцию reversed для среза из последних пяти элементов, так что они выводятся от наиболее часто встречающихся к наименее часто встречающимся. Функция reversed возвращает итератор, так что фактически новый список не создается даже для этих пяти элементов. На самом деле, обратный срез сработал бы так же эффективно. Как и в ходе предыдущих практических работ, оба инструмента ИИ создали работающий программный код, но с вариациями, которые могут вызывать проблемы в производственных средах с большими наборами данных.
   Итоги 175 Итоги Словари — мощные структуры данных, используемые для множества различных целей, включая внутреннюю оптимизацию самого языка Python. Доступ к элементам словаря осуществляется по ключам, а не по их порядку в словаре. Ключи словаря должны быть неизменяемыми, и любой неизменяемый объект может стать ключом словаря. Все словари хранят элементы в том порядке, в котором они добавлялись, но упорядоченные словари (из модуля collections) обладают рядом дополнительных функций. В силу того, что ключи выступают как часть данных, словари можно применять для таких задач, как подсчет элементов, кэширование значений или создание разреженных матриц. Благодаря применению ключей доступ к наборам данных осуществляется гораздо эффективнее и с меньшим объемом кода по сравнению с другими решениями.
8 Управляющие конструкции В этой главе: 3 Принятие решений: оператор if-elif-else 3 Структурное сопоставление шаблонов 3 Повторение кода с помощью цикла while 3 Итерация по списку с помощью цикла for 3 Применение генераторов списков и словарей 3 Разграничение операторов и блоков при помощи отступов 3 Оценка логических значений и выражений Python предоставляет полный набор элементов управляющих конструкций, включая циклы и условные операторы. Все эти элементы подробно рассматриваются далее в главе. 8.1. Оператор if-elif-else Одной из важнейших управляющих конструкций в большинстве языков программирования является ветвление, то есть принятие решения о выполнении или пропуске фрагмента кода на основе некоторого условия. В Python, как и во многих других языках, эта конструкция состоит из трех частей: блока if, необязательного одного или нескольких блоков elif, которые проверяются лишь
   8.1. Оператор if-elif-else 177 в том случае, если условие if (и все предыдущие блоки elif) ложно (false), и последнего необязательного блока else, который выполняется лишь в том случае, если не были выполнены ни блок if, ни блок(и) elif. Общая форма конструкции if-elif-else в Python выглядит следующим образом1: if условие1: тело1 elif условие2: тело2 elif условие3: тело3 . . . elif условие(n-1): тело(n-1) else: тело(n) Это означает: если условие1 равно True (истинно), выполняется тело1; в противном случае, если условие2 равно True, выполняется тело2; в противном случае… и так далее, пока не будет найдено условие, равное True, или не будет достигнут блок else (тогда выполняется тело(n)). Блоки тело представляют собой последовательности из одного или нескольких операторов Python, разделенных символами новой строки и находящихся на одном уровне отступа. К примеру, если вы хотите узнать, является ли предположение в игре на угадывание чисел меньше или больше загаданного значения или равно ему, можно написать следующий код: guess = 4 target = 7 if guess < target: print("Low") elif guess == target: print("Correct!") else: print("High") Если предположение не меньше и не угадано, оно должно быть больше Low Разумеется, для проверки каждого конкретного условия вся конструкция целиком не потребуется. Вы можете опустить части elif и else. Если условие не может найти тело для выполнения (ни одно условие не дает результат True или часть else отсутствует), оно не выполняет никаких действий. Наличие тела после оператора if является обязательным. Впрочем, здесь можно использовать оператор pass (как и в любом месте Python, где синтаксически 1 В реальном коде обычно используют английские имена переменных, но для понимания примеров здесь и далее они переведены на русский. — Примеч. ред.
   178 Глава 8. Управляющие конструкции требуется оператор). Оператор pass служит в качестве «заполнителя», но он не выполняет никаких действий: if x < 5: pass else: x = 5 8.2. Структурное сопоставление шаблонов с помощью оператора match Начиная с версии 3.10, в Python появился оператор match-case, позволяющий выбирать из нескольких условий. Он похож на операторы case и switch в других языках программирования, однако обладает более широкими возможностями. Официальное название для функции оператора match — структурное сопоставление шаблонов (structural pattern matching), и слово «структурное» играет здесь немаловажную роль: в отличие от своих аналогов в других языках, оператор match-case может устанавливать соответствие не только на основе равенства значений, но и совпадения по типам. Общая структура оператора match-case выглядит следующим образом: match выражение: case проверяемое_выражение: блок кода при совпадении case _: блок кода при отсутствии совпадения (необязательный) Обычно выражение (expression) представляет собой переменную или объект, а проверяемое_выражение (test_expression) может быть конкретным значением или объектом либо функцией типа или конструктором класса. Также можно сопоставить несколько вариантов с помощью оператора | (ИЛИ). Дополнительно можно указать вариант case_, который выбирается при отсутствии совпадений с остальными проверяемыми выражениями. Если ни одно из условий не совпадает, код не выполняется. Например, следующий код проверит совпадения со строкой "A" или целым числом 0, одним из целочисленных значений (1, 2 или 3), или любой другой строкой либо любым другим целым числом: x = 5 match x: Совпадает со строкой «A» case "A": print("A") Совпадает с любой другой строкой case str(): print("some other string") Равно 0 case 0: print("Zero") Совпадает с любым из целых чисел 1, 2 или 3 case 1 | 2 | 3: print(f"value {x} in range") Совпадает с любым другим целым числом case int(): Выполняется, если совпадений print(f"The integer {x}") не обнаружено (необязательно) case _: print("Neither string nor int")
   match x: Совпадает со строкой «A» case "A": print("A") Совпадает с любой другой строкой case str(): print("some other string") Равно 0 179 8.3. Цикл while case 0: print("Zero") Совпадает с любым из целых чисел 1, 2 или 3 case 1 | 2 | 3: print(f"value {x} in range") Совпадает с любым другим целым числом case int(): Выполняется, если совпадений print(f"The integer {x}") не обнаружено (необязательно) case _: print("Neither string nor int") Сопоставление шаблонов — это очень мощный, но при этом обманчиво сложный инструмент. Принимая во внимание все возможные способы сопоставления выражений, вполне возможно получить код, который поведет себя непредсказуе­ мо. Перед началом применения match в коде стоит внимательно ознакомиться с документацией (неплохой отправной точкой может послужить руководство по языку Python на сайте https://docs.python.org), а также предусмотреть возможные пограничные случаи и способы их проверки. 8.3. Цикл while Базовый цикл while уже неоднократно встречался вам в книге. Полный цикл while выглядит так: while условие: тело else: код после цикла Здесь условие является логическим выражением (при вычислении которого будет получен результат True или False). Пока условие равно True, тело цикла будет выполняться повторно. Если же условие принимает значение False, то цикл while выполняет блок кода после цикла, а затем завершается. Если условие изначально равно False, тело не будет выполнено ни разу — только блок кода после цикла. Тело и код после цикла представляют собой последовательности из одного или нескольких операторов Python, разделенных символами новой строки и находящихся на одном уровне отступа. Интерпретатор Python использует этот уровень для их разграничения. Никакие другие ограничители (например, квадратные или фигурные скобки) не требуются. Имейте в виду, что блок else в цикле while не является обязательным и применяется редко. Это объясняется тем, что при отсутствии оператора break в теле цикла вот такой цикл: while условие: тело else: код после цикла и такой цикл: while условие: тело код после цикла
   180 Глава 8. Управляющие конструкции делают одно и то же, однако вторая форма более понятна. С другой стороны, если в теле цикла используется оператор break, то блок else не выполняется. Это может быть полезно, так как избавляет от необходимости устанавливать флаг перед break и проверять его после завершения цикла. К примеру, цикл while, который повторяется до тех пор, пока пользователь не введет "Q", может выглядеть так: response = "" while response != "Q": response = input("Q to quit, B to break") if response == "B": break else: print("no break") Оператор break завершит цикл while с пропуском блока else Блок else выполнится лишь в том случае, если оператор break не был вызван В теле цикла while можно использовать два специальных оператора: break и continue. Если выполняется break, то цикл while немедленно завершается и, как говорилось выше, не выполняется даже код после цикла (при наличии else-блока). Если же выполняется continue, то оставшаяся часть тела цикла пропускается, условие проверяется снова и цикл продолжает работу в обычном режиме. 8.4. Цикл for Цикл for в Python отличается от циклов for в некоторых других языках программирования. В традиционном варианте при каждой итерации происходит увеличение значения переменной и его проверка (в частности, так обычно выполняются циклы for в языке C). В Python цикл for перебирает значения, возвращаемые любым итерируемым объектом, то есть любым объектом, который может возвращать последовательность значений. Например, цикл for может перебрать все элементы списка, кортежа или строки. Однако итерируемый объект также может быть специальной функцией range или особым типом функций — генератором или генераторным выражением, обладающим достаточно широкими возможностями. Общую структуру цикла for можно представить следующим образом: for элемент в последовательности: тело else: код после цикла Тело цикла выполняется один раз для каждого элемента последовательности. Вначале переменной элемент присваивается первый элемент последовательности, после чего выполняется тело цикла; затем переменной элемент присваивается второй элемент последовательности, после чего снова выполняется тело цикла, и так далее для всех оставшихся элементов последовательности.
   8.4. Цикл for 181 Блок else не является обязательным. Как и блок else цикла while, он используется достаточно редко, однако, как и в цикле while, блок код после цикла, следующий за else, при вызове оператора break не выполняется. Операторы break и continue выполняют в цикле for те же функции, что и в цикле while. Оператор break немедленно завершает цикл for, при этом не выполняется даже код после цикла (при наличии блока else). При выполнении оператора continue в цикле for оставшаяся часть тела цикла пропускается, и цикл переходит к следующей итерации в обычном режиме. Следующий небольшой цикл выводит обратную величину каждого числа в спис­ ке x, пропуская нули и останавливаясь, если встречается отрицательное число: x = [1.0, 2.0, 3.0, 0.0, -1.0] for n in x: if n == 0.0: print("skipping 0.0") continue elif n < 0.0: print("no negative numbers!") break print(1 / n) else: print("Loop completed normally") Цикл возвращается к началу, переходя к следующему элементу n Цикл немедленно завершается; блок else пропускается Выполняется только в случае, если не вызывается оператор break 1.0 0.5 0.3333333333333333 skipping 0.0 no negative numbers! 8.4.1. Функция range Иногда требуется выполнить цикл с явно заданными индексами (такими, как позиции значений в списке). Чтобы сгенерировать последовательность индексов списка для цикла for, можно воспользоваться функцией range в сочетании с функцией len. Следующий код выводит позиции (индексы) всех элементов списка, в которых будут обнаружены отрицательные числа: x = [1, 3, -7, 4, 9, -5, 4] for i in range(len(x)): if x[i] < 0: print("Found a negative number at index ", i) Для заданного числа n вызов range(n) возвращает последовательность 0, 1, 2, …, n – 2, n – 1. Таким образом, при передаче длины списка (вычисленной функцией len) будет получена последовательность индексов для элементов этого списка. Функция range не создает список целых чисел в Python, как может показаться на первый взгляд. Вместо этого она создает объект типа range (диапазон), предоставляющий последовательность целых чисел по запросу. Это может пригодиться при использовании явных циклов для перебора огромных списков. К примеру,
   182 Глава 8. Управляющие конструкции вместо того чтобы создавать список из 10 миллионов элементов, что потребует существенных затрат памяти, можно воспользоваться вызовом range(10000000), который займет лишь малую часть памяти, и сгенерировать последовательность целых чисел от 0 до (но не включая) 10000000, как того требует цикл for. 8.5. Управление диапазоном с помощью аргументов для начала и шага Можно воспользоваться двумя вариантами функции range, чтобы лучше контролировать создаваемую ею последовательность. Если использовать range с двумя числовыми аргументами, первый аргумент задает начальное число получаемой последовательности, а второй — конечное число, до которого доходит (не включая его самого) получаемая последовательность. Приведем несколько примеров: list(range(3, 7)) [3, 4, 5, 6] list(range(2, 10)) Функция list() используется для возврата значений функции range() в виде списка [2, 3, 4, 5, 6, 7, 8, 9] list(range(5, 3)) [] Функция list() включена здесь лишь для того, чтобы элементы, возвращаемые функцией range, выглядели как список, что, быть может, не так часто используется в коде готового приложения. Возможность генерирования чисел в обратном порядке не поддерживается, поэтому выражение list(range(5, 3)) выводит пустой список. Чтобы вести отсчет в обратном порядке или с любым шагом, отличным от 1, необходимо передать range необязательный третий аргумент, задающий величину шага, с которым ведется отсчет: list(range(0, 10, 2)) [0, 2, 4, 6, 8] list(range(5, 0, -1)) [5, 4, 3, 2, 1] Последовательности, возвращаемые range, всегда включают начальное значение, передаваемое в первом аргументе, и никогда не включают конечное значение, передаваемое во втором аргументе.
   8.8. Функция zip 183 8.6. Цикл for и распаковка кортежа Распаковку кортежа можно использовать для того, чтобы сделать чище некоторые циклы for. Следующий код берет список двухэлементных кортежей и вычисляет значение суммы произведений двух чисел каждого кортежа (достаточно распространенная математическая операция в некоторых областях): somelist = [(1, 2), (3, 7), (9, 5)] result = 0 for t in somelist: result = result + (t[0] * t[1]) То же самое, но чище: somelist = [(1, 2), (3, 7), (9, 5)] result = 0 for x, y in somelist: result = result + (x * y) В этом коде сразу же за ключевым словом for вместо обычной одиночной переменной следует кортеж x, y. При каждой итерации цикла for x содержит элемент 0 текущего кортежа из list (списка), а у — элемент 1 текущего кортежа из list. Такое применение кортежей реализовано в Python для удобства; оно показывает Python, что каждый элемент списка должен быть кортежем соответствующего размера для распаковки в переменные, имена которых указаны после for. 8.7. Функция enumerate Распаковку кортежа можно сочетать с функцией enumerate для перебора как самих элементов, так и их индексов. Такое решение схоже с применением функции range, однако у него есть преимущество: программный код выглядит яснее и понятнее. Как и в примере выше, следующий код выводит все позиции списка, в которых обнаружены отрицательные числа: Функция enumerate() возвращает x = [1, 3, -7, 4, 9, -5, 4] кортежи вида: индекс, элемент for i, n in enumerate(x): if n < 0: Обращается к элементу print("Found a negative number at index ", i) Индекс тоже доступен без индекса Функция enumerate возвращает кортежи вида индекс, элемент. При этом можно обратиться к любому из этих двух значений по отдельности. 8.8. Функция zip Иногда бывает полезно объединить два и более итерируемых объекта до того, как выполнить их перебор в цикле. Функция zip берет соответствующие элементы
   184 Глава 8. Управляющие конструкции из одного или нескольких итерируемых объектов и объединяет их в кортежи, пока не достигнет конца самого короткого итерируемого объекта: x = [1, 2, 3, 4] y = ['a', 'b', 'c'] z = zip(x, y) list(z) В списке y 3 элемента; в списке x — 4 [(1, 'a'), (2, 'b'), (3, 'c')] В списке z только 3 элемента Попробуйте сами: циклы и операторы if Допустим, имеется список x = [1, 3, 5, 0, -1, 3, -2], из которого нужно удалить все отрицательные числа. Напишите код для решения этой задачи. Как бы вы подсчитали общее количество отрицательных чисел в списке y = [[1, -1, 0], [2, 5, -9], [-2, -3, 0]]? Какой код вы бы использовали для вывода very low, если значение x меньше −5; low — если оно в диапазоне от −5 (включительно) до 0 (не включительно); neutral, если оно равно 0; high, если оно в диапазоне от 0 (не включительно) до 5 (включительно); и very high, если оно больше 5? 8.9. Генераторы списков, множеств и словарей Использование цикла for для перебора списка, изменения или выбора отдельных элементов и создания нового списка либо словаря очень распространено. Такие циклы часто выглядят примерно так: x = [1, 2, 3, 4] x_squared = [] for item in x: x_squared.append(item * item) x_squared [1, 4, 9, 16] Подобные операции встречаются настолько часто, что в Python для них существует специальная сокращенная запись, называемая генератором (comprehension). Генератор списка, множества или словаря можно представить как однострочную запись цикла for, которая создает из некой последовательности новый список, множество или словарь. Синтаксис генератора списка выглядит так: новый_список = [выражение1 for переменная in старый_список if выражение2] Обратите внимание, что на месте старый_список может быть любая последовательность, а результатом генератора становится список именно благодаря заключению его в квадратные скобки []. Аналогичным образом будет выглядеть и генератор множества, однако в таком случае все выражение будет заключено в фигурные скобки {}, а возвращаемое значение будет множеством:
   8.9. Генераторы списков, множеств и словарей 185 новое_множество = {выражение1 for переменная in старый_список if выражение2} И наконец, генератор словаря похож на генератор множества, но нуждается в отдельных выражениях для ключа и для значения (к примеру, список может содержать двухэлементные кортежи): новый_словарь = {выражение1:выражение2 for переменная in старый_список if выражение3} В обоих случаях основная часть выражения напоминает начало цикла for — for переменная in список, также присутствует некое выражение, использующее эту переменную для создания нового ключа или значения, и необязательное условное выражение, которое на основании значения переменной принимает решение о ее включении в новый список или словарь. Следующий код выполняет те же действия, что и предыдущий, но с использованием генератора списка: x = [1, 2, 3, 4] x_squared = [item * item for item in x] x_squared [1, 4, 9, 16] Для выбора элементов из списка можно даже задействовать операторы if: x = [1, 2, 3, 4] x_squared = [item * item for item in x if item > 2] x_squared [9, 16] Генераторы словарей работают почти так же, но необходимо указать как ключ, так и значение. Если нужно написать код, похожий на приведенный выше, но так, чтобы число было ключом, а квадрат числа — значением в словаре, можно воспользоваться генератором словаря, например, таким образом: x = [1, 2, 3, 4] x_squared_dict = {item: item * item for item in x} x_squared_dict {1: 1, 2: 4, 3: 9, 4: 16} Генераторы списков, множеств и словарей обладают исключительной гибкостью и мощью, и потому, освоив их, вам будет куда проще работать со списками. Старайтесь экспериментировать с ними и применять всякий раз, когда пишете цикл for для обработки списка элементов. 8.9.1. Генераторные выражения В генераторных выражениях не используются квадратные или фигурные скобки (как в генераторах списков или словарей); вместо них применяются круглые скобки. Следующий пример представляет собой версию генератора списка, описанного выше, но реализованную при помощи генераторного выражения:
   186 Глава 8. Управляющие конструкции x = [1, 2, 3, 4] x_squared = (item * item for item in x) x_squared <generator object <genexpr> at 0x102176708> for square in x_squared: print(square,) 1 4 9 16 Обратите внимание: различия не ограничиваются заменой квадратных скобок; также это выражение не возвращает список. Вместо этого оно возвращает объект-генератор, который, как продемонстрировано в примере выше, может быть задействован в качестве итератора в цикле for, что очень похоже на работу функции range(). Преимущество генераторных выражений заключается в том, что весь список не генерируется в памяти, это позволяет генерировать сколь угодно большие последовательности с минимальными затратами памяти. Попробуйте сами: генераторы Какой генератор списков вы бы применили для обработки списка x, чтобы удалить все отрицательные значения? Создайте генератор, возвращающий только нечетные числа от 1 до 100. (Подсказка: нечетные числа можно отличить по наличию остатка от деления на 2; чтобы узнать остаток от деления на 2, используйте % 2.) Напишите код для создания словаря, содержащего числа от 11 до 15 и их кубы. 8.10. Операторы, блоки и отступы Так как в управляющих конструкциях, упоминавшихся в этой главе, впервые применялись блоки и отступы, сейчас самое время вернуться к этой теме. В языке Python отступы операторов используются для установления границ различных блоков (или тел) управляющих конструкций. Блок состоит из одного или нескольких операторов, обычно разделенных символами новой строки. Примерами операторов в языке Python являются: оператор присваивания, вызовы функций, функция print, оператор-заполнитель pass и оператор del. Управляющие конструкции (if-elif-else, циклы while и for) являются составными операторами: клауза составного оператора: блок клауза составного оператора: блок Составной оператор содержит одну или несколько клауз (частей), за каждой из которых следуют блоки с отступом. Составные операторы также могут
   8.10. Операторы, блоки и отступы 187 располагаться в блоках, как и любые другие операторы. В таком случае они создают вложенные блоки. Вам может встретиться и пара особых случаев. Хотя это и трудно назвать хорошим стилем языка Python, тем не менее несколько операторов можно разместить в одной строке, разделив их точками с запятой. Однострочный блок можно разместить в той же строке после двоеточия клаузы составного оператора: x = 1; y = 0; z = 0 if x > 0: y = 1; z = 10 else: y = -1 print(x, y, z) 1 1 10 Неправильные отступы в коде приводят к возникновению исключения. Вам могут встретиться две формы такого исключения. Первая: x = 1 x = 2 File "<ipython-input-21-c75cba843d9b>", line 2 x = 2 ^ IndentationError: unexpected indent В этом коде отступом снабжена строка, в которой отступа быть не должно. В базовом режиме место, в котором возникла проблема, помечается указателем (^). Одна из ситуаций, в которой может возникнуть эта ошибка, особенно запутанна. Если вы работаете в редакторе, отображающем табуляции с шагом в четыре пробела (или в интерактивном режиме Windows, где первая табуляция отступает лишь на четыре пробела от приглашения), и делаете отступ одной строки шириной в четыре пробела, а затем отступ следующей строки при помощи табуляции, то может показаться, что эти две строки имеют одинаковые отступы. Однако вы получите исключение, поскольку Python соотносит табуляцию с восемью пробелами. Лучший способ избежать этой проблемы — использовать в коде Python только пробелы. Если вы вынуждены пользоваться табуляцией для создания отступов или же имеете дело с кодом, в котором применяется табуляция, никогда не сочетайте ее с пробелами. Что касается базового режима интерактивной оболочки и оболочки Python IDLE, вероятно, вы заметили, что после самого внешнего уровня отступов требуется дополнительная строка: >>> x = 1 ... if x == 1: ... y = 2 ... if y > 0: ... z = 2 ... v = 0 >>> x = 2
   188 Глава 8. Управляющие конструкции После строки z = 2 дополнительная строка не нужна, а вот после строки v = 0 в базовой оболочке одна строка необходима. Эта строка не нужна, если вы работаете в Jupyter Notebooks или размещаете ваш код в модуле в файле. Вторая форма исключения возникает в том случае, когда отступ оператора в блоке меньше допустимого: x = 1 if x == 1: y = 2 z = 2 File "<tokenize>", line 4 z = 2 ^ IndentationError: unindent does not match any outer indentation level В данном примере строка z = 2 неправильно выровнена под строкой y = 2. Такая форма встречается редко, однако я все же отмечу ее, поскольку в похожих случаях это может сбить вас с толку. Python позволяет делать отступы любой величины и не жалуется на разнобой при условии, что вы соблюдаете одинаковую ширину отступов в пределах одного блока. Не злоупотребляйте этой гибкостью. Рекомендуемым стандартом считаются четыре пробела для каждого уровня отступа. В завершение этой темы стоит поговорить о разбиении операторов на несколько строк, в чем, несомненно, необходимость лишь возрастает по мере увеличения уровня отступов. Чтобы разбить строку кода явным образом, воспользуйтесь символом обратной косой черты \. Также можно неявно разбить любой оператор между токенами внутри набора разделителей (), {} или [] (то есть при вводе набора значений в списке, кортеже или словаре; набора аргументов в вызове функции; или любого выражения в квадратных скобках). Отступ для строки продолжения оператора можно сделать на любой желаемый уровень: print('string1', 'string2', 'string3' \ , 'string4', 'string5') string1 string2 string3 string4 string5 x = 100 + 200 + 300 \ + 400 + 500 X 1500 v = [100, 300, 500, 700, 900, 1100, 1300] V [100, 300, 500, 700, 900, 1100, 1300] max(1000, 300, 500,
   8.11. Логические значения и выражения 189 800, 1200) 1200 x = (100 + 200 + 300 + 400 + 500) X 1500 Если разбивать строку с помощью обратной косой черты \, при этом любые отступы (табуляции или пробелы) становятся частью строки и она должна завершаться символом \. Во избежание этой ситуации обязательно заключайте разбиваемые сегменты в кавычки. Помните, что любые строковые литералы, разделенные пробельными символами, автоматически объединяются интерпретатором Python: "strings separated by whitespace " \ ''' are automatically''' ' concatenated ' 'strings separated by whitespace are automatically concatenated' x = 1 if x > 0: string1 = "this string broken by a backslash will end up \ with the indentation tabs in it " string1 'this string broken by a backslash will end up ➥indentation tabs in it ' with the if x > 0: string1 = "this can be easily avoided by splitting the " \ "string in this way" string1 'this can be easily avoided by splitting the string in this way' 8.11. Логические значения и выражения В предыдущих примерах управляющих конструкций условные выражения (conditional tests) использовались довольно очевидным образом, и при этом не пояснялось, что считается истинным (True) или ложным (False) в Python, либо какие выражения могут использоваться там, где требуется проверочное условие. Затронем эти аспекты Python теперь. В Python существует объект логического типа, которому может присваиваться значение True или False. Любое выражение с логической операцией возвращает True или False.
   190 Глава 8. Управляющие конструкции 8.11.1. Использование объектов Python как логических значений В отношении логических значений Python следует примеру языка C. Как известно, в C целое число 0 интерпретируется как ложное значение, а все остальные целые числа — как истинные. Python обобщает этот принцип: 0 и пустые значения обозначают False, а все остальные значения — True. На практике это означает следующее. Числа 0, 0.0 и 0+0j — False; все остальные числа — True. Пустая строка "" — False; любая другая строка — True. Пустой список [] — False; любой другой список — True. Пустой словарь {} — False; любой другой словарь — True. Пустое множество set() — False; любое другое множество — True. Специальное значение Python None всегда означает False. Мы еще не рассматривали некоторые структуры данных Python, однако в общем действует то же правило. Если структура данных пуста или равна 0, она интерпретируется как ложное значение в логическом контексте, в противном случае она принимается за истинное значение. Некоторые объекты (к примеру, файловые объекты и объекты кода) не имеют разумного определения 0 или пустого элемента, поэтому их не следует применять в логическом контексте. 8.11.2. Операторы сравнения и логические операторы Для сравнения объектов можно применять обычные операторы сравнения: <, <=, >, >= и т. п. Оператор == проверяет равенство, а != — неравенство. Кроме того, существуют операторы in и not in для проверки наличия элементов в последовательностях (списках, кортежах, строках и словарях), а также операторы is и is not для проверки идентичности двух объектов. Выражения, возвращающие логическое значение, можно объединять в более сложные выражения с помощью операторов and, or и not. Следующий фрагмент кода проверяет, входит ли значение, присвоенное переменной, в заданный диапазон: if 0 < x and x < 10: ... Для подобных составных операторов Python предлагает удобную сокращенную запись. Диапазон записывается так, как это делается в математических работах: if 0 < x < 10: ... При этом действуют различные правила приоритета: если сомневаетесь, добавьте круглые скобки, чтобы Python точно интерпретировал выражение так, как вам нужно. Вероятно, круглые скобки уместно использовать в сложных выражениях
   8.11. Логические значения и выражения 191 независимо от того, необходимо это или нет, чтобы в будущем специалист по поддержке вашего кода четко понимал, что в нем происходит. За дополнительной информацией о приоритете операций обращайтесь к документации Python. Оставшаяся часть этого раздела содержит более сложный материал. Если вы только приступили к изучению языка, ее можно пока пропустить. Операторы and и or возвращают объекты. Оператор and возвращает либо первый объект со значением false (который будет получен при вычислении выражения), либо последний объект. Аналогичным образом оператор or возвращает либо первый объект со значением true, либо последний объект. Это может показаться немного запутанным, но работает корректно. Если выражение с and содержит хотя бы один ложный элемент, то под воздействием этого элемента для всего выражения вычисляется результат False и возвращается значение False. Если все элементы равны True, то результат всего выражения также равен True и возвращается последнее значение, которое тоже должно быть равно True. Для оператора or верно обратное: даже одного элемента True достаточно для того, чтобы все выражение интерпретировалось как True и возвращалось первое найденное значение True. Если ни одного значения True не найдено, возвращается последнее значение (False). Иными словами, как и во многих других языках, вычисление останавливается сразу же при обнаружении истинного выражения для оператора or или ложного значения для оператора and: [2] and [3, 4] [3, 4] [] and 5 [] [2] or [3, 4] [2] Требуется проверить второй элемент Только первый элемент нуждается в проверке Только первый элемент нуждается в проверке [] or 5 5 Требуется проверить второй элемент Операторы == и != проверяют, содержат ли их операнды одинаковые значения, и востребованы намного чаще операторов is и is not, которые проверяют, являются ли их операнды одним и тем же объектом: x = [0] y = [x, 1] x is y[0] True x = [0] x is y[0] Переменные x и y ссылаются на один и тот же объект Переменной x присваивается другой объект
   192 Глава 8. Управляющие конструкции False x == y[0] True Если этот пример вам недостаточно понятен, перечитайте раздел 5.6 «Вложенные списки и глубокие копии». Быстрая проверка: логические значения и истинность Определите, истинны или ложны следующие выражения: 1, 0, -1, [0], 1 and 0, 1 > 0 or []. 8.12. Простая программа для анализа текстового файла Чтобы вы лучше поняли, как работают программы на Python, в этом разделе будет рассмотрен небольшой пример, который в общих чертах повторяет функциональность утилиты UNIX wc: он выводит количество строк, слов и символов в файле. Программный код в листинге 8.1 рассчитан на программистов, которые только осваивают Python, то есть написан предельно просто. Листинг 8.1. word_count.py #!/usr/bin/env python3 """ Считывает файл и возвращает количество строк, слов и символов - по аналогии с утилитой UNIX wc """ infile = open('word_count.tst') lines = infile.read().split("\n") line_count = len(lines) word_count = 0 char_count = 0 Открывает файл Читает файл, разбивает его на строки Определяет количество строк с помощью функции len() Инициализирует другие счетчики for line in lines: Перебирает в цикле строки файла words = line.split() word_count += len(words) Выполняет разбивку по словам char_count += len(line) Возвращает количество символов print("File has {0} lines, {1} words, {2} characters".format( line_count, word_count, char_count)) Выводит результаты Проверить работу этой программы можно при помощи файла, приведенного в листинге 8.2, который содержит абзац из резюме этой главы.
   8.13. Практическая работа: рефакторинг word_count 193 Листинг 8.2. word_count.tst Python provides a complete set of control flow elements, including while and for loops, and conditionals. Python uses the level of indentation to group blocks of code with control elements. Если вы запускаете предыдущий код в Juputer Notebook, не забудьте также выполнить ячейку, создающую файл word_count.tst. Если вы сохранили код в файле word_count.py, вы также можете запустить его из командной строки: naomi@mac:~/quickpythonbook/code $ python3 word_count.py В любом случае, если файл word_count.tst находится в том же каталоге, вы получите следующий вывод: File has 4 lines, 30 words, 186 characters Этот пример может дать вам представление о программе на Python. Код получился достаточно компактным, и большая часть работы выполняется в трех строках цикла for. Более того, программу можно написать еще короче и выразительнее, как мы увидим в практической работе этой главы. Многие питонисты считают такую краткость одной из самых сильных сторон языка Python. 8.13. Практическая работа: рефакторинг word_count Перепишите программу подсчета слов из раздела 8.12, сделав ее короче. Возможно, вам стоит вспомнить рассмотренные выше операции со строками и спис­ ками, а также подумать о других способах организации программного кода. Возможно, вам также захочется сделать программу «умнее», чтобы словами считались только строки, состоящие из букв алфавита, а не прочие символы или знаки препинания. 8.13.1. Решение задачи при помощи кода, сгенерированного ИИ В этой практической работе мы выполним рефакторинг существующей программы, то есть переработаем ее в сторону улучшения. Наша цель — сделать программный код короче и (необязательно) научить его учитывать только слова, пропуская небуквенные символы или цифры. Для рефакторинга программы нам нужно предоставить чат-боту на основе ИИ существующий код и ряд указаний относительно того, как мы хотим его переработать. 1. Указать, какой код мы хотим переработать. 2. Пояснить, какова цель рефакторинга: сделать код короче. 3. (По желанию.) Внести в код изменения так, чтобы в качестве слов учитывались лишь алфавитные строки.
   194 Глава 8. Управляющие конструкции 8.13.2. Решения и обсуждение Поскольку мое решение и решения инструментов ИИ основываются на одной и той же программе, они будут довольно похожи. Однако, как и прежде, реализации, предложенные средствами ИИ, менее бережно относятся к ресурсам, предпочитая считывать весь файл в память. Программное решение, созданное человеком Решение, написанное программистом, немного длиннее, поскольку я снова решила считывать файл построчно: """ Считывает файл и возвращает количество строк, слов и символов - по аналогии с утилитой UNIX wc """ # инициализировать счетчики line_count = 0 word_count = 0 Обнуляет счетчики char_count = 0 # открыть файл with open('word_count.tst') as infile: for line in infile: line_count += 1 char_count += len(line) word_count += len(line.split()) Обрабатывает файл построчно # вывести ответы при помощи метода format() print("File has {0} lines, {1} words, {2} characters".format(line_count, word_count, char_count)) Это означает, что все переменные count (счетчики) должны быть инициализированы с нулевым значением. Другим следствием построчного считывания является необходимость прибавления ко всем счетчикам значений для каждой строки. При таком подходе можно работать с файлами практически любого размера, однако, как мы увидим, в результате код получается немного длиннее, чем в вариантах, предложенных ИИ. Считайте это инстинктом старого специалиста по обработке данных, хотя, возможно, в большинстве случаев такая осторожность будет излишней. Программное решение, сгенерированное ИИ В Colaboratory мы просто выполним рефакторинг кода предыдущей программы счетчика слов, так что промпт будет следующим: Выполнить рефакторинг приведенной выше программы подсчета слов, чтобы сделать ее короче. Возможно, стоит вспомнить операции со строками и списками, а также подумать о других способах организации программного кода. Для Copilot мы приводим существующий код в окне чата и добавляем инструкции, включая необязательную задачу подсчета слов, состоящих только из букв:
   8.13. Практическая работа: рефакторинг word_count 195 Выполнить рефакторинг этого кода, чтобы сделать его короче. Возможно, стоит вспомнить операции со строками и списками, а также подумать о других способах организации программного кода. Также хотелось бы сделать программу "умнее", чтобы словами считались только алфавитные строки, а не прочие символы или знаки препинания. Обе версии похожи, поэтому начнем с варианта, предложенного Copilot: """ Считывает файл и возвращает количество строк, слов и символов - по аналогии с утилитой UNIX wc Метод readlines возвращает список строк """ (с символами новой строки) with open('word_count.tst') as infile: Генераторное выражение lines = infile.readlines() line_count = len(lines) для получения количества word_count = sum(len(line.split()) for line in lines) слов в каждой строке char_count = sum(len(line) for line in lines) print(f"{line_count} lines, {word_count} words, {char_count} characters") Генераторное выражение для получения длины каждой строки Этот вариант считывает файл целиком, но при этом использует метод readlines(), возвращающий список строк. Таким образом, нет необходимости разбивать строки, однако это может вызвать трудности в случае с файлом очень большого размера. После того как строки считаны, количество строк является значением счетчика строк, а количество слов и символов определяется с помощью функции sum, вызываемой для генераторных выражений, перебирающих список строк. Если мы хотим в качестве слов подсчитывать лишь алфавитные строки, строка word_count станет немного сложнее: Copilot предлагает поместить генератор списка внутри генераторного выражения для фильтрации по строковому методу isalpha: word_count = sum(len([word for word in line.split() if word.isalpha()]) for line in lines) Так или иначе, код, предложенный Copilot, краток и отлично работает при условии, что размер данных не слишком велик для памяти. Вариант Colaboratory имеет важное отличие в теле кода: with open('word_count.tst') as infile: lines = infile.read().split("\n") Делит весь файл на строки по символу новой строки Здесь весь файл считывается в память, а затем разбивается на строки методом split по символу новой строки ("\n"). Таким образом, программе приходится выполнять операцию разбиения на строки всего файла, чего успешно избегает мое решение и версия Copilot. Собственно, точно так же работает и исходный код, от которого данный вариант наследует и одну погрешность в вычислениях.
   196 Глава 8. Управляющие конструкции Так, при разделении строки вы получаете список подстрок, разбитых указанной строкой (или пробельными символами, если такая строка не указана). Что не учитывается, так это сама строка-разделитель ("\n"). Поскольку в нашем примере четыре строки, разделенные тремя переносами строки, исходный код и вариант Colaboratory выдают количество символов на три меньше, чем должно быть: 186 вместо корректных 189, возвращаемых решениями программиста и Copilot. В итоге версии, сгенерированные средствами ИИ, неплохо справились с нашей задачей рефакторинга. Хотя ни один из вариантов, предложенных ИИ, не экономил память, как я, версии Copilot удалось повысить производительность по сравнению с исходным кодом программы за счет удаления функции разделения; кроме того, в ней исправлена ошибка в исходном коде, связанная с подсчетом символов. Итоги Отступы в языке Python применяются для группировки блоков кода. В Python имеются циклы while и for, условные операторы с if-elif-else и оператор match, позволяющий выбирать из нескольких вариантов. Ряд типовых способов применения цикла for можно заменить на генераторы списков, словарей или множеств. Генераторные выражения похожи на генераторы, но, как правило, используют меньше памяти. В языке Python используются логические значения True и False, на которые могут ссылаться переменные. Большинство объектов Python также могут интерпретироваться как логические значения, при этом ноль или пустое значение приравнивается к False, а все остальные — к True.
9 Функции В этой главе: 3 Определение функций 3 Использование параметров функций 3 Передача изменяемых объектов в качестве параметров 3 Понятие локальных и глобальных переменных 3 Создание и использование генераторных функций 3 Создание и использование лямбда-выражений 3 Применение декораторов Предполагается, что читатели уже встречались с опрелениями функций хотя бы в одном из языков программирования и знакомы с такими базовыми понятиями, как определение функций, аргументы, параметры и т. п. 9.1. Базовые определения функций Основной синтаксис для определения функции в языке Python таков: def имя(параметр1, параметр2, . . .): тело
   198 Глава 9. Функции Как и в случае с управляющими структурами, Python использует отступы для разграничения тела определения функции. В следующем простом примере код для вычисления факториала помещен в тело функции, что позволяет вызывать функцию fact для получения факториала числа: def fact(n): '''Возвращает факториал заданного числа.''' r = 1 while n > 0: r = r * n Значение, возвращаемое в код, n = n - 1 вызывающий функцию return r Необязательная строка документации (или докстринг) Если функция содержит оператор return, как в случае выше, то значение будет возвращено в код, вызывающий функцию, и может быть присвоено переменной. Если return не используется, функция вернет значение None. Получить значение строки документации (докстринг), которую мы видим во второй строке предыдущей функции, можно путем вывода fact.__doc__. Строки документации предназначены для описания внешнего поведения функции и принимаемых ею параметров, тогда как комментарии должны содержать внутренние сведения о работе кода. Строки документации следуют сразу же за первой строкой после определения функции; обычно они заключаются в тройные кавычки, что позволяет создавать многострочные описания. Существуют инструменты просмотра, извлекающие первую из строк документации. В соответствии со стандартной практикой оформления многострочных строк документации в первой строке дается краткое описание функции, вторая строка остается пустой, а после нее следует остальная информация. Процедура или функция? В некоторых языках программирования функция, не возвращающая значения, называется процедурой. Хотя вы можете (и наверняка будете) писать функции, не содержащие оператор return, такие функции не будут процедурами в полном смысле этого слова. Все процедуры языка Python являются функциями. Если в теле процедуры явным образом не задан оператор return, возвращается специальное значение None, а при выполнении return result немедленно возвращается значение result. После выполнения return никакие другие инструкции в теле функции не выполняются. Поскольку в Python нет полноценных процедур, я буду в обоих случаях пользоваться термином функция. Хотя все функции Python возвращают значения, вы сами решаете, использовать полученное значение или нет: fact(4) 24 x = fact(4) x 24 Возвращаемое значение не присваивается переменной Значение выводится, но не сохраняется Возвращаемое значение присваивается переменной x
Возвращаемое значение не присваивается переменной 9.2. Параметры функций    fact(4) 24 x = fact(4) x 199 Значение выводится, но не сохраняется Возвращаемое значение присваивается переменной x 24 Даже если функция возвращает значение, не обязательно его сохранять и использовать. Если возвращаемое значение функции не присвоено переменной, оно будет отброшено; хотя если вызов функции является последним оператором в ячейке, возвращаемое значение будет отображено. Если значение присвоено переменной, этой переменной и ее значением можно пользоваться так же, как и любой другой переменной. 9.2. Параметры функций Большинству функций требуются параметры; в каждом языке имеется собственная спецификация определения параметров функций. Python гибок в этом отношении: в языке предусмотрено три способа определения параметров функций, которые будут кратко описаны в данном разделе. 9.2.1. Позиционные параметры Самый простой способ передачи параметров функций в Python — по позиции. В первой строке функции указываются имена переменных для каждого параметра; при вызове функции параметры, указанные в коде вызова, сопоставляются с переменными параметров функции в соответствии с порядком их следования. Следующая функция вычисляет результат возведения x в степень y: def power(x, y): r = 1 while y > 0: r = r * x y = y - 1 return r power(3, 3) 27 Данный метод требует, чтобы количество параметров в коде вызова точно соответствовало количеству параметров в определении функции; в противном случае возникает исключение TypeError: power(3) -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-5-1bbb88d7a603> in <cell line: 1>() ----> 1 power(3) TypeError: power() missing 1 required positional argument: 'y'
   200 Глава 9. Функции Значения по умолчанию Параметры функции могут иметь значения по умолчанию, которые присваиваются в первой строке определения функции, например: def fun(arg1, arg2 = default2, arg3= default3, . . .) Значения по умолчанию могут быть заданы любому количеству параметров. Параметры со значениями по умолчанию должны определяться последними в списке параметров, потому что Python, как и большинство языков, сопоставляет аргументы с параметрами на основании их позиций. Количество передаваемых функции аргументов должно быть достаточным для того, чтобы последнему параметру в списке, не имеющему значения по умолчанию, соответствовал передаваемый аргумент. Более гибкий механизм передачи аргументов описан в следующем разделе, «Передача аргументов по имени параметра». Следующая функция также вычисляет результат возведения x в степень y. Но если значение y не указано при вызове функции, по умолчанию используется значение 2, и функция просто возвращает квадрат числа x: def power(x, y=2): r = 1 while y > 0: r = r * x y = y - 1 return r Следующая интерактивная сессия демонстрирует работу аргумента по умолчанию: power(3, 3) 27 power(3) 9 9.2.2. Передача аргументов по имени параметра Аргументы также могут передаваться функции по имени ее соответствующего параметра, а не по его позиции. Продолжая предыдущий интерактивный пример, можно ввести: power(y=2, x=3) 9 Поскольку для аргументов функции power в последнем вызове указаны имена, их порядок может быть произвольным; аргументы связаны с одноименными параметрами в определении функции power, поэтому в результате вы получаете 3^2. Такой способ передачи аргументов называется передачей именованных аргументов.
   9.2. Параметры функций 201 Передача именованных аргументов в сочетании с возможностью использования аргументов по умолчанию может быть чрезвычайно полезна при определении функции с большим количеством возможных аргументов, большинство из которых имеет общие значения по умолчанию. Рассмотрим функцию, которая должна создавать список с информацией о файлах в текущем каталоге; в ней используются логические аргументы для обозначения того, должен ли список включать информацию о размере файла, дате последнего изменения и т. д. для каждого файла. Определение подобной функции будет выглядеть примерно так: def list_file_info(size=False, create_date=False, mod_date=False, ...): ...get file names... if size: # код для получения размеров файлов if create_date: # код для получения дат создания # сделать любые другие необходимые действия return fileinfostructure Затем эту функцию можно вызывать из другой части кода, используя передачу именованных аргументов, чтобы указать, что вам нужна лишь определенная информация (в этом примере запрашиваются размер файла и дата его изменения, но не дата создания): fileinfo = list_file_info(size=True, mod_date=True) Такой способ передачи аргументов особенно хорошо подходит для функций с очень сложным поведением; в частности, подобные функции нередко встречаются при создании графических пользовательских интерфейсов (GUI). Если вам когда-либо придется работать с пакетом tkinter для построения графических интерфейсов в Python, вы убедитесь в том, что эти необязательные аргументы, обозначаемые ключевым словом, просто бесценны. 9.2.3. Переменное количество аргументов Функции Python также можно определить для обработки переменного количества аргументов. Это можно сделать двумя способами. Первый способ предназначен для относительно знакомого случая, при котором неизвестное количество аргументов в конце последовательности аргументов собирается в список, традиционно называемый *args в перечне параметров. Во втором способе произвольное количество аргументов, передаваемых по ключевому слову и не имеющих параметров с соответствующим именем в перечне параметров функции, объединяется в словарь, традиционно именуемый **kwargs. Оба этих механизма мы и рассмотрим ниже. Работа с неопределенным количеством позиционных аргументов Если имени последнего параметра функции предшествует символ *, все лишние аргументы без ключевых слов в вызове функции (то есть позиционные
   202 Глава 9. Функции аргументы, не назначенные другому параметру) объединяются и присваиваются заданному параметру в качестве кортежа. Ниже продемонстрирован простой способ реализации функции для вычисления наибольшего значения в списке чисел. Сначала реализуйте функцию: def maximum(*numbers): if len(numbers) == 0: return None else: maxnum = numbers[0] for n in numbers[1:]: if n > maxnum: maxnum = n return maxnum * означает захват любого количества параметров numbers — это список всех параметров Добавление * перед числами позволяет сохранить все позиционные параметры в виде списка. Теперь протестируем поведение функции: maximum(3, 2, 8) 8 maximum(1, 5, 9, -2, 2) 9 Работа с неопределенным количеством именованных аргументов Также можно обрабатывать произвольное количество именованных аргументов. Если последний параметр в перечне снабжен префиксом **, все лишние аргументы, передаваемые по ключевому слову, собираются в словарь. Ключом для каждого элемента словаря является ключевое слово (имя параметра) избыточного аргумента, а значением — значение самого аргумента. Именованный аргумент считается избыточным в данном контексте, если ключевое слово, по которому он передавался, не соответствует ни одному из имен параметров в определении функции. Пример: ** означает захват всех именованных параметров def example_fun(x, y, **other): print(f"x: {x}, y: {y}, keys in 'other': {list(other.keys())}.") other_total = 0 Именованные параметры for k in other.keys(): объединяются в словарь other other_total = other_total + other[k] print(f"The total of values in 'other' is {other_total}") Использование ** позволяет поместить все параметры, несопоставленные по ключевому слову, в словарь, где ключами будут имена, а значениями — значения параметров.
   9.3. Изменяемые объекты в качестве аргументов 203 Тестирование этой функции в интерактивной сессии показывает, что функция успешно обрабатывает аргументы с ключевыми словами foo и bar, несмотря на то что foo и bar не являются именами параметров в определении функции: example_fun(2, y="1", foo=3, bar=4) x: 2, y: 1, keys in 'other': ['foo', 'bar'] The total of values in 'other' is 7 9.2.4. Совмещение способов передачи аргументов Формально все возможности передачи аргументов функций Python могут применяться одновременно, хотя, если не принять меры, разобраться в таком коде будет непросто. Общее правило смешанной передачи аргументов заключается в том, что сначала идут позиционные аргументы, затем именованные, за ними следуют неопределенный позиционный аргумент с символом * и, наконец, неопределенный именованный аргумент с символом **. За полной информацией обращайтесь к документации. Быстрая проверка: функции и параметры Как написать функцию, которая могла бы принимать любое количество неименованных аргументов, а затем выводить их значения в обратном порядке? Что нужно сделать, чтобы создать процедуру или пустую функцию — то есть функцию без возвращаемого значения? Что произойдет, если присвоить возвращаемое значение функции переменной? 9.3. Изменяемые объекты в качестве аргументов Аргументы передаются по ссылке на объект. Параметр становится новой ссылкой на объект. Для неизменяемых объектов (таких, как кортежи, строки и числа) любые действия с параметром не имеют никакого эффекта за пределами функции. Но если вы передаете изменяемый объект (например, список, словарь или экземпляр класса), любая модификация объекта изменяет то, на что ссылается аргумент за пределами функции. Переназначение параметра не влияет на аргумент, как показано на рис. 9.1 и 9.2: def f(n, list1, list2): list1.append(3) list2 = [4, 5, 6] n = n + 1 x = 5 y = [1, 2] z = [4, 5] f(x, y, z) x, y, z (5, [1, 2, 3], [4, 5]) В список list1 добавлен один элемент Список list2 заменен внутри функции на новый список
   204 Глава 9. Функции На рис. 9.1 показано состояние переменных до выполнения кода. Обратите внимание, что переменные n и x ссылаются на неизменяемый объект со значением 5, переменные y и list1 — на список [1, 2], а переменные z и list2 — на список [4,5]. Выполнив функцию, мы получим результат, показанный на рис. 9.2. Рисунок 9.2 демонстрирует, что происходит при вызове функции f. Сама переменная x не меняется, поскольку объект, на который она ссылается, неизменяем. Вместо этого параметр функции n начинает ссылаться на новый объект со значением 6. Аналогично переменная z остается неизменной, так как внутри функции f соответствующий ей параметр list2 стал ссылаться на новый объект, [4, 5, 6]. Изменения коснулись лишь переменной y, поскольку фактический список, на который она указывает, был изменен внутренне. Начало выполнения f(x,y,z) x n Конец выполнения f(x,y,z) x 5 5 y list1 y 6 list1 [1, 2, 3] [1, 2] z n list2 [4, 5] Рис. 9.1. В начале выполнения функции f() исходные переменные и параметры функции ссылаются на одни и те же объекты z list2 [4, 5] [4, 5, 6] Рис. 9.2. В конце работы функции f() объект, на который ссылается переменная y, был изменен внутренне (благодаря действиям параметра list1 внутри функции), тогда как n и list2 стали ссылаться на другие объекты 9.3.1. Изменяемые объекты в качестве значений по умолчанию Передача изменяемых объектов в качестве значений параметров может привести к ошибкам, но зачастую это самый удобный и эффективный способ работы с функциями. Правда, при этом необходимо помнить, что изменение объекта может иметь побочный эффект за пределами функции. Гораздо хуже использовать изменяемый объект в качестве значения параметра по умолчанию, а затем изменять его. К примеру, предположим, я собираюсь проверить список на наличие нечетных чисел и добавить их к списку нечетных чисел, который я передаю в качестве параметра:
   9.3. Изменяемые объекты в качестве аргументов 205 def odd_numbers(test_list, odds): for number in test_list: if number % 2: odds.append(number) return odds odds = [] odds = odd_numbers([1, 5, 7, 9, 10], odds) odds [1, 5, 7, 9] Это работает, но, допустим, вы решите, что не хотите возиться с созданием пустого списка для нечетных чисел, и сделаете пустой список значением по умолчанию для параметра odds. Теперь вам не нужно явным образом создавать или передавать этот список, верно? def odd_numbers(test_list, odds=[]): for number in test_list: if number % 2: odds.append(number) return odds Пустой список в качестве значения параметра по умолчанию odds = odd_numbers([1, 5, 7, 9, 10]) odds [1, 5, 7, 9] Кажется, что все работает как надо, но если запустить ту же функцию второй раз, результат изменится: odds = odd_numbers([1, 5, 7, 9, 10]) odds [1, 5, 7, 9, 1, 5, 7, 9] При использовании значения параметра по умолчанию Python назначает объект, который будет применяться по умолчанию, при первой компиляции функции, и он не меняется в течение всего времени работы программы. Таким образом, если в качестве параметра по умолчанию выступает изменяемый объект и в ходе работы программы он подвергается изменениям (что мы и сделали, добавив элементы в список нечетных чисел), то каждый раз, когда используется данное значение по умолчанию, это будет один и тот же объект и он будет отражать все случаи обращения к нему при вызове этой функции. Быстрая проверка: изменяемые параметры функции К чему может привести изменение списка или словаря, который был передан функции в качестве параметра? Какие операции с большой вероятностью приведут к изменениям, заметным за пределами функции? Какие шаги можно предпринять, чтобы минимизировать этот риск?
   206 Глава 9. Функции 9.4. Локальные, нелокальные и глобальные переменные Вернемся к определению функции fact, приведенному в начале главы: def fact(n): """Возвращает факториал заданного числа.""" r = 1 while n > 0: r = r * n n = n - 1 return r Переменные r и n являются локальными для любого конкретного вызова функции факториала; изменения, внесенные в них во время выполнения функции, не отразятся ни на каких переменных за пределами этой функции. Любые переменные в перечне параметров функции и любые переменные, созданные внутри функции путем присваивания (например, r = 1 в функции fact), являются локальными для данной функции. Переменную можно явно объявить глобальной перед ее использованием с помощью оператора global. Функция может обращаться к глобальным переменным и изменять их. Такие переменные существуют за пределами функций; обращаться к ним и изменять их также могут другие функции (если эти переменные будут объявлены в них глобальными) или программный код, не находящийся внутри функции. Следующий пример демонстрирует различия между локальными и глобальными переменными: def fun(): global a a = 1 b = 2 В этом примере определяется функция, для которой a является глобальной переменной, а b — локальной. Эта функция пытается изменить a и b. А теперь протестируем эту функцию: a = "one" b = "two" fun() a 1 b 'two' Присваивание переменной a внутри функции fun является присваиванием глобальной переменной a, также существующей за пределами fun. Поскольку
   9.4. Локальные, нелокальные и глобальные переменные 207 переменная a объявлена глобальной (global) в функции fun, в результате присваивания глобальная переменная изменяется и теперь ссылается на значение 1 вместо значения "one". С переменной b дело обстоит иначе: локальная переменная b внутри функции fun изначально ссылается на то же значение, что и переменная b за пределами fun, но в результате присваивания b начинает указывать на новое значение, локальное для функции fun. Также существует оператор nonlocal (нелокальный), близкий по смыслу к оператору global, — он заставляет идентификатор ссылаться на ранее связанную переменную в ближайшей внешней области видимости. Области видимости и пространства имен более подробно рассматриваются в главе 10, но суть в том, что global используется для переменной верхнего уровня, тогда как nonlocal может ссылаться на любую переменную во внешней области видимости, как иллюстрирует пример в листинге 9.1. Листинг 9.1. Файл nonlocal.py g_var в глобальной области видимости g_var = 0 nl_var в глобальной области видимости nl_var = 0 print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var)) def test(): nl_var в контексте test (неглобальном) nl_var = 2 print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var)) def inner_test(): Применение ключевого слова global привязывает g_var в inner_test к g_var верхнего уровня global g_var nonlocal nl_var Применение ключевого слова nonlocal g_var = 1 привязывает nl_var к nl_var в test nl_var = 4 print("in inner_test-> g_var: {0} nl_var: {1}".format(g_var, nl_var)) inner_test() print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var)) test() print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var)) В приведенном выше коде показано, как использование ключевых слов global и nonlocal позволяет сделать так, чтобы переменные в одной области видимости ссылались либо на переменную верхнего уровня модуля, либо на ближайшую одноименную переменную во внешней области видимости. При выполнении эта программа выводит следующий результат: top level-> g_var: 0 nl_var: 0 in test-> g_var: 0 nl_var: 2 in inner_test-> g_var: 1 nl_var: 4 in test-> g_var: 1 nl_var: 4 top level-> g_var: 1 nl_var: 0
   208 Глава 9. Функции Обратите внимание: значение nl_var верхнего уровня не изменилось, что произошло бы, если бы функция inner_test содержала строку global nl_var. Вывод: если вы хотите присвоить значение переменной, существующей за пределами функции, эту переменную следует явно объявить нелокальной или глобальной. Однако если вы всего лишь считываете переменную, существующую вне функции, объявлять ее нелокальной или глобальной не нужно. Если Python не находит имя переменной в локальной области видимости функции, он пытается найти это имя во внешних областях видимости и в глобальной области видимости. Следовательно, обращения к глобальным переменным автоматически направляются к нужной глобальной переменной. Лично я не рекомендую использовать такой прием. Ваш код будет намного понятнее, если все глобальные переменные будут явно объявлены таковыми. Более того, глобальные переменные в функциях стоит задействовать лишь в отдельных редких случаях. Попробуйте сами: глобальные и локальные переменные Допустим, x = 5. Какое значение будет присвоено переменной x после выполнения приведенной ниже функции funct_1()? А после выполнения функции funct_2()? def funct_1(): x = 3 def funct_2(): global x x = 2 9.5. Присваивание функций переменным Функции, как и другие объекты Python, могут быть присвоены переменным, как показано в следующем примере: def f_to_kelvin(degrees_f): return 273.15 + (degrees_f - 32) * 5 / 9 def c_to_kelvin(degrees_c): return 273.15 + degrees_c Определяет функцию f_to_kelvin Определяет функцию c_to_kelvin abs_temperature = f_to_kelvin abs_temperature(32) 273.15 Присваивает переменной ссылку на функцию abs_temperature = c_to_kelvin abs_temperature(0) 273.15 В приведенном выше коде мы определяем две различные функции: одна для преобразования температуры по Фаренгейту в температуру по Кельвину, а другая — температуры по Цельсию в температуру по Кельвину. Затем можно Defines the f_to_kelvin kelvin function. Defines the c_to_kelvin function. Assigns function to variable. -
   9.6. Лямбда-выражения 209 присвоить ссылку на любую из этих функций переменной abs_temperature. После этого при вызове этой переменной будет выполняться та функция, на которую переменная в данный момент ссылается. Функции можно помещать в списки, кортежи или словари: t = {'FtoK': f_to_kelvin, 'CtoK': c_to_kelvin} t['FtoK'](32) 273.15 t['CtoK'](0) 273.15 Обращается к функции f_to_kelvin как к значению в словаре Создает словарь с обеими функциями в качестве значений Обращается к функции c_to_kelvin как к значению в словаре Переменную, ссылающуюся на функцию, можно использовать так же, как и саму функцию. Последний пример демонстрирует применение словаря для вызова различных функций в зависимости от значений строк, выступающих в качестве ключей. К такой схеме нередко прибегают в случаях, когда требуется выбирать разные функции в зависимости от строкового значения; зачастую она заменяет структуру switch из таких языков, как C и Java. 9.6. Лямбда-выражения Короткие функции наподобие только что приведенных также могут определяться при помощи лямбда-выражения (lambda) следующего вида: lambda параметр1, параметр2, . . .: выражение Лямбда-выражения — это короткие анонимные функции, которые удобно определять непосредственно в коде, как правило, в одной строке с помощью оператора lambda. Часто такие мини-функции необходимо передать другой функции, например, как функцию key при сортировке списков с помощью метода sort. В таких случаях большие функции, как правило, не требуются, и было бы неудобно определять такую функцию где-то отдельно от места ее применения. Словарь из предыдущего подраздела может определяться вместе с определениями функций: t2 = {'FtoK': lambda deg_f: 273.15 + (deg_f - 32) * 5 / 9, 'CtoK': lambda deg_c: 273.15 + deg_c} Лямбда-функции в качестве t2['FtoK'](32) значений словаря 273.15 В данном примере лямбда-выражения (lambda) заданы в качестве значений элементов словаря, вместо использования формально определенных функций. Обратите внимание: лямбда-выражения (lambda) не содержат оператора return, поскольку результат выражения возвращается автоматически.
   210 Глава 9. Функции 9.7. Функции-генераторы Функция-генератор (generator function) представляет собой особый вид функции, которую можно использовать для определения собственных итераторов. При определении функции-генератора значение каждой итерации возвращается с помощью ключевого слова yield. При вызове функции-генератора она возвращает объект-генератор (generator object), который можно использовать в качестве итератора. Каждый раз при вызове функции-генератора она выполняет код до ключевого слова yield, использует yield для возврата значения и при следующем вызове возобновляет выполнение сразу после yield. Функция-генератор перестает возвращать значения, когда итерации заканчиваются, а также при достижении пустого оператора return или конца функции. Локальные переменные в функции-генераторе, в отличие от обычных функций, сохраняются между вызовами: def four(): Устанавливает начальное значение x равным 0 x = 0 while x < 4: print("in generator, x =", x) Возвращает текущее значение x yield x x += 1 Увеличивает значение x for i in four(): print(f"Value from generator {i}") in generator, x = 0 Value from generator in generator, x = 1 Value from generator in generator, x = 2 Value from generator in generator, x = 3 Value from generator 0 Вызов функции-генератора создает объект-генератор (итератор) для использования в цикле for 1 2 3 Когда в цикле for вызывается функция four, возвращается объект-генератор, который является итератором, управляющим циклом for. Обратите внимание, что данная функция-генератор содержит цикл while, ограничивающий число выполнений генератора. В зависимости от способа применения функция-генератор, в которой не предусмотрено условие остановки, может при вызове образовать бесконечный цикл. yield и yield from Начиная с Python версии 3.3, наряду с ключевым словом yield появилось новое ключевое слово для генераторов yield from. В основном yield from позволяет объединять генераторы в цепочку; yield from ведет себя так же, как yield, за исключением того, что вся генераторная механика делегируется субгенератору. В упрощенном виде это выглядит так:
   9.8. Декораторы 211 def subgen(x): for i in range(x): yield i def gen(y): yield from subgen(y) for q in gen(6): print(q) 0 1 2 3 4 5 Этот пример позволяет вынести выражение yield из основного генератора, что упрощает рефакторинг. Функции-генераторы также можно применять с оператором in, чтобы проверить, содержится ли значение в производимой ими последовательности: 2 in four() in generator x = 0 in generator x = 1 in generator x = 2 True 5 in four() in generator in generator in generator in generator False x x x x = = = = 0 1 2 3 Быстрая проверка: функции-генераторы Что необходимо изменить в предыдущем коде функции four(), чтобы она работала с любым числом? Что нужно добавить в код, чтобы появилась возможность задавать начальную точку отсчета? 9.8. Декораторы Поскольку функции в языке Python являются объектами первого уровня, их можно присваивать переменным, в чем вы уже убедились. Они также могут быть переданы в качестве аргументов другим функциям и возвращены как результат выполнения других функций.
   212 Глава 9. Функции К примеру, можно написать функцию Python, которая принимает в качестве параметра другую функцию, оборачивает ее в следующую функцию, выполняющую связанные действия, а затем возвращает новую функцию. Полученную комбинацию можно использовать вместо исходной функции: def decorate(func): print("in decorate function, decorating ", func.__name__) def wrapper_func(*args): print("Executing", func.__name__) return func(*args) return wrapper_func def myfunction(parameter): print(parameter) myfunction = decorate(myfunction) in decorate function, decorating myfunction myfunction("hello") Executing myfunction hello Декоратор — синтаксическое удобство для данного процесса, позволяющее оборачивать одну функцию в другую с помощью добавления всего одной строки. При этом эффект будет абсолютно таким же, как в приведенном выше коде, однако итоговый код гораздо чище и легче читается. Проще говоря, создание декоратора состоит из двух частей: определение функции, которая будет оборачивать или «декорировать» другие функции, и затем применение символа @, за которым следует функция-декоратор непосредственно перед определением оборачиваемой функции. Функция-декоратор должна принимать функцию в качестве параметра и возвращать функцию, как показано ниже: def decorate(func): print("in decorate function, decorating", func.__name__) def wrapper_func(*args): Выводит имя print("Executing", func.__name__) оборачиваемой функции return func(*args) Возвращает оборачиваемую функцию return wrapper_func @decorate def myfunction(parameter): print(parameter) myfunction декорируется с помощью @decorate in decorate function, decorating myfunction myfunction("hello") Executing myfunction hello Вызывается оборачиваемая функция
   9.9. Практическая работа: полезные функции 213 Функция decorate выводит имя функции, которую она декорирует, при ее определении. По завершении работы декоратор возвращает оборачиваемую функцию. Это в точности такой же процесс, как и в нашем первом примере, однако вместо явного кода функция myfunction декорируется с помощью @decorate. Затем оборачиваемая функция вызывается по своему исходному имени. Использование декоратора для оборачивания одной функции другой может быть удобно для решения целого ряда задач. Так, в веб-фреймворках (таких, как Django) декораторы используются для проверки ввода пользовательских учетных данных перед выполнением функции, а в графических библиотеках декораторы могут использоваться для регистрации функции в графическом фреймворке. Попробуйте сами: декораторы Как бы вы изменили код функции-декоратора, чтобы она не выдавала лишние сообщения и заключала возвращаемое значение оборачиваемой функции в теги "<html>" и "</html>", чтобы вызов myfunction("hello") возвращал "<html>hello</html>"? 9.9. Практическая работа: полезные функции Возвращаясь к практическим работам в главах 6 и 7, сделаем рефакторинг кода для очистки и подсчета слов в текстовом файле в функции для очистки и обработки данных. Цель состоит в том, чтобы большая часть программной логики была перенесена в функции. При выборе типов функций и параметров действуйте на свое усмотрение, но помните, что функции должны выполнять только одну задачу и не иметь побочных эффектов за пределами функции. 9.9.1. Решение задачи при помощи кода, сгенерированного ИИ Данная задача похожа на ту, что была рассмотрена в предыдущей главе, однако, вместо того чтобы просить средства ИИ доработать наш программный код на уровне строк, на этот раз нам нужно улучшить его структуру. Одним из факторов, упрощающих нашу задачу как в плане создания промпта, так и в плане генерирования самого кода, является то, что мы начинаем с существующего, рабочего кода, который, разумеется, необходимо передать текущим сессиям чат-ботов на основе ИИ. Решая, что именно запрашивать в промпте, нужно помнить о том, что необходимо четко указать, чего мы хотим, а именно чтобы код был переписан с использованием функций и чтобы основная часть обработки данных выполнялась в этих функциях. 9.9.2. Решения и обсуждение Решения данной задачи предполагают понимание и модификацию существующего кода. Как мы увидим, для моделей ИИ это не всегда проще.
   214 Глава 9. Функции Программное решение, созданное человеком Вариант, созданный человеком, работает и справляется с задачей вполне приемлемо, если не сказать блестяще. Было несложно взять решение из предыдущей главы, написанное программистом, и преобразовать отдельные части программного кода в функции. Секрет создания функций заключался в том, чтобы придумать им подходящие имена и убедиться, что функции возвращают значения, а основной код программы работает с этими возвращаемыми значениями. Одна из опасностей подобного рефакторинга заключается в том, что без получения значения, возвращаемого из функции, старые переменные в итоге могут оказаться глобальными переменными. Так, поскольку переменная cleaned_line используется как в функции, так и в вызывающем коде, то пока мы не удостоверимся в том, что cleaned_line в вызывающем коде установлена на возвращаемое функцией значение, она рискует превратиться в глобальную переменную, и это весьма затруднит отладку: import string punct = str.maketrans('', '', string.punctuation) def clean_line(line): """изменяет регистр и удаляет знаки препинания""" # привести весь текст к единому регистру cleaned_line = line.lower() # удалить знаки препинания cleaned_line = cleaned_line.translate(punct) return cleaned_line def get_words(line): """разбивает строку на слова и объединяет с помощью новой строки""" words = line.split() return "\n".join(words) + "\n" def count_words(words): """принимает список очищенных слов, возвращает словарь счетчика""" word_count = {} for word in words: count = word_count.setdefault(word, 0) word_count[word] += 1 return word_count def word_stats(word_count): """Принимает словарь счетчика слов и возвращает первые и последние пять записей""" word_list = list(word_count.items()) word_list.sort(key=lambda x: x[1]) least_common = word_list[:5] most_common = word_list[-1:-6:-1] return most_common, least_common with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as outfile:
   9.9. Практическая работа: полезные функции 215 for line in infile: cleaned_line = clean_line(line) cleaned_words = get_words(cleaned_line) # записать все слова построчно outfile.write(cleaned_words) moby_words = [] with open('moby_01_clean.txt') as infile: for word in infile: if word.strip(): moby_words.append(word.strip()) word_count = count_words(moby_words) most, least = word_stats(word_count) print("Most common words:") for word in most: print(word) print("\nLeast common words:") for word in least: print(word) Программные решения, сгенерированные ИИ Для Colaboratory я разместила старый код в двух ячейках прямо над активной ячейкой, а в промпте попросила чат-бот осуществить рефакторинг этого кода: Сделай рефакторинг кода в предыдущих двух ячейках, преобразовав его в функции для очистки и обработки данных. Цель состоит в том, чтобы наибольшая часть логики была перенесена в функции. В Colaboratory было сгенерировано следующее: # Функция для очистки строки текста, преобразования ее в нижний регистр и # удаления знаков препинания def clean_line(line): line = line.lower() line = line.translate(str.maketrans('', '', string.punctuation)) return line # Функция для разбиения строки текста на слова и записи каждого слова в новой # строке файла def write_words_to_file(line, output_file): words = line.split() for word in words: output_file.write(word + '\n') # Функция для подсчета вхождений каждого слова в списке слов def count_word_occurrences(words): word_counts = {} for word in words: if word not in word_counts: word_counts[word] = 0
   216 Глава 9. Функции word_counts[word] += 1 return word_counts # Функция для поиска наиболее/наименее распространенных слов в словаре def find_most_and_least_common_words(word_counts): most_common = sorted(word_counts.items(), key=lambda item: item[1], reverse=True)[:5] least_common = sorted(word_counts.items(), key=lambda item: item[1])[:5] return most_common, least_common # Открыть входной файл для чтения with open('moby_01.txt', 'r') as input_file: # Открыть выходной файл для записи with open('moby_01_clean.txt', 'w') as output_file: # Перебрать каждую строку входного файла for line in input_file: # Очистить строку текста clean_line = clean_line(line) # Записать каждое слово из очищенной строки в выходной файл write_words_to_file(clean_line, output_file) # Открыть файл и присвоить прочитанный текст переменной with open('moby_01_clean.txt', 'r') as f: text = f.read() # Разделить текст на слова words = text.split() # Подсчитать количество вхождений каждого слова word_counts = count_word_occurrences(words) # Найти наиболее и наименее распространенные слова most_common, least_common = find_most_and_least_common_words(word_counts) # Вывести результаты print("Most common words:") for word, count in most_common: print(f"{word}: {count}") print("\nLeast common words:") for word, count in least_common: print(f"{word}: {count}") Запустив версию Colaboratory, мы обнаружим пару сюрпризов: -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-4-dd9d43b3f3c2> in <cell line: 34>() 38 for line in input_file: 39 # Очистить строку текста ---> 40 clean_line = clean_line(line) 41 42 # Записать каждое слово из очищенной строки в выходной файл
   9.9. Практическая работа: полезные функции 217 <ipython-input-4-dd9d43b3f3c2> in clean_line(line) 7 def clean_line(line): 8 line = line.lower() ----> 9 line = line.translate(str.maketrans('', '', string.punctuation)) 10 return line 11 NameError: name 'string' is not defined Ошибка здесь в том, что модуль string не был импортирован. Добавление строки import string в верхней части ячейки или файла исправит ситуацию. Однако после этого мы снова получаем ошибку: -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-5-dd9d43b3f3c2> in <cell line: 34>() 38 for line in input_file: Перебирает в цикле все строки ввода 39 # Очистить строку текста ---> 40 clean_line = clean_line(line) 41 42 # Записать каждое слово из очищенной строки в выходной файл TypeError: 'str' object is not callable Имя clean_line присвоено результату работы функции clean_line Все верно — в версию, созданную Colaboratory из собственного исходного кода, вкралась ошибка. Трассировка показывает, что в строке 40 возвращаемому значению функции clean_line, представляющему собой очищенную от знаков препинания строку, присваивается имя clean_line, совпадающее с именем функции. Это вполне допустимо в языке Python, но поскольку мы находимся в цикле, то следующая итерация цикла попытается использовать clean_line как функцию; и раз теперь это не функция, а переменная, которая ссылается на строку, то функция не может быть выполнена. Код заработает, если мы используем другое имя переменной для ссылки на возвращаемое значение функции, например cleaned_line: for line in input_file: # Очистить строку текста cleaned_line = clean_line(line) # Записать каждое слово из очищенной строки в выходной файл write_words_to_file(cleaned_line, output_file) Однако учтите, что мы должны изменить имя и там, где вызывается функция, и там, где мы задействуем ее возвращаемое значение тремя строками ниже. В остальном рефакторинг Colaboratory работает корректно, даже итерируя файл построчно, в то время как в исходном коде весь файл считывался целиком. Для Copilot процедура была аналогичной, но попроще. Я скопировала обе программы в один файл; затем выделила весь код, щелкнула правой кнопкой мыши и открыла Copilot; и наконец, запустила встроенный чат с похожим промптом:
   218 Глава 9. Функции Сделай рефакторинг выбранного кода, преобразовав его в функции для очистки и обработки данных. Цель состоит в том, чтобы перенести наибольшую часть логики в функции. Затем в форме разделенного экрана с указанием отличий Copilot выдал листинги исходного кода и его рефакторинга, который был принят без изменений. Copilot предложил следующее решение: # Сгенерировано Copilot import string def clean_line(line): # Преобразовать строку в нижний регистр line = line.lower() # Удалить знаки препинания из строки line = line.translate(str.maketrans('', '', string.punctuation)) return line def write_words_to_file(words, output_file): # Записать каждое слово в выходной файл for word in words: output_file.write(word + '\n') def count_word_occurrences(words): # Подсчитать количество вхождений каждого слова, используя словарь word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts def print_common_words(word_counts, n): # Отсортировать количество слов по убыванию sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести количество вхождений n наиболее распространенных слов print(f"{n} most common words:") for word, count in sorted_word_counts[:n]: print(f"{word}: {count}") def print_least_common_words(word_counts, n): # Отсортировать количество слов по убыванию sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести количество вхождений n наименее распространенных слов print(f"{n} least common words:") for word, count in sorted_word_counts[-n:]:
   9.9. Практическая работа: полезные функции 219 print(f"{word}: {count}") def process_file(input_file_path, output_file_path): # Открыть входной файл для чтения with open(input_file_path, 'r') as input_file: # Открыть выходной файл для записи with open(output_file_path, 'w') as output_file: # Перебрать каждую строку входного файла for line in input_file: # Очистить строку cleaned_line = clean_line(line) # Разделить строку на слова words = cleaned_line.split() # Записать слова в выходной файл write_words_to_file(words, output_file) def main(): input_file_path = 'moby_01.txt' output_file_path = 'moby_01_clean.txt' # Обработать файл process_file(input_file_path, output_file_path) # Открыть файл with open(output_file_path, 'r') as file: # Прочитать содержимое файла content = file.read() # Разделить содержимое файла на слова words = content.split() # Подсчитать количество вхождений слов word_counts = count_word_occurrences(words) # Вывести пять наиболее распространенных слов и количество их вхождений print_common_words(word_counts, 5) # Вывести пять наименее распространенных слов и количество их вхождений print_least_common_words(word_counts, 5) if __name__ == "__main__": main() Переработанная версия Copilot не содержит ошибок и в целом выполнена хорошо, но она по-прежнему считывает весь файл целиком, а не построчно, как это было в исходной версии. Один из недостатков обоих вариантов рефакторинга, выполненных средствами ИИ, заключается в том, что созданные функции документируются комментариями, начинающимися с символа #. В версии Colaboratory эти комментарии приводятся перед определением функции, а в версии Copilot — после него:
   220 Глава 9. Функции # Функция для подсчета вхождений каждого слова в списке слов def count_word_occurrences(words): def clean_line(line): # Преобразовать строку в нижний регистр Безусловно, наличие комментариев лучше, чем их отсутствие, однако для документирования функций в языке Python предпочтительнее использовать строку документации, заключенную в тройные кавычки, сразу после определения функции, как это сделано в версии, написанной программистом: Def clean_line(line): """изменяет регистр и удаляет знаки препинания""" Строка документации (докстринг) Это не просто вопрос эстетики — комментарии будут полностью игнорироваться при загрузке кода, в то время как строка документации станет значением свойства __doc__ объекта функции, доступным для использования инструментами документирования и функцией help() Python. В целом оба инструмента ИИ справились с изменением структуры нашего предыдущего кода и переносом основного функционала программы в функции. Даже с учетом небольшой ошибки, допущенной Colaboratory, возможность рефакторинга структуры, а не только отдельных строк кода весьма полезна. Итоги Функция определяется посредством ключевого слова def, имени, круглых скобок и двоеточия, за которыми следует блок кода, оканчивающийся необязательным оператором return. Аргументы могут передаваться по позиции или по имени параметра. Для параметров функций могут задаваться значения по умолчанию. Функции могут объединять аргументы в кортежи, что дает возможность определять функции, принимающие неограниченное количество аргументов. Функции могут собирать аргументы в словари, что предоставляет возможность определять функции, принимающие неограниченное количество аргументов, передаваемых по имени параметра. К внешним переменным можно легко обращаться внутри функции при помощи ключевых слов global или nonlocal. Ключевое слово lambda служит для создания анонимных встроенных функций. Объекты-генераторы создаются функциями, в которых вместо оператора return используется yield или yield from. В языке Python функции являются объектами первого класса, и это означает, что они могут присваиваться переменным, к ним можно обращаться с помощью переменных и декорировать.
10 Модули и правила области видимости В этой главе: 3 Определение модуля 3 Написание первого модуля 3 Использование оператора import 3 Изменение пути поиска модуля 3 Создание приватных имен в модулях 3 Импортирование стандартной библиотеки и сторонних модулей 3 Правила области видимости и пространств имен в языке Python В языке Python модули служат для структурирования больших проектов. Стандартная библиотека Python разделена на модули для повышения ее управляемости. Разбивать собственный код на модули не обязательно, тем не менее, если вы пишете программы объемом свыше нескольких страниц или собираетесь использовать ваш код многократно, вам, вероятнее всего, стоит так поступить. 10.1. Что такое модуль? Модуль — это файл с программным кодом. В нем содержатся определения набора функций или других объектов Python, а имя модуля является производным от имени файла.
   222 Глава 10. Модули и правила области видимости В модулях чаще всего хранится исходный код Python, но они также могут быть скомпилированными объектными файлами C или C++. Скомпилированные модули и исходные модули Python используются одинаковым образом. Помимо группировки взаимосвязанных объектов Python, модули помогают избежать проблем с конфликтами имен. Допустим, вы написали для своей программы модуль с именем mymodule, в котором определена функция reverse. В той же программе вы можете захотеть использовать написанный кем-то другим модуль othermodule, в котором тоже определена функция с именем reverse, делающая нечто отличное от вашей функции reverse. В языке, не поддерживающем модули, невозможно применять две разные функции с одним именем, к примеру, reverse. В Python это делается легко: в вашей основной программе вы просто обращаетесь к функциям по именам mymodule.reverse и othermodule.reverse. Применение имен модулей позволяет корректно одновременно выполнять две функции reverse в силу того, что Python использует пространства имен. Пространство имен по существу представляет собой словарь идентификаторов, доступных для блока, функции, класса, модуля и т. д. Пространства имен более подробно рассматриваются в конце главы, а пока просто запомните, что каждый модуль имеет собственное пространство имен, что помогает предотвратить конфликты имен. Кроме того, модули упрощают работу с Python. Многие стандартные функции Python не встроены в ядро языка, а предоставляются с помощью специальных модулей, которые можно загружать по мере необходимости. Работа с модулями в Colaboratory Так как в качестве предпочтительной среды для работы с Python мы выбрали Colaboratory, необходимо немного рассказать о том, как организовать в ней работу с модулями. Чтобы использовать модуль в Colaboratory, его необходимо загрузить в текущую сессию блокнота, что можно сделать, нажав на иконку Files слева, а затем на иконку Upload вверху. В блокнотах, прилагаемых к этой книге, имеется код для создания модуля или его извлечения из репозитория кода, чтобы при необходимости он был под рукой. Так, в блокноте к этой главе первая ячейка кода содержит код для написания первого модуля, и этот код необходимо выполнить, прежде чем пытаться получить доступ к модулю в следующих ячейках. Если вы работаете с интерпретатором Python, установленным на вашем локальном компьютере, то вам придется вручную скопировать все модули в текущий каталог, в котором вы используете интерпретатор Python. 10.2. Первый модуль Пожалуй, модули проще всего изучать на примере создания собственного модуля, к чему мы и приступим в этом разделе.
   10.2. Первый модуль 223 Поддержка создания или редактирования простого модуля Python в Colaboratory не слишком удобна, однако существует ряд вспомогательных решений. Для примеров, которые мы рассмотрим в тексте книги, прилагается код для записи содержимого модуля на диск, чтобы можно было импортировать полученный модуль. Обязательно выполните ячейку, содержащую этот код. Ниже приведен код, который ячейка в блокноте использует для создания файла модуля на диске. Обратите внимание, что все содержимое файла помещается в тройные одинарные кавычки, а затем применяются функция open для открытия файла для записи и метод write для записи в файл: open("mymath.py", "w").write( '''"""mymath - наш пример математического модуля""" pi = 3.14159 def area(r): """area(r): возвращает площадь круга с радиусом r.""" return(pi * r * r) ''') Листинг 10.1 — это простой листинг файла mymath.py. В дальнейшем мы воспользуемся таким стилем отображения листингов файлов, даже если в соответствующих блокнотах вспомогательный код будет либо записывать файл, либо извлекать его из репозитория. Листинг 10.1. Файл mymath.py """mymath - наш пример математического модуля""" pi = 3.14159 def area(r): """area(r): возвращает площадь круга с радиусом r.""" global pi return(pi * r * r) Код этого модуля просто присваивает значение pi и определяет функцию area. Для всех файлов с кодом Python настоятельно рекомендуется использовать расширение .py; по нему интерпретатор Python опознает файлы с исходным кодом Python. Как и в случае с функциями, в первой строке модуля можно разместить строку документации. Теперь попробуйте ввести следующее: pi ---------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-1-f84ab820532c> in <cell line: 1>() ----> 1 pi NameError: name 'pi' is not defined area(2) ----------------------------------------------------------------------
   224 Глава 10. Модули и правила области видимости NameError Traceback (most recent call last) <ipython-input-2-8be925061d22> in <cell line: 1>() ----> 1 area(2) NameError: name 'area' is not defined Эти ошибки означают, что в языке Python нет встроенной константы pi или функции area. Теперь попытаемся импортировать этот модуль: import mymath pi Загружает модуль из файла mymath.py --------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-2-85b32257cf04> in <cell line: 2>() 1 import mymath ----> 2 pi NameError: name 'pi' is not defined Вы подключили определения константы pi и функции area из файла mymath.py с помощью оператора import (который автоматически добавляет расширение .py при поиске файла, определяющего модуль с именем mymath). Однако новые определения недоступны для прямого обращения; при попытке ввести просто pi возникает ошибка, как и при попытке ввести просто area(2). Для обращения к pi и area их имена должны предварять имя содержащего их модуля: mymath.pi 3.14159 mymath.area(2) 12.56636 mymath.__doc__ Содержит докстринг модуля 'mymath - наш пример математического модуля' mymath.area.__doc__ Докстринг функции area 'area(r): возвращает площадь круга с радиусом r.' Обращение к атрибутам модуля с предваряющим добавлением имени содержащего их модуля гарантирует безопасность имен. Другой модуль, подгруженный к программе, может также содержать константу pi (и возможно, автор этого модуля счел значение числа «пи» равным 3.14 или 3.14159265), но это нас не волнует. Даже при импортировании другого модуля к его версии pi придется обращаться по имени othermodulename.pi, которое отличается от mymath.pi. Такая
   10.2. Первый модуль 225 форма доступа часто называется квалификацией (то есть pi квалифицируется именем модуля mymath). Константу pi также можно называть атрибутом mymath. Определения внутри модуля могут обращаться к другим определениям в этом модуле без предваряющего указания имени модуля. Функция mymath.area обращается к константе mymath.pi по короткому имени pi. При желании также можно запросить отдельные имена из модуля и импортировать их таким образом, чтобы не предварять их именем модуля, применив для этого инструкцию в формате from имя_модуля import атрибут: from mymath import pi pi 3.14159 area(2) -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-10-8be925061d22> in <cell line: 1>() ----> 1 area(2) NameError: name 'area' is not defined Имя pi становится напрямую доступным, поскольку вы запросили конкретно этот объект, применив инструкцию from mymath import pi. Однако к функции area все равно придется обращаться по имени mymath.area, поскольку она не была явным образом импортирована. Перезагрузка модуля При разработке и тестировании модуля в интерактивном режиме вы вносите изменения в модуль, а затем повторно импортируете его в текущую сессию для тестирования. Но если вы измените ваш модуль на диске или загрузите новую версию в Colaboratory, повторный ввод команды import не приведет к его перезагрузке, поскольку Python отслеживает загруженные модули и не станет загружать один и тот же модуль дважды. Для загрузки свежей версии модуля с диска существует два варианта: можно зайти в меню Runtime в Colaboratory и выбрать Restart Session, что создаст новую сессию интерпретатора Python, или можно воспользоваться для этой цели функцией reload из модуля importlib. Модуль importlib предоставляет интерфейс к механизмам, лежащим в основе импортирования модулей: import mymath, importlib importlib.reload(mymath) <module 'mymath' from '/content/mymath.py'> Когда модуль перезагружается (или импортируется в первый раз), выполняется анализ (парсинг) его кода. При обнаружении ошибки генерируется синтаксическое
   226 Глава 10. Модули и правила области видимости исключение. Если все в порядке, создается файл с расширением .pyc (например, mymath.pyc), содержащий байт-код Python. При перезагрузке модуля программа не возвращается точно к такому же состоянию, как при запуске новой сессии и первом импортировании модуля. Впрочем, имеющие­ ся различия обычно не вызывают проблем. Если эта тема вас заинтересовала, найти подробную информацию о reload можно в разделе, посвященном модулю importlib, в справочнике по языку Python на странице https://docs.python.org/3/reference/import. html в разделе importlib. Разумеется, модулями можно пользоваться не только из интерактивной оболочки Python. Их также можно импортировать в скрипты (и другие модули, если на то пошло), добавив соответствующие операторы import в начало файла программы. Во внутренних механизмах Python интерактивная сессия и скрипт также считаются модулями. Подведем итоги. Модуль представляет собой файл, определяющий один или несколько объектов Python. Если имя файла модуля — modulename.py, то имя модуля в языке Python — modulename. Можно использовать модуль с именем modulename с помощью оператора import modulename. После выполнения этого оператора к объектам, определенным в данном модуле, можно обращаться в формате modulename. имя_объекта. Конкретные имена из модуля можно напрямую импортировать в программу с помощью оператора from modulename import имя_объекта. Этот оператор делает имя_объекта доступным для программы без необходимости предварительно добавлять к нему имя_модуля и полезен для импорта часто используемых имен. 10.3. Оператор import Оператор import может иметь три различные формы. Простейшая из них следующая: import имя_модуля Она осуществляет поиск модуля Python с заданным именем, выполняет парсинг и предоставляет к нему доступ. Импортирующий код может использовать содержимое модуля, но любые ссылки в этом коде на имена внутри модуля по-прежнему должны предваряться именем модуля. Если модуль с указанным именем не найден, возникает ошибка. О том, где именно Python ищет модули, будет рассказано в разделе 10.4.
   10.4. Путь поиска модулей 227 Вторая форма позволяет явным образом импортировать в код конкретные имена из модуля: from имя_модуля import имя1, имя2, имя3, . . . Каждое имя имя1, имя2 и так далее из модуля имя_модуля становится доступным для импортирующего кода; после выполнения оператора import в коде можно использовать имя1, имя2, имя3 и так далее, не предваряя их именем модуля. Наконец, существует третья, обобщенная форма оператора from… import…: from имя_модуля import * Символ * обозначает все экспортируемые имена из имя_модуля. Оператор from modulename import * импортирует все публичные имена из имя_модуля (то есть имена, не начинающиеся с символа подчеркивания) и делает их доступными для импортирующего кода без необходимости предварительно указывать имя модуля. Но если в модуле существует список имен __all__ (или файл пакета __init__.py), то именно эти имена будут импортироваться независимо от того, начинаются они с символа подчеркивания или нет. Например, можно применить такой стиль к файлу mymath.py: from mymath import * Мы сможем напрямую обращаться к pi и area() без указания имени модуля. При использовании последней формы импорта следует соблюдать осторожность. Если одно и то же имя определяется в двух модулях и вы импортируете оба модуля при помощи такой формы импорта, возникнет конфликт имен и имя из первого модуля будет заменено именем из второго модуля. Кроме того, в такой форме читателю вашего кода будет труднее определить происхождение используемых имен. С любой из двух предшествующих форм оператора import вы предоставляете читателю кода конкретную информацию об их источнике. Впрочем, некоторые модули (к примеру, tkinter) присваивают своим функциям имена, с которыми их происхождение становится очевидным, а риск конфликтов имен исключается. Также обобщенная форма import нередко используется для того, чтобы избежать лишних нажатий клавиш в интерактивной оболочке. 10.4. Путь поиска модулей Каталоги, в которых Python ищет модули, указываются в переменной с именем path; к ней можно обратиться через модуль с именем sys. Введите следующее: import sys sys.path import sys
   228 Глава 10. Модули и правила области видимости sys.path ['/content', '/env/python', '/usr/lib/python310.zip', '/usr/lib/python3.10', '/usr/lib/python3.10/lib-dynload', '', '/usr/local/lib/python3.10/dist-packages', '/usr/lib/python3/dist-packages', Содержимое листинга зависит от вашей установки Python '/usr/local/lib/python3.10/dist-packages/IPython/extensions', '/root/.ipython'] Приведенное выше значение зависит от конфигурации вашей системы. Независимо от деталей, sys.path указывает список каталогов, в которых Python производит поиск (по порядку) при попытке выполнить оператор import. Задействуется первый обнаруженный модуль, удовлетворяющий запросу оператора import. Если подходящий модуль не найден, возникает исключение ImportError. Переменная sys.path инициализируется значением переменной окружения (операционной системы) PYTHONPATH, если таковая существует, или же значением по умолчанию, зависящим от вашей установки. Кроме того, при каждом запуске скрипта Python в качестве первого элемента списка, на который ссылается переменная sys.path этого скрипта, добавляется каталог, в котором находится этот скрипт, что обеспечивает удобный способ определения расположения выполняемой программы Python. В интерактивной сессии, подобной приведенной выше, первому элементу sys.path присваивается пустая строка, что Python воспринимает как указание начать поиск модулей с текущего каталога. 10.4.1. Где следует размещать собственные модули В примере, приведенном в начале главы, модуль mymath доступен для Python, поскольку, во-первых, при интерактивном выполнении Python sys.path начинается с пустой строки "", предписывающей Python выполнять поиск модулей в текущем каталоге, и, во-вторых, вы загрузили файл mymath.py в текущий каталог Colaboratory. В реальной рабочей среде эти условия не обязательно будут выполняться. Вы не будете запускать Python в интерактивном режиме, а файлы с кодом Python не будут находиться в текущем каталоге. Чтобы ваши программы могли использовать написанные вами модули, необходимо: разместить ваши модули в одном из каталогов, в которых Python обычно ищет модули; разместить все модули, используемые программой Python, в одном каталоге с программой; создать каталог (или каталоги) для хранения ваших модулей и модифицировать список, на который ссылается переменная sys.path, включив в него этот новый каталог (или каталоги).
   10.4. Путь поиска модулей 229 Из этих трех вариантов первый, разумеется, реализуется проще всего; тем не менее этот вариант следует выбирать только в том случае, если ваша версия Python включает локальные каталоги с кодом в путь поиска модулей по умолчанию. Такие каталоги специально предназначены для кода, относящегося к конкретному сайту (то есть кода, специфичного для вашего компьютера); они не будут перезаписаны при новой установке Python, поскольку не являются ее частью. Если ваша переменная sys.path ссылается на такие каталоги, вы можете разместить в них свои модули. Второй вариант хорошо подходит для модулей, связанных с конкретной программой. Просто храните такие модули вместе с программой. Третий вариант выбирается для модулей, привязанных к конкретному сайту, которые будут использоваться в нескольких программах на этом сайте. Переменную sys.path можно изменить разными способами. Ей можно присвоить значение в коде; сделать это несложно, но тогда местонахождение каталогов жестко фиксируется в программном коде. Можно задать переменную среды PYTHONPATH, что тоже относительно просто, но такое изменение может затронуть не всех пользователей сайта; наконец, можно добавить значение в путь поиска по умолчанию при помощи файла .pth. Примеры настройки PYTHONPATH приведены в документации Python в разделе «Настройка и использование Python» (подраздел «Командная строка и среда»). Каталог или каталоги, присвоенные этой переменной, подставляются в начало значений в объекте, на который ссылается переменная sys.path. При применении PYTHONPATH будьте внимательны и избегайте определения модуля с таким же именем, что и у одного из существующих библиотечных модулей, которым вы пользуетесь. Если это произойдет, ваш модуль будет найден раньше библиотечного модуля. Иногда это именно то, что нужно, но такие случаи относительно редки. Проблемы можно избежать при помощи файла .pth. В этом случае каталог или каталоги, добавленные вами, присоединяются к sys.path. Последний из этих механизмов лучше всего пояснить на примере. В Windows файл .pth можно поместить в каталог, на который указывает sys.prefix. Допустим, у вас sys. prefix указывает на каталог c:\program files\python; разместите файл из листинга 10.2 в этом каталоге. Листинг 10.2. Файл mymodules.pth mymodules c:\Users\naomi\My Documents\python\modules При следующем запуске интерпретатора Python в переменную sys.path будут добавлены каталоги c:\program files\python\mymodules и c:\Users\naomi\My Documents\ python\modules (если они существуют). Теперь можно размещать свои модули в этих каталогах. Обратите внимание: существует риск перезаписи каталога mymodules при новой установке Python. Каталог modules безопаснее для работы. Возможно, вам также придется переместить или создать файл mymodules.pth
   230 Глава 10. Модули и правила области видимости при обновлении Python. Если вам нужна более подробная информация об использовании файлов .pth, обращайтесь к описанию модуля site в документации к стандартной библиотеке Python. 10.5. Приватные имена в модулях Ранее в этой главе уже говорилось о том, что с помощью from module import * можно импортировать почти все имена из модуля. Исключение составляют идентификаторы в модуле, начинающиеся с символа подчеркивания (_), которые нельзя импортировать с помощью from module import *. Можно писать модули, предназначенные для импорта, при помощи from module import *, но при этом ограничить импортирование некоторых функций или переменных. Если все внутренние имена (то есть имена, к которым нельзя обратиться за пределами модуля) будут начинаться с символа подчеркивания, вы тем самым гарантируете, что from module import * вернет только те имена, которые понадобятся пользователю. Чтобы увидеть, как работает этот механизм, представьте, что у вас имеется файл modtest.py с кодом, как в листинге 10.3. Листинг 10.3. Файл modtest.py """modtest: наш тестовый модуль""" def f(x): return x Имя приватной функции def _g(x): return x a = 4 _b = 2 Имя приватной переменной Теперь запустите интерактивную сессию и введите следующее: from modtest import * f(3) 3 _g(3) -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-15-787abdf7161a> in <cell line: 1>() ----> 1 _g(3) NameError: name '_g' is not defined a 4 _b --------------------------------------------------------------------------
   10.6. Модули стандартной библиотеки Python и сторонние 231 NameError Traceback (most recent call last) <ipython-input-17-8352e74fe22a> in <cell line: 1>() ----> 1 _b NameError: name '_b' is not defined Как видите, имена f и a импортируются, а _g и _b за пределами modtest остаются скрытыми. Обратите внимание, что это происходит лишь при использовании from ... import *. Для обращения к _g или _b можно поступить так: import modtest modtest._b 2 from modtest import _g _g(5) 5 Соглашение об использовании нижнего подчеркивания для обозначения приватных имен распространяется не только на модули, а в целом на весь язык Python. 10.6. Модули стандартной библиотеки Python и сторонние В начале главы уже упоминалось о том, что стандартный дистрибутив Python разбивается на модули для того, чтобы с ним было удобнее работать. После установки Python вы получаете доступ ко всей функциональности библиотечных модулей. Все, что для этого требуется, — импортировать соответствующие модули, функции, классы и т. п. В этой книге приводятся многие широко используемые и полезные модули стандартной библиотеки. Однако дистрибутив Python включает гораздо больше модулей, чем описано в нашем курсе. Вам следует как минимум просмотреть содержание документации к стандартной библиотеке Python. Доступные сторонние модули и ссылки на них приведены в каталоге Python Package Index (pyPI), который будет рассмотрен в главе 19. Чтобы эти модули можно было импортировать в ваши программы, необходимо загрузить их и установить в каталоге, входящем в путь поиска модулей. Быстрая проверка: модули Предположим, имеется модуль с именем new_math, содержащий функцию с именем new_divide. Какими способами можно импортировать, а затем использовать эту функцию? Каковы плюсы и минусы каждого способа? Допустим, модуль new_math содержит функцию _helper_math(). Как символ подчеркивания повлияет на способ ее импортирования?
   232 Глава 10. Модули и правила области видимости 10.7. Правила области видимости и пространств имен в языке Python Интерес к правилам области видимости и пространствам имен Python будет повышаться по мере роста вашего опыта программирования на этом языке. Если вы только начинаете программировать на Python, вероятно, вам будет достаточно бегло просмотреть этот раздел, чтобы уловить основные идеи. За более подробной информацией обращайтесь к описанию пространств имен в документации к стандартной библиотеке Python. Понятие пространства имен является здесь центральным. Пространство имен (namespace) в Python представляет собой сопоставление, направленное от идентификаторов к объектам, — то есть способ, при помощи которого Python отслеживает, какие переменные и идентификаторы активны и на что они указывают. Так, выражение x = 1 добавляет переменную x в пространство имен (при условии, что она еще не входит в него) и связывает ее со значением 1. У блока кода, выполняемого в Python, есть три пространства имен: локальное, глобальное и встроенное (рис. 10.1). Встроенное пространство имен Встроенные функции Глобальное пространство имен Функции модуля Переменные модуля Локальное пространство имен Когда во время выполнения программы Локальные Локальные обнаруживается идентификатор, Python функции переменные сначала ищет его в локальном пространстве имен. Если идентификатор не найден, поиск продолжается в глобальном Рис. 10.1. Порядок проверки пространстве имен. Если идентификатор пространств имен при поиске идентификаторов и здесь не обнаружен, проверяется встроенное пространство имен. Если и там его нет, Python решает, что произошла ошибка, и выдает исключение NameError. Для модуля, команды, выполненной в интерактивной сессии, или скрипта, запускаемого из файла, глобальные и локальные пространства имен совпадают. Создание любой переменной и функции или импортирование чего-либо из другого модуля приводит к появлению новой записи (или привязки) в этом пространстве имен. Однако при вызове функции создается локальное пространство имен, в котором для каждого параметра вызова создается отдельная привязка. Новая привязка добавляется в это локальное пространство имен при каждом создании переменной внутри функции. Тогда как глобальным пространством имен функции
   10.7. Правила области видимости и пространств имен в языке Python 233 является глобальное пространство имен содержащего данную функцию блока (то есть модуля, файла скрипта или интерактивной сессии). Оно не зависит от динамического контекста, из которого совершается вызов. Во всех перечисленных случаях встроенным пространством имен является пространство имен модуля __builtins__. Среди прочего, этот модуль содержит все встроенные функции, уже встречавшиеся вам в книге (такие, как len, min, max, int, float, list, tuple, range, str и repr), а также другие встроенные классы Python, в частности, исключения (такие, как NameError). Начинающих программистов на Python иной раз сбивает с толку возможность переопределения элементов во встроенном модуле. Например, если создать в программе список и присвоить его переменной с именем list, вы в дальнейшем не сможете использовать встроенную функцию list. При поиске сначала будет обнаружена привязка для вашей переменной list. Имена функций, модулей и других объектов в этом отношении не исключение. Для любого заданного идентификатора всегда задействуется самое последнее вхождение привязки. Впрочем, хватит разговоров — пора рассмотреть несколько примеров. В них используются две встроенные функции: locals и globals. Эти функции возвращают словари, содержащие привязки в локальном и глобальном пространствах имен соответственно. Посмотрим, что представляют собой эти два пространства имен в нашей текущей сессии: locals() {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '_ih': ['', ... (Many other entries} globals() {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '_ih': ['', ... (Many other entries} Локальное и глобальное пространства имен для данной сессии блокнота совпадают. Они содержат три исходные пары «ключ-значение» для внутреннего
   234 Глава 10. Модули и правила области видимости использования: (1) строка документации __doc__, (2) имя основного модуля __name__ (которым всегда является __main__ для интерактивных сессий и скриптов, запускаемых из файлов) и (3) модуль, используемый для встроенного пространства имен __builtins__ (модуль __builtins__). Если теперь создать переменную и импортировать модули, вы увидите, что в пространствах имен было создано несколько привязок (в самом конце длинного списка): z = 2 import math from cmath import cos globals() 'z': 2, 'math': <module 'math' (built-in)>, 'cos': <function cmath.cos(z, /)>} locals() 'z': 2, 'math': <module 'math' (built-in)>, 'cos': <function cmath.cos(z, /)>} math.ceil(3.4) 4 Как и ожидалось, локальное и глобальное пространства имен остаются эквивалентными. В них были добавлены записи для z как числа, math как модуля и cos как функции из модуля cmath. Можно воспользоваться ключевым словом del для удаления этих новых привязок из пространства имен (включая привязки модулей, созданные с помощью операторов import): del z, math, cos locals() {'__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__name__': '__main__', '__doc__': None} math.ceil(3.4) -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-26-307799385d47> in <cell line: 1>() ----> 1 math.ceil(3.4) NameError: name 'math' is not defined import math math.ceil(3.4) 4
   10.7. Правила области видимости и пространств имен в языке Python 235 Ничего критичного в удалении привязок нет, поскольку вы можете снова импортировать модуль math и работать с ним. Такое использование del может быть особенно удобным в интерактивном режиме. ПРИМЕЧАНИЕ Применение del, а затем повторно import не отобразит изменения, внесенные в модуль на диске. Модуль не удаляется из памяти, чтобы затем снова загрузиться с диска, — вместо этого сама привязка сначала удаляется, а затем возвращается в пространство имен. Если вы хотите отразить изменения, внесенные в файл, вам все равно придется воспользоваться функцией перезагрузки importlib.reload. Для любителей поэкспериментировать: да, del может использоваться для удаления записей __doc__, __main__ и __builtins__. Однако стоит воздержаться от подобных действий, так как это не пойдет на пользу вашей сессии! А теперь присмотримся к функции, созданной в интерактивной сессии: def f(x):\ print("Entry local: ", locals()) y = x print("Exit local: ", locals()) z = 2 f(z) Entry local: {'x': 2} Exit local: {'x': 2, 'y': 2} Взглянув на локальные переменные изнутри функции, мы увидим, что, как и ожидалось, при ее вызове параметр x уже является исходной записью в локальном пространстве имен функции f, но к моменту завершения работы функции к нему будет добавлена переменная y. В рабочей среде, как правило, вызываются функции, определенные в модулях. Их глобальное пространство имен — это пространство имен модуля, в котором эти функции определены. Допустим, вы создали файл с кодом, как в листинге 10.4. Листинг 10.4. Файл scopetest.py """scopetest: тестовый модуль для области видимости""" v = 6 def f(x): """f: функция тестирования области видимости""" print("global: ", list(globals().keys())) print("entry local:", locals()) y = x w = v print("exit local:", locals().keys()) Обратите внимание: мы выводим только ключи (идентификаторы) словаря, возвращаемого при помощи globals, чтобы снизить количество лишних элементов
   236 Глава 10. Модули и правила области видимости в результатах. Выводятся только ключи, потому что модули оптимизированы для хранения всего словаря __builtins__ в качестве поля значения ключа __builtins__: import scopetest z = 2 scopetest.f(z) global: ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'v', 'f'] entry local: {'x': 2} exit local: dict_keys(['x', 'w', 'y']) Теперь глобальное пространство имен относится к модулю scopetest и включает функцию f и переменную v, которой присвоено целое число (но не переменную z из вашей интерактивной сессии). Таким образом, при создании модуля у вас есть полный контроль над пространствами имен его функций. 10.7.1. Встроенное пространство имен Мы рассмотрели локальные и глобальные пространства имен. Перейдем к встроенному пространству имен. В этом примере представлена еще одна встроенная функция — dir, возвращающая для заданного модуля список определенных в нем имен: dir(__builtins__) ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__IPYTHON__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'aiter', 'all', 'anext', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display', 'divmod', 'enumerate', 'eval', 'exec', 'execfile', 'filter', 'float', 'format', 'frozenset',
   10.7. Правила области видимости и пространств имен в языке Python 237 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', 'runfile', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] Список получился весьма внушительным. Записи, заканчивающиеся на Error и Exit , — это имена встроенных в Python исключений. Мы рассмотрим их в главе 14. Последнюю группу (от abs до zip) образуют встроенные функции языка Python. Многие из них вам уже встречались, другие еще встретятся, но не все они будут освещены в этой книге. При необходимости для любой встроенной функции можно легко найти описание в документации к стандартной библиотеке Python. Также есть возможность получить строку документации для любой из них при помощи функции help() или вывести докстринг прямо на экран: print(max.__doc__) max(iterable, *[, default=obj, key=func]) -> value max(arg1, arg2, *args, *[, key=func]) -> value With a single iterable argument, return its biggest item. The default keyword-only argument specifies an object to return if the provided iterable is empty. With two or more arguments, return the largest argument. Как отмечалось ранее, начинающие программисты Python иногда случайно переопределяют встроенную функцию: list("Peyto Lake") ['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e'] list = [1, 3, 5, 7] list("Peyto Lake") Присваивает list значение [1, 3, 5, 7] -------------------------------------------------------------------------TypeError Traceback (most recent call last) <ipython-input-39-7845f2807d9f> in <cell line: 2>() 1 list = [1, 3, 5, 7] ----> 2 list("Peyto Lake") TypeError: 'list' object is not callable Интерпретатор Python прекращает поиск после новой привязки list в качестве переменной list, хотя вы и используете синтаксис встроенной функции list. Разумеется, то же самое произойдет при попытке использовать один идентификатор дважды в одном пространстве имен. Предыдущее значение будет перезаписано независимо от его типа:
   238 Глава 10. Модули и правила области видимости import mymath mymath = mymath.area mymath.pi AttributeError Traceback (most recent call last) <ipython-input-40-4e7325de62ae> in <cell line: 3>() 1 import mymath 2 mymath = mymath.area ----> 3 mymath.pi AttributeError: 'function' object has no attribute 'pi' Если вам известно о таких случаях, для вас это не станет большой проблемой. В конце концов, повторное использование идентификаторов, даже для разных типов объектов, не сделает код более читабельным. Если же вы случайно совершите подобную ошибку в интерактивном режиме, проблема легко решается: удалите нежелательную привязку (переменную) с помощью del, чтобы снова получить доступ к переопределенному встроенному объекту (или функции), или заново импортируйте ваш модуль для восстановления доступа: del list list("Peyto Lake") ['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e'] import mymath mymath.pi 3.14159 Функции locals и globals могут пригодиться в качестве простых инструментов отладки. Функция dir не возвращает текущие настройки, но при вызове без параметров она возвращает отсортированный список идентификаторов в локальном пространстве имен. Этот прием помогает в поиске опечаток в именах переменных, которые обычно автоматически обнаруживаются компилятором в языках с обязательными объявлениями: x1 = 6 xl = x1 - 2 x1 Букву «L» в нижнем регистре легко спутать с цифрой «1» 6 dir() ... 'mymath', 'quit', 'x1', 'xl', 'z'] Переменные в Jupyter Notebook Инструмент переменных в Colaboratory, доступ к которому осуществляется с помощью значка {x} слева, также позволяет просматривать значения переменных, которые в данный момент активны в сеансе работы с блокнотом.
   10.8. Практическая работа: создание модуля 239 Быстрая проверка: пространства имен и область видимости Имеется переменная width в модуле make_window.py. В каком из следующих контекстов width входит в область видимости? (A) Внутри самого модуля. (B) Внутри функции resize() из этого модуля. (C) Внутри скрипта, импортировавшего модуль make_window.py. 10.8. Практическая работа: создание модуля Переместите функции, созданные в конце главы 9, в отдельный модуль. И хотя вы можете при желании включить в модуль код для его запуска в качестве основной программы, цель состоит в том, чтобы все функции можно было свободно применять из других скриптов. Для проверки создайте новый Jupyter Notebook и напишите код для загрузки модуля и работы с ним, чтобы получить те же результаты, что и с помощью кода из главы 9. Загрузка модуля в Colaboratory Если вы написали свой модуль в редакторе кода или интегрированной среде разработки, вам потребуется загрузить его в Colaboratory. Для этого нужно выбрать иконку папки с файлами, а затем нажать на иконку загрузки документа (как показано на рис. 10.2), в результате чего откроется диалоговое окно загрузки файла. Рис. 10.2. Иконки файлов и загрузки в Colaboratory
   240 Глава 10. Модули и правила области видимости 10.8.1. Решение задачи при помощи кода, сгенерированного ИИ Эта практическая работа не ставит своей целью написание нового кода, а вновь предполагает переработку существующего кода с применением новой структуры. Функции, которые мы создали в предыдущей главе, теперь необходимо переместить в отдельный файл или модуль, а остальной код, задействующий эти функции, требуется изменить так, чтобы он импортировал этот модуль и корректно вызывал из него функции. Чтобы быть уверенными в том, что мы должным образом разделяем компоненты программного кода, в нашем промпте должно быть явно указано на необходимость размещения кода функций в одном файле, а клиентского кода — в другом. Наконец, нужно удостовериться в том, что чат-боты имеют доступ к коду, рефакторинг которого мы выполнили в предыдущей главе — либо в определенных ячейках нашего текущего блокнота, либо в текущем файле, если мы работаем с VS Code и Copilot. 10.8.2. Решения и обсуждение В этой задаче нужно не столько писать код, сколько реорганизовать и доработать уже существующий. Функции можно просто скопировать и вставить в новый файл модуля, а новый основной файл нужно изменить так, чтобы функции модуля корректно импортировались и вызывались в зависимости от того способа, каким они были импортированы. К примеру, было бы вполне допустимо (хотя и сомнительно с точки зрения хорошего стиля Python) импортировать все разом в активное пространство имен: from clean_string_module import * В таком случае в основной программе можно было бы по-прежнему использовать просто имена функций (без указания модуля). Если же применить форму импортирования import clean_string_module, то каждый вызов функции придется предварять именем модуля. Программное решение, созданное человеком Решение, предложенное программистом, достаточно прямолинейно: оно просто переносит функции в файл модуля text_processing_author.py, а затем импортирует модуль и добавляет его имя перед всеми вызовами функций из этого модуля: with open("text_processing_author.py", "w") as f: f.write(r''' # Авторская версия import string punct = str.maketrans('', '', string.punctuation) def clean_line(line): """изменяет регистр и удаляет знаки препинания""" # привести весь текст к единому регистру
   10.8. Практическая работа: создание модуля cleaned_line = line.lower() # удалить знаки препинания cleaned_line = cleaned_line.translate(punct) return cleaned_line def get_words(line): """разбивает строку на слова и объединяет символами новой строки""" words = line.split() return "\n".join(words) + "\n" def count_words(words): """принимает список очищенных слов, возвращает словарь подсчета слов""" word_count = {} for word in words: count = word_count.setdefault(word, 0) word_count[word] += 1 return word_count def word_stats(word_count): """Принимает словарь подсчета слов и возвращает первые и последние пять записей""" word_list = list(word_count.items()) word_list.sort(key=lambda x: x[1]) least_common = word_list[:5] most_common = word_list[-1:-6:-1] return most_common, least_common ''' ) import text_processing_author with open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as outfile: for line in infile: cleaned_line = text_processing_author.clean_line(line) cleaned_words = text_processing_author.get_words(cleaned_line) # записать все слова построчно outfile.write(cleaned_words) moby_words = [] with open('moby_01_clean.txt') as infile: for word in infile: if word.strip(): moby_words.append(word.strip()) word_count = text_processing_author.count_words(moby_words) most, least = text_processing_author.word_stats(word_count) 241
   242 Глава 10. Модули и правила области видимости print("Most common words:") for word in most: print(word) print("\n Least common words:") for word in least: print(word) Программные решения, сгенерированные ИИ Для Colaboratory этот процесс столь же прост. Поскольку код, который мы разработали в практической работе предыдущей главы, является нашей отправной точкой, требуется добавить его (то есть код, содержащий функции для очистки/ обработки текста) в ячейку нашего текущего блокнота. Поместив код в ячейку, мы можем указать чат-боту ссылаться на эту ячейку для создания нашего модуля и клиентского кода, что мы и сделаем при помощи следующего промпта: Упакуй функции в ячейке выше в автономный модуль так, чтобы его можно было использовать в другом коде. Затем напиши код, который будет использовать этот модуль для достижения того же результата, что и в ячейке выше. Результаты Colaboratory выглядят многообещающе, и мы видим, что чат-бот догадался даже записать модуль в файл, заключив фактический код модуля в длинную строку с тройными кавычками, как это сделано в версии программиста. Впрочем, как мы убедимся чуть позже, ИИ все же оказался недостаточно сообразительным. В любом случае, Colaboratory предлагает следующее: # Сгенерировано Colaboratory # prompt: Упакуй функции в ячейке выше в автономный модуль так, # чтобы его можно было использовать в другом коде. Затем напиши код, # который будет использовать этот модуль для достижения того же # результата, что и в ячейке выше. # Сохранить функции в модуль с именем "text_processing.py" with open("text_processing.py", "w") as f: f.write(""" def clean_line(line): # Преобразовать строку в нижний регистр line = line.lower() # Удалить знаки препинания из строки line = line.translate(str.maketrans('', '', string.punctuation)) return line def write_words_to_file(words, output_file): # Записать каждое слово в выходной файл for word in words: output_file.write(word + '\n') def count_word_occurrences(words): # Подсчитать количество вхождений каждого слова, используя словарь
   10.8. Практическая работа: создание модуля word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts def print_common_words(word_counts, n): # Отсортировать количество слов по убыванию sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести количество n наиболее распространенных слов и их вхождений print(f"{n} most common words:") for word, count in sorted_word_counts[:n]: print(f"{word}: {count}") def print_least_common_words(word_counts, n): # Отсортировать количество слов по убыванию sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести количество n наименее распространенных слов и их вхождений print(f"{n} least common words:") for word, count in sorted_word_counts[-n:]: print(f"{word}: {count}") def process_file(input_file_path, output_file_path): # Открыть входной файл для чтения with open(input_file_path, 'r') as input_file: # Открыть выходной файл для записи with open(output_file_path, 'w') as output_file: # Перебрать каждую строку во входном файле for line in input_file: # Очистить строку cleaned_line = clean_line(line) # Разделить строку на слова words = cleaned_line.split() """) # Записать слова в выходной файл write_words_to_file(words, output_file) # Использовать модуль import text_processing input_file_path = 'moby_01.txt' output_file_path = 'moby_01_clean.txt' # Обработать файл text_processing.process_file(input_file_path, output_file_path) 243
244    Глава 10. Модули и правила области видимости # Открыть файл with open(output_file_path, 'r') as file: # Считать содержимое файла content = file.read() # Разделить содержимое на слова words = content.split() # Подсчитать количество вхождений слов word_counts = text_processing.count_word_occurrences(words) # Вывести пять наиболее распространенных слов и количество их вхождений text_processing.print_common_words(word_counts, 5) # Вывести пять наименее распространенных слов и количество их вхождений text_processing.print_least_common_words(word_counts, 5) Источником проблемы является способ, каким Python записывает в файл большую строку, содержащую код модуля. При попытке выполнить эту ячейку мы получаем странную ошибку: File "/usr/local/lib/python3.10/dist-packages/IPython/core/ interactiveshell.py", line 3553, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "<ipython-input-1-b6ebc2b78dfe>", line 70, in <cell line: 70> import text_processing File "/content/text_processing.py", line 14 output_file.write(word + ' ^ SyntaxError: unterminated string literal (detected at line 14) При загрузке сгенерированного модуля text_processing.py в строке 14 мы увидим output_file.write(word + ' ') вместо output_file.write(word + '\n') что, собственно, и намеревался сделать наш ИИ-ассистент. Иными словами, при записи файла модуля на диск escape-последовательность '\n', которую, по нашему замыслу, модуль должен добавлять к обрабатываемым им строкам текста, вместо этого записывается как перевод строки в самом коде, что и приводит к ошибке. Зная причину, исправить это легко — чтобы символ '\n' преждевременно не превращался в реальный управляющий символ, нам нужно указать Python, чтобы он трактовал всю строку как необработанный текст, который не следует интерпретировать в обычном режиме. Решить это можно, добавив
   10.8. Практическая работа: создание модуля 245 префикс r непосредственно перед тройными кавычками, как в предыдущем варианте, написанном программистом: with open("text_processing.py", "w") as f: f.write(r""" Разобравшись с этим моментом и повторно запустив программу, мы наталкиваемся еще на одно исключение: -------------------------------------------------------------------------NameError Traceback (most recent call last) <ipython-input-2-472b2a5e47db> in <cell line: 76>() 74 75 # Обработать файл ---> 76 text_processing.process_file(input_file_path, output_file_path) 77 78 # Открыть файл 1 frames /content/text_processing.py in clean_line(line) 5 6 # Удалить знаки препинания из строки ----> 7 line = line.translate(str.maketrans('', '', string.punctuation)) 8 9 return line NameError: name 'string' is not defined Изучив код модуля, мы обнаружим, что он не импортирует модуль string, необходимый для предоставления знаков препинания в string.punctuation. Для устранения этой проблемы можно добавить import string в верхнюю часть кода нашего модуля: with open("text_processing.py", "w") as f: f.write(r""" import string def clean_line(line): Однако когда мы сделаем это и выполним ячейку, чтобы повторно сгенерировать наш модуль, ошибка, похоже, никуда не исчезнет. Происходит так потому, что исходный неисправленный модуль все еще загружен в текущую сессию Python, как упоминалось во врезке «Перезагрузка модуля» раздела 10.2. Опять же, мы можем либо перезапустить сеанс (в меню Runtime в Colaboratory), либо импортировать функцию reload из importlib: import importlib importlib.reload(text_processing) Как только мы внесем эту правку, вновь сгенерированный модуль заработает как нужно.
   246 Глава 10. Модули и правила области видимости Для Copilot мы можем просто открыть файл из главы 9, созданный в VS Code, выбрать функции и предложить чат-боту следующий промпт: Упакуй функции в ячейке выше в автономный модуль так, чтобы его можно было использовать в другом коде. Затем напиши код, который будет использовать этот модуль для достижения того же результата, что и в ячейке выше. Для этого необходимо использовать полную сессию чата Copilot (доступ к ней осуществляется с помощью иконки «речевого облака» слева). Затем Copilot сначала описывает шаги, которые он собирается выполнить. План такой: 1. Создать новый файл Python с именем text_processing.py. 2. Скопировать все функции из активного текстового выделения в этот новый файл. 3. В конце файла добавить условие if __name__ == "__main__", чтобы разрешить или запретить выполнение частей кода при импорте модулей. 4. Внутри этого условного оператора написать код, использующий функции для достижения того же результата, что и в исходном коде. Для этого нужно вызвать process_file с указанием путей к входному и выходному файлам. 5. В текущем файле Python пользователя импортировать новый модуль и задействовать его функции. Затем Copilot генерирует файл модуля и основной файл, использующий этот модуль, при этом оба файла нужно сохранить вручную с именами, указанными в комментариях в верхней части каждого файла: # ИМЯ ФАЙЛА: word_processing.py import string def clean_line(line): line = line.lower() line = line.translate(str.maketrans('', '', string.punctuation)) return line def write_words_to_file(words, output_file): for word in words: output_file.write(word + '\n') def count_word_occurrences(words): word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts def print_common_words(word_counts, n): sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) print(f"{n} most common words:") for word, count in sorted_word_counts[:n]: print(f"{word}: {count}")
   10.8. Практическая работа: создание модуля 247 def print_least_common_words(word_counts, n): sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) print(f"{n} least common words:") for word, count in sorted_word_counts[-n:]: print(f"{word}: {count}") def process_file(input_file_path, output_file_path): with open(input_file_path, 'r') as input_file: with open(output_file_path, 'w') as output_file: for line in input_file: cleaned_line = clean_line(line) words = cleaned_line.split() write_words_to_file(words, output_file) Copilot прекрасно справился с задачей создания файла модуля, сделав рабочий файл с корректным импортом модуля string. Мое единственное замечание к этому модулю — в нем удалены комментарии, которые документировали каждую функцию. Как отмечалось в прошлой главе, документировать функции посредством комментариев — далеко не идеальный стиль Python, однако в данном случае удаление даже этой информации является минусом: # ИМЯ ФАЙЛА: main.py import word_processing input_file_path = "path_to_input_file" output_file_path = "path_to_output_file" # Обработать файл word_processing.process_file(input_file_path, output_file_path) # Подсчитать количество вхождений слов with open(output_file_path, 'r') as output_file: words = output_file.read().split() word_counts = word_processing.count_word_occurrences(words) # Вывести наиболее и наименее распространенные слова word_processing.print_common_words(word_counts, 10) word_processing.print_least_common_words(word_counts, 10) 10 most common words: the: 14 and: 9 i: 9 of: 8 is: 7 a: 6 it: 6 me: 5 to: 5 in: 4 10 least common words: hours: 1
   248 Глава 10. Модули и правила области видимости previous: 1 were: 1 out: 1 sight: 1 land: 1 look: 1 at: 1 crowds: 1 watergazers: 1 Файл main.py должен был лишь загрузить и задействовать модуль, что он и сделал. Подправить потребовалось лишь один аспект — ИИ использовал path_to_input_file и path_to_output_file вместо конкретных имен файлов данных. Чат-бот вывел под сгенерированным кодом напоминание: «Пожалуйста, замените path_to_your_input_file и path_to_your_output_file на реальные пути к вашим входным и выходным файлам», и как только имена файлов были изменены на moby_01.txt и moby_01_clean.txt, код заработал корректно. Данное упражнение является большим шагом вперед в плане абстракции — мы перешли от создания кода к организации этого кода в функции, а затем к организации этих функций в модуль и его корректному вызову. На каждом этапе пути мы убеждались в том, что варианты, предложенные средствами ИИ, впечатляют своей способностью генерировать возможные решения, хотя порой они допускают мелкие недочеты и элементарные ошибки. Итоги Модули Python позволяют поместить взаимосвязанный код и объекты в отдельный файл. Использование модулей также способствует предотвращению конфликтов имен переменных, поскольку импортируемые объекты, как правило, используются в связке с именем содержащего их модуля. Оператор import можно применять для импорта всего модуля или для доступа к конкретным элементам модуля. Элементы, имена которых начинаются с символа подчеркивания, не импортируются с помощью конструкции from module import *. Пространство имен — это словарь, связывающий объекты с их именами для определенной области видимости. Объекты в глобальной области видимости доступны во всем модуле. Объекты в локальной области видимости доступны лишь в непосредственно окружающем их пространстве имен. Объекты во встроенном пространстве имен доступны всегда.
11 Программы на Python В этой главе: 3 Создание простейшей программы 3 Создание программы, напрямую выполняемой в Linux/UNIX 3 Написание программ на macOS 3 Выбор параметров выполнения в Windows 3 Объединение программ и модулей 3 Распространение приложений на Python До настоящего момента вы работали с интерпретатором языка Python преимущественно в интерактивном режиме в Colaboratory. Однако для применения кода в реальных проектах вам, возможно, придется создавать полноценные программы или скрипты на Python. Некоторые разделы этой главы посвящены программам командной строки. Если у вас имеется опыт работы с Linux/UNIX, вероятно, вам знакомы скрипты, которые можно запускать из командной строки, задавая им аргументы и параметры, которые могут использоваться для передачи информации, а также, возможно, для перенаправления ввода и вывода. Если же вам привычнее Windows или Mac, эта концепция может показаться новой, а ее ценность — неочевидной. Безусловно, можно работать с Python из командной строки в любой из этих сред, однако с устройствами на базе Android или iOS это немного сложнее. Поскольку в качестве эталонной рабочей среды мы использовали Colaboratory, мы обсудим, как запускать скрипты командной строки из блокнота.
   250 Глава 11. Программы на Python Конечно, скрипты командной строки иногда менее удобны для использования в графической среде. Однако на Mac поддерживается оболочка командной строки UNIX, а Windows предоставляет расширенные возможности командной строки. В целом вам будет полезно прочитать большую часть этой главы. Возможно, эти приемы пригодятся вам самим или же вам потребуется разобраться в программе, где они реализованы. В частности, методы командной строки весьма и весьма полезны при обработке большого количества файлов. 11.1. Создание простейшей программы Любая группа операторов Python, последовательно размещенных в файле, может использоваться в качестве программы или скрипта. Тем не менее стандартный и практичный подход основан на введении дополнительной структуры. В простейшей форме эта задача сводится к созданию управляющей функции в файле и вызову этой функции (листинг 11.1). Листинг 11.1. Файл script1.py def main(): Управляющая функция main print("this is our first test script file") main() Вызов main В этом скрипте main является управляющей (и единственной) функцией. Сначала она определяется, а затем вызывается. Хотя в маленькой программе это ни на что не влияет, такая структура предоставляет больше возможностей и средств контроля при создании крупных приложений, поэтому рекомендуется с самого начала выработать привычку ею пользоваться. 11.1.1. Запуск скрипта из командной строки Если мы введем содержимое script1.py в ячейку в Colaboratory, все будет отлично работать. Кроме того, можно воспользоваться специальным синтаксисом в ячейке кода для запуска команд в хост-среде Colaboratory, которая представляет собой версию Ubuntu Linux. Запустить команду из «командной строки» в Colaboratory можно при помощи тех же команд, что и в Linux, но с символом ! в качестве префикса. Единственный подвох заключается в том, что файл должен быть сохранен в данном сеансе Colaboratory. В блокнотах для этой книги есть ячейки, содержащие код, который либо извлекает файлы скриптов из репозитория GitHub, либо записывает код в файл. Чтобы убедиться в том, что данный код ведет себя ожидаемым образом, необходимо либо выполнить эти ячейки, либо загрузить файлы вручную. В Colaboratory запуск скрипта при помощи префикса ! выглядит так: ! python script1.py this is our first test script file
   11.1. Создание простейшей программы 251 Если вы используете Linux/UNIX, убедитесь в том, что каталог Python включен в путь поиска, а текущим является каталог с вашим скриптом. Затем можно применить следующую команду: python script1.py Если вы используете Mac с macOS, процедура будет такой же, как и в других системах UNIX. Запустите программу терминала (она находится в подпапке Utilities папки Applications). Также в macOS предусмотрено несколько других вариантов запуска скриптов, которые будут описаны ниже. Если вы работаете в Windows, откройте терминал Windows Terminal (в Windows 11 его можно найти, щелкнув правой кнопкой мыши по кнопке меню «Пуск»), командную строку (в зависимости от версии Windows она может находиться в разных местах меню; в Windows 10 она расположена в меню Windows System) или PowerShell. Любая из этих программ откроется в вашей домашней папке, а при необходимости можно воспользоваться командой cd для перехода в подкаталог. Если script1.py был сохранен на рабочем столе, процесс запуска будет выглядеть примерно так: C:\Users\naomi> cd Desktop Переход в папку «Рабочий стол» (Desktop) Запуск script1.py C:\Users\naomi\Desktop> python script1.py this is our first test script file Вывод script1.py C:\Users\naomi\Desktop> Далее в этой главе мы рассмотрим другие способы вызова скриптов, но для наших примеров в Colaboratory будем придерживаться варианта с префик­ сом !. 11.1.2. Аргументы командной строки При работе со скриптами командной строки часто необходимо получить параметры или аргументы из командной строки, в которой запущен скрипт. Существует простой механизм передачи аргументов командной строки. Так, все аргументы командной строки находятся в списке, обратиться к которому можно через sys. argv. Имейте в виду: для доступа к этому списку нужно сначала импортировать модуль sys (листинг 11.2). Листинг 11.2. Файл script2.py import sys Импортирует модуль sys def main(): print("this is our second test script file") Обращается к списку в sys.argv print(sys.argv) main()
   252 Глава 11. Программы на Python При вызове этого скрипта командой (пропустите !, если вы работаете не в блокноте) ! python script2.py arg1 arg2 3 вы получите следующий результат: this is our second test script file ['script2.py', 'arg1', 'arg2', '3'] Как видите, аргументы командной строки были сохранены в sys.argv в виде списка строк. Заметьте, что первым элементом в этом списке является сама команда. Следует также отметить, что любые числа, введенные в командной строке, будут поступать в виде строковых представлений этих чисел и, возможно, потребуется преобразовать их в нужный тип перед использованием. 11.1.3. Выполнение кода только как основного скрипта Самая распространенная структура скрипта Python содержит еще один элемент — оператор if, который окружает основной код, выполняющийся при запуске файла в качестве основного скрипта (main). Это нужно для того, чтобы предотвратить выполнение этого кода при импорте файла как модуля, что будет показано далее в этой главе. Такая мера предосторожности необходима в случае, когда мы хотим использовать скрипт как в качестве автономного сценария, так и в качестве модуля. Добавление этой функции в скрипт сводится к размещению следующего условного теста вокруг кода, который требуется выполнить, когда файл запускается в качестве скрипта: if __name__ == '__main__': main() else: Блок else является необязательным # код инициализации для конкретного модуля (если таковой имеется) Если файл с такой структурой вызывается как скрипт, переменной __name__ присваивается значение __main__, и следовательно, будет вызвана управляющая функция main. Если же данный файл был импортирован в модуль каким-либо другим скриптом, то его имя будет совпадать с именем файла и код не будет выполнен. С другой стороны, если мы включим совершенно необязательный и редко используемый блок else, то он будет выполнен лишь в том случае, если файл был импортирован как модуль. При создании скрипта я стараюсь с самого начала использовать эту структуру. Это позволяет импортировать файл в сессию и интерактивно тестировать и отлаживать функции по мере их создания или запускать модуль как скрипт. Пускай это и не добавляет достаточно функциональности небольшому скрипту, но по мере роста его объема (что часто случается) это может обеспечить большую гибкость в его использовании.
   11.1. Создание простейшей программы 253 11.1.4. Перенаправление ввода и вывода скрипта Перенаправление содержимого файла или вывода другого процесса в скрипт вместо использования ввода с клавиатуры или перенаправление вывода скрипта на вход еще одного процесса часто используется, в частности, в системах Unix/ Linux. Такое перенаправление ввода и вывода позволяет объединять небольшие специализированные программы для выполнения бесконечного множества более сложных задач. Чтобы перенаправить файл во ввод, вместо того чтобы заставлять пользователя вводить данные, необходимо указать, что вы хотите считывать данные из стандартного ввода, который в Python доступен при помощи объекта stdin модуля sys. Также можно указать запись в стандартный вывод посредством sys. stdout, хотя на самом деле функция print также записывает данные в stdout по умолчанию. Чтобы перенаправить данные из файла во ввод, в командной строке используется символ <, а для отправки вывода в файл — символ >. Сказанное выше проще пояснить на примере короткого скрипта, приведенного в листинге 11.3. Листинг 11.3. Файл replace.py import sys Читает данные из sys.stdin (стандартный def main(): ввод) и сохраняет в contents contents = sys.stdin.read() sys.stdout.write(contents.replace(sys.argv[1], sys.argv[2])) if __name__ == "__main__": Заменяет первый аргумент main() на второй; записывает результат в sys.stdout (стандартный вывод) Этот скрипт получает входные данные, считывая стандартный поток ввода (sys. stdin) как файл. В отличие от функции input, он будет продолжать считывание данных (или ожидать чтения) до тех пор, пока не встретит индикатор конца файла. Если запустить этот скрипт без перенаправления стандартного ввода, он продолжит считывать данные из терминала и ждать, пока пользователь не введет сочетание клавиш Ctrl+D, обозначающее конец «файла». По завершении чтения он записывает в стандартный вывод все прочитанное, при этом заменяя все вхождения первого аргумента вторым аргументом. Вызванный следующим образом, с перенаправлением как ввода, так и вывода, скрипт помещает в outfile копию infile, заменяя все вхождения строки zero на 0: python replace.py zero 0 < infile > outfile Перенаправляет ввод (<) на чтение из infile и вывод (>) на запись в outfile Обратите внимание, что такое перенаправление, как правило, имеет смысл лишь из командной строки.
   254 Глава 11. Программы на Python В целом в результате выполнения строки python script.py arg1 arg2 arg3 arg4 < infile > outfile любые операции с input или sys.stdin будут направлены из infile, а любые операции с print или sys.stdout — в outfile. Эффект аналогичен тому, как если бы sys.stdin был связан с infile в режиме 'r' (чтение), а sys.stdout — с outfile в режиме 'w' (запись). Кроме того, можно применить >> для обозначения добавления данных в файл, а не их перезаписи: python replace.py a A < infile >> outfile В этой строке вывод дописывается в outfile, а не перезаписывает его, как в предыдущем примере. Вы также можете использовать конвейер | для направления вывода одной коман­ ды в качестве входных данных для другой команды: python replace.py 0 zero < infile | python replace.py 1 one > outfile Этот код сначала запускает replace.py с параметрами 0 zero, считывая содержимое infile и заменяя все 0 на zero. Вывод этого процесса направляется на вход второго запуска replace.py, заменяющего все 1 в этом потоке на one и записывающего результаты в outfile, который в итоге будет иметь содержимое infile, со всеми вхождениями 0, замененными на zero, и всеми вхождениями 1, замененными на one. Этот прием может показаться весьма необычным для тех, кто привык мыслить категориями больших программ, особенно в Windows или iOS. Тем не менее объединение небольших специализированных программ таким способом — это мощнейший инструмент, лежащий в основе традиций кодирования Unix. 11.1.5. Модуль argparse Скрипт можно настроить так, чтобы он принимал не только аргументы, но и параметры командной строки. Модуль argparse обеспечивает поддержку парсинга разных типов аргументов и даже может генерировать справки по использованию. Для работы с модулем argparse необходимо создать экземпляр ArgumentParser, наполнить его аргументами, а затем считать как необязательные, так и позиционные аргументы. Применение модуля проиллюстрировано в листинге 11.4. Листинг 11.4. Файл opts.py from argparse import ArgumentParser Добавляет отступ и аргументы входного файла def main(): parser = ArgumentParser() parser.add_argument("indent", type=int, help="отступ для отчета") parser.add_argument("input_file", help="читать данные из этого файла") parser.add_argument("-f", "--file", dest="filename", Добавляет необяhelp="записать отчет в FILE", metavar="FILE") зательные параparser.add_argument("-x", "--xray", метры -f или --file help="указать коэффициент интенсивности xray")
from argparse import ArgumentParser Добавляет отступ и аргументы    входного файла def main(): parser = ArgumentParser() parser.add_argument("indent", type=int, help="отступ для программы отчета") 255 11.1. Создание простейшей parser.add_argument("input_file", help="читать данные из этого файла") parser.add_argument("-f", "--file", dest="filename", Добавляет необяhelp="записать отчет в FILE", metavar="FILE") зательные параparser.add_argument("-x", "--xray", метры -f или --file help="указать коэффициент интенсивности xray") перед указанием parser.add_argument("-q", "--quiet", имени файла action="store_false", dest="verbose", default=True, help="не выводить статусные сообщения в stdout") args = parser.parse_args() Добавляет аргумент -q или --quiet; по умолчанию установлен в True print("аргументы:", args) if __name__ == "__main__": main() Этот код создает экземпляр ArgumentParser, а затем добавляет два позиционных аргумента, indent и input_file, которые вводятся после парсинга всех необязательных аргументов. Позиционные аргументы — это аргументы без префикса (обычно "-"), которые являются обязательными; кроме того, в данном случае аргумент indent нужно парсить как целочисленный тип int. Следующая строка добавляет необязательный аргумент для указания имени файла при помощи ключей '-f' или '--file'. Последний ключ командной строки "quiet" предоставляет возможность отключить режим подробного вывода, который по умолчанию имеет значение True, то есть включен (для этого используется action="store_false"). То, что эти ключи начинаются с префикса "-", сообщает анализатору, что они являются необязательными. Параметр action="store_false" указывает, что если аргумент задан, то соответствующему атрибуту будет присвоено значение False. Модуль argparse возвращает объект пространства имен, содержащий аргументы в форме атрибутов. Для получения значений аргументов можно использовать запись через точку. Если аргумент для ключа не задан, его значение равно None. Таким образом, запуск предыдущего скрипта командой ! python opts.py -x100 -q -f outfile 2 arg2 Параметры указываются после имени скрипта выведет следующий результат: аргументы: Namespace(indent=2, input_file='arg2', filename='outfile', ➥xray='100', verbose=False) При обнаружении недействительного или отсутствии обязательного аргумента parse_args генерирует ошибку: ! python opts.py -x100 -r Так, при выполнении этой команды будет получен следующий вывод:
   256 Глава 11. Программы на Python usage: opts.py [-h] [-f FILE] [-x XRAY] [-q] indent input_file opts.py: error: the following arguments are required: indent, input_file 11.1.6. Применение модуля fileinput В скриптах также может пригодиться модуль fileinput. Он обеспечивает поддержку обработки строк ввода из одного или нескольких файлов. Он автоматически считывает аргументы командной строки (из sys.argv), принимая их в качестве списка входных файлов. Затем модуль позволяет перебирать строки из этих файлов в цикле. Пример простого скрипта (который отсекает строки, начинающиеся с ## ) в листинге 11.5 призван продемонстрировать базовый принцип работы модуля. Листинг 11.5. Файл script4.py import fileinput def main(): for line in fileinput.input(): if not line.startswith('##'): print(line, end="") if __name__ == "__main__": main() Теперь предположим, что у вас имеются файлы данных, приведенные в листингах 11.6 и 11.7. Листинг 11.6. Файл sole1.tst ## sole1.tst: тестовые данные для функции sole 0 0 0 0 100 0 ## Листинг 11.7. Файл sole2.tst ## sole2.tst: дополнительные тестовые данные для функции sole 12 15 0 ## 100 100 0 Допустим также, что скрипт запускается следующей командой: ! python script4.py sole1.tst sole2.tst Результатом работы скрипта станет следующий вывод, в котором строки комментариев удалены, а данные двух файлов объединены: 0 0 0 0 100 0 12 15 0 100 100 0
   11.1. Создание простейшей программы 257 При отсутствии аргументов командной строки считывается только стандартный ввод. Если одним из аргументов является дефис (-), с этого места считывается стандартный ввод. Модуль fileinput предоставляет ряд других функций. Эти функции позволяют в любой момент определить общее количество просчитанных строк (lineno), число строк, считанных из текущего файла (filelineno), имя текущего файла (filename), является ли данная строка первой строкой файла (isfirstline) и/или считывается в настоящее время стандартный ввод (isstdin). Вы можете в любой момент перейти к следующему файлу (nextfile) или закрыть весь поток (close). Короткий скрипт в листинге 11.8 (который объединяет строки из входных файлов и добавляет разделители начала файла) демонстрирует способы применения этих функций. Листинг 11.8. Файл script5.py import fileinput def main(): for line in fileinput.input(): if fileinput.isfirstline(): print("<start of file {0}>".format(fileinput.filename())) print(line, end="") if __name__ == "__main__": main() Вызов команды python script5.py file1 file2 выводит следующий результат (пунктирные линии обозначают строки в исходных файлах): <start of file file1> ....................... ....................... <start of file file2> ....................... ....................... Быстрая проверка: скрипты и аргументы Сопоставьте следующие способы взаимодействия с командной строкой и соответствующий случай применения для каждого из них. Несколько аргументов и ключей Без аргументов или только один аргумент Обработка нескольких файлов Использование скрипта в качестве фильтра sys.argv Использование модуля file_input Перенаправление стандартного ввода и вывода Использование модуля argparse
   258 Глава 11. Программы на Python Наконец, если вызвать fileinput.input с аргументом в виде одного имени файла или списка имен файлов, они будут задействованы в качестве файлов входных данных вместо аргументов из sys.argv. Функция fileinput.input также поддерживает ключ inplace, который сохраняет вывод в том файле, из которого были считаны входные данные (также предусмотрена возможность сохранения исходного файла в качестве резервной копии). Подробное описание этого ключа см. в документации. 11.2. Запуск скриптов в разных операционных системах Запуск скриптов Python из командной строки или оболочки различается в зависимости от операционной системы. В этом разделе мы кратко рассмотрим запуск скриптов в Windows, macOS и Linux. 11.2.1. Создание скрипта, исполняемого непосредственно в UNIX Если вы работаете в Colaboratory, как упоминалось ранее, или в операционной системе типа UNIX, то можете легко сделать сценарий исполняемым, выполнив всего два шага. Сначала добавьте следующую строку в начало файла: #! /usr/bin/env python3 Затем измените режим файла, чтобы сделать его исполняемым, при помощи команды chmod, выполнив следующее: ! chmod +x replace.py Если вы разместите свой скрипт где-нибудь по указанному вами пути (к примеру, в директории bin), то сможете выполнить его независимо от того, в каком каталоге вы находитесь, введя его имя и необходимые аргументы. В Colaboratory мы по умолчанию размещаем файлы в каталоге /content. Так, мы можем выполнить replace.py, набрав ! replace.py zero 0 < infile > outfile Символы < и > перенаправляют ввод и вывод в файл и из него, что отлично работает в системах Linux и Mac (и в Colaboratory), но может не действовать в Windows. Если вы пишете административные скрипты в UNIX, вам могут пригодиться некоторые библиотечные модули. К их числу относятся модуль grp для обращения к базе данных группы, pwd для получения доступа к базе данных паролей, resource для обращения к информации об использовании ресурсов, syslog для работы со средствами системного журнала syslog и stat для работы
   11.2. Запуск скриптов в разных операционных системах 259 с информацией о файле или каталоге, полученной в результате вызова os.stat. Подробные сведения об этих модулях можно найти в документации к стандартной библиотеке Python. 11.2.2. Скрипты на macOS Во многих отношениях скрипты Python на macOS ведут себя так же, как и в Linux/UNIX. Можно запускать скрипты Python из окна терминала, как и на любом компьютере с операционной системой на базе UNIX. При этом на Mac вы также можете запускать программы на Python из основного файлового менеджера Finder: либо перетащив файл скрипта в приложение Python Launcher, либо настроив Python Launcher как приложение по умолчанию для открытия вашего скрипта (или, при желании, всех файлов с расширением .py). Существует несколько вариантов работы с Python на Mac. Описание всех возможностей выходит за рамки нашей книги; для получения исчерпывающей информации посетите сайт www.python.org и ознакомьтесь с подразделом Using Python on a Mac (Использование Python на Mac) раздела Python Setup and Usage (Установка и Использование Python) документации для вашей версии Python (https://docs.python.org/3.10/using/mac.html). Если вас интересует написание административных скриптов для macOS, обратите внимание на пакеты, обеспечивающие совместимость архитектуры Apple OSA (Open Scripting Architecture) и Python. 11.2.3. Параметры выполнения скриптов в Windows В Windows имеется несколько вариантов запуска скрипта, различающихся по возможностям и простоте использования. К сожалению, конкретный состав этих параметров и их настройка существенно зависят от версий Windows. Один из возможных вариантов — установка подсистемы Linux для Windows, обеспечивающей среду Linux, которую можно запускать в Windows как приложение. Эта среда на базе Ubuntu Linux предоставляет все возможности систем Linux, рассмотренные ранее. Для получения сведений о других вариантах запуска Python в вашей системе обратитесь к онлайн-документации по Python для вашей версии языка в разделе Using Python on Windows. Запуск скрипта из окна командной строки или PowerShell Для запуска скрипта откройте окно командной строки или окно PowerShell. Ко­ гда вы окажетесь в командной строке и перейдете в папку, где находятся ваши скрипты, вы сможете использовать Python для их запуска практически так же, как и в системах UNIX/Linux/macOS: > python replace.py zero 0 < infile > outfile
   260 Глава 11. Программы на Python Python не запускается? Если Python не запускается при вводе python3 в командной строке Windows, скорее всего, это связано с тем, что местоположение исполняемого файла Python не указано в вашем системном пути. Вам нужно либо вручную добавить исполняемый файл Python в системную переменную среды PATH, либо перезапустить программу установки, чтобы она сделала это за вас. За дополнительной информацией об установке Python в Windows обращайтесь к разделу Python Setup and Usage (Установка и Использование Python) онлайн-документации Python. В нем вы найдете подраздел об использовании Python в Windows, содержащий инструкции по установке Python. В целом установка Python из Microsoft Store позволит вам без проблем начать работу. Это самый гибкий способ запуска скрипта в Windows, поскольку он позволяет применять перенаправление ввода и вывода. В Windows можно отредактировать переменные среды (см. предыдущий раздел), добавив .py в качестве специального расширения, чтобы сделать ваши скрипты автоматически исполняемыми: PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.PY Попробуйте сами: сделайте скрипт исполняемым Поэкспериментируйте с запуском скриптов на вашей платформе. Кроме того, попробуйте перенаправить ввод и вывод для ваших скриптов. 11.3. Программы и модули Для небольших скриптов, содержащих лишь несколько строк кода, достаточно и одной функции. Но если скрипт вырастает за пределы этого размера, рекомендуется отделить управляющую функцию от остального кода. Оставшаяся часть этого раздела демонстрирует этот прием и некоторые его преимущества. Начнем с примера, в котором используется простая управляющая функция. Скрипт в листинге 11.9 возвращает англоязычное название для заданного числа в диапазоне от 0 до 99. Листинг 11.9. Файл script6.py #! /usr/bin/env python3 import sys # сопоставления для преобразований _1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine'} _10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve', '3': 'thirteen', '4': 'fourteen', '5': 'fifteen', '6': 'sixteen', '7': 'seventeen', '8': 'eighteen', '9': 'nineteen'} _20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty', '6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'}
   11.3. Программы и модули 261 def num2words(num_string): if num_string == '0': return'zero' if len(num_string) > 2: return "Sorry can only handle 1 or 2 digit numbers" num_string = '0' + num_string Заполняет нулем слева, если tens, ones = num_string[-2], num_string[-1] число состоит из одной цифры if tens == '0': return _1to9dict[ones] elif tens == '1': return _10to19dict[ones] else: return _20to90dict[tens] + ' ' + _1to9dict[ones] def main(): print(num2words(sys.argv[1])) Вызывает num2words if __name__ == "__main__": с первым аргументом main() Если запустить программу командой python script6.py 59 будет выведен следующий результат: fifty nine Управляющая функция вызывает функцию num2words с соответствующим аргументом и выводит результат. Вызов принято размещать в нижней части, но иногда определение управляющей функции встречается в начале файла. Я предпочитаю размещать эту функцию внизу, непосредственно над вызовом, чтобы мне не приходилось прокручивать код вверх и искать ее, выяснив ее имя в нижней части листинга. Кроме того, такая практика позволяет четко отделять код скрипта от остальной части файла, что может быть полезно при объединении скриптов и модулей. Программисты комбинируют скрипты с модулями, когда они хотят предоставить доступ к функциям, созданным ими в скрипте, другим модулям или скриптам. Кроме того, модуль может быть специально написан так, чтобы он мог запускаться как скрипт — либо для того, чтобы обеспечить простой пользовательский интерфейс, либо для предоставления механизмов его автоматического тестирования. Как отмечалось ранее, объединить скрипт и модуль достаточно просто. Нужно лишь обернуть вызов управляющей функции следующим условным тестом: if __name__ == '__main__': main() else: # код инициализации для конкретного модуля (если таковой имеется) Повторюсь: при создании скрипта я рекомендую использовать эту структуру с самого начала. Такой прием позволяет импортировать скрипт в сессию и в интерактивном режиме тестировать и отлаживать функции по мере их создания.
   262 Глава 11. Программы на Python Внешняя отладка потребуется только для управляющей функции. Если скрипт постепенно разрастается и в нем появляются новые функции, которые можно применять и в других программах, я могу легко использовать эти функции, импортируя скрипт как модуль, с возможностью дальнейшего выделения функций в отдельный модуль. Скрипт в листинге 11.10 является продолжением предыдущего скрипта, однако он был модифицирован для безопасного использования в качестве модуля. Функциональность также была расширена, благодаря чему теперь можно вводить числа в диапазоне от 0 до 999999999999999, а не только от 0 до 99. Управляющая функция (main) проверяет корректность своего аргумента, а также удаляет из него все разделители разрядов (например, запятые, используемые в англоязычных странах), чтобы при вводе можно было использовать более удобную форму записи числа, например 1234567. Листинг 11.10. Файл n2w.py Справка по использованию; включает пример #! /usr/bin/env python3 """n2w: модуль для преобразования чисел в имена числительные; содержит функцию num2words. Также может быть запущен как скрипт. Использование в качестве скрипта: n2w num (Преобразует число в английское числительное) num: целое число в диапазоне от 0 до 999,999,999,999,999 Сопоставления для преобразований, (запятые необязательны). Пример: n2w 10,003,103 только для использования внутри модуля Число 10,003,103 пишется: десять миллионов три тысячи сто три """ import sys, string, argparse _1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine'} _10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve', '3': 'thirteen', '4': 'fourteen', '5': 'fifteen', '6': 'sixteen', '7': 'seventeen', '8': 'eighteen', '9': 'nineteen'} _20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty', '6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'} _magnitude_list = [(0, ''), (3, ' thousand '), (6, ' million '), (9, ' billion '), (12, ' trillion '),(15, '')] def num2words(num_string): """num2words(num_string): преобразует числа в английские числительные""" if num_string == '0': Удаляет запятые return 'zero' из числа num_string = num_string.replace(",", "") num_length = len(num_string) Обрабатывает особые max_digits = _magnitude_list[-1][0] условия (число равно 0 if num_length > max_digits: или слишком велико) return "Sorry, can't handle numbers with more than "{0} digits".format(max_digits) num_string = '00' + num_string word_string = '' for mag, name in _magnitude_list: if mag >= num_length: return word_string " \ Заполняет число нулями слева Инициализирует строку для числительного
Удаляет запятые из числа Обрабатывает особые условия (число равно 0 или слишком велико) 11.3. Программы и модули    return 'zero' num_string = num_string.replace(",", "") num_length = len(num_string) max_digits = _magnitude_list[-1][0] if num_length > max_digits: return "Sorry, can't handle numbers with more than "{0} digits".format(max_digits) " \ 263 Заполняет число нулями слева num_string = '00' + num_string Инициализирует строку word_string = '' для числительного for mag, name in _magnitude_list: if mag >= num_length: return word_string else: hundreds, tens, ones = num_string[-mag-3], \ num_string[-mag-2], num_string[-mag-1] if not (hundreds == tens == ones == '0'): word_string = _handle1to999(hundreds, tens, ones) + \ name + word_string def _handle1to999(hundreds, tens, ones): Создает строку, if hundreds == '0': содержащую числительное return _handle1to99(tens, ones) Только для использования внутри модуля else: return _1to9dict[hundreds] + ' hundred ' + _handle1to99(tens, ones) def _handle1to99(tens, ones): if tens == '0': return _1to9dict[ones] elif tens == '1': return _10to19dict[ones] else: return _20to90dict[tens] + ' ' + _1to9dict[ones] def test(): Функция для режима values = sys.stdin.read().split() тестирования модуля for val in values: print("{0} = {1}".format(val, num2words(val))) Собирает все значения def main(): для этого аргумента parser = argparse.ArgumentParser(usage=__doc__) в список parser.add_argument("num", nargs='*') parser.add_argument("-t", "--test", dest="test", action='store_true', default=False, help="Тестовый режим: считывает из stdin") args = parser.parse_args() if args.test: Запускается в тестовом режиме, если задана test() переменная test else: Обнаруживает ошибки KeyErrors try: при наличии в аргументе result = num2words(args.num[0]) нецифровых символов except KeyError: parser.error('argument contains non-digits') else: print("For {0}, say: {1}".format(args.num[0], result)) if __name__ == '__main__': main() Вызывает функцию main при else: запуске в качестве скрипта print("n2w loaded as a module") Если этот файл запускается как скрипт, встроенной переменной __name__ будет присвоено значение __main__. Если же он импортируется как модуль, ее значение будет n2w. Помните, что переменные и функции с именами, начинающимися с символа подчеркивания ("_"), предназначены только для внутреннего использования самим модулем и не должны задействоваться клиентским кодом.
   264 Глава 11. Программы на Python Функция main иллюстрирует назначение управляющей функции для скрипта командной строки, которая, по сути, создает простой пользовательский интерфейс. Она может выполнять следующие задачи. Проверить правильность количества аргументов командной строки и корректность их типов; в случае несоответствия уведомить об этом пользователя и вывести информацию по использованию. В данном случае функция проверяет наличие одного аргумента, но явным образом не выясняет, состоит ли этот аргумент только из цифр. Возможно, работать в специальном режиме. В данном случае аргумент "-test" переводит вас в тестовый режим. Сопоставлять аргументы командной строки с теми, что требуются функциям, и вызывать их соответствующим образом. В данном случае запятые удаляются и вызывается единственная функция num2words. Возможно, перехватывать и выводить более понятное пользователю сообщение об ожидаемых исключениях. В данном случае перехватываются ошибки KeyError, которые возникают при обнаружении в аргументах нецифровых символов. (Лучшим решением было бы явно проверить наличие нецифровых символов в аргументе с помощью модуля регулярных выражений, о котором будет рассказано далее. Такое решение гарантирует, что приложение не скроет ошибки KeyError, возникающие по другим причинам.) При необходимости преобразовать вывод в более удобную для пользователя форму, что в данном примере происходит в операторе print. Если бы это был скрипт для запуска в Windows, вероятно, лучше было бы разрешить пользователю открывать его двойным щелчком — то есть использовать input для запроса параметра вместо того, чтобы передавать его в командной строке, и оставить вывод на экране, завершая скрипт командой: input("Press the Enter key to exit") Хотя, возможно, вы все равно захотите сохранить тестовый режим в качестве опции командной строки. Тестовый режим в листинге 11.11 предоставляет возможность регрессионного тестирования для модуля и его функции num2words. В данном случае для работы с ним вам потребуется разместить набор чисел в файле. Листинг 11.11. Файл n2w.tst 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 98 99 100 101 102 900 901 999 999,999,999,999,999 1,000,000,000,000,000 Затем введите ! python n2w.py --test < n2w.tst
   11.3. Программы и модули 265 0 = zero 1 = one 2 = two 3 = three 4 = four 5 = five 6 = six 7 = seven 8 = eight 9 = nine 10 = ten 11 = eleven 12 = twelve 13 = thirteen 14 = fourteen 15 = fifteen 16 = sixteen 17 = seventeen 18 = eighteen 19 = nineteen 20 = twenty 21 = twenty one 98 = ninety eight 99 = ninety nine 100 = one hundred 101 = one hundred one 102 = one hundred two 900 = nine hundred 901 = nine hundred one 999 = nine hundred ninety nine 999,999,999,999,999 = nine hundred ninety nine trillion nine hundred ninety nine billion nine hundred ninety nine million nine hundred ninety nine thousand nine hundred ninety nine 1,000,000,000,000,000 = Sorry, can't handle numbers with more than 15 digits Можно сохранить вывод в файл, воспользовавшись перенаправлением — к примеру, > test_output.txt. После этого вывод легко проверить на корректность. Этот пример выполнялся много раз в процессе создания и может быть перезапущен после любого изменения функции num2words или любой из вызываемых им функций. Разумеется, я отлично понимаю, что о полном исчерпывающем тестировании речи не идет. Ведь свыше 999 триллионов допустимых вариантов входных данных для этой программы остались без проверки! Нередко поддержка тестового режима является единственной причиной, по которой в модуле реализуется функция main. Я знаю как минимум одну компанию, чья политика разработки всегда требует внедрения хотя бы одного такого теста для каждого создаваемого модуля Python. Встроенные типы объектов данных и методы Python обычно упрощают этот процесс, и разработчики, практикующие такую методику, единогласно сходятся во мнении, что затраченные усилия того стоят. Мы еще коснемся тестирования кода на Python в разделе 19.1.15 главы 19.
   266 Глава 11. Программы на Python Применительно к данному примеру было бы просто создать отдельный файл, содержащий только ту часть функции main, которая обрабатывает аргумент, и импортировать n2w в этот файл. В таком случае в функции main файла n2w.py остался бы лишь тестовый режим. Быстрая проверка: программы и модули Какую проблему призвано предотвратить использование конструкции if __name__ == "__main__": и как она с этим справляется? Можете ли вы предложить другой способ предотвращения этой проблемы? 11.4. Распространение приложений на Python Существует целый ряд способов распространения скриптов и приложений, написанных на языке Python. Естественно, можно просто поделиться исходными файлами, вероятно, упакованными в архив .zip или .tar, однако это далеко не идеальный вариант, особенно если конечный пользователь недостаточно знаком с Python. Если приложения кросс-платформенны и у их пользователя установлена соответствующая версия языка Python, то теоретически можно предоставить только байт-код в файловом формате .pyc. Но такой способ не рекомендуется. В действительности, вопрос о том, как лучше распространять Python-прило­же­ ния и управлять их зависимостями, являлся предметом многочисленных обсу­ ждений, в ходе которых предлагались (и продолжают предлагаться) различные решения. В рамках нашей книги мы не сможем даже поверхностно рассмотреть все доступные варианты и потому сосредоточимся на основных. 11.4.1. Wheel-пакеты В настоящее время стандартным способом упаковки и дистрибуции модулей и приложений Python является формат двоичных пакетов, именуемых wheelпакетами (или wheels — файлы с расширением .whl). Wheel-пакеты спроектированы с тем расчетом, чтобы сделать установку кода Python более надежной и упростить управление зависимостями. Подробное описание wheel-пакетов выходит за рамки настоящей главы, однако исчерпывающие сведения о требованиях к wheel-пакетам и описание самого процесса их создания можно найти в руководстве пользователя «Python Packaging User Guide» по адресу https:// packaging.python.org. 11.4.2. zipapp и pex Если у вас имеется приложение, состоящее из нескольких модулей, его можно распространять в виде исполняемого zip-файла. Применение этого формата основано на двух особенностях Python. Во-первых, если архив zip содержит файл с именем __main__.py, Python может использовать этот файл как точку входа в архив и выполнить файл __main__.py
   11.4. Распространение приложений на Python 267 напрямую. Кроме того, содержимое архива zip добавляется в sys.path, поэтому оно доступно для импортирования и выполнения с помощью __main__.py. Во-вторых, zip-файлы позволяют добавлять произвольный контент в начало архива. Если добавить шебанг-строку1 (начинающуюся с символов #!) с путем к интерпретатору Python (к примеру, #!/usr/bin/env python3) и предоставить файлу необходимые разрешения, он превращается в автономный исполняемый файл. На самом деле, создать исполняемое zip-приложение (zipapp) вручную не так уж сложно. Создайте архив zip, содержащий файл __main__.py, добавьте шебангстроку в его начало и настройте права доступа. Начиная с версии Python 3.5, модуль zipapp входит в стандартную библиотеку; он позволяет создавать zip-приложения как из командной строки, так и с использованием API библиотеки. Более мощный инструмент — pex — не входит в стандартную библиотеку, но доступен в репозитории Python Package Index (PyPI) через установщик пакетов pip (Package Installer for Python). Pex выполняет ту же основную задачу, но предоставляет куда больше опций и возможностей, и при необходимости он доступен для Python 2.7. В любом случае zip-файлы приложений удобны для упаковки и распространения многофайловых приложений Python, готовых к работе. 11.4.3. py2exe и py2app Хотя целью этой книги не является подробное описание инструментов, специ­ фичных для конкретных платформ, все же стоит упомянуть, что инструмент py2exe создает автономные программы для Windows, а инструмент py2app делает то же самое на платформе macOS. Под автономностью подразумевается то, что программа представляет собой отдельный исполняемый файл, работающий на устройствах, где Python не установлен. Автономные исполняемые файлы неидеальны во многих отношениях, поскольку они, как правило, имеют больший размер и уступают по гибкости нативным приложениям Python. Тем не менее в некоторых ситуациях они оказываются лучшим, а иногда и единственным решением. 11.4.4. Создание исполняемых программ с помощью freeze Создать исполняемую программу на языке Python без его установки на устройство можно также с помощью инструмента freeze. Соответствующие инструкции приведены в файле Readme в каталоге freeze из подкаталога Tools, вложенного в каталог исходного кода Python. Если вы собираетесь применять freeze, вам, вероятно, придется скачать для этого дистрибутив исходного кода Python. 1 Шебанг-строка (англ. shebang line) — это строка комментария, которая указывает операционной системе, какой интерпретатор нужно использовать для выполнения скрипта. — Примеч. ред.
   268 Глава 11. Программы на Python В процессе «заморозки» программы на Python (то есть преобразования скриптов в автономные исполняемые файлы) вы создаете файлы на языке C, которые затем компилируются и связываются при помощи компилятора C. Последний должен быть установлен в вашей системе. Созданное таким образом замороженное приложение будет работать только на той платформе, для которой используемый вами компилятор C предоставляет исполняемые файлы. Существует несколько других инструментов, которые пытаются тем или иным способом конвертировать и упаковать приложение вместе с интерпретатором/ средой Python в автономное приложение. Однако в целом этот путь остается сложным и запутанным, и, возможно, лучше его избегать, если только у вас нет веских причин, избытка времени и ресурсов для того, чтобы заставить эту схему работать. 11.5. Практическая работа: создание программы В главе 8 вы создали версию утилиты UNIX wc для подсчета строк, слов и символов в файле. Теперь, когда в вашем распоряжении появилось больше инструментов, проведите рефакторинг этой программы для того, чтобы она стала ближе к оригиналу. В частности, ваша программа должна иметь параметры для вывода только количества строк (-l), только слов (-w) и только символов (-c). Если ни один из этих параметров не задан, выводятся все три характеристики. Но если присутствует хотя бы один из параметров, выводится только указанная статистика. Чтобы несколько усложнить задачу, добавьте параметр -L для вывода наибольшей длины строки. Можете протестировать вашу версию с помощью системной команды wc в Colaboratory или в системе под управлением Linux/UNIX. Для получения справки по wc и ее параметрам введите ! wc --help в Colaboratory. 11.5.1. Решение задачи при помощи кода, сгенерированного ИИ В данной практической работе мы осуществляем рефакторинг нашей предыдущей программы, поэтому нам нужно убедиться в том, что сессия чат-бота имеет доступ к ее коду. Для Colaboratory мы можем скопировать текст программы из практической работы главы 8 в ячейку нашего текущего блокнота. Решения, сгенерированные тогда ИИ-инструментами Colaboratory и Copilot, были очень схожи, однако в программном коде Colaboratory была выявлена небольшая ошибка, поэтому в этой практической работе в качестве отправной точки для обоих вариантов мы воспользуемся версией Copilot. Помимо предоставления кода, нам необходимо указать чат-боту, что требуется скопировать функции параметров -l, -w, -c и -L утилиты Unix wc. Можно предположить, что чат-бот ИИ имеет доступ к точному описанию параметров wc. Colaboratory был предложен следующий промпт:
   11.5. Практическая работа: создание программы 269 Необходимо сделать рефакторинг программного кода, приведенного в предыдущей ячейке, в программу командной строки, которая работает подобно утилите wc в Unix и поддерживает параметры -l, -w, -c и -L. Вывод программы должен быть аналогичен выводу утилиты wc. Промпт для Copilot вводится после выбора той же программы, а после генерирования программного кода он сохраняется в новом файле: Необходимо сделать рефакторинг выделенного программного кода в программу командной строки, которая работает подобно утилите wc в Unix и поддерживает параметры -l, -w, -c и -L. Вывод программы должен быть аналогичен выводу утилиты wc. Поскольку мы хотим, чтобы ИИ-бот создал новый файл, этот промпт следует ввести в полноэкранном интерфейсе чата. 11.5.2. Решения и обсуждение Фактический код для подсчета строк, слов и символов такой же, как и в главе 8, но обработка параметров может оказаться немного сложнее. В частности, применительно к утилите wc, если не указан ни один из параметров -l, -w или -c, это означает, что нужно выбрать все параметры, в то время как параметр -L должен задаваться явно, независимо от наличия или отсутствия других параметров. Чтобы добиться того же результата, придется немного поэкспериментировать. Еще одна часть программы, которую необходимо написать, — это способ нахо­ ждения наибольшей длины строки. Проще всего это сделать, проверяя и обновляя переменную по мере чтения каждой строки. Программное решение, созданное человеком Наибольший объем кода в решении, предложенном программистом, приходится на настройку объекта парсера (анализатора аргументов) argparse и последующую обработку параметров, в то время как сама обработка файла занимает довольно мало времени. Для того чтобы поведение нашей программы походило на работу утилиты wc, в приведенном ниже коде применяются некоторые характерные конструкции Python, на которые стоит обратить внимание: #!/usr/bin/env python3 # Файл: word_count_program.py """ Считывает файл и возвращает количество строк, слов и символов – по аналогии с утилитой UNIX wc """ import sys import argparse def main(): # инициализировать счетчики line_count = 0 word_count = 0 char_count = 0
   270 Глава 11. Программы на Python longest_line = 0 parser = argparse.ArgumentParser(usage=__doc__) parser.add_argument("-c", "--characters", action="store_true", dest="chars", default=False, help="отобразить количество символов") parser.add_argument("-w", "--words", action="store_true", dest="words", default=False, help="отобразить количество слов") parser.add_argument("-l", "--lines", action="store_true", dest="lines", default=False, help="отобразить количество строк") parser.add_argument("-L", "--longest", action="store_true", dest="longest", default=False, help="отобразить наибольшую длину строки") parser.add_argument("filename", help="читает данные из этого файла") args = parser.parse_args() filename = args.filename # открыть файл with open(filename) as infile: for line in infile: line_count += 1 char_count += len(line) words = line.split() word_count += len(words) if len(line) > longest_line: longest_line = len(line) Переменная longest_line обновляется, если текущая строка длиннее default_args = any([getattr(args, _) for _ in ('chars', 'words', 'lines', 'longest')]) Генератор списка для получения значений параметров; any() проверяет, if not default_args: установлены ли какие-либо из них args.chars = args.lines = args.words = True if args.lines: print(f"{line_count:3}", if args.words: print(f"{word_count:4}", if args.chars: print(f"{char_count:4}", if args.longest: print(f'{longest_line}', print(f'{filename}') if __name__ == '__main__': main() end=" ") end=" ") end=" ") Присваивание значений можно выполнять цепочкой Задание ширины полей в f-строках с помощью символа : и указание аргумента end=" " для вывода в одной строке из одной цифры end=" ") Первое, на что следует обратить внимание, — это настройка п а р с е р а ArgumentParser. Заметьте, что все аргументы параметров по умолчанию имеют значение False, то есть если значение параметра не указано, в парсере ничего не сохранится. Это важно, поскольку мы пытаемся решить, что выводить на экран. Если же параметр указан, то выводится его значение. Таким образом, если в командной строке указаны один, два, три или все параметры, то будут выведены соответствующие величины.
   11.5. Практическая работа: создание программы 271 Сложность заключается в том, чтобы распознать тот случай, когда никаких параметоров не задано, а значит, должен отображаться стандартный набор из количества строк, слов и символов. Данное решение справляется с этой задачей в стиле Python, однако людям (и ИИ-ботам), лишь начинающим знакомство с этим языком программирования, оно может показаться несколько замысловатым: мы задействуем генератор списка, который включает вызов функции getattr в парсере с каждым из возможных параметров: 'chars', 'words', 'lines' и 'longest'. Если в командной строке ничего не задано, это будет список значений False, а значит, и вызов функции any для этого списка также вернет False. И напротив, если хотя бы одно из этих значений равно True, то any также вернет True. Того же результата можно было бы добиться проверкой условия при помощи длинного оператора if с многократным использованием or, но такой код будет смотреться куда более громоздко. Вторая часть головоломки заключается в том, что делает код, если значение в default_args равно False. Следует отметить, что все три переменные задаются в одной строке, в цепочке из трех операторов =. Такое выражение допустимо (более того, это совершенно приемлемый стиль Python), поскольку применение оператора = дает возвращаемое значение, а именно он возвращает значение, присвоенное переменной. Таким образом, каждая операция = возвращает True, устанавливая переменную слева от выражения до тех пор, пока все переменные не будут установлены. Наконец, как и в программе из главы 8, такое решение перебирает обрабатываемый файл в цикле построчно, каждый раз обновляя значения всех характеристик. Этот алгоритм я применяю почти автоматически, поскольку он редко сказывается на скорости выполнения программы и позволяет обрабатывать файлы любого размера. В прошлом мне уже не раз приходилось возвращаться к этому подходу, так что теперь он стал для меня совершенно привычным. Программные решения, сгенерированные ИИ Это решение от Colaboratory было одним из четырех предложенных вариантов, очень похожих друг на друга. Оно выбрано потому, что его вывод наиболее близок к выводу утилиты wc. Впрочем, стоит отметить, что «наиболее близкий» не означает «правильный», в чем нам и предстоит убедиться: # prompt: Необходимо сделать рефакторинг программного кода, приведенного # в предыдущей ячейке, в программу командной строки, которая работает подобно # утилите wc в Unix и поддерживает параметры -l, -w, -c и -L. Вывод программы # должен быть аналогичен выводу утилиты wc. #!/usr/bin/env python3 """ Считывает файл и возвращает количество строк, слов и символов аналогично утилите wc в Unix. """
   272 Глава 11. Программы на Python import argparse def main(): parser = argparse.ArgumentParser(usage=__doc__) parser.add_argument("filename", nargs='*') parser.add_argument("-l", "--lines", dest="lines", action='store_true', default=False, help="Показать только количество строк") parser.add_argument("-w", "--words", dest="words", action='store_true', default=False, help="Показать только количество слов") parser.add_argument("-c", "--chars", dest="chars", action='store_true', default=False, help="Показать только количество символов") parser.add_argument("-L", "--longest", dest="longest", action='store_true', default=False, help="Показать наибольшую длину строки") args = parser.parse_args() for filename in args.filename: with open(filename) as infile: lines = infile.readlines() line_count = len(lines) word_count = sum(len(line.split()) for line in lines) char_count = sum(len(line) for line in lines) longest_line_length = max(len(line) for line in lines) if args.lines: print(f"{line_count} {filename}") elif args.words: print(f"{word_count} {filename}") elif args.chars: print(f"{char_count} {filename}") elif args.longest: print(f"{longest_line_length} {filename}") else: print(f"{line_count} {word_count} {char_count} {filename}") if __name__ == '__main__': main() Это решение корректно создает ArgumentParser и добавляет необходимые параметры. Оно также сохраняет алгоритм, принятый в исходном коде (как обсу­ ждалось в практической работе к главе 8), по которому весь файл считывается в память целиком, а затем разбивается на строки по символу новой строки \n. Затем оно может использовать длину полученного списка в качестве счетчика строк и с помощью генераторных выражений перебирать список строк, чтобы вычислить количество слов и самую длинную строку. Недостатком этого решения, как уже говорилось в главе 8, является то, что весь файл должен быть считан в память и во время работы генераторного выражения отдельный список всех слов также должен находиться в памяти. Это может
   11.5. Практическая работа: создание программы 273 создать трудности при обработке файлов очень большого размера, в то время как утилита wc и мое решение не имеют такого ограничения. Еще один недочет этого решения состоит в том, что его вывод работает не совсем так, как у утилиты wc. Решение добавляет имя файла после числового вывода, что правильно, и оно может обрабатывать любой отдельно выбранный параметр, а также вариант с отсутствием параметров. Но взгляните внимательно на настройку if-elif-else. Что произойдет, если, скажем, будут выбраны оба параметра, -l и -w? Верно: будет отображено лишь количество строк, в то время как для утилиты wc это означало бы, что будут выведены и количество строк, и количество слов. Изначально решение Copilot было менее похожим на утилиту wc, чем версия Colaboratory, но после того, как мы уточнили наш промпт, оно оказалось самым близким к оригиналу. Чтобы создать автономный файл, а не просто переписать фрагменты кода в существующем файле, нам нужно было открыть полноценный интерфейс чата в VS Code, как мы делали в прошлой главе. Перед тем как приступить к созданию кода, Copilot составил план, а затем сгенерировал код, приведенный ниже. 1. Импортировать модуль argparse для обработки аргументов командной строки. 2. Определить функцию count_stats , которая в качестве входных данных принимает путь к файлу, а возвращает количество строк, слов и символов. 3. Создать экземпляр argparse.ArgumentParser для обработки аргументов командной строки. 4. Добавить аргументы для параметров -l, -w, -c и -L. Эти аргументы должны быть необязательными и по умолчанию иметь значение False. 5. Добавить аргумент, указывающий путь к файлу. 6. Провести парсинг аргументов с помощью argparse.ArgumentParser.parse_ args(). 7. Вызвать count_stats, указав путь к файлу из проанализированных аргументов. 8. В зависимости от заданных параметров вывести количество строк, слов и символов. # wc.py import argparse def count_stats(file_path): with open(file_path) as infile: lines = infile.readlines() line_count = len(lines)
   274 Глава 11. Программы на Python word_count = sum(len(line.split()) for line in lines) char_count = sum(len(line) for line in lines) max_line_length = max(len(line) for line in lines) return line_count, word_count, char_count, max_line_length def main(): parser = argparse.ArgumentParser(description= 'Подсчитать слова, строки и символы в файле.') parser.add_argument('file', help='Файл для подсчета') parser.add_argument('-l', '--lines', action='store_true', help='Подсчитать строки') parser.add_argument('-w', '--words', action='store_true', help='Подсчитать слова') parser.add_argument('-c', '--chars', action='store_true', help='Подсчитать символы') parser.add_argument('-L', '--max-line-length', action='store_true', help='Вывести наибольшую длину строки') args = parser.parse_args() line_count, word_count, char_count, max_line_length = count_stats(args.file) if args.lines: print(f'Lines: {line_count}') if args.words: print(f'Words: {word_count}') if args.chars: print(f'Characters: {char_count}') if args.max_line_length: print(f'Max line length: {max_line_length}') if __name__ == '__main__': main() На первый взгляд план неплох, хотя создание отдельной функции для обработки не так уж необходимо. Однако в этом решении есть ряд недостатков. Во-первых, в отличие от wc, значения выводятся в отдельных строках, да еще и с метками, описывающими эти значения. Во-вторых, если в командной строке не указано ни одного параметра, то вывод не производится. Разумеется, это не является ошибочным с точки зрения обработки текста, но не соответствует поведению wc, что было одним из требований запроса. Поскольку в чате легко продолжить взаимодействие с инструментом ИИ, я попросила внести коррективу при помощи следующего промпта: Параметры и их вывод должны быть такими же, как и в утилите wc. Это было указано в исходном промпте, но, быть может, формулировка «аналогичен» допускает некую вариативность, поэтому на этот раз мы потребуем, чтобы вывод программы был «такой же». Ответ Copilot содержал показанный далее комментарий и новый план.
   11.5. Практическая работа: создание программы 275 Утилита wc в Unix по умолчанию выводит количество строк, слов и байтов, причем вывод осуществляется в одну строку. Параметр -l выводит только количество строк, -w — только количество слов, -c — количество байтов, а -L — наибольшую длину строки. Если параметры не указаны, выводятся все значения. План был таков. 1. Изменить функцию count_stats так, чтобы она возвращала не количество символов, а количество байтов. 2. Изменить функцию main так, чтобы она отображала счетчики в одной строке в следующем порядке: количество строк, количество слов, количество байтов, имя файла. 3. Если указаны параметры -l, -w, -c или -L, выводить только соответствующее количество. 4. Если параметр не указан, выводить все счетчики. На этот раз комментарии и план гораздо более точны и полностью отражают запрошенные параметры. Copilot прав в том, что технически параметр -c указывает на необходимость подсчета байтов, а не символов (в зависимости от кодировки некоторые символы могут занимать более 1 байта), но если не принимать это в расчет, то более существенное изменение заключается в обработке выводимых данных. В этом отношении вывод сгенерированной программы заметно сближается с выводом утилиты wc, и, как это нередко случается, ИИ выдает нечто похожее на решение программиста. Наибольшая часть кода осталась без изменений, но вот что было предложено для вывода результатов: if args.lines: print(line_count, end=' ') if args.words: print(word_count, end=' ') if args.bytes: print(byte_count, end=' ') if args.max_line_length: print(max_line_length, end=' ') if not any([args.lines, args.words, args.bytes, args.max_line_length]): print(f'{line_count} {word_count} {byte_count} {args.file}') else: print(args.file) Как видите, этот код проверяет все параметры парсера и выводит значение по умолчанию, если ни один из них не указан. Он также надлежащим образом обрабатывает любую комбинацию параметров и выводит только значения, не снабжая их метками. Более того, он корректно отображает имя файла для всех вариантов, хотя при первом прочтении кода его логика может показаться не вполне очевидной — при отсутствии параметров вывод имени файла для указанных параметров выполняется в операторе else.
   276 Глава 11. Программы на Python Хорошая новость заключается в том, что оба чат-бота ИИ знали, как работает утилита wc, поэтому не было необходимости вдаваться в подробности в промптах. С другой стороны, для получения желаемого результата потребовалось уделить некоторое внимание деталям и уточняющим промптам. Итоги Скрипты и модули Python в самой базовой форме представляют собой просто последовательности операторов Python, сохраненные в файле. Модули можно настроить так, чтобы они могли запускаться как скрипты, а скрипты — так, чтобы их можно было импортировать в качестве модулей. Скрипты можно сделать исполняемыми в командной строке UNIX, macOS или Windows. Их можно настроить для поддержки перенаправления ввода и вывода командной строки, а модуль argparse позволяет парсить сложные комбинации аргументов командной строки. В macOS для запуска программ Python можно использовать Python Launcher: как для отдельных файлов, так и в качестве приложения по умолчанию для открытия файлов Python. В Windows скрипты можно вызывать несколькими способами: открывая их двойным щелчком, из окна Выполнить или из окна командной строки. Сценарии Python могут распространяться в форме скриптов, байт-кода или специальных wheel-пакетов. Инструменты py2exe, py2app и freeze создают исполняемые программы Python, которые могут работать на устройствах, где не установлен интерпретатор Python.
12 Работа с файловой системой В этой главе: 3 Управление путями 3 Получение информации о файлах 3 Выполнение операций с файловой системой 3 Обработка всех файлов в поддереве каталога Работа с файлами состоит из двух аспектов: основных операций ввода-вывода (они рассматриваются в главе 13) и взаимодействия с файловой системой (к примеру, именование, создание, перемещение или обращение к файлам), что немного сложнее, поскольку в разных операционных системах установлены различные соглашения файловых систем (то есть существуют свои особенности работы с ними). Поскольку в качестве рабочей среды по умолчанию мы выбрали Google Colaboratory, работающую на Linux, большинство примеров в этой главе будут основаны на Linux. Однако вполне возможно, что вам понадобится написать скрипты, которые будут обращаться к файлам, работающим на других платформах, поэтому по мере необходимости мы будем упоминать о том, как те же операции работают в разных ОС, например в Windows. Вы можете освоить основные операции файлового ввода-вывода без изучения всех возможностей, предоставляемых Python для упрощения кросс-платформенного взаимодействия с файловыми системами, однако я так поступать не рекомендую.
   278 Глава 12. Работа с файловой системой Ознакомьтесь хотя бы с первой частью этой главы, описывающей инструменты, необходимые для обращения к файлам в формате, не зависящем от конкретной операционной системы. Позже, при использовании основных операций вводавывода, вы сможете таким образом открывать соответствующие файлы. 12.1. Модули os и os.path в сравнении с pathlib Традиционным способом обработки путей к файлам и операций с файловой системой в Python являлось использование функций, включенных в модули os и os.path. Эти функции неплохо справлялись со своей задачей, однако код на их основе нередко оказывался слишком громоздким. Начиная с версии Python 3.5, был добавлен новый модуль pathlib, предоставляющий более объектно-ориентированный и унифицированный способ выполнения тех же операций. Так как во множестве существующих программ все еще используется старый стиль, я сохранила такие примеры с описанием их работы. С другой стороны, у pathlib имеется масса преимуществ, и, с большой вероятностью, она станет новым стандартом, поэтому после каждого примера с использованием традиционных средств я привожу пример (и краткое пояснение там, где это необходимо) того, как те же действия выполняются с помощью pathlib. 12.2. Пути и путевые имена Все операционные системы обращаются к файлам и каталогам при помощи строк с именами этих файлов или каталогов. Такие строки обычно называются путевыми именами (pathnames) или чаще просто путями (paths), и я буду придерживаться второго термина. Тот факт, что пути представляют собой строки, создает немало потенциальных трудностей при работе с ними. Во избежание этих сложностей Python предоставляет разработчику отличные функции, однако для их эффективного применения необходимо уяснить первопричины возникающих проблем. Их рассмотрению и посвящен этот раздел. Семантика путей в различных операционных системах весьма схожа, поскольку почти в каждой из них файловая система строится на основе древовидной структуры: диск является корнем дерева, а каталоги, подкаталоги и т. д. — его ветвями. Следовательно, в большинстве операционных систем обращение к конкретному файлу осуществляется в соответствии с общим фундаментальным принципом: по пути, в котором указана цепочка от корня дерева файловой системы (диска) до самого файла. (Сравнение корня с жестким диском — слишком сильное упрощение, однако оно близко к истине в достаточной мере для того, чтобы быть полезным для целей этой главы.) Такой путь состоит из последовательности каталогов, которые необходимо пройти, чтобы добраться до нужного файла. В различных операционных системах действуют разные правила относительно синтаксиса путей. Например, в путях Linux/UNIX для разделения
   12.2. Пути и путевые имена 279 последовательных имен файлов или каталогов используется символ /, тогда как в Windows этой цели традиционно служит символ \. Кроме того, файловая система UNIX имеет один корень (который обозначается символом / в качестве первого символа пути), тогда как в файловой системе Windows у каждого диска имеется свой корень с меткой A:\, B:\, C:\ и т. д. (при этом C: традиционно назначается основным, системным диском). Из-за этих различий пути по-разному выглядят в разных операционных системах. Файл с именем C:\data\myfile в ОС Windows может называться /data/myfile в UNIX и Mac OS. Python располагает функциями и константами для выполнения основных операций с путями, благодаря которым можно не беспокоиться о таких синтаксических нюансах. Существуют также различия в том, как операционная система обрабатывает строки в верхнем и нижнем регистрах. В Linux и некоторых файловых системах iOS регистр имеет значение, поэтому file_a и File_A — это разные имена файлов. Windows традиционно игнорирует различия в регистре. Не забывайте об этом и следуйте единому правилу, к примеру, используйте только строчные буквы в именах файлов, чтобы избежать путаницы, если вашей программе придется взаимодействовать с файлами в разных операционных системах. Приложив немного усилий, вы сможете писать программы на Python так, что они будут работать правильно и независимо от текущей файловой системы. 12.2.1. Абсолютные и относительные пути Большинство операционных систем поддерживают два типа путей. Абсолютные пути указывают точное местонахождение файла (или каталога) в файловой системе без каких-либо неясностей, поскольку они содержат полный путь к файлу (или каталогу), начиная от корня файловой системы. Относительные пути указывают положение файла (или каталога) относительно какой-то другой точки в файловой системе, но позиция этой точки не указывается в самом относительном пути; вместо этого абсолютная начальная точка для относительных путей определяется их контекстом. В качестве примеров приведу два абсолютных пути в Windows: C:\Program Files\Doom D:\backup\June и два абсолютных пути в Linux и один в Mac: /bin/Doom /floppy/backup/June /Applications/Utilities Вот два относительных пути в Windows: mydata\project1\readme.txt games\tetris
   280 Глава 12. Работа с файловой системой и относительные пути в Linux/UNIX/Mac: mydata/project1/readme.txt games/tetris Utilities/Java Относительные пути нуждаются в контексте для их привязки. Как правило, такой контекст предоставляется одним из двух способов. Проще всего добавить относительный путь к существующему абсолютному, получая таким образом новый абсолютный путь. Допустим, в Windows у вас имеется относительный путь Start Menu\Programs\Startup и абсолютный C:\ Users\Administrator. Объединив их, вы получите новый абсолютный путь C:\ Users\Administrator\Start Menu\Programs\Startup, ссылающийся на определенное местоположение в файловой системе. Присоединив тот же относительный путь к другому абсолютному пути (например, C:\Users\myuser), вы получите путь к папке Startup в каталоге Profiles другого пользователя (myuser). Второй способ получения контекста для относительных путей основан на неявной ссылке на текущий рабочий каталог — тот каталог, который программа Python считает своим местоположением на всем протяжении своего выполнения. Команды Python могут неявно задействовать текущий рабочий каталог при получении относительного пути в аргументе. Например, при использовании команды os.listdir(path) с аргументом относительного пути его точкой привязки (якорем) будет текущий рабочий каталог, а результатом выполнения команды — список имен файлов в каталоге, путь к которому формируется добавлением текущего рабочего каталога к аргументу относительного пути. 12.2.2. Текущий рабочий каталог Когда вы редактируете документ на компьютере, у вас есть представление о том, в какой точке файловой структуры компьютера вы находитесь — в одном каталоге (папке) с файлом, с которым вы работаете. Аналогичным образом при каждом запуске интерпретатор Python знает свое текущее положение в структуре каталогов в любой момент времени. Это важно, поскольку ваш скрипт на Python (.py-файл), к примеру, может запросить список файлов, хранящихся именно в этом каталоге. Каталог, в котором находится интерпретатор Python в момент выполнения скрипта, называется текущим рабочим каталогом. Нужно понимать, что этот каталог может отличаться от того каталога, где расположен скрипт (.py-файл). Чтобы увидеть это в действии, запустите интерпретатор Python и выполните команду os.getcwd (get current working directory — получить текущий рабочий каталог) для того, чтобы увидеть, каким был текущий рабочий каталог для интерпретатора Python в момент его запуска: import os os.getcwd() '/content'
   12.2. Пути и путевые имена 281 Обратите внимание, что os.getcwd используется как вызов функции без аргументов; тем самым подчеркивается тот факт, что возвращаемое ею значение не является константой, а будет изменяться по мере ввода команд, модифицирующих текущий рабочий каталог. В Colaboratory этим каталогом, вероятно, будет '/content', или каталог, в котором находится сам исполняемый файл интерпретатора Python, или же каталог, в котором вы находились в момент запуска интерпретатора Python. На компьютерах под управлением Windows в путь вставляются дополнительные символы \, так как в этой операционной системе обратная косая черта \ служит разделителем компонентов пути, а в строках Python (что обсуждалось в разделе 6.3.1) символ \ имеет особое значение, если он не экранирован другим таким символом. Теперь введите команду: os.listdir(os.curdir) ['.config', 'sample_data'] Константа os.curdir возвращает строку, отражающую текущий каталог. В UNIX и Windows текущий каталог обозначается одинарной точкой, но для обеспечения переносимости программ всегда используйте os.curdir вместо просто точки. Эта строка является относительным путем; следовательно, функция os.listdir, когда ей передается os.curdir, фактически присоединяет этот относительный путь к пути текущего рабочего каталога, чтобы определить, содержимое какого именно каталога нужно показать. Команда os.listdir(os.curdir) возвращает список всех файлов или подкаталогов, находящихся в текущем рабочем каталоге (в Colaboratory это будет список, показанный ранее). Чтобы просмотреть содержимое каталога 'sample_data', выполните команды: os.chdir('sample_data') os.getcwd() Функция смены каталога Как видите, Python переходит в каталог, указанный в качестве аргумента функции os.chdir. Другой вызов os.listdir(os.curdir) вернет список файлов в каталоге 'sample_data', поскольку os.curdir теперь будет интерпретироваться относительно нового текущего рабочего каталога. Многие операции с файловыми системами в Python используют текущий рабочий каталог подобным образом. 12.2.3. Обращение к каталогам с помощью pathlib Чтобы получить текущий каталог с помощью pathlib, выполните следующее: import pathlib cur_path = pathlib.Path() cur_path.cwd() PosixPath('/content/sample_data')
   282 Глава 12. Работа с файловой системой Средствами модуля pathlib нельзя изменить текущий каталог так, как это делает os.chdir() (см. предыдущий раздел), поскольку путь по определению является конкретным местоположением каталога. Однако для работы с новым каталогом можно создать новый объект Path, как будет описано ниже в разделе 12.2.5. 12.2.4. Управление путями Теперь, когда у вас есть базовые знания о путях к файлам и каталогам, пришло время рассмотреть средства языка Python для управления ими. К их числу относится ряд функций и констант подмодуля os.path, предоставляющих возможность управления путями без явной привязки к синтаксису конкретной ОС. Пути по-прежнему представляются в виде строк, но вам не придется думать о них как о таковых или управлять ими вручную. Для начала создадим несколько путей для разных операционных систем при помощи функции os.path.join. Обратите внимание: при импортировании модуля os также добавляется подмодуль os.path; явный оператор import os.path не требуется. В нашем блокноте можно написать примерно следующее: import os print(os.path.join('bin', 'utils', 'disktools')) bin/utils/disktools Функция os.path.join интерпретирует свои аргументы как последовательность имен каталогов или файлов и объединяет их в единую строку, формируя относительный путь в формате, корректном для основной операционной системы. В Windows это означает, что имена компонентов пути будут объединяться символами обратной косой черты \. Иными словами, os.path.join позволяет формировать пути к файлам из последовательности имен каталогов или файлов, не беспокоясь о правилах текущей операционной системы. Функция os.path. join является основным способом создания путей без привязки к конкретной операционной системе, в которой будут выполняться ваши программы. Аргументы os.path.join не обязательно должны представлять собой имя одного каталога или файла; это также могут быть подпути, которые объединяются для построения более длинного пути. Следующий пример демонстрирует данную возможность в среде Windows, а также показывает тот случай, когда в строках необходимо использовать две обратные косые черты. Обратите внимание, что при вводе пути можно использовать и символы обратной косой черты /, поскольку Python автоматически преобразует их перед обращением к Windows: import os print(os.path.join('mydir\\bin', 'utils\\disktools\\chkdisk')) mydir\bin\utils\disktools\chkdisk
   12.2. Пути и путевые имена 283 Разумеется, если вы всегда пользуетесь os.path.join для создания путей, вам вряд ли придется беспокоиться о такой ситуации. Чтобы записать этот пример в переносимой форме, введите следующие команды: path1 = os.path.join('mydir', 'bin'); path2 = os.path.join('utils', 'disktools', 'chkdisk') print(os.path.join(path1, path2)) mydir\bin\utils\disktools\chkdisk В ОС Windows Это также будет корректно работать в системах на базе Linux/Unix: mydir/bin/utils/disktools/chkdisk В системе Linux Команда os.path.join также учитывает различие между абсолютными и относительными путями. В Linux/UNIX абсолютный путь всегда начинается с символа / (поскольку одиночный символ косой черты обозначает каталог верхнего уровня всей системы, который содержит все остальное, включая различные доступные накопители). В UNIX относительным путем считается любой корректный путь, не начинающийся с символа /. В любой из операционных систем семейства Windows ситуация усложняется, потому что в Windows правила именования относительных и абсолютных путей не столь очевидны. Не стану погружаться в технические подробности, а просто скажу, что в таких случаях проще всего воспользоваться следующими упрощенными правилами для путей в Windows. Путь, начинающийся с буквы диска, двоеточия и символа \, является абсолютным: C:\Program Files\Doom . (Обратите внимание, что сочетание символов C: без завершающего символа \ не может быть использовано для ссылки на каталог верхнего уровня на диске C: — для этой цели следует использовать запись C:\. Это требование обусловлено соглашениями DOS, а не архитектурой Python.) Путь, не начинающийся ни с буквы диска, ни с символа \, является относительным: mydirectory\letters\business. Путь, начинающийся с символов \\, за которыми следует имя сервера, является путем к сетевому ресурсу. Все остальные сочетания считаются недействительными путями. Независимо от используемой ОС, команда os.path.join не проверяет корректность составляемых ею путей. Можно создавать пути, которые включают символы, запрещенные правилами именования путей вашей ОС. Если такая проверка на корректность необходима, возможно, лучшим решением будет самостоятельно написать небольшую функцию проверки корректности пути. Команда os.path.split возвращает кортеж из двух элементов, разделяя путь на две части: голову (часть пути, ведущая к последнему компоненту) и хвост (последний компонент пути, то есть имя файла или каталога, который также называют базовым именем). В системе Windows это может выглядеть так:
   284 Глава 12. Работа с файловой системой import os print(os.path.split(os.path.join('some', 'directory', 'path'))) ('some/directory', 'path') Функция os.path.basename возвращает только базовое имя пути, а функция os.path.dirname возвращает его часть до базового имени, не включая его, как в следующем примере: import os os.path.basename(os.path.join('some', 'directory', 'path.jpg')) 'path.jpg' os.path.dirname(os.path.join('some', 'directory', 'path.jpg')) 'some/directory' Для обработки расширений файлов (которые в большинстве файловых систем указывают тип файла через точку — классическая Mac OS (HFS) составляет заметное исключение) в Python имеется функция os.path.splitext: os.path.splitext(os.path.join('some', 'directory', 'path.jpg')) ('some/directory/path', '.jpg') Последний элемент возвращаемого кортежа содержит расширение указанного файла с точкой (если таковое имеется). Первый элемент содержит все символы исходного аргумента за исключением расширения с точкой. Для управления путями можно также задействовать более специализированные функции. Так, функция os.path.commonprefix(путь1, путь2, …) находит общий префикс для набора путей (при его наличии). Это может оказаться полезным, когда вам требуется найти каталог самого нижнего уровня, содержащий все файлы из заданного набора. Функция os.path.expanduser разворачивает ярлыки имен пользователей в путях (например, ~user в UNIX). Аналогично функция os.path.expandvars делает то же самое с переменными среды. Вот пример для системы Windows: import os os.path.expandvars('$HOME\\temp') 'C:\\Users\\administrator\\personal\\temp' 12.2.5. Управление путями с помощью pathlib Как и в предыдущем разделе, начнем с создания путей для разных операционных систем с использованием методов объекта Path. Сначала запустите Python в Windows:
   12.2. Пути и путевые имена 285 from pathlib import Path cur_path = Path() print(cur_path.joinpath('bin', 'utils', 'disktools')) bin/utils/disktools Того же результата можно добиться при помощи оператора /: cur_path / 'bin' / 'utils' / 'disktools' PosixPath('bin/utils/disktools') Обратите внимание: в представлении объекта Path всегда используются символы /, но в объектах Path в Windows символы / автоматически преобразуются в \, как того требует ОС. Поэтому при попытке сделать то же самое в UNIX: cur_path = Path() print(cur_path.joinpath('bin', 'utils', 'disktools')) bin/utils/disktools свойство parts возвращает кортеж со всеми компонентами пути. К примеру: a_path = Path('bin/utils/disktools') print(a_path.parts) ('bin', 'utils', 'disktools') Свойство name возвращает только базовое имя пути, свойство parent возвращает часть пути до базового имени (не включая его), а свойство suffix — расширение файла с точкой, используемое в большинстве файловых систем для обозначения его типа (за исключением MacOS). Приведем пример: a_path = Path('some', 'directory', 'path.jpg') a_path.name 'path.jpg' print(a_path.parent) some/directory a_path.suffix '.jpg' Ряд других методов, связанных с объектами Path, позволяет гибко управлять как путями к файлам, так и самими файлами, и потому вам стоит ознакомиться с документацией к модулю pathlib. Вполне возможно, что модуль pathlib сделает вашу жизнь проще, а код для работы с файлами — компактнее.
   286 Глава 12. Работа с файловой системой 12.2.6. Полезные константы и функции В вашем распоряжении имеется несколько полезных констант и функций для работы с путями, благодаря которым ваш код Python станет более кроссплатформенным. Основными константами являются os.curdir и os.pardir. Каждая из них определяет символ, используемый операционной системой для указания пути к текущему и родительскому каталогам соответственно. В Windows, а также в Linux/UNIX и macOS это указатели . и .. соответственно; они могут выступать в качестве обычных элементов путей. Так, следующее выражение os.path.isabs(os.path.join(os.pardir, path)) выполняет проверку: является ли путь, указывающий на элемент path в родительском каталоге (то есть ../path), абсолютным. Константа os.curdir особенно полезна для запроса команд в текущем рабочем каталоге. К примеру, команда os.listdir(os.curdir) возвращает список имен файлов в текущем рабочем каталоге (поскольку os.curdir является относительным путем, а функция os.listdir всегда интер- претирует относительные пути, отталкиваясь от текущего рабочего каталога). Константа os.name возвращает имя модуля Python, который импортирован для обработки данных, связанных с операционной системой. Приведем пример в Colaboratory: import os os.name 'posix' Поскольку Colaboratory использует Linux в качестве хостовой ОС, значение os.name возвращает строку 'posix'. Обратите внимание, что os.name возвращает 'nt' даже в Windows 10 или 11. Большинство версий Windows идентифицируются как 'nt'. В macOS и Linux/UNIX также выводится posix. Можно воспользоваться этим ответом для выбора операций в зависимости от платформы, на которой работает ваш код: import os if os.name == 'posix': root_dir = "/" elif os.name == 'nt': root_dir = "C:\\" else: print("Don't understand this operating system!") На момент написания книги единственным другим возможным значением для os.name, отличным от приведенных во фрагменте кода, является 'java', которое возвращает Jython (Python, работающий на виртуальном устройстве Java).
   12.3. Получение информации о файлах 287 В программах иногда встречается атрибут sys.platform, выдающий более точную информацию. Даже в Windows 11 sys.platform присваивается значение win32, несмотря на то что устройство работает под управлением 64-разрядной версии операционной системы. В Linux вы увидите linux, а в macOS — darwin. Список возможных значений менялся по мере появления новых платформ, тем не менее их актуальный перечень всегда можно найти в документации по sys.platform. Все переменные среды и связанные с ними значения доступны в словаре с именем os.environ. В большинстве операционных систем этот словарь включает переменные, связанные с путями, — как правило, пути поиска для двоичных файлов и т. п. К настоящему моменту вы ознакомились с основными аспектами работы с путями в Python. Теперь вы знаете достаточно, чтобы открывать файлы для чтения или записи. В остальной части главы мы рассмотрим дополнительную информацию о путях, проверке того, на что они указывают, полезных константах и другие немаловажные детали. Быстрая проверка: управление путями Как бы вы задействовали функции модуля os, чтобы получить путь к файлу с именем test.log и создать новый путь в том же каталоге для файла с именем test.log.old? Как можно сделать то же самое при помощи модуля pathlib? Какой путь вы получите при создании объекта Path модуля pathlib из os.pardir? Попробуйте и выясните. 12.3. Получение информации о файлах Предполагается, что пути должны указывать местонахождение реальных файлов и каталогов на вашем жестком диске. Скорее всего, вы передаете путь, когда требуется узнать что-то о файле или каталоге, на который он указывает. Для этой цели в Python имеются различные функции. Самые востребованные функции Python для получения информации о путях — os.path.exists, os.path.isfile и os.path.isdir. Все они принимают в качестве аргумента один путь. Функция os.path.exists возвращает True, если ее аргумент является путем к существующему объекту файловой системы. Функция os.path.isfile возвращает True только в том случае, если полученный путь указывает на стандартный файл данных какого-либо типа (к этой категории относятся и исполняемые файлы). В противном случае возвращается False, включая возможность того, что аргумент path не ссылается на какой-либо объект файловой системы. Функция os.path.isdir возвращает True только в том случае, если ее аргумент path указывает на каталог; иначе возвращается False. Следующие примеры корректно выполняются в моей операционной системе. Не исключено, что в вашей системе вам придется использовать другие пути, чтобы изучить поведение этих функций:
   288 Глава 12. Работа с файловой системой import os os.path.exists('/content/sample_data/') True os.path.exists('/content/sample_data/README.md') True os.path.exists('/content/sample_data/ljsljkflkjs') False os.path.isdir('/content/sample_data/') True os.path.isdir('/content/sample_data/README.md') False os.path.isfile('/content/sample_data/README.md') True Некоторые аналогичные функции выполняют более специализированные запросы. Так, функции os.path.islink и os.path.ismount полезны в Linux и других системах UNIX, поддерживающих файловые символические ссылки и точки монтирования. Они возвращают True, если путь указывает на файл, являющийся символической ссылкой или точкой монтирования соответственно. При этом os.path.islink не возвращает True для файлов ярлыков Windows (файлы с расширением .lnk) по простой причине: эти файлы не являются настоящими ссылками. ОС не назначает таким файлам ярлыков специального статуса, и программы не могут прозрачно использовать их так, как если бы они были настоя­ щими файлами. Однако os.path.islink возвращает True в системах Windows для настоящих символических ссылок, созданных с помощью команды mklink(). Функция os.path.samefile(path1, path2) возвращает True только в том случае, когда два аргумента path указывают на один и тот же файл. Функция os.path. isabs(path) возвращает True, если ее аргумент является абсолютным путем; в противном случае возвращается False. Функции os.path.getsize(path), os.path.getmtime(path) и os.path.getatime(path) возвращают размер, время последнего изменения и время последнего обращения к пути соответственно. Для работы с объектами Path предусмотрены одноименные методы, дублирующие по функциональности рассмотренные ранее методы из модуля os. 12.3.1. Получение информации о файлах с помощью scandir Кроме перечисленных функций os.path, для получения более полной информации о файлах в каталоге также можно воспользоваться функцией os.scandir,
   12.4. Другие операции с файловой системой 289 возвращающей итератор объектов os.DirEntry. Объекты os.DirEntry содержат атрибуты файла для каждого элемента каталога, так что использование os.scandir может быть быстрее и эффективнее сочетания os.listdir (см. следующий раздел) с операциями os.path. Если, к примеру, вам нужно определить, является элемент каталога файлом или каталогом, преимуществом os.scandir будет доступ к дополнительной информации о содержимом директории, а не только к именам. У объектов os.DirEntry есть методы, соответствующие функциям os.path, упомянутым в предыдущем разделе, включая exists, is_dir, is_file, is_socket и is_symlink. Функция os.scandir также поддерживает контекстный менеджер посредством with, и его применение рекомендуется для корректного освобождения ресурсов. Следующий пример кода перебирает все элементы каталога и выводит имя каждого из них с указанием, является ли он файлом: with os.scandir("..") as my_dir: for entry in my_dir: print(entry.name, entry.is_file()) .config False sample_data False 12.4. Другие операции с файловой системой Кроме получения сведений о файлах, Python позволяет выполнять некоторые операции с файловой системой напрямую через набор базовых, но в высшей степени полезных команд модуля os. В этом разделе описаны только полноценные кросс-платформенные операции. Эти команды (и их аналоги в pathlib, которые мы обсудим позже) поддерживаются в Windows, macOS и Linux (включая Colaboratory). Во многих операционных системах доступны более сложные функции файловой системы; вы можете узнать о них подробнее, обратившись к документации основной библиотеки Python. Как вам уже известно, для получения списка файлов в каталоге служит функция os.listdir: os.chdir('/content/sample_data') os.listdir(os.curdir) ['anscombe.json', 'README.md', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] Обратите внимание, что, в отличие от команды для просмотра содержимого каталога (list-directory) во многих языках и оболочках, Python не включает индикаторы os.curdir (.) и os.pardir (..) в список, возвращаемый os.listdir.
   290 Глава 12. Работа с файловой системой Функция glob из одноименного модуля (унаследовавшего имя от классической утилиты UNIX для сопоставления шаблонов) расширяет символы подстановки и последовательности символов в пути в стиле оболочки Linux/UNIX, возвращая файлы из текущего рабочего каталога, соответствующие шаблону. Так, подстановочный знак * представляет любую последовательность символов, ? — один отдельный символ. Последовательность символов ([h,H] или [0-9]) соответствует любому отдельному символу в этой последовательности: import glob glob.glob("*") ['anscombe.json', 'README.md', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] glob.glob("*json") ['anscombe.json'] glob.glob("?.tmp") ['1.tmp', '2.tmp', 'a.tmp'] glob.glob("[0-9].tmp") ['1.tmp', '2.tmp'] Для переименования (перемещения) файла или каталога служит функция os.rename: os.rename('README.md', 'README.md.old') os.listdir(os.curdir) ['anscombe.json', 'README.md.old', '1.tmp', '2.tmp', 'a.tmp', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] Эта команда может использоваться для перемещения файлов как между каталогами, так и внутри них. Удаление файлов данных осуществляется функцией os.remove: os.remove('a.tmp') os.listdir(os.curdir)
   12.4. Другие операции с файловой системой 291 ['anscombe.json', 'README.md.old', '1.tmp', '2.tmp', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] Обратите внимание на тот факт, что функцию os.remove нельзя использовать для удаления каталогов. Это ограничение введено из соображений безопасности, чтобы предотвратить случайное удаление вложенных структур каталога. Файлы могут создаваться путем записи в них, что уже обсуждалось в главе 11. Для создания каталогов задействуйте функции os.makedirs или os.mkdir. Разница между ними заключается в том, что os.mkdir не создает промежуточных каталогов, даже если они необходимы, тогда как os.makedirs создает все нужные директории: os.makedirs('mydir') os.listdir(os.curdir) ['anscombe.json', 'README.md.old', 'mydir', '1.tmp', '2.tmp', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] os.path.isdir('mydir') True Для удаления каталога служит функция os.rmdir. Она удаляет только пустые каталоги. Попытка применить эту операцию к каталогу, содержащему файлы или подкаталоги, создаст исключение: os.rmdir('mydir') os.listdir(os.curdir) ['anscombe.json', 'README.md.old', '1.tmp', '2.tmp', 'california_housing_train.csv', 'california_housing_test.csv', 'mnist_test.csv', 'mnist_train_small.csv'] Для удаления непустых каталогов используется функция shutil.rmtree, которая рекурсивно удаляет все файлы в дереве каталога. Ее применение подробно описано в документации к стандартной библиотеке Python.
   292 Глава 12. Работа с файловой системой 12.4.1. Другие операции с файловой системой при помощи pathlib Объекты Path содержат большинство упомянутых ранее методов, но с некоторыми отличиями. Так, метод iterdir аналогичен функции os.listdir, за исключением того, что он возвращает итератор объектов Path вместо списка строк: new_path = cur_path.joinpath('/content', 'sample_data') list(new_path.iterdir()) [PosixPath('/content/sample_data/anscombe.json'), PosixPath('/content/sample_data/.ipynb_checkpoints'), PosixPath('/content/sample_data/README.md.old'), PosixPath('/content/sample_data/1.tmp'), PosixPath('/content/sample_data/2.tmp'), PosixPath('/content/sample_data/california_housing_train.csv'), PosixPath('/content/sample_data/california_housing_test.csv'), PosixPath('/content/sample_data/mnist_test.csv'), PosixPath('/content/sample_data/mnist_train_small.csv')] Обратите внимание, что в среде Windows возвращаемые пути являются объектами WindowsPath, а в macOS и Linux — объектами PosixPath. У объектов Path модуля pathlib также имеется встроенный метод glob, который тоже возвращает не список строк, а итератор объектов Path. В остальном эта функция ведет себя практически как функция glob.glob, продемонстрированная выше: list(cur_path.glob("*")) [PosixPath('anscombe.json'), PosixPath('.ipynb_checkpoints'), PosixPath('README.md.old'), PosixPath('1.tmp'), PosixPath('2.tmp'), PosixPath('california_housing_train.csv'), PosixPath('california_housing_test.csv'), PosixPath('mnist_test.csv'), PosixPath('mnist_train_small.csv')] list(cur_path.glob("*json")) [PosixPath('anscombe.json')] list(cur_path.glob("?.tmp")) [PosixPath('1.tmp'), PosixPath('2.tmp')] list(cur_path.glob("[0-9].tmp")) [PosixPath('1.tmp'), PosixPath('2.tmp')] Для переименования (перемещения) файла или каталога используется метод rename объекта Path:
   12.4. Другие операции с файловой системой 293 old_path = Path('README.md.old') new_path = Path('README.md') old_path.rename(new_path) list(cur_path.iterdir()) [PosixPath('anscombe.json'), PosixPath('README.md'), PosixPath('.ipynb_checkpoints'), PosixPath('1.tmp'), PosixPath('2.tmp'), PosixPath('california_housing_train.csv'), PosixPath('california_housing_test.csv'), PosixPath('mnist_test.csv'), PosixPath('mnist_train_small.csv')] Команда может использоваться для перемещения файлов как между каталогами, так и внутри каталогов. Файлы данных удаляются методом unlink: new_path = Path('1.tmp') new_path.unlink() list(cur_path.iterdir()) [PosixPath('anscombe.json'), PosixPath('README.md'), PosixPath('.ipynb_checkpoints'), PosixPath('2.tmp'), PosixPath('california_housing_train.csv'), PosixPath('california_housing_test.csv'), PosixPath('mnist_test.csv'), PosixPath('mnist_train_small.csv')] Обратите внимание: как и в случае с os.remove, метод unlink не может использоваться для удаления каталогов. Это ограничение введено из соображений безопасности, чтобы предотвратить случайное удаление вложенных структур каталога. Для создания каталога с помощью объекта Path используйте метод mkdir этого объекта. Если передать методу mkdir параметр parents=True, он создаст все необходимые промежуточные каталоги; в противном случае при отсутствии промежуточного каталога возникает исключение FileNotFoundError: new_path = Path ('mydir') new_path.mkdir(parents=True) list(cur_path.iterdir()) [PosixPath('anscombe.json'), PosixPath('README.md'), PosixPath('.ipynb_checkpoints'), PosixPath('mydir'), PosixPath('2.tmp'), PosixPath('california_housing_train.csv'), PosixPath('california_housing_test.csv'),
   294 Глава 12. Работа с файловой системой PosixPath('mnist_test.csv'), PosixPath('mnist_train_small.csv')] new_path.is_dir() True Для удаления каталога служит метод rmdir. Он удаляет только пустые каталоги. При попытке вызвать его для непустого каталога возникает исключение: new_path = Path('mydir') new_path.rmdir() list(cur_path.iterdir()) [PosixPath('anscombe.json'), PosixPath('README.md'), PosixPath('.ipynb_checkpoints'), PosixPath('2.tmp'), PosixPath('california_housing_train.csv'), PosixPath('california_housing_test.csv'), PosixPath('mnist_test.csv'), PosixPath('mnist_train_small.csv')] Как видно из предыдущих примеров, объекты Path несколько отличаются от функций модуля os. Важно помнить, что каждый объект Path представляет собой конкретный путь или местоположение в системе, тогда как функции os предназначены для работы с любым путем или местоположением каталога. 12.5. Обработка всех файлов в поддереве каталога Наконец, для рекурсивного анализа иерархий файловой системы существует весьма полезная функция os.walk. С ее помощью можно обойти всю структуру каталогов (дерево каталогов), причем для каждого пройденного каталога возвращаются три вещи: корень (или путь к этому каталогу), список вложенных каталогов (подкаталогов) и список файлов. Функция os.walk вызывается с указанием пути к начальному (или верхнему) каталогу и может иметь три необязательных аргумента: os.walk(directory, topdown=True, onerror=None, followlinks= False). Аргумент directory задает начальный путь к каталогу. Если аргумент topdown равен True или не задан, файлы каждого каталога обрабатываются до его подкаталогов, в результате чего будет получен список, идущий сверху вниз. С другой стороны, если аргумент topdown равен False, сначала обрабатываются подкаталоги каждого каталога, и обход дерева каталогов осуществляется снизу вверх. Параметру onerror можно присвоить функцию обработки ошибок, возникающих при вызовах os.listdir, которые по умолчанию игнорируются. Функция os.walk по умолчанию не переходит в папки, которые являются символическими ссылками, если только не передать ей параметр followlinks=True.
   12.6. Практическая работа: другие операции с файлами 295 При вызове os.walk создает итератор, который рекурсивно применяет себя ко всем каталогам, содержащимся в параметре top. Иначе говоря, для каждого подкаталога subdir в names функция os.walk рекурсивно вызывает саму себя в форме os.walk(subdir, ...). Обратите внимание, что если аргумент topdown равен True или не задан, то список подкаталогов может быть изменен (любыми операторами или методами модификации списка) перед тем, как его элементы будут использованы на следующем уровне рекурсии; вы можете задействовать его для управления тем, в какие подкаталоги (если таковые имеются) будет переходить os.walk. Чтобы получить представление о работе os.walk, я рекомендую обойти дерево каталогов и вывести значения, возвращаемые для каждого каталога. Для демонстрации возможностей функции os.walk выведем список содержимого текущего рабочего каталога и всех вложенных каталогов вместе со счетчиком количества элементов в каждом из них, не перечисляя при этом содержимое каталогов .config: import os for root, dirs, files in os.walk(os.curdir): print("{0} has {1} files".format(root, len(files))) if ".config" in dirs: Проверяет наличие каталога с именем .config dirs.remove(".config") Удаляет .config (только каталог .config) из списка каталогов Этот пример достаточно сложен, и чтобы раскрыть весь потенциал функции os.walk, вам, вероятно, стоит поэкспериментировать с ней для полного понимания механизма ее работы. Функция copytree модуля shutil осуществляет рекурсивное копирование всех файлов в каталоге и всех его подкаталогах, сохраняя режим доступа и статистическую информацию (то есть время обращения/изменения). Модуль shutil также содержит уже упоминавшуюся функцию rmtree для удаления каталога и всех его подкаталогов, а также несколько функций для создания копий отдельных файлов. Их поведение подробно описано в документации к стандартной библиотеке. 12.6. Практическая работа: другие операции с файлами Как бы вы вычислили общий размер всех файлов в каталоге с расширением .test, которые не являются символическими ссылками? Если в первом ответе вы прибегли к os и os.path, попробуйте выполнить ту же задачу при помощи pathlib, и наоборот. Отталкиваясь от вашего решения, напишите код, перемещающий обнаруженные файлы .test в новый подкаталог под именем backup, расположенный в том же каталоге. Не забудьте прежде всего удостовериться в том, что в вашем начальном каталоге и его подкаталогах имеется хотя бы несколько файлов с разрешением .test. В Colaboratory обязательно выполните ячейку прямо перед ответами.
   296 Глава 12. Работа с файловой системой 12.6.1. Решение задачи при помощи кода, сгенерированного ИИ Это задание предполагает использование творческого подхода: начальная часть, хотя и не представляет значительной трудности, требует сочетания различных компонентов модулей os или pathlib для получения рабочего решения, на основе которого затем строится полный вариант решения. Поскольку задачу можно решить при помощи любого из этих модулей, рекомендуется реализовать оба варианта для получения комплексного опыта. Для Copilot были предложены следующие промпты: Напиши скрипт на Python для подсчета общего размера всех файлов с расширением .test (кроме символических ссылок) в текущем каталоге и его подкаталогах. Добавляем перенос файлов в каталог резервных копий backup: Измени этот скрипт таким образом, чтобы он также перемещал файлы с расширением .test в подкаталог текущего каталога с именем backup. Делаем рефакторинг кода с использованием модуля pathlib: Сделай рефакторинг предыдущего скрипта, используя модуль pathlib вместо os и shutil. Промпты для Colaboratory такие же, но с добавлением «в предыдущей ячейке» для обоих вариантов рефакторинга кода. 12.6.2. Решения и обсуждение Хотя скрипты в этой практической работе небольшие, они достаточно насыщенные, так как для их выполнения требуется несколько различных элементов модулей os и pathlib и эти элементы должны сочетаться определенным образом. Программное решение, созданное человеком Мое первое решение использует pathlib, поскольку, на мой взгляд, обработка путей как объектов делает код более чистым и последовательным. Первая часть задачи — получение общего размера всех файлов .test — довольно проста благодаря различным методам класса Path: import pathlib cur_path = pathlib.Path("sample_data") size = 0 for text_path in cur_path.rglob("*.test"): if not text_path.is_symlink(): size += text_path.stat().st_size print(size) Находит все файлы с именами, оканчивающимися на .test Убеждается в том, что это не символические ссылки Извлекает размер файла и добавляет его к суммарному размеру Этот код должен справиться с тремя задачами: найти целевые файлы, что решает метод glob; проверить, являются ли они символическими ссылками, что
   12.6. Практическая работа: другие операции с файлами 297 выполняется специальным методом; получить размер с помощью метода stat() и добавить его к суммарному размеру. То, что все эти методы являются частью объекта Path для текущего каталога, делает программный код лаконичным и понятным. Следующая задача — сделать рефакторинг этого кода таким образом, чтобы в дополнение к поиску целевых файлов и подсчету их общего размера переместить эти файлы в подкаталог с именем backup: import pathlib cur_path = pathlib.Path("sample_data") new_path = pathlib.Path("backup") new_path.mkdir(exist_ok=True) Создает каталог backup, если он отсутствует size = 0 for text_path in cur_path.rglob("*.test"): if not text_path.is_symlink(): size += text_path.stat().st_size text_path.rename(new_path / text_path.name) Перемещает в новый каталог, переименовав путь к файлу print(size) Первое изменение: нам необходимо убедиться в существовании каталога backup. Создадим для него объект Path и используем его для формирования реального каталога с этим именем на диске, но лишь в случае, если такого каталога еще нет. Параметр exist_ok=True гарантирует, что каталог будет создан, если он не существует, но при этом не возникнет исключение, если такой каталог уже имеется. Вторая необходимая операция — непосредственно перемещение файла. В традициях Unix файл можно переместить, изменив имя его расположения. В нашем случае мы можем взять текущий путь к файлу, добавить к его началу путь к каталогу backup с последующим вызовом метода rename («переименовать») для приведения пути к новому значению. Использование rename для перемещения файла может показаться странным, но важно понимать: фактическое местоположение байтов на диске остается неизменным — меняется только их адресация в файловой системе, то есть в действительности мы лишь даем файлу новый путь. Заключительная часть упражнения — рефакторинг нашего решения при помощи другого модуля. Поскольку я начала с pathlib, теперь пришло время задействовать os. Обычно я бы просто провела рефакторинг окончательного решения, но для сохранения чистоты и последовательности кода я разделила процесс на те же два этапа: сначала вычисление суммарного размера, а затем реализация перемещения файлов: import os cur_path = "." size = 0 for root, dirs, files in os.walk(os.curdir): for file in files: test_path = os.path.join(root, file) Обходит все каталоги Находит файлы, заканчивающиеся на .test, и проверяет, не являются ли они символическими ссылками if (not os.path.islink(test_path) and os.path.splitext(test_path)[-1] == ''): print(os.path.join(root, file)) Извлекает размер
import os cur_path = "."    298 Глава 12. Работа с файловой системой size = 0 for root, dirs, files in os.walk(os.curdir): for file in files: test_path = os.path.join(root, file) Обходит все каталоги Находит файлы, заканчивающиеся на .test, и проверяет, не являются ли они символическими ссылками if (not os.path.islink(test_path) and os.path.splitext(test_path)[-1] == ''): print(os.path.join(root, file)) size += os.path.getsize(test_path) print(f"{size}") Извлекает размер файла и добавляет его к суммарному размеру Это решение применяет метод os.walk для получения всех файлов в каталоге и его подкаталогах, и оно явно выглядит более громоздким и сложным для понимания (по сравнению с предыдущим подходом). Основной цикл задействует os.walk для получения каждого каталога и рекурсивно использует его в качестве корневого до тех пор, пока не обойдет все подкаталоги текущего каталога. Для каждого найденного файла требуется установить, что он не является символической ссылкой и имеет расширение .test. При выполнении этих условий мы получаем размер файла и добавляем его к общей сумме. Рефакторинг кода для перемещения целевых файлов требует аналогичных изменений, что и в версии с pathlib, за исключением того, что здесь у нас нет оператора / из pathlib для объединения фрагментов путей: import os cur_path = "." new_path = os.path.join(cur_path,"backup") # создание каталога backup os.makedirs(new_path, exist_ok=True) Создает каталог backup, size = 0 если он отсутствует for root, dirs, files in os.walk(os.curdir): for file in files: test_path = os.path.join(root, file) print(test_path) if (not os.path.islink(test_path) and os.path.splitext(test_path)[-1] == ".test"): print(os.path.join(root, file)) size += os.path.getsize(test_path) Перемещает в новый os.rename(test_path, os.path.join(new_path, каталог, переименовав путь os.path.basename(test_path))) print(f"{size}") Модификации кода для перемещения файлов аналогичны тем, что применялись с объектами Path, однако код получился несколько длиннее и, на мой взгляд, его определенно сложнее читать. Тем не менее, если у вас имеются конкретные предпочтения, этот подход вполне допустим. И хотя «Дзен языка Python» гласит: «Должен существовать один и желательно только один очевидный способ сделать это», в данном случае по мере развития языка сформировались два равноценных способа выполнения файловых операций такого типа.
   12.6. Практическая работа: другие операции с файлами 299 Программные решения, сгенерированные ИИ Решение задачи при помощи средств ИИ оказалось немного сложнее. Как отмечалось выше, промпт к первой части задачи следующий: Напиши скрипт на Python для подсчета общего размера всех файлов с расширением .test (кроме символических ссылок) в текущем каталоге и его подкаталогах. В ответ Copilot предложил следующий план первой части решения задачи. 1. Импортировать модуль os. 2. Присвоить переменной size значение 0. 3. При помощи os.walk обойти все файлы в текущем каталоге и его подкаталогах. 4. Для каждого файла проверить, не является ли он символической ссылкой, используя os.path.islink. 5. Если файл не является символической ссылкой, проверить, имеет ли он расширение .test, задействовав os.path.splitext. 6. Если файл имеет расширение .test, прибавить размер файла к значению size посредством os.path.getsize. 7. По окончании цикла вывести значение size. Это подробный план действий для решения задачи, и довольно удобно просмотреть, какие именно шаги будет выполнять чат-бот: # txt_file_size.py import os size = 0 for root, dirs, files in os.walk(os.curdir): for file in files: file_path = os.path.join(root, file) if (not os.path.islink(file_path) and os.path.splitext(file)[1] == '.test'): size += os.path.getsize(file_path) print(f"Total size of all files that are not symlinks: {size} bytes") Вот промпт для второй части: Измени этот скрипт таким образом, чтобы он также перемещал файлы с расширением .test в подкаталог текущего каталога с именем backup. Copilot предложил следующий план. 1. Импортировать модули os и shutil. 2. Присвоить переменной size значение 0.
   300 Глава 12. Работа с файловой системой 3. Создать каталог с именем backup в текущем каталоге при помощи os.makedirs, если он еще не существует. 4. При помощи os.walk обойти все файлы в текущем каталоге и его подкаталогах. 5. Для каждого файла проверить, не является ли он символической ссылкой, используя os.path.islink. 6. Если файл не является символической ссылкой, проверить, имеет ли он расширение .test, задействовав os.path.splitext. 7. Если файл имеет расширение .test, прибавить размер файла к значению size посредством os.path.getsize. 8. Переместить файл в каталог backup при помощи shutil.move. 9. По окончании цикла вывести значение size. Полученный в результате код был простым и почти не отличался от моего решения: # txt_file_size_and_move.py import os import shutil size = 0 os.makedirs('backup', exist_ok=True) for root, dirs, files in os.walk(os.curdir): for file in files: file_path = os.path.join(root, file) if (not os.path.islink(file_path) and os.path.splitext(file)[1] == '.test' and "backup" not in file_path): size += os.path.getsize(file_path) shutil.move(file_path, 'backup') print(f"Total size of all .test files that are not symlinks: {size} bytes") Единственное существенное отличие от решения, созданного программистом, — использование модуля shutil для перемещения целевых файлов. Такой подход вполне допустим, хотя он и игнорирует тот факт, что os.rename может выполнить ту же задачу без импорта дополнительного модуля. Лично я предпочитаю импортировать как можно меньше модулей. В этом примере все просто и понятно, однако по мере роста проектов импортированных модулей часто становится все больше, что может усложнить парсинг. По моему мнению, лучший способ этого избежать — не спешить с добавлением новых модулей. Следующим и заключительным шагом является рефакторинг кода с использованием другого модуля — в данном случае pathlib вместо os и shutil. Промпт для этого звучит так:
   12.6. Практическая работа: другие операции с файлами 301 Сделай рефакторинг предыдущего скрипта, используя модуль pathlib вместо os и shutil. План преобразования кода с учетом использования pathlib, предложенный Copilot, был следующим. 1. Импортировать модуль pathlib. 2. Присвоить переменной size значение 0. 3. Создать объект pathlib.Path для текущего каталога и подкаталога с именем backup. 4. При помощи Path.mkdir создать каталог с именем backup, если он еще не существует. 5. При помощи Path.rglob обойти все файлы в текущем каталоге и его подкаталогах. 6. Для каждого файла проверить, не является ли он символической ссылкой, используя Path.is_symlink. 7. Если файл не является символической ссылкой, проверить, имеет ли он расширение .test, задействовав Path.suffix. 8. Если файл имеет расширение .test, прибавить размер файла к значению size посредством Path.stat. 9. Переместить файл в каталог backup при помощи Path.rename. 10. По окончании цикла вывести значение size. Полученный код очень похож на решение, созданное программистом: # txt_file_size_and_move.py from pathlib import Path size = 0 backup_dir = Path('backup') backup_dir.mkdir(exist_ok=True) for file in Path().rglob('*'): if not file.is_symlink() and file.suffix == '.test': size += file.stat().st_size file.rename(backup_dir / file.name) print(f"Total size of all .test files that are not symlinks: {size} bytes") По правде говоря, об этом коде нельзя сказать ничего такого, о чем бы мы уже не упомянули при обсуждении решения, написанного мной. ИИ-бот Colaboratory оказался не настолько близок к совершенству. Три из четырех предложенных решений нарушали принцип инкапсуляции основного кода в функцию, поэтому мы выбрали вариант, который этого избежал, — чтобы
   302 Глава 12. Работа с файловой системой сохранить единообразие кода. Впрочем, все решения Colaboratory использовали одинаковый подход к реализации базовой функциональности. Был предложен следующий промпт: Напиши скрипт на Python для подсчета общего размера всех файлов с расширением .test (кроме символических ссылок) в текущем каталоге и его подкаталогах. На что Colaboratory выдал следующее решение: # промпт: Напиши скрипт на Python для подсчета общего размера всех файлов # с расширением .test (кроме символических ссылок) # в текущем каталоге и его подкаталогах. import os total_size = 0 for root, _, files in os.walk('.'): for file in files: if (file.endswith('.test') and not os.path.islink(os.path.join(root, file))): total_size += os.path.getsize(os.path.join(root, file)) print(f"Total size of .test files: {total_size} bytes") Этот программный код вполне приемлем, однако стоит отметить пару моментов. Во-первых, применение «_» в первом цикле for — общепринятая практика в языке Python. Символ «_» является допустимым для имени переменной и часто применяется самостоятельно для обозначения временной переменной цикла, которой присвоено неважное или неиспользующееся значение. Во-вторых, хотя конструкция оператора if вполне допустима, она получилась довольно длинной и сложной для восприятия. Вторая часть задания, которая заключается в перемещении целевых файлов в резервный каталог backup, имеет следующий промпт: Измени скрипт в предыдущей ячейке таким образом, чтобы он также перемещал файлы с расширением .test в подкаталог текущего каталога с именем backup. Код получился вполне приемлемым: # промпт: Измени скрипт в предыдущей ячейке таким образом, # чтобы он также перемещал файлы с расширением .test # в подкаталог текущего каталога с именем backup. import os total_size = 0 backup_dir = os.path.join(os.getcwd(), 'backup') # Создать каталог backup, если он еще не существует if not os.path.exists(backup_dir):
   12.6. Практическая работа: другие операции с файлами 303 os.makedirs(backup_dir) for root, _, files in os.walk('.'): for file in files: if (file.endswith('.test') and not os.path.islink(os.path.join(root, file))): total_size += os.path.getsize(os.path.join(root, file)) # Переместить файл в каталог backup os.rename(os.path.join(root, file), os.path.join(backup_dir, file)) print(f"Total size of .test files: {total_size} bytes") Помимо предыдущих замечаний, стоит отметить, что при создании каталога backup эта версия выполняет явную проверку его существования вместо применения параметра exist_ok=True для упрощения кода. Заключительный этап — рефакторинг кода с использованием pathlib — оказался слабым местом Colaboratory. Предлагался следующий промпт: Перепиши скрипт в предыдущей ячейке, используя модуль pathlib вместо os и os. path. Не используй модули os или os.path. Несмотря на явные указания промпта, в решении применяются os и os.path: # промпт: Перепиши скрипт в предыдущей ячейке, используя модуль pathlib # вместо os и os.path. Не используй модули os или os.path. from pathlib import Path total_size = 0 backup_dir = Path.cwd().joinpath('backup') # Создать каталог backup, если он еще не существует if not backup_dir.exists(): backup_dir.mkdir(parents=True) for root, _, files in Path('.').walk(): for file in files: if file.endswith('.test') and not file.is_symlink(): total_size += file.stat().st_size # Переместить файл в каталог backup file.rename(backup_dir.joinpath(file)) print(f"Total size of .test files: {total_size} bytes") В данном решении есть ряд моментов, заслуживающих комментариев. Во-первых, здесь применен метод Path.walk(), появившийся лишь в Python 3.12. Само по себе это не проблема, но может стать препятствием в средах с более ранними версиями Python. Более того, на момент написания этой книги Colaboratory работала на Python 3.10, поэтому она не смогла выполнить собственное предложенное решение.
   304 Глава 12. Работа с файловой системой Кроме того, использование walk() без крайней необходимости приводит к созданию двух вложенных циклов for: внешнего для обхода каталогов и внутреннего для обработки файлов каждого каталога. Вложенные циклы for довольно часто приводят к ощутимому замедлению работы кода, особенно по мере увеличения числа элементов, поэтому одним из преимуществ использования pathlib является то, что метод rglob автоматически обрабатывает рекурсивный обход всех подкаталогов. И последний важный момент: при выборе walk() вместо рекурсивного rglob требуется явная проверка соответствия имени файла целевому шаблону. В итоге и Copilot, и Colaboratory сгенерировали корректные варианты, однако решение Copilot продемонстрировало качество, сопоставимое с кодом, написанным программистом. Итоги Язык Python предоставляет набор функций и констант для работы со ссылками файловой системы (путями) и управления операциями с файловой системой способом, не зависящим от основной операционной системы. Для более сложных и специализированных операций с файловой системой, требующих учета особенностей определенных ОС, в Python существует два основных модуля: os и pathlib. Язык Python может работать как с относительными, так и с абсолютными путями независимо от операционной системы, используя модули os и pathlib. Модули os и pathlib предоставляют исчерпывающий набор инструментов для получения информации о файлах и каталогах. Оба модуля для работы с файлами реализуют стандартный набор операций по управлению файловой системой, включая перемещение, переименование, копирование и удаление файлов и каталогов. Все файлы в каталоге можно обрабатывать с помощью функций os.walk или Path.rglob и методов из модулей os или pathlib. Для удобства в табл. 12.1 и 12.2 приведено краткое описание функций, рассмотренных в этой главе. Таблица 12.1. Краткое описание значений и функций файловой системы Функция Значение или операция файловой системы os.getcwd(), Path.cwd() Получает текущий каталог os.name Обеспечивает общую идентификацию платформы sys.platform Предоставляет конкретную информацию о платформе os.environ Отображает переменные среды os.listdir(path) Получает список файлов в каталоге
   Итоги 305 Функция Значение или операция файловой системы os.scandir(path) Получает итератор для объектов os.DirEntry в каталоге os.chdir(path) Изменяет каталог os.path.join(elements), Path. joinpath(elements) Объединяет элементы в путь os.path.split(path) Разделяет путь на голову (путь к каталогу) и хвост (последний элемент пути – имя файла или каталога) Path.parts Кортеж элементов пути os.path.splitext(path) Разделяет путь на корневое имя и расширение файла Path.suffix Расширение файла пути os.path.basename(path) Получает базовое имя пути Path.name Базовое имя пути os.path.commonprefix(list_of_ paths) Получает общий префикс для всех путей в списке os.path.expanduser(path) Разворачивает ~ или ~user в абсолютный путь os.path.expandvars(path) Разворачивает переменные среды в пути os.path.exists(path) Проверяет существование объекта, на который указывает путь os.path.isdir(path), Path. is_dir() Проверяет, является ли каталогом объект, на который указывает путь os.path.isfile(path), Path. is_file() Проверяет, является ли файлом объект, на который указывает путь os.path.islink(path), Path. is_link() Проверяет, является ли символической ссылкой (не ярлыком Windows) объект, на который указывает путь os.path.ismount(path) Проверяет, является ли точкой монтирования объект, на который указывает путь os.path.isabs(path), Path. is_absolute() Проверяет, является ли путь абсолютным os.path.samefile(path_1, path_2) Проверяет, указывают ли два пути на один и тот же объект файловой системы os.path.getsize(path) Получает размер файла, на который указывает путь os.path.getmtime(path) Получает время последнего изменения пути os.path.getatime(path) Получает время последнего обращения к пути os.rename(old_path, new_path) Переименовывает файл, используя старый и новый пути os.mkdir(path) Создает каталог по указанному пути os.makedirs(path) Создает каталог и все необходимые промежуточные каталоги по указанному пути os.rmdir(path) Удаляет каталог по указанному пути
   306 Глава 12. Работа с файловой системой Функция Значение или операция файловой системы glob.glob(pattern) Получает список путей к файлам и каталогам, соответствующих шаблону с подстановочными знаками os.walk(path) Получает все имена файлов в дереве каталога Таблица 12.2. Частичный список свойств и функций модуля pathlib Метод или свойство Значение или операция Path.cwd() Получает текущий каталог Path.joinpath(elements) или Path / element / element Объединяет элементы в новый путь Path.parts Кортеж из элементов пути Path.suffix Расширение файла, на который указывает путь Path.name Базовое имя пути Path.exists() Проверяет существование объекта, на который указывает путь Path.is_dir() Проверяет, является ли каталогом объект, на который указывает путь Path.is_file() Проверяет, является ли файлом объект, на который указывает путь Path.is_symlink() Проверяет, является ли символической ссылкой (не ярлыком Windows) объект, на который указывает путь Path.is_absolute() Проверяет, является ли путь абсолютным Path.samefile(Path2) Проверяет, указывают ли два пути на один и тот же файл Path1.rename(Path2) Переименовывает файл Path.mkdir([parents=True]) Создает каталог, указанный в пути; если аргумент parents равен True, также создает необходимые про- межуточные каталоги Path.rmdir() Удаляет каталог, на который указывает путь Path.glob(pattern) Получает пути файлов и каталогов, соответствующие шаблону с подстановочными знаками
13 Чтение и запись файлов В этой главе: 3 Открытие файлов и файловые объекты 3 Закрытие файлов 3 Открытие файлов в разных режимах 3 Чтение и запись текстовых или двоичных данных 3 Перенаправление консольного ввода-вывода 3 Использование модуля struct 3 Сериализация объектов в файлы с помощью модуля pickle 3 Сохранение объектов в модуле shelve Для многих разработчиков операции с файлами — неотъемлемая часть ежедневной работы. Простые текстовые файлы являются наиболее распространенным типом файлов и применяются в самых разных ситуациях: от файлов с необработанными данными и лог-файлов (журналов событий) до файлов с исходным кодом и пр. Если текстовые файлы не справляются с задачей, можно использовать двоичные — они позволяют хранить практически любые данные, будь то специфичные двоичные форматы (с использованием модуля struct) или даже объекты Python (при помощи модулей pickle и shelve). В этой главе мы рассмотрим чтение и запись данных в файлы в языке Python.
   308 Глава 13. Чтение и запись файлов 13.1. Открытие файлов и файловые объекты Один неоспоримый факт о вычислительных системах: файлы присутствуют везде. Данные, которые мы используем, хранятся в файлах; программный код, который их обрабатывает, — это тоже файлы; и в ходе работы с данными мы то и дело записываем результаты в файлы. Мы их скачиваем, копируем, ищем, прикрепляем, шифруем и архивируем. Но самое распространенное действие с файлами — это их открытие и чтение. В языке Python для открытия и чтения файлов используются встроенная функция open() и различные встроенные операции чтения. Вместо работы с путями и именами файлов напрямую Python оперирует файловыми объектами (file objects), которые отслеживают состояние файла (в частности, сколько данных уже прочитано или записано). Все операции ввода-вывода файлов в Python выполняются с помощью файловых объектов. Вот пример небольшой программы, которая считывает одну строку из текстового файла с именем myfile: with open('myfile', 'r') as file_object: line = file_object.readline() Первый аргумент функции open() — это путь к файлу. В предыдущем примере мы открываем файл myfile, который должен существовать в текущем рабочем каталоге. Сама функция open() не читает данные из файла — вместо этого она возвращает файловый объект, через который можно обратиться к содержимому открытого файла. Обратите внимание на использование ключевого слова with; это означает, что файл будет открыт с помощью контекстного менеджера, о котором мы подробнее поговорим в главе 14. Пока достаточно знать, что такой способ открытия файлов лучше справляется с потенциальными ошибками ввода-вывода и в целом считается предпочтительным. Файловые объекты имеют различные методы для чтения и записи данных, навигации по файлу и других операций. В предыдущем примере первый вызов readline() считывает и возвращает первую строку файлового объекта (все до первого символа новой строки включительно или весь файл, если такого символа нет); каждый следующий вызов readline() возвращает очередную строку, если она существует. Следующий код открывает файл по абсолютному пути: c:\My Documents\test\ myfile: import os file_name = os.path.join("c:", "My Documents", "test", "myfile") file_object = open(file_name, 'r') В данном примере файл открывается напрямую (но не читается), демонстрируя безопасный способ формирования пути посредством функции os.path.join(), которая объединяет элементы в соответствии с текущими соглашениями операционной системы.
   13.3. Открытие файлов для записи и в других режимах 309 13.2. Закрытие файлов После того как все данные из файлового объекта были считаны или записаны в него, этот объект следует закрыть. Его закрытие высвобождает системные ресурсы, позволяет другому коду выполнять чтение исходного файла или записывать в него данные и в целом повышает надежность программы. Для небольших скриптов незакрытый файловый объект обычно не приводит к особым последствиям, так как такие объекты автоматически закрываются при завершении скрипта или программы. Для более крупных программ наличие большого количества открытых файловых объектов может привести к исчерпанию системных ресурсов и прекращению работы программы. Если файловый объект больше не нужен, его следует закрыть методом close. При этом фрагмент кода, приведенный выше, примет вид: file_object = open("myfile", 'r') line = file_object.readline() # . . . любое дальнейшее чтение из file_object . . . file_object.close() Применение ключевого слова with (которое автоматически вызывает контекстный менеджер) — это также хороший способ автоматического закрытия файлов после завершения работы с ними: with open("myfile", 'r') as file_object: line = file_object.readline() # . . . любое дальнейшее чтение из file_object . . . Здесь, несмотря на отсутствие явного вызова метода close(), файл закрывается как часть контекстного менеджера, инициированного ключевым словом with. 13.3. Открытие файлов для записи и в других режимах Второй аргумент команды open — это строка, которая указывает режим открытия файла. Режим 'r' означает «Открыть файл для чтения», 'w' — «Открыть файл для записи» (все существующие данные в файле будут удалены), 'a' — «Открыть файл для добавления записи» (новые данные будут добавлены в конец существующих). Если вы хотите открыть файл для чтения, второй аргумент можно опустить, так как режим 'r' используется по умолчанию. Следующий фрагмент кода записывает в файл строку «Hello, World»: file_object = open("myfile", 'w') file_object.write("Hello, World\n") file_object.close() В зависимости от используемой операционной системы функция open может иметь доступ к дополнительным режимам работы с файлами. Эти режимы не являются необходимыми для большинства целей. По мере написания более
   310 Глава 13. Чтение и запись файлов сложных программ на Python рекомендуется обращаться к официальной справочной документации за дополнительной информацией. Функция open может принимать необязательный третий аргумент, который определяет способ буферизации операции чтения или записи для данного файла. Буферизация — это процесс хранения данных в памяти до тех пор, пока не накопится достаточно данных для чтения или записи, чтобы оправдать временные затраты на обращение к диску. Другие параметры open управляют кодировкой текстовых файлов и обработкой символов новой строки в текстовых файлах. Опять же, это не те аспекты настройки функции, о которых вам придется часто беспокоиться, однако по мере углубленного изучения Python вам, возможно, захочется ознакомиться с ними подробнее. 13.4. Функции для чтения и записи текстовых и двоичных данных Я уже представила самую распространенную функцию для чтения текстовых файлов readline. Эта функция читает и возвращает одну строку из файлового объекта, включая любой символ новой строки в конце строки. Если в файле не осталось данных, которые можно было бы считать, readline возвращает пустой строковый объект, что позволяет, к примеру, легко подсчитать количество строк в файле: file_object = open("myfile", 'r') count = 0 while file_object.readline() != "": count = count + 1 print(count) file_object.close() Применительно к данной конкретной задаче для подсчета всех строк еще проще воспользоваться встроенным методом readlines, который считывает все строки в файле и возвращает их в виде списка строковых объектов, где каждый элемент расположен в отдельной строке (включая завершающие символы новой строки): file_object = open("myfile", 'r') print(len(file_object.readlines())) file_object.close() Разумеется, если вам когда-либо придется подсчитывать строки в очень большом файле, этот метод может привести к нехватке памяти в компьютере, поскольку он сразу считывает весь файл в память целиком. Аналогичным образом, readline может вызвать переполнение памяти при попытке чтения строки из крупного файла без символов новой строки (хотя такая ситуация маловероятна). Для обработки подобных задач методы readline и readlines могут принимать необязательный аргумент, контролирующий объем данных, считываемых за один раз. Подробности изложены в справочной документации Python.
   13.4. Функции для чтения и записи текстовых и двоичных данных 311 Еще один способ перебора всех строк в файле основан на использовании файлового объекта как итератора в цикле for: file_object = open("myfile", 'r') count = 0 for line in file_object: count = count + 1 print(count) file_object.close() К преимуществам этого способа можно отнести то, что строки считываются и обрабатываются по одной, так что даже при работе с очень большими файлами о переполнении памяти можно не волноваться. Еще одно достоинство — простота и читаемость программного кода. При чтении текстовых файлов может возникнуть проблема: строки могут заканчиваться разными символами в зависимости от ОС, в которой файл был создан. В текстовом режиме на Mac строки по умолчанию оканчиваются символом \r, а в Windows используются пары символов \r\n. По умолчанию файловый объект Python читает текстовые файлы в «универсальном» режиме, преобразуя оба варианта символов конца строки в \n. Если затем записать эти строковые объекты в другой текстовый файл, они больше не будут содержать специфичные для ОС окончания строк, что может вызвать проблемы. Режим обработки символов новой строки можно задать через параметр newline при открытии файла: укажите newline="\n", "\r" или "\r\n", в результате чего только эта последовательность будет применяться в качестве символа новой строки: input_file = open("myfile", newline="\n") В данном примере симоволом новой строки будет считаться только "\n". Если файл открыт в двоичном режиме, параметр newline не требуется, поскольку все байты возвращаются точно в том виде, в каком они находятся в файле. Можно также использовать newline="" с функцией open, что позволит принимать любые варианты в качестве символа новой строки, но при этом возвращать их в том виде, в котором они использовались в файле, без преобразования. Методам чтения readline и readlines соответствуют методы записи write и writelines (обратите внимание: функции writeline не существует). Метод write записывает один строковый объект, который может включать несколько строк, если в нем содержатся символы новой строки, как в следующем примере: myfile.write("Hello\nWorld\nHow are you?") Метод write не добавляет символ новой строки после записи своего аргумента; если вы хотите, чтобы вывод включал новую строку, вам будет нужно добавить ее самостоятельно. При открытии файла в текстовом режиме (с использованием 'w') все символы \n автоматически преобразуются в стандартные для текущей
   312 Глава 13. Чтение и запись файлов ОС окончания строк (то есть '\r\n' в Windows или '\r' в macOS). Чтобы избежать этого преобразования, явно укажите параметр newline при открытии файла. Название метода writelines может вводить в заблуждение, поскольку он не обязательно записывает строки; он принимает список строковых объектов в качестве аргумента и записывает их один за другим в заданный файловый объект без добавления символов новой строки. Если строковые объекты в списке заканчиваются символами новой строки, они записываются как строки; в противном случае они фактически объединяются в файле. Но writelines представляет собой точную обратную операцию для readlines в том смысле, что его можно задействовать для списка, возвращаемого readlines, чтобы создать файл, идентичный файлу, из которого производилось чтение. При условии, что myfile.txt существует и является текстовым файлом, следующий фрагмент кода создает его точную копию с именем myfile2.txt: input_file = open("myfile.txt", 'r', newline="") lines = input_file.readlines() input_file.close() output = open("myfile2.txt", 'w') output.writelines(lines) output.close() Хотя эти базовые операции чтения и записи файлов просты, по моему опыту, их вполне достаточно для решения широкого круга повседневных задач в программировании. 13.4.1. Двоичный режим В некоторых случаях может потребоваться считать все данные из файла в один байтовый объект (объект типа bytes), особенно если данные не являются строковым объектом и вы хотите полностью разместить их в памяти, чтобы обрабатывать как последовательность байтов. Или же может понадобиться считать данные в виде байтовых объектов фиксированного размера. Например, можно читать данные без явных символов новой строки, где каждая строка считается последовательностью символов фиксированного размера. Для этого откройте файл в двоичном режиме чтения ('rb') и используйте метод read. Без аргументов этот метод читает все данные от текущей позиции и возвращает их как байтовый объект. При использовании одного целочисленного аргумента он читает указанное количество байтов (или меньше, если в файле недостаточно данных для выполнения запроса) и возвращает байтовый объект заданного размера: input_file = open("myfile", 'rb') header = input_file.read(4) data = input_file.read() input_file.close() Выполняет чтение в двоичном режиме Считывает четыре байта в переменную header Считывает оставшиеся данные в виде байтового объекта Первая строка открывает файл для чтения в двоичном режиме, вторая считывает первые четыре байта в переменную header, которая ссылается на байтовый
   13.5. Чтение и запись при помощи pathlib 313 объект, а третья строка считывает оставшуюся часть файла в виде байтового объекта как единый блок данных. Помните, что при открытии файла в двоичном режиме вы работаете только с байтовыми объектами, а не со строковыми. Чтобы преобразовать данные в строковый объект, необходимо декодировать байтовые объекты в строковые. Этот момент играет немаловажную роль при работе с сетевыми протоколами, где потоки данных могут выглядеть как текстовые файлы, но должны интерпретироваться именно как байтовые, а не как строковые объекты. Быстрая проверка: двоичный режим Зачем добавлять символ "b" в строку режима открытия файла — например, в open("file", "wb")? Допустим, вы хотите открыть файл с именем myfile.txt и записать дополнительные данные в конец файла. Какую команду вы бы использовали для открытия myfile. txt? Какую команду вы бы выбрали, чтобы повторно открыть файл и прочитать его данные с начала? 13.5. Чтение и запись при помощи pathlib Помимо мощного арсенала средств работы с путями, который подробно рассматривался в главе 12, объект Path также может использоваться для чтения и записи текстовых и двоичных файлов. Данная возможность может быть удобна тем, что она не требует явного вызова open и close, а для текстовых и двоичных операций применяются отдельные методы. Однако есть и ограничение: посредством методов объекта Path нельзя добавлять данные, поскольку запись полностью заменяет любое существующее содержимое: from pathlib import Path p_text = Path('my_text_file') p_text.write_text('Text file contents') 18 Количество записанных символов p_text.read_text() 'Text file contents' p_binary = Path('my_binary_file') p_binary.write_bytes(b'Binary file contents') 20 Количество записанных байтов p_binary.read_bytes() b'Binary file contents' Данные в виде байтового объекта В этих примерах объект Path выполняет чтение и запись в двоичном режиме при помощи своих методов read_bytes и write_bytes.
   314 Глава 13. Чтение и запись файлов 13.6. Консольный ввод-вывод и перенаправление Иногда не требуется считывать или записывать файлы, а необходимо взаимодействие с пользователем через командную строку. Это может быть удобно, например, для простых утилит и задач тестирования. Для запроса и чтения входных строковых объектов можно задействовать встроенный метод input: x = input("enter file name to use: ") enter file name to use: myfile х 'myfile' Запрос к пользователю является необязательным и может представлять собой любой строковый объект (включая f-строки). Ввод пользователя завершается нажатием клавиши Enter, но символ новой строки в конце строки ввода удаляется. Функция input обрабатывает все данные как строковые объекты, поэтому для чтения чисел с ее помощью необходимо явно преобразовать возвращаемый строковый объект в корректный числовой тип. В следующем примере используется преобразование в целочисленный тип (int): x = int(input("enter your number: ")) enter your number: 39 X 39 В этом примере код попытается преобразовать любой ввод в целое число (int), что создаст исключение, если преобразование невозможно. Функция input записывает запрос в стандартный вывод и считывает данные из стандартного ввода. Для получения низкоуровневого доступа к этим потокам и к стандартным ошибкам используется модуль sys, имеющий атрибуты sys.stdin, sys.stdout и sys.stderr. Эти атрибуты могут рассматриваться как специа­лизированные файловые объекты. Для sys.stdin доступны методы read, readline и readlines. Для sys.stdout и sys.stderr можно задействовать как стандартную функцию print, так и методы write и writelines, которые работают так же, как и с другими файловыми объектами: import sys print("Write to the standard output.") Write to the standard output. sys.stdout.write("Write to the standard output.\n")
   13.6. Консольный ввод-вывод и перенаправление Write to the standard output. 30 315 sys.stdout.write возвращает количество символов, записанных в окне командной строки, но не в Colaboratory В следующем примере иллюстрируется применение sys.stdin для получения входных данных. Следует учитывать, что этот метод не работает в среде Colaboratory, поскольку она требует полного контроля над стандартными потоками ввода-вывода. Обходное решение — сохранить код как файл Python и запустить его напрямую с префиксом !, как описано в разделе 11.1.1 главы 11: s = sys.stdin.readline() An input line S 'An input line\n' Обычно нет особой необходимости использовать стандартный ввод и вывод, однако в некоторых особых случаях это может пригодиться, особенно если требуется перенаправлять ввод-вывод в файлы или из файлов. Можно перенаправить стандартный ввод на чтение из файла. Аналогично стандартный вывод или стандартная ошибка могут быть настроены на запись в файлы, а затем программным путем восстановлены до исходных значений с помощью sys.__stdin__, sys.__stdout__ и sys.__stderr__: Открывает файл import sys Устанавливает файл outfile.txt f = open("outfile.txt", 'w') в качестве нового sys.stdout sys.stdout = f sys.stdout.writelines(["A first line.\n", Пишет в outfile.txt "A second line.\n"]) (перенаправленный sys.stdout) print("A line from the print function") sys.stdout = sys.__stdout__ f.close() Сбрасывает sys.stdout на значение по умолчанию После переназначения sys.stdout на файл outfile.txt вызов sys.stdout.writelines() запишет в файл две строки: «A first line» и «A second line». Аналогично, функция print также будет записывать данные в этот файл, поэтому в конце примера кода, приведенного выше, файл outfile.txt будет содержать третью строку «A line from the print function»: A first line. A second line. A line from the print function Предупреждение относительно Colaboratory и Jupyter Поскольку Colaboratory (и Jupyter в целом) строже контролирует стандартный ввод и вывод, применение sys.__stdout__ (а также sys.__stdin__ и sys.__stderr__), показанное ранее, не будет работать так, как указано в ячейке Colaboratory. Вместо
   316 Глава 13. Чтение и запись файлов этого необходимо сохранить исходное значение sys.stdout перед его изменением, а затем восстановить его после использования перенаправленного значения: import sys Сохраняет исходный sys.stdout в old_sys_stdout f = open("outfile.txt", 'w') Перенаправляет old_sys_stdout = sys.stdout sys.stdout на файл f sys.stdout = f sys.stdout.writelines(["A first line.\n", "A second line.\n"]) print("A line from the print function") sys.stdout = old_sys_stdout Восстанавливает значение sys.stdout f.close() из old_sys_stdout ! cat outfile.txt Функция print может быть перенаправлена на любой файл без изменения стандартного вывода sys.stdout при помощи ее параметра file: import sys f = open("outfile.txt", 'w') Открывает файл print("A first line.\n", "A second line.\n", file=f) f.close() Пишет в файл Этот код запишет в файл две строки: A first line. A second line. Перенаправление вывода функции print представляет практическую ценность, поскольку у нее простой и привычный синтаксис. При перенаправлении стандартного вывода вы по-прежнему получаете системные приглашения и трассировку ошибок, но никакая другая информация не отображается. Обычно этот метод используется при запуске скрипта или программы. Однако иногда во время интерактивной сессии необходимо временно перенаправить стандартный вывод в файл, чтобы сохранить то, что в противном случае может исчезнуть с экрана. Приведенный ниже небольшой модуль реализует набор функций для такой задачи (листинг 13.1). Листинг 13.1. Файл mio.py """mio: module, (содержит функции capture_output, restore_output, print_file и clear_file )""" import sys _file_object = None def capture_output(file="capture_file.txt"): """capture_output(file='capture_file.txt'): перенаправляет стандартный вывод в 'file'.""" global _file_object print("output will be sent to file: {0}".format(file)) print("restore to normal by calling 'mio.restore_output()'") _file_object = open(file, 'w') sys.stdout = _file_object def restore_output():
   13.7. Обработка структурированных двоичных данных 317 """restore_output(): приводит стандартный вывод обратно к значению по умолчанию (также закрывает файл захвата)""" global _file_object sys.stdout = sys.__stdout__ _file_object.close() print("standard output has been restored back to normal") def print_file(file="capture_file.txt"): """print_file(file="capture_file.txt"): выводит содержимое заданного файла в стандартный вывод""" f = open(file, 'r') print(f.read()) f.close() def clear_file(file="capture_file.txt"): """clear_file(file="capture_file.txt"): очищает содержимое заданного файла""" f = open(file, 'w') f.close() Здесь функция capture_output() перенаправляет стандартный вывод в файл (по умолчанию capture_file.txt). Функция restore_output() приводит стандартный вывод к значению по умолчанию. Если функция capture_output не была выполнена, print_file() выводит этот файл в стандартный вывод, а clear_file() очищает его текущее содержимое. Попробуйте сами: перенаправление вывода Напишите код, использующий модуль mio.py из листинга 13.1, для записи всего вывода скрипта в файл с именем myfile.txt, восстановления стандартного вывода на экран и последующего вывода содержимого этого файла на экран. Примечание: это решение не будет работать в Colaboratory или в Jupyter Notebooks, если только вы не сохраните код листинга mio.py и ваш тестовый код в файлы Python, а затем не выполните ваш файл с использованием префикса !. Блокнот кода содержит ячейку, которая запишет mio.py в файл Python. 13.7. Обработка структурированных двоичных данных с помощью модуля struct В целом при работе с собственными файлами вам, скорее всего, не понадобится считывать или записывать двоичные данные в Python. Для простейшего хранения данных удобнее применять ввод-вывод текста или байтов. Для более сложных приложений Python предоставляет возможность простого чтения или записи произвольных объектов Python (пиклинг, описанный в разделе 13.8). Такой вариант куда лучше застрахован от ошибок, чем прямая запись и чтение собственных двоичных данных, поэтому я настоятельно рекомендую использовать именно его. Однако существует по крайней мере один случай, в котором вам пригодится умение считывать или записывать двоичные данные, а именно при работе
   318 Глава 13. Чтение и запись файлов с файлами, которые созданы и используются в других программах. В этом разделе мы обсудим, как сделать это при помощи модуля struct. Более подробное описание можно найти в справочной документации к языку Python. Как вам уже известно, Python поддерживает явный двоичный ввод и вывод с использованием байтовых объектов вместо строковых при открытии файла в двоичном режиме. Но поскольку многие двоичные файлы зависят от конкретной структуры парсинга значений, написание собственного кода для чтения и правильного разделения таких файлов на переменные часто оказывается слишком трудоемким. Вместо этого можно использовать стандартный модуль struct, который позволяет обрабатывать эти данные как отформатированные последовательности байтов с определенным значением. Допустим, вы хотите читать и писать в двоичный файл с именем data, содержащий набор записей, сгенерированных программой на языке C. Каждая запись состоит из короткого целого числа C (типа short int), числа с плавающей точкой двойной точности C (типа double) и последовательности из семи символов, которая должна интерпретироваться как строковый объект, содержащий семь символов. Требуется сохранить эти данные в список кортежей Python, где каждый кортеж содержит объекты следующих типов: целое число, число с плавающей точкой и строка. Прежде всего необходимо определить строку форматирования, понятную модулю struct, которая сообщает модулю, как упакованы данные в одной из ваших записей. Строка форматирования использует символы, понятные модулю struct, для обозначения того, какой тип данных ожидается в той или иной позиции записи. Например, символ 'h' указывает на наличие одного короткого целого числа С, а символ 'd' — на наличие числа C с плавающей точкой двойной точности. Как нетрудно догадаться, 's' обозначает строку. Любому из них может предшествовать целое число, указывающее количество значений; в данном случае '7s' обозначает строку из семи символов. Следовательно, для наших записей правильная строка форматирования имеет вид 'hd7s'. Модуль struct поддерживает широкий диапазон числовых, символьных и строковых форматов. Полный обзор его возможностей представлен в документации к стандартной библиотеке Python. Как вы, вероятно, уже догадались, модуль struct также может принимать значения языка Python и преобразовывать их в упакованные последовательности байтов. Такое преобразование выполняется функцией struct.pack(), которая принимает строку форматирования в качестве первого аргумента, за которым следует число аргументов, удовлетворяющее требованиям строки форматирования. Для создания двоичной записи и ее сохранения в файл data можно написать примерно такой код: import struct record_format = 'hd7s' data_record = struct.pack(record_format, 42, 3.14, b'goodbye') data_file = open("data", "wb") data_file.write(data_record)
   13.7. Обработка структурированных двоичных данных 319 data_record b'*\x00\x00\x00\x00\x00\x00\x00\x1f\x85\xebQ\xb8\x1e\t@goodbye' В результате работы кода значения преобразуются и упаковываются в заданном двоичном формате. Перед началом чтения записей из файла требуется установить размер буфера чтения, то есть определить, сколько байтов нужно считывать за один раз. К счастью, в составе модуля struct предусмотрена функция calcsize, которая принимает строку форматирования в качестве аргумента и возвращает количество байтов, необходимых для хранения данных в указанном формате. Для чтения каждой записи используется метод read, описанный ранее в этой главе. Затем функция struct.unpack удобно возвращает кортеж значений, выполняя парсинг прочитанной записи в соответствии с вашей строкой форматирования. Функция struct.unpack почти обратна функции struct.pack. Почти связано с тем, что struct.unpack возвращает кортеж значений Python, а struct.pack не получает кортеж таких значений; вместо этого в первом аргументе передается строка форматирования, а затем дополнительные аргументы в достаточном для нее количестве. Программа для считывания двоичного файла данных очень проста: import struct record_format = 'hd7s' record_size = struct.calcsize(record_format) result_list = [] with open("data", 'rb') as input: Считывает одну запись while True: record = input.read(record_size) Проверяет, пуста ли запись if not record: break result_list.append(struct.unpack(record_format, record)) print(result_list) Распаковывает запись в кортеж; добавляет к результатам [(42, 3.14, b'goodbye')]] Если запись пуста, значит, достигнут конец файла, поэтому цикл завершается. Обратите внимание, что проверка файла на корректность не выполняется; если размер последней записи не соответствует ожидаемому, функция struct.unpack генерирует ошибку. Возможности модуля struct этим не ограничиваются; в строку форматирования можно вставлять другие специальные символы, означающие, что данные должны читаться или записываться с использованием прямого (big-endian), обратного (little-endian) или машинно-зависимого (machine-native-endian) формата порядка байтов (по умолчанию используется машинно-зависимый порядок), а также для того, чтобы задать размер таких типов объектов, как короткое целое
   320 Глава 13. Чтение и запись файлов число (short integer) языка C, либо как машинно-зависимый (по умолчанию), либо как стандартный размер для C. Если вам понадобятся эти возможности, полезно знать, что они существуют. Как всегда, подробности вы легко найдете в документации к стандартной библиотеке Python. Быстрая проверка: модуль struct Предложите несколько вариантов использования, в которых модуль struct был бы полезен как для чтения, так и для записи двоичных данных. 13.8. Пиклинг объектов в файлы Python позволяет записывать в файл любую структуру данных, считывать ее из файла или воссоздавать с помощью всего нескольких команд. Такая возможность, называемая сериализацией (или пиклингом в Python), может показаться несколько необычной, однако она бывает весьма полезной, поскольку избавляет от написания множества страниц кода только для того, чтобы сохранить состоя­ ние программы в файл (а также от написания аналогичных объемов кода лишь для того, чтобы потом считать это состояние обратно). В Python эта функциональность реализована в модуле pickle . Механизм пиклинга обладает высокой мощностью при сравнительной простоте использования. Как сказано в документации Python: «Пиклинг (pickling) — это процесс преобразования иерархии объектов Python в поток байтов, а распиклинг (unpickling) — обратная операция, при которой поток байтов (из двоичного файла или байтового объекта) преобразуется обратно в иерархию объектов». Таким образом, пиклинг — это способ преобразования объектов Python в текущей сессии Python в последовательность байтов, пригодную для хранения и/или перемещения. Допустим, полное состояние программы содержится в трех объектах, на которые ссылаются переменные: a, b и c. Сохранить это состояние в файл с именем state можно следующим образом: import pickle a = 42 b = 3.14 c = "test" file = open("state", 'wb') pickle.dump(a, file) pickle.dump(b, file) pickle.dump(c, file) file.close() Не важно, какими именно были значения объектов, присвоенных переменным a, b и c. Их содержимое может быть любым — от обычных чисел до сложных списков словарей, содержащих экземпляры пользовательских классов. Метод pickle.dump сохраняет все.
   13.8. Пиклинг объектов в файлы 321 Теперь, чтобы восстановить эти данные при последующем запуске программы, достаточно выполнить следующий код: import pickle file = open("state", 'rb') a = pickle.load(file) b = pickle.load(file) c = pickle.load(file) file.close() print(f"{a=} {b=} {c=}") a=42 b=3.14 c='test' Вызов метода pickle.load восстанавливает любые данные, которые ранее хранились в объектах, присвоенных переменным a, b и c. Модуль pickle позволяет сохранить таким образом практически любые данные. Он работает со списками, кортежами, числами, строками, словарями и практически любыми структурами, созданными на основе этих типов объектов, в том числе и со всеми экземплярами классов. Он также корректно работает с совместно используемыми объектами, циклическими ссылками и другими сложными структурами памяти, сохраняя совместно используемые объекты только в одном экземпляре и восстанавливая их как совместно используемые объекты, а не как идентичные копии. С другой стороны, объекты кода (в которых Python хранит скомпилированный байт-код) и системные ресурсы (например, файлы или сокеты) не подлежат пиклингу. Как правило, сохранять полное состояние программы при помощи метода pickle не нужно. К примеру, многие приложения позволяют одновременно открыть несколько документов. Если сохранить полное состояние программы, вы фактически сохраните все открытые документы в одном файле. Простой и эффективный способ сохранения только тех данных, которые действительно необходимы, заключается в написании функции сохранения, которая заносит все нужные данные в словарь, а затем задействовать pickle для сохранения этого словаря. Затем можно использовать дополняющую функцию восстановления, которая считывает словарь обратно (снова с помощью pickle), а значения из словаря присваиваются соответствующим переменным программы. Одно из преимуществ этого приема заключается в том, что он исключает возможность считывания значений в неправильном порядке — то есть в порядке, отличном от того, в котором эти значения были сохранены. Применив этот прием к предыдущему примеру, вы получите код, который выглядит примерно так: import pickle def save_data(data_dict): with open("state", 'wb') as file: pickle.dump(data_dict, file) def restore_data(): with open("state", 'rb') as file: data_dict = pickle.load(file)
   322 Глава 13. Чтение и запись файлов return data_dict if __name__ == '__main__': data_dict = {'a': 42, 'b': 3.14, 'c': "test" } save_data(data_dict) restored_data = restore_data() print(restored_data) {'a': 42, 'b': 3.14, 'c': 'test'} Этот пример несколько искусственный, поскольку в реальности вам вряд ли часто потребуется сохранять состояние переменных верхнего уровня в интер­ активном режиме. Однако если вам нужно сохранить значения нескольких переменных или состояние нескольких объектов и при этом нет проблем с безопасностью, пиклинг представляет собой эффективное решение. Практическим применением пиклинга может стать расширенная версия примера с кэшированием из главы 7. В той главе рассматривалась функция с тремя аргументами, производившая затратные по времени вычисления. В ходе выполнения программы функция не раз вызывалась с совпадавшими наборами аргументов. Кэширование результатов в словаре с аргументами в качестве ключей позволило добиться значительного повышения быстродействия. Однако при этом сессии программы запускались по несколько раз в течение дней, недель и месяцев. А следовательно, проведение пиклинга кэша позволит избежать того, чтобы начинать кэширование заново с каждой сессией. В листинге 13.2 приведена упрощенная версия модуля, применимого для этой цели. Код в листинге 13.2 предполагает, что кэш-файл уже существует. Для первичной инициализации кэш-файла следует использовать следующий код: import pickle file = open("solecache",'wb') pickle.dump({}, file) file.close() Листинг 13.2. Файл sole.py """модуль sole: содержит функции sole, save, show""" import pickle _sole_mem_cache_d = {} При загрузке модуля выполняется _sole_disk_file_s = "solecache" код инициализации file = open(_sole_disk_file_s, 'rb') _sole_mem_cache_d = pickle.load(file) file.close() Публичные функции def sole(m, n, t): """sole(m, n, t): выполняет вычисления sole при помощи кэша.""" global _sole_mem_cache_d if (m, n, t) in _sole_mem_cache_d: return _sole_mem_cache_d[(m, n, t)] else: # . . . выполняет очень ресурсоемкие вычисления . . . Преобразует в строку для наглядности result = f"{m=}, {n=}, {t=}" _sole_mem_cache_d[(m, n, t)] = result return result
_sole_mem_cache_d = pickle.load(file) file.close() Публичные функции    def sole(m, n, t): """sole(m, n, t): выполняет вычисления sole при помощи кэша.""" global _sole_mem_cache_d 323 13.8. Пиклинг объектов в файлы if (m, n, t) in _sole_mem_cache_d: return _sole_mem_cache_d[(m, n, t)] else: # . . . выполняет очень ресурсоемкие вычисления . . . Преобразует в строку для наглядности result = f"{m=}, {n=}, {t=}" _sole_mem_cache_d[(m, n, t)] = result return result def save(): """save(): сохраняет обновленный кэш на диск.""" global _sole_mem_cache_d, _sole_disk_file_s file = open(_sole_disk_file_s, 'wb') pickle.dump(_sole_mem_cache_d, file) file.close() def show(): """show(): выводит кэш""" global _sole_mem_cache_d print(_sole_mem_cache_d) С программой можно поэкспериментировать следующим образом: sole(1, 2, 3) sole(11, 22, 33) show() save() {(1, 2, 3): 'm=1, n=2, t=3', (11, 22, 33): 'm=11, n=22, t=33'} Разумеется, вам также потребуется заменить комментарий # . . . выполняет очень ресурсоемкие вычисления фактическими вычислительными операциями, а не просто преобразовать объекты, на которые ссылаются переменные, в строковые. Обратите внимание, что в рабочем коде в этой ситуации, скорее всего, следовало бы использовать абсолютный путь к кэш-файлу, да и само имя файла, вероятно, не стоит жестко прописывать в коде. Кроме того, в данном примере не предусмотрена конкурентность: если два пользователя запустят перекрывающиеся сессии, сохранятся только изменения, внесенные последним из них. Если эта ситуация критична, можно существенно ограничить окно перекрытия, используя метод обновления словаря в функции save. 13.8.1. Когда лучше обойтись без пиклинга Несмотря на то что применение сериализованного объекта в предыдущем варианте имеет некоторый смысл, не стоит забывать о ряде недостатков пиклинга. Пиклинг не отличается быстротой и эффективностью в экономии памяти. Даже сохранение сериализованных объектов в формате JSON выполняется быстрее и занимает меньше места на диске. Пиклинг не является безопасным — загрузка сериализованного объекта, содержащего вредоносный код, может привести к исполнению произвольных команд на вашем компьютере. Поэтому следует избегать применения пиклинга при существовании малейшей угрозы несанкционированного доступа и модификации сериализованных данных.
   324 Глава 13. Чтение и запись файлов Быстрая проверка: пиклинг Подумайте, будет ли пиклинг хорошим решением в следующих случаях. • Сохранение ряда переменных состояния между запусками программного кода. • Ведение списка рекордов в игре. • Хранение имен пользователей и паролей. • Хранение большого словаря английских терминов. 13.9. Сохранение объектов в модуле shelve Эта тема относится к числу углубленных, хотя особо сложной ее не назовешь. Объект shelve можно представить в виде словаря, хранящего данные не в памяти, а в файле на диске; таким образом, вы пользуетесь всеми удобствами обращения по ключу, но без ограничений объема доступной оперативной памяти. Пожалуй, этот раздел представляет наибольший интерес для людей, работа которых связана с хранением или доступом к фрагментам данных больших файлов, поскольку модуль Python shelve решает именно эту задачу: он позволяет читать и записывать фрагменты данных в больших файлах без чтения или записи всего файла целиком. Для приложений, часто обращающихся к большим файлам (таких, как приложения баз данных), экономия времени может оказаться значительной. Модуль shelve несложен, равно как и модуль pickle (используемый модулем shelve). В данном разделе мы рассмотрим этот модуль на примере адресной книги. Обычно объем адресных книг невелик, что позволяет считать весь файл адресов при запуске приложения и записать его при завершении работы. Но если у вас очень много друзей, а адресная книга слишком велика для такого решения, будет лучше воспользоваться модулем shelve и не беспокоиться об объеме данных. Предположим, что каждая запись вашей адресной книги представляет собой кортеж из трех элементов с именем, номером телефона и адресом вашего знакомого. Каждая запись индексируется по фамилии человека, к которому она относится. Такая схема настолько проста, что приложение может быть реализовано в интерактивной сессии Python. Сначала импортируйте модуль shelve и откройте адресную книгу. Метод shelve. open создает файл адресной книги, если его не существует: import shelve book = shelve.open("addresses") Теперь добавьте пару записей. Учтите, что мы работаем с объектом, возвращае­ мым shelve.open, как со словарем (хотя это словарь, ключами которого могут быть только строковые объекты): book['flintstone'] = ('fred', '555-1234', '1233 Bedrock Place') book['rubble'] = ('barney', '555-4321', '1235 Bedrock Place') Наконец, закройте файл и завершите сессию:
   13.9. Сохранение объектов в модуле shelve 325 book.close() Что теперь? Снова запустите Python из того же каталога и откройте ту же адресную книгу: import shelve book = shelve.open("addresses") Но теперь вместо того, чтобы вводить данные, убедитесь в том, что введенные ранее данные сохранились: book['flintstone'] ('fred', '555-1234', '1233 Bedrock Place') Файл адресной книги, созданный вызовом shelve.open в первой интерактивной сессии, сработал как постоянный словарь. Введенные данные были сохранены на диске, даже если операции записи явно не выполнялись. Именно это и делает модуль shelve. В более широком смысле shelve.open возвращает объект shelf, который поддерживает базовые операции словарей, присваивание или поиск по ключу, использование методов del, in и keys. Однако, в отличие от обычных словарей, объекты shelf хранят свои данные на диске, а не в памяти. К сожалению, у объектов shelf есть одно существенное ограничение по сравнению со словарями: они могут использовать в качестве ключей только строковые объекты (в отличие от широкого спектра типов данных, допустимых в качестве ключей словаря). Важно понимать преимущества объектов shelf перед словарями при работе с большими наборами данных. Метод shelve.open открывает доступ к файлу; он не загружает весь объект shelf в память. Обращения к файлу происходят лишь по мере необходимости (обычно при поиске элемента), а файл имеет такую структуру, что операции поиска выполняются очень быстро. Даже если файл данных очень велик, для нахождения нужного объекта достаточно пары обращений к диску; это может повысить эффективность вашей программы в целом ряде отношений. Программа может запускаться быстрее, поскольку ей не нужно целиком загружать в память потенциально большие файлы. Кроме того, программа может работать быстрее, так как для нее доступен больший объем памяти (а следовательно, сокращается количество кода, выгружаемого в виртуальную память). Появляется возможность работы с наборами данных, которые иначе просто не уместились бы в памяти. Применение модуля shelve сопряжено с некоторыми ограничениями. Как отмечалось ранее, ключи объектов shelf могут быть только строковыми объектами, но по ним в качестве значений можно хранить любые объекты Python, поддерживающие пиклинг. Хотя поиск по ключам работает достаточно быстро для файлового хранилища, учтите, что добавление новых значений может быть ресурсоемкой операцией — частое обновление большого количества ключей приведет к резкому снижению производительности. Кроме того, объекты shelf не подходят для многопользовательских баз данных, поскольку не реализуют
   326 Глава 13. Чтение и запись файлов механизмов управления параллельным доступом. И не забывайте закрывать объект shelf по завершении работы — в ряде случаев это необходимо для записи внесенных изменений (добавлений или удалений) на диск. В текущем виде пример кэша из листинга 13.1 прекрасно подходит для обработки с помощью модуля shelve. К примеру, вам не придется полагаться на то, что пользователь явным образом сохранит свою работу на диске. Единственная возможная проблема — отсутствие полного низкоуровневого контроля при запи­си обратно в файл. Быстрая проверка: объект shelf Использование объекта shelf очень напоминает работу со словарем. Чем же отличаются объекты shelf? Каких недостатков следует ожидать при использовании объекта shelf? 13.10. Практическая работа: последние доработки утилиты wc В утилите wc имеется еще одна возможность, которая пока отсутствует в текущей версии Python, — поддержка перенаправления ввода, что особенно распространено в средах командной строки в Linux/Unix. Нужно сделать рефакторинг нашего кода так, чтобы он поддерживал перенаправление ввода. То есть при наличии файла в качестве аргумента командной строки утилита должна читать и обрабатывать этот файл, а если файл не указан, она должна читать и обрабатывать данные из стандартного ввода (stdin). Пример с именем файла: ! python wc_redirect.py wc_redirect.py 62 182 2058 wc_redirect.py Файл указан в качестве аргумента; обработка выполняется в обычном режиме Пример с перенаправлением (без имени файла): ! python wc_redirect.py < wc_redirect.py 62 182 2058 Файловый аргумент отсутствует, но ввод посредством < перенаправляется из файла СОВЕТ При использовании модуля argparse нужно предусмотреть обработку как наличия, так и отсутствия аргумента с именем входного файла. Это обусловлено тем, что в случае поступления ввода через перенаправление argparse не получит имени файла. Для решения этой проблемы изучите документацию argparse по параметру nargs (доступно по ссылке https://mng.bz/9YEr), особенно опции с символом ‘?’. 13.10.1. Решение задачи при помощи кода, сгенерированного ИИ Нам предстоит окончательно доработать нашу версию утилиты wc, что приблизит ее функциональность к почти полному соответствию встроенной версии
   13.10. Практическая работа: последние доработки утилиты wc 327 Unix. В рамках данной задачи необходимо, чтобы ИИ-ассистент, основываясь на последней версии кода из главы 11, реализовал поддержку перенаправления ввода из стандартного ввода и перенаправления вывода в стандартный вывод. В изначальной постановке задачи требовалось реализовать как перенаправление ввода, так и возможность раздельного подсчета символов в текстовом режиме и байтов в двоичных файлах (с использованием параметра -c для байтов и -m для символов). Эта необходимость обусловлена тем, что файлы, содержащие символы Unicode, могут фактически содержать больше байтов в двоичном режиме, чем символов в текстовом режиме. Однако интеграция обеих этих функций значительно усложнила задачу для программиста, а для доступных в середине 2024 года версий Copilot и Colaboratory оказалась и вовсе невыполнимой. Сгенерированный этими средствами ИИ код либо требовал ручной доработки для запуска, либо содержал ошибки в обработке различий между текстовым и двоичным режимами, либо имел обе эти проблемы. Из-за такого высокого уровня сложности я упростила задачу, однако если вы хотите проверить свои навыки программирования, то можете попробовать самостоятельно добавить эту функцию и «уговорить» чат-бота ИИ создать рабочее решение. 13.10.2. Решения и обсуждение При обновлении нашей прежней утилиты wc для обработки стандартного ввода нужно решить две задачи. Как упоминалось в совете к данной практической работе, первая задача заключается в том, что скрипт должен уметь обрабатывать случаи, когда имя файла не указывается в качестве аргумента. Вторая задача заключается в том, что код должен считывать данные из sys.stdin при отсутствии аргумента с именем файла. Реализация этого функционала без избыточного дублирования кода требует тщательного проектирования, однако некоторые параметры аргументов по умолчанию в модуле argparse могут существенно упростить эту задачу. Наконец, если вы используете Colaboratory (или другую среду Jupyter Notebooks), то следует помнить: чтобы работать со скриптом wc и перенаправлением ввода-вывода, необходимо сохранить программный код в файл с расширением .py и запускать через интерпретатор Python с префиксом !. В этом случае перенаправление будет работать как положено. Программное решение, созданное человеком В моей версии используется аргумент FileType из модуля argparse, что гарантирует автоматическое открытие файла при его указании в параметрах командной строки. Такой подход позволяет унифицировать обработку как файловых объектов, так и потока sys.stdin (который можно рассматривать как постоянно открытый файл), так как часть кода, отвечающая за чтение, не открывает файл и работает одинаково с обоими источниками входных данных:
328    Глава 13. Чтение и запись файлов #!/usr/bin/env python3 # Файл: word_count_program.py """ Считывает файл и возвращает число строк, слов, и символов – аналогично утилите wc в UNIX """ import sys import argparse def main(): # инициализация счетчиков line_count = 0 word_count = 0 char_count = 0 byte_count = 0 longest_line = 0 parser = argparse.ArgumentParser(usage=__doc__) parser.add_argument("-c", "--chars", action="store_true", dest="chars", default=False, help="отобразить количество символов") parser.add_argument("-w", "--words", action="store_true", dest="words", default=False, help="отобразить количество слов") parser.add_argument("-l", "--lines", action="store_true", dest="lines", default=False, help="отобразить количество строк") parser.add_argument("-L", "--longest", action="store_true", dest="longest", default=False, help="отобразить наибольшую длину строки") parser.add_argument("file", nargs='?', type=argparse.FileType('r'), argparse.FileType(‘r’) default=sys.stdin, откроет для чтения файл help="читать данные из этого файла") с указанным именем args = parser.parse_args() with args.file as infile: for line in infile.readlines(): line_count += 1 char_count += len(line) words = line.split() word_count += len(words) if len(line) > longest_line: longest_line = len(line) Нет необходимости открывать файл, поскольку он открывается парсером Если файл не назван, используется стандартный ввод stdin в качестве файла default_args = any([getattr(args, _) for _ in ('chars', 'words', 'lines', 'longest')]) if not default_args: args.chars = args.lines = args.words = True if args.lines: print(f"{line_count:3}", end=" ") if args.words: print(f"{word_count:4}", end=" ") if args.chars: print(f"{char_count:4}", end=" ") if args.longest:
   13.10. Практическая работа: последние доработки утилиты wc print(f'{longest_line}', end=" ") if args.file.name != '<stdin>': print(f'{args.file.name}', end=" ") print() 329 Не выводить имя файла, когда используется стандартный ввод stdin if __name__ == '__main__': main() Основные изменения в этой версии заключаются в добавлении аргументов к парсеру (анализатору аргументов). Модуль argparse предлагает тип аргумента FileType, который можно использовать для автоматического открытия файла (в данном случае — для чтения в текстовом режиме, задаваемом параметром 'r'). Опция nargs регулирует количество необходмых аргументов. В данном случае символ ? указывает, что парсер ожидает ноль или один аргумент с именем файла. Если получено ноль аргументов с именем файла, парсер использует в качестве файла значение по умолчанию — стандартный ввод sys.stdin. Хотя парсер берет на себя большую часть работы, требуется внести две корректировки в остальную часть кода. Во-первых, больше не нужно открывать файл, поскольку это сделает парсер (и sys.stdin открывать не нужно тоже) и мы можем просто использовать файловый объект из парсера при обработке файла. Во-вторых, в случае отсутствия аргумента файла следует исключить вывод имени стандартного ввода (‘<stdin>’), поэтому перед выводом имени файла следует проверить его наличие. Программные решения, сгенерированные ИИ Инструменту Copilot был дан следующий промпт: Сделай рефакторинг текущего файла wc_copilot.py так, чтобы скрипт поддерживал перенаправление ввода-вывода. Вывод должен быть аналогичен выводу утилиты wc. План Copilot включал следующие шаги. 1. Импортировать модуль sys. 2. Изменить функцию count_stats так, чтобы она принимала файловый объект вместо пути к файлу. 3. Изменить функцию count_stats для чтения файла в байтовом режиме. 4. Настроить argparse.ArgumentParser в основной функции main для приема файлового объекта вместо пути к файлу. 5. Если файл не указан в качестве аргумента, необходимо считывать данные из стандартного ввода при помощи sys.stdin. 6. Перенаправление вывода будет автоматически обработано оболочкой. 7. Привести формат вывода в соответствие с утилитой wc. Исходя из этого плана, Copilot сгенерировал следующий программный код, очень похожий на решение, написанное программистом, за исключением двух моментов: он не выводит имя файла и не обрабатывает вариант по умолчанию
   330 Глава 13. Чтение и запись файлов (когда отсутствие ключей эквивалентно передаче флагов -l -w -c для вывода количества строк, слов и символов). # wc_copilot.py import argparse import sys def count_stats(file): lines = file.readlines() line_count = len(lines) word_count = sum(len(line.split()) for line in lines) char_count = sum(len(line) for line in lines) max_line_length = max(len(line) for line in lines) return line_count, word_count, char_count, max_line_length def main(): parser = argparse.ArgumentParser(description='Count lines, words, and characters in a file.') parser.add_argument('file', nargs='?', type=argparse.FileType('r'), default=sys.stdin, help='Файл для подсчета') parser.add_argument('-l', '--lines', action='store_true', help='Подсчитать количество строк') parser.add_argument('-w', '--words', action='store_true', help='Подсчитать количество слов') parser.add_argument('-c', '--chars', action='store_true', help='Подсчитать количество символов') parser.add_argument('-L', '--max-line-length', action='store_true', help='Вывести наибольшую длину строки') args = parser.parse_args() line_count, word_count, char_count, max_line_length =\ count_stats(args.file) output = [] if args.lines: output.append(str(line_count)) if args.words: output.append(str(word_count)) if args.chars: output.append(str(char_count)) if args.max_line_length: output.append(str(max_line_length)) print(' '.join(output)) if __name__ == '__main__': main() Как отмечалось ранее, такое решение демонстрирует сходство с версией программиста, но с двумя отличиями: оно считывает весь файл целиком и не в полной мере соответствует заданной спецификации. А именно не выводит имя файла при чтении файла, а при отсутствии параметров не выводит ничего (в отличие от утилиты wc, которая по умолчанию подразумевает использование -l -w -c, если параметры не заданы).
   13.10. Практическая работа: последние доработки утилиты wc 331 В Colaboratory использовался очень похожий промпт: Сделай рефакторинг ячейки выше, чтобы скрипт поддерживал перенаправление вводавывода. Вывод должен быть аналогичен выводу утилиты wc. Colaboratory не отобразил поэтапный план действий, а вместо этого предложил пользователю целых четыре варианта решения на выбор. Все версии работали примерно одинаково, но ни одна из них не выводила имя файла. Я выбрала первый вариант, так как в нем присутствовал ряд интересных отличий в механизмах обработки входных файлов и реализации системы перенаправления ввода-вывода. # Промпт: Сделай рефакторинг ячейки выше, чтобы скрипт поддерживал перенаправление # ввода-вывода. Вывод должен быть аналогичен выводу утилиты wc. import argparse import sys """ Считывает файл и возвращает число строк, слов, и символов – аналогично утилите wc в UNIX """ Допускает любое количество def main(): аргументов в виде имен файлов, parser = argparse.ArgumentParser(usage=__doc__) значение по умолчанию не задано parser.add_aargument("filename", nargs='*') parser.add_argument("-l", "--lines", dest="lines", action='store_true', default=False, help="Показать только количество строк") parser.add_argument("-w", "--words", dest="words", action='store_true', default=False, help="Показать только количество слов") parser.add_argument("-c", "--chars", dest="chars", action='store_true', default=False, help="Показать только количество символов") parser.add_argument("-L", "--longest", dest="longest", action='store_true', default=False, help="Показать наибольшую длину строки") args = parser.parse_args() if not sys.stdin.isatty(): # Считать из стандартного ввода lines = sys.stdin.readlines() else: for filename in args.filename: with open(filename) as infile: lines = infile.readlines() Если sys.stdin не является терминалом (а следовательно, это файл), используется sys.stdin Перебирает в цикле список аргументов, содержащих имена файлов line_count = len(lines) word_count = sum(len(line.split()) for line in lines) char_count = sum(len(line) for line in lines) longest_line_length = max(len(line) for line in lines) if args.lines: print(f"{line_count}") elif args.words:
   332 Глава 13. Чтение и запись файлов print(f"{word_count}") elif args.chars: print(f"{char_count}") elif args.longest: print(f"{longest_line_length}") else: print(f"{line_count} {word_count} {char_count}") if __name__ == '__main__': main() Первое интересное отличие этой версии заключается в том, что можно указывать несколько имен файлов в качестве аргументов, используя опцию «*» для аргумента. Это означает, что имена файлов будут возвращены в виде списка, поэтому для обработки файлов потребуется цикл по этому списку имен. На первый взгляд эта версия больше похожа на утилиту wc, чем другие решения, поскольку wc корректно обрабатывает несколько указанных файлов: $ wc wc_redirect.py wc_redirect_colab.py 61 182 2058 wc_redirect.py 53 162 1853 wc_redirect_colab.py 114 344 3911 total Проблема в том, что, хотя это решение и считывает все файлы, в реальности оно обрабатывает лишь последний файл в списке: $ python wc_redirect_colab.py wc_redirect.py wc_redirect_colab.py 53 162 1853 Для корректной работы цикл должен охватывать весь процесс обработки и вывода данных (что создало бы проблемы с обработкой перенаправлений), а также подсчитывать суммарные значения по каждой категории. Иными словами, следует критически оценивать предлагаемые решения, которые кажутся рабочими, однако неверно определяют масштаб процесса. Второе отличие этой версии заключается в способе использования перенаправления. Она проверяет, является ли stdin терминалом, при помощи if not sys. stdin.isatty():, и если это не так, то предполагается, что ввод должен быть перенаправлен из файла, и осуществляется чтение из stdin. Эта методика работает как с вводом, перенаправлением из файла, так и с выводом другой программы, однако, в отличие от утилиты wc и решения программиста, не поддерживает прямой ввод с консоли и вместо этого вызывает исключение. К примеру, в моем решении, если ввести команду без имени файла, она будет ожидать ввода данных с клавиатуры, завершаемых нажатием <Enter> и Ctrl+D1: 1 Важное техническое примечание: в системах Unix/Linux/macOS символ конца файла (EOF, End of File) передается комбинацией Ctrl+D. Обычно это делается на пустой строке или после нажатия Enter; в системе Windows (CMD/PowerShell) символ конца файла передается комбинацией Ctrl+Z, после чего обязательно нужно нажать Enter. — Примеч. ред.
   Итоги $ python wc_redirect.py this is a line typed in on the terminal 1 9 40 Вывод 333 Команда введена в командную строку без указания имени файла Ввод с клавиатуры с последующим нажатием <Enter> и Ctrl+D Вместо этого решение Colaboratory сгенерирует исключение: $ python wc_redirect_colab.py Traceback (most recent call last): File "/home/naomi/Dropbox/QPB4/code/samples/wc_redirect_colab.py", ➥line 53, in <module> main() File "/home/naomi/Dropbox/QPB4/code/samples/wc_redirect_colab.py", ➥line 36, in main line_count = len(lines) ^^^^^ UnboundLocalError: cannot access local variable 'lines' where it is ➥not associated with a value Происходит это потому, что для использования перенаправления проверяется наличие нетерминального стандартного ввода stdin. Для многих программ это не имеет особого значения, но важно понимать, что сгенерированный код не обеспечивает полного соответствия исходным требованиям. И наконец, из-за использования структуры if-elif-else эта версия выводит данные только для одной опции или (если опции не указаны) значения по умолчанию для строк, слов и символов. Опять же, хотя код кажется рабочим, требуется тщательный анализ логики его выполнения на предмет обработки различных случаев ожидаемым образом. Итоги В языке Python для ввода и вывода файлов используются файловые объекты с методами для открытия, чтения, записи и закрытия файлов. Хотя Python по умолчанию работает с файлами как с текстом, существует также режим двоичных данных, при котором файлы обрабатываются как байтовые объекты. В языке Python реализованы эффективные механизмы перенаправления стандартных потоков ввода, вывода и ошибок для чтения и записи файлов. Объекты Path из модуля pathlib можно применять для работы как с текстовыми, так и с двоичными данными. Помимо чтения и записи текста, модуль struct позволяет работать с данными в структурированных двоичных форматах. Модуль pickle предоставляет простой, безопасный и мощный способ сериализации (пиклинга) и десериализации (распиклинга) структур Python любой степени сложности. Модуль shelve предлагает удобный способ сохранения и загрузки объектов (похожий на работу со словарем), где ключами могут быть только строковые объекты.
14 Исключения В этой главе: 3 Основные сведения об исключениях 3 Обработка исключений в Python 3 Применение ключевого слова with В этой главе рассматриваются исключения — особый механизм языка программирования, предназначенный специально для обработки нестандартных ситуаций в ходе выполнения программы. Хотя основное применение исключений связано с обработкой ошибок во время выполнения программы, но их также можно эффективно использовать и для многих других целей. Python предоставляет исчерпывающий набор встроенных исключений, а также позволяет пользователям определять собственные исключения для решения специфических задач. Концепция использования исключений в качестве механизма обработки ошибок существует уже довольно давно. Однако во многих популярных языках, таких как C и Perl (часто используемых для системного программирования и написания скриптов), исключения не предусмотрены. Даже программисты на C++ (где исключения поддерживаются) зачастую с ними не знакомы. Эта глава не требует от вас предварительного знакомства с исключениями — вместо этого предлагается подробное объяснение материала.
   14.1. Введение в исключения 335 14.1. Введение в исключения В этом разделе вы узнаете об исключениях и способах их использования. Если вы уже знакомы с исключениями, переходите сразу к разделу 14.2 «Исключения в Python». 14.1.1. Общие принципы обработки ошибок и исключений В любой программе в ходе ее выполнения могут возникнуть ошибки. Чтобы продемонстрировать работу с исключениями, возьмем для примера текстовый редактор, который записывает файлы на диск и, следовательно, может столк­ нуться с нехваткой свободного места до того, как будут записаны все данные. Есть несколько способов решения этой проблемы. Решение 1. Оставить все как есть Простейшее решение проблемы дискового пространства — считать, что для любых операций записи в файлы всегда хватает дискового пространства и беспокоиться тут не о чем. К сожалению, такой вариант встречается на практике чаще всего. Для небольших программ, работающих с малыми объемами данных, он еще приемлем, но для критически важных приложений он абсолютно не годится. Решение 2. Каждая функция возвращает статус выполнения (успех или неудача) Следующий уровень сложности в обработке ошибок заключается в осознании их неизбежности и создании методологии их обнаружения и обработки с применением стандартных механизмов языка. Среди множества известных подходов наиболее типичным является метод, при котором каждая функция или процедура возвращает статус своего выполнения, указывающий на успешность или сбой операции, в то время как фактические результаты могут передаваться через параметр по ссылке. Посмотрим, как такое решение может работать в гипотетической программе текстового редактора. Допустим, программа вызывает одну высокоуровневую функцию save_to_file для сохранения текущего документа в файл. Эта функция вызывает подфункции для сохранения в файле разных частей документа: save_text_to_file для сохранения самого текста, save_prefs_to_file для сохранения пользовательских настроек данного документа, save_formats_to_file для сохранения пользовательских форматов данного документа и т. п. Все эти подфункции могут, в свою очередь, вызывать другие подфункции, сохраняющие еще более мелкие фрагменты в файл. На самом нижнем уровне располагаются встроенные системные функции, которые записывают в файл простые данные и сообщают об успехе или сбое операций записи в файл. Код обработки ошибок можно было бы включить в каждую функцию, в которой не исключена ошибка нехватки дискового пространства, однако вряд ли
   336 Глава 14. Исключения это имеет практический смысл. Единственное, что может сделать обработчик ошибки, — вывести диалоговое окно, которое уведомляет пользователя о нехватке места на диске, предлагает удалить какие-нибудь файлы и повторить попытку сохранения. Нет смысла копировать этот код во все части программы, где осуществляется запись на диск. Вместо этого следует поместить один блок кода обработки ошибок в основную функцию записи на диск: save_to_file. К сожалению, чтобы функция save_to_file могла определить, когда вызывать этот код обработки ошибки, каждая вызываемая ею функция, записывающая данные на диск, должна сама проверять наличие ошибок, связанных с нехваткой пространства на диске, и возвращать значение состояния, обозначающее успех или сбой операции записи на диск. Кроме того, функция save_to_file должна явным образом проверять каждый вызов любой функции записи, даже если для нее не имеет значения, в какой именно функции произошел сбой. Код, использующий C-подобный синтаксис, будет выглядеть примерно так: const ERROR = 1; const OK = 0; int save_to_file(filename) { int status; status = save_prefs_to_file(filename); if (status == ERROR) { ...handle the error... } status = save_text_to_file(filename); if (status == ERROR) { ...handle the error... } status = save_formats_to_file(filename); if (status == ERROR) { ...handle the error... } . . . } int save_text_to_file(filename) { int status; status = ...lower-level call to write size of text... if (status == ERROR) { return(ERROR); } status = ...lower-level call to write actual text data... if (status == ERROR) { return(ERROR); } . . . } То же относится к save_prefs_to_file, save_formats_to_file и всем остальным функциям, которые либо сами записывают данные в файл напрямую, либо (тем или иным образом) вызывают функции, выполняющие запись данных в файл.
   14.1. Введение в исключения 337 При такой методологии код для обнаружения и обработки ошибок может занять существенную часть программы, поскольку каждая функция и процедура, содержащая вызовы, которые могут привести к ошибке, должна включать код для проверки на наличие такой ошибки. Часто у программистов не хватает времени и сил, чтобы реализовать полную проверку ошибок по такой схеме; в результате чего программы получаются ненадежными и подверженными сбоям. Решение 3. Механизм исключений Очевидно, что большая часть кода проверки ошибок в программах предыдущего типа в основном повторяется: код проверяет наличие ошибок при каждой попытке записи в файл и при обнаружении ошибки передает сообщение о сбое операции вызывающей процедуре. Сама же ошибка нехватки дискового пространства обрабатывается лишь в одном месте: в высокоуровневой функции save_to_file. Иными словами, наибольшую часть обработки ошибок составляет служебный код, который связывает место возникновения ошибки с местом ее обработки. На самом деле нужно избавиться от этого вспомогательного кода и написать код следующего вида: def save_to_file(filename) try to execute the following block save_text_to_file(filename) save_formats_to_file(filename) save_prefs_to_file(filename) . . . except that, if the disk runs out of space while executing the above block, do this ...handle the error... def save_text_to_file(filename) ...lower-level call to write size of text... ...lower-level call to write actual text data... . . . Здесь код обработки ошибок полностью отделен от низкоуровневых функций; ошибка (если она возникает) генерируется встроенными процедурами записи в файл и передается напрямую в save_to_file, где (как предполагается) ею займется код обработки ошибок. Хотя такой код невозможно написать на C, языки, поддерживающие обработку исключений, реализуют именно такое поведение — и разумеется, Python принадлежит к их числу. Исключения позволяют писать более чистый код и лучше обрабатывать случаи возникновения ошибок. 14.1.2. Более формальное определение исключений Процесс генерации исключения называется возникновением исключения (raising или throwing an exception). В предыдущем примере все исключения генерируются функциями записи на диск, однако создавать исключения могут и любые
   338 Глава 14. Исключения другие функции, в том числе их может явно генерировать ваш собственный код. В предыдущем примере исключение создавалось бы низкоуровневыми функция­ ми записи на диск (не показанными в листинге) в случае нехватки свободного пространства на диске. Реакция на исключение называется перехватом исключения (catching an exception), а код, обрабатывающий исключение, называется кодом обработки исключения (exception-handling code), или просто обработчиком исключения (exception handler). В приведенном выше примере строка except that… перехватывает исключение записи на диск, а код на месте строки …handle the error… стал бы обработчиком исключения для ошибок записи на диск (нехватки дискового пространства). Также в программе могут быть обработчики для других типов исключений и даже другие обработчики для исключений того же типа (но в другом месте кода). 14.1.3. Обработка разных типов исключений От того, какое именно событие вызывает исключение, будет зависеть, какие действия должна будет выполнить программа. Исключение, возникающее при исчерпании пространства на диске, должно обрабатываться совсем не так, как исключение, генерируемое при нехватке оперативной памяти, и оба этих исключения не имеют ничего общего с исключением, возникающим при ошибке деления на ноль. Один из способов обработки разных типов исключений состоит в глобальной записи сообщения об ошибке с указанием причины исключения, при которой все обработчики исключений анализируют это сообщение об ошибке и предпринимают соответствующие действия. На практике же намного более гибким оказался другой метод. Вместо того чтобы задавать единственный тип исключения, Python, как и многие современные языки с поддержкой исключений, устанавливает разные типы исключений для различных видов проблем. В зависимости от произошедшего события могут возникать разные типы исключений. Кроме того, коду, обрабатывающему исключения, можно указать перехватывать только исключения определенных типов. Кстати, эта возможность используется ранее в этой главе, в псевдокоде из решения 3 в том месте, где сказано: except that, if the disk runs out of space . . ., do this. Такой псевдокод указывает, что данный код обработки исключений интересуется только исключениями, связанными с нехваткой дискового пространства. Другие типы исключений не будут перехвачены данным обработчиком исключений. Например, они могут перехватываться обработчиком, который ищет числовые исключения, иначе (если такой обработчик не существует) произойдет аварийное завершение программы с ошибкой. 14.2. Исключения в Python Далее в этой главе речь пойдет о механизмах обработки исключений, встроенных в Python. Весь механизм исключений языка Python строится на основе
   14.2. Исключения в Python 339 объектно-ориентированного подхода, которому он обязан своей гибкостью и расширяемостью. Впрочем, даже если вы не знакомы с объектно-ориентированным программированием (ООП), вы все равно сможете пользоваться исключениями. Исключение представляет собой объект, автоматически генерируемый кодом Python с помощью оператора raise. После создания объекта оператор raise, генерирующий исключение, изменяет нормальную последовательность выполнения программы Python. Вместо того чтобы перейти к следующему после raise оператору (или тому, что вызвало исключение), в текущей цепочке вызовов ищется обработчик, способный обработать возникшее исключение. Если такой обработчик найден, он вызывается и может получить доступ к объекту исключения для сбора дополнительной информации. Если подходящий обработчик исключений не найден, то программа останавливает работу с сообщением об ошибке. Проще просить прощения, чем разрешения Подход к обработке ошибок в Python в целом отличается от подхода в таких языках, как, например, Java. Эти языки стремятся выявить как можно больше потенциальных ошибок заранее, поскольку обработка исключений после их возникновения может быть сложной. Такой стиль описан в первой части этой главы, иногда он обозначается сокращением LBYL (Look Before You Leap, то есть «Осмотрись, прежде чем прыгать» — аналог «Не зная броду, не суйся в воду»). С другой стороны, Python использует исключения для устранения ошибок уже после их возникновения. И хотя такой подход может показаться рискованным, при разумном использовании исключений код получается менее громоздким и лучше читается, а ошибки обрабатываются только в случае их возникновения. Подход Python к обработке ошибок часто описывается сокращением EAFP (Easier to Ask Forgiveness than Permission, то есть «Легче просить прощения, чем разрешения»). 14.2.1. Типы исключений в языке Python Можно генерировать различные типы исключений для того, чтобы отразить фактическую причину ошибки или сообщить о возникшей нестандартной ситуации. В Python 3.13 предусмотрены следующие типы исключений1: BaseException (Базовое исключение) BaseExceptionGroup (Группа базовых исключений) GeneratorExit (Выход из генератора) KeyboardInterrupt (Остановка с клавиатуры) SystemExit (Выход из системы) Exception (Исключение) 1 Поскольку названия стандартных типов исключений и сообщения об ошибках, которые выдает сам интерпретатор Python, всегда на английском языке (хотя некоторые IDE могут иметь плагины для перевода, а также ваши сообщения в коде могут быть на русском), для удобства читателя и лучшего понимания далее после названия типа исключения на английском языке в скобках приведен русский перевод. — Примеч. ред.
   340 Глава 14. Исключения ArithmeticError (Арифметическая ошибка) FloatingPointError (Ошибка плавающей точки) OverflowError (Ошибка переполнения) ZeroDivisionError (Ошибка деления на ноль) AssertionError (Ошибка утверждения) AttributeError (Ошибка атрибута) BufferError (Ошибка буфера) EOFError (Ошибка EOF) ExceptionGroup [BaseExceptionGroup] (Группа исключений [Базовая группа исключений]) ImportError (Ошибка импорта) ModuleNotFoundError (Ошибка "Модуль не найден") LookupError (Ошибка поиска) IndexError (Ошибка индекса) KeyError (Ошибка ключа) MemoryError (Ошибка памяти) NameError (Ошибка имени) UnboundLocalError (Ошибка несвязанной локальной переменной) OSError (Ошибка ОС) BlockingIOError (Ошибка блокировки ввода-вывода) ChildProcessError (Ошибка дочернего процесса) ConnectionError (Ошибка соединения) BrokenPipeError (Ошибка поврежденного канала) ConnectionAbortedError (Ошибка остановки соединения) ConnectionRefusedError (Ошибка отказа в соединении) ConnectionResetError (Ошибка сброса соединения) FileExistsError (Ошибка "Файл существует") FileNotFoundError (Ошибка "Файл не найден") InterruptedError (Ошибка остановки) IsADirectoryError (Ошибка "Является каталогом") NotADirectoryError (Ошибка "Не является каталогом") PermissionError (Ошибка разрешения) ProcessLookupError (Ошибка поиска процесса) TimeoutError (Ошибка превышения времени ожидания) ReferenceError (Ошибка ссылки) RuntimeError (Ошибка времени выполнения) NotImplementedError (Ошибка нереализованной функциональности) PythonFinalizationError (Ошибка финализации Python) RecursionError (Ошибка рекурсии) StopAsyncIteration (Остановка асинхронной итерации) StopIteration (Остановка итерации) SyntaxError (Ошибка синтаксиса) IncompleteInputError (Ошибка неполного ввода) IndentationError (Ошибка отступа) TabError (Ошибка табуляции) SystemError (Ошибка системы) TypeError (Ошибка типа) ValueError (Ошибка значения) UnicodeError (Ошибка Unicode) UnicodeDecodeError (Ошибка декодирования Unicode) UnicodeEncodeError (Ошибка кодирования Unicode) UnicodeTranslateError (Ошибка преобразования Unicode) Warning (Предупреждение) BytesWarning (Предупреждение о байтах) DeprecationWarning (Предупреждение об устаревании)
   14.2. Исключения в Python 341 EncodingWarning (Предупреждение о кодировке) FutureWarning (Предупреждение о будущих изменениях) ImportWarning (Предупреждение об импорте) PendingDeprecationWarning (Предупреждение о предстоящем устаревании) ResourceWarning (Предупреждение о ресурсах) RuntimeWarning (Предупреждение о времени выполнения) SyntaxWarning (Предупреждение о синтаксисе) UnicodeWarning (Предупреждение о Unicode) UserWarning (Пользовательское предупреждение) Набор исключений Python имеет иерархическую структуру, на что указывают отступы в приведенном выше списке исключений. Как упоминалось в предыдущей главе, их алфавитный список можно вывести из модуля __builtins__. Каждый тип исключения представляет собой класс Python, который наследуется от своего родительского типа исключения. Но если вы еще не знакомы с ООП, не беспокойтесь. Например, исключение IndexError является экземпляром класса LookupError, а также (за счет наследования) классов Exception и BaseException. Представленная иерархия является осознанным решением: подавляющее большинство исключений наследуются от класса Exception, при этом строго рекомендуется, чтобы любые заданные пользователем исключения также наследовались от Exception, а не BaseException. Дело в том, что если у вас имеется код, подобный следующему: try: # Что-то выполнить except Exception: # Обработать исключения то можно остановить выполнение кода в блоке try клавишами Ctrl+C без активации кода обработки исключений, поскольку исключение KeyboardInterrupt является подклассом BaseException, а не Exception. Описания значения любого типа исключений можно найти в документации, однако вы быстро освоитесь с самыми распространенными типами исключений в процессе программирования! 14.2.2. Возникновение исключений Исключения генерируются многими встроенными функциями Python: alist = [1, 2, 3] element = alist[7] -------------------------------------------------------------------------IndexError Traceback (most recent call last) <ipython-input-7-189983935c4d> in <cell line: 2>() 1 alist = [1, 2, 3] ----> 2 element = alist[7] IndexError: list index out of range
   342 Глава 14. Исключения Код проверки ошибок, встроенный в Python, обнаруживает, что вторая строка ввода запрашивает элемент по несуществующему индексу списка, и возникает исключение IndexError. Это исключение распространяется вплоть до верхнего уровня (интерактивного интерпретатора Python), который обрабатывает его, выводя сообщение о возникновении исключения. Исключения также можно явно генерировать в собственном коде с помощью оператора raise. Простейшая форма этого оператора выглядит так: raise exception(args) Часть кода exception(args) создает исключение. Аргументы нового исключения обычно содержат данные, позволяющие определить, что произошло, но об этом я расскажу чуть позже. После создания исключения оператор raise передает его вверх по всему стеку вызовов функций Python, которые привели к выполнению данной строки. Исключение передается ближайшему (в контексте стека) обработчику, предназначенному для перехвата исключений соответствующего типа. Если до верхнего уровня программы обработчик не найден, программа завершается с ошибкой или (в интерактивном режиме) выводит сообщение об ошибке в консоль. Попробуйте выполнить следующее: raise IndexError("Just kidding") -------------------------------------------------------------------------IndexError Traceback (most recent call last) <ipython-input-8-680ffd4ba189> in <cell line: 1>() ----> 1 raise IndexError("Just kidding") IndexError: Just kidding Использование raise здесь создает ошибку, которая на первый взгляд похожа на все сообщения об ошибках индексации списка, которые вы видели до сих пор. Однако при ближайшем рассмотрении оказывается, что это не так. На самом деле сообщаемая ошибка не столь серьезна, как в других случаях. Использование строкового аргумента при создании исключения является распространенной практикой. Большинство встроенных исключений Python при передаче первого аргумента считают, что этот аргумент содержит сообщение, которое должно выводиться для объяснения сути произошедшего. Однако это не всегда так, поскольку каждый тип исключения представляет собой отдельный класс, а интерпретация аргументов, ожидаемых при создании нового исключения этого класса, зависит только от определения класса. Кроме того, исключения, создаваемые программистами, часто используются не только для обработки ошибок, а следовательно, они могут и не получать текстового сообщения. 14.2.3. Перехват и обработка исключений Главное в исключениях вовсе не то, что они приводят к остановке программы и выводят сообщение об ошибке. Добиться такого поведения в программе
   14.2. Исключения в Python 343 никогда не представляло существенной сложности. Особенность исключений именно в том, что они не обязательно должны приводить к остановке программы. Определив соответствующие обработчики исключений, вы можете гарантировать, что часто встречающиеся особые ситуации не приведут к сбою программы: возможно, будет выведено сообщение об ошибке для пользователя, выполнен ряд других действий или даже исправлена проблема — и все это без аварийного завершения работы. Базовый синтаксис для перехвата и обработки исключений в Python использует ключевые слова try, except, иногда else и выглядит следующим образом: try: body except exception_type1 as var1: exception_code1 except exception_type2 as var2: exception_code2 except (exception_type3, exception_type4 exception_code3 . except: default_exception_code else: else_body finally: finally_body as var3: Выполнение оператора try начинается с исполнения кода в основном блоке (body) оператора. Если оно проходит успешно (то есть не было возникновения исключений, которые должны быть перехвачены оператором try), выполняется блок else_body, после чего выполнение оператора try завершается. Поскольку присутствует оператор finally, далее исполняется блок finally_body. Если же в блоке try возникает исключение, осуществляется последовательный поиск среди блоков except с целью обнаружения обработчика, тип исключения которого соответствует сгенерированному. При обнаружении соответствующего обработчика except возникшее исключение присваивается переменной, указанной для данного типа исключения, и выполняется блок кода обработки исключения. Если строка вида except exception_type as var: соответствует какому-либо возникшему исключению exc, создается переменная var, и значение exc присваивается ей перед выполнением кода обработки исключения в операторе except. Указание переменной var не является обязательным — можно использовать конструкцию вида except exception_type:, которая тоже будет перехватывать исключения указанного типа, но не будет присваивать их ни одной переменной. Блок except также может содержать кортеж исключений, и тогда будет перехватываться любое из них. Если подходящий блок except не обнаружен, то возникшее исключение не может быть обработано оператором try, и передается дальше по цепочке вызовов в надежде, что какой-либо внешний оператор try сможет его обработать.
   344 Глава 14. Исключения Последний блок except в операторе try может не указывать конкретных типов исключений — в таком случае он будет обрабатывать их все. Этот подход может быть удобен для отладки и очень быстрого прототипирования, но в целом не рекомендуется, поскольку такой обработчик приводит к маскировке всех ошибок в блоке except и может явиться причиной непредсказуемого поведения программы. Блок else оператора try является необязательным и применяется редко. Он выполняется только в том случае, если основной блок оператора try работает без ошибок. Блок finally оператора try также необязателен и выполняется после блоков try, except и else. Если в блоке try возникает исключение, которое не обрабатывается ни одним из блоков except, то это исключение возникает снова после выполнения блока finally. Так как блок finally выполняется всегда, в нем обычно содержится код освобождения ресурсов после обработки исключений, например, для закрытия файлов, сброса переменных и других подобных операций. Попробуйте сами: перехват исключений Напишите код, который получает от пользователя два числа и делит первое число на второе. Проверьте и перехватите исключение, возникающее в том случае, если второе число равно 0 (ZeroDivisionError — Ошибка деления на ноль). 14.2.4. Установление новых исключений Вы можете легко задать собственное исключение. Следующие две строки сделают это за вас: class MyError(Exception): pass Этот код создает класс, который наследует все свойства и методы базового класса Exception. Впрочем, можете не вникать в такие детали, если вас интересует лишь практическая сторона вопроса. Это исключение можно генерировать, перехватывать и обрабатывать точно так же, как и любое другое. Если передать ему один аргумент (и исключение не будет перехвачено и обработано), исключение и его аргумент отобразятся в завершающей части трассировки стека: raise MyError("Some information about what went wrong") -------------------------------------------------------------------------MyError Traceback (most recent call last) <ipython-input-10-04991f64f605> in <cell line: 1>() ----> 1 raise MyError("Some information about what went wrong") MyError: Some information about what went wrong
   14.2. Исключения в Python 345 Конечно, этот аргумент будет доступен и в написанном вами обработчике: try: raise MyError("Some information about what went wrong") except MyError as error: print("Situation:", error) Результат: Situation: Some information about what went wrong При генерации пользовательского исключения с множественными аргументами они передаются в обработчик в виде кортежа, доступного через переменную args ошибки: try: raise MyError("Some information", "my_filename", 3) except MyError as error: print("Situation: {0} with file {1}\n error code: {2}".format( error.args[0], error.args[1], error.args[2])) Результат: Situation: Some information with file my_filename error code: 3 Так как тип исключения является обычным классом Python, который наследует от корневого класса Exception, несложно создать свою подыерархию типов исключений для использования в коде. Не стоит беспокоиться об этой процедуре при первом прочтении книги. Вы всегда сможете вернуться к ней после того, как ознакомитесь с главой 15. Способ создания собственных исключений зависит от ваших конкретных задач. Если вы пишете небольшую программу, которая может генерировать лишь несколько уникальных ошибок или исключений, создайте подкласс основного класса Exception, как было показано выше. Если вы пишете большую многофайловую библиотеку кода для конкретной области (скажем, для метеорологического прогнозирования), возможно, вам стоит установить уникальный класс под названием WeatherLibraryException, а затем задать все уникальные исключения библиотеки как его подклассы. Быстрая проверка: исключения как классы Если MyError наследует от Exception, чем отличаются конструкции except Exception as e и except MyError as e? 14.2.5. Группы исключений В Python 3.11 были добавлены два новых класса исключений: BaseExceptionGroup, наследующий от BaseException, и ExceptionGroup, наследующий от Exception. Цель групп исключений — объединять исключения вместе, чтобы обрабатывать
   346 Глава 14. Исключения несколько исключений одновременно. Хотя обычно исключения возникают по одному, в параллельном программировании несколько исключений могут возникнуть практически одновременно. Для обработки нескольких исключений был добавлен класс ExceptionGroup, позволяющий объединять их в специальное исключение. Обработка групп исключений осуществляется с помощью оператора except* (вместо except). Каждый оператор except* обрабатывает только те исключения из группы, которые соответствуют его типу. Рассмотрим пример: try: raise ExceptionGroup("Multiple exceptions",[TypeError(), Возникают три FileNotFoundError(), ValueError()]) исключения except* TypeError: одновременно Перехватывает message += f" Handling TypeError\n" ошибки типа TypeErrors except* IOError: message += f" Handling IOError\n" Перехватывает ошибки типа IOError, except* ValueError: включая ошибки FileNotFoundError message += f" Handling ValueError\n" finally: Перехватывает ошибки типа ValueErrors print(message) Handling TypeError Handling IOError Handling ValueError Обработка всех трех исключений В данном фрагменте кода генерируется исключение класса ExceptionGroup, к которому относятся три разных исключения: TypeError, FileNotFoundError и ValueError. Операторы except* последовательно осуществляют проверку наличия этих исключений, при этом второй обработчик, настроенный на IOError, перехватывает все дочерние классы (включая FileNotFoundError). Как демонстрирует вывод программы, вместо обработки только первого исключения осуществляется комплексный перехват всех трех исключений. 14.2.6. Отладка программ с помощью оператора assert Оператор assert является специализированной формой оператора raise: assert выражение, аргумент Если выражение дает результат False, а системной переменной __debug__ присваивается True, возникает исключение AssertionError с необязательным аргументом. Переменной __debug__ по умолчанию присвоено значение True, а для ее отключения следует запустить интерпретатор Python с ключом -O или -OO либо присвоить системной переменной PYTHONOPTIMIZE значение True. Необязательный аргумент может использоваться для передачи описания. Если переменной __debug__ присвоено значение False, генератор кода не создает код для операторов assert. Можно использовать операторы assert в качестве инструмента отладки на этапе разработки, сохраняя эти проверки в исходном
   14.2. Исключения в Python 347 коде для использования в будущем без влияния на производительность во время обычного выполнения программы: x = (1, 2, 3) assert len(x) > 5, "len(x) not > 5" -------------------------------------------------------------------------AssertionError Traceback (most recent call last) <ipython-input-13-52a42f33655e> in <cell line: 2>() 1 x = (1, 2, 3) ----> 2 assert len(x) > 5, "len(x) not > 5" AssertionError: len(x) not > 5 Попробуйте сами: оператор assert Составьте короткую программу, которая запрашивает у пользователя число, а затем при помощи оператора assert генерирует исключение, если число равно 0. Протестируйте программу и убедитесь в том, что оператор assert срабатывает; затем отключите его одним из способов, упомянутых в этом разделе. 14.2.7. Иерархия наследования исключений В этом разделе я разовью упомянутую ранее концепцию иерархической организации исключений Python и объясню, как эта структура влияет на их перехват в блоках except. Следующий код: try: body except LookupError as error: exception code except IndexError as error: exception code перехватывает исключения двух типов: IndexError и LookupError. Так случилось, что IndexError является подклассом LookupError. Если основной блок (body) генерирует исключение IndexError, эта ошибка сначала проверяется выражением except LookupError as error:, а поскольку IndexError наследует от LookupError, первая же проверка except срабатывает. Второй блок except никогда не срабатывает, поскольку он полностью перекрывается первым блоком except. И наоборот, если поменять порядок этих двух блоков except, это может иметь практическую ценность: тогда первый блок будет обрабатывать исключения IndexError, а второй — все остальные LookupError, не являющиеся IndexError. 14.2.8. Пример: программа Python для записи на диск В этом разделе мы вернемся к примеру программы текстового редактора, которая должна проверять наличие свободного пространства при записи документа на диск:
   348 Глава 14. Исключения def save_to_file(filename) : try: save_text_to_file(filename) save_formats_to_file(filename) save_prefs_to_file(filename) . . . except IOError: ...handle the error... def save_text_to_file(filename): ...lower-level call to write size of text... ...lower-level call to write actual text data... . . . Обратите внимание, каким малозаметным стал код обработки ошибок. Он обернут вокруг основной последовательности вызовов записи на диск в функции save_to_file. Ни одна из вспомогательных функций записи на диск не нуждается в собственном коде обработки ошибок. Гораздо легче сначала разработать программу, а потом добавить код обработки ошибок. Программисты часто поступают именно так, хотя эта последовательность и не оптимальна. Еще одно любопытное наблюдение: этот код реагирует не конкретно на ошибки переполнения диска; он реагирует на исключения IOError, которые вызываются автоматически встроенными функциями Python всякий раз, когда он не может завершить запрос ввода-вывода по какой-либо причине. Вероятно, для ваших целей этого будет достаточно, однако если вам необходимо выявить условия переполнения диска, можно сделать пару вещей. В основном блоке except может проверить, сколько места доступно на диске. Если его недостаточно, очевидно, проблема связана с переполнением диска и должна быть обработана в блоке except; в противном случае код в блоке except может направить IOError далее по цепочке вызовов для обработки другим блоком except. Если этого решения недостаточно, можно действовать более радикально — к примеру, изучить исходный код функций Python на языке C, записывающих данные на диск, и при необходимости генерировать собственные исключения DiskFull. Я так поступать не рекомендую, однако все же полезно знать об этой возможности на случай, если она когда-нибудь вам понадобится. 14.2.9. Пример: исключения в обычных вычислениях Несмотря на то что исключения чаще всего применяются для обработки ошибок, они также могут быть невероятно полезны в некоторых случаях, относящихся к штатным операциям. Рассмотрим сложности, возникающие при реализации аналога электронной таблицы. Как и большинство таких таблиц, он должен поддерживать арифметические операции с ячейками, в том числе допускать хранение в ячейках нечисловых значений. В таком приложении пустые ячейки,
   14.2. Исключения в Python 349 используемые в числовых расчетах, могут интерпретироваться как содержащие значение 0. Ячейки с любыми другими строковыми данными могут считаться недопустимыми и маркироваться значением None в Python. Любые вычисления с недопустимым значением должны возвращать недопустимое значение. Первым делом нужно написать функцию, которая будет анализировать строку из ячейки электронной таблицы и возвращать соответствующее значение: def cell_value(string): try: return float(string) except ValueError: if string == "": return 0 else: return None Благодаря средствам обработки исключений языка Python написать такую функцию совсем не сложно. Код пытается преобразовать строку из ячейки в число и вернуть результат в блоке try при помощи встроенной функции float. Функция float генерирует исключение ValueError, если она не может преобразовать строковый аргумент в число, поэтому код перехватывает это исключение и возвращает либо 0, либо None в зависимости от того, является ли аргумент пустой или непустой строкой. На следующем шаге нужно как-то решить проблему с тем, что некоторым арифметическим операциям придется иметь дело со значением None. В языках без поддержки исключений для этого обычно пришлось бы определять специальный набор арифметических функций, которые проверяют свои аргументы на наличие значения None, а затем использовать эти функции вместо встроенных арифметических для выполнения всех математических операций в электронной таблице. Однако реализация такого процесса требует много времени, а результат подвержен ошибкам и замедляет выполнение программы, поскольку, по сути, вы создаете собственный интерпретатор внутри электронной таблицы. В нашем проекте применяется другой подход: все формулы электронной таблицы могут быть функциями Python, которые получают в качестве аргументов координаты x и y анализируемой ячейки вместе с самой электронной таблицей и вычисляют результат для заданной ячейки при помощи стандартных арифметических операторов Python, используя cell_value для извлечения необходимых значений из электронной таблицы. В блоке try можно установить функцию safe_apply, которая применяет одну из этих формул к соответствующим аргументам и возвращает либо результат формулы, либо None в зависимости от того, была ли формула вычислена успешно: def safe_apply(function, x, y, spreadsheet): try: return function(x, y, spreadsheet) except TypeError: return None
   350 Глава 14. Исключения Этих двух изменений достаточно для того, чтобы встроить концепцию пустых (None) значений в семантику электронной таблицы. Попытка реализовать эту возможность без использования исключений станет весьма поучительным упражнением. 14.2.10. Где можно использовать исключения Исключения — естественный выбор для обработки практически любых исключительных ситуаций. К сожалению, обработка ошибок часто добавляется уже тогда, когда основной код программы почти полностью написан, однако исключения тем и хороши, что прекрасно подходят для внедрения кода обработки ошибок постфактум (или, если смотреть на вещи оптимистичнее, для добавления дополнительной обработки ошибок «задним числом»). Исключения также в высшей степени полезны в обстоятельствах, когда большой объем вычислений может потребоваться отменить, если становится очевидным, что вычислительная ветвь вашей программы стала бесперспективной. Пример с электронной таблицей является как раз одним из таких случаев; другие примеры включают алгоритмы парсинга и оптимального выбора методом ветвей и границ. Быстрая проверка: исключения Приводят ли исключения Python к остановке работы программы? Допустим, вы хотите, чтобы обращение к словарю x всегда возвращало значение None, если ключ отсутствует в словаре (то есть при возникновении исключения KeyError). Каким кодом вы бы для этого воспользовались? Попробуйте сами: исключения Какой код вы бы применили для создания пользовательского исключения ValueTooLarge и генерации этого исключения, если переменной x присвоено зна- чение, превышающее 1000? 14.3. Контекстные менеджеры с ключевым словом with Некоторые процедуры — к примеру, чтение файлов — придерживаются предсказуемой схемы с четко установленными началом и концом. В случае чтения из файла его часто требуется открыть всего один раз — при чтении данных. Затем файл можно закрыть. Применив логику исключений, такой код работы с файлом можно представить следующим образом: try: infile = open(filename) data = infile.read() finally: infile.close()
   14.3. Контекстные менеджеры с ключевым словом with 351 В Python 3 реализован обобщенный механизм обработки подобных ситуаций, а именно контекстные менеджеры. Они оборачивают блок кода и управляют выполнением операций при входе в блок и выходе из него, при этом помечаются ключевым словом with. Файловые объекты являются контекстными менеджерами, и этой возможностью можно воспользоваться при чтении файлов: with open(filename) as infile: data = infile.read() Эти две строки кода эквивалентны пяти предыдущим. В обоих случаях вы знаете, что файл будет закрыт сразу же после последнего чтения независимо от того, успешно была выполнена операция или нет. Во втором случае закрытие файла также гарантируется тем, что оно является частью управления контекстом файлового объекта, так что вам не придется писать для этого специальный код. Иначе говоря, благодаря ключевому слову with в сочетании с контекстным менеджером (в данном случае файловым объектом) вам не придется писать рутинный код освобождения ресурсов. Начиная с Python 3.10, устранено ограничение на однострочную запись оператора with. Теперь можно задействовать круглые скобки для разделения выражения на несколько строк, что существенно повышает читаемость кода. Так, следующий пример с вложенными операциями чтения и записи файлов в одном with: with open("empty.txt") as infile, open("other.txt", "w") as outfile: data = infile.read() outfile.write(data) можно записать в более удобочитаемом виде: with ( open("empty.txt") as infile, open("other.txt", "w") as outfile ): data = infile.read() outfile.write(data) Быстрая проверка: менеджеры контекста Допустим, вы задействуете контекстный менеджер в скрипте, который выполняет чтение и/или запись нескольких файлов. Какой из следующих подходов, на ваш взгляд, будет лучшим? • Поместить весь скрипт в блок, управляемый оператором with. • Использовать один оператор with для всех операций чтения файлов, а другой — для всех операций их записи. • Использовать оператор with каждый раз, когда вы читаете или записываете файл (к примеру, при построчной обработке). • Использовать оператор with для каждого файла, который вы читаете или записываете.
   352 Глава 14. Исключения Как нетрудно предположить, при необходимости можно создавать собственные контекстные менеджеры. Подробнее о том, как это делать, а также о различных возможностях работы с ними можно узнать в документации модуля contextlib стандартной библиотеки. Контекстные менеджеры отлично подходят для таких задач, как блокировка/ разблокировка ресурсов, закрытие файлов, совершение транзакций в базе данных и т. п. С момента появления они фактически стали отраслевым стандартом для подобных случаев использования. 14.4. Практическая работа: добавление исключений Вспомните модуль для подсчета вхождений слов, написанный в главе 9. Какие ошибки могут реально возникнуть в его функциях? Проведите рефакторинг этих функций так, чтобы они корректно обрабатывали подобные исключения. 14.4.1. Решение задачи при помощи кода, сгенерированного ИИ Эту задачу легко сформулировать в промпте. Если доступна предыдущая версия кода, достаточно предоставить исходный программный код и предложить инструменту ИИ выполнить рефакторинг, добавив корректную обработку исключений. 14.4.2. Решения и обсуждение Подвох этой задачи кроется в слове «корректную». Указать чат-боту добавить исключения в код — дело нехитрое, но вот окажутся ли они «корректными» — это большой вопрос. Как мы сейчас убедимся, для Colaboratory эта проблема оказалась посложнее, чем для Copilot. Программное решение, созданное человеком Мое решение включает модификацию функций обработки текста, а также добавление обработки исключений ввода-вывода для функций, работающих с чтением и записью файлов: # Версия автора import string punct = str.maketrans('', '', string.punctuation) class EmptyStringError(Exception): Устанавливает пользовательское pass исключение def clean_line(line): """изменяет регистр и удаляет знаки препинания""" # вызвать исключение, если строка пустая if not line.strip(): raise EmptyStringError() Генерирует пользовательское исключение # привести все к единому регистру cleaned_line = line.lower()
   14.4. Практическая работа: добавление исключений 353 # удалить знаки препинания cleaned_line = cleaned_line.translate(punct) return cleaned_line def count_words(words): """принимает список очищенных слов, возвращает словарь подсчета""" word_count = {} for word in words: try: count = word_count.setdefault(word, 0) except TypeError: # если слово нехешируемое, перейти к следующему pass word_count[word] += 1 return word_count def word_stats(word_count): """Принимает словарь подсчета слов и возвращает первые и последние пять записей""" word_list = list(word_count.items()) try: word_list.sort(key=lambda x: x[1]) except TypeError as e: Перехватывает исключение TypeError print(f"Error sorting word list: {e}") least_common = word_list[:5] most_common = word_list[-1:-6:-1] return most_common, least_common def get_words(line): """разделяет строку на слова и заново объединяет символами новой строки""" words = line.split() return "\n".join(words) + "\n" def clean_file(filename, outfilename): try: with open(filename) as infile, open(outfilename, "w") as outfile: for line in infile: if line.strip(): cleaned_line = clean_line(line) cleaned_words = get_words(cleaned_line) # записать все слова построчно outfile.write(cleaned_words) except IOError as e: print(f"Error reading or writing to file: {e}") def load_words(cleaned_filename): words = [] print(cleaned_filename) try: with open(cleaned_filename) as infile: for word in infile: if word.strip(): words.append(word.strip()) except IOError as e: Перехватывает ошибки файлового ввода-вывода
   354 Глава 14. Исключения print(f"Error reading or writing to file: {e}") return words clean_file("moby_01.txt", "moby_clean.txt") moby_words = load_words("moby_clean.txt") word_count = count_words(moby_words) most, least = word_stats(word_count) print("Most common words:") for word in most: print(word) print("\nLeast common words:") for word in least: print(word) Это решение добавляет пользовательское исключение Empty­StringError по большей части в иллюстративных целях. На самом деле, в данном случае особой необходимости в пользовательском исключении нет, и в обычной ситуации я бы, вероятно, не стала его добавлять — разве что в специ­фических случаях, требующих явного контроля передачи пустых строк. Кроме того, в коде явно проверяется исключение TypeError для таких, возможно, редких случаев, как попытка использования нехешируемых ключей или сортировки списков с разнотипными элементами (к примеру, если не все элементы являются строками), а также для обработки любых ошибок при чтении или записи файлов. Программные решения, сгенерированные ИИ Решения, предлагаемые ИИ, демонстрируют большее разнообразие по сравнению с тем, что мы видели ранее. Впрочем, это неудивительно, учитывая не вполне четкую природу данной задачи. Copilot предложил неплохое решение. Использовался следующий промпт: Переработай код в текущем файле word_count_copilot.py так, чтобы он корректно обрабатывал ошибки, которые могут возникнуть в этих функциях. Результаты получились интересными и вполне приемлемыми. Copilot предложил следующий план. 1. В функции write_words_to_file обработать исключение IOError, которое может возникнуть при записи в файл. 2. В функции count_word_occurrences проверить, является ли входной параметр итерируемым объектом. Если нет — сгенерировать исключение TypeError. 3. В функциях print_common_words и print_least_common_words проверить, является ли n положительным целым числом. Если нет, сгенерировать исключение ValueError. 4. В функциях print_common_words и print_least_common_words обработать исключение IndexError, которое может возникнуть в том случае, когда количество слов меньше n.
   14.4. Практическая работа: добавление исключений 355 Итоговый код затронул лишь те функции, которые нуждались в рефакторинге. Я добавила их в полный файл, приведенный ниже, обозначив начало и конец доработанного участка комментариями: # Сгенерировано Copilot import string def clean_line(line): # Преобразовать строку в нижний регистр line = line.lower() # Удалить знаки препинания из строки line = line.translate(str.maketrans('', '', string.punctuation)) return line #=== Переработанный код для добавления обработки исключений======== def write_words_to_file(words, output_file): try: for word in words: output_file.write(word + '\n') except IOError as e: Обрабатывает ошибку файлового ввода-вывода print(f"Error writing to file: {e}") def count_word_occurrences(words): if not hasattr(words, '__iter__'): raise TypeError("Input must be iterable") Генерирует исключение TypeError, если список слов не является итерируемым объектом word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts Вызывает исключение, если запрашивается def print_common_words(word_counts, n): отрицательное количество позиций if not isinstance(n, int) or n <= 0: raise ValueError("n must be a positive integer") sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) print(f"{n} most common words:") Генерирует исключение try: IndexError (в этом коде for word, count in sorted_word_counts[:n]: такого не произойдет) print(f"{word}: {count}") except IndexError as e: print(f"Less than {n} words in input: {e}") def print_least_common_words(word_counts, n): if not isinstance(n, int) or n <= 0: raise ValueError("n must be a positive integer") sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1]) print(f"{n} least common words:") try: for word, count in sorted_word_counts[:n]: print(f"{word}: {count}") except IndexError as e:
   356 Глава 14. Исключения print(f"Less than {n} words in input: {e}") #=== Конец переработанного кода для добавления обработки исключений=== def process_file(input_file_path, output_file_path): # Открыть входной файл для чтения with open(input_file_path, 'r') as input_file: # Открыть выходной файл для записи with open(output_file_path, 'w') as output_file: # Перебрать каждую строку во входном файле for line in input_file: # Очистить строку cleaned_line = clean_line(line) # Разделить строку на слова words = cleaned_line.split() # Записать слова в выходной файл write_words_to_file(words, output_file) def main(): input_file_path = 'moby_01.txt' output_file_path = 'moby_01_clean.txt' # Обработать файл process_file(input_file_path, output_file_path) # Открыть файл with open(output_file_path, 'r') as file: # Прочитать содержимое файла content = file.read() # Разделить содержимое файла на слова words = content.split() # Подсчитать количество вхождений слов word_counts = count_word_occurrences(words) # Вывести пять наиболее часто встречающихся слов и количество их вхождений print_common_words(word_counts, 5) # Вывести пять наименее часто встречающихся слов и количество их вхождений print_least_common_words(word_counts, 5) if __name__ == "__main__": main() Выглядит убедительно, однако все это не совсем верно. Обработка потенциальных ошибок ввода-вывода файлов действительно необходима и реализована корректно. Проверка на итерируемость списка слов тоже имеет смысл, хотя, возможно, это излишняя перестраховка. Да, код завершится с ошибкой, если параметр words не будет итерируемым, однако списки, кортежи, файлы и другие стандартные типы по определению поддерживают итерацию. На практике вероятность получения неитерируемого входного значения настолько мала, что, возможно, эту проверку имеет смысл убрать.
   14.4. Практическая работа: добавление исключений 357 Два других исключения, которые проверяются в этом коде: не является ли параметр n (количество позиций для возвращения) отрицательным и не превышает ли количество позиций числа слов. В первом случае, если n окажется отрицательным, код сгенерирует исключение ValueError, предупреждающее о том, что значение отрицательное. Это в не- котором смысле логично: если запрашивается отрицательное количество позиций, то значения возвращены не будут, хотя ошибка при этом не возникнет. Второй случай — когда перехватывается исключение IndexError — на самом деле лишен всякого смысла. Учитывая то, как написан код, ошибка не возникнет даже в том случае, если значение n будет больше количества элементов в списке sorted_word_counts. Даже если sorted_word_counts пуст, выражение sorted_word_counts[:100] не вызовет ошибку, а просто вернет пустой список. Таким образом, это конкретное исключение никогда не возникнет, а сам код может в будущем лишь сбить с толку. Colaboratory предложил заметно отличающийся вариант кода. И снова промпт ссылался на ячейку, содержащую исходный код, которая располагалась непосредственно над целевой ячейкой: Сделай рефакторинг кода в ячейке выше так, чтобы он корректно обрабатывал ошибки, которые могут возникнуть в этих функциях. Результат оказался несколько разочаровывающим, как мы увидим далее: # промпт: Сделай рефакторинг кода в ячейке выше так, чтобы он корректно # обрабатывал ошибки, которые могут возникнуть в этих функциях. import string # Функция для очистки строки текста путем преобразования ее в нижний регистр # и удаления знаков препинания def clean_line(line): try: line = line.lower() line = line.translate(str.maketrans('', '', string.punctuation)) except Exception as e: raise Exception("Error cleaning line: {}".format(e)) return line # Функция для разделения строки текста на слова # и записи каждого слова построчно в файл def write_words_to_file(line, output_file): try: words = line.split() for word in words: output_file.write(word + '\n') except Exception as e: raise Exception("Error writing words to file: {}".format(e)) # Функция для подсчета количества вхождений каждого слова в списке слов def count_word_occurrences(words): word_counts = {} for word in words:
   358 Глава 14. Исключения try: if word not in word_counts: word_counts[word] = 0 word_counts[word] += 1 except Exception as e: raise Exception("Error counting word occurrences: {}".format(e)) return word_counts # Функция для поиска наиболее и наименее часто втречающихся слов # в словаре подсчета слов def find_most_and_least_common_words(word_counts): try: most_common = sorted(word_counts.items(), key=lambda item: item[1], reverse=True)[:5] least_common = sorted(word_counts.items(), key=lambda item: item[1])[:5] except Exception as e: raise Exception("Error finding most and least common words: ➥{}".format(e)) return most_common, least_common # Открыть входной файл для чтения with open('moby_01.txt', 'r') as input_file: # Открыть выходной файл для записи with open('moby_01_clean.txt', 'w') as output_file: # Перебрать каждую строку во входном файле for line in input_file: # Очистить строку текста try: cleaned_line = clean_line(line) except Exception as e: print("Error cleaning line: {}".format(e)) continue # Записать каждое слово из очищенной строки в выходной файл try: write_words_to_file(cleaned_line, output_file) except Exception as e: print("Error writing words to file: {}".format(e)) continue # Открыть файл и присвоить текст переменной try: with open('moby_01_clean.txt', 'r') as f: text = f.read() except Exception as e: raise Exception("Error opening file: {}".format(e)) # Разделить текст на слова try: words = text.split() except Exception as e: raise Exception("Error splitting text into words: {}".format(e)) # Подсчитать количество вхождений каждого слова
   14.4. Практическая работа: добавление исключений 359 try: word_counts = count_word_occurrences(words) except Exception as e: raise Exception("Error counting word occurrences: {}".format(e)) # Найти наиболее и наименее часто встречающиеся слова try: most_common,least_common = find_most_and_least_common_words(word_counts) except Exception as e: raise Exception("Error finding most and least common words: ➥{}".format(e)) # Вывести результаты print("Most common words:") for word, count in most_common: print(f"{word}: {count}") print("\nLeast common words:") for word, count in least_common: print(f"{word}: {count}") Прежде всего этот фрагмент кода содержит значительное количество блоков try-except — порядка десяти, — что может создать впечатление, будто обра- ботка ошибок реализована на отлично. К сожалению, такой подход к обработке исключений имеет ряд проблем. Во-первых, практически весь код программы помещен внутрь блоков try-except, включая операции со строками, вероятность возникновения ошибки в которых крайне мала. К примеру, вызов функции clean_line проверяется на наличие исключений, а затем внутри самой функции повторно осуществляется контроль возможных ошибок при выполнении строковых операций. Несмотря на то что такая тщательная проверка на ошибки может показаться оправданной, на практике удачным такое решение не назовешь. Взять хотя бы тот факт, что обилие конструкций try-except снижает читаемость кода: необходимость анализа этих блоков затрудняет восприятие и понимание логики программы. Вторая проблема заключается в том, как реализованы блоки try-except. Во всех случаях проверяется исключение Exception, то есть самый общий тип исключения, перехватывающий любые ошибки Python, кроме немногих, унаследованных напрямую от BaseException. Что еще хуже — при перехвате исключения код просто генерирует новое исключение с сообщением, в котором лишь содержится имя самого исключения и приблизительно указывается место возникновения ошибки. Подобная практика недопустима: хотя такая реализация блоков tryexcept практически равносильна их отсутствию, фактически она снижает информативность, подавляя трассировку выполнения. Вариант Copilot содержит одну полностью бесполезную проверку исключения, а у Colaboratory ситуация еще хуже. Даже когда проверки исключений не совсем бесполезны, они менее информативны, чем обработка исключений Python по умолчанию.
   360 Глава 14. Исключения Итоги Согласно философии языка Python, ошибки не должны оставаться незамеченными, если только они не были явно подавлены. Механизм обработки исключений и классы исключений в Python образуют развитую систему для обработки ошибок времени выполнения в вашем коде. Типы исключений Python организованы в иерархическую структуру, поскольку исключения, как и все объекты языка Python, являются классами. Блоки try, except, else и finally, а также выбор или даже создание новых типов исключений позволяют чрезвычайно точно управлять обработкой или игнорированием исключений. Пользовательское исключение можно установить путем наследования, предпочтительно от базового класса Exception. Исключения могут генерироваться при выполнении заданного условия с помощью оператора assert. Вызов контекстного менеджера при помощи ключевого слова with обеспечивает выполнение необходимых операций даже в случае возникновения исключения, как, например, при чтении или записи файла.
Часть 3 Расширенные возможности языка Предыдущие главы содержали обзор основных возможностей языка Python, то есть тех инструментов, что применяются подавляющим большинством программистов в повседневной практике. Теперь нам предстоит знакомство с более продвинутыми возможностями языка, такими как принципы работы с классами и объектами в Python, регулярные выражения, пакеты и стандартная библиотека. И хотя вам, возможно, не понадобится прибегать к этим средствам ежедневно (все зависит от специфики ваших задач), знания в этих областях крайне важны, когда возникает такая необходимость.
15 Классы и объектноориентированное программирование В этой главе: 3 Определение классов 3 Использование переменных экземпляра и декоратора @property 3 Определение методов 3 Определение классовых переменных и методов класса 3 Наследование от других классов 3 Создание приватных (закрытых) переменных и методов 3 Наследование от нескольких классов (множественное наследование) В этой главе рассматриваются классы Python, предназначенные для хранения данных и программного кода. Хотя большинство программистов, вероятно, знакомы с классами и объектами в других языках, я не стану строить предположений относительно вашего уровня знаний конкретных языков или парадигм. К тому же, в этой главе описаны только доступные в Python конструкции; в ней не излагаются основы объектно-ориентированного программирования (ООП). 15.1. Определение классов Класс в Python — это, по сути, тип данных. Все типы данных, встроенные в Python, являются классами, и Python предоставляет мощные инструменты
   15.1. Определение классов 363 для управления всеми аспектами поведения класса. Класс определяется при помощи ключевого слова class: class MyClass: тело Тело класса представляет собой последовательность операторов Python. Обычно они включают присваивания значений переменным и определения функций, хотя это не обязательно, так как тело может состоять из единственного оператора pass. По общепринятому соглашению идентификаторы классов Python записываются в стиле CapCase — то есть каждое слово в их названии начинается с заглавной буквы, чтобы выделить идентификаторы в коде. После определения класса можно создать новый объект этого типа (экземпляр класса), вызвав имя класса как функцию: instance = MyClass() 15.1.1. Использование экземпляра класса как структуры или записи Экземпляры классов могут использоваться как структуры или записи. В отличие от структур C или классов Java, поля данных экземпляра не обязательно объявлять заранее, они могут создаваться «на ходу». В следующем небольшом фрагменте кода определяется класс с именем Circle , создается экземпляр Circle, полю radius экземпляра присваивается значение, после чего это поле задействуется для вычисления длины окружности: class Circle: pass my_circle = Circle() my_circle.radius = 5 print(2 * 3.14 * my_circle.radius) 31.4 Как в Java и многих других языках программирования, для обращения к полям экземпляра/структуры и присваивания им значений применяется точечная нотация. Чтобы поля экземпляров класса инициализировались автоматически, следует включить в тело класса метод инициализации __init__. Он служит для инициа­ лизации атрибутов создаваемого объекта и принимает ссылку self в качестве первого аргумента. Метод __init__ похож на конструктор в языке Java, но он ничего не конструирует, а только инициализирует поля класса. Кроме того, в отличие от классов Java и C++, классы Python могут иметь только один метод __init__. Следующий фрагмент кода создает окружности с радиусом 1 по умолчанию:
   364 Глава 15. Классы и объектно-ориентированное программирование class Circle: self в качестве параметра для __init__() def __init__(self): self.radius = 1 Создает экземпляр класса Circle my_circle = Circle() Поле radius уже установлено print(2 * 3.14 * my_circle.radius) my_circle.radius = 5 print(2 * 3.14 * my_circle.radius) 6.28 31.400000000000002 Поле radius переопределяется Другой результат при другом значении поля radius По общепринятому соглашению первый аргумент __init__ всегда именуется self. Он присваивается вновь созданному экземпляру класса Circle при выполнении __init__. Затем код использует определение класса. Сначала создается экземпляр объекта класса Circle. В следующей инструкции учитывается тот факт, что поле radius уже инициализировано. Полю radius также можно присвоить другое значение. В итоге второй вывод команды print отличается от первого. В Python также имеется нечто большее, чем просто конструктор: метод __new__, который вызывается при создании объекта и возвращает неинициализированный объект. Если только вы не создаете подкласс неизменяемого типа, такого как str или int, или не используете метакласс для изменения процесса создания объекта, необходимость переопределять уже существующий метод __new__ возникает крайне редко. Полноценное применение ООП открывает воистину безграничные возможности, и я рекомендую всем новичкам обратиться к обширной литературе по этой теме. Оставшаяся часть этой главы посвящена конструкциям ООП языка Python. 15.2. Переменные экземпляра Переменные экземпляра — это основная возможность ООП. Еще раз взгляните на класс Circle: class Circle: def __init__(self): self.radius = 1 Здесь radius — переменная экземпляра объектов класса Circle. Иными словами, каждый экземпляр класса Circle имеет собственную копию переменной radius, а значение, на которое ссылается эта копия, может отличаться от значений, присвоенных переменной radius других экземпляров. Python позволяет создавать переменные экземпляра по мере необходимости, присваивая их значение полю экземпляра класса: экземпляр.переменная = значение Если переменная еще не существует, она создается автоматически. Именно так в __init__ была создана переменная radius.
   15.3. Методы 365 При любом способе работы с переменными экземпляра (будь то присваивание или обращение) требуется явно указать содержащий их экземпляр — то есть применить синтаксис экземпляр.переменная. Ссылка на переменную без указания экземпляра указывает не на переменную экземпляра, а на локальную переменную в выполняемом методе. В этом отношении Python отличается от таких языков, как С++ и Java, где обращения к переменным экземпляра такие же, как и к переменным функций локальных методов. Мне нравится требование Python о явном указании экземпляра, поскольку оно более наглядно отличает переменные экземпляра от локальных переменных функций. Попробуйте сами: переменные экземпляра Какой код вы бы использовали для создания класса Rectangle (прямоугольник)? 15.3. Методы Метод представляет собой функцию, связанную с конкретным классом. Вы уже видели специальный метод __init__, который вызывается для нового экземпляра при его создании. В следующем примере для класса Circle определяется другой метод, area; он вычисляет и возвращает площадь круга для любого экземпляра Circle. Как и большинство методов, определяемых пользователем, метод area вызывается с синтаксисом вызова методов, который напоминает обращения к переменным экземпляра: class Circle: def __init__(self): self.radius = 1 def area(self): return self.radius * self.radius * 3.14159 c = Circle() c.radius = 3 print(c.area()) 28.27431 Синтаксис вызова метода состоит из экземпляра, за которым следует точка, а затем имя метода, вызываемого для экземпляра. Такая форма называется связанным вызовом метода (bound method invocation), где связывание экземпляра происходит автоматически. Альтернативно возможно вызвать метод как несвязанный (unbound), обратившись к нему через содержащий его класс. Этот способ менее удобен и почти никогда не применяется на практике, поскольку при таком вызове метода в его первом аргументе должен передаваться экземпляр класса, в котором этот метод определен, и код получается менее понятным: print(Circle.area(c)) 28.27431
   366 Глава 15. Классы и объектно-ориентированное программирование Как и __init__, метод area присутствует в виде функции в теле определения класса. В первом аргументе любого метода передается экземпляр, для которого он вызывается; по действующему соглашению его принято называть self. Во многих языках программирования экземпляр, часто называемый this, неявный и никогда явно не передается, тогда как по философии проектирования Python предпочтительно, чтобы все выражалось явным образом. Методы могут вызываться с аргументами, если определения методов принимают эти аргументы. Следующая версия класса Circle добавляет аргумент в метод __init__, чтобы можно было создавать окружности с заданным значением радиуса без необходимости устанавливать его после создания экземпляра: class Circle: def __init__(self, radius): self.radius = radius def area(self): return self.radius * self.radius * 3.14159 Обратите внимание на два применения radius. Переменная self.radius — это переменная экземпляра с именем radius. Переменная radius без указателя экземпляра — локальный параметр функции с именем radius. Это две разные переменные! На практике локальному параметру функции можно дать имя r или rad, чтобы исключить возможную путаницу. С таким определением класса Circle можно создавать экземпляры с произвольным значением radius одним вызовом класса Circle. Следующая команда создает экземпляр Circle с радиусом, равным 5: c = Circle(5) В методах можно использовать все стандартные возможности функций Python: значения аргументов по умолчанию, дополнительные аргументы, именованные аргументы и т. д. Первая строка __init__ может выглядеть так: def __init__(self, radius=1): В таком случае все последующие вызовы класса Сircle будут работать с дополнительным аргументом или без него; Circle() вернет экземпляр с radius = 1, а Circle(3) — с radius = 3. В процедуре вызова методов в Python нет никакого волшебства; можно считать ее сокращенной записью для обычного вызова функций. Так, встретив вызов метода в формате экземпляр.метод(арг1, арг2, . . .), Python преобразует его в обычный вызов функции по следующим правилам. 1. Выполнить поиск имени метода в пространстве имен экземпляра. Если метод был изменен или добавлен в данный экземпляр, ему отдается предпочтение перед методами класса или родительского класса. Поиск проводится по правилам, описанным в разделе 15.4.1 этой главы.
   15.4. Классовые переменные 367 2. Если метод не обнаружен в пространстве имен экземпляра, продолжить поиск в типе класса экземпляра. В предыдущих примерах класс — это Circle, тип экземпляра c. 3. Если метод и здесь не найден, продолжить поиск метода в родительских классах. 4. После того как метод найден, вызвать его напрямую как обычную функцию Python; при этом экземпляр передается в первом аргументе функции, а все остальные аргументы вызова метода сдвигаются на одну позицию вправо. Таким образом, запись экземпляр.метод(арг1, арг2, …) превращается в класс. метод(экземпляр, арг1, арг2, …). Попробуйте сами: переменные и методы экземпляра Обновите код класса Rectangle таким образом, чтобы размеры можно было задавать при создании экземпляра, по аналогии с классом Circle. Также добавьте метод area(). 15.4. Классовые переменные Классовая переменная (class variable) представляет собой переменную, связанную с классом, а не с его конкретным экземпляром и доступную для всех экземпляров класса. Классовая переменная может служить для отслеживания информации на уровне класса — например, количества экземпляров класса, созданных в любой момент времени. Python поддерживает классовые переменные, хотя их использование требует несколько больших усилий от разработчика, чем во многих других языках. Кроме того, придется следить за возможными конфликтами имен между классовыми переменными и переменными экземпляров. Классовая переменная создается присваиванием в теле класса, а не в функции __init__. После ее создания переменная становится видимой для всех экземпляров класса. Так, с помощью классовой переменной можно предоставить доступ к значению pi всем экземплярам класса Circle: class Circle: pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return self.radius * self.radius * Circle.pi Создав это определение, можно, к примеру, ввести: Circle.pi 3.14159 Circle.pi = 4 Circle.pi
   368 Глава 15. Классы и объектно-ориентированное программирование 4 Circle.pi = 3.14159 Circle.pi 3.14159 Этот пример иллюстрирует, как именно должна работать классовая переменная; она связана с классом, который ее определяет, и содержится в нем. Обратите внимание: в этом примере мы обращаемся к Circle.pi до того, как будет создан хотя бы один экземпляр класса. Очевидно, что Circle.pi существует независимо от каких-либо конкретных экземпляров класса Circle. К классовой переменной также можно обратиться из метода класса, используя имя класса. Это делается в определении Circle.area, где функция area конкретно ссылается на Circle.pi. На практике это дает желаемый эффект: правильное значение pi получается из класса и применяется в вычислениях: c = Circle(3) c.area() 28.27431 Возможно, вам не понравится, что имя класса жестко задается в коде в методах этого класса. Этого можно избежать при помощи специального атрибута __class__, доступного для всех экземпляров класса Python. Этот атрибут возвращает класс, к которому принадлежит экземпляр, например: Circle <class '__main__.Circle'> c.__class__ <class '__main__.Circle'> Класс с именем Circle внутренне представлен абстрактной структурой данных, и именно эта структура данных получается при обращении к специальному атрибуту __class__ экземпляра c (который является экземпляром класса Circle). Следующий пример демонстрирует, как получить значение Circle.pi из c без явного указания имени класса Circle: c.__class__.pi 3.14159 Этот код можно было использовать в самом методе area для того, чтобы избавиться от внутреннего упоминания класса Circle, просто заменив ссылку Circle.pi на self.__class__.pi.
   15.4. Классовые переменные 369 15.4.1. Необычное поведение классовых переменных У классовых переменных есть одна неочевидная особенность, которая может сбить вас с толку, если вы не осведомлены о ней заранее. Если Python не находит переменную экземпляра с заданным именем, он пытается найти и вернуть значение из классовой переменной с тем же именем. И только если подходящая классовая переменная не будет найдена, Python сообщит об ошибке. Классовые переменные позволяют эффективно реализовать значения по умолчанию для переменных экземпляров; просто создайте классовую переменную с тем же именем и соответствующим значением по умолчанию, и вы избежите затрат времени и памяти на инициализацию переменной экземпляра при каждом создании экземпляра класса. С другой стороны, без выдачи ошибки нетрудно по невнимательности обратиться к переменной экземпляра вместо классовой переменной. В этом разделе, прибегнув к предыдущему примеру, я продемонстрирую, как работают классовые переменные. Вы можете обратиться к переменной c.pi даже в том случае, если объект c не содержит связанной переменной экземпляра с именем pi. Python сначала пытается найти такую переменную экземпляра; не обнаружив ее, продолжает поиск и находит классовую переменную pi в Circle: c = Circle(3) c.pi 3.14159 Этот результат может совпадать или не совпадать с вашими пожеланиями. Такой прием удобен, но он может привести к ошибкам, так что пользуйтесь им аккуратно. А что произойдет, если вы попытаетесь использовать c.pi как настоящую классовую переменную и измените ее в одном экземпляре с тем, чтобы эти изменения были видны всем экземплярам класса? И снова мы воспользуемся предыдущим определением Circle: c1 = Circle(1) c2 = Circle(2) c1.pi = 3.14 c1.pi 3.14 c2.pi 3.14159 Circle.pi 3.14159 Этот пример работает не так, как в случае с настоящей классовой переменной; объект c1 теперь содержит собственную копию pi, отличную от копии Circle.
   370 Глава 15. Классы и объектно-ориентированное программирование pi, к которой обращается c2. Происходит так потому, что присваивание c1.pi создает переменную экземпляра в c1; на классовую переменную Circle.pi эта операция никак не влияет. Последующие обращения к c1.pi возвращают значение из этой переменной экземпляра, тогда как при последующих обращениях к c2.pi Python ищет переменную экземпляра pi в объекте c2, не находит ее и переходит к поиску значения классовой переменной Circle.pi. Если вы хотите изменить значение классовой переменной, обращайтесь к ней по имени класса, а не через ссылку на экземпляр (self). 15.5. Статические методы и методы классов Классы Python также могут содержать методы, которые являются аналогами статических методов в таких языках программирования, как Java. Кроме того, в Python поддерживаются методы классов, которые устроены чуть сложнее. 15.5.1. Статические методы Как и в Java, статические методы можно вызывать даже в том случае, если ни один экземпляр класса еще не был создан. При этом их также можно вызывать с помощью экземпляра класса. Для создания статического метода следует применять декоратор @staticmethod, как показано в листинге 15.1. Листинг 15.1. Файл circle.py """Модуль circle: содержит класс Circle.""" class Circle: Классовая переменная, содержащая список """Класс Circle""" всех созданных объектов Circle all_circles = [] pi = 3.14159 def __init__(self, r=1): """Создает экземпляр Circle с заданным значением радиуса""" При инициализации объекта он self.radius = r добавляет себя в список all_circles self.__class__.all_circles.append(self) def area(self): """Вычисляет площадь круга для объекта Circle""" return self.__class__.pi * self.radius * self.radius @staticmethod def total_area(): """Статический метод для вычисления общей площади всех объектов Circle""" total = 0 for c in Circle.all_circles: total = total + c.area() return total Теперь в интерактивном режиме введите следующие инструкции: import circle c1 = circle.Circle(1) c2 = circle.Circle(2)
   15.5. Статические методы и методы классов 371 circle.Circle.total_area() 15.70795 c2.radius = 3 circle.Circle.total_area() 31.415899999999997 Также обратите внимание на использование строк документации. В реальном модуле их следовало бы сделать более содержательными, например, перечислить в докстринге класса доступные методы, а также сведения о применении в строке документации каждого метода: circle.__doc__ 'Модуль circle: содержит класс Circle.' circle.Circle.__doc__ 'класс Circle' circle.Circle.area.__doc__ 'Вычисляет площадь круга для объекта Circle' 15.5.2. Методы класса Методы классов похожи на статические методы тем, что их можно вызывать до того, как создан экземпляр класса, либо с помощью уже созданного экземпляра класса. Однако методы класса неявно получают класс, к которому они принадлежат, в качестве первого параметра, поэтому их проще кодировать (листинг 15.2). Листинг 15.2. Файл circle_cm.py """Модуль circle_cm: содержит класс Circle.""" class Circle: """Класс Circle""" all_circles = [] Переменная, содержащая список всех all_circles pi = 3.14159= [] Переменная, содержащая созданных объектов Circle список всех pi = 3.14159 def __init__(self, r=1): созданных объектов Circle def __init__(self, r=1): """Создает экземпляр Circle с заданным значением радиуса""" """Создает Circle с заданным значением радиуса""" self.radiusэкземпляр = r self.radius = r self.__class__.all_circles.append(self) def self.__class__.all_circles.append(self) area(self): def area(self): """Вычисляет площадь круга для объекта Circle""" """Вычисляет площадь круга объекта Circle""" return self.__class__.pi * для self.radius * self.radius return self.__class__.pi * self.radius * self.radius @classmethod @classmethod def total_area(cls): def total_area(cls): total = 0 total for c = in0cls.all_circles: for c in cls.all_circles: total = total + c.area() total = total + c.area() return total return total Декоратор @classmethod Декоратор @classmethod Вместо self используется cls Вместо self используется cls cls можно использовать вместо Circle cls можно использовать вместо Circle
   372 Глава 15. Классы и объектно-ориентированное программирование import circle_cm c1 = circle_cm.Circle(1) c2 = circle_cm.Circle(2) circle_cm.Circle.total_area() 15.70795 c2.radius = 3 circle_cm.Circle.total_area() 31.415899999999997 Декоратор @classmethod применяется перед определением метода. Параметру класса традиционно присваивается имя cls. Можно использовать cls вместо Circle. При применении метода класса вместо статического метода вам не придется жестко задавать в коде имя класса в total_area. В результате любые подклассы Circle по-прежнему смогут вызвать total_area и обращаться к своим полям и методам (а не к полям и методам Circle). Попробуйте сами: методы класса Напишите метод класса, аналогичный total_area(), который возвращает общую длину окружности для всех объектов Circle. Напомню, что длина окружности вычисляется как 2 × радиус окружности × число π. 15.6. Наследование Механизм наследования в языке Python реализован проще и гибче, чем в компилируемых языках (таких, как Java и C++), поскольку динамическая природа Python не накладывает столько ограничений на язык программирования. Чтобы понять, как работает наследование в Python, создадим еще один класс по шаблону класса Circle, рассмотренного ранее в этой главе. К примеру, можно определить дополнительный класс для квадратов: class Square: def __init__(self, side=1): self.side = side Длина стороны квадрата Если теперь вы захотите использовать эти классы в графическом редакторе, они должны хранить информацию о том, в каком месте поверхности изображения расположен каждый объект. Это можно реализовать, задав координаты x и y для каждого экземпляра: class Square: def __init__(self, side=1, x=0, y=0): self.side = side self.x = x
   15.6. Наследование 373 self.y = y class Circle: def __init__(self, radius=1, x=0, y=0): self.radius = radius self.x = x self.y = y Такой подход работает, но приводит к появлению большого количества повторяющегося кода при увеличении числа классов геометрических фигур, так как для каждой фигуры потребуется хранить информацию о текущей позиции. Вы, конечно, поняли, к чему я клоню: ведь это типичная ситуация для применения наследования в объектно-ориентированном языке. Вместо того чтобы определять переменные x и y в каждом классе геометрической фигуры, можно абстрагировать их в общий класс Shape, а каждый класс, определяющий конкретную фигуру, будет наследовать от такого общего класса. В языке Python этот прием выглядит примерно так: class Shape: def __init__(self, x, y): self.x = x self.y = y class Square(Shape): Указывает, что Square наследует от Shape def __init__(self, side=1, x=0, y=0): super().__init__(x, y) Должен вызвать self.side = side Указывает, что Circle метод __init__ class Circle(Shape): наследует от Shape класса Shape def __init__(self, r=1, x=0, y=0): super().__init__(x, y) self.radius = r Чтобы применить наследование в Python, необходимо (как правило) выполнить два требования, которые продемонстрированы в коде классов Circle и Square, выделенном жирным шрифтом. Первое требование — определение иерархии наследования; для этого классы, от которых наследует текущий класс, указываются в круглых скобках непосредственно за именем этого класса, определяемого посредством ключевого слова class. В приведенном выше коде оба класса Circle и Square наследуют от Shape. Второе, менее очевидное требование — необходимость явного вызова метода __init__ наследуемых классов. Python не делает этого автоматически, но можно воспользоваться функцией super, чтобы Python сам определил нужный наследуемый класс. В примере кода эта задача решается инструкциями super().__init__(x,y). Код вызывает функцию инициализации Shape с инициализируемым экземпляром и соответствующими аргументами. Без этого в данном примере у объектов Circle и Square не были бы установлены переменные экземпляров x и y. Вместо функции super можно вызвать метод __init__ класса Shape с явным указанием наследуемого класса с помощью Shape.__init__(self, x, y); это также обеспечит вызов функции инициализации Shape вместе с инициализацией экземпляра. Однако по мере написания большой программы вы обнаружите,
   374 Глава 15. Классы и объектно-ориентированное программирование что этот способ менее гибкий, поскольку имя наследуемого класса жестко прописано в коде и это может создать дополнительные трудности при изменении структуры и иерархии наследования. С другой стороны, у функции super также имеются свои нюансы, которые проявляются в более сложных случаях. Так как эти два способа плохо сочетаются друг с другом, четко документируйте, каким из них вы пользуетесь в своем коде. Кроме того, наследование вступает в силу в том случае, когда вы пытаетесь задействовать метод, определенный не в подклассе или производном классе, а в его родительском классе. Чтобы посмотреть, как это происходит, определите в классе Shape еще один метод с именем move, который перемещает фигуру на заданное расстояние. Этот метод изменяет координаты x и y фигуры на величину, определяемую аргументами метода. Определение класса Shape теперь выглядит так: class Shape: def __init__(self, x, y): self.x = x self.y = y def move(self, delta_x, delta_y): self.x = self.x + delta_x self.y = self.y + delta_y Если вы введете это определение для Shape и предыдущие определения для Circle и Square, то в интерактивной сессии сможете получить следующее: c = Circle(1) c.move(3, 4) f"c.x = {c.x}, c.y = {c.y}" 'c.x = 3, c.y = 4' Если вы протестируете этот код в интерактивной сессии, не забудьте заново ввести класс Circle после переопределения класса Shape. В этом примере класс Circle не содержал определения метода move непосредственно в своем теле, но поскольку он наследует от класса, реализующего move, все экземпляры Circle смогли задействовать move. Пользуясь более традиционной терминологией ООП, можно сказать, что все методы Python являются виртуальными — то есть если метод не существует в текущем классе, то Python ищет его в списке родительских классов и применяет первый найденный метод. Попробуйте сами: наследование Перепишите код для класса Rectangle так, чтобы он наследовал от Shape. Учитывая, что у квадратов много общего с прямоугольниками, стоит ли сделать один из них наследующим от другого? Если да, то какой класс должен стать базовым, а какой наследующим? Как бы вы написали код добавления метода area() для класса Square? Следует ли переместить метод area в базовый класс Shape, чтобы он наследовался классами Circle, Square и Rectangle? Если да, то какие проблемы могут возникнуть?
   15.7. Наследование с использованием классовых переменных 375 15.7. Наследование с использованием классовых переменных и переменных экземпляра Наследование позволяет экземпляру наследовать атрибуты класса. Переменные экземпляра связываются с экземплярами объектов, так что для каждого экземпляра имеется лишь одна переменная экземпляра с заданным именем. Рассмотрим пример. Используя приведенные ниже определения классов: class P: z = "Hello" def set_p(self): self.x = "Class P" def print_p(self): print(self.x) class C(P): def set_c(self): self.x = "Class C" def print_c(self): print(self.x) выполните следующий код: c = C() c.set_p() c.print_p() Class P c.print_c() Class P c.set_c() c.print_c() Class C c.print_p() Class C Объект c в этом примере является экземпляром класса C. Класс C наследует от класса P, но экземпляр c наследует не от некоего невидимого экземпляра класса P. Он наследует методы и классовые переменные непосредственно от класса P. Так как существует только один экземпляр (c), любая ссылка на переменную экземпляра x в вызове метода для c должна указывать на c.x. Это утверждение справедливо независимо от того, какой класс определяет метод, вызываемый для c. Как видите, при вызове для c оба метода set_p и print_p, определенные в классе P, обращаются к той же переменной, что и методы, set_c и print_c, когда они вызываются для c.
   376 Глава 15. Классы и объектно-ориентированное программирование В целом от переменных экземпляров требуется именно такое поведение, поскольку вполне логично, что ссылки на переменные экземпляра с одинаковыми именами будут относиться к одной и той же переменной. Однако иногда требуется другое поведение, которое может быть реализовано при помощи закрытых (приватных) переменных (см. раздел 15.9). Классовые переменные наследуются, но необходимо действовать осторожно, чтобы избежать конфликтов имен, и помнить об особенностях их поведения, описанных в подразделе, посвященном классовым переменным. В следующем примере классовая переменная z определяется для родительского класса P, а обратиться к ней можно тремя способами: через экземпляр c, через производный класс C или напрямую через родительский класс P: f"{c.z=} {C.z=} {P.z=}" 'c.z='Hello' C.z='Hello' P.z='Hello'' Однако если вы попытаетесь задать классовую переменную z через класс C, для класса C будет создана новая классовая переменная. Этот результат не повлияет на саму классовую переменную P (с обращением через P). Но при последующих обращениях через класс C или его экземпляр c будет выводиться эта новая переменная, а не исходная: C.z = "Bonjour" f"{c.z=} {C.z=} {P.z=}" 'c.z='Bonjour' C.z='Bonjour' P.z='Hello'' Аналогичным образом при попытке установить значение z через экземпляр c будет создана новая переменная экземпляра, и в итоге вы получите три разные переменные: c.z = "Привет" f"{c.z=} {C.z=} {P.z=}" 'c.z='Привет' C.z='Bonjour' P.z='Hello'' 15.8. Резюмируем: основные возможности классов Python Все, о чем говорилось выше, относится к основам работы с классами и объектами в Python. Прежде чем двигаться дальше, я объединю эти основы в одном примере. Ниже мы создадим пару классов, реализующих изученные нами возможности, а затем рассмотрим их в действии. Начнем с создания базового класса:
   15.8. Резюмируем: основные возможности классов Python 377 Метод __init__ принимает экземпляр class Shape: (self) и два параметра def __init__(self, x, y): self.x = x Обращение к переменным экземпляра через self self.y = y def move(self, delta_x, delta_y): Метод move принимает экземпляр self.x = self.x + delta_x (self) и два параметра self.y = self.y + delta_y Значение переменной экземпляра устанавливается в методе move Затем создадим подкласс, наследующий от базового класса Shape: Класс Circle наследует от класса Shape pi и all_circles — классовые переменные класса Circle Метод __init__ класса Circle принимает class Circle(Shape): экземпляр (self) и 3 параметра со значениями pi = 3.14159 all_circles = [] Метод __init__ класса Circle использует def __init__(self, r=1, x=0, y=0): super() для вызова __init__ класса Shape super().__init__(x, y) self.radius = r self.__class__.all_circles.append(self) В методе __init__ экземпляр @classmethod добавляет себя в список def total_area(cls): all_circles area = 0 total_area — это метод класса, for circle in cls.all_circles: принимающий в качестве area += cls.circle_area(circle.radius) return area параметра сам класс (cls) @staticmethod Применяет параметр cls для def circle_area(radius): обращения к статическому return Circle.pi * radius * radius Обращается к классовой переменной pi; также возможна запись __class__.pi методу circle_area circle_area — это статический метод, не получающий в качестве параметров ни self, ни cls Теперь можно создать несколько экземпляров класса Circle и испытать их на деле. Так как для метода __init__ класса Circle указаны параметры по умолчанию, можно создать объект Circle без передачи каких-либо параметров: c1 = Circle() c1.radius, c1.x, c1.y (1, 0, 0) Если же задать параметры, они будут использованы для установки значений экземпляра: c2 = Circle(2, 1, 1) c2.radius, c2.x, c2.y (2, 1, 1)
   378 Глава 15. Классы и объектно-ориентированное программирование Если вызвать метод move(), Python не найдет move() в классе Circle, поэтому он переместится вверх по иерархии наследования и задействует метод move() класса Shape: c2.move(2, 2) c2.radius, c2.x, c2.y (2, 3, 3) Кроме того, поскольку в процессе своей работы метод __init__ добавляет ссылку на каждый экземпляр в список, присвоенный классовой переменной, вы получаете экземпляры Circle: Circle.all_circles [<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at 0x7fa88835eb00>] [c1, c2] [<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at 0x7fa88835eb00>] Вы также можете вызвать метод класса total_area() как через сам класс Circle, так и через его экземпляр: Circle.total_area() 15.70795 c2.total_area() 15.70795 Наконец, можно вызвать статический метод circle_area(): опять же либо через сам класс, либо через экземпляр. Будучи статическим методом, circle_area не получает в качестве аргумента экземпляр класса или класс и ведет себя скорее как независимая функция в пространстве имен класса. На практике статические методы часто применяются для включения в класс вспомогательных функций: Circle.circle_area(c1.radius) 3.14159 c1.circle_area(c1.radius) 3.14159 Эти примеры демонстрируют базовое поведение классов в Python. Теперь, ко­ гда вы изучили основы работы с классами, можно переходить к более сложным темам.
   15.9. Закрытые переменные и закрытые методы 379 15.9. Закрытые переменные и закрытые методы Закрытая переменная (private variable) и закрытый метод (private method) не видны за пределами методов класса, в котором они определяются. Закрытые (приватные) переменные и методы полезны по двум причинам: они повышают уровень безопасности и надежности программного кода за счет избирательного ограничения доступа к важным или критичным частям реализации объекта, а также предотвращают конфликты имен, которые могут возникнуть из-за применения наследования. Класс может определить закрытую переменную и наследовать от класса, определяющего закрытую переменную с тем же именем, но это не создает проблем, поскольку закрытость переменных гарантирует хранение их отдельных копий. Закрытые переменные упрощают чтение кода, так как они явно указывают на то, что должно использоваться лишь внутри класса. Все остальное относится к интерфейсу класса. В большинстве языков программирования закрытые переменные определяются с помощью ключевого слова «private» или подобного. Синтаксис Python проще, к тому же он нагляднее демонстрирует, какие переменные или методы являются закрытыми, а какие нет. Любой метод или переменная экземпляра, чье имя начинается (именно начинается, а не заканчивается!) с двойного символа подчеркивания (__), являются закрытыми; а все остальные — нет. В качестве примера рассмотрим следующее определение класса: class Mine: def __init__(self): self.x = 2 self.__y = 3 def print_y(self): print(self.__y)) Начальное двойное подчеркивание определяет __y как закрытую переменную На основе этого определения создадим экземпляр класса: m = Mine() Переменная x закрытой не является, поэтому к ней можно обратиться напрямую: print(m.x) 2 Переменная __y закрытая. При попытке обратиться к ней напрямую возникает ошибка: print(m.__y) -------------------------------------------------------------------------AttributeError Traceback (most recent call last) <ipython-input-74-c09dfe1cad43> in <cell line: 1>() ----> 1 print(m.__y) AttributeError: 'Mine' object has no attribute '__y'
   380 Глава 15. Классы и объектно-ориентированное программирование Метод print_y не является закрытым, но поскольку он принадлежит классу Mine, он может обратиться к переменной __y и вывести ее значение: m.print_y() 3 Наконец, следует отметить, что при компиляции кода в байт-код механизм обеспечения приватности искажает имена закрытых переменных и методов. В частности, к имени переменной добавляется префикс _имякласса: dir(m) ['_Mine__y', ... 'print_y', 'x'] Это делается для предотвращения любых случайных доступов. При желании разработчик может намеренно имитировать искажение имен и получить доступ к значению переменной. Однако реализация искажения в столь легко читаемой форме существенно упрощает отладку. Попробуйте сами: закрытые переменные экземпляра Модифицируйте код класса Rectangle так, чтобы переменные размеров сторон стали закрытыми. Какое ограничение на работу с классом накладывает такая правка? 15.10. Использование декоратора @property для гибкой работы с переменными экземпляра Python позволяет программисту получать доступ к переменным экземпляра напрямую — без промежуточных методов получения (геттер) и установки (сеттер) значения, часто используемых в Java и других объектно-ориентированных языках. Отсутствие таких промежуточных методов делает код классов Python чище и проще, однако в некоторых ситуациях подобные методы получения и установки значения могут пригодиться. Представьте, что значение нужно проверить перед присваиванием его переменной экземпляра или же значение атрибута должно вычисляться «на лету». В обоих случаях методы получения и установки решат задачу, но за счет потери простого прямого доступа к переменным экземпляра, характерного для Python. В таких ситуациях следует использовать свойство (property). Оно сочетает в себе возможность передачи доступа к переменной экземпляра через аналоги методов получения и установки значения с прямым доступом к переменной экземпляра при помощи точечной нотации.
   15.10. Использование декоратора @property для гибкой работы 381 Для создания свойства используется декоратор @property с методом, имя которого совпадает с именем свойства: class Temperature: def __init__(self): self._temp_fahr = 0 @property def temp(self): return (self._temp_fahr - 32) * 5 / 9 Без метода установки свойство temp доступно только для чтения. Чтобы получить возможность изменять его, необходимо добавить следующий метод установки: @temp.setter def temp(self, new_temp): self._temp_fahr = new_temp * 9 / 5 + 32 Теперь для получения и установки свойства temp можно использовать стандартную точечную нотацию. Обратите внимание: имя метода остается прежним, но декоратор меняется на имя свойства (в данном случае temp), тогда как .setter показывает, что здесь определяется метод установки для свойства temp: t = Temperature() t._temp_fahr 0 t.temp Метод получения возвращает значение в градусах Цельсия -17.77777777777778 t.temp = 34 t._temp_fahr 93.2 Метод установки преобразует значение и присваивает его переменной _temp_fahr t.temp 34.0 Метод получения преобразует значение 0 переменной _temp_fahr перед его возвратом в градусы по Цельсию, а метод установки преобразует значение 34 обратно в градусы по Фаренгейту. Одним из существенных преимуществ возможности добавления свойств в Python является то, что можно сначала разработать программу, используя старые доб­ рые переменные экземпляра, а впоследствии в любой момент легко заменить их на свойства там, где это понадобится, — при этом клиентский код менять не придется. Ведь доступ остается неизменным — в нем по-прежнему применяется точечная нотация.
   382 Глава 15. Классы и объектно-ориентированное программирование Попробуйте сами: свойства Измените размеры в классе Rectangle, преобразовав их в свойства с методами получения и установки, не допускающими использования отрицательных значений. 15.11. Правила определения области видимости и пространств имен для экземпляров класса Теперь у вас есть все необходимое для того, чтобы составить представление о правилах определения области видимости и пространствах имен для экземпляров класса. В методе класса напрямую доступно локальное пространство имен (параметры и переменные, объявленные в методе), глобальное пространство имен (функции и переменные, объявленные на уровне Встроенное модуля) и встроенное пространство имен пространство имен (встроенные функции и встроенные исключения). Поиск в этих трех пространВстроенные Встроенные ствах имен производится в следующем исключения функции порядке: локальное, глобальное и встроенное (рис. 15.1). Глобальное Через переменную self также доступны пространство пространство имен экземпляра (переменимен ные экземпляра, закрытые переменные Функции Переменные экземпляра и переменные экземпляра модуля модуля родительского класса), пространство имен его класса (методы, классовые переменные, закрытые методы и закрытые Локальное классовые переменные) и пространство пространство имен имен его родительского класса (методы и классовые переменные родительского Локальные Параметры класса). Поиск в этих трех пространствах переменные имен осуществляется в следующем порядке: экземпляр, класс, а затем родиРис. 15.1. Непосредственно доступные тельский класс (рис. 15.2). пространства имен К закрытым переменным экземпляра, закрытым методам и закрытым классовым переменным родительского класса нельзя получить доступ с помощью self. Класс может скрывать эти имена от своих дочерних классов. Модуль в листинге 15.3 объединяет эти два примера для наглядной демонстрации того, к чему можно получить доступ из метода.
   15.11. Правила определения области видимости и пространств имен Родительский класс Методы родительского класса Классовые переменные родительского класса (чтение) Классовые переменные родительского класса (запись) Методы Классовые переменные Закрытые методы Закрытые классовые переменные Переменные экземпляра Закрытые переменные экземпляра Переменные экземпляра родительского класса Закрытые переменные экземпляра родительского класса Закрытые классовые переменные родительского класса Класс Экземпляр класса Рис. 15.2. Пространства имен переменной self Листинг 15.3. Файл cs.py """Модуль cs: модуль для демонстрации области видимости класса.""" mod_var ="module variable: mod_var" def mod_func(): return ("module level function: mod_func()") class SuperClass: super_class_var = "superclass class variable: self.super_class_var" __priv_super_class_var = "private superclass class variable: no access" def __init__(self): self.super_instance_var = "superclass instance variable: ➥self.super_instance_var " self.__psiv = "private superclass instance variable: no access" def super_class_method(self): return "superclass method: self.super_class_method()" def superclass_priv_method(self): return "superclass private method: no access" class Class_(SuperClass): class_var = "class variable: self.class_var or Class_.class_var ➥(for assignment)" __priv_class_var = "class private variable: self.__priv_class_var or ➥ Class_.__priv_class_var " def __init__(self): SuperClass.__init__(self) self.__priv_instance_var = "private instance variable: 383
   384 Глава 15. Классы и объектно-ориентированное программирование ➥self.__priv_instance_var" def method_2(self): return "method: self.method_2()" def __priv_method(self): return "private method: self.__priv_method()" def method_1(self, param="parameter: param"): local_var = "local variable: local_var" self.instance_var = "instance variable: self.instance_var" print("Local") print("Access local, global and built-in namespaces directly") print("local namespace:", list(locals().keys())) print(param) Параметр print(local_var) Локальная переменная print() print("global namespace:", list(globals().keys())) print(mod_var) Переменная модуля print(mod_func()) Функция модуля print() print("Access instance, class, and superclass namespaces through ➥'self'") print("Instance namespace:", self.__dict__) print(self.instance_var) Переменная экземпляра print(self.__priv_instance_var) Закрытая переменная экземпляра print(self.super_instance_var) Переменная экземпляра родительского класса print("\nClass_ namespace:", Class_.__dict__) print(self.class_var) Классовая переменная print(self.method_2()) print(self.__priv_class_var) Метод Закрытая классовая переменная print(self.__priv_method()) Закрытый метод print("\nSuperclass namespace:", SuperClass.__dict__) print(self.super_class_method()) Метод родительского класса print(self.super_class_var) Классовая переменная родительского класса через экземпляр Вывод получается довольно длинным, поэтому рассмотрим его по частям. В первой части локальное пространство имен метода method_1 класса Class_ содержит параметры self (которая является переменной экземпляра) и param, а также локальную переменную local_var (к которым можно получить прямой доступ): import cs c = cs.Class_() c.method_1() Access local, global and built-in namespaces directly local namespace: ['self', 'param', 'local_var'] parameter: param local variable: local_var
   15.11. Правила определения области видимости и пространств имен 385 Далее, глобальное пространство имен метода method_1 содержит переменную модуля mod_var и функцию модуля mod_func (которая, как описано в предыдущем разделе, может использоваться для предоставления функциональности метода класса). Также имеются классы, определенные в модуле (класс Class_ и родительский класс SuperClass). Ко всем этим классам можно получить прямой доступ: global namespace: ['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__builtin__', '__builtins__', 'mod_var', 'mod_func', 'SuperClass', ] module variable: mod_var module level function: mod_func() Пространство имен экземпляра c содержит переменную экземпляра instance_var и переменную экземпляра родительского класса super_instance_var (которая, как описано в предыдущем разделе, ничем не отличается от обычной переменной экземпляра). Оно также содержит искаженное имя закрытой переменной экземпляра __piv (к которой можно получить доступ через self) и искаженное имя закрытой переменной экземпляра родительского класса __psiv (к которой нельзя получить доступ): Access instance, class, and superclass namespaces through 'self' Instance namespace: {'super_instance_var': 'superclass instance variable: self.super_instance_var ', '_SuperClass__psiv': 'private superclass instance variable: no access', '_Class___priv_instance_var': 'private instance variable: self.__priv_instance_var', 'instance_var': 'instance variable: self.instance_var'} instance variable: self.instance_var private instance variable: self.__priv_instance_var superclass instance variable: self.super_instance_var Пространство имен класса Class_ содержит классовую переменную class_var и искаженное имя закрытой классовой переменной __priv_class_var. К обеим переменным можно получить доступ через self, но для присваивания им значений необходимо использовать класс Class_. Он также содержит два метода класса method_1 и method_2, и еще искаженное имя закрытого метода __priv_method (к которому можно получить доступ через self): Class_ namespace: {'__module__': '__main__', 'class_var': 'class variable: self.class_var or Class_.class_var (for assignment)', '_Class___priv_class_var': 'class private variable: self.__priv_class_var or Class_.__priv_class_var ', '__init__': <function Class_.__init__ at 0x7b4b3e3dbeb0>, 'method_2': <function Class_.method_2 at 0x7b4b3e3db640>, '_Class___priv_method': <function Class_.__priv_method at 0x7b4b3e3da560>, 'method_1': <function Class_.method_1 at 0x7b4b3e3db520>, '__doc__': None} class variable: self.class_var or Class_.class_var (for assignment) method: self.method_2() class private variable: self.__priv_class_var or Class_.__priv_class_var private method: self.__priv_method() Наконец, пространство имен родительского класса SuperClass содержит классовую переменную родительского класса super_class_var (к которой можно получить доступ через self, но для присваивания ей значения необходимо
   386 Глава 15. Классы и объектно-ориентированное программирование использовать родительский класс SuperClass) и метод родительского класса super_class_method. Оно также содержит искаженные имена закрытого метода родительского класса superclass_priv_method и закрытой классовой переменной родительского класса __priv_super_class_var; к двум последним нельзя получить доступ через self: Superclass namespace: {'__module__': '__main__', 'super_class_var': 'superclass class variable: self.super_class_var', '_SuperClass__priv_super_class_var': 'private superclass class variable: no access', '__init__': <function SuperClass.__init__ at 0x7b4b3e3d9360>, 'super_class_method': <function SuperClass.super_class_method at 0x7b4b3e3d9480>, 'superclass_priv_method': <function SuperClass.superclass_priv_method at 0x7b4b3e3d9120>, '__dict__': <attribute '__dict__' of 'SuperClass' objects>, '__weakref__': <attribute '__weakref__' of 'SuperClass' objects>, '__doc__': None} superclass method: self.super_class_method() superclass class variable: self.super_class_var Чтобы полностью разобраться в этом примере, потребуется некоторое время. Можно использовать его для справки или сделать отправной точкой для собственных исследований. Как и в случае с большинством других концепций в Python, вы можете закрепить материал, поэкспериментировав с несколькими упрощенными примерами. 15.12. Деструкторы и управление памятью Ранее вам уже встречались инициализаторы классов (методы __init__). Для класса также можно определить деструктор. Однако, в отличие от C++, совсем не обязательно создавать и вызывать деструктор, чтобы гарантированно освободить память, используемую экземпляром. Python предоставляет автоматическое управление памятью на базе механизма подсчета ссылок. Иными словами, Python отслеживает количество ссылок на экземпляр; когда это число достигнет нуля, память, выделенная под этот экземпляр, освобождается, а у всех объектов Python, на которые он ссылался, счетчики ссылок уменьшаются на 1. Таким образом, вам почти никогда не придется определять деструктор. Впрочем, время от времени может возникнуть ситуация, когда необходимо явным образом освободить память из-под внешнего ресурса при удалении объекта. В таких случаях лучше всего воспользоваться контекстным менеджером, обсуждавшимся в главе 14. Как упоминалось ранее, модуль contextlib из стандартной библиотеки позволяет создать пользовательский контекстный менеджер для вашей конкретной ситуации. 15.13. Множественное наследование Компилируемые языки накладывают строгие ограничения на применение множественного наследования — то есть способности объектов наследовать данные
   15.13. Множественное наследование 387 и поведение от более чем одного родительского класса. Например, правила множественного наследования в C++ настолько сложны, что многие разработчики предпочитают обходиться без него. В языке Java, хотя в нем и реализован механизм интерфейсов, множественное наследование запрещено. Python не устанавливает подобные ограничения для множественного наследования. Класс может наследовать от любого количества родительских классов точно так же, как и от одного родителского класса. В самом простом случае ни один из задействованных классов — включая те, что наследуются косвенно через родительский класс, — не содержит переменных экземпляра или методов с совпадающими именами. В этом случае наследующий класс функционирует как синтез собственных определений и всех определений, унаследованных от его родительских классов. Допустим, класс A наследует от классов B, C и D, класс B наследует от классов E и F, а класс D наследует от класса G (рис. 15.3). Также предположим, что в этих классах нет одноименных методов. В таком случае экземпляр класса A может использоваться так, как Рис. 15.3. Иерархия если бы он был экземпляром любого из классов наследования B–G, а также класса A; экземпляр класса B может использоваться как экземпляр классов E или F, а также класса B; наконец, экземпляр класса D может использоваться как экземпляр класса G и класса D. В форме программного кода определения классов выглядят так: class E: . . . class F: . . . class G: . . . class D(G): . . . class C: . . . class B(E, F): . . . class A(B, C, D): . . . Ситуация усложняется, когда некоторые классы содержат одноименные методы, поскольку в таком случае интерпретатору Python приходится решать, какое из совпадающих имен следует выбрать. Предположим, вы хотите разрешить вызов метода a.f() для экземпляра a класса A, причем метод f не определен в A, но задан во всех классах F, C и G. Какой метод будет вызван?
   388 Глава 15. Классы и объектно-ориентированное программирование Ответ определяется тем порядком, в котором Python просматривает базовые классы1 при поиске метода, не определенного в исходном классе, для которого этот метод был вызыван. В простейших случаях Python перебирает базовые классы исходного класса слева направо, но он всегда просматривает все выше­ стоящие классы (классы-предки) базового класса перед тем, как переходить к следующему базовому классу. При попытке выполнить вызов a.f() поиск осуществляется примерно так. 1. Сначала Python ищет метод в классе объекта, для которого вызывается метод, то есть в классе A. 2. Поскольку в классе A метод f не определен, Python переходит к поиску в базовых классах класса A. Первым базовым классом для A является класс B, поэтому Python начинает поиск в B. 3. Поскольку в классе B метод f тоже не определен, Python продолжает поиск в базовых классах класса B. Поиск начинается с первого базового класса для B, то есть с класса E. 4. Поскольку в классе E метод f также не определен, а сам класс не имеет базовых классов, поиск в E на этом заканчивается. Python возвращается к классу B и продолжает искать в следующем базовом классе для B, то есть в классе F. Класс F действительно содержит метод f, и поскольку это был первый найденный метод с заданным именем, именно этот метод и будет задействован. Методы с именем f в классах C и G игнорируются. Безусловно, подобная внутренняя логика не способствует созданию самого понятного или простого в сопровождении программного кода. С более сложными иерархиями в игру вступают дополнительные факторы, обеспечивающие, с одной стороны, недопущение повторного обхода одного и того же класса, а с другой — поддержку согласованной работы вызовов через super. Вероятно, приведенная выше иерархия сложнее тех, что будут встречаться вам на практике. Если придерживаться более стандартных подходов ко множественному наследованию — таких, как создание классов-примесей (mixin classes) или 1 В контексте ООП и Python термины «базовый класс» и «родительский класс» являются синонимами и используются для обозначения класса, от которого происходит наследование. Однако между ними есть небольшие различия. 1. Родительский класс, или суперкласс (parent class), — этот термин чаще всего используется, когда подразумевают прямого родителя (того, кто указан в скобках при создании класса). Если class B(A):, то A — прямой родитель для B. 2. Базовый класс (base class) — этот термин подчеркивает, что класс служит фундаментом (базой) для создания новых классов. Часто используется как синоним родительского, но может означать и любой класс выше по иерархии. 3. Вышестоящий класс, или класс-предок (ancestor class), — это любой класс выше по дереву наследования: родитель, родитель родителя («дедушка») и так далее, вплоть до самого верхнего класса. — Примеч. ред.
   15.14. Практическая работа: HTML-классы 389 классов-дополнений (addin classes), — можно легко сохранить читаемость кода и избежать конфликтов имен. Некоторые считают, что множественное наследование — это плохо. Им и правда можно злоупотребить, однако Python никоим образом не заставляет вас к нему прибегать. Одна из самых серьезных опасностей — это чрезмерно глубокие иерархии наследования, и в некоторых случаях множественное наследование как раз помогает избежать этой проблемы. Так или иначе, эта тема выходит за рамки данной книги. Приведенный здесь пример лишь демонстрирует, как множественное наследование работает в Python, и не пытается объяснить варианты его использования (например, в классах-примесях или классах-дополнениях). 15.14. Практическая работа: HTML-классы В этой практической работе мы создадим классы для представления HTMLдокумента. Чтобы упростить задачу, будем считать, что каждый элемент может содержать только текст и один подэлемент. Так, элемент <html> содержит лишь элемент <body>, а элемент <body> — (необязательный) текст и элемент <p>, содержащий только текст. Главное, что необходимо реализовать, — это метод __str__(), который, в свою очередь, вызывает метод __str__() своего подэлемента, так что при вызове функции str() для элемента <html> возвращается весь документ. Можно предположить, что весь текст элемента предшествует подэлементу. Пример вывода с использованием классов: para = p(text="this is some body text") doc_body = body(text="This is the body", subelement=para) doc = html(subelement=doc_body) print(doc) <html> <body> This is the body <p> this is some body text </p> </body> </html> 15.14.1. Решение задачи при помощи кода, сгенерированного ИИ Эта задача хорошо подходит для решения при помощи средств ИИ в том виде, в котором она сформулирована, поскольку в ней указано, как именно ее нужно решать, и приведены примеры программного кода и вывода. Результаты это подтверждают — ИИ-ассистент справился с этой задачей лучше, чем с любой из предыдущих.
   390 Глава 15. Классы и объектно-ориентированное программирование 15.14.2. Решения и обсуждение Суть этой задачи заключается в применении возможностей механизма наследования для того, чтобы избежать дублирования кода и задействовать объектноориентированный подход для моделирования необходимого поведения объектов при решении задачи. Это подразумевает, что основная часть логики должна быть реализована в базовом классе, тогда как подклассы в идеале должны содержать минимум программного кода. Программное решение, созданное человеком В предложенном мною решении основная логика обработки сосредоточена в базовом классе element, а созданные затем подклассы не содержат дополнительного кода и полностью опираются на функциональность, предоставляемую базовым классом: class element: def __init__(self, text=None, subelement=None): self.subelement = subelement self.text = text def __str__(self): value = f"<{self.__class__.__name__}>\n" if self.text: value += f"{self.text}\n" if self.subelement: value += str(self.subelement) value += f"</{self.__class__.__name__}>\n" return value class html(element): pass class body(element): pass Получает класс экземпляра, а затем имя класса Оператор pass минимально необходим для создания «пустого» класса class p(element): pass #тест para = p(text="this is some body text") doc_body = body(text="This is the body", subelement=para) doc = html(subelement=doc_body) print(doc) Как отмечалось выше, такое решение помещает весь код в базовый класс element, что упрощает внесение изменений в будущем. Также в качестве текста соответствующих HTML-тегов применяются сами имена классов, благодаря чему добавление новых классов для поддержки других HTML-элементов станет совсем несложным. Поскольку я привыкла записывать HTML-теги строчными буквами, имена классов у меня тоже оформлены в нижнем регистре, что слегка нарушает стандартный стиль Python, согласно которому имена классов должны
   15.14. Практическая работа: HTML-классы 391 начинаться с заглавной буквы. В данном случае я бы сослалась на «Дзен Python», где сказано: «Практичность важнее чистоты», — но боюсь, что многие со мной не согласятся. Программные решения, сгенерированные ИИ Промпт для решения Copilot был весьма похож на постановку предыдущей задачи: Создай классы для представления HTML-документа, предполагая при этом, что каждый элемент может содержать только текст и один подэлемент. Следовательно, элемент <html> содержит только элемент <body>, а элемент <body> содержит (необязательный) текст и элемент <p>, содержащий только текст. Главное, что требуется реализовать, — это метод str(). Он, в свою очередь, вызывает метод str() своего подэлемента, так что при вызове функции str() для элемента возвращается весь HTML-документ. Предполагается, что текст всегда предшествует подэлементу. Ниже приведен пример вывода при использовании классов: para = p(text="this is some body text") doc_body = body(text="This is the body", subelement=para) doc = html(subelement=doc_body) print(doc) Copilot составил следующий план. 1. Создать базовый класс Element с методом __str__, который возвращает текст и строковое представление подэлемента. 2. Создать классы Html, Body и P, наследующие от Element. 3. Каждый из этих классов должен иметь атрибут класса tag, представляющий HTML-тег. 4. Метод __str__ в базовом классе должен формировать HTML-строку, опираясь на значение tag. Реализация, предложенная Copilot, напоминает мое решение, за исключением пары стилистических отличий: class Element: tag = "" def __init__(self, text="", subelement=None): self.text = text self.subelement = subelement def __str__(self): result = f"<{self.tag}>\n" if self.text: result += self.text + "\n" if self.subelement: result += str(self.subelement) result += f"</{self.tag}>\n" return result class Html(Element): tag = "html"
   392 Глава 15. Классы и объектно-ориентированное программирование class Body(Element): tag = "body" class P(Element): tag = "p" # Использование: para = P(text="this is some body text") doc_body = Body(text="This is the body", subelement=para) doc = Html(subelement=doc_body) print(doc) Представленный код вполне хорош, и я бы ничего в нем менять не стала. Он аналогичен решению, написанному программистом, поскольку основная функциональность сосредоточена в базовом классе. Существует лишь пара отличий от варианта, предложенного мной. В частности, здесь соблюдено принятое в Python соглашение: имена классов начинаются с заглавной буквы. Учитывая, что в исходном примере HTML-теги были записаны в нижнем регистре, решение использовать атрибут класса tag для хранения имени тега в нижнем регистре выглядит вполне разумным. Такой подход абсолютно приемлем — он не влияет ни на объем, ни на производительность кода, и, как по мне, удобочитаемость тоже не страдает. Так держать, Copilot! А вот версия от Colaboratory откровенно разочаровала. Поскольку окно ввода промпта для генерации кода в Colaboratory ограничено 400 символами, а промпт, использованный для Copilot, был почти вдвое длиннее, формулировка задачи была помещена в ячейку выше, и Colaboratory получила следующий промпт: Сгенерируй код, описанный в ячейке выше. Полученный по данному промпту результат однозначно свидетельствует о том, что модель восприняла задание корректно. Тем не менее представленное решение содержит значительное количество дублирующегося кода и не реализует преимущества, предоставляемые механизмом наследования. class html: def __init__(self, subelement): self.subelement = subelement def __str__(self): return f'<html>\n{str(self.subelement)}\n</html>' class body: def __init__(self, text, subelement=None): self.text = text self.subelement = subelement def __str__(self): sub = '' if self.subelement: sub = f'\n{str(self.subelement)}' return f'<body>\n{self.text}{sub}\n</body>' class p: def __init__(self, text):
   Итоги 393 self.text = text def __str__(self): return f'<p>\n{self.text}\n</p>' para = p(text="this is some body text") doc_body = body(text="This is the body", subelement=para) doc = html(subelement=doc_body) print(doc) На самом деле, это был единственный из четырех предложенных вариантов, в котором использовались f-строки, — остальные применяли конкатенацию при помощи оператора +, хотя в целом следовали тому же подходу. Что важнее, в этом решении для каждого тега создается отдельный класс, но при этом отсутствует общий базовый класс, от которого наследовались бы все элементы. Это означает, что обработка дублируется и различия между тремя элементами минимальны. Представленный код производит впечатление решения, написанного начинающим разработчиком, и в таком виде он точно не прошел бы формальную проверку качества кода. Итоги Определение класса фактически означает создание нового типа данных. Метод __init__ используется для инициализации данных при создании нового экземпляра класса, но в языке Python он не является конструктором в полном смысле (хотя для 99 % задач работает именно как конструктор, и его часто так называют). Параметр self ссылается на текущий экземпляр класса и передается в качестве первого параметра методам класса. Статические методы могут вызываться без создания экземпляра класса, поэтому они не получают параметр self. Методам класса передается параметр cls, который является ссылкой на класс вместо self. Все методы языка Python являются виртуальными. Иначе говоря, если метод не переопределяется в подклассе и не является закрытым для родительского класса, он доступен для всех подклассов. Классовые переменные наследуются от родительских классов, если только они не начинаются с двух символов подчеркивания (__) — в этом случае они являются закрытыми и не видны подклассам напрямую. Методы могут быть объявлены закрытыми аналогичным образом. Свойства позволяют реализовать атрибуты с определенными методами получения (геттер) и установки (сеттер) значения, но при этом они ведут себя как обычные атрибуты экземпляра. В языке Python поддерживается множественное наследование, которое часто используется с классами-примесями.
16 Регулярные выражения В этой главе: 3 Основные сведения о регулярных выражениях 3 Создание регулярных выражений со специальными символами 3 Использование сырых (необработанных) строк в регулярных выражениях 3 Извлечение из строк текста, совпадающего с шаблоном 3 Замена текста при помощи регулярных выражений У кого-то может возникнуть вопрос, почему в этой книге вообще рассматриваются регулярные выражения. Они реализуются всего одним модулем Python. Будучи достаточно специализированным и продвинутым инструментом, регулярные выражения не включаются даже в стандартную библиотеку в таких языках, как C или Java. Однако, если вы пишете на Python, то, скорее всего, вы занимаетесь парсингом текста. И в таком случае регулярные выражения слишком полезны, чтобы ими пренебрегать. Если вы используете Perl, Tcl или Linux/UNIX, то, возможно, вы уже знакомы с регулярными выражениями; если нет — в этой главе мы подробно познакомимся с ними. 16.1. Что такое регулярное выражение? Регулярное выражение (regular expression, сокращенно regex) представляет собой способ распознавания, а часто и извлечения данных из текста по определенным
   16.2. Регулярные выражения со специальными символами 395 шаблонам. Если регулярное выражение распознает фрагмент текста или строку, говорят, что оно соответствует этому тексту или строке. Регулярное выражение определяется строкой, в которой некоторые символы (называемые метасимволами) могут иметь особое значение, что позволяет одному регулярному выражению соответствовать множеству различных конкретных строк. Впрочем, это проще понять на примере, чем из сухих объяснений. Следующая программа при помощи регулярного выражения подсчитывает, сколько строк в текстовом файле содержит слово hello. Строка, в которой слово hello встречается более одного раза, учитывается лишь один раз: import re regexp = re.compile("hello") count = 0 file = open("textfile", 'r') for line in file.readlines(): if regexp.search(line): count = count + 1 file.close() print(count) Программа начинается с импорта модуля регулярных выражений Python под названием re. Затем она принимает текстовую строку "hello" в качестве текстового регулярного выражения и преобразует ее в скомпилированное регулярное выражение функцией re.compile. Компиляция не является строго необходимой, тем не менее скомпилированные регулярные выражения могут заметно ускорить выполнение кода, поэтому они почти всегда используются в программах, обрабатывающих большие объемы текста. Для чего может использоваться регулярное выражение, скомпилированное из строки "hello"? Для распознавания вхождений слова "hello" в строках текста; иными словами, с его помощью можно установить, содержит ли строка подстроку "hello". Эта задача решается методом search, который возвращает None, если регулярное выражение не найдено в строковом аргументе; Python интерпретирует None как False в логическом контексте. Если совпадение найдено, Python возвращает специальный объект, при помощи которого можно получить различную информацию о совпадении (к примеру, в какой позиции строки оно было обнаружено). Эту тему мы обсудим чуть позднее. 16.2. Регулярные выражения со специальными символами У предыдущего примера есть один недостаток: он подсчитывает, сколько строк содержат "hello", но игнорирует строки с "Hello", поскольку при поиске совпадений учитывается регистр символов. Одно из возможных решений заключается в том, чтобы взять два регулярных выражения — одно для "hello", другое для "Hello" — и сопоставить каждую
   396 Глава 16. Регулярные выражения строку с обоими. Тем не менее лучше воспользоваться более продвинутыми возможностями регулярных выражений. Приведите вторую строку к следующему виду: regexp = re.compile("hello|Hello") В этом регулярном выражении используется специальный символ | (вертикальная черта). Специальные символы в регулярных выражениях не интерпретируются буквально, а имеют особое значение. Так, метазнак | означает «или», поэтому регулярное выражение соответствует "hello" или "Hello". Эту же задачу можно решить и так: regexp = re.compile("(h|H)ello") В дополнение к | это регулярное выражение использует круглые скобки для группировки, это в данном случае означает, что | выбирает между строчной и прописной буквой H. Полученное регулярное выражение соответствует букве h или H, за которой следует ello. Еще один способ выполнения того же поиска выглядит так: regexp = re.compile("[hH]ello") Специальные символы квадратные скобки [ и ] принимают строку символов между ними и соответствуют любому отдельному символу из этой строки. Существует специальная сокращенная запись для обозначения диапазонов символов внутри квадратных скобок [ и ]. Например, диапазон [a-z] соответствует одиночному символу от a до z, а [0-9A-Z] — любой цифре или символу верхнего регистра и т. д. Иногда в диапазон [] (символьный класс) требуется включить обычный дефис; в этом случае его следует поставить на первое место, чтобы он не воспринимался как начало диапазона: [-012] соответствует дефису, 0, 1 или 2 и ничему другому. В регулярных выражениях Python доступно довольно много специальных символов, и описание всех нюансов их применения выходит за рамки данной книги. Полный перечень специальных символов, доступных в регулярных выражениях Python, а также описание их значений приведены в онлайн-документации модуля регулярных выражений re стандартной библиотеки. В оставшейся части этой главы я буду описывать специальные символы по мере их появления. Быстрая проверка: специальные символы в регулярных выражениях Какое регулярное выражение вы бы использовали для поиска строк, представляющих числа от −5 до 5? Каким регулярным выражением вы бы воспользовались, чтобы найти шестнадцатеричную цифру? Предположим, что допустимыми шестнадцатеричными цифрами являются 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, A, a, B, b, C, c, D, d, E, e, F, f.
   16.3. Регулярные выражения и сырые (необработанные) строки 397 16.3. Регулярные выражения и сырые (необработанные) строки Функции, которые компилируют регулярные выражения или ищут соответствия с ними, интерпретируют определенные символьные последовательности в строках как специальные конструкции, имеющие особое значение в контексте регулярных выражений. К примеру, функции регулярных выражений regex понимают, что \n обозначает символ новой строки. Но если в качестве регулярных выражений используются обычные строки Python, функции regex, как правило, не распознают такие специальные последовательности, так как многие из этих последовательностей обладают особым значением и в обычных строках. Тот же \n означает новую строку в контексте обычных строк Python, и интерпретатор автоматически заменяет строковую последовательность \n символом новой строки до того, как функция regex ее увидит. В результате функция regex компилирует строки с вложенными управляющими символами новой строки, а не с вложенными последовательностями \n. В случае с \n эта ситуация ни на что не влияет, поскольку функции regex корректно интерпретируют символ новой строки и делают именно то, что от них ожидается: они ищут для него соответствие другим символам новой строки в заданном тексте. Теперь рассмотрим следующую специальную последовательность \\, которая обозначает одинарную обратную косую черту \ в регулярных выражениях. Допустим, вы хотите проверить текст на вхождение строки "\ten". Поскольку вы уже знаете, что для представления символа обратной косой черты служит последовательность \\, можно попытаться поступить так: regexp = re.compile("\\ten") Этот пример компилируется без ошибок, но результат будет неверным. Проблема в том, что комбинация \\ также обозначает один символ \ в строках Python. До вызова re.compile Python интерпретирует введенную строку как \ten — именно это значение и передается re.compile. В контексте регулярных выражений \t означает символ табуляции, так что скомпилированное регулярное выражение будет искать символ табуляции, за которым следуют два символа en. Чтобы решить эту проблему при использовании обычных строк Python, потребуется последовательность из четырех символов \\\\. Python интерпретирует первые два символа как специальную последовательность, обозначающую один символ \. То же самое происходит со второй парой \\, в результате чего в строке Python фактически оказываются два символа \\. Затем эта строка передается функции re.compile, которая интерпретирует эти две фактические обратные косые черты \\ как специальную последовательность регулярного выражения, обозначающую одну обратную косую черту \. В итоге мы имеем следующий код: regexp = re.compile("\\\\ten")
   398 Глава 16. Регулярные выражения Все это выглядит как головоломка, поэтому в Python предусмотрен способ определения строк, не применяющий обычные правила Python к специальным символам. Строки, определенные подобным способом, называются сырыми (необработанными) (raw strings). 16.3.1. Сырые строки спешат на помощь Сырая строка выглядят аналогично обычной строке, за исключением того, что начальной кавычке строки предшествует символ r. Ниже приведены несколько примеров сырых строк: r"Hello" r"""\tTo be\n\tor not to be""" r'Goodbye' r'''12345''' Как видите, можно оформлять сырые строки как в одиночных, так и в двойных кавычках, используя при этом обычный формат или тройные кавычки. Также при желании вместо строчной r можно использовать заглавную R. Какой бы вариант вы ни выбрали, запись сырой строки может рассматриваться как инструкция для Python, означающая: «Не обрабатывать специальные последовательности в этой строке». В примерах выше все сырые строки эквивалентны своим обычным строковым аналогам, за исключением второго примера, в котором последовательности \t и \n не интерпретируются как табуляция и новая строка, а остаются в виде последовательностей из двух символов, начинающихся с символа обратной косой черты \. Сырые строки не являются отдельным типом строк — это всего лишь иной способ определения строк. Чтобы наглядно в этом убедиться, выполните несколько примеров в интерактивном режиме: r"Hello" == "Hello" True r"\the" == "\\the" True r"\the" == "\the" False print(r"\the") \the print("\the") he
   16.4. Извлечение из строк текста, совпадающего с шаблоном 399 При использовании сырых строк с регулярными выражениями вам не придется беспокоиться о неожиданных взаимодействиях между специальными последовательностями строк и специальными последовательностями регулярных выражений. Используются только специальные последовательности регулярных выражений. При этом предыдущий пример с регулярным выражением принимает вид regexp = re.compile(r"\\ten") и работает как положено. Скомпилированное регулярное выражение ищет одну обратную косую черту \, за которой следуют буквы ten. Поэтому необходимо выработать привычку применять сырые строки везде, где определяются регулярные выражения. Именно так мы и будем поступать в оставшейся части этой главы. 16.4. Извлечение из строк текста, совпадающего с шаблоном Одно из распространенных применений регулярных выражений — простой парсинг текста по шаблону. Это один из тех навыков, которыми вы должны овладеть, к тому же это отличный способ запомнить как можно больше специальных символов регулярных выражений. Допустим, у вас имеется текстовый файл со списком имен людей и их телефонных номеров. Каждая строка файла выглядит так: фамилия, имя отчество: телефон За фамилией следуют запятая и пробел, далее идут имя, пробел, отчество, двое­ точие, пробел и телефон. Однако ситуация усложняется: отчество может отсутствовать, а номер телефона может быть записан в различных форматах, в зависимости от страны и предпочтений пользователя. В рамках задачи будем считать, что все телефонные номера представлены в формате США/Канады: трехзначный код региона, трехзначный код станции и четырехзначный абонентский номер, разделенные дефисами (например, 800-123-4567). При этом код региона может отсутствовать (то есть номер может иметь форму 800-123-4567 либо 123-4567). Вы могли бы, конечно, написать программу, которая явным образом извлекает данные из строки в указанном формате, однако такое решение было бы весьма трудоемким и чревато ошибками. Благодаря регулярным выражениям подобного рода задачи решаются куда проще. Начнем с составления регулярного выражения, соответствующего строкам в заданном формате. В ближайших абзацах встретится несколько новых для вас специальных символов. Не беда, если с первого раза вам не удастся уяснить значение каждого из них; пока что главное — понять суть происходящего.
   400 Глава 16. Регулярные выражения В целях упрощения допустим, что фамилия, имя и отчество состоят из букв и, возможно, дефисов. Можно использовать квадратные скобки [], о назначении которых рассказывалось ранее, чтобы задать шаблон, включающий только символы, допустимые в именах: [-a-zA-Z] Этот шаблон соответствует одному дефису, одной строчной букве или одной прописной букве. Чтобы соответствовать имени целиком (например, McDonald), необходимо повторить этот шаблон. Метасимвол + повторяет то, что ему предшествует, один или более раз по мере необходимости для сопоставления с обрабатываемой строкой. Таким образом, шаблон [-a-zA-Z]+ соответствует одному имени — Kenneth, McDonald или Perkin-Elmer. Он также соответствует некоторым строкам, не являющимся именами, таким как --- или -a-b-c-, однако для целей нашего примера это допустимо. А что с телефонным номером? Специальная последовательность \d соответствует любой цифре, а дефис вне квадратных скобок [] обозначает обычный дефис. Хороший шаблон для телефонного номера выглядит так: \d\d\d-\d\d\d-\d\d\d\d Три цифры, за которыми следует дефис, затем три цифры и после них еще один дефис, а после него четыре цифры. Этот шаблон соответствует только телефонным номерам, включающим код региона, а в вашем списке могут быть номера, у которых этого кода нет. Поэтому лучшим решением будет заключить часть шаблона с кодом региона в круглые скобки, тем самым объединив ее в группу, и поставить после этой группы специальный символ ?, означающий, что компонент выражения, находящийся непосредственно перед ?, является необязательным: (\d\d\d-)?\d\d\d-\d\d\d\d Этот шаблон соответствует телефонному номеру, который может содержать (или не содержать) код региона. Аналогичный прием позволит вашему выражению учесть тот факт, что у одних людей из списка есть отчество (или инициалы), а у других его нет. (Чтобы сделать отчество необязательным, примените группировку и специальный символ ?.) Фигурные скобки {} позволяют указать количество повторений шаблона, поэтому предыдущие примеры шаблона для телефонного номера можно записать так: (\d{3}-)?\d{3}-\d{4} Этот шаблон также подразумевает необязательную группу из трех цифр с последующим дефисом, за которой идут три цифры, дефис и еще четыре цифры.
   16.4. Извлечение из строк текста, совпадающего с шаблоном 401 Запятые, двоеточия и пробелы не имеют специального значения в регулярных выражениях; они обозначают сами себя. Собрав все воедино, мы получаем шаблон следующего вида: [-a-zA-Z]+, [-a-zA-Z]+( [-a-zA-Z]+)?: (\d{3}-)?\d{3}-\d{4} Вероятно, реальный шаблон будет более сложным, так как нельзя полагаться на то, что после запятой следует ровно один пробел, после имени и отчества — ровно один пробел и ровно один пробел после двоеточия. Впрочем, эти уточнения нетрудно добавить позднее. Этот шаблон позволяет проверить, имеет ли строка ожидаемый формат, однако проблема в том, что мы еще не умеем извлекать с его помощью данные. Поэтому пока все, что мы можем, — это написать программу вроде этой: import re Фамилия и запятая regexp = re.compile(r"[-a-zA-Z]+," Имя r" [-a-zA-Z]+" r"( [-a-zA-Z]+)?" Отчество (необязательно) r": (\d{3}-)?\d{3}-\d{4}" Двоеточие и номер телефона ) file = open("textfile", 'r') for line in file.readlines(): if regexp.search(line): print("Yeah, I found a line with a name and number. So what?") file.close() Заметьте, что регулярное выражение разбито на части благодаря тому, что Python неявным образом объединяет любые наборы строк, разделенные пробельными символами. С ростом шаблона такой прием существенно упростит его поддержку и понимание. Он также решает возможную проблему излишней длины строки, выходящей за правый край экрана. К счастью, регулярные выражения позволяют не только проверять наличие совпадений, но и извлекать данные из шаблонов. Первым шагом должна стать группировка каждого подшаблона, соответствующего извлекаемому фрагменту данных, при помощи круглых скобок (). Затем каждому подшаблону присваивается уникальное имя при помощи специальной последовательности ?P<имя>, как показано ниже: (?P<last>[-a-zA-Z]+), (?P<first>[-a-zA-Z]+)( (?P<middle>([-a-zA-Z]+)))?: (?P<phone>(\d{3}-)?\d{3}-\d{4} (Обратите внимание: это выражение должно вводиться без разбивки на строки. Из-за нехватки места на печатной странице мы не можем привести его в надлежащем виде.) Во избежание путаницы: вопросительные знаки в ?P<...> и специальные символы ?, указывающие, что отчество и код региона не являются обязательными,
   402 Глава 16. Регулярные выражения не имеют между собой ничего общего. То, что они обозначаются одним символом, — всего лишь досадное совпадение. Теперь, когда элементам шаблона присвоены имена, можно извлечь для них соответствия при помощи метода group. Дело в том, что при успешном соответствии функция search возвращает не только значение True, но и структуру данных с информацией о соответствии. Вы можете написать простую программу для извлечения имен и телефонов из вашего списка и их вывода вот так: import re Фамилия и запятая regexp = re.compile(r"(?P<last>[-a-zA-Z]+)," r" (?P<first>[-a-zA-Z]+)" Имя r"( (?P<middle>([-a-zA-Z]+)))?" Отчество (необязательно) r": (?P<phone>((\d{3}-)?\d{3}-\d{4}))" Двоеточие и номер телефона ) file = open("textfile", 'r') for line in file.readlines(): result = regexp.search(line) if result == None: Соответствий не обнаружено print("Oops, I don't think this is a record") else: last_name = result.group('last') first_name = result.group('first') middle_name = result.group('middle') if middle_name == None: middle_name = "" phone_number = result.group('phone') print(f"Name: {first_name} {middle_name} {last_name} Number: {phone_number}") file.close() Ряд важных моментов. Чтобы узнать, найдено ли соответствие, можно проверить значение, возвращаемое search. Если оно равно None, то соответствие не найдено; в противном случае соответствие найдено и можно извлечь информацию из объекта, возвращаемого search. Метод group используется для извлечения данных, соответствующих именованным подшаблонам. При вызове передается имя интересующего вас подшаблона. Поскольку подшаблон middle (отчество) не является обязательным, нельзя рассчитывать на то, что ему будет соответствовать значение — даже если поиск соответствий в целом для шаблона был успешным. Так, если соответствие найдено, но совпадений по отчеству нет, при использовании group для доступа к данным подшаблона отчества возвращается значение None. Одна часть телефонного номера является обязательной, а другая — нет. Если соответствие найдено, подшаблону phone обязательно будет соответствовать некий связанный с ним текст, поэтому вам не нужно беспокоиться о том, что его значение будет None.
   16.5. Замена текста при помощи регулярных выражений 403 Попробуйте сами: извлечение совпавшего текста Для международных звонков обычно требуется символ + и код страны. Предположим, что код страны состоит из двух цифр. Как бы вы изменили приведенный выше программный код, чтобы извлекать + и код страны из телефонного номера? (Опять же, код страны присутствует не во всех номерах.) Как бы вы изменили программный код, чтобы он обрабатывал коды стран длиной от одной до трех цифр? 16.5. Замена текста при помощи регулярных выражений Помимо извлечения строк из текста, модуль регулярных выражений Python также предназначен для поиска строк в тексте и замены их другими строками. Для выполнения этой задачи используется метод замены sub. В следующем примере все вхождения "the the" (распространенная опечатка) заменяются на одинарные вхождения "the": import re string = "If the the problem is textual, use the the re module" pattern = r"the the" regexp = re.compile(pattern) regexp.sub("the", string) 'If the problem is textual, use the re module' Метод sub использует объект регулярного выражения, для которого он был вызван (в данном случае regexp), для сканирования своего второго аргумента (string в нашем примере) и создает новую строку, заменяя все совпадающие подстроки значением первого аргумента (в данном примере "the"). 16.5.1. Использование функции с методом sub А что, если вы хотите заменить совпавшие подстроки на новые, производные от их собственных значений? Вот тут и проявляется изящество языка Python. Первый аргумент sub — заменяющая подстрока ("the" в примере выше) — может вообще не быть строкой. На его месте может быть и функция. Если это функция, Python вызывает ее для текущего объекта соответствия match, а затем дает возможность функции вычислить и вернуть заменяющую строку. Чтобы увидеть эту функцию в действии, создадим пример, который принимает строку с целочисленными значениями (без десятичной точки и дробной части) и возвращает строку с теми же числовыми значениями, но уже в виде чисел с плавающей точкой (с точкой и нулем в конце): import re int_string = "1 2 3 4 5" def int_match_to_float(match_obj):
   404 Глава 16. Регулярные выражения return(match_obj.group('num') + ".0") pattern = r"(?P<num>[0-9]+)" regexp = re.compile(pattern) regexp.sub(int_match_to_float, int_string) '1.0 2.0 3.0 4.0 5.0' В данном случае шаблон ищет число, состоящее из одной или нескольких цифр (часть [0-9]+). Но этой группе также присваивается имя (часть ?P<num>...), чтобы функция формирования строки замены могла извлечь любую совпавшую подстроку, обращаясь к этому имени. Затем метод sub сканирует переданную строку "1 2 3 4 5", разыскивая совпадения для [0-9]+. При обнаружении совпадающей подстроки sub создает объект match, который точно определяет, какая именно подстрока совпала с шаблоном. Затем sub вызывает функцию int_match_to_float, передавая этот объект match в качестве единственного аргумента. Функция int_match_to_float использует метод group для извлечения совпавшей подстроки из объекта match (по имени группы num) и создает новую строку, выполняя конкатенацию совпавшей подстроки с ".0". Затем метод sub получает новую строку и включает ее в качестве подстроки в общий результат. Наконец, sub вновь начинает сканирование сразу после того места, где было обнаружено последнее совпадение, и продолжает этот процесс до тех пор, пока не перестанет находить совпадения. Все, что не совпало с шаблоном, остается без изменений. К примеру, если вместо этого использовать строку "1 2 3 4 5 Python", будет возвращено "1.0 2.0 3.0 4.0 5.0 Python", где слово «Python» и также, конечно, все пробелы останутся без изменений. Попробуйте сами: замена текста В упражнении из раздела 16.4 вы доработали регулярное выражение для телефонного номера, чтобы оно также распознавало код страны. Как бы вы использовали функцию, чтобы любые номера без кода страны теперь содержали код +1 (код страны для США и Канады)? 16.6. Практическая работа: нормализация телефонных номеров В США и Канаде телефонные номера состоят из десяти цифр, обычно разделенных на код региона из трех цифр, код станции из трех цифр и абонентский номер из четырех цифр. Как упоминалось в разделе 16.4, такие номера может предварять (или не предварять) код страны +1. Однако на практике существует множество способов форматирования телефонных номеров, вот лишь некоторые из них: (NNN) NNN-NNNN, NNN-NNN-NNNN, NNN NNN-NNNN, NNN. NNN.NNNN, NNN NNN NNNN. Кроме того, код страны может отсутствовать, не содержать + и обычно (но не всегда) отделяется от номера пробелом или дефисом. Уф!
   16.6. Практическая работа: нормализация телефонных номеров 405 В этой практической работе вам предстоит создать нормализатор телефонных номеров, преобразующий номера произвольного вида в унифицированный формат 1-NNN-NNN-NNNN. Возможны следующие представления номеров: +1 223-456-7890 1-223-456-7890 +1 223 456-7890 (223) 456-7890 1 223 456 7890 223.456.7890 Дополнительно: первые цифры кода региона (то есть первой группы из трех цифр) и кода станции (второй группы из трех цифр) должны быть в диапазоне от 2 до 9, а вторая цифра кода региона не может быть равна 9. На основе этих условий необходимо осуществить валидацию входных данных. В случае их нарушения сгенерировать исключение ValueError с текстом "Недействительный телефонный номер». 16.6.1. Решение задачи при помощи кода, сгенерированного ИИ Программисты любят шутить: если у вас имеется проблема и вы собрались решить ее при помощи регулярного выражения, тогда вы получаете две проблемы. В этом есть доля истины, поскольку порой весьма непросто заставить регулярное выражение сделать все, что от него требуется, и при этом не натворить ничего лишнего. При проверке кода важно быть особенно внимательным и удостовериться в том, что тесты охватывают все желаемые положительные совпадения, а также те случаи, где совпадений быть не должно. Не стоит полагаться на ИИ-ассистента в надежде, что он создаст идеальное решение или сгенерирует правильные тестовые сценарии. 16.6.2. Решения и обсуждение Существует как минимум два подхода к решению данной задачи. Первый вариант — найти наиболее вероятные совпадения с шаблоном телефонного номера (именно так я и поступила). Второй вариант — удалить из текста все, кроме цифр, и затем добавить разделители. Если вы уверены в том, что входные данные представляют собой телефонные номера, второй подход, возможно, предпочтительнее. Программное решение, созданное человеком В моем решении функция нормализации используется в качестве аргумента в основном методе регулярных выражений. Внутри этой функции применяются еще два дополнительных регулярных выражения для выявления недопустимых номеров. Функция, переданная методу regexp.sub def return_number(match_obj): # добавляет код страны, если это необходимо country = match_obj.group("country") Добавляет код страны, if not country: если это необходимо country = "1" # ДОПОЛНИТЕЛЬНО: сгенерировать исключение ValueError, если номер недействителен if not re.match(r"[2-9][0-8]\d", Проверяет код региона match_obj.group("area")):
Функция, переданная    методу regexp.sub def return_number(match_obj): # добавляет код страны, если это необходимо country = match_obj.group("country") Добавляет код страны, country: 406if notГлава 16. Регулярные выражения если это необходимо country = "1" # ДОПОЛНИТЕЛЬНО: сгенерировать исключение ValueError, если номер недействителен if not re.match(r"[2-9][0-8]\d", Проверяет код региона match_obj.group("area")): raise ValueError("invalid phone number area code {}".format(match_ obj.group("area"))) if not re.match(r"[2-9]\d\d", Проверяет код станции match_obj.group("exch") ): raise ValueError("invalid phone number exchange {}".format(match_obj. group("exch"))) return(f"{country}-{match_obj.group('area')}-{match_obj.group('exch')} ➥-{match_obj.group('number')}") regexp = re.compile(r"\+?(?P<country>\d{1,3})?[- .]?\(?(?P<area>\d{3})\) ➥?[- .]?(?P<exch>(\d{3}))[- .](?P<number>\d{4})") Вызывает основное for number in test_numbers: регулярное выражение print(regexp.sub(return_number, number)) # Тест phone_numbers = ["+1 223-456-7890", "1-223-456-7890", "+1 223 456-7890", "(223) 456-7890", "1 223 456 7890", "999.456.7890", "1-989-111-2222", "223.456.7890"] regexp = re.compile(r"\+?(?P<country>\d{1,3})?[- .]?\(?(?P<area>\d{3})\) ➥?[- .]?(?P<exch>(\d{3}))[- .](?P<number>\d{4})") for number in test_numbers: try: normalized_phone_number = regexp.sub(return_number, number) print(f'{normalized_phone_number}') except ValueError as e: print(f'{number} is not a valid phone number: {e}') 1-223-456-7890 1-223-456-7890 1-223-456-7890 1-223-456-7890 1-223-456-7890 999.456.7890 is invalid: invalid phone number area code 999 1-989-111-2222 is invalid: invalid phone number exchange 111 1-223-456-7890 Несмотря на то что сами регулярные выражения бывают весьма сложными, код, использующий их, как правило, остается простым. В данном решении основную работу выполняет функция return_number, передаваемая в качестве первого параметра методу sub основного регулярного выражения. Эта функция проверяет, указан ли код страны, и при его отсутствии добавляет +1. Затем она использует два других регулярных выражения для проверки
   16.6. Практическая работа: нормализация телефонных номеров 407 корректности кода региона и кода станции, генерируя исключение ValueError с поясняющим сообщением при обнаружении несоответствия. Очевидной, но не реализованной в текущем варианте программного кода оптимизацией является предварительная компиляция регулярных выражений, отвечающих за проверку кода региона и кода станции. Основное регулярное выражение ориентировано на распознавание наиболее типичных разделителей, однако оно не обрабатывает номера, в которых применяются иные разделяющие символы либо альтернативные схемы группировки цифр. При более взвешенном подходе целесообразнее было бы вообще не учитывать разделители и просто извлекать все цифры. Программные решения, сгенерированные ИИ Copilot предложил вполне рациональное решение: сначала удалить все нецифровые символы, а уже потом заняться проверкой и форматированием. При этом использовалась следующая формулировка промпта: Используя регулярные выражения, напиши код для нормализации телефонных номеров в форматах (NNN) NNN-NNNN, NNN-NNN-NNNN, NNN NNN-NNNN, NNN.NNN.NNNN и NNN NNN NNNN (с кодом страны или без него, со знаком + или без него) в унифицированный формат 1-NNN-NNN-NNNN. Также сгенерируй исключение ValueError, если первая цифра кода региона и кода станции не укладывается в диапазон 2–9 или если вторая цифра кода региона равна 9. Ниже представлен сгенерированный Copilot программный код: import re def normalize_phone_number(phone_number): phone_number = re.sub(r'\D', '', phone_number) Удаляет все нецифровые символы (\D) Длина должна быть 10 или 11 if len(phone_number) not in [10, 11]: raise ValueError("Invalid phone number length") Одновременно проверяет if not re.match(r'^[2-9][0-8][0-9][2-9]', код региона и код станции phone_number[-10:]): raise ValueError("Invalid area code or exchange code") Добавляет код страны, если это необходимо if len(phone_number) == 10: phone_number = '1' + phone_number phone_number = re.sub(r'(\d{1})(\d{3})(\d{3})(\d{4})', r'\1-\2-\3-\4', phone_number) Вставляет дефисы return phone_number между элементами # Проверить функцию. phone_numbers = ["+1 223-456-7890", "1-223-456-7890", "+1 223 456-7890", "(223) 456-7890", "1 223 456 7890", "999.456.7890", "1-989-111-2222", "223.456.7890"]
   408 Глава 16. Регулярные выражения for number in test_numbers: try: normalized_phone_number = normalize_phone_number(number) print(f'{normalized_phone_number}') except ValueError as e: print(f'{number} is invalid: {e}') 1-223-456-7890 1-223-456-7890 1-223-456-7890 1-223-456-7890 1-223-456-7890 999.456.7890 is invalid: Invalid area code or exchange code 1-989-111-2222 is invalid: Invalid area code or exchange code 1-223-456-7890 На первом этапе этот код удаляет из строки все нецифровые символы (\D), оставляя только цифры. В случае если длина полученной строки не равна 10 либо 11 символам, генерируется исключение, сигнализирующее об ошибке формата. Следующее регулярное выражение применяется к последним десяти цифрам номера и проверяет, соответствуют ли первые четыре из них установленным ограничениям: цифра 1 недопустима на первой и четвертой позициях, а цифра 9 — на второй. Также проверяется третья позиция, но там разрешены любые цифры. Далее, если длина строки составляет всего 10 символов, к ее началу добавляется 1. В завершение задействуется метод sub, который группирует цифры в соответствии с шаблоном и вставляет между ними разделители (дефисы). Пожалуй, представленное решение даже превосходит мой вариант в гибкости и надежности. Впрочем, в нем отсутствует предварительная компиляция регулярных выражений, что все же желательно реализовывать в рабочем коде. Следует отметить еще два момента. Во-первых, если во входной строке встретятся буквы и другие символы наряду с цифрами, то это решение примет такую строку без каких-либо возражений при условии, что общее количество цифр составляет 10 или 11. Такое поведение может быть нежелательным. А во-вторых, что не так уж существенно, — сообщение об ошибке, генерируемое при обнаружении недопустимых цифр в коде региона или в коде станции, недостаточно информативно и могло бы быть чуть более удобным для отладки. В Colaboratory был сформулирован аналогичный промпт (в отдельном блокноте и в новой сессии с целью исключения эффекта переноса контекста от предыдущих решений). Как и раньше, чтобы обойти ограничение на длину промпта, его текст был введен в текстовую ячейку, а ИИ-ассистенту дана на нее ссылка: Напиши код для нормализации телефонных номеров в соответствии с указаниями, приведенными в ячейке выше. К сожалению, все четыре варианта, сгенерированные Colaboratory, оказались неудовлетворительными: два из них вообще не запускались без ошибок. Следующий из вариантов ошибочно счел номер с кодом страны 1 неправильным,
   16.6. Практическая работа: нормализация телефонных номеров 409 при этом допустив номер с кодом региона 999. Вдобавок символы пробелов, точек, дефисов и скобок удалялись посредством громоздкой цепочки вызовов строкового метода replace: phone_number.replace(' ', '').replace('.', '').replace('-', '').replace('(', '').replace(')', '') В оставшемся решении реализован подход, аналогичный предложенному Copilot, но с рядом проблемных моментов: # промпт: Напиши код для нормализации телефонных номеров в соответствии # с указаниями, приведенными в ячейке выше. import re def normalize_phone_number(phone_number): """ Нормализует телефонный номер в формат 1-NNN-NNN-NNNN. Аргументы: phone_number: телефонный номер для нормализации. Возвращает: Нормализованный телефонный номер. Генерирует исключения: ValueError: Если формат телефонного номера некорректен. """ # Удалить все нецифровые символы. phone_number = re.sub(r'\D', '', phone_number) Удаляет все нецифровые символы (\D) # Проверить, соответствует ли формат телефонного номера допустимому. if not re.match(r'^(1)?\d{10}$', phone_number): Проверяет допустимую длину, raise ValueError('Invalid phone number format.') префикс (при наличии) # Проверить, что первая цифра кода региона и кода станции находится # в диапазоне 2-9, а вторая цифра кода региона не равна 9. if not re.match(r'^1?[2-9]\d{2}[2-9]\d{6}$', phone_number): raise ValueError('Invalid area code or exchange code.') # Нормализовать телефонный номер в формат 1-NNN-NNN-NNNN. return '-'.join([phone_number[0:1], phone_number[1:4], phone_number[4:7], phone_number[7:]]) Проверяет первые цифры кодов # Тестирование функции. phone_numbers = ["+1 223-456-7890", "1-223-456-7890", "+1 223 456-7890", "(223) 456-7890", "1 223 456 7890", "999.456.7890", "1-989-111-2222", "223.456.7890"] for number in test_numbers: try: региона и станции Объединяет элементы, разделенные по позиции в строке
   410 Глава 16. Регулярные выражения normalized_phone_number = normalize_phone_number(number) print(f'{normalized_phone_number}') except ValueError as e: print(f'{number} is invalid: {e}') 1-223-456-7890 1-223-456-7890 1-223-456-7890 2-234-567-890 1-223-456-7890 9-994-567-890 1-989-111-2222 is invalid: Invalid area code or exchange code. 2-234-567-890 Две основные проблемы этого решения состоят в следующем. Во-первых, оно на самом деле не добавляет префикс 1 в случае его отсутствия, а это ключевое требование задачи. Из-за этого, как следствие, происходит смещение групп цифр и результат выводится в неожиданном формате N-NNN-NNN-NNN. Да, технически такой номер, возможно, и сработает, но он не соответствует заданному формату нормализации. Во-вторых, данное решение не реализует дополнительную часть задания, поскольку не проверяет условие о том, что вторая цифра кода региона не должна быть равна 9. Это несколько странно, учитывая, что соответствующее требование явно указано самим чат-ботом в комментарии, размещенном непосредственно над регулярным выражением. С точки зрения производительности из-за использования регулярного выражения для вставки дефисов версия Copilot работает примерно вдвое медленнее моего решения, которое оказалось самым быстрым. Что неудивительно — при всех своих возможностях регулярные выражения, как правило, не отличаются высокой скоростью исполнения, что следует учитывать, если быстродействие для вас имеет решающее значение. Итоги Регулярное выражение (regex) — это средство распознавания и, в ряде случаев, извлечения данных из текста, соответствующего определенным шаблонам. Работа с регулярными выражениями в языке Python осуществляется при помощи модуля re, входящего в стандартную библиотеку. Полный список специальных символов регулярных выражений и их описание можно найти в официальной документации по Python. Добавление символа r перед строкой указывает интерпретатору Python на необходимость трактовать ее как сырую и не обрабатывать экранирующие последовательности. К числу самых распространенных методов работы с регулярными выражениями относятся search и sub.
   Итоги 411 В дополнение к методам search и sub в Python доступны и другие средства: они позволяют выполнять разделение строк, извлекать дополнительную информацию из объектов соответствия match, определять позиции подстрок в основной строке аргумента, а также осуществлять точный контроль над итерацией поиска по регулярному выражению в строке аргумента. Помимо специальной последовательности \d, которая может использоваться для обозначения цифрового символа, в официальной документации представлен широкий перечень других специальных последовательностей. Также существуют опции регулярных выражений, позволяющие влиять на ряд менее очевидных аспектов сопоставления при применении сложных шаблонов. Методам работы с регулярными выражениями можно передать в качестве аргумента функцию обработки соответствий вместо выражений замены.
17 Типы данных как объекты В этой главе: 3 Обращение с типами данных как с объектами 3 Работа с типами данных 3 Создание пользовательских классов 3 Понятие утиной (неявной) типизации 3 Применение специальных атрибутов метода 3 Создание подклассов на основе встроенных типов данных Вы уже освоили основные типы данных Python и научились создавать собственные при помощи классов. Во многих языках программирования на этом тема типов данных была бы исчерпана. Однако Python относится к языкам с динамической типизацией, то есть определение типов происходит во время выполнения программы, а не на этапе компиляции. Это одна из причин, по которым так легко заниматься разработкой на Python. Кроме того, он позволяет (а порой вынуждает) производить вычисления с типами объектов, а не только с самими объектами. 17.1. Типы данных тоже являются объектами Запустите сессию Python и попробуйте выполнить следующие команды:
   17.2. Работа с типами данных 413 type(5) <class 'int'> type(['hello', 'goodbye']) <class 'list'> В этом примере вы впервые встречаетесь со встроенной функцией type в Python. Эта функция применима к любому объекту Python и возвращает тип объекта. В данном случае функция сообщает, что 5 является целым числом (int), а ['hello', 'goodbye'] — списком, хотя вы, наверное, и так уже об этом догадывались. Интереснее другое: в ответ на вызовы type Python возвращает объекты; <class 'int'> и <class 'list'> являются экранными представлениями возвращенных объектов. А какой объект вернет вызов type(5)? Это нетрудно узнать. Просто вызовите type для этого результата: type_result = type(5) type(type_result) <class 'type'> Вызов type возвращает объект, типом которого является <class 'type'>; его можно назвать объектом типа (type object). В Python объекты типа — это то, что мы обычно называем классом. И хотя термины «тип» и «класс» используются в разных контекстах, в Python они обозначают одно и то же. Данное тождество — следствие унификации типов данных и классов в Python 3. Несмотря на несколько запутанное понятие о том, что метатипом класса или типа является <class 'type'>, важно уяснить, что классы (или типы) в Python — это такие же объекты, как и почти все остальное. 17.2. Работа с типами данных Теперь, когда вы знаете, что типы данных в Python можно представлять в виде объектов типов, возникает вопрос: что с ними можно делать? Их можно сопоставлять на предмет равенства, поскольку в Python любые два объекта поддерживают сравнение: type("Hello") == type("Goodbye") True type("Hello") == type(5) False Типы "Hello" и "Goodbye" одинаковы (оба являются строками), но типы "Hello" и 5 различаются. Среди прочего, этот прием может использоваться для проверки типов в определениях функций и методов.
   414 Глава 17. Типы данных как объекты 17.3. Типы данных и пользовательские классы Наиболее распространенная причина интереса к типам объектов (особенно к экземплярам пользовательских классов) заключается в определении того, является ли конкретный объект экземпляром класса. Определив тип объекта, программа может обработать его соответствующим образом. Проще пояснить это на примере. Для начала создадим пару пустых классов с простой иерархией наследования: class A: pass class B(A): pass Теперь создайте экземпляр класса B: b = B() Как и следовало ожидать, результат вызова функции type для b подтверждает, что b является экземпляром класса B, определенного в текущем пространстве имен __main__: type(b) <class '__main__.B'> Точно такую же же информацию можно получить при обращении к специальному атрибуту __class__ экземпляра: b.__class__ <class '__main__.B'> Мы будем много работать с этим классом для извлечения дополнительной информации, поэтому сохраним его, присвоив переменной: b_class = b.__class__ Теперь, чтобы подчеркнуть, что в Python все является объектом, докажем, что класс, полученный от b, — это тот же класс, который был определен с именем B: b_class == B True В этом примере необязательно было сохранять класс объекта b — он у вас уже был. Однако важно подчеркнуть фундаментальный принцип: классы в Python представляют собой полноценные объекты, которые можно сохранять, присвоив их переменным, или передавать функциям в качестве аргумента. Имя класса — в нашем случае класса b — можно узнать из его атрибута __name__: b_class.__name__ 'B'
   17.3. Типы данных и пользовательские классы 415 Чтобы узнать, от каких классов наследует данный класс, обратитесь к атрибуту __bases__, который содержит кортеж всех его базовых классов: b_class.__bases__ (<class '__main__.A'>,) Совместное использование атрибутов __class__, __bases__ и __name__ позволяет провести полный анализ структуры наследования классов применительно к любому экземпляру. Впрочем, две встроенные функции, isinstance и issubclass, позволяют получить большую часть необходимой информации в более удобном виде. К примеру, вам следует воспользоваться функцией isinstance, чтобы определить, является ли объект, переданный в функцию или метод, экземпляром ожидаемого типа: class C: pass class D: pass class E(D): pass x = 12 c = C() d = D() e = E() isinstance(x, E) False isinstance(c, E) Проверяет принадлежность экземпляра к классу E False isinstance(e, E) True isinstance(e, D) Проверяет на соответствие классу D True isinstance(d, E) Проверяет d на соответствие E False y = 12 isinstance(y, type(5)) Использует type() с примером класса True Функция issubclass применяется только для классовых типов данных:
   416 Глава 17. Типы данных как объекты issubclass(C, D) False issubclass(E, D) True issubclass(D, D) Класс является подклассом самого себя True issubclass(e.__class__, D) True Для экземпляров классов мы сначала проверяем объекты x, c и e на соответствие классу E. Далее мы видим, что e является экземпляром класса D, так как E наследуется от D. Однако d не является экземпляром класса E. Если под рукой не окажется объекта класса, как в случае со встроенными типами данных, можно воспользоваться экземпляром класса в сочетании с функцией type. Заметьте, что класс считается подклассом самого себя. Быстрая проверка: типы данных Допустим, вы хотите удостовериться в том, что объект x является списком (list), прежде чем пытаться добавлять к нему элемент. Какой код вы напишете? В чем будет заключаться различие между применением type() и isinstance()? Будет ли это соответствовать принципу программирования LBYL (Look Before You Leap, то есть «Осмотрись, прежде чем прыгать») или EAFP (Easier to Ask Forgiveness than Permission, то есть «Легче просить прощения, чем разрешения»)? К каким еще вариантам можно прибегнуть, кроме явной проверки типа? 17.4. Утиная (неявная) типизация Посредством функций type, isinstance и issubclass можно с относительной легкостью корректно определить иерархию наследования объекта или класса. И хотя сделать это не так уж сложно, в языке Python также имеется средство, благодаря которому работать с объектами становится еще проще: утиная типизация. Этим термином (который происходит от поговорки «Если кто-то ходит, как утка, и крякает, как утка, то это, вероятно, и есть утка») обозначается применяемый в Python способ определения того, соответствует ли тип объекта требуемому для выполнения заданной операции; при этом основное внимание уделяется интерфейсу объекта, а не его типу. Так, если для операции необходим итератор, сам объект вовсе не обязан быть производным классом какого-то конкретного итератора. Важно лишь, чтобы объект, используемый в качестве итератора, мог генерировать последовательность объектов ожидаемым образом.
   17.5. Что такое специальный метод? 417 Напротив, в таких языках, как Java, установлены более жесткие правила наследования. Проще говоря, утиная типизация означает, что в Python совсем не обязательно беспокоиться о проверке типов аргументов функций или методов и т. п. Вместо этого следует полагаться на удобочитаемый и документированный код в сочетании с тщательным тестированием, позволяющим убедиться в том, что объект «крякает, как утка» там, где потребуется. Утиная типизация помогает повысить гибкость хорошо написанного кода, а в сочетании с более мощными объектно-ориентированными средствами дает возможность создавать классы и объекты практически для любой ситуации. Особенно в крупных программных проектах гибкость утиной типизации может привести к тому, что ошибки останутся незамеченными. В последнее время все чаще стараются избегать утиной типизации или ограничивать ее в пользу явных аннотаций типов, которые могут быть проверены программами статического анализа типов. 17.5. Что такое специальный метод? Специальный метод (special method attribute) представляет собой атрибут класса Python, имеющий особое значение для самого интерпретатора. Он определяется как метод, но не предназначен для прямого вызова из пользовательского кода. Специальные методы обычно не вызываются напрямую; вместо этого они вызываются автоматически языком Python по требованию объекта этого класса. Специальные методы помечаются символами двойного подчеркивания в начале и конце их названий. По этой причине их часто называют дандер-методами (dunder methods), сокращенно от «двойного подчеркивания» (double underscore). О них также иногда говорят как о «магических» методах («magic» methods), поскольку они обеспечивают большую часть «магии» классов Python. Возможно, простейшим примером служит специальный метод __str__. Если он определен в классе, то каждый раз, когда экземпляр этого класса используется там, где интерпретатору необходимо строковое представление этого экземпляра в удобочитаемой для пользователя форме, вызывается специальный метод __str__, а возвращаемое им значение используется в качестве требуемой строки. Чтобы увидеть этот метод в действии, определим класс, представляющий красный, зеленый и синий цвета (RGB) в виде тройки чисел, по одному для каждой цветовой составляющей. Кроме определения стандартного метода __init__ для инициализации экземпляров класса зададим специальный метод __str__, возвращающий строки в удобочитаемом формате. Определение будет выглядеть примерно так. Листинг 17.1. Файл color_module.py class Color: def __init__(self, red, green, blue): self._red = red
418    Глава 17. Типы данных как объекты self._green = green self._blue = blue def __str__(self): return f"Color: R={self._red:d},G={self._green:d},B={self._blue:d}" Если разместить это определение в файле с именем color_module.py, его можно будет загрузить и использовать обычным способом: from color_module import Color В Colaboratory можно просто выполнить код в ячейке и работать с ним без импортирования: c = Color(15, 35, 3) Действие специального метода __str__ можно увидеть, если для вывода c использовать print: print(c) Color: R=15, G=35, B=3 Хотя специальный метод __str__ явно не вызывался в вашем коде, Python все равно задействовал его: интерпретатор знает, что если у объекта есть атрибут __str__, то он предназначен для преобразования объекта в строковое представление, понятное пользователю. Это одна из определяющих характеристик специальных методов: они позволяют задать поведение, которое автоматически встраивается в работу Python. Среди прочего, специальные методы позволяют создавать классы, экземпляры которых ведут себя синтаксически и семантически эквивалентно таким встроенным типам, как списки или словари. Так, к примеру, можно определять объекты, идентичные встроенным спискам Python с точки зрения интерфейса, но использующие сбалансированные деревья вместо массивов для хранения данных. Для программиста такой объект будет выглядеть как обычный список, но с ускоренными операциями вставки, более медленной итерацией и прочими отличиями в быстродействии, которые могут оказаться полезными для реализации конкретной задачи. В оставшейся части этой главы рассматриваются более развернутые примеры с использованием специальных методов. В ней не обсуждаются все доступные подобные методы, но концепция изложена достаточно подробно для того, чтобы вы смогли легко воспользоваться другими специальными методами, описанными в документации к стандартной библиотеке для встроенных типов. 17.6. Придание объекту свойств списка В этом примере используется большой текстовый файл с информацией о людях; каждая запись представляет собой одну строку с именем человека, его возрастом и местом проживания, и эти поля внутри записи разделены двойным двоеточием (::). Строки такого файла могут выглядеть примерно следующим образом:
   17.7. Специальный метод __getitem__ 419 . . . John Smith::37::Springfield, Massachusetts Ellen Nelle::25::Springfield, Connecticut Dale McGladdery::29::Springfield, Hawaii . . . Допустим, необходимо собрать информацию о распределении возрастов людей в этом файле. Существует много способов обработки строк в подобных файлах. Вот один из них: fileobject = open(filename, 'r') lines = fileobject.readlines() fileobject.close() for line in lines: ... do whatever ... Теоретически этот способ может сработать, однако он считывает в память сразу весь файл. Если файл будет слишком большим и не поместится в памяти (а подобные файлы бывают просто громадными), программа не будет работать. Вот другое решение: fileobject = open(filename, 'r') for line in fileobject: ... do whatever ... fileobject.close() Такой код позволил бы побороть проблему нехватки памяти, считывая файл построчно. Он работал бы отлично, но предположим, что понадобилось бы еще упростить открытие файла, а из каждой строки этого файла получить лишь первые два поля (имя и возраст). Следовательно, вам потребовалось бы какое-то средство, позволяющее (по крайней мере, для цикла for) обрабатывать текстовый файл как список строк, но без считывания всего содержимого файла в память целиком. 17.7. Специальный метод __getitem__ Решение состоит в использовании специального метода __getitem__, который можно определить в любом пользовательском классе. Это позволит экземплярам такого класса поддерживать синтаксис и поведение, характерные для обращения к элементам списка. Если класс AClass в Python реализует метод __getitem__, а obj является его экземпляром, то выражения вида x = obj[n] и for x in obj: корректны и работают как ожидается. С объектом obj можно работать почти так же, как со списком.
   420 Глава 17. Типы данных как объекты Ниже приведен окончательный код для класса LineReader (пояснения следуют далее): class LineReader: def __init__(self, filename): self.fileobject = open(filename, 'r') def __getitem__(self, index): line = self.fileobject.readline() if line == "": self.fileobject.close() raise IndexError else: return line.split("::")[:2] Открывает файл для чтения Пытается считать строку Если данных больше нет… …закрывает файловый объект… …и генерирует исключение IndexError for name, age in LineReader("filename"): ... do whatever ... В противном случае разделяет строку и возвращает первые два поля Реализация метода __getitem__() позволяет работать с экземплярами этого класса как с итерируемыми объектами цикла for, благодаря чему считывание файла выполняется построчно на каждой итерации и продолжается до тех пор, пока при достижении конца данных не будет сгенерировано исключение IndexError — то есть до того момента, когда операция чтения возвратит пустую строку, сигнализируя об окончании файла. На первый взгляд этот пример выглядит хуже предыдущего решения, поскольку он содержит больше кода и сложнее для понимания. Однако, большая часть кода находится в классе, который можно выделить в отдельный модуль — например, myutils. Тогда программа примет вид: import myutils for name, age in myutils.LineReader("filename"): ... do whatever ... Класс LineReader обрабатывает все детали открытия файла, последовательного чтения строк и закрытия файла. Начальный этап разработки немного увеличивается, зато благодаря такому инструменту работа с большими текстовыми файлами, содержащими по одной записи в каждой строке, упрощается и становится менее подверженной ошибкам. Отмечу, что, хотя в языке Python имеется ряд мощных средств чтения файлов, у этого примера есть свое преимущество: он достаточно прост для понимания. Стоит лишь раз уяснить, как он работает, и вы сможете применять этот принцип во многих ситуациях. 17.7.1. Как это работает LineReader является классом, а метод __init__ открывает файл с заданным именем для чтения и сохраняет открытый файловый объект для последующего доступа. Чтобы уяснить, как работает метод __getitem__, необходимо учесть три следующих момента:
   17.7. Специальный метод __getitem__ 421 Любой объект, определяющий __getitem__ как метод экземпляра, может возвращать элементы так, словно он является списком: все обращения в форме object[i] преобразуются интерпретатором в вызов метода вида object.__getitem__(i), который обрабатывается как обычный вызов метода. В конечном счете он выполняется в форме __getitem__(object, i) с применением версии __getitem__, заданной в классе. В первом аргументе каждого вызова __getitem__ передается объект, из которого извлекаются данные, а во втором — индекс (позиция) этих данных. Поскольку циклы for поочередно обращаются к каждому элементу данных в списке, цикл вида for arg in sequence: работает, многократно вызывая __getitem__ с последовательно возрастающими индексами. Цикл for сначала присваивает arg значение sequence.__getitem__(0), затем sequence.__ getitem__(1) и т. д. Цикл for перехватывает исключения IndexError и обрабатывает их, останавливая цикл. Именно так завершаются циклы for при работе с обычными списками или последовательностями. Класс LineReader предназначен только для использования в цикле for, а цикл for всегда генерирует вызовы с последовательно увеличивающимся индексом: __getitem__(self, 0), __getitem__(self, 1), __getitem__(self, 2) и т. д. Программный код в разделе 17.7 использует эту особенность и возвращает строки одну за другой, игнорируя аргумент index. Зная это, нетрудно понять, как объект LineReader эмулирует последовательность в цикле for. При каждой итерации цикла для объекта вызывается специальный метод __getitem__; в результате объект считывает следующую строку из сохраненного файлового объекта и анализирует эту строку. Если строка не пустая, то она возвращается. Пустая строка означает, что достигнут конец файла; объект закрывает файловый объект и генерирует исключение IndexError. Исключение IndexError перехватывается внешним циклом for, который при этом завершается. Помните, что данный пример приведен исключительно в иллюстративных целях. Как правило, для итерации по строкам файла достаточно конструкции for line in fileobject:, однако данный пример демонстрирует, насколько легко в языке Python реализовать объекты, ведущие себя как списки или другие типы данных. Быстрая проверка: __getitem__ Этот пример использования __getitem__ очень ограничен, и во многих ситуациях он не будет выполняться корректно. В каких случаях в приведенной реализации произойдет сбой или она будет работать неправильно? 17.7.2. Реализация полной функциональности списка В приведенном примере объект класса LineReader ведет себя как объект спис­ ка только в той степени, что он правильно реагирует на последовательные
   422 Глава 17. Типы данных как объекты обращения к строкам файла, из которого читаются данные. Но как расширить эту функциональность, чтобы поведение LineReader (или объектов других классов) в большей степени напоминало поведение списков? Прежде всего метод __getitem__ должен научиться каким-то образом обрабатывать свой аргумент index. Поскольку класс LineReader создавался преимущественно для того, чтобы предотвратить считывание большого файла в память, будет бессмысленно сохранять весь файл в памяти и возвращать соответствующую строку. Пожалуй, самое разумное, что можно сделать, — проверять, чтобы индекс при каждом вызове __getitem__ был на 1 больше индекса из предыдущего вызова __getitem__ (или равен 0 при первом вызове __getitem__ для экземпляра LineReader), а также генерировать ошибку в том случае, если это условие не выполняется. При таком решении гарантируется, что экземпляры LineReader будут использоваться лишь в циклах for, как и предполагалось. В целом язык Python предоставляет несколько специальных методов, относящихся к поведению списков. Так, специальный метод __setitem__ позволяет определить логику обработки операций индексного присваивания, таких как obj[n] = val. Другие специальные методы предоставляют менее очевидную функциональность списков; в частности, атрибут __add__ позволяет объектам реагировать на оператор +, а следовательно, выполнять свою версию конкатенации списков. Для того чтобы класс полностью имитировал список, необходимо задать еще несколько специальных методов. Тем не менее применение соответствующих специальных методов языка Python позволяет в конечном счете достичь полной функциональной эквивалентности со списками. Далее приведен пример, еще ближе подводящий нас к реализации полноценного класса-списка. 17.8. Наделение объекта полной функциональностью списка Специальный метод __getitem__ является одним из множества специальных атрибутов функций в Python, которые можно задать в классе с целью предоставления его экземплярам специфического поведения. Для иллюстрации того, как специальным методам удается расширять возможности языка, органично интегрируя их в синтаксис Python, рассмотрим другой, более универсальный пример. При работе со списками нередко предполагается, что все элементы конкретного списка принадлежат одному и тому же типу, будь то строки или числовые значения. Некоторые языки, такие как C++, позволяют жестко задавать такое ограничение. В больших программах возможность объявить список как содержащий элементы определенного типа помогает отслеживать ошибки: попытка добавить элемент неподходящего типа в типизированный список вызывает сообщение об ошибке, что позволяет выявить проблему на ранних этапах разработки. Python не содержит встроенной функциональности типизованных списков, и мало кто из программистов Python об этом жалеет. Однако если требуется
   17.8. Наделение объекта полной функциональностью списка 423 обеспечить типовую однородность списка, специальные методы Python позволяют легко создать класс, который будет вести себя как типизированный список. Ниже представлен начальный фрагмент реализации подобного класса, в котором активно применяются встроенные функции type и isinstance для контроля типов объектов: class TypedList: def __init__(self, example_element, initial_list=[]): Пример допустимого типа self.type = type(example_element) if not isinstance(initial_list, list): raise TypeError("Second argument of TypedList must " "be a list.") for element in initial_list: if not isinstance(element, self.type): raise TypeError("Attempted to add an element of " "incorrect type to a typed list.") self.elements = initial_list[:] Аргумент example_element определяет тип объектов, которые может содержать список, предоставляя образец типа элемента. Класс TypedList в том виде, как он определен здесь, позволяет выполнять вызовы в форме x = TypedList ('Hello', ["List", "of", "strings"]) Первый аргумент 'Hello' вообще не встраивается в итоговую структуру данных. Он всего лишь является примером типа элементов, которые должны содержаться в списке (в данном случае строки). Второй параметр конструктора представляет собой необязательный список, предназначенный для инициализации исходного набора значений. Метод __init__ класса TypedList осуществляет проверку типов всех элементов переданного списка на соответствие типу, заданному в виде образца в первом аргументе. При обнаружении любого несовпадения типов генерируется исключение. В этой версии класс TypedList не может служить списком, потому что он не реагирует на стандартные способы присваивания или чтения элементов списка. Для устранения этого недостатка достаточно задать специальные методы __setitem__ и __getitem__. Метод __setitem__ автоматически вызывается интерпретатором всякий раз при выполнении команды вида TypedListInstance[i] = value, а метод __getitem__ — при каждом вычислении выражения TypedListInstance[i] для возврата значения i-го элемента экземпляра TypedListInstance. Ниже приведена обновленная версия класса TypedList. Поскольку контроль типов добавляемых элементов будет выполняться часто, соответствующая операция инкапсулируется в новом закрытом методе __check: class TypedList: def __init__(self, example_element, initial_list=[]): self.type = type(example_element) if not isinstance(initial_list, list): raise TypeError("Second argument of TypedList must "
   424 Глава 17. Типы данных как объекты "be a list.") for element in initial_list: self.__check(element) self.elements = initial_list[:] def __check(self, element): if type(element) != self.type: raise TypeError("Attempted to add an element of " "incorrect type to a typed list.") def __setitem__(self, i, element): self.__check(element) self.elements[i] = element def __getitem__(self, i): return self.elements[i] Теперь экземпляры класса TypedList больше похожи на списки. К примеру, уже допустим следующий код: x = TypedList("", 5 * [""]) x[2] = "Hello" x[3] = "There" print(x[2] + ' ' + x[3]) Hello There a, b, c, d, e = x a, b, c, d ('', '', 'Hello', 'There') Обращения к элементам x в команде print обрабатываются методом __getitem__, который передает их экземпляру списка, хранящемуся в объекте TypedList. Присваивания x[2] и x[3] обрабатываются методом __setitem__, который осуществляет проверку присваиваемого элемента на соответствие типу, после чего выполняет присваивание в списке, содержащемся в self.elements. Последняя строка вызывает метод __getitem__ для извлечения первых пяти элементов объекта x, которые затем по порядку присваиваются переменным a, b, c, d и e. Вызовы __getitem__ и __setitem__ интерпретатор выполняет автоматически. Чтобы в окончательной версии класса TypedList его экземпляры вели себя как объекты списков во всех отношениях, потребуется написание дополнительного кода. Необходимо доработать специальные методы __setitem__ и __getitem__, чтобы объекты TypedList поддерживали синтаксис срезов, а также обращения к отдельным элементам. Еще требуется определить метод __add__ для выполнения сложения списков (конкатенации), а с ним и метод __mul__ для их умножения. Нужно задать метод __len__, чтобы результат вызова len(TypedListInstance) вычислялся корректно, и метод __delitem__, чтобы класс TypedList надлежащим образом обрабатывал команды del. Кроме того, необходимо реализовать метод append, чтобы к экземплярам TypedList можно было добавлять элементы при помощи стандартного метода списков append. Аналогичным образом следует определить методы insert и extend.
   17.9. Наследование от встроенных типов 425 Попробуйте сами: реализация специальных методов списка Попытайтесь самостоятельно реализовать для класса TypedList специальные методы __len__ и __delitem__, а также метод append. 17.9. Наследование от встроенных типов Приведенный выше пример — хороший способ потренироваться в создании собственного класса, похожего на список, с нуля, однако это требует немалых усилий. На практике, если вы собираетесь реализовать собственную структуру, похожую на список, по аналогии с продемонстрированным примером, стоит вместо этого рассмотреть вариант наследования от типа list либо от класса UserList. 17.9.1. Наследование от list Вместо того чтобы создавать класс типизированного списка с нуля, как это делалось в предыдущих примерах, вы можете наследовать свой класс от list и переопределить все методы, которые должны учитывать допустимый тип. Существенное преимущество такого подхода заключается в том, что ваш класс по умолчанию обладает стандартной реализацией всех операций со списками, поскольку сам является списком. Главное, о чем не следует забывать: каждый тип в языке Python является классом, и если вам нужно изменить поведение встроенного типа, имеет смысл рассмотреть возможность наследования от него: class TypedListList(list): def __init__(self, example_element, initial_list=[]): self.type = type(example_element) if not isinstance(initial_list, list): raise TypeError("Second argument of TypedList must " "be a list.") for element in initial_list: self.__check(element) super().__init__(initial_list) def __check(self, element): if type(element) != self.type: raise TypeError("Attempted to add an element of " "incorrect type to a typed list.") def __setitem__(self, i, element): self.__check(element) super().__setitem__(i, element) x = TypedListList("", 5 * [""]) x[2] = "Hello" x[3] = "There" print(x[2] + ' ' + x[3]) Hello There a, b, c, d, e = x a, b, c, d
   426 Глава 17. Типы данных как объекты ('', '', 'Hello', 'There') x[:] ['', '', 'Hello', 'There', ''] del x[2] x[:] ['', '', 'There', ''] x.sort() x[:] ['', '', '', 'There'] Теперь остается совсем немного: реализовать метод для проверки типа добавляемых элементов, а затем скорректировать __setitem__ для того, чтобы эта проверка осуществлялась перед вызовом базовой реализации метода __setitem__ типа list. Прочие методы, такие как sort и del, будут работать без дополнительного кода. Переопределение встроенного типа может существенно сэкономить время, если вам нужно изменить лишь небольшую часть его поведения, поскольку основная логика класса остается без изменений. 17.9.2. Наследование от UserList Если необходима модифицированная версия списка (как в предыдущих примерах), существует и третий вариант: вы можете наследовать свой класс от UserList, оберточного класса над list, входящего в модуль collections. Класс UserList был разработан для более ранних версий Python, когда наследование от встроенного типа list еще не предусматривалось, однако остается актуальным, особенно в данном случае, поскольку предоставляет доступ к внутреннему списку через атрибут data: from collections import UserList class TypedUserList(UserList): def __init__(self, example_element, initial_list=[]): self.type = type(example_element) if not isinstance(initial_list, list): raise TypeError("Second argument of TypedList must " "be a list.") for element in initial_list: self.__check(element) super().__init__(initial_list) def __check(self, element): if type(element) != self.type: raise TypeError("Attempted to add an element of " "incorrect type to a typed list.") def __setitem__(self, i, element):
   17.10. Когда следует применять специальные методы 427 self.__check(element) self.data[i] = element def __getitem__(self, i): return self.data[i] x = TypedUserList("", 5 * [""]) x[2] = "Hello" x[3] = "There" print(x[2] + ' ' + x[3]) Hello There a, b, c, d, e = x a, b, c, d ('', '', 'Hello', 'There') x[:] ['', '', 'Hello', 'There', ''] del x[2] x[:] ['', '', 'There', ''] x.sort() x[:] ['', '', '', 'There'] Этот пример во многом аналогичен наследованию от list, однако в данной реализации список элементов предоставляется во внутреннем атрибуте data. В ряде случаев наличие прямого доступа к базовой структуре данных является преимуществом. Помимо UserList, модуль содержит также оберточные классы UserDict и UserString. 17.10. Когда следует применять специальные методы Как правило, к использованию специальных методов следует подходить осторожно. Другим программистам, которым придется работать с вашим кодом, может быть непонятно, почему один объект типа последовательности корректно обрабатывает стандартный синтаксис обращения по индексу, тогда как другой — нет. Мои общие рекомендации сводятся к тому, чтобы задействовать специальные методы в одном из двух сценариев. Если в моей программе есть часто используемый класс, который в чем-то ведет себя как встроенный тип Python, я по мере необходимости определяю
   428 Глава 17. Типы данных как объекты специальные методы. Подобные случаи чаще всего связаны с объектами, которые ведут себя как последовательности в том или ином отношении. Если у меня имеется класс, который воспроизводит поведение встроенного класса полностью или с незначительными отклонениями, я могу либо определить все соответствующие специальные методы, либо реализовать наследование от соответствующего встроенного типа Python с последующим распространением. Примером второго подхода могут служить списки, реализованные как сбалансированные деревья: при таком решении доступ осуществляется медленнее, тогда как операция вставки работает быстрее, чем в случае стандартных списков. Не стоит считать эти правила непреложными. К примеру, нередко бывает полезно определить для класса специальный метод __str__, чтобы можно было включить в отладочный код команду print (экземпляр) и получить на экране информативное, удобное представление вашего объекта в текстовой форме. Быстрая проверка: специальные методы и наследование типов Допустим, вы хотите реализовать тип данных, аналогичный словарю, который допускает в качестве ключей только строки (возможно, с целью обеспечить поведение, схожее с объектом shelf, описанным в главе 13). Какие существуют подходы к построению такого класса? Каковы преимущества и ограничения каждого из них? 17.11. Практическая работа: создание словаря со строковыми ключами В предыдущей «Быстрой проверке» говорится о создании словаря, допускающего в качестве ключей исключительно строки. Расширим эту концепцию и реализуем словарь, ограничивающий как ключи, так и значения строковым типом данных. Такой словарь может применяться, к примеру, для кэширования URL-адресов и веб-страниц в веб-приложении. Как отмечалось ранее при рассмотрении списков, существует три основных подхода: создать класс с нуля, наследовать встроенный тип словаря или класс UserDict. С точки зрения оптимального баланса между простотой реализации и функциональностью рекомендуется наследовать от встроенного типа dict с последующим переопределением метода __setitem__(). Однако следует учитывать, что словари можно также создавать на основе других словарей или серии кортежей, содержащих пары «ключ-значение», минуя тем самым вызов метода __setitem__(). Для корректной обработки подобных случаев необходимо переопределить метод __init__(). Один из способов — проанализировать тип входного параметра, а затем отдельно убедиться в том, что все ключи и значения являются строками. Однако проще сначала вызвать метод __init__() родительского класса, а уже затем выполнять проверку содержимого
   17.11. Практическая работа: создание словаря со строковыми ключами 429 self. В качестве заключительного задания предлагается дополнить класс переопределением метода __init__(), чтобы ключи и значения гарантированно имели только строковый тип. 17.11.1. Решение задачи при помощи кода, сгенерированного ИИ Прежде всего имейте в виду, что ИИ-ассистенту необходимо указать предпочтительный метод реализации. Поскольку рекомендуется создание подкласса от dict, это следует явно уточнить в промпте — к тому же требование наследования от dict упростит саму формулировку запроса. Решение, сгенерированное средством ИИ, также должно корректно обрабатывать методы __setitem__() и __init__(). 17.11.2. Решения и обсуждение Как отмечалось ранее, для реализации полноценного решения необходимо переопределить методы __setitem__() и __init__(), чтобы обеспечить допустимость исключительно строковых значений в роли как ключей, так и значений словаря. Переопределить __setitem__() достаточно просто, тогда как __init__() требует большей аккуратности, поскольку он может принимать аргументы двух типов: либо последовательность кортежей «ключ-значение», либо другой словарь. Программное решение, созданное человеком Мое решение реализует достаточно прямолинейный подход: типы ключей и значений проверяются в методе __setitem__(), а также в методе __init__() осуществляется проверка ключей и значений экземпляра self: """ Создать словарь, допускающий в качестве ключей и значений только строки """ class StringDict(dict): def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('key is not a string') if not isinstance(value, str): raise TypeError('value is not a string') super().__setitem__(key, value) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) key_error = any(not isinstance(_, str) for _ in self.keys()) if key_error: raise TypeError('key is not a string') value_error = any(not isinstance(_, str) for _ in self.values()) if value_error: raise TypeError('value is not a string') Проверка типа Проверка типов при итерации по ключам и значениям
   430 Глава 17. Типы данных как объекты В методе __setitem__() применяется функция isinstance для проверки типа, и генерируется исключение TypeError с поясняющим сообщением, если ключ или значение не являются строками. Если они оба являются строками, вызывается метод родительского класса для добавления элемента в объект self. Реализация метода __init__() должна быть чуть более сложной. Поскольку в него может быть передана либо последовательность кортежей вида «ключзначение», либо словарь, написание универсального программного кода для проверки обоих форматов требует дополнительных усилий. Проще сначала вызвать метод __init__ родительского класса, чтобы он добавил значения обоих типов в новый объект self, а затем выполнить проверку этого объекта посредством его методов keys и values. Здесь мы применяем генераторное выражение — оно похоже на генератор списка, но более эффективно расходует оперативную память. Функция any возвращает True, если в сгенерированной последовательности обнаруживается хотя бы один элемент, не являющийся экземпляром класса str. Другой характерной чертой этих генераторных выражений является применение _ в качестве переменной, что является распространенным приемом в Python и указывает на ее полуанонимный, временный характер. На первый взгляд может показаться некорректным выполнять проверку self, поскольку это подразумевает, что объект уже создан. Формально объект существует, однако на практике, если в методе __init__ будет сгенерировано исключение, это приведет к удалению объекта. Программные решения, сгенерированные ИИ Для Copilot промпт был простым: Создай подкласс словаря, допускающий использование только строк в качестве ключей и значений. Copilot разработал следующий план. 1. Создать подкласс dict с именем StringDict. 2. Переопределить метод __setitem__, чтобы проверить, являются ли ключ и значение экземплярами str. Если нет, сгенерировать TypeError. 3. Переопределить метод __init__ для вызова метода __init__ родительского класса, а затем проверить все ключи и значения в словаре. Если что-либо из них не является экземпляром типа str, сгенерировать TypeError. Далее был предложен следующий программный код: class StringDict(dict): def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('keys must be strings') if not isinstance(value, str): raise TypeError('values must be strings') super().__setitem__(key, value)
   17.11. Практическая работа: создание словаря со строковыми ключами 431 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for key, value in self.items(): if not isinstance(key, str): raise TypeError('keys must be strings') if not isinstance(value, str): raise TypeError('values must be strings') Сходство с решением, предложенным программистом, удивительно, хотя в ряде моментов вариативность решений объективно ограничена. Основное различие состоит в том, что в методе __init__ в предложенном коде применяются обычные циклы for, и исключение генерируется немедленно при обнаружении значения, тип которого отличен от str. По зрелому размышлению приходится признать, что данное решение предпочтительнее моего. Так, в моем варианте осуществляется полный перебор ключей и значений объекта self, тогда как версия Copilot останавливает выполнение программы при первой ошибке. В большинстве случаев это вряд ли будет иметь существенное значение, однако при наличии множества элементов недопустимых типов, передаваемых в __init__, данный подход может оказаться куда более эффективным. Для Colaboratory потребовался более развернутый промпт. Первым был тот, что предлагался и Copilot: Создай подкласс словаря, допускающий использование только строк в качестве ключей и значений. В результате был получен лишь класс, переопределяющий __setitem__, чего явно недостаточно: class StringDict(dict): def __setitem__(self, key, value): if not isinstance(key, str) or not isinstance(value, str): raise TypeError("Both keys and values must be strings.") super().__setitem__(key, value) Тогда промпт был расширен до следующего: Создай подкласс словаря, допускающий использование только строк в качестве ключей и значений, в том числе при создании экземпляра. Такой промпт сработал лучше, и вот какой результат был выдан: class StringDict(dict): def __init__(self, *args, **kwargs): super().__init__() for key, value in dict(*args, **kwargs).items(): self[key] = value Создает анонимный словарь Вызывает метод __setitem__ def __setitem__(self, key, value): if not isinstance(key, str) or not isinstance(value, str): raise TypeError("Both keys and values must be strings.") super().__setitem__(key, value)
   432 Глава 17. Типы данных как объекты Данный код является компактным и функционирует корректно. Следует отметить некоторые различия в подходе по сравнению с решением Copilot. В частности, метод __init__ формирует отдельный анонимный (временный) словарь посредством вызова dict с соответствующими аргументами. Подход рабочий, однако при достаточно большом объеме начальных аргументов он может оказаться неэффективным с точки зрения памяти, поскольку анонимный словарь не будет удален из нее до тех пор, пока все его элементы не будут добавлены в self — инициализируемый словарь. Впрочем, проблемы могут возникнуть лишь при обработке чрезвычайно больших объемов данных, поскольку дублируется только структура словаря, а не сами элементы. Кроме того, этот подход не проверяет значения в методе __init__, а вместо этого добавляет каждый элемент в цикле, всякий раз вызывая __setitem__, где и происходит проверка. Преимущество такого способа в том, что код проверки не дублируется. Также стоит отметить, что в этом варианте программного кода и ключ, и значение проверяются в одном составном операторе if. Это абсолютно допустимо, но я бы так делать не стала, поскольку в таком случае невозможно точно указать в исключении, что именно вызвало ошибку — ключ или значение. Кроме того, составные операторы труднее воспринимаются программистом при чтении кода. Тем не менее в целом это решение вполне приемлемо. Несмотря на различия в подходах, простота наследования уже существующей структуры данных предопределила тот факт, что во многих отношениях решения этой задачи оказались похожи и проще, чем при использовании UserDict или реализации функционала словаря с чистого листа. Итоги Типы и классы в Python — это обычные объекты. Типы допускают привязку к переменным и сравнение на равенство с другими объектами. Python располагает средствами для проверки типа объектов там, где это требуется в коде. Утиная (неявная) типизация, при которой опора делается на поведение объектов, а не на их тип, позволяет писать более гибкий, адаптивный код. Специальные (или «магические») методы имеют имена, начинающиеся и заканчивающиеся символом двойного нижнего подчеркивания __ (так называе­ мые дандер-методы, от англ. dunder (method) — разговорное сокращение от double underscore, «двойное подчеркивание»), и могут быть переопределены с целью модификации поведения пользовательских классов. Наследование от встроенных классов позволяет создавать похожие классы с настраиваемым поведением. Реализация в языке Python концепции утиной типизации, специальных методов и механизмов наследования предоставляет широкие возможности для конструирования и сочетания классов.
18 Пакеты В этой главе: 3 Определение пакета 3 Создание простого пакета 3 Изучение конкретного примера 3 Использование атрибута __all__ 3 Правильное использование пакетов Модули упрощают многократное использование небольших фрагментов кода. Трудности возникают, когда проект разрастается и код, который необходимо использовать повторно, выходит за рамки — физические или логические — того, что помещается в один файл. Если один огромный файл модуля трудно назвать удовлетворительным решением, то множество мелких несвязанных модулей ничем не лучше. Решением проблемы является объединение связанных модулей в пакет. В этой главе мы рассмотрим пакеты Python в виде иерархии каталогов и файлов на диске. ПРИМЕЧАНИЕ Довольно часто под «пакетом» понимают объединение одного или нескольких модулей или пакетов Python в единый файл дистрибутива (архив), предназначенный для загрузки в репозиторий пакетов, такой как PyPI (он упоминается в следующей главе). Количество инструментов для создания таких пакетов постоянно растет, и подробное описание этого процесса выходит за рамки данной книги. Хорошей отправной точкой для изучения темы создания дистрибутивов пакетов Python станет руководство «Python Packaging User Guide», которое доступно по адресу https://packaging.python.org/en/latest/.
   434 Глава 18. Пакеты 18.1. Что такое пакет? Модуль представляет собой файл с программным кодом. Модуль определяет группу взаимосвязанных функций Python или других объектов. Имя модуля является производным от имени файла. Если вы понимаете, как работают модули, разобраться с пакетами будет несложно, поскольку пакет представляет собой каталог с программным кодом — и, возможно, несколькими вложенными каталогами. Пакет содержит группу (обычно) логически связанных файлов с кодом (модулей). Имя пакета определяется именем главного каталога пакета. Пакеты являются естественным расширением концепции модуля; они предназначены для очень больших проектов. По аналогии с тем, как модули объединяют взаимосвязанные функции, классы и переменные, пакеты группируют взаимосвязанные модули. 18.2. Первый пример: mathproj Чтобы понять, как пакеты применяются на практике, рассмотрим структуру проекта, который по своей природе является очень масштабным: универсальный математический пакет, построенный по образцу Mathematica, Maple или MATLAB. Maple, к примеру, содержит тысячи файлов, и иерархическая структура крайне необходима для его упорядочения. Назовем этот проект собирательно mathproj. Существует множество способов организации подобного проекта, однако целесообразно структурно подразделить его на две части: ui — элементы пользовательского интерфейса и comp — компоненты, отвечающие за вычисления. В comp вычислительные аспекты имеет смысл дополнительно разделить на symbolic (символические: вещественные и комплексные символические вычисления, как в школьном курсе алгебры) и numeric (числовые: вещественные и комплексные числовые расчеты — например, численное интегрирование). Для каждой из этих частей (symbolic и numeric) разумно предусмотреть свой файл constants.py. В файле constants.py из числовой части проекта значение pi определяется следующим образом: pi = 3.141592 тогда как в файле constants.py в символической части проекта определение pi выглядит так: class PiClass: def __str__(self): return "PI" pi = PiClass()
   18.3. Реализация пакета mathproj 435 Это означает, что такое имя, как pi, может присутствовать в двух разных файлах с одинаковыми именами constants.py (и импортироваться из них), как показано на рис. 18.1. Файл constants.py из символической части проекта определяет pi как абстрактный объект Python, единственный экземпляр класса PiClass. По мере развития системы в этом классе могут реализовываться различные операции, возвращающие символические, а не числовые результаты. Существует естественное соответствие между структурой проекта и структурой каталогов. Каталог верхнего уровня проекта c именем mathproj содержит подкаталоги ui и comp; в свою очередь, comp содержит подкаталоги symbolic и numeric; наконец, каждый из подкаталогов, symbolic и numeric, содержит собственный файл constants.py. mathproj ui comp symbolic numeric constants.py constants.py Рис. 18.1. Математический пакет, разделенный на компоненты пользовательского интерфейса UI и блок вычислительных компонентов comp, включающие в себя элементы символических и числовых вычислений С учетом данной структуры каталогов и при условии, что корневой каталог mathproj присутствует в одном из путей поиска Python, к обеим версиям pi можно обращаться из любого кода — как внутри, так и вне пакета mathproj — через mathproj.symbolic.constants.pi и mathproj.numeric.constants.pi. Иными словами, в Python имя элемента пакета отражает путь к файлу, содержащему этот элемент. В этом и заключается суть пакетов. Они представляют собой механизм организации больших массивов кода Python в единое целое, обеспечивающий возможность распределения кода по файлам и каталогам и применения иерархической схемы именования модулей и подмодулей на основе структуры каталогов, в которых находятся файлы пакета. К сожалению, на практике работать с пакетами не так просто, как может показаться на первый взгляд: различные технические нюансы делают их использование сложнее, чем в чистой теории. Поэтому оставшуюся часть главы мы посвятим обсуждению прикладных аспектов работы с пакетами. 18.3. Реализация пакета mathproj Далее в главе механизм работы пакетов иллюстрируется на примере пакета mathproj (см. рис. 18.2). Обратите внимание: необходимо различать исходный файл с расширением .py, содержащий код модуля, и сам модуль, представляе­ мый содержимым этого файла, но не включающий расширение .py в свое имя.
   436 Глава 18. Пакеты Структура файлов, входящих в пакет из примера, приведена в листингах 18.1– 18.6. Файл, представленный в листинге 18.1, — это инициализационный файл __ init__.py основного пакета. Он выводит сообщение, подтверждающее загрузку пакета, и устанавливает для него атрибут __all__. Листинг 18.1. Файл mathproj/__init__.py print("Hello from mathproj init") __all__ = ['comp'] version = 1.03 mathproj _ _init_ _.py _ _init_ _.py comp c1.py numeric _ _init.py_ _ n1.py n2.py Рис. 18.2. Пример структуры математического пакета, дополненной файлами __init__.py Файл в листинге 18.2 — это инициализационный файл __init__.py подпакета comp, который выводит другое сообщение, подтверждающее загрузку подпакета, и устанавливает для него атрибут __all__. Листинг 18.2. Файл mathproj/comp/__init__.py __all__ = ['c1'] print("Hello from mathproj.comp init") Файл в следующем листинге 18.3 — это файл c1.py основного пакета, который присваивает атрибуту x вложенного пакета comp значение 1.0. Листинг 18.3. Файл mathproj/comp/c1.py x = 1.00 Файл в листинге 18.4 — это инициализационный файл __init__.py подпакета numeric, который просто выводит сообщение, подтверждающее загрузку подпакета.
   18.3. Реализация пакета mathproj 437 Листинг 18.4. Файл mathproj/comp/numeric/__init__.py print("Hello from numeric init") Файл в листинге 18.5 — это файл n1.py подпакета numeric, который импортирует элементы основного пакета и подпакета comp, а также определяет функцию g(). Листинг 18.5. Файл mathproj/comp/numeric/n1.py from mathproj import version from mathproj.comp import c1 from mathproj.comp.numeric.n2 import h def g(): print("version is", version) print(h()) И наконец, файл в листинге 18.6 — это файл n2.py подпакета numeric, который определяет функцию h(). Листинг 18.6. Файл mathproj/comp/numeric/n2.py def h(): return "Called function h in module n2" Для работы с примерами данной главы при использовании блокнота Colaboratory достаточно запустить ячейку установки, чтобы создать необходимые файлы и каталоги. Если используется другая установка Python, нужно самостоятельно создать каталог mathproj и его подкаталоги и сохранить в них соответствующие файлы. Затем следует убедиться в том, что при выполнении этих примеров текущим рабочим каталогом Python является каталог, содержащий проект mathproj. ПРИМЕЧАНИЕ Для большинства листингов, приведенных в этой книге, нет необходимости запускать новую оболочку Python перед выполнением каждого из них. Как правило, фрагменты кода можно выполнять в одной и той же оболочке Python, применявшейся для предыдущих запусков, и получать корректный результат. Однако к листингам данной главы такой способ неприменим: для их надлежащего выполнения пространство имен Python должно быть чистым (не измененным предыдущими операторами import). Если вы планируете выполнять следующие примеры, убедитесь в том, что каждый из них запускается в отдельной сессии. В среде Colaboratory это можно сделать через меню Runtime (Среда выполнения), выбрав команду Restart Session (Перезапустить сессию). В блокноте перед ячейками, которым требуется чистая сессия, будут выведены соответствующие комментарии. 18.3.1. Файлы __init__.py в пакетах Вы уже наверняка заметили, что во всех каталогах вашего пакета — mathproj, mathproj/comp и mathproj/numeric — присутствует файл инициализации с именем __init__.py. Он решает две задачи:
   438 Глава 18. Пакеты Python распознает каталог как пакет, если в нем присутствует файл __init__.py. И хотя условие это необязательное, оно эффективно предотвращает случайное импортирование каталогов, содержащих посторонний код Python, в качестве пакетов. Файл __init__.py автоматически выполняется интерпретатором Python при первой загрузке основного пакета или подпакета. При этом можно задать любую инициализацию пакета, какую вы сочтете необходимой. Первый пункт обычно важнее. Для многих пакетов в файл __init__.py не нужно включать никакой код; просто проследите за тем, чтобы в каталоге присутствовал пустой файл __init__.py. 18.3.2. Основы использования пакета mathproj Прежде чем переходить к подробностям работы с пакетами, посмотрим, как обратиться к элементам пакета mathproj. Запустите новую оболочку Python и выполните следующую инструкцию: import mathproj Hello from mathproj init Если все пройдет успешно, появится новое приглашение без сообщений об ошибках. Кроме того, код из файла mathproj/__init__.py должен вывести сообщение "Hello from mathproj init". Вскоре мы поговорим о файлах __init__.py подробнее; а пока достаточно знать, что эти файлы автоматически выполняются при первой загрузке пакета. Файл mathproj/__init__.py присваивает переменной version значение 1.03. Переменная version находится в области видимости пространства имен пакета mathproj, а после создания вы можете обратиться к ней через mathproj даже за пределами файла mathproj/__init__.py: mathproj.version 1.03 По способу применения пакеты очень похожи на модули; они могут предоставить доступ к определенным в них объектам через атрибуты. И это вполне естественно, поскольку пакеты являются обобщенной формой модулей. 18.3.3. Загрузка подпакетов и подмодулей А теперь посмотрим, как различные файлы, определенные в пакете mathproj, взаимодействуют друг с другом. Для этого попробуем вызвать функцию g , определенную в файле mathproj/comp/numeric/n1.py. Первый очевидный вопрос — был ли загружен этот модуль? Вы уже загрузили mathproj, но как насчет подпакета? Чтобы понять, знает ли о нем интерпретатор, введите:
   18.3. Реализация пакета mathproj 439 mathproj.comp.numeric.n1 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'mathproj' has no attribute 'n1' Иначе говоря, загрузки модуля верхнего уровня пакета недостаточно для того, чтобы загрузить все его подмодули; и это соответствует философии Python, согласно которой ничего не должно происходить у вас за спиной. Ясность важнее лаконичности. Это ограничение достаточно легко преодолевается. Импортируйте нужный вам модуль и выполните функцию g из этого модуля: import mathproj.comp.numeric.n1 Hello from mathproj.comp init Hello from numeric init mathproj.comp.numeric.n1.g() version is 1.03 Called function h in module n2 Однако обратите внимание на побочный эффект загрузки mathproj.comp. numeric.n1: вывод строк, начинающихся с Hello. Эти две строки выводятся функцией print в файлах __init__.py из каталогов mathproj/comp и mathproj/ comp/numeric. Другими словами, прежде чем Python сможет импортировать mathproj.comp.numeric.n1, он должен импортировать mathproj.comp, а затем mathproj.comp.numeric. Всякий раз, когда пакет импортируется впервые, выполняется связанный с ним файл __init__.py; отсюда и появление строк Hello. Чтобы убедиться в том, что в процессе импортирования mathproj.comp.numeric. n1 импортируются mathproj.comp и mathproj.comp.numeric, можно проверить, что сессия Python знает теперь о mathproj.comp и mathproj.comp.numeric: mathproj.comp <module 'mathproj.comp' from 'mathproj/comp/__init__.py'> mathproj.comp.numeric <module 'mathproj.comp.numeric' from 'mathproj/comp/numeric/__init__.py'> 18.3.4. Операторы import внутри пакетов Файлы в пакете не получают автоматического доступа к объектам, определенным в других файлах того же пакета. Как и во внешних модулях, для явного обращения к объектам из других файлов пакета необходимо использовать операторы import. Чтобы увидеть, как такое использование import работает на практике, вернемся к подпакету n1. Файл n1.py содержит следующий код:
   440 Глава 18. Пакеты from mathproj import version from mathproj.comp import c1 from mathproj.comp.numeric.n2 import h def g(): print("version is", version) print(h()) В функции g используется как переменная version из пакета mathproj верхнего уровня, так и функция h из модуля n2, а следовательно, модуль, содержащий g, должен импортировать version и h, чтобы получить к ним доступ. Переменная version импортируется так же, как и в операторе import за пределами пакета mathproj, — инструкцией from mathproj import version. В этом примере функция h явно импортируется через from mathproj.comp.numeric.n2 import h, причем этот прием работает в любом файле; явное импортирование файлов пакетов разрешено всегда. И поскольку n2.py находится в одном каталоге с n1.py, также можно воспользоваться относительным импортом, поставив точку перед именем подмодуля. Иными словами, в третью строку n1.py можно включить инструкцию from .n2 import h и все будет работать как положено. Дополнительные точки позволяют подниматься на более высокие уровни иерархии пакетов, при этом можно добавлять имена модулей. Так, вместо from mathproj import version from mathproj.comp import c1 from mathproj.comp.numeric.n2 import h инструкции импорта в n1.py можно было бы записать в следующем виде: from ... import version from .. import c1 from . n2 import h Относительные импорты удобны и быстро вводятся, но помните, что они вычисляются относительно значения свойства __name__ модуля. А значит, любой модуль, запускаемый как основной (и имеющий __name__, равное __main__), не может использовать относительные импорты. 18.4. Атрибут __all__ Если вы посмотрите на различные файлы __init__.py, определенные в пакете mathproj, то заметите, что некоторые из них задают атрибут с именем __all__. Этот атрибут связан с выполнением операторов вида from ... import * и заслуживает отдельного пояснения. Казалось бы, если внешний код выполняет оператор from mathproj import *, он должен импортировать все открытые имена из mathproj. Однако на практике все обстоит сложнее. Основная проблема заключается в том, что некоторые операционные системы неоднозначно трактуют регистр символов в именах файлов.
   18.4. Атрибут __all__ 441 Поскольку объекты в пакетах могут задаваться как файлами, так и каталогами, это приводит к неопределенности — под каким именно именем может быть импортирован подпакет. При выполнении оператора from mathproj import * в каком виде будет импортироваться пакет comp — comp, Comp или COMP? Если полагаться лишь на имя, полученное от операционной системы, результаты могут быть непредсказуемыми. Данная проблема не имеет надежного решения, поскольку она обусловлена изначальными недостатками в проектировании операционных систем. В качестве оптимального обходного варианта был введен атрибут __all__. Если атрибут __all__ присутствует в файле __init__.py, он должен содержать список строк, определяющий те имена, которые должны импортироваться при выполнении from ... import * для указанного пакета. При его отсутствии оператор from ... import * не производит никаких действий. Поскольку в текстовых файлах регистр символов всегда значим, имена, под которыми импортируются объекты, становятся однозначными. Если же операционная система воспринимает comp и COMP как одно и то же, нас это не должно волновать. При помощи меню Runtime (Среда выполнения) перезапустите сессию; затем попытайтесь выполнить следующее: from mathproj import * Hello from mathproj init Hello from mathproj.comp init Атрибут __all__ в файле mathproj/__init__.py содержит одну запись — comp, поэтому оператор import импортирует только comp. Достаточно легко проверить, знает ли теперь сессия Python о существовании comp: comp <module 'mathproj.comp' from 'mathproj/comp/__init__.py'> Однако имейте в виду, что рекурсивное импортирование имен оператором from ... import * не поддерживается. Атрибут __all__ в пакете comp содержит c1, однако оператор from mathproj import * не загружает c1 автоматически, как по волшебству: c1 Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'c1' is not defined Чтобы добавить имена из mathproj.comp, вам придется снова выполнить явный импорт: from mathproj.comp import c1 c1 <module 'mathproj.comp.c1' from 'mathproj/comp/c1.py'>
   442 Глава 18. Пакеты 18.5. Правильное использование пакетов Структура большинства ваших собственных пакетов не должна быть такой же сложной, как в приведенных выше примерах. Механизм пакетов допускает большую гибкость в степени сложности и глубине вложенности, и, хотя несомненно то, что можно создавать сложные пакеты, далеко не так очевидно, что это стоит делать. Приведу несколько советов, применимых в большинстве случаев. Пакеты не должны опираться на глубоко вложенные структуры каталогов. За редким исключением — когда речь идет о действительно огромных объемах кода, — в этом нет никакой необходимости. Для большинства пакетов достаточно одного каталога верхнего уровня, а двухуровневая иерархия прекрасно подходит почти для всех остальных случаев. Как сказано в «Дзене Python» (см. приложение): «Плоское лучше, чем вложенное». Несмотря на возможность задействовать атрибут __all__ для сокрытия имен от оператора from ... import *, просто не включая их в список, такой подход, вероятно, менее предпочтителен в силу своей непоследовательности. Если требуется предотвратить загрузку элемента при импортировании всех компонентов, это можно реализовать, добавив начальное подчеркивание к имени, что обсуждалось в главе 10, посвященной модулям. Быстрая проверка: пакеты Допустим, вы пишете пакет, который получает URL-адрес страницы, извлекает все имеющиеся на ней изображения, масштабирует их до стандартного размера и сохраняет их. Не вдаваясь в подробности реализации каждой из указанных задач, каким образом вы объединили бы все эти функции в рамках пакета? 18.6. Практическая работа: создание пакета В главе 14 вы добавили обработку ошибок в модуль очистки текста и подсчета вхождений слов, созданный в главе 10. Выполните рефакторинг кода, преобразовав его в пакет, содержащий как минимум один модуль для функций очистки, другой для функций обработки и (необязательно) еще один для пользовательских исключений (при наличии таковых). Затем напишите простую функцию main, которая задействует все модули пакета. 18.6.1. Решение задачи при помощи кода, сгенерированного ИИ Главная задача данной практической работы — определить, что включить в каждую функцию. Поскольку возможны различные подходы к ее решению, мы примем любые варианты разделения, не содержащие очевидных ошибок.
   18.6. Практическая работа: создание пакета 443 18.6.2. Решения и обсуждение Как вы помните, в главе 10 мы создали модуль функций очистки текста (фрагмента первой главы романа Германа Мелвилла «Моби Дик, или Белый кит») и подсчета в нем вхождений каждого слова. Несмотря на то что тот модуль был достаточно компактным и удобным для работы, его архитектура не была оптимальной, так как отсутствовало функциональное разделение — функции очистки данных, подсчета слов и обработки пользовательских исключений были собраны в одном модуле. Применение пакета позволяет разделить программный код на отдельные функциональные модули (или даже подпакеты, если проект достаточно большой). Это упрощает работу с программой и ее сопровождение. Программное решение, созданное человеком Я создала пакет word_count , преобразовав модуль функций, написанный в главе 10, в пакет, состоящий из трех модулей. Первый файл — это __init__. py, который импортирует пользовательское исключение из exceptions.py, функции очистки данных clean_line и get_words из cleaning.py, а также функции count_words и word_stats из counter.py. Импорт этих элементов в пространство имен основного пакета избавляет пользователя от необходимости указывать исходный модуль — все будет доступно на верхнем уровне пакета (листинг 18.7). Листинг 18.7. Файл __init__.py #__init__.py from word_count.exceptions import EmptyStringError from word_count.cleaning import clean_line, get_words from word_count.counter import count_words, word_stats Вторым файлом является exceptions.py, в котором определено пользовательское исключение. Хотя такой подход вовсе не обязателен, часто бывает разумно выносить собственные исключения в отдельный файл и импортировать их по мере необходимости (листинг 18.8). Листинг 18.8. Файл exceptions.py # exceptions.py class EmptyStringError(Exception): Pass Третий файл — это cleaning.py, в котором содержится код, отвечающий за очистку данных. Обратите внимание, что он импортирует пользовательское исключение (листинг 18.9).
   444 Глава 18. Пакеты Листинг 18.9. Файл cleaning.py # cleaning.py from word_count.exceptions import EmptyStringError punct = str.maketrans("", "", "!.,:;-?") def clean_line(line): """изменяет регистр и удаляет знаки препинания""" # генерирует исключение, если строка пуста # раскомментируйте для проверки EmptyStringError # if not line.strip(): # raise EmptyStringError() # приведение к единому регистру cleaned_line = line.lower() # удаление знаков препинания cleaned_line = cleaned_line.translate(punct) return cleaned_line def get_words(line): """разделяет строку на слова и снова объединяет их символами новой строки""" words = line.split() return "\n".join(words) + "\n" Последний файл — это counter.py, в нем размещен код, который непосредственно выполняет подсчет слов и сбор статистики. Обратите внимание, что теперь этот модуль функционирует автономно, принимая на вход лишь список слов и словарь счетчиков слов (листинг 18.10). Листинг 18.10. Файл counter.py # counter.py def count_words(words): """Принимает список очищенных слов, возвращает словарь с результатами подсчета""" word_count = {} for word in words: try: count = word_count.setdefault(word, 0) except TypeError: # Если объект 'word' не является хешируемым, перейти к следующему слову. pass word_count[word] += 1 return word_count def word_stats(word_count): """Принимает словарь подсчета слов и возвращает первые и последние пять записей""" word_list = list(word_count.items()) word_list.sort(key=lambda x: x[1]) try: least_common = word_list[:5] most_common = word_list[-1:-6:-1] except IndexError as e:
   18.6. Практическая работа: создание пакета 445 # Если список пустой или слишком короткий, просто вернуть список least_common = word_list most_common = list(reversed(word_list)) return most_common, least_common Данные функции идентичны тем, что применялись в решении практической работы из главы 10, однако теперь они распределены по трем модулям, а файл __init__.py служит для их подключения при импорте пакета. Тестовый код также почти идентичен решению из главы 10, за исключением директив импорта для работы с пакетом. Основное отличие заключается в том, что импортируется пакет целиком, после чего задействуются соответствующие компоненты для очистки данных и подсчета вхождений: import word_count if __name__ == "__main__": with (open("moby_01.txt") as infile, open("moby_01_clean.txt", "w") as outfile): for line in infile: cleaned_line = word_count.clean_line(line) cleaned_words = word_count.get_words(cleaned_line) # записать все слова построчно outfile.write(cleaned_words) moby_words = [] with open('moby_01_clean.txt') as infile: for word in infile: if word.strip(): moby_words.append(word.strip()) word_counter = word_count.count_words(moby_words) most, least = word_count.word_stats(word_counter) print("Most common words:") for word in most: print(word) print("\nLeast common words:") for word in least: print(word) Этот код будет работать, если файл находится в каталоге, содержащем каталог пакета word_count, либо в ячейке Colaboratory, либо если каталог пакета word_ count включен в путь поиска модулей Python. Программные решения, сгенерированные ИИ Решение, предложенное Copilot, откровенно разочаровало. Copilot был дан следующий промпт (при нахождении в том же каталоге, что и файлы практической работы из главы 10):
   446 Глава 18. Пакеты Сделай рефакторинг кода из файла word_processing.py, преобразовав его в пакет, включающий как минимум один модуль для функций очистки, другой — для функций обработки, а также (необязательно) отдельный модуль для пользовательских исключений (при их наличии). Затем сделай рефакторинг main.py для работы с новым пакетом. Был предложен следующий план. 1. Создать каталог пакета с именем word_processing. 2. Внутри него создать три модуля: cleaning.py, processing.py и exceptions.py. 3. Перенести функции очистки в cleaning.py. 4. Перенести функции обработки в processing.py. 5. При наличии пользовательских исключений поместить их в exceptions.py. 6. В main.py импортировать необходимые функции из пакета word_processing и применить их в коде. Несмотря на то что ИИ-ассистент сформулировал вполне логичную структуру пакета word_processing, большая часть кода не была перенесена: вместо этого ИИ ограничился включением меток в духе «разместите здесь вашу реализацию». Ниже приведен листинг программного кода функции clean_line (листинг 18.11). Листинг 18.11. Модуль word_processing/cleaning.py def clean_line(line): # Реализация функции очистки Pass Обратите внимание, что ИИ не скопировал сам код — только имя функции. В листинге 18.12 проиллюстрирован импорт из предыдущего модуля и приведены функции, реализующие очистку исходного файла и его запись в другой файл. Листинг 18.12. Модуль word_processing/processing.py from .cleaning import clean_line def write_words_to_file(words, output_file): # Реализация функции write_words_to_file pass def print_common_words(word_counts, n): # Реализация функции print_common_words pass def print_least_common_words(word_counts, n): # Реализация функции print_least_common_words pass def process_file(input_file_path, output_file_path): with open(input_file_path, 'r') as input_file:
   18.6. Практическая работа: создание пакета 447 with open(output_file_path, 'w') as output_file: for line in input_file: cleaned_line = clean_line(line) words = cleaned_line.split() write_words_to_file(words, output_file) В этом модуле также отсутствует реализация большинства функций, за исключением process_file. Затем создается модуль, содержащий пользовательские исключения (листинг 18.13). Листинг 18.13. Модуль word_processing/exceptions.py # Если имеются пользовательские исключения, определите их здесь Поскольку пользовательские исключения являлись необязательными, отсутствие их реализации допустимо. В завершение представлен основной модуль, выполняющий импорт функций из модуля обработки данных (листинг 18.14). Листинг 18.14. Модуль main.py from word_processing.processing import process_file # Использование функции process_file function из пакета word_processing process_file('input.txt', 'output.txt') Данный модуль действительно осуществляет импорт из модуля обработки и вызывает функцию process_file. Эта функция лишь выполняет чтение исходного файла и запись его в виде отдельных слов в другой файл. Как упоминалось ранее, базовая структура библиотеки в целом соответствует условиям промпта, хотя с формальной точки зрения функции write_words_to_file и process_file скорее относятся к очистке, чем к обработке. Главное разочарование заключается в том, что, несмотря на использование имен функций из исходного модуля, ИИ-ассистент не перенес их реализацию, за исключением process_file. Кроме того, в main.py пакет задействован не в полной мере по сравнению с оригиналом. В завершение стоит отметить, что Copilot не создавал файл __init__.py до тех пор, пока не получил явное указание в следующем промпте: Также добавь файл init.py, который надлежащим образом импортирует функции пакета. В ответ на этот промпт Copilot предложил вариант, приведенный в листинге 18.15. Листинг 18.15. Модуль word_processing/__init__.py from .cleaning import clean_line from .processing import (write_words_to_file, print_common_words, print_least_common_words, process_file) Также он сделал следующее корректное предложение по импорту пакета:
   448 Глава 18. Пакеты from word_processing import (clean_line, write_words_to_file, print_common_words, print_least_common_words, process_file) В целом это было одно из самых бесполезных решений от Copilot — и первое, в котором пришлось вернуться и переработать промпт, чтобы четко указать нужный результат. Инструменты ИИ иногда не понимают даже, казалось бы, совершенно ясные запросы, и заранее предсказать такие случаи невозможно. Главное — быть готовым к такому повороту событий и корректировать промпт, если потребуется. Новый промпт содержал явное указание для Copilot задействовать уже имеющийся программный код функций: Сделай рефакторинг кода из файла word_processing.py, преобразовав его в пакет, включающий как минимум один модуль для функций очистки, другой — для функций обработки, а также (необязательно) отдельный модуль для пользовательских исключений (при их наличии). Обязательно примени код из исходного файла, чтобы воспроизвести его функциональность. Затем сделай рефакторинг main.py для работы с новым пакетом. Предложенный план на этот раз практически не изменился, однако в него был добавлен шаг по созданию файла __init__.py, который был корректно реализован. Результат получился существенно более полным: большая часть функций теперь содержала программный код. Тем не менее функции clean_line (в word_processing/cleaning.py) и write_words_to_file (в word_processing/ processing.py) по-прежнему были представлены лишь в виде комментариев. В связи с этим был сформулирован дополнительный промпт для того, чтобы Copilot дописал их код: Предоставь код для функций clean_line и write_words_to_file. Copilot сформировал программный код, после чего предоставил готовые файлы в полном объеме. В первую очередь Copilot сгенерировал модуль word_processing/cleaning.py, содержащий реализацию функции clean_line (листинг 18.16). Листинг 18.16. Модуль word_processing/cleaning.py def clean_line(line): # Удалить знаки препинания и привести все к нижнему регистру line = re.sub(r'[^\w\s]', '', line) line = line.lower() return line В соответствии со вторым дополнительным промптом Copilot корректно скопировал код функции, однако не добавил импорт модуля re. И хотя можно было бы еще поэкспериментировать с промптом, целесообразнее было вручную добавить недостающую строку в начало файла. import re
   18.6. Практическая работа: создание пакета 449 Вторым был сгенерирован файл для функций обработки (листинг 18.17). Листинг 18.17. Модуль word_processing/processing.py def count_word_occurrences(words): word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts def print_common_words(word_counts, n): sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) print(f"{n} most common words:") for word, count in sorted_word_counts[:n]: print(f"{word}: {count}") def print_least_common_words(word_counts, n): sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) print(f"{n} least common words:") for word, count in sorted_word_counts[-n:]: print(f"{word}: {count}") def write_words_to_file(words, output_file): for word in words: output_file.write(word + '\n') def process_file(input_file_path, output_file_path): with open(input_file_path, 'r') as input_file: with open(output_file_path, 'w') as output_file: for line in input_file: cleaned_line = clean_line(line) words = cleaned_line.split() write_words_to_file(words, output_file) При использовании модифицированного и второго промптов все функции были корректно заполнены, однако снова отсутствовал один оператор импорта. На этот раз не была импортирована функция clean_line из модуля cleaning.py. Как и в предыдущем случае, вместо дополнительной настройки промпта проще было самостоятельно вставить нужную строку в начало файла. from text_processor.cleaning import clean_line Затем в соответствии с промптом Copilot создал модуль исключений, но, поскольку реализация пользовательских исключений была необязательной, он вполне обоснованно не добавил ни одной такой конструкции (лис­ тинг 18.18).
   450 Глава 18. Пакеты Листинг 18.18. Модуль word_processing/exceptions.py # Если имеются пользовательские исключения, определите их здесь После явного указания Copilot наконец-то создал модуль __init__.py (листинг 18.19). Листинг 18.19. Модуль word_processing/__init__.py from .cleaning import clean_line from .processing import ( count_word_occurrences, print_common_words, print_least_common_words, write_words_to_file, process_file ) В этом модуле присутствуют все операторы импорта, они корректно и аккуратно отформатированы. Последним создается файл main.py — он выполняет все необходимые импорты, а затем вызывает различные функции (листинг 18.20). Листинг 18.20. Модуль main.py from word_processing import ( process_file, count_word_occurrences, print_common_words, print_least_common_words ) # Пример использования input_file_path = 'input.txt' output_file_path = 'output.txt' process_file(input_file_path, output_file_path) # Предполагается, что имеется некий список слов words = ["example", "words", "for", "testing", "example", "words"] word_counts = count_word_occurrences(words) print_common_words(word_counts, 3) print_least_common_words(word_counts, 3) Представленное решение далеко от идеала: имена файлов для исходного и очищенного текста жестко заданы в коде, после чего очищенный файл попросту игнорируется, а для тестирования программы задействуется произвольный словесный ряд ["example", "words", "for", "testing", "example", "words"], снабженный комментарием «Предполагается, что имеется некий список слов». Вместо этого следовало бы считать очищенный текстовый файл и воспользоваться методом split() для получения требуемого списка. Существует два способа это исправить. Первый — вернуться к промпту и указать имена файлов, а также потребовать, чтобы для подсчета использовался
   18.6. Практическая работа: создание пакета 451 очищенный файл. Но при таком подходе добавляется еще один шаг в процесс разработки, и, возможно, вам придется не раз корректировать промпт, чтобы получить желаемый результат. Второй вариант — внести исправления самостоятельно: подправить имена файлов и добавить код для чтения и разделения нужного файла, чтобы получить список слов для подсчета. При разработке с применением средств ИИ важно учитывать данные компромиссы и принимать решение, в каких случаях целесообразна дальнейшая разработка промптов, а когда эффективнее внести изменения вручную и продолжить работу. У программного решения Colaboratory обнаружились свои недостатки. В новой сессии блокнота, куда был загружен только код из главы 10, ИИ-ассистенту был задан следующий промпт: Сделай рефакторинг кода из модуля, созданного в ячейке выше, преобразовав его в пакет, включающий как минимум один модуль для функций очистки, другой — для функций обработки, а также (необязательно) отдельный модуль для пользовательских исключений (при их наличии). Затем сделай рефакторинг main() для работы с новым пакетом. С учетом ограничений среды Colaboratory, где работа строится скорее вокруг ячеек, чем файлов, решение выглядело многообещающе, хотя весь код оказался в одной ячейке. Чтобы облегчить анализ, разберем содержимое этой ячейки по частям, начиная с промпта и системной команды, создающей каталог пакета text_processor: # Сделай рефакторинг кода из модуля, созданного в ячейке выше, преобразовав его # в пакет, включающий как минимум один модуль для функций очистки, # другой — для функций обработки, а также (необязательно) отдельный модуль # для пользовательских исключений (при их наличии). Затем сделай рефакторинг main() # для работы с новым пакетом. # Создать каталог пакета !mkdir text_processor Команда для создания каталога пакета Формулировка промпта достаточно простая и однозначная, а вызов системной команды для создания каталога пакета обязателен до выполнения остальных шагов. Следующий раздел содержит код первого файла — text_processor/cleaning. py — с функцией очистки текста: # Создать файл для функций очистки %%writefile text_processor/cleaning.py #### ЭТОТ ОПЕРАТОР ИМПОРТА ДОБАВЛЕН АВТОРОМ import string def clean_line(line): # Привести строку к нижнему регистру line = line.lower() Импорт модуля string для знаков препинания
   452 Глава 18. Пакеты # Удалить из строки знаки препинания line = line.translate(str.maketrans('', '', string.punctuation)) return line При размещении кода в отдельной ячейке он исполняется корректно, так как «магическая» команда %%writefile стоит в самом начале. Однако при включении всего кода в одну ячейку это вызывает ошибку. Кроме того, в решении чат-бота отсутствует импорт модуля string, необходимого для доступа к знакам препинания. Далее следует раздел, содержащий функции обработки: # Создать файл для функций обработки %%writefile text_processor/processing.py #### ЭТОТ ОПЕРАТОР ИМПОРТА ДОБАВЛЕН АВТОРОМ from text_processor.cleaning import clean_line Необходимо вызвать функцию clean_line def write_words_to_file(words, output_file): # Записать каждое слово в выходной файл for word in words: output_file.write(word + '\n') def count_word_occurrences(words): # Подсчитать количество вхождений каждого слова с помощью словаря word_counts = {} for word in words: if word in word_counts: word_counts[word] += 1 else: word_counts[word] = 1 return word_counts def print_common_words(word_counts, n): # Отсортировать результаты подсчета количества слов в порядке убывания sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести n наиболее часто встречающихся слов и количество их вхождений print(f"{n} most common words:") for word, count in sorted_word_counts[:n]: print(f"{word}: {count}") def print_least_common_words(word_counts, n): # Отсортировать результаты подсчета количества слов в порядке убывания sorted_word_counts = sorted(word_counts.items(), key=lambda x: x[1], reverse=True) # Вывести n наименее часто встречающихся слов и количество их вхождений print(f"{n} least common words:") for word, count in sorted_word_counts[-n:]: print(f"{word}: {count}")
   18.6. Практическая работа: создание пакета 453 def process_file(input_file_path, output_file_path): # Открыть входной файл для чтения with open(input_file_path, 'r') as input_file: # Открыть выходной файл для записи with open(output_file_path, 'w') as output_file: # Перебрать каждую строку во входном файле for line in input_file: # Очистить строку cleaned_line = clean_line(line) # Разделить строку на слова words = cleaned_line.split() # Записать слова в выходной файл write_words_to_file(words, output_file) Этот фрагмент кода создает второй модуль в составе пакета — text_processor/ processing.py. Будь он размещен в отдельной ячейке, все бы работало корректно. Однако снова упущен необходимый оператор импорта — функция clean_line не импортирована из модуля cleaning. После добавления недостающего оператора импорта пакет становится работоспособным. Далее представлен код файла text_processor/__init__.py: # Создайте файл __init__.py для того, чтобы преобразовать каталог в пакет %%writefile text_processor/__init__.py from .cleaning import clean_line from .processing import process_file, count_word_occurrences, print_common_words, print_least_common_words Данный блок формирует файл __init__.py в структуре пакета, обеспечивая автоматический импорт функций из модулей cleaning и processing при импорте самого пакета. Заключительная часть содержит программный код, задействующий только что созданный пакет: # Рефакторинг main() для работы с новым пакетом %%writefile main.py import text_processor input_file_path = 'moby_01.txt' output_file_path = 'moby_01_clean.txt' # Обработать файл text_processor.process_file(input_file_path, output_file_path) # Открыть файл with open(output_file_path, 'r') as file: # Прочитать содержимое файла content = file.read() # Разделить содержимое файла на отдельные слова words = content.split()
   454 Глава 18. Пакеты # Подсчитать количество вхождений слов word_counts = text_processor.count_word_occurrences(words) # Вывести 5 наиболее часто встречающихся слов и количество их вхождений text_processor.print_common_words(word_counts, 5) # Вывести 5 наименее часто встречающихся слов и количество их вхождений text_processor.print_least_common_words(word_counts, 5) Представленный выше код генерирует файл main.py, который импортирует пакет и вызывает его функции в установленном порядке. Имена файлов с исходным и очищенным текстом прописаны напрямую в коде, но соответствуют ожидаемым. Последний фрагмент кода — это команда для запуска скрипта main.py: # Запустить основной скрипт !python main.py Эта команда просто вызывает системный интерпретатор Python, передавая ему в качестве аргумента скрипт main.py. Как отмечалось выше, основная проблема решения в среде Colaboratory заключалась в том, что при запуске ячейки возникала ошибка на первой директиве %%writefile, поскольку «волшебные» команды Jupyter должны располагаться строго в начале ячейки. Если переместить каждый раздел с %%writefile в отдельную ячейку и добавить два недостающих оператора импорта, то при последовательном выполнении ячеек все файлы создаются должным образом. Хотя решение, предложенное ИИ-инструментом Colaboratory, не работает сразу, после внесения указанных выше корректив оно в конечном счете удовлетворяет заданным требованиям. Исправленная версия с правильным разделением по ячейкам и добавлением недостающих операторов импорта включена в Jupyter Notebook для этой главы. Для данной практической работы почти не потребовалось писать новый код. Вместо этого задача заключалась в усвоении структуры пакетов Python и умении правильно выбрать и разместить элементы существующей программы в нужных частях нового пакета. И хотя большинству опытных программистов подобная задача покажется вполне заурядной, для наших ИИ-ассистентов она оказалась крепким орешком. Оба чат-бота сгенерировали полезные фрагменты, но ни один из них не смог самостоятельно создать полностью работающее решение. Хотя переформулировка промптов может отчасти исправить ситуацию, всегда стоит помнить о соотношении затраченного времени: дорабатывать промпт или просто написать код вручную. Это особенно актуально при применении средств ИИ для решения задач более сложного уровня.
   Итоги 455 Итоги Пакеты позволяют создавать библиотеки кода, охватывающие множество файлов и каталогов. Применение пакетов обеспечивает более эффективную организацию крупных наборов кода, чем это возможно при использовании отдельных модулей. При наличии файла __init__.py Python распознает содержащий его каталог как пакет и исполняет этот файл при импорте пакета. Подпакеты следует импортировать явно — либо в пользовательском коде, либо в файле __init__.py самого пакета. Остерегайтесь избыточной вложенности каталогов в пакетах более чем на один-два уровня, за исключением случаев разработки крупных и комплексных библиотек. Атрибут __all__ применяется для скрытия элементов при импорте с помощью подстановочного знака *, однако предпочтительнее явно исключать их, используя префикс «_» в именах.
19 Работа с библиотеками Python В этой главе: 3 Управление различными типами данных — строками, числами и другими 3 Файловые операции и хранение данных 3 Доступ к службам операционной системы 3 Применение интернет-протоколов и форматов 3 Инструменты разработки и отладки 3 Доступ к каталогу пакетов Python (PyPI) 3 Установка библиотек Python и виртуальных сред при помощи pip и venv Уже давно создатели Python провозгласили, что одним из ключевых преимуществ языка является его философия «все включено», или, буквально, «батарейки в комплекте». Это означает, что в базовую установку Python входит богатейшая, полнофункциональная стандартная библиотека, позволяющая справиться с широким спектром задач без необходимости установки дополнительных библиотек. В этой главе дан краткий обзор избранных компонентов стандартной библиотеки, а также приведен ряд рекомендаций по поиску и установке внешних модулей. Поскольку содержание главы носит исключительно справочный характер, практическая работа или другие задания в ней не предусмотрены.
   19.1. «Все включено»: стандартная библиотека 457 19.1. «Все включено»: стандартная библиотека То, что считается библиотекой в Python, состоит из нескольких компонентов, включающих встроенные типы данных и константы, которыми можно воспользоваться, не прибегая к оператору import, а именно числа и списки, встроенные функции и исключения. Самую большую часть библиотеки составляет обширная коллекция модулей. Вместе с Python на компьютер также устанавливаются библиотеки для работы с разными типами данных и файлов, взаимодействия с операционной системой, создания серверных и клиентских приложений для множества интернет-протоколов, а также разработки и отладки программного кода. Далее следует обзор ключевых возможностей. Несмотря на то что в нем упоминаются многие важные модули, за наиболее полной и актуальной информацией я рекомендую обратиться к справочному руководству по библиотеке, входящему в официальную документацию Python. Особенно перед тем, как искать сторонние библиотеки, не поленитесь просмотреть, что уже входит в стандартную поставку Python, — не исключено, что вас ждет приятный сюрприз. 19.1.1. Управление различными типами данных Стандартная библиотека, разумеется, содержит средства поддержки встроенных типов Python, которые мы затронем в этом разделе. Кроме того, в стандартной библиотеке можно выделить три категории модулей, связанных с типами данных: строковые службы, специализированные типы данных и числовые модули. К строковым службам относятся модули, указанные в табл. 19.1, предназначенные для работы со строками и байтовыми объектами. Эти модули в основном обрабатывают текстовые строки, байтовые объекты и операции с кодировкой Unicode. Таблица 19.1. Модули строковых служб Модуль Описание и возможные способы применения string Сравнение со строковыми константами (такими, как digits или whitespace); форматирующие строки (см. главу 6) re Поиск и замена текста с использованием регулярных выражений (см. главу 16) struct Интерпретация байтовых объектов как упакованных двоичных данных, чтение и запись структурированных данных в файлы и из них difflib Применение вспомогательных функций для вычисления дельт, выявление различий между строками или последовательностями, создание патчей и diffфайлов textwrap Перенос, обтекание и выравнивание текста, форматирование текста путем разделения строк или добавления пробелов
   458 Глава 19. Работа с библиотеками Python Категория «специализированные типы данных» представляет собой разноплановый набор модулей, охватывающих различные типы данных — в частности, время, дату и коллекции, как показано в табл. 19.2. Таблица 19.2. Модули специализированных типов данных Модуль Описание и возможные способы применения datetime, calendar Операции с датой, временем и календарем collections Контейнерные типы данных enum Позволяет создавать классы-перечисления, связывающие символьные имена со значениями констант array Эффективная реализация массивов с числовыми элементами sched Планировщик событий queue Класс синхронизированной очереди copy Операции поверхностного и глубокого копирования объектов pprint Структурированный вывод данных typing Поддержка аннотаций типов в коде, в частности, для параметров и возвращаемых значений функций Как следует из их названия, числовые и математические модули предназначены для работы с числами и математических вычислений. Самые популярные из них приведены в табл. 19.3. Эти модули содержат все необходимое для создания собственных числовых типов и реализации широкого спектра математических операций. Таблица 19.3. Числовые и математические модули Модуль Описание и возможные способы применения numbers Числовые абстрактные базовые классы math, cmath Математические функции для работы с вещественными и комплексными числами decimal Арифметические вычисления с фиксированной и плавающей точкой в десятичной системе statistics Функции для вычисления математической статистики fractions Рациональные числа random Генерирование псевдослучайных чисел, случайный выбор элементов, перемешивание последовательностей itertools Функции, создающие итераторы для эффективной организации циклов functools Функции высшего порядка и операции над вызываемыми объектами operator Стандартные операторы в виде функций
   19.1. «Все включено»: стандартная библиотека 459 19.1.2. Файловые операции и хранение данных Еще одна обширная категория стандартной библиотеки включает инструменты для работы с файлами, хранилищем и организации постоянного хранения данных. Сводка по ней приведена в табл. 19.4. В эту категорию входят модули для доступа к файловой системе, сериализации данных, их сжатия и обработки специализированных файловых форматов. Таблица 19.4. Модули для работы с файлами и хранения данных Модуль Описание и возможные способы применения os.path Стандартные операции с путями к файлам pathlib Объектно-ориентированная работа с путями fileinput Итерация по строкам из нескольких входных потоков filecmp Сравнение файлов и каталогов tempfile Создание временных файлов и каталогов glob, fnmatch Работа с путями и именами файлов при помощи шаблонов в стиле UNIX linecache Произвольный доступ к текстовым строкам shutil Выполнение высокоуровневых операций с файлами pickle, shelve Сериализация и постоянное хранение объектов Python sqlite3 Использование интерфейса DB-API 2.0 для взаимодействия с базами данных SQLite zlib, gzip, bz2, zipfile, tarfile Работа с архивными файлами и сжатием csv Чтение и запись файлов в формате CSV configparser Работа с парсером конфигурационных файлов; чтение и запись конфигураций в формате .ini (в стиле Windows) 19.1.3. Доступ к службам операционной системы Эта категория также весьма обширна и включает модули для взаимодействия с операционной системой. Как показано в табл. 19.5, сюда входят средства обработки параметров командной строки, перенаправления потоков ввода-вывода (файлового и печати), организации логирования, поддержки многопоточности или многопроцессной обработки, а также подключения внешних библиотек (в основном написанных на C) к программам на Python.
   460 Глава 19. Работа с библиотеками Python Таблица 19.5. Модули для работы с операционной системой Модуль Описание os Универсальные интерфейсы для взаимодействия с операционной системой io Основные средства работы с потоками time Доступ ко времени и его преобразование optparse Эффективный парсер ключей командной строки logging Система логирования для Python getpass Переносимый механизм ввода пароля curses Управление терминалом с символьным интерфейсом platform Доступ к идентификационным данным базовой платформы ctypes Библиотека для работы с внешними (обычно С) функциями в Python select Ожидание завершения ввода-вывода threading Высокоуровневый интерфейс многопоточности multiprocessing Интерфейс многозадачности на основе процессов subprocess Управление подпроцессами 19.1.4. Работа с интернет-протоколами и форматами данных Эта категория содержит модули, обеспечивающие кодирование и декодирование стандартных форматов, применяемых при сетевом взаимодействии, включая MIME, JSON, XML и др. Кроме того, она содержит средства для реализации серверов и клиентов сетевых сервисов (преимущественно HTTP), а также модуль для создания собственных сетевых служб на основе сокетов. Наиболее востребованные модули перечислены в табл. 19.6. Таблица 19.6. Модули поддержки интернет-протоколов и форматов данных Модуль Описание socket, ssl Интерфейс низкоуровневого сетевого взаимодействия и обертка SSL для сокетных объектов email Пакет для работы с электронной почтой и MIME json Кодирование и декодирование JSON mailbox Управление почтовыми ящиками в различных форматах mimetypes Сопоставление имен файлов с MIME-типами base64, binhex, binascii, quopri, uu Кодирование/декодирование файлов или потоков с различными кодировками
   19.1. «Все включено»: стандартная библиотека Модуль Описание html.parser, html.entities Парсинг HTML и XHTML xml.parsers.expat, xml. dom, xml.sax, xml.etree. ElementTree Набор парсеров и инструментов для работы с XML cgi, cgitb Поддержка CGI (Common Gateway Interface) wsgiref Служебные средства и эталонная реализация WSGI urllib.request, urllib. parse Открытие и парсинг URL-адресов ftplib, poplib, imaplib, nntplib, smtplib, telnetlib Клиенты для различных интернет-протоколов socketserver Фреймворк для создания сетевых серверов на основе сокетов http.server Серверы HTTP xmlrpc.client, xmlrpc. server Клиент и сервер XML-RPC 461 19.1.5. Средства разработки и отладки, службы времени выполнения В языке Python предусмотрен набор модулей для отладки, тестирования, модификации и интерактивной работы с программным кодом во время его выполнения. Как показано в табл. 19.7, эта категория включает два инструмента тестирования, профилировщики, модули для работы с трассировкой ошибок, сбором мусора и т. д., а также модули, позволяющие настроить процесс импорта других модулей. Таблица 19.7. Модули разработки, отладки и времени выполнения Модуль Описание pydoc Генератор документации и система справки в реальном времени doctest Тестирование интерактивных примеров Python unittest Фреймворк для модульного тестирования test.support Служебные функции для тестов pdb Отладчик Python profile, cProfile Профилировщики Python timeit Измерение времени выполнения небольших фрагментов кода trace Трассировка (пошаговое отслеживание) выполнения инструкций Python sys Параметры и функции, специфичные для конкретной платформы
   462 Глава 19. Работа с библиотеками Python Модуль Описание atexit Функции обработки выхода из программы __future__ Определения будущих операторов — директивы активации будущих возможностей Python gc Интерфейс управления сбором мусора inspect Анализ и исследование объектов во время выполнения imp Доступ к внутренней реализации системы импорта zipimport Импортирование модулей из zip-архивов modulefinder Поиск модулей, используемых скриптом 19.2. За пределами стандартной библиотеки Хотя философия Python «все включено» и хорошо укомплектованная стандартная библиотека обеспечивают широкие возможности без установки дополнительных компонентов, со временем может возникнуть потребность в таких функциональных средствах, которые не поставляются вместе с языком. Ниже приводится обзор возможных путей решения, если необходимая вам функциональность не входит в арсенал стандартной библиотеки. 19.3. Установка дополнительных библиотек Python Чтобы найти нужный пакет или модуль Python, иногда бывает достаточно ввести описание необходимой вам функции (скажем, теги mp3 и Python) в поисковой системе и выбрать подходящие результаты. Такой способ охватывает максимально широкий круг решений, однако важно помнить, что не все источники одинаково надежны и заслуживают доверия. В конечном счете именно вы отвечаете за безопасность и работоспособность программ, запускаемых на вашем компьютере, поэтому обязательно учитывайте происхождение и качество кода до того, как им воспользуетесь. Если повезет, вам может попасться нужный модуль, уже адаптированный под вашу операционную систему — в виде установщика для Windows или macOS либо в виде пакета для вашей Linux-системы. Это может быть одним из самых удобных способов добавления библиотеки в Python, поскольку инсталлятор или менеджер пакетов сам позаботится обо всех зависимостях и разместит модуль в вашей системе надлежащим образом. Такой метод особенно подходит для установки комплексных библиотек, например научных, которые предъявляют особые требования к процессу сборки и корректному установлению зависимостей. С другой стороны, если не брать в расчет научные библиотеки, то предварительно подготовленные пакеты — скорее исключение, чем правило в мире Python. Такие пакеты часто оказываются несколько устаревшими, что может привести
   19.5. Установка библиотек Python при помощи pip и venv 463 к проблемам совместимости с более новыми версиями, и предоставляют меньше свободы выбора места установки и способов настройки. 19.4. Индекс пакетов Python (The Python Package Index) Хотя пакеты с исходным кодом вполне справляются со своей задачей, есть одна загвоздка: сначала нужно найти подходящий пакет, а это может оказаться весьма хлопотным делом. И даже если вы его отыскали, нет гарантии, что репозиторий безопасен, а сам пакет надежен. Словом, желательно иметь проверенный источник с удобной навигацией для скачивания таких пакетов. Чтобы удовлетворить эту потребность, с годами появилось множество репозиториев пакетов Python. В настоящее время официальным (но далеко не единственным) репозиторием кода Python является «Индекс пакетов Python» (Python Package Index, сокращенно PyPI), доступный через сайт python.org. Перейти на него можно как с главной страницы веб-сайта, так и по прямой ссылке: https://pypi. python.org. На PyPI размещены пакеты для различных версий Python — более чем из 604 000 проектов на момент написания данной книги. Пакеты упорядочены по дате и имени, но их также можно искать по ключевым словам, категориям, версиям Python и другим параметрам. Если требуемого функционала нет в стандартной библиотеке, следующим шагом, как правило, становится поиск на PyPI. 19.5. Установка библиотек Python при помощи pip и venv Если вам понадобится сторонний пакет, который не поставляется в виде готового установщика для вашей платформы, придется прибегнуть к «родному» способу установки пакетов в Python. Это сопряжено с рядом трудностей. Сначала нужно найти и скачать нужный модуль. Грамотная установка даже одного модуля Python потребует изрядных хлопот по настройке путей Python и разрешений вашей системы. В таких случаях наличие стандартного механизма установки существенно упрощает задачу. Не требуется для Colaboratory Отличная новость для пользователей Colaboratory: процессы, описанные далее в этом разделе, вероятнее всего, вам не понадобятся. В Colaboratory предустановлены самые востребованные сторонние библиотеки, и поскольку каждая сессия запускается в собственной временной среде, вам не нужно будет вручную управлять виртуальными средами. Остальные материалы раздела будут полезны тем, кто работает с локальной установкой Python и самостоятельно настраивает виртуальные среды.
   464 Глава 19. Работа с библиотеками Python Для решения обеих проблем в Python существует менеджер пакетов pip. Он ищет модуль в каталоге Python Package Index, загружает его со всеми зависимостями и берет на себя установку. Базовый синтаксис pip очень прост. К примеру, чтобы установить популярную библиотеку requests из командной строки, достаточно выполнить команду: $ python3 -m pip install requests Чтобы обновить библиотеку до последней версии, достаточно добавить параметр --upgrade: $ python3 -m pip install --upgrade requests Наконец, если требуется установить конкретную версию пакета, укажите ее после имени следующим образом: $ python3 -m pip install requests==2.32.3 $ python3 -m pip install requests>=2.32 Имейте в виду, что в среде Colaboratory большинство распространенных пакетов (в том числе requests) уже предустановлены, однако при необходимости вы можете установить и другие пакеты с помощью описанных выше команд, предваряя их восклицательным знаком !: ! python3 -m pip install requests Многие поставки Python уже содержат pip, однако в ряде дистрибутивов Linux (например, основанных на Debian или Ubuntu) pip выделен в отдельный пакет, который нужно устанавливать отдельно. 19.5.1. Установка с флагом --user Во многих случаях установка пакета Python в основную системную версию интерпретатора либо невозможна, либо крайне нежелательна. К примеру, необходимо установить новейшую версию библиотеки, в то время как другая программа (или сама ОС) зависит от более старой. Или, возможно, вы не обладаете привилегиями доступа для модификации основной установки Python в системе. В таких случаях можно установить библиотеку с флагом --user. С этим флагом библиотека устанавливается в домашнем каталоге пользователя, недоступном для других пользователей. Так, чтобы установить requests только для себя, введите: $ python -m pip install --user requests Как сказано выше, этот способ особенно полезен при работе в системе, в которой вы не обладаете достаточными административными правами для установки программ, или если вы желаете параллельно установить другую версию модуля. Если ваши потребности выходят за рамки основных методов установки,
   19.5. Установка библиотек Python при помощи pip и venv 465 описанных здесь, начните с раздела «Установка модулей Python» («Installing Python Modules») в документации Python. 19.5.2. Виртуальные среды Если вы хотите избежать установки библиотек в системный Python, есть другой, более предпочтительный вариант — виртуальная среда (virtual environment, сокращенно virtualenv). Виртуальная среда — это изолированная файловая структура, содержащая собственную инсталляцию Python и всех дополнительных пакетов. Благодаря тому что интерпретатор и пакеты работают изолированно, они не вступают в конфликт с системными или другими виртуальными средами. Это позволяет задействовать для разных приложений разные версии интерпретатора и библиотечных модулей. Создание виртуальной среды складывается из двух этапов. Сначала создается сама среда: myuser@mymachine$ python -m venv test-env Эта команда создает окружение, в котором Python и pip устанавливаются в каталоге с именем test-env. После создания среды ее необходимо активировать. В Windows это делается так: C:\Users\myuser> test-env\Scripts\activate.bat В командной строке Windows отображается текущий каталог (домашний каталог пользователя) В системах Unix или macOS для активации виртуальной среды можно использовать команду source или ее сокращенную форму .: myuser@mymachine$ source test-env/bin/activate username@machinename — стандартная строка запроса в системах Linux и Unix При активации среды можно по-прежнему применять pip для управления пакетами, однако в виртуальной среде pip является автономной командой: (test-env) myuser@mymachine$ pip install requests После активации имя виртуальной среды подставляется в начало строки запроса Кроме того, та версия Python, с которой вы создали среду, становится версией по умолчанию в этой среде и будет применяться при каждой ее активации. Виртуальные среды представляют собой эффективный способ управления проектами и их зависимостями и в настоящее время считаются стандартным подходом, особенно среди разработчиков, ведущих несколько проектов. Для получения дополнительной информации обратитесь к разделу «Виртуальные
   466 Глава 19. Работа с библиотеками Python среды и пакеты» («Virtual Environments and Packages») в онлайн-документации Python. 19.5.3. Прочие варианты Для управления пакетами и виртуальными средами есть ряд альтернативных решений, и, учитывая непрекращающиеся разработки в этой области, можно предположить, что к моменту выхода книги появятся новые инструменты. На момент написания я пользуюсь Pyenv как для управления виртуальными средами, так и для установки разных версий интерпретатора Python. Имеется ряд альтернативных систем, выполняющих те же функции, что и pip, в их числе poetry, rye, uv и др. Преимущество pip и venv в том, что они являются официальными стандартными средствами в текущих версиях Python. По мере роста вашего опыта и осознания конкретных требований к разработке, вы, вероятно, захотите освоить и другие инструменты. Существует еще одна широко востребованная сборка Python — Anaconda, особенно популярная в сфере научных вычислений и data science. В ней используется собственный менеджер пакетов под названием conda, эффективно решающий задачи управления сложными зависимостями. Хотя на момент написания книги репозиторий Anaconda содержит около 8000 пакетов — значительно меньше, чем предлагает PyPI, — его преимущество заключается в специализации на библиотеках для науки о данных и смежных дисциплин. Итоги Стандартная библиотека Python чрезвычайно обширна и охватывает больше типовых задач, чем аналогичные средства в большинстве других языков программирования. Поэтому перед поиском сторонних модулей рекомендуется внимательно изучить ее содержимое. Стандартная библиотека включает среди прочего модули для работы с различными типами данных, управления файлами и файловыми системами, доступа к службам операционной системы, обработки интернет-протоколов, а также разработки и отладки Python-кода. Если все же возникает необходимость в установке стороннего модуля, наилучшим решением обычно становятся предварительно собранные пакеты, адаптированные под вашу операционную систему. Однако такие пакеты нередко бывают устаревшими, и отыскать подходящий не всегда просто. Для установки пакетов исходного кода принято использовать pip, а лучший способ избежать конфликтов между несколькими проектами — создавать виртуальные среды при помощи модуля venv. При поиске внешних модулей прежде всего стоит заглянуть в репозиторий PyPI.
Часть 4 Работа с данными Эта часть книги посвящена применению языка Python на практике, в частности, для обработки данных, что является одной из его сильных сторон. Начнем мы с азов обработки файлов, затем перейдем к чтению и записи простых текстовых (плоских) файлов, работе с более структурированными форматами (такими, как JSON и Excel), использованию баз данных и анализу данных при помощи Python. В отличие от остального материала книги, эти главы носят более проектноориентированный характер и в целом предназначены для того, чтобы помочь вам обрести реальные практические навыки применения Python для работы с данными. Главы и проекты этой части можно осваивать в той последовательности и в том сочетании, которые наилучшим образом соответствуют вашим целям и задачам.
20 Общая подготовка файлов В этой главе: 3 Перемещение и переименование файлов 3 Сжатие и шифрование файлов 3 Выборочное удаление файлов В главе рассматриваются основные операции, которыми можно воспользоваться при управлении постоянно растущим массивом файлов. Это могут быть логфайлы или данные из обычного потока данных, — как бы то ни было, их нельзя просто взять и разом все удалить. Как организовать их хранение, обработку и последующую утилизацию по заданному плану — при этом без участия пользователя? 20.1. Проблема: бесконечный поток файлов данных Многие системы генерируют постоянный поток файлов данных. К ним могут относиться журналы работы сервера электронной торговли или результаты регулярной обработки, ночные выгрузки информации о продуктах с серверов, автоматические ленты данных по онлайн-рекламе или хронологические сводки биржевых котировок; словом, они могут стекаться из тысяч различных источников. Как правило, это простые (или так называемые плоские) текстовые файлы без сжатия и структурирования, которые содержат исходные данные,
   20.2. Сценарий: кошмарный товарный фид 469 либо передаваемые на вход, либо являющиеся побочным выводом других процессов. Однако, несмотря на их непритязательный характер, содержащаяся в таких файлах информация обладает потенциальной ценностью, вследствие чего они не подлежат удалению по завершении дня, что приводит к их неуклонному накоплению. Со временем объем файлов становится столь велик, что их обработка вручную представляется трудноосуществимой, а потребляемые ресурсы хранения — неприемлемо высокими. 20.2. Сценарий: кошмарный товарный фид Типичная ситуация, с которой я не раз сталкивалась, — ежедневный фид данных о товарах. Такого рода сведения могут поступать от поставщиков или отправляться для сервисов онлайн-продаж и электронного маркетинга, но базовые аспекты остаются неизменными. Рассмотрим пример товарного фида, поступающего от поставщика. Файл фида принимается ежедневно и содержит по одной строке на каждый товар, поставляемый компанией. Каждая строка включает поля, в которых указаны: артикул/ код товара (номер SKU), присвоенный поставщиком; краткое описание товара; его стоимость и габариты (высота, длина и ширина); статус товара (к примеру, в наличии или в ожидании поступления), а также, вероятно, ряд других параметров в зависимости от специфики бизнеса. В дополнение к основному файлу с информацией вы, скорее всего, получаете и другие — возможно, содержащие сведения о сопутствующих товарах, подробные характеристики товара и т. п. В результате вам ежедневно приходит несколько файлов с одинаковыми именами, которые сохраняются в один и тот же каталог для последующей обработки. Допустим, вы получаете три связанных файла: item_info.txt, item_attributes. txt и related_items.txt. Эти файлы поступают каждый день и проходят обра- ботку. Если бы обработка была единственным требованием, беспокоиться было бы не о чем: достаточно лишь позволить новым файлам заменять старые. А что, если данные нельзя удалять? Возможно, необработанные данные нужно хранить на случай, если точность обработки окажется под сомнением и вам потребуется поднять старые файлы. А быть может, вы захотите отслеживать динамику данных во времени. Какова бы ни была причина, необходимость хранить файлы означает, что их нужно каким-то образом обработать. Самое простое, что можно сделать, — датировать файлы днем получения и переместить их в архивную папку. Это позволяет ежедневно принимать новый набор файлов, обрабатывать его, переименовывать, а затем перемещать в архив для хранения, повторяя эту процедуру снова и снова, не теряя данные. По прошествии нескольких дней структура каталогов может выглядеть примерно так:
   470 Глава 20. Общая подготовка файлов working/ Главный рабочий каталог с текущими item_info.txt файлами для обработки item_attributes.txt related_items.txt archive/ Подкаталог для архивации item_info_2024-09-15.txt обработанных файлов item_attributes_2024-09-15.txt related_items_2024-09-15.txt item_info_2024-09-16.txt item_attributes_2024-09-16.txt related_items_2024-09-16.txt item_info_2024-09-17.txt item_attributes_2024-09-17.txt related_items_2024-09-17.txt ... Рассмотрим, какие шаги следует предпринять для организации этого процесса. Сначала необходимо переименовать файлы, добавив к их имени текущую дату. Для этого требуется получить список файлов, подлежащих переименованию, и извлечь их базовые имена без расширения. Затем к базовому имени добавляется строковое представление текущей даты и возвращается на место расширение, после чего файл переименовывается и перемещается в архивный каталог. Быстрая проверка: рассмотрение вариантов Какие существуют варианты решения перечисленных задач? Какие модули стандартной библиотеки вы бы задействовали для этих целей? Если хотите, сейчас вы можете сделать паузу и написать код для реализации этой процедуры. Затем сравните ваше решение с тем, что будет разработано позднее в этой книге. Имена файлов можно получить несколькими способами. Если вы твердо уверены в том, что имена файлов остаются неизменными, а файлов не так много, то можете жестко задать их в коде скрипта. Однако более надежный способ основан на применении модуля pathlib и метода glob объекта пути: import pathlib cur_path = pathlib.Path(".") FILE_PATTERN = "*.txt" path_list = cur_path.glob(FILE_PATTERN) print(list(path_list)) [PosixPath('item_attributes.txt'), PosixPath('related_items.txt'), PosixPath('item_info.txt')] Теперь можно перебрать в цикле файловые пути, соответствующие шаблону FILE_PATTERN, и применить необходимые изменения. Помните, что к имени каждого файла нужно добавить дату, а также переместить переименованные файлы в архивный каталог. При использовании модуля pathlib вся операция может выглядеть так, как показано в листинге 20.1.
   20.2. Сценарий: кошмарный товарный фид 471 Листинг 20.1. Файл files_01.py import datetime import pathlib FILE_PATTERN = "*.txt" ARCHIVE = "archive" Устанавливает шаблон сопоставления для файлов def main(): date_string = datetime.date.today().strftime("%Y-%m-%d") Создает строковое представление cur_path = pathlib.Path(".") даты на основе текущей даты archive_path = cur_path.joinpath(ARCHIVE) archive_path.mkdir(exist_ok=True) Создает архивный каталог, paths = cur_path.glob(FILE_PATTERN) если он отсутствует for path in paths: new_filename = f"{path.stem}_{date_string}{path.suffix}" new_path = archive_path.joinpath( new_filename) Создает путь, соединяя path.rename(new_path) if __name__ == '__main__': main() Одной командой переименовывает (и перемещает) файл путь к архиву и новое имя файла Ключевыми компонентами данного скрипта являются: поиск всех файлов, соответствующих шаблону *.txt, формирование строки с текущей датой для добавления к именам файлов, проверка существования архивного каталога и последующее перемещение и переименование файлов. Следует отметить, что применение объектов Path упрощает выполнение этой процедуры, так как они позволяют извлекать базовое имя файла и расширение без явного парсинга. Более того, операция оказывается проще, чем может показаться, поскольку метод rename в действительности перемещает файл, если указать путь с его новым расположением. Скрипт получается очень простым, и он эффективно справляется с задачей буквально в нескольких строках кода. В следующих разделах мы рассмотрим способы обработки более сложных требований. Быстрая проверка: потенциальные проблемы Поскольку приведенное выше решение и правда отличается минимализмом, то вполне вероятно, что возникнет немало ситуаций, в которых оно окажется недостаточно эффективным. Какие потенциальные проблемы могут возникнуть, если воспользоваться скриптом из примера? И как их можно устранить? Возьмем схему именования файлов, которая использует год, месяц и день в указанном порядке. Какими преимуществами обладает такая схема? И какие у нее недостатки? Найдутся ли у вас доводы в пользу размещения строкового представления даты в другой позиции имени файла (к примеру, в начале или в конце)?
   472 Глава 20. Общая подготовка файлов 20.3. Дальнейшее структурирование Хотя решение по хранению файлов, представленное в предыдущем разделе, вполне рабочее, у него имеется ряд недостатков. В частности, по мере накопления файлов управление ими усложняется: за год в одном каталоге окажется 365 наборов связанных файлов, и единственный способ найти нужные — просматривать их имена. Трудности лишь возрастут, если файлы будут поступать чаще или если их набор расширится. Немного упростит ситуацию изменение стратегии архивирования файлов. Вместо переименования самих файлов с добавлением даты их получения можно создавать отдельный подкаталог для каждого набора и именовать его в соответствии с датой получения. Структура каталогов может выглядеть следующим образом: working/ item_info.txt item_attributes.txt related_items.txt archive/ 2024-09-15/ item_info.txt item_attributes.txt related_items.txt 2024-09-16/ item_info.txt item_attributes.txt related_items.txt 2042-09-17/ item_info.txt item_attributes.txt related_items.txt Главный рабочий каталог с текущими файлами для обработки Главный подкаталог для архивации обработанных файлов Подкаталоги для каждого набора файлов, названные по дате получения К преимуществам этой схемы следует отнести то, что файлы каждого набора хранятся вместе. Сколько бы наборов файлов у вас ни было и сколько бы файлов ни входило в набор, вы легко найдете все файлы конкретного набора. Попробуйте сами: реализация с несколькими каталогами Как бы вы изменили разработанный ранее код для архивации каждого набора файлов в подкаталогах с именами, соответствующими дате их получения? Не торопитесь, напишите код и протестируйте его. Как выясняется, архивация файлов по подкаталогам не требует значительно больших усилий по сравнению с первым решением. Достаточно одного дополнительного шага — создать подкаталог перед переименованием файла. Лис­ тинг 20.2 демонстрирует один из способов выполнения этого шага. Листинг 20.2. Файл files_02.py import datetime import pathlib
   20.4. Оптимизация хранения: сжатие и упорядочивание 473 FILE_PATTERN = "*.txt" ARCHIVE = "archive" def main(): date_string = datetime.date.today().strftime("%Y-%m-%d") cur_path = pathlib.Path(".") archive_path = cur_path.joinpath(ARCHIVE) archive_path.mkdir(exist_ok=True) new_path = archive_path.joinpath(date_string) new_path.mkdir(exist_ok=True) paths = cur_path.glob(FILE_PATTERN) for path in paths: path.rename(new_path.joinpath(path.name)) Этот каталог необходимо создать только при первом запуске скрипта Этот каталог необходимо создать только один раз, перед перемещением в него файлов if __name__ == '__main__': main() Основные элементы этого решения такие же, как и в предыдущем, однако сочетаются они несколько иначе. В данном случае строка с датой служит для создания подкаталога в архивном каталоге, а не добавляется к имени файла. Затем файл перемещается в этот каталог, но имя файла при этом не изменяется. Такое решение группирует взаимосвязанные файлы, упрощая их пакетную обработку. Быстрая проверка: другие решения Как бы вы написали скрипт, выполняющий то же самое без помощи модуля pathlib? Какие библиотеки и функции вы бы задействовали? 20.4. Оптимизация хранения: сжатие и упорядочивание До сих пор мы в основном занимались управлением группами полученных файлов. Однако с течением времени массив файлов данных продолжает расти, и в какой-то момент объем занимаемого ими дискового пространства становится критичным. Здесь возможны различные подходы. Один из них — увеличить размер диска. Если вы работаете в облачной среде, то такое решение может оказаться простым и недорогим. Не забывайте при этом, что добавление пространства не устраняет саму проблему, а лишь откладывает ее решение на будущее. 20.4.1. Сжатие файлов Если занимаемый файлами объем становится критичным, следующим возможным подходом является их сжатие. Существует множество методов сжатия
   474 Глава 20. Общая подготовка файлов отдельного файла или набора файлов, однако в целом они имеют схожие принципы. В данном разделе мы рассмотрим архивирование ежедневных файлов данных в один zip-файл. Если файлы в основном текстовые и достаточно большие, экономия дискового пространства за счет их сжатия может быть весьма существенной. Для этого скрипта в качестве имени каждого zip-файла выступает то же строковое представление даты, к которому присоединяется расширение .zip. В листинге 20.2 мы создали новый каталог в архивном каталоге и переместили в него файлы, при этом сформировалась следующая структура каталогов: working/ archive/ 2024-09-15.zip 2024-09-16.zip 2024-09-17.zip Главный рабочий каталог для обработки текущих файлов Zip-файлы, содержащие файлы дня, указанного в их названии Разумеется, для работы с zip-файлами необходимо изменить некоторые этапы нашей процедуры. Попробуйте сами: псевдокод архивации в zip-файлы Напишите псевдокод для решения, сохраняющего файлы данных в zip-файлах. Какие модули и функции вы намерены использовать? Попробуйте оформить ваше решение в коде и убедитесь в том, что оно работает. Одно из ключевых нововведений в обновленном скрипте — это подключение библиотеки zipfile, а вместе с ней — добавление кода для создания нового объекта ZipFile в архивном каталоге, после чего объектом zipfile можно пользоваться для записи файлов данных в новый zip-архив. Поскольку при этом фактическое перемещение файлов не производится, возникает необходимость удаления исходных файлов из рабочего каталога. Возможное решение представлено в листинге 20.3. Листинг 20.3. Файл files_03.py import datetime import pathlib import zipfile Импорт библиотеки zipfile FILE_PATTERN = "*.txt" ARCHIVE = "archive" def main(): date_string = datetime.date.today().strftime("%Y-%m-%d") cur_path = pathlib.Path(".") archive_path = cur_path.joinpath(ARCHIVE) archive_path.mkdir(exist_ok=True)
   20.4. Оптимизация хранения: сжатие и упорядочивание 475 paths = cur_path.glob(FILE_PATTERN) zip_file_path = cur_path.joinpath(ARCHIVE, date_string + ".zip") Создает путь к zip-файлу в архивном каталоге Открывает новый объект zip_file = zipfile.ZipFile(str(zip_file_path), ZipFile для записи "w") for path in paths: Записывает текущий файл в zip-файл zip_file.write(str(path)) path.unlink() Удаляет текущий файл из рабочего if __name__ == '__main__': main() каталога Этот фрагмент кода чуть сложнее, поскольку теперь вместо создания вложенного каталога мы задействуем строку с датой для именования и создания объекта zipfile. Затем содержимое каждого файла по очереди записывается в этот объект, а сам файл исключается из файловой системы (при помощи метода unlink) или, проще говоря, удаляется. Когда объект zipfile выходит за пределы области видимости в конце скрипта, он автоматически закрывается. 20.4.2. Упорядочивание файлов Сжатие файлов данных в zip-архивы может обеспечить впечатляющую экономию дискового пространства и в ряде случаев является достаточной мерой. Однако если у вас огромный массив файлов или если файлы плохо поддаются сжатию (как, например, изображения в формате JPEG), проблема нехватки хранилища может оставаться актуальной. Более того, может оказаться, что ваши данные почти не меняются и, следовательно, нет необходимости сохранять архивную копию каждого набора в долгосрочной перспективе. Иными словами, если целе­сообразно сохранять ежедневные данные за последнюю неделю или месяц, то дальнейшее хранение каждого отдельного набора данных может быть неоправданным с точки зрения расхода ресурсов. Для данных, возраст которых составляет несколько месяцев, вполне допустимо оставлять лишь один набор в неделю или даже в месяц. Процесс удаления файлов по достижении ими определенного срока хранения иногда обозначают термином упорядочивание (grooming)1. Предположим, что после нескольких месяцев ежедневного получения наборов данных и их архивирования в zip-файлы вам было поручено сохранять только один файл в неделю для файлов старше одного месяца. 1 Упорядочивание (англ. grooming) — процесс организации файловой структуры и обес­ печения «гигиены» хранилища. В отличие от очистки самих данных, оно направлено на внешнюю форму: переименование файлов, удаление устаревших версий, архивацию и распределение по папкам. — Примеч. ред.
   476 Глава 20. Общая подготовка файлов Простейший скрипт для упорядочивания удаляет все файлы, которые больше не нужны, — в данном случае все файлы, кроме одного в неделю, для файлов, возраст которых превышает один месяц. При проектировании такого скрипта полезно знать ответы на два вопроса. Поскольку требуется сохранять по одному файлу в неделю, не проще ли будет выбрать конкретный день недели для сохранения? С какой периодичностью следует выполнять такое упорядочивание: ежедневно, еженедельно или ежемесячно? Если принято решение проводить его каждый день, разумно провести объединение с архивирующим скриптом. В противном случае — при необходимости выполнять упорядочивание файлов лишь раз в неделю или раз в месяц — эти операции имеет смысл реализовать в виде отдельных скриптов. В нашем примере для большей ясности будет написан отдельный скрипт для упорядочивания файлов, который может запускаться с произвольным интервалом. Допустим, вы решили, что сохраняться должны только файлы, полученные во вторник, если с момента их получения прошло больше одного месяца. Ниже приведен пример такого скрипта (листинг 20.4). Листинг 20.4. Файл files_04.py from datetime import datetime, timedelta import pathlib import zipfile FILE_PATTERN = "*.zip" ARCHIVE = "archive" ARCHIVE_WEEKDAY = 1 def main(): cur_path = pathlib.Path(".") zip_file_path = cur_path.joinpath(ARCHIVE) paths = zip_file_path.glob(FILE_PATTERN) current_date = datetime.today() Получает объект datetime для текущего дня Вызов path.stem возвращает for path in paths: имя файла без расширения name = path.stem path_date = datetime.strptime(name, "%Y-%m-%d") path_timedelta = current_date - path_date if (path_timedelta > timedelta(days=30) and path_date.weekday() != ARCHIVE_WEEKDAY): path.unlink() if __name__ == '__main__': main() Вызов timedelta(days = 30) создает объект timedelta с интервалом в 30 дней; метод weekday() возвращает порядковый номер дня недели (понедельник = 0) Вызов strptime делает парсинг строки, преобразуя ее в объект datetime Вычитание одной даты из другой возвращает объект timedelta
   Итоги 477 Приведенный фрагмент кода наглядно демонстрирует, как применение модулей datetime и pathlib в Python позволяет выполнять упорядочивание файлов по дате всего в нескольких строках. Поскольку имена архивных файлов формируются на основе дат их получения, можно определить пути к этим файлам при помощи метода glob, извлечь имя без расширения посредством stem и, используя strptime, преобразовать его в объект datetime. Затем, применив объекты timedelta и метод weekday() модуля datetime, можно установить возраст файла и день недели, а затем удалить (методом unlink) те файлы, срок хранения которых уже истек. Быстрая проверка: рассмотрите различные параметры Выделите немного времени, чтобы обдумать различные варианты упорядочивания файлов. Как бы вы изменили код из листинга 20.4, чтобы сохранять лишь один файл в месяц? И каким образом вы изменили бы его, чтобы для файлов за предыдущий месяц и более ранних выполнялось упорядочивание с сохранением одного файла в неделю? (Заметьте: это не то же самое, что «старше 30 дней»!) Итоги Модуль pathlib может существенно упростить такие операции с файлами, как получение базового имени и расширения, перемещение и переименование, а также поиск по шаблону с подстановочными знаками. По мере роста количества и сложности файлов автоматическое архивирование становится необходимостью, и Python предоставляет ряд простых способов его реализации. Перемещать данные в заданные каталоги так же легко, как переименовывать файлы, — и это способствует их эффективному управлению. Запись данных в сжатые архивы, такие как zip-файлы, позволяет радикально сократить объем используемого дискового пространства. По мере увеличения объема данных часто бывает полезно выполнять упорядочивание данных, удаляя файлы старше определенного срока (а также, возможно, и другие операции).
21 Обработка файлов данных В этой главе: 3 Применение процедур извлечения, преобразования и загрузки данных (ETL) 3 Чтение текстовых файлов данных (обычный текст и CSV) 3 Чтение файлов электронных таблиц 3 Нормализация, очистка и сортировка данных 3 Запись файлов данных Значительная часть доступных нам данных хранится в текстовых файлах. Это могут быть как неструктурированные данные — к примеру, подборка сообщений в микроблоге или литературных текстов, — так и более структурированные, где каждая строка представляет собой запись, а поля отделены специальным символом-разделителем — запятой, символом табуляции или вертикальной чертой (|). Текстовые файлы достигают порой гигантских размеров; набор данных может состоять из десятков и даже сотен файлов, а хранящийся в них текст может быть неполным или содержать много «мусора». При таком разнообразии практически неизбежно возникает необходимость считывания и предварительной обработки данных из текстовых файлов. В главе рассматриваются стратегии и подходы к решению этой задачи при помощи языка Python.
   21.2. Чтение текстовых файлов 479 21.1. Знакомство с ETL Необходимость извлекать данные из файлов, осуществлять их разбор, приводить к удобному формату, а затем каким-то образом с ними работать возникла с появлением самих файлов данных. Существует даже устоявшийся термин для таких процедур — ETL (extract-transform-load), то есть «извлечение, преобразование и загрузка». Извлечение подразумевает чтение данных из источника и при необходимости — их синтаксический анализ, парсинг. Преобразование включает очистку и нормализацию полученных данных, а также их объединение, разбиение или реструктурирование записей. Под загрузкой понимается помещение преобразованных данных в новое хранилище: в другой файл или в базу данных. В данной главе рассматриваются основы реализации ETL-подхода при помощи Python, начиная с текстовых файлов данных и заканчивая сохранением результатов преобразования в других файлах. Более структурированные форматы данных описываются в главе 23, а работа с базами данных — в главе 24. 21.2. Чтение текстовых файлов Первая составляющая ETL — извлечение — подразумевает открытие файла и чтение его содержимого. На первый взгляд звучит просто, но даже на этом этапе могут возникнуть трудности — к примеру, с размером файла. Если файл слишком велик для размещения в памяти и последующих операций с ним, код необходимо структурировать так, чтобы он обрабатывал небольшие сегменты файла (возможно, по одной строке). 21.2.1. Текстовые кодировки: ASCII, Unicode и другие Еще одним подводным камнем при работе с данными является текстовая кодировка. Эта глава посвящена работе с текстовыми файлами, и действительно, значительная часть данных, передаваемых на практике, представлена в этом формате. Однако сама природа текста может существенно различаться в зависимости от конкретного приложения, пользователя и, безусловно, от национальных стандартов. Иногда под «текстом» понимается набор символов в кодировке ASCII, содержащей 128 символов, из которых лишь 95 являются отображаемыми. Преимущество ASCII заключается в том, что она представляет собой минимальный универсальный стандарт для передачи данных, а ее главный недостаток — в полной неспособности охватить все богатство мировых алфавитов и систем письменности. Так, при чтении файлов в кодировке ASCII практически неизбежны ошибки, связанные с символами, не входящими в ее набор, — будь то немецкая ü, португальская ç или любая другая буква любого языка за пределами английского алфавита. Ошибки эти возникают из-за того, что кодировка ASCII оперирует 7-битными значениями, тогда как байты в типичном файле состоят из 8 бит, что позволяет представить 256 возможных значений вместо 128 в 7-битной системе. Эти дополнительные значения обычно применяются для хранения расширенного набора символов, куда входят дополнительные знаки препинания (такие, как короткое
   480 Глава 21. Обработка файлов данных и длинное тире), различные специальные обозначения (товарный знак, знак авторского права и знак градуса), а также буквы с надстрочной и подстрочной диакритикой. Проблема всегда заключалась в том, что при чтении текстового файла, столкнувшись с символом из 128 значений вне диапазона ASCII, вы не сможете точно сказать, как он был закодирован. Так, к примеру, символ 214 может обозначать знак деления, букву Ö или нечто совсем иное. Без доступа к исходному коду или программному обеспечению, создавшему такой файл, определить это невозможно. Unicode и UTF-8 Один из способов избежать путаницы — использовать Unicode. Кодировка Unicode под названием UTF-8 принимает базовые ASCII-символы без изменений, но также поддерживает практически неограниченное множество других символов и знаков в соответствии со стандартом Unicode. Благодаря своей универсальности и гибкости на момент написания книги кодировка UTF-8 используется в 98 % веб-страниц. Это означает, что при чтении текстовых файлов лучше всего ориентироваться на UTF-8. Если файлы содержат только ASCII-символы, они будут прочитаны надлежащим образом, при этом вы будете застрахованы и на тот случай, когда другие символы закодированы в UTF-8. Радует тот факт, что строковый тип данных Python 3 по умолчанию рассчитан на работу с Unicode. Даже при использовании Unicode возможны случаи, когда не удается коррект­ но закодировать символы, содержащиеся в тексте. К счастью, функция open в Python поддерживает необязательный аргумент errors, который указывает, как обрабатывать ошибки кодировки при чтении или записи файлов. По умолчанию используется значение 'strict', генерирующее исключение при каждом обнаружении ошибки кодировки. Среди альтернативных вариантов — 'ignore' (проблемный символ игнорируется), 'replace' (заменяется маркерным символом, часто ?), 'backslashreplace' (заменяется на экранирующую последовательность с обратной косой чертой) и 'surrogateescape' (при чтении проблемный символ присваивается зарезервированной кодовой точке в пространстве символов Unicode, а при записи восстанавливается в исходную последовательность байтов). Предпочтительный способ обработки или разрешения ошибок кодировки зависит от специфики вашей конкретной задачи. Рассмотрим короткий пример файла, в котором присутствует недопустимый символ UTF-8, и проследим, как этот символ обрабатывается с применением разных параметров. Начнем с записи файла в байтовом представлении, используя двоичный режим: open('test.txt', 'wb').write(bytes([65, 66, 67, 255, 192,193])) В результате выполнения этой команды создается файл, содержащий строку «ABC», за которой следуют три символа вне диапазона ASCII. Визуальное представление этих символов может различаться в зависимости от примененной кодировки. Если для просмотра содержимого файла воспользоваться текстовым редактором Vim, возможен следующий вывод:
   21.2. Чтение текстовых файлов 481 ABCÿÀÁ Теперь, когда у вас имеется файл, попробуйте выполнить его чтение с использованием параметра обработки ошибок по умолчанию — 'strict': x = open('test.txt').read() -------------------------------------------------------------------------UnicodeDecodeError Traceback (most recent call last) <ipython-input-2-2cb3105c1e5f> in <cell line: 1>() ----> 1 x = open('test.txt').read() /usr/lib/python3.10/codecs.py in decode(self, input, final) 320 # декодировать входные данные (с учетом буфера) 321 data = self.buffer + input --> 322 (result, consumed) = self._buffer_decode(data, self.errors, final) 323 # сохранять недекодированный ввод до следующего вызова 324 self.buffer = data[consumed:] UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte Четвертый байт со значением 255 не является допустимым символом UTF-8 в данной позиции, поэтому при использовании параметра 'strict' генерируется исключение. Посмотрим теперь, как другие режимы обработки ошибок справляются с этим же файлом, учитывая, что последние три символа вызывают ошибку: open('test.txt', errors='ignore').read() 'ABC' open('test.txt', errors='replace').read() 'ABC ' Недопустимые символы заменены на chr(65533)значение 2 open('test.txt', errors='surrogateescape').read() 'ABC\udcff\udcc0\udcc1' open('test.txt', errors='backslashreplace').read() 'ABC\\xff\\xc0\\xc1' Если вы хотите просто удалить все проблемные символы, следует применить параметр 'ignore'. Параметр 'replace' лишь помечает позиции недопустимых символов, тогда как остальные параметры стремятся сохранить их без какойлибо интерпретации. 21.2.2. Неструктурированный текст Файлы с неструктурированным текстом проще всего читать, но сложнее всего извлекать из них информацию. Обработка неструктурированного текста может
   482 Глава 21. Обработка файлов данных сильно различаться в зависимости от характера самого текста и поставленных задач, поэтому всестороннее обсуждение обработки текста выходит за рамки темы данной книги. Тем не менее краткий пример позволит проиллюстрировать ключевую проблематику и подготовит почву для обсуждения файлов со структурированными текстовыми данными. Одной из первичных задач является определение того, что составляет базовую логическую единицу в файле. Если вы работаете с текстовым массивом, будь то тысячи сообщений в микроблоге, роман «Моби Дик» или подборка новостных материалов, необходимо уметь разбивать данные на цельные блоки. В случае с сообщениями в микроблогах каждый фрагмент текста может уместиться в одной строке, что позволяет считывать и обрабатывать файл построчно с относительной простотой. Если же мы имеем дело с текстом романа «Моби Дик» или хотя бы новостной статьей, задача может оказаться куда более сложной. Как правило, рассматривать весь роман или всю статью как единый блок нецелесообразно. Следовательно, необходимо решить, что будет являться структурной единицей текста, и разработать стратегию деления файла на соответствующие фрагменты. Возможно, вы предпочтете разбить текст на абзацы. В таком случае потребуется выяснить, каким образом абзацы обозначены в файле, и учесть это при написании программного кода. Если абзацы совпадают со строками текстового файла — задача упрощается. Однако нередки ситуации, когда переносов строк в файле значительно больше, чем фактических абзацев, и тогда вам придется потрудиться. Рассмотрим теперь пару примеров: Зовите меня Измаил. Несколько лет тому назад — когда именно, не важно — я обнаружил, что в кошельке у меня почти не осталось денег, а на земле не осталось ничего, что могло бы еще занимать меня, и тогда я решил сесть на корабль и поплавать немного, чтобы поглядеть на мир и с его водной стороны. Это у меня проверенный способ развеять тоску и наладить кровообращение. Всякий раз, как я замечаю угрюмые складки в углах своего рта; всякий раз, как в душе у меня воцаряется промозглый, дождливый ноябрь; всякий раз, как я ловлю себя на том, что начал останавливаться перед вывесками гробовщиков и пристраиваться в хвосте каждой встречной похоронной процессии; в особенности же всякий раз, как ипохондрия настолько овладевает мною, что только мои строгие моральные принципы не позволяют мне, выйдя на улицу, добросовестно сбивать шляпы со всех без исключения встречных и поперечных, я понимаю, что мне пора отправляться в плавание, и как можно скорее. Это заменяет мне пулю и пистолет. Катон с философическим жестом бросается грудью на меч — я же спокойно поднимаюсь на борт корабля. И ничего удивительного здесь нет. Люди просто не отдают себе в этом отчета, а так ведь многие рано или поздно по-своему начинают испытывать к океану приблизительно такие же чувства, как и я. Взгляните, к примеру, на город-остров Манхэттен, словно атолл коралловыми рифами, опоясанный товарными пристанями, за которыми шумит коммерция кольцом прибоя. На какую бы улицу вы тут ни свернули — она обязательно приведет вас к воде. А деловой центр города и самая его оконечность — это Бэттери, откуда тянется величественный мол, омываемый
   21.2. Чтение текстовых файлов 483 волнами и овеваемый ветрами, которые всего лишь за несколько часов до этого дули в открытом море. Взгляните же на толпы людей, что стоят там и смотрят на воду. В приведенном фрагменте, представляющем собой начало романа «Моби Дик, или Белый кит»1, разбиение строк осуществляется примерно так, как это выглядит на печатной странице, а границы абзацев обозначаются одной пустой строкой. Для обработки текста по абзацам следует разделять его по таким пустым строкам. К счастью, это легко сделать методом split() строкового типа. Каждый символ новой строки представлен как "\n". Соответственно, последняя строка абзаца завершается символом новой строки, и если следующая строка пустая, она следует непосредственно за предыдущей в виде второго символа "\n": moby_text = open("moby_01.txt").read() moby_paragraphs = moby_text.split("\n\n") print(moby_paragraphs[1]) Считывает содержимое файла целиком, возвращая единый объект строкового типа Разделяет текст на части по двум идущим подряд (смежным) символам новой строки Взгляните, к примеру, на город-остров Манхэттен, словно атолл коралловыми рифами, опоясанный товарными пристанями, за которыми шумит коммерция кольцом прибоя. На какую бы улицу вы тут ни свернули — она обязательно приведет вас к воде. А деловой центр города и самая его оконечность — это Бэттери, откуда тянется величественный мол, омываемый волнами и овеваемый ветрами, которые всего лишь за несколько часов до этого дули в открытом море. Взгляните же на толпы людей, что стоят там и смотрят на воду. Разделить текст на абзацы можно, разбив его по двум символам новой строки — moby_text.split("\n\n"). Это самый простой первый шаг при обработке неструктурированного текста, однако зачастую требуется его предварительная нормализация. Допустим, вы хотите подсчитать частоту вхождений каждого слова в текстовом файле. Простое разбиение текста по пробельным символам выдаст список слов, однако корректный подсчет будет затруднен, поскольку This, this, this. и this, будут рассматриваться как разные элементы. Для исправной работы алгоритма необходимо предварительно нормализовать текст — удалить знаки препинания и привести все слова к единому регистру. Программный код, создающий нормализованный список слов для приведенного выше фрагмента, может выглядеть следующим образом: moby_text = open("moby_01.txt").read() Считывает содержимое файла целиком, moby_paragraphs = moby_text.split("\n\n") возвращая единый объект строкового типа moby = moby_paragraphs[1].lower() Приводит все символы к нижнему регистру moby = moby.replace(".", "") Удаляет точки moby = moby.replace(",", "") Удаляет запятые moby_words = moby.split() print(moby_words) 1 Перевод И. Бернштейн. — Примеч. пер.
   484 Глава 21. Обработка файлов данных ['взгляните', 'к', 'примеру', 'на', 'город-остров', 'манхэттен', 'словно', 'атолл', 'коралловыми', 'рифами', 'опоясанный', 'товарными', 'пристанями', 'за', 'которыми', 'шумит', 'коммерция', 'кольцом', 'прибоя', 'на', 'какую', 'бы', 'улицу', 'вы', 'тут', 'ни', 'свернули', '—', 'она', 'обязательно', 'приведет', 'вас', 'к', 'воде', 'а', 'деловой', 'центр', 'города', 'и', 'самая', 'его', 'оконечность', '—', 'это', 'бэттери', 'откуда', 'тянется', 'величественный', 'мол', 'омываемый', 'волнами', 'и', 'овеваемый', 'ветрами', 'которые', 'всего', 'лишь', 'за', 'несколько', 'часов', 'до', 'этого', 'дули', 'в', 'открытом', 'море', 'взгляните', 'же', 'на', 'толпы', 'людей', 'что', 'стоят', 'там', 'и', 'смотрят', 'на', 'воду'] Сначала мы применяем строковый метод .lower, после чего дважды задействуем метод .replace для удаления двух знаков препинания. Однако, как уже отмечалось в главе 6, в таких случаях предпочтительнее прибегнуть к методу .translate, позволяющему удалить все знаки препинания за одну операцию, что повышает эффективность обработки. Быстрая проверка: нормализация Внимательно присмотритесь к сгенерированному списку слов. Вы видите какие-­ нибудь проблемы с нормализацией? Какие еще трудности могут возникнуть с текстами большего объема? Как бы вы подошли к решению этих проблем? 21.2.3. Плоские (простые текстовые) файлы с разделителями Хотя неструктурированные текстовые файлы легче других форматов поддаются считыванию, отсутствие внутренней организации данных также является и их существенным недостатком. Обычно бывает куда удобнее определить для файла хоть какую-то структуру, чтобы упростить выборку отдельных значений. Простейший подход — разделить содержимое файла на строки, располагая по одной единице информации в каждой строке. В частности, это может быть список имен файлов для обработки, перечень лиц, чьи имена нужно вывести на печать (например, для бейджей), либо ряд температурных показаний, полученных с удаленного датчика. В подобных случаях парсинг данных крайне прост: строка считывается и при необходимости приводится в нужный формат. После этого файл готов к работе. Впрочем, на практике ситуация чаще всего не настолько проста. Как правило, необходимо сгруппировать несколько взаимосвязанных элементов данных, чтобы ваша программа могла считывать их вместе. Общепринятый способ решения — размещать связанные фрагменты информации в одной строке, разделяя их специальным символом. В таком случае при построчном чтении файла специальные символы служат для разбиения строк на поля и присвоения значений соответствующим переменным для последующей обработки. В листинге 21.1 приведен пример температурных данных, представленных в формате с разделителями:
   21.2. Чтение текстовых файлов 485 Листинг 21.1. Файл temp_data_ pipes_00a.txt State|Month Day, Year Code|Avg Daily Max Air Temperature (F)|Record Count ➥for Daily Max Air Temp (F) Illinois|1979/01/01|17.48|994 Illinois|1979/01/02|4.64|994 Illinois|1979/01/03|11.05|994 Illinois|1979/01/04|9.51|994 Illinois|1979/05/15|68.42|994 Illinois|1979/05/16|70.29|994 Illinois|1979/05/17|75.34|994 Illinois|1979/05/18|79.13|994 Illinois|1979/05/19|74.94|994 Данные в файле разделяются символом конвейеризации, то есть каждое поле в строке отделено вертикальной чертой (|). В рассматриваемом случае данные состоят из четырех полей: места (штата), даты наблюдений, средней максимальной температуры и количества метеостанций, предоставивших данные. Другими общепринятыми разделителями являются символ табуляции и запятая. Пожалуй, запятая используется чаще всего, однако в качестве разделителя может выступать любой символ, присутствие которого не ожидается в самих данных (об этом чуть позже). Разделение полей запятыми настолько распространено, что такая структурная организация данных получила особое название — CSV (comma-separated values — значения, разделенные запятыми), а файлы такого типа обычно имеют расширение .csv, указывающее на формат их содержимого. Попробуйте сами: чтение файла Составьте программу, считывающую текстовый файл (допустим, с именем temp_data_ pipes_00a.txt, как показано в примере), разделяющую каждую строку файла на список значений и добавляющую этот список в общий список записей. С какими трудностями или вопросами вы столкнулись при написании этой программы? Как бы вы привели последние три поля к соответствующим им типам (дате, вещественному и целому числу)? Какой бы символ ни выступал в качестве разделителя, если он вам известен, можно написать собственный код на языке Python, чтобы разбить каждую строку на отдельные поля и вернуть их в виде списка. Применительно к рассмотренному выше примеру можно воспользоваться методом split() для преобразования строки в список значений: line = "Illinois|1979/01/01|17.48|994" print(line.split("|")) ['Illinois', '1979/01/01', '17.48', '994'] Следует отметить, что данный прием, несмотря на простоту реализации, сохраняет все значения в строковом формате, что может создавать определенные трудности для их последующей обработки.
   486 Глава 21. Обработка файлов данных 21.2.4. Модуль csv Если вам часто приходится обрабатывать файлы данных с разделителями, стоит поближе познакомиться с модулем csv и его возможностями. Когда меня просили назвать любимый модуль из стандартной библиотеки Python, я не раз называла csv — не потому, что он какой-то особо эффектный (это не так), а потому, что за все годы работы он, пожалуй, сэкономил мне больше времени и уберег меня от большего числа моих же потенциальных ошибок, чем любой другой модуль. Модуль csv — идеальный пример философии Python «все включено» («батарейки в комплекте»). Хотя вы и сами можете написать код чтения файлов с разделителями (более того, это не так уж сложно), намного проще и надежнее использовать модуль Python. Модуль csv был тщательно протестирован и оптимизирован, и он предоставляет ряд возможностей, которые вы вряд ли бы стали реализовывать самостоятельно, но которые оказываются невероятно полезными и экономят массу времени, когда они уже доступны. Взгляните на предыдущие данные и решите, как бы вы считали их при помощи модуля csv. Код парсинга данных должен читать каждую строку и удалять завершающий символ новой строки, а затем разбить строку, используя вертикальную черту | в качестве разделителя, и присоединить список значений к общему списку записей. Ваше решение может выглядеть так: results = [] for line in open("temp_data_pipes_00a.txt"): fields = line.strip().split("|") results.append(fields) results [['Notes'], ['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)'], ['Illinois', '1979/01/01', '17.48', '994'], ['Illinois', '1979/01/02', '4.64', '994'], ['Illinois', '1979/01/03', '11.05', '994'], ['Illinois', '1979/01/04', '9.51', '994'], ['Illinois', '1979/05/15', '68.42', '994'], ['Illinois', '1979/05/16', '70.29', '994'], ['Illinois', '1979/05/17', '75.34', '994'], ['Illinois', '1979/05/18', '79.13', '994'], ['Illinois', '1979/05/19', '74.94', '994']] Если вы захотите сделать то же самое, прибегнув к модулю csv, ваш код может быть примерно таким: import csv results = [fields for fields in csv.reader(open("temp_data_pipes_00a.txt", newline=''), delimiter="|")] results [['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)',
   21.2. Чтение текстовых файлов 'Record Count for Daily Max Air Temp (F)'], '17.48', '994'], ['Illinois', '1979/01/02', '1979/01/03', '11.05', '994'], ['Illinois', ['Illinois', '1979/05/15', '68.42', '994'], '70.29', '994'], ['Illinois', '1979/05/17', '1979/05/18', '79.13', '994'], ['Illinois', 487 ['Illinois', '1979/01/01', '4.64', '994'], ['Illinois', '1979/01/04', '9.51', '994'], ['Illinois', '1979/05/16', '75.34', '994'], ['Illinois', '1979/05/19', '74.94', '994']] В этом простом примере выигрыш по сравнению с самостоятельной реализацией решения не так уж велик. Тем не менее код получился на две строки короче и немного понятнее, к тому же вам не пришлось беспокоиться об удалении символов новой строки. Главные преимущества станут очевидными, когда вы столкнетесь с более трудными случаями. Приведенные данные взяты из реально существующего источника, однако они были предварительно упрощены и очищены. Неадаптированные данные куда сложнее: количество полей больше, часть полей заключена в кавычки, тогда как остальные — нет, а первое поле остается пустым. В исходном формате разделителем служит знак табуляции, а здесь для наглядности они разделены запятыми (листинг 21.2). Листинг 21.2. Файл temp_data_01.csv "Notes","State","State Code","Month Day, Year","Month Day, Year Code",Avg Daily Max Air Temperature (F),Record Count for Daily Max Air Temp (F),Min Temp for Daily Max Air Temp (F),Max Temp for Daily Max Air Temp (F),Avg Daily Max Heat Index (F),Record Count for Daily Max Heat Index (F),Min for Daily Max Heat Index (F),Max for Daily Max Heat Index (F),Daily Max Heat Index (F) % Coverage ,"Illinois","17","Jan 01, 1979","1979/01/01",17.48,994,6.00,30.50,Missing,0,Missing,Missing,0.00% ,"Illinois","17","Jan 02, 1979","1979/01/02",4.64,994,-6.40,15.80,Missing,0,Missing,Missing,0.00% ,"Illinois","17","Jan 03, 1979","1979/01/03",11.05,994,-0.70,24.70,Missing,0,Missing,Missing,0.00% ,"Illinois","17","Jan 04, 1979","1979/01/04",9.51,994,0.20,27.60,Missing,0,Missing,Missing,0.00% ,"Illinois","17","May 15, 1979","1979/05/15",68.42,994,61.00,75.10,Missing,0,Missing,Missing,0.00% ,"Illinois","17","May 16, 1979","1979/05/16",70.29,994,63.40,73.50,Missing,0,Missing,Missing,0.00% ,"Illinois","17","May 17, 1979","1979/05/17",75.34,994,64.00,80.50,82.60,2,82 .40,82.80,0.20% ,"Illinois","17","May 18, 1979","1979/05/18",79.13,994,75.50,82.10,81.42,349, 80.20,83.40,35.11% ,"Illinois","17","May 19, 1979","1979/05/19",74.94,994,66.90,83.10,82.87,78,8 1.60,85.20,7.85% Заметьте — некоторые поля содержат запятые. Такие поля принято заключать в кавычки, чтобы указать, что они не подлежат разбиению по символу-разделителю. Это довольно распространенная практика — как и в данном случае — заключать в кавычки отдельные поля, особенно те из них, в которых может встретиться символ-разделитель. Впрочем, нередко в кавычки заключаются
   488 Глава 21. Обработка файлов данных даже такие поля, в которых он вряд ли когда-либо обнаружится, — как, опятьтаки, в данном примере. Для обработки файлов данных подобного рода программный код, написанный без использования библиотек, становится слишком громоздким и обременительным. Так, разбивка строки по символу-разделителю уже невозможна без предварительной проверки — требуется учитывать только те разделители, которые находятся вне кавычек. Также необходимо удалять кавычки у закавыченных строковых значений, причем такие записи могут встречаться в каком угодно поле или вовсе отсутствовать. Благодаря модулю csv вам не придется ничего менять в самом коде. Более того, поскольку по умолчанию в качестве разделителя выступает запятая, вам не требуется даже указывать ее явно: results2 = [fields for fields in csv.reader(open("temp_data_01.csv", newline=''))] results2 [['Notes', 'State', 'State Code', 'Month Day, Year', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)', 'Min Temp for Daily Max Air Temp (F)', 'Max Temp for Daily Max Air Temp (F)', 'Avg Daily Max Heat Index (F)', 'Record Count for Daily Max Heat Index (F)', 'Min for Daily Max Heat Index (F)', 'Max for Daily Max Heat Index (F)', 'Daily Max Heat Index (F) % Coverage'], [], ['', 'Illinois', '17', 'Jan 01, 1979', '1979/01/01', '17.48', '994', '6.00', '30.50', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'Jan 02, 1979', '1979/01/02', '4.64', '994', '-6.40', '15.80', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'Jan 03, 1979', '1979/01/03', '11.05', '994', '-0.70', '24.70', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'Jan 04, 1979', '1979/01/04', '9.51', '994', '0.20', '27.60', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'May 15, 1979', '1979/05/15', '68.42', '994', '61.00', '75.10', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'May 16, 1979', '1979/05/16', '70.29', '994', '63.40', '73.50', 'Missing', '0', 'Missing', 'Missing', '0.00%'], ['', 'Illinois', '17', 'May 17, 1979', '1979/05/17', '75.34', '994', '64.00', '80.50', '82.60', '2', '82.40', '82.80', '0.20%'], ['', 'Illinois', '17', 'May 18, 1979', '1979/05/18', '79.13', '994', '75.50', '82.10', '81.42', '349', '80.20', '83.40', '35.11%'], ['', 'Illinois', '17', 'May 19, 1979', '1979/05/19', '74.94', '994', '66.90', '83.10', '82.87', '78', '81.60', '85.20', '7.85%']] Обратите внимание на то, что лишние кавычки были удалены, а все значения полей с запятыми остались без изменений — и все это без включения новых символов в команду. Быстрая проверка: работа с кавычками Подумайте, как бы вы подошли к решению проблем, связанных с заключенными в кавычки полями и символами-разделителями, не будь у вас модуля csv. Что, по вашему мнению, было бы проще реализовать: обработку кавычек или встроенных разделителей?
   21.3. Файлы Excel 489 21.2.5. Чтение csv-файла как списка словарей В предыдущих примерах строки данных возвращались в виде списка полей. Во многих случаях этого вполне достаточно, и все же иногда удобнее считывать строки в словари, где имя каждого поля служит ключом. Для такого варианта использования модуль csv предоставляет класс DictReader, который может принять список имен полей в качестве параметра либо прочитать их из первой строки данных. Код для открытия данных посредством DictReader примет следующий вид: results = [fields for fields in csv.DictReader(open("temp_data_01.csv", newline=''))] results[0] {'Notes': '', 'State': 'Illinois', 'State Code': '17', 'Month Day, Year': 'Jan 01, 1979', 'Month Day, Year Code': '1979/01/01', 'Avg Daily Max Air Temperature (F)': '17.48', 'Record Count for Daily Max Air Temp (F)': '994', 'Min Temp for Daily Max Air Temp (F)': '6.00', 'Max Temp for Daily Max Air Temp (F)': '30.50', 'Avg Daily Max Heat Index (F)': 'Missing', 'Record Count for Daily Max Heat Index (F)': '0', 'Min for Daily Max Heat Index (F)': 'Missing', 'Max for Daily Max Heat Index (F)': 'Missing', 'Daily Max Heat Index (F) % Coverage': '0.00%' Обратите внимание на то, что словарь сохраняет исходный порядок полей: results[0]['State'] 'Illinois' Применение DictReader при работе с конкретными полями сложных данных существенно упрощает выбор нужного поля; к тому же такой подход делает код несколько яснее. Впрочем, следует учитывать и тот факт, что обработка объемных наборов данных займет у DictReader примерно в два раза больше времени. 21.3. Файлы Excel В этой главе мы рассмотрим еще один широко распространенный файловый формат, а именно файл Excel, в котором Microsoft Excel хранит электронные таблицы. Файлы Excel безусловно заслуживают нашего внимания, поскольку подход к их обработке во многом аналогичен подходу к файлам с разделителями. И в самом деле, поскольку Excel способен как читать, так и записывать CSVфайлы, наиболее быстрым и удобным способом получения данных из Excelдокумента зачастую оказывается его открытие в табличном редакторе Excel и последующее сохранение в формате CSV. Впрочем, такой подход не всегда
   490 Глава 21. Обработка файлов данных целесообразен, особенно при наличии большого количества файлов. И хотя теоретически возможно автоматизировать процесс открытия и сохранения каждого файла в CSV-формате, зачастую быстрее работать с файлами Excel напрямую. Углубленное изучение файлов электронных таблиц с их возможностями по сохранению нескольких листов в одном файле, макросов и различных средств форматирования выходит за рамки книги. Вместо этого мы рассмотрим пример чтения простого файла с единственным листом — просто для извлечения данных. Дело в том, что в стандартной библиотеке Python нет модуля для чтения или записи файлов Excel, поэтому для работы с этим форматом необходимо установить сторонний. К счастью, существует несколько модулей, позволяющих справиться с этой задачей. В приведенном примере используется модуль openpyxl, доступный в официальном репозитории пакетов Python. Установить его можно, выполнив следующую команду в терминале: $pip install openpyxl На рис. 21.1 отображены знакомые нам данные температурных измерений, но уже в виде электронной таблицы: Рис. 21.1. Отображение температурных данных, представленных в формате электронной таблицы Прочитать этот файл несложно, и все же потребуется больше усилий по сравнению с файлами CSV. Сначала нужно загрузить рабочую книгу; затем — получить нужный лист; после этого можно выполнять перебор строк в цикле, извлекая значения соответствующих ячеек. Ниже приведен пример кода для чтения электронной таблицы: from openpyxl import load_workbook wb = load_workbook('temp_data_01.xlsx') results = [] ws = wb.worksheets[0] for row in ws.iter_rows(): results.append([cell.value for cell in row]) print(results) [['Notes', 'State', 'State Code', 'Month Day, Year', 'Month Day, Year
   21.3. Файлы Excel 491 Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)', 'Min Temp for Daily Max Air Temp (F)', 'Max Temp for Daily Max Air Temp (F)', 'Avg Daily Max Heat Index (F)', 'Record Count for Daily Max Heat Index (F)', 'Min for Daily Max Heat Index (F)', 'Max for Daily Max Heat Index (F)', 'Daily Max Heat Index (F) % Coverage'], [None, 'Illinois', 17, 'Jan 01, 1979', '1979/01/01', 17.48, 994, 6, 30.5, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'Jan 02, 1979', '1979/01/02', 4.64, 994, -6.4, 15.8, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'Jan 03, 1979', '1979/01/03', 11.05, 994,-0.7, 24.7, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'Jan 04, 1979', '1979/01/04', 9.51, 994, 0.2, 27.6, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'May 15, 1979', '1979/05/15', 68.42, 994, 61, 75.1, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'May 16, 1979', '1979/05/16', 70.29, 994, 63.4, 73.5, 'Missing', 0, 'Missing', 'Missing', '0.00%'], [None, 'Illinois', 17, 'May 17, 1979', '1979/05/17', 75.34, 994, 64, 80.5,82.6, 2, 82.4, 82.8, '0.20%'], [None, 'Illinois', 17, 'May 18, 1979', '1979/05/18', 79.13, 994, 75.5, 82.1, 81.42, 349, 80.2, 83.4, '35.11%'],[None, 'Illinois', 17, 'May 19, 1979', '1979/05/19', 74.94, 994,66.9, 83.1, 82.87, 78, 81.6, 85.2, '7.85%']] Этот программный код возвращает те же результаты, что и куда более легкий алгоритм для файла CSV. Неудивительно, что код чтения электронной таблицы получается непростым, ведь электронные таблицы сами по себе являются гораздо более сложными объектами. Кроме того, важно иметь ясное представление о том, каким образом данные хранятся в электронной таблице. Если в ней применяется форматирование, несущее смысловую нагрузку, если необходимо игнорировать либо специально обрабатывать метки или учитывать формулы и ссылки, то потребуется более детальное изучение соответствующих аспектов и написание более сложной программы. С электронными таблицами часто возникают и другие трудности. На момент написания книги электронные таблицы обычно ограничивались примерно миллионом строк. Хотя это значение и кажется огромным, все чаще приходится иметь дело с наборами данных еще большего размера. Кроме того, электронные таблицы иногда автоматически применяют неверное форматирование. В одной из компаний, с которыми я сотрудничала, применялись номера деталей из цифры и как минимум одной буквы, за которой следовала комбинация цифр и букв. Таким образом, номер детали мог иметь вид 1E20. Многие электронные таблицы автоматически интерпретируют строку 1E20 как научную запись числа и преобразуют ее в формат 1.00E+20 (1 × 10), оставляя при этом 1F20 в виде строки. По какой-то причине предотвратить подобное поведение оказалось делом не из легких, тогда как при работе с большим объемом данных такая ошибка может остаться незамеченной вплоть до последних этапов обработки (если вообще будет обнаружена). Поэтому я рекомендую по возможности выбирать CSV или другие текстовые форматы с разделителями. Поскольку пользователи, как правило, имеют возможность сохранять таблицу в CSV, то нет никакого смысла дополнительно усложнять данные и мириться с искажениями формата, присущими электронным таблицам.
   492 Глава 21. Обработка файлов данных 21.4. Очистка данных Одной из типичных проблем при обработке текстовых массивов данных являются так называемые грязные данные. Под термином «грязные» подразумеваются различного рода сюрпризы в содержимом таких файлов, включая пустые значения, не поддерживаемые кодировкой знаки, а также лишние пробельные символы. Данные также могут оказаться неотсортированными или расположенными в порядке, затрудняющем их обработку. Процесс устранения таких ошибок называется очисткой данных. 21.4.1. Очистка В самом простом случае может потребоваться обработка файла, экспортированного из электронной таблицы или финансовой программы, в котором числовые значения, относящиеся к денежным единицам, содержат символы процентов и валют (такие, как %, $, £, €), а также включают разделители в виде точки или запятой. Данные из других источников могут содержать различные неожиданные элементы, затрудняющие обработку всего массива, если их заранее не выявить. Снова обратимся к температурным данным, приведенным выше. Первая строка выглядит так: [None, 'Illinois', 17, 'Jan 01, 1979', '1979/01/01', 17.48, 994, 6, 30.5, 2.89, 994, -13.6, 15.8, 'Missing', 0, 'Missing', 'Missing', '0.00%'] Некоторые столбцы, такие как 'State' (поле 2) и 'Notes' (поле 1), явно содержат текст, и с ними вряд ли понадобится выполнять какие-либо операции. Помимо этого, есть два поля с датами в разных форматах, и вполне возможно, что потребуется производить над ними вычисления — быть может, отсортировать данные, сгруппировать строки по месяцам или дням либо установить, сколько времени прошло между двумя записями. Остальные поля, по всей видимости, содержат разные типы чисел; температуры представлены дробными числами, а количество записей — целыми. Впрочем, следует отметить, что в показателях температур по индексу жары есть некая особенность: если величина в поле 'Max Temp for Daily Max Air Temp (F)' (Максимальная из среднесуточных максимальных температур) ниже 80, то значения полей, связанных с индексом жары, не указываются, а помечаются как 'Missing' (Отсутствует), а количество записей при этом равно 0. Также заметьте, что поле 'Daily Max Heat Index (F) % Coverage' (% охвата данных для суточного максимального индекса жары) представлено в виде процента от общего числа температурных записей, соответствующих условиям для расчета индекса жары. Оба этих фактора затрудняют выполнение математических вычислений над соответствующими полями, поскольку значение 'Missing' и числовые строки с символом процента воспринимаются как строковые, а не как числовые величины. Очистку данных такого рода можно выполнять на разных этапах. Довольно часто я очищаю данные прямо во время их считывания из файла, при этом я, скорее всего, заменила бы 'Missing' на None или пустую строку в процессе их
   21.4. Очистка данных 493 построчной обработки. Впрочем, можно оставить метки 'Missing' без изменений и реализовать логику программы таким образом, чтобы для этих значений вычисления не производились. Попробуйте сами: очистка данных Как бы вы обрабатывали поля со значениями 'Missing' при выполнении математических вычислений? Можете ли вы написать фрагмент кода, вычисляющий среднее значение для одного из таких столбцов? Что бы вы сделали со столбцом Daily Max Heat Index (F) % Coverage, чтобы можно было также произвести расчет среднего уровня охвата? Как вы считаете, связана ли вообще данная задача с тем, как обрабатываются в таблице записи 'Missing'? 21.4.2. Сортировка Как упоминалось выше, данные в текстовом файле нередко бывает полезно предварительно отсортировать. Сортировка данных облегчает обнаружение и обработку повторяющихся значений, а также позволяет сгруппировать взаи­ мосвязанные строки для ускорения и упрощения последующих процедур. Однажды мне попался файл на 20 миллионов строк, содержащий атрибуты и значения, и произвольное количество этих записей нужно было сопоставить с элементами основной номенклатурной таблицы (SKU). Сортировка записей по идентификатору товара позволила существенно ускорить процесс сбора атрибутов для каждого наименования. Выбор конкретного метода сортировки зависит от размера файла и доступной оперативной памяти, а также от сложности сортировки. Если все строки файла свободно помещаются в память, проще всего считать их в список и воспользоваться методом sort. К примеру, если файл datafile содержит такие строки: ZZZZZZ CCCCCC QQQQQQ AAAAAA то можно отсортировать их следующим образом: lines = open("datafile").readlines() lines.sort() print(lines) ['AAAAAA\n', 'CCCCCC\n', 'QQQQQQ\n', 'ZZZZZZ\n'] Также можно воспользоваться функцией sorted() — скажем, sorted_lines = sorted(lines). Функция сохраняет порядок строк в исходном списке, что, как правило, излишне. Недостаток функции sorted() заключается в том, что она создает новую копию списка. Процесс занимает чуть больше времени и вдвое больше памяти, что может быть весьма критично. Если размер набора данных превышает объем памяти, а операция сортировки достаточно проста (к примеру, выполняется по легкодоступному полю),
   494 Глава 21. Обработка файлов данных целесообразно предварительно обработать данные при помощи внешней утилиты, такой как команда sort в UNIX: ! sort datafile > datafile.srt ! cat datafile.srt AAAAAA CCCCCC QQQQQQ ZZZZZZ И в том и в другом случае сортировка может осуществляться в обратном порядке и по значению ключа, а не только по началу строки. Для таких ситуаций необходимо ознакомиться с документацией выбранного средства сортировки. Простой пример на Python — сортировка строк текста без учета регистра. Для этого методу sort передается функция-ключ, преобразующая элемент в нижний регистр перед сравнением: lines.sort(key=str.lower) В следующем примере используется функция lambda для игнорирования первых пяти символов каждой строки: lines.sort(key=lambda x: x[5:]) Применение функций-ключей для настройки поведения сортировки в Python — вещь чрезвычайно удобная, однако следует помнить, что функция-ключ многократно вызывается в процессе сортировки, так что слишком сложная функция может привести к серьезной потере производительности, особенно при большом наборе данных. 21.4.3. Сложности и подводные камни очистки данных Похоже, разновидностей «грязных» данных столько же, сколько источников и вариантов их применения. В файлах данных вам то и дело будут встречаться нестандартные элементы, которые могут как снизить точность обработки всего массива, так и привести к сбою загрузки данных. И поскольку невозможно составить исчерпывающий перечень возможных проблем и способов их решения, я могу дать лишь несколько общих советов. Остерегайтесь пробельных и нулевых символов. Основная сложность с пробельными символами заключается в их невидимости, что, впрочем, не исключает их потенциальной вредоносности. Лишние пробельные символы в начале и в конце строк, пробельные символы вокруг отдельных полей, табуляция вместо пробелов (или наоборот) — все это может затруднить загрузку и обработку данных, причем такие проблемы не всегда легко заметить. Аналогичным образом, текстовые файлы, содержащие нулевые символы (ASCII 0), могут выглядеть корректно при визуальной проверке, но вызывать сбои на этапе загрузки или обработки.
   21.5. Запись файлов данных 495 Остерегайтесь знаков препинания. Следует учитывать потенциальные сложности, связанные со знаками препинания. Лишние запятые или точки способны нарушить формат CSV-файлов и затруднить интерпретацию числовых значений, а неэкранированные либо непарные кавычки могут привести к ошибкам в обработке. Разделяйте процесс на этапы и осуществляйте поэтапную отладку. Отладка становится значительно проще, если разбивать всю процедуру на отдельные этапы, то есть размещать каждую операцию на отдельной строке, писать более развернутый код и использовать дополнительные переменные. Такая избыточность оправданна, поскольку, во-первых, она облегчает интерпретацию возникающих исключений, а во-вторых, упрощает отладку при помощи операторов print, систем логирования или отладчика Python. Кроме того, может оказаться полезным сохранять промежуточные данные после каждого этапа и уменьшать объем обрабатываемого файла до всего лишь нескольких строк, вызывающих ошибку. 21.5. Запись файлов данных Заключительный этап ETL может включать сохранение преобразованных данных в базу данных (что мы рассмотрим в главе 23), однако на практике чаще происходит запись данных в файлы. Эти файлы могут затем использоваться в качестве входных данных для других приложений или аналитических процессов, осуществляемых как вручную, так и автоматически. Обычно для таких файлов существует конкретная спецификация: какие поля должны быть включены, как они должны называться, в каком формате представлены, какие ограничения на них накладываются и т. п. 21.5.1. CSV и другие файлы с разделителями Пожалуй, самой простой задачей является запись данных в файлы CSV. Поскольку вы уже загрузили, выполнили парсинг, очистили и преобразовали данные, вряд ли вы столкнетесь с какими-либо нерешенными проблемами в самих данных. И снова модуль csv из стандартной библиотеки Python существенно упростит вашу работу. Процесс записи файлов с разделителями при помощи модуля csv, по существу, представляет собой обратную операцию по отношению к чтению. Как и прежде, вам требуется лишь указать символ-разделитель, который вы хотите использовать, и вновь модуль csv сам обрабатывает все случаи, когда символ-разделитель входит в состав какого-либо поля: temperature_data = [['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)'], ['Illinois', '1979/01/01', '17.48', '994'], ['Illinois', '1979/01/02', '4.64', '994'], ['Illinois', '1979/01/03', '11.05', '994'], ['Illinois', '1979/01/04', '9.51', '994'], ['Illinois', '1979/05/15', '68.42', '994'], ['Illinois', '1979/05/16', '70.29', '994'], ['Illinois', '1979/05/17', '75.34', '994'],
   496 Глава 21. Обработка файлов данных ['Illinois', '1979/05/18', '79.13', '994'], ['Illinois', '1979/05/19', '74.94', '994']] csv.writer(open("temp_data_03.csv", "w", newline='')).writerows(temperature_data) Этот код создает следующий файл: State,"Month Day, Year Code",Avg Daily Max Air Temperature (F),Record Coun ➥ for Daily Max Air Temp (F) Illinois,1979/01/01,17.48,994 Illinois,1979/01/02,4.64,994 Illinois,1979/01/03,11.05,994 Illinois,1979/01/04,9.51,994 Illinois,1979/05/15,68.42,994 Illinois,1979/05/16,70.29,994 Illinois,1979/05/17,75.34,994 Illinois,1979/05/18,79.13,994 Illinois,1979/05/19,74.94,994 Как и при чтении из файла в формате CSV, при помощи DictWriter можно записывать словари вместо списков. Если вы собираетесь использовать DictWriter, следует учитывать пару моментов. Так, необходимо заранее задать список имен полей при создании объекта, а для записи строк заголовка в начало выходного файла применяется метод writeheader. Допустим, у вас имеются те же данные, но представленные в формате словаря: data = [{'State': 'Illinois', 'Month Day, Year Code': '1979/01/01', 'Avg Daily Max Air Temperature (F)': '17.48', 'Record Count for Daily Max Air Temp (F)': '994'}] fields = ['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)'] При помощи объекта DictWriter из модуля csv можно записать каждую строку (словарь) в соответствующие поля файла CSV: fields = ['State', 'Month Day, Year Code', 'Avg Daily Max Air Temperature (F)', 'Record Count for Daily Max Air Temp (F)'] dict_writer = csv.DictWriter(open("temp_data_04.csv", "w"), fieldnames=fields) dict_writer.writeheader() dict_writer.writerows(data) del dict_writer 21.5.2. Запись файлов Excel Запись файлов электронных таблиц имеет много общего с их чтением, что неудивительно. Вначале необходимо создать рабочую книгу или файл электронной таблицы; затем создать лист (или несколько листов); и наконец, записать данные в соответствующие ячейки. Создание новой электронной таблицы из файла данных CSV может выглядеть так:
   21.6. Практическая работа: метеорологические наблюдения 497 from openpyxl import Workbook data_rows = [fields for fields in csv.reader(open("temp_data_01.csv"))] wb = Workbook() ws = wb.active ws.title = "temperature data" for row in data_rows: ws.append(row) wb.save("temp_data_02.xlsx") Также предусмотрена возможность добавления форматирования в ячейки при записи их в файл электронной таблицы. Дополнительные сведения о добавлении форматирования можно найти в документации к модулю XlsxWriter. 21.5.3. Архивирование файлов данных При наличии нескольких взаимосвязанных файлов данных либо при работе с файлами значительного объема стоит рассмотреть возможность их сжатия в архив. Несмотря на распространенность различных форматов архивирования, формат ZIP по-прежнему остается популярным и практически универсальным для большинства пользователей вне зависимости от платформы. Рекомендации по упаковке файлов данных в ZIP-архив с помощью модуля zipfile приведены в главе 20. 21.6. Практическая работа: метеорологические наблюдения Файл данных о погоде, приведенный в этой главе (Illinois_weather_1979-2011. txt), упорядочен по месяцам и затем по округам штата Иллинойс (Illinois) с 1979 по 2011 год. Напишите код, который обрабатывает этот файл для извлечения данных по Чикаго, округ Кук (Chicago, Cook County), в файл CSV или электронной таблицы. При этом строки 'Missing' должны заменяться пустыми строками, а проценты — преобразовываться в десятичные дроби. Также подумайте о том, какие поля содержат повторяющуюся информацию (а следовательно, могут быть пропущены или сохранены в другом месте). Чтобы удостовериться в том, что все сделано правильно, откройте файл в редакторе электронных таблиц. Кроме того, в среде Colaboratory можно воспользоваться файловым обозревателем (значок файла в крайней левой части интерфейса) и открыть файл двойным щелчком, чтобы просмотреть данные в табличной форме. ПРИМЕЧАНИЕ Поскольку в конце файла содержится дополнительная документация, необходимо прекратить обработку файла, как только в первом поле строки встретится "---". Для справки всегда полезно просмотреть как минимум несколько строк исходного файла:
   498 Глава 21. Обработка файлов данных ['"Notes"\t"Month"\t"Month Code"\t"County"\t"County Code"\tAvg Daily Max Air Temperature (F)\tRecord Count for Daily Max Air Temp (F)\tMin Temp for Daily Max Air Temp (F)\tMax Temp for Daily Max Air Temp (F)\tAvg Daily Min Air Temperature (F)\tRecord Count for Daily Min Air Temp (F)\tMin Temp for Daily Min Air Temp (F)\tMax Temp for Daily Min Air Temp (F)\tAvg Daily Max Heat Index (F)\tRecord Count for Daily Max Heat Index (F)\tMin for Daily Max Heat Index (F)\tMax for Daily Max Heat Index (F)\tDaily Max Heat Index (F) % Coverage\n', '\t"Jan"\t"1"\t"Adams County, IL"\t"17001"\t31.89\t19437\t 10.00\t68.90\t18.01\t19437\t-26.20\t50.30\tMissing\t0\tMissing\tMissing\ t0.00%\n', '\t"Jan"\t"1"\t"Alexander County, IL"\t"17003"\t41.07\t6138\t2.60\t73.20\t26.48\t6138\t 14.00\t60.30\tMissing\t0\tMissing\tMissing\t0.00%\n', '\t"Jan"\t"1"\t"Bond County, IL"\t"17005"\t35.71\t6138\t 2.70\t69.50\t22.18\t6138\t-17.90\t57.20\tMissing\t0\tMissing\tMissing\ t0.00%\n'] 21.6.1. Решение задачи при помощи кода, сгенерированного ИИ Поскольку эта задача в значительной степени носит процедурный характер и практически не требует проработки архитектуры, она вполне подходит для автоматизированного решения с использованием ИИ. Как и прежде, нам также необходимо убедиться в том, что вся требуемая информация изложена в промпте ясно и полно. 21.6.2. Решения и обсуждение Это задание существенно проще тех, что встречались в некоторых предыдущих главах, так как здесь мы просто выполняем базовую очистку данных. Основные шаги — загрузить файл, исправить или отбросить некоторые поля данных, а затем сохранить данные в файл формата CSV. Программное решение, созданное человеком Решение довольно простое: открываются входной и выходной файлы, исходные поля считываются, обрабатываются и сохраняются. import csv with (open("Illinois_weather_1979-2011.txt") as infile, Открывает как входные, так и выходные файлы open("Illinois_weather_1979-2011.csv", "w") as outfile): Проверяет начало раздела for row in csv.reader(infile, delimiter="\t"): документации if row[0] == '---': break Удаляет первые два поля строки if "Cook" in row[3]: del row[0:2] Заменяет "Missing" row = [item.replace("Missing", "") пустой строкой for item in row] row[-1] = row[-1].replace("%", "") Удаляет row[-1] = (float(row[-1])/100 if str.isdigit(row[-1]) символ % else row[-1]) из последнего Преобразует процент csv.writer(outfile).writerow(row) поля в десятичное число
   21.6. Практическая работа: метеорологические наблюдения 499 Как уже упоминалось, последние несколько строк файла содержат документацию, поэтому мы прекращаем обработку, когда встречается разделитель "---". Поскольку нас интересуют данные по Чикаго (округ Кук), мы обрабатываем лишь те строки, в которых поле "County" содержит "Cook". В качестве альтернативы можно заглянуть в файл и увидеть, что код округа Кук — "17031", и обрабатывать только строки, где это значение указано в поле "County Code". В ходе обработки строк первым шагом является удаление двух начальных полей: первое поле ("Notes") является пустым, а второе содержит название месяца, которое нам не требуется, поскольку числовой номер месяца присутствует в следующем поле. Следующие два действия — это замена "Missing" (Отсутствует) на пустую строку в любом поле, а затем удаление символа процента и преобразование текстовой строки с процентным значением в десятичное число (к примеру, "10" превращается в 0,1). Программа завершается записью обработанных полей в CSV-файл. Программные решения, сгенерированные ИИ Системе Copilot был предоставлен следующий промпт, сформулированный на основе задания данной практической работы: Предоставленный здесь файл с метеорологическими наблюдениями (Illinois_ weather_1979_2011.txt) содержит данные по месяцам, а затем по округам штата Иллинойс за период с 1979 по 2011 год. Напиши программный код для обработки этого файла и извлечения данных по Чикаго (округ Кук) в один CSV-файл или файл электронной таблицы. Этот процесс включает замену строковых значений 'Missing' на пустые строки и преобразование процентных значений в десятичные числа. Также можно рассмотреть, какие поля данных являются повторяющимися (и, следовательно, могут быть пропущены или сохранены в другом месте). Обрати внимание: в конце файла присутствует раздел документации, поэтому обработку следует прекратить, когда первое поле в строке будет равно "---". В ответ Copilot предложил следующий вполне логичный план. 1. Открыть файл Illinois_weather_1979-2011.txt для чтения. 2. Создать новый CSV-файл для записи обработанных данных. 3. Перебирать построчно входной файл: а. б. Если строка начинается с "---", прекратить обработку. Если строка содержит данные по округу Кук (Чикаго), обработать ее: i. Заменить каждое значение 'Missing' на пустую строку. ii. Преобразовать процентные значения в десятичные числа. iii. Записать обработанные данные в CSV-файл. 4. Закрыть оба файла. Как и следовало ожидать, Copilot достаточно успешно выполнил задание, предложив решение, которое почти справляется с поставленной задачей. Он сгенерировал следующий программный код:
   500 Глава 21. Обработка файлов данных import csv input_file = 'Illinois_weather_1979-2011.txt' output_file = 'Chicago_weather_1979-2011.csv' with (open(input_file, 'r') as infile, open(output_file, 'w', newline='') as outfile): reader = csv.reader(infile, delimiter='\t') writer = csv.writer(outfile) headers = next(reader) writer.writerow(headers) # Записать заголовки в выходной файл for row in reader: if row[0].startswith('---'): break if 'Cook' in row: Проверяет наличие в строке поля ‘Cook processed_row = [field.replace('Missing', '') for field in row] processed_row = [str(float(field.strip('%')) / 100) if '%' in field else field for field in processed_row] writer.writerow(processed_row) Сгенерированный код во многом аналогичен версии, написанной программистом, но с рядом отличий. Прежде всего он удачно обрабатывает заголовки, копируя их в выходной файл, в отличие от варианта, написанного вручную. Кроме того, преобразование процентных значений в десятичные реализовано в одной строке вместо двух, а проверка производится по всем полям, а не только по последнему. Эти отличия можно считать оправданными и даже положительными. Наконец, сохраняются все поля, включая пустое поле "Notes", что неудивительно, поскольку удаление или объединение полей — это уже оценочное суждение. Единственная проблема в том, что при запуске этого кода результирующий файл Chicago_weather_1979-2011.csv содержит лишь заголовок. Причина в том, что проверка наличия «Cook» осуществляется по всей строке (как по списку полей): if 'Cook' in row: Чтобы это условие выполнилось, одно из полей строки должно быть в точности равно «Cook», в то время как в интересующих нас строках содержится поле "Cook County, IL". Решение — просто указать, что подстрока "Cook" должна содержаться в поле row[3]: if 'Cook' in row[3]: С этим единственным изменением версия Copilot работает идеально. Конечно, мы могли бы долго экспериментировать с формулировкой промпта, чтобы получить полное решение, однако рациональнее добавить три символа вручную. Промпт для Colaboratory несколько отличался:
   21.6. Практическая работа: метеорологические наблюдения 501 Представленный здесь файл (Illinois_weather_1979–2011.txt) содержит данные по штату Иллинойс за период с 1979 по 2011 год. Напиши код для обработки этого файла и извлечения данных по Чикаго (округ Кук) в один CSV-файл. Замени вхождения строки со значением 'Missing' на пустые строки и преобразуй проценты в десятичные числа. Используй заголовки входного файла, чтобы определить, какие поля повторяются (и, следовательно, могут быть пропущены). Останови обработку файла, когда первым полем в строке будет "---". Промпт пришлось отредактировать: во-первых, из-за ограничения размером 600 символов в среде Colaboratory, а во-вторых, поскольку при первоначальной формулировке генератор кода неправильно распределял поля. Обновленная версия промпта помогла обойти обе эти проблемы: import csv def process_illinois_weather_data(): """Обрабатывает файл погодных данных Иллинойса и извлекает данные по Чикаго в формате CSV.""" with open('Illinois_weather_1979-2011.txt', 'r') as infile, \ open('chicago_weather.csv', 'w', newline='') as outfile: reader = csv.reader(infile, delimiter='\t') writer = csv.writer(outfile) header = next(reader) writer.writerow(header) for row in reader: if row[0] == '---': break if row[1] == 'Cook': Проверяет, равно ли поле row[1] строке ‘Cook’ new_row = [] for cell in row: if cell == 'Missing': new_row.append('') elif '%' in cell: new_row.append(str(float(cell.replace('%', '')) / 100)) else: new_row.append(cell) writer.writerow(new_row) process_illinois_weather_data() print("Chicago weather data extracted to chicago_weather.csv") У этого подхода есть небольшие отличия. Хотя он сохраняет все поля (как и версия Copilot), способ обработки записей у него другой. Вместо изменения полей внутри строки он обрабатывает каждое поле по отдельности, добавляет его в новую строку, а по завершении обработки записывает эту новую строку в файл. Такой подход, возможно, чуть менее эффективен, поскольку для каждой строки создается новый список, однако на практике это вряд ли существенно влияет на производительность.
   502 Глава 21. Обработка файлов данных Как и в версии, предложенной Copilot, данный код записывает в файл только строку заголовков — и по весьма схожей причине: условие отбора строк с данными по округу Кук основано на проверке полного равенства поля row[1] строке 'Cook'. Это неверно по двум причинам: во-первых, как упоминалось выше, фактическое значение поля — "Cook County, IL", а во-вторых, вместо поля County (округ) здесь проверяется поле Month (месяц). Правка здесь требуется такая же, как и раньше. Эта строка кода должна выглядеть так: if 'Cook' in row[3]: После внесения этого изменения программа выдает корректный результат. Как уже отмечалось, данная задача достаточно тривиальна и потому не представляет особой сложности для ИИ-ассистентов по сравнению с более комплексными заданиями. Основные трудности у чат-ботов возникли при выборе корректного метода и поля для проверки округа, а также при оценке полей на предмет избыточности. Явное указание соответствий и инструкций в промпте, вероятно, улучшило бы результат, но потребовало бы больших усилий при его составлении. Итоги ETL («извлечение, преобразование и загрузка») — это процесс извлечения данных из исходного формата, обеспечения их согласованности и последующего сохранения в формате, пригодном для дальнейшего использования. ETL является базовым этапом в большинстве процессов обработки данных. При работе с текстовыми файлами могут возникнуть проблемы с кодировкой, однако Python предоставляет средства для устранения некоторых из них при считывании файлов. Наиболее распространенным способом обработки широкого спектра символов в простых текстовых файлах является использование кодировки Unicode (UTF-8). Файлы с разделителями и файлы CSV встречаются повсеместно, и работать с ними удобнее всего при помощи модуля csv. Файлы электронных таблиц (в формате Excel) представляют собой более сложный формат по сравнению с CSV, однако с ними можно производить операции чтения и записи при помощи сторонних библиотек. Символы денежных единиц, знаки препинания и пустые значения — основные источники ошибок при обработке текстовой информации; внимательно отслеживайте их при очистке данных. Предварительная очистка и сортировка файла данных может существенно ускорить выполнение следующих этапов. Процесс записи данных в рассмотренных форматах осуществляется аналогично их чтению.
22 Получение данных по сети В этой главе: 3 Получение файлов по протоколам FTP/SFTP, SSH/SCP и HTTPS 3 Извлечение данных через API 3 Форматы структурированных файлов данных: JSON и XML 3 Сбор данных с веб-страниц (скрейпинг) Итак, мы познакомились с обработкой текстовых файлов данных. В этой главе рассматривается использование Python при передаче файлов данных по сети. В одних случаях это могут быть текстовые файлы или файлы электронных таблиц, о которых рассказывалось в главе 21, в других — структурированные форматы, доступные через REST- или SOAP-API. Иногда для получения данных приходится извлекать их из веб-страниц путем скрейпинга. В этой главе мы обсудим все эти ситуации и рассмотрим типичные примеры их использования. 22.1. Загрузка файлов Прежде чем приступить к обработке файлов данных, их нужно каким-то образом получить. Иногда это очень просто — к примеру, вы вручную загружаете zip-архив или файлы автоматически поступают на ваш компьютер от внешнего источника. Но чаще процесс оказывается более трудоемким. К примеру, нужно скачать множество файлов с удаленного сервера или файлы требуется получать на регулярной основе. Последовательность действий иногда бывает настолько запутанной,
   504 Глава 22. Получение данных по сети что выполнять их вручную — сплошное мучение. Во всех таких случаях имеет смысл автоматизировать получение файлов данных при помощи языка Python. Прежде всего хотелось бы отметить, что использование скрипта Python — это не единственный и, возможно, не всегда лучший способ загрузки файлов. Во врезке ниже приводится разъяснение факторов, которые я учитываю, принимая решение о том, стоит ли использовать скрипт Python для извлечения файлов. И если в вашем случае применение Python действительно оправданно, то в этом разделе вы найдете ряд типовых шаблонов, к которым сможете прибегнуть в вашей работе. Стоит ли использовать Python? Несмотря на то что использование Python для получения файлов может быть весьма эффективным, оно не всегда является оптимальным вариантом. При принятии решения стоит учитывать два момента. • Есть ли более простые варианты? В зависимости от операционной системы и вашего опыта может оказаться, что простые скрипты оболочки и инструменты командной строки проще и удобнее настроить. Если эти средства вам недоступны или вы недостаточно уверенно владеете ими (возможно, последнее относится к тем, кто будет заниматься поддержкой ваших программ), то можно рассмотреть возможность использования скрипта Python. • Является ли процесс получения данных особенно сложным или тесно связанным с их обработкой? Хотя такие ситуации всегда нежелательны, но все же они встречаются. Лично я руководствуюсь следующим правилом: если скрипт оболочки занимает больше нескольких строк кода или мне приходится всерьез задумываться над тем, как в нем что-то реализовать, вероятно, пора переходить на Python. 22.1.1. Получение файлов с FTP-сервера с помощью Python Протокол FTP (File Transfer Protocol) существует уже очень давно, однако он до сих пор остается простым и удобным средством передачи файлов в тех случаях, когда безопасность не слишком критична. Чтобы подключиться к FTP-серверу при помощи Python, можно воспользоваться модулем ftplib из стандартной биб­ лиотеки. Последовательность действий предельно проста: создайте объект FTP, подключитесь к серверу, а затем войдите в систему, указав имя пользователя и пароль (или, как часто бывает, с именем пользователя anonymous и пустым паролем). Чтобы продолжить работу с метеорологическими данными, можно подключиться к FTP-серверу Национального управления океанических и атмосферных исследований (NOAA): import ftplib ftp = ftplib.FTP('tgftp.nws.noaa.gov') ftp.login() '230 Login successful.'
   22.1. Загрузка файлов 505 После подключения объект ftp используется для просмотра списка файлов и смены текущего каталога: ftp.cwd('data') '250 Directory successfully changed.' ftp.nlst() ['climate', 'fnmoc', 'forecasts', 'hurricane_products', 'ls_SS_services', 'marine', 'nsd_bbsss.txt', 'nsd_cccc.txt', 'observations', 'products', 'public_statement', 'raw', 'records', 'summaries', 'tampa', 'watches_warnings', 'zonecatalog.curr', 'zonecatalog.curr.tar'] Затем можно, к примеру, получить свежую авиационную сводку погоды (METAR) для международного аэропорта О’Хара в Чикаго (O’Hare International Airport): x = ftp.retrbinary('RETR observations/metar/decoded/KORD.TXT', open('KORD.TXT', 'wb').write) '226 Transfer complete.' Метод ftp.retrbinary принимает два параметра — путь к файлу на удаленном сервере и метод обработки данных этого файла на вашей стороне — в данном случае метод write файла с тем же именем, открытого для двоичной записи. Просмотрев файл KORD.TXT, вы увидите, что он содержит загруженные данные: x = ftp.retrbinary('RETR observations/metar/decoded/KORD.TXT', open('KORD. TXT', 'wb').write) open('KORD.TXT', 'r').readlines() ["CHICAGO O'HARE INTERNATIONAL, IL, United States (KORD) ➥41-59N 087-55W 200M\n", 'Aug 19, 2024 - 10:51 PM EDT / 2024.08.20 0251 UTC\n', 'Wind: from the NNE (030 degrees) at 13 MPH (11 KT):0\n', 'Visibility: 10 mile(s):0\n', 'Sky conditions: partly cloudy\n', 'Temperature: 68.0 F (20.0 C)\n', 'Dew Point: 55.9 F (13.3 C)\n', 'Relative Humidity: 65%\n', 'Pressure (altimeter): 30.17 in. Hg (1021 hPa)\n', 'Pressure tendency: 0.03 inches (1.0 hPa) higher than three hours ago\n', 'ob: KORD 200251Z 03011KT 10SM SCT039 SCT050 20/13 A3017 ➥RMK AO2 SLP212 T02000133 53010\n', 'cycle: 3\n'] Модуль ftplib также позволяет устанавливать соединения с серверами посредством криптографического протокола TLS (Transport Layer Security), если вместо FTP используется класс FTP_TLS: ftp = ftplib.FTP_TLS('tgftp.nws.noaa.gov')
   506 Глава 22. Получение данных по сети 22.1.2. Получение файлов с помощью SFTP При наличии повышенных требований к уровню безопасности, в частности, в корпоративном контексте передачи бизнес-данных по сети, достаточно часто применяется SFTP — полнофункциональный протокол с поддержкой файлового доступа, передачи файлов и управления ими посредством защищенного соединения на базе протокола SSH (Secure Shell). И хотя SFTP означает SSH File Transfer Protocol (протокол передачи файлов по SSH), а FTP означает File Transfer Protocol (протокол передачи файлов), эти два протокола не имеют ничего общего в своем происхождении. SFTP не является реализацией FTP на базе SSH, а представляет собой новый протокол, специально разработанный для применения в связке с SSH. Передача файлов на базе SSH привлекает тем, что протокол SSH уже является фактическим стандартом для обращения к удаленным серверам, а включить поддержку SFTP на сервере несложно (и зачастую она есть по умолчанию). В стандартной библиотеке Python отсутствует клиентский модуль SFTP/SCP, однако разработанная в сообществе библиотека paramiko управляет операциями SFTP и обеспечивает подключение по протоколу SSH. Проще всего установить paramiko через pip. Если бы на сайте Национального управления океанических и атмосферных исследований, упоминавшемся ранее в этой главе, применялся протокол SFTP (а он не применяется, и потому приведенный ниже код работать не будет!), то эквивалент предыдущего примера с использованием SFTP выглядел бы так: import paramiko t = paramiko.Transport((hostname, port)) t.connect(username, password) sftp = paramiko.SFTPClient.from_transport(t) Также следует отметить, что, хотя библиотека paramiko и поддерживает выполнение команд на удаленном сервере с получением вывода аналогично обычной сессии SSH, в ней нет встроенной функции scp. На практике потребность в этой функции возникает крайне редко: если задача сводится к передаче по SSH одного-двух файлов, то куда проще воспользоваться утилитой командной строки scp. 22.1.3. Получение файлов по протоколу HTTP/HTTPS Последний распространенный способ получения файлов данных, который мы рассмотрим в этой главе, — через HTTP- или HTTPS-соединение. Вероятно, этот вариант проще всех остальных, поскольку, по существу, вы просто получаете данные с веб-сервера, а поддержка доступа к веб-серверам сегодня распространена повсеместно. В данном случае также вовсе не обязательно прибегать к Python. Существует множество утилит командной строки, которые позволяют получать файлы через HTTP/HTTPS-соединения и обеспечивают практически
   22.1. Загрузка файлов 507 весь необходимый функционал. Среди них самыми популярными являются wget и curl. Впрочем, если у вас есть причины выполнять загрузку с помощью кода Python, процесс будет ненамного сложнее. Вне всякого сомнения, библиотека requests является самым простым и надежным средством работы с HTTP/ HTTPS-серверами из Python. Для ее установки достаточно ввести команду pip install requests, после чего получение файла не составит особого труда: необходимо лишь импортировать библиотеку requests и применить соответствующий метод по протоколу HTTP (обычно GET) для подключения к серверу и получения данных. В следующем примере выполняется загрузка ежемесячных температурных данных для аэропорта Хитроу (Heathrow Airport) с 1948 года — этот текстовый файл предоставляется веб-сервером. При желании вы можете ввести URL в своем браузере, загрузить страницу и сохранить ее. Впрочем, если страница велика или загружать приходится слишком много страниц, проще применить код следующего вида: import requests response = requests.get("http://www.metoffice.gov.uk/pub/data/weather/uk/ climate/ ➥stationdata/heathrowdata.txt") Ответ веб-сервера содержит достаточно обширную информацию, включая заголовки, возвращенные веб-сервером, что может оказаться полезным при отладке в случае ошибок. Однако чаще всего вас будет интересовать именно возвращае­ мое содержимое. Для получения этих данных следует обратиться к свойству text, содержащему тело ответа в виде строки, или к свойству content, которое возвращает тело ответа в формате bytes: print(response.text) Heathrow (London Airport) Location 507800E 176700N, Lat 51.479 Lon -0.449, 25m amsl Estimated data is marked with a * after the value. Missing data (more than 2 days missing in month) is marked by ---. Sunshine data taken from an automatic Kipp & Zonen sensor marked with a #, otherwise sunshine data taken from a Campbell Stokes recorder. yyyy mm tmax tmin af rain sun degC degC days mm hours 1948 1 8.9 3.3 --85.0 -1948 2 7.9 2.2 --26.0 -1948 3 14.2 3.8 --14.0 -1948 4 15.4 5.1 --35.0 -1948 5 18.1 6.9 --57.0 — ... Обычно текст ответа записывается в файл для последующей обработки, но в зависимости от целей иногда производится предварительная очистка или даже непосредственная обработка данных.
   508 Глава 22. Получение данных по сети Попробуйте сами: загрузка файла Допустим, вы работаете с файлом данных из нашего примера и хотите разделить каждую строку на отдельные поля; как бы вы это сделали? Какая еще обработка может потребоваться? Попробуйте написать код для загрузки данных из этого файла и вычислить среднегодовой уровень осадков или среднюю максимальную и минимальную температуру для каждого года (что будет посложнее). 22.2. Получение данных через API Механизм получения данных через API является широко распространенной практикой, отражающей тенденцию к разделению приложений на автономные службы, взаимодействующие через программные интерфейсы (API). API могут быть устроены по-разному, однако чаще всего они задействуют обычные HTTP/ HTTPS-протоколы и стандартные HTTP-методы: GET, POST, PUT и DELETE. Получение данных таким способом имеет много общего с загрузкой файлов, как описано в разделе 22.1.3, только в данном случае данные предоставляются не в виде статического файла. Вместо того чтобы отправлять статические файлы с данными, приложение отправляет запрос другому источнику данных и динамически формирует информацию по запросу. Несмотря на значительное разнообразие возможных архитектур API, одной из наиболее популярных является RESTful-интерфейс (REpresentational State Transfer, «передача состояния представления»), который работает через те же HTTP/HTTPS-протоколы, что и обычный интернет. Способов реализации API также существует множество, однако на практике данные чаще всего получают с помощью GET-запроса — то есть тем же способом, каким ваш браузер запрашивает веб-страницы. В случае GET-запросов параметры, определяющие выборку данных, обычно добавляются к URL в форме строки запроса. В качестве проверки можно запросить текущие погодные условия в Чикаго на Open-Meteo.com, задействовав следующий URL-адрес: https://mng.bz/BX9g. Компоненты, следующие за символом ?, являются параметрами строки запроса, указывающими широту и долготу географической точки, а также два интересующих нас показателя: температуру и скорость ветра. В данном случае API возвращает информацию в формате JSON, который мы рассмотрим в разделе 22.3.1. Заметьте, что параметры в строке запроса разделяются амперсандом (&). Зная необходимый URL, можно воспользоваться библиотекой requests для получения данных через API и обработать их на лету либо сохранить в файл для последующих операций. Самый простой способ выполнения этой процедуры идентичен получению файла: response = requests.get("https://api.open-meteo.com/v1/forecast? ➥latitude=41.882&longitude=-87.623&current=temperature_2m, ➥wind_speed_10m") weather = response.text weather
   22.2. Получение данных через API 509 {"latitude":41.879482,"longitude":-87.64975,"generationtime_ms":0.026941299 438476562,"utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation": "GMT","elevation":185.0,"current_units":{"time":"iso8601","interval": "seconds","temperature_2m":"°C","wind_speed_10m":"km/h"},"current":{"time": "2024-09-02T02:00","interval":900,"temperature_2m":16.9, "wind_speed_10m":13.3}} Помните о необходимости экранировать пробелы и большинство знаков препинания в параметрах запросов, поскольку они недопустимы в URL-адресах. Впрочем, это требование не является строго обязательным, поскольку библиотека requests, как и многие веб-браузеры, выполняет экранирование URL-адресов автоматически. И напоследок рассмотрим еще один пример. Допустим, вы хотите получить криминальную сводку по Чикаго за период между полуднем и часом дня 10 января 2024 года. В соответствии с требованиями данного API вы указываете диапазон дат посредством параметров строки запроса $where=date between <начальная_ дата> и <конечная_дата>, где начальная и конечная временные метки указываются в формате ISO и заключаются в кавычки. Таким образом, URL для получения криминальной сводки за этот час по Чикаго будет таким: https://mng.bz/N1ad: result = requests.get("https://data.cityofchicago.org/resource/ ➥6zsd-86xi.json?$where=date between '2024-01-10T12:01:00' and ➥'2024-01-10T12:10:00'") result.json() [{'id': '13334404', 'case_number': 'JH111115', 'date': '2024-01-10T12:01:00.000', 'block': '009XX W WINONA ST', 'iucr': '2020', 'primary_type': 'NARCOTICS', 'description': 'POSSESS - AMPHETAMINES', 'location_description': 'APARTMENT', 'arrest': True, 'domestic': False, 'beat': '2024', 'district': '020', 'ward': '48', 'community_area': '3', 'fbi_code': '18', 'x_coordinate': '1169132', 'y_coordinate': '1934325', 'year': '2024', 'updated_on': '2024-01-18T15:41:44.000', 'latitude': '41.975305734', 'longitude': '-87.653416364', 'location': {'type': 'Point', 'coordinates': [-87.653416364, 41.975305734]}, 'location_address': '', 'location_city': '', 'location_state': '', 'location_zip': ''},
   510 Глава 22. Получение данных по сети {'id': '13334420', 'case_number': 'JH111020', 'date': '2024-01-10T12:01:00.000', 'block': '009XX W WINONA ST', 'iucr': '1330', 'primary_type': 'CRIMINAL TRESPASS', 'description': 'TO LAND', 'location_description': 'APARTMENT', 'arrest': True, 'domestic': False, 'beat': '2024', 'district': '020', 'ward': '48', 'community_area': '3', 'fbi_code': '26', 'x_coordinate': '1169132', 'y_coordinate': '1934325', 'year': '2024', 'updated_on': '2024-01-18T15:41:44.000', 'latitude': '41.975305734', 'longitude': '-87.653416364', 'location': {'type': 'Point', 'coordinates': [-87.653416364, 41.975305734]}, 'location_address': '', 'location_city': '', 'location_state': '', 'location_zip': ''}, ... Несколько записей пропущено ] В рассматриваемом примере в URL-адресе недопустимы некоторые символы, такие как пробелы. Это еще один случай, когда библиотека requests демонстрирует свое стремление упростить работу программиста: перед отправкой запроса она надлежащим образом экранирует URL-адрес, приводя его к следующему виду: https://mng.bz/dXvX. Обратите внимание, что все пробелы заменены на %20 и вам даже не пришлось об этом задумываться. Попробуйте сами: обращение к API Напишите код для получения данных с портала данных Чикаго (Chicago Data Portal, https://data.cityofchicago.org/). Просмотрите поля, упоминаемые в результатах, и попробуйте произвести выборку записей по другому полю в сочетании с временным диапазоном. 22.3. Структурированные форматы данных API могут предоставлять данные в виде простого текста, однако на практике в этих целях гораздо чаще применяются структурированные форматы, самыми распространенными из которых являются JSON и XML. Оба эти формата
   22.3. Структурированные форматы данных 511 организуют текстовые данные таким образом, чтобы обеспечить гибкое и эффективное представление комплексной информации с учетом иерархических взаимосвязей. 22.3.1. Данные в формате JSON Формат JSON (сокращение от JavaScript Object Notation, «нотация объектов JavaScript») появился в 1999 году. Он состоит всего из двух конструкций: пары «ключ-значение», называемые структурами, очень похожи на словари Python, а упорядоченные последовательности значений, называемые массивами, очень похожи на списки Python. В формате JSON ключами могут быть только строки в двойных кавычках, а значениями — строки в двойных кавычках, числовые значения, логические значения true и false, null, массивы либо другие структуры. Такая организация делает JSON легким и эффективным средством представления данных, удобным как для их передачи по сети, так и для восприятия человеком. Популярность JSON такова, что практически каждый язык программирования предоставляет средства преобразования между JSON и собственными типами данных. В Python такую функциональность реализует модуль json, включенный в стандартную библиотеку, начиная с версии 2.6. Его исходная версия, находящаяся на внешнем сопровождении, доступна под названием simplejson, однако в Python 3 куда чаще используется стандартный модуль — за исключением случаев, когда требуется поддержка особых возможностей, таких как работа с десятичными числами (тип decimal), реализованная исключительно в simplejson. Метеорологические и криминальные сводки, полученные из API в разделе 22.2, представлены в формате JSON. Для передачи JSON по сети его необходимо сериализовать, то есть преобразовать в последовательность байтов. И хотя пакеты данных, полученные от API метеорологического ресурса open-meteo.com и портала данных Чикаго, выглядят как JSON, по сути, это байтовые строки, представляющие сериализованные объекты JSON. Чтобы преобразовать такую байтовую строку в настоящий объект JSON, а затем в словарь Python, необходимо прибегнуть к функции json.loads(). Так, чтобы получить погодную сводку, можно выполнить тот же запрос, что и ранее, но на этот раз преобразовать результат в словарь Python: import json import requests response = requests.get("https://api.open-meteo.com/v1/forecast?latitude ➥=41.882&longitude=-87.623&current=temperature_2m,wind_speed_10m") weather = json.loads(response.text) weather {'latitude': 41.879482, 'longitude': -87.64975, 'generationtime_ms': 0.025987625122070312, 'utc_offset_seconds': 0,
   512 Глава 22. Получение данных по сети 'timezone': 'GMT', 'timezone_abbreviation': 'GMT', 'elevation': 185.0, 'current_units': {'time': 'iso8601', 'interval': 'seconds', 'temperature_2m': '°C', 'wind_speed_10m': 'km/h'}, 'current': {'time': '2024-09-02T02:15', 'interval': 900, 'temperature_2m': 16.9, 'wind_speed_10m': 13.8}} weather['current']['temperature_2m'] 16.9 Обратите внимание на то, что вызов json.loads() получает строковое представление объекта JSON и преобразует (или загружает) его в словарь Python. Кроме того, функция json.load() способна считывать данные из любого объекта, реализующего интерфейс файлового потока, то есть поддерживающего метод read. Стандартный формат отображения словаря на экране может оказаться весьма трудным для чтения. Форматированный вывод, также известный как структурная распечатка программного кода или «красивая печать» (pretty printing), заметно облегчает зрительное восприятие структурированных данных. Для просмотра содержимого словаря из примера воспользуйтесь модулем pprint стандартной библиотеки Python: from pprint import pprint as pp pp(weather) from pprint import pprint as pp pp(weather) {'current': {'interval': 900, 'temperature_2m': 16.9, 'time': '2024-09-02T02:15', 'wind_speed_10m': 13.8}, 'current_units': {'interval': 'seconds', 'temperature_2m': '°C', 'time': 'iso8601', 'wind_speed_10m': 'km/h'}, 'elevation': 185.0, 'generationtime_ms': 0.025987625122070312, 'latitude': 41.879482, 'longitude': -87.64975, 'timezone': 'GMT', 'timezone_abbreviation': 'GMT', 'utc_offset_seconds': 0} Обе функции загрузки, json.loads() и json.load() , можно настроить для управления процессами парсинга и декодирования исходного JSON в объекты Python, тогда как по умолчанию используется преобразование, приведенное в табл. 22.1.
   22.3. Структурированные форматы данных 513 Таблица 22.1. Стандартные правила декодирования JSON в Python JSON Python object dict array list string str number (int) int number (real) float true True false False null None Получение JSON при помощи библиотеки requests В этом разделе мы задействовали библиотеку requests для получения данных в формате JSON и последующего их преобразования в объект Python методом json. loads(). Этот прием хорошо зарекомендовал себя в работе, и поскольку библиотеку requests часто используют для решения этой конкретной задачи, в ней предусмотрена сокращенная запись: объект ответа (response) содержит метод json(), который автоматически выполняет преобразование. Таким образом, в данном примере вместо weather = json.loads(response.text) можно записать weather = response.json() Результат тот же самый, однако этот вариант проще, лучше читается и в большей степени соответствует стилю Python. Для записи данных в формате JSON в файл или их сериализации в строку применяются функции, обратные load() и loads(), — это dump() и dumps(). Функция json.dump() принимает в параметре объект файла, поддерживающий метод write(), тогда как json.dumps() возвращает строковое представление. Обе функции поддерживают гибкую настройку преобразования в формат JSON, однако по умолчанию применяется схема преобразования, представленная в таб­л. 22.1. Таким образом, чтобы сохранить метеосводку в файл JSON, можно сделать следующее: outfile = open("weather_01.json", "w") json.dump(weather, outfile) Записывает строку в выходной файл outfile.close() json.dumps(weather) Возвращает строку {"latitude": 41.879482, "longitude": -87.64975, "generationtime_ms": 0.025987625122070312, "utc_offset_seconds": 0, "timezone": "GMT", "timezone_abbreviation": "GMT", "elevation": 185.0, "current_units": {"time": "iso8601", "interval": "seconds", "temperature_2m": "\u00b0C", "wind_speed_10m": "km/h"}, "current": {"time": "2024-09-02T02:15", "interval": 900, "temperature_2m": 16.9, "wind_speed_10m": 13.8}}
   514 Глава 22. Получение данных по сети Как можно заметить, весь объект закодирован в одну строку. При использовании функции json.dump (без окончания «s») эта строка записывается в указанный файл. В свою очередь, json.dumps (с окончанием «s») возвращает сериализованную строку. Здесь может быть полезно отформатировать ее в более удобочитае­ мом виде, как мы делали ранее при помощи модуля pprint. Для этого можно воспользоваться параметром indent (отступ) в функциях dump и dumps: print(json.dumps(weather, indent=2)) { } "latitude": 41.879482, "longitude": -87.64975, "generationtime_ms": 0.025987625122070312, "utc_offset_seconds": 0, "timezone": "GMT", "timezone_abbreviation": "GMT", "elevation": 185.0, "current_units": { "time": "iso8601", "interval": "seconds", "temperature_2m": "\u00b0C", "wind_speed_10m": "km/h" }, "current": { "time": "2024-09-02T02:15", "interval": 900, "temperature_2m": 16.9, "wind_speed_10m": 13.8 } Следует, однако, учитывать, что при повторных вызовах функции json.dump() для последовательной записи нескольких объектов в файл результатом будет набор корректных строк в формате JSON, но при этом содержимое файла в целом не будет являться допустимым объектом JSON. Попытка считать такой файл одним вызовом json.load() приведет к ошибке. Если требуется сериализовать несколько объектов как единый объект JSON, следует предварительно поместить их в список (или, что еще лучше, в составной объект или структуру), а уже затем записать этот результирующий элемент в файл. Если у вас есть два или более элемента метеоданных, которые требуется сохранить в файл в формате JSON, вам нужно сделать выбор. Один из вариантов — вызывать json.dumps() отдельно для каждого объекта, что приведет к созданию файла, содержащего несколько строк JSON. Если допустить, что weather_list — это список объектов с метеосводками, можно составить следующий код: outfile = open("weather_list.json", "w") for report in weather_list: outfile.write(json.dumps(report) + "\n") outfile.close()
   22.3. Структурированные форматы данных 515 При выборе такого варианта вам придется загружать каждую строку как отдельный объект в формате JSON: weather_list2 = [] for line in open("weather_list.json"): weather_list2.append(json.loads(line)) В качестве альтернативы можно упаковать весь список в один объект JSON. Поскольку массивы верхнего уровня в JSON могут представлять потенциальную уязвимость, рекомендуется поместить массив в словарь: outfile = open("weather_obj.json", "w") weather_obj = {"reports": weather_list, "count": 2} json.dump(weather_obj, outfile) outfile.close() При таком подходе можно загрузить объект JSON из файла одной операцией: with open("weather_obj.json") as infile: weather_obj = json.load(infile) Второй вариант хорошо подходит для файлов JSON относительно небольшого размера, а вот для очень больших файлов он далеко не идеален из-за усложнения обработки ошибок и возможной нехватки памяти. Попробуйте сами: сохранение криминальной сводки в формате JSON Модифицируйте код, написанный в разделе 22.2, для загрузки криминальных сводок по Чикаго. Затем преобразуйте полученные данные из строки в формате JSON в объект Python. После этого попытайтесь сохранить события из сводки в виде набора отдельных строк JSON в одном файле и как единый объект JSON — в другом. Потом определите, какой код потребуется для загрузки каждого из этих файлов. 22.3.2. Данные в формате XML Язык разметки XML (eXtensible Markup Language) существует с конца XX века. Как и HTML, он построен на синтаксисе тегов в угловых скобках, а элементы вкладываются друг в друга, образуя древовидную структуру. Предполагалось, что XML будет хорошо читаться как машинами, так и людьми, однако нередко он бывает настолько многословным и запутанным, что людям в нем непросто разобраться. Тем не менее, поскольку XML — это устоявшийся стандарт, данные, использующие XML-разметку, встречаются достаточно часто. И хотя он предназначен для машинного чтения, время от времени возникает необходимость преобразовать XML в более удобные для работы форматы. В качестве примера загрузим XML-версию демографической сводки Всемирного банка по Чили: result = requests.get( "https://api.worldbank.org/v2/country/CL/indicator/SP.POP.TOTL?format=xml") pop_data = result.text[3:] Пропускает лишние символы
   516 Глава 22. Получение данных по сети print(pop_data) <?xml version="1.0" encoding="utf-8"?> <wb:data page="1" pages="2" per_page="50" total="64" sourceid="2" lastupdated="2024-06-28" xmlns:wb="http://www.worldbank.org"> <wb:data> <wb:indicator id="SP.POP.TOTL">Population, total</wb:indicator> <wb:country id="CL">Chile</wb:country> <wb:countryiso3code>CHL</wb:countryiso3code> <wb:date>2023</wb:date> <wb:value>19629590</wb:value> <wb:unit /> <wb:obs_status /> <wb:decimal>0</wb:decimal> </wb:data> <wb:data> <wb:indicator id="SP.POP.TOTL">Population, total</wb:indicator> <wb:country id="CL">Chile</wb:country> <wb:countryiso3code>CHL</wb:countryiso3code> <wb:date>2022</wb:date> <wb:value>19603733</wb:value> <wb:unit /> <wb:obs_status /> <wb:decimal>0</wb:decimal> </wb:data> … Пропущено несколько записей </wb:data> Приведен лишь первый раздел документа, остальную часть мы пропустили. Но даже такой небольшой фрагмент наглядно демонстрирует основные недостатки XML-данных. В частности, вы видите, насколько «многословен» этот формат: иной раз теги занимают больше места, чем содержащиеся в них значения. Здесь также хорошо видны другие отличительные особенности XML — древовидная структура данных и применение объемного заголовка с метаданными до начала фактических записей. Если распределить форматы данных по шкале сложности, то самыми простыми по структуре окажутся текстовые форматы с разделителями (CSV и его аналоги), а самыми сложными — XML-данные. Данный пример иллюстрирует еще одно характерное свойство XML-формата, усложняющее процедуру извлечения данных: возможность хранения информации как в виде атрибутов, так и в виде текстовых значений внутри тегов. Если взглянуть на первый элемент wb:data в начале фрагмента кода выше, можно увидеть, что наряду с прочими элементами данных он содержит свои метаданные в виде тегов и атрибутов. Чтобы понять, как обрабатывать тот или иной элемент данных, нужно тщательно изучить их структуру либо обратиться к спецификации по формату. При такой сложной структуре даже простое извлечение данных из XML может оказаться задачей не из легких. Существует ряд методик работы с этим форматом. В стандартной библиотеке Python имеются модули для парсинга и обработки XML-документов, но ни один из них не обеспечивает оптимального решения для простого извлечения данных.
   22.3. Структурированные форматы данных 517 Самое удобное средство для решения этой задачи я обнаружила в библиотеке xmltodict, содержащей одноименный модуль, который анализирует данные XML и возвращает словарь, отражающий древовидную структуру. Собственно, «за кулисами» он задействует XML-парсер Expat стандартной библиотеки, преобразует XML-документ в дерево и создает с его помощью словарь, благодаря чему xmltodict может обработать любую разметку, с которой справляется парсер, а также может взять словарь и при необходимости «вернуть» его в XML, что делает его очень полезным инструментом. За несколько лет применения я убедилась в том, что библиотеки xmltodict с лихвой хватает для всех моих задач, связанных с XML. Чтобы установить xmltodict, запустите команду pip install xmltodict. Для преобразования XML в словарь можно импортировать xmltodict и вызвать метод parse для строки в формате XML: import xmltodict population = xmltodict.parse(pop_data) population В данном случае для компактности стоит напрямую передать содержимое файла методу parse. После парсинга этот объект данных представляет собой упорядоченный словарь с такими же значениями, которые он имел бы, если бы был загружен из следующего файла JSON: {'wb:data': {'@page': '1', '@pages': '2', '@per_page': '50', '@total': '64', '@sourceid': '2', '@lastupdated': '2024-06-28', '@xmlns:wb': 'http://www.worldbank.org', 'wb:data': [{'wb:indicator': {'@id': 'SP.POP.TOTL', '#text': 'Population, total'}, 'wb:country': {'@id': 'CL', '#text': 'Chile'}, 'wb:countryiso3code': 'CHL', 'wb:date': '2023', 'wb:value': '19629590', 'wb:unit': None, 'wb:obs_status': None, 'wb:decimal': '0'}, {'wb:indicator': {'@id': 'SP.POP.TOTL', '#text': 'Population, total'}, 'wb:country': {'@id': 'CL', '#text': 'Chile'}, 'wb:countryiso3code': 'CHL', 'wb:date': '2022', 'wb:value': '19603733', 'wb:unit': None, 'wb:obs_status': None, 'wb:decimal': '0'}, … Пропущено несколько записей ] } }
   518 Глава 22. Получение данных по сети Обратите внимание, что атрибуты извлечены из тегов, но перед их именами стоит символ @, указывающий на то, что они являлись атрибутами соответствующего родительского тега. В случае, если XML-узел содержит как текстовое значение, так и вложенный элемент, ключ для текстового содержимого обозначается как "#text", как это видно в элементе "wb:indicator" внутри каждого элемента "wb:data". Если элемент встречается несколько раз, он преобразуется в список — как это происходит с элементами "wb:data" в рассматриваемом примере. Поскольку в Python довольно легко работать со словарями и списками, даже вложенными, xmltodict представляет собой эффективное средство для работы с большинством XML-документов. На практике я пользуюсь этим на протяжении нескольких лет в рабочих системах для обработки разнообразных XML-данных и ни разу не сталкивалась с проблемами. Попробуйте сами: получение и парсинг XML Напишите код для получения в формате XML данных о численности населения Чили, размещенных по адресу https://mng.bz/Ea6R. Затем воспользуйтесь xmltodict для преобразования XML в словарь Python и вывода отчета о том, как изменилась численность населения страны за последние 25 лет. 22.4. Сбор данных с веб-страниц (скрейпинг) Иногда данные размещены непосредственно на веб-сайте и по каким-либо причинам недоступны из других источников. В таких случаях целесообразно собирать данные непосредственно с веб-страниц с помощью процесса, называемого «краулинг» (crawling) или «скрейпинг» (scraping). Прежде чем мы продолжим говорить о скрейпинге, следует сделать оговорку: сбор данных с веб-сайтов, владельцем или администратором которых вы не являетесь, — практика в лучшем случае неоднозначная с юридической точки зрения. Здесь много неопределенностей и противоречий, включая условия использования сайта, способ доступа к его содержимому и то, в каких целях будут использоваться собранные данные. Если вы не управляете сайтом, данные с которого вы намереваетесь собирать, то на вопрос «Законно ли скрейпить этот сайт?» в ответ вы чаще всего услышите: «Ну, это как посмотреть…» Если вы все-таки решите собирать данные с действующего сайта, вам также следует учитывать нагрузку, которую вы тем самым создадите. И если проверенный временем веб-ресурс с высоким трафиком, скорее всего, выдержит практически любую нагрузку, то менее мощный и малопосещаемый сайт может быть буквально парализован. Поэтому как минимум необходимо проследить за тем, чтобы скрейпинг не обернулся непреднамеренной атакой типа отказа в обслуживании (DoS — denial-of-service). И напротив, я не раз оказывалась в ситуациях, когда проще было извлечь требуемые данные с нашего собственного сайта, чем запрашивать их по корпоративным
   22.4. Сбор данных с веб-страниц (скрейпинг) 519 каналам. Хотя сбор данных с веб-страниц входит в арсенал навыков программиста, это слишком обширная тема, чтобы подробно ее здесь рассматривать. Вместо этого я приведу очень простой пример, демонстрирующий общий принцип, а затем дам рекомендации для более сложных случаев. Скрейпинг веб-сайта состоит из двух частей: загрузки веб-страницы и собственно извлечения из нее данных. Загрузку страницы можно осуществить с помощью библиотеки requests, что не составляет особого труда. В качестве иллюстрации рассмотрим в листинге 22.1 HTML-код простой веб-страницы с минимумом контента, без подключения CSS или JavaScript. Листинг 22.1. Файл test.html <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <html> <head> <title>Title</title> </head> <body> <h1>Heading 1</h1> This is plain text, and is boring <span class="special">this is special</span> Here is a <a href="http://bitbucket.dev.null">link</a> <hr> <address>Ann Address, Somewhere, AState 00000 </address> </body> </html> Предположим, на этой странице вас интересуют данные только двух видов: все элементы класса "special" и все гиперссылки. Теоретически можно обработать HTML-файл, осуществляя поиск по подстрокам 'class="special"' и "<a href>", а затем написать код для извлечения их содержимого. Однако даже с применением регулярных выражений такой подход будет трудоемким, подверженным ошибкам и сложным для поддержки. Гораздо проще воспользоваться специализированной библиотекой для синтаксического анализа HTML, такой как Beautiful Soup. Если вы захотите опробовать следующий код и поэкспериментировать с парсингом HTML-страниц, установите пакет командой pip install bs4. Установка библиотеки Beautiful Soup существенно упрощает процедуру парсинга HTML-документа. Здесь мы исходим из того, что веб-страница уже получена (допустим, при помощи библиотеки requests), поэтому остается лишь выполнить парсинг ее HTML-структуры. В первую очередь загрузим текст и создадим объект парсера Beautiful Soup: import bs4 html = open("test.html").read() bs = bs4.BeautifulSoup(html, "html.parser")
   520 Глава 22. Получение данных по сети Этого кода вполне достаточно, чтобы преобразовать HTML в объект парсера bs. Объект парсера Beautiful Soup обладает множеством интересных возможностей, и, если вы работаете с HTML, вам определенно стоит немного потестировать его, чтобы разобраться, чем он может вам пригодиться. В этом же примере нас интересуют лишь два аспекта: извлечение содержимого по HTML-тегу и получение данных по атрибуту класса. Начнем с поиска ссылки. HTML-тег для гиперссылки — <a> (Beautiful Soup по умолчанию приводит все теги к нижнему регистру). Для поиска всех тегов ссылок можно вызвать объект bs, передав "a" в качестве параметра: a_list = bs("a") print(a_list) [<a href="http://bitbucket.dev.null">link</a>] Теперь у вас имеется список всех тегов HTML-ссылок (в данном случае тег всего лишь один). Если этот список — все, что вам нужно, уже неплохо, однако на самом деле элементы, возвращаемые списком, также являются объектами парсера и могут самостоятельно справиться с оставшейся работой по извлечению гиперссылки и ее текстового содержимого: a_item = a_list[0] a_item.text 'link' a_item["href"] 'http://bitbucket.dev.null' Другим видом данных, который нас интересует, являются элементы с атрибутом класса "special". Для их извлечения следует воспользоваться методом select объекта парсера, как показано ниже: special_list = bs.select(".special") print(special_list) [<span class="special">this is special</span>] special_item = special_list[0] special_item.text 'this is special' special_item["class"] ['special'] Поскольку элементы, возвращаемые при поиске по тегу или методом select, сами являются экземплярами парсера, их можно использовать вложенно, что позволяет извлекать практически любые данные из HTML или даже XML.
   22.4. Сбор данных с веб-страниц (скрейпинг) 521 Попробуйте сами: парсинг HTML Взяв за основу файл forecast.html (его можно создать в блокноте для данной главы; он также приведен в листинге 22.2), напишите скрипт с использованием библиотеки Beautiful Soup, который извлекает данные и сохраняет их в CSV-файл. Листинг 22.2. Файл forecast.html <html> <body> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Tonight</b></div> <div class="grid col-75 forecast-text">A slight chance of showers and thunderstorms before 10pm. Mostly cloudy, with a low around 66. West southwest wind around 9 mph. Chance of precipitation is 20%. New rainfall amounts between a tenth and quarter of an inch possible.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Friday</b></div> <div class="grid col-75 forecast-text">Partly sunny. High near 77, with temperatures falling to around 75 in the afternoon. Northwest wind 7 to 12 mph, with gusts as high as 18 mph.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Friday Night</b></div> <div class="grid col-75 forecast-text">Mostly cloudy, with a low around 63. North wind 7 to 10 mph.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Saturday</b></div> <div class="grid col-75 forecast-text">Mostly sunny, with a high near 73. North wind around 10 mph.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Saturday Night</b></div> <div class="grid col-75 forecast-text">Partly cloudy, with a low around 63. North wind 5 to 10 mph.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Sunday</b></div> <div class="grid col-75 forecast-text">Mostly sunny, with a high near 73.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Sunday Night</b></div> <div class="grid col-75 forecast-text">Mostly cloudy, with a low around 64.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Monday</b></div> <div class="grid col-75 forecast-text"> Mostly sunny, with a high near 74.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Monday Night</b></div>
   522 Глава 22. Получение данных по сети <div class="grid col-75 forecast-text">Mostly clear, with a low around 65.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Tuesday</b></div> <div class="grid col-75 forecast-text">Sunny, with a high near 75.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Tuesday Night</b></div> <div class="grid col-75 forecast-text">Mostly clear, with a low around 65.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Wednesday</b></div> <div class="grid col-75 forecast-text">Sunny, with a high near 77.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Wednesday Night</b></div> <div class="grid col-75 forecast-text">Mostly clear, with a low around 67.</div> </div> <div class="row row-forecast"> <div class="grid col-25 forecast-label"><b>Thursday</b></div> <div class="grid col-75 forecast-text">A chance of rain showers after 1pm. Mostly sunny, with a high near 81. Chance of precipitation is 30%.</div> </div> </body> </html> 22.5. Практическая работа: сбор метеоданных Задействуйте API, описанный в разделе 22.2, чтобы собрать сводку средних температур для выбранного места (можно взять Чикаго или указать широту и долготу другой географической точки) за месяц. Для обращения к архивному API необходимо чуть изменить URL-адрес "https://archive-api.open-meteo.com/v1/era5?latitude=<latitude>&longitude=<longi tude>&start_date=<YYYY-MM-DD>&end_date=<YYYY-MM-DD>&daily=temperature_2m_mean" следующим образом: "https://archive-api.open-meteo.com/v1/era5?latitude=41.879&longitude=-87.64975 ➥&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean" Стоит отметить, что вы получите два списка: список дат и связанный с ним список средних температур в градусах Цельсия для этих дат. Необходимо преобразовать данные таким образом, чтобы их можно было импортировать в электронную таблицу и визуализировать.
   22.5. Практическая работа: сбор метеоданных 523 22.5.1. Решение задачи при помощи кода, сгенерированного ИИ В этой практической работе нет сложных элементов проектирования. Она лишь требует досконального знания процедуры доступа к API HTTP и обработки возвращаемого JSON. Таким образом, это как раз та задача, с которой ИИ-ассистент должен хорошо справляться. 22.5.2. Решения и обсуждение Данная задача сводится к получению файла с HTTP-сервера и последующей обработке возвращаемого JSON для объединения двух параллельных списков. Программное решение, созданное человеком В данном решении выбран простой подход к задаче с применением библиотеки requests для получения и обработки данных, а затем сохранения обработанных данных в файл: import requests import csv response = requests.get("https://archive-api.open-meteo.com/v1/era5?latitude =41.879&longitude=-87.64975 ➥&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean") weather_history = response.json() Метод json() объекта response weather_data = zip(weather_history["daily"]["time"], weather_history["daily"]["temperature_2m_mean"]) возвращает данные в формате JSON with open("weather_history.csv", "w", newline="") as file: writer = csv.writer(file) writer.writerow(["Date", "Temperature"]) writer.writerows(weather_data) Функция zip() возвращает итератор объединенных списков Метод writerows() записывает все строки из итератора weather_data В нашем решении мы воспользовались тем, что библиотека requests может возвращать данные в формате JSON, что избавляет от необходимости самостоятельно преобразовывать текст в JSON (парсить JSON-строку). Два списка объединяются посредством функции zip(), а полученные данные записываются в файл формата CSV. Следует учитывать, что функция zip() возвращает итератор, а не список. Для метода writerows() это не проблема, но если вы хотите просмотреть результаты, необходимо задействовать функцию list() для приведения итератора к списковому типу. Программные решения, сгенерированные ИИ Промпт, переданный Copilot, существенно упрощен по сравнению с задачей практической работы, а имена полей указаны явно: URL-адрес https://archive-api.open-meteo.com/v1/era5?latitude=41.879&longitude=87.64975&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean
   524 Глава 22. Получение данных по сети вернет ежедневные данные о погоде в Чикаго в формате JSON. Данные будут содержать поле "daily" с двумя списками: "time" и "temperature_2m_mean". Необходимо загрузить данные и сохранить поля "time" и "temperature_2m_ mean" в CSV-файл, пригодный для последующей загрузки в электронную таблицу и визуализации в виде графика. Данный промпт сформулирован таким образом, чтобы избежать путаницы с названиями полей и исключить всю несущественную информацию. Итоговый программный код оказался вполне пригодным: import requests import csv # URL для получения данных о погоде url = "https://archive-api.open-meteo.com/v1/era5?latitude=41.879&longitude=87.64975&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean" # Получение данных response = requests.get(url) data = response.json() # Извлечение соответствующих полей time_data = data['daily']['time'] temperature_data = data['daily']['temperature_2m_mean'] # Имя выходного файла CSV output_file = 'Chicago_weather_July_2024.csv' # Запись данных в файл CSV with open(output_file, 'w', newline='') as csvfile: writer = csv.writer(csvfile) writer.writerow(['Date', 'Temperature (°C)']) # Запись заголовка for time, temperature in zip(time_data, temperature_data): writer.writerow([time, temperature]) Этот код вполне корректен и работает в точности так, как было указано, без каких-либо изменений. Он даже перебирает данные построчно для записи в CSV-файл, тогда как в версии программиста весь массив данных сначала преобразуется в список, который затем записывается одной операцией. В данном случае, когда количество элементов заранее известно, подобная мера по экономии памяти не является действительно необходимой, но приятно было увидеть, что и это учтено. Несколько избыточным в данном контексте выглядит применение функции zip() для объединения двух списков. В моей реализации это оправдано созданием единого списка пар значений, тогда как в данном случае пары формируются непосредственно в процессе записи каждой строки, и данные могли бы считываться напрямую из обоих списков. Тем не менее я бы сказала, что применение zip() делает код чуть более естественным для Python. В остальном он очень похож на мою версию и хорошо работает без каких-либо правок. Colaboratory был предложен аналогичный промпт:
   22.5. Практическая работа: сбор метеоданных 525 URL-адрес https://archive-api.open-meteo.com/v1/era5?latitude=41.879&longitude=87.64975&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean вернет ежедневные данные о погоде в Чикаго в формате JSON. Данные будут содержать поле "daily" с двумя списками: "time" и "temperature_2m_mean". Необходимо загрузить данные и сохранить поля "time" и "temperature_2m_ mean" в CSV-файл, пригодный для последующей загрузки в электронную таблицу и визуализации в виде графика. Этот запрос также хорошо сработал в Colaboratory, хотя ИИ-ассистент предпочел несколько иной подход: import csv import requests def download_and_save_chicago_weather(): """Загружает данные о погоде в Чикаго и сохраняет их в файл CSV.""" url = "https://archive-api.open-meteo.com/v1/era5?latitude=41.879&longitu de=-87.64975 ➥&start_date=2024-07-01&end_date=2024-07-31&daily=temperature_2m_mean" response = requests.get(url) if response.status_code == 200: weather_data = response.json() if "daily" in weather_data: daily_data = weather_data["daily"] if "time" in daily_data and "temperature_2m_mean" in daily_data: with open('chicago_weather_july.csv', 'w', newline='') as outfile: writer = csv.writer(outfile) writer.writerow(['Date', 'Mean Temperature (°C)']) ➥ # Запись строки заголовка for i in range(len(daily_data["time"])): writer.writerow([daily_data["time"][i], daily_ data["temperature_2m_mean"][i]]) print("Chicago weather data for July 2024 saved to ➥ chicago_weather_july.csv") else: print("Error: 'time' or 'temperature_2m_mean' not found ➥in daily data.") else: print("Error: 'daily' field not found in weather data.") else: print("Error downloading data:", response.status_code) download_and_save_chicago_weather() Во-первых, основной функционал представленной программы сосредоточен в функции download_and_save_chicago_weather(), что можно рассматривать как удачный элемент архитектуры. Существенной разницы в способе получения данных не наблюдается, однако реализован достаточно обширный механизм проверки и обработки ошибок. Это полезно, особенно когда подобные проверки генерируются без дополнительных усилий со стороны программиста, хотя и не слишком соответствует канонам Python-стиля. Вместо использования
   526 Глава 22. Получение данных по сети операторов if и функции print было бы более характерно для языка Python применять исключения — в частности, KeyError для случаев, когда в данных отсутствует нужное поле, и, возможно, один из вариантов OSError для обработки сбоев при загрузке данных (хотя, скорее всего, ошибка будет сгенерирована библиотекой requests еще до достижения кодом этого участка). Во-вторых, для объединения двух списков этот код использует не функцию zip(), а цикл for, который, получив длину списков, извлекает дату и среднюю температуру, а затем записывает их напрямую в CSV-файл. Это вполне эффективно, хотя, на мой взгляд, отчасти выбивается из стиля программирования на Python. В итоге при решении сравнительно простой и лаконично сформулированной задачи и Colaboratory, и Copilot сгенерировали программный код, который можно использовать без каких-либо правок. Итоги В языке Python в составе модуля ftplib реализован FTP-клиент, предназначенный для получения данных по FTP. Для получения файлов по SFTP можно воспользоваться библиотекой paramiko. Применение скрипта на языке Python не всегда является оптимальным способом получения файлов. Следует учитывать альтернативные консольные средства, такие как curl и wget. Библиотека requests — лучший кандидат для получения файлов по HTTP/ HTTPS с помощью Python. Получение файлов через API при помощи библиотеки requests очень похоже на получение статических файлов. Параметры API-запросов зачастую необходимо заключать в кавычки (экранировать) и добавлять к URL в виде строки запроса. Наиболее распространенным структурированным форматом для передачи данных является JSON. XML — более сложный формат структурированных данных, который продолжает применяться в современных системах. Посредством такой библиотеки, как Beautiful Soup, можно выполнить сбор данных с веб-сайта (скрейпинг) и осуществить синтаксический разбор (парсинг) HTML. Сбор данных с сайтов (скрейпинг), которыми вы не владеете, может нарушать закон или этические нормы, а также требует осторожности во избежание чрезмерной нагрузки на сервер.
23 Сохранение данных В этой главе: 3 Хранение данных в реляционных базах данных 3 Работа с Python DB-API 3 Доступ к базам данных через объектно-реляционное отображение (ORM) 3 Принципы работы нереляционных баз данных (NoSQL) и их отличия от реляционных баз данных Если у вас имеются данные, прошедшие предварительную обработку, вполне вероятно, что вы захотите сохранить их — и не только сохранить, но и получить к ним доступ в будущем с минимальными усилиями. Для хранения и выборки значительных объемов данных, как правило, применяется система управления базами данных (СУБД) или, выражаясь более привычным языком, база данных (БД). Реляционные базы данных — такие, как PostgreSQL, MySQL и SQL Server, — десятилетиями считались оптимальным выбором для хранения информации благодаря своей надежности и функциональности, да и сейчас во множестве случаев они остаются предпочтительным решением. В последние годы набирают популярность нереляционные (NoSQL) базы данных, включая MongoDB и Redis (а также их многочисленные облачные аналоги), которые оказываются весьма эффективными применительно к целому спектру задач. Подробное описание баз данных могло бы занять десятки томов, поэтому мы рассмотрим
   528 Глава 23. Сохранение данных лишь ряд конкретных вариантов, демонстрирующих работу с реляционными (SQL) и нереляционными (NoSQL) базами данных с помощью языка Python. 23.1. Реляционные базы данных Реляционные базы данных давно уже стали стандартом хранения и обработки данных. Эта технология проверена временем и получила повсеместное распространение. Python может подключаться к широкому спектру реляционных СУБД, однако в рамках данного издания не представляется возможным (и целе­ сообразным) рассматривать специфику работы с каждой из них. Поскольку работа с базами данных в Python во многом унифицирована, я продемонстрирую основные приемы на примере одной из них — sqlite3, — а затем мы обсудим некоторые различия и факторы, которые следует учитывать при выборе и применении реляционной базы данных для хранения информации. 23.1.1. API баз данных (DB-API) в Python Как отмечалось выше, в Python обращения к реляционным БД осуществляются схожим образом в разных реализациях баз данных благодаря спецификации PEP 249 (www.python.org/dev/peps/pep-0249/), где регламентированы стандартные правила подключения к реляционным БД. Этот стандарт, часто обозначаемый термином «API баз данных» («Database API» или DB-API), призван способствовать «созданию кода, обладающего лучшей переносимостью между базами данных и расширенным доступом к ним». Благодаря DB-API примеры использования SQLite, которые встречаются в этой главе, практически идентичны тем, что применяются для PostgreSQL, MySQL или других баз данных. 23.2. SQLite: работа с базой данных sqlite3 Хотя в Python имеются модули для работы со многими базами данных, в приведенных далее примерах мы рассмотрим модуль sqlite3. Пусть он и не предназначен для масштабных приложений с интенсивным трафиком, зато обладает двумя ключевыми преимуществами. Поскольку sqlite3 входит в стандартную библиотеку, его можно использовать в любых случаях, где требуется база данных, без необходимости подключения дополнительных зависимостей. Модуль sqlite3 хранит все свои записи в локальном файле и не нуждается в отдельном сервере, в отличие от PostgreSQL, MySQL и других более крупных баз данных. Благодаря этим особенностям sqlite3 идеально подходит для небольших приложений и быстрого прототипирования.
   23.2. SQLite: работа с базой данных sqlite3 529 Чтобы использовать базу данных sqlite3, вам прежде всего понадобится объект соединения Connection. Для его получения следует вызвать функцию connect с именем файла, который будет использоваться для хранения данных: import sqlite3 conn = sqlite3.connect("datafile.db") Также можно сохранить данные в памяти, указав вместо имени файла строку ":memory:". Для хранения в Python целочисленных, строковых и вещественных типов данных дополнительная настройка не требуется. Если необходимо, чтобы sqlite3 автоматически преобразовывал результаты выборок по отдельным столбцам в соответствующие типы, полезно передать параметру detect_types значение sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES. Это предписывает объекту Connection анализировать имена и типы столбцов в запросах и пытаться сопоставить их с уже определенными вами конвертерами. Второй этап — создание объекта Cursor на основе установленного соединения: cursor = conn.cursor() cursor <sqlite3.Cursor object at 0xb7a12980> На этом этапе вы можете выполнять запросы к базе данных. В рассматриваемой ситуации, ввиду отсутствия в БД каких-либо таблиц или записей, требуется сначала создать таблицу и внести в нее несколько записей: cursor.execute("create table people (id integer primary key, name text, count integer)") cursor.execute("insert into people (name, count) values ('Bob', 1)") cursor.execute("insert into people (name, count) values (?, ?)", ("Jill", 15)) conn.commit() Последний запрос insert демонстрирует предпочтительный способ создания запросов с переменными. Вместо того чтобы строить строку запроса вручную, безопаснее использовать знак вопроса ? в качестве заполнителя для каждой переменной, а затем передать переменные в виде кортежа методу execute. Этот способ хорош тем, что не нужно беспокоиться о неправильном экранировании значений; sqlite3 сделает все за вас. Также в запросе можно задействовать имена переменных с префиксом : и передать соответствующий словарь со значениями, которые нужно вставить: cursor.execute("insert into people (name, count) values (:username, " " :usercount)", {"username": "Joe", "usercount": 10}) <sqlite3.Cursor at 0x7ceda4437ac0> После того как таблица будет заполнена данными, вы сможете запрашивать их при помощи команд SQL, снова используя либо ? для связывания переменных, либо имена и словари:
   530 Глава 23. Сохранение данных result = cursor.execute("select * from people") print(result.fetchall()) [(1, 'Bob', 1), (2, 'Jill', 15), (3, 'Joe', 10)] result = cursor.execute("select * from people where name like :name", {"name": "bob"}) print(result.fetchall()) [(1, 'Bob', 1)] cursor.execute("update people set count=? where name=?", (20, "Jill")) result = cursor.execute("select * from people") print(result.fetchall()) [(1, 'Bob', 1), (2, 'Jill', 20), (3, 'Joe', 10)] В дополнение к методу fetchall метод fetchone позволяет получить одну строку результата, а метод fetchmany возвращает заданное количество записей. Для удобства также можно перебирать (итерировать) строки объекта cursor так же, как и строки файла: result = cursor.execute("select * from people") for row in result: print(row) (1, 'Bob', 1) (2, 'Jill', 20) (3, 'Joe', 10) И наконец, следует помнить о том, что по умолчанию sqlite3 не выполняет немедленную фиксацию транзакций. Этот факт означает, что имеется возможность выполнить откат транзакции в случае сбоя, но одновременно требуется явный вызов метода commit объекта Connection для гарантированного сохранения всех внесенных изменений. Особенно рекомендуется делать это до закрытия соединения с базой данных, поскольку сам по себе метод close не выполняет автоматическую фиксацию активных транзакций: cursor.execute("update people set count=? where name=?", (20, "Jill")) conn.commit() conn.close() В табл. 23.1 приведен обзор наиболее распространенных операций с базой данных sqlite3. Как правило, для управления базой данных sqlite3 этих операций достаточно. Разумеется, существуют различные параметры, позволяющие осуществлять тонкую настройку их поведения; подробную информацию см. в документации по Python.
   23.3. MySQL, PostgreSQL и другие реляционные базы данных 531 Таблица 23.1. Основные операции с базой данных sqlite3 Операция Команда sqlite3 Установка соединения с базой данных conn = sqlite3.connect(filename) Создание курсора для установленного соединения cursor = conn.cursor() Выполнение запроса с помощью курсора cursor.execute(query) Возвращение результатов запроса cursor.fetchall(),cursor.fetchmany(num_rows), cursor.fetchone() for row in cursor: .... Фиксация транзакции в базе данных conn.commit() Закрытие соединения conn.close() Попробуйте сами: создание и изменение таблиц При помощи модуля sqlite3 напишите программный код, создающий таблицу базы данных для метеосводок по штату Иллинойс, загруженных из плоского файла, приведенного в листинге 21.2. Допустим, что в вашем распоряжении имеются аналогичные данные и по другим штатам, а также требуется сохранять расширенную информацию о самих штатах. Каким образом можно изменить структуру базы данных, чтобы для хранения сведений о штатах использовалась связанная таблица? 23.3. MySQL, PostgreSQL и другие реляционные базы данных Как отмечалось ранее в этой главе, несколько других реляционных баз данных располагают клиентскими библиотеками, реализующими DB-API. Следовательно, взаимодействие с такими базами данных в Python осуществляется схожим образом, но есть ряд различий, на которые стоит обратить внимание. В отличие от SQLite, эти базы данных требуют наличия сервера баз данных, к которому подключается клиент, причем этот сервер может располагаться на другом компьютере. Поэтому для установления соединения требуется указание большего количества параметров — как правило, это адрес хоста, имя учетной записи и пароль. Способ подстановки параметров в запросы (к примеру, вместо "select * from test where name like :name" может использоваться другой формат, такой как ?, %s или %(name)s). Эти различия не слишком значительны, но они могут препятствовать полной переносимости кода между различными базами данных.
   532 Глава 23. Сохранение данных 23.4. Оптимизация работы с базами данных при помощи объектно-реляционного отображения (ORM) Существуют определенные трудности, связанные с клиентскими библиотеками баз данных, реализующими DB-API, о которых мы упоминали ранее в этой главе, в частности, с необходимостью написания запросов на чистом SQL. Различные системы управления реляционными базами данных имеют небольшие отличия в реализации языка SQL, вследствие чего идентичные SQL-запросы могут не работать при миграции с одной базы данных на другую — например, при локальной разработке с использованием sqlite3 и последующем развертывании в производственной среде на MySQL или PostgreSQL. Также, как отмечалось выше, методы передачи параметров в SQL-запросы отличаются от реализации к реализации. Второй минус заключается в необходимости применять в коде чистые SQLзапросы. Включение SQL-инструкций непосредственно в программный код усложняет его сопровождение, особенно при большом их количестве. Некоторые из этих запросов будут шаблонными и типичными, другие — сложными и запутанными, при этом все они требуют тщательного тестирования, что может стать обременительным. Необходимость писать SQL-код означает, что вам нужно мыслить как минимум на двух языках: Python и определенном диалекте SQL. В одних случаях применение чистого SQL стоит затраченных усилий, в других — нет. С учетом перечисленных факторов сформировалась потребность в более удобном управлении базами данных на языке Python, которое не требует от разработчика знаний, выходящих за рамки обычного написания кода на Python. Решением данной задачи является объектно-реляционное отображение (ORM — object relational mapper), осуществляющее преобразование (или сопоставление) типов и структур реляционных баз данных в объекты Python. Одними из наиболее распространенных ORM в экосистеме Python, наряду со множеством других, являются Django ORM и SQLAlchemy. Django ORM тесно интегрирован с веб-фреймворком Django и, как правило, не применяется вне его контекста. Поскольку в данной книге не рассматривается фреймворк Django, подробный разбор Django ORM не приводится. Однако стоит отметить, что он является стандартным выбором для приложений на Django, отличным решением с развитым набором инструментов и широкой поддержкой сообщества. 23.4.1. SQLAlchemy SQLAlchemy — еще одна популярная система объектно-реляционного отображения в мире Python. Основная задача SQLAlchemy заключается в автоматизации рутинных операций с базами данных и предоставлении интерфейса к данным на основе объектов Python, с сохранением за разработчиком полного контроля
   23.4. Оптимизация работы с базами данных 533 над базой данных и прямого доступа к чистому SQL-коду. В этом разделе мы рассмотрим базовые примеры записи данных в реляционную базу данных и их последующего извлечения при помощи SQLAlchemy. SQLAlchemy уже предустановлена в Colaboratory, однако в случае использования другой среды выполнения можно установить SQLAlchemy при помощи менеджера пакетов pip: pip install sqlalchemy SQLAlchemy предоставляет несколько способов взаимодействия с базой данных и ее таблицами. Несмотря на то что система ORM при необходимости или вашем желании дает возможность писать SQL-запросы, ее главное преимущество заключается именно в сопоставлении таблиц и столбцов реляционной базы данных с объектами языка Python. С помощью SQLAlchemy можно воспроизвести действия, описанные в разделе 23.2: создать таблицу, добавить три строки, выполнить запрос к таблице и обновить одну строку. Для применения объектно-реляционного отображения необходимо выполнить несколько дополнительных настроек, однако в крупных проектах такие усилия более чем оправданны. Сначала импортируем компоненты, необходимые для установления соединения с базой данных и сопоставления таблицы с объектами Python: from sqlalchemy import (create_engine, select, MetaData, Table, Column, Integer, String) from sqlalchemy.orm import sessionmaker Из базового пакета sqlalchemy нам понадобятся функции create_engine и select, а также классы MetaData и Table. Поскольку при инициализации объекта таблицы необходимо описывать схему, нужно также импортировать класс Column и классы, определяющие тип данных каждого столбца, — в данном случае Integer и String. Нам также потребуется функция sessionmaker из подпакета sqlalchemy.orm. А теперь можно заняться подключением к базе данных: Создает объект ядра взаимоdbPath = 'datafile2.db' действия (engine) с базой данных engine = create_engine('sqlite:///%s' % dbPath) metadata = MetaData() Создает объект таблицы people people = Table('people', metadata, Column('id', Integer, primary_key=True), Column('name', String), Column('count', Integer), ) Создает сессию Session = sessionmaker(bind=engine) session = Session() Создает таблицу в базе данных metadata.create_all(engine) Для установления соединения необходимо создать экземпляр объекта ядра, соответствующий выбранной базе данных. Далее следует инициализировать объект
   534 Глава 23. Сохранение данных MetaData, служащий контейнером для управления таблицами и их схемами. Затем создается объект Table под названием people, в конструкторе которого указывается имя таблицы в базе данных, экземпляр объекта MetaData, а также описание создаваемых столбцов с их типами данных. Наконец, при помощи функции sessionmaker для объекта ядра создается класс Session, который используется для создания экземпляра объекта сессии. На этом этапе соединение с базой данных установлено, и завершающей операцией является вызов метода create_all для создания таблицы. Следующим этапом после создания таблицы станет добавление в нее нескольких записей. И снова SQLAlchemy предоставляет для этого целый спектр возможностей, но мы в данном случае выберем прямолинейный подход. А именно создадим объект insert и выполним его вызовом execute: people_ins = people.insert().values(name='Bob', count=1) str(people_ins) 'INSERT INTO people (name, count) VALUES (?, ?)' session.execute(people_ins) <sqlalchemy.engine.result.ResultProxy object at 0x7f126c6dd438> session.commit() Здесь применяется метод insert() для создания объекта запроса insert, при этом указываются поля и значения, которые в них вставляются. Объект insert обозначен как people_ins; вызов функции str() показывает, что «за кулисами» была сформирована корректная SQL-команда INSERT. После этого метод execute объекта сессии производит операцию вставки, а метод commit обеспечивает фиксацию транзакции в базе данных (помните, что без вызова commit изменения в базе данных не будут сохранены): session.execute(people_ins, [ {'name': 'Jill', 'count':15}, {'name': 'Joe', 'count':10} ]) <sqlalchemy.engine.result.ResultProxy object at 0x7f126c6dd908> Можно было бы ограничиться вставкой одной записи, однако для оптимизации процедуры мы передали список словарей, где указаны имена полей и соответствующие значения для каждой операции вставки, чтобы выполнить несколько вставок одновременно. Если затем выбрать содержимое таблицы people, можно увидеть, что добавлены две новые записи: session.commit() result = session.execute(select(people)) for row in result: print(row)
   23.4. Оптимизация работы с базами данных 535 (1, 'Bob', 1) (2, 'Jill', 15) (3, 'Joe', 10) Операции выборки можно осуществлять проще, применяя метод select() вместе с методом where() для поиска конкретной записи: result = session.execute(select(people).where(people.c.name == 'Jill')) for row in result: print(row) (2, 'Jill', 15) Здесь мы ищем все записи, у которых значение в столбце name совпадает с 'Jill'. Заметьте, что в выражении where используется инструкция people.c.name, где c означает, что name является столбцом таблицы people. Наконец, доступен метод update(), позволяющий отредактировать значение в базе данных: result = session.execute(people.update().values(count=20).where (people.c.name == 'Jill')) session.commit() result = session.execute(select(people).where(people.c.name == 'Jill')) for row in result: print(row) (2, 'Jill', 20) Чтобы выполнить обновление, необходимо задействовать метод update() в связке с методом values(), указывающим значения, которые требуется обновить, и методом where(), задающим конкретную запись, затрагиваемую обновлением. Сопоставление объектов таблиц с классами До сих пор мы работали с объектами таблиц напрямую, однако в SQLAlchemy предусмотрена возможность сопоставить таблицу с классом. Преимущество такого подхода заключается в том, что столбцы базы данных сопоставляются с атрибутами класса; в качестве иллюстрации создадим класс People: from sqlalchemy.orm import declarative_base Base = declarative_base() class People(Base): __tablename__ = "people" id = Column(Integer, primary_key=True) name = Column(String) count = Column(Integer) results = session.query(People).filter_by(name='Jill') for person in results: print(person.id, person.name, person.count) 2 Jill 20
   536 Глава 23. Сохранение данных Вставка может выполняться простым созданием экземпляра сопоставленного класса и его добавлением в сессию: new_person = People(name='Jane', count=5) session.add(new_person) session.commit() results = session.query(People).all() for person in results: print(person.id, person.name, person.count) 1 2 3 4 Bob 1 Jill 20 Joe 10 Jane 5 Процесс обновления данных также относительно прост. Необходимо извлечь нужную запись, изменить значения в сопоставленном экземпляре, а затем добавить обновленную запись в сессию для сохранения изменений в базе данных: jill = session.query(People).filter_by(name='Jill').first() jill.name 'Jill' jill.count = 22 session.add(jill) session.commit() results = session.query(People).all() for person in results: print(person.id, person.name, person.count) 1 2 3 4 Bob 1 Jill 22 Joe 10 Jane 5 Удаление выполняется аналогично обновлению: извлекается нужная запись, после чего для ее исключения из базы данных используется метод delete() объекта сессии: jane = session.query(People).filter_by(name='Jane').first() session.delete(jane) session.commit() jane = session.query(People).filter_by(name='Jane').first() print(jane) None Применение SQLAlchemy требует чуть больше подготовительной настройки по сравнению с непосредственным использованием SQL, тем не менее оно дает ряд существенных преимуществ. В частности, ORM освобождает разработчика от необходимости учитывать тонкие различия в реализации SQL в разных базах данных. Приведенный пример будет одинаково хорошо работать с sqlite3,
   23.4. Оптимизация работы с базами данных 537 MySQL и PostgreSQL без каких-либо изменений в коде, кроме указания строки для create_engine и обеспечения наличия подходящего драйвера базы данных. Другое преимущество заключается в том, что взаимодействие с данными может происходить через объекты Python, что более удобно для разработчиков, не имеющих опыта работы с SQL. Вместо построения SQL-инструкций они могут использовать объекты и методы Python. Попробуйте сами: использование ORM Используя базу данных из предыдущего примера, реализуйте класс SQLAlchemy для сопоставления с таблицей данных. Примените его для чтения записей из этой таблицы. 23.4.2. Изменение схемы базы данных при помощи Alembic В ходе разработки программного обеспечения, взаимодействующего с реляционными базами данных, довольно часто, если не сказать всегда, возникает необходимость изменить структуру (схему) базы данных уже после начала работы. Внезапно обнаруживается, что нужно добавить новые поля или изменить их типы и т. д. Теоретически можно вручную производить изменения как в самих таблицах базы данных, так и в коде ORM, обеспечивающем к ним доступ, однако такой подход сопряжен с рядом недостатков. В частности, бывает трудно откатить такие изменения, а также нелегко отследить, какая именно конфигурация базы данных соответствует конкретной версии вашего кода. Проблема решается средствами миграции баз данных, которые упрощают внесение изменений и их последующее отслеживание. Миграции записываются в виде программного кода. Они должны включать код как для применения необходимых изменений, так и для их отмены. Далее изменения могут отслеживаться, применяться и отменяться в надлежащей последовательности. В результате база данных может быть безопасно возвращена к любому из состояний, в которых она находилась в процессе разработки. В качестве иллюстрации в этом разделе мы кратко рассмотрим Alembic — популярный и компактный инструмент миграции для SQLAlchemy. Прежде всего необходимо открыть окно командной строки в рабочем каталоге проекта, установить Alembic и создать стандартную среду с помощью команды alembic init: ! pip install alembic ! alembic init alembic Этот код создает файловую структуру, необходимую Alembic для миграций данных. Среди сгенерированных файлов присутствует alembic.ini, требующий правки как минимум в одном месте. В частности, необходимо отредактировать параметр sqlalchemy.url, приведя его в соответствие с вашими текущими настройками. В блокноте Colaboratory для этого предусмотрена ячейка, которую
   538 Глава 23. Сохранение данных достаточно выполнить, тогда как в других средах правку следует вносить вручную в текстовом редакторе, найдя следующую строку: sqlalchemy.url = driver://user:pass@localhost/dbname Замените эту строку следующей: sqlalchemy.url = sqlite:///datafile.db Поскольку вы работаете с файлом sqlite локально, имя пользователя или пароль не требуются. Следующим шагом является создание ревизии с помощью команды revision: ! alembic revision -m "create an address table" Generating /home/naomi/qpb_testing/alembic/versions/384ead9efdfd_create_a_ test_address Идентификатор ревизии в имени ➥_table.py ... done файла будет отличаться Этот код создает скрипт ревизии с именем 384ead9efdfd_create_a_test_address_table.py в каталоге alembic/versions. Файл выглядит так: """создание таблицы адресов Идентификатор ревизии в имени файла будет отличаться ID ревизии: 384ead9efdfd Предыдущая ревизия: Дата создания: 2017-07-26 21:03:29.042762 """ from alembic import op import sqlalchemy as sa # идентификаторы ревизии, используемые Alembic revision = '384ead9efdfd' down_revision = None branch_labels = None depends_on = None def upgrade(): pass def downgrade(): Pass Как видите, в заголовке файла указаны идентификатор ревизии и дата. Кроме того, присутствует переменная down_revision, определяющая порядок отката версий. При создании следующей ревизии значение переменной down_revision должно ссылаться на идентификатор текущей. Для осуществления миграции необходимо обновить скрипт ревизии, дополнив его инструкциями: в методе upgrade() — описание процедуры внесения изменений, а в методе downgrade() — процедуры их обратного применения (операция выполняется в отдельной ячейке блокнота):
   23.4. Оптимизация работы с базами данных 539 def upgrade(): op.create_table( 'address', sa.Column('id', sa.Integer, primary_key=True), sa.Column('address', sa.String(50), nullable=False), sa.Column('city', sa.String(50), nullable=False), sa.Column('state', sa.String(20), nullable=False), ) def downgrade(): op.drop_table('address') После написания кода можно применить обновление. Но сначала переключитесь обратно в окно оболочки Python и посмотрите, какие таблицы имеются в базе данных: for table in metadata.sorted_tables: print(table.name) people Как и следовало ожидать, в базе данных всего одна таблица, созданная ранее. Теперь можно выполнить команду Alembic upgrade, чтобы применить обновление и добавить новую таблицу. Перейдите в командную строку и выполните следующее: ! alembic upgrade head INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 384ead9efdfd, ➥create an address table Если вернуться в оболочку Python и выполнить проверку, вы увидите, что в базе данных появились две дополнительные таблицы: for table in metadata.sorted_tables: print(table.name) ['alembic_version', 'people', 'address'] Первая из двух новых таблиц, 'alembic_version', создана Alembic для отслеживания текущей версии базы данных (эти сведения нужны для будущих обновлений и откатов). Вторая новая таблица — 'address' — была добавлена в процессе обновления и готова к работе. Чтобы вернуть базу данных в предыдущее состояние, достаточно выполнить в командной строке команду Alembic downgrade. Передача аргумента -1 указывает Alembic на необходимость понижения версии ровно на один шаг: ! alembic downgrade -1 INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running downgrade 384ead9efdfd -> , create an address table
   540 Глава 23. Сохранение данных Проверка в сессии Python показывает, что все вернулось к исходному состоянию: for table in metadata.sorted_tables: print(table.name) people Разумеется, при желании вы сможете снова запустить обновление командой upgrade и восстановить таблицу, добавить новые ревизии, выполнить обновления и т. д. Попробуйте сами: изменение базы данных с помощью Alembic Поэкспериментируйте с созданием обновления Alembic, которое добавляет в базу данных таблицу state со столбцами для идентификатора, полного названия и сокращенного обозначения штата. Выполните обновление и откат. Какие еще изменения понадобятся, если предполагается использовать таблицу state вместе с уже существующей таблицей данных? 23.5. Нереляционные базы данных (NoSQL) Несмотря на давнюю популярность, реляционные базы данных — отнюдь не единственный способ организации хранения данных. Реляционная модель ставит акцент на нормализации информации в рамках взаимосвязанных таблиц, тогда как другие модели строятся на альтернативных принципах представления данных. Такие базы данных обычно называют нереляционными, или NoSQL-базами, поскольку они, как правило, не придерживаются структуры строк/столбцов/ таблиц, для описания которой и был создан SQL. Нереляционные базы данных обрабатывают данные не как набор строк, столбцов и таблиц, а как пары «ключ-значение», как индексированные документы и даже как графы. Существует множество NoSQL-систем, каждая из которых реализует собственные механизмы организации данных. В отличие от реляционных моделей, такие базы данных, как правило, не следуют строгим принципам нормализации, что зачастую позволяет ускорить и упростить операции выборки. В качестве иллюстрации в данной главе рассматривается применение Python для взаимодействия с двумя наиболее распространенными представителями класса NoSQL — Redis и MongoDB. Здесь мы лишь слегка затронем богатый арсенал средств, предоставляемых нереляционными базами данных и их библио­ теками для Python, однако я попытаюсь дать вам общее представление об этих возможностях. Те из вас, кто уже знаком с Redis или MongoDB, откроют для себя особенности работы их клиентских библиотек в Python, ну а те, кто лишь приступил к освоению нереляционных баз данных, увидят, как функционируют базы данных этого типа. Следует помнить, что Redis и MongoDB представляют собой лишь два примера нереляционных баз данных, которые мы использовали в демонстрационных целях. По мере того как за последние годы распространились облачные сервисы,
   23.6. Хранилища «ключ-значение» на примере Redis 541 появилось множество других решений, каждое из которых обладает своими преимуществами и недостатками. 23.6. Хранилища «ключ-значение» на примере Redis Redis представляет собой систему управления нереляционными базами данных, которая хранит данные в виде пар «ключ-значение» прямо в оперативной памяти. Благодаря этому операции поиска отличаются высокой скоростью; архитектура, ориентированная на сетевой доступ, обеспечивает применимость в широком спектре задач. Redis часто используют для кэширования, в роли брокера сообщений, а также для выполнения быстрых поисковых операций. Название Redis (Remote Dictionary Server — «удаленный сервер-словарь») удачно отражает концептуальную сущность системы: она функционирует подобно словарю языка Python, реализованному в качестве сетевой службы. Следующий пример дает представление о том, как Redis взаимодействует с Python. Если вы знакомы с командной строкой Redis или клиентскими биб­ лиотеками Redis для других языков программирования, эти краткие примеры помогут вам быстро освоить применение Redis в Python. Если Redis для вас в новинку, данный материал раскроет его основные принципы работы; более подробную информацию вы сможете найти на https://redis.io. Существует целый ряд клиентских библиотек Python для Redis; на момент написания книги (по информации с веб-сайта Redis) рекомендуется использовать библиотеку redis-py. Установить ее можно командой pip install redis. Запуск сервера Redis Для учебных экспериментов вам потребуется доступ к работающему серверу Redis. В зависимости от того, какой уровень контроля вам необходим (и сколько усилий вы готовы затратить), у вас есть несколько вариантов. Пожалуй, самый простой способ — воспользоваться облачным сервером Redis, размещенным на платформе Redis с возможностью создания бесплатной учетной записи. Главное преимущество здесь в том, что вам не нужно устанавливать или обслуживать сервер. Бесплатная учетная запись доступна по адресу https://redis.io/ try-free/. Это рекомендуемый вариант для большинства читателей. Если у вас установлен Docker, запуск в нем экземпляра Redis — это тоже простой и быстрый способ получить готовый к работе сервер. Можно запустить экземпляр Redis, выполнив в командной строке инструкцию вида docker run -p 6379:6379 redis. В операционных системах семейства Linux Redis можно установить через системный диспетчер пакетов, тогда как в среде macOS для данной цели следует воспользоваться командой brew install redis. Для платформы Windows рекомендуется обратиться к официальному ресурсу https://redis.io или найти актуальные способы запуска Redis под Windows. После завершения процедуры установки вам может потребоваться ознакомление с доступными в сети руководствами по проверке работоспособности сервера Redis.
   542 Глава 23. Сохранение данных После подключения к серверу вы сможете повторить приведенные ниже примеры простых взаимодействий с Redis из Python. Сначала необходимо импортировать библиотеку Redis и создать объект соединения: import redis r = redis.Redis( # ниже укажите адрес хоста host='', port=10032, # ниже укажите пароль password='') При создании соединения Redis можно указать ряд параметров, включая адрес хоста, порт, а также пароль или ключ SSH. Если сервер работает на localhost с портом по умолчанию 6379, то дополнительные параметры указывать не требуется. После установки соединения можно использовать его для работы с хранилищем «ключ-значение». Первым делом можно воспользоваться методом keys() для получения списка ключей в базе данных. Он возвращает список имеющихся ключей (при их наличии). Затем можно создать несколько ключей разных типов и опробовать различные способы извлечения их значений: r.keys() Пусто, поскольку ключи еще не заданы [] r.set('a_key', 'my value') True r.keys() Возвращает True в случае успешного выполнения операции [b'a_key'] v = r.get('a_key') v b'my value' r.incr('counter') 1 r.get('counter') b'1' r.incr('counter') 2 r.get('counter') b'2'
   23.6. Хранилища «ключ-значение» на примере Redis 543 Приведенные выше примеры демонстрируют, как получить список ключей в базе данных Redis, как присвоить значение ключу, а также как создать ключ со счетчиком (counter) и увеличить его значение. Обратите внимание, что при первом выводе списка ключей возвращается пустой список, поскольку ключи еще не заданы. При установке значения ключа возвращается True в случае успешного выполнения операции. Для целочисленных значений можно воспользоваться методом incr, чтобы увеличить текущее значение ключа на 1 (или вернуть 1, если ключа не существует). Следующие примеры связаны с хранением массивов или списков: r.rpush("words", "one") 1 r.rpush("words", "two") 2 r.lrange("words", 0, -1) [b'one', b'two'] r.rpush("words", "three") 3 r.lrange("words", 0, -1) [b'one', b'two', b'three'] r.llen("words") 3 r.lpush("words", "zero") 4 r.lrange("words", 0, -1) [b'zero', b'one', b'two', b'three'] r.lrange("words", 2, 2) [b'two'] r.lindex("words", 1) b'one' r.lindex("words", 2) b'two' Операция возвращает новую длину массива (списка)
   544 Глава 23. Сохранение данных Изначально ключа "words" нет в базе данных, но операция добавления или помещения значения (в конец, то есть справа — отсюда буква «r» в rpush) создает этот ключ, инициализирует пустой список в качестве его значения, а затем добавляет значение 'one'. Возвращаемое значение — 1, новая длина массива (списка). Повторное применение rpush добавляет следующее слово в конец и возвращает новую длину — 2. Для получения значений из списка можно использовать функцию lrange(), указав ключ, начальный и конечный индексы, где -1 означает конец списка. Обратите внимание, что элементы также могут добавляться в начало списка (слева) вызовом lpush(). Функция lindex() служит для получения отдельного значения по аналогии с lrange(), за исключением того, что ей передается индекс необходимого значения. Одной из ключевых особенностей Redis, обусловливающей его эффективность для задач кэширования, является возможность установить срок действия для пары «ключ-значение». По истечении этого периода ключ и значение автоматически удаляются, что особенно востребовано при использовании Redis в качестве кэша. Срок действия ключа (в секундах) можно задать непосредственно при установке его значения: r.setex("timed", 10, "10 seconds") True r.pttl("timed") 5208 r.pttl("timed") -2 r.pttl("timed") b"timed" in r.keys() False В этом случае срок действия ключа "timed" устанавливается указанием количества секунд (10) в качестве первого параметра после ключа. Затем при использовании метода pttl() оставшееся время отображается в миллисекундах. По истечении срока возвращается значение –2, это означает, что соответствующая пара «ключ-значение» была автоматически удалена из базы данных, и это подтверждается отсутствием b"timed" в результате выполнения r.keys(). Эта функция и возможность точного управления ею, предоставляемая Redis, действительно полезны. Так, реализация простого кэширования может уместиться в минимальном объеме кода.
   23.7. Документы в MongoDB 545 Следует учитывать, что Redis работает в оперативной памяти, что подразумевает непостоянный характер хранения информации: при сбое сервера она может быть частично утрачена. Для снижения риска потери данных Redis предоставляет различные механизмы обеспечения их сохранности — от записи каждого изменения на диск в реальном времени до создания периодических снимков данных (снапшотов) или полного отказа от сохранения на диск. Также можно использовать методы save() и bgsave() клиентской библиотеки Python для принудительного создания снапшота: save() блокирует выполнение до завершения сохранения, а bgsave() выполняет операцию в фоновом режиме. В этой главе затронута лишь небольшая часть возможностей Redis, ее типов данных и способов работы с ними. Если вы захотите узнать больше, в интернете можно найти ряд источников документации, включая https://redislabs.com и https://redis-py.readthedocs.io. Быстрая проверка: сферы применения хранилищ «ключ-значение» Для каких типов данных и приложений хранилище типа «ключ-значение», подобное Redis, принесет наибольшую пользу? 23.7. Документы в MongoDB Другой популярной нереляционной базой данных является MongoDB, которую иногда называют документо-ориентированной БД, поскольку она оперирует не табличными структурами (строками и столбцами), а документами. Архитектура MongoDB обеспечивает горизонтальное масштабирование на множестве узлов в рамках распределенных кластеров с возможностью обработки миллиардов документов. Документы в MongoDB сериализуются в формате BSON (двоичный JSON), представляя собой совокупность пар «ключ-значение» по аналогии с JSON-объектами или словарями Python. Следующие примеры демонстрируют способы применения Python для работы с коллекциями и документами MongoDB, однако следует предупредить: MongoDB оптимальна для задач, требующих масштабируемости, распределенного хранения данных, высокоскоростных операций вставки, работы со сложными и динамическими схемами и т. п. Однако во многих случаях MongoDB — далеко не лучший вариант, поэтому перед окончательным выбором тщательно проанализируйте ваши требования и изучите альтернативные средства. Запуск сервера MongoDB Как и в случае с Redis, если вы хотите опробовать функционал MongoDB, вам потребуется доступ к серверу этой базы данных. Самый простой вариант — создать бесплатную учетную запись и инстанс (экземпляр) на MongoDB.com, перейдя по ссылке https://account.mongodb.com/account/register. После регистрации вы сможете создать бесплатный кластер Mongo Atlas и свободно поэкспериментировать. Этот вариант рекомендуется большинству читателей. Однако если у вас нет особого же-
   546 Глава 23. Сохранение данных лания тестировать MongoDB на практике, достаточно теоретического ознакомления со следующим разделом. Совет: обязательно сохраните имя пользователя, пароль и строку подключения, которые отображаются при создании кластерной конфигурации. Если все ваши операции завершаются с ошибкой превышения времени ожидания, то не исключено, что вам придется зайти в раздел Network Access (Сетевой доступ) в настройках безопасности панели управления и выбрать Allow Access from Anywhere (Разрешить подключение с любых адресов), если простого разрешения для вашего текущего IP-адреса недостаточно для установления соединения. По аналогии с Redis, при желании запустить MongoDB локально проще всего воспользоваться Docker. Если он установлен, выполните в командной строке: > docker run -p 27017:27017 mongo. В операционных системах семейства Linux установка может быть проведена с помощью встроенного диспетчера пакетов. Для macOS достаточно выполнить команду brew install mongodb. Пользователям Windows следует зайти на www.mongodb.com, чтобы скачать версию для Windows и ознакомиться с инструкциями по установке. Как и в случае с Redis, дополнительные сведения по конфигурации и инициализации серверного процесса доступны в интернете. Как и для Redis, в Python представлено несколько клиентских библиотек для подключения к базе данных MongoDB. Чтобы получить представление о том, как они работают, возьмем библиотеку PyMongo. Сначала ее необходимо установить при помощи pip: ! pip install pymongo После установки PyMongo можно подключиться к серверу MongoDB, создав инстанс (экземпляр) MongoClient и указав стандартную информацию для подключения: from pymongo import MongoClient client = MongoClient(host='*** connection string here ***') Используйте строку подключения, полученную при создании инстанса (экземпляра) В ходе создания кластера требовалось задать логин и пароль, а кроме того, вам была предоставлена соответствующая строка подключения. Если вы ее сохранили, можете воспользоваться ею для установки соединения. Чтобы найти строку подключения, выберите кнопку Connect на панели администрирования кластера, затем перейдите в раздел Drivers, убедитесь в том, что в качестве драйвера указан Python, после чего на третьем шаге отобразится строка подключения, содержащая имя пользователя, однако пароль нужно будет ввести вручную. MongoDB организована по принципу базы данных, содержащей коллекции, а каждая коллекция, в свою очередь, может включать документы. При этом создание баз данных и коллекций до обращения к ним не является обязательным. При их отсутствии они автоматически генерируются в ходе первой операции вставки данных. Если же осуществляется попытка выборки из несуществующей коллекции или базы данных, результатом будет пустое множество.
   23.7. Документы в MongoDB 547 Чтобы протестировать клиентскую библиотеку, создайте образец документа — к примеру, в виде словаря Python: import datetime a_document = {'name': 'Jane', 'age': 34, 'interests': ['Python', 'databases', 'statistics'], 'date_added': datetime.datetime.now() } db = client.my_data Выбирает базу данных collection = db.docs (еще не созданную) result = collection.find_one() Выбирает коллекцию в базе данных db.list_collection_names() (также еще не созданную) [] Поиск выполняется по первому элементу; не генерирует исключение, даже если коллекция или база данных еще не существуют Здесь мы подключаемся к базе данных с именем my_data и коллекции документов docs. Хотя их еще нет, они создаются автоматически при первом обращении. За- метьте, что не генерируется никаких исключений, несмотря на то что база данных и коллекция изначально отсутствуют. Однако при запросе списка коллекций мы получаем пустой список, так как в коллекции еще ничего не сохранено. Для сохранения документа используйте метод коллекции insert_one(), который Исполь при успешном выполнении операции возвращает уникальный идентификатор полученную при созда документа (ObjectId): collection.insert_one(a_document) InsertOneResult(ObjectId('66dfb0c090d33ea950f087e1'), acknowledged=True) db.list_collection_names() Уникальный идентификатор документа (ObjectId) ['docs'] Теперь, когда документ был сохранен в коллекции docs, эта коллекция отображается при запросе списка имен коллекций в базе данных. Как только документ размещен в коллекции, можно выполнять с ним операции поиска, обновления, полной замены и удаления: collection.find_one() Возвращает первую запись (документ) {'_id': ObjectId('66dfb0c090d33ea950f087e1'), 'name': 'Jane', 'age': 34, 'interests': ['Python', 'databases', 'statistics'], 'date_added': datetime.datetime(2024, 9, 10, 2, 36, 48, 617000)} Исполь полученную при созда Начнем с извлечения первой записи (документа в MongoDB) методом find_ one():
   548 Глава 23. Сохранение данных from bson.objectid import ObjectId collection.find_one({"_id": ObjectId('66dfb0c090d33ea950f087e1')}) Возвращает запись (документ), соответствующую заданному критерию, — в данном случае ObjectId {'_id': ObjectId('66dfb0c090d33ea950f087e1'), 'name': 'Jane', 'age': 34, 'interests': ['Python', 'databases', 'statistics'], 'date_added': datetime.datetime(2024, 9, 10, 2, 36, 48, 617000)} Запись (документ) также можно получить по ее ObjectId, но для этого нужно сначала импортировать класс ObjectId из модуля bson.objectid. С помощью ObjectId можно также обновить и конкретную запись: Обновляет запись (документ) в соответствии с содержимым объекта $set collection.update_one({"_id":ObjectId('66dfb0c090d33ea950f087e1')}, {"$set": {"name":"Ann"}}) UpdateResult({'n': 1, 'electionId': ObjectId('7fffffff00000000000002b7'), 'opTime': {'ts': Timestamp(1725936314, 21), 't': 695}, 'nModified': 1, 'ok': 1.0, '$clusterTime': {'clusterTime': Timestamp(1725936314, 21), 'signature': {'hash': b'\xd7F\xb8?\xa3a\x87\xd9W\x92\xc4\x17\x7f*\xf8\xbe?\x07\xc2\x07', 'keyId': 7368510527980437523}}, 'operationTime': Timestamp(1725936314, 21), 'updatedExisting': True}, acknowledged=True) collection.find_one({"_id":ObjectId('66dfb0c090d33ea950f087e1')}) {'_id': ObjectId('66dfb0c090d33ea950f087e1'), 'name': 'Ann', 'age': 34, 'interests': ['Python', 'databases', 'statistics'], 'date_added': datetime.datetime(2024, 9, 10, 2, 36, 48, 617000)} При обновлении записи (документа) все ее поля будут изменены на поля переданного объекта. Аналогичным образом можно полностью заменить запись (документ) другим объектом: Заменяет запись (документ) новым объектом collection.replace_one({"_id":ObjectId('66dfb0c090d33ea950f087e1')}, {"name":"Maria"}) UpdateResult({'n': 1, 'electionId': ObjectId('7fffffff00000000000002b7'), 'opTime': {'ts': Timestamp(1725936426, 6), 't': 695}, 'nModified': 1, 'ok': 1.0, '$clusterTime': {'clusterTime': Timestamp(1725936426, 6), 'signature': {'hash': b'=\xef,\x1b\xe5\xd5\xa3`\xf9"\xcb\x1a\xb4X\xec\x96\xa8\xbdJN', 'keyId': 7368510527980437523}}, 'operationTime': Timestamp(1725936426, 6), 'updatedExisting': True}, acknowledged=True) collection.find_one({"_id":ObjectId('66dfb0c090d33ea950f087e1')}) {'_id': ObjectId('66dfb0c090d33ea950f087e1'), 'name': 'Maria'} Предыдущая запись (документ) заменяется более короткой
   23.7. Документы в MongoDB 549 В данном случае осуществляется полная замена исходной записи (документа) новой. Такой подход может быть более эффективным в зависимости от степени сложности объектов. Наконец, мы можем выполнить удаление по идентификатору ObjectId: collection.delete_one({"_id":ObjectId( '66dfb0c090d33ea950f087e1')}) Удаляет запись (документ), соответствующую спецификации (заданным критериям) DeleteResult({'n': 1, 'electionId': ObjectId('7fffffff00000000000002b7'), 'opTime': {'ts': Timestamp(1725936489, 7), 't': 695}, 'ok': 1.0, '$clusterTime': {'clusterTime': Timestamp(1725936489, 7), 'signature': {'hash': b'\xcd\xf3\x18\xdc\xe7\x86\xba\x81\xbdU\\n\xec\xbe\x8e\xc6\xf3\xe8M\xb6', 'keyId': 7368510527980437523}}, 'operationTime': Timestamp(1725936489, 7)}, acknowledged=True) collection.find_one() Коллекция пуста, результат отсутствует Следует отметить, что для поиска соответствий MongoDB выполняет сопоставление на основе словарей, содержащих поля и их значения. Словари также используются для указания операторов, таких как $lt (меньше чем) и $gt (больше чем), а также команд — к примеру, $set для обновления. Другой отличительной особенностью MongoDB является сохранение коллекции в системе после удаления всех документов, то есть коллекция продолжает существовать, пока не будет явно удалена командой drop: db.collection_names() ['docs'] collection.drop() db.collection_names() [] Безусловно, возможности MongoDB этим не ограничиваются. Кроме работы с отдельной записью (документом), существуют также разновидности тех же команд для работы с несколькими записями — такие, как find_many и update_many. К тому же MongoDB поддерживает индексирование для повышения быстродействия и содержит ряд методов для группировки, подсчета и агрегирования данных, наряду со встроенным методом отображения-свертки (map-reduce). Быстрая проверка: способы применения MongoDB Вспомните различные образцы данных, приведенные выше, а также другие наборы данных, с которыми вы имели дело. На ваш взгляд, какие из них хорошо подойдут для хранения в такой базе данных, как MongoDB? А какие будут явно неподходящими — и почему?
   550 Глава 23. Сохранение данных 23.8. Практическая работа: создание базы данных Выберите один из наборов данных, рассматривавшихся в последних главах. Решите, какой тип базы данных лучше подойдет для хранения этих данных. Создайте такую базу данных и напишите код для загрузки в нее данных. Затем выберите два самых распространенных и/или вероятных типа критериев поиска и напишите код для получения как одиночных, так и нескольких соответствую­ щих записей. Это задание с открытым решением, поэтому «официального» ответа к нему нет. Удачи в выполнении! Итоги В Python реализован API баз данных (DB-API) — универсальный интерфейс для клиентских приложений, взаимодействующих с различными реляционными базами данных. SQLite — это файловая реляционная база данных, входящая в стандартную библиотеку Python. PostgreSQL и MySQL — популярные серверные решения для управления базами данных, традиционно применяемые в связке с языком Python. Объектно-реляционное отображение (ORM) позволяет сделать программный код еще более универсальным для разных баз данных. Технология ORM обеспечивает взаимодействие с реляционными базами данных через код и объекты Python (вместо написания чистых SQL-запросов). Такие инструменты, как Alembic, работают в сочетании с ORM и позволяют использовать программный код для внесения обратимых изменений в схемах реляционных баз данных. Хранилища типа «ключ-значение», такие как Redis, представляют собой нереляционные базы данных (NoSQL) и реализуют высокоскоростной доступ к информации в оперативной памяти, не требуя применения языка SQL. MongoDB обеспечивает масштабируемость и использует документо-ориентированную модель, не накладывающую строгих структурных ограничений, характерных для реляционных баз данных.
24 Анализ данных В этой главе: 3 Преимущества Python для работы с данными 3 Применение pandas 3 Агрегирование данных 3 Построение графиков с помощью Matplotlib В предыдущих главах мы рассмотрели некоторые аспекты применения Python для сбора и очистки данных. Теперь пришло время узнать, какими средствами для их обработки и анализа располагает Python. 24.1. Средства Python для анализа данных В этой главе речь пойдет о популярных библиотеках Python, предназначенных для исследования данных в Jupyter: pandas и Matplotlib. Я коснусь лишь некоторых из их возможностей, поскольку моя цель — продемонстрировать потенциал этих библиотек и предоставить начальный набор инструментов для выполнения такого рода задач при помощи Python. 24.1.1. Преимущества Python для анализа данных Python стал одним из ведущих языков в сфере науки о данных, и его популярность в этой области неуклонно растет. И хотя, как отмечалось ранее, Python не
   552 Глава 24. Анализ данных отличается максимальным быстродействием, некоторые библиотеки для работы с данными, такие как NumPy, в основном написаны на C и оптимизированы до такой степени, что скорость исполнения кода уже не является проблемой. Кроме того, такие факторы, как доступность и удобочитаемость кода, нередко оказываются важнее производительности вычислений; на первый план все чаще выходит сокращение временных затрат на разработку. Язык Python отличается исключительной удобочитаемостью и доступностью, а собственные возможности языка в сочетании с инструментами, разработанными в сообществе Python, делают его невероятно мощным средством обработки и анализа данных. 24.1.2. Python может быть эффективнее электронных таблиц На протяжении десятилетий электронные таблицы оставались основным инструментом для оперативного анализа данных. Специалисты, хорошо владеющие таблицами, могут творить с ними настоящие чудеса: объединять разные, но логически связанные наборы данных, строить сводные таблицы (pivot tables), применять таблицы подстановки (lookup tables) для связывания наборов данных и многое другое. И все же, несмотря на их повсеместную распространенность, у электронных таблиц есть свои ограничения, которые можно преодолеть средствами языка Python. Одним из таких ограничений, о котором я уже упоминала, является предельно допустимое количество строк (порядка 1 миллиона) в большинстве программ для работы с электронными таблицами, что недостаточно для многих наборов данных. Другое ограничение кроется в самой концепции таблиц. Электронная таблица представляет собой двумерную матрицу, состоящую из строк и столбцов, или в лучшем случае «стопку» таких матриц, что существенно сужает спектр возможностей для оперирования комплексными данными. При помощи Python разработчик может преодолеть ограничения табличных процессоров и реализовать произвольные операции с данными. Python предоставляет возможность свободного сочетания встроенных структур данных (списков, кортежей, множеств и словарей), а также создания пользовательских классов для инкапсуляции данных и связанного с ними поведения в соответствии с требованиями задачи. 24.2. Python и pandas В процессе исследования и обработки данных приходится выполнять множество типовых операций: загрузку данных в списки или словари, очистку данных, их фильтрацию. Большинство этих действий повторяются, следуют стандартным шаблонам и при всей своей простоте зачастую оказываются весьма трудоемкими и утомительными. Если вам кажется, что это веская причина автоматизировать такие задачи, то вы не одиноки. Один из современных инструментов работы с данными в Python, ставший уже отраслевым стандартом — библиотека
   24.2. Python и pandas 553 pandas — был создан именно в целях автоматизации рутинных операций с на- борами данных. 24.2.1. Почему стоит использовать pandas? Библиотека pandas создавалась для упрощения обработки и анализа табличных или реляционных данных. Она реализует стандартную инфраструктуру для хранения данных и удобные средства для выполнения часто повторяющихся операций. В результате она представляет собой скорее расширение языка Python, нежели просто отдельный модуль, поскольку меняет сам подход к взаимодействию с данными. Когда вы вникнете в логику работы pandas, вы сможете делать поистине впечатляющие вещи и экономить массу времени. Впрочем, для полноценного освоения возможностей библиотеки потребуется время. Как и любой специализированный инструмент, pandas демонстрирует максимальную эффективность при использовании по целевому назначению. Ряд простых примеров, которые я приведу ниже, призван дать общее представление о том, насколько хорошо pandas подойдет для решения ваших задач. 24.2.2. Установка pandas В среде Colaboratory пакеты pandas, matplotlib и numpy предустановлены. При отсутствии библиотеки pandas ее легко установить посредством pip. Поскольку ею нередко пользуются в связке с библиотекой matplotlib для визуализации данных, рекомендуется установить оба инструмента через командную строку виртуальной среды Jupyter следующей командой: > pip install pandas matplotlib В ячейке Jupyter Notebook можно ввести: !pip install pandas matplotlib Если вы работаете с pandas, эти три строки кода значительно упростят вам жизнь: %matplotlib inline import pandas as pd import numpy as np Первая строка — «волшебная» функция Jupyter, позволяющая библиотеке matplotlib визуализировать данные прямо в ячейке, где расположен ваш код (что очень удобно). Вторая строка осуществляет импорт библиотеки pandas с общепринятым псевдонимом pd, что соответствует соглашениям сообщества и оптимизирует процесс написания кода. Последняя строка также импортирует библиотеку numpy, которую pip установит автоматически вместе с pandas. Хотя pandas отчасти зависит от numpy, в следующих примерах мы не будем использовать ее напрямую. Тем не менее рекомендуется выработать привычку ее обязательного импортирования.
554    Глава 24. Анализ данных 24.2.3. Датафреймы Одной из основных структур данных в pandas является датафрейм, или фрейм данных (data frame), который представляет собой двумерную матрицу, весьма схожую с таблицей реляционной базы данных, но хранящуюся в оперативной памяти. Для создания датафрейма достаточно передать ему данные. В качестве простейшего примера рассмотрим создание числовой матрицы размерности 3×3. В Python такая матрица реализуется в виде списка списков: grid = [[1,2,3], [4,5,6], [7,8,9]] print(grid) [[1, 2, 3], [4, 5, 6], [7, 8, 9]] К сожалению, в Python такая матрица не будет выглядеть как структурированная таблица, если не приложить дополнительных усилий. Посмотрим, что можно сделать с той же матрицей, преобразовав ее в датафрейм pandas: import pandas as pd df = pd.DataFrame(grid) print(df) 0 1 2 0 1 4 7 1 2 5 8 2 3 6 9 Этот код достаточно прост. Все, что потребовалось, — преобразовать матрицу в объект DataFrame df. Теперь вывод больше напоминает традиционную таблицу, а у строк и столбцов появилась нумерация. Разумеется, довольно утомительно запоминать, какой номер соответствует какому столбцу, поэтому присвоим столбцам имена: df = pd.DataFrame(grid, columns=["one", "two", "three"] ) print(df) 0 1 2 one 1 4 7 two 2 5 8 three 3 6 9 Польза от именования столбцов не сразу очевидна, однако их названия можно использовать для еще одного удобного приема в pandas — возможности выбора столбцов по имени. Например, если вам нужны данные только из столбца "two", их можно очень легко получить: print(df["two"]) 0 1 2 5
   24.2. Python и pandas 555 2 8 Name: two, dtype: int64 Вот здесь мы уже сэкономили время по сравнению с использованием «чистого» кода Python. Чтобы получить только второй столбец матрицы, пришлось бы использовать генератор списков, не забывая об элементе с индексом 0 (причем вывод все равно не был бы таким наглядным): print([x[1] for x in grid]) [2, 5, 8] Перебирать значения в столбце датафрейма можно так же легко, как и в списке, созданном с помощью генератора: for x in df["two"]: print(x) 2 5 8 Неплохо для начала, но использование списка столбцов в двойных квадратных скобках позволяет добиться большего — мы получаем подмножество датафрейма, которое также является датафреймом. Вместо среднего столбца выберем первый и последний столбцы нашего датафрейма в виде нового объекта: edges = df[["one", "three"]] print(edges) 0 1 2 one 1 4 7 three 3 6 9 У датафрейма также имеется несколько методов, которые применяют одну и ту же операцию с заданным аргументом к каждому своему элементу. К примеру, если необходимо добавить 2 к каждому элементу в датафрейме edges, можно воспользоваться методом add(): print(edges.add(2)) 0 1 2 one 3 6 9 three 5 8 11 Здесь также аналогичного результата можно достичь при помощи генераторов списков и/или вложенных циклов, но эти методы не столь удобны. Нетрудно заметить, как подобная функциональность существенно упрощает работу — особенно тем, кого больше интересует информация, содержащаяся в данных, чем процесс их очистки.
   556 Глава 24. Анализ данных 24.3. Очистка данных В предыдущих главах мы рассмотрели несколько способов применения Python для очистки данных. Теперь, когда в нашем распоряжении появилась библиотека pandas, я покажу на примерах, как использовать ее возможности для этих же целей. При описании следующих операций я буду приводить способы их реализации на «чистом» коде Python, чтобы проиллюстрировать отличия pandas, а также показать, почему эта библиотека подходит не для всех сценариев использования (или не для всех пользователей, если на то пошло). 24.3.1. Загрузка и сохранение данных с помощью pandas Библиотека pandas обладает впечатляющим набором методов для загрузки данных из различных источников. Она поддерживает несколько форматов файлов (включая текстовые файлы с фиксированной шириной полей и разделителями, электронные таблицы, JSON, XML и HTML), а также чтение из баз данных SQL, Google BigQuery, HDF и даже данных из буфера обмена. Следует учитывать, что многие из этих операций фактически не являются частью самой pandas; для их обработки требуются другие установленные библиотеки (например, библиотека SQLAlchemy для работы с базами данных SQL). Это различие важно прежде всего в случае возникновения ошибок: зачастую проблема, которую нужно решить, находится за пределами pandas, и приходится разбираться с работой базовой библиотеки. Файлы JSON легко читаются методом read_json(). Так, файл mars_data_01. json содержит данные о погоде на Марсе за сутки, переданные марсоходом «Opportunity» (к сожалению, уже вышедшим из строя). Для чтения файла можно воспользоваться методом read_json из pandas: mars = pd.read_json("mars_data_01.json") Этот код возвращает датафрейм следующего вида: abs_humidity atmo_opacity ls max_temp max_temp_fahrenheit min_temp min_temp_fahrenheit pressure pressure_string season sol sunrise sunset terrestrial_date wind_direction wind_speed report None Sunny 296 -1 30.2 -72 -97.6 869 Higher Month 10 1576 2017-01-11T12:31:00Z 2017-01-12T00:46:00Z 2017-01-11 -None
   24.3. Очистка данных 557 Чтобы ознакомиться с другим примером, демонстрирующим простоту чтения данных в pandas, загрузим данные файла CSV с температурными показателями из главы 21 (листинг 21.2). Для чтения CSV-файла служит метод read_csv(): Обратите внимание: символ \ в конце строки заголовка указывает на то, что таблица не помещается в одну строку, и оставшиеся столбцы выводятся ниже temp = pd.read_csv("temp_data_01.csv") ➥ 12 4 13 5 6 14 \ 0 1 2 3 4 5 6 7 8 1979/01/01 1979/01/02 1979/01/03 1979/01/04 1979/05/15 1979/05/16 1979/05/17 1979/05/18 1979/05/19 17.48 4.64 11.05 9.51 68.42 70.29 75.34 79.13 74.94 0 1 2 3 4 5 6 7 8 15 NaN NaN NaN NaN NaN NaN 82.4 80.2 81.6 17 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0020 0.3511 0.0785 16 NaN NaN NaN NaN NaN NaN 82.8 83.4 85.2 994 994 994 994 994 994 994 994 994 7 8 9 10 11 6.0 -6.4 -0.7 0.2 61.0 63.4 64.0 75.5 66.9 30.5 15.8 24.7 27.6 75.1 73.5 80.5 82.1 83.1 2.89 -9.03 -2.17 -0.43 51.30 48.09 50.84 55.68 58.59 994 994 994 994 994 994 994 994 994 -13.6 -23.6 -18.3 -16.3 43.3 41.1 44.3 50.0 50.9 15.8 6.6 12.9 16.3 57.0 53.0 55.7 61.1 63.2 NaN NaN NaN NaN NaN NaN 82.60 81.42 82.87 0 0 0 0 0 0 2 349 78 Безусловно, загрузка файла в один этап обладает неоспоримыми преимуществами, и, как видно из примера, библиотека pandas без труда справилась с этой задачей. Также можно заметить, что первый пустой столбец преобразован в NaN («not a number» — «не число»). Тем не менее сохраняется проблема маркировки отсутствующих данных значением 'Missing', и, возможно, имеет смысл преобразовать и эти значения в NaN: temp = pd.read_csv("temp_data_01.csv", na_values=['Missing']) Параметр na_values управляет тем, какие значения будут преобразовываться в NaN при загрузке. В данном случае добавляется строка 'Missing', чтобы преобразовать строку датафрейма из NaN Illinois ➥2.89 994 17 Jan 01, 1979 1979/01/01 17.48 994 6.0 30.5 -13.6 15.8 Missing 0 Missing Missing 0.00% NaN Illinois ➥2.89 994 17 Jan 01, 1979 -13.6 15.8 NaN в 1979/01/01 17.48 994 0 NaN NaN 0.00% 6.0 30.5
   558 Глава 24. Анализ данных Такое преобразование может быть особенно полезным, если в вашем файле данных отсутствующие значения по тем или иным причинам обозначены разными способами: NA, N/A, ?, - и т. п. В таком случае стоит сначала изучить данные, чтобы выявить все встречающиеся обозначения, а затем перезагрузить файл с параметром na_values, чтобы привести все варианты к единому NaN. Сохранение данных Для сохранения содержимого датафрейма объекты DataFrame в pandas поддерживают столь же широкий набор методов. Датафрейм с простой матрицей из нашего примера можно записать несколькими способами. К примеру, команда df.to_csv("df_out.csv", index=False) Установка параметра index в значение False указывает на то, что индексы строк не будут записываться в файл записывает файл, который выглядит примерно так: one,two,three 1,2,3 4,5,6 7,8,9 Аналогичным образом матрицу данных можно преобразовать в объект JSON или записать ее в файл: Передача пути к файлу в качестве аргумента записывает данные JSON в этот файл, а не возвращает его как результат df.to_json() '{"one":{"0":1,"1":4,"2":7},"two":{"0":2,"1":5,"2":8}, "three":{"0":3,"1":6,"2":9}}' 24.3.2. Очистка данных при помощи датафрейма Преобразование определенных значений в NaN при загрузке — одна из многочисленных простых операций по очистке данных, выполнение которой становится тривиальной задачей благодаря библиотеке pandas. Более того, возможности датафрейма этим не ограничиваются: он поддерживает ряд операций, которые могут существенно упростить процедуру очистки данных. Чтобы убедиться в этом, снова откройте CSV-файл с температурными показателями, однако на этот раз вместо заголовков для столбцов используйте функцию range() с параметром names, чтобы присвоить столбцам числовые обозначения — это упростит обращение к ним. Из предыдущих упражнений с этим файлом вы помните, что первое поле каждой строки — поле "Notes" — не содержит данных и загружается значениями NaN. Хотя этот столбец можно игнорировать, было бы проще, если бы его вообще не было. С этой целью можно снова прибегнуть к функции range(), но на этот раз начать с 1, указав тем самым pandas загружать все столбцы, кроме первого. Если же вам точно известно, что все значения относятся к штату Иллинойс, а поле с расширенным форматом даты не представляет для вас ценности, можно начать загрузку информации из файла со столбца номер 4, что значительно повысит управляемость данными:
   24.3. Очистка данных 559 Установка параметра header=0 отключает чтение заголовка в качестве названий столбцов temp = pd.read_csv("temp_data_01.csv", na_values=['Missing'], header=0, names=range(18), usecols=range(4,18)) print(temp) 0 1 2 3 4 5 6 7 8 4 1979/01/01 1979/01/02 1979/01/03 1979/01/04 1979/05/15 1979/05/16 1979/05/17 1979/05/18 1979/05/19 5 17.48 4.64 11.05 9.51 68.42 70.29 75.34 79.13 74.94 0 1 2 3 4 5 6 7 8 15 NaN NaN NaN NaN NaN NaN 82.4 80.2 81.6 17 0.00% 0.00% 0.00% 0.00% 0.00% 0.00% 0.20% 35.11% 7.85% 16 NaN NaN NaN NaN NaN NaN 82.8 83.4 85.2 6 994 994 994 994 994 994 994 994 994 7 6.0 -6.4 -0.7 0.2 61.0 63.4 64.0 75.5 66.9 8 30.5 15.8 24.7 27.6 75.1 73.5 80.5 82.1 83.1 9 2.89 -9.03 -2.17 -0.43 51.30 48.09 50.84 55.68 58.59 10 994 994 994 994 994 994 994 994 994 11 -13.6 -23.6 -18.3 -16.3 43.3 41.1 44.3 50.0 50.9 12 15.8 6.6 12.9 16.3 57.0 53.0 55.7 61.1 63.2 13 NaN NaN NaN NaN NaN NaN 82.60 81.42 82.87 14 0 0 0 0 0 0 2 349 78 Теперь датафрейм состоит только из тех столбцов, с которыми мы собираемся работать. Тем не менее сохраняется проблема обработки последнего столбца, отображающего процент индекса жары: его значения по-прежнему представляют собой строки с символом "%", а не числовые значения. Это становится очевидным, если посмотреть на значение в 17-м столбце первой строки: temp[17][0] '0.00%' Чтобы исправить эту проблему, необходимо сделать две вещи: удалить символ % в конце значения и привести значение из строки к числу. При желании, если вы хотите представить полученный процент в виде десятичной дроби, его следует разделить на 100. Первый шаг реализуется просто — pandas позволяет использовать одну команду, чтобы применить операцию сразу ко всему столбцу: temp[17] = temp[17].str.strip("%") temp[17][0] '0.00' Этот код берет столбец и вызывает для него строковый метод strip(), чтобы удалить замыкающий символ %. Если теперь взглянуть на первое (или любое другое) значение в столбце, вы увидите, что лишний знак процента пропал. Стоит
   560 Глава 24. Анализ данных также отметить, что для достижения того же результата можно использовать и другие операции — например, replace("%", ""). Вторая операция — преобразование строки в числовое значение. И снова pandas позволяет выполнить эту операцию всего одной командой: temp[17] = pd.to_numeric(temp[17]) temp[17][0] 0.0 Теперь значения столбца 17 стали числовыми, и при желании можно воспользоваться методом div(), чтобы окончательно преобразовать их в стандартные дроби с обычным количеством знаков после запятой: temp[17] = temp[17].div(100) temp[17] 0 0.0000 1 0.0000 2 0.0000 3 0.0000 4 0.0000 5 0.0000 6 0.0020 7 0.3511 8 0.0785 Name: 17, dtype: float64 Более того, такого же результата можно добиться с помощью всего одной строки кода, объединив все три операции в цепочку вызовов: temp[17] = pd.to_numeric(temp[17].str.strip("%")).div(100) При всей простоте данного примера он дает наглядное представление о том, насколько удобно применять pandas для очистки данных. Библиотека располагает обширным набором операций для преобразования данных, а также возможностью использовать пользовательские функции, поэтому сложно представить ситуацию, когда pandas не поможет оптимизировать процедуру их очистки. И хотя арсенал возможностей pandas может поначалу показаться просто ошеломляющим, официальная документация к библиотеке, размещенная по адресу http://pandas.pydata.org, содержит множество отличных руководств и видеоуроков, которые помогут освоить ее богатый функционал. Попробуйте сами: очистка данных с помощью pandas и без Поэкспериментируйте с методами и операциями, упомянутыми выше. После того как последний столбец приведен к виду десятичной дроби, как бы вы преобразовали его обратно в строку с замыкающим символом %? Для сравнения загрузите те же данные в простой список Python при помощи модуля csv и примените те же модификации в «чистом» коде Python.
   24.4. Агрегирование и обработка данных 561 24.4. Агрегирование и обработка данных Приведенные выше примеры демонстрируют широкий спектр возможностей pandas по выполнению комплексных операций над данными с применением минимального количества команд. Как и следовало ожидать, не менее развитый функционал предусмотрен и для операций агрегирования данных. В этом разделе мы рассмотрим ряд простых примеров, иллюстрирующих лишь некоторые из множества доступных здесь возможностей. Учитывая их обширный выбор, основное внимание уделено процедурам слияния датафреймов, простому агрегированию данных, а также методам группировки и фильтрации. 24.4.1. Слияние датафреймов В процессе обработки данных довольно часто возникает необходимость связать два набора данных. Допустим, один файл содержит количество звонков, совершенных сотрудниками отдела продаж за месяц, а в другом хранятся денежные объемы продаж по территориям, закрепленным за каждым из них: calls = pd.read_csv("sales_calls.csv") print(calls) 0 1 2 3 4 5 6 7 8 9 10 11 Team member Jorge Jorge Jorge Jorge Ana Ana Ana Ana Ali Ali Ali Ali Territory 3 3 3 3 1 1 1 1 2 2 2 2 Month 1 2 3 4 1 2 3 4 1 2 3 4 Calls 107 88 84 113 91 129 96 128 120 85 87 87 revenue = pd.read_csv("sales_revenue.csv") print(revenue) 0 1 2 3 4 5 6 7 8 9 10 11 Territory 1 1 1 1 2 2 2 2 3 3 3 3 Month 1 2 3 4 1 2 3 4 1 2 3 4 Amount 54228 61640 43491 52173 36061 44957 35058 33855 50876 57682 53689 49173
   562 Глава 24. Анализ данных Вне всякого сомнения, крайне полезно увязать выручку с деятельностью сотрудников. Однако, несмотря на простоту структуры обоих файлов, их слияние средствами «чистого» кода Python — задача далеко не тривиальная. При этом в библиотеке pandas для нее предусмотрена специальная функция, позволяющая произвести слияние двух датафреймов: calls_revenue = pd.merge(calls, revenue, on=['Territory', 'Month']) Функция merge создает новый датафрейм, объединяя два исходных датафрейма по столбцам, указанным в соответствующем параметре. Она работает аналогично операции JOIN в реляционных базах данных, формируя таблицу, сочетающую в себе столбцы из обоих файлов: print(calls_revenue) Team member Territory 0 Jorge 3 1 Jorge 3 2 Jorge 3 3 Jorge 3 4 Ana 1 5 Ana 1 6 Ana 1 7 Ana 1 8 Ali 2 9 Ali 2 10 Ali 2 11 Ali 2 Month 1 2 3 4 1 2 3 4 1 2 3 4 Calls 107 88 84 113 91 129 96 128 120 85 87 87 Amount 50876 57682 53689 49173 54228 61640 43491 52173 36061 44957 35058 33855 В данном примере между строками двух наборов данных имеется однозначное соответствие («один к одному»), однако функция merge также поддерживает слияния типа «один ко многим» и «многие ко многим», а также возможность выполнения правосторонних и левосторонних соединений. Быстрая проверка: слияние наборов данных Как бы вы реализовали слияние этих двух наборов данных средствами «чистого» кода Python? 24.4.2. Выборка данных Не менее полезной является возможность отбирать (или фильтровать) строки в датафрейме по некоторому условию. Допустим, в примере с данными о продажах вы хотите получить сведения только для территории 3, что легко осуществить: print(calls_revenue[calls_revenue.Territory==3]) 0 1 2 3 Team member Jorge Jorge Jorge Jorge Territory 3 3 3 3 Month 1 2 3 4 Calls 107 88 84 113 Amount 50876 57682 53689 49173
   24.4. Агрегирование и обработка данных 563 Здесь выбираются лишь те строки, у которых код территории равен 3, при этом само выражение revenue.Territory==3 используется в качестве индекса (индексатора) датафрейма. С точки зрения «чистого» кода Python такой синтаксис не имеет смысла и недопустим, но для датафреймов pandas он работает, обеспечивая куда более компактную запись. Разумеется, возможны и более сложные выражения. Скажем, если необходимо выбрать только те строки, у которых объем продаж на один звонок превышает 500, можно применить следующее выражение: print(calls_revenue[calls_revenue.Amount/calls_revenue.Calls>500]) Team member Jorge Jorge Ana Ali 1 2 4 9 Territory 3 3 1 2 Month 2 3 1 2 Calls 88 84 91 85 Amount 57682 53689 54228 44957 Что еще лучше, можно рассчитать этот столбец и добавить его в датафрейм с помощью аналогичной операции: calls_revenue['Call_Amount'] = calls_revenue.Amount/calls_revenue.Calls print(calls_revenue) 0 1 2 3 4 5 6 7 8 9 10 11 Team member Jorge Jorge Jorge Jorge Ana Ana Ana Ana Ali Ali Ali Ali Territory 3 3 3 3 1 1 1 1 2 2 2 2 Month 1 2 3 4 1 2 3 4 1 2 3 4 Calls 107 88 84 113 91 129 96 128 120 85 87 87 Amount 50876 57682 53689 49173 54228 61640 43491 52173 36061 44957 35058 33855 Call_Amount 475.476636 655.477273 639.154762 435.159292 595.912088 477.829457 453.031250 407.601562 300.508333 528.905882 402.965517 389.137931 И снова встроенная логика pandas заменяет более громоздкую структуру «чистого» кода Python. Быстрая проверка: выборка в Python Какую структуру кода Python вы бы использовали для выбора строк, удовлетворяю­ щих определенным условиям? 24.4.3. Группировка и агрегирование Как вы могли ожидать, библиотека pandas также содержит разнообразные инструменты для статистической сводки и агрегирования данных. В частности, вычисление суммы, среднего значения, медианы, минимума и максимума по
   564 Глава 24. Анализ данных столбцу осуществляется при помощи методов с четкими, интуитивно понятными именами: print(calls_revenue.Calls.sum()) print(calls_revenue.Calls.mean()) print(calls_revenue.Calls.median()) print(calls_revenue.Calls.max()) print(calls_revenue.Calls.min()) 1215 101.25 93.5 129 84 Если, к примеру, необходимо получить все строки, для которых объем продаж на один звонок превышает медиану, объедините этот прием с операцией выборки: print(calls_revenue.Call_Amount.median()) print(calls_revenue[calls_revenue.Call_Amount >= calls_revenue.Call_Amount.median()]) 464.2539427570093 Team member Territory 0 Jorge 3 1 Jorge 3 2 Jorge 3 4 Ana 1 5 Ana 1 9 Ali 2 Month 1 2 3 1 2 2 Calls 107 88 84 91 129 85 Amount 50876 57682 53689 54228 61640 44957 Call_Amount 475.476636 655.477273 639.154762 595.912088 477.829457 528.905882 Кроме возможности вычисления сводных значений, нередко возникает задача сгруппировать данные по содержимому других столбцов. В этом простом примере для группировки данных можно воспользоваться методом groupby. Скажем, вы хотите узнать суммарное количество звонков и объемы продаж по месяцам или территориям. В таком случае используйте эти поля в методе датафрейма groupby: print(calls_revenue[['Month', 'Calls', 'Amount']].groupby(['Month']).sum()) Month 1 2 3 4 Calls Amount 318 302 267 328 141165 164279 132238 135201 print(calls_revenue[['Territory', 'Calls', 'Amount']].groupby(['Territory']). sum()) Territory 1 2 3 Calls Amount 444 379 392 211532 149931 211420
   24.5. Построение графика данных 565 В обоих случаях вы выбираете столбцы, по которым производится агрегирование, группируете их по значениям одного из столбцов и (в данном примере) суммируете значения для каждой группы. Аналогично можно воспользоваться любыми другими методами, упоминавшимися ранее в главе. Повторю, все эти примеры достаточно просты и призваны наглядно продемонстрировать ряд примечательных возможностей pandas в части обработки и выборки данных. Если представленная концепция соответствует вашим ожиданиям и потребностям, рекомендую обратиться к официальной документации библио­ теки для более глубокого изучения ее функционала: https://pandas.pydata.org/. Попробуйте сами: группировка и агрегирование Поэкспериментируйте с pandas и данными из предыдущих примеров. Сможете ли вы получить информацию о звонках и объемах продаж, сгруппированную по участникам команды и по месяцам? 24.5. Построение графика данных Еще одной впечатляющей возможностью, предоставляемой библиотекой pandas, является непосредственная визуализация данных, содержащихся в структуре датафрейма. Хотя Python и среда Jupyter Notebook реализуют массу инструментов для построения графиков и диаграмм, pandas позволяет использовать matplotlib напрямую из датафрейма. (Библиотека matplotlib обладает богатым набором функций для графического представления данных, заслуживающим отдельного изучения, однако в данном контексте мы ограничимся рассмотрением базовых примеров. Тем, кого интересует визуализация данных, следует обратить пристальное внимание на matplotlib.) Как вы помните, при инициализации сессии Jupyter одной из первых вводимых команд была «магическая» команда активации встроенного режима построения графиков matplotlib: %matplotlib inline Благодаря интегрированным средствам визуализации нам не составит труда построить график для нашего примера (рис. 24.1). Так, для отображения суммарных квартальных значений продаж по территориям к цепочке команд достаточно добавить метод .plot.bar(), который сгенерирует столбчатую диаграмму непосредственно в блокноте: calls_revenue[['Territory', 'Calls']].groupby(['Territory']).sum().plot.bar() Возможны и другие варианты. Метод plot() сам по себе или в виде .plot.line() строит линейный график, .plot.pie() создает круговую диаграмму и т. д. При помощи pandas и matplotlib можно легко строить диаграммы и графики в Jupyter Notebook. Однако отмечу, что при всей простоте многие задачи визуа­ лизации данных решаются в этой связке далеко не идеально.
   566 Глава 24. Анализ данных Рис. 24.1. Столбчатая диаграмма датафрейма pandas в Jupyter Notebook Попробуйте сами: построение графика Постройте линейный график среднего ежемесячного объема продаж на один звонок. 24.6. Когда не стоит использовать pandas Приведенные примеры демонстрируют лишь малую толику возможностей, предоставляемых библиотекой pandas в области очистки, исследования и обработки данных. Как упоминалось в начале этой главы, pandas — превосходный инструмент, отлично справляющийся с теми задачами, для которых он проектировался. Впрочем, это не означает, что он является идеальным решением для всех пользователей и во всех ситуациях. Существуют объективные причины, по которым можно предпочесть стандартный Python (или альтернативные библиотеки). Во-первых, как я уже говорила, полноценное освоение pandas сопоставимо с изучением нового языка, для чего у вас может не найтись ни времени, ни особого желания. Кроме того, pandas не всегда является оптимальным решением для промышленного применения, в частности, при обработке очень больших наборов данных с минимальной потребностью в математических операциях или данных, плохо поддающихся преобразованию в форматы, совместимые с pandas. Так, обработка огромных массивов информации о товарах вряд ли сильно выиграет от применения pandas; то же самое относится и к базовой обработке транзакционных потоков. Вывод: инструменты следует выбирать обдуманно, в зависимости от конкретных задач. Во многих случаях pandas действительно упростит работу с данными, но есть и такие ситуации, для которых лучше подойдет обычный код Python.
   Итоги 567 Итоги Python обладает множеством преимуществ для работы с данными, включая возможность обработки больших массивов информации и высокую гибкость при оперировании данными в соответствии с поставленными задачами. Позволяя фильтровать, группировать и выполнять различные операции с элементами данных, Python зачастую предоставляет более мощные и гибкие средства управления данными, чем электронные таблицы. Библиотека pandas представляет собой инструментарий, существенно облегчающий выполнение типовых операций обработки данных. Для управления данными pandas использует структуру под названием «дата­ фрейм». Библиотека pandas располагает методами для загрузки и сохранения данных из файлов различных распространенных форматов, включая CSV и JSON. Операции над столбцами в pandas существенно облегчают процедуру очистки данных, поскольку применяются ко всем записям датафрейма в рамках соответствующего столбца. Метод merge библиотеки pandas позволяет выполнять слияние датафреймов по общему полю. Интеграция pandas с библиотекой matplotlib существенно облегчает процесс визуализации данных, обеспечивая возможность построения графиков с помощью единой команды plot. Несмотря на многочисленные преимущества, библиотека pandas не всегда является оптимальным выбором, особенно для работы с крупными массивами данных, не адаптированными для ее структур или требующими обработки, близкой к реальному времени.
Практический пример В данном примере мы шаг за шагом рассмотрим, как при помощи Python осуществить получение данных, их очистку и визуализацию. Быть может, проект этот и невелик по объему, однако он объединяет целый ряд возможностей языка, обсуждавшихся в этой книге, и дает возможность проследить за циклом реализации проекта от начала и до конца. Практически на каждом этапе я кратко упоминаю альтернативные подходы и возможные улучшения, которые можно внести. Глобальное изменение климата является предметом многочисленных дискуссий, правда, дискуссии эти основываются на анализе данных в планетарном масштабе. Предположим, существует необходимость определения температурной динамики в конкретной локации. Одним из методов исследования являются получение хронологических данных для заданной географической точки, их обработка и последующая визуализация с целью точного установления наблюдаемых тенденций. Получение кода практического примера Как и программы в других главах, данный практический пример выполнен при помощи Jupyter Notebook в среде Colaboratory. Блокнот, содержащий данный текст и программный код, можно найти в репозитории исходного кода по адресу https:// github.com/nceder/qpb4e в файле с именем Case_Study.ipynb, размещенном в каталоге code/Case Study. Код можно выполнить в стандартной оболочке Python; адаптированная под нее версия хранится в репозитории под именем Case Study.py. Однако обратите внимание, что перед запуском кода вам потребуется установить пакеты requests, pandas и matplotlib. К счастью, в интернете имеются бесплатные источники архивных метеоданных. Мы воспользуемся информацией из Глобальной сети исторической климатологии (Global Historical Climatology Network), которая содержит данные со всего мира. Вы также можете найти другие источники с данными в других форматах, но описанные здесь основные этапы и процессы применимы к любому набору данных.
   Загрузка данных 569 Загрузка данных Первым делом необходимо получить данные. Архив ежедневных исторических метеосводок, доступный по адресу https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/, представляет собой огромный массив данных. Прежде всего нужно разобраться, какие файлы нам потребуются и где конкретно они хранятся; затем можно переходить к скачиванию. Получив данные, можно переходить к их обработке, а затем к отображению результатов. Для загрузки файлов, доступных через HTTPS, нам понадобится библиотека requests, которая уже предустановлена в Colaboratory. Если вы работаете в другой среде, то можете установить requests, выполнив pip install requests в командной строке. После инсталляции requests скачайте файл readme.txt, содержащий информацию о форматах и размещении необходимых нам файлов данных: # импорт библиотеки requests import requests # получение файла readme.txt r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/readme.txt') readme = r.text Открыв файл readme, вы увидите примерно следующее: print(readme) README FILE FOR DAILY GLOBAL HISTORICAL CLIMATOLOGY NETWORK (GHCN-DAILY) Version 3.22 --------------------------------------------------------------------------How to cite: Note that the GHCN-Daily dataset itself now has a DOI (Digital Object Identifier) so it may be relevant to cite both the methods/overview journal article as ➥well as the specific version of the dataset used. The journal article describing GHCN-Daily is: Menne, M.J., I. Durre, R.S. Vose, B.E. Gleason, and T.G. Houston, 2012: An ➥overview of the Global Historical Climatology Network-Daily Database. Journal of ➥Atmospheric and Oceanic Technology, 29, 897-910, doi:10.1175/JTECH-D-11-00103.1. To acknowledge the specific version of the dataset used, please cite: Menne, M.J., I. Durre, B. Korzeniewski, S. McNeal, K. Thomas, X. Yin, S. ➥Anthony, R. Ray, R.S. Vose, B.E.Gleason, and T.G. Houston, 2012: Global Historical ➥Climatology Network Daily (GHCN-Daily), Version 3. [indicate subset used following decimal, e.g. Version 3.12]. NOAA National Climatic Data Center. http://doi.org/10.7289/V5D21VHZ [access ➥date].
   570 Практический пример В частности, нас интересует раздел II с перечнем содержимого1: II. СОДЕРЖАНИЕ ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/daily all: gsn: hcn: by_year: grid: papers: figures: Каталог с файлами ".dly" для всех GHCN-Daily Каталог с файлами ".dly" для сети GCOS Surface Network (GSN) Каталог с файлами ".dly" для HCN США Каталог с файлами GHCN Daily, разобранными на годичные подмножества со временем проведения наблюдений, где оно доступно. См. файлы /by_year/readme.txt и /by_year/ghcn-daily-by_year-format.rtf для получения дополнительной информации Каталог с сеточным набором данных GHCN-Daily, известным как HadGHCND Каталог с pdf-версиями журнальных статей, относящихся к набору данных GHCN-Daily Каталог, содержащий иллюстрации (графики), обобщающие инвентарные данные станций GHCN-Daily ghcnd-all.tar.gz: ghcnd-gsn.tar.gz: ghcnd-hcn.tar.gz: TAR-архив GZIP-сжатых файлов в каталоге "all" TAR-архив GZIP-сжатого каталога "gsn" TAR-архив GZIP-сжатого каталога "hcn" ghcnd-countries.txt: ghcnd-inventory.txt: ghcnd-version.txt: Список кодов стран (FIPS) и их названий Файл, содержащий список периодов наблюдений (инвентаризаций) для каждой станции и каждого элемента (параметра) Список станций и их метаданных (например, координаты) Список кодов штатов США и провинций Канады, используемых в ghcnd-stations.txt Файл, в котором указана текущая версия GHCN Daily readme.txt: status.txt: Данный файл Заметки о текущем статусе GHCN-Daily ghcnd-stations.txt: ghcnd-states.txt: Просматривая список доступных файлов, вы видите, что в файле ghcndinventory.txt приведен список периодов наблюдений для каждой станции, что поможет вам найти подходящий набор данных, а в файле ghcnd-stations. txt перечислены сами станции, что должно помочь найти станцию, ближайшую к вашему местоположению. С этих двух файлов и следует начать: II. СОДЕРЖАНИЕ ftp://ftp.ncdc.noaa.gov/pub/data/ghcn/daily all: gsn: hcn: by_year: 1 Каталог с файлами ".dly" для всех GHCN-Daily Каталог с файлами ".dly" для Сети поверхностных наблюдений GCOS (GSN) Каталог с файлами ".dly" для сети HCN США Каталог с файлами GHCN Daily, разобранными на годичные подмножества со временем проведения наблюдений, где оно доступно. См. файлы Здесь и далее фрагменты технической документации приводятся в переводе на русский язык. — Примеч. пер.
   Загрузка данных grid: papers: figures: /by_year/readme.txt и /by_year/ghcn-daily-by_year-format.rtf для получения дополнительной информации Каталог с набором данных GHCN-Daily с разметкой, известным как HadGHCND Каталог с pdf-версиями журнальных статей, относящихся к набору данных GHCN-Daily Каталог, содержащий иллюстрации (графики), обобщающие инвентарные данные станций GHCN-Daily ghcnd-all.tar.gz: ghcnd-gsn.tar.gz: ghcnd-hcn.tar.gz: TAR-архив GZIP-сжатых файлов в каталоге "all" TAR-архив GZIP-сжатого каталога "gsn" TAR-архив GZIP-сжатого каталога "hcn" ghcnd-countries.txt: ghcnd-inventory.txt: ghcnd-stations.txt: ghcnd-states.txt: ghcnd-version.txt: readme.txt: status.txt: 571 Список кодов стран (FIPS) и их названий Файл, содержащий список периодов регистрации данных для каждой станции и каждого элемента (параметра) Список станций и их метаданных (например, координаты) Список кодов штатов США и провинций Канады, используемых в ghcnd-stations.txt Файл, в котором указана текущая версия GHCN Daily Данный файл Заметки о текущем статусе GHCN-Daily # Получение файлов периодов наблюдений (инвентаризаций) и списка станций r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd ➥inventory.txt') inventory_txt = r.text r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/ghcnd ➥stations.txt') stations_txt = r.text Полученные файлы можно сохранить на локальном диске, чтобы их не пришлось загружать заново, если вам потребуется вернуться к исходным данным: # сохранение файлов инвентаризации и станций на диск для последующего использования with open("inventory.txt", "w") as inventory_file: inventory_file.write(inventory_txt) with open("stations.txt", "w") as stations_file: stations_file.write(stations_txt) Начнем с файла инвентаризации (периодов наблюдений). Вот как выглядят первые 137 символов: print(inventory_txt[:137]) ACW00011604 17.1167 -61.7833 TMAX 1949 1949 ACW00011604 17.1167 -61.7833 TMIN 1949 1949 ACW00011604 17.1167 -61.7833 PRCP 1949 1949 Из раздела VII файла readme.txt видно, что файл инвентаризации имеет следующий формат:
572    Практический пример VII. ФОРМАТ "ghcnd-inventory.txt" -----------------------------Переменная Столбцы Тип -----------------------------ID 1-11 Строковый LATITUDE 13-20 Вещественный LONGITUDE 22-30 Вещественный ELEMENT 32-35 Строковый FIRSTYEAR 37-40 Целочисленный LASTYEAR 42-45 Целочисленный ------------------------------ Эти переменные имеют следующие определения: ID Идентификационный код станции. Полный список станций и их метаданные см. в файле ghcnd-stations.txt. LATITUDE Широта расположения станции (в десятичных градусах). LONGITUDE Долгота расположения станции (в десятичных градусах). ELEMENT Тип элемента. Определения элементов см. в разделе III. FIRSTYEAR Первый год данных, не отмеченных флагами, для данного элемента. LASTYEAR Последний год данных, не отмеченных флагами, для данного элемента. Из этого описания можно сделать вывод, что список периодов наблюдений содержит почти всю необходимую информацию для поиска интересующей вас станции. Ближайшие станции можно найти по широте и долготе, а затем воспользоваться полями FIRSTYEAR и LASTYEAR для нахождения станции с записями, охватывающими продолжительный период времени. Остается один вопрос: что содержится в поле ELEMENT? Файл рекомендует обратиться к разделу III. В разделе III (который мы подробно рассмотрим ниже) приведено следующее описание основных элементов: ELEMENT — это тип элемента. Существует пять основных элементов, а также ряд дополнительных. Пять основных элементов: PRCP SNOW SNWD TMAX TMIN = = = = = Осадки (в десятых долях миллиметра) Количество осадков в виде снега (мм) Высота снежного покрова (мм) Максимальная температура (в десятых долях градусов Цельсия) Минимальная температура (в десятых долях градусов Цельсия) Для целей нашего примера требуются элементы TMAX и TMIN, содержащие максимальную и минимальную температуру в десятых долях градусов Цельсия. Парсинг данных по периодам наблюдений (инвентаризациям) В файле readme.txt указывается, какая информация хранится в файле периодов наблюдений (инвентаризаций), что позволяет выполнить парсинг данных
   Загрузка данных 573 в более удобный формат. Конечно, разобранные данные можно просто сохранить в формате списка списков или списка кортежей, однако, приложив немного дополнительных усилий, можно задействовать декоратор dataclass из модуля dataclasses для создания пользовательского класса с именованными атрибутами (в качестве альтернативы также можно рассмотреть именованный кортеж): # использование dataclass для создания пользовательского класса Inventory from dataclasses import dataclass @dataclass class Inventory: station:str latitude: float longitude: float element:str start:int end:int Применить созданный класс Inventory несложно: вы просто создаете каждый экземпляр из соответствующих значений, которые в данном случае представляют собой разобранную строку данных периодов наблюдений (инвентаризаций). Парсинг выполняется в два этапа. Во-первых, необходимо выделить срезы строки в соответствии с заданными размерами полей. Как видно из описания полей в файле readme, между полями также присутствует дополнительный пробел, который необходимо учитывать при разработке подхода к парсингу. В данном случае благодаря явному заданию каждого среза дополнительные пробелы игнорируются. Кроме того, поскольку размеры полей STATION и ELEMENT точно соответствуют хранящимся в них значениям, вам не нужно беспокоиться об удалении из них лишних пробелов. Во-вторых, было бы неплохо привести значения широты и долготы к вещественному типу (float), а начальный и конечный годы — к целочисленному (int). Вы могли бы сделать это на более позднем этапе очистки данных, ведь если данные не согласованы и не содержат значений, которые корректно преобразуются в каждой строке, то вам, возможно, стоит подождать. Однако в нашем случае данные позволяют выполнить эти преобразования прямо на этапе парсинга, поэтому сейчас мы так и сделаем: # Разбор строк инвентаризации (данных периодов наблюдений) и приведение ряда # значений к типу чисел с плавающей точкой и целочисленному типу inventory = [Inventory(x[0:11], float(x[12:20]), float(x[21:30]), x[31:35], int(x[36:40]), int(x[41:45])) for x in inventory_txt.split("\n") if x.startswith("US")] for line in inventory[:5]: print(line) Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, element='TMAX', start=2008, end=2016) Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, element='TMIN', start=2008, end=2016)
   574 Практический пример Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, element='PRCP', start=2008, end=2016) Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, element='SNWD', start=2009, end=2016) Inventory(station='US10RMHS145', latitude=40.5268, longitude=-105.1113, element='PRCP', start=2004, end=2004) Выбор станции по широте и долготе Теперь, когда данные периодов наблюдений (инвентаризаций) загружены, можно использовать широту и долготу для нахождения станций, ближайших к вашему местоположению, а затем выбрать самую продолжительную серию температурных показателей на основании начального и конечного годов. Уже в первой строке данных встречаются два обстоятельства, требующих нашего внимания. В наборе данных присутствуют различные типы элементов, но нас интересуют лишь элементы TMIN и TMAX для самой низкой и самой высокой температуры. Ни одна из первых записей периодов наблюдений не охватывает более чем несколько лет. Если вам нужны данные в исторической перспективе, следует отыскать более длительную последовательность температурных показателей. Для того чтобы быстро выбрать необходимую информацию, можно при помощи генератора списка создать подсписок, содержащий только элементы периодов наблюдений с типом TMIN или TMAX. Дополнительным критерием отбора является продолжительность наблюдений, поэтому в процессе формирования данного подмножества необходимо обеспечить соответствие условиям: год начала наблюдений предшествует 1920 году, а год окончания — не ранее 2024 года. Таким образом, мы рассматриваем только станции с более чем 100-летней историей наблюдений: inventory_temps = [x for x in inventory if x.element in ['TMIN', 'TMAX'] and x.end >= 2015 and x.start < 1920] inventory_temps[:5] [Inventory(station='USC00010583', element='TMAX', Inventory(station='USC00010583', element='TMIN', Inventory(station='USC00011694', element='TMAX', Inventory(station='USC00011694', element='TMIN', Inventory(station='USC00012813', element='TMAX', latitude=30.8839, longitude=-87.7853, start=1915, end=2024), latitude=30.8839, longitude=-87.7853, start=1915, end=2024), latitude=32.8158, longitude=-86.6044, start=1893, end=2024), latitude=32.8158, longitude=-86.6044, start=1893, end=2024), latitude=30.5467, longitude=-87.8808, start=1917, end=2024)] При взгляде на первые пять записей нового списка видно, что выборка заметно улучшилась. Теперь в нем содержатся лишь записи температурных показателей и продолжительные серии данных, судя по начальному и конечному годам. Остается проблема выбора ближайшей станции. Для этого следует сравнить широту и долготу данных станций с широтой и долготой вашего местоположения.
   Загрузка данных 575 Существуют различные способы получения широты и долготы произвольной точки, но, вероятно, самый простой из них — воспользоваться картографическим приложением или интернет-поиском. (Для делового центра Чикаго — ЧикагоЛуп (Chicago Loop) я нашла широту 41.882 и долготу -87.629.) Поскольку вас интересуют станции, ближайшие к вашему местоположению, задача подразумевает сортировку списка на основании близости широты и долготы станций к широте и долготе вашего местоположения. Отсортировать список несложно, в том числе и по широте/долготе. Но как отсортировать по расстоянию от вашей текущей широты и долготы? Для этого нужно определить функцию-ключ для процедуры сортировки, которая вычисляет разницу между вашей широтой и широтой станции, разницу между вашей долготой и долготой станции, и объединяет их в одно число. Единственное, о чем важно помнить, — перед сложением необходимо вычислить абсолютные значения разностей, чтобы суммирование отрицательной разности с положительной не сбило с толку процедуру сортировки: # Центр Чикаго, по данным онлайн-карты latitude, longitude = 41.882, -87.629 inventory_temps.sort(key=lambda x: x.longitude)) abs(latitude-x.latitude) + abs(longitude- inventory_temps[:20] [Inventory(station='USC00110338', latitude=41.7803, longitude=-88.3092, element='TMAX', start=1893, end=2024), Inventory(station='USC00110338', latitude=41.7803, longitude=-88.3092, element='TMIN', start=1893, end=2024), Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861, element='TMAX', start=1897, end=2024), Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861, element='TMIN', start=1897, end=2024), Inventory(station='USC00476922', latitude=42.7028, longitude=-87.7858, element='TMAX', start=1896, end=2024), Inventory(station='USC00476922', latitude=42.7028, longitude=-87.7858, element='TMIN', start=1896, end=2024), Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297, element='TMAX', start=1897, end=2024), Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297, element='TMIN', start=1897, end=2024), Inventory(station='USC00115825', latitude=41.3714, longitude=-88.4333, element='TMAX', start=1912, end=2024), Inventory(station='USC00115825', latitude=41.3714, longitude=-88.4333, element='TMIN', start=1912, end=2024), Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267, element='TMAX', start=1893, end=2024), Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267, element='TMIN', start=1893, end=2024), Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685, element='TMAX', start=1902, end=2024), Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685, element='TMIN', start=1902, end=2024),
   576 Практический пример Inventory(station='USW00014848', element='TMAX', Inventory(station='USW00014848', element='TMIN', Inventory(station='USC00124657', element='TMAX', Inventory(station='USC00124657', element='TMIN', Inventory(station='USC00478937', element='TMAX', Inventory(station='USC00478937', element='TMIN', latitude=41.7072, longitude=-86.3164, start=1893, end=2024), latitude=41.7072, longitude=-86.3164, start=1893, end=2024), latitude=41.3053, longitude=-86.6286, start=1897, end=2024), latitude=41.3053, longitude=-86.6286, start=1897, end=2024), latitude=42.9986, longitude=-88.2525, start=1894, end=2024), latitude=42.9986, longitude=-88.2525, start=1894, end=2024)] Выбор станции и получение ее метаданных При анализе первых 20 записей вновь отсортированного списка можно заметить, что неплохим кандидатом является первая станция USC00110338. Она содержит данные как по TMIN, так и по TMAX, а период наблюдений начинается в 1893 году и продолжается по 2017 год — данные более чем за 120 лет. Сохраним эту станцию в переменной station и выполним парсинг уже полученных метаданных для извлечения дополнительной информации о данной станции. В файле readme можно найти следующие сведения о данных станций: IV. ФОРМАТ ФАЙЛА "ghcnd-stations.txt" -----------------------------Переменная Столбцы Тип -----------------------------ID 1-11 Строковый LATITUDE 13-20 Вещественный LONGITUDE 22-30 Вещественный ELEVATION 32-37 Вещественный STATE 39-40 Строковый NAME 42-71 Строковый GSN FLAG 73-75 Строковый HCN/CRN FLAG 77-79 Строковый WMO ID 81-85 Строковый -----------------------------Переменные имеют следующие определения: ID идентификационный код метеостанции. Обратите внимание: первые два символа обозначают код страны в соответствии со стандартом FIPS, третий символ является кодом сети, идентифицирующим используемую систему нумерации станций, а последующие восемь символов содержат непосредственный идентификатор станции. Полный список кодов стран см. в файле "ghcnd-countries.txt". Список кодов штатов/провинций/территорий см. в файле "ghcnd-states.txt". Код сети может принимать следующие значения: 0 = не задано (станция определяется восемью алфавитно-цифровыми символами).
   Загрузка данных 577 1 = идентификатор CoCoRaHS (Community Collaborative Rain, Hail, and Snow). Для согласования со стандартом GHCN Daily все числа в исходных идентификаторах CoCoRaHS дополнены слева до четырех цифр. Кроме того, удалены символы "-" и "_", чтобы длина идентификатора не превышала 11 символов с префиксом "US1". Например, идентификатор CoCoRaHS "AZ-MR-156" в in GHCN-Daily превращается в "US1AZMR0156". C = идентификатор U.S. Cooperative Network (последние шесть символов идентификатора GHCN Daily ID). E = идентификатор, используемый в несмешанном наборе данных ECA&D. M = идентификатор Всемирной метеорологической организации (последние пять символов идентификатора GHCN-Daily ID). N = идентификатор, используемый в данных Национального метеорологического или гидрологического центра. R = идентификатор Межведомственных дистанционно управляемых автоматических метеорологических станций США (RAWS). S = идентификатор станции SNOTEL (SNOwpack TELemetry) — Службы охраны природных ресурсов США. W = идентификатор WBAN (последние пять символов идентификатора GHCN-Daily). LATITUDE LONGITUDE ELEVATION STATE NAME GSN FLAG широта станции (в десятичных градусах). долгота станции (в десятичных градусах). высота станции (в метрах, для отсутствующих значений = -999.9). почтовый код штата (только для станций в США). название станции. флаг, указывающий, является ли станция частью сети GSN (GCOS Surface Network). Значение флага задается проверкой числа из поля WMOID по официальному списку станций GSN. Определены два значения: пустое = станция не входит в GSN или код станции WMO недоступен. GSN = станция GSN. HCN/ флаг, указывающий, является ли станция частью сети HCN (Historical Climatology CRN FLAG Network) США. Определены три значения: пустое = станция не входит в сеть HCN или CRN. HCN = станция входит в сеть HCN. CRN = станция входит в сеть CRN (Climate Reference Network) или Региональную климатическую сеть. WMO ID идентификатор станции во Всемирной метеорологической организации. Если станция не имеет кода WMO (или код еще не был назначен этой станции), поле остается пустым. Хотя поля метаданных могут представлять больший интерес для углубленного исследования, нам нужно лишь сопоставить начальный и конечный годы из данных периодов наблюдений с остальными метаданными станций, содержащимися в файле станций. Перебрать содержимое файла станций и найти станцию с нужным идентификатором можно несколькими способами. Можно создать цикл for, который перебирает строки файла и останавливается при нахождении нужной станции; можно разбить данные на строки, а затем отсортировать их и воспользоваться бинарным поиском и т. д. В зависимости от природы и объема данных могут быть уместны разные подходы. В нашем случае, поскольку данные уже загружены и не
   578 Практический пример слишком велики, мы воспользуемся генератором списка для получения списка, единственным элементом которого является искомая станция: station_id = 'USC00110338' # Парсинг станций Station = namedtuple("Station", ['station_id', 'latitude', 'longitude', 'elevation', 'state', 'name', 'start', 'end']) stations = [(x[0:11], float(x[12:20]), float(x[21:30]), float(x[31:37]), x[38:40].strip(), x[41:71].strip()) for x in stations_txt.split("\n") if x.startswith(station_id)] station = Station(*stations[0] + (inventory_temps[0].start, inventory_temps[0].end)) print(station) Station(station_id='USC00110338', latitude=41.7803, longitude=-88.3092, elevation=205.7, state='IL', name='AURORA WATER', start=1915, end=2024) На данный момент мы определили, что нас интересуют данные метеорологической станции в Авроре, штат Иллинойс (Aurora, Illinois) — ближайшей станции к центру Чикаго, располагающей температурными данными более чем за 100 лет. Получение и парсинг метеоданных После того как мы выбрали станцию, необходимо получить ее метеоданные и провести их парсинг. Эта процедура весьма схожа с той, что применялась в предыдущем разделе. Получение данных Сначала загрузите файл данных и сохраните его на случай, если он понадобится в будущем: # скачать ежесуточные записи для выбранной станции r = requests.get('https://www1.ncdc.noaa.gov/pub/data/ghcn/daily/all/{}.dly' ➥.format(station.station_id)) weather = r.text # сохранить данные в текстовый файл, чтобы не пришлось загружать их повторно with open('weather_{}.txt'.format(station), "w") as weather_file: weather_file.write(weather) # считывание данных из сохраненного файла ежесуточных записей при необходимости # (только в том случае, если необходимо начать процесс заново без повторной # загрузки файла) with open('weather_{}.txt'.format(station)) as weather_file: weather = weather_file.read()
   Получение и парсинг метеоданных 579 print(weather[:540]) USC00110338189301TMAX -11 ➥6 -83 6 -33 6 -178 6 ➥6 -106 6 -61 6 -94 6 ➥6 -78 6 -33 6 44 6 USC00110338189301TMIN -50 ➥6 -222 6 -178 6 -250 6 ➥6 -311 6 -200 6 -233 6 ➥6 -178 6 -200 6 -33 I6 6 -44 -150 6 -33 6 -89 I6 6 -139 -200 6 -178 6 -156 6 6 -139 -128 6 -33 6 -22 6 6 -250 -206 6 -156 6 -139 6 6 -83 -172 6 -33 6 6 6 6 -144 -267 6 -89 6 -167 6 6 -100 6 -83 6 -72 -200 6 -189 6 -150 -33 6 6 6 -33 6 -178 6 -228 6 -144 -272 6 -294 6 -294 -200 6 -194 6 -194 Парсинг метеоданных Итак, данные успешно загружены, и вы видите, что они несколько сложнее данных станций и периодов наблюдений (инвентаризаций). Очевидно, пришло время вернуться к файлу readme.txt и разделу III, в котором приведено описание файла метеоданных. У вас множество вариантов, поэтому отфильтруйте их, оставив только те, что представляют для вас интерес, и исключите остальные типы элементов, а также всю систему флагов, описывающих источник, качество и тип значений: III. ФОРМАТ ФАЙЛОВ ДАННЫХ (ФАЙЛЫ ".dly") Каждый файл ".dly" содержит данные по одной станции. Имя файла соответствует идентификационному коду станции. Например, файл ➥"USC00026481.dly" содержит данные для станции с идентификационным кодом ➥USC00026481). Каждая запись в файле содержит данные за один месяц. Поля (переменные) в каждой строке включают следующее: -----------------------------Переменная Столбцы Тип -----------------------------ID 1-11 Символьный YEAR 12-15 Целочисленный MONTH 16-17 Целочисленный ELEMENT 18-21 Символьный VALUE1 22-26 Целочисленный MFLAG1 27-27 Символьный QFLAG1 28-28 Символьный SFLAG1 29-29 Символьный VALUE2 30-34 Целочисленный MFLAG2 35-35 Символьный QFLAG2 36-36 Символьный SFLAG2 37-37 Символьный . . . . . . . . . VALUE31 262-266 Целочисленный MFLAG31 267-267 Символьный QFLAG31 268-268 Символьный SFLAG31 269-269 Символьный -----------------------------Определения этих переменных:
   580 Практический пример ID идентификационный код станции. Полный список станций и их метаданные см. в файле ghcnd-stations.txt. YEAR год, к которому относится запись. MONTH месяц, к которому относится запись. ELEMENT тип показателя (элемента). Существует пять основных показателей, а также ряд дополнительных. Пять основных показателей: PRCP SNOW SNWD TMAX TMIN = = = = = Осадки (в десятых долях миллиметра) Количество осадков в виде снега (мм) Высота снежного покрова (мм) Максимальная температура (в десятых долях градуса Цельсия) Минимальная температура (в десятых долях градуса Цельсия) ... VALUE1 — значение для первого дня месяца (данные отсутствуют = -9999). MFLAG1 — флаг измерения для первого дня месяца. QFLAG1 — флаг качества для первого дня месяца. SFLAG1 — флаг источника для первого дня месяца. VALUE2 — значение для второго дня месяца. MFLAG2 — флаг измерения для второго дня месяца. QFLAG2 — флаг качества для второго дня месяца. SFLAG2 — флаг источника для второго дня месяца. ... и т. д. до 31-го числа месяца. Обратите внимание: если месяц содержит менее 31 дня, то переменным для отсутствующих дней присваивается признак отсутствия данных (например, для апреля VALUE31 = −9999, MFLAG31 = пустое значение, QFLAG31 = пустое значение, SFLAG31 = пустое значение). Сейчас нас интересует прежде всего то, что идентификатор станции занимает 11 символов строки, год — следующие 4 символа, месяц — следующие 2 символа, а элемент (параметр) — следующие 4 символа после него. Далее следует 31 позиция для ежесуточных данных; каждая позиция состоит из 5 символов для температуры, выраженной в десятых долях градуса Цельсия, и 3 символов флагов. Как упоминалось ранее, на флаги в этом упражнении можно не обращать внимания. Также видно, что отсутствующие значения температуры обозначаются –9999, если этого дня нет в месяце, поэтому, к примеру, для типичного февраля 29-е, 30-е и 31-е значения будут равны –9999. При обработке данных в этом упражнении мы стремимся определить общие тенденции, поэтому беспокоиться о каждом отдельном дне не обязательно. Вместо этого стоит вычислить средние значения за месяц. Можно сохранить
   Получение и парсинг метеоданных 581 максимальное, минимальное и среднее значения за целый месяц и работать с этими величинами. Это означает, что обработка каждой строки метеоданных должна осуществляться следующим образом. 1. Разбить строку на отдельные поля, отбрасывая или игнорируя флаги для всех ежесуточных значений. 2. Удалить данные со значением –9999, преобразовать год и месяц в целые числа, а значения температуры — в числа с плавающей точкой. Помните, что значения температуры выражены в десятых долях градуса Цельсия. 3. Вычислить среднее значение, отобрать максимальное и минимальное значения. Здесь возможен один из двух путей. Во-первых, можно сделать несколько итераций по данным, разбить их на поля, отбросить заполнители, преобразовать строки в числа и, наконец, вычислить сводные значения. Во-вторых, можно написать функцию, которая выполняет все эти операции подряд для каждой строки, и сделать все за один проход. Оба решения будут корректными. В данном случае мы выберем второй подход и создадим функцию parse_line для выполнения всех преобразований данных: def parse_line(line): """ выполняет парсинг строки метеоданных, удаляет значения -9999 (данные отсутствуют) """ # возвращаем None, если строка пуста if not line: return None # выделяем первые 4 поля и строку со значениями температур record, temperature_string = (line[:11], int(line[11:15]), int(line[15:17]), line[17:21]), line[21:] # вызываем исключение, если строка температур слишком короткая if len(temperature_string) < 248: raise ValueError("String not long enough - {} {}".format(temperature_ string, str(line))) # используем генератор списка для temperature_string, чтобы извлечь и # преобразовать значения values = [float(temperature_string[i:i + 5])/10 for i in range(0, 248, 8) if not temperature_string[i:i + 5].startswith("-9999")] # получаем количество значений, максимум и минимум, и вычисляем среднее count = len(values) tmax = round(max(values), 1) tmin = round(min(values), 1) mean = round(sum(values)/count, 1) # добавляем сводные значения температуры к извлеченным ранее полям записи
   582 Практический пример # и возвращаем результат return record + (tmax, tmin, mean, count) Если протестировать функцию с первой строкой необработанных метеорологических данных, вы получите следующий результат: parse_line(weather[:270]) ('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31) Похоже, эта функция успешно справляется с разбором данных. А раз функция работает, вы можете разобрать все метеоданные и либо сохранить их, либо продолжить обработку: # Обработка всех метеоданных # Генератор списка, пустые строки не обрабатываются weather_data = [parse_line(x) for x in weather.split("\n") if x] len(weather_data) weather_data[:10] [('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', 1893, 1893, 1893, 1893, 1893, 1893, 1893, 1893, 1893, 1893, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 'TMAX', 'TMIN', 'PRCP', 'SNOW', 'WT16', 'WT18', 'TMAX', 'TMIN', 'PRCP', 'SNOW', 4.4, -20.0, -7.8, 31), -3.3, -31.1, -19.2, 31), 8.9, 0.0, 1.1, 31), 10.2, 0.0, 1.0, 31), 0.1, 0.1, 0.1, 2), 0.1, 0.1, 0.1, 11), 5.6, -17.2, -0.9, 27), 0.6, -26.1, -11.7, 27), 15.0, 0.0, 2.0, 28), 12.7, 0.0, 0.6, 28)] Теперь все метеорологические записи (не только температурные показатели) распарсены и занесены в список. Сохранение метеоданных в базе данных (необязательно) На данный момент все метеорологические записи (а при желании также записи станций и периодов наблюдений) можно сохранить в базе данных. Это позволит вам вернуться к ним позднее и использовать те же данные без хлопот, связанных с повторной загрузкой и парсингом. К примеру, следующий код показывает, как сохранить погодные данные в базе данных sqlite3: import sqlite3 conn = sqlite3.connect("weather_data.db") cursor = conn.cursor() # создаем таблицу weather для метеоданных
   Получение и парсинг метеоданных 583 create_weather = """CREATE TABLE "weather" ( "id" text NOT NULL, "year" integer NOT NULL, "month" integer NOT NULL, "element" text NOT NULL, "max" real, "min" real, "mean" real, "count" integer)""" cursor.execute(create_weather) conn.commit() # сохраняем разобранные данные таблицы weather в базе данных for record in weather_data: cursor.execute("""insert into weather (id, year, month, element, max, ➥min, mean, count) values (?,?,?,?,?,?,?,?) """, record) conn.commit() После того как данные сохранены, их можно загрузить из базы данных с помощью кода, аналогичного следующему, который извлекает только записи TMAX: cursor.execute("""select * from weather where element='TMAX' order by year, ➥ month""") tmax_data = cursor.fetchall() tmax_data[:5] [('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', 1893, 1893, 1893, 1893, 1893, 1, 2, 3, 4, 5, 'TMAX', 'TMAX', 'TMAX', 'TMAX', 'TMAX', 4.4, -20.0, -7.8, 31), 5.6, -17.2, -0.9, 27), 20.6, -7.2, 5.6, 30), 28.9, 3.3, 13.5, 30), 30.6, 7.2, 19.2, 31)] Выборка данных и построение графиков Поскольку в постановке задачи речь идет только о температуре, необходимо выбрать из массива данных лишь температурные записи. Это достаточно легко осуществить при помощи пары генераторов списков: один получает список TMAX, а другой — TMIN. Также можно воспользоваться функциональностью pandas для графического представления данных и исключения ненужных записей. Так как сейчас нас интересует использование «чистого» Python-кода, а не pandas, выберем первый вариант: tmax_data = [x for x in weather_data if x[3] == 'TMAX'] tmin_data = [x for x in weather_data if x[3] == 'TMIN'] tmin_data[:5] [('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', ('USC00110338', 1893, 1893, 1893, 1893, 1893, 1, 2, 3, 4, 5, 'TMIN', 'TMIN', 'TMIN', 'TMIN', 'TMIN', -3.3, -31.1, -19.2, 31), 0.6, -26.1, -11.7, 27), 3.3, -13.3, -4.6, 31), 12.2, -5.6, 2.2, 30), 14.4, -0.6, 5.7, 31)]
   584 Практический пример Использование pandas для графического представления данных На текущем этапе данные очищены и подготовлены к построению графика. Чтобы упростить создание графического представления, целесообразно воспользоваться библиотеками pandas и matplotlib в соответствии с методикой, изложенной в главе 24. В среде Colaboratory оба пакета предустановлены и готовы к работе. В любой другой среде их можно установить, выполнив в командной строке следующие команды: pip install pandas matplotlib Для подключения библиотеки pandas достаточно выполнить ее стандартный импорт, а вот для активации matplotlib в среде Colaboratory целесообразно применить специальную «магическую» команду Jupyter: import pandas as pd %matplotlib inline «Магическая» команда для активации matplotlib Теперь можно импортировать библиотеку pandas и создать датафреймы для данных TMAX и TMIN: tmax_df = pd.DataFrame(tmax_data, columns=['Station', 'Year', 'Month', 'Element', 'Max', 'Min', 'Mean', 'Days']) tmin_df = pd.DataFrame(tmin_data, columns=['Station', 'Year', 'Month', 'Element', 'Max', 'Min', 'Mean', 'Days']) Можно, конечно, построить график ежемесячных значений, однако 125 лет наблюдений, помноженные на 12 месяцев, формируют массив из 1500 точек данных, при этом сезонная цикличность также затрудняет выявление закономерностей. Вместо этого, пожалуй, будет разумнее усреднить максимальные, минимальные и средние ежемесячные показатели до годовых значений и построить на их основе график. Безусловно, это можно сделать и в «чистом» коде Python, но поскольку данные уже загружены в датафрейм pandas, стоит воспользоваться его возможностями для группировки по годам и вычисления средних значений: # выбираем столбцы Year, Min, Max, Mean, группируем их по году, # вычисляем средние значения и строим линейный график tmin_df[['Year','Min', 'Mean', 'Max']].groupby('Year').mean().plot( kind='line', figsize=(16, 8)) Полученный результат характеризуется значительным разбросом данных, однако, похоже, он все же свидетельствует о том, что минимальная температура за последние 20 лет растет. Обратите внимание: если вы захотите построить такой же график, не прибегая к Jupyter Notebook и matplotlib, вы по-прежнему можете использовать pandas, но в этом случае вам нужно будет сохранить данные в CSV-файл или файл Microsoft Excel при помощи методов датафрейма to_csv или to_excel. Затем полученный файл можно загрузить в приложение для работы с электронными таблицами и построить график уже там.
Приложение. Руководство по документации Python Наиболее авторитетным и актуальным источником справочной информации о языке Python является документация, входящая в поставку самого интерпретатора. Учитывая это, куда полезнее изучить способы доступа к этой документации, чем читать страницы печатного издания с ее сокращенной версией. Стандартный пакет документации включает несколько разделов, в том числе инструкции по документированию, распространению, установке и расширению Python на различных платформах; именно с него логично начинать поиск ответов на вопросы о языке. Скорее всего, двумя самыми полезными разделами документации Python станут для вас «Справочник по библиотеке» (Library Reference) и «Справочник по языку» (Language Reference). «Справочник по библиотеке» абсолютно необходим, поскольку он содержит подробное описание как встроенных типов данных, так и всех модулей, входящих в состав Python. «Справочник по языку» предоставляет разъяснение того, как устроено ядро Python: в нем изложена официальная позиция по основам языка с объяснением принципов работы типов данных, операторов и прочих элементов. Кроме того, стоит ознакомиться с разделом «Что нового» (What’s New), особенно после выхода обновленной версии Python, так как в нем кратко описаны все изменения, внесенные в новую версию. П.1. Доступ к онлайн-документации Python Многие считают, что самый удобный способ работы с документацией Python — перейти на docs.python.org и ознакомиться с представленными там материалами. Несмотря на необходимость подключения к интернету, у этого способа есть несомненное преимущество: наличие наиболее полных и актуальных сведений по всем версиям языка, включая последнюю. Учитывая, что для многих проектов приходится регулярно искать дополнительную информацию и материалы,
   586 Приложение. Руководство по документации Python постоянно открытая вкладка браузера, посвященная онлайн-документации Python, позволит всегда иметь под рукой полный справочник по этому языку программирования. Если вы хотите сохранить полный комплект справочной документации Python локально на вашем компьютере, планшете или устройстве для чтения электронных книг, то можете загрузить его с веб-сайта docs.python.org в форматах PDF, HTML, обычный текст, Texinfo и EPUB. П.2. Лучшие практики: как стать питонистом Каждый язык программирования формирует собственные традиции и культуру, ярким примером чему является Python. Многие опытные Python-разработчики (или питонисты (Pythonistas), как они сами себя иногда называют) весьма щепетильно относятся к написанию программного кода, соответствующего идио­ матическим канонам и передовым практикам Python. Такой код Python высоко ценится и считается истинно питоническим (Pythonic), в противоположность коду на Python, демонстрирующему стилистические черты Java, C или JavaScript. Основная сложность, с которой сталкиваются все новички в программировании на Python, — это понимание того, как научиться писать именно такой, питонический код? Безусловно, чтобы по-настоящему прочувствовать язык и его стиль, потребуются некоторое время и усилия, однако в оставшейся части этого приложения мы поговорим о том, с чего стоит начать. П.2.1. Десять советов о том, как стать питонистом Советами, приведенными в этом разделе, я обычно делюсь на занятиях для разработчиков среднего уровня. По моему убеждению, эти рекомендации действительно способствуют повышению навыков программирования на языке Python. Не стану утверждать, что все со мной полностью согласны, однако, судя по моему многолетнему опыту, эти советы уверенно выведут вас на путь настоящего питониста. Осмыслите «Дзен Python». «Дзен Python», или PEP 20, обобщает концептуальные основы, заложенные в язык Python на этапе его проектирования, и часто цитируется в дискуссиях о критериях питоничности кода. Пусть принципы «Красивое лучше, чем уродливое» и «Простое лучше, чем сложное» станут вашими мантрами в процессе разработки. «Дзен Python» приводится в конце этого приложения; впрочем, вы всегда можете вывести его на экран, запустив команду import this в строке приглашения интерпретатора Python. Следуйте рекомендациям PEP 8. PEP 8 — это официальное руководство по стилю Python, которое также приводится ниже в этом приложении. PEP 8 содержит разнообразные полезные советы: от форматирования кода и выбора
   П.2. Лучшие практики: как стать питонистом 587 имен переменных до применения языка. Если вы хотите писать питонический код, вам необходимо освоить PEP 8. Освойте работу с документацией. Python располагает богатой, отлично поддерживаемой системой документации, и вам следует как можно чаще к ней обращаться. Пожалуй, самой полезной является документация стандартной библиотеки, однако учебные руководства и практические инструкции (файлы how-to ) также являются ценнейшим кладезем информации для продуктивной работы с языком. Старайтесь как можно чаще писать как можно меньше кода. Хотя совет этот может относиться ко многим языкам, он особенно актуален для Python. Подразумевается, что надо стремиться писать программы как можно короче и проще (но не короче и не проще, чем необходимо) и активно практиковать такой стиль программирования. Читайте как можно больше кода Python. С самого начала сообщество Python понимало, что читать программный код еще важнее, чем писать его. Читайте как можно больше кода на языке Python и по возможности обсуждайте прочитанный код с другими. Отдавайте предпочтение встроенным структурам данных. Прежде чем писать собственные классы для хранения данных, обратитесь ко встроенным структурам Python. Различные типы данных Python можно комбинировать друг с другом практически с безграничной гибкостью, а ведь за ними стоят годы отладки и оптимизации. Пользуйтесь их преимуществами. Освойте генераторы (generators, comprehensions). Начинающие программисты на Python почти всегда упускают из виду, какую важную роль в языке играют генераторы списков, словарей и генераторные выражения. Находите примеры их применения в коде Python, который вы читаете, и упражняйтесь в их написании. Вы не станете питонистом, пока не сможете написать генератор списков практически не задумываясь. Пользуйтесь стандартной библиотекой. Когда встроенных инструментов недостаточно, обращайтесь к стандартной библиотеке. Компоненты стандартной библиотеки — это и есть те самые «батарейки в комплекте» языка Python. Они прошли испытание временем, были оптимизированы и документированы лучше любого другого кода Python. Старайтесь по возможности применять их. Пишите как можно меньше классов. Создавайте собственные классы лишь в том случае, когда без этого не обойтись. Опытные питонисты обычно очень расчетливо подходят к написанию классов. Они знают, что проектирование хорошего класса — нетривиальная задача, а все созданные ими классы придется тестировать и отлаживать. Будьте осмотрительны с фреймворками. Фреймворки могут казаться привлекательными, особенно для программистов, только начинающих освоение языка, поскольку они предоставляют множество мощных инструментов для
   588 Приложение. Руководство по документации Python ускорения разработки. Безусловно, следует пользоваться фреймворками, ко­ гда они обеспечивают реальные преимущества, однако необходимо помнить и об обратной стороне медали. Может оказаться, что на изучение странностей непитонического фреймворка у вас уйдет больше времени, чем на изучение самого языка Python, или что вам придется подстраиваться под ограничения фреймворка, хотя должно быть наоборот. П.3. PEP 8: Стандарт стилевого оформления кода Python Ниже приведена слегка адаптированная выдержка из PEP 8. Этот документ, разработанный Гвидо ван Россумом (Guido van Rossum) и Барри Уорсо (Barry Warsaw), является ближайшим аналогом руководства по стилю в экосистеме Python. Некоторые узкоспециализированные разделы были опущены, однако основные моменты удалось охватить. Стремитесь к тому, чтобы ваш код по возможности соответствовал PEP 8. Ваш стиль программирования на Python от этого только выиграет. Чтобы ознакомиться с полным текстом PEP 8 и всеми остальными PEP, предложенными за всю историю Python, перейдите в раздел документации на peps. python.org и обратитесь к индексу PEP. Документы PEP — ценный источник исторической и культурной информации о Python. Кроме того, в них разъясняются актуальная проблематика и перспективные направления развития языка. П.3.1. Введение Документ регламентирует соглашения по оформлению кода Python, входящего в стандартную библиотеку основного дистрибутива Python. Рекомендуется обратиться к сопутствующему справочному PEP, в котором описываются принципы стилевого оформления кода на C в C-реализации Python1. Приведенный текст адаптирован из оригинального очерка Гвидо ван Россума «Руководство по стилю Python» с включением отдельных фрагментов из руководства по стилю Барри Уорсо2. В случае возникновения конфликтов для целей настоящего PEP предпочтение отдается стилевым правилам Гвидо. Данный документ PEP может все еще быть неполным (по правде говоря, таким он может и остаться ;-)) Глупое постоянство — удел малых умов Одно из ключевых наблюдений Гвидо заключается в том, что мы читаем код намного чаще, чем пишем его. Представленные здесь рекомендации призваны улучшить удобочитаемость кода и обеспечить его единообразие в пределах 1 2 PEP 7, Style Guide for C Code, van Rossum, https://www.python.org/dev/peps/pep-0007/. Barry Warsaw. “GNU Mailman Coding Style Guide”, http://barry.warsaw.us/software/ STYLEGUIDE.txt.
   П.3. PEP 8: Стандарт стилевого оформления кода Python 589 обширного разнообразия программ на Python. Как сказано в PEP 20: «Читаемость имеет значение»1. Вся суть руководства по стилю сводится к вопросу согласованности. Согласованность с данным руководством важна, но куда важнее согласованность в рамках проекта. А еще важнее она в пределах одного модуля или функции. Однако самое важное — знать, когда можно отступить от правил. Бывают ситуа­ ции, когда руководство по стилю попросту неприменимо. Если сомневаетесь, руководствуйтесь здравым смыслом. Рассмотрите другие примеры и выбирайте оптимальный вариант. И не стесняйтесь задавать вопросы! Существуют две веские причины для нарушения любого конкретного правила. Если применение правила затруднит чтение кода (даже для тех, кто привык читать код, написанный с соблюдением правил). Ради согласованности с окружающим кодом, который также отклоняется от данного правила (возможно, в силу исторических причин); впрочем, это и шанс исправить чужие ошибки (в соответствии с принципами экстремального программирования). П.3.2. Оформление кода Отступы Используйте четыре пробела на уровень отступа. В очень старом коде, который вам не хотелось бы портить, можно продолжать использовать табуляции из восьми пробелов. Табуляции или пробелы? Никогда не смешивайте символы табуляции с пробелами. Самый популярный способ создания отступов в Python — только при помощи пробелов. Второй по популярности способ — только при помощи символов табуляции. Код, отступы в котором сделаны табуляциями и пробелами, следует преобразовать, чтобы в нем присутствовали только пробелы. При запуске интерпретатора Python из командной строки с ключом -t выводятся пре­ дупреждения о некорректном смешении табуляций и пробелов. С ключом -tt эти предупреждения преобразуются в ошибки. Настоятельно рекомендуется использовать эти опции! В новых проектах настоятельно рекомендуется использовать исключительно пробелы вместо табуляций. В большинстве редакторов программного кода имеются инструменты для этого. 1 PEP 20, “The Zen of Python”, www.python.org/dev/peps/pep-0020/.
   590 Приложение. Руководство по документации Python Максимальная длина строки Максимальная длина строки не должна превышать 79 символов. Многие устройства до сих пор ограничены 80-символьными строками; кроме того, ограничение окон 80 символами позволяет размещать несколько окон рядом. Стандартный механизм переноса на таких устройствах нарушает визуальную структуру кода, затрудняя его понимание. Поэтому рекомендуется ограничивать все строки 79 символами. Для длинных текстовых блоков (строк документации или комментариев) рекомендуется устанавливать ограничение в 72 символа. Предпочтительным способом переноса длинных строк является использование неявного продолжения строки в Python внутри конструкций, ограниченных круглыми, квадратными и фигурными скобками. При необходимости можно заключить выражение в дополнительную пару круглых скобок. Проследите за тем, чтобы перенесенное продолжение строки имело соответствующий отступ. Бинарные операторы следует разбивать перед оператором, а не после него. Приведем ряд примеров: class Rectangle(Blob): def __init__(self, width, height, color='black', emphasis=None, highlight=0): if (width == 0 and height == 0 and color == 'red' and emphasis == 'strong' or highlight > 100:) raise ValueError("sorry, you lose") if (width == 0 and height == 0 and (color == 'red' or emphasis is None)): raise ValueError("I don't think so -- values are %s, %s" % (width, height)) Blob.__init__(self, width, height, color, emphasis, highlight) Пустые строки Разделяйте определения функций и классов верхнего уровня двумя пустыми строками. Определения методов внутри класса разделяются одной пустой строкой. Дополнительные пустые строки могут применяться (умеренно) для разделения групп взаимосвязанных функций. Допускается пропуск пустых строк между взаимосвязанными однострочными определениями (к примеру, в наборе пустых реализаций или заглушек). Используйте пустые строки внутри функций умеренно для обозначения логических разделов. Интерпретатор Python воспринимает символ перевода страницы Control+L (^L)1 как пробел. Многие инструменты рассматривают эти символы как разделители 1 В тексте упоминается символ Ctrl-L (^L). Хотя интерпретатор Python действительно его поддерживает, в современной разработке (с использованием автоматических
   П.3. PEP 8: Стандарт стилевого оформления кода Python 591 страниц, поэтому вы можете применять их для визуального отделения связанных разделов вашего файла. Операторы импорта Операторы импорта1, как правило, следует размещать на отдельных строках — например: import os import sys Не объединяйте их в одну строку: import sys, os Тем не менее следующая инструкция допустима: from subprocess import Popen, PIPE Операторы импорта всегда размещаются в начале файла, сразу после комментариев к модулю и строк документации (докстрингов), но перед глобальными переменными и константами модуля. Операторы импорта должны быть сгруппированы в следующем порядке: 1. Импорты из стандартной библиотеки. 2. Импорты из сторонних модулей. 3. Локальные импорты текущего приложения или библиотеки. Группы операторов импорта разделяются между собой одной пустой строкой. После операторов импорта размещаются все необходимые спецификации __all__. Относительные импорты для использования внутри пакетов крайне не рекомендуются. Всегда используйте абсолютный путь пакета для всех импортов. Даже сейчас, когда PEP 328 полностью реализован в Python 2.52, предложенный в нем 1 2 форматтеров типа *Black* или *Ruff*) этот символ практически не используется и часто считается «мусором» в коде. Современные инструменты организации кода предпочитают логическое разделение на файлы и модули, а не визуальное разделение страницами внутри одного файла. — Примеч. ред. Текст в этом подразделе относится к эпохе Python 2.5. Современные нормы PEP 8 для Python 3 сильно изменились в этом вопросе. Как обстоят дела сейчас (в Python 3.10+): 1) Явные относительные импорты (например, from . import submodule) теперь разрешены и даже рекомендуются для внутрипакетных импортов. Они позволяют перемещать пакет целиком, не меняя код внутри него. 2) Абсолютные импорты по-прежнему являются предпочтительными по умолчанию, так как они более информативны. 3) Неявные относительные импорты (просто import submodule внутри пакета) полностью запрещены в Python 3. — Примеч. ред. PEP 328, Imports: Multi-Line and Absolute/Relative, www.python.org/dev/peps/pep-0328/.
592    Приложение. Руководство по документации Python стиль явных относительных импортов активно не приветствуется; абсолютные импорты более переносимы и, как правило, лучше читаются. При импорте класса из модуля, содержащего этот класс, обычно допустима следующая запись: from myclass import MyClass from foo.bar.yourclass import YourClass Если это приводит к конфликту имен в локальной области видимости, используйте полную запись: import myclass import foo.bar.yourclass и обращайтесь к ним как myclass.MyClass и foo.bar.yourclass.YourClass. Пробелы в выражениях и операторах А теперь о наболевшем — избегайте лишних пробелов в перечисленных ниже случаях. Непосредственно внутри круглых, квадратных или фигурных скобок: Да: spam(ham[1], {eggs: 2}) Нет: spam( ham[ 1 ], { eggs: 2 } ) Непосредственно перед запятой, точкой с запятой или двоеточием: Да: if x == 4: print x, y; x, y = y, x Нет: if x == 4 : print x , y ; x , y = y , x Непосредственно перед круглой скобкой, открывающей список аргументов при вызове функции: Да: spam(1) Нет: spam (1) Непосредственно перед открывающей квадратной скобкой при индексировании или создании среза: Да: dict['key'] = list[index] Нет: dict ['key'] = list [index] Более одного пробела вокруг оператора присваивания (или другого оператора) для выравнивания его с другим оператором:
   П.3. PEP 8: Стандарт стилевого оформления кода Python 593 Да: x = 1 y = 2 long_variable = 3 Нет: x = 1 y = 2 long_variable = 3 Прочие рекомендации Всегда окружайте следующие бинарные операторы одним пробелом с обеих сторон1: операторы присваивания (=), комбинированного присваивания (+=, -= и др.), сравнения (==, <, >, !=, <>2, <=, >=, in, not in, is, is not) и логические операторы (and, or, not). Ставьте пробелы до и после арифметических операторов. Да: i = i + 1 submitted += 1 x = x * 2 - 1 hypot2 = x * x + y * y c = (a + b) * (a - b) Нет: i=i+1 submitted +=1 x = x*2 - 1 hypot2 = x*x + y*y c = (a+b) * (a-b) Не ставьте пробелы до или после знака = при обозначении именованного аргумента или значения параметра по умолчанию. Да: def complex(real, imag=0.0): return magic(r=real, i=imag) 1 2 В актуальной редакции PEP 8 это правило было смягчено: теперь рекомендуется не ставить пробелы вокруг операторов с более высоким приоритетом, чтобы визуально подчеркнуть вложенность и группировку операций (например, x = x*2 – 1 вместо x = x * 2 – 1). — Примеч. ред. Обратите внимание на оператор <> (не равно). В современном Python 3 он не поддерживается — его полностью заменил оператор !=. Однако <> является корректным для версии 2.5, рассматриваемой в этой книге. — Примеч. ред.
   594 Приложение. Руководство по документации Python Нет: def complex(real, imag = 0.0): return magic(r = real, i = imag) Использование составных операторов (несколько инструкций в одной строке) в целом не приветствуется. Да: if foo == 'blah': do_blah_thing() do_one() do_two() do_three() Лучше избегать: if foo == 'blah': do_blah_thing() do_one(); do_two(); do_three() Хотя иногда допустимо размещать операторы if/for/while с небольшим телом на одной строке, категорически не рекомендуется применять данный подход для многосоставных операторов. Также избегайте сворачивания таких длинных строк! Лучше избегать: if foo == 'blah': do_blah_thing() for x in lst: total += x while t < 10: t = delay() Нет: if foo == 'blah': do_blah_thing() else: do_non_blah_thing() try: something() finally: cleanup() do_one(); do_two(); do_three(long, argument, list, like, this) if foo == 'blah': one(); two(); three() П.4. Комментарии Комментарии, идущие вразрез с кодом, хуже, чем их отсутствие. При внесении правок в программу прежде всего обновляйте комментарии! Комментарии должны быть законченными предложениями. Если комментарий представляет собой фразу или предложение, его первое слово должно быть записано с прописной буквы, если только оно не является идентификатором, начинающимся со строчной буквы (никогда не меняйте регистр идентификаторов!). Если комментарий короткий, точку в конце можно опустить. Блочные комментарии обычно содержат один или несколько абзацев, состоящих из законченных предложений, при этом каждое предложение должно завершаться точкой.
   П.4. Комментарии 595 Используйте два пробела после точки, завершающей предложение. Для английского языка действуют правила из руководства по стилю Странка и Уайта (Strunk & White)1. Python-разработчикам из неанглоязычных стран: просьба писать комментарии на английском, если только вы не уверены на 120 % в том, что ваш код никогда не придется читать людям, не владеющим вашим родным языком. Блочные комментарии Блочные комментарии обычно относятся к коду (или его части), следующему за ними, и снабжаются отступом того же уровня, что и код. Каждая строка блочного комментария начинается с # и одного пробела (за исключением текста с отступом внутри самого комментария). Абзацы внутри блочного комментария разделяются строкой, содержащей один символ #. Внутристрочные комментарии Не следует увлекаться внутристрочными комментариями. Такой комментарий представляет собой аннотацию, расположенную на той же строке, что и оператор (исполняемая инструкция). Внутристрочные комментарии должны отделяться от оператора как минимум двумя пробелами и начинаться с символа # и одного пробела. Как правило, можно легко обойтись без внутристрочных комментариев. Если в них утверждается очевидное, они только сильно отвлекают. Не поступайте так: x = x + 1 # Увеличить x Впрочем, иногда они бывают полезны: x = x + 1 # Компенсируем границу Строки документации (докстринги) Правила написания хороших строк документации (докстрингов) увековечены в PEP 2572. Пишите строки документации для всех публичных модулей, функций, классов и методов. Докстринги не являются необходимыми для непубличных методов, 1 2 Имеется в виду книга «Элементы стиля» проф. Уильяма Странка-мл. и писателя Э. Б. Уайта, прославленное руководство по стилю американского английского языка в традициях формальной грамматики, увидевшее свет в 1920 г. и существенно доработанное в 1959 г. Э. Б. Уайтом. — Примеч. пер. PEP 257, Docstring Conventions, Goodger, van Rossum, www.python.org/dev/peps/pep-0257/.
   596 Приложение. Руководство по документации Python однако в такой метод следует включить комментарий с описанием того, что этот метод выполняет. Такой комментарий должен следовать после строки def. В PEP 257 приведены правила написания хороших строк документации. Самое важное из них гласит, что последовательность """, завершающая многострочный текст, должна располагаться в отдельной строке; при этом желательно, чтобы ей предшествовала пустая строка, к примеру: """Возвращает foobang Необязательный plotz указывает, что сначала нужно выполнить манипуляцию с bizbaz. """ Для докстрингов, укладывающихся в одну строку, закрывающая последовательность """ может располагаться на той же строке. Учет версий Если необходимо сохранять служебные метки систем контроля версий Subversion, CVS или RCS1 в файле исходного кода, делайте это следующим образом: __version__ = "$Revision: 68852 $" # $Source$ Эти строки включаются после строки документации модуля, перед любым другим кодом, и отделяются сверху и снизу пустыми строками. П.4.1. Правила именования В правилах именования стандартной библиотеки Python царит некоторый беспорядок, поэтому добиться полного единообразия вряд ли удастся. Тем не менее ниже приведены стандарты именования, рекомендуемые на данный момент. Новые модули и пакеты (включая сторонние фреймворки) должны разрабатываться в соответствии с этими стандартами, однако если в существую­ щей библиотеке используется другой стиль, предпочтение следует отдавать внутренней согласованности. Описания: стили именования Существует множество различных стилей именования. Их полезно различать независимо от того, где и для чего они применяются. 1 В современном Python системы Subversion, CVS и RCS практически не используются — их вытеснил Git. Старые системы автоматически обновляли метаданные (номера версий, даты) прямо в тексте исходного кода, но с переходом индустрии на Git эта практика стала анахронизмом. Git хранит историю изменений отдельно от содержимого файлов, что позволяет не засорять код служебными метками и сохранять его целостность. — Примеч. ред.
   П.4. Комментарии 597 Обычно выделяются следующие стили именования: b (одна буква в нижнем регистре); B (одна буква в верхнем регистре); lowercase (нижний регистр); lower_case_with_underscores (нижний регистр с подчеркиваниями, так называемый snake_case или «змеиный стиль»); UPPERCASE (верхний регистр); UPPER_CASE_WITH_UNDERSCORES (верхний регистр с подчеркиваниями, так называемый SCREAMING_SNAKE_CASE или «Кричащий змеиный стиль»); CapitalizedWords или CapWords (слова начинаются с буквы верхнего регистра). Также этот стиль иногда называют StudlyCaps, а чаще всего — PascalCase («стиль Pascal»). Примечание: при использовании аббревиатур в стиле CapWords нужно писать все буквы аббревиатуры в верхнем регистре. Таким образом, HTTPServerError лучше, чем HttpServerError. mixedCase (смешанный регистр) — отличается от предыдущего стиля начальным символом в нижнем регистре. Также часто называется camelCase («верблюжий стиль»), так как заглавные буквы внутри слова напоминают горбы верблюда1. Capitalized_Words_With_Underscores (слова, начинающиеся с буквы в верхнем регистре, с подчеркиваниями — выглядит ужасно!). Также существует стиль, использующий короткий уникальный префикс для группировки взаимосвязанных имен. В Python он встречается редко, но я упоминаю его для полноты изложения. Так, функция os.stat() возвращает кортеж, элементам которого традиционно присваиваются имена вида st_mode, st_size, st_mtime и т. д. (Это сделано для того, чтобы подчеркнуть соответствие полям структуры struct системного вызова POSIX, что служит подсказкой для программистов, знакомых с данным интерфейсом.) 1 Названия стилей именования в этом списке адаптированы под стандарты современного практического программирования на момент публикации русскоязычной версии данной книги. В оригинале PEP 8 автор Гвидо ван Россум относит термин CamelCase (верблюжий стиль) к стилю именования CapitalizedWords. Однако в современной индустрии (особенно в сообществах Java и JavaScript) под camelCase почти всегда понимают стиль именования mixedCase (начинающийся со строчной буквы). Чтобы избежать путаницы, в данном переводе этой книги используются общепринятые сегодня термины: PascalCase для слов с заглавной буквы и camelCase для смешанного регистра. — Примеч. ред.
   598 Приложение. Руководство по документации Python Библиотека X11 использует префикс X для всех своих публичных функций. В Python такой стиль обычно считается избыточным, поскольку именам атрибутов и методов предшествует имя объекта, а именам функций — имя модуля. Кроме того, различаются следующие специальные форматы с использованием начальных и конечных подчеркиваний (они, как правило, легко сочетаются с любым стилем именования). _single_leading_underscore (один начальный символ подчеркивания). Слабый индикатор «для внутреннего использования». Так, команда from M import * не импортирует объекты, имена которых начинаются с символа подчеркивания. single_trailing_underscore_ (один конечный символ подчеркивания). Используется по соглашению во избежание конфликтов с ключевыми словами Python, к примеру: tkinter.Toplevel (master, class_='ClassName'). __double_leading_underscore (два начальных символа подчеркивания). Активирует искажение имен (name mangling) для атрибутов классов (внутри класса FooBar атрибут __boo превращается в _FooBar__boo; см. обсуждение ниже). __double_leading_and_trailing_underscore__ (два начальных и два конечных символа подчеркивания). Специальные («магические») объекты или атрибуты в пространствах имен, управляемых пользователем, такие как __init__, __import__ или __file__. Никогда не придумывайте такие названия; применяйте их строго в соответствии с документацией. Предписания: правила именования Нежелательные имена. Никогда не используйте символы l (буква «эл» нижнего регистра), O (буква «оу» верхнего регистра) или I (буква «ай» верхнего регистра) в качестве односимвольных имен переменных. В некоторых шрифтах эти символы неотличимы от цифр 1 (один) и 0 (ноль). При необходимости использовать l заменяйте ее на заглавную L. Имена пакетов и модулей. У модулей должны быть короткие имена, записанные только символами нижнего регистра. В именах модулей можно использовать подчеркивания, если это улучшает удобочитаемость. У пакетов Python должны быть короткие имена, записанные в нижнем регистре, при этом применение подчеркиваний не рекомендуется.
   П.4. Комментарии 599 Поскольку имена модулей сопоставляются с именами файлов, а некоторые файловые системы игнорируют регистр символов и обрезают длинные имена, важно, чтобы имена модулей были достаточно короткими — данное ограничение не актуально для UNIX-систем, однако может вызвать сложности при переносе кода на устаревшие версии Mac, Windows или DOS. Если модуль расширения, реализованный на C или C++, сопровождается модулем Python, предоставляющим более высокоуровневый интерфейс (например, более объектно-ориентированный), имя модуля на C или C++ должно начинаться с символа подчеркивания (к примеру, _socket). Имена классов. Почти без исключений в именах классов применяется стиль PascalCase (CapWords). Классы, предназначенные для внутреннего использования, дополнительно предваряются начальным символом подчеркивания. Имена исключений. Поскольку исключения должны быть классами, на них распространяются правила именования классов. При этом в именах исключений следует употреб­ лять суффикс Error (если исключение действительно является ошибкой). Имена глобальных переменных. (Будем надеяться, что эти переменные предназначены исключительно для использования внутри одного модуля.) Правила практически те же, что и для функций. Модули, предназначенные для использования через from M import *, должны использовать механизм __all__ для предотвращения экспорта глобальных переменных либо применять устоявшееся соглашение — добавлять символ подчеркивания перед такими переменными (чтобы указать на их непубличный статус внутри модуля). Имена функций. Имена функций должны записываться в нижнем регистре, с разделением слов символами подчеркивания («змеиный стиль») для удобства чтения. Использование стиля mixedCase (смешанный регистр или «верблюжий стиль») разрешается только в тех контекстах, где подобный стиль уже преобладает (к примеру, в threading.py), для сохранения обратной совместимости. Аргументы функций и методов. Всегда используйте self в качестве первого аргумента методов экземпляра. Всегда используйте cls в качестве первого аргумента методов класса. Если имя аргумента функции конфликтует с зарезервированным ключевым словом, предпочтительнее добавить к нему один замыкающий символ подчеркивания, чем использовать сокращение или искажать написание. Таким образом, print_ лучше, чем prnt. (Возможно, подобные конфликты лучше предотвращать за счет синонимической замены.)
   600 Приложение. Руководство по документации Python Имена методов и переменные экземпляров. Применяйте правила именования функций («змеиный стиль»): нижний регистр символов, слова разделяются подчеркиваниями для удобства чтения. Используйте один начальный символ подчеркивания только для непубличных методов и переменных экземпляра. Во избежание конфликтов имен с производными классами используйте два начальных подчеркивания для активации правил искажения имен в Python. Python искажает такие имена, добавляя к ним имя класса: если в классе Foo определен атрибут с именем __a, к нему нельзя будет обратиться как Foo.__a. (Настойчивый пользователь все же сможет получить к нему доступ, обратившись по имени Foo._Foo__a.) Как правило, двойное начальное подчеркивание следует использовать только во избежание конфликтов имен с атрибутами в классах, предназначенных для наследования. Учтите: существует полемика по поводу применения __names (см. ниже). Константы. Константы обычно объявляются на уровне модуля и записываются в верхнем регистре с разделением слов подчеркиваниями. Примеры: MAX_OVERFLOW, TOTAL. Проектирование с учетом наследования. Всегда определяйтесь заранее, должны ли методы класса и переменные экземпляра (совокупно называемые атрибутами) быть публичными или ­непубличными. Если сомневаетесь, делайте выбор в пользу непубличных; позже проще будет сделать непубличный атрибут публичным, чем наоборот. Публичные атрибуты классов предназначены для использования независимыми клиентами вашего класса, не знающими его внутренней структуры; при этом вы обязуетесь избегать изменений, нарушающих обратную совместимость. Непубличные атрибуты не предназначены для использования сторонними лицами; вы не даете никаких гарантий, что такие атрибуты не изменятся или даже не будут удалены. В данном контексте мы не употребляем термин закрытый (private), поскольку как таковых закрытых атрибутов в Python на самом деле не существует (без выполнения избыточного объема работы). Другую категорию составляют атрибуты, являющиеся частью API для подклассов (часто называемые защищенными (protected) в других языках). Некоторые классы проектируются для дальнейшего наследования с целью расширения или изменения отдельных аспектов поведения класса. При разработке таких классов постарайтесь определиться и явным образом обозначить, какие атрибуты являются публичными, какие — частью API для подклассов, а какие предназначены исключительно для использования внутри вашего базового класса.
   П.4. Комментарии 601 С учетом вышесказанного, рекомендации по «питоническому стилю» здесь следующие. Публичные атрибуты не должны иметь начальных символов подчеркивания. Если имя публичного атрибута конфликтует с зарезервированным ключевым словом, добавьте один завершающий символ подчеркивания к имени атрибута. Такой вариант предпочтительнее сокращения или искаженного написания. (Впрочем, невзирая на это правило, cls является предпочтительным вариантом написания для любой переменной или аргумента, заведомо являющихся ссылкой на класс — особенно для первого аргумента метода класса.) Примечание 1. См. предыдущую рекомендацию по именованию аргументов для методов класса. Для простых публичных атрибутов данных лучше предоставлять доступ напрямую через имя атрибута, без использования сложных методов получения/установки (геттеров/сеттеров). Помните, что Python предоставляет простой путь для будущих улучшений: если выяснится, что простой атрибут данных нуждается в добавлении функционального поведения, вы сможете использовать свойства (properties). Они позволят скрыть логику реализации за привычным синтаксисом доступа к атрибуту данных. Примечание 1. Свойства работают только в классах нового стиля. Примечание 2. Старайтесь, чтобы функциональное поведение свойства не имело побочных эффектов (хотя такие побочные эффекты, как кэширование, в целом допустимы). Примечание 3. Избегайте использования свойств для операций, затратных в вычислительном отношении; синтаксис обращения к атрибуту создает у вызывающей стороны впечатление, что доступ к нему относительно прост. Если класс предназначен для наследования и у вас имеются атрибуты, которые не должны использоваться подклассами, рассмотрите возможность присваивания им имен с двойным начальным подчеркиванием и без завершающих символов подчеркивания. При этом интерпретатор Python включает алгоритм искажения имен, который добавляет имя класса в начало имени атрибута. Тем самым предотвращаются конфликты имен в случае, когда подклассы непреднамеренно содержат атрибуты с тем же именем. Примечание 1. В искаженном имени используется только простое имя класса, поэтому если у подкласса совпадут и имя класса, и имя атрибута, конфликт имен все равно будет возможен. Примечание 2. Искажение имен несколько усложняет некоторые задачи — такие, как отладка и вызов функции __getattr__(). Впрочем, алгоритм искажения имен хорошо документирован и легко выполняется вручную.
   602 Приложение. Руководство по документации Python Примечание 3. Не всем нравится искажение имен. Постарайтесь выдержать баланс между необходимостью предотвращения случайных конфликтов имен и возможностью использования таких атрибутов опытными разработчиками. П.4.2. Рекомендации по программированию Следует разрабатывать код, обеспечивающий кросс-платформенную совместимость и не создающий ограничений для альтернативных реализаций Python (таких, как PyPy, Jython, IronPython, Pyrex, Psyco и аналогичных). В частности, не полагайтесь на эффективную реализацию в интерпретаторе CPython конкатенации строк «на месте» для инструкций вида a+=b или a=a+b. В Jython эти операторы выполняются медленнее. В частях библиотеки, критичных к производительности, следует использовать форму ''.join(). Это гарантирует, что конкатенация выполняется за линейное время в разных реализациях. Сравнения с уникальными экземплярами (singletons), такими как None, всегда должны выполняться с помощью операторов is или is not. Применение операторов равенства в данном случае недопустимо. Кроме того, остерегайтесь писать if x, когда в действительности имеется в виду if x is not None — к примеру, при проверке, было ли значение переменной или аргумента, по умолчанию равное None, заменено каким-то другим значением. Это «другое» значение может иметь тип (например, контейнер), который в логическом контексте оценивается как False! Применяйте исключения, основанные на классах. Строковые исключения в новом коде запрещены, так как эта возможность была удалена из языка, начиная с версии Python 2.6. Модули или пакеты должны определять собственный базовый класс исключений, специфичный для конкретной предметной области. Такой класс должен наследоваться от встроенного класса Exception. Необходимо всегда снабжать его строкой документации (докстрингом): class MessageError(Exception): """Базовый класс для ошибок в пакете электронной почты.""" Общие правила именования классов применимы и к исключениям. В случае, если исключение является ошибкой, вам следует добавлять суффикс Error к именам классов исключений. Исключения, не относящиеся к категории ошибок, обходятся без специальных суффиксов. При генерации исключения используйте форму raise ValueError('message') вместо устаревшей raise ValueError, 'message'. Форма с круглыми скобками предпочтительна, поскольку при наличии длинных аргументов или строк с форматированием отпадает необходимость в применении символов продолжения строки: наличие скобок решает эту задачу. Устаревший синтаксис удален, начиная с Python 3.
   П.4. Комментарии 603 При перехвате исключений по возможности указывайте их конкретные типы вместо использования пустого блока except:. К примеру, используйте: try: import platform_specific_module except ImportError: platform_specific_module = None Пустой блок except: перехватывает также исключения SystemExit и KeyboardInterrupt, что затрудняет остановку программы при помощи Ctrl+C и может маскировать другие проблемы. Если необходимо перехватить все исключения, указывающие на ошибки в программе, следует применять except Exception:. Хорошим практическим правилом считается ограничивать применение пустых блоков except: двумя случаями. Если обработчик исключения выводит трассировку стека или сохраняет ее в журнале (логирует); по крайней мере, пользователь будет знать о возникновении ошибки. Если коду необходимо выполнить очистку ресурсов, после чего он позволяет исключению распространяться выше с помощью raise — однако для таких случаев предпочтительным способом обработки является try...finally. Кроме того, для всех конструкций try/except следует помещать в блок try минимально необходимый объем кода. Это также помогает избежать путаницы в багах. Да: try: value = collection[key] except KeyError: return key_not_found(key) else: return handle_value(value) Нет: try: # Слишком широко! return handle_value(collection[key]) except KeyError: return key_not_found(key) Также перехватывает ошибку KeyError, возникающую при вызове handle_value() Используйте строковые методы вместо модуля string. Строковые методы выполняются значительно быстрее и используют тот же API, что и строки Unicode. Отступайте от этого правила лишь при необходимости поддержания обратной совместимости с версиями, более ранними, чем Python 2.0. Применяйте ''.startswith() и ''.endswith() вместо срезов строк для проверки наличия префиксов или суффиксов. Методы startswith() и endswith() выглядят чище и менее подвержены ошибкам.
   604 Приложение. Руководство по документации Python Да: if foo.startswith('bar'): Нет: if foo[:3] == 'bar': Исключение составляют случаи, когда ваш код должен работать в Python 1.5.2 (но будем надеяться, что этого не потребуется!). Сравнение типов объектов всегда следует выполнять с помощью isinstance() вместо прямого сравнения типов. Да: if isinstance(obj, int): Нет: if type(obj) is type(1): Проверяя, является ли объект строкой, помните, что он также может быть строкой Unicode! В Python 2.3 str и unicode имеют общий базовый класс basestring, поэтому можно сделать следующее: if isinstance(obj, basestring): В Python 2.2 модуль types содержит тип StringTypes, определенный для этой цели, например: from types import StringTypes if isinstance(obj, StringTypes): В Python 2.0 и 2.1 нужно поступать так: from types import StringType, UnicodeType if isinstance(obj, StringType) or \ isinstance(obj, UnicodeType) : Для последовательностей (строк, списков, кортежей) используйте тот факт, что пустые последовательности считаются ложными (False): Да: if not seq: if seq: Нет: if len(seq) if not len(seq) Не создавайте строковые литералы, в которых наличие конечных пробельных символов имеет значение. Такие символы практически неразличимы визуально,
   П.5. Дзен Python 605 и некоторые редакторы (а с недавнего времени и reindent.py) автоматически удаляют их. Не сравнивайте логические (булевы) значения с True и False при помощи ==. Да: if greeting: Нет: if greeting == True: Еще хуже: if greeting is True: В отношении авторских прав следует отметить, что этот документ стал общественным достоянием. П.4.3. Другие руководства по стилю Python Хотя PEP 8 остается самым авторитетным руководством по стилю Python, есть и другие варианты. В целом эти руководства не противоречат PEP 8, а предоставляют более широкий спектр примеров и содержат более полное объяснение того, как сделать код питоническим. Одним из рекомендуемых источников является The Elements of Python Style, доступный бесплатно по ссылке https:// mng.bz/dXlw. Еще одно полезное руководство — The Hitchhiker’s Guide to Python1 Кеннета Рейтца (Kenneth Reitz) и Тани Шлюссер (Tanya Schlusser), также бесплатно доступное по адресу https://docs.python-guide.org. По мере того как язык и навыки программистов продолжают эволюционировать, неизбежно будут появляться новые и новые руководства по этой тематике. Я рекомендую знакомиться с ними и использовать их преимущества, но лишь после того, как вы освоите PEP 8. П.5. Дзен Python Приведенный ниже документ — PEP 20, он же «Дзен Python», — представляет собой квинтэссенцию философии Python, изложенную в полушутливой форме. Он не только входит в документацию Python, но и включен в качестве «пасхалки» в сам интерпретатор: для его отображения в интерактивной среде следует выполнить команду import this. Тим Петерс (Tim Peters), один из пионеров Python-разработки, в 1999 году кратко сформулировал основополагающие принципы программирования на 1 На русском языке: Кеннет Рейтц, Таня Шлюссер. Автостопом по Python. Питер, 2017. — Примеч. ред.
   606 Приложение. Руководство по документации Python Python от BDFL (Benevolent Dictator for Life — «Великодушный пожизненный диктатор»1), сведя их к 20 афоризмам, лишь 19 из которых были записаны. Дзен Python Красивое лучше, чем уродливое. Явное лучше, чем неявное. Простое лучше, чем сложное. Сложное лучше, чем запутанное. Плоское лучше, чем вложенное. Разреженное лучше, чем плотное. Читаемость имеет значение. Особые случаи не настолько особые, чтобы нарушать правила. При этом практичность важнее безупречности. Ошибки не должны проходить незамеченными. Если только они не подавлены явно. Встретив двусмысленность, отбрось искушение угадать. Должен существовать один и желательно только один очевидный способ сделать это. Хотя он поначалу может быть и неочевиден, если вы не голландец. Сейчас лучше, чем никогда. Хотя никогда зачастую лучше, чем прямо сейчас. Если реализацию сложно объяснить — идея плоха. Если реализацию легко объяснить — идея, возможно, хороша. Пространства имен — отличная штука! Будем делать их больше! 1 Имеется в виду Гвидо ван Россум, создатель языка Python. — Примеч. пер.
Комьюнити рецензентов и переводчиков ИТ-литературы Миссия участников клуба — обеспечить высокое качество профессиональной переводной литературы в России. «Книжные дебагеры» проверяют корректность терминологии и подписей на схемах и иллюстрациях, чтобы сделать книги более понятными русскоязычному читателю. Стать участником Read IT Club может любой ИТ-специалист, готовый поделиться опытом с сообществом. присоединиться к нам
ВОСПОЛЬЗУЙТЕСЬ В ОЗ М ОЖ Н ОСТЬЮ ПРИОБРЕСТИ КНИГИ НА САЙТЕ ИЗДАТЕЛЬСТВА piter.com со скидкой по промокоду 20% PITER