Текст
                    

Python All-in-One 3rd Edition John K. Shovich Alan Simpson
Python все в одном 3-е издание Джон Шовик, PhD Алан Симпсон
УДК 004.43 ББК 32.973.26-018.2 Ш78 John K. Shovich, Alan Simpson PYTHON ALL-IN-ONE FOR DUMMIES 3rd edition For Dummies® trademark is the exclusive property of Wiley and is used under license. Original English language edition Copyright © 2024 by John Wiley & Sons, Inc. All rights reserved including the right of reproduction in whole in part in any form. This translation published by arrangement with John Wiley & Sons, Inc. Python все в одном для чайников, 3-е издание © 2024 by John Wiley & Sons, Inc. Все права, включая право на воспроизведение, защищены полностью или частично в любой форме. Этот перевод опубликован по соглашению с John Wiley & Sons, Inc. Wiley, the Wiley Publishing Logo, For Dummies, Dummies Man and related trade dress are trademarks or registered trademarks of John Wiley and Sons, Inc. and/or its affiliates in the United States and/or other countries. Used by permission. Wiley, логотип the Wiley Publishing, For Dummies, «Для чайников», Dummies Man и связанные с ними элементы являются зарегистрированными товарными знаками John Wiley and Sons, Inc. и/или ее филиалами в США и других странах. Используется с разрешения. Ш78 Шовик, Джон. Python все в одном для чайников / Джон Шовик, Алан Симпсон ; [перевод с английского В. М. Сапуновой]. — 3-е издание. — Москва : Эксмо, 2026. — 800 с. — (Для чайников). ISBN 978-5-04-201503-8 «Python. Все в одном для чайников» — это всеобъемлющее руководство для начинающих программистов, желающих освоить язык Python с нуля. В издании последовательно разобраны базовые конструкции языка, работа с данными, функции, классы и объекты, обработка исключений, а также основы работы с файлами и модулями. Особое внимание уделено практическим примерам и пошаговым инструкциям, которые помогут закрепить теорию на практике. Книга подойдет как для самостоятельного изучения, так и в качестве справочного пособия на первых этапах работы с Python. УДК 004.43 ББК 32.973.26-018.2 ISBN 978-5-04-201503-8 © Сапунова В. М., перевод на русский язык, 2026 © Оформление. ООО «Издательство «Эксмо», 2026
ОГЛАВЛЕНИЕ ВВЕДЕНИЕ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .17 Об этой книге . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Ошибочные заблуждения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Что вам нужно для будущей работы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Условные обозначения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Помимо книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Что делать дальше . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 РАЗДЕЛ 1. НАЧАЛО РАБОТЫ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Глава 1. Начало работы с Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Почему Python так популярен . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 Выбор правильной версии Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Инструменты для успешной работы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 Установка Python и VS Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .30 Установка расширения Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 Использование ИИ для написания кода на Python . . . . . . . . . . 32 Использование GitHub Copilot . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 Использование Jupyter Notebook для программирования . . . . . . . . 35 Глава 2. Использование интерактивного режима, получение справки и создание приложений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .38 Использование интерактивного режима Python . . . . . . . . . . . . . . . . . . 39 Открытие терминала . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Определение версии Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .40 Вход в интерпретатор Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Ввод команд . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 Использование встроенной справки Python . . . . . . . . . . . . . . . 42 Выход из интерактивной справки . . . . . . . . . . . . . . . . . . . . . . . . . . 44 Поиск помощи в сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Обилие бесплатного материала . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 Организация папки с кодом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 Набор, редактирование и отладка кода Python . . . . . . . . . . . . . . . . . . 46 Написание кода на Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .48 Сохранение кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 Запуск Python в VS Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Изучение основ отладки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 Закрытие файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Написание кода в Jupyter Notebook . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Создание папки для Jupyter Notebooks . . . . . . . . . . . . . . . . . . . 52 Создание и сохранение блокнота Jupyter . . . . . . . . . . . . . . . . . 53 Глава 3. Элементы и синтаксис Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Дзен Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 Знакомство с объектно ориентированным программированием . . .58 Почему отступы имеют огромное значение . . . . . . . . . . . . . . . . . . . . . . 59 Использование модулей Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 Понимание синтаксиса импорта модулей . . . . . . . . . . . . . . . . . . 62 Использование псевдонима с модулями . . . . . . . . . . . . . . . . . . . 63 Глава 4. Создание вашего первого приложения на Python . . . . . . . . . . . . . . . . . . . 64 Открытие файла приложения Python . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 Написание и использование комментариев в Python . . . . . . . . . . . . . 66 Понимание типов данных в Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .68 Слова (строки) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 Логический тип данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Работа с операторами Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Арифметические операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 Операторы сравнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 Логические операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Создание и использование переменных . . . . . . . . . . . . . . . . . . . . . . . . . 77 Имена переменных в Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 Создание переменных в коде . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Манипуляции с переменными . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 Сохранение работы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 Запуск вашего Python-приложения в VS Code . . . . . . . . . . . . 81 Понимание синтаксиса и его важности . . . . . . . . . . . . . . . . . . . . . . . . . . 83 РАЗДЕЛ 2. ПОНИМАНИЕ БАЗОВЫХ КОНСТРУКЦИЙ PYTHON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Глава 1. Работа с числами, текстом и датами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Работа с числами, текстом и датами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Вычисления с помощью функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Дополнительные математические функции . . . . . . . . . . . . . . . . . . . . . . 95 Форматирование чисел . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .98 Форматирование с помощью f-строк . . . . . . . . . . . . . . . . . . . . . .98 6
Отображение денежных сумм . . . . . . . . . . . . . . . . . . . . . . . . . . .100 Форматирование процентных чисел . . . . . . . . . . . . . . . . . . . . . 101 Форматирование ширины и выравнивания . . . . . . . . . . . . . . . 105 Работа с необычными числами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .108 Двоичные, восьмеричные и шестнадцатеричные числа . . . . . . . . . .108 Комплексные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Манипуляции со строками . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .111 Манипулирование строками с помощью методов . . . . . . . . . . 117 Знакомство с датами и временем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 Работа с датами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 Работа со временем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 Учет часовых поясов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 Работа с часовыми поясами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 Глава 2. Управление действиями . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 Основные операторы управления действием программы . . . . . . . . 138 Принятие решений с помощью if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 Добавление else к логике if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 Обработка нескольких операторов else с помощью elif . . . . 146 Тернарные операции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 Повторение процесса с помощью цикла for . . . . . . . . . . . . . . . . . . . . 150 Перебор чисел в диапазоне . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 Перебор строки в цикле . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 Перебор элементов списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 Выход из цикла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 Цикл с continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 Вложенные циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 Циклы while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 Перезапуск циклов while с помощью continue . . . . . . . . . . . . . 161 Прерывая циклы while с break . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 Глава 3. Быстрая работа со списками и кортежами . . . . . . . . . . . . . . . . . . . . . . . . . . 165 Определение и использование списков . . . . . . . . . . . . . . . . . . . . . . . . 165 Обращение к элементам списка по позиции . . . . . . . . . . . . . . 166 Перебор элементов списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 Проверка наличия элемента в списке . . . . . . . . . . . . . . . . . . . . . 169 Определение длины списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 Добавление элемента в конец списка . . . . . . . . . . . . . . . . . . . . 170 Вставка элемента в список . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 Изменение элемента в списке . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 Объединение списков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 Удаление элементов списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 Очистка списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 Подсчет количества вхождений элемента в список . . . . . . . . 177 7
Поиск индекса элемента списка . . . . . . . . . . . . . . . . . . . . . . . . . . 178 Сортировка списков по алфавиту . . . . . . . . . . . . . . . . . . . . . . .180 Обратный список . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 Копирование списка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 Использование списковых включений . . . . . . . . . . . . . . . . . . . . 184 Что такое кортеж и зачем он нужен? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 Работа с множествами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 Глава 4. Работа с большими данными с помощью словарей . . . . . . . . . . . . . . 193 Понимание структуры словарей данных . . . . . . . . . . . . . . . . . . . . . . . . 194 Создание словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 Доступ к данным словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 Определение длины словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 Проверка наличия ключа в словаре . . . . . . . . . . . . . . . . . . . . . . 201 Получение данных из словаря с помощью get() . . . . . . . . . . .202 Изменение значения ключа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .203 Добавление или изменение данных словаря . . . . . . . . . . . . . .204 Перебор элементов словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .206 Методы словарей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .209 Копирование словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 Удаление элементов словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 Развлекаемся с многоключевыми словарями . . . . . . . . . . . . . . 214 «Загадочные» методы fromkeys и setdefault . . . . . . . . . . . . . . 217 Вложенные словари . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .220 Создание словаря с помощью включений . . . . . . . . . . . . . . . . 223 Глава 5. Управление большими фрагментами кода . . . . . . . . . . . . . . . . . . . . . . . . .224 Создание функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225 Комментирование функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 Передача информации в функцию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 Необязательные параметры со значениями по умолчанию 230 Передача нескольких значений в функцию . . . . . . . . . . . . . . . 231 Использование именованных аргументов (kwargs) . . . . . . . . 233 Передача нескольких значений в списке . . . . . . . . . . . . . . . . .235 Передача произвольного количества аргументов . . . . . . . .239 Возвращение значений из функций . . . . . . . . . . . . . . . . . . . . . . . . . . . .240 Анонимные функции (лямбда-функции) . . . . . . . . . . . . . . . . . . . . . . . . 242 Глава 6. Python и классы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 Осваиваем классы и объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 Создание класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .255 Создание экземпляра класса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 Наделение объекта атрибутами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .258 8
Создание экземпляра класса . . . . . . . . . . . . . . . . . . . . . . . . . . .260 Изменение значения атрибута . . . . . . . . . . . . . . . . . . . . . . . . . . .263 Определение атрибутов со значениями по умолчанию . . .264 Добавление методов в класс . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 Передача параметров в методы . . . . . . . . . . . . . . . . . . . . . . . . . .269 Вызов метода класса через имя класса . . . . . . . . . . . . . . . . . . 271 Использование переменных класса . . . . . . . . . . . . . . . . . . . . . . 273 Использование методов класса . . . . . . . . . . . . . . . . . . . . . . . . . . 275 Использование статических методов . . . . . . . . . . . . . . . . . . . . 278 Понимание наследования классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 Создание базового (основного) класса . . . . . . . . . . . . . . . . . .282 Определение подкласса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .284 Переопределение значения по умолчанию в подклассе . .286 Добавление новых параметров в подкласс . . . . . . . . . . . . . . . 287 Вызов метода базового класса . . . . . . . . . . . . . . . . . . . . . . . . . . 292 Использование одного и того же имени дважды . . . . . . . . . .293 Глава 7. Обход ошибок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .298 Понимание исключений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .299 Корректная обработка ошибок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .302 Конкретизация исключений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304 Предотвращение сбоев приложения . . . . . . . . . . . . . . . . . . . . . . . . . . .307 Добавление блока else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 Использование try . . . except . . . else . . . finally . . . . . . . . . . . . . . . . . . . . 312 Создание собственных исключений . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 На рисунке 7 .7 показан полный код этого примера . . . . . . . . . 317 РАЗДЕЛ 3. РАБОТА С БИБЛИОТЕКАМИ . . . . . . . . . . . . . . . . . . . . 319 Глава 1. Работа с внешними файлами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321 Понимание текстовых и двоичных файлов . . . . . . . . . . . . . . . . . . . . . 322 Открытие и закрытие файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 Чтение содержимого файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332 Перебор содержимого файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .334 Перебор с помощью readlines() . . . . . . . . . . . . . . . . . . . . . . . . . .334 Построчное чтение с помощью readline() . . . . . . . . . . . . . . . . .336 Добавление в конец файла или перезапись . . . . . . . . . . . . . .338 Использование tell() для определения позиции указателя . .340 Перемещение указателя с помощью seek() . . . . . . . . . . . . . . . 341 Чтение и копирование двоичного файла . . . . . . . . . . . . . . . . . . . . . . .342 Работа с CSV-файлами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .345 Открытие CSV-файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 9
Преобразование строк . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350 Преобразование в целые числа . . . . . . . . . . . . . . . . . . . . . . . . .352 Преобразование в дату . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .352 Преобразование в логический тип . . . . . . . . . . . . . . . . . . . . . . . .355 Преобразование в числа с плавающей точкой . . . . . . . . . . .355 Преобразование данных из CSV в объекты и словари . . . . . . . . .358 Импорт CSV в объекты Python . . . . . . . . . . . . . . . . . . . . . . . . . .358 Импорт CSV в словари Python . . . . . . . . . . . . . . . . . . . . . . . . . . .364 Глава 2. Работа с данными JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .368 Организация данных JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .369 Понимание сериализации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372 Загрузка данных из JSON-файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373 Преобразование даты Excel в дату JSON . . . . . . . . . . . . . . . . 375 Перебор JSON-файла с ключами . . . . . . . . . . . . . . . . . . . . . . . 377 Преобразование временных меток Firebase в даты Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379 Загрузка JSON без ключей из строки Python . . . . . . . . . . . . 380 Загрузка ключевых JSON-данных из строки Python . . . . . .382 Изменение данных JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .384 Удаление данных из словаря . . . . . . . . . . . . . . . . . . . . . . . . . . . .384 Выгрузка данных Python в JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .386 Глава 3. Взаимодействие с интернетом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390 Как работает интернет . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 Разбираемся в загадочном URL . . . . . . . . . . . . . . . . . . . . . . . . . . . 391 Исследование HTTP-заголовков . . . . . . . . . . . . . . . . . . . . . . . . . .393 Открытие URL из Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .395 Отправка веб-запросов с помощью Python . . . . . . . . . . . . . . . 397 Веб-скрейпинг с помощью Python . . . . . . . . . . . . . . . . . . . . . . .398 Парсинг части страницы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .402 Сохранение обработанного контента . . . . . . . . . . . . . . . . . . . . 403 Сохранение собранных данных в JSON-файл . . . . . . . . . . . . 406 Сохранение собранных данных в CSV-файл . . . . . . . . . . . . .407 Глава 4. Библиотеки, пакеты и модули . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .410 Понимание стандартной библиотеки Python . . . . . . . . . . . . . . . . . . . . 410 Использование функции dir() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412 Использование функции help() . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 Изучение встроенных функций . . . . . . . . . . . . . . . . . . . . . . . . . . . 415 Изучение пакетов Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416 Импорт модулей Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418 Создание собственных модулей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 10
Управление виртуальными окружениями . . . . . . . . . . . . . . . . . . . . . . .424 Создание виртуального окружения . . . . . . . . . . . . . . . . . . . . . .425 Активация виртуального окружения . . . . . . . . . . . . . . . . . . . . . .426 Создание файла requirements .txt . . . . . . . . . . . . . . . . . . . . . . . . 427 Деактивация виртуального окружения . . . . . . . . . . . . . . . . . . .428 РАЗДЕЛ 4. ИСПОЛЬЗОВАНИЕ ИСКУССТВЕННОГО ИНТЕЛЛЕКТА . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431 Глава 1. Демистификация искусственного интеллекта . . . . . . . . . . . . . . . . . . . . . .433 ИИ — это набор методов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .434 Нейронные сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .435 Машинное обучение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441 TensorFlow — фреймворк для глубокого обучения . . . . . . . . .443 Текущие ограничения ИИ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .444 Глава 2. Создание нейронной сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .445 Понимание нейронных сетей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447 Слои нейронов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .448 Веса и смещения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 Функция активации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 Функция потерь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .452 Построение простой нейронной сети на Python . . . . . . . . . . . . . . . .453 Программный код нейронной сети на Python . . . . . . . . . . . . .454 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .458 Запуск кода нейронной сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . .463 Использование TensorFlow для той же нейронной сети . . . .466 Установка библиотеки TensorFlow для Python . . . . . . . . . . . . . 467 Построение нейронной сети на Python с использованием TensorFlow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .468 Загрузка данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .470 Определение модели нейронной сети и ее слоев . . . . . . . . . .470 Компиляция модели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .470 Настройка и обучение модели . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 Оценка модели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 472 Проверка результатов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 Изменение на трехслойную нейронную сеть в TensorFlow и Keras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480 Глава 3. Практическое машинное обучение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .483 Поиск решений там, где их нет . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .484 11
Создание нейронной сети для распознавания типов одежды . . . .486 Настройка программной среды . . . . . . . . . . . . . . . . . . . . . . . . . .487 Получение данных из набора Fashion-MNIST . . . . . . . . . . . . . 490 Обучение сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 490 Тестирование сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 490 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .492 Проверка результатов обучения и оценки . . . . . . . . . . . . . . . . .494 Тестирование отдельного тестового изображения . . . . . . . .495 Тестирование на внешних изображениях . . . . . . . . . . . . . . . . . 497 Результаты, раунд 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .498 Код модели СНС . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500 Результаты, раунд 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503 Визуализация с помощью Matplotlib . . . . . . . . . . . . . . . . . . . . . . . . . . 503 Изучаем машинное обучение глубже . . . . . . . . . . . . . . . . . . . . . . . . . . .507 Глава 4. Игра с искусственным интеллектом — на картах, микроконтроллерах и в облаке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 508 Изучаем ограничения Raspberry Pi и ИИ . . . . . . . . . . . . . . . . . . . . . . . 509 Добавление аппаратного ИИ к Raspberry Pi . . . . . . . . . . . . . . . . . . . . 512 Исследуем способы использования ИИ в облаке . . . . . . . . . . . . . . . 514 Google Cloud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515 Amazon Web Services . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 516 IBM Cloud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517 Microsoft Azure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517 ИИ на графической карте . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518 Где найти больше интересного про ИИ на Python . . . . . . . . . . . . . . . 521 РАЗДЕЛ 5. DATA SCIENCE ИЛИ НАУКА О ДАННЫХ . . . . . . . 523 Глава 1. Понимание пяти областей науки о данных . . . . . . . . . . . . . . . . . . . . . . . . . .525 Работа с по-настоящему большими данными . . . . . . . . . . . . . . . . . . .526 Объем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .526 Многообразие . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527 Скорость . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .528 Управление объемом, многообразием и скоростью данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .529 Полный вперед: пятиэтапный процесс науки о данных . . . . . . . . . .529 Сбор данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530 Обработка данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530 Анализ данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 531 Представление результатов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .533 Поддержка данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .534 12
Глава 2. Исследование больших данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .535 Знакомство с NumPy, pandas и Matplotlib . . . . . . . . . . . . . . . . . . . . . . .536 NumPy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .536 Matplotlib . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .538 Выполнение вашего первого проекта по науке о данных . . . . . . . .539 Бриллианты — лучшие друзья специалиста по анализу данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .539 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .543 Визуализация данных с помощью Matplotlib . . . . . . . . . . . . . .544 Глава 3. Использование больших данных из Google Cloud . . . . . . . . . . . . . . .552 Что такое большие данные? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .553 Понимание Google Cloud и BigQuery . . . . . . . . . . . . . . . . . . . . . . . . . .553 Платформа Google Cloud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .554 BigQuery от Google . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .554 Компьютерная безопасность в облаке . . . . . . . . . . . . . . . . . . .555 Регистрация в BigQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .556 Чтение больших данных Medicare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .556 Настройка проекта и аутентификации . . . . . . . . . . . . . . . . . . . . 557 Первый код для работы с большими данными . . . . . . . . . . . . .559 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .562 Проведение анализа данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . .565 Процент оплаты по штатам . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567 Теперь перейдем к визуализации . . . . . . . . . . . . . . . . . . . . . . . . .568 Поиск самого загрязненного города в мире в режиме реального времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .569 РАЗДЕЛ 6. ВЗАИМОДЕЙСТВИЕ С ОБОРУДОВАНИЕМ . . . . 571 Глава 1. Введение в физические вычисления . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .573 Физические вычисления — это действительно увлекательно . . . . . 574 Что такое Raspberry Pi? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 574 Создание проектов, которые двигаются и взаимодействуют с окружающим миром . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 577 Восприятие окружающей среды с помощью Raspberry Pi . . . . . . . 579 Порты GPIO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 579 Библиотеки GPIO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 580 Покупка и сборка оборудования для «Hello World» . . . . . . . . 581 Управление светодиодом с помощью Python . . . . . . . . . . . . . . . . . . .585 Но это еще не все . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .587 13
Глава 2. Никакой пайки! Использование разъемов Grove для конструирования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 590 Работа с системой Grove . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 591 Выбор базового блока Grove . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 591 Базовый блок Arduino . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .592 Базовый блок Raspberry Pi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .593 Защита от ошибок с помощью разъема Grove . . . . . . . . . . . . .594 Разъемы Grove . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597 Цифровой Grove — все о единицах и нулях . . . . . . . . . . . . . . . . 597 Grove аналоговый: Когда единиц и нулей недостаточно . . . .599 Grove UART (или последовательный интерфейс) — побитовая передача . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 600 Grove I2C — использование I2C для понимания окружающего мира . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .602 Подключение с помощью кабелей Grove . . . . . . . . . . . . . . . . . . . . . . 604 GPS-модуль Adafruit Ultimate . . . . . . . . . . . . . . . . . . . . . . . . . . 606 Глава 3. Считывание данных об окружающем мире . . . . . . . . . . . . . . . . . . . . . . 608 Понимание I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 609 Включение I2C на Raspberry Pi . . . . . . . . . . . . . . . . . . . . . . . . . . 611 Оборудование для измерения температуры и влажности . . 612 Считывание температуры и влажности с I2C-устройства с помощью Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 616 Разбор программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619 Измерение кислорода и пламени . . . . . . . . . . . . . . . . . . . . . . . . . 622 Аналого-цифровые преобразователи (АЦП) . . . . . . . . . . . . . .623 Датчик кислорода Grove . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .624 Подключаем оборудование для эксперимента с кислородом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .625 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .628 Интерпретация результатов . . . . . . . . . . . . . . . . . . . . . . . . . . . . .629 Что дальше? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 631 Глава 4. Приводим механизмы в движение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .632 Изучаем электродвигатели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .633 Малые двигатели постоянного тока . . . . . . . . . . . . . . . . . . . . .633 Сервоприводы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .634 Шаговые двигатели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .635 Управление двигателем постоянного тока . . . . . . . . . . . . . . . . . . . . . .636 Драйвер двигателя Grove I2C . . . . . . . . . . . . . . . . . . . . . . . . . . . 637 Программное обеспечение для двигателей постоянного тока на Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 640 14
Управление сервоприводом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .644 Программное обеспечение сервопривода на Python . . . . . .648 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .649 Заставляем шаговый двигатель шагать . . . . . . . . . . . . . . . . . . . . . . . . . 651 Программное обеспечение для шагового двигателя на Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .659 Разбор кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .662 РАЗДЕЛ 7. СОЗДАНИЕ РОБОТОВ . . . . . . . . . . . . . . . . . . . . . . . . . . . 663 Глава 1. Введение в робототехнику . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .665 Робот не всегда похож на человека . . . . . . . . . . . . . . . . . . . . . . . . . . . .665 Не у каждого робота есть руки или колеса . . . . . . . . . . . . . . . . . . . . .666 Робот-пекарь от Wilkinson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .668 Бакстер, робот-бариста . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .668 Тостер Griffin с поддержкой Bluetooth . . . . . . . . . . . . . . . . . . .670 Понимание основных составляющих робота . . . . . . . . . . . . . . . . . . . . 671 Компьютеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 671 Двигатели и актуаторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 672 Коммуникации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 672 Датчики . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673 Программирование роботов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673 Глава 2. Создание вашего первого робота на Python . . . . . . . . . . . . . . . . . . . . . . . . 675 Знакомство с марсоходом PiCar-B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 676 Что нужно для сборки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 677 Знакомство с компонентами робота . . . . . . . . . . . . . . . . . . . . . .678 Сборка робота . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 688 Тестирование робота . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 690 Калибровка сервоприводов . . . . . . . . . . . . . . . . . . . . . . . . . . . . 690 Подготовка к запуску тестов вашего ровера на Python . . . .693 Установка программного обеспечения для тестирования PiCar-B на Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .693 Тестовый код PiCar-B на Python . . . . . . . . . . . . . . . . . . . . . . . . . .694 Тестирование видео с камеры Pi . . . . . . . . . . . . . . . . . . . . . . . . . .695 Глава 3. Программирование вашего робота-ровера . . . . . . . . . . . . . . . . . . . . . . . . . .697 Создание простого высокоуровневого интерфейс а на Python . . 697 Функция motorForward() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .698 Функция wheelsLeft() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .699 Функция wheelsPercent() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .699 Выполнение одиночного движения с помощью Python . . . . . . . . . 700 15
Функции класса RobotInterface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701 Функции управления передними светодиодами . . . . . . . . . . .702 Функции для управления светодиодной лентой . . . . . . . . . . .703 Функция ультразвукового датчика расстояния . . . . . . . . . .706 Основные функции управления двигателем . . . . . . . . . . . . . 707 Функции сервоприводов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .708 Общая функция для сервоприводов . . . . . . . . . . . . . . . . . . . . . . 711 Тестирование интерфейса робота на Python . . . . . . . . . . . . . . . . . . . 711 Координация движений моторов с датчиками . . . . . . . . . . . . . . . . . . 715 Создание Python-мозга для нашего робота . . . . . . . . . . . . . . . . . . . . . 719 Обзор программного обеспечения Adeept . . . . . . . . . . . . . . . . . . . . . . 727 Что дальше? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 728 Глава 4. Использование искусственного интеллекта в робототехнике . . . . . 729 Проекты этой главы: все для собак . . . . . . . . . . . . . . . . . . . . . . . . . . . . .730 Настройка первого проекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 731 Машинное обучение с использованием TensorFlow . . . . . . . . . . . . . . 732 Код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735 Как работает код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 737 Результаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 741 Тестирование обученной сети . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 742 Код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 743 Как работает код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 745 Результаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 747 Распознавание кошек и собак нашим роботом . . . . . . . . . . . . . . . . . .750 Код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 751 Как это работает . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 754 Результаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 754 Настройка второго проекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 757 Программа FindAndChaseTheBall .py на Python . . . . . . . . . . . . . . . . . .758 Структура программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 759 Поток ультразвукового датчика . . . . . . . . . . . . . . . . . . . . . . . . . .760 Поток отображения видео . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .760 Поток анализа кадров OpenCV . . . . . . . . . . . . . . . . . . . . . . . . . . . 761 Основная программа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 766 Конфигурация программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 766 Настройка цвета мяча . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 767 Погоня за мячом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 769 Примечания к программе . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 770 ИИ и будущее робототехники . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 772 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 773 Об авторах . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 794 Благодарности авторов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 794 Посвящение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 795
Введение В озможности Python огромны. Этот язык программирования с каждым годом все больше на слуху: в 2017 году, согласно рейтингу журнала IEEE Spectrum, он был признан самым популярным языком в мире. Сейчас Python по-прежнему входит в число наиболее широко используемых языков, если не является самым распространенным. Это особенно справедливо для таких актуальных областей, как искусственный интеллект, data science (наука о данных) и робототехника. Благодаря своей доступности и простоте, Python становится первым выбором для тех, кто только начинает постигать азы программирования. Это объясняется рядом причин: простым и понятным синтаксисом, легкостью в обработке данных, а также поддержкой сильного сообщества пользователей и огромным спектром библиотек и возможностей. Об этой книге Эта книга — справочное руководство, которое поможет вам изучить Python и научиться применять его в современных областях, таких как data science, искусственный интеллект, физические основы вычислительной техники и робототехника. Если вы хотите погрузиться в мир увлекательного программирования — эта книга для вас. В ней вы найдете прочную основу для тем, с которыми будете работать в будущем. «Python все в одном для чайников, 3-е издание» станет вашим гидом в языке Python. Изучив основы, вы отправитесь в увлекательное путешествие по миру интересных библиотек и технологий: Raspberry Pi, робототехника, ИИ, data science и многое другое. Все они тесно связаны с Python. Когда вы принимаетесь за работу над новыми проектами и технологиями, Python, ваш верный помощник, будет всегда рядом с разнообразным набором библиотек, готовых к использованию. Эта книга крайне практична — от начала до конца вы будете работать с примерами и кодом. Предполагается, что вы будете вводить код, запускать его, а затем модифицировать для решения своих задач. Вы не просто Введение 17
покупаете робота — вы создаете его, чтобы понять все составляющие части и разобраться, как Python взаимодействует с роботом для управления его двигателями и датчиками. Сложность таких областей, как искусственный интеллект, data science и робототехника, преодолима с помощью Python: он делает ИИ доступным, упрощает работу с данными и предоставляет код для управления роботом. Более того, Python позволяет объединять эти области, например, применяя искусственный интеллект в робототехнике. В этой книге мы шаг за шагом проведем вас от основ Python к программированию искусственного интеллекта. Вы освоите машинное обучение и нейронные сети на практике — с помощью Python, TensorFlow и реальных проектов, чтобы работать с настоящим программным обеспечением, а не просто с учебными примерами. Затем вместе с Python читатели отправятся в захватывающий мир больших данных и data science, где на практике применят свои навыки для анализа больших и публичных датасетов, таких как медицинских или относящихся к окружающей среде. Наконец, вы познакомитесь с волшебным миром физических вычислений. С помощью недорогого, компактного и невероятно популярного компьютера Raspberry Pi мы покажем вам, как использовать Python для управления двигателями и считывания показаний датчиков. Это подводит нас к кульминации — разделу «Создание роботов», где вы построите робота и будете управлять им с помощью Python и написанных вами программ, даже используя искусственный интеллект. Это даже отдаленно не похоже на радиоуправляемую машинку из вашего детства! В этой книге вас ждет — наука о данных на Python, робототехника и искусственный интеллект в увлекательных практических проектах. Эта книга не обещает сделать вас экспертом, но даст вам отличную базу: познакомит с терминологией и покажет широкие возможности Python во всех этих сферах. Приятного чтения и успехов в дальнейшем обучении! Ошибочные заблуждения Мы предполагаем, что вы умеете пользоваться компьютером на базовом уровне. Если вы можете включить компьютер и работать с мышью, вы готовы к этой книге. Мы также исходим из того, что вы еще не умеете программировать, хотя к концу книги у вас появятся значительные навыки в этом. Если же мы ошибаемся, и вы уже знаете Python (или любой 18 Python все в одном для чайников
другой язык программирования), смело переходите к разделу 4 — там вы найдете для себя много нового. Наша цель — помочь вам освоить язык Python, а затем познакомить с удивительными технологиями и устройствами, которые его используют. Ко всем материалам мы даем наглядные примеры. Если у вас возникнут трудности, рекомендуем поискать информацию в интернете, изучить дополнительные материалы и после этого вернуться к книге. Что вам нужно для будущей работы Для выполнения проектов из разделов с 4 -7 вам понадобится стартовый набор на базе Raspberry Pi 3B+ https://amzn.to/2WzYdoY или 4B https:// amzn.to/3nIH8W8. Также потребуются компоненты, перечисленные в этом разделе и сгруппированные по соответствующим темам. Использование Raspberry Pi 4B в роботе, описанном в разделе 7, значительно сократит время работы от батареи. Кроме того, с некоторыми типами аккумуляторов робот может не обеспечить Pi 4B достаточной ПОДСКАЗКА мощности для запуска. Раздел 6 Для выполнения заданий из этого раздела вам понадобятся: » Плата Pi2Grover на https://shop.switchdoc.com (на shop. switchdoc.com действует скидка $5 .00 по промокоду PI2DUMMIES) . » Модуль Grove с синим светодиодом в комплекте с кабелем на сайте www.seeedstudio.com . » Набор соединительных проводов Grove со штыревыми разъемами (кабели для преобразования разъемов Grove на 4-контактный штыревой разъем) на сайтах https://mouser.com или www.seeedstudio.com . » Набор кабелей Grove с гнездовыми разъемами на сайтах mouser.com или www.seeedstudio.com . » Датчик температуры и влажности Grove HDC1080 (I2C) доступен на сайтах Mouser, Amazon или Seeed Studio . Обратите внимание: если вы покупаете на Amazon датчик не от Grove, вам потребуется кабель с гнездовым разъемом Введение 19
для Grove (как описано в главе 2 раздел 6) . Такой кабель также можно приобрести на Mouser или Seeed Studio . » Grove датчик кислорода можно приобрести на сайтах www.seeedstudio.com или amazon.com . » Преобразователь Pi2Grover (Raspberry-Pi-to-Grove) и четырехканальный 16-битный АЦП (аналого-цифровой преобразователь) Grove доступны в магазине shop.switchdoc.com . » Драйвер двигателя Grove I2C (в комплекте с кабелем) . Его можно заказать на www.seeedstudio.com или Amazon.com . » Два небольших двигателя постоянного тока, такие как модель с Adafruit (www.adafruit.com/product/711) . Они также есть в наличии на Amazon.com . » Микросервопривод SG90 Эти сервоприводы недорогие и широко доступны на eBay.com или Amazon.com . Благодаря низкой цене, два или более привода обойдутся вам меньше чем в $10 . » Шаговый двигатель 28BYJ-48 с драйвером ULN2003 на 5V можно найти, например, на eBay или Amazon (https://amzn. to/2BuNDVl) . Этот тип двигателя недорогой, поэтому вы можете приобрести пять штук всего за $12 . Убедитесь, что покупаете модели именно с платами драйверов, как те, что представлены по ссылке на Amazon . Раздел 7 Для проектирования робота из раздела 7 вам потребуется: 20 » Убедитесь, что вы покупаете именно марсоход Adeept Raspberry Pi PiCar-B, а не модель PiCar- A . Для поиска используйте полное название: «Adeept Mars Rover PiCar-B» . Приобрести PiCar-B можно на сайтах Amazon.com (https:// amzn.to/36dukPU), eBay.com и Adeept.com . » Два литий-полимерных аккумулятора формата 18650: напряжение 3 .7 V, емкость 5000 мА·ч . Их можно купить на Amazon.com и во многих других местах . Python все в одном для чайников
Условные обозначения Какая же книга серии «Для чайников» обойдется без значков, которые указывают на действительно полезную информацию, способную ускорить ваше продвижение? Ниже мы кратко описываем назначение каждого значка, используемого в этой книге. Значок «Совет» предлагает полезную рекомендацию, которая поможет облегчить вашу работу. СОВЕТ Этот значок отмечает интересный и полезный факт — то, что вам стоит запомнить для дальнейшего использования. ЗАПОМНИТЕ Значок «Осторожно» указывает на скрытую опасность или «подводный камень». Увидев его, будьте внимательны и действуйте осторожно. ОСТОРОЖНО ПРИМЕЧАНИЕ Этот значок сообщает, что рядом находится технический материал. Если вы не настроены на технические детали, этот фрагмент можно пропустить. Помимо книги Помимо материала, содержащегося в печатной или электронной версии этой книги, вы также получите доступ к полезным онлайн-материалам. Чтобы ознакомиться с ними, просто перейдите на сайт www.dummies.com и введите в поле поиска фразу «Python All-in-One For Dummies cheat sheet». Кроме того, мы разместили весь исходный код для примеров из этой книги по адресу: www.dummies.com/go/pythonaiofd3e . Нажмите на вкладку Downloads в левой колонке, и вы увидите ссылки на код, сгруппированные по соответствующим мини-книгам. Введение 21
В книге используются два загружаемых файла-образа для создания SD-карт компьютеров Raspberry Pi. Они также доступны по адресу: www.dummies.com/go/pythonaiofd3e: » » 2023.09–23.PAIOSDCardV2.zip — используется в разделах 4 - 5 -6 и большей части раздела 7 . 2023.09–23.BusterPAIOSDcardV1.zip — используется в последней части раздела 7 . Что делать дальше Руководство построено так, что вы можете читать главы или разделы в произвольном порядке — в зависимости от того, какие темы вас интересуют больше всего. Решать, куда двигаться дальше, предстоит вам! Раздел 1 — отличное место для начала, если вы никогда раньше не работали с Python. Знание основ и общей терминологии поможет вам при чтении последующих глав, где эти термины и команды используются постоянно.
1 НАЧАЛО РАБОТЫ
В ЭТОМ РАЗДЕЛЕ Глава 1: Начало работы с Python Глава 2: Использование интерактивного режима, получение справки и создание приложений Глава 3: Элементы и синтаксис Python Глава 4: Создание вашего первого приложения на Python
В ЭТОЙ ГЛАВЕ: » Почему Python считается одним из самых популярных языков программирования » Какие инструменты помогают эффективно работать с Python » Как писать программы на Python в среде VS Code » Как использовать Jupyter Notebook для написания и выполнения кода Python Гл а в а 1 Начало работы с Python Е сли вы читаете эту главу, вероятно, вы и так понимаете, что Python — отличный язык для тех, кто хочет найти хорошую работу в программировании, расширить свои навыки в области передовых технологий (искусственный интеллект, машинное обучение, data science или робототехника) или просто разрабатывать приложения. Поэтому мы не станем убеждать вас в его преимуществах — Python говорит сам за себя. В основе нашего подхода — практика. В отличие от многих учебников по программированию, которые написаны так, будто вы уже опытный разработчик, мы не будем пропускать моменты, кажущиеся очевидными. Эта книга отличается тем, что мы не предполагаем, будто вы уже программируете на Python или каком-либо другом языке. Мы исходим лишь из того, что вы умеете пользоваться компьютером и знакомы с основами, такими как файлы и папки. Мы также понимаем: вы не собираетесь устраиваться в кресле у камина и читать страницу за страницей теоретический материал о Python, как какой-нибудь скучный роман. У вас нет на это времени. Поэтому мы сразу перейдем к делу и сосредоточимся на практике, потому что только так и можно по-настоящему научиться. Мы никогда не видели, чтобы ГЛАВА 1 Начало работы с Python 25
кто-то прочитал книгу о Python, а потом сел за компьютер и начал профессионально программировать. Человеческий мозг так не работает — мы учимся на практике и через повторение, а для этого нужно работать руками. Почему Python так популярен Как и обещали, мы не будем долго убеждать вас в преимуществах Python. Вместо этого кратко перечислим причины его популярности. Популярность Python во многом обусловлена его соответствием требованиям современных направлений IT. Ключевые из них — машинное обучение, робототехника, искусственный интеллект и data science — останутся ведущими технологиями в обозримом будущем. Python уже обладает богатым набором инструментов для этих сфер, тогда как многие старые языки не успевают за их стремительным развитием. Подобно тому, как существуют разные марки зубной пасты, шампуня, автомобилей и многих других товаров, так же существуют и разные языки программирования. Среди них — Java, C, C++ (произносится «си плюс плюс») и C# (произносится «си шарп»). Все они служат одной цели — программированию, — так же как все марки зубной пасты остаются зубной пастой. А вот основные причины, по которым Python сегодня так популярен: » » Изучать Python относительно легко . » Python предоставляет больше готовых инструментов для работы с актуальными современными технологиями, такими как data science, машинное обучение, искусственный интеллект и робототехника, чем большинство других языков программирования . Все необходимое для изучения (и работы) на Python — бесплатно . HTML, CSS И JAVASCRIPT Вы, возможно, слышали о таких языках программирования, как HTML, CSS и JavaScript . Однако это не традиционные языки программирования для создания приложений или другого универсального программного обеспечения . HTML и CSS — это специали- 26 РАЗДЕЛ 1 Начало работы
зированные инструменты для верстки веб-страниц . Что касается JavaScript, то, хотя это и полноценный язык программирования, он в значительной степени заточен под веб-разработку и не относится к той же категории языков общего назначения, что Python или Java . Таким образом, если ваша цель — проектировать и создавать веб-сайты, вам в любом случае придется изучить HTML, CSS и JavaScript, независимо от того, знакомы ли вы уже с Python или каким-либо другим языком . Выбор правильной версии Python В мире существуют разные версии Python, и у многих начинающих программистов это вызывает закономерные вопросы: » » » Почему существуют разные версии? Чем они отличаются друг от друга? Какую версию выбрать для изучения? Вопросы отличные! Давайте начнем с первого. Версия программы похожа на год выпуска автомобиля. Вы можете выбрать Ford Mustang 1968 года, 1990, 2019 или 2020. Все они — Ford Mustang. Разница лишь в том, что модель с более поздним годом выпуска — самая современная. Она отличается от предыдущих версий тем, что в ней учтены улучшения, основанные на опыте эксплуатации старых моделей, а также добавлены новые функции, актуальные для своего времени. Языки программирования (как и большинство других программных продуктов) работают по тому же принципу. Однако, как правило, мы не привязываем их к годам выпуска, поскольку они выходят не ежегодно, а по мере готовности. Тем не менее принцип остается тем же: версия с самым высоким номером является новейшей и самой свежей. Она включает улучшения, основанные на опыте работы с более ранними версиями, а также функции, актуальные для текущего момента. По аналогии с десятичной точкой в денежных суммах, которая отделяет доллары от центов, точка в номере версии указывает на степень изменений в программном обеспечении. Значительные изменения обычно приводят к увеличению целой части номера. Менее существенные изменения отражаются в цифрах после десятичной точки. В таблице 1.1 ГЛАВА 1 Начало работы с Python 27
показано, как увеличивались номера версий Python с течением времени. Мы пропустили несколько промежуточных релизов, поскольку нет необходимости детально разбираться в различиях между каждой версией. Эта таблица призвана лишь продемонстрировать простой принцип: чем новее версия, тем выше ее номер — это все, что важно знать. Если вы были внимательны, то могли заметить, что версия 3.0 появилась в декабре 2008 года, а версия 2.7 вышла только в 2010. Возникает вопрос: если версии подобны годам выпуска автомобилей, тогда как такое возможно? Таблица 1.1. Примеры версий Python и даты их выпуска Версия Дата выпуска Python 3 .12 Октябрь 2023 Python 3 .11 Октябрь 2022 Python 3 .10 Октябрь 2021 Python 3 .9 Октябрь 2020 Python 3 .8 Октябрь 2019 Python 3 .7 Июнь 2018 Python 3 .6 Декабрь 2016 Python 3 .5 Сентябрь 2015 Python 3 .4 Март 2014 Python 3 .3 Сентябрь 2012 Python 3 .2 Февраль 2011 Python 3 .1 Июнь 2009 Python 3 .0 Декабрь 2008 Python 2 .7 Июль 2010 Python 2 .6 Октябрь 2008 Python 2 .0 Октябрь 2000 Python 1 .6 Сентябрь 2000 Python 1 .5 Февраль 1997 Python 1 .0 Январь 1994 Аналогия с годами выпуска автомобилей показывает, что большее число соответствует более новой версии. Однако в нумерации версий Python чем больше число, тем новее версия. Самое значительное изменение происходит при смене первой цифры: оно обычно настолько велико, что программное обеспечение, написанное для предыдущих версий, может перестать работать в новой. 28 РАЗДЕЛ 1 Начало работы
Представьте компанию-разработчика, чей рыночный продукт написан на Python 2, а в его создание были вложены миллионы долларов. В таком случае переход на текущую версию может потребовать практически полной переработки кода, что вряд ли вызовет энтузиазм. Именно поэтому старые версии Python часто продолжают поддерживаться и развиваться независимо от актуальных релизов — это необходимо для помощи разработчикам и компаниям, сделавшим серьезные инвестиции в предыдущие версии. Главный вопрос, который волнует большинство начинающих: «С какой версии мне начать обучение?». Ответ на него крайне прост: начинайте с самой актуальной на данный момент. Узнать текущую версию легко: когда вы зайдете на официальный сайт Python.org, на странице загрузки будет указана последняя стабильная сборка. Именно ее рекомендуют разработчики, и именно ее стоит использовать. Единственная причина изучить устаревшую версию (например, 2.x или 2.7) может возникнуть, если вас взяли на проект, где компания требует работы с конкретным релизом. Для начинающего программиста такая ситуация — редкость, ведь пока он только начинает работать в этой сфере. Однако в реальном мире некоторые компании сильно зависят от старого ПО, поэтому при найме ищут специалистов со знанием определенной версии. В этой книге мы сосредоточимся на версиях Python, актуальных на конец 2023 года, начиная с Python 3.11 и новее. Не беспокойтесь о различиях в версиях, затрагивающих цифры после первой точки. Например, версия 3.11.1 достаточно похожа на версию 3.11.2, и различия между ними несущественны, особенно для начинающих. Большая часть возможностей Python остается неизменной во всех последних версиях, поэтому можете не сомневаться, что вы изучаете актуальный материал, который не успеет устареть в ближайшее время. Инструменты для успешной работы Теперь нужно подготовить компьютер для практического изучения Python. Для начала вам понадобятся интерпретатор Python и редактор кода. Редактор позволяет набирать код, а интерпретатор — запускать его. Когда вы запускаете (или выполняете) код, вы даете компьютеру команду: «Сделай то, что написано в моей программе». ПРИМЕЧАНИЕ Термин «код» относится к инструкциям для компьютера, написанным на языке программирования. Термином «кодирование» часто описывают сам процесс написания кода. Редактор кода — это приложеГЛАВА 1 Начало работы с Python 29
ние, предназначенное для набора кода; его можно сравнить с такими текстовыми процессорами, как Word или Pages, которые используются для набора текста на обычном языке, например на английском. Подобно тому, как в мире существует множество марок зубной пасты, мыла и шампуня, существует и множество редакторов кода, хорошо работающих с Python. Среди них нет правильных или неправильных, хороших или плохих, лучших или худших. Это просто разные продукты, которые в целом выполняют одни и те же функции, но отличаются подходом и приоритетами создателей. Если вы уже начали изучать Python и довольны своим текущим инструментом, можете спокойно продолжать работать с ним и не обращать внимания на наши рекомендации. Если же вы только знакомитесь с программированием, мы советуем использовать Visual Studio Code — это превосходная и бесплатная среда для обучения. Установка Python и VS Code Редактор, который мы рекомендуем и будем использовать в этой книге, — это Visual Studio Code. Однако чаще его называют просто VS Code. Вот основные причины, по которым он стал нашим фаворитом: » » Это отличный редактор для изучения программирования . » Его относительно просто освоить и использовать . » » » Он работает практически одинаково в Windows, Mac и Linux . Это превосходный инструмент для профессиональной разработки, которым пользуются миллионы программистов по всему миру . Он бесплатный . Он прекрасно интегрируется с GitHub Copilot, что позволяет использовать современный генеративный ИИ для ускорения обучения и самой разработки . Чтобы использовать VS Code в качестве редактора для изучения и работы с Python, вам потребуется установить три компонента: сам Python, редактор VS Code и соответствующее расширение для него. Если вы уже имели опыт установки приложений, этот процесс не вызовет затруднений. Во время установки просто следуйте инструкциям на экране. Если вы сомневаетесь в выборе каких-либо параметров, выбирайте вариант по умолчанию (рекомендуемый). 30 РАЗДЕЛ 1 Начало работы
Вот пошаговая инструкция по загрузке и установке Python и VS Code: 1. Откройте веб-браузер и перейдите на сайт www.python.org . 2. Нажмите на кнопку «Download» (Загрузить). Если будет предложено выбрать версию, выберите рекомендуемую стабильную. 3. Найдите в папке «Загрузки» скачанный файл и запустите его двойным щелчком мыши, чтобы начать установку Python. В процессе установки просто следуйте инструкциям на экране, принимая предложенные настройки по умолчанию. 4. Перейдите по адресу: https://code.visualstudio.com/ и скачайте актуальную версию VS Code для вашей операционной системы. 5. Найдите скачанный файл установки VS Code, дважды щелкните по нему и следуйте инструкциям на экране. После установки VS Code вы сможете запускать его, как и любое другое приложение в вашей системе. В Windows: нажмите кнопку «Пуск» и найдите в меню Visual Studio Code. На Mac: приложение можно найти в папке «Программы» (Applications) или в Launchpad. Установка расширения Python Чтобы программировать на Python в VS Code, необходимо установить соответствующее расширение. После запуска VS Code на левой панели вы увидите ряд значков. Если навести курсор на любой из них, появится его название. Нажмите на значок «Расширения» (рисунок 1.1), введите Python в поисковую строку на появившейся панели и нажмите «Установить» напротив расширения Python от Microsoft (оно также показано на рисунке 1.1). После установки расширения Python в VS Code появятся два связанных компонента: Python и Pylance. Не беспокойтесь, это штатная ситуация. Pylance — это вспомогательное расширение, которое добавляет полезные функции для работы с Python-кодом. Для активации всех возможностей просто перезапустите VS Code. ГЛАВА 1 Начало работы с Python 31
Рисунок 1.1. Установка расширения Python в VS Code Использование ИИ для написания кода на Python Современный генеративный ИИ действительно способен писать код на Python за вас. Однако это не так просто, как отдать команду: «Напиши Python-приложение, которое сделает меня миллиардером». К сожалению, пока что это так не работает. Вам придется разбивать задачи на более мелкие части и использовать точную техническую терминологию. Другими словами, вам все равно потребуется изучить Python на уровне, достаточном для формулировки точных запросов к ИИ. Практически любой такой запрос — независимо от выбранного ИИ-сервиса — будет начинаться со слов «Напиши код на Python для…». ИИ умеет многое, и, если вы точно не укажете, что вам нужен код на Python, в ответ можно получить что угодно: HTML, JavaScript или вовсе отсутствие кода. Поэтому прежде всего важно это понимать. Поскольку мы пишем это в конце 2023 года, генеративный ИИ все еще остается довольно новой и быстроразвивающейся технологией. Мы не можем давать никаких гарантий относительно цен и доступности сервисов. Вероятно, эти аспекты будут часто меняться в ближайшие годы, пока компании-конкуренты борются за позиции и долю рынка. Однако 32 РАЗДЕЛ 1 Начало работы
на момент создания книги для написания кода на Python вы можете использовать следующие ИИ-сервисы. Большинство из них предоставляют такую возможность бесплатно, но, повторим, мы не беремся предсказать, как ситуация сложится в будущем. Сервис URL ChatGPT chat .openai .com Claude claude .ai Bard bard .google .com Copilot chat .bing .com Использование GitHub Copilot GitHub Copilot — это еще один ИИ-инструмент, способный писать код. Он основан на GPT-4 от OpenAI, так же как и ChatGPT, но специально предназначен для работы с кодом и интегрируется непосредственно в редактор VS Code. Вам совершенно необязательно использовать GitHub Copilot для изучения Python по этой книге, однако вы можете обнаружить, что он здорово помогает в учебном процессе. На момент написания книги GitHub предлагал Copilot бесплатно для студентов. Также существуют платные тарифы, чей ценник начинается от 10 долларов в месяц. Чтобы начать пользоваться Copilot, вам нужно зарегистрироваться на GitHub и приобрести (или запросить) доступ к нему. Помните, что этот инструмент новый, поэтому любые инструкции, которые я привожу здесь, могут устареть на момент чтения книги. Возможно, вам придется поискать в интернете по запросу «использование Copilot с VS Code», чтобы найти самые актуальные руководства. Вот основные шаги: 1. Если у вас еще нет учетной записи на GitHub, перейдите на сайт GitHub.com и создайте аккаунт. Запомните свое имя пользователя и пароль — они потребуются для дальнейшей настройки . Затем установите расширение GitHub Copilot в VS Code . Для этого: 2. Запустите Visual Studio Code, если он еще не открыт. ГЛАВА 1 Начало работы с Python 33
3. На панели слева нажмите на значок «Расширения» (Extensions). В строке поиска введите «GitHub Copilot». Перед вами появится список расширений Copilot . 4. В списке результатов найдите основное расширение GitHub Copilot (не «Copilot Labs» или другие варианты) и нажмите кнопку «Install» (Установить), как показано на рисунке 1.2. Рисунок 1.2. Расширение GitHub Copilot в VS Code Справа вы увидите панель с инструкциями и подсказками. Пока что с ними ничего делать не нужно. Обратите внимание на нижний левый угол окна VS Code — там находится значок аватара для управления учетными записями (рисунок 1.3). Нажмите на этот значок и выберите пункт «Sign In with GitHub to Use GitHub Copilot» («Войти с GitHub, чтобы использовать GitHub Copilot»). Следуйте появившимся на экране инструкциям, чтобы войти в учетную запись GitHub и настроить Copilot. Помните: настройка Copilot не является обязательной, это лишь дополнительная возможность. Не чувствуйте себя обязанными завершать процесс покупки подписки. Однако если вы все же установите Copilot как расширение, его название будет отображаться в списке «Installed Extensions» («Установленные расширения») в VS Code. Кроме того, в правом нижнем углу экрана вы увидите маленький значок Copilot (он также показан на рисунке 1.3). Вы можете нажать на него в любое время, чтобы отключить Copilot, если он мешает вам сосредоточиться. Чтобы снова активировать Copilot, просто нажмите на значок еще раз. 34 РАЗДЕЛ 1 Начало работы
Рисунок 1.3. Значки учетных записей и Copilot в VS Code Использование Jupyter Notebook для программирования Jupyter Notebook — это популярный инструмент для написания кода на Python. Название «Jupyter» образовано от названий трех языков программирования, которые он изначально поддерживал: Julia, Python и R. Julia и R широко распространены в области data science. Python является более универсальным языком и также чрезвычайно популярен в data science, но при этом подходит для разработки проектов любого типа, а не только для научных исследований. Что касается части названия «Notebook» («Блокнот»), то она связана с тем, что код и пояснения к нему размещаются в ячейках, напоминающих страницы обычной записной книжки. Jupyter Notebook можно легко использовать прямо в VS Code. Для этого потребуется всего лишь установить соответствующее расширение. Вот как это сделать: 1. Если вы закрыли VS Code, откройте его сейчас. 2. Нажмите на значок Extensions на панели слева, введите Jupyter в поле поиска, а затем нажмите кнопку Install, чтобы установить расширение Jupyter от Microsoft, как показано на рисунке 1.4. В правой части стартовой страницы вы увидите некоторые инструкции по использованию Jupyter. Но в данный момент вам ничего не нужно делать. ГЛАВА 1 Начало работы с Python 35
Просто помните, что когда вам потребуется создать новый блокнот Jupyter, вы можете выполнить следующие действия: 1. Выберите View  Command Palette в строке меню VS Code. 2. Начните вводить jupy, а затем выберите пункт Create: New Jupyter Notebook. Мы перейдем к особенностям написания кода на Python в следующей главе. Пока что, если вы выполнили все шаги по установке расширений, описанные в этой главе, VS Code должен быть готов к работе. Если вы нажмете на значок Extensions в VS Code и посмотрите на левую панель, у вас должны быть установлены расширения Python и Jupyter, как показано на рисунке 1.5. Помните, однако, что GitHub Copilot является необязательным, поэтому не стоит думать, что вам необходимо приобретать его сейчас для изучения Python. Рисунок 1.4. Установка расширения Jupyter в VS Code Простые задачи, которые вы выполнили в этой главе, будут полезны вам в процессе обучения, а также в профессиональном программировании после освоения основ, и теперь мы приглашаем вас перейти к следующей главе этого раздела, чтобы глубже погрузиться в Python и использование инструментов, которые теперь доступны на вашем компьютере. 36 РАЗДЕЛ 1 Начало работы
Рисунок 1.5. Расширения VS Code для Python и Jupyter отображаются слева в разделе «Установленные»
В ЭТОЙ ГЛАВЕ: » Как использовать интерактивный режим Python » Как создать рабочее пространство для разработки » Как организовать папку для хранения своего кода » Как вводить, редактировать и отлаживать программы » Как писать код в среде Jupyter Notebook Гл а в а 2 Использование интерактивного режима, получение справки и создание приложений П осле установки VS Code (с ней вы познакомились в главе 1) вы готовы глубже погрузиться в написание кода на Python. В этой главе мы кратко рассмотрим интерактивные функции, справочную систему и возможности редактирования кода в VS Code и Jupyter Notebook. Вероятно, вам уже не терпится перейти к более продвинутым темам, таким как data science, искусственный интеллект или робототехника. Однако освоение этих сложных разделов пойдет гораздо легче, если вы сначала уверенно овладеете инструментами, которые для этого нужны. 38 РАЗДЕЛ 1 Начало работы
Использование интерактивного режима Python Чтобы получить первый практический опыт работы с Python, можно просто начать вводить команды в интерактивном режиме. Идеальное место для этого — панель терминала в VS Code. С нее мы и начнем в этой главе. ЦВЕТА И ЗНАЧКИ В VS CODE В этой книге для панели Terminal в VS Code используется цветовая схема с черным текстом на белом фоне . У вас же, в зависимости от настроек, цвета могут быть другими . Не беспокойтесь, вы можете выбрать любую понравившуюся вам цветовую схему . Если вы хотите привести внешний вид терминала в соответствие с иллюстрациями в книге, выполните следующие действия: В Windows: выберите в меню File  Preferences  Theme  Color Theme  Light (Visual Studio) . На macOS: выберите в меню Code  Preferences  Theme  Color Theme  Light (Visual Studio) . Открытие терминала Чтобы запустить интерактивный режим Python в VS Code, сделайте следующее: 1. Откройте VS Code. 2. Перейдите в меню View (Вид) и выберите пункт Terminal (Терминал). 3. Если в верхней части открывшейся панели слово Terminal неактивно (не выделено или не подчеркнуто), кликните по нему (рисунок 2.1). Рисунок 2.1. Панель Terminal в VS Code ГЛАВА 2 Использование интерактивного режима, получение справки 39
Первая строка ввода называется командной и обычно показывает данные вашей операционной системы, включая имя пользователя. Например, на Mac она может выглядеть так: Alans-Air:~ alan$ (где Alans-Air — это имя вашего компьютера). В Windows строка будет другой, например: C:\Users\Alan>, где вместо Alan будет ваше имя пользователя, а начальный путь может отличаться от C:\Users. Например, на Mac мы видим такую командную строку: Alans-Air:~ alan$ А в Windows: C:\Users\Alan> В зависимости от версии Windows и текущей конфигурации, вы можете увидеть командную строку следующего вида, где xxx будет заменено на ваше имя пользователя: PS C:\Users\xxx> Это означает, что вы используете PowerShell. Вам не нужно ничего менять. Вместо Alan вы увидите свое имя пользователя, а путь также может отличаться от C:\Users. Определение версии Python Чтобы узнать версию Python, введите в строке следующую команду и нажмите Enter. Убедитесь, что перед первым дефисом стоит пробел, а других пробелов в команде нет: python —version Если вы увидите сообщение об ошибке — не беспокойтесь. Чуть позже я расскажу, как это исправить. При отсутствии ошибки отобразится версия Python, например, Python 3.x.x (x – это номер версии). Это означает, что все настроено правильно, и следующие шаги можно пропустить. Если на Mac вы получаете ошибку Сommand Not Found, используйте команду: python3 —version 40 РАЗДЕЛ 1 Начало работы
Эта команда часто требуется из-за того, что Mac OS ранних версий поставлялись со старыми версиями Python. Поэтому на протяжении всей книги вам нужно вводить в терминале python3 вместо python. Я буду напоминать об этом по мере необходимости. Но подождите: ошибка может возникнуть и в Windows. В зависимости от версии системы вы также можете получить сообщение об ошибке, когда введете python. В таком случае попробуйте ввести py: py —version Вход в интерпретатор Python Любая команда, с помощью которой вы проверяете версию Python, также запустит сам интерпретатор, где вы сможете вводить код Python напрямую. Вы поймете, что находитесь в интерпретаторе, когда командная строка изменится на три знака «больше» (>>>). Чтобы войти в него, введите ту же команду python, которая работала у вас раньше, но без указания —version. Например, просто введите python (или python3, или py ) и ничего больше, затем нажмите Enter:/ Python ЗАПОМНИТЕ Когда вам (или кому-либо другому) говорят «введите команду», это означает, что нужно набрать команду и затем нажать Enter. До нажатия Enter ничего не произойдет. Поэтому если просто набрать команду и ждать результата, можно потратить много времени. В итоге вы должны увидеть информацию о версии Python и строку >>>, которая означает, что интерпретатор Python запущен. Ввод команд Ввод команд в интерпретаторе Python ничем не отличается от их ввода в любом другом месте. Вы должны правильно набрать команду, а затем нажать Enter. Если вы допустите ошибку в написании команды, вы, скорее всего, увидите сообщение об ошибке — это просто интерпретатор сообщает вам, что он не понимает, что вы имеете в виду. ГЛАВА 2 Использование интерактивного режима, получение справки 41
Но не волнуйтесь, интерпретатор таким вы сломать не сможете. Например, вы вводите команду: howdy После того как вы нажмете Enter, вы увидите техническую абракадабру. Это сообщение о том, что интерпретатор не знает, что означает «howdy», и не может выполнить команду. Ничего не сломалось. Вы просто вернулись к началу ввода >>>, где можете попробовать снова, как показано на рисунке 2.2. Рисунок 2.2. Python не знает, что означает howdy Использование встроенной справки Python В одной из строк на рисунке 2.2 упоминается, что вы можете ввести команду help в интерпретаторе Python. Обратите внимание, что кавычки вводить не нужно — просто наберите слово help и нажмите Enter. В ответ интерпретатор выведет сообщение: Type help() for interactive help, or help(object) for help about object. (Введите help() для интерактивной справки или help(object) для получения справки об объекте.) Теперь интерпретатор предлагает ввести help() с пустыми скобками или help(объект), указав конкретный объект (в примере это object). Не забудьте нажать Enter после ввода команды. Попробуйте ввести следующее: help() Обратите внимание, что пробелов в команде нет. После нажатия Enter на экране появится информация об интерактивной справке Python, аналогичная показанной на рисунке 2.3. 42 РАЗДЕЛ 1 Начало работы
Рисунок 2.3. Интерактивная справочная система Python Надпись help> в нижней части окна означает, что вы больше не находитесь ни в оболочке операционной системы, ни в интерпретаторе Python (который всегда показывает >>>), а перешли в специальный режим справки. Как указано на экране, вы можете ввести название любого модуля, ключевого слова или темы, чтобы получить информацию по нужному вопросу. Возможно, как начинающему пользователю, вам пока не нужна конкретная справочная информация. Но полезно знать, что помощь всегда доступна на тот случай, если она вам понадобится. В Python, как и в других языках, используются ключевые слова с особым значением. Для вывода их списка запустите интерпретатор Python и в приглашении help> введите команду: keywords После нажатия Enter отобразится список ключевых слов (рисунок 2.4). Рисунок 2.4. Справка по ключевым словам ГЛАВА 2 Использование интерактивного режима, получение справки 43
Над списком ключевых слов расположено сообщение, которое информирует, что в строке help> можно ввести любое из них, чтобы получить справку. Например, если ввести слово class, вы увидите информацию о классах в Python, как показано на рисунке 2.5. Речь идет не о школьных занятиях, а о классах, которые вы создаете в Python (после того как освоите основы и будете готовы к более сложным темам). Рисунок 2.5. Справка по классам Python Технические термины в справке поначалу могут показаться сложными. Однако, изучая Python, помните: вы в любой момент можете открыть интерактивную справку и найти нужную информацию. СОВЕТ Надпись — More — означает, что текст не поместился на экране и есть продолжение, но это не означает, что вам нужно что-то вводить. Для прокрутки нажимайте Пробел или Enter каждый раз, когда она появляется. По окончании текста вы окажетесь в строке help>. Для досрочного выхода нажмите Q. Выход из интерактивной справки Чтобы выйти из интерактивной справки Python, введите exit() и нажмите Enter. Если вы введете команду с ошибкой, например, просто exit, интерпретатор покажет подсказку: Use exit() or Ctrl-Z plus Return to exit. СОВЕТ 44 Не путайтесь между обозначениями «Ctrl-Z» и «Ctrl+Z» для комбинаций клавиш — они означают одно и то же. РАЗДЕЛ 1 Начало работы
О том, что вы вышли из интерпретатора Python, вы поймете по изменению строки ввода в терминале: вместо >>> появится строка вашей операционной системы. Это показано на рисунке 2.6. Рисунок 2.6. Возврат к строке операционной системы Поиск помощи в сети Встроенная справка Python — отличный инструмент, если вы точно знаете, что ищете. Но для начинающих она может показаться сложной. Если вы не уверены в терминологии, лучше обратиться к интернет-ресурсам. Для поиска видеоуроков идеально подойдут видеохостинги, а для текстовых ответов на вопросы — Stack Overflow. И, разумеется, в любой ситуации вас выручат поисковые системы, такие как Google или Bing. ЗАПОМНИТЕ Какой бы поисковой системой вы ни пользовались, всегда начинайте запрос со слова python. Это важно, потому что многие языки программирования имеют схожие концепции. Без указания языка результаты поиска могут оказаться непредсказуемыми. Обилие бесплатного материала Отличное подспорье для учащихся — бесчисленное количество свободного материала в интернет. Когда от возможностей языка вроде Python голова идет кругом, компактная инструкция на одной странице помогает систематизировать информацию, сделав ее менее пугающей и более удобной для восприятия. ГЛАВА 2 Использование интерактивного режима, получение справки 45
Конечно, использование таких памяток-шпаргалок — это не настоящее жульничество, если только вы не пользуетесь ей во время теста, который нужно сдавать по памяти. Но написание кода в реальной жизни сильно отличается от ответов на вопросы с множественным выбором. Поэтому то, что мы часто называем шпаргалкой в техническом мире, — это просто еще один инструмент, помогающий людям учиться. Существуют разные типы памяток; то, какие подойдут именно вам, зависит от вашего стиля обучения. Чтобы посмотреть, что доступно, зайдите в Google, Bing или любую другую поисковую систему и выполните поиск по запросу free python cheat sheet. Большинство из них представлены в удобном формате: их можно скачать, распечатать и держать под рукой, пока вы изучаете, казалось бы, бесконечные возможности написания кода на Python. Организация папки с кодом В этом разделе вы создадите папку для хранения всего Python-кода, который будете писать при изучении этой книги. Это позволит держать весь код в одном месте, чтобы его было легко найти, когда он понадобится. Вы можете разместить эту папку где угодно и назвать как угодно. В Windows вы можете перейти в папку, внутри которой хотите создать новую папку (Алан использует OneDrive, но вы можете использовать рабочий стол, документы или любую другую папку). Щелкните правой кнопкой мыши по пустому месту в папке и выберите «Новая папка» (на Mac) или «Создать  папку» (в Windows). Введите имя папки и нажмите Enter. Чтобы следовать примерам из этой главы, назовите вашу папку AIO Python. Набор, редактирование и отладка кода Python Редактор в VS Code, как вы знаете, служит для набора и редактирования текста. Поскольку код по своей сути является текстом, этот редактор имеет специальную настройку для работы с ним, за что и получил название «редактор кода». Именно в нем вам предстоит создавать большую часть программного кода. 46 РАЗДЕЛ 1 Начало работы
Как правило, код удобно организовывать по папкам (этот подход мы рекомендуем для примеров этой главы). Первым делом откройте папку с вашим проектом в редакторе. Есть несколько способов это сделать. Если вы новичок, начните с этих шагов: 1. Запустите VS Code через меню «Пуск» в Windows или Launchpad на Mac. 2. В строке меню VS Code выберите File  Open Folder. В открывшемся окне перейдите к нужной папке, выделите ее щелчком мыши и нажмите кнопку Select Folder. Имя открытой папки отображается в верхней части панели Explorer слева (рисунок 2 .7) . СОВЕТ Каждый создаваемый вами файл с кодом Python будет текстовым файлом с расширением .py. Мы рекомендуем сохранять все файлы, создаваемые для этой книги, в папке AIO Python (процесс ее создания описан в предыдущем разделе). Эта папка должна быть видна каждый раз, когда открыты VS Code и ваше рабочее пространство Python 3. Чтобы создать файл с расширением .py, выполните следующие действия: 1. Если вы еще этого не сделали, откройте VS Code и вашу рабочую папку AIO Python . 2. Если панель Explorer (Проводник) не активна, щелкните на соответствующий значок в верхнем левом углу окна VS Code. 3. Чтобы создать файл непосредственно в этой папке, наведите курсор на ее имя и нажмите кнопку New File («Новый файл»), которая появится справа. Она показана на рисунке 2.7. 4. Введите имя файла, обязательно указав расширение .py (для примера назовите его hello .py), и нажмите Enter. После этого в редакторе откроется новый файл . Его имя появится на новой вкладке, а само содержимое вы будете набирать в большой рабочей области ниже . Обратите внимание, что файл также появится в проводнике — внутри папки AIO Python, — ведь это его место хранения . ГЛАВА 2 Использование интерактивного режима, получение справки 47
Рисунок 2.7. Значок New File появляется справа от имени папки Написание кода на Python Теперь, когда файл .py открыт, можно приступать к написанию кода на Python. Как это часто бывает при изучении нового языка программирования, начнем с создания простой программы «Hello World». Для этого выполните следующие шаги: 1. Щелкните мышью справа от строки 1 в области редактирования. 2. Введите команду: print("Hello World") Во время ввода вы можете заметить появляющиеся на экране подсказки . Это работает система IntelliSense: она анализирует ваш код и показывает информацию о ключевых словах . Количество отображаемых данных может зависеть от использования GitHub Copilot . Не обращайте внимания на эти подсказки — просто продолжайте печатать . 3. После ввода строки нажмите Enter. Новая строка кода появится в файле. Помимо этого, вы можете заметить несколько других изменений, как показано на рисунке 2.8: 48 » На значке Explorer появляется точка (или, возможно, 1 в кружке), указывающая на наличие несохраненных изменений . » Имя hello.py на вкладке отображается с точкой, что также означает, что в файле есть несохраненные изменения . РАЗДЕЛ 1 Начало работы
Сохранение кода Код, который вы набираете в VS Code, по умолчанию не сохраняется автоматически. Решить эту проблему можно двумя способами. Первый — самостоятельно сохранять файл после внесения важных изменений. Проще всего это сделать, выбрав в меню VS Code пункт File  Save или нажав сочетание клавиш Ctrl+S (Windows / Linux) или Cmd+S (macOS). Рисунок 2.8. Файл hello.py содержит код Python с несохраненными изменениями Второй способ, который мы рекомендуем, — это настройка функции Автосохранения (Auto Save). Она автоматически сохраняет все внесенные изменения. Чтобы включить ее, выберите в меню пункт File  Auto Save. Рядом с этим пунктом появится галочка, подтверждающая, что функция активна. Чтобы отключить автосохранение, просто выберите этот пункт меню еще раз. Запуск Python в VS Code Чтобы протестировать ваш код на Python в VS Code, его нужно запустить. Самый простой способ — щелкнуть правой кнопкой мыши по имени файла (в данном примере это hello.py) и выбрать Run Python File in Terminal, как показано на рисунке 2.9. Если появится запрос на выбор интерпретатора Python, просто укажите тот, который вы скачали и устанавливали из главы 1, раздела 1. Обычно он помечен как Recommended (Рекомендуемый). ГЛАВА 2 Использование интерактивного режима, получение справки 49
Если у вас возникнут трудности, выберите в строке меню VS Code View  Command Palette. В появившейся строке введите Python и выберите пункт Python: Select Interpreter. Затем укажите рекомендованный вариант. После этого попробуйте снова запустить hello.py, как было описано выше. В нижней части окна VS Code откроется панель терминала. Вы увидите командную строку, за которой следует комментарий о запуске кода в интерпретаторе Python (python.exe). Ниже появится вывод программы: в данном примере это фраза "Hello World". После этого снова появится приглашение командной строки, как показано на рисунке 2.10. Пусть это приложение и не самое захватывающее, но теперь вы знаете, как писать, сохранять и выполнять программы на Python в VS Code — навык, который вы будете часто использовать по мере изучения этой книги и на протяжении всей вашей карьеры программиста на Python. Рисунок 2.9. Запуск hello.py Рисунок 2.10. Вывод программы hello.py 50 РАЗДЕЛ 1 Начало работы
Изучение основ отладки ЗАПОМНИТЕ Когда вы только начинаете писать код, ошибки неизбежны. Помните, что в этом нет ничего страшного — вы ничего не сломаете и не разрушите. Просто код не будет работать так, как ожидается. Перед запуском кода вы можете заметить несколько визуальных индикаторов, которые укажут на ошибки: » Количество ошибок в файле отображается красным цветом рядом с его именем на панели проводника в левой части экрана . » Общее количество ошибок и предупреждений отображается в нижнем левом углу окна Visual Studio Code . » Проблемный код будет подчеркнут волнистой линией . На рисунке 2.11 мы написали PRINT заглавными буквами, что недопустимо в Python. Python чувствителен к регистру, и правильная команда пишется строчными буквами: print.Помните, что, если в примере команда показана строчными буквами, вы тоже должны набирать ее строчными. Рисунок 2.11. PRINT неправильно набран в файле hello.py Чтобы запустить файл в терминале, нужно исправить ошибку. В примере на рисунке 2.11 для этого требуется просто заменить PRINT на print и сохранить изменения (если автосохранение отключено). После этого можно щелкнуть по файлу правой кнопкой мыши и выбрать Run Python ГЛАВА 2 Использование интерактивного режима, получение справки 51
File in Terminal (Запустить файл Python в терминале), чтобы выполнить исправленный код. Закрытие файла Чтобы закрыть файл в VS Code после работы, нажмите на крестик (X) на его вкладке или в меню выберите Файл  Закрыть редактор. Файл исчезнет из редактора, но останется виден в проводнике (левой панели VS Code). Чтобы открыть его снова, просто кликните по нему в проводнике. Написание кода в Jupyter Notebook В главе 1 этого раздела вы узнали, что можно писать и выполнять код Python в Jupyter Notebook. Теперь мы покажем, как создавать, сохранять и открывать блокноты. В качестве примера мы создадим подпапку с названием Jupyter Notebooks внутри папки AIO Python. Разумеется, вы можете сохранять свои блокноты в любом месте и называть их, как угодно. Создание папки для Jupyter Notebooks Папка для Jupyter Notebooks ничем не отличается от обычной папки. Создать ее можно любым привычным для вашей операционной системы способом. Мы разместили нашу папку внутри созданной ранее папки AIO Python, чтобы хранить все файлы для этой книги в одном месте. 1. Откройте вашу папку AIO Python (или любую другую папку, которую вы создали для работы с файлами этой книги) в Finder (Mac) или Проводнике (Windows). 2. Щелкните правой кнопкой мыши на свободном месте внутри этой папки и выберите Создать  Папку (Windows) или Новая папка (Mac). 3. Введите Jupyter Notebooks в качестве имени папки и нажмите Enter. 52 РАЗДЕЛ 1 Начало работы
Теперь, когда у вас есть специальная папка для блокнотов Jupyter, можно переходить к созданию самого блокнота, как описано далее. Создание и сохранение блокнота Jupyter Блокноты Jupyter можно как создавать, так и запускать их код прямо в Visual Studio Code. Если вы создали папку Jupyter Notebook внутри папки AIO Python (как было предложено в предыдущем разделе), ее название должно отображаться в панели проводника VS Code (при условии, что открыта папка AIO Python). Если папка создана, но не видна в проводнике, нажмите значок «Обновить проводник» (круглая стрелка) рядом с заголовком AIO PYTHON в верхней части панели. Чтобы создать блокнот Jupyter и сохранить его в нужной папке, выполните следующие действия: 1. Откройте VS Code (если он еще не запущен). 2. На панели проводника щелкните по значку Jupyter Notebooks . Нажмите на значок плюса (+), который появится над панелью, чтобы создать новый файл. Введите имя файла 01 Notebook . ipynb . Важно точно указать расширение файла —.ipynb, чтобы он был распознан как блокнот Jupyter. ЗАПОМНИТЕ После ввода имени файла и нажатия Enter он откроется в виде блокнота Jupyter в редакторе справа, как показано на рисунке 2.12. Рисунок 2.12. Блокнот Jupyter ГЛАВА 2 Использование интерактивного режима, получение справки 53
Если вы не включили автосохранение, как упоминалось ранее, сохраняйте изменения вручную по мере работы с блокнотом в следующих разделах. ЗАПОМНИТЕ Под строкой меню и панелью инструментов блокнота вы увидите большое поле для ввода текста. Эта область называется ячейкой. Именно в нее вы будете вводить код. Ввод и выполнение кода в блокноте Когда вы открываете блокнот, вы видите как минимум одну ячейку. Рядом вы можете заметить опции «+Code» и «+Markdown». При нажатии на «Code» появляется совершенно новая ячейка кода, то есть ячейка, в которой можно выполнять код Python. В ее верхнем левом углу всегда есть треугольник, направленный вправо; он нужен для запуска кода Python, написанного в этой ячейке. Сейчас перед вами должна быть одна такая ячейка кода. Вы можете использовать ее для ввода и выполнения команд на Python. Давайте попробуем это сделать, выполнив следующие шаги: 1. Щелкните внутри ячейки кода (справа от треугольника) и введите следующую команду: print("Hello World") Обратите внимание, что слово print нужно писать строчными (маленькими) буквами . 2. Чтобы выполнить код, нажмите кнопку Run (Выполнить), выглядящую как значок треугольника, направленного вправо, который находится слева от ячейки с кодом, либо кнопку Run All (Выполнить все) над блоком кода (рисунок 2.13). Рисунок 2.13. Нажмите кнопку Run или Run All, чтобы запустить код 54 δôχõñτïτõϊωñτ РАЗДЕЛ 1 Начало работы
При условии, что в вашем коде нет ошибок, результат выполнения программы на Python (надпись «Hello World») появится сразу под ячейкой с кодом в блокноте Jupyter. Если появится запрос на выбор интерпретатора Python, просто выберите тот, который вы недавно установили (обычно он помечен как «Рекомендуемый»). Также установите все необходимые расширения, которые предложит VS Code. Каждый созданный вами блокнот — это отдельный файл. Закрыть блокнот можно так же, как и обычный файл .py: щелкнув на крестик (X) на вкладке с его именем или выбрав в меню пункт File  Close Editor. Чтобы снова открыть блокнот, щелкните по его имени в панели проводника (Explorer). Этой информации должно быть достаточно, чтобы начать изучать и запускать Python на вашем компьютере. Я не буду подробно останавливаться на VS Code и Jupyter Notebooks, поскольку это не сам Python, а инструменты, которые используют программисты. На разных интернет-ресурсах вы найдете множество бесплатных руководств по работе с VS Code и Jupyter Notebooks в связке с Python. Поэтому в процессе изучения языка смело обращайтесь к этим дополнительным материалам, когда это будет необходимо.
В ЭТОЙ ГЛАВЕ: » Что такое «Дзен Python» и какие принципы лежат в основе языка » Введение в объектно ориентированное программирование » Почему отступы в Python имеют решающее значение » Как использовать модули Python Гл а в а 3 Элементы и синтаксис Python М ногие языки программирования сосредоточены на том, какие действия выполняет компьютер и каким образом они реализуются. Такой подход делает их изучение сложным: программисту приходится подстраиваться под логику машины. Философия Python принципиально иная. Этот язык создан для того, чтобы быть ближе к человеку: он ориентирован не на устройство компьютера, а на то, как люди думают, работают и общаются между собой. Ярким примером такой человекоориентированной философии является Дзен Python — набор принципов, отражающих простоту, ясность и читаемость кода. Именно с него мы начнем эту главу. Дзен Python Дзен Python (рисунок 3.1) — это набор руководящих принципов, лежащих в основе разработки языка. Его можно найти в виде своеобразной пасхалки: скрытой функции или шутки, доступной только тем, кто достаточно хорошо знаком с языком. Чтобы открыть Дзен Python, выполните следующие шаги: 56 РАЗДЕЛ 1 Начало работы
1. Запустите Visual Studio Code привычным способом (через Launchpad на Mac или меню «Пуск» в Windows) и откройте папку AIO Python . 2. Если терминал не отображается, включите его через меню: View  Terminal. 3. В терминале введите python и нажмите Enter. Если возникнет ошибка: на Mac используйте команду python3, в Windows — py . 4. В интерактивной строке Python (>>>) введите: import this После этого вы увидите список коротких афоризмов. Возможно, придется прокрутить окно терминала или увеличить его размер, чтобы просмотреть все строки. Эти ироничные выражения несут философский смысл: код должен быть понятнее для человека, чем для машины. Рисунок 3.1. Дзен Python Иногда Дзен Python называют PEP 20. PEP расшифровывается как Python Enhancement Proposal («Предложение по улучшению Python»). Интересно, что в тексте Дзена записано 19 принципов, хотя сам номер — 20. Последний, двадцатый, каждый может придумать сам. Существует множество PEP, и все они доступны на официальном сайте python.org/dev/peps. Чаще всего упоминают PEP 8 — руководство по стилю написания кода на Python. Его главный принцип звучит так: «читаемость имеет значение». Иными словами, программа должна быть ГЛАВА 3 Элементы и синтаксис Python 57
удобна и понятна в первую очередь для людей. На первых порах при изучении Python чужой код может показаться вам чем-то вроде тарабарщины, написанной «инопланетянами». Это нормальное ощущение для начинающих: стиль и структура еще не знакомы. Однако с ростом опыта закономерности станут заметнее, и вы все легче будете читать и понимать чужие программы. Это, в свою очередь, отличный способ учиться программированию на практике. ЗАПОМНИТЕ В этой книге мы постепенно будем знакомить вас с особенностями стиля Python. Не стоит пытаться заучивать правила PEP 8 заранее: без практики они будут казаться сухими и скучными. Пока просто запомните: если где-то упоминается PEP, а особенно PEP 8, речь идет о руководстве по стилю кода Python, которое всегда можно найти на сайте Python .org или с помощью простого веб-поиска по запросу «pep 8». Знакомство с объектно ориентированным программированием Прежде чем углубляться в технические детали, важно отметить, что существует несколько подходов к проектированию языков программирования. Одним из самых успешных и широко применяемых является объектно ориентированное программирование (ООП). Эта философия моделирования основывается на представлении мира как набора объектов, у которых есть свойства и методы, то есть характеристики и действия. Рассмотрим пример автомобиля. Каждый конкретный автомобиль — это объект. Машины отличаются друг от друга маркой, моделью, годом выпуска, цветом и размером, но все они служат одной базовой цели: перевозить людей из точки А в точку Б. Кроме свойств, автомобили обладают и общими методами — действиями, которые с ними можно выполнять. Например: управлять движением, поворачивать, ускоряться, тормозить, регулировать температуру в салоне. Все эти операции выполняются через элементы управления, понятные любому водителю. В языке программирования объект — это не материальная вещь, а программная сущность. В Python объект создается с помощью класса — своеобразной «фабрики», которая может производить множество объектов для разных целей (например, спортивные автомобили, внедорожники или седаны). Все объекты одного класса обладают одинаковыми базо- 58 РАЗДЕЛ 1 Начало работы
выми методами управления, так же как любой автомобиль управляется рулем, педалями и коробкой передач. Python полностью поддерживает объектно ориентированный подход. Его основа — ключевые слова и конструкции, позволяющие работать с объектами как в собственных программах, так и в программах других разработчиков. Освоив базовый синтаксис, вы сможете без труда использовать чужие классы и объекты. Это похоже на навык вождения: научившись управлять одной машиной, вы в целом умеете водить любую — органы управления будут знакомыми и предсказуемыми. Почему отступы имеют огромное значение Одной из особенностей, которая сразу выделяет Python среди других языков программирования, является способ оформления кода. В Python для обозначения блоков кода используются отступы, а не круглые или фигурные скобки. Если вы только начинаете изучать программирование, это может показаться мелочью. Но на самом деле такой подход делает код гораздо чище и легче для чтения. Тем, кто уже знаком с другими языками (например, JavaScript), известно, сколько усилий уходит на работу со скобками, чтобы правильно управлять вложенностью кода. Рассмотрим простой пример на JavaScript. Если вы встречали игрушку «Магический шар», то, возможно, понимаете, что делает эта программа, но сейчас это не столь важно. Обратите внимание на множество круглых и фигурных скобок, а также точки с запятой: document.addEventListener("DOMContentLoaded", function () {var question = prompt("Ask magic 8 ball a question");var answer = Math.floor(Math.random() * 8) + 1; if (answer == 1) {alert("It is certain");} else if (answer == 2) {alert("Outlook good");} else if (answer == 3) {alert("You may rely on it") ;} else if (answer == 4) {alert("Ask again later");} else if (answer == 5) {alert("Concentrate and ask again");} else if (answer == 6) {alert ("Reply hazy, try again");} else if (answer == 7) {alert("My reply is no");} else if (answer == 8) {alert("My sources say no")} else {alert ("That's not a question");}alert("The end");}) ГЛАВА 3 Элементы и синтаксис Python 59
Код в таком виде выглядит неряшливо и плохо читается. Сделать его удобнее можно, если разбить на несколько строк и добавить отступы. Заметьте, что в JavaScript это необязательно — программа будет работать и без форматирования. Но для человека правильно оформленный код значительно понятнее. Ниже показан тот же самый фрагмент в более аккуратной записи: document.addEventListener("DOMContentLoaded", function () { var question = prompt("Ask magic 8 ball a question"); var answer = Math.floor(Math.random() * 8) + 1; if (answer == 1) { alert("It is certain"); } else if (answer == 2) { alert("Outlook good"); } else if (answer == 3) { alert("You may rely on it"); } else if (answer == 4) { alert("Ask again later"); } else if (answer == 5) { alert("Concentrate and ask again"); } else if (answer == 6) { alert("Reply hazy, try again"); } else if (answer == 7) { alert("My reply is no"); } else if (answer == 8) { alert("My sources say no") } else { alert("That's not a question"); } alert("The end"); }) В JavaScript круглые и фигурные скобки обязательны, потому что именно они показывают, где начинается и заканчивается блок кода. Отступы при этом можно использовать только для удобства чтения — язык не заставляет их соблюдать. В Python все наоборот. Здесь нет фигурных скобок или специальных символов для выделения блоков. Их роль выполняют отступы. Поэтому они не просто желательны, а обязательны: именно отступы определяют структуру программы и напрямую влияют на ее выполнение. Благодаря этому код на Python проще воспринимать: сразу видно, что к чему относится, и ничто не отвлекает — ни скобки, ни лишние символы. Ниже приведен пример того же кода, но уже на Python: 60 РАЗДЕЛ 1 Начало работы
import random question = input("Ask magic 8 ball a question") answer = random.randint(1, 8) if answer == 1: print("It is certain") elif answer == 2: print("Outlook good") elif answer == 3: print("You may rely on it") elif answer == 4: print("Ask again later") elif answer == 5: print("Concentrate and ask again") elif answer == 6: print("Reply hazy, try again") elif answer == 7: print("My reply is no") elif answer == 8: print("My sources say no") else: print("That's not a question") print("The end") СОВЕТ В начале примера на Python вы могли заметить строку, начинающуюся со слова import. Такие строки встречаются в языке довольно часто. В следующем разделе мы разберемся, зачем они нужны и как с ними работать. Использование модулей Python Одна из причин популярности Python — его простота. В основе языка лежит небольшой и чистый базовый синтаксис, именно с него и стоит начинать изучение. Но этим возможности Python не ограничиваются. Для него создано множество модулей, которые можно бесплатно подключить и использовать в своем коде. Все они написаны на самом Python, но вам не нужно вникать во внутреннее устройство: достаточно просто импортировать модуль и работать с его функциями. Большинство модулей решают конкретные задачи: от работы с датами и временем до научных вычислений и искусственного интеллекта. Прелесть в том, что другие программисты уже потратили время на их разработку, тестирование и отладку, а вы можете просто использовать готовые решения. ГЛАВА 3 Элементы и синтаксис Python 61
Например, программа «Магический шар» из предыдущего раздела начинается со строки: import random В базовый язык Python не встроена функция генерации случайных чисел. Конечно, можно было бы попытаться написать ее самостоятельно, но в этом нет необходимости: модуль random уже реализует все нужные возможности. Подключив его с помощью import random, вы даете программе понять, что хотите использовать функции этого модуля. Например, чтобы сгенерировать случайное число от 1 до 8, достаточно одной строки: answer = random.randint(1, 8) Понимание синтаксиса импорта модулей Как упоминалось ранее, в вашем собственном коде Python необходимо импортировать модуль, прежде чем вы сможете использовать его возможности. Синтаксис для этого следующий: import modulename [as alias] Код, написанный в таком обобщенном формате, где некоторые части выделены курсивом, а некоторые заключены в квадратные скобки, иногда называют синтаксической диаграммой, поскольку она показывает не буквально то, что нужно набирать, а синтаксис (формат) кода. Вот как представлена информация в такой синтаксической диаграмме: ЗАПОМНИТЕ 62 » Код чувствителен к регистру, что означает необходимость набирать import и as строчными буквами, как показано . » Все, что написано курсивом, является заполнителем для информации, которую вы должны предоставить в своем собственном коде . Например, в вашем коде вы замените modulename на имя модуля, который хотите импортировать . » Все, что заключено в квадратные скобки, является необязательным, поэтому вы можете набирать команду как с этой частью, так и без нее . Вы можете набирать строку импорта в любом месте, где пишете код Python: в командной строке Python (>>>), в файле .py или в блокноте Jupyter. В файле .py всегда размещайте операторы импорта в начале, чтобы их возможности были доступны для остального кода. РАЗДЕЛ 1 Начало работы
Использование псевдонима с модулями Как вы уже видели, модуль в Python можно подключить с помощью команды import. При этом ему можно присвоить псевдоним — короткое имя, которое будет использоваться вместо полного названия. Для этого после имени модуля пишут ключевое слово as и выбранное вами имя. СОВЕТ Обычно выбирают короткий вариант, который легко набирать и запоминать. Это особенно полезно, если модуль имеет длинное название или используется во многих местах программы. Например, вместо того чтобы писать import random можно сократить импорт так: import random as rnd Теперь в коде не придется использовать полное имя random. Достаточно короткого варианта: answer = rnd.randint(1, 8) В данном примере это выглядит как мелочь, но с большими модулями экономия заметна: код становится короче и читается проще. На этом мы заканчиваем вводную часть. Теперь, когда у вас есть базовое представление об импорте и модулях, самое время перейти к практике и попробовать работать с реальным кодом Python. Увидимся в следующей главе!
В ЭТОЙ ГЛАВЕ: » Как открывать Python-файлы » Как использовать комментарии в Python » Что представляют собой типы данных в Python » Как работать с операторами Python » Как создавать переменные » Что такое синтаксис и как он устроен в Python » Как правильно организовывать код Гл а в а 4 Создание вашего первого приложения на Python И так, вы хотите создать приложение на Python? Независимо от того, собираетесь ли вы разрабатывать веб-сайт, анализировать данные или автоматизировать задачи с помощью скриптов, эта глава даст вам базовые знания для начала работы. Большинство людей используют языки программирования, такие как Python, для создания прикладных программ — их обычно называют приложениями, программами или просто «apps». Чтобы приступить к этому, нужно уметь писать код в редакторе и постепенно осваивать сам язык (в данном случае — Python). Как и в любом языке, сначала важно понимать отдельные «слова», чтобы затем строить из них предложения и в конечном итоге блоки кода, которые будут обеспечивать работу приложения. Сначала вы познакомитесь с процессом создания файла, в котором будет размещаться ваш код. Затем изучите основные типы данных, переменные и операторы — своего рода словарный запас Python, а также его синтаксис. По мере освоения 64 РАЗДЕЛ 1 Начало работы
материала вы узнаете, как сохранять приложение и добавлять комментарии к коду, чтобы вы и другие разработчики могли легко понять его устройство и назначение. Готовы начать? Открытие файла приложения Python В этой книге для изучения Python и создания приложений мы будем использовать популярный редактор Visual Studio Code (VS Code). Предполагается, что вы уже настроили рабочую среду, как описано в предыдущих главах, и знаете, как открыть VS Code. Чтобы продолжить работу с материалом этой главы, выполните следующие шаги: 1. Откройте VS Code с помощью Launchpad на Mac или меню «Пуск» в Windows. Затем выберите Файл  Открыть папку или используйте встроенный проводник VS Code, чтобы открыть папку AIO Python, созданную вами в главе 1 раздела 1. Вы можете заметить новую папку с именем .vscode среди других файлов . Не переживайте, — это служебная информация редактора, на которую пока можно не обращать внимания . 2. Если вы уже создали файл hello.py (см. главу 2 раздела 1), щелкните по нему, чтобы открыть. Если файла нет, создайте его сейчас, а затем вернитесь к этому разделу. 3. Выделите весь текст в первой строке и удалите его — так вы начнете работу с чистого листа. К этому моменту файл hello.py должен быть открыт в редакторе (рисунок 4.1). Если у вас открыты другие вкладки, закройте их, нажав X в заголовке каждой. Рисунок 4.1. Файл hello. py открытый для редактирования в VS Code ГЛАВА 4 Создание вашего первого приложения на Python 65
Написание и использование комментариев в Python Прежде чем написать первую строку кода, добавим комментарий программиста. Комментарий — это текст внутри программы, который не выполняется и не влияет на работу кода. Возникает вопрос: «Если комментарий ничего не делает, зачем он нужен?» Для начинающего разработчика комментарии особенно полезны: они помогают оставлять заметки для себя о том, что делает тот или иной участок кода. Такие подсказки здорово упрощают обучение и помогают лучше понимать написанное. Однако комментарии в коде полезны не только новичкам. В командной работе профессиональные разработчики часто используют их, чтобы объяснить коллегам назначение своего кода. Комментарии также помогают самому автору: при повторном просмотре программы легко вспомнить, зачем была выбрана та или иная реализация. Так как комментарий не является частью выполняемого кода, писать в нем можно все что угодно. Главное — правильно обозначить, что это комментарий. В Python для этого есть два способа: » » начать строку со знака решетки #; заключить текст в тройные кавычки ("""или ''') . Если комментарий короткий и занимает одну строку, обычно используют знак решетки. После него принято ставить пробел (хотя это и необязательно), например: Чтобы написать комментарий в Python в своем коде: 1. В VS Code щелкните рядом с цифрой 1 во вкладке hello.py и введите: # This is a Python comment in my first Python app. 2. Нажмите Enter. Теперь на первой строке файла появится комментарий (рисунок 4 .2) . Если вы используете тему оформления по умолчанию, текст комментария будет выделен зеленым цветом . 66 РАЗДЕЛ 1 Начало работы
Рисунок 4.2. Комментарий в hello.py Хотя многострочные комментарии нам пока не понадобятся, стоит знать, что в Python можно писать длинные пояснения, заключая их в тройные кавычки. Такие комментарии часто называют строками документации (docstrings). Их обычно размещают в начале модуля, функции, класса или метода, то есть в ключевых строительных блоках приложения. Пример многострочного комментария: В Visual Studio Code комментарии обычно выделяются другим цветом, чтобы отличать их от кода. Короткие комментарии, начинающиеся с #, отображаются зеленым цветом, а строки документации — коричневым, что помогает визуально отделить их от исполняемого кода Python. В вашем коде может быть неограниченное количество комментариев. Если вы ждете, что после написания комментария что-то произойдет, то не ждите. Когда вы работаете в редакторе, код ничего не делает, пока вы его не запустите. А сейчас у нас есть только комментарий, поэтому даже если мы запустим этот код, ничего не произойдет, ведь комментарии предназначены для людей, а не для компьютеров. Прежде чем начать писать код, вам нужно освоить самые базовые вещи, а именно… Понимание типов данных в Python Мы постоянно работаем с информацией — пишем текст, используем числа, составляем списки. В повседневной жизни мы редко задумываемся о том, чем именно отличается число от текста. Числа — это количественные значения, например 10 или 123.45. Текст — это буквы, слова и символы. Для компьютера различие между ними принципиально: с числами можно выполнять арифметические операции (сложение, вычитание, умножение, деление), а с буквами и словами — нет. ГЛАВА 4 Создание вашего первого приложения на Python 67
Например, всем очевидно, что 1 + 1 = 2. Но выражение A + A не имеет математического смысла: оно не равно ни B, ни AA, ни чему-либо еще, потому что буквы не являются количественными величинами. Представьте: вы можете купить 12 яблок, потому что 12 — это число, выражающее количество. Но нельзя купить «трубку яблок» — слово «трубка» описывает предмет, а не число и не измеряемую величину. Числа В Python числа записываются строго по определенным правилам: • • • начинаться они могут с цифры (0–9), точки (для дробных значений) или знака минус (–) — для отрицательных чисел; число может содержать только одну десятичную точку; в записи числа не допускаются буквы, пробелы, знаки валюты и другие символы, не относящиеся к обычному числовому формату . В таблице 4.1 приведены примеры корректных и некорректных чисел в Python. Таблица 4.1. Примеры корректных и некорректных чисел в Python Число Корректно или нет? Причина 1 Корректно Целое число 1 .1 Корректно Число с десятичной точкой 1234567 .89 Корректно Большое число, без запятых -2 Корректно Отрицательное число (минус перед значением) 0 .99 Корректно Меньше 1, начинается с 0 $1 .99 Некорректно Содержит знак $ 12,345 .67 Некорректно Содержит запятую 1101 3232 Некорректно Содержит пробел 91740–3384 Некорректно Содержит дефис 123 .45 .6789 Некорректно Содержит два дефиса 123 Oak Tree Lane Некорректно Содержит пробел и слова 68 РАЗДЕЛ 1 Начало работы
Число Корректно или нет? Причина (267)555 .1234 Некорректно Содержит скобки и дефис 127 .0 .0 .1 Некорректно Несколько точек, а разрешена одна СОВЕТ СОВЕТ Если вы переживаете, что строгие правила записи чисел помешают использовать денежные суммы, почтовые индексы или адреса — не волнуйтесь. Python позволяет хранить и обрабатывать любые виды данных, просто для разных задач применяются разные типы. В повседневных программах вы чаще всего будете иметь дело с простыми числами — именно такими, как показано в первых четырех примерах правильных чисел. Однако при изучении более сложного кода, особенно в научных или математических приложениях, можно встретить записи, содержащие буквы e или j. Это связано с тем, что Python поддерживает три основных типа чисел. С ними вы познакомитесь в следующих разделах. Целые числа Целое число — это любое число без десятичной точки, положительное или отрицательное. Размер таких чисел в Python не ограничен. Примеры: 0, –1, 999999999999999. Все они являются допустимыми целыми числами. Числа с плавающей точкой Число с плавающей точкой (или float) — это любое допустимое число с десятичной точкой. Например: 1.1, –1.1, 123456.789012345. Как и у целых чисел ограничений по размеру нет. Для работы с очень большими числами используется научная нотация: буква e указывает степень числа 10. Например: • • 234e3 = 234000 (буквально 234 × 10³) 234e1000 = 234 × 10¹⁰⁰⁰ ГЛАВА 4 Создание вашего первого приложения на Python 69
Даже если в записи нет точки, число с e считается числом с плавающей точкой. При записи больших чисел в Python не используйте запятые. Например: • • 1000 — корректно; 1,000 — ошибка . То же самое с миллионом: • • 1000000 — корректно; 1,000,000 — ошибка . Комплексные числа В большинстве случаев достаточно знать только целые и числа с плавающей точкой. Но Python также поддерживает комплексные числа — они записываются с буквой j, обозначающей мнимую часть. Пример: z = 2 + 3j Здесь 2 — действительная часть, а 3j — мнимая. Если вы не сталкивались с комплексными числами в математике, скорее всего, они вам не понадобятся. Обычно их используют только в специальных областях (например, инженерных или научных расчетах). Слова (строки) Строки в Python — это, по сути, противоположность числам. • • С числами можно выполнять арифметические операции (сложение, вычитание, умножение, деление) . Строки же предназначены для работы с текстом — именами, адресами, предложениями и любыми другими последовательностями символов . Строка называется так потому, что представляет собой цепочку символов: букв, пробелов, знаков пунктуации и даже цифр (если они не используются как числа). Для человека строка обычно имеет смысл — например, «Иван Петров» или «ул. Ленина, 15». Но компьютер не «понимает» текст: для него 70 РАЗДЕЛ 1 Начало работы
строка — это просто набор символов, с которым нельзя выполнять арифметические действия. Чтобы Python понял, что перед ним строка, ее нужно заключить в кавычки. Причем можно использовать как двойные ("), так и одинарные (© ) кавычки. Примеры допустимых строк: "Hi there, I am a string" 'Hello world' "123 Oak Tree Lane" "(267)555-1234" "18901-3384" Обратите внимание, что в строках можно использовать цифры (0–9), а также дефисы и точки. Все это все равно считается строками, если заключено в кавычки. Если строка содержит апостроф (одинарную кавычку), всю строку следует заключать в двойные кавычки, например: ОСТОРОЖНО "Mary's dog said Woof" В этом случае двойные кавычки позволяют избежать путаницы. Если же написать так: 'Mary's dog said Woof' Python решит, что строка начинается с первой одинарной кавычки и заканчивается на апострофе в слове Mary's. Оставшийся текст программа не сможет обработать, и код не будет работать. Аналогично, если строка содержит двойные кавычки, ее лучше заключить в одинарные: 'The dog of Mary said "Woof".' Первая одинарная кавычка открывает строку, последняя закрывает, а двойные внутри не вызывают ошибок. Что делать, если строка содержит и одинарные, и двойные кавычки? Например: Mary's dog said "Woof". ГЛАВА 4 Создание вашего первого приложения на Python 71
Такой случай действительно может вызвать затруднение. К счастью, создатели Python это предусмотрели и предложили решение — использование экранирующих символов. Экранирование позволяет «обойти» специальное значение символов (например, одинарных или двойных кавычек) и сделать так, чтобы они воспринимались как обычные знаки внутри строки. Чтобы экранировать символ, поставьте перед ним обратную косую черту (\). Важно: используйте именно обратную косую черту (наклоненную к предыдущему символу: \), иначе код не будет работать правильно. Продолжая предыдущий пример, строку можно записать несколькими способами: 'Mary\'s dog said "Woof".' Или использовать двойные кавычки снаружи и экранировать внутренние двойные: "Mary's dog said \"Woof\"." Оба варианта будут работать корректно. Еще одно распространенное применение обратной косой черты — символ новой строки \n. Он вставляет перенос строки там, где в обычном тексте вы нажали бы Enter. Например: "The old pond\nA frog jumped in,\nKerplunk!" При выводе пользователю эта строка будет выглядеть так: The old pond A frog jumped in, Kerplunk! Каждый \n превратился в переход на новую строку. Логический тип данных Третий основной тип данных в Python не является ни числом, ни строкой. Он называется логическим или булевым (в честь математика Джорджа Буля). Такой тип данных может принимать только одно из двух значений: True или False. 72 РАЗДЕЛ 1 Начало работы
В коде Python программисты обычно сохраняют эти значения в переменных (о переменных речь пойдет чуть позже). Например: x = True или x = False Определить, что это именно логические значения, легко: СОВЕТ • • • они не заключены в кавычки, как строки; они не являются числами; и очень важно — они пишутся с заглавной буквы в начале. То есть правильно писать только так: True и False. Работа с операторами Python Как мы уже обсуждали, в Python (и в программировании вообще) данные можно представить тремя основными типами: числа, строки и логические значения. Но помимо хранения информации компьютеры полезны еще и тем, что умеют выполнять над ней различные операции: • • • • математические вычисления; сравнения; поиск; и другие действия, которые помогают находить и упорядочивать данные . В Python для этого существует целый набор специальных символов — операторов. Они позволяют работать с разными типами данных и сравнивать их между собой. В этом разделе мы приведем обзор всех операторов, чтобы у вас был удобный справочник. Подробности того, как и где использовать каждый из них, вы узнаете позже. Пока же достаточно просто запомнить, что такие инструменты в Python есть. ГЛАВА 4 Создание вашего первого приложения на Python 73
Арифметические операторы Как ясно из названия, арифметические операторы используются для выполнения математических действий: сложения, вычитания, умножения, деления и других вычислений. В следующей таблице (таблица 4.2) приведены основные арифметические операторы Python: Таблица 4.2. Арифметические операторы Python Оператор Описание Пример + Сложение 1+1=2 - Вычитание 10–1 = 9 * Умножение 3 * 5 = 15 / Деление 10 / 5 = 2 .0 % Остаток от деления 11% 5 = 1 ** Возведение в степень 3**2 = 9 // Целочисленное деление (дробная часть откидывается) 11 // 5 = 2 Первые четыре оператора из таблицы такие же, как вы изучали еще в начальной школе. Последние три — немного сложнее, поэтому разберем их подробнее: 74 » Деление с остатком (модуль) % Возвращает остаток от деления . Например: 11% 5 = 1, потому что 11 делится на 5 дважды (2 × 5 = 10), и остается остаток 1 . Именно это значение и возвращает оператор % . » Возведение в степень ** В Python нельзя писать число «вверх (как в математике: 3²), поэтому используется двойная звездочка . Например: 3 ** 2 = 9 (3 в квадрате, то есть 3 × 3) 3 ** 4 = 81 (3 в четвертой степени, то есть 3 × 3 × 3 × 3) РАЗДЕЛ 1 Начало работы
» Целочисленное деление // Это деление, при котором дробная часть просто отбрасывается (без округления) . Например: 9 / 5 = 1 .8 (обычное деление) 9 // 5 = 1 (дробная часть .8 отрезана, а не округлена до 2) Операторы сравнения Компьютеры тоже умеют «принимать решения» во время работы программы. Конечно, это не человеческие решения, а выбор, основанный на четких фактах. Эти факты выражаются через сравнения. В Python для этого существует набор специальных символов — операторов сравнения. С их помощью можно написать код, который проверяет условия и действует в зависимости от результата. В таблице 4.3 перечислены основные операторы сравнения в Python. Таблица 4.3. Операторы сравнения Python Оператор Значение < Меньше чем <= Меньше чем или равно > Больше чем >= Больше чем или равно == Равно != Не равно is Совпадение объектов is not Отрицание совпадения объектов Большинство из них интуитивно понятны, поэтому мы не будем останавливаться на них слишком подробно. Два последних связаны с объектами Python — о них мы подробно поговорим в главе 6 раздела 2. Сейчас достаточно освоить основы. ГЛАВА 4 Создание вашего первого приложения на Python 75
Логические операторы Логические операторы работают с булевыми значениями (True или False) и помогают проверять, является ли одно или несколько условий истинными или ложными. В таблице 4.4 приведены основные логические операторы Python. Таблица 4.4. Логические операторы Python Оператор Пример кода Что проверяет or x or y Истинно, если хотя бы одно из условий истинно and x and y Истинно, только если оба условия истинны not Not x Инвертирует значение: True становится False, а False становится True Руководство по стилю Python (PEP 8) рекомендует всегда ставить пробелы вокруг операторов. Это делает код легче читаемым. Иными словами: • • • введите пробел; затем сам оператор; и еще один пробел перед продолжением строки кода . Вот простой пример. Не беспокойтесь, если пока не до конца понимаете его смысл — важно только обратить внимание на оформление, а именно на пробелы вокруг операторов равенства (==) и больше (>): num = 10 if num > 0: print("Positive number") else: print("Negative number") Первая строка сохраняет число 10 в переменной с именем num. Это делается с помощью оператора присваивания (=), который записывает значение в переменную. Затем идет условие if, которое проверяет: больше ли num нуля. 76 РАЗДЕЛ 1 Начало работы
• • Если условие истинно, программа выводит: "Positive number" . В противном случае срабатывает блок else, и на экран выводится: "Negative number" . Например, если изменить первую строку на: num = –1 и снова запустить программу, результат будет "Negative number", так как –1 — это отрицательное число. Мы использовали имя переменной num, чтобы показать работу операторов и оформление пробелов вокруг них. Но так как переменные мы еще не обсуждали подробно, этот пример мог показаться вам немного загадочным. Не переживайте — в следующем разделе мы подробно разберем, что такое переменные и как с ними работать. Создание и использование переменных Переменные — это важная часть Python и любого языка программирования. Это просто «контейнер» для информации, который может изменяться. Например, когда вы заходите на сайт со своей учетной записью, на главной странице может отображаться ваше имя пользователя. Оно показывается там потому, что при создании аккаунта было сохранено, а при входе в систему помещается в переменную. Именно содержимое этой переменной (ваше имя) выводится в браузере. В коде переменная представляется именем, а не самим значением. Еще один способ понять переменные — через пример с покупками. Итоговая цена всегда рассчитывается по формуле: Итоговая цена = Количество * Цена за единицу Здесь «Количество» и «Цена за единицу» — это переменные. Подставляя разные числа, вы получаете правильный результат: • • если вы покупаете 3 горлицы по $1 .00 за штуку, итоговая цена = 3 * 1 .00 = $3 .00; если вы берете 2 дюжины роз по $1 .50 за штуку, итоговая цена = 24 * 1 .50 = $36 .00 . Таким образом, переменные позволяют работать с меняющимися данными, а программа выполняет вычисления в зависимости от их значений. ГЛАВА 4 Создание вашего первого приложения на Python 77
Имена переменных в Python В примерах выше мы использовали такие условные имена, как «Количество» и «Цена за единицу». В реальном коде Python вы можете придумывать свои собственные имена переменных, но они должны соответствовать определенным правилам: ЗАПОМНИТЕ » Имя переменной должно начинаться с буквы или со знака подчеркивания (_) . » После первого символа можно использовать только буквы, цифры и подчеркивания . » Имена чувствительны к регистру: переменные value, Value и VALUE — это три разные переменные . » Имена переменных не заключаются в кавычки и не могут их содержать . » Имя не должно совпадать с ключевым словом Python (например, if, while, class) . » Согласно рекомендациям стиля PEP 8, в Python для имен переменных принято использовать: только строчные буквы и слова разделять знаком подчеркивания (_) . PEP 8, о котором мы уже упоминали, — это руководство по стилю написания кода в Python. Оно не является строгим набором обязательных правил, но служит хорошим ориентиром для того, чтобы код был единообразным и удобным для чтения. Тем не менее в реальных проектах вы можете встретить имена переменных, не соответствующие PEP 8. Например, довольно часто используется верблюжий регистр (camelCase), при котором первая буква имени остается строчной, а каждое новое слово пишется с заглавной: extendedPrice или unitPrice. Такая форма распространена, и ее можно увидеть даже в Python-коде. Однако опытные разработчики, строго придерживающиеся стиля, предпочитают вариант, рекомендуемый PEP 8 — нижнее подчеркивание для разделения слов: extended_price unit_price Считается, что такой стиль проще для чтения и лучше подходит для Python-сообщества. 78 РАЗДЕЛ 1 Начало работы
Создание переменных в коде Чтобы создать переменную в Python, используется следующий синтаксис: variablename = value variablename — придуманное вами название. В простых примерах можно использовать короткие имена вроде x или y (как в математике), но в ре- альных программах лучше давать более осмысленные имена. Это облегчает понимание кода: по названию сразу видно, какие данные хранятся в переменной. value — это то, что будет сохранено в переменной. Оно может быть числом, строкой, логическим значением (True или False) или даже резуль- татом вычисления. Символ = в Python называется оператором присваивания. Он присваивает значение (справа) переменной (слева). Например, здесь: x = 10 Здесь числу 10 присвоено имя переменной x. А здесь: user_name = "Alan" В этом случае строка "Alan" помещается в переменную с именем user_ name. Манипуляции с переменными Большая часть программирования сводится к тому, чтобы сохранять значения в переменных и выполнять с ними различные операции с помощью операторов. Ниже приведены простые примеры, которые помогут вам освоиться. Если у вас все еще открыт VS Code с единственным комментарием в файле, выполните следующие шаги прямо в редакторе: 1. Под строкой # This is a Python comment in my first Python app. напечатайте этот код и нажмите Enter: # This variable contains an integer ГЛАВА 4 Создание вашего первого приложения на Python 79
2. Введите следующую строку (не забудьте поставить пробелы до и после знака =) и нажмите Enter: quantity = 10 3. Введите следующее и нажмите Enter: # This variable contains a float 4. Введите следующее (не вводите знак доллара!) и нажмите Enter: unit_price = 1.99 5. Введите следующее и нажмите Enter: # This variable contains the result of multiplying quantity на unit_price 6. Введите следующее (обязательно с пробелами вокруг операторов) и нажмите Enter: extended_price = quantity * unit_price 7. Введите следующее и нажмите Enter: # Show the results 8. Наконец, введите это и нажмите Enter: print(extended_price) Ваше приложение на Python создает несколько переменных, сохраняет в них значения и вычисляет новое значение extended_price на основе содержимого переменных quantity и unit_price. Последняя строка программы выводит значение переменной extended_price на экран. Помните: комментарии не выполняются при запуске кода. Они служат только подсказками для вас (или других разработчиков), объясняя, что происходит в программе. Если у вас включен GitHub Copilot, он может предлагать продолжение кода. Подсказки отображаются приглушенным серым цветом. Чтобы принять предложение Copilot, нажмите клавишу Tab — перепечатывать вручную не нужно. На рисунке 4.3 показано, как все должно выглядеть. Если вы где-то допустите ошибку, редактор может подсветить ее волнистыми линиями — это могут быть как реальные ошибки в коде, так и стилистические замечания 80 РАЗДЕЛ 1 Начало работы
(например, лишний пробел или отсутствие перевода строки в конце файла). При написании кода нужно быть особенно внимательным. В обычной переписке с человеком небольшие опечатки не мешают понять смысл, но у компьютера нет ни глаз, ни воображения, ни способности «додумать». Если в коде есть ошибка, программа просто не будет работать. Рисунок 4.3. Ваше первое Pythonприложение, набранное в VS Code Другими словами, если код написан неверно, программа не запустится или будет работать неправильно. Все очень просто — никаких исключений: либо код корректен, либо он не выполняется вовсе. Сохранение работы Написание кода похоже на создание любого другого документа на компьютере. Если вы не сохраните изменения, они могут пропасть к следующему разу, когда вы откроете редактор. Поэтому, если у вас не включено автосохранение (как описано во второй главе этого раздела), выберите в меню Файл  Сохранить. Запуск вашего Python-приложения в VS Code Теперь можно запустить приложение и проверить, работает ли оно. Самый простой способ — щелкнуть правой кнопкой мыши по имени файла hello .py в панели проводника и выбрать пункт «Запустить Python-файл в терминале», как показано на рисунке 4.4. ГЛАВА 4 Создание вашего первого приложения на Python 81
Если код введен правильно, в панели терминала появится результат: 19.9 Этот вывод создается командой print(extended_price) в вашем коде. Значение равно 19.9, потому что количество (10), умноженное на цену за единицу (1.99), действительно дает 19.9. Рисунок 4.4. Щелкните правой кнопкой мыши по файлу .py и выберите «Запустить файл Python в терминале» Рисунок 4.5. Число 19 .9 — это результат выполнения команды print (extended_ price) в коде Предположим, вашему приложению нужно вычислить общую стоимость 14 товаров по цене $26.99 за каждый. Для этого вовсе не нужно писать новую программу. Достаточно изменить существующий код: • • 82 заменить значение переменной quantity с 10 на 14; заменить значение переменной unit_price на 26 .99 (без знака доллара) . РАЗДЕЛ 1 Начало работы
После изменений программа будет выглядеть так: # This is a Python comment in my first Python app. # This variable contains an integer quantity = 14 # This variable contains a float unit_price = 26.99 # This variable contains the result of multiplying quantity times unit_price extended_price = quantity * unit_price # Show some results on the screen. print(extended_price) Сохраните свою работу (если у вас не включено автосохранение). Затем снова запустите приложение: щелкните правой кнопкой мыши по имени файла и выберите «Run Python File in Terminal», как и в первый раз. В нижней панели терминала VS Code вы увидите результат: 377.85999999999996 Значение выглядит странно — оно не округлено до центов и не похоже на денежную сумму. Но это нормальное поведение Python при работе с числами с плавающей точкой. Не переживайте об этом сейчас. Прежде чем прыгать с шестом, нужно научиться хотя бы ползать. Поэтому пока просто радуйтесь тому, что ваше приложение работает и выдает результат. Понимание синтаксиса и его важности Если заглянуть в словарь, то синтаксис определяется как «расположение слов и фраз для создания правильно построенных предложений в языке». В языках программирования, таких как Python, нет понятий «предложение» или «грамматика» в привычном смысле. Но у Python есть свои «слова» (ключевые элементы кода), и между ними, как и в обычном тексте, нужны пробелы. Кроме того, очень важно соблюдать правильный порядок этих «слов». ГЛАВА 4 Создание вашего первого приложения на Python 83
Синтаксис играет ключевую роль и в человеческих языках, ведь именно порядок слов формирует смысл. Например, сравните три похожих предложения: Мария поцеловала Джона. Джон поцеловал Марию. Поцеловала Мария Джона. Все они содержат одинаковые слова, но смысл разный. Первые два ясно указывают, кто кого поцеловал, а последнее звучит двусмысленно. В программировании правильный синтаксис не менее важен, а во многом даже важнее. Когда вы общаетесь с человеком, он обычно может понять смысл даже при ошибках в речи или письме — из контекста. У компьютеров же нет мозга, воображения и понятия «контекста». Они не могут догадаться, что вы имели в виду. Если синтаксис нарушен, программа просто не будет работать. Таким образом, синтаксис в языках программирования играет даже более важную роль, чем в человеческих языках. Если вернуться к самому первому коду из этой главы, можно заметить, что почти все строки собственно кода (не считая комментариев, которые начинаются с #) следуют такому синтаксису: variablename = value где variablename — это придуманное вами имя переменной, а value — значение, которое вы в нее сохраняете. Это работает именно потому, что запись соответствует правильному синтаксису. Но если попытаться написать код неправильно, например так: value = variablename ничего не получится — Python просто выдаст ошибку. Например, вот правильный способ сохранить значение 10 в переменной с именем x: x = 10 Можно подумать, что запись можно перевернуть местами, но в Python это не сработает: 10 = x 84 РАЗДЕЛ 1 Начало работы
Если вы введете такую строку в VS Code, редактор сразу подсветит ее волнистым подчеркиванием. Это означает, что вы нарушили синтаксис — такой код выполнить невозможно. Если навести курсор мыши на подчеркнутый участок, появится подсказка с описанием ошибки. А если запустить программу с таким кодом, ничего страшного не произойдет: компьютер не «сломается». Но вы получите сообщение об ошибке примерно такого вида: File ".../AIO Python/hello.py", line 10 10=x ^^ SyntaxError: cannot assign to literal here Часть SyntaxError указывает на то, что Python не понимает эту строку кода, так как нарушен правильный синтаксис. Чтобы исправить ошибку, достаточно переписать строку правильно: x = 10 Теперь рассмотрим, как устроены отдельные строки кода. В Python строка кода обычно заканчивается переносом строки или точкой с запятой. Например, вот три строки кода: first_name = "Alan" last_name = "Simpson" print(first_name, last_name) Также допустимо объединять несколько операторов в одной строке с помощью точки с запятой: first_name = "Alan"; last_name = "Simpson" print(first_name, last_name) Или, если вам так больше нравится, весь код можно записать в одну строку, разделив команды точкой с запятой: first_name = "Alan"; last_name = "Simpson"; print(first_name, last_name) Программа будет работать одинаково — неважно, заканчивается ли каждая строка переносом или точкой с запятой. Обратите внимание на имена переменных: first_name last_name ГЛАВА 4 Создание вашего первого приложения на Python 85
ЗАПОМНИТЕ Они написаны в нижнем регистре, а слова разделены подчеркиванием. Такой стиль называется snake_case и является принятым соглашением об именовании в Python. Но важно понимать: соглашение — это не то же самое, что правило синтаксиса. Например, вы могли бы назвать переменные так: FirstName LastName и это не вызовет ошибки. Код останется рабочим. Соглашения нужны для того, чтобы программисты писали код в едином стиле. Это делает программы более читаемыми, особенно при работе в командах. До этого момента вы рассматривали отдельные строки кода. Но в Python есть и блоки кода, где несколько строк объединяются вместе. Вот простой пример: x = 10 if x == 0: print("x is zero") else: print("x is", x) print("All done") ПРИМЕЧАНИЕ В Python знак == (два знака равенства) используется для сравнения значений и проверки их равенства. Это отличается от = (одного знака равенства), который служит оператором присваивания, то есть присваивает переменной значение. Первая строка x = 10 присваивает переменной x значение 10. Затем условие if x == 0 проверяет, равно ли это значение нулю. Если x действительно равно 0, выполняется команда print("x is zero"), и на экране появится соответствующее сообщение. Если же x не равно 0, этот код пропускается, и выполняется блок после else:, где команда print("x is", x) выводит текущее значение переменной. В любом случае последняя строка print("All done!") выполнится всегда, так как она не относится ни к условию, ни к ветке else. ЗАПОМНИТЕ 86 Как видите, в Python отступы играют очень важную роль. В приведенном примере выполняется только одна из строк с отступом — в зависимости от значения переменной x. По мере изучения книги вы узнаете все детали работы с отступами, а пока достаточно запомнить: синтаксис и отступы в Python критически важны, поэтому при написании кода нужно быть максимально внимательным. РАЗДЕЛ 1 Начало работы
В этой главе вы узнали, как набирать код, сохранять его, запускать, вносить изменения, снова сохранять и снова запускать. Именно эти действия составляют основу работы программиста при создании приложений на любом языке программирования. Чем больше вы будете практиковаться, тем естественнее они станут для вас. Не переживайте: вам не придется перечитывать эту главу снова и снова, чтобы закрепить материал. Эти навыки вы будете использовать на протяжении всей книги, постепенно двигаясь от новичка к уверенным шагам в роли Python-разработчика XXI века.

2 ПОНИМАНИЕ БАЗОВЫХ КОНСТРУКЦИЙ PYTHON
В ЭТОМ РАЗДЕЛЕ Глава 1: Работа с числами, текстом и датами Глава 2: Управление действиями Глава 3: Ускоряем работу со списками и кортежами Глава 4: Обработка больших данных с помощью словарей Глава 5: Работа с крупными фрагментами кода Глава 6: Python и классы Глава 7: Обход ошибок
В этой главе вы узнаете: » Как работать с целыми числами » Как выполнять операции с числами с плавающей запятой » Как упрощать работу со строками » Как использовать логические значения True и False » Как работать с датами и временем Гл а в а 1 Работа с числами, текстом и датами Работа с числами, текстом и датами К омпьютерные языки вообще, и Python в частности, обрабатывают информацию немного иначе, чем мы привыкли в повседневной жизни. К этому нужно привыкнуть. В мире программирования: » числа — это величины, с которыми можно выполнять арифметические действия: сложение, вычитание, умножение и деление; » Python различает целые числа (integers) и числа с плавающей точкой (float); » слова и тексты (например, имена или адреса) хранятся как строки — от английского string, что означает «цепочка символов»; » существуют также логические значения: True (Истина) и False (Ложь) . ГЛАВА 1 Работа с числами, текстом и датами 91
В реальной жизни мы постоянно имеем дело и с еще одним видом информации — датами и временем. В Python для них нет встроенного типа данных, но есть бесплатный модуль, который можно подключить в любой момент и работать с такими данными. Эта глава научит вас эффективно использовать разные типы данных в Python. Вычисления с помощью функций Функция в Python похожа на функцию в калькуляторе: вы передаете ей какое-то значение, а она возвращает результат. Например, в большинстве калькуляторов (и языков программирования) есть функция извлечения квадратного корня: вы вводите число, а получаете его корень. В Python функции обычно записываются по такому синтаксису: variablename = function_name([param, param]) Поскольку большинство функций возвращают результат, обычно начинают с создания переменной, в которой этот результат будет храниться. Сначала указывают имя переменной, затем ставят знак =, после чего пишут имя функции и круглые скобки. Внутри скобок можно передать нуль, одно или несколько значений, такие значения называются параметрами функции. Например, функция abs() принимает одно число и возвращает его абсолютное значение. Проще говоря, если передать ей отрицательное число, она вернет то же число, но положительное, а если передать положительное, то результат будет таким же, как введенное число. Иными словами, функция abs() преобразует отрицательные числа в положительные. В качестве примера (который вы можете попробовать в Jupyter Notebook, в интерактивной оболочке Python или в файле .py в VS Code) создадим переменную x и присвоим ей значение –4. Затем создадим переменную y и сохраним в нее абсолютное значение x, вызвав функцию abs(). 92 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 1.1. Пример использования функции abs() Хотя функция всегда возвращает только одно значение, она может принимать сразу несколько аргументов. Например, функция round() принимает число в качестве первого аргумента, а вторым аргументом указывает количество знаков после десятичной точки, до которых нужно округлить число (например, 2 — для округления до сотых). В примере на рисунке 1.2 мы создали переменную x с числом, содержащим много цифр после десятичной точки. Затем сохранили в переменную y результат работы функции round(x, 2), то есть то же самое число, округленное до двух знаков после запятой. После этого мы вывели оба значения на экран. Рисунок 1.2. Пример использования функции round() У Python есть множество встроенных функций для работы с числами, и часть из них приведена в таблице 1.1. Некоторые функции могут показаться сложными, если вы не увлекаетесь математикой, но это не должно ГЛАВА 1 Работа с числами, текстом и датами 93
вас смущать. Как правило, если вы не понимаете, что делает функция, значит, она вам пока и не нужна. Тем не менее, если вам интересно разобраться, всегда можно поискать информацию: просто введите в интернете слово python и название функции. А чтобы получить полный список всех встроенных функций, найдите по запросу python 3 built-in functions (встроенные функции Python 3). Таблица 1.1. Некоторые встроенные функции Python для работы с числами Функция Назначение abs(x) Возвращает абсолютное значение числа x (преобразует отрицательные числа в положительные) bin(x) Возвращает строку с двоичным представлением числа x float(x) Преобразует строку или число x в тип данных float format (x, y) Возвращает x, отформатированное по шаблону y . В современных версиях Python вместо этого обычно используют f-строки hex(x) Возвращает строку с шестнадцатеричным представлением числа x (с префиксом 0x) int(x) Преобразует x в целое число, отбрасывая (но не округляя) дробную часть и все цифры после нее max Принимает любое количество чисел и возвращает наибольшее (x, y, z, …) min( x, y, z, …) Принимает любое количество чисел и возвращает наименьшее oct(x) Возвращает строку с восьмеричным представлением числа x (с префиксом 0o) round(x, y) Округляет число x до y знаков после запятой str(x) Преобразует число x в строку type(x) Возвращает тип данных значения x На рисунке 1.3 приведены примеры правильного синтаксиса Python при использовании встроенных математических функций. Эти примеры помогут вам увидеть, как именно записываются вызовы функций и какие результаты они возвращают. 94 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 1.3. Примеры работы со встроенными математическими функциями в интерактивной оболочке Python Вы также можете вкладывать функции друг в друга, то есть использовать одну функцию внутри другой. Например, если переменной z присвоить значение –999.9999, то выражение print(int(abs(z))) выведет 999. Сначала функция abs(z) преобразует отрицательное число в положительное, а затем функция int() отбрасывает десятичную часть вместе со всеми цифрами после нее, оставляя только целое число. Дополнительные математические функции Помимо встроенных функций Python, существуют и другие, которые можно подключить из специальных модулей. Один из таких модулей — math, содержащий набор полезных математических функций. Если вам нужны эти функции в приложении, добавьте в начало файла .py или ячейки Jupyter строку: import math То же самое относится и к работе в интерактивной оболочке: сначала нужно ввести import math, чтобы сделать функции модуля доступными. ГЛАВА 1 Работа с числами, текстом и датами 95
Одной из функций модуля math является sqrt(), вычисляющая квадратный корень числа. Но поскольку она не относится к встроенным функциям, вызвать ее напрямую нельзя. Без предварительного импорта модуля попытка выполнить такой код: print(sqrt(81)) закончится ошибкой, потому что Python не найдет функцию sqrt() среди встроенных. Чтобы воспользоваться функцией из модуля, нужно сначала импортировать этот модуль, а затем при вызове указывать его имя и точку перед именем функции. Например, пусть у вас есть переменная x, и вы хотите вычислить ее квадратный корень. Для этого нужно импортировать модуль math и вызвать функцию так: import math print(sqrt(81)) В результате на экране появится 9.0 — это и есть квадратный корень из числа 81, как показано на рисунке 1.4. Рисунок 1.4. Использование функции sqrt() из модуля math Модуль math содержит множество полезных функций: тригонометрические и гиперболические вычисления, возведение в степень, логарифмы, преобразование углов, а также встроенные константы вроде pi и e. Мы не будем подробно разбирать все возможности этого модуля, так как продвинутая математика редко нужна большинству пользователей. Если же вы захотите узнать больше, просто выполните поиск по запросу python 3 math module functions и ознакомьтесь со списком всех функций. В таблице 1.2 показаны некоторые примеры, которые могут пригодиться на практике. 96 РАЗДЕЛ 2 Понимание базовых конструкций Python
Отдельно стоит упомянуть константы pi, e и tau. Это не функции, в которые передаются значения, а именно константы: они всегда возвращают одно и то же значение. Поэтому вы можете просто использовать их напрямую или присвоить переменной. На рисунке 1.5 приведены примеры работы с константами модуля math. Таблица 1.2. Некоторые функции из математического модуля Python Функция / константа Назначение math.acos(x) Возвращает арккосинус x в радианах math.atan(x) Возвращает арктангенс x в радианах math.atan2(y, x) Возвращает арктангенс отношения y/x в радианах math.ceil(x) Возвращает наименьшее целое число, большее или равное x (округление вверх) math.cos(x) Возвращает косинус x, где x выражено в радианах math.degrees(x) Преобразует угол x из радиан в градусы math.e Математическая константа e (приблизительно 2 .718281…) math.exp(x) Возвращает значение e в степени x, где e — основание натуральных логарифмов math.factorial(x) Возвращает факториал числа x math.floor(x) Возвращает наибольшее целое число, меньшее или равное x (округление вниз) math.isnan(x) Возвращает True, если x не является числом (NaN), и False в противном случае math.log(x, y) Возвращает логарифм числа x по основанию y math.log2(x) Возвращает двоичный (по основанию 2) логарифм числа x math.pi Математическая константа π (приблизительно 3 .141593…) math.pow(x, y) Возвращает x, возведенное в степень y math.radians(x) Преобразует угол x из градусов в радианы math.sin(x) Возвращает синус x, где x выражено в радианах math.sqrt(x) Возвращает квадратный корень из x math.tan(x) Возвращает тангенс x, где x выражено в радианах math.tau Математическая константа ⁰ (приблизительно 6 .283185…) ГЛАВА 1 Работа с числами, текстом и датами 97
Рисунок 1.5. Дополнительные эксперименты со встроенными математическими константами Форматирование чисел Со временем в Python появилось несколько способов отображения чисел в более привычном для людей виде. Например, большинство предпочитает видеть денежные суммы как $1,234.56, а не как 1234.560065950695405695405959. Начиная с версии Python 3.6, самым простым и удобным способом форматирования чисел стали f-строки (formatted string literals). Форматирование с помощью f-строк Форматные строки, или f-строки, — это самый простой и удобный способ форматирования данных в Python. Чтобы их использовать, достаточно поставить перед строкой букву f (или прописную F), а затем в кавычках указать текст вместе с выражениями, которые нужно подставить. 98 РАЗДЕЛ 2 Понимание базовых конструкций Python
Пример: f"Hello {username}" Буква f перед первой кавычкой указывает Python, что это форматная строка. Внутри кавычек обычный текст называется литеральной частью и отображается на экране именно так, как он написан. Все же, что заключено в фигурные скобки {}, является выражением — своеобразным местозаполнителем, который при выполнении кода заменяется на свое значение. Внутри фигурных скобок можно разместить выражение — это может быть формула для вычислений, имя переменной или их комбинация. Например: username ="Alan" print(f"Hello {username}") При запуске этого кода функция print выводит слово Hello, затем пробел и значение переменной username, как показано на рисунке 1.6. Рисунок 1.6. Простейшая форматированная строка для форматирования Вот еще один пример выражения внутри фигурных скобок — формула quantity * unit_price: unit_price = 49.99 quantity = 30 print(f"Subtotal: ${quantity * unit_price}") В итоге получаем такой результат: Subtotal: $1499.7 Формат $1499.7 не очень удобен для отображения денежных сумм. Обычно мы хотим видеть число с разделением тысяч запятыми и с двумя знаками после десятичной точки, как, например: Subtotal: $1,499.70 ГЛАВА 1 Работа с числами, текстом и датами 99
К счастью, форматированные строки позволяют легко управлять отображением чисел, включая разделение тысяч и нужное количество знаков после запятой. В следующем разделе вы узнаете, как это делается. Отображение денежных сумм Чтобы вывести денежное значение с разделителями тысяч и двумя знаками после запятой для центов, в Python используют строку форматирования внутри фигурных скобок f-строки. Такая строка начинается с двоеточия: и пишется сразу после имени переменной или выражения перед закрывающей фигурной скобкой. Например, чтобы добавить запятую как разделитель тысяч, укажите ее после двоеточия: :, Используя текущий пример, код будет выглядеть так: print(f"Subtotal: ${quantity * unit_price:,}") При выполнении этой инструкции программа выведет: Subtotal: $1,499.7 Чтобы отобразить центы двумя цифрами, после запятой в строке форматирования укажите: .2f .2f означает «два десятичных знака в фиксированном формате», то есть число всегда будет отображаться с ровно двумя знаками после запятой. Следующий код выведет число с разделителями тысяч и двумя знаками после запятой: print(f"Subtotal: ${quantity * unit_price:,.2f}") Вот что отобразится при выполнении этого кода: Subtotal: $1,499.70 Отлично! Именно такой формат мы и хотели получить. Поэтому, когда нужно вывести число с разделителями тысяч и ровно двумя знаками после десятичной точки, используйте форматированную строку с указанием спецификатора .2f. 100 РАЗДЕЛ 2 Понимание базовых конструкций Python
ИНТЕРНАЦИОНАЛИЗАЦИЯ В PYTHON Для поддержки интернационализации Python использует стандарт POSIX, что позволяет отображать денежные суммы в формате, принятом в конкретной стране . Чтобы воспользоваться этой возможностью, в начале программы добавьте импорт модуля: import locale . Затем вызовите функцию locale.setlocale(), чтобы задать нужную страну и ее формат . После этого можно использовать locale.currency() для отображения числа в локальном денежном формате . Параметр grouping=True включает разделители тысяч (в тех языках и странах, где это поддерживается) . Например, следующий код сохраняет число в переменной sample_amount, устанавливает локаль для Франции (fr_FR) и выводит результат в формате французской валюты: import locale # Just some random number to work with sample_amount = 1234.56 # Set the locale to france locale.setlocale(locale.LC_ALL, "fr_FR") # Use locale.currency() to show sample amount in locale format Как вы можете догадаться, модуль locale довольно обширный, ведь он должен учитывать особенности множества стран и языков . К счастью, вам не нужно заучивать все детали наизусть . Достаточно при необходимости выполнить поиск по запросу «интернационализация Python» — так вы быстро найдете актуальную информацию о том, как правильно обрабатывать культурные форматы в своем приложении . Форматирование процентных чисел Предположим, ваше приложение должно учитывать налог с продаж. Для этого программе нужно знать налоговую ставку, которая в коде записывается в виде десятичного числа. Например, если налог равен 6,5%, то в Python его следует указать как 0.065 (или .065, что равнозначно): sales_tax_rate = 0.065 Число с ведущим нулем (0.065) или без него (.065) для Python совершенно одинаково, поэтому используйте тот вариант записи, который вам удобнее. ГЛАВА 1 Работа с числами, текстом и датами 101
Такой формат идеально подходит для внутреннего представления данных в Python, и менять его не нужно. Однако при выводе значения пользователю простое использование функции print() покажет его именно в том виде, в каком Python хранит число: sales_tax_rate = 0.065 print(f"Sales Tax Rate {sales_tax_rate}") Sales Tax Rate 0.065 Когда вы показываете налоговую ставку пользователю, скорее всего, будет удобнее представить ее в привычном виде — например, 6.5%, а не .065. Для этого можно применить тот же принцип, что и при работе с фиксированным количеством знаков после запятой (.2f). Только вместо спецификатора f нужно использовать символ %. print(f"Sales Tax Rate {sales_tax_rate:.2%}") При выполнении этого кода Python автоматически умножает налоговую ставку на 100 и добавляет к результату знак процента (%), как показано на рисунке 1.7. Рисунок 1.7. Форматирование числа в процентах В предыдущих примерах мы использовали .2%, чтобы выводить число с двумя знаками после запятой. Но вы можете выбрать любое количество знаков — от нуля (без десятичной части) до любого уровня точности. Например, формат .1% отобразит число с одним десятичным знаком: print(f"Sales Tax Rate {sales_tax_rate:.1%}") При выполнении этой строки на экран выводится: Sales Tax Rate 6.5% Если заменить 1 на 9, получится такой код: print(f"Sales Tax Rate {sales_tax_rate:.9%}") В таком случае процент будет выведен с девятью знаками после запятой: Sales Tax Rate 6.500000000% 102 РАЗДЕЛ 2 Понимание базовых конструкций Python
Форматные строки в Python необязательно использовать только внутри функции print(). Их можно вычислить заранее и сохранить результат в переменной, чтобы потом отобразить или использовать в программе. Как и любая другая строка, f-строка должна быть заключена в кавычки — одинарные, двойные или тройные. При этом тройные кавычки могут быть как одинарными (© © © ), так и двойными ("""). Внешний тип кавычек не влияет на результат: форматированная строка будет работать одинаково. На рисунке 1.8 показан пример с разными вариантами кавычек. СОВЕТ Для одинарных (© ) и двойных (") кавычек используйте соответствующие клавиши клавиатуры. Если нужны тройные кавычки, то можно поста© © ) или двойные ("""). Главное — вить подряд три любых — одинарные (© завершать строку тем же символом, которым она была начата. Например, все строки, показанные на рисунке 1.8, являются корректным кодом и будут обработаны Python одинаково. Рисунок 1.8. Форматная строка может быть заключена в одинарные, двойные или тройные кавычки Создание многострочных форматных строк Если вам нужен многострочный вывод, разрывы строк в f-строках можно вставлять разными способами. » Вы можете создать многострочный вывод, вставляя символ переноса строки \n в любую часть однострочной f-строки . Главное — чтобы \n находился в текстовой (литеральной) части строки, а не внутри фигурных скобок . Например: ГЛАВА 1 Работа с числами, текстом и датами 103
user1 = "Alberto" user2 = "Babs" user3 = "Carlos" output = f" {user1}\n{user2}\n{user3}" При выполнении этот код выведет: Alberto Babs Carlos » Использование тройных кавычек . Если обернуть f-строку в тройные кавычки (одинарные или двойные), символ \n не нужен . Вы можете просто разрывать строку прямо внутри форматной строки там, где хотите получить перенос . Например, в коде на рисунке 1 .9 форматная строка заключена в тройные кавычки и содержит несколько переносов . При выполнении программы эти разрывы сохраняются и отображаются точно так же . Как вы заметили, при использовании тройных кавычек вывод полностью сохраняет разрывы строк и даже пробелы внутри форматной строки. Но у такого подхода есть недостаток: числа не выравниваются, и десятичные точки оказываются вразнобой. В реальных программах обычно стараются располагать значения по правому краю, чтобы все выглядело аккуратно и легко читалось. К счастью, форматные строки позволяют управлять шириной поля и выравниванием текста, так что эту проблему можно решить. Рисунок 1.9. Многострочная форматная строка, заключенная в тройные кавычки 104 РАЗДЕЛ 2 Понимание базовых конструкций Python
Форматирование ширины и выравнивания Вы можете управлять как шириной поля, так и выравниванием содержимого внутри него. Для этого после двоеточия в форматной строке добавьте один из специальных символов: < — выравнивание по левому краю; ^ — выравнивание по центру; > — выравнивание по правому краю. Символ ставится сразу после двоеточия, а затем указывается ширина поля. Например, следующий код задает ширину 20 символов и выравнивает содержимое по правому краю: :>20 На рисунке 1.9 все денежные суммы были выровнены по левому краю, так как это поведение используется в Python по умолчанию. Чтобы расположить числа по правому краю — так, как мы привыкли видеть денежные значения, — в форматной строке нужно указать символ >. А чтобы все числа занимали одинаковую ширину, после > добавляется число, определяющее ширину поля. Например, в коде на рисунке 1.10 у каждой форматной строки указано >9. Это заставляет каждое число отображаться по правому краю в поле шириной девять символов. В результате все значения аккуратно выстроены: цифры справа совпадают, а знаки доллара расположены слева, при этом справа от них добавляются пробелы, чтобы соблюсти одинаковую ширину. Если внимательно посмотреть на рисунок 1.10, может возникнуть вопрос: почему знаки доллара не выровнены вплотную к самим числам? Дело в том, что символ $ находится вне фигурных скобок и воспринимается как часть обычной строки. Поэтому на него не влияет указание >9, относящееся только к числу внутри скобок. Перевыравнивание знаков доллара оказывается чуть сложнее, чем может показаться. Причина в том, что спецификатор .2f можно применять только к числам. Если же добавить $ перед значением, преобразовав все в строку, число перестанет быть числом — и форматирование .2f уже работать не будет. ГЛАВА 1 Работа с числами, текстом и датами 105
Рисунок 1.10. Все денежные суммы выровнены по правому краю в пределах девяти символов (>9) Сложно — не значит невозможно, просто немного неудобно. Один из способов — сначала преобразовать каждое число в строку с нужным форматом, затем добавить знак доллара слева, а уже после этого задать ширину и выравнивание для полученной строки. Например, следующий код создает переменную s_subtotal, в которой к отформатированному числу добавляется символ $. Благодаря этому знак доллара располагается вплотную к первой цифре суммы, без пробелов между ними: s_subtotal = "$" + f"{subtotal:,.2f}" В приведенном коде предполагается, что переменная subtotal содержит какое-то число. Для примера можно взять значение 1598.402, хотя это может быть любая сумма. Выражение f"{subtotal:,.2f}" отформатирует число так, чтобы оно содержало два знака после десятичной точки и разделитель тысяч в виде запятой. В итоге получится строка: 1,598.40 Результатом работы форматной строки всегда будет строка, а не число. Поэтому, когда мы применяем выражение f"{subtotal:,.2f}", Python возвращает строковое представление значения. Далее к этой строке можно добавить (или, как говорят программисты, сконкатенировать) символ доллара в самом начале. Например: "$"+ 106 РАЗДЕЛ 2 Понимание базовых конструкций Python
Теперь результат выглядит как $1,598.40. Эта окончательная строка сохраняется в переменной s_subtotal. Обратите внимание на префикс s_: он служит подсказкой, что это строковый вариант исходного числа subtotal, а не само число. Чтобы вывести эту денежную сумму по правому краю в поле шириной девять символов, используйте спецификатор >9 в новой f-строке, примененной уже к переменной s_subtotal: f"{s_subtotal:>9}" СОВЕТ Если вы забудете, как использовать более сложные конструкции с print и f-строками, всегда можно обратиться к ChatGPT или другому генеративному ИИ. Например, вы можете попросить: «python print f для выравнивания денежных сумм по правому краю с двумя десятичными знаками» или «python print f для вывода процентов с двумя цифрами после запятой». Скопируйте сгенерированный код, вставьте его в свой проект и при необходимости адаптируйте под задачу. На рисунке 1.11 приведен полный пример с готовым результатом: все числа аккуратно выровнены по правому краю, а символы доллара расположены именно там, где мы привыкли их видеть. Рисунок 1.11. Все денежные суммы аккуратно выровнены ГЛАВА 1 Работа с числами, текстом и датами 107
Работа с необычными числами В повседневной жизни мы обычно сталкиваемся с простыми числами — количеством предметов или денежными суммами. Однако в Python есть и инструменты для работы с более «экзотичными» типами: системами счисления, отличными от десятичной, а также с мнимыми числами. При этом вам совсем необязательно глубоко изучать эти возможности, чтобы пользоваться Python в повседневной работе. Они нужны лишь тогда, когда того требует ваша деятельность или учебные задания. В следующем разделе мы рассмотрим несколько числовых систем, часто используемых в информатике: двоичную, восьмеричную и шестнадцатеричную. Двоичные, восьмеричные и шестнадцатеричные числа Если вам приходится работать с числами в системах счисления с основанием 2, 8 или 16, Python предоставляет все необходимое. У него есть специальные обозначения для записи таких чисел, а также функции для их преобразования между различными системами. В таблице 1.3 показаны три наиболее распространенные недесятичные системы счисления и цифры, которые в них используются. Таблица 1.3. Python для чисел с основанием 2, 8 и 16 Система Также называется Используемые цифры Префикс в Python Функция для преобразования Двоичная Binary 0, 1 0b bin() Восьмеричная Octal 0, 1, 2, 3, 4, 5, 6, 7 0b oct() Шестнадцатеричная Hexadecimal or hex 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F 0x hex() 108 РАЗДЕЛ 2 Понимание базовых конструкций Python
Большинство людей в своей повседневной жизни никогда не сталкиваются с двоичными, восьмеричными или шестнадцатеричными числами. Поэтому, если все это звучит для вас непривычно и даже немного пугающе, — не переживайте. Если вы раньше о них не слышали, то, скорее всего, больше и не услышите после того, как закончите этот раздел. СОВЕТ Если вы хотите глубже разобраться в различных системах счисления, просто воспользуйтесь поисковой системой и поищите информацию о двоичной, восьмеричной, десятичной или шестнадцатеричной системах. В Python можно легко преобразовывать числа из одной системы счисления в другую — как в командной строке, так и в собственных приложениях. Для этого достаточно вызвать функцию преобразования внутри print(), а внутрь скобок передать число, которое вы хотите преобразовать. Например, следующая команда выведет шестнадцатеричный эквивалент числа 255: print(hex(255)) Результатом работы команды будет 0xff, где префикс 0x показывает, что число записано в шестнадцатеричной системе, а ff — это шестнадцатеричный эквивалент десятичного числа 255. Чтобы преобразовать числа из двоичной, восьмеричной или шестнадцатеричной систем обратно в десятичную, никаких дополнительных функций не требуется. Достаточно просто использовать print() с самим числом. Например, команда print(0xff) выведет 255 — десятичное представление шестнадцатеричного числа ff. На рисунке 1.12 показаны и другие примеры, которые вы можете опробовать в командной строке Python. ГЛАВА 1 Работа с числами, текстом и датами 109
Рисунок 1.12. Эксперименты с двоичными, восьмеричными и шестнадцатеричными числами Комплексные числа Комплексные числа — это еще один «необычный» тип чисел, с которым большинству программистов, скорее всего, никогда не придется работать. Исключение составляют области, где они действительно необходимы: электротехника, высшая математика или отдельные научные дисциплины. Комплексное число записывается в виде a + bi, где a и b — действительные числа, а i — мнимая единица, для которой выполняется уравнение i² = –1. Так как не существует действительного числа, квадрат которого равен –1, это значение и называется мнимым. В математике мнимую единицу чаще обозначают буквой i, но в Python для этой цели используется буква j (как и в электротехнике, где i уже зарезервирована для обозначения тока). Если ваше приложение требует работы с комплексными числами, в Python можно использовать встроенную функцию complex() для их создания. Синтаксис выглядит так: z = complex(a, b), где a — действительная часть числа, а b — мнимая: complex(real, imaginary) 110 РАЗДЕЛ 2 Понимание базовых конструкций Python
Замените real на действительную часть комплексного числа, а imaginary — на мнимую. Например, в коде или прямо в командной строке Python вы можете попробовать такой пример: z = complex(2,-3) Переменной z присваивается комплексное число 2.3j. После этого используем функцию print() для вывода содержимого переменной: print(z) На экране отобразится комплексное число (2.3j). Если нужно получить отдельные части этого числа, используйте атрибуты .real и .imag. Атрибут .real возвращает действительную часть, а .imag — мнимую. Например, следующий код выведет 2.0, то есть действительную часть числа z: print(z.real) Аналогично, если обратиться к атрибуту .imag, то результатом будет –3.0, что соответствует мнимой части числа z: print(z.imag) И снова — если все это кажется вам слишком сложным или непонятным, не переживайте. Для освоения Python работа с комплексными числами совсем не обязательна. Этот функционал существует лишь для тех, кому он действительно нужен. ПРИМЕЧАНИЕ Если же ваша работа связана с использованием комплексных чисел, обратите внимание на модуль cmath. Он расширяет возможности Python и предлагает набор функций для работы с комплексными числами. Чтобы узнать больше, достаточно выполнить поиск в интернете по запросу “python cmath”. Манипуляции со строками В Python, как и в других языках программирования, слова и фрагменты текста называются строками. Само слово «строка» — это сокращение от выражения «цепочка символов». В отличие от чисел, строка не имеет количественного значения. (Основы работы со строками мы уже ГЛАВА 1 Работа с числами, текстом и датами 111
рассматривали в главе 4 раздела 1.) В этом разделе вы познакомитесь с приемами работы со строками в Python и узнаете, как использовать их в своих программах. Конкатенация строк Строки в Python можно объединять с помощью знака плюс (+). Этот процесс называется конкатенацией строк. Однако начинающих часто сбивает с толку одна важная деталь: компьютер не понимает, где именно в строке должен стоять пробел. Для него слово и, скажем, бутерброд с колбасой — это всего лишь набор символов. Поэтому при объединении строк пробелы автоматически не вставляются. Например, в коде ниже переменная full_name формируется как конкатенация трех строк: first_name = middle_init last_name = full_name = "Alan" = "C" "Simpson" first_name + middle_init + last_name Когда вы выполните этот код и выведете содержимое переменной full_ name, то увидите, что Python действительно объединил строки в одну длинную последовательность символов: AlanCSimpson В полученном выводе нет ошибки — строка сформирована правильно. Но обычно мы ожидаем увидеть пробелы между словами и частями имени человека. Так как Python не добавляет их автоматически, пробелы нужно указывать вручную. Самый простой способ — вставить строку, состоящую из одного пробела. Для этого используют кавычки с пробелом внутри, например: " " Если забыть указать пробел между кавычками, результат снова получится слитным, потому что внутри кавычек ничего нет. Например: "" Вы можете указать внутри кавычек сразу несколько пробелов, если хотите, чтобы они появились в выводе. Но в большинстве случаев достаточно одного. В следующем примере мы добавляем пробел между first_name и last_name, а после middle_init — точку и пробел: first_name = "Alan" middle_init = "C" last_name = "Simpson" 112 РАЗДЕЛ 2 Понимание базовых конструкций Python
full_name = first_name + " " + middle_init + ". " + last_name print(full_name) Результат работы этого кода — содержимое переменной full_name — больше напоминает привычный формат имени: Alan C. Simpson Разумеется, объединять все части имени с пробелами в одну переменную вовсе необязательно. Того же результата можно достичь сразу при выводе, используя форматированную строку. В этом случае в коде не будет переменной full_name, а нужный текст отобразится напрямую: print(f"{first_name} {middle_init}. {last_name}") Определение длины строки Чтобы узнать количество символов в строке, в Python используют встроенную функцию len() (сокращение от слова length — «длина»). В подсчет включаются и пробелы, так как они тоже считаются символами длиной в один знак. Пустая строка — это пара кавычек без пробелов и других символов внутри — имеет длину 0. Рассмотрим несколько примеров. В переменную s1 мы поместим пустую строку, в s2 — строку, содержащую один пробел, а в s3 — строку с буквами и пробелами. Затем с помощью трех вызовов print() выведем длину каждой строки: s1 = "" s2 = " " s3 = "A B C" print(len(s1)) print(len(s2)) print(len(s3) Результат работы программы показан ниже. Он вполне предсказуем, если помнить, что функция len() измеряет длину строки как количество символов, включая пробелы: 0 1 5 ГЛАВА 1 Работа с числами, текстом и датами 113
Работа с основными строковыми операторами В Python есть несколько операторов для работы с последовательностями данных. Но у строк есть особенность, которая поначалу может показаться странной: нумерация символов начинается не с 1, а с 0. Для человека это выглядит непривычно, но для компьютера такой подход гораздо эффективнее. Например, строка на рисунке 1.13 состоит из пяти символов. Однако последний символ в ней — буква E, потому что первый символ имеет номер 0, второй — 1, и т. д. Рисунок 1.13. Позиции символов в строке начинаются с 0, а не с 1 В таблице 1.4 приведены основные операторы Python 3, которые используются для работы со строками. Эти операторы позволяют проверять наличие символов, объединять строки и выполнять другие полезные операции. Таблица 1.4. Операторы последовательностей Python для работы со строками Оператор Назначение x in s Возвращает True, если строка x встречается где-либо в строке s x not in s Возвращает True, если x не содержится в строке s s * n или n * s Повторяет строку s n раз s[i] Символ на позиции i внутри строки s s[i: j] Срез строки s, начиная с позиции i и до (не включая) позиции j s[i: j: k] Срез строки s от i до j с шагом k . Например, k=2 — каждый второй символ, k=3 — каждый третий и т .д . min(s) Возвращает наименьший (минимальный) символ строки s max(s) Возвращает наибольший (максимальный) символ строки s На рисунке 1.14 показаны примеры использования строковых операторов в Jupyter Notebook. 114 РАЗДЕЛ 2 Понимание базовых конструкций Python
Если результат работы функции print() кажется вам неожиданным или «неправильным», важно помнить два ключевых факта о строках в Python: » » Первый символ всегда имеет индекс 0 . Каждый пробел считается за отдельный символ, поэтому при подсчете не забывайте учитывать и их . Например, вы могли заметить, что функция min(s) иногда возвращает пробел. Это происходит потому, что пробел в Python считается «меньше» любых букв. Почему так? Все дело в ASCII-кодировке (American Standard Code for Information Interchange — американский стандартный код для обмена информацией). В ней каждому символу, включая буквы, цифры, знаки препинания и пробелы, соответствует уникальный числовой код. Именно сравнение этих числовых значений и позволяет Python определять, какой символ «меньше» или «больше». Рисунок 1.14. Эксперименты со строковыми операторами в Jupyter Notebook На рисунке 1.15 показана таблица ASCII-кодов для многих распространенных символов. В ней можно заметить, что пробелы и знаки препинания располагаются «ниже» заглавной буквы A, потому что имеют меньшие ASCII-коды. Заглавные буквы, в свою очередь, тоже «ниже» ГЛАВА 1 Работа с числами, текстом и датами 115
строчных, так как их коды меньше. Может возникнуть вопрос: что же с символами, которым присвоены номера от 0 до 31? Эти позиции тоже заняты, но не печатаемыми знаками, а так называемыми управляющими символами. Они невидимы и используются, например, при нажатии клавиши Ctrl в сочетании с другими клавишами. В Python есть две полезные функции для работы с кодировкой ASCII. Функция ord() принимает символ и возвращает его числовой код, например print(ord("A")) вернет 65, потому что заглавная буква A соответствует коду 65 в таблице ASCII. Функция chr() делает обратное: принимает число и возвращает символ для этого кода, так print(chr(65)) выведет букву A. Я использовал ASCII для примера, потому что этот набор относительно небольшой. Однако в современном мире чаще применяется Unicode — кодировка, которая включает все символы ASCII, а также тысячи дополнительных знаков, которых нет на стандартной клавиатуре. Например, символ 00A3 — это знак британского фунта стерлингов (£). Чтобы вывести Unicode-символ в Python, нужно заключить его код, предваренный \u, в кавычки: print('\u00A3') выведет символ £. Рисунок 1.15. ASCII-коды для распространенных символов 116 РАЗДЕЛ 2 Понимание базовых конструкций Python
Манипулирование строками с помощью методов В Python 3 любая строка рассматривается как объект типа str (string). Сокращение str для обозначения строки отличает Python 3 от более ранних версий языка, где строки назывались полностью — string objects. Такое изменение часто сбивает с толку новичков, поэтому важно просто запомнить: в Python 3 слово str всегда означает строку символов. В Python существует множество методов для работы со строками, которые еще называют методами str. Их можно использовать для анализа, преобразования и обработки строковых объектов. Общий синтаксис вызова метода выглядит так: string.methodname(params) Здесь string — это сама строка, которую вы хотите обработать (это может быть как строковый литерал в кавычках, так и имя переменной), methodname — название метода из таблицы 1.5, а params — параметры, которые передаются методу, если они необходимы. В таблице 1.5 символ s в первом столбце используется как обозначение любой строки. Таблица 1.5. Встроенные методы для строк в Python 3 Метод Назначение s.capitalize() Возвращает строку с первой буквой в верхнем регистре, а остальные — в нижнем s.count(x[, y[, z]]) Возвращает количество вхождений подстроки x в строку s . Дополнительно можно указать y (начало поиска) и z (конец) s.find(x[, y[, z]]) Возвращает индекс первого вхождения подстроки x в s . Если y и z заданы, поиск ограничивается этим диапазоном . Возвращает –1, если подстрока не найдена s.index(x[, y[, z]]) То же, что и find(), но вызывает ошибку, если подстрока не найдена s.isalpha() Возвращает True, если строка состоит только из букв (A–Z, a–z или печатных Unicode-символов) s.isdecimal() Возвращает True, если строка состоит только из десятичных цифр (0–9) s.islower() Возвращает True, если строка содержит буквы и все они строчные s.isnumeric() Возвращает True, если строка состоит только из цифр ГЛАВА 1 Работа с числами, текстом и датами 117
Метод Назначение s.isprintable() Возвращает True, если строка содержит только печатные символы . Пробелы считаются печатными, а переводы строк и табуляции — нет s.istitle() Возвращает True, если каждое слово в строке начинается с заглавной буквы, за которой идут строчные s.isupper() Возвращает True, если все буквы в строке заглавные s.lower() Возвращает копию строки, где все буквы преобразованы в строчные s.lstrip() Удаляет пробелы, табуляции и переводы строк слева s.replace(x, y) Возвращает копию строки s, где все подстроки x заменены на y s.rfind(x[, y[, z]]) Как find(), но выполняет поиск справа налево . Возвращает –1, если подстрока не найдена s.rindex(x[, y[, z]]) Как rfind(), но вызывает ошибку, если подстрока не найдена s.rstrip() Удаляет пробелы, табуляции и переводы строк справа s.strip() Удаляет пробелы и управляющие символы с обеих сторон строки s.swapcase() Возвращает копию строки, где заглавные буквы заменены на строчные и наоборот s.title() Возвращает строку, где первая буква каждого слова заглавная, остальные — строчные s.upper() Возвращает строку, где все буквы преобразованы в заглавные Вы можете попробовать эти методы самостоятельно в Jupyter Notebook, в командной строке Python или в обычном файле с расширением .py. На рисунке 1.16 показаны примеры использования строковых методов с тремя переменными s1, s2 и s3. Код и результат его выполнения расположены один под другим, чтобы сразу было видно, какой метод что возвращает. 118 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 1.16. Эксперименты с функциями строк в Python ЗАПОМНИТЕ Не нужно стараться запомнить или подробно разобраться в каждом строковом методе. Достаточно знать, что они существуют, и при необходимости вы всегда сможете к ним обратиться. Если вдруг понадобится список строковых функций и операторов, можно попросить об этом генеративный ИИ или просто выполнить поиск в интернете по запросу «python string methods», чтобы узнать, какие методы доступны в Python. Знакомство с датами и временем В программировании часто приходится работать с датами и временем — будь то для планирования задач, расчета сроков выполнения, проверки просрочек или фиксации точного момента, когда произошло какое-либо событие. Причин для их использования действительно много. Однако, в отличие от строк или чисел, у Python нет встроенного отдельного типа данных для работы с датами и временем. Для этого обычно используется модуль datetime. Как и любой другой модуль, его нужно сначала импортировать. Делается это командой: import datetime. Чтобы сократить количество набора, можно указать псевдоним ГЛАВА 1 Работа с числами, текстом и датами 119
(например, dt): import datetime as dt. В этом случае в коде нужно будет обращаться к функциям и классам модуля через dt, а не через datetime. Модуль datetime предоставляет удобные инструменты для работы с датами и временем в Python. Он поддерживает три основных способа хранения информации, которые позволяют описывать дату, время и часовые пояса: » datetime.date — объект даты, содержащий месяц, день и год (без времени); » datetime.time — объект времени, включающий часы, минуты, секунды, микросекунды и, при необходимости, данные о часовом поясе (без даты); » datetime.datetime — объект, объединяющий дату и время, а также, при необходимости, сведения о часовом поясе . В этом списке для наглядности используется полное слово datetime, но если вы импортировали модуль с псевдонимом, например dt, то в коде можно обращаться к этим типам через него. В следующих разделах мы подробно рассмотрим каждый из этих типов данных. Работа с датами Тип данных datetime.date удобен в тех случаях, когда нужно работать только с датой, а время роли не играет. Создать объект даты можно двумя способами. Первый вариант — получить сегодняшнюю дату из внутренних часов компьютера с помощью метода today(). Второй вариант — указать год, месяц и день (именно в таком порядке) в круглых скобках. ЗАПОМНИТЕ Важно помнить: при передаче месяца или дня в datetime.date() не используйте ведущие нули. Например, 1 апреля 2024 года нужно записывать как 2024, 4, 1, а не 2024, 04, 01, иначе программа выдаст ошибку. После импорта модуля datetime вы можете вызвать date.today(), чтобы получить текущую дату, или использовать синтаксис date(год, месяц, день) для создания даты вручную. Пример кода ниже показывает оба варианта: # Import the datetime module, nickname dt import datetime as dt # Store today's date in a variable named today. today = dt.date.today() # Store some other date in a variable called last_of_teens last_of_teens = dt.date(2019, 12, 31) 120 РАЗДЕЛ 2 Понимание базовых конструкций Python
ДАТА И ВРЕМЯ НА ВАШЕМ КОМПЬЮТЕРЕ Если ваш компьютер подключен к интернету, его внутренние часы и календарь должны показывать правильное время . Это достигается за счет того, что компьютер синхронизирует свои часы с универсальным координированным временем (UTC) через специальные сетевые протоколы . Далее система автоматически подстраивает отображаемую дату и время под ваш часовой пояс и учитывает переход на летнее время (если в вашем регионе он используется) . Проще говоря, то, что вы видите на экране компьютера, должно совпадать с датой и временем на ваших настенных часах и календаре . Попробуйте ввести этот код в Jupyter notebook, в командной строке Python или в файле с расширением .py. Чтобы увидеть результат, используйте функцию print() для отображения содержимого каждой переменной, как показано на рисунке 1.17. Значение переменной today у вас, скорее всего, будет другим, потому что оно всегда соответствует текущей дате на момент выполнения кода. Рисунок 1.17. Эксперименты с объектами datetime. date в Jupyter notebook Вы можете получить отдельные части даты с помощью атрибутов .month, .day и .year. Например, в той же ячейке Jupyter или в командной строке Python выполните следующий код: print(last_of_teens.month) print(last_of_teens.day) print(last_of_teens.year) ГЛАВА 1 Работа с числами, текстом и датами 121
Результат отобразит каждый компонент даты на отдельной строке: 12 31 2019 Как вы уже заметили на первом примере в рисунке 1.17, по умолчанию дата в Python выводится в формате гггг-мм-дд. Однако вы можете настроить отображение дат и времени так, как вам нужно. Для этого используйте форматные строки (о которых мы говорили ранее) вместе со специальными директивами, приведенными в таблице 1.6. Эти директивы позволяют задавать разные варианты отображения — как для дат, так и для времени (о времени речь пойдет позже в этой главе). Учтите, что для директив, связанных с локальным форматом, результат зависит от страны и региональных настроек. Например, дата 18 декабря 2023 года при использовании %x в США будет отображаться как 12/18/23, а в странах, где сначала пишут день, а потом месяц — как 18/12/2023. Таблица 1.6. Форматирование строк для дат и времени Директива Описание Пример %a День недели (сокращенно) Sun %A День недели (полностью) Sunday %w Номер дня недели (0–6, где 0 — воскресенье) 0 %d День месяца (с ведущим нулем) 31 %b Месяц (сокращенно) Jan %B Месяц (полностью) January %m Номер месяца (01–12) 01 %y Год без века 19 %Y Год с веком 2024 %H Часы (00–23) 23 %I Часы (01–12) 11 %p AM/PM PM %M Минуты (00–59) 01 %S Секунды (00–59) 01 %f Микросекунды (000000–999999) 495846 122 РАЗДЕЛ 2 Понимание базовых конструкций Python
Директива Описание Пример %z Смещение от UTC -0500 %Z Часовой пояс ETC %j Номер дня в году (001–366) 300 %U Номер недели в году (воскресенье — первый день) 50 %W Номер недели в году (понедельник — первый день) 50 %c Локальная версия даты и времени Tue Dec 31 23:59:59 2024 %x Локальная версия даты 12/31/24 %X Локальная версия времени 23:59:59 %% Символ % % ПРИМЕЧАНИЕ В некоторых руководствах вы можете встретить совет использовать метод strftime() для форматирования дат и времени. Это вполне рабочий и правильный способ, и он до сих пор широко применяется. Однако в Python начиная с версии 3.6 появился более современный и удобный подход — форматные строки (f-strings). Мы будем использовать именно их, так как они делают код проще для чтения и понимания, а также, скорее всего, станут основным способом форматирования в будущем. При работе со строками форматирования важно помнить, что все дополнительные символы — пробелы, запятые, косые черты и т.д. — нужно указывать прямо внутри форматной строки в тех местах, где вы хотите их видеть в результате. Например, вот такой код: print(f"{last_of_teens:%A, %B %d, %Y}") выведет: Tuesday, December 31, 2019 А если вам нужно отобразить дату в привычном американском формате мм/дд/гггг, используйте %m/%d/%Y. Например: todays_date = f"{today:%m/%d/%Y}" ГЛАВА 1 Работа с числами, текстом и датами 123
При выполнении вы получите что-то вроде: 11/19/2024 В таблице 1.7 показаны еще несколько примеров строк форматирования, которые можно попробовать с разными датами. Таблица 1.7. Примеры строк форматирования даты Строка формата Пример результата %a, %b %d %Y Sat, Jun 01 2024 %x 06/01/24 %m-%d-%y 06 .01 .24 This %A %B %d This Saturday June 01 %A %B %d is day number %j of %Y Saturday June 01 is day number 152 of 2024 Работа со временем Если нужно работать только со временем, а дата не важна, используйте класс datetime.time. Базовый синтаксис создания объекта времени выглядит так: variable = datetime.time([hour[, minute[, second[, microsecond]]]]) Все параметры являются необязательными. Например, если вызвать функцию без аргументов: midnight = dt.time() print(midnight) Результат будет таким: 00:00:00, что соответствует полуночи. Если проверить тип объекта: print(type(midnight)), вы получите: <class 'datetime.time'> Это подтверждает, что midnight действительно является объектом времени из класса datetime. 124 РАЗДЕЛ 2 Понимание базовых конструкций Python
Четвертый аргумент — микросекунды (миллионные доли секунды). Например, следующий код создает значение, соответствующее последней миллионной доле секунды перед полуночью: almost_midnight = dt.time(23, 59, 59, 999999) print(almost_midnight) 23:59:59.999999 Как и в случае с датами, вы можете управлять отображением времени с помощью строк форматирования и специальных директив. Для этого используйте директивы времени из таблицы 1.6. В таблице 1.8 приведены примеры работы с форматом на основе времени 23:59:59.999999. Таблица 1.8. Примеры форматирования времени Строка форматирования Пример %I:%M %p 11:59 PM %H:%M:%S and %f microseconds 23:59:59 and 999999 microseconds %X 23:59:59 Иногда возникает необходимость работать только с датами, иногда — только со временем. Но бывают ситуации, когда нужно указать точный момент, включающий и дату, и время одновременно. В таких случаях применяется класс datetime из модуля datetime. Этот класс предоставляет метод now(), который возвращает текущую дату и время, основываясь на системных часах вашего компьютера: import datetime as dt right_now = dt.datetime.now() print(right_now) Результат будет зависеть от момента запуска, но формат всегда такой: 2024-11-19 14:03:07.525975 Здесь 2024.11.19 — это 19 ноября 2024 года, 14:03 — часы и минуты, а 7.525975 — доли секунды. Вы также можете создать объект datetime, указав год, месяц и день (эти параметры обязательны), а также при необходимости — часы, минуты, секунды и микросекунды. Если параметры времени опущены, Python автоматически устанавливает их в нуль. Общий синтаксис выглядит так: datetime(year, month, day, hour[, minute[, second[, ГЛАВА 1 Работа с числами, текстом и датами 125
microsecond]]]) Например, следующий код создает объект, соответствующий 31 декабря 2024 года в 23:59: import datetime as dt new_years_eve = dt.datetime(2024, 12, 31, 23, 59) print(new_years_eve) Вывод будет таким (по умолчанию без дополнительного форматирования): 2024-12-31 23:59:00 В таблице 1.9 (которая идет дальше в книге) показаны примеры, как красиво форматировать дату и время с помощью директив, о которых мы уже говорили в таблице 1.6. Вычисление временных интервалов Иногда одной только даты или времени бывает недостаточно. Нам важно знать не когда произошло событие, а как долго оно длилось — в годах, месяцах, неделях, днях, часах, минутах или секундах. В программировании такая «длительность» называется временным интервалом. В Python для работы с интервалами используется класс datetime.timedelta из модуля datetime. С его помощью можно легко вычислять разницу между датами и временем или прибавлять/вычитать определенные промежутки (например, добавить 7 дней к дате или вычислить, сколько дней прошло между двумя событиями). Таблица 1.9. Примеры строк форматирования даты и времени 126 Строка форматирования Пример %A, %B %d at %I:%M%p Вторник, 31 декабря в 11:59 вечера %m/%d/%y at %H:%M%p 12/31/24 в 23:59 вечера %I:%M %p on %b %d 11:59 вечера 31 декабря %x 12/31/24 %c Вт., дек. 31 23:59:00 2024 %m/%d/%y at %I:%M %p 12/31/24 в 11:59 вечера %I:%M %p on %m/%d/%y 11:59 вечера в 12/31/2024 РАЗДЕЛ 2 Понимание базовых конструкций Python
Объект timedelta в Python появляется автоматически, когда вы вычитаете одну дату, время или объект datetime из другого. Такой объект показывает промежуток между двумя моментами. Представьте, что вы хотите вычислить количество дней между Новым годом и Днем Победы. Для этого достаточно создать две переменные с этими датами, а затем определить третью переменную days_between, в которую сохранится разница, вычисленная простым вычитанием более ранней даты из более поздней. import datetime as dt new_years_day = dt.date(2024, 1, 1) memorial_day = dt.date(2024, 5, 9) days_between = memorial_day - new_years_day print(days_between) print(type(days_between)) Когда мы вычитаем одну дату из другой, Python автоматически создает специальный объект под названием timedelta. Этот объект хранит не саму дату, а промежуток времени между двумя датами. В нашем примере разница между 1 января 2024 года и 9 мая 2024 года составила 129 дней. Если вывести значение переменной days_between, то мы увидим: 129 days, 0:00:00 <class 'datetime.timedelta'> Здесь 129 days означает количество дней между датами, а 0:00:00 показывает часть, связанную со временем. Так как мы не задавали часы, минуты и секунды, они автоматически равны нулю. Если проверить тип данных этой переменной с помощью функции type(), результат будет таким: <class 'datetime.timedelta'>. То есть days_between — это объект timedelta, который Python использует для работы с длительностями и временными интервалами. Вычисление timedelta происходит автоматически, когда вы вычитаете одну дату из другой. В результате вы получаете не конкретную дату, а промежуток времени между ними. Но не обязательно всегда работать только с разницей между датами — вы можете и сами задать любой временной интервал с помощью класса timedelta. Синтаксис выглядит так: datetime.timedelta(days, seconds, microseconds, milliseconds, minutes,hours, weeks) ГЛАВА 1 Работа с числами, текстом и датами 127
Здесь вы можете указать дни, секунды, минуты, часы или даже недели — все зависит от того, какой именно интервал вам нужен. Чтобы на практике увидеть, как работает разница во времени, попробуйте написать такой код. Сначала импортируйте модуль datetime и создайте объект даты с помощью .date(). Затем создайте объект timedelta, указав нужное количество дней. Если прибавить этот интервал к дате, вы получите новую дату. В нашем примере это дата, которая наступит через 129 дней после 1 января 2024 года: import datetime as dt new_years_day = dt.date(2024, 1, 1) duration = dt.timedelta(days=129) print(new_years_day + duration) 2024-05-9 Точно так же можно использовать вычитание. Если начать с 9 мая 2024 года и вычесть 129 дней, мы снова получим 1 января 2024 года: import datetime as dt memorial_day = dt.date(2024, 5, 9) duration = dt.timedelta(days=129) print(memorial_day - duration) 2024-01-01 Разницу во времени можно вычислять не только между датами, но и между объектами datetime, которые включают и дату, и время. Если вас интересует только продолжительность внутри одного дня, просто задайте одинаковую дату, но разное время. Например, вот код: import datetime as dt start_time = dt.datetime(2024, 3, 31, 8, 0, 0) finish_time = dt.datetime(2024, 3, 31, 14, 34, 45) time_between = finish_time - start_time print(time_between) print(type(time_between)) 6:34:45 <class 'datetime.timedelta'> Это означает, что между началом и окончанием прошло 6 часов, 34 минуты и 45 секунд. Python показывает это как объект timedelta, потому что это именно продолжительность, а не обычное «время на часах». Можно работать и с интервалами, которые охватывают разные даты. Например, можно сравнить текущий момент времени с днем рождения (до минуты) и посмотреть, сколько времени прошло. 128 РАЗДЕЛ 2 Понимание базовых конструкций Python
import datetime as dt now = dt.datetime.now() birthdatetime = dt.datetime(1995, 3, 31, 8, 26) age = now - birthdatetime print(age) print(type(age)) В примере с datetime.now() мы получили возраст в виде большого интервала с днями, часами, минутами, секундами и даже микросекундами. Например: 10362 days, 21:47:04.355252 <class 'datetime.timedelta'> Здесь видно, что прошло 10362 дня, плюс почти 22 часа, а микросекунды появились потому, что datetime.now() берет время компьютера с максимальной точностью. Но если вам нужны только дни (например, чтобы рассчитать возраст человека), можно работать не с datetime, а с объектами date. Такой объект хранит только дату без времени. В коде ниже мы создаем сегодняшнюю дату и дату рождения, а затем вычитаем одно из другого: import datetime as dt today = dt.date.today() birthdate = dt.date(2000, 12, 31) delta_age = (today - birthdate) print(delta_age) В этом примере мы использовали today = dt.date.today(), чтобы сохранить в переменной текущую дату. Это работает почти так же, как datetime.now(), но без часов, минут и секунд — только дата. Дальше мы создали переменную delta_age, в которую попала разница между сегодняшним днем и датой рождения. Если вывести ее, результат будет примерно таким: 8261 days, 0:00:00 Допустим, вам действительно нужен возраст в годах. Вы можете преобразовать timedelta в количество дней, добавив .days к timedelta. Это можно поместить в другую переменную с именем days_old. Вывод days_old и ее типа показывает, что days_old является int — обычным целым числом, с которым можно выполнять математические операции. ГЛАВА 1 Работа с числами, текстом и датами 129
Например, в следующем коде переменная days_old получает значение delta_age.days, которое представляет собой delta_age из предыдущей строки, преобразованное в количество дней: delta_age = (today - birthdate) days_old = delta_age.days print(days_old, type(days_old)) 8261 <class 'int'> Это означает, что прошло 8261 день с момента рождения. Чтобы узнать возраст в годах, нужно разделить это число на 365. Если вы используете обычное деление (/), результат будет с дробной частью. Но если вам нужно только целое количество лет, используйте оператор целочисленного деления //. Он просто отбрасывает дробную часть и оставляет только целое число: years_old = days_old // 365 print(years_old) Если вы хотите посчитать не только годы, но и месяцы, можно сделать это приближенным способом. Для начала нужно узнать, сколько дней осталось после того, как мы посчитали полные годы. Это легко сделать: берем остаток от деления общего количества дней на 365. Затем делим эти оставшиеся дни на 30 (приблизительная длина месяца). Полученное число даст примерное количество месяцев. На рисунке 1.18 показан пример в Jupyter Notebook, где шаг за шагом выполняются вычисления. Каждый этап снабжен комментариями, которые помогают понять, что именно делает код и как получается результат. СОВЕТ 130 Помните, что для сложных задач, связанных с датами и временем, можно обращаться за помощью к генеративному искусственному интеллекту. Например, в VS Code с помощью GitHub Copilot или в сервисах вроде ChatGPT вы можете попросить: «Напиши код на Python для вычисления разницы между двумя датами» или «Напиши код на Python, чтобы вывести текущую дату и время в формате США». Такие инструменты умеют подсказывать готовые решения, которые вы сможете сразу использовать или адаптировать под свои задачи. РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 1.18. Вычисление возраста в годах и месяцах из объекта timedelta Учет часовых поясов Как вы уже знаете, время в разных уголках Земли отличается. Когда у вас наступает полдень, в другой части мира может быть глубокая ночь. Это связано с тем, что планета разделена на часовые пояса. На рисунке 1.19 показана карта этих поясов. Если хотите рассмотреть ее подробнее, просто найдите в интернете «карта часовых поясов». Чтобы все это можно было согласовать, существует универсальное время, называемое UTC (универсальное координированное время). Вы могли встречать и другие названия: GMT (среднее время по Гринвичу) или «Zulu-time», которое часто используется военными. Все они означают одно и то же — время, отсчитываемое по нулевому меридиану Земли (0 градусов долготы), проходящему через Гринвич. Именно этот меридиан делит карту часовых поясов ровно пополам. ГЛАВА 1 Работа с числами, текстом и датами 131
Рисунок 1.19. Часовые пояса Сегодня, когда речь заходит о часовых поясах, почти все используют специальную базу данных tz (ее еще называют базой Олсона). В ней хранится список всех часовых поясов мира с учетом регионов и местных особенностей. Именно на нее опираются программы и языки программирования, в том числе Python, когда нужно работать с датами и временем. Полный список часовых поясов очень большой, и приводить его здесь нет смысла. Но для примера в таблице 1.10 показаны некоторые часовые пояса США. В первом столбце указано официальное название из базы tz. Во втором — привычное название часового пояса. А в последних двух столбцах — смещение от UTC для стандартного времени и для летнего времени. Зачем вообще разбираться во всех этих часовых поясах? Дело в том, что в Python даты и время могут храниться в двух разных видах. 132 » Наивные значения даты и времени — это такие объекты, в которых нет информации о часовом поясе . Например, 2024 .03 .15 10:00 может означать 10 утра где угодно — в Москве, Нью-Йорке или Токио . » Информированные значения даты и времени — это даты и время, в которых явно указано, к какому часовому поясу они относятся . Например, 2024 .03 .15 10:00 UTC+3 однозначно указывает, что это 10 утра по московскому времени . РАЗДЕЛ 2 Понимание базовых конструкций Python
Таблица 1.10. Примеры часовых поясов из базы Олсона Часовой пояс Привычное название Смещение от UTC Смещение от UTC в летнее время Etc/UTC Всемирное координированное время (UTC) +00:00 +00:00 America/Anchorage Анкоридж (Аляска, США) -09:00 -08:00 America/Adak Адак (Алеутские острова, США) -10:00 -09:00 America/Phoenix Финикс (Аризона, США) -07:00 -07:00 America/Chicago Чикаго (Центральный часовой пояс, США) -06:00 -05:00 America/New_York Нью-Йорк (Восточный часовой пояс, США) -05:00 -04:00 America/Indiana/ Indianapolis Индианаполис (Индиана, США) -05:00 -04:00 America/Honolulu Гонолулу (Гавайи, США) -10:00 -10:00 America/ Indiana/Knox Нокс (Индиана, США) -06:00 -05:00 America/Detroit Детройт (Мичиган, США) -05:00 -04:00 America/Denver Денвер (Горный часовой пояс, США) -07:00 -06:00 America/ Los Angeles Лос-Анджелес (Тихоокеанский часовой пояс) -08:00 -07:00 Pacific/Pago_Pago Паго-Паго (Американское Самоа, США) -11:00 -11:00 Все объекты timedelta и даты, которые вы создаете с помощью метода .date(), всегда считаются наивными, то есть не содержат информации о часовом поясе. То же самое относится и к объектам времени (time()) или даты и времени (datetime()): по умолчанию они тоже будут наивными. Однако у этих двух типов есть возможность добавить сведения о часовом поясе. Это бывает полезно, например, если вы хотите показывать время событий пользователям, находящимся в разных регионах и часовых поясах. ГЛАВА 1 Работа с числами, текстом и датами 133
Работа с часовыми поясами Когда вы получаете текущее время с системных часов компьютера, оно отображается в вашем локальном часовом поясе, но при этом в самом объекте времени не хранится информация о том, какой именно это пояс. Чтобы определить разницу между локальным временем и универсальным временем (UTC), можно сравнить результат now() (текущее локальное время) и utcnow() (время по UTC). Если вы вычислите разницу между ними, то получите смещение вашего часового пояса. Например, когда выполнялся код, локальное время было 13:02, а время UTC — 18:02. Разница составила 5:00:00, то есть пять часов. Это значит, что локальный часовой пояс находится на 5 часов позади UTC (UTC-5). Рисунок 1.20. Определение разницы между вашим временем и временем UTC Обратите внимание, что если вы вычитаете более раннее время из более позднего, то в результате можно получить отрицательное значение, что на первый взгляд выглядит странно. Например: time_difference = (here_now - utc_now) Difference: -1 day, 19:00:00 На самом деле это все равно означает разницу в пять часов. Дело в том, что, если из 24 часов (одного дня) вычесть 1 день и 19 часов, получится те же самые –5 часов. Немного хитро, но логично, если понять принцип. Важно помнить: правая сторона карты часовых поясов — это восток, а солнце всегда встает на востоке. Значит, когда в вашем часовом поясе рассвет только наступает, то в часовых поясах справа (восточнее) он уже произошел, а в поясах слева (западнее) еще нет. 134 РАЗДЕЛ 2 Понимание базовых конструкций Python
Если же вам нужно работать не просто с разницей во времени, а с конкретными названиями часовых поясов, то можно использовать библиотеку dateutil, которая значительно расширяет возможности модуля datetime. Для этого вам понадобится класс tz и функция gettz (сокращение от get timezone — получить часовой пояс). Чтобы подключить их, добавьте в ваш код строку импорта сразу после строки с import datetime: # import datetime and dateutil tz import datetime as dt from dateutil.tz import gettz После подключения утилиты gettz вы можете использовать ее для указания конкретного часового пояса. Для этого достаточно вызвать gettz(© имя'), где © имя© — это строка с названием часового пояса из базы данных Олсона. Например, если вам нужно восточное время США, используйте © America/New_York© , а если универсальное координированEtc/UTC© . ное время — © Таким образом, вы можете создавать объекты даты и времени, которые будут не «наивными», а «осведомленными» о своем часовом поясе. На рисунке 1.21 показан пример, где с помощью datetime.now() мы получаем текущее время сразу для пяти разных зон — UTC и четырех часовых поясов США. Рисунок 1.21. Текущая дата и время для пяти различных часовых поясов ГЛАВА 1 Работа с числами, текстом и датами 135
Все времена в США указаны как стандартное время, поскольку в конце ноября в США никто не переходит на летнее время (DST). Давайте посмотрим, что происходит, если мы планируем событие на июль, когда в США снова действует летнее время. В этом примере (рисунок 1.22) мы снова используем datetime и gettz из модуля dateutil, как и раньше. Но на этот раз нам не нужно текущее время — нас интересует конкретное событие, запланированное на 4 июля 2020 года в 19:00 по местному времени. Для этого мы создаем объект datetime с помощью такой строки: event = dt.datetime(2020,7,4,19,0,0) Так как часовой пояс здесь не указан, Python по умолчанию считает, что время задано в вашем локальном часовом поясе. Результат сохраняется в переменной event. Следующая строка кода (которая идет сразу после комментария с символом #) выводит дату и время в локальном формате. Мы добавили слово 'Local:' в начало строки, а также символ новой строки (\n) в конец, чтобы результат выглядел аккуратнее и был визуально отделен от остального текста: # Show local date and time print(f"Local: {event:%D %I:%M %p %Z}\n") Рисунок 1.22. Дата и время запланированного события в нескольких часовых поясах 136 РАЗДЕЛ 2 Понимание базовых конструкций Python
При запуске эта команда выведет локальную дату и время в заданном формате. Например: Local: 07/04/20 07:00 PM Остальная часть программы вычисляет корректные дату и время для пяти разных часовых поясов, чтобы показать, как одно и то же событие может отображаться в зависимости от региона: name = event.astimezone(gettz("tzname")) Здесь первое слово name — это просто название переменной, придуманное нами. В выражении event.astimezone() переменная event указывает на исходное время события, которое мы задали ранее. Функция astimezone() — это встроенный метод модуля dateutil, и ее общий синтаксис выглядит так: astimezone(gettz("tzname")) В каждой строке кода, где мы вычисляем дату и время для определенного часового пояса, вместо tzname подставляется название часового пояса из базы данных tz. На выводе (см. рисунок 1.22) видно, что одно и то же событие показано сразу в пяти разных часовых поясах. Обратите внимание, что для США время автоматически учитывает переход на летнее время. Например, вы увидите EDT (Eastern Daylight Time — Североамериканское восточное летнее время), так как событие запланировано на июль, когда на восточном побережье действует именно этот режим. Когда вы смотрите на результат, первая дата и время соответствуют вашему часовому поясу, как и ожидалось. Для остальных строк время корректируется с учетом разницы между часовыми поясами, чтобы событие отображалось правильно для каждой зоны. Если вы думаете: «Какая же это сложная путаница!» — вы абсолютно правы. Все это действительно не выглядит простым или интуитивным, и уж точно не вызывает восторга. Но если в вашей задаче важно учитывать часовые пояса, изученные методы помогут вам получить нужный результат. ПРИМЕЧАНИЕ Если поискать в интернете информацию о работе с часовыми поясами в Python, вы наверняка встретите рекомендации использовать модуль arrow вместо dateutil. Дело в том, что arrow считается более удобным и современным инструментом. Однако мы не будем подробно рассматривать его в этой книге: arrow не входит в стандартную библиотеку Python, а значит, требует дополнительной установки. Кроме того, наш учебник и без того охватывает большой объем информации, и если бы мы попытались включить абсолютно все, вам пришлось бы возить книгу в тележке.
В этой главе вы узнаете: » Как принимать решения с помощью if » Как выполнять повторяющиеся действия с for » Как создавать циклы с while Гл а в а 2 Управление действиями Д о этого момента в книге мы в основном обсуждали хранение информации в компьютере — чаще всего в переменных, с которыми могут работать Python и сам компьютер. Это ключевой шаг, ведь данные должны быть в удобной для машины форме, чтобы с ними можно было что-то делать. Представьте, что это часть «иметь», то есть обладать информацией для дальнейшей работы. Теперь пришло время перейти к части «делать» — использовать эту информацию, чтобы создавать что-то полезное или интересное. В этой главе мы сосредоточимся на самых важных и часто применяемых действиях, которые позволяют компьютеру действительно «работать». И начнем мы с того, что компьютеры умеют делать особенно хорошо, быстро и эффективно: принимать решения. Основные операторы управления действием программы Вы управляете действиями своей программы (и компьютера), заставляя ее принимать решения, а для этого чаще всего нужны сравнения. Чтобы сравнивать значения, в Python используются специальные символы — операторы сравнения. Их также называют реляционными операторами, потому что они помогают определить, как два элемента связаны между собой. 138 РАЗДЕЛ 2 Понимание базовых конструкций Python
Кроме того, Python предоставляет три логических оператора (их еще называют булевыми), с помощью которых можно объединять сразу несколько сравнений и получать итоговое решение. Эти операторы записываются простыми английскими словами, которые интуитивно отражают их смысл. В таблице 2.1 приведены основные операторы сравнения, а в таблице 2.2 — логические операторы. Таблица 2.1. Основные операторы сравнения Оператор Значение == Равно != Не равно < Меньше > Больше <= Меньше или равно >= Больше или равно Таблица 2.2. Логические операторы ПРИМЕЧАНИЕ Оператор Значение and И — выражение истинно, если оба условия истинны or Или — выражение истинно, если хотя бы одно условие истинно not Не — возвращает противоположное значение (отрицание) Термин «логический» (или Boolean) появился благодаря Джорджу Булю — математику XIX века, который разработал основы алгебры логики. Именно его идеи легли в основу принципов, на которых работают современные компьютеры. Если вам захочется узнать больше, просто поищите информацию о нем в интернете. В программировании логические операторы чаще всего применяются в условных конструкциях if … else, которые позволяют управлять поведением программы. В Python для этого используется оператор if, с помощью которого можно задать условие: если оно выполняется, выполняется один блок кода, а если нет — другой. ГЛАВА 2 Управление действиями 139
Принятие решений с помощью if В Python оператор if используется для принятия решений, и именно с него начинается знакомство с условными конструкциями. Он проверяет условие и, если оно истинно (True), выполняет блок кода, связанный с этим условием. В самом простом виде синтаксис выглядит так: if condition: do this do this no matter what Итак, первая строка с командой «выполнить это» (do this) будет выполнена только в том случае, если условие окажется истинным. Если же условие ложно, эта строка будет пропущена. При этом вторая строка выполнится в любом случае — независимо от результата проверки. Обратите внимание, что обе эти строки написаны без отступов. Отступы в Python имеют ключевое значение, и совсем скоро вы в этом убедитесь. Но для начала давайте посмотрим на несколько простых примеров с базовым синтаксисом if. Такие примеры можно легко опробовать самостоятельно — в Jupyter Notebook или в файле с расширением .py. На рисунке 2.1 приведен элементарный пример: переменной sun присваивается строка "down". Затем оператор if проверяет, совпадает ли значение переменной sun со строкой "down". Если условие выполняется, программа выводит сообщение Good night! После этого выполнение продолжается как обычно, и на экран выводится сообщение I am here. Рисунок 2.1. Результат простого оператора if, когда условие оказывается истинным ОСТОРОЖНО Когда вы проверяете равенство в Python, всегда используйте два знака равенства подряд без пробелов между ними (==). Это важное правило, о котором легко забыть. Если вы напишете его неправильно, программа не будет работать так, как ожидается. Если у вас установлен и включен GitHub Copilot в VS Code (подробнее о нем см. в главе 1 раздела 1), он может предлагать вам возможный вариант кода при вводе условия if. Подсказка появится серым текстом. Чтобы принять ее, достаточно нажать Tab, а затем при необходимости изменить предложенный код. 140 РАЗДЕЛ 2 Понимание базовых конструкций Python
Если запустить тот же пример, но присвоить переменной sun любое другое значение, кроме «down», то первый вызов print() будет пропущен. Однако следующая строка выполнится в любом случае, потому что она не зависит от условия. Такой результат показан на рисунке 2.2. Рисунок 2.2. Результат простого оператора if, когда условие оказывается ложным Во втором примере переменная sun не равна "down", поэтому условие оказалось ложным. В этом случае строка кода, связанная с условием if, игнорируется, и выполняется только следующая команда. В рассмотренных примерах действие при истинности условия было записано в той же строке, что и сам оператор if. Однако на практике гораздо чаще требуется выполнить не одну, а несколько строк кода. В таких случаях каждая строка, которая должна выполняться только при истинности условия, записывается с отступом. А те строки, что идут без отступа, выполняются всегда, независимо от того, было ли условие истинным. В Python принято использовать отступы длиной четыре пробела. Это не строгое правило языка (можно выбрать другое количество пробелов или даже табуляцию), но важно, чтобы все строки внутри одного блока имели одинаковый отступ. Даже если условие запускает только одну строку кода, программисты обычно все равно используют отступ — это делает код более понятным и аккуратным. В результате стандартный синтаксис оператора if в Python выглядит так: if condition: do this ... do this no matter what Если условие оказалось истинным, выполняется строка с командой «выполнить это», а также все остальные строки, у которых такой же отступ. Как только встречается строка без отступа, она уже не зависит от условия — Python выполнит ее всегда. Поэтому простой пример с переменной sun можно записать так: sun = "down" if sun == "down": ГЛАВА 2 Управление действиями 141
print("Good night!") print("I am here") Как видно на рисунке 2.3, такой код ведет себя точно так же, как если бы все команды были записаны в одну строку. Когда переменная sun равна "down", Python сначала выводит сообщение "Good night!", а затем выполняет второй оператор print. Если же значение переменной sun отличается от "down", строка с выводом "Good night!" пропускается, и выполняется только следующий оператор. Рисунок 2.3. Результат выполнения простого оператора if, когда условие истинно, а затем ложно Если вы задумываетесь, какой стиль записи операторов if лучше — в одну строку или в несколько, то все зависит от того, что именно считать «лучше». Если речь идет о скорости выполнения, то разницы нет — оба варианта работают одинаково быстро, и вы ее просто не заметите. Но если под «лучше» понимать удобство чтения кода человеком, то здесь большинство программистов выберет второй способ — с отступами под оператором if. Важно помнить: вы можете добавить любое количество строк с отступами после if. Эти строки выполнятся только в том случае, если условие окажется истинным. Если условие ложно — они будут проигнорированы. При этом строки без отступа, идущие дальше, выполняются всегда, потому что они не зависят от условия. Ниже показан пример, где под оператором if находятся сразу четыре строки, выполняющиеся только тогда, когда условие истинно: total = 100 sales_tax_rate = 0.065 taxable = True if taxable: print(f"Subtotal : ${total:.2f}") 142 РАЗДЕЛ 2 Понимание базовых конструкций Python
sales_tax = total * sales_tax_rate print(f"Sales Tax: ${sales_tax:.2f}") total = total + sales_tax print(f"Total : ${total:.2f}") ЗАПОМНИТЕ В Python логические значения True и False всегда пишутся с заглавной буквы в начале и строчными буквами дальше. Если написать их по-другому (например, true или false), язык не распознает их как булевы значения, и код работать не будет. Обратите внимание на такой пример: if taxable: Этот код полностью корректен, потому что переменная taxable заранее определена как логическая (она может принимать только True или False). Поэтому проверка if taxable: уже сама по себе означает «если taxable равно True». Иногда можно встретить другой вариант записи: if taxable == True: Он тоже будет работать, но в нем нет необходимости, так как taxable и так содержит булево значение. В примере мы начинаем с трех переменных: total (сумма покупки), sales_tax_rate (ставка налога с продаж) и taxable (логическая переменная, указывающая, облагается ли сумма налогом). Если taxable установлено в True, выполняются все строки кода с отступами под оператором if, и итоговая сумма выводится уже с учетом налога (см. рисунок 2.4). Если же taxable имеет значение False, то блок кода под if пропускается. В этом случае итоговая сумма остается равной исходной, без добавления налога (см. рисунок 2.5). ПРИМЕЧАНИЕ Фигурные скобки и формат .2f, показанные на рисунках 2.4 и 2.5, используются только для красивого отображения чисел (например, двух знаков после запятой). Они не влияют на работу условия if и никак не связаны с логикой принятия решений. ГЛАВА 2 Управление действиями 143
Рисунок 2.4. Когда taxable имеет значение True, sales_tax добавляется к total Рисунок 2.5. Когда taxable имеет значение False, sales_tax не добавляется к total Добавление else к логике if В примерах, которые мы рассматривали до этого, блок кода выполнялся только в том случае, если условие оказывалось истинным. Если же условие было ложным, программа просто переходила дальше, игнорируя этот блок. Но в реальных задачах часто нужно предусмотреть альтернативное действие: один блок выполняется, если условие истинно, а другой — если оно ложно. Для этого в Python используется конструкция else. Она добавляется сразу после блока if. Код с отступами под else: выполняется только в том случае, если условие не выполнилось (то есть оказалось ложным). Синтаксис выглядит так: if condition: do indented lines here ... else: 144 РАЗДЕЛ 2 Понимание базовых конструкций Python
do indented lines here ... do remaining un-indented lines no matter what На рисунке 2.6 показан простой пример использования конструкции if ... else. В этой программе мы получаем текущее время с помощью datetime.now(). Затем проверяем, меньше ли значение часа 12. Если да, программа выводит сообщение Good morning («Доброе утро»). Если же час больше или равен 12, выполняется блок после else, и на экран выводится сообщение Good afternoon («Добрый день»). После этого, вне зависимости от времени суток, программа дополнительно печатает строку I hope you are doing well! («Надеюсь, у вас все хорошо!»). Таким образом, результат работы программы зависит от текущего времени: утром вы получите утреннее приветствие, а днем — дневное, но в обоих случаях финальное сообщение остается одинаковым. Рисунок 2.6. Вывод приветствия в зависимости от времени суток Глядя на этот пример, вы можете подумать: «Хм, здорово! Но что будет, если сейчас 11 вечера? Вряд ли уместно выводить «Добрый день!» В таких случаях одной конструкции if ... else недостаточно. Нам нужно больше гибкости, чтобы программа могла выбирать между несколькими вариантами в зависимости от времени суток. Именно для этого в Python существует оператор elif (сокращение от else if). Он позволяет добавлять дополнительные условия к проверке, создавая цепочку решений: если не выполнено первое условие, проверяется второе, затем третье и т. д. ГЛАВА 2 Управление действиями 145
Обработка нескольких операторов else с помощью elif Когда простая конструкция if ... else не охватывает все варианты, на помощь приходит оператор elif (сокращение от else if). С его помощью можно добавить столько дополнительных условий, сколько нужно. Важно помнить: » » » блок if проверяется первым; если он ложен, Python переходит к проверке elif; заключительный блок else можно добавить, а можно и пропустить — он выполнится только тогда, когда все предыдущие условия окажутся ложными. В самой простой форме конструкция if с elif, но без else, выглядит так: if condition: do these indented lines of code ... elif condition: do these indented lines of code ... do these un-indented lines of code no matter what Иногда при использовании конструкции if ... elif может случиться так, что ни одно из условий не окажется истинным, и тогда строки с отступом просто не выполнятся. Рассмотрим пример: light_color = "green" if light_color == "green": print("Go") elif light_color == "red": print("Stop") print("This code executes no matter what") В этом случае результат будет таким: Go This code executes no matter what А если изменить значение переменной light_color на "red", то выполнится другой блок: light_color = "red" 146 РАЗДЕЛ 2 Понимание базовых конструкций Python
if light_color == "green": print("Go") elif light_color == "red": print("Stop") print("This code executes no matter what") Результат теперь будет: Stop This code executes no matter what Предположим, что вы задаете цвет сигнала светофора как любой другой, кроме зеленого или красного, например "yellow": light_color = "yellow" if light_color == "green": print("Go") elif light_color == "red": print("Stop") print("This code executes no matter what") В этом случае программа выведет: This code executes no matter what Это происходит потому, что ни одно из условий — light_color == "green" или light_color == "red" — не оказалось истинным. Поэтому код внутри блоков if и elif не выполняется, а срабатывает только строка без отступа. Чтобы обработать ситуацию, когда значение не подходит ни под одно из указанных условий, можно добавить блок else. Он выполняется только тогда, когда все предыдущие проверки оказались ложными. Пример: light_color = "yellow" if light_color == "green": print("Go") elif light_color == "red": print("Stop") else: print("Proceed with caution") Результат работы программы будет таким: Proceed with caution This code executes no matter what ГЛАВА 2 Управление действиями 147
Так как переменная light_color содержит значение "yellow", оба условия в конструкции if оказываются ложными. В результате выполняется только блок else. Такой подход работает для любого значения переменной, кроме "red" или "green", ведь else не проверяет конкретное условие. Его задача — сыграть роль «по умолчанию»: если ни одно из предыдущих условий не выполнилось, запускается код в блоке else. Рассмотрим еще один пример, в котором мы используем конструкцию if ... elif ... else. Сначала переменной age присваивается значение 31. Далее программа последовательно проверяет условия и в зависимости от результата выбирает, какое сообщение вывести: age = 31 if age < 21: beverage = "milk" elif age >= 21 and age < 80: beverage = "beer" else: beverage = "prune juice" print("Have a " + beverage) Комментарии в Python не обязательны, но они значительно повышают читаемость кода и помогают лучше понять его логику при повторном обращении. Например, тот же самый код можно записать с комментариями так: age = 31 if age < 21: # If under 21, no alcohol beverage = "milk" elif age >= 21 and age < 80: # Ages 21 - 79, suggest beer beverage = "beer" else: # If 80 or older, prune juice might be a good choice. beverage = "prune juice" print("Have a " + beverage) ЗАПОМНИТЕ 148 Если вдруг вы забыли, как правильно пишется конструкция if ... elif ... else, не переживайте — вам не нужно держать все правила в голове. Вы всегда можете попросить GitHub Copilot, ChatGPT, Claude.ai или другой генеративный ИИ показать пример. В ответ вы получите шаблон с правильным синтаксисом на Python, который можно использовать как основу для своего кода. РАЗДЕЛ 2 Понимание базовых конструкций Python
Тернарные операции В программировании конструкции if ... else встречаются очень часто. Поэтому во многих языках появился сокращенный способ записи таких выражений. Его называют тернарным оператором (от слова «три», так как участвуют три части: условие, значение при True и значение при False). В Python это тоже есть, но чаще используется название условное выражение. Смысл остается тем же, что и у обычного if ... else: выбрать одно из двух значений в зависимости от условия. Разница только в записи — она более компактная. Общий шаблон выглядит так: value_if_true if condition else value_if_false Условие в тернарном операторе всегда должно быть выражением, которое возвращает True или False. Если условие выполняется, результатом станет значение, указанное в части value_if_true. Если условие ложно — возвращается значение из части value_if_false. Рассмотрим пример: здесь переменная sales_tax_rate получает значение 0.065, если товар облагается налогом, и 0, если нет. product = "Plushie" unit_price = 4.99 taxable = False # Sales tax rate value depends on taxable status sales_tax_rate = 0.065 if taxable else 0 print(sales_tax_rate) Строка, которая задает налоговую ставку, присвоит переменной sales_ tax_rate значение 0.065, если товар облагается налогом (то есть если переменная taxable равна True). В противном случае переменная получит значение 0. По сути, это работает точно так же, как и стандартная конструкция if ... else, просто в более компактной форме. Вот эквивалентный вариант кода с использованием обычного if ... else: product = "Plushie" unit_price = 4.99 taxable = False ГЛАВА 2 Управление действиями 149
# Sales tax rate value depends on taxable status if taxable: sales_tax_rate = 0.065 else: sales_tax_rate = 0 print(sales_tax_rate) Повторение процесса с помощью цикла for Принятие решений — это важный элемент написания любых приложений: игр, систем искусственного интеллекта, управления роботами и многого другого. Однако в программировании часто возникает другая задача — нужно что-то посчитать или многократно выполнить один и тот же набор действий. В таких случаях удобно использовать цикл for, который позволяет повторять одну или несколько строк кода столько раз, сколько нужно. Перебор чисел в диапазоне Если заранее известно, сколько раз должен выполниться цикл, проще всего использовать конструкцию for вместе с функцией range(). Базовый синтаксис выглядит так: for x in range(y): do this do this ... un-indented code is executed after the loop Вместо x вы можете использовать любое имя переменной, а вместо y — любое число или диапазон чисел. Если в range() указать одно число, цикл начнется с 0 и закончится на этом числе минус один. Например, запустите следующий код: for x in range(7): print(x) print("All done") 150 РАЗДЕЛ 2 Понимание базовых конструкций Python
Результатом выполнения этого кода будет последовательный вывод значений переменной x от 0 до 6, а затем сообщение о завершении работы цикла. Числа появляются потому, что цикл for перебирает все значения в диапазоне range(7), начиная с 0 и заканчивая 6 включительно. После того как цикл завершен, срабатывает строка без отступа, которая выполняется уже вне цикла. Вывод будет следующим: 0 1 2 3 4 5 6 All done Иногда начинающие программисты удивляются, почему цикл for в Python считает от 0, а не от 1. Дело в том, что это стандартное поведение: если вы указываете только одно число в range(), цикл всегда начинается с 0. Но вы легко можете изменить это, указав два числа — начальное и конечное — через запятую. Первое число задает, с какого значения начинается счет, а второе указывает, где он должен закончиться (цикл останавливается перед этим значением, то есть фактически работает «на единицу меньше»). Посмотрим на пример: for x in range(1,10): print(x) print("All done") Когда вы запустите этот код, цикл действительно начнется с числа 1 и будет выполняться до тех пор, пока не дойдет до числа, которое на единицу меньше указанного последнего значения. В нашем примере последнее число в range(1, 10) — это 10, поэтому цикл завершится на 9: 1 2 3 4 5 6 7 8 9 All done ГЛАВА 2 Управление действиями 151
Если вы хотите, чтобы цикл действительно посчитал от 1 до 10, нужно указать диапазон range(1, 11). Почему 11? Потому что второе число в range() всегда является границей, но не включается в диапазон. Это вы можете увидеть на рисунке 2.7. Рисунок 2.7. Цикл от 1 до 10 Перебор строки в цикле В цикле for необязательно использовать функцию range(). Вместо этого можно передать строку, и тогда цикл выполнится столько раз, сколько в строке символов. На каждой итерации переменная цикла (например, x, но имя может быть любым) будет содержать один символ строки, начиная с первого и двигаясь слева направо. Синтаксис при этом будет таким: for x in string do this do this ... do this when the loop is done Цикл for можно использовать не только с диапазонами чисел, но и со строками. Здесь переменная цикла (x в примере) будет поочередно принимать значение каждого символа строки. Как обычно, x можно заменить любым другим именем переменной. А сама строка может быть задана прямо в кавычках или храниться в переменной. Например, вот простой код, который можно попробовать в Jupyter Notebook или в файле .py: 152 РАЗДЕЛ 2 Понимание базовых конструкций Python
for x in "snorkel": print(x) print("Done") При запуске этого кода цикл последовательно выведет все буквы слова snorkel, по одной на каждой строке. После того как цикл завершится, выполнение программы перейдет к первой строке без отступа, и на экране будет напечатано слово Done. s n o r k e l Done Строка для перебора в цикле for необязательно должна быть записана прямо в кавычках. Она может храниться в переменной, а в цикле вы просто указываете имя этой переменной. Например: my_word = "snorkel" for x in my_word: print(x) print("Done") Результат работы программы будет точно таким же: по одной букве слова snorkel на строку и в конце слово Done. Разница только в том, что здесь мы перебираем содержимое переменной my_word, а не строковый литерал "snorkel". Python понимает, что нужно взять именно значение переменной, так как оно не заключено в кавычки. s n o r k e l Done ГЛАВА 2 Управление действиями 153
Перебор элементов списка В Python список — это набор элементов, записанных через запятую и заключенных в квадратные скобки. Такой список можно легко перебрать с помощью цикла for. Например, в следующем коде список указан прямо в первой строке цикла: for x in ["The", "rain", "in", "Spain"]: print(x) print("Done") Такой цикл выполняется по одному разу для каждого элемента списка. При каждом проходе переменная x принимает значение одного из элементов, начиная с первого и двигаясь слева направо. В результате выполнения приведенного выше кода на экран выводятся все элементы списка по очереди, а затем строка "Done". Именно такой вывод показан на рисунке 2.8. Рисунок 2.8. Перебор элементов списка Вы можете сохранить список в переменной и затем использовать эту переменную внутри цикла for, вместо того чтобы писать сам список прямо в коде. На рисунке 2.9 приведен пример: переменной seven_dwarves присвоен список из семи имен. Как и прежде, список заключен в квадратные скобки — именно они говорят Python, что это список. Далее цикл for перебирает каждый элемент списка и выводит имя одного гнома за один проход. Вместо привычного x в примере использовано имя переменной dwarf. Но вы можете выбрать любое другое корректное имя: например, x, little_person, name_of_fictional_entity или даже goober_wocky. Главное правило — имя, указанное в первой строке цикла for, должно совпадать с тем, которое вы используете внутри цикла. 154 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 2.9. Перебор элементов списка Выход из цикла Иногда бывает нужно, чтобы цикл не доходил до конца списка или диапазона, а завершался раньше, как только выполнится какое-то условие. Для этого в Python используется оператор break. Его обычно пишут внутри блока if. Когда программа встречает break, выполнение цикла сразу же прекращается, даже если еще остались элементы для перебора. В результате будет выглядеть так: for x in items: if condition: [do this ... ] break do this Квадратные скобки в записи синтаксиса нужны только для пояснения и в сам код не входят — они показывают, что часть конструкции является необязательной. Теперь рассмотрим практический пример. Допустим, у нас есть список ответов на тест. Мы хотим пройтись по нему с помощью цикла for. При этом введем правило: если встретится пустой ответ, программа должна вывести сообщение «Не завершено» и остановить цикл, не проверяя остальные элементы списка. В приведенном ниже примере список содержит только заполненные ответы, поэтому условие с пустой строкой не выполняется, и цикл доходит до конца: answers = ["A", "C", "B", "D"] for answer in answers: ГЛАВА 2 Управление действиями 155
if answer == "": print("Incomplete") break print(answer) print("Loop is done") В результате мы получим все четыре ответа: A C B D Loop is done Теперь давайте рассмотрим тот же пример, но добавим в список пустой элемент. В Python пустая строка обозначается как "". В нашем случае третий элемент списка будет именно таким: answers = ["A", "C", "", "D"] for answer in answers: if answer == "": print("Incomplete") break print(answer) print("Loop is done") В итоге получится так: A C Incomplete Loop is done Логика работы цикла такая: пока каждый элемент списка содержит какой-то ответ, условие в блоке if не выполняется, и цикл продолжает свою работу до конца списка. Но если программа встречает пустой ответ, выполняется команда print("Incomplete"), а затем срабатывает оператор break. Это означает, что цикл немедленно прерывается и управление передается первой строке кода за его пределами — в нашем случае это print("Loop is done"). 156 РАЗДЕЛ 2 Понимание базовых конструкций Python
Цикл с continue Цикл можно не только прервать, но и пропустить отдельный шаг, продолжив выполнение со следующего элемента. Для этого используется оператор continue. Если break полностью завершает цикл, то continue просто возвращает выполнение в начало цикла, игнорируя оставшийся код внутри текущей итерации. Ниже приведен пример, аналогичный предыдущему, но вместо того, чтобы останавливать цикл при встрече пустого ответа, мы используем continue. В результате программа просто пропустит этот элемент и перейдет к следующему: answers = ["A", "C", "", "D"] for answer in answers: if answer == "": print("Incomplete") continue print(answer) print("Loop is done") Вот что напечатает программа. Пустой элемент не выводится: при его обнаружении срабатывает print("Incomplete"), затем continue возвращает выполнение в начало цикла, и перебор продолжается со следующего ответа. A C Incomplete D Loop is done Вложенные циклы В Python можно без проблем использовать один цикл внутри другого — это и называется вложенными циклами. Главное — следить за отступами, ведь именно они определяют, к какому циклу относится конкретная строка кода. Рассмотрим пример (см. рисунок 2.10). Внешний цикл перебирает слова "Fist", "Second" и "Third". На каждой итерации он печатает текущее слово, а затем запускает внутренний цикл, который выводит числа от 1 до 3. Для этого используется range(3), а к значению x внутри него прибавляется 1, чтобы печатать не 0–2, а привычные 1–3. ГЛАВА 2 Управление действиями 157
Рисунок 2.10. Вложенные циклы Таким образом, вложенные циклы позволяют выполнять повторяющиеся действия внутри других повторяющихся действий — мощный инструмент, который часто используется в программировании. Циклы while В Python кроме цикла for есть еще цикл while. Разница между ними не очень большая, но принцип работы отличается. • • Цикл for обычно используется тогда, когда заранее известно количество повторений — например, перебор диапазона чисел или элементов списка. Цикл while, наоборот, выполняется до тех пор, пока условие остается истинным. То есть количество итераций заранее неизвестно: цикл будет повторяться, пока выполняется проверка. Базовый синтаксис выглядит так: while condition: 158 РАЗДЕЛ 2 Понимание базовых конструкций Python
do this ... do this ... do this when the loop is done При работе с циклами while очень важно помнить: условие выхода должно когда-нибудь выполниться. Если оно никогда не станет ложным, вы получите бесконечный цикл. Такой цикл будет повторяться бесконечно, пока не произойдет ошибка или вы сами не прервете его работу — например, закроете программу или даже перезагрузите компьютер. На рисунке 2.11 показан пример, где цикл while выполняется ограниченное количество раз. Это достигается благодаря трем моментам: » Сначала создаем переменную-счетчик counter и задаем ей начальное значение 65 . » Запускаем цикл while, который должен выполняться, пока counter < 91 . » Внутри цикла на каждом проходе увеличиваем счетчик на единицу (counter += 1) . В какой-то момент counter станет равен 91, условие counter < 91 перестанет быть истинным, и цикл завершится . Функция chr() в теле цикла преобразует текущее числовое значение counter в соответствующий символ по таблице ASCII. Перебор кодов от 65 до 90 дает все заглавные латинские буквы от A до Z, что и показано на рисунке 2.11. Распространенная ошибка в таких конструкциях — забыть увеличить счетчик. Если удалить строку counter += 1 (как на рисунке 2.12), условие цикла так и останется истинным, и программа будет бесконечно печатать A. Такой «бесконечный цикл» продолжится, пока вы его не остановите вручную или пока программа не упадет по ошибке. Если вы случайно запустили бесконечный цикл в Jupyter Notebook, паниковать не стоит. Все решается довольно просто. Нажмите квадратную кнопку Stop, которая находится справа от кнопки Run. При наведении на нее курсора появится подсказка Interrupt the Kernel, что в переводе с программистского жаргона означает «остановить выполнение». После этого весь код в блокноте прекратит работу. Чтобы снова вернуться к исходному состоянию, перезапустите ядро с помощью кнопки с изогнутой стрелкой (справа от кнопки Stop). Теперь вы сможете внести исправления в код и попробовать его запустить снова. ГЛАВА 2 Управление действиями 159
Рисунок 2.11. Цикл while работает, пока counter меньше 91 Рисунок 2.12. Бесконечный цикл while 160 РАЗДЕЛ 2 Понимание базовых конструкций Python
СОВЕТ Если вы запутались в синтаксисе и не помните, как правильно написать цикл в Python, можно не тратить время на поиск в документации. Достаточно обратиться к Copilot или другому генеративному ИИ и попросить его написать цикл именно того типа, который вам нужен. Например, можно запросить цикл for, цикл while или цикл, который проходит по каждому символу строки. Важно формулировать запрос максимально просто и так, чтобы он отражал задачу, которую вы хотите решить прямо сейчас. В ответ вы получите рабочий пример кода, на который сможете опираться. Перезапуск циклов while с помощью continue Внутри цикла можно использовать условие if вместе с оператором continue. Когда программа встречает continue, выполнение сразу возвращается к началу цикла, и проверка условия выполняется снова. Такой прием позволяет пропускать отдельные шаги и продолжать работу с оставшимися. На рисунке 2.13 показан пример, как это выглядит на практике. Рисунок 2.13. Цикл while с continue Цикл while работает до тех пор, пока значение переменной counter меньше 10. Внутри цикла переменной number присваивается случайное ГЛАВА 2 Управление действиями 161
число от 1 до 999. Затем программа проверяет, является ли это число четным, с помощью условия: if int(number / 2) == number / 2: Здесь важно понимать, что функция int() возвращает только целую часть числа. Например, если случайное число равно 5, при делении на 2 получится 2.5. Вызов int(2.5) вернет 2, так как дробная часть отбрасывается. В итоге условие 2 == 2.5 окажется ложным, значит, оператор continue не выполнится. Программа напечатает это нечетное число, увеличит счетчик и перейдет к следующему шагу цикла. Если следующим случайным числом окажется, например, 12, то при делении на 2 получится ровно 6. Так как и исходное значение, и результат функции int() совпадают, условие окажется истинным. В этом случае сработает оператор continue, и программа не выполнит оставшийся код внутри цикла для этого числа, а сразу вернется к его началу. Таким образом, цикл while будет продолжать работу, но каждая встреча с четным числом приведет к пропуску оставшихся строк внутри цикла. После того как условие counter < 10 перестанет быть истинным, цикл завершится, и программа перейдет к выполнению первой строки кода после цикла. Прерывая циклы while с break Оператор break можно рассматривать как инструмент для немедленной остановки цикла while, даже если его условие все еще остается истинным. То есть вместо того, чтобы ждать, пока условие станет ложным, программа завершает цикл сразу же, как только встречает break. Базовый синтаксис выглядит так: while condition1: do this ... if condition2: break do this when the loop is done Цикл может завершиться двумя способами: либо его основное условие становится ложным, либо выполняется дополнительное условие, при котором срабатывает оператор break. В любом из этих случаев выполнение программы продолжается с первой строки кода за пределами 162 РАЗДЕЛ 2 Понимание базовых конструкций Python
цикла — в примере это строка с текстом «выполнить это, когда цикл завершен». Вот пример. Программа должна напечатать до десяти случайных чисел, которые не делятся на 5 без остатка. Но количество реально выведенных чисел может быть меньше 10, потому что при появлении числа, делящегося на 5 без остатка, цикл сразу прерывается. Поэтому заранее можно сказать только одно: программа выведет от нуля до десяти чисел, не делящихся на 5. Сколько именно чисел будет показано при каждом запуске, предсказать невозможно — ведь мы не знаем, когда именно среди случайных значений встретится число, кратное 5. import random print("Numbers that aren't evenly divisible by 5") counter = 0 while counter < 10: # Get a random number number = random.randint(1,999) if int(number / 5) == number / 5: # If it's evenly divisible by 5, bail out. break # Otherwise, print it and keep going for a while. print(number) # Increment the loop counter. counter += 1 print("Loop is done") При первом запуске программы результат может выглядеть так, как показано на рисунке 2.14. При повторном запуске вывод уже может отличаться и больше напоминать рисунок 2.15. Точный результат предсказать невозможно, потому что каждое случайное число действительно выбирается случайным образом. Именно эта непредсказуемость и делает работу со случайными числами важной частью многих игр и симуляций. ГЛАВА 2 Управление действиями 163
Рисунок 2.14. Цикл while с оператором break Рисунок 2.15. Тот же код, что и на рисунке 2 .14, при втором запуске
В этой главе вы узнаете: » Как определять списки » Как работать со списками » Что такое кортежи и как их использовать » Что представляют собой множества и как с ними работать Гл а в а 3 Быстрая работа со списками и кортежами И ногда в программе вы работаете с одним значением — например, именем человека, ценой товара или логином пользователя. Но нередко приходится иметь дело с целыми наборами данных: списком имен, перечнем товаров с их ценами и т.д. В других языках программирования такие наборы часто называют массивами, а в Python они представлены списками и другими типами коллекций. Python предоставляет множество простых, удобных и быстрых инструментов для работы с такими коллекциями данных. В этой главе мы познакомимся с основными приемами и возможностями. Как всегда, советуем вам повторять примеры вместе с нами в Jupyter Notebook или в файле с расширением .py, ведь практика значительно помогает лучше усвоить теорию. Определение и использование списков Простейший тип коллекции данных в Python — это список. В предыдущей главе мы уже сталкивались с ним. Список — это набор элементов, разделенных запятыми и заключенных в квадратные скобки. Обычно ГЛАВА 3 Быстрая работа со списками и кортежами 165
список сохраняют в переменную с помощью оператора =. Создавать списки так же просто, как и обычные переменные. Если список содержит числа, их не нужно брать в кавычки. Например, вот список с результатами теста: scores = [88, 92, 78, 90, 98, 84] Если список содержит строки, их обязательно нужно заключать в одинарные или двойные кавычки. Например: students = ["Mark", "Amber", "Todd", "Anita", "Sandy"] Вы можете вывести содержимое списка так же, как и любую переменную: print(students). После выполнения этой команды на экране появится: ['Mark', 'Amber', 'Todd', 'Anita', 'Sandy'] Такой вывод может показаться немного необычным, ведь элементы списка показываются вместе со скобками и кавычками. Но это нормально — именно так Python отображает список целиком. Если же вам нужен более привычный вывод, не переживайте: в Python есть разные способы красиво выводить содержимое списка. Обращение к элементам списка по позиции Каждый элемент списка имеет свой порядковый номер, или индекс. Нумерация начинается с 0, поэтому первый элемент списка имеет индекс 0, второй — 1, третий — 2 и т .д. Эти индексы вы не видите напрямую, но Python использует их, чтобы «знать», где находится каждый элемент. Чтобы обратиться к конкретному элементу, нужно указать имя списка и номер позиции в квадратных скобках. Общий синтаксис такой: listname[x] ДЕЙСТВИТЕЛЬНО ДЛИННЫЕ СПИСКИ В этой главе мы используем только короткие списки, чтобы примеры оставались простыми и легко читаемыми . Но в реальной практике списки могут содержать сотни и даже тысячи элементов, и при этом их содержимое может постоянно меняться . Вводить такие длинные списки прямо в код было бы неудобно и запутанно . 166 РАЗДЕЛ 2 Понимание базовых конструкций Python
Поэтому в реальных проектах данные для списков обычно хранят во внешних файлах или базах данных . Это делает работу с ними проще и понятнее . Все методы работы со списками, которые вы изучаете в этой главе, одинаково применимы и к данным, загруженным из внешних источников . Разница лишь в том, что сначала нужно будет написать код для загрузки этих данных в список . Но прежде, чем переходить к большим наборам данных, важно освоить основы на небольших списках. Так вы сможете уверенно использовать все приемы, когда придет время работать с длинными и сложными структурами. Чтобы обратиться к конкретному элементу списка, нужно указать его индекс (номер позиции) в квадратных скобках после имени списка. При этом важно помнить: Python начинает счет с нуля, а не с единицы. То есть первый элемент списка имеет индекс 0, второй — 1, и т .д. Например, создадим список студентов и выведем первый элемент: students = ["Mark", "Amber", "Todd", "Anita", "Sandy"] print(students[0]) ПРИМЕЧАНИЕ В результате первым будет Mark. Когда программисты говорят об элементах списка, они часто проговаривают индекс как «sub». То есть students[0] можно произнести как «students sub zero». Теперь посмотрим еще один пример со списком чисел. Если мы хотим вывести элемент с индексом 4, то получим значение 84: scores = [88, 92, 78, 90, 84] print(scores[4]) 84 В Python очень важно помнить, что список имеет строгие границы. Если вы попытаетесь обратиться к элементу, которого там нет, программа выдаст ошибку. Такой сбой называется ошибкой выхода за пределы списка (IndexError). Слово index как раз указывает на число в квадратных скобках. Например, посмотрите на рисунок 3.1. Результат будет таким: IndexError: list index out of range. Почему? Все просто: в списке есть элементы с индексами от 0 до 4, а вот scores[5] не существует. ГЛАВА 3 Быстрая работа со списками и кортежами 167
То есть доступны только: • • • • • scores[0]  88 scores[1]  92 scores[2]  78 scores[3]  90 scores[4]  84 А вот scores[5] приведет к ошибке, потому что Python строго следит за границами списка (рисунок 3.1). Рисунок 3.1. Возникает ошибка выхода за пределы индекса, поскольку scores[5] не существует Перебор элементов списка В Python очень удобно перебирать элементы списка с помощью цикла for. Такой цикл позволяет пройтись по каждому элементу списка один за другим, без необходимости указывать индексы. Общий синтаксис выглядит так: for x in list: Здесь: x — это имя переменной, в которую на каждой итерации будет подставляться очередной элемент списка; list — это имя списка, который вы перебираете. Чтобы код был понятнее, программисты часто используют множественное число для имени списка (например, students, scores), а в цикле — СОВЕТ 168 РАЗДЕЛ 2 Понимание базовых конструкций Python
единственное число для переменной (student, score). Такой подход делает код более читаемым и «говорящим». Например: for score in scores: print(score) Очень важно помнить: в Python отступы играют ключевую роль. Все, что должно выполняться внутри цикла, пишется с одинаковым отступом (обычно это четыре пробела). Если вы не сделаете отступ, код либо не запустится, либо будет работать не так, как вы ожидали. На рисунке 3.2 приведен более полный пример, где видно, как цикл перебирает список и печатает каждое значение одно за другим. Если вы попробуете этот код в Jupyter Notebook или в файле .py, результат будет точно таким же: все элементы списка будут выведены по строкам, а после завершения цикла выполнение программы продолжится. Рисунок 3.2. Перебор элементов списка Проверка наличия элемента в списке В Python можно легко проверить, есть ли нужный элемент в списке. Для этого используется ключевое слово in вместе с именем списка. Такая проверка особенно удобна внутри операторов if или при присвоении значения переменной. Например, в коде на рисунке 3.3 создается список имен. Затем мы проверяем наличие двух имен — Anita и Bob. Результаты этих проверок сохраняются в переменные. Когда мы выводим значения переменных, то видим, что для имени Anita возвращается True, потому что оно есть в списке, а для имени Bob — False, так как его в списке нет. ГЛАВА 3 Быстрая работа со списками и кортежами 169
Рисунок 3.3. Проверка наличия элемента в списке Определение длины списка Чтобы определить, сколько элементов находится в списке, в Python используется встроенная функция len() (от английского length — длина). Нужно просто передать имя списка в круглых скобках. Например, выполните следующий код в Jupyter Notebook, командной строке Python или в любом другом месте: students = ["Mark", "Amber", "Todd", "Anita", "Sandy"] print(len(students)) Результат будет таким: 5 Это значит, что в списке пять элементов. При этом важно помнить: индексация элементов списка начинается с 0, а не с 1. Поэтому последний элемент ("Sandy") находится не в students[5], а в students[4]. Добавление элемента в конец списка Когда нужно добавить новый элемент в список, Python предлагает очень удобный инструмент — метод .append(). Он всегда вставляет элемент в конец списка. В круглых скобках вы указываете, что именно нужно добавить: это может быть либо значение напрямую (строка, число и т.д.), либо переменная. 170 РАЗДЕЛ 2 Понимание базовых конструкций Python
На рисунке 3.4 показан пример использования метода .append(). Строка students.append("Goober") добавляет в список новое имя Goober. А строка students.append(new_student) добавляет то значение, которое хранится в переменной new_student. Важно помнить: метод .append() всегда помещает новый элемент в конец списка. Поэтому, когда вы выведете список на экран, оба новых имени окажутся в самом конце последовательности. Рисунок 3.4. Добавление двух новых имен в конец списка В Python очень удобно проверять, есть ли элемент в списке, прежде чем добавлять его. Это позволяет избежать дублирования значений. Например, в коде ниже программа сначала проверяет, содержится ли имя "Amanda" в списке. Поскольку оно уже там есть, добавление не произойдет: student_name = "Amanda" # Add student_name but only if not already in the list. if student_name in students: print(student_name + " already in the list") else: students.append(student_name) print(student_name + " added to the list") Вставка элемента в список Метод append() всегда добавляет элемент в конец списка, но иногда нужно вставить его в определенное место. Для этого в Python используется метод insert(). Его синтаксис выглядит так: listname.insert(position, item) • listname — имя списка, в который вы хотите добавить элемент; ГЛАВА 3 Быстрая работа со списками и кортежами 171
• • position — индекс позиции, куда нужно вставить элемент (например, 0 сделает его первым элементом, 1 — вторым и т .д .); item — значение или переменная с этим значением . Например, следующий код добавляет "Lupe" в начало списка: # Create a list of strings (names). students = ["Mark", "Amber", "Todd", "Anita", "Sandy"] student_name = "Lupe" # Add student name to front of the list. students.insert(0, student_name) # Show me the new list. print(students) После запуска этого кода команда print(students) выведет список уже с добавленным новым именем следующим образом: ['Lupe', 'Mark', 'Amber', 'Todd', 'Anita', 'Sandy'] Изменение элемента в списке В Python элементы списка можно изменять так же просто, как и значения переменных — с помощью оператора присваивания =. Главное — указать индекс элемента в квадратных скобках, чтобы Python понял, какой именно элемент нужно заменить. Синтаксис выглядит так: listname[index] = newvalue • • index — номер элемента (начиная с 0); newvalue — новое значение, которое вы хотите поместить в список . Например, в этом коде мы создаем список имен и заменяем имя с индексом 3 (четвертый элемент, так как отсчет начинается с 0): # Create a list of strings (names). students = ["Mark", "Amber", "Todd", "Anita", "Sandy"] students[3] = "Hobart" print(students) Когда вы запустите этот код, список изменится: вместо имени Anita на позиции с индексом 3 теперь будет Hobart. В результате на экране отобразится: ['Mark', 'Amber', 'Todd', 'Hobart', 'Sandy'] 172 РАЗДЕЛ 2 Понимание базовых конструкций Python
Объединение списков Если у вас есть два списка и вы хотите объединить их в один, можно воспользоваться методом extend(). Его синтаксис выглядит так: original_list.extend(additional_items_list) В коде вместо original_list укажите имя списка, к которому вы хотите добавить новые элементы, а вместо additional_items_list — имя списка, элементы которого должны присоединиться к первому. Вот пример с двумя списками list1 и list2: После выполнения list1.extend(list2) список list1 будет содержать все элементы обоих списков: # Create two lists of Names. list1 = ["Zara", "Lupe", "Hong", "Alberto", "Jake"] list2 = ["Huey", "Dewey", "Louie", "Nader", "Bubba"] # Add list2 names to list1. list1.extend(list2) # Print list 1. print(list1) ['Zara', 'Lupe', 'Hong', 'Alberto', 'Jake', 'Huey', 'Dewey', 'Louie', 'Nader', 'Bubba'] Проще простого, правда? Кроме метода .extend(), списки можно объединять и с помощью знака плюс (+). Такой способ называется конкатенацией. Он создает новый список, в который помещаются все элементы из обоих списков. Пример: list1 = ["Mark", "Amber", "Todd"] list2 = ["Anita", "Sandy"] newlist = list1 + list2 print(newlist) Результат будет таким: ['Mark', 'Amber', 'Todd', 'Anita', 'Sandy'] В отличие от extend(), здесь исходные списки (list1 и list2) остаются без изменений. ГЛАВА 3 Быстрая работа со списками и кортежами 173
Удаление элементов списка В Python есть метод .remove(), который позволяет удалить из списка любой указанный элемент. Если этот элемент встречается в списке несколько раз, метод удаляет только первое его вхождение, оставляя остальные без изменений. Рассмотрим пример: # Create a list of strings. letters = ["A", "B", "C", "D", "C", "E", "C"] # Remove "C" from the list. letters.remove("C") # Show me the new list. print(letters) Как видно, удалена только первая буква "C", а остальные остались в списке. ['A', 'B', 'D', 'C', 'E', 'C'] Иногда нужно удалить не одно, а все вхождения определенного элемента в списке. Для этого можно использовать цикл while, который будет повторять команду .remove(), пока нужный элемент остается в списке. Например, следующий код удалит все буквы "C" из списка: while "C" in letters: letters.remove("C") Если же вы хотите удалить элемент не по значению, а по его позиции, используйте метод .pop(). В отличие от .remove(), он работает с индексами. Укажите номер элемента в скобках — и именно этот элемент будет удален. Кроме того, если вызвать .pop() без параметров, Python автоматически удалит последний элемент списка. Рассмотрим пример: # Create a list of strings. letters = ["A", "B", "C", "D", "E", "F", "G"] # Remove the first item. letters.pop(0) # Remove the last item. letters.pop() # Show me the new list. print(letters) 174 РАЗДЕЛ 2 Понимание базовых конструкций Python
При запуске этого кода видно, что удаление первого и последнего элементов списка прошло успешно. В выводе остается только середина списка, что подтверждает корректную работу метода .pop(). ['B', 'C', 'D', 'E', 'F'] Когда вы удаляете элемент из списка с помощью метода .pop(), у вас есть возможность сохранить этот элемент в переменной. Это удобно, если вы планируете использовать его позже в программе. Например, на рисунке 3.5 показан тот же код, что и в предыдущем примере, но здесь удаленные элементы сохраняются в переменных first_removed и last_ removed. В конце программа выводит оставшийся список, а также показывает, какие именно буквы были удалены. Рисунок 3.5. Убираем элементы списка с помощью pop() В Python есть еще одна команда для удаления элементов из списка — del (сокращение от delete). Она позволяет удалить элемент по его индексу (позиции) в списке. Напомним, что первый элемент всегда имеет индекс 0. Например, следующий код удаляет элемент с индексом 2 из списка: # Create a list of strings. letters = ["A", "B", "C", "D", "E", "F", "G"] # Remove item sub 2. del letters[2] print(letters) При выполнении приведенного выше кода список отображается так: ['A', 'B', 'D', 'E', 'F', 'G'] Буква C была удалена, и это верно, ведь индексация элементов идет с нуля: A — это элемент с индексом 0, B — с индексом 1, а C — с индексом 2. ГЛАВА 3 Быстрая работа со списками и кортежами 175
Команда del позволяет удалять не только отдельные элементы, но и весь список целиком. Для этого достаточно убрать квадратные скобки и индекс. Например, в коде на рисунке 3.6 создается список, а затем полностью удаляется. Если после этого попытаться вывести список с помощью print(), возникнет ошибка, так как список больше не существует. Важно помнить: в отличие от метода pop(), который возвращает удаленный элемент и может быть сохранен в переменной, команда del просто удаляет объект и ничего не возвращает. Рисунок 3.6. Удаление списка с последующей попыткой его вывода вызывает ошибку Очистка списка Если вам нужно удалить все элементы из списка, но при этом сохранить сам список (то есть оставить его «пустым»), используйте метод .clear(). В результате список продолжает существовать, но уже не содержит данных. Другими словами, после вызова .clear() вы получаете пустой список. Посмотрите на следующий код: # Create a list of strings. letters = ["A", "B", "C", "D", "E", "F", "G"] # Clear the list of all entries. letters.clear() # Show me the new list. print(letters) [] 176 РАЗДЕЛ 2 Понимание базовых конструкций Python
Результат выполнения будет таким: []. Пустые квадратные скобки означают, что список все еще существует, но в нем больше нет элементов. Подсчет количества вхождений элемента в список Чтобы узнать, сколько раз определенный элемент встречается в списке, используйте метод .count(). Его синтаксис очень простой: listname.count(x) Здесь listname — это имя списка, в котором вы ищете, а x — элемент, количество вхождений которого нужно подсчитать. В качестве x можно указать либо конкретное значение, либо имя переменной, где это значение хранится. grades.count("B") Например, в коде на рисунке 3.7 подсчитывается, сколько раз буква "B" встречается в списке. Для этого используется метод .count() с переданным в скобках литералом "B": Рисунок 3.7. Подсчет элементов в списке ГЛАВА 3 Быстрая работа со списками и кортежами 177
Обратите внимание, что буква "B" заключена в кавычки. Это значит, что мы имеем дело с литералом — конкретным значением, а не с именем переменной. В примере мы также подсчитали количество оценок "C", но сохранили результат в переменной, чтобы показать немного другой способ записи. Оба варианта работают одинаково, что видно по результатам выполнения программы. Кроме того, мы подсчитали количество "F" прямо в коде, передав результат метода .count() в функцию print(). Так как в списке нет оценок "F", метод вернул 0. Это тоже видно на выводе. Здесь мы не подсчитывали все возможные оценки — цель примера лишь показать, как работает синтаксис Python. Мы не создаем полноценную программу для анализа школьных журналов, а просто учимся пользоваться методами списка. ЗАПОМНИТЕ И еще один важный момент: если вы хотите объединить число и строку в одном сообщении, сначала преобразуйте число в строку с помощью функции str(). Иначе возникнет ошибка вида: can only concatenate str (not "int") to str. Это сообщение говорит о том, что в Python можно складывать (конкатенировать) только строки со строками. Здесь int означает целое число (integer), а str — строку (string). Поиск индекса элемента списка Иногда бывает нужно не только проверить, есть ли элемент в списке, но и узнать его точную позицию. Для этого в Python существует метод .index(), который возвращает номер индекса (позицию) элемента в списке. Синтаксис выглядит так: listname.index(x) Как обычно, замените listname на имя списка, в котором нужно выполнить поиск, а x — на значение, которое вы ищете (это может быть как строковой литерал в кавычках, так и имя переменной). Конечно, нет гарантии, что элемент действительно присутствует в списке или встречается там только один раз. Если элемента нет, возникнет ошибка. Если элемент встречается несколько раз, метод .index() вернет индекс только первого совпадения. 178 РАЗДЕЛ 2 Понимание базовых конструкций Python
На рисунке 3.8 показан пример, где программа завершается с ошибкой на строке f_index = grades.index(look_for), поскольку в списке нет оценки F. Рисунок 3.8. Код ломается, когда пытается найти позицию несуществующего элемента Простой способ избежать такой ошибки — сначала проверить, есть ли элемент в списке, а уже потом обращаться к его индексу. Для этого можно использовать оператор if. Если элемент отсутствует, программа выводит сообщение об этом. Если же элемент найден, метод .index() возвращает его позицию, и она выводится в сообщении: # Create a list of strings. grades = ["C", "B", "A", "D", "C", "B", "C"] # Decide what to look for look_for = "F" # See if the item is in the list. if look_for in grades: # If it's in the list, get and show the index. print(str(look_for) + " is at index " + str(grades. index(look_for))) else: # If not in the list, don't even try for index number. print(str(look_for) + " isn't in the list.") ГЛАВА 3 Быстрая работа со списками и кортежами 179
Сортировка списков по алфавиту В Python есть удобный метод .sort(), который позволяет отсортировать элементы списка. В самой простой форме он работает так: • • если список состоит из строк, элементы сортируются в алфавитном порядке; если в списке числа — они упорядочиваются от меньшего к большему . Чтобы выполнить такую сортировку, достаточно вызвать метод с пустыми скобками: listname.sort() Замените listname на имя вашего списка. На рисунке 3.9 показан пример, где мы сортируем список строк и список чисел. Достаточно просто применить метод .sort() к каждому списку, после чего код выводит содержимое уже отсортированных списков. Рисунок 3.9. Сортировка строк и чисел СОВЕТ Если в вашем списке строки содержат смесь заглавных и строчных букв, порядок сортировки может показаться странным. В этом случае можно использовать параметр key, который указывает Python, как сравнивать элементы. Например, запись listname.sort(key=lambda s: s.lower()) заставляет Python сравнивать строки в нижнем регистре, и сортировка будет выглядеть так, как вы ожидаете. Подробное объяснение работы с ключами сортировки вы найдете во главе 5 раздела 2. С датами работать чуть сложнее, чем со строками или числами, потому что их нельзя просто записать как текст, например 12/31/2020. Если оставить дату в виде строки, Python будет сортировать ее как обычный текст, а это приведет к неправильному порядку. 180 РАЗДЕЛ 2 Понимание базовых конструкций Python
Чтобы даты можно было сортировать корректно, они должны иметь специальный тип данных — date. Для этого используется модуль datetime и его метод date(), который создает объект даты. Такие объекты можно хранить в списках и обрабатывать так же, как и любые другие элементы. Например, вот как можно создать список из четырех дат: import datetime as dt dates = [dt.date(2020, 12, 31), dt.date(2019, 1, 31), dt.date(2018, 2, 28), dt.date(2020, 1, 1)] Для компьютера не имеет значения, как именно вы создаете список — можно сразу перечислить все элементы или добавлять их по одному. Но для человека такой код будет проще читать и понимать, если даты вводить постепенно. Это особенно удобно, когда список получается длинным или каждая дата несет особое значение. В примере на рисунке 3.10 мы сначала создаем пустой список с именем datelist: Рисунок 3.10. Сортировка и отображение дат в удобном формате Затем мы добавляли даты по одной в список, используя синтаксис dt.date (год, месяц, день). После того как список был сформирован, мы вызвали метод datelist. sort(), чтобы отсортировать даты в хронологическом порядке — от самой ранней к самой поздней. ГЛАВА 3 Быстрая работа со списками и кортежами 181
Обратите внимание: если вывести список с помощью обычного print(datelist), то вы получите не только сами даты, но и их представление как объектов Python. Например: [datetime.date(2018, 2, 28), datetime.date(2019, 1, 31), datetime.date (2020, 1, 1), datetime.date(2020, 12, 31)] Такой список не очень удобно читать, если выводить его целиком одной командой print(). Поэтому вместо этого мы прошли циклом по каждой дате и вывели ее отдельно, применив форматную строку %m/%d/%Y. В результате каждая дата отображается на новой строке в привычном формате мм/дд/гггг, как показано в нижней части рисунка 3.10. Если же вам нужно отсортировать список не по возрастанию, а по убыванию, используйте параметр reverse=True внутри скобок метода sort(). Обратите внимание: слово True всегда пишется с заглавной буквы. На рисунке 3.11 показаны примеры сортировки всех трех списков в обратном порядке с использованием этого параметра. Рисунок 3.11. Сортировка строк, чисел и дат в обратном порядке 182 РАЗДЕЛ 2 Понимание базовых конструкций Python
Обратный список Вы также можете поменять порядок элементов в списке на противоположный с помощью метода .reverse(). Важно понимать, что это не одно и то же, что сортировка в обратном порядке. Когда вы используете сортировку с параметром reverse=True, элементы сначала выстраиваются по определенным правилам (например, строки — от Я до А, числа — от большего к меньшему, даты — от поздней к ранней). А метод .reverse() просто переворачивает список «как есть», меняя местами элементы так, чтобы они шли в обратном порядке относительно исходного, без какой-либо сортировки. В примере ниже мы изменяем порядок имен в списке на обратный и выводим результат: # Создаем список строк. names = ["Zara", "Lupe", "Hong", "Alberto", "Jake"] # Изменяем список на обратный. names.reverse() # Выводим результат. print(names) ['Jake', 'Alberto', 'Hong', 'Lupe', 'Zara'] Копирование списка Если нужно работать с копией списка, не затрагивая исходный, можно воспользоваться методом .copy(). Это особенно удобно, когда вы хотите изменить порядок элементов или провести с ними другие операции, но сохранить оригинальный список без изменений. Посмотрите на пример: # Создаем список строк. names = ["Zara", "Lupe", "Hong", "Alberto", "Jake"] # Создаем копию списка. backward_names = names.copy() # Изменяем копию на обратную. backward_names.reverse() # Выводим результат. print(names) print(backward_names) ГЛАВА 3 Быстрая работа со списками и кортежами 183
['Zara', 'Lupe', 'Hong', 'Alberto', 'Jake'] ['Jake', 'Alberto', 'Hong', 'Lupe', 'Zara'] В таблице 3.1 собраны методы, с которыми вы уже познакомились в этой главе. В дальнейшем вы увидите, что эти методы применимы не только к спискам, но и к другим так называемым итерируемым объектам. Этот термин обозначает любую структуру данных, элементы которой можно последовательно перебирать — по одному за раз. Использование списковых включений Работая с Python, вы довольно быстро столкнетесь с таким понятием, как списковое включение (list comprehension). Название может показаться пугающим, но на самом деле это всего лишь другой, более компактный способ работы со списками. Чаще всего списковые включения используют для создания нового списка на основе уже существующего. Базовый синтаксис выглядит так: new_list = [expression for item in iterable if condition Таблица 3.1. Методы работы со списками 184 Метод Что делает append() Добавляет элемент в конец списка clear() Удаляет все элементы из списка, оставляя его пустым copy() Создает копию списка count() Подсчитывает, сколько раз элемент встречается в списке extend() Добавляет элементы из одного списка в конец другого index() Возвращает индекс (позицию) элемента в списке insert() Вставляет элемент в список в указанную позицию pop() Удаляет элемент из списка и возвращает его (можно сохранить в переменной) remove() Удаляет один элемент из списка РАЗДЕЛ 2 Понимание базовых конструкций Python
Метод Что делает reverse() Меняет порядок элементов списка на обратный sort() Сортирует список по возрастанию sort(reverse=True) Сортирует список по убыванию В своем коде заменяйте выделенные курсивом слова в синтаксисе следующим образом: » » » » » new_list — имя нового списка, который вы хотите создать; expression — значение, которое будет добавлено в новый список для каждого элемента итерируемого объекта; item — отдельный элемент из исходного списка, который перебирается в цикле; iterable — полный список или другой итерируемый объект, из которого берутся элементы; условие — выражение, которое отфильтровывает элементы, исключая те, что не соответствуют заданному критерию. Все, что вы можете сделать с помощью спискового включения, можно реализовать и с использованием привычных методов, которые мы уже изучили. Разница лишь в том, что списковое включение позволяет выполнить ту же задачу гораздо короче — буквально в одной строке кода. Ниже приведено несколько примеров. Сначала посмотрим на код, а затем подробно разберем, что именно в нем происходит. # Простой список чисел numbers = [1, 2, 3, 4, 5, 6, 7, 8, 10] # Идентичный список чисел same_numbers = [number for number in numbers] print(same_numbers) # Только четные числа в этом списке even_numbers = [number for number in numbers if number % 2 == 0] print(even_numbers) ГЛАВА 3 Быстрая работа со списками и кортежами 185
# Только нечетные числа в этом списке odd_numbers = [number for number in numbers if number % 2 != 0] print(odd_numbers) # Все числа в квадрате squared_numbers = [number ** 2 for number in numbers] print(squared_numbers) Код начинается со списка под названием numbers, в котором хранятся числа от 1 до 10. В первом примере мы создаем новый список с именем same_numbers, в который просто копируются все элементы исходного списка. Поскольку никаких дополнительных условий мы не задавали, новый список полностью совпадает с оригиналом. Далее идет более интересный пример. В следующей строке кода создается список even_numbers, в который попадают только те элементы из исходного списка, которые делятся на два без остатка (то есть четные числа): even_numbers = [number for number in numbers if number % 2 == 0] Условие в конце выражения — if number % 2 == 0 — исключает все нечетные числа. Это работает потому, что оператор % возвращает остаток от деления. Если число делится на два без остатка, результат равен нулю, и такое число считается четным. Для нечетных чисел остаток всегда не равен нулю, поэтому они в список не попадают. В итоге переменная even_numbers будет содержать [2, 4, 6, 8, 10]. Теперь рассмотрим противоположный случай. В этой строке кода создается список odd_numbers, куда включаются только нечетные числа: odd_numbers = [number for number in numbers if number % 2 != 0] В этой строке создается новый список с именем odd_numbers, в который попадают только нечетные числа из исходного списка. Условие if number % 2!= 0 отсекает все четные значения, так как у них остаток от деления на 2 равен нулю, а у нечетных — всегда не равен нулю. В заключительном примере мы видим, что в списковом включении можно не только фильтровать элементы, но и изменять их значения. Здесь каждый элемент возводится в квадрат, прежде чем попасть в новый список: squared_numbers = [number ** 2 for number in numbers] Запись number ** 2 означает «число в квадрате». То есть каждое значение исходного списка возводится во вторую степень перед тем, как 186 РАЗДЕЛ 2 Понимание базовых конструкций Python
попасть в новый список. В данном случае никаких дополнительных условий нет, поэтому в новый список попадают все элементы, но уже в измененном виде. Результат работы выглядит так: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] Важно помнить, что списковое включение — это всего лишь сокращенный способ создания списков на основе уже существующих. Все то же самое можно сделать и более традиционным кодом с циклами. Поэтому если вам пока трудно воспринимать такой лаконичный синтаксис, не переживайте — со временем он станет привычным. Что такое кортеж и зачем он нужен? Помимо списков, в Python есть еще одна похожая структура данных — кортеж (tuple). Произносить это слово можно по-разному: кто-то говорит «тьюпл», а кто-то — в рифму со словом couple («капл»). Но пишется оно именно как tuple (а не tupple или toupe), поэтому будем придерживаться варианта «тьюпл». Впрочем, строгого единственного варианта произношения, скорее всего, нет — и это не мешает программистам время от времени спорить на эту тему. Название может показаться необычным, но суть очень проста: кортеж — это список, который нельзя изменить после создания. Другими словами, если список можно редактировать (добавлять, убирать или изменять элементы), то кортеж остается неизменным. Зачем это может быть полезно? Представьте, что вы работаете с данными, которые должны оставаться неизменными. Например, в интернетмагазине цены товаров, однажды сохраненные в определенном месте программы, не должны меняться пользователем случайно или намеренно. Если бы такие данные хранились в обычном списке, кто угодно мог бы их изменить. Кортеж же надежно фиксирует значения. Синтаксис кортежа практически такой же, как у списка, только вместо квадратных скобок используются круглые: prices = (29.95, 9.98, 4.95, 79.98, 2.95) Большинство методов работы со списками, которые вы видели в таблице 3.1, не подходят для кортежей, потому что они изменяют элементы, а кортеж — неизменяемая структура. Однако с кортежами все же можно выполнять некоторые полезные операции. ГЛАВА 3 Быстрая работа со списками и кортежами 187
Например, вы можете узнать количество элементов с помощью функции len(): print(len(prices)) Этот код выведет длину кортежа prices. Также можно использовать метод .count(), чтобы посчитать, сколько раз конкретное значение встречается в кортеже: print(prices.count(4.95)) А если нужно проверить, содержится ли определенное значение в кортеже, воспользуйтесь оператором in: print(4.95 in prices) Если значение есть, вы получите True. Если его нет — результатом будет False. Если элемент есть в кортеже, его можно найти с помощью метода .index(), который возвращает позицию элемента. Однако если такого элемента нет, программа выдаст ошибку. Чтобы избежать ошибки, лучше сначала проверить наличие элемента с помощью оператора in. Если элемента нет, можно вернуть условное значение, например –1: look_for = 12345 if look_for in prices: position = prices.index(look_for) else: position = –1 print(position) В этом коде, если число 12345 есть в кортеже, в переменную position запишется его индекс. Если элемента нет — программа вернет –1. Кортежи также можно перебирать в цикле, как и списки. Например, следующий код выводит каждое значение с символом доллара и двумя цифрами после десятичной точки: # Перебираем и красиво отображаем элементы кортежа for price in prices: print(f"${price:.2f}") Результат будет таким: $29.95 $9.98 188 РАЗДЕЛ 2 Понимание базовых конструкций Python
$4.95 $79.98 Важно помнить, что изменить элементы кортежа нельзя. Например, такой код вызовет ошибку: prices[1] = 234.56 Вы получите сообщение об ошибке: TypeError: 'tuple' object does not support item assignment (ошибка типа: объект tuple не поддерживает присваивание элементов). Это означает, что оператор присваивания = не может использоваться для изменения значений внутри кортежа, потому что кортеж — неизменяемая структура данных. Любой метод, который изменяет список или даже создает его копию с изменениями, вызовет ошибку, если применить его к кортежу. Например, методы списка .append(),.clear(),.copy(),.extend(),.insert(), .pop(),.remove(),.reverse() и .sort() неприменимы к кортежам. Именно поэтому кортежи удобно использовать тогда, когда нужно хранить и показывать данные пользователю, но при этом защитить их от изменений. Работа с множествами В Python существуют еще и множества — еще один способ организации данных. Главное отличие множества от списка заключается в том, что его элементы не имеют фиксированного порядка. Даже если вы запишете элементы множества в определенной последовательности, при работе с ним никакой индекс (позиция элемента) использоваться не будет. Для создания множества применяются фигурные скобки {}. Напомним: для списка используются квадратные скобки [], а для кортежа — круглые (). Например, вот так можно задать множество чисел: sample_set = {1.98, 98.9, 74.95, 2.5, 1, 16.3} Множества во многом похожи на списки и кортежи. Например, вы можете использовать функцию len(), чтобы узнать количество элементов, ГЛАВА 3 Быстрая работа со списками и кортежами 189
или оператор in, чтобы проверить, содержится ли определенное значение в множестве. Однако есть и важные отличия. Во-первых, элементы множества не имеют индексов, поэтому вы не сможете обратиться к ним по позиции, как в списке. Во-вторых, изменить конкретный элемент множества нельзя. Более того, порядок элементов в множестве не фиксирован, поэтому к нему неприменимы методы вроде .sort() (сортировка) или .reverse() (обратный порядок). Зато у множеств есть свои полезные методы. Например, вы можете добавить новый элемент с помощью метода .add(): sample_set.add(11.23) И еще одна особенность: в отличие от списка, множество никогда не хранит дубликатов. То есть каждое значение в нем может встречаться только один раз. Даже если вы несколько раз добавите число 11.23 в множество, оно все равно будет храниться там только один раз. Это важное свойство множеств — они не допускают дубликатов. Если нужно добавить сразу несколько значений, используйте метод .update(). Добавляемые элементы должны быть оформлены как спи- сок в квадратных скобках: sample_set.update([88, 123.45, 2.98]) Также множества можно копировать. Однако стоит помнить: так как множества не имеют фиксированного порядка элементов, при выводе копии порядок может отличаться от оригинала. Рассмотрим пример: # Определяем множество с именем sample_set sample_set = {1.98, 98.9, 74.95, 2.5, 1, 16.3} # Показываем все множество print(sample_set) # Создаем копию и показываем ее ss2 = sample_set.copy() print(ss2) {1.98, 98.9, 2.5, 1, 74.95, 16.3} {16.3, 1.98, 98.9, 2.5, 1, 74.95} 190 РАЗДЕЛ 2 Понимание базовых конструкций Python
На рисунке 3.12 приведен пример кода и его результат. В нем создается множество с именем sample_set, после чего выполняются несколько операторов print() для вывода содержимого. Первая строка, которая выводит все множество целиком, выглядит так: print(sample_set) Рисунок 3.12. Эксперименты с множествами Python Эта строка кода выводит число 6, так как во множестве хранится шесть элементов: print(len(sample_set)) Следующая строка возвращает True, потому что значение 74.95 действительно содержится в множестве sample_set: print(74.95 in sample_set) ГЛАВА 3 Быстрая работа со списками и кортежами 191
Комментарии в коде поясняют работу остальных строк. Особое внимание стоит обратить на эту команду внутри цикла ближе к концу примера: print(f"{price:>6.2f}") Здесь каждое число форматируется так, чтобы отображаться с двумя знаками после запятой. Запись >6.2f означает, что число будет выровнено по правому краю и занимать не менее шести символов, включая дробную часть. Списки и кортежи — это наиболее часто используемые структуры данных в Python. Множества встречаются реже, но знать о них полезно, так как они упрощают работу с уникальными элементами. Наконец, четвертая важная структура данных в Python — это словарь (dictionary), с которым мы познакомимся в следующей главе.
В этой главе вы узнаете: » Как создавать словари » Как выполнять перебор элементов словаря » Как копировать словари » Как удалять элементы из словаря » Как использовать словари с несколькими ключами Гл а в а 4 Работа с большими данными с помощью словарей С ловари (в некоторых языках программирования их называют ассоциативными массивами) во многом похожи на списки, о которых мы говорили в главе 3. Главное отличие заключается в том, что доступ к элементам словаря осуществляется не по их позиции, а по ключу. Ключ — это уникальный идентификатор элемента, который чаще всего бывает строкой или числом. Важно, чтобы ключ был уникальным: два элемента с одинаковым ключом существовать не могут. Почему это так важно? Представьте себе телефонные номера, адреса электронной почты или номера социального страхования. Если бы у двух людей оказался один и тот же номер телефона, звонок по этому номеру доставался бы обоим. Если бы у нескольких человек был одинаковый адрес электронной почты, все они получали бы одни и те же письма. А если бы у разных людей совпадал номер социального страхования, то долги одного из них могли бы автоматически «повиснуть» на всех. В этой главе мы подробно разберем, что такое словари в Python, как их создавать и как использовать в ваших программах. ГЛАВА 4 Работа с большими данными с помощью словарей 193
Понимание структуры словарей данных Как мы уже отмечали, словарь во многом напоминает список, но с одним важным отличием: у каждого элемента словаря есть уникальный ключ. С этим ключом связывается значение, которое может быть чем угодно — числом, строкой, списком, кортежем и даже другим словарем. Удобно представлять словарь в виде таблицы: • • первый столбец хранит ключи, которые уникальны для каждого элемента; второй столбец хранит значения, связанные с этими ключами (они могут быть как уникальными, так и повторяться) . На рисунке 4.1 показан простой пример: в левой колонке таблицы находятся ключи, а в правой — значения, которые этим ключам соответствуют. Рисунок 4.1. Словарь с ключами в левом столбце и значениями в правом В левой колонке примера показаны сокращенные варианты имен сотрудников. Подобные сокращения часто используются компаниями при создании учетных записей пользователей или адресов электронной почты. Значение, связанное с каждым ключом, необязательно должно быть строкой или числом. В Python значением может быть любой тип данных — например, список или кортеж. На рисунке 4.2 приведен словарь, где для каждого ключа хранится список. Этот список содержит четыре элемента: имя сотрудника, год (например, год приема на работу или год рождения), число (например, количество иждивенцев для налоговой отчетности) и логическое значение True или False (которое может указывать, например, наличие корпоративного телефона). 194 РАЗДЕЛ 2 Понимание базовых конструкций Python
Не так важно, что именно означает каждый элемент в списке — главное здесь в том, что для каждого ключа хранится набор связанных данных, упакованный в список. Рисунок 4.2. Словарь со списками в качестве значений Значение в словаре может быть не только числом, строкой или списком, но и целым словарем. В этом случае у значения появляются собственные ключи, каждый из которых описывает отдельный элемент данных. Другими словами, вместо того чтобы хранить все данные сотрудника в одной строке списка, вы можете создать для каждого сотрудника небольшой словарь с ключами, которые явно указывают, что именно хранится в данных. Такой подход делает структуру более читаемой и удобной для работы. Например, для пользователя htanaka можно создать словарь, в котором каждому ключу соответствует своя единица информации. Такой словарь показан на рисунке 4.3. Рисунок 4.3. Словарь для одного сотрудника Словарь для другого сотрудника может использовать те же самые ключи — full_name, year_hired, dependents и has_company_cell. Однако значения у этих ключей будут уже другими, так как они относятся к другому человеку. Таким образом, структура словаря остается одинаковой, а данные внутри — уникальными для каждого сотрудника. На рисунке 4.4 показан пример такой записи. ГЛАВА 4 Работа с большими данными с помощью словарей 195
Рисунок 4.4. Словарь для другого сотрудника Использование нескольких ключей для каждой записи словаря — это нормальная практика в Python. Такой подход делает работу с данными гораздо удобнее, ведь вы можете напрямую обращаться к нужному элементу по имени ключа, заключая его в квадратные скобки. Например: ppatel[΄full_name΄] = 'Priya Patel' ppatel[΄year_hired΄] = 2015 ppatel[΄dependents΄] = 1 ppatel[΄has_company_cell΄] = True Здесь каждое поле наглядно показывает, к каким данным вы обращаетесь. Сравните это с доступом к элементам по позиции, как в списке: ppatel[0] ppatel[1] ppatel[2] ppatel[3] = = = = 'Priya Patel' 2015 1 True Хотя оба варианта работают, использование ключей делает код гораздо понятнее для человека. Создание словаря Чтобы создать словарь, используйте следующий базовый синтаксис: name = {key:value, key:value, key:value, key:value, ...} Здесь name — это имя словаря, которое вы придумываете сами. Обычно оно отражает то, к чему относятся пары ключ:значение. Пары записываются в фигурных скобках. Ключ чаще всего представляет собой строку в кавычках, но также может быть числом или даже кортежем. После двоеточия (:) указывается значение, которым может быть любое допустимое значение в Python: число, строка, список или другой объект. 196 РАЗДЕЛ 2 Понимание базовых конструкций Python
Многоточие (...) в примере просто показывает, что таких пар может быть сколько угодно. Главное — не забывать разделять их запятыми. Чтобы сделать код более удобным для чтения, программисты часто записывают каждую пару ключ:значение на новой строке. При этом сам синтаксис словаря не меняется. Единственное отличие в том, что после каждой запятой добавляется перенос строки: name = { key:value, key:value, key:value, key:value, ... } Чтобы закрепить материал, попробуйте создать свой словарь на практике. Для этого откройте блокнот Jupyter, файл с расширением .py или командную строку Python и введите следующий код. В примере мы создаем словарь с именем people, в котором хранится несколько пар ключ:значение. Ключи и значения записаны как строки, поэтому они заключены в кавычки. Каждая пара отделяется двоеточием, а между парами ставится запятая. Обратите внимание: даже одна пропущенная кавычка, запятая, двоеточие или фигурная скобка может привести к ошибке и программа не запустится. people = { 'htanaka': 'Haru Tanaka', 'ppatel': 'Priya Patel', 'bagarcia': 'Benjamin Alberto Garcia', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi', 'hajackson': 'Hanna Jackson', 'papatel': 'Pratyush Aarav Patel', 'hrjackson': 'Henry Jackson' } СОВЕТ Если вы вдруг забыли, как именно создается словарь в Python, не переживайте. Всегда можно обратиться за помощью к инструментам вроде Copilot, ChatGPT или другим генеративным ИИ. Достаточно сформулировать запрос вроде: «Напишите словарь Python с 10 числовыми ключами и 10 строковыми значениями». В ответ вы получите корректный шаблон кода со всеми нужными скобками, запятыми и двоеточиями, который можно использовать как основу для своей программы. ГЛАВА 4 Работа с большими данными с помощью словарей 197
Доступ к данным словаря После того как вы добавили данные в словарь, с ними можно работать разными способами. Если вы просто выполните print(people) — то есть вызовете функцию print() с именем словаря в скобках, — на экране появится весь словарь целиком: print(people) {'htanaka': 'Haru Tanaka', 'ppatel': 'Priya Patel', 'bagarcia': 'Benjamin Alberto Garcia', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi', 'hajackson': 'Hanna Jackson', 'papatel': 'Pratyush Aarav Patel', 'hrjackson': 'Henry Jackson'} Однако на практике вам редко нужно сразу все содержимое словаря. Чаще всего требуется получить конкретное значение по ключу. Для этого используется следующий синтаксис: dictionaryname[key] Здесь dictionaryname — это имя словаря, а key — ключ, по которому вы хотите получить значение. Например, чтобы вывести значение по ключу 'zmin', можно написать: print(people['zmin']) Можно сказать, что строка print(people['zmin']) означает «напечатать значение словаря people по индексу zmin». Здесь слово индекс обозначает конкретный ключ. При выполнении программы Python возвращает значение, соответствующее этому ключу, то есть полное имя для zmin. На рисунке 4.5 показан результат выполнения этого кода в ячейке Jupyter Notebook. Обратите внимание: в примере zmin заключен в кавычки, потому что это строка. Но при желании можно использовать переменную, в которой хранится эта строка. В этом случае кавычки не нужны. Например: person = 'zmin' print(people[person]) Здесь сначала создается переменная person, которая содержит строку 'zmin'. Затем Python использует эту переменную как ключ, чтобы получить значение из словаря. 198 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 4.5. Вывод значения для ключа zmin в словаре people А теперь вопрос: как вы думаете, что произойдет при выполнении этого кода? person = 'hrjackson' print(people[person]) На экран будет выведено Henry Jackson, потому что именно это значение соответствует ключу hrjackson. А теперь посмотрим на другой пример: person = 'schmeedledorp' print(people[person]) В этом случае программа завершится ошибкой. Причина проста: в словаре people нет ключа со значением 'schmeedledorp'. На рисунке 4.6 показан результат такой попытки — Python сообщает, что указанного ключа не существует. ГЛАВА 4 Работа с большими данными с помощью словарей 199
Рисунок 4.6. Python сообщает, что schmeedledorp не существует Определение длины словаря Количество элементов в словаре называется его длиной. Точно так же, как и для списков, для получения длины словаря используется функция len(). Синтаксис очень простой: len(dictionaryname) Где dictionaryname — это имя словаря, длину которого вы хотите узнать. Например, рассмотрим такой код: people = { 'htanaka': 'Haru Tanaka', 'ppatel': 'Priya Patel', 'bagarcia': 'Benjamin Alberto Garcia', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi', 'hajackson': 'Hanna Jackson', 'papatel': 'Pratyush Aarav Patel', 'hrjackson': 'Henry Jackson' } # Count the number of key:value pairs and put in a variable. 200 РАЗДЕЛ 2 Понимание базовых конструкций Python
howmany = len(people) # Show how many. print(howmany) Когда вы выполните оператор print, результатом будет число 8 — именно столько пар ключ:значение содержится в словаре, и это значение хранится в переменной howmany. СОВЕТ Как нетрудно догадаться, пустой словарь, в котором нет ни одного элемента, имеет длину 0. Создать такой словарь очень просто: достаточно присвоить имени словаря пустые фигурные скобки: my_dict = {}. Проверка наличия ключа в словаре Иногда перед обращением к значению полезно убедиться, что нужный ключ действительно есть в словаре. Для этого в Python используется ключевое слово in. Оно возвращает True, если ключ существует, и False, если такого ключа нет. На рисунке 4.7 приведен простой пример с двумя операторами print(). Первый проверяет, присутствует ли в словаре значение hajackson. Второй проверяет, присутствует ли в словаре значение schmeedledorp. Как видно, первый оператор print() выводит True, поскольку hajackson присутствует в словаре. Второй оператор print() выводит False, поскольку schmeedledorp в словаре отсутствует. Рисунок 4.7. Проверка существования ключа в словаре ГЛАВА 4 Работа с большими данными с помощью словарей 201
Получение данных из словаря с помощью get() Если попытаться обратиться к несуществующему ключу в словаре напрямую, программа завершится с ошибкой. Это неудобно и выглядит грубо. Более элегантный способ обработки таких ситуаций — использовать метод .get(). Базовый синтаксис: dictionaryname.get(key) Замените dictionaryname на имя словаря, в котором выполняется поиск, а key — на ключ, значение которого вы хотите получить. Обратите внимание: метод .get() использует круглые скобки, а не квадратные. Например, если в словаре people вы ищете человека с ключом © bagarcia© , код будет выглядеть так: # Look for a person. person = 'bagarcia' print(people.get(person)) Результат будет точно таким же, как если бы вы использовали квадратные скобки. Разница проявляется, когда вы пытаетесь получить значение по несуществующему ключу. В этом случае программа не выдает ошибку и не прерывается. Вместо этого метод .get() возвращает специальное значение None, показывая, что в словаре people нет человека с ключом schmeedledorp© . Именно это вы видите на рисунке 4.8. © У метода .get() есть еще одно удобное свойство: вы можете передать ему второй аргумент. Это значение будет возвращено в том случае, если ключ в словаре не найден. Например, в приведенном ниже коде мы снова ищем ключ © schmeedledorp© . Но теперь, вместо стандартного None, метод вернет более наглядное сообщение: print(people.get('schmeedledorp', 'Unbeknownst to this dictionary')) В результате программа выведет строку Unbeknownst to this dictionary. Такой подход позволяет вам заранее задать понятное сообщение или даже значение по умолчанию, что делает работу программы более предсказуемой и удобной для пользователя. 202 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 4.8. Элегантный способ Python сказать, что schmeedledorp не существует Изменение значения ключа Словари в Python относятся к изменяемым типам данных. Это значит, что вы можете изменять их содержимое прямо в процессе работы программы (хотя «заставить словарь замолчать» действительно не получится). Синтаксис для изменения значения очень простой: dictionaryname[key] = newvalue Замените dictionaryname именем словаря, key — ключом, идентифицирующим элемент, а newvalue — новым желаемым значением. Предположим, что Ханна Джексон вышла замуж и сменила фамилию на Джексон-Смит. Ключ для ее записи остается прежним (hajackson), но значение нужно изменить. Сделать это можно так: people['hajackson'] = "Hanna Jackson-Smith" print(people['hajackson']) После выполнения кода вывод покажет обновленное значение: Hanna Jackson-Smith. Таким образом, вы можете легко обновлять данные в словаре, не создавая новый объект (рисунок 4.9). ГЛАВА 4 Работа с большими данными с помощью словарей 203
Рисунок 4.9. Изменение значения, связанного с ключом в словаре ПРИМЕЧАНИЕ В реальных приложениях данные словаря обычно не ограничиваются только оперативной памятью. Чаще всего их сохраняют во внешнем файле, чтобы изменения оставались даже после завершения работы программы. Для этого потребуется дополнительный код, который будет отвечать за запись и чтение данных из файла. Однако, прежде чем углубляться в такие задачи, важно освоить базовые принципы работы со словарями. Поэтому пока мы продолжим изучение самой структуры словаря и основных приемов ее использования. Добавление или изменение данных словаря В Python для добавления нового элемента в словарь или изменения значения уже существующего ключа используется метод update(). Общий синтаксис выглядит так: dictionaryname.update(key, value) • • • вместо dictionaryname указывается название словаря, с которым вы работаете; key — это имя элемента, который вы хотите добавить или изменить; value — это новое значение для указанного ключа . Если ключа еще нет в словаре, он будет добавлен вместе со значением. Если ключ уже существует, то его значение обновится новым. Рассмотрим на примере. Создадим словарь, в котором будут храниться имена людей: # Создаем словарь данных с именами людей people = { 204 РАЗДЕЛ 2 Понимание базовых конструкций Python
'papatel': 'Pratyush Aarav Patel', 'hrjackson': 'Henry Jackson' } Теперь изменим значение для существующего ключа hrjackson и добавим новый элемент в словарь: # Изменяем значение ключа hrjackson people.update({'hrjackson' : 'Henrietta Jackson'}) # Добавляем новый элемент в словарь people.update({'wwiggins' : 'Wanda Wiggins'}) Первая строка с методом update() изменяет значение по существующему ключу hrjackson: people.update({'hrjackson' : 'Henrietta Jackson'}) Так как ключ © hrjackson©уже есть в словаре (он содержал значение 'Henry Jackson'), метод просто заменяет старое значение новым — теhrjackson© соответствует 'Henrietta Jackson© . перь © Вторая строка с методом update() выглядит так: people.update({'wwiggins' : 'Wanda Wiggins'}) Здесь в словаре нет ключа © wwiggins© , поэтому изменить значение для него невозможно. В этом случае метод добавляет новую пару ключ:значение. В результате в словаре появляется новый элемент: ключ © wwiggins© Wanda Wiggins© . и значение © При использовании метода update() программисту не нужно явно указывать, следует ли изменить значение или добавить новый элемент. Этот выбор делается автоматически по простому правилу: » » Если ключ уже есть в словаре • • Значение этого ключа обновляется новым . Новый элемент не создается, потому что в словаре не может быть двух одинаковых ключей . Если ключа в словаре еще нет • • Добавляется новая пара ключ:значение . Это единственно возможное действие, так как изменять пока нечего . Таким образом, процесс всегда один и тот же: метод update() проверяет наличие ключа и либо обновляет значение, либо добавляет новый ГЛАВА 4 Работа с большими данными с помощью словарей 205
элемент. Все это происходит автоматически, а программисту остается лишь указать, какой ключ и какое значение использовать. После выполнения всех команд в примере словарь содержит три элемента: » » » papatel; hrjackson (с уже измененным значением); wwiggins. Чтобы убедиться в этом, можно добавить следующий код в конец программы: # Показать, что сейчас находится в словаре for person in people.keys(): print(person + " = " + people[person]) Этот цикл проходит по всем ключам словаря и для каждого ключа выводит его вместе со значением. После запуска программа напечатает: papatel = Pratyush Aarav Patel hrjackson = Henrietta Jackson wwiggins = Wanda Wiggins Таким образом, мы видим полное содержимое словаря после всех изменений и добавлений. Обновить значение для ключа можно не только методом update(), но и с помощью обычного оператора присваивания =. htanaka['dependents'] = 1 В этом случае для ключа © dependents©устанавливается значение 1. Если такого ключа в словаре htanaka еще не было, он будет создан. Если ключ уже существует, его значение просто заменится. Перебор элементов словаря Как мы уже упоминали, словарь можно перебирать в цикле for, так же как списки или кортежи. Однако со словарями есть дополнительные возможности. Если в цикле указать только имя словаря, то перебор будет происходить по его ключам: 206 РАЗДЕЛ 2 Понимание базовых конструкций Python
for person in people: print(person) htanaka ppatel bagarcia zmin afarooqi hajackson papatel hrjackson Чтобы получить значения, а не ключи, можно оставить цикл тем же, но обращаться к элементу словаря по ключу: for person in people: print(people[person]) В этом случае программа выводит все значения, соответствующие ключам. Например: Haru Tanaka Priya Patel Benjamin Alberto Garcia Zhang Min Ayesha Farooqi Hanna Jackson Pratyush Aarav Patel Henry Jackson Таким образом, если в цикле использовать просто имя словаря, мы перебираем ключи, а чтобы вывести значения, нужно указывать dictionaryname[key]. Чтобы получить сразу все значения словаря без обращения по ключам, можно использовать метод .values(). Пример: for person in people.values(): print(person) В этом случае переменная person последовательно принимает все значения словаря people. На экран будут выведены те же имена, что и в предыдущем примере: Haru Tanaka ГЛАВА 4 Работа с большими данными с помощью словарей 207
Priya Patel Benjamin Alberto Garcia Zhang Min Ayesha Farooqi Hanna Jackson Pratyush Aarav Patel Henry Jackson Наконец, вы можете перебирать не только ключи или значения, но и сразу оба элемента словаря. Для этого после имени словаря в цикле for используется метод .items(). В этом случае после for необходимо указать две переменные: одна будет ссылаться на ключ, а другая — на его значение. Чтобы отобразить обе переменные во время прохода по словарю, их имена нужно использовать внутри скобок функции print(). Например, в цикле, показанном на рисунке 4.10, используются две переменные — key и value (хотя можно выбрать любые другие имена, например x и y) для перебора элементов people .items(). При каждом проходе цикла оператор print выводит одновременно и ключ, и значение. Чтобы разделить их, в вызове функции print() добавлен знак равенства (в кавычках). В результате выполнения программы формируется список, в котором после каждого ключа отображается символ = и соответствующее значение. Рисунок 4.10. Перебор словаря с помощью items() и двух имен переменных 208 РАЗДЕЛ 2 Понимание базовых конструкций Python
Методы словарей Если вы внимательно следили за материалом от главы к главе, то могли заметить: многие методы, доступные для словарей, похожи на методы, которые мы использовали для списков, кортежей и множеств. Поэтому сейчас самое время свести их воедино и перечислить в таблице 4.1 все основные методы, которые можно применять к словарям. Некоторые из этих методов вы уже встречали в примерах этой главы, а с остальными мы познакомимся чуть позже. Таблица 4.1. Методы словарей Метод Что он делает clear() Очищает словарь, удаляя все пары ключ: значение copy() Создает и возвращает копию словаря fromkeys() Создает новый словарь с указанными ключами и одинаковым значением get() Возвращает значение по ключу, или None, если ключ не найден items() Возвращает представление всех элементов словаря в виде пар (ключ, значение) keys() Возвращает представление всех ключей словаря pop() Возвращает представление всех значений словаря popitem() Удаляет элемент по указанному ключу и возвращает его значение setdefault() Удаляет и возвращает последнюю добавленную пару ключ:значение update() Возвращает значение по указанному ключу . Если ключа нет — добавляет его с заданным значением и возвращает это значение values() Обновляет значение существующего ключа или добавляет новую пару ключ:значение ГЛАВА 4 Работа с большими данными с помощью словарей 209
Копирование словаря Иногда бывает нужно создать копию словаря, чтобы работать с ней отдельно от исходного. Для этого у словарей есть метод .copy(). Общий синтаксис выглядит так: newdictionaryname = dictionaryname.copy() newdictionaryname — имя нового словаря, которое вы придумываете сами. • dictionaryname — имя существующего словаря, который вы хотите скопировать. На рисунке 4.11 показан простой пример. Сначала создается словарь с именем people. Затем создается новый словарь peeps2, который является копией people. Результат показывает, что оба словаря содержат одинаковые пары ключ:значение. Рисунок 4.11. Копирование словаря Удаление элементов словаря В Python существует несколько способов удалить данные из словаря. Один из самых простых — использовать ключевое слово del (сокращение от delete — «удалить»). С помощью del можно удалить элемент по его ключу. 210 РАЗДЕЛ 2 Понимание базовых конструкций Python
Общий синтаксис: del dictionaryname[key] • • dictionaryname — название словаря, из которого вы хотите удалить элемент; key — ключ того элемента, который нужно удалить . Рассмотрим следующий код. Сначала создается словарь people с тремя элементами. Затем с помощью инструкции del people["zmin"] удаляzmin© : ется элемент с ключом © # Определяем словарь с именами people = { 'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi', } # Показываем исходный словарь print(people) # Удаляем элемент с ключом zmin del people["zmin"] # Показываем содержимое словаря после удаления print(people) Как видно, после выполнения команды del элемент с ключом 'zmin' полностью удален из словаря. {'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi'} {'htanaka': 'Haru Tanaka', 'afarooqi': 'Ayesha Farooqi'} Очень важно помнить: если после ключевого слова del указать только имя словаря без конкретного ключа, будет удален весь словарь целиком. В этом случае удаляется не только содержимое, но и сам объект с его именем. Если в предыдущем примере вместо del people["zmin"] было del people то при следующей попытке обратиться к словарю с помощью print(people) возникнет ошибка, так как объект people больше не существует. {'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi'} ---------------------------------------------------------NameError Traceback (most recent call last) ГЛАВА 4 Работа с большими данными с помощью словарей 211
<ipython-input-32-24401f5e8cf0> in <module>() 13 14 # Show what's in people now. ---> 15 print(people) NameError: name 'people' is not defined Если нужно удалить все пары ключ:значение из словаря, но при этом сохранить сам словарь (его имя и сам объект), используйте метод .clear(). dictionaryname.clear() Рассмотрим следующий код. Сначала создается словарь с именами людей и тремя парами ключ:значение. Затем выводится его содержимое, чтобы мы могли увидеть, что в нем хранится. После этого вызывается метод people.clear(), который удаляет все элементы словаря: # Определяем словарь с именами людей people = { 'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi', } # Показываем исходный словарь print(people) # Удаляем все данные из словаря people.clear() # Показываем, что осталось после удаления print(people) Сначала Python выводит три пары ключ:значение, которые были добавлены в словарь. После вызова метода .clear() словарь остается существовать, но становится пустым. Python показывает это в виде {}, что обозначает «пустой словарь». {'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi'} {} Метод .pop() предоставляет еще один способ удаления данных из словаря. Его особенность в том, что он выполняет сразу две задачи: 212 » Если результат работы метода сохранить в переменную, то эта переменная получит значение удаленного ключа . » Независимо от того, сохраняете ли вы результат или нет, указанный ключ вместе с его значением будет удален из словаря . РАЗДЕЛ 2 Понимание базовых конструкций Python
На рисунке 4.12 показан пример работы метода .pop(). Сначала программа выводит весь словарь. Затем выполняется команда: adios = people.pop("zmin"). Эта строка удаляет элемент с ключом 'zmin' из словаря и одновременно сохраняет его значение в переменную adios. После этого мы выводим содержимое переменной adios и видим, что в ней хранится строка Zhang Min, которая ранее была значением для ключа "zmin". Повторный вывод всего словаря подтверждает, что ключ 'zmin' и его значение были удалены. Таким образом, метод .pop() одновременно возвращает значение удаленного элемента и удаляет сам элемент из словаря. Рисунок 4.12. Извлечение элемента из словаря Словари также поддерживают метод .popitem(), который работает немного иначе, чем .pop(). Общий синтаксис: dictionaryname = popitem() Особенности работы • • В ранних версиях Python метод popitem() удалял случайный элемент словаря . Это могло быть полезно, например, в играх, где нужно убирать элементы произвольно . Начиная с версии Python 3 .7 (той, что используется в этой книге), метод popitem() всегда удаляет последнюю добавленную пару ключ:значение . Метод .popitem() работает не так, как .pop(). Если результат метода .popitem() сохранить в переменную, то в ней окажется не только значение элемента, а сразу и ключ, и значение в виде кортежа. После этого словарь уже не будет содержать эту пару ключ:значение. ГЛАВА 4 Работа с большими данными с помощью словарей 213
Если в примере с рисунка 4.12 заменить строку adios = people. = people.popitem() то результат работы программы будет таким: pop("zmin") на adios {'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min', 'afarooqi': 'Ayesha Farooqi'} ('afarooqi', 'Ayesha Farooqi') {'htanaka': 'Haru Tanaka', 'zmin': 'Zhang Min'} Развлекаемся с многоключевыми словарями До этого момента мы рассматривали словари, где каждому ключу соответствовало только одно значение. Например, сокращение имени человека связывалось с его полным именем. Однако в реальных задачах часто бывает нужно хранить несколько характеристик для одного объекта. Предположим, что знать только полное имя сотрудника недостаточно. Мы также хотим хранить: • • • год приема на работу; дату рождения; и информацию о том, выдан ли этому сотруднику корпоративный ноутбук . В этом случае словарь для одного человека может выглядеть так: employee = { 'name': 'Haru Tanaka', 'year_hired': 2005, 'dob': '11/23/1987', 'has_laptop': False } Здесь ключи ('name', 'year_hired', 'dob', 'has_laptop') описывают разные свойства одного и того же объекта — сотрудника. Словари удобно использовать, когда нужно хранить несколько свойств одного объекта. Например, пусть вы создаете словарь товаров в магазине. Для каждого товара требуется указать: • 214 его название; РАЗДЕЛ 2 Понимание базовых конструкций Python
• • • цену за единицу; облагается ли он налогом; и количество единиц на складе . Выглядеть словарь для товара будет так: product = { 'name': 'Солнцезащитные очки Ray-Ban Wayfarer', 'unit_price': 112.99, 'taxable': True, 'in_stock': 10 } В этом словаре ключи ('name', 'unit_price', 'taxable', 'in_stock') описывают разные характеристики одного товара. Значения указывают его конкретные параметры: название модели, цену, налоговый статус и количество на складе. Обратите внимание, что во всех примерах ключи словаря заключены в кавычки. В коде можно использовать как одинарные (©© ), так и двойные (" " кавычки) — разницы нет. • • • Строки (например, 'Haru Tanaka', '11/23/1987', 'Солнцезащитные очки Ray-Ban Wayfarer') должны быть в кавычках . Если этого не сделать, Python воспримет запись как набор чисел или выражение . Например, без кавычек дата рождения 11/23/1987 будет интерпретирована как деление 11 ÷ 23 ÷ 1987, что, очевидно, бессмысленно . Логические значения записываются как True и False (с заглавной буквы), без кавычек . Числа (например, 2005, 10, 112 .99) также пишутся без кавычек . Значением в словаре может быть не только одиночное значение, но и целая коллекция — список, кортеж или множество. Это полезно, если нужно хранить несколько вариантов для одного свойства. Предположим, солнцезащитные очки выпускаются в двух цветах: черном и «черепаховом». В словарь можно добавить ключ 'models' и указать список этих моделей: product = { 'name': 'Солнцезащитные очки Ray-Ban Wayfarer', 'unit_price': 112.99, 'taxable': True, 'in_stock': 10, 'models': ['Черные', 'Черепаховые'] } ГЛАВА 4 Работа с большими данными с помощью словарей 215
Чтобы вывести значение, связанное с определенным ключом, используется простой синтаксис: dictionaryname[key]. Используя словарь из предыдущего примера, можно отобразить все его данные так: print(product['name']) print(product['unit_price']) print(product['taxable']) print(product['in_stock']) print(product['models']) В итоге результат будет таким: Солнцезащитные очки Ray-Ban Wayfarer 112.99 True 10 ['Черные', 'Черепаховые'] Чтобы сделать вывод словаря удобнее для чтения, можно: » » » Добавить поясняющий текст к каждому оператору print() . Использовать форматированные строки, чтобы красиво отобразить значения (например, цену с символом $ и двумя знаками после запятой) . Организовать цикл по списку, чтобы каждая модель выводилась на отдельной строке с отступом . product = { 'name' : 'Солнцезащитные очки Ray-Ban Wayfarer ', 'unit_price' : 112.99, 'taxable' : True, 'in_stock' : 10, 'models' : ['Черные', 'Черепаховые'] } print('Название: ', product['name']) print('Цена: ', f"${product['unit_price']:.2f}") print('Облагается налогом: ', product['taxable']) print('В наличии:', product['in_stock']) print('Модели:') for model in product['models']: print(" " * 10 + model) Результат выполнения: Название: Солнцезащитные очки Ray-Ban Wayfarer Цена: $112.99 216 РАЗДЕЛ 2 Понимание базовых конструкций Python
Облагается налогом: True В наличии: 10 Модели: Черные Черепаховые Выражение " " * 10 означает «напечатать строку, состоящую из одного пробела, десять раз подряд». • • • Если между кавычками только пробел, получится отступ в 10 пробелов . Если там будет, например, "-", то результатом будет строка из дефисов (–————-) . Если кавычки пустые (""), то умножение даст пустую строку, то есть никакого вывода не будет . «Загадочные» методы fromkeys и setdefault В Python словари поддерживают два метода — fromkeys() и setdefault(), которые часто вызывают недоумение у начинающих программистов. Действительно, они используются не так часто, как, например, update() или pop(). Но давайте разберемся, что они делают и когда могут пригодиться. Метод fromkeys() создает новый словарь, в котором заранее задается набор ключей. Общий синтаксис: newdictionaryname = dict.fromkeys(iterable[,value]) • • • new_dictionary — имя нового словаря . Вы можете выбрать любое название: необязательно использовать общее имя вроде product . Это может быть, например, уникальный идентификатор товара — UPC (универсальный код продукта) или SKU (складской учетный номер); iterable — любой итерируемый объект, то есть структура данных, элементы которой можно перебирать в цикле . Чаще всего здесь используют список; value — необязательный параметр . Если его не указать, каждому ключу в новом словаре присваивается значение None . Это означает, что у ключа пока нет конкретного значения . ГЛАВА 4 Работа с большими данными с помощью словарей 217
В следующем примере создается словарь с именем DWC001 (это SKU — уникальный номер товара в инвентаре). Ему передается список имен ключей, заключенных в квадратные скобки и разделенных запятыми. Такой список Python воспринимает как корректный список (list). Значения для ключей мы не указали, поэтому по умолчанию каждому из них присваивается None: DWC001 = dict.fromkeys(['name', 'unit_price', 'taxable', 'in_ stock', 'models']) print(DWC001) {'name': None, 'unit_price': None, 'taxable': None, 'in_stock': None, 'models': None} Теперь предположим, что вы не хотите вручную вводить все имена ключей. Вместо этого можно использовать те же ключи, которые уже определены в другом словаре. Для этого применяется метод .keys(), который возвращает итерируемый список имен ключей. Например, в следующем коде создается словарь для товаров, содержащий только набор ключей без значений. Затем выполняется команда: DWC001 = dict.fromkeys(product.keys()) print(DWC001) В результате создается новый словарь DWC001, имеющий те же ключи, что и предыдущий. Так как в методе dict.fromkeys(product.keys()) значения не указаны, каждому ключу автоматически присваивается значение None. # Создаем общий словарь для товаров product = { 'name': '', 'unit_price': 0, 'taxable': True, 'in_stock': 0, 'models': [] } # Создаем словарь с именем DWC001, имеющий те же ключи, что и словарь товаров DWC001 = dict.fromkeys(product.keys()) # Показываем содержимое нового словаря print(DWC001) 218 РАЗДЕЛ 2 Понимание базовых конструкций Python
Финальный оператор print() выводит содержимое нового словаря. Видно, что он содержит все те же ключи, что и словарь product, но каждому значению присвоено {'name': None, 'unit_price': None, 'taxable': None, 'in_stock': None, 'models': None} Метод .setdefault() позволяет добавить в словарь новый ключ с предопределенным значением. Важно отметить, что .setdefault() не изменяет существующие значения. Даже если значение ключа равно None, метод не заменит его новым, а просто вернет текущее. Такой подход может быть полезен, если вы уже определили словари, а потом захотели добавить в них новую пару свойство:значение, но только туда, где этого свойства еще нет. На рисунке 4.13 показан пример, где мы создали словарь DWC001, используя те же ключи, что и в словаре товаров. После этого вызываются методы: DWC001.setdefault('taxable', True) DWC001.setdefault('reorder_point', 10) • • В первой строке метод setdefault('taxable', True) проверяет, есть ли в словаре ключ с именем 'taxable'. Поскольку такой ключ уже существует, его значение не изменяется (оно остается тем, каким было) . Во второй строке метод setdefault('reorder_point', 10) добавляет новый ключ 'reorder_point' со значением 10, так как такого ключа еще не было в словаре . Рисунок 4.13. Эксперименты с fromkeys и setdefault ГЛАВА 4 Работа с большими данными с помощью словарей 219
Как видно из вывода кода, после выполнения операций fromkeys и setdefault новый словарь имеет те же ключи, что и словарь product, плюс новую пару ключ:значение — reorder_point: 10, которая была добавлена вторым вызовом setdefault. Однако ключ taxable в этом выводе по-прежнему имеет значение None, потому что метод setdefault не изменяет значение уже существующего ключа. Он добавляет новый ключ со значением по умолчанию только в том случае, если такого ключа в словаре еще нет. Что же делать, если вы действительно хотите установить значение по умолчанию для taxable равным True вместо None? Простое решение — использовать стандартный синтаксис присваивания: dictionaryname[key] = newvalue. В этом случае значение существующего ключа taxable изменяется с None на True. Второй вывод на рисунке 4.13 подтверждает, что изменение сработало. Вложенные словари К этому моменту вы, наверное, уже заметили, что в программе может понадобиться сразу несколько словарей, каждый со своим уникальным именем. Но возникает вопрос: как удобно работать со всей этой совокупностью словарей, не обращаясь к каждому по отдельности? Ответ заключается в том, чтобы использовать вложенные словари. Идея проста: каждый словарь становится значением в другом, «содержaщем» словаре. В этом случае: ключом будет уникальный идентификатор (например, универсальный код продукта — UPC или складской номер — SKU), значением станет словарь, в котором уже хранятся все пары ключ: значение, относящиеся к этому элементу. Общий синтаксис: containingdictionaryname = { key: {dictionary}, key: {dictionary}, key: {dictionary}, ... } Это базовый синтаксис для словаря словарей. Чтобы использовать его в своем коде, замените все выделенные курсивом имена-заполнители на конкретные значения: 220 РАЗДЕЛ 2 Понимание базовых конструкций Python
» » » containingdictionaryname — имя словаря в целом. Оно может быть любым, но должно отражать его назначение, например inventory (инвентарь) или employees (сотрудники); key — уникальный идентификатор каждого элемента. В качестве ключа можно использовать универсальный код продукта (UPC), складской номер (SKU), имя пользователя или даже последовательный номер. Главное условие — ключи не должны повторяться; {dictionary} — это набор пар ключ:значение для одного элемента. Они заключаются в фигурные скобки. Если таких словарей несколько, после закрывающей скобки ставится запятая. На рисунке 4.14 показан пример, где создается словарь товаров (во множественном числе, так как он содержит несколько единиц). Словарь товаров включает четыре отдельных элемента. У каждого из них есть свой уникальный ключ — например, RB0011, DWC0317 и т.д. Это внутренние складские номера, которые компания использует для управления запасами. Значением каждого такого ключа является словарь, содержащий пары ключ: значение, описывающие сам товар. Внутри словаря используются такие ключи, как: Рисунок 4.14. Множество словарей товаров, которые находятся в большем словаре с таким же значением • • • name — название товара; price — цена за единицу; models — список доступных моделей или вариантов товара . Сложный синтаксис со множеством фигурных скобок, запятых и двоеточий иногда затрудняет восприятие словаря и делает его ввод менее удобным. Если выйти за пределы Python, те же самые данные вполне можно хранить в виде таблицы — например, в текстовом файле, электронной таблице или базе данных. В этом случае ключи словаря становятся ГЛАВА 4 Работа с большими данными с помощью словарей 221
заголовками столбцов, а каждая строка таблицы соответствует одному элементу словаря. Так для словаря товаров данные можно изобразить в виде таблицы с названием «Товары», как показано в таблице 4.2. Таблица 4.2. Список товаров ID (ключ) Название Цена Модели RB00111 Солнцезащитные очки Ray-Ban 112 .98 Черные, черепаховые DWC0317 Дрон с камерой 72 .95 Белый, черный MTS0540 Футболка 2 .95 Маленькая, средняя и большая ECD2989 Умная колонка Echo Dot 29 .99 - Используя комбинацию форматированных строк и циклов, можно сделать так, чтобы Python отображал данные из словаря в аккуратном табличном виде. На рисунке 4.15 показан пример подобного кода в Jupyter Notebook. Под самим кодом приведен и результат его выполнения, где данные словаря представлены в виде таблицы. Рисунок 4.15. Вывод словарей данных, отформатированных в виде строк и столбцов 222 РАЗДЕЛ 2 Понимание базовых конструкций Python
Создание словаря с помощью включений Иногда данные для словаря хранятся в двух отдельных списках — один содержит ключи, другой — значения. Чтобы объединить их в единый словарь, можно использовать словарное включение (dictionary comprehension) вместе с функцией zip(). Функция zip() попарно соединяет элементы из двух списков так, чтобы каждому ключу соответствовало свое значение. Базовый синтаксис выглядит так: data_dict = {key: value for key, value in zip(keys_list, values_ list)} • • • • data_dict — любое имя, которое вы хотите дать новому словарю; keys_list — имя списка, содержащего ключи; values_list — имя списка, содержащего значения; zip(keys_list, values_list) создает пары (ключ: значение), которые затем используются для построения словаря . В следующем примере для простоты имена списков заданы как keys и values: # Простые списки values = ["Первый", "Второй", "Третий", "Четвертый", "Пятый"] keys = [1, 2, 3, 4, 5] # Используем списковое включение для создания словаря из списков data_dict = {key: value for (key, value) in zip(keys, values)} print(data_dict) Результат выполнения: {1: 'Первый', 2: 'Второй', 3: 'Третий', 4: 'Четвертый', 5: 'Пятый'} До этого момента мы сосредоточивались на самых базовых строительных блоках Python — элементах, из которых складываются любые программы. Но чтобы перейти от этих «кирпичиков» к созданию настоящих «зданий» (полноценных приложений), нужны новые навыки и дополнительные знания. Мы начнем знакомиться с ними в следующей главе. Увидимся там!
В этой главе вы узнаете: » Как создавать собственные функции » Как добавлять комментарии внутри функции » Как передавать данные в функцию » Как возвращать значения из функции » Что такое анонимные функции и как они работают Гл а в а 5 Управление большими фрагментами кода В этой главе вы узнаете, как упрощать работу с крупными программами, создавая собственные функции. Функции позволяют разбивать программу на небольшие части — задачи, которые можно вызывать в разных местах приложения. Это особенно полезно, если вам нужно многократно выполнять одну и ту же операцию. Вместо того чтобы копировать один и тот же фрагмент из десятка строк кода снова и снова, вы помещаете его в функцию и просто вызываете ее тогда, когда это необходимо. Такой подход делает код компактнее и удобнее для сопровождения. Если позже вы захотите что-то изменить или исправить ошибку, вам не придется искать и редактировать один и тот же код в нескольких местах. Достаточно внести исправление только в одном месте — внутри функции. Чтобы выполнить задачу, описанную в функции, вы вызываете ее из программы. Делается это так же, как с любыми встроенными функциями, например print(): достаточно указать имя функции в коде. Вы можете придумывать функции с любыми удобными для вас именами. Это дает возможность «персонализировать» Python — создавать команды, которые лучше отражают смысл задач именно вашего приложения. 224 РАЗДЕЛ 2 Понимание базовых конструкций Python
Создание функции Создавать собственные функции в Python очень просто. Вы можете попробовать это прямо в ячейке Jupyter Notebook или в отдельном файле с расширением .py, чтобы получить практический опыт. Для начала используйте ключевое слово def (сокращение от define — «определить»). После него напишите придуманное имя функции, затем сразу круглые скобки (без пробелов перед ними или внутри). Строка должна заканчиваться двоеточием. def hello(): На этом этапе функция уже существует, но пока ничего не делает. Чтобы задать действия функции, нужно написать код на следующих строках. При этом важно показать, что эти строки принадлежат функции. Для этого каждая строка кода внутри функции пишется с отступом (обычно это 4 пробела). ЗАПОМНИТЕ В Python отступы играют решающую роль. В языке нет отдельной команды, которая указывала бы конец функции. Вместо этого границы функции определяются именно отступами. Все строки, которые имеют отступ и идут после строки с ключевым словом def, считаются частью этой функции. Как только появляется строка без отступа (выравненная на том же уровне, что и строка с def), это означает, что функция закончилась и начинается новый участок программы. Таким образом, правильное использование отступов — это способ сообщить Python, какие строки относятся к функции, а какие находятся вне ее. Чтобы функция выполняла какое-то действие, добавьте строку с отступом сразу под строкой def. Для начала заставим функцию выводить слово 'Hello'. Для этого используем оператор print() с отступом под строкой def. Теперь ваш код будет выглядеть так: def hello(): print('Hello') Если запустить этот код сейчас, на экране ничего не произойдет. Это потому, что код внутри функции не выполняется до тех пор, пока сама функция не будет вызвана. Вызываются собственные функции так же, как и встроенные: нужно написать имя функции и добавить круглые скобки в конце. Например, если вы следуете инструкциям, нажмите Enter, чтобы перейти на новую строку, затем введите hello() (без пробелов внутри и без отступа). ГЛАВА 5 Управление большими фрагментами кода 225
Так как эта строка не является частью функции, она должна быть выровнена по левому краю. Весь код будет выглядеть так: def hello(): print('Hello') hello() Тем не менее, если вы просто написали этот код в ячейке Jupyter или в файле .py, на экране ничего не произойдет. Чтобы функция выполнилась, нужно запустить код — обычным способом в Jupyter Notebook или в VS Code (если вы работаете с файлом .py в этой среде). После выполнения кода вы должны увидеть результат — слово Hello, выведенное на экран, как показано на рисунке 5.1. Рисунок 5.1. Написание и вызов простой функции с именем hello() Комментирование функции Комментарии в коде не являются обязательными, но они помогают сделать программу понятнее. Принято первую строку после оператора def писать как строку документации — текст в тройных кавычках (""" """), который кратко описывает, что делает функция. Кроме того, часто добавляют комментарий, начинающийся со знака #, прямо справа от имени функции в первой строке. Такие комментарии никак не влияют на выполнение программы. Они нужны только для вас или других программистов, чтобы было проще понять назначение кода. На рисунке 5.2 показан пример с двумя комментариями, добавленными к нашей маленькой функции hello() в редакторе VS Code. Рисунок 5.2. Пример функции с добавленными комментариями 226 РАЗДЕЛ 2 Понимание базовых конструкций Python
Передача информации в функцию Функции могут не только выполнять команды, но и принимать информацию для обработки. Чтобы передать данные в функцию, укажите имя параметра в строке def для каждого элемента информации, который функция должна получить. Имя параметра подчиняется тем же правилам, что и имя переменной: • • • оно должно начинаться с буквы или символа подчеркивания; после этого могут идти буквы, цифры или подчеркивания; пробелы и знаки препинания в имени использовать нельзя . Желательно выбирать такие имена параметров, которые отражают их назначение — это делает код легче для чтения. Тем не менее допускается использовать и общие имена вроде x или y. Важно помнить: любое имя, указанное в качестве параметра, локально для функции. Это значит, что, если у вас есть переменная с именем x за пределами функции и параметр с именем x внутри нее, изменения внутри функции будут касаться только локального x. Внешняя переменная при этом останется без изменений. Такое поведение переменных внутри функций имеет технический термин — локальная область видимости. Это означает, что переменные, созданные или измененные внутри функции, существуют только в пределах этой функции и не влияют на остальной код. Когда выполнение функции заканчивается, все ее локальные переменные буквально перестают существовать. В то же время переменные, определенные вне функции, никак не затрагиваются событиями внутри нее, даже если имеют такие же имена. Это удобно, потому что при написании функции вы можете не переживать о случайном изменении внешних переменных, которые случайно называются так же, как ваши внутренние. ПРИМЕЧАНИЕ Хотя локальные переменные исчезают после завершения работы функции, сама функция может возвращать значение. Это возвращаемое значение становится доступным вне функции. Подробнее о том, как именно работает возврат значений, мы поговорим чуть позже. Предположим, вы хотите, чтобы функция hello приветствовала пользователя по имени (и у вас есть эта информация, сохраненная в переменной). Для этого нужно: ГЛАВА 5 Управление большими фрагментами кода 227
» указать имя параметра в скобках функции — оно будет выступать в роли заполнителя для входящей информации; » внутри функции использовать этот параметр, чтобы работать с переданными данными . Допустим, мы хотим передать имя человека в функцию hello и использовать его в операторе print(). Можно выбрать любое имя параметра, например x: def hello(x): # Тренировочная функция """ Строка документации с описанием функции """ print('Hello ' + x) В примере ниже переменная x в скобках hello(x) является параметром — заполнителем для информации, которая передается в функцию. Внутри функции эта x относится только к значению, переданному в вызове. Любые переменные с именем x вне функции не имеют никакого отношения к этому параметру. Хотя можно использовать общие имена вроде x, это не помогает сделать код понятным. Гораздо лучше выбирать описательные имена, например name или user_name. def hello(user_name): # Тренировочная функция """ Строка документации с описанием функции """ print('Hello ' + user_name) В функции print() мы специально добавили пробел после буквы «o» в слове 'Hello', чтобы в выводе между приветствием и именем оставалось пространство. Когда у функции есть параметр, при ее вызове необходимо обязательно передать значение. Если этого не сделать, функция не сработает и возникнет ошибка. В коде ниже функция определена с параметром user_name, но вызвана без аргумента: def hello(user_name): # Тренировочная функция """ Строка документации с описанием функции """ print('Hello ' + user_name) hello() При запуске появится сообщение об ошибке: hello() missing 1 required positional argument: 'user_nam' 228 РАЗДЕЛ 2 Понимание базовых конструкций Python
Это сообщение, говоря простыми словами, означает: функция hello ожидала получить значение в параметр user_name, но не получила его. Для нашей функции hello нужно передавать именно строку. Это видно по тому, что внутри функции выполняется конкатенация — соединение переданного значения с другой строкой (© Hello© ). Если попытаться объединить строку с числом, Python выдаст ошибку. Значение можно передать прямо в вызове функции, используя литерал (конкретное значение). def hello(user_name): # Тренировочная функция """ Строка документации с описанием функции """ print('Hello ' + user_name) hello('Alan') Здесь строка 'Alan' передается напрямую в вызове hello('Alan'). hello('Alan') Для передачи информации в функцию можно использовать не только строковый литерал, но и переменную. Например, в коде на рисунке 5.3 строка 'Alan' сохранена в переменной this_ person. Затем функция вызывается с этой переменной в качестве аргумента: this_person = "Alan" hello(this_person) При выполнении кода результат будет: Hello Alan Таким образом, в функцию можно передавать как конкретные данные, так и значения, хранящиеся в переменных. Рисунок 5.3. Передача данных в функцию через переменную ГЛАВА 5 Управление большими фрагментами кода 229
Необязательные параметры со значениями по умолчанию В предыдущем разделе мы говорили, что, если вызвать функцию, которая ожидает параметр, но не передать его, возникнет ошибка. Это не совсем так. Функцию можно написать так, чтобы передача параметра была необязательной. Для этого нужно указать, какое значение использовать по умолчанию, если аргумент не передан. Функцию hello() можно переписать так, чтобы она работала и без аргумента. Для этого после имени параметра указывается значение по умолчанию: def functioname(parametername=defaultvalue): Единственное отличие от обычного объявления параметра — это часть = defaultvalue. def hello(user_name = 'nobody'): # Тренировочная функция """ Строка документации, описывающая функцию """ На рисунке 5.4 показана функция после внесенных изменений вместе с результатами ее тестирования. Рисунок 5.4. Необязательный параметр со значением по умолчанию, добавленный в функцию hello() Сначала функция вызывается с аргументом: hello('Alan') Вывод программы будет: Hello Alan Во второй строке, используемой для тестирования функции, вызов выполняется без передачи значения. То есть функция вызывается с пустыми скобками: 230 РАЗДЕЛ 2 Понимание базовых конструкций Python
hello() Так как аргумент не указан, параметр user_name принимает значение по умолчанию — 'nobody'. В результате программа выводит: Hello nobody Передача нескольких значений в функцию До этого момента во всех примерах мы передавали в функцию только одно значение. Однако в Python нет ограничений — вы можете передавать столько значений, сколько нужно. Для этого при объявлении функции укажите имя параметра для каждого значения и разделите параметры запятыми. Предположим, вы хотите передать в функцию сразу несколько значений: имя пользователя, фамилию и, например, дату. Для этого можно определить три параметра следующим образом: def hello(fname, lname, datestring): # Тренировочная функция """ Строка документации с описанием функции """ print('Hello ' + fname + ' ' + lname) print('The date is ' + datestring) Здесь параметры fname, lname и datestring являются обязательными. Поэтому при вызове функции нужно указать все три значения. hello('Alan', 'Simpson', '12/31/2024') На рисунке 5.5 показан пример выполнения этого кода с функцией hello(), принимающей три параметра. Рисунок 5.5. Функция hello с тремя параметрами ГЛАВА 5 Управление большими фрагментами кода 231
Если вы хотите использовать в функции сразу несколько параметров, часть из которых обязательные, а часть необязательные, нужно соблюдать правильный порядок. Необязательные параметры должны указываться последними. Код ниже работать не будет: def hello(fname, lname='unknown', datestring): Если попытаться его выполнить, появится ошибка: SyntaxError: non-default argument follows default argument. Эта ошибка означает, что при определении функции нельзя указывать обязательный параметр после необязательного. Если вы хотите использовать и обязательные, и необязательные параметры, то сначала нужно перечислить все обязательные (в любом порядке), а уже после них — необязательные параметры со значениями по умолчанию. def hello(fname, lname, datestring=''): msg = 'Hello ' + fname + ' ' + lname if len(datestring) > 0: msg += ' The date is ' + datestring print(msg) Код внутри функции выполняет следующие шаги: » создает переменную msg и помещает в нее строку "Hello" вместе с именем и фамилией; » если переданная строка даты не пустая (ее длина больше 0), добавляет к msg текст "The date is" и саму дату; » выводит текущее содержимое переменной msg . На рисунке 5.6 показаны два варианта вызова этой функции. Первый вызов передает три значения, а второй — только два. Оба варианта работают, потому что третий параметр (datestring) является необязательным. В первом случае функция выводит полное сообщение с датой, а во втором — только приветствие без даты. СОВЕТ 232 Если вы вдруг забудете синтаксис для создания функции, всегда можно обратиться за помощью к инструментам вроде GitHub Copilot, ChatGPT или другому ИИ-ассистенту. Такие инструменты могут быстро сгенерировать нужный код. Например, если попросить написать функ- РАЗДЕЛ 2 Понимание базовых конструкций Python
цию Python с именем hello, которая принимает параметры firstname и lastname, вы получите базовый код для ее определения. Рисунок 5.6. Вызов функции hello() с тремя параметрами и повторно с двумя параметрами Использование именованных аргументов (kwargs) Если вы когда-либо просматривали официальную документацию Python на сайте python.org, то наверняка встречали термин kwargs. Это сокращение от keyword arguments — «именованные аргументы». Такой способ передачи данных в функцию является альтернативой позиционным аргументам. Термин «аргумент» — это техническое название для значения, которое вы передаете параметрам функции. До этого момента мы использовали в примерах только позиционные аргументы. То есть значения передавались строго в том порядке, в каком были перечислены параметры функции. Вот и пример функции с тремя параметрами: def hello(fname, lname, datestring=''): Вызов функции с позиционными аргументами: hello("Alan", "Simpson") ГЛАВА 5 Управление большими фрагментами кода 233
Python предполагает, что: • • • "Alan" — это значение для параметра fname, так как оно передано первым; "Simpson" — это значение для параметра lname, так как оно идет вторым; datestring остается пустым, потому что это третий параметр в определении функции, и аргумент для него не был передан . Вместо того чтобы полагаться на порядок аргументов, можно явно указать, какой параметр чему соответствует. Для этого используется синтаксис параметр=значение при вызове функции. hello(datestring='12/31/2019', lname='Simpson', fname='Alan') Когда вы запускаете этот код, он работает без ошибок, даже если порядок переданных аргументов не совпадает с порядком параметров в определении функции (def). Причина в том, что при использовании именованных аргументов Python ориентируется не на позиции, а на имена Alan© явно связан с параметром fname, параметров. Например, аргумент © потому что в вызове указано fname='Alan'. Тот же принцип работает и при передаче переменных. Порядок снова не имеет значения. В примере ниже значения сначала сохраняются в переменные appt_date, last_name и first_name. Затем при вызове функции hello() в качестве аргументов указываются имена этих переменных: appt_date = '12/30/2019' last_name = 'Janda' first_name = 'Kylie' hello(datestring=appt_date, lname=last_name, fname=first_name) На рисунке 5.7 показан результат выполнения кода обоими способами — сначала с позиционными аргументами, а затем с именованными. Как видно, все работает корректно. При использовании именованных аргументов нет никакой неоднозначности, какой аргумент к какому параметру относится, потому что каждое имя параметра явно указано в вызывающем коде. 234 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 5.7. Выполнение кода с именованными аргументами Передача нескольких значений в списке До этого момента мы передавали в функцию только по одному элементу данных за раз. Но в Python можно передавать и целые итерируемые объекты. Итерируемый объект — это все, что Python способен перебирать в цикле, извлекая одно значение за другим. Самый простой и, пожалуй, наиболее часто используемый пример итерируемого объекта — это список. Во многих случаях функции полезны именно тогда, когда они возвращают результат, не изменяя исходные данные. Например, вы можете захотеть, чтобы функция возвращала копию списка, отсортированную в алфавитном порядке, но при этом не меняла оригинальный список. Решение простое: 1. Внутри функции создается копия исходного списка . 2. К копии применяется метод sort() для сортировки . 3. При необходимости можно использовать sort(reverse=True), чтобы отсортировать в обратном порядке . ГЛАВА 5 Управление большими фрагментами кода 235
Ниже показана новая функция с именем alphabetize(), которая принимает один аргумент — original_list. Полное объявление параметра выглядит так: original_list=[]. Квадратные скобки указывают на пустой список как значение по умолчанию, если аргумент не передан. Другими словами, запись =[] гарантирует, что функция не вызовет ошибку при случайном вызове без параметра: в этом случае будет создан пустой список. def alphabetize(original_list=[]): """ Принимает список и выводит строку с отсортированными элементами""" # Создаем рабочую копию переданного списка sorted_list = original_list.copy() # Сортируем рабочую копию sorted_list.sort() # Создаем новую пустую строку для результата final_list = '' # Добавляем в строку каждый элемент с запятой и пробелом for name in sorted_list: final_list += name + ', ' # Убираем последнюю запятую и пробел, если строка не пустая final_list = final_list[:-2] # Печатаем отсортированный список print(final_list) Первая строка кода определяет функцию. В параметре указано original_ list=[]. Значение по умолчанию (=[]) не является обязательным, но оно добавлено специально, чтобы при случайном вызове функции без аргументов не возникала ошибка. В таком случае создается пустой список. На рисунке 5.8 показана функция, введенная в редакторе Visual Studio Code. Под определением функции добавлена строка без отступа, которая вызывает ее и передает список имен для сортировки. Первым шагом функция создает копию переданного списка. Для этого используется метод .copy(). Копия сохраняется в новой переменной sorted_list: sorted_list = original_list.copy(). Теперь любые изменения будут применяться к копии, а исходный список останется без изменений. sorted_list = original_list.copy() 236 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 5.8. Наша функция alphabetize() в редакторе VS Code На этом этапе переменная sorted_list все еще остается обычной копией исходного списка и пока не отсортирована. Следующая строка выполняет сортировку элементов в списке по алфавиту: sorted_list.sort() После сортировки функция формирует строку со всеми элементами списка, разделенными запятыми. Сначала создается новая переменная final_list, которая инициализируется как пустая строка (два одинарных апострофа без пробела между ними): final_list = ' ' Затем цикл for проходит по всем элементам списка sorted_list и добавляет каждый элемент в строку final_list, ставя после него запятую и пробел: for name in sorted_list: final_list += name + ', ' После выполнения цикла в конце строки final_list остаются лишние символы — запятая и пробел. Чтобы убрать их, используется срез: final_list = final_list[:-2] ГЛАВА 5 Управление большими фрагментами кода 237
Этот прием удаляет два последних символа при условии, что строка не пустая. Последний оператор просто выводит содержимое переменной final_ list, чтобы показать результат: print(final_list). Чтобы вызвать функцию, передайте список имен в круглых скобках: alphabetize(['Schrepfer', 'Maier', 'Santiago', 'Adams']) При выполнении этого кода функция отсортирует список и выведет имена в алфавитном порядке, разделенные запятыми. Как и обычно, в функцию можно передать не только сам список, но и переменную, которая его содержит. Например: names = ['Schrepfer', 'Maier', 'Santiago', 'Adams'] alphabetize(names) Результат будет тем же самым — функция выведет имена в алфавитном порядке: Adams, Maier, Santiago, Schrepfer Обратите внимание: если вам не нужно сохранять оригинальный список без изменений, можно обойтись без создания копии. В этом случае достаточно просто отсортировать сам исходный список. Пример: def alphabetize(original_list=[]): """ Принимает список и сортирует его на месте. """ original_list.sort() В этом примере функция сортирует исходный список, а не его копию. Однако, если запустить функцию в таком виде, на экране ничего не появится, потому что внутри функции нет команды print(). Чтобы увидеть результат, нужно создать список, передать его в функцию и затем вывести: fruits=['kiwi','pear','strawberry','plum','banana','artichoke'] alphabetize(fruits) print(fruits) Последний оператор print() отобразит список в алфавитном порядке: ['artichoke', 'banana', 'kiwi', 'pear', 'plum', 'strawberry'] 238 РАЗДЕЛ 2 Понимание базовых конструкций Python
Передача произвольного количества аргументов Ранее мы уже познакомились с тем, как список может служить способом передачи сразу нескольких значений в функцию. Но в Python существует и другой подход: функцию можно написать так, чтобы она принимала любое количество аргументов. Имейте в виду: этот метод не всегда является самым быстрым или самым удобным. Используйте тот вариант, который проще и лучше подходит для вашей задачи. Чтобы позволить функции принимать произвольное количество аргументов, в ее определении используется специальная запись *args. Звездочка (*) перед именем параметра указывает Python, что в функцию можно передать любое количество значений. def sorter(*args): Все значения, которые вы передаете функции через *args, внутри нее собираются в кортеж с именем args. Напомним, что кортеж — это неизменяемый список: его элементы можно просматривать, но нельзя менять. Если вам нужно изменить эти данные (например, отсортировать), придется сначала создать из кортежа список и уже работать с ним. Для этого используют простую команду: newlist = list(args) Эта строка означает: «создать переменную newlist и записать в нее список всех элементов, которые находятся в кортеже args». После этого список можно сортировать или изменять, как угодно. def sorter(*args): """ Принимает любое количество аргументов. Внутри функции они становятся кортежем args. """ # Создаем список из кортежа. newlist = list(args) # Сортируем список и выводим результат. newlist.sort() print(newlist) ГЛАВА 5 Управление большими фрагментами кода 239
На рисунке 5.9 показан пример выполнения этой функции в ячейке Jupyter Notebook. В функцию передается набор чисел в качестве аргументов. Результат работы функции — это список, в который собраны все переданные значения и который выведен в отсортированном виде. Как видно из примера, список действительно отсортирован, как и ожидалось. Рисунок 5.9. Функция, принимающая любое количество аргументов с *args Возвращение значений из функций До этого момента большинство наших функций просто выводили результат на экран с помощью print(), чтобы мы могли проверить, что все работает правильно. Однако в реальных программах чаще всего функции не выводят данные напрямую, а возвращают какое-то значение, которое затем можно сохранить в переменную и использовать дальше в коде. Для этого служит оператор return. Обычно строка с return располагается в конце функции. Синтаксис очень простой: пишется слово return, затем пробел и имя переменной (или выражение), которое нужно вернуть. Ниже приведена измененная версия функции alphabetize. В ней нет оператора print(). Вместо этого функция возвращает отсортированный список final_list: 240 РАЗДЕЛ 2 Понимание базовых конструкций Python
def alphabetize(original_list=[]): """ Принимает список и возвращает строку с отсортированными элементами """ # Создаем копию исходного списка sorted_list = original_list.copy() # Сортируем копию sorted_list.sort() # Формируем строку для вывода final_list = '' # Проходим по отсортированному списку и добавляем имя, запятую и пробел for name in sorted_list: final_list += name + ', ' # Убираем последнюю запятую и пробел final_list = final_list[:-2] # Возвращаем итоговую строку return final_list На практике чаще всего результат работы функции сохраняют в переменной, чтобы затем использовать его в программе. Рассмотрим пример: random_list = ['McMullen', 'Keaser', 'Maier', 'Wilson', 'Yudt', 'Gallagher', 'Jacobs'] alpha_list = alphabetize(random_list) print(alpha_list) 1. В первой строке создается переменная random_list . Она содержит список имен в произвольном порядке . 2. Во второй строке переменная random_list передается в функцию alphabetize() . Функция возвращает строку с именами в алфавитном порядке, и это значение 3. Последняя строка с командой print(alpha_list) выводит содержимое этой переменной, то есть список имен, отсортированный по алфавиту . На рисунке 5.10 показан результат выполнения, приведенного выше кода в ячейке Jupyter Notebook. На экране выводится строка с именами, расположенными в алфавитном порядке. ГЛАВА 5 Управление большими фрагментами кода 241
Рисунок 5.10. Вывод строки, возвращаемой функцией Анонимные функции (лямбда-функции) В Python есть особый вид функций — анонимные функции, которые также называют лямбда-функциями. Слово «анонимный» используется потому, что у такой функции может не быть имени (хотя при необходимости его можно задать). Название «лямбда» связано с ключевым словом lambda, которое используется для создания таких функций в Python. Иными словами, всякий раз, когда вы видите слово lambda в коде, оно означает, что в этой строке определяется анонимная функция. Минимальная форма записи лямбда-функции выглядит так: lambda arguments : expression • • arguments — данные (аргументы), которые передаются в функцию; expression — выражение, результат вычисления которого будет возвращен этой функцией . Одно из распространенных применений лямбда-функций связано с сортировкой строк, когда в списке встречаются имена и с прописных, и со строчных букв. 242 РАЗДЕЛ 2 Понимание базовых конструкций Python
Допустим, у нас есть список: Adams, Ma, diMeola, Zandusky Если мы добавим эти имена в список, отсортируем его стандартным способом и выведем результат, код будет выглядеть так: names = ['Adams', 'Ma', 'diMeola', 'Zandusky'] names.sort() print(names) Результат: ['Adams', 'Ma', 'Zandusky', 'diMeola'] То, что имя diMeola оказалось после Zandusky, выглядит странно (и, скорее всего, вы тоже согласитесь, что это «неправильно»). Но нужно помнить: компьютеры сортируют данные не так, как мы их воспринимаем. Причина в том, что сортировка в Python основана на кодировке Unicode. В Unicode каждому символу соответствует числовое значение. У строчных букв эти значения больше, чем у прописных. Поэтому все слова, начинающиеся со строчной буквы, автоматически ставятся после слов, начинающихся с заглавной. В Python у метода .sort() есть возможность указать специальное выражение с параметром key=, которое задает правило сортировки. Это позволяет вам управлять тем, как именно будут сравниваться элементы. Синтаксис выглядит так: .sort(key = transform) Часть transform в конструкции .sort(key=transform) позволяет указать функцию, которая будет применяться к каждому элементу перед сортировкой. Иногда можно обойтись встроенными функциями. Например, если нужно отсортировать строки по длине, подойдет функция len: names.sort(key=len) ГЛАВА 5 Управление большими фрагментами кода 243
В нашем случае сортировка по длине строк не помогает. Когда мы используем key=len, Python просто располагает элементы списка от самого короткого слова к самому длинному: ['Ma', 'Adams', 'diMeola', 'Zandusky'] Когда мы пробовали использовать key=len, сортировка выполнялась по длине строк, а не по алфавиту, — это не решает нашу задачу. Нельзя также напрямую написать key=lower или key=upper, потому что lower и upper — это методы строк, а не встроенные функции Python. Проверить список встроенных функций можно, например, набрав в интернете запрос: python 3 built-in functions. Чтобы все-таки использовать преобразование в нижний регистр при сортировке, можно создать собственную функцию с помощью def. Пример: def lowercaseof(anystring): """ Преобразует строку в нижний регистр """ return anystring.lower() Эта функция принимает строку и возвращает ее копию, в которой все буквы переведены в нижний регистр. Мы придумали имя функции lowercaseof, а параметр anystring служит заполнителем для любой строки, которую вы передадите функции. Внутри функции строка return anystring.lower() возвращает копию этой строки, где все буквы преобразованы в нижний регистр с помощью метода .lower(), доступного для объектов типа str. ПРИМЕЧАНИЕ Предположим, вы написали эту функцию в ячейке Jupyter или в файле .py. Если вызвать ее так: print(lowercaseof('Zandusky')) Вы получите результат: zandusky. Именно так показано на рисунке 5.11: исходная строка преобразуется в строчные буквы. Рисунок 5.11. Тестирование пользовательской функции Теперь у нас есть собственная функция, которая преобразует любую строку в нижний регистр. Как применить ее для сортировки? Все очень просто. 244 РАЗДЕЛ 2 Понимание базовых конструкций Python
В параметре key= укажите имя вашей функции. Ранее мы условно обозначали ее как transform, теперь вместо этого пишем имя нашей функции — lowercaseof. Таким образом, вызов метода сортировки будет выглядеть так: def lowercaseof(anystring): """ Преобразует строку в нижний регистр """ return anystring.lower() names = ['Adams', 'Ma', 'diMeola', 'Zandusky'] names.sort(key=lowercaseof) print(names) Когда вы выполните этот код, список имен будет отсортирован в правильном алфавитном порядке. Это происходит потому, что Python сравнивает строки в нижнем регистре (внутри сортировки), но сами данные при этом не меняются. Другими словами, сортировка выполняется по строкам, приведенным к нижнему регистру, а в исходном списке сохраняется оригинальный вид — с прописными и строчными буквами. 'Adams', 'diMeola', 'Ma', 'Zandusky' Если вы еще не уснули после всего прочитанного, то, возможно, подумали: «Хорошо, мы решили задачу сортировки. Но ведь речь шла о лямбда-функциях. Где же они?» Действительно, пока мы обходились без лямбда-функции. Но это как раз тот случай, когда ее удобно применить. Посмотрите: наша функция lowercaseof() делает всю работу всего одной строкой: return anystring.lower() Если задача функции сводится к такому простому однострочному выражению, то можно обойтись без def и имени функции. Вместо этого используется лямбда-выражение. lambda parameters : expression • • parameters — это входные данные для функции (может быть один или несколько); expression — это то, что функция возвращает . ГЛАВА 5 Управление большими фрагментами кода 245
Вместо parameters вы указываете одно или несколько имен параметров, которые сами придумываете. Это такие же имена, какие обычно пишутся в скобках после def и имени функции при объявлении обычной функции. Вместо expression пишется то, что функция должна возвращать. Обратите внимание: в лямбда-выражении не используется слово return. В нашем случае ключ сортировки можно записать с помощью лямбдавыражения так: lambda anystring: anystring.lower() • • anystring — имя параметра, которое будет ссылаться на каждую строку из списка; anystring.lower() — выражение, результат которого возвращает лямбда-функция . Теперь становится ясно, почему такую функцию называют анонимной. Нам больше не нужна строка с объявлением def lowercaseof(). Вместо нее мы используем компактное лямбда-выражение, в котором остаются только параметр, двоеточие и выражение, результат которого нужно вернуть. Главное преимущество этого подхода — отсутствие необходимости создавать отдельную внешнюю функцию. Все сводится к короткой записи прямо в месте вызова. Например, при сортировке списка ключ можно указать так: lambda anystring: anystring.lower(). На рисунке 5.12 приведен полный код и его результат. Список имен отсортирован правильно, без написания дополнительной пользовательской функции вроде lowercaseof(). Для этого достаточно использовать лямбда-выражение в параметре key. Рисунок 5.12. Использование лямбдавыражения в качестве ключа сортировки 246 РАЗДЕЛ 2 Понимание базовых конструкций Python
Обратите внимание, что имя параметра anystring довольно длинное. В Python принято использовать короткие имена, иногда даже однобуквенные. Такой стиль делает код короче и читаемее. Например, вы можете заменить anystring на s (или любую другую букву). Код при этом будет работать точно так же: names = ['Adams', 'Ma', 'diMeola', 'Zandusky'] names.sort(key=lambda s: s.lower()) print(names) В начале раздела мы отмечали, что лямбда-функции необязательно должны оставаться анонимными. При желании вы можете дать им имя и затем вызывать так же, как и любую другую функцию. Лямбда-функцию можно использовать не только как анонимную, но и присвоить ей имя. В этом случае она будет работать так же, как и обычная функция, определенная через def. Рассмотрим два примера: 1. Функция currency Эта функция принимает число и преобразует его в строку с денежным форматом США: • • • добавляет знак доллара в начале; ставит запятые между тысячами; оставляет две цифры после десятичной точки . currency = lambda n: f"${n:,.2f}" 2. Функция percent Эта функция принимает число и отображает его в виде процентов: • • • умножает значение на 100; округляет до двух знаков после запятой; добавляет символ % в конце . percent = lambda n: f"{n:.2%}" ГЛАВА 5 Управление большими фрагментами кода 247
На рисунке 5.13 показан пример, где в верхней части ячейки Jupyter определены обе функции — currency и percent. Далее несколько операторов print() вызывают эти функции по имени, передавая им тестовые значения. Каждый вызов демонстрирует, как число преобразуется и выводится в заданном формате. Рисунок 5.13. Две анонимные функции для форматирования чисел Функции currency и percent удобно реализовать как однострочные лямбда-выражения, потому что все вычисление выполняется всего одной строкой: f"${n:,.2f}" для форматирования числа в денежном виде, f" {n:.2%}" для отображения числа в процентах. Однако то, что это возможно, вовсе не значит, что нужно всегда использовать именно лямбда. Иногда привычная запись через def выглядит более читаемо, особенно для начинающих. Вот эквивалент этих функций в виде обычных определений: # Отображение числа в денежном формате def currency(n): return f"${n:,.2f}" # Отображение числа в процентном формате def percent(n): return f"{n:.2%}" Если применить более развернутый синтаксис при определении функций, можно передавать дополнительные параметры. Например, удобно добавить возможность задавать ширину поля для вывода числа и вырав- 248 РАЗДЕЛ 2 Понимание базовых конструкций Python
нивать его по правому краю. Это позволяет красиво располагать данные в столбцах. На рисунке 5.14 показан вариант функций currency() и percent(), где добавлен второй параметр — ширина вывода. Этот параметр является необязательным и по умолчанию равен 15. Если его не указывать, Python будет использовать именно это значение. Рисунок 5.14. Две функции для форматирования чисел с фиксированной шириной Например, при вызове функции: print(currency(9999)) вы получите строку $9,999.00, дополненную пробелами слева так, чтобы ее общая длина составила 15 символов. Если же указать ширину явно, например: print(currency(9999, 20)) то число $9,999.00 будет выровнено по правому краю уже в поле шириной 20 символов, и слева добавится больше пробелов. Метод .rjust() — это встроенный строковый метод Python, который выравнивает строку по правому краю, добавляя слева нужное количество пробелов (или других символов) до указанной ширины. СОВЕТ Аналогично, метод .ljust() выравнивает строку по левому краю, добавляя пробелы справа. Причем в обоих случаях вместо пробелов можно указать любой другой символ-заполнитель. Иногда такие методы могут показаться немного запутанными. Если вы сомневаетесь, легко найти пояснения: достаточно выполнить поиск в интернете по запросу «python left justify» или «python right justify». ГЛАВА 5 Управление большими фрагментами кода 249
Ну а еще проще — попросить ИИ написать небольшой пример кода специально для вас. Теперь у вас есть представление о том, как создавать собственные функции в Python. В реальных проектах это особенно важно: если один и тот же фрагмент кода вам нужен много раз, не стоит копировать его в разные места. Вместо этого: поместите код в отдельную функцию, вызывайте ее по имени там, где нужно. Преимущество очевидно: если впоследствии нужно что-то изменить, вы правите код только в одном месте — внутри функции. Все вызовы автоматически используют обновленную версию.
В этой главе вы узнаете: » Что такое классы и объекты » Как создавать собственные классы » Как инициализировать объект внутри класса » Как задавать и изменять атрибуты объекта » Как добавлять методы в класс » Как использовать наследование классов Гл а в а 6 Python и классы В предыдущей главе мы говорили о функциях. Они помогают разбивать код на отдельные блоки, каждый из которых выполняет свою задачу. В этой главе мы пойдем дальше и познакомимся с классами. Классы позволяют организовывать не только код, но и данные, связывая их воедино. Мы обещаем: впереди вас ждет все великолепие и удобство объектно ориентированного программирования (да, мы немного драматизируем, но только чуть-чуть). Дело в том, что классы — это одна из ключевых особенностей современных языков программирования, и Python здесь не исключение. Если вы пока не знаете, чем класс отличается от объекта (и даже не подозреваете, что это может быть важно), — не переживайте. Это абсолютно нормально. В начале пути так чувствуют себя почти все. В этой главе мы будем объяснять все так, будто вы впервые сталкиваетесь с этими понятиями. ГЛАВА 6 Python и классы 251
Осваиваем классы и объекты Как вы уже знаете, Python является объектно ориентированным языком программирования. Объектно ориентированное программирование (сокращенно ООП) уже много десятилетий остается важнейшей концепцией в мире разработки программ. Сам термин «объект» появился потому, что эта модель во многом похожа на то, как мы воспринимаем объекты в реальном мире. Каждый объект — это «вещь», обладающая определенными свойствами и особенностями, которые делают ее уникальной. Возьмем, к примеру, стул. Стул — это объект. И стульев существует бесчисленное множество: большие и маленькие, деревянные и металлические, синие и красные. У каждого из них есть свои характеристики — размер, форма, цвет, материал. Но несмотря на все различия, все они остаются стульями. Подумайте об автомобилях. Мы без труда узнаем машину, когда видим ее (ну, почти всегда). Хотя все автомобили разные, у них есть определенные атрибуты — характеристики, которые описывают конкретный экземпляр: год выпуска, марка, модель, цвет. Эти свойства делают каждый автомобиль уникальным. Кроме того, у всех автомобилей есть набор методов — действий, которые они могут выполнять. Например, любой автомобиль может ехать, останавливаться или поворачивать. Да, детали реализации могут отличаться, но суть остается той же. В объектно ориентированном программировании мы можем представить класс Car как своего рода фабрику. Класс задает общую модель: какие атрибуты и методы есть у машины. А уже конкретные автомобили, созданные с помощью этого класса, — это объекты (рисунок 6.1). Каждый объект работает независимо: изменения в одном автомобиле никак не влияют на другие машины или сам класс Car. Если идея с фабрикой кажется абстрактной, попробуйте представить класс как чертеж. 252 РАЗДЕЛ 2 Понимание базовых конструкций Python
δòτωω'EV ηöψðêϊöý μυöχíý Рисунок 6.1. Разные объекты автомобилей Например, возьмем собак. Конечно, у собак нет бумажного чертежа, но у них есть ДНК — своего рода инструкция, по которой формируется каждое животное (рисунок 6.2). ДНК можно рассматривать как аналог класса в Python. Класс Dog определяет общие свойства и поведение всех собак. Каждая конкретная собака — это объект этого класса, со своими атрибутами (например, порода, цвет, размер) и методами (например, есть или спать). Аналогичный подход можно применить и к людям. Допустим, у вас есть клуб, и вы хотите отслеживать его участников. Вы можете создать класс Member, чтобы хранить данные о каждом человеке. Экземпляры этого класса будут содержать такие атрибуты, как имя пользователя и полное имя. Кроме того, у них могут быть методы: • • .archive() — для деактивации учетной записи; .restore() — для ее повторной активации. Таким образом, класс задает структуру и поведение, а объекты — это его конкретные экземпляры с собственными данными. ГЛАВА 6 Python и классы 253
δòτωω(SK ηöψðêϊöý μυöχíý Рисунок 6.2. Класс Dog обладает различными собаками δòτωω 1IQFIV ηöψðêϊöý ηöψðêϊöý ηöψðêϊöý ηöψðêϊöý ηöψðêϊöý ηöψðêϊöý ηöψðêϊöý μυöχíý μυöχíý μυöχíý μυöχíý μυöχíý μυöχíý Рисунок 6.3. Класс Member и объекты μυöχíý Методы .archive() и .restore() можно рассматривать как поведение объекта, которое позволяет управлять членством. Это похоже на то, как педаль газа, тормоз и руль позволяют управлять автомобилем. На рисунке 6.3 показана эта идея. 254 РАЗДЕЛ 2 Понимание базовых конструкций Python
Главное, что нужно запомнить: каждый экземпляр класса — это независимый объект. Вы можете изменять его атрибуты или вызывать его методы, и это никак не повлияет ни на сам класс, ни на другие экземпляры. Например, если перекрасить один автомобиль, завод и другие выпущенные машины останутся прежними. Точно так же изменение одного объекта класса в Python не влияет на остальные. Итак, вернемся к основным понятиям. Вся эта история с классами и экземплярами связана с особым подходом к написанию кода — объектно ориентированным программированием (сокращенно ООП). Python, как и большинство современных языков программирования, поддерживает этот подход. Вот ключевые термины, которые нужно хорошо понять: » Класс — это фрагмент кода, на основе которого можно создавать объекты . Представляйте класс как чертеж или фабрику, где каждый созданный объект будет независимым экземпляром . » Экземпляр — это конкретный объект, созданный из класса . Он сочетает в себе данные и функции для работы с этими данными . Иногда экземпляр называют просто объектом . Например, разные автомобили можно рассматривать как объекты, созданные одной фабрикой—классом . » Атрибут — характеристика объекта, то есть его свойство . Атрибут хранит информацию о конкретном экземпляре . В коде атрибуты записываются через точку, например: member.user_name может содержать имя пользователя для одного участника клуба . » Метод — это функция, связанная с классом . Она определяет действия, которые объект может выполнять . Вызов метода также идет через точку, например: member.archive() . Такой метод может деактивировать членство пользователя . Создание класса Вы можете создавать собственные классы. Назвать класс можно как угодно, если это корректное имя: оно должно начинаться с буквы или подчеркивания и не содержать пробелов или знаков пунктуации. ГЛАВА 6 Python и классы 255
По общему соглашению, имя класса пишут с заглавной буквы, чтобы отличать его от переменных. Чтобы создать новый класс, нужно начать строку со слова class, поставить пробел, написать имя класса и завершить строку двоеточием. Например, чтобы создать класс с именем Member, используйте такую конструкцию: class Member: Чтобы сделать код более понятным, рядом с определением класса можно добавить комментарий. Кроме того, часто используют строку документации (docstring) — текст в тройных кавычках сразу после строки с объявлением класса. Эта строка появляется в подсказках редакторов, например в VS Code, и помогает быстро понять назначение класса. # Определяем новый класс с именем Member. class Member: """ Класс для создания нового участника. """ На этом этапе мы определили новый класс, но сам по себе он пока бесполезен. Чтобы класс стал действительно полезным, необходимо указать, какие атрибуты будут у объектов, созданных на его основе. Именно атрибуты делают экземпляры класса уникальными и позволяют хранить данные, связанные с конкретным объектом. ПУСТЫЕ КЛАССЫ Если вы начнете писать класс с конструкции class имя: и сразу запустите код, Python выдаст ошибку . Это произойдет потому, что после двоеточия интерпретатор ожидает увидеть хотя бы одну инструкцию внутри класса . Чтобы избежать ошибки, можно воспользоваться специальным ключевым словом pass . Оно говорит Python: «Здесь пока ничего нет, но это место будет заполнено позже» . Такой прием полезен, если вы еще не закончили писать код, но уже хотите проверить, что программа работает . Пример: # Определяем новый класс с именем Member class Member: pass Таким образом, вы сообщаете Python: «Да, класс пока пустой, но не выдавай ошибку и продолжай выполнение программы» . 256 РАЗДЕЛ 2 Понимание базовых конструкций Python
Создание экземпляра класса Чтобы объекты, создаваемые из класса, сразу получали нужные свойства, в класс добавляют специальный метод __init __. Его название — это сокращение от слова initialize («инициализировать»). С технической точки зрения __init__ — это обычная функция, но определенная внутри класса и с особым именем — две нижние черты, затем слово init, и еще две нижние черты. Из-за этих двойных подчеркиваний этот метод часто называют dunder init (от английского double underscore). СОВЕТ Общий синтаксис выглядит так: def __init__(self[, suppliedprop1, suppliedprop2, ...]): В терминологии ООП оператор __init__ называется конструктором, поскольку он создает (конструирует) объект. ПРИМЕЧАНИЕ В объектно ориентированном программировании метод __init__ называют конструктором, потому что он отвечает за создание (конструирование) объекта. • • • Ключевое слово def (сокращение от define) используется для определения функции или метода . Специальное имя __init__ сообщает Python, что этот метод нужно выполнять автоматически при создании нового экземпляра класса . Первый параметр self — это ссылка на сам создаваемый объект . Благодаря ему мы можем задавать или изменять свойства именно этого объекта . Технически вместо слова self можно написать любое другое имя, и код все равно будет работать. Однако почти все программисты Python используют именно self. Это считается хорошей практикой: код становится более понятным и единообразным. Работать с классами проще, если начинать с самых элементарных примеров. Давайте создадим класс под названием Member, который будет использоваться для хранения информации об участниках. Каждый раз, когда мы захотим создать нового участника, мы будем передавать ему имя пользователя (uname) и полное имя (fname). По традиции, перед кодом можно добавить комментарий, чтобы сразу было понятно, что он делает. А сразу под строкой с объявлением класса ГЛАВА 6 Python и классы 257
можно разместить строку документации (docstring, заключенную в тройные кавычки). Она будет служить не только напоминанием, но и подсказкой в редакторах кода (например, VS Code через IntelliSense). # Класс Member для создания объектов-участников class Member: """ Класс для участника, содержащего имя пользователя и полное имя """ def __init__(self, uname, fname): Когда выполняется строка def __init__, Python создает новый объект внутри класса. На этапе создания он обозначается словом self. Параметры uname и fname получают данные, которые вы передадите при создании объекта. Однако сам объект пока еще пуст — без данных он не имеет практической ценности. Объект становится полезным только тогда, когда в нем появляются атрибуты — свойства, содержащие уникальную информацию. Поэтому следующим шагом внутри конструктора нужно присвоить значения параметров атрибутам объекта. Наделение объекта атрибутами Теперь, когда создан новый объект класса Member, можно добавить ему атрибуты и заполнить их значениями. Например, пусть у каждого участника будет: • • атрибут .user_name, в котором хранится имя пользователя (например, для входа в систему); атрибут .full_name, в котором записано полное имя участника . Для задания этих атрибутов внутри конструктора используйте строки: self.user_name = uname self.full_name = fname 258 РАЗДЕЛ 2 Понимание базовых конструкций Python
• • В первой строке создается атрибут user_name для объекта self, и ему присваивается значение, переданное в параметре uname . Во второй строке создается атрибут full_name, которому присваивается значение из параметра fname . С учетом комментариев весь класс будет выглядеть так: # Определяем класс Member для работы с участниками class Member: """ Создание нового участника. """ def __init__(self, uname, fname): # Определяем атрибуты и присваиваем им значения self.user_name = uname self.full_name = fname Видите, что происходит? Строка __init__ создает новый пустой объект с именем self. Разберем по шагам: • • • Когда выполняется строка def __init__, Python создает новый объект с именем self . Затем строка self.user_name = uname добавляет атрибут user_ name этому объекту и записывает в него значение, переданное в параметре uname . Аналогично строка self.full_name = fname создает атрибут full_name и сохраняет в нем значение параметра fname . Обратите внимание на соглашения об именах: • • СОВЕТ Имена классов в Python принято начинать с заглавной буквы (например, Member) . Имена атрибутов должны следовать правилам именования переменных: только строчные буквы, а для разделения слов используется подчеркивание (например, user_name, full_name) . Если вам трудно придумать, как правильно описать класс, можно обратиться за помощью к инструментам с искусственным интеллектом. Например, вы можете попросить Copilot, Claude .ai или ChatGPT сгенерировать пример класса Python с именем Member, который будет управлять данными о членстве. Такой код можно использовать как отправную точку: вы получите базовую структуру класса и сможете постепенно изменять и дополнять ее под свои задачи. ГЛАВА 6 Python и классы 259
Создание экземпляра класса После того как вы определили свой класс, можно создавать его экземпляры (объекты). Делается это очень просто — с помощью такого синтаксиса: this_instance_name = Member('uname', 'fname') Здесь: • • this_instance_name — это имя переменной, которую вы придумываете сами . Она будет ссылаться на созданный объект (примерно так же, как щенка можно назвать своим именем, хотя все они происходят от общего класса Dog) . 'uname' и 'fname' — это данные, которые вы передаете в конструктор: имя пользователя и полное имя . Важно: убедитесь, что код для создания экземпляра пишется вне класса, то есть без отступов. В противном случае Python решит, что этот код является частью определения класса, и выдаст ошибку. Предположим, вы хотите создать участника с именем new_guy, где имя пользователя будет Rambo, а полное имя — Rocco Moe. Для этого используйте такой код: new_guy = Member('Rambo', 'Rocco Moe') Если код выполняется без ошибок, значит, экземпляр создан. Но как убедиться, что внутри него действительно есть нужные данные? Для проверки можно вывести сам объект или его атрибуты. Например: print(new_guy) # вывод всего объекта print(new_guy.user_name) # вывод имени пользователя print(new_guy.full_name) # вывод полного имени print(type(new_guy)) # вывод типа переменной • • • Первая строка (print(new_guy)) покажет объект целиком, но результат может выглядеть не очень информативно . Вторая и третья строки выведут значения конкретных атрибутов — имя пользователя и полное имя . Последняя строка (type(new_guy)) сообщит, что переменная new_guy — это объект класса Member . На рисунке 6.4 приведен пример: в верхней части ячейки Jupyter написан код создания экземпляра класса Member, а ниже показан результат его выполнения. 260 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.4. Создание участника из класса Member в ячейке Jupyter Как видно на рисунке, первая строка вывода выглядит так: <__main__.Member object at 0x000002175EA2E160> Эта запись сообщает, что переменная new_guy действительно является объектом, созданным из класса Member. Число после at указывает адрес объекта в памяти компьютера. Вам не нужно запоминать или разбираться в этом значении — оно служит только для внутренней работы Python. Следующие строки вывода: Rambo Rocco Moe <class '__main__.Member'> Здесь первая строка отображает атрибут user_name объекта new_guy, вторая строка — его атрибут full_name, а последняя строка показывает тип объекта. Из нее видно, что new_guy действительно является экземпляром класса Member. ОСТОРОЖНО Чтобы не путаться в терминах, стоит знать: в Python слова «объект» и «свойство» часто используются как синонимы слов «экземпляр» и «атрибут». То есть экземпляр new_guy класса Member можно смело называть объектом. А его атрибуты full_name и user_name можно называть свойствами объекта. Таким образом, если вы услышите, что кто-то го- ГЛАВА 6 Python и классы 261
ворит «свойства объекта» вместо «атрибуты экземпляра», — знайте, речь идет об одном и том же. Разобраться во всех этих терминах и концепциях может быть непросто, но запомните самое важное: объект — это просто удобный способ собрать воедино информацию о какой-то сущности, которая принадлежит к определенному типу. Все такие сущности чем-то похожи между собой (например, все собаки остаются собаками, а все автомобили — автомобилями), но каждая из них отличается своими уникальными характеристиками. Эти различия задаются атрибутами объекта: не все собаки принадлежат к одной породе, и не все машины окрашены в один цвет. В примерах мы специально использовали разные имена для параметров (uname, fname) и для атрибутов (user_name, full_name). Это помогает визуально отделить данные, которые передаются в конструктор, от данных, которые сохраняются внутри объекта. Но это не строгое правило. На практике программисты часто называют параметры и атрибуты одинаково — и это тоже абсолютно нормально. Вместо того чтобы использовать сокращенные имена параметров, такие как uname и fname, можно просто назвать их так же, как и атрибуты — user_name и full_name. Такое совпадение имен никак не изменит поведение программы. Важно понимать, что здесь одно и то же имя выполняет две разные роли: • • сначала оно выступает в качестве параметра конструктора (заполнителя для данных, которые мы передаем при создании объекта); затем то же имя используется как атрибут объекта (self.user_ name и self.full_name), куда сохраняются эти данные . Различие видно по записи: параметр — это просто user_name, а атрибут — это self.user_name. На рисунке 6.5 приведен пример кода, где параметры конструктора были переименованы с uname на user_name и с fname на full_name. Результат работы программы остается точно таким же, как и раньше. Python совершенно не смущает тот факт, что одно и то же имя используется в разных контекстах. 262 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.5. Класс Member с параметрами и атрибутами user_name и full_name Когда вы начинаете вводить имя класса и открывающую скобку в VS Code, встроенная система подсказок IntelliSense сразу отображает список его параметров и первую строку документации. Это видно на рисунке 6.6. Именно поэтому так важно давать параметрам осмысленные имена и добавлять к классу строку документации. Эти элементы служат не только пояснением для других программистов, но и удобным напоминанием для вас самих: в будущем будет проще вспомнить, как правильно пользоваться этим классом. Рисунок 6.6. VS Code отображает подсказку при доступе к вашим пользовательским классам . Изменение значения атрибута Ранее вы видели, что у кортежей можно определять пары ключ:значение, похожие на пары атрибут: значение у экземпляров класса. Но есть важное ГЛАВА 6 Python и классы 263
различие: кортежи являются неизменяемыми (immutable). Это значит, что после их создания изменить их содержимое уже невозможно. С объектами дело обстоит иначе. После того как объект создан, вы можете в любой момент изменить значение любого его атрибута. Для этого используется очень простой синтаксис: object_name.attribute_name = value • • • object_name — имя объекта, который вы уже создали на основе класса; attribute_name — имя атрибута, который хотите изменить; value — новое значение, которое вы хотите присвоить атрибуту. На рисунке 6.7 показан пример: после создания объекта new_guy выполняется строка new_guy.user_name = "Princess" В результате атрибут user_name объекта new_guy меняется на Princess. При этом атрибут full_name остается прежним, так как в коде мы его не изменяли. Определение атрибутов со значениями по умолчанию При создании объекта совсем необязательно указывать значения для всех его атрибутов. Иногда удобно сразу задать некоторым атрибутам значения по умолчанию. Для этого внутри метода __init__ достаточно использовать привычный синтаксис: self.attribute_name = значение, где attribute_name — это имя атрибута, а значение — то, что вы хотите задать по умолчанию. Таким значением может быть, например: • • • True или False для логического признака; текущая дата; любое другое значение, которое Python может вычислить автоматически без вашего участия . Таким образом, каждый новый объект будет получать этот атрибут уже заполненным, даже если при его создании вы ничего специально не передавали. 264 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.7. Изменение значения атрибута объекта Предположим, вы хотите, чтобы у каждого нового участника автоматически фиксировалась дата создания аккаунта. Для этого можно завести атрибут date_joined. Кроме того, полезно иметь атрибут для контроля входа пользователей — например, is_active. Его можно использовать, чтобы активировать или деактивировать учетную запись. Логично, что при создании нового участника он должен быть активен, поэтому по умолчанию этот атрибут можно установить в значение True. Если вы планируете работать с датами и временем, нужно подключить модуль datetime. Обычно такие импорты помещают в самое начало файла, еще до определения класса. После этого вы можете добавить в класс новые атрибуты. Например: self.date_joined = dt.date.today() self.is_active = True • date_joined — чтобы автоматически сохранять дату создания нового участника . Для этого используем метод today() из модуля datetime; ГЛАВА 6 Python и классы 265
• is_active — чтобы отслеживать активность учетной записи . При создании нового участника этот атрибут можно сразу установить в True . Вот пример обновленного кода: import datetime as dt # Определяем новый класс с именем Member class Member: """ Создание нового участника. """ def __init__(self, user_name, full_name): # Определяем атрибуты и присваиваем им значения self.user_name = user_name # По умолчанию date_joined устанавливается на сегодняшнюю дату self.date_joined = dt.date.today() # Изначально устанавливаем is_active в True self.is_active = True ОСТОРОЖНО Важно помнить: если вы забудете импортировать модуль datetime в начале программы, при запуске получите сообщение об ошибке. В нем Python укажет, что не знает, что такое dt.date.today(). Исправить это просто: добавьте строку импорта import datetime as dt в начало вашего кода и выполните его еще раз. После этого программа будет работать корректно. Для атрибутов date_joined и is_active не требуется передавать какие-либо дополнительные данные при создании объекта — они получают значения автоматически из кода. Такое значение называется значением по умолчанию: оно присваивается атрибуту в момент создания объекта. Разумеется, значение по умолчанию можно изменить в любой момент, как и у любого другого атрибута. Для этого используется привычный синтаксис: object_name.attribute_name = value Например, допустим, вы используете атрибут is_active, чтобы отмечать, может ли пользователь входить на ваш сайт. Если один из участников окажется назойливым троллем, и вы решите запретить ему доступ, достаточно присвоить атрибуту is_active значение False: new_member.is_active = False 266 РАЗДЕЛ 2 Понимание базовых конструкций Python
СОХРАНЕНИЕ ИЗМЕНЕНИЙ ДАННЫХ Возникает логичный вопрос: зачем создавать все эти классы и объекты, если все исчезает сразу после завершения программы? Какой смысл заводить нового участника, если его данные нельзя сохранить и использовать, например, для контроля входа на сайт или других задач? На самом деле, временное существование объектов в памяти программы — это только часть истории . Все, что вы создаете и чем управляете с помощью классов и объектов, может быть сохранено в внешнем хранилище . Чаще всего для этого используют файлы или базы данных . Там данные могут храниться неограниченно долго, и вы сможете получать к ним доступ в любое время . Вопрос постоянного хранения (или, как часто говорят, персистентности данных) мы подробно разберем позже, в третьем разделе книги . Но прежде, чем переходить к этой более сложной теме, важно хорошо освоить основы Python и научиться уверенно работать с классами и объектами . Добавление методов в класс У любого объекта, который вы создаете, могут быть атрибуты — данные об этом объекте. Например, у собаки это может быть порода и цвет, у автомобиля — марка и модель. Но кроме данных объекты могут обладать и поведением. Собаки умеют есть, спать и лаять, а автомобили — ехать, останавливаться и поворачивать. В Python такое «поведение» реализуется с помощью методов. Метод — это по сути та же функция, о которых мы говорили в предыдущей главе. Разница в том, что метод привязан к определенному классу и его объектам. Отличить методы от атрибутов просто: у методов после имени всегда идут круглые скобки. Чтобы добавить метод в свой класс, используйте следующий синтаксис: def ethod_name (self[, param1, param2, ...]): • Вместо method_name придумайте подходящее имя метода . По соглашению в Python оно должно быть записано строчными буквами, без пробелов (при необходимости можно использовать подчеркивания) . ГЛАВА 6 Python и классы 267
• • ЗАПОМНИТЕ Параметр self всегда указывается первым и служит ссылкой на конкретный объект, к которому относится метод . После self вы можете добавить другие параметры через запятую — так же, как в обычных функциях . Не путайте квадратные скобки в приведенном синтаксисе с частью самого кода. Они показаны только для того, чтобы подчеркнуть: параметры после self можно указывать, а можно и не указывать. В реальном коде никаких квадратных скобок писать не нужно. Предположим, вы хотите добавить в класс метод .show_date_joined(). Его задача — возвращать отформатированную строку, в которой указаны имя участника и дата его регистрации. Определить такой метод можно следующим образом: # Метод для возврата отформатированной строки с датой регистрации def show_date_joined(self): return f"{self.full_name} joined on {self.date_ joined:%m/%d/%y}" Здесь: • • • имя метода — show_date_joined; при вызове метод формирует строку, в которой объединяются полное имя участника (self.full_name) и дата регистрации (self. date_joined); формат даты задается через %m/%d/%y, то есть месяц/день/год . Чтобы вызвать метод у объекта, используется следующий синтаксис: object_name.method_name() • • • object_name — имя объекта, к которому вы обращаетесь; method_name — имя метода, который вы хотите запустить; круглые скобки обязательны (и без пробелов внутри) . Если в методе класса определен только параметр self, то при вызове вы ничего не передаете — Python автоматически подставит текущий объект. Но если метод принимает дополнительные параметры, их нужно указать в скобках, точно так же, как при вызове обычной функции. На рисунке 6.8 приведен полный пример: создается объект и затем у него вызывается метод, который возвращает данные в нужном формате. 268 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.8. Изменение значения атрибута объекта Обратите внимание: как видно на рисунке 6.8, метод show_date_joined() находится внутри класса. Его определение имеет такой же отступ, как и метод __init__, а строки кода внутри метода сдвинуты еще на один уровень. Вне класса мы создаем объект с помощью строки: new_guy = Member('Rambo', 'Rocco Moe') Теперь у объекта new_guy есть доступ ко всем методам класса. Когда мы вызываем new_guy.show_date_joined() метод возвращает отформатированную строку, например: Rocco Moe joined 11/18/20, где дата соответствует дню запуска программы. Передача параметров в методы В методы можно передавать данные точно так же, как и в обычные функции — указывая параметры в круглых скобках. Главное помнить: параметр self всегда идет первым и служит для ссылки на объект, поэтому никаких значений ему передавать не нужно. ГЛАВА 6 Python и классы 269
Например, давайте создадим метод .set_active(), который будет управлять состоянием участника. Этот метод принимает один аргумент (True или False) и присваивает его атрибуту .is_active. Код метода может выглядеть так: # Метод для активации (True) или деактивации (False) аккаунта. def set_active(self, yesno): """ Установить активность: True — активный, False — деактивирован """ self.is_active = yesno Строка документации внутри метода не является обязательной. Тем не менее она может оказаться полезной: при вводе кода в VS Code вы будете видеть подсказку, напоминающую, какие значения допустимо передавать методу. При вызове метод ничего не выводит на экран — его задача только изменить значение атрибута .is_active у конкретного объекта на то, что вы указали в параметре yesno. ЗАПОМНИТЕ Важно понимать разницу между функцией и методом. Метод — это та же функция, но привязанная к определенному классу. Функции можно использовать в любом месте программы, а методы существуют только как часть класса и работают с его объектами. На рисунке 6.9 приведен полный код класса вместе с тестирующим примером. Сначала строка new_guy = Member('Rambo', 'Rocco Moe') создает новый объект-участник с именем new_guy. Затем команда print(new_guy. is_active) выводит значение атрибута .is_active. Так как при создании всех новых участников этому атрибуту автоматически присваивается значение True, на экране отобразится именно оно. Строка new_guy.set_is_active(False) вызывает метод set_is_active() для объекта new_guy и передает ему логическое значение False. В результате атрибут .is_active, который по умолчанию равнялся True, изменяется на False. Далее команда print(new_guy.is_active) подтверждает, что вызов метода действительно сработал: теперь значение атрибута is_active у объекта new_guy равно False. 270 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.9. Добавление и тестирование метода set_is_active() Вызов метода класса через имя класса До этого момента вы вызывали методы объекта так: specificobject.method() Это привычный и самый часто используемый способ. Однако в Python есть и альтернатива: можно вызвать метод через имя самого класса. Такой синтаксис может сделать код более наглядным, особенно если вы хотите подчеркнуть принадлежность метода к конкретному классу: Classname.method(specificobject) ГЛАВА 6 Python и классы 271
Здесь: • • • ClassName — это имя класса (по соглашению оно всегда пишется с заглавной буквы); method — имя метода, который вы хотите вызвать; specific_object — объект этого класса, который вы уже создали и к которому будет применен метод . Например, давайте создадим нового участника с именем wilbur с помощью класса Member: wilbur = Member('wblomgren', 'Wilbur Blomgren') Здесь wilbur — это конкретный объект (экземпляр класса Member), который мы только что создали. Теперь у этого объекта доступны все атрибуты и методы класса. Чтобы вызвать метод show_date_joined() и увидеть дату регистрации участника, используем уже знакомый синтаксис: print(wilbur.show_date_joined()) Существует и альтернативный способ вызвать метод: не через сам объект, а напрямую через имя класса. Для этого нужно указать имя класса, затем имя метода, а в круглых скобках передать конкретный объект, для которого метод должен выполниться. Например, тот же метод show_date_joined() можно вызвать так: print(Member.show_date_joined(wilbur)) Результат будет точно таким же, как и при вызове wilbur.show_date_ joined(). На экране отобразится строка с именем участника и датой его регистрации (разумеется, дата будет зависеть от того, когда вы запустите код): Wilbur Blomgren joined on 11/18/24 Этот второй способ вызова метода не является ни быстрее, ни медленнее, ни лучше, ни хуже первого. Он ничем не отличается по сути — это просто альтернативный синтаксис. Некоторые разработчики предпочитают именно его, потому что строка, начинающаяся с Member, сразу ясно указывает, что метод show_date_ joined() принадлежит классу Member. Такой стиль делает код более читаемым и понятным — как для других программистов, так и для вас самих через какое-то время, когда вы будете просматривать собственное приложение и могли бы уже забыть детали его написания. 272 РАЗДЕЛ 2 Понимание базовых конструкций Python
Использование переменных класса До этого момента мы рассматривали примеры атрибутов, которые называются переменными экземпляра. Такое название объясняется тем, что они принадлежат каждому отдельному объекту (экземпляру класса) и могут отличаться от экземпляра к экземпляру. Например, в классе Dog у одной собаки атрибут dog.breed может содержать значение «Пудель», а у другой — «Шнауцер». То есть каждое значение хранится индивидуально для конкретного объекта. Помимо переменных экземпляра, которые мы рассматривали ранее, в Python существуют еще и переменные класса. Они принадлежат не отдельному объекту, а самому классу и автоматически применяются ко всем его экземплярам. Главное отличие: переменные экземпляра всегда создаются и изменяются через self, то есть привязываются к конкретному объекту. А переменные класса объявляются один раз внутри класса, но вне методов — обычно выше метода __init__. Для них используется обычный синтаксис: variablename = value Где variablename — это выбранное вами название, а value — то, что вы хотите сохранить в этой переменной. Например, предположим, что при регистрации каждому новому участнику вы хотите давать три месяца (90 дней) бесплатного доступа. Вы пока не уверены, останется ли это правилом навсегда, поэтому не стоит «зашивать» число 90 в разных местах кода. Вместо этого удобнее вынести его в переменную класса. Так значение будет автоматически применяться ко всем экземплярам, а при необходимости вы сможете изменить его всего в одном месте: # Определяем класс Member для создания объектов-участников class Member: """Создание объекта-участника""" free_days = 90 def __init__(self, user_name, full_name): Здесь free_days — это переменная класса, потому что она объявлена внутри класса, но не принадлежит ни одному из методов. Предположим, что вам нужно автоматически рассчитывать дату окончания бесплатного пробного периода для каждого нового участника. ГЛАВА 6 Python и классы 273
В вашем классе уже есть атрибут date_joined, который хранит дату регистрации. Теперь можно добавить еще один атрибут — free_expires, в котором будет записана дата окончания бесплатного доступа. Чтобы вычислить эту дату, нужно просто прибавить количество бесплатных дней (free_days) к дате регистрации. Логично может показаться, что это делается вот так: self.free_expires = dt.date.today() + dt.timedelta(days=free_days) Если запустить такой код, возникнет ошибка: имя free_days не найдено. Дело в том, что внутри метода Python не подставляет автоматически атрибуты класса по «голому» имени. Чтобы обратиться к переменной класса, нужно указать имя класса или обратиться через экземпляр. Правильно так: self.free_expires = dt.date.today() + dt.timedelta(days=Member. free_days) На рисунке 6.10 представлена более полная версия примера. Из исходного класса удалены некоторые строки, чтобы сосредоточиться на новых элементах. В начале класса задается переменная: free_days = 365 Она определяет количество дней бесплатного периода — в данном случае целый год. (В предыдущем примере мы использовали 90, но суть остается той же: вы можете выбрать любое число дней.) Далее в методе __init__ используется конструкция: self.free_expires = dt.date.today() + dt.timedelta(days=Member. free_days) Здесь к текущей дате прибавляется количество дней, указанное в переменной free_days. В результате у каждого нового объекта автоматически появляется атрибут free_expires, указывающий дату окончания бесплатного периода. Если создать участника, например: wilbur = Member("wblomgren", "Wilbur Blomgren") 274 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.10. Переменная free_days является переменной класса в классе Member Что, если позже вы решите, что трех месяцев бесплатного доступа слишком много, и достаточно будет 90 дней? В этом случае не нужно переписывать весь класс заново. Достаточно изменить значение переменной класса прямо в коде: # Устанавливаем новое значение по умолчанию для бесплатных дней Member.free_days = 90 После выполнения этой строки переменная free_days для класса Member будет равна 90. Теперь при создании нового объекта, например: wilbur = Member("wblomgren", "Wilbur Blomgren") у участника по-прежнему появятся атрибуты date_joined и free_expires. Но в отличие от прежнего варианта, где дата окончания бесплатного периода вычислялась как 365 дней после регистрации, теперь wilbur.free_expires будет установлено на 90 дней позже date_joined. Использование методов класса Напомним, что метод — это функция, привязанная к определенному классу. До этого мы работали с методами экземпляра, например .show_ date_joined() или .set_is_active(). Такие методы всегда вызываются у конкретного объекта (экземпляра класса), и их работа связана именно с этим объектом. ГЛАВА 6 Python и классы 275
Однако в Python можно создавать и методы класса. Как видно из названия, они принадлежат самому классу целиком, а не отдельному экземпляру. То есть метод класса работает с классом в целом и может использоваться для выполнения действий, которые одинаково относятся ко всем его объектам. По своей сути методы класса похожи на переменные класса: они не зависят от конкретного объекта, а применяются ко всему классу сразу. Как и в случае с переменными класса, при определении методов класса не используется ключевое слово self. Напомним, что self всегда ссылается на конкретный объект (экземпляр класса), а не на класс в целом. Поэтому, если вы хотите, чтобы метод работал именно с классом, а не с отдельным экземпляром, определять его как def name(self): нельзя — в таком случае метод сразу «привязывается» к конкретному объекту. Вместо этого для методов класса используется особый синтаксис, который мы рассмотрим далее. К сожалению, для создания метода класса недостаточно просто убрать слово self. Так работать не будет. Чтобы определить метод класса, необходимо указать специальную строку перед его определением: @classmethod Символ @ в начале означает, что к функции применяется декоратор. В данном случае это декоратор classmethod. Не пугайтесь нового термина — «декоратор» просто обозначает особый способ изменить поведение функции или метода. Здесь он сообщает Python, что создаваемый метод будет относиться ко всему классу в целом, а не к его отдельным экземплярам. Декоратор — это инструмент, который изменяет или расширяет функциональность объекта, к которому он применяется. В случае с @classmethod он делает так, что метод работает не с конкретным экземпляром, а со всем классом сразу. После строки с декоратором нужно определить сам метод. Общий синтаксис выглядит так: def method_name(cls, x, ...): Здесь: • 276 method_name — это имя метода, которое вы выбираете сами (как обычно, строчными буквами, без пробелов); РАЗДЕЛ 2 Понимание базовых конструкций Python
• • cls — обязательный первый параметр . Он играет ту же роль, что и self в методах экземпляра, только относится не к объекту, а к самому классу . После cls вы можете указать любые дополнительные параметры, которые будут использоваться внутри метода, разделяя их запятыми, как в обычной функции . Предположим, вы хотите добавить метод, который будет задавать количество бесплатных дней для всех участников сразу. Такой метод полезен тем, что позволяет изменить значение переменной класса free_days еще до создания новых объектов. В результате все объекты получат одинаковое значение по умолчанию. Ниже приведен пример такого метода класса. Сначала мы создаем переменную класса free_days, а затем определяем метод для ее изменения: # Методы класса всегда идут с декоратором @classmethod # и используют cls вместо self @classmethod def set_free_days(cls, days): cls.free_days = days Этот код сообщает Python, что при вызове метода set_free_days() у класса необходимо установить значение переменной класса free_days равным числу, которое вы передадите в качестве аргумента. Проще говоря, метод обновляет общее значение free_days для всего класса, и это новое значение будет применяться ко всем вновь создаваемым объектам. На рисунке 6.11 показан полный пример кода в ячейке Jupyter (вы можете набрать его сами и запустить) вместе с результатами его выполнения. Рисунок 6.11. Метод set_ free_days() является методом класса Member ГЛАВА 6 Python и классы 277
ЗАПОМНИТЕ Важно помнить, что в Python заглавные и строчные буквы имеют значение. Обычно программисты используют строчные буквы — так пишется большинство имен переменных и функций. Однако для имен классов действует особое правило: по соглашению они всегда начинаются с заглавной буквы. Это значит, что при обращении к классу вы тоже должны использовать заглавную букву в его имени. Использование статических методов Когда вы уже начинаете думать, что все о классах стало ясно, оказывается, у них есть еще один тип методов. В Python он называется статическим методом. Такой метод создается с помощью декоратора @staticmethod. На первый взгляд здесь все просто. Главное отличие статического метода от методов экземпляра и методов класса заключается в том, что он не привязан ни к конкретному объекту (экземпляру класса), ни к самому классу целиком. По сути, статический метод — это обычная функция, которую помещают внутрь класса только для того, чтобы логически связать ее с этим классом. В тех случаях, когда вам нужен статический метод, перед его определением нужно указать строку @staticmethod. Под ней вы описываете сам метод — так же, как и любой другой, но без параметров self или cls, потому что статический метод не привязан ни к конкретному объекту, ни к самому классу. Вот простой пример статического метода, который возвращает текущее время: @staticmethod def current_time(): now = dt.datetime.now() return f"{now:%I:%M %p}" Теперь у нас есть метод current_time(), который не принимает никаких аргументов и никак не зависит ни от конкретного объекта, ни от самого класса. Его задача проста: получить текущую дату и время с помощью строки now = dt.datetime.now() и вернуть результат в удобном для чтения формате, например 12:00 PM. На рисунке 6.12 показан полный пример: статический метод оформлен ближе к концу класса. Когда вы вызываете его из кода вне класса, например так: print(Member.current_time()) 278 РАЗДЕЛ 2 Понимание базовых конструкций Python
он корректно возвращает текущее время, и при этом вам не нужно создавать объект класса или указывать какие-либо параметры. Рисунок 6.12. Класс Member теперь имеет статический метод с названием current_ time() Понимание наследования классов Приверженцы объектно ориентированного программирования часто говорят о наследовании классов, подклассах и других похожих терминах. Для новичков это может звучать слишком абстрактно, но в Python (и в жизни) эта концепция встречается постоянно. Ранее мы сравнивали ДНК собаки с классом в Python: это как чертеж или фабрика, которая определяет общие свойства и поведение для всех собак. При этом каждая собака уникальна, но все они принадлежат к одному большому классу — Dog. Именно это и отражает идея наследования: разные объекты могут иметь собственные особенности, но при этом все они остаются представителями одного класса. На рисунке 6.13 это показано на примере класса Dog, который объединяет всех собак. ГЛАВА 6 Python и классы 279
Рисунок 6.13. Собаки как объекты класса Dog Каждая собака уникальна (хотя, конечно, ни одна не сравнится с вашей). Тем не менее то, что объединяет всех собак, — это общие характеристики, которые они наследуют от класса Dog. Концепции классов и наследования, которые используются в Python и других объектно ориентированных языках программирования, придуманы не для того, чтобы усложнить жизнь программистам. Наоборот, это удобный способ хранить, организовывать и понимать информацию. Ведь большую часть знаний об окружающем мире проще описывать через классы, подклассы, под-подклассы и т.д. — вплоть до конкретных объектов. Например, собаки — это далеко не единственные животные с «собачьими» признаками. В природе есть и другие похожие существа: волки, койоты, шакалы. Все они имеют схожие характеристики, потому что принадлежат к более общему классу, который можно назвать Псовые (Canidae). На рисунке 6.14 показано, как разные животные наследуют общие черты этого класса. δòτωω οχíñòτωω Рисунок 6.14. Несколько разных видов животных похожи на собак 280 ξêüυñöý РАЗДЕЛ 2 Понимание базовых конструкций Python
Если продолжить нашу аналогию с собаками, то можно подняться еще на несколько уровней выше. Все псовые относятся к классу млекопитающих, потому что они рожают живых детенышей и кормят их молоком. Все млекопитающие, в свою очередь, входят в более широкий класс животных. А животные, как и растения, — это часть огромного царства живых организмов. Таким образом, собака обладает всеми своими свойствами не только потому, что она относится к классу Dog, но и потому, что она наследует характеристики всех предыдущих уровней. Она является собакой, потому что унаследовала признаки псовых. Она — псовое, потому что унаследовала признаки млекопитающих. Она — млекопитающее, потому что унаследовала признаки животных. И наконец, она — животное, потому что унаследовала свойства живых существ. ПРИМЕЧАНИЕ Прежде чем биологи начнут поправлять нас, сразу уточним: да, мы знаем, что Mammalia — это настоящий биологический класс, Canis — это род, а дальше идут виды. Но в нашей книге мы используем слова «класс» и «подкласс» не строго в биологическом смысле, а для того, чтобы провести параллель с миром программирования. Наша цель — показать, что и в Python работает похожая система: есть классы, из которых можно создавать подклассы и конкретные объекты. Поэтому, пожалуйста, воспринимайте биологическую аналогию как удобный образ, а не как точный урок по зоологии. Разумеется, эта идея не ограничивается только собаками. В животном мире есть и другие примеры. Возьмем кошек. С одной стороны — это домашний котенок по имени Бутси, с которым приятно играть или спать в обнимку. С другой — представители семейства кошачьих покрупнее: львы, тигры или ягуары. Согласитесь, вряд ли вам захочется делить кровать с кем-то из них. СОВЕТ Если вы поищете в интернете «иерархия живых существ» и откроете вкладку «Изображения», вы увидите множество схем классификации природы. На них хорошо видно, как наследование свойств идет от общего к частному: от крупных категорий к конкретному организму. Это именно та идея, которая лежит в основе наследования классов в Python. Даже аналогия с автомобилями отлично вписывается в концепцию наследования. На самом верху иерархии находятся транспортные средства. Ниже — лодки, самолеты и автомобили. Под автомобилями можно выделить легковые машины, грузовики, фургоны и т.д., вплоть до конкретного автомобиля на улице. Таким образом, классы и подклассы — это не изобретение программистов, а удобный способ описать то, как устроен наш мир. Новое здесь ГЛАВА 6 Python и классы 281
только то, что мы должны объяснить эту логику компьютеру — машине, которая ничего не понимает сама по себе, а просто следует нашим инструкциям. С точки зрения Python, самый простой способ реализовать наследование — создать подклассы внутри существующего класса. Родительский класс определяет общие характеристики для всех своих объектов. Подклассы же описывают особенности, которые присущи только им, не заменяя и не нарушая того, что наследуется от родительского класса. Создание базового (основного) класса Подклассы наследуют все атрибуты и методы у так называемого базового класса (его еще называют родительским классом). На самом деле базовый класс — это просто обычный класс Python, ничем не отличающийся по синтаксису от тех, что мы уже рассматривали. Чтобы показать принцип работы наследования, давайте снова используем класс Member, но оставим в нем только самое необходимое. Это позволит сосредоточиться именно на наследовании, а не на дополнительных деталях. Пример базового класса: # Класс Member будет использоваться как базовый для всех типов пользователей import datetime as dt class Member: """ Атрибуты и методы класса Member предназначены для всех участников """ # По умолчанию срок действия учетной записи — 365 дней (1 год) expiry_days = 365 # Конструктор для инициализации нового объекта-участника def __init__(self, first_name, last_name): # Атрибуты экземпляра, общие для всех участников self.first_name = first_name self.last_name = last_name # Автоматический расчет даты окончания действия учетной записи self.expiry_date = dt.date.today() + dt.timedelta(days=self.expiry_days) По умолчанию срок действия новой учетной записи истекает через один год. Именно поэтому в классе сначала задается переменная класса expiry_days со значением 365. Она используется в дальнейшем коде для вычисления даты окончания действия учетной записи на основе теку- 282 РАЗДЕЛ 2 Понимание базовых конструкций Python
щей даты. Позже вы увидите, что выбор переменной класса здесь неслучаен — ее значение легко можно будет переопределить в подклассах. Чтобы пример оставался простым и наглядным, в этой версии класса Member конструктор принимает только два параметра: first_name и last_ name. На рисунке 6.15 приведен пример тестирования кода с условным участником по имени Джо. При выводе его атрибутов first_name, last_name и expiry_date программа демонстрирует ожидаемое поведение: для объекта с именем Joe Anybody атрибут expiry_date устанавливается на один год позже текущей даты запуска программы. Рисунок 6.15. Упрощенный класс Member Теперь представим, что ваша задача — создать два разных типа участников: администраторов (Admins) и обычных пользователей (Users). При этом и те и другие должны обладать одинаковым набором атрибутов, которые уже определены в базовом классе Member. Определяя Admin и User как подклассы Member, вы автоматически наделяете их всеми свойствами и методами, описанными в базовом классе. Таким образом, нет необходимости заново прописывать код для имени, фамилии или даты окончания учетной записи — все это они унаследуют от Member. ГЛАВА 6 Python и классы 283
Определение подкласса Чтобы создать подкласс, важно помнить две вещи: 1. Подкласс должен располагаться ниже базового класса в коде . 2. Он пишется без отступов, так как не является частью базового класса . Синтаксис очень простой: class subclass_name(baseclass_name): • • Вместо SubclassName укажите имя нового подкласса . Вместо BaseclassName используйте имя базового класса, который вы определили ранее . Например, если у нас есть базовый класс Member, то для создания подкласса Admin мы пишем: class Admin(Member): А чтобы добавить еще один подкласс с именем User: class User(Member): Если оставить классы пустыми, Python выдаст ошибку, потому что внутри них нет ни одной инструкции. Чтобы этого избежать, можно использовать ключевое слово pass. Оно служит как заглушка и сообщает интерпретатору: «Да, класс пока пустой, но все в порядке, продолжай работать». Чтобы не забыть назначение каждого класса, полезно добавить комментарии: # Подкласс для администраторов class Admin(Member): pass # Подкласс для пользователей class User(Member): pass При создании подклассов вам не нужно напрямую ссылаться на базовый класс Member. Все, что определено в классе Member, автоматически наследуется классами Admin и User. Это значит, что вы можете создавать объекты этих подклассов точно так же, как если бы они были экземплярами базового класса. 284 РАЗДЕЛ 2 Понимание базовых конструкций Python
Например, вот как можно создать администратора по имени Энни: ann = Admin('Annie', 'Angst') Аналогично, чтобы создать обычного пользователя, используйте класс User: uli = User('Uli', 'Ungula') На рисунке 6.16 мы добавили несколько операторов print, чтобы наглядно показать результат создания двух объектов. В примере Энни создается как администратор (Admin), а Юли — как обычный пользователь (User). Несмотря на то что это разные подклассы, оба они автоматически наследуют все атрибуты, определенные в базовом классе Member. Код базового класса Member расположен выше в примере, поэтому здесь мы его не повторяем — он остается без изменений. Рисунок 6.16. Создание и тестирование классов администратора и пользователя Теперь вы видите, что подклассы наследуют все параметры, которые принимает базовый класс, и присваивают их атрибутам точно так же, как это делает сам класс Member. Однако на данном этапе Admin и User ГЛАВА 6 Python и классы 285
фактически ничем не отличаются друг от друга — по сути, это просто объекты того же класса Member. В реальных приложениях, конечно, администраторы и обычные пользователи обычно имеют разные возможности и поведение. В следующих разделах мы рассмотрим, как именно можно добавить такие различия и расширить функциональность подклассов. Переопределение значения по умолчанию в подклассе Одним из самых простых способов использовать подклассы является переопределение значения атрибута, заданного по умолчанию в базовом классе. Например, в классе Member мы определили переменную класса expiry_ days, которая используется для вычисления даты окончания срока действия учетной записи. Но представим, что для администраторов вы хотите задать другое правило: их учетные записи никогда не должны истекать (или должны иметь нереально большой срок действия, чтобы технически все же существовала дата окончания). Чтобы изменить поведение подкласса, можно задать новое значение переменной, которая уже определена в базовом классе. В таком случае оно автоматически переопределит исходное. Например, в нашем базовом классе Member переменная expiry_days равна 365, то есть срок действия учетной записи составляет один год. Но для администраторов мы хотим другое правило: их аккаунты не должны истекать в течение жизни пользователя. Для этого достаточно изменить значение переменной expiry_days прямо в подклассе Admin: # Подкласс для администраторов class Admin(Member): # Учетные записи администраторов не истекают в течение 100 лет expiry_days = 365.2422 * 100 Теперь при создании объекта Admin для расчета даты окончания будет использоваться 100 лет (365.2422 дней × 100). Обычные пользователи по-прежнему получают срок действия в 365 дней, а администраторы — столетний «запас». 286 РАЗДЕЛ 2 Понимание базовых конструкций Python
Добавление новых параметров в подкласс Иногда у объектов подкласса появляются характеристики, которых нет у базового класса. В такой ситуации нужно добавить в подкласс собственный параметр и сохранить его в атрибуте. Это немного сложнее, чем простая замена значения по умолчанию, но такой прием используется очень часто, поэтому важно с ним познакомиться. В следующем примере мы шаг за шагом разберем, как можно добавить новый параметр в подкласс, сохранив при этом все возможности, унаследованные от базового класса. Когда у подкласса появляются новые данные, которых нет в базовом классе, нужно определить собственный метод __init__. В нем сохраняется все, что было у конструктора базового класса, а также добавляются новые параметры. Например, представим, что у администраторов есть секретный код. Этот код должен задаваться при создании объекта класса Admin. Но при этом нам все равно нужно сохранять имя и фамилию администратора, как это делается в классе Member. В таком случае строка с конструктором в подклассе будет выглядеть так: def __init__(self, first_name, last_name, secret_code): Уровень отступа будет таким же, как у строк выше. Когда вы добавляете собственный метод __init__ в подкласс, важно не забыть про параметры базового класса. Их тоже нужно инициализировать. Но напрямую копировать код из базового класса не стоит — это было бы неудобно и нарушало бы принцип повторного использования. Вместо этого в Python применяется специальная функция super(). Она позволяет обратиться к базовому классу и вызвать его метод __init__, передав туда необходимые параметры. Синтаксис выглядит так: super().__init__(param1, param2, ...) Обратите внимание на использование super(). Это ключевое слово ссылается на родительский класс (его также называют суперклассом). Когда вы пишете super(), Python как бы «поднимается» к базовому классу и обрабатывает текущий объект так, будто он был создан из родительского класса, а не из подкласса. ГЛАВА 6 Python и классы 287
Благодаря этому вы можете вызывать методы суперкласса, не дублируя его код в подклассе. Такой подход делает ваш код короче, понятнее и легче для поддержки. Вместо param1, param2 и других заглушек вы должны подставить реальные имена параметров, которые ожидает базовый класс. То есть вы явно указываете, какие данные хотите передать родительскому классу. Важно помнить: при вызове super().__init__() вы указываете все параметры базового класса (кроме self), потому что self уже автоматически передается Python. В нашем примере базовый класс Member принимает только два параметра — first_name и last_name. Поэтому строка для вызова конструктора базового класса в подклассе Admin будет такой: super().__init__(first_name, last_name) Все параметры, которые не передаются в базовый класс, нужно назначить внутри подкласса вручную. Для этого используется привычная конструкция: self.secret_code = parametername Где attribute_name — это имя нового атрибута, который вы хотите добавить объекту, а parametername — соответствующий параметр из конструктора. В нашем примере базовый класс Member работает только с именем (first_ name) и фамилией (last_name). Подкласс Admin добавляет дополнительный параметр secret_code. Так как базовый класс о нем ничего не знает, мы сохраняем его прямо в объекте подкласса: self.secret_code = secret_code На рисунке 6.17 показан пример, в котором мы создали администратора по имени Энни и передали ей секретный код PRESTO. Вывод всех ее атрибутов подтверждает, что дата истечения срока действия учетной записи рассчитана правильно, а секретный код успешно сохранен. Для сравнения мы также создали обычного пользователя по имени Юли. Его данные никак не изменились от того, что мы внесли дополнительные настройки в класс Admin. Однако обратите внимание: у Юли пока нет секретного кода. Если попытаться вывести его прямо сейчас, Python вернет ошибку, так как в классе User этот атрибут не определен. 288 РАЗДЕЛ 2 Понимание базовых конструкций Python
Одно из возможных решений — просто помнить, что у обычных пользователей секретного кода нет, и никогда не пытаться его выводить. Но гораздо лучше будет предусмотреть такую ситуацию в коде. Для этого можно гарантировать, что каждый объект пользователя, независимо от его типа, имеет атрибут secret_code. У обычных пользователей он будет пустым, а у администраторов — содержать реальное значение. Такой подход избавляет от ошибок и делает логику программы более предсказуемой: только администраторы смогут работать с действительными секретными кодами, тогда как у обычных пользователей они всегда будут пустыми. Если участник сначала создается как обычный пользователь, то у него секретный код отсутствует (например, хранится как пустая строка). Но если позже этот же пользователь становится администратором, не нужно пересоздавать объект или менять класс. В Python достаточно просто обновить его атрибут secret_code, присвоив ему реальное значение вместо пустого. Рисунок 6.17. Подкласс Admin имеет новый параметр secret_code ГЛАВА 6 Python и классы 289
Если в вашем приложении класс назначает атрибут secret_code всем пользователям (а не только администраторам), вы сможете безопасно выводить его значение даже для обычных участников. В этом случае никакой ошибки не произойдет — просто секретный код для обычного пользователя будет отображаться как пустая строка (""). Чтобы задать такое поведение, можно добавить в конструктор базового класса Member строку, которая устанавливает значение по умолчанию: # По умолчанию у пользователя нет секретного кода self.secret_code = "" Таким образом, хотя в подклассе User мы ничего не делаем с атрибутом secret_code, ошибки при обращении к нему не возникнет. У каждого пользователя этот атрибут уже будет существовать благодаря базовому классу, просто его значение по умолчанию останется пустой строкой (""). На рисунке 6.18 показан полный пример: в коде определены оба подкласса (Admin и User), а затем выполняется попытка вывести uli. secret_code. Как и ожидалось, никакой ошибки нет — программа просто ничего не показывает, ведь секретный код обычному пользователю не присвоен. Мы оставили класс User только с оператором pass, чтобы код был максимально простым. В реальном проекте вы, скорее всего, добавите в этот подкласс собственные параметры или значения по умолчанию, специфичные для пользователей. Однако важно помнить: синтаксис остается тем же для всех подклассов, и принципы, рассмотренные в этом разделе, одинаково применимы ко всем вашим классам и их подклассам. 290 РАЗДЕЛ 2 Понимание базовых конструкций Python
Рисунок 6.18. Полные подклассы Admin и User ГЛАВА 6 Python и классы 291
Вызов метода базового класса Методы, которые вы определяете в базовом классе, работают одинаково как для самого базового класса, так и для всех его подклассов. Чтобы в этом убедиться, давайте добавим в конец класса Member новый метод show_expiry(self). Этот метод будет возвращать строку с именем участника и датой окончания срока его учетной записи: class Member: """ Атрибуты и методы класса Member предназначены для всех """ # По умолчанию новая учетная запись истекает через год (365 дней) expiry_days = 365 # Инициализация объекта участника def __init__(self, first_name, last_name): # Атрибуты (переменные экземпляра) для всех self.first_name = first_name self.last_name = last_name # Вычисление даты истечения срока от текущей даты self.expiry_date = dt.date.today() + dt.timedelta(days=self.expiry_days) # По умолчанию секретный код пустой self.secret_code = '' # Метод в базовом классе def show_expiry(self): return f"{self.first_name} {self.last_name} " + \ f"expires on {self.expiry_date}" Метод show_expiry() при вызове формирует и возвращает строку, в которой указаны имя и фамилия участника, а также дата окончания действия его учетной записи. Если оставить подклассы без изменений и выполнить такой код: ann = Admin('Annie', 'Angst', 'PRESTO') print(ann.show_expiry()) uli = User('Uli', 'Ungula') print(uli.show_expiry()) то на экране отобразятся имя и дата окончания срока для Энни и Юли. 292 РАЗДЕЛ 2 Понимание базовых конструкций Python
Пример возможного результата (точные даты будут зависеть от дня запуска программы): Annie Angst срок действия истекает 2124.12.04 Uli Ungula срок действия истекает 2024.12.04 Использование одного и того же имени дважды Иногда возникает вопрос: что произойдет, если использовать одинаковое имя метода и в базовом классе, и в подклассе? Ответ прост: Python всегда будет выполнять наиболее конкретную реализацию, то есть ту, которая определена в подклассе. Метод из базового класса будет вызван только в том случае, если в подклассе метода с таким именем нет. Рассмотрим пример: class Member: """ Атрибуты и методы класса Member """ # Конструктор: создает объект с именем и фамилией def __init__(self, first_name, last_name): # Атрибуты (переменные экземпляра) для всех self.first_name = first_name self.last_name = last_name # Метод в базовом классе def get_status(self): return f"{self.first_name} is a Member." # Подкласс для администраторов class Admin(Member): def get_status(self): return f"{self.first_name} is an Admin." # Подкласс для обычных пользователей class User(Member): def get_status(self): return f"{self.first_name} is a regular User." В нашем примере метод get_status() определен как в базовом классе Member, так и в двух его подклассах — Admin и User. Этот метод выводит имя участника и его статус. На рисунке 6.19 показан результат работы программы, где создаются три объекта: администратор, обычный пользователь и участник, который не принадлежит ни к одной из этих категорий. ГЛАВА 6 Python и классы 293
Рисунок 6.19. Три метода с одинаковым именем get_ status() При вызове метода get_status() Python выбирает именно ту версию метода, которая определена в классе данного объекта: • • • для объекта-администратора используется метод из класса Admin; для объекта-пользователя — метод из класса User; для обычного участника — метод из базового класса Member . Таким образом, один и тот же метод может работать по-разному в зависимости от типа объекта. Это называется переопределением методов и является одним из ключевых принципов объектно ориентированного программирования. 294 РАЗДЕЛ 2 Понимание базовых конструкций Python
В Python существует встроенная функция help(), которую можно вызвать для любого класса, чтобы получить справочную информацию о нем. Например, если в конце программы из примера на рисунке 6.19 добавить строку: help(Admin) и снова выполнить код, на экране появится подробное описание класса Admin. Этот вывод показан на рисунке 6.20. Пока вам не нужно вникать во все детали того, что отображается в справке. Главное, на что стоит обратить внимание, — это раздел «Порядок разрешения методов» (Method Resolution Order, MRO). Он показывает, в каком порядке Python ищет методы и атрибуты для объектов данного класса. Для Admin этот раздел выглядит так: Порядок разрешения методов: Admin Member builtins.object Порядок разрешения методов (MRO) определяет, где именно Python будет искать метод или атрибут, если у класса и его подклассов есть методы с одинаковыми именами (например, get_status). Когда вы вызываете get_status() для объекта класса Admin, Python действует так: 1. Сначала он проверяет сам класс Admin . Если метод найден здесь — он сразу же выполняется . 2. Если в Admin метод отсутствует, поиск продолжается в базовом классе Member . 3. Если и там метода нет, Python обращается к самому верхнему классу object, который является родителем для всех классов в Python и содержит ряд встроенных методов . Таким образом, при работе с иерархией классов Python всегда выбирает наиболее конкретную реализацию: • • • если метод определен в подклассе, используется именно он; если его нет, используется метод базового класса; если и там метода нет, остаются только встроенные методы класса object . ГЛАВА 6 Python и классы 295
Если же ни в одном из этих мест нужный метод не найден, Python выдаст ошибку (исключение). На практике чаще всего причина такой ошибки банальна — простая опечатка в имени метода. Рисунок 6.20. Вывод команды help(Admin) Пример встроенного метода — это __dict__. Название dict (сокращение от dictionary, словарь) заключено в двойные подчеркивания. Если выполнить команду: print(Admin.__dict__) ошибки не будет, даже несмотря на то, что мы сами никогда не определяли метод с таким именем. Это работает потому, что у всех классов есть встроенный атрибут __dict__. Он возвращает словарь, в котором содержатся все методы и атрибуты класса — как те, что вы определили сами, так и встроенные. Таким образом, порядок разрешения методов (MRO), о котором мы говорили ранее, — это тема, которой пока не стоит слишком глубоко заниматься. На начальном этапе достаточно помнить: если вы вызываете метод, которого Python не может найти ни в самом классе, ни в базовом классе, ни во встроенном классе object, то вы получите ошибку. Например, вот такой вызов: 296 РАЗДЕЛ 2 Понимание базовых конструкций Python
print(Admin.snookums()) завершится ошибкой: ---> print(Admin.snookums()) AttributeError: type object 'Admin' has no attribute 'snookums' Эта ошибка сообщает, что Python не знает, что такое snookums(). Как мы уже отмечали, в реальной практике подобные ошибки чаще всего возникают из-за опечатки или неправильного написания имени метода. Классы (и их подклассы) играют важную роль в Python. Освоив материал этой главы, вы сможете не только создавать собственные классы, но и лучше понимать код, написанный другими программистами. Прежде чем завершить этот раздел, нам нужно рассмотреть еще одну фундаментальную тему: как Python обрабатывает ошибки и какие средства предоставляет для управления ими прямо в вашем коде.
В этой главе вы узнаете: » Что такое исключения » Как корректно обрабатывать ошибки » Как сделать так, чтобы приложение не завершалось сбоем » Как использовать конструкции try… except…else…finally » Как вызывать собственные исключения Гл а в а 7 Обход ошибок М ы все хотим, чтобы программы всегда работали идеально. Но в реальности этого достичь невозможно: возникают ситуации, которые мешают выполнению кода. И проблема здесь не в вас или в самой программе. Чаще всего источник ошибок — это действия пользователей, которые делают что-то неправильно. Обработка ошибок нужна для того, чтобы заранее предусмотреть такие ситуации, перехватить возникающие ошибки и корректно уведомить пользователя о проблеме. Благодаря этому он сможет понять, что пошло не так, и исправить ситуацию. Важно понимать: методы обработки ошибок не предназначены для исправления ошибок в самом коде. Если вы допустили логическую ошибку или опечатку, ее нужно устранять самостоятельно. Речь идет исключительно об ошибках во время выполнения программы, которые невозможно полностью контролировать. Таким образом, обработка ошибок позволяет заменить непонятное техническое сообщение, которое выдал бы Python, на простое и понятное объяснение проблемы. В идеале вы также подскажете пользователю, что нужно сделать, чтобы исправить ситуацию. Помните: пользователи могут исправить только окружение программы, но не ваш код. 298 РАЗДЕЛ 2 Понимание базовых конструкций Python
Понимание исключений В Python, как и в большинстве других языков программирования, термин «исключение» обозначает ошибку, которая возникает либо в коде, либо во время выполнения программы. Иными словами, исключение — это все, что мешает корректной работе вашей программы. Чтобы увидеть простой пример, попробуем открыть файл с помощью Python. Для этого используется функция open(). Общий синтаксис выглядит так: name = open(filename) Здесь: • • name — это переменная, в которую вы сохраните ссылку на открытый файл (вы можете выбрать любое имя переменной); filename — это имя файла, который вы хотите открыть . Если файл находится в той же папке, что и сам Python-скрипт, путь к файлу указывать не нужно. В этом случае Python автоматически будет искать файл в текущей рабочей папке. На рисунке 7.1 показан пример в Visual Studio Code. В рабочей папке находятся два файла: • • showfilecontents.py — файл с кодом Python; people.csv — файл с данными, который мы собираемся открыть . Рисунок 7.1. Файлы showfilecontents.py и people.csv в папке в VS Code ГЛАВА 7 Обход ошибок 299
Файл showcontents.py содержит сам код на Python, а файл people.csv — это данные (информация о людях). На рисунке 7.2 показано содержимое файла people.csv: в верхней части — в Excel (для удобного чтения), а в нижней — в текстовом редакторе (именно так Python и другие языки видят его внутри). Содержимое файла сейчас не имеет большого значения. Важно другое: все, что вы будете изучать здесь, одинаково работает с любыми внешними файлами. Код Python для работы с этим файлом предельно простой — всего две строки (не считая комментариев): # Открываем файл, который находится в этой же папке the_file = open('people.csv') # Выводим имя файла print(the_file.name) Рисунок 7.2. Содержимое файла people. csv в Excel (сверху) и текстовом редакторе (снизу) Первая строка кода открывает файл с именем people.csv. Вторая строка выводит его имя на экран. Если вы запустите программу showfilecontents.py (например, щелкнув правой кнопкой мыши по файлу в VS Code и выбрав «Запустить файл Python в терминале»), то в консоли вы увидите название people.csv — при условии, что файл действительно находится в папке. Но что произойдет, если файл отсутствует? Такое бывает по разным причинам: файл могли не скопировать вовремя, его могли удалить или просто ошибиться в названии. Например, легко случайно набрать расширение .cvs вместо .csv. 300 РАЗДЕЛ 2 Понимание базовых конструкций Python
В любой из этих ситуаций программа не найдет файл и вызовет исключение, то есть завершится с ошибкой. В терминале вы увидите сообщение, подобное этому (как на рисунке 7.3): Traceback (most recent call last): File "c:/Users/acsimpson/Desktop/exceptions/ showfilecontents.py", line 2, in <module> the_file = open('people.csv') FileNotFoundError: [Errno 2] No such file or directory: 'people.csv' Traceback (обратная трассировка) — это подробный отчет Python об ошибке. Если бы программа вызвала несколько функций перед тем, как произошла ошибка, traceback показал бы всю цепочку этих вызовов — от последнего к более ранним. В нашем примере возникло только одно исключение, поэтому вывод довольно короткий. • • Строка File … line 2 сообщает, что ошибка возникла во втором ряду файла showfilecontents.py . Сразу после этого Python показывает саму строку кода, вызвавшую проблему: the_file = open('people.csv') Рисунок 7.3. Файл showfile contents. py вызывает исключение В последней строке traceback Python сообщает о самом исключении: FileNotFoundError: [Errno 2] No such file or directory: 'people.csv' ГЛАВА 7 Обход ошибок 301
Здесь FileNotFoundError — это название ошибки, указывающей на то, что указанный файл не найден. У многих исключений есть дополнительный код ошибки (в данном случае ERRNO 2). Этот код зависит от операционной системы и может отличаться на разных компьютерах. Поэтому при обработке ошибок обычно обращают внимание именно на название исключения — в нашем случае FileNotFoundError. Иными словами, главное, что нужно вынести из этого сообщения: программа не смогла найти файл people.csv, и именно это привело к сбою. ПРИМЕЧАНИЕ Иногда вы можете встретить выражение «throw an exception» («выбросить исключение») вместо «raise an exception» («вызвать исключение»). В контексте Python это одно и то же — оба выражения описывают ситуацию, когда программа сообщает об ошибке и передает управление механизму обработки исключений. В разных языках программирования используется разная терминология: например, в Java и C# чаще говорят throw, а в Python принято использовать ключевое слово raise. Но смысл у них одинаковый: программа сталкивается с проблемой и «поднимает флаг», чтобы ее можно было обработать. Последняя строка сообщения об ошибке — No such file or directory: 'people.csv' — однозначно говорит, что Python не может выполнить open('people.csv'), потому что в текущей папке нет файла с таким именем. Исправить ситуацию можно по-разному: изменить имя в коде или привести имя файла к ожидаемому. Поскольку расширение .csv — общепринятый формат «значения, разделенные запятыми», логичнее переименовать файл people.cvs в people.csv, чтобы он соответствовал тому, что ищет программа. Это и корректно по стандарту, и избавляет от лишних правок кода. Корректная обработка ошибок Чтобы обрабатывать ошибку отсутствующего файла более корректно, вместо стандартного сообщения об ошибке Python можно вывести понятный текст для пользователя. Для этого применяется конструкция try…except, которая позволяет «поймать» исключение и заменить его собственным сообщением. 302 РАЗДЕЛ 2 Понимание базовых конструкций Python
Базовый синтаксис выглядит так: try: # Действия, которые должен выполнить код except Exception: # Что делать, если действия выполнить не удалось Теперь перепишем пример showfilecontents.py с открытием файла people.csv: try: # Открыть файл и показать его имя the_file = open('people.csv') print(the_file.name) except Exception: print("Sorry, I don't see a file named people.csv here") Так как файл, который программа должна открыть, может отсутствовать, мы используем конструкцию try. Внутри этого блока выполняется попытка открыть файл. Если файл найден, программа продолжает работу и с помощью оператора print() выводит его имя. Однако если в процессе открытия возникает исключение (например, файл отсутствует), программа не завершает выполнение с типичной для Python «страшной» ошибкой. Вместо этого управление передается в блок except, и пользователю выводится понятное сообщение о проблеме. Такой подход делает программу более надежной и дружественной для начинающего пользователя, как это показано на рисунке 7.4. Рисунок 7.4. Файл showfilecontents.py перехватывает ошибку и отображает понятное сообщение ГЛАВА 7 Обход ошибок 303
Конкретизация исключений В нашем предыдущем примере программа корректно обрабатывала ситуацию, когда файл отсутствует. Однако в реальных приложениях может возникать множество разных ошибок, и иногда требуется реагировать на каждую из них по-разному. Чтобы сделать обработку более гибкой, можно использовать не один общий блок except, а несколько — для разных типов исключений. В следующих разделах мы рассмотрим, как именно это реализуется и чем полезен такой подход. Предположим, вы вручную изменили имя файла на people.csv, как и планировалось изначально. Как уже показывалось ранее, если код выполняется без ошибок, программа просто выводит имя файла. Теперь после строки, которая выводит имя файла, добавлена еще одна строка кода: try: # Открыть файл и показать его имя the_file = open('people.csv') print(the_file.name) print(the_file.wookems()) except Exception: print("Sorry, I don't see a file named people.csv here") Когда вы запускаете этот код, с именем файла все в порядке — программа выводит people.csv, как и ожидалось. Однако следующая строка кода print(the_file.wookems()) вызывает ошибку, поскольку в Python не существует метода с именем wookems(). Тем не менее сообщение об ошибке остается тем же, что и раньше, — программа сообщает, что не видит файла people.csv, хотя на самом деле проблема не в файле, а в вызове несуществующего метода. Результат выполнения кода: people.csv Sorry, I don't see a file named people.csv here Возникает закономерный вопрос: почему сообщение об ошибке говорит, что файл people.csv не найден, если мы точно знаем, что файл существует, а ошибка на самом деле вызвана другой строкой кода? Проблема заключается в строке except Exception: Эта конструкция означает: «Если в блоке try произойдет любое исключение, выполнить код, расположенный после except». 304 РАЗДЕЛ 2 Понимание базовых конструкций Python
Именно поэтому программа не различает, какая именно ошибка возникла — отсутствует ли файл, или, как в нашем случае, вызван несуществующий метод. Все ошибки обрабатываются одинаково, и выводится одно и то же сообщение. Чтобы решить эту проблему, нужно заменить общее исключение Exception на конкретный тип ошибки, который вы хотите перехватывать. Но как узнать, какое именно исключение нужно указать? Очень просто: если выполнить код без блока try…except, Python выведет сообщение об ошибке. Оно выглядит примерно так: FileNotFoundError: [Errno 2] No such file or directory: 'people. csv' Первое слово — FileNotFoundError — это название исключения. Именно его можно использовать вместо общего имени Exception. Пример исправленного кода: try: # Открыть файл и показать его имя the_file = open('people.csv') print(the_file.name) print(the_file.wookems()) except FileNotFoundError: print("Sorry, I don't see a file named people.csv here") Разумеется, обработка ошибки, связанной с отсутствием файла, не решает проблему с неправильным названием метода. Однако это уже не исключение, которое нужно перехватывать, а ошибка в коде, которую следует исправить вручную — заменив .wookems() на правильное имя метода. Зато теперь программа не выводит вводящее в заблуждение сообщение "Sorry, I don't see a file named people.csv here". Код выполняется корректно и выводит имя файла, как и ожидалось. Затем, дойдя до строки с вызовом несуществующего метода .wookems(), Python сообщает об ошибке, но уже правильно: AttributeError: 'TextIOWrapper' object has no attribute 'wookems' (объект не имеет атрибута 'wookems'). Именно это показано на рисунке 7.5. ГЛАВА 7 Обход ошибок 305
Рисунок 7.5. Показано правильное сообщение об ошибке И снова важно подчеркнуть: если вы задумались об обработке ошибки, вызванной методом .wookems(), — это не тот случай, когда стоит писать обработчик исключений. Исключения возникают тогда, когда происходит что-то внешнее, на что программа не может повлиять: например, отсутствует файл, нет доступа к Сети или переполнена память. А вот программные ошибки, такие как опечатки, неверные имена методов или логические просчеты, — это внутренние ошибки в коде. Их не нужно «обрабатывать» с помощью try…except; их следует исправлять программисту, чтобы программа работала корректно. СОВЕТ 306 Современные инструменты на базе генеративного искусственного интеллекта — такие как GitHub Copilot, ChatGPT, Claude .ai и другие — могут помочь вам написать обработчики исключений на Python. Достаточно просто попросить их, например: «Напиши обработчик исключения Python для случая, когда файл отсутствует». Если же вы хотите обработать другой тип ошибки, замените фразу «отсутствие файла» на описание нужного вам исключения — и ИИ сгенерирует подходящий пример кода. РАЗДЕЛ 2 Понимание базовых конструкций Python
Предотвращение сбоев приложения Вы можете объединять несколько операторов except в одном блоке try, чтобы обрабатывать разные типы ошибок. Помните: когда возникает исключение, Python проверяет обработчики по порядку сверху вниз. Как только он находит подходящий обработчик, выполнение переходит к нему, а остальные блоки except пропускаются. Если же возникнет исключение, для которого обработчик не предусмотрен, Python выведет стандартное сообщение об ошибке и завершит выполнение программы. Однако есть способ предотвратить даже такую ситуацию — чтобы приложение не «падало» из-за непредвиденных ошибок. Чтобы полностью избавиться от стандартных сообщений об ошибках Python, можно добавить универсальный обработчик исключений в конце блока try. Для этого используется строка except Exception:. Она означает: «Если произошла ошибка, которую не обработали предыдущие блоки except, используй этот обработчик». Другими словами, такой универсальный обработчик перехватывает любые исключения, не обработанные ранее. Вот пример, где предусмотрены два обработчика: один для ошибки отсутствия файла, а второй — для всех остальных случаев: try: # Открываем файл и показываем его имя the_file = open('people.csv') # Печатаем пару пустых строк, затем первую строку из файла print('\n\n', the_file.readline()) # Закрываем файл The_file.wookems() except FileNotFoundError: print("Sorry, I don't see a file named people.csv here") except Exception: print("Sorry, something else went wrong") При выполнении этого кода вывод будет следующим: ЗАПОМНИТЕ Username,FirstName,LastName,Role,DateJoined Sorry, something else went wrong ГЛАВА 7 Обход ошибок 307
Первая строка — это содержимое первой строки файла people.csv. Вторая строка — результат работы второго блока except, который выводит сообщение «Извините, что-то пошло не так» (Sorry, something else went wrong). Такое сообщение слишком общее и не помогает понять, что именно пошло не так. При написании собственных обработчиков исключений всегда следите за порядком их расположения. Код, обрабатывающий конкретные ошибки (например, FileNotFoundError), должен стоять перед общим обработчиком (Exception). Если сделать наоборот, общий обработчик перехватит все ошибки раньше, и программа не дойдет до нужного блока, который предназначен для конкретной ситуации. Вместо того чтобы просто выводить общее сообщение при возникновении неизвестного исключения, можно сохранить текст ошибки в переменную и затем вывести ее содержимое. Так вы сможете увидеть реальное сообщение об ошибке, которое поможет понять, что именно произошло. Как обычно, имя переменной вы выбираете сами, но чаще всего используют короткие обозначения вроде e или err — от слова error («ошибка»). Например, рассмотрим переработанный вариант предыдущего кода. В общем обработчике except Exception теперь добавлено выражение as e. Оно означает: «Какое бы исключение не было перехвачено, сохранить сообщение об ошибке в переменной с именем e». Затем следующая строка print(e) выводит содержимое этой переменной, то есть реальный текст ошибки: try: # Открыть файл и показать его имя the_file = open('people.csv') # Вывести пару пустых строк, затем первую строку из файла print('\n\n', the_file.readline()) the_file.wookems() except FileNotFoundError: print("Sorry, I don't see a file named people.csv here") except Exception as e: print(e) При запуске этого кода вывод будет таким: Username,FirstName,LastName,Role,DateJoined '_io.TextIOWrapper' object has no attribute 'wookems' 308 РАЗДЕЛ 2 Понимание базовых конструкций Python
Первая строка — это просто первая строка текста из файла people.csv. Файл существует, ошибок при его открытии нет, значит, эта часть программы отработала корректно. Вторая строка: '_io.TextIOWrapper' object has no attribute 'wookems' На первый взгляд, это сообщение может показаться непонятным, но оно гораздо информативнее, чем общее «Что-то пошло не так». Фраза object has no attribute 'wookems' ясно указывает, что ошибка связана с вызовом несуществующего метода wookems. Таким образом, ошибка была корректно обработана, программа не завершилась сбоем, и вы получили полезную информацию о причине проблемы — пусть и в техническом виде, не предназначенном для конечного пользователя. Добавление блока else В предыдущем примере мы использовали один обработчик для ошибки отсутствия файла и второй — для всех остальных случаев. Однако в реальных программах может возникать гораздо больше типов ошибок, и для каждой из них можно добавить свой блок except. Но что происходит, если ни одно исключение не возникает? В этом случае выполнение кода можно продолжить в обычном режиме. Для этого в конструкции try…except можно добавить блок else: — он выполняется только тогда, когда ошибок не было. Пример общей структуры: try: Действие, которое может вызвать исключение catch распространенное исключение: Объяснить проблему catch Exception as e: Показать общее сообщение об ошибке else: Продолжить выполнение здесь, только если исключения не возникли ГЛАВА 7 Обход ошибок 309
Если перевести этот код на простой русский язык, логика работы выглядит так: 1. Попробовать выполнить действие (например, открыть файл) . 2. Если файл отсутствует — сообщить об этом пользователю и остановить выполнение . 3. Если возникла какая-то другая ошибка — вывести общее сообщение об ошибке и также остановиться . 4. Если никаких ошибок не произошло — продолжить выполнение программы . Ограничивая блок try только тем действием, которое с наибольшей вероятностью может вызвать исключение, вы предотвращаете выполнение остальной части программы, если что-то пойдет не так. Если же исключение не возникает, выполнение продолжается в обычном режиме — код в блоке else выполняется, а обработчики ошибок больше не имеют значения. Ниже приведен полный пример с комментариями, поясняющими работу каждого блока: try: # Открыть файл с именем people.csv the_file = open('people.csv') # Отслеживать распространенную ошибку и остановить программу, если она возникнет except FileNotFoundError: print("Sorry, I don't see a file named people.csv here ") # Перехватить любую непредвиденную ошибку except Exception as err: print(err) # В противном случае, если до сих пор ничего плохого не произошло, продолжить выполнение else: # Если мы здесь, значит файл уже должен быть открыт print('\n') # Напечатать пустую строку # Напечатать каждую строку из файла for one_line in the_file: print(one_line) the_file.close() print("Success!") 310 РАЗДЕЛ 2 Понимание базовых конструкций Python
ОСТОРОЖНО Как и всегда в Python, отступы имеют решающее значение. Убедитесь, что ваш код оформлен с теми же отступами, которые показаны в примерах этой главы. Если отступы сделать неправильно, программа может работать некорректно или вовсе не запуститься. На рисунке 7.6 показан полный код и результаты его выполнения в VS Code. Рисунок 7.6. Код с блоком try, обработчиками исключений и блоком else:, который выполняется, когда исключений не возникает ГЛАВА 7 Обход ошибок 311
Использование try. . . except. . . else. . . finally Если взглянуть на полный синтаксис обработки исключений в Python, можно заметить еще один дополнительный блок, который располагается в самом конце конструкции. Он выглядит так: try: попытаться выполнить это действие except ErrorType as e: если происходит ErrorType, выполнить этот код except: если происходит любая другая ошибка, выполнить этот код else: если исключений не возникло, продолжить нормальное выполнение здесь finally: этот код выполняется всегда — независимо от того, произошла ошибка или нет Блок finally выполняется при завершении блока try в любом случае. Например, если вы находитесь внутри функции и один из блоков except использует return для выхода из функции, код в finally все равно будет выполнен. Без такой возможности блок finally был бы эквивалентен размещению его кода после и вне блока try. Для иллюстрации рассмотрим следующий пример. В начале программы переменной file_is_open присваивается значение False. Затем выполняется попытка открыть файл. Если файл не удается открыть, первый блок except перехватывает ошибку, и программа не завершается аварийно. При этом переменная file_is_open остается равной False. Если же файл успешно открыт, выполняется блок else, в котором переменная file_is_open устанавливается в значение True, а содержимое файла выводится на экран. 312 РАЗДЕЛ 2 Понимание базовых конструкций Python
Наконец, в блоке finally код проверяет, установлена ли переменная file_is_open в значение True. Если да, файл закрывается. Таким образом, независимо от того, возникла ошибка или нет, программа завершает работу корректно, а файл гарантированно закрывается перед выполнением следующего кода. # Переменная, которая изначально имеет значение False file_is_open = False try: # Открываем файл с именем people.csv the_file = open("people.csv") # Отслеживаем распространенную ошибку и останавливаем программу, если она возникает except FileNotFoundError: print("Sorry, I don't see a file named people.csv here ") # Если до этого момента не произошло никаких ошибок, продолжаем работу else: # Файл успешно открыт — устанавливаем статус открытия в True file_is_open = True # Показываем содержимое файла for one_line in the_file: print(one_line) finally: # Этот код выполняется в любом случае. Если файл открыт, он закрывается. if file_is_open: the_file.close() print("File is closed now") СОВЕТ Если вы забыли синтаксис конструкции try…except…else…finally в Python, просто попросите GitHub Copilot, Claude .ai, ChatGPT или другой инструмент с генеративным искусственным интеллектом написать пример кода с этими блоками. Так вы быстро получите готовый образец с правильным базовым синтаксисом, который можно использовать как шаблон для своих программ. Эти примеры показывают, что вы можете точно контролировать поведение программы в той ее части, где возможны ошибки со стороны пользователя или другие внешние исключения. При этом остальная часть кода продолжает выполняться нормально и без сбоев. ГЛАВА 7 Обход ошибок 313
Создание собственных исключений В Python существует множество встроенных исключений, которые помогают распознавать и определять ошибки, возникающие при написании и тестировании программ — особенно на начальном этапе обучения. Однако вы не ограничены только встроенными возможностями. Если в вашем приложении может возникнуть особая ситуация, которая не покрывается стандартными исключениями, вы можете создать собственное исключение и обрабатывать его так, как нужно именно вашему коду. ПРИМЕЧАНИЕ Подробный список всех встроенных исключений, которые может перехватывать Python, вы можете найти в официальной документации по адресу: https://docs.python.org/3/library/exceptions.html. Общий синтаксис для вызова собственного исключения выглядит так: raise error Здесь вместо слова error нужно указать имя конкретного типа ошибки, который вы хотите вызвать — например, FileNotFoundError. Если же ситуация, которую вы хотите обработать, не соответствует ни одному из встроенных типов ошибок, можно использовать: raise Exception. В этом случае будет выполнен код из блока except Exception: — то есть общий обработчик исключений. В качестве примера рассмотрим ситуацию, когда для успешного выполнения программы должны выполняться два условия: » » Файл people.csv должен существовать, чтобы его можно было открыть. Файл people.csv должен содержать более одной строки данных. Первая строка используется для заголовков столбцов, а не для самих данных, поэтому если в файле присутствует только эта строка, такой файл считается пустым. Вот пример того, как можно организовать обработку исключений для такой ситуации: # Переменная, которая изначально имеет значение False file_is_open = False try: # Открываем файл с именем people.csv 314 РАЗДЕЛ 2 Понимание базовых конструкций Python
the_file = open("people.csv") # Подсчитываем количество строк в файле line_count = len(the_file.readlines()) # Если строк меньше двух, вызываем исключение if line_count < 2: raise Exception # Обрабатываем ошибку отсутствия файла except FileNotFoundError: print('\ nThere is no people.csv file here') # Обрабатываем все остальные исключения except Exception as e: print('\n\n An unexpected error occurred.') # Если до этого момента ничего плохого не случилось, продолжаем выполнение else: # К этому моменту файл должен быть открыт. Устанавливаем статус открытия в True file_is_open = True the_file.seek(0) # Сбрасываем указатель файла на начало for one_line in the_file: print(one_line) finally: # Этот блок выполняется в любом случае, поэтому файл закрывается, если был открыт if file_is_open: the_file.close() print("File is closed now") Разберем код по шагам. Сначала программа пытается открыть файл people.csv: try: # Открываем файл с именем people.csv the_file = open('people.csv') Если файл people.csv не существует, управление перейдет в следующий обработчик исключения, который сообщит пользователю об отсутствии файла: except FileNotFoundError: print('\nThere is no people.csv file here') ГЛАВА 7 Обход ошибок 315
Предположим, файл найден, ошибки нет, и файл успешно открыт. Следующая строка подсчитывает количество строк в файле: line_count = len(the_file.readlines()) Если файл пуст, количество строк будет равно 0. Если же файл содержит только заголовки столбцов, например: Username,FirstName,LastName,DateJoined вы хотите, чтобы остальной код выполнялся только в том случае, если в файле есть как минимум две строки, то есть заголовки и хотя бы одна строка с данными. Поэтому, если количество строк меньше двух, программа должна вызвать исключение. Для такой ситуации нет встроенного предопределенного типа ошибки, но можно использовать общее исключение, вызвав его с помощью инструкции raise Exception (с заглавной буквы E): if line_count < 2: raise Exception Обработчик для общих исключений выглядит так: # Обрабатывает все остальные исключения except Exception as e: print('\n\nAn unexpected error occurred.') Если ошибка не возникла, файл открывается, и его содержимое отображается. После завершения работы с файлом он закрывается — так же, как и в предыдущем примере кода. Все исключения в Python являются объектами (экземплярами классов), которые наследуются от базового класса Exception. Чтобы создать собственное исключение, нужно использовать класс Exception как базовый класс, а затем определить свой подкласс на его основе — аналогично тому, как, например, класс Member мог быть базовым для разных типов пользователей. Вот пример объявления пользовательского класса исключения: class CustomException(Exception): def __init__(self, message="A custom exception occurred"): self.message = message super().__init__(self.message) 316 РАЗДЕЛ 2 Понимание базовых конструкций Python
Класс CustomException наследуется от класса Exception. Это означает, что он может вести себя так же, как любое встроенное исключение Python, но при этом использовать ваши собственные формулировки сообщений об ошибках. Чтобы обрабатывать пользовательские исключения, нужно добавить отдельный блок except для этого класса. В нем можно вывести любое сообщение, переданное при вызове исключения. Такой блок следует разместить выше последнего общего обработчика (except Exception:), чтобы он сработал раньше: except CustomException as e: # Обработка пользовательского исключения print(f"Caught a custom exception: {e.message}") Теперь вы можете использовать созданный класс для генерации пользовательских исключений. Вернемся к нашему предыдущему примеру, где нужно вызвать исключение, если файл people.csv содержит слишком мало строк. В этом случае используйте raise CustomException и передайте текст своего сообщения об ошибке: # Если строк меньше двух, вызываем пользовательское исключение if line_count < 2: raise CustomException("Not enough rows!") На рисунке 7.7 показан полный код этого примера. При запуске программы, если файл people.csv отсутствует, появится следующее сообщение: There is no people.csv file here. Если файл people.csv существует, но пуст или содержит только заголовки столбцов, программа выведет сообщение: Not enough rows! Если же ни одна из этих ошибок не возникла, выполняется код из блока else, и на экран выводится содержимое файла. Как видите, обработка исключений позволяет заранее предусмотреть возможные ошибки, связанные с уязвимыми местами в программе. Речь идет не о логических ошибках или ошибках в самом коде, а о внешних ресурсах, от которых зависит корректная работа приложения — например, о файлах, подключениях к базе данных или сетевых запросах. ГЛАВА 7 Обход ошибок 317
Когда такие ресурсы недоступны или недостаточны, программе необязательно аварийно завершаться и выводить непонятное техническое сообщение об ошибке, которое только запутает пользователя. Вместо этого можно перехватить исключение и вывести понятный текст, объясняющий, что именно произошло и как это исправить. В этом и заключается главная цель обработки исключений — сделать ваши программы более надежными, предсказуемыми и дружелюбными к пользователю. Рисунок 7.7. Добавлено пользовательское исключение EmptyFileError для обработки ошибок, связанных с пустыми файлами
3 РАБОТА С БИБЛИОТЕКАМИ
В ЭТОМ РАЗДЕЛЕ Глава 1: Работа с внешними файлами Глава 2: Работа с данными JSON Глава 3: Взаимодействие с интернетом Глава 4: Библиотеки, пакеты и модули
В этой главе вы узнаете: » Как работать с текстовыми и двоичными файлами » Как открывать, закрывать и читать содержимое файла » Как выполнять перебор строк в файле » Как читать и копировать двоичные файлы » Как работать с файлами формата CSV » Как импортировать данные из CSV в объекты и словари Гл а в а 1 Работа с внешними файлами П рактически все, что хранится на вашем компьютере — будь то документ, программа, фильм, фотография или любой другой тип данных, находится в файле. Большинство файлов организованы в папки (или директории). Вы можете просматривать их с помощью Finder (на Mac) или Проводника Windows (в Windows). Python предоставляет множество инструментов для создания, чтения и записи файлов самых разных типов. В этой главе вы познакомитесь с основными приемами работы с файлами на Python и научитесь использовать эти навыки на практике. ГЛАВА 1 Работа с внешними файлами 321
Понимание текстовых и двоичных файлов Файлы делятся в основном на два типа: » Текстовые файлы — содержат обычные символы текста . Если открыть такой файл в текстовом редакторе, вы увидите его содержимое в читаемом виде . Текст может быть написан на любом языке, но в нем будут использоваться обычные символы, которые можно набрать с клавиатуры . » Двоичные файлы — хранят данные в виде байтов, и человеку их содержимое прочитать трудно . Если попытаться открыть двоичный файл в текстовом редакторе, вы увидите набор непонятных символов, похожих на изображение на рисунке 1 .1 . (Делать это, кстати, не рекомендуется .) Рисунок 1.1. Как выглядит двоичный файл в программе для редактирования текстовых файлов ОСТОРОЖНО 322 Если вы случайно откроете двоичный файл в текстовом редакторе и увидите непонятный набор символов — не паникуйте. Просто закройте файл или программу и выберите «Нет», если появится запрос на сохранение. С файлом все будет в порядке, пока вы не сохраняете изменения. РАЗДЕЛ 3 Работа с библиотеками
На рисунке 1.2 показаны примеры различных типов текстовых и двоичных файлов, с некоторыми из которых вы, вероятно, уже сталкивались. Конечно, существуют и другие форматы, но представленные здесь — одни из наиболее распространенных. Рисунок 1.2. Распространенные текстовые и двоичные файлы Как и в случае с любым другим кодом на Python, вы можете использовать Jupyter Notebook, VS Code или практически любой другой редактор кода для написания своих программ. В этой главе используется VS Code — просто потому, что его панель проводника (расположенная слева, когда она открыта) наглядно показывает содержимое папки, в которой вы работаете. Это удобно при создании, открытии и управлении файлами во время изучения примеров. Открытие и закрытие файлов Чтобы открыть файл из программы на Python, используйте следующий синтаксис: open(filename.ext[,mode]) Здесь filename.ext обозначает имя файла, который вы хотите открыть. Если файл находится не в той же директории, что и ваш Python-код, нужно указать полный путь к нему. Например, если вы используете Windows и хотите открыть файл foo.txt, расположенный на рабочем столе пользователя Alan, путь будет выглядеть так: C:/Users/Alan/Desktop/foo.txt ГЛАВА 1 Работа с внешними файлами 323
Обратите внимание, что в Python принято использовать прямые слеши (/), а не обратные (\), привычные для Windows. Оба варианта работают, но прямые слеши делают код более универсальным и совместимым с другими операционными системами. Параметр mode является необязательным (на что указывают квадратные скобки в синтаксисе). Он определяет, какой тип доступа к файлу вы хотите предоставить программе. Для этого используются следующие однобуквенные обозначения: » r (чтение) — открывает файл только для чтения . В этом режиме Python не может изменять содержимое файла . Это режим по умолчанию, если не указан другой . Если файл не существует, возникает исключение FileNotFoundError . » r+ (чтение/запись) — открывает файл для чтения и записи одновременно . » a (добавление) — открывает файл и позволяет добавлять новые данные в конец файла, не изменяя существующее содержимое . Если файл не существует, Python создает его . » w (запись) — открывает файл для записи . Если файл уже существует, его содержимое будет перезаписано . Если файла нет, он будет создан . » x (создание) — создает новый файл, если его еще не существует . Если файл уже существует, вызывается исключение FileExistsError . Дополнительную информацию об обработке исключений см. в главе 7 раздела 2. ЗАПОМНИТЕ Вы также можете указать, какого типа файл открывается или создается. Если вы уже выбрали один из предыдущих режимов (r, w, a, x, r+), просто добавьте к нему дополнительную букву, уточняющую тип файла. Если использовать только одну из следующих букв отдельно, файл откроется в режиме чтения: » 324 t (текстовый режим) — открывает файл как текстовый, позволяя Python читать и записывать текстовые данные . Это режим по умолчанию, если не указано иное . РАЗДЕЛ 3 Работа с библиотеками
» b (двоичный режим) — открывает файл как двоичный, позволяя Python читать и записывать данные в виде байтов . Метод open() можно использовать двумя основными способами. Первый способ заключается в том, чтобы сохранить открытый файл в переменной и обращаться к нему по этому имени в дальнейшем коде: var = open(filename.ext[,mode]) Здесь var вы можете заменить любым удобным именем переменной. В Python часто используют короткое имя — например, просто букву f, что делает код компактным и привычным для чтения. После открытия файла вы можете получить доступ к его содержимому разными способами — мы подробно рассмотрим их чуть позже в этой главе. А пока давайте просто считаем все содержимое файла и выведем его на экран. Для этого скопируем данные в переменную filecontents, а затем используем простую функцию print(). Пример: чтобы открыть файл quotes.txt, прочитать все его содержимое и вывести его на экран, используйте следующий код: f = open('quotes.txt') filecontents = f.read() print(filecontents) При таком способе работы файл остается открытым до тех пор, пока вы явно не закроете его. Для этого используется имя переменной, с которой был открыт файл, и метод .close(): f.close() ЗАПОМНИТЕ Всегда закрывайте файлы, которые больше не нужны вашему приложению. Если этого не делать, в системе будут постепенно накапливаться открытые файловые дескрипторы, что может привести к возникновению исключений и даже к аварийному завершению программы. В худшем случае это способно привести к повреждению открытых файлов или потере данных. Второй способ работы с файлами — это использование контекстного менеджера, или контекстного программирования. Такой код начинается со слова with. Вы также присваиваете файлу имя переменной, но делаете это в конце строки перед двоеточием. Двоеточие обозначает начало блока with, и весь код с отступом под ним относится к открытому файлу (так же, как код с отступом внутри цикла или условия). ГЛАВА 1 Работа с внешними файлами 325
Главное преимущество этого способа — вам не нужно вручную закрывать файл: Python делает это автоматически, как только выполнение выходит за пределы блока with. Пример: # ---------------- Контекстный синтаксис with open('quotes.txt') as f: filecontents = f.read() print(filecontents) # Строка без отступа ниже находится вне блока with.... print('File is closed: ', f.closed) Следующий пример демонстрирует простое приложение, которое открывает файл quotes.txt, считывает и отображает его содержимое, а затем закрывает файл. В первом методе файл необходимо закрыть вручную с помощью метода .close(). Во втором методе используется контекстный менеджер with, и Python закрывает файл автоматически, поэтому вызывать .close() не нужно. # - Базовый синтаксис для открытия, чтения и отображения содержимого файла f = open('quotes.txt') filecontents = f.read() print(filecontents) # Возвращает True, если файл закрыт, иначе False print('File is closed: ', f.closed) # Закрывает файл вручную f.close() # Закрыть файл print() # Напечатать пустую строку # ---------------- Контекстный синтаксис with open('quotes.txt') as f: filecontents = f.read() print(filecontents) # Строка без отступа ниже находится вне блока with.... print('File is closed: ', f.closed) Вывод этого приложения: Вечер для меня был прекрасен, но не этот. Граучо Маркс Разница между глупостью и гениальностью в том, что у гениальности есть пределы. Альберт Эйнштейн 326 РАЗДЕЛ 3 Работа с библиотеками
Мы все здесь, на Земле, чтобы помогать другим; зачем здесь другие — я понятия не имею. У. Х. Оден Заканчивать предложение предлогом — это то, с чем я мириться не буду Уинстон Черчилль Файл закрыт: False Вечер для меня был прекрасен, но не этот. Граучо Маркс Разница между глупостью и гениальностью в том, что у гениальности есть пределы. Альберт Эйнштейн Мы все здесь, на Земле, чтобы помогать другим; зачем здесь другие — я понятия не имею. У. Х. Оден Заканчивать предложение предлогом — это то, с чем я мириться не буду Уинстон Черчилль Файл закрыт: True (Мы не можем гарантировать, что эти известные цитаты действительно принадлежат указанным авторам.) В конце первого вывода значение f.closed равно False, потому что проверка выполняется до того, как метод .close() закрывает файл. В конце второго вывода значение f.closed равно True, даже без явного вызова .close(), поскольку код, расположенный с отступом под строкой with:, автоматически закрывает файл после выхода из блока. В оставшейся части этой главы мы будем использовать контекстный синтаксис, поскольку он считается предпочтительным и рекомендуемым способом работы с файлами. Это также хорошая привычка, которую стоит выработать с самого начала изучения Python. Предыдущий пример работает без проблем, потому что quotes.txt — это простой текстовый файл, содержащий только ASCII-символы — буквы, цифры и знаки пунктуации, которые можно набрать на стандартной английской клавиатуре. Теперь рассмотрим другой пример. В следующем коде программа пытается открыть файл формата .jpg, который представляет собой графическое изображение, а не текстовый файл: with open('happy_pickle.jpg') as f: filecontents = f.read() print(filecontents) ГЛАВА 1 Работа с внешними файлами 327
то появится ошибка: UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 in position 40: character maps to <undefined> Это сообщение, мягко говоря, не самое понятное. Оно появляется потому, что Python пытается прочитать двоичный файл (.jpg) как текстовый, используя стандартное текстовое кодирование. Теперь предположим, что мы открываем текстовый файл — например, names.txt, который, как и quotes.txt, должен содержать обычный текст. Используем для этого следующий код: with open('names.txt') as f: filecontents = f.read() print(filecontents) При выполнении этого кода может появиться другое, не менее загадочное сообщение об ошибке: Что же здесь происходит? Первая проблема связана с тем, что файл с расширением .jpg — это графическое изображение, а значит, он является двоичным, а не текстовым. Когда Python пытается открыть его в текстовом режиме (по умолчанию используется режим 'r'), он не может корректно интерпретировать байты изображения как символы текста — и именно поэтому возникает ошибка UnicodeDecodeError. Чтобы открыть .jpg-файл правильно, нужно добавить букву b к режиму открытия. Режим 'rb' означает «чтение в двоичном режиме». Пример: with open('happy_pickle.jpg', 'rb') as f: filecontents = f.read() print(filecontents) Выполнение этого кода не вызывает ошибки, однако вывод программы выглядит совсем не как изображение: \x07~}\xba\xe7\xd2\x8c\x00\x0e|\xbd\xa8\x12l+\xca\xf7\xae\ xa5\x9e^\x8d\x89 \x7f\xde\xb4f>\x98\xc7\xfc\xcf46d\xcf\x1c\xd0\xa6\x98m$\ 328 РАЗДЕЛ 3 Работа с библиотеками
xb6(U\x8c\xa6\x83 \x19\x17\xa6>\xe6\x94\x96|g\'4\xab\xdd\xb8\xc8=\xa9[\x8b\ xcc`\x0e8\xa3 \xb0;\xc6\xe6\xbb(I.\xa3\xda\x91\xb8\xbd\xf2\x97\xdf\xc1\xf4\ xefI\xcdy \x97d\x1e`;\xf64\x94\xd7\x03 То, что вы видите, — это двоичные данные изображения, представленные в виде байтов (последовательностей вроде \x07, \xba, \xe7 и т.д.). Python успешно открыл файл, но вместо картинки показал его сырое содержимое, которое не предназначено для чтения человеком. Если открыть happy_pickle.jpg в графическом приложении или даже просто в VS Code, изображение будет отображено корректно — как показано на рисунке 1.3. Рисунок 1.3. Как должен выглядеть файл happy_ pickle.jpg Почему же файл выглядит «искаженным» в Python? Функция print() выводит сырые байты, из которых состоит файл. Само по себе отображение необработанных байтов — не проблема и не ошибка; просто это не лучший способ работать с файлом формата .jpg в данном контексте. С файлом names.txt ситуация другая. Это текстовый файл (как и quotes.txt), но если открыть его и взглянуть на содержимое (рисунок 1.4), вы увидите множество необычных символов, которые обычно не встречаются в ASCII (то есть помимо привычных цифр, латинских букв и знаков препинания на стандартной клавиатуре). ГЛАВА 1 Работа с внешними файлами 329
Рисунок 1.4. Файл Names. txt является текстовым, но содержит множество символов, не относящихся к английскому алфавиту Все эти необычные символы указывают на то, что файл names.txt — это не простой ASCII-текстовый файл. Скорее всего, он сохранен в кодировке UTF-8 — это разновидность текстового файла, которая поддерживает гораздо больше символов, чем стандартный набор ASCII (в том числе буквы разных алфавитов, акценты, спецсимволы и т.д.). Чтобы корректно открыть такой файл, нужно сообщить Python, что следует ожидать символы в кодировке UTF-8. Для этого добавьте параметр encoding='utf-8' в функцию open(), как показано на рисунке 1.5. Результат выполнения программы в этом случае будет соответствовать реальному содержимому файла names.txt. Рисунок 1.5. Отображение содержимого файла names . txt 330 РАЗДЕЛ 3 Работа с библиотеками
При открытии файлов в Python важно учитывать три основных момента: » » » Обычный текстовый файл (ASCII) можно открывать в режиме 'r', или просто не указывать режим вовсе — это значение используется по умолчанию. Двоичный файл (например, изображение, видео или исполняемый файл) необходимо открывать с указанием буквы b в режиме, например 'rb' или 'wb'. Текстовый файл с нестандартными символами (например, содержащий кириллицу, акценты или специальные знаки) следует открывать как текстовый, но с указанием параметра encoding='utf-8' в функции open(). ЧТО ТАКОЕ UTF-8? UTF-8 расшифровывается как Unicode Transformation Format, 8-bit (в переводе — 8-битный формат преобразования Юникода) . Это стандартизированный способ представления символов — букв, цифр и знаков — на компьютерах . Изначально существовал набор символов ASCII, включавший в себя только латинские буквы, цифры и знаки препинания . Он отлично подходил для ранних компьютеров, но оказался недостаточным, когда появилась необходимость работать с другими языками и письменностями . Со временем было предложено множество стандартов кодировок, однако именно UTF-8 стал наиболее удобным и универсальным . Сегодня он фактически является мировым стандартом для всего, что связано с интернетом, и поэтому его использование — лучший выбор для любого проекта, где важна совместимость и поддержка разных языков . Если вы сталкиваетесь с проблемами при открытии файла, который должен быть в формате UTF-8, но не открывается, просто выполните поиск в интернете по запросу «конвертировать файл в кодировку UTF‑8» . Вы легко найдете подходящее приложение или онлайн-инструмент для вашей операционной системы, чтобы выполнить нужное преобразование . ГЛАВА 1 Работа с внешними файлами 331
Чтение содержимого файла Ранее в этой главе вы уже видели, как можно использовать метод .read() для чтения содержимого открытого файла. Однако это не единственный способ читать данные из файла. В Python существуют три основных метода для этого: ПРИМЕЧАНИЕ » read([size]) — считывает весь файл, если скобки оставить пустыми . Если указать число в скобках, метод прочитает определенное количество символов (для текстового файла) или байтов (для двоичного файла) . » readline() — считывает одну строку из текстового файла . Чтение останавливается, когда встречается символ новой строки \n, который завершает текущую строку и переводит курсор на следующую . » readlines() — считывает все строки файла и возвращает их в виде списка . Поскольку двоичные файлы не предназначены для ввода с клавиатуры, любые символы новой строки, встречающиеся в них, являются произвольными. Поэтому методы readline() и readlines() полезны только для текстовых файлов. Оба метода — read() и readlines() — считывают все содержимое файла. Разница лишь в том, что read() читает файл как один большой блок данных, тогда как readlines() считывает файл построчно и сохраняет каждую строку как отдельный элемент списка. Например, следующий код открывает файл quotes.txt, считывает все его содержимое и затем выводит его на экран: with open('quotes.txt') as f: content = f.read() print(content) Переменная content хранит копию всего содержимого текстового файла. Мы выводим эту переменную, чтобы увидеть содержимое файла на экране. 332 РАЗДЕЛ 3 Работа с библиотеками
Символ новой строки \n в конце каждой строки файла указывает Python начать новую строку при выводе. Вот тот же код, но использующий метод readlines() вместо read(): with open('quotes.txt') as f: content = f.readlines() print(content) Вывод этого кода будет следующим: ["Вечер для меня был прекрасен, но не этот.\n", 'Граучо Маркс\n', ' Разница между глупостью и гениальностью в том, что у гениальности есть пределы.\n', 'Альберт Эйнштейн \n', ' Мы все здесь, на Земле, чтобы помогать другим; зачем здесь другие — я понятия не имею.\n', 'У. Х. Оден \n', ' Заканчивать предложение предлогом — это то, с чем я мириться не буду.\n', 'Уинстон Черчилль\n'] Квадратные скобки вокруг вывода показывают, что это список. Каждый элемент списка заключен в кавычки и отделен запятыми. Символ \n в конце каждого элемента — это символ новой строки, который завершает строку в файле. В отличие от readlines() (во множественном числе), метод readline() читает только одну строку из файла. Он начинает чтение с текущей позиции и останавливается у первого символа новой строки. Повторный вызов readline() прочитает следующую строку, и т.д. Например, если запустить следующий код: with open('quotes.txt') as f: content = f.readline() print(content) Результат будет таким: Вечер для меня был прекрасен, но не этот. Если выполнить еще один вызов readline(), он прочитает следующую строку. Как вы можете догадаться, при работе с методами readline() и readlines() чаще всего используется цикл, чтобы получить доступ ко всем строкам файла — это позволяет управлять процессом чтения более гибко. ГЛАВА 1 Работа с внешними файлами 333
Перебор содержимого файла Вы можете перебирать содержимое файла, используя методы readlines() или readline(). Метод readlines() всегда считывает весь файл целиком. Поэтому, если файл слишком большой, компьютеру может не хватить оперативной памяти (ОЗУ), прежде чем чтение завершится. Если же вы знаете, что файл небольшой (например, содержит несколько тысяч строк или меньше), метод readlines() — это удобный и быстрый способ получить все данные сразу. Результатом его работы будет список строк, поэтому при переборе вы фактически проходите по элементам списка, а не по самому файлу. Вы также можете перебирать и двоичные файлы, однако они не содержат текстовых строк. Поэтому при работе с ними данные читаются блоками, как будет показано в конце этого раздела. Перебор с помощью readlines() Когда вы читаете файл с помощью метода readlines(), Python считывает весь файл сразу и сохраняет его содержимое в виде списка. Это означает, что вы не перебираете сам файл построчно, а проходите по элементам списка, где каждый элемент представляет собой одну строку файла. Код для такого перебора выглядит так: with open('quotes.txt') as f: # Сначала считывает все строки, затем перебирает их for one_line in f.readlines(): print(one_line) Если запустить этот код, вывод будет иметь двойной межстрочный интервал. Это происходит потому, что каждый элемент списка уже заканчивается символом новой строки \n, а функция print() по умолчанию добавляет еще один символ новой строки после каждой итерации цикла. Чтобы сохранить одинарный интервал между строками, добавьте параметр end='' к оператору print. Обратите внимание: необходимо использовать две одинарные или две двойные кавычки без пробела после знака =. 334 РАЗДЕЛ 3 Работа с библиотеками
Пример: with open('quotes.txt') as f: # Сначала считывает все строки, затем перебирает их for one_line in f.readlines(): print(one_line, end='') Результат вывода на экране: Вечер для меня был прекрасен, но не этот. Граучо Маркс Разница между глупостью и гениальностью в том, что у гениальности есть пределы. Альберт Эйнштейн Мы все здесь, на Земле, чтобы помогать другим; зачем здесь другие — я понятия не имею. У. Х. Оден Заканчивать предложение предлогом — это то, с чем я мириться не буду Уинстон Черчилль Предположим, вы довольны текущим выводом, но хотите немного его улучшить. Например, сделать небольшой отступ (в пару пробелов) перед именем автора под каждой цитатой и добавить пустую строку после имени. Как это можно реализовать? В Python есть встроенная функция enumerate(), которая, при использовании со списком, нумерует элементы во время прохода по циклу, начиная с нуля. Поэтому вместо обычного цикла for, показанного ранее, вы можете написать: При каждом проходе цикла: • • one_line[0] — это номер строки (счетчик); one_line[1] — это текст самой строки . Чтобы определить, является ли строка четной или нечетной, используется оператор деления с остатком %, который возвращает остаток после деления. Если вы вычисляете % 2 для четного числа, результат всегда будет 0; для нечетного — 1. ГЛАВА 1 Работа с внешними файлами 335
С помощью этого принципа можно оформить вывод следующим образом: with open('quotes.txt') as f: # Сначала считывает все строки, затем проходит по ним циклом. # Подсчитывает каждую строку, начиная с нуля. for one_line in enumerate(f.readlines()): # Если счетчик четный - печатает без дополнительного переноса строки. if one_line[0] % 2 == 0: print(one_line[1], end='') # Если нечетный - добавляет два пробела и пустую строку после имени. else: print(' ' + one_line[1]) Результат выглядит так: Вечер для меня был прекрасен, но не этот. Граучо Маркс Разница между глупостью и гениальностью в том, что у гениальности есть пределы. Альберт Эйнштейн Мы все здесь, на Земле, чтобы помогать другим; зачем здесь другие — я понятия не имею. У. Х. Оден Заканчивать предложение предлогом — это то, с чем я мириться не буду Уинстон Черчилль Построчное чтение с помощью readline() Если вы не уверены в размере файла, который собираетесь прочитать, или в объеме оперативной памяти компьютера, на котором работает ваше приложение, использование метода readlines() может быть рискованным. Если памяти окажется недостаточно для хранения всего содержимого файла, приложение может аварийно завершить работу из-за нехватки ресурсов. Для более безопасной работы можно считывать файл построчно, чтобы в памяти находилась только одна строка содержимого файла в каждый момент времени. 336 РАЗДЕЛ 3 Работа с библиотеками
Чтобы использовать этот метод, нужно открыть файл, прочитать одну строку и сохранить ее в переменную. Затем выполняется цикл while, который продолжается до тех пор, пока переменная не станет пустой строкой (то есть пока не будет достигнут конец файла). Поскольку каждая строка в файле содержит текст, переменная не будет пустой до тех пор, пока не будет прочитана последняя строка. Пример кода: with open('quotes.txt') as f: one_line = f.readline() while one_line: print(one_line, end='') one_line = f.readline() Для больших файлов этот метод является предпочтительным, поскольку файл никогда не загружается целиком в память. Единственная потенциальная проблема — забыть добавить вызов .readline() внутри цикла, чтобы перейти к следующей строке. Если это упустить, программа попадет в бесконечный цикл, который будет снова и снова выводить первую строку файла. Если вы окажетесь в такой ситуации, нажмите Ctrl+C в панели терминала, где выполняется код, чтобы остановить цикл. Вы можете добиться того же форматирования, при котором имя с отступом располагается под каждой цитатой, а между записями добавляется пустая строка, используя метод .readline() в Python. В коде следует начать со счетчика, значение которого равно 1. Затем создайте цикл, который будет считывать файл построчно. Внутри цикла при каждом проходе увеличивайте значение счетчика на 1. Для четных строк добавляйте отступ и дополнительный пробел, как показано ниже: # Сохраняем число для использования в качестве счетчика цикла counter = 1 # Открываем файл with open('quotes.txt') as f: # Читаем одну строку из файла one_line = f.readline() # Пока есть строки для чтения... while one_line: # Если счетчик четный, печатаем с отступом в два пробела if counter % 2 == 0: print(' ' + one_line) ГЛАВА 1 Работа с внешними файлами 337
# В противном случае - печатаем без перевода строки в конце else: print(one_line, end='') # Увеличиваем счетчик counter += 1 # Читаем следующую строку one_line = f.readline() Результат выполнения этого цикла такой же, как и при использовании второго варианта с readlines(): каждое имя автора выводится с отступом, а за ним следует дополнительная пустая строка благодаря использованию print() без параметра end=''. Вы можете использовать генеративный искусственный интеллект для написания кода, работающего с файлами. Например, попросите Copilot или ChatGPT создать код на Python, который открывает текстовый файл с именем whatever.txt и выполняет цикл по его содержимому. Попросите также добавить обработчик исключений на случай, если файл не найден. После этого вы сможете изменить сгенерированный код под свои нужды. СОВЕТ Добавление в конец файла или перезапись ОСТОРОЖНО При работе с файлами важно понимать разницу между записью и добавлением. Если файл уже содержит данные, и вы откроете его в режиме записи ('w'), то новое содержимое перезапишет (заменит) все, что было в файле ранее. Отменить это действие невозможно — предыдущие данные будут безвозвратно потеряны. Поэтому, если содержимое файла важно, убедитесь, что вы не откроете его в режиме 'w' по ошибке. Чтобы добавить новые данные в конец файла, откройте его в режиме добавления ('a') и используйте метод .write() для записи информации. Предположим, вы хотите добавить имя Peña Calderón в файл names. txt, который использовался ранее в этой главе. Поскольку это имя, как и другие имена в файле, содержит специальные символы, выходящие за пределы английского алфавита, необходимо указать кодировку UTF-8. Кроме того, если вы хотите, чтобы каждое имя отображалось на отдельной строке, добавьте символ \n (перевод строки) в конец добавляемого текста. 338 РАЗДЕЛ 3 Работа с библиотеками
Пример кода: # Новое имя для добавления с \n для обозначения конца строки new_name = 'Peña Calderón\n' # Открываем names.txt в режиме добавления с указанием кодировки with open('names.txt', 'a', encoding='utf-8') as f: f.write(new_name) Чтобы проверить, что добавление прошло успешно, начните новый блок кода без отступов, чтобы файл names.txt автоматически закрылся после завершения предыдущего блока with. Затем откройте файл в режиме чтения ('r') и выведите его содержимое. На рисунке 1.6 показан код, который добавляет новое имя в файл и затем отображает содержимое файла names.txt после внесенных изменений. Рисунок 1.6. Новое имя добавлено в конец файла names.txt СОВЕТ Ввод специальных символов, таких как ñ и ó, обычно требует использования клавиши Alt в сочетании с набором трех- или четырехзначного кода. Например, комбинация Alt+164 вводит символ ñ, а Alt+0243 — символ ó. Конкретный способ ввода может различаться в зависимости от операционной системы и редактора, который вы используете. При необходимости вы можете найти инструкции в интернете, выполнив поиск по запросам вроде «как напечатать n с тильдой в Windows» или «как напечатать o с ударением на Mac». ГЛАВА 1 Работа с внешними файлами 339
Использование tell() для определения позиции указателя При переборе файла его содержимое читается сверху вниз и слева направо. Python использует указатель позиции, чтобы отслеживать текущее место чтения в файле. Когда вы читаете текстовый файл с помощью метода readline(), указатель всегда направлен на символ, с которого начинается следующая строка. Если файл только что открыт, позиция символа равна 0, то есть указатель находится в начале файла. При каждом выполнении readline() указатель перемещается к началу следующей строки. Пример кода и его вывода показан ниже: with open('names.txt', encoding='utf-8') as f: # Читаем первую строку для начала. print(f.tell()) one_line = f.readline() # Продолжаем читать по одной строке, пока они не закончатся. while one_line: print(one_line[:-1], f.tell()) one_line = f.readline() 0 Björk Guðmundsdóttir 25 36 Бopиc Hикoлaeвич Eльцин 82 Nguy n T n Dũng 104 Peña Calderón 121 Первое значение — 0 — показывает позицию указателя сразу после открытия файла. Число 25 в конце следующей строки указывает позицию указателя после чтения первой строки. Число 36 — позицию после второй строки, и т.д., вплоть до 121 в конце, когда указатель находится в конце файла. Если попытаться сделать то же самое, используя метод readlines(), результат будет совсем другим. Пример кода: with open('names.txt', encoding='utf-8') as f: print(f.tell()) # Сначала считывает все строки, затем выполняет цикл for one_line in f.readlines(): print(one_line[:-1], f.tell()) И результат: 340 РАЗДЕЛ 3 Работа с библиотеками
0 Björk Guðmundsdóttir 121 121 Бopиc Hикoлaeвич Eльцин 121 Nguy n T n Dũng 121 Peña Calderón 121 Указатель, как и ожидалось, начинает с позиции 0. Однако каждая последующая строка показывает 121 в конце, поскольку метод readlines() считывает весь файл целиком при своем выполнении, и после этого указатель остается в конце файла — на позиции 121. Цикл при этом фактически проходит по копии содержимого файла в памяти, а не по самому файлу — чтение с диска уже не происходит. Перемещение указателя с помощью seek() Если метод tell() показывает, где находится указатель во внешнем файле, то метод seek() позволяет перемещать указатель в нужное место. Синтаксис следующий: file.seek(position[,whence]) • • • Здесь file вы можете заменить на имя переменной открытого файла . Параметр position указывает, куда именно переместить указатель. Например, значение 0 возвращает указатель в начало файла. Параметр whence является необязательным и определяет, откуда в файле выполняется отсчет позиции. Доступны следующие варианты: » » » 0 — отсчет от начала файла; 1 — отсчет от текущего положения указателя; 2 — отсчет от конца файла (в этом случае позиция задается отрицательным числом) . Если параметр whence не указан, по умолчанию используется 0 (начало файла). Наиболее распространенное применение метода seek() — это возврат указателя в начало файла, чтобы повторно пройтись по его содержимому. Синтаксис для этого прост: .seek(0). ГЛАВА 1 Работа с внешними файлами 341
Чтение и копирование двоичного файла Предположим, у вас есть приложение, которое изменяет двоичный файл, и вы хотите для безопасности всегда работать с копией исходного файла. Двоичные файлы могут быть очень большими, поэтому, вместо того чтобы открывать весь файл сразу и рисковать исчерпать память, вы можете читать и записывать его частями. Двоичные файлы не содержат читаемого человеком текста. В них также нет текстовых строк, поэтому методы readline() и readlines() не подходят для перебора двоичных файлов. Однако вы можете использовать метод .read() с указанием размера читаемого блока. На рисунке 1.7 показан файл binarycopy.py, который создает копию любого двоичного файла. Далее мы разберем этот код пошагово, чтобы вы поняли, как он работает. Первый шаг — указать файл, который вы хотите скопировать. В этом примере используется файл happy_pickle.jpg, который, как показано на рисунке, находится в той же папке, что и сам файл binarycopy.py: # Указываем файл для копирования file_to_copy = 'happy_pickle.jpg' Рисунок 1.7. Файл binarycopy.py копирует любой двоичный файл 342 РАЗДЕЛ 3 Работа с библиотеками
Чтобы создать пустой файл для копирования, сначала нужно задать его имя. Следующий код выполняет эту задачу: # Создаем новое имя файла, добавляя _copy перед расширением name_parts = file_to_copy.split('.') new_file = name_parts[0] + '_copy.' + name_parts[1] Первая строка после комментария разделяет исходное имя файла на две части по символу точки: name_parts[0] содержит happy_pickle, а name_parts[1] — jpg. Затем переменной new_file присваивается значение, состоящее из первой части имени с добавлением _copy, за которой следует точка и расширение файла. После выполнения этой строки переменная new_file будет содержать значение: happy_pickle_copy.jpg. Чтобы создать копию файла, откройте исходный файл в режиме rb (чтение в двоичном формате). Затем откройте новый файл, в который нужно скопировать содержимое, в режиме wb (запись в двоичном формате). При записи Python создает файл с указанным именем, если он еще не существует. Если файл уже существует, Python открывает его с указателем в начале (позиция 0), поэтому все, что будет записано, заменит существующее содержимое, а не добавит к нему. В коде показано, что мы используем переменную original_file для файла, из которого выполняется копирование, и переменную copy_to — для файла, в который записываются данные. Как и всегда в Python, отступы имеют решающее значение: # Открываем исходный файл в режиме двоичного чтения with open(file_to_copy, 'rb') as original_file: # Создаем или открываем файл для копирования with open(new_file, 'wb') as copy_to: Если использовать метод .read() для чтения всего двоичного файла целиком, существует риск, что файл окажется слишком большим и переполнит оперативную память, что приведет к сбою программы. Чтобы избежать этой проблемы, программа читает файл не полностью, а небольшими порциями — по 4 КБ (4096 байт) за раз. ГЛАВА 1 Работа с внешними файлами 343
Каждая такая порция сохраняется в переменной chunk: # Считываем фрагмент исходного файла (4КБ) chunk = original_file.read(4096) Следующая строка организует цикл, который последовательно считывает фрагменты файла один за другим. При каждом проходе цикла указатель автоматически перемещается к следующему фрагменту. В конце концов он достигает конца файла, где уже не остается данных для чтения. Когда это происходит, переменная chunk становится пустой, то есть ее длина равна 0. Таким образом, цикл выполняется до тех пор, пока в файле остаются фрагменты для чтения: # Выполняем цикл, пока есть фрагменты для чтения while len(chunk) > 0: Внутри цикла первая строка записывает последний прочитанный фрагмент в файл copy_to. Следующая строка считывает следующий фрагмент размером 4 КБ из исходного файла. Этот процесс продолжается до тех пор, пока все содержимое original_file не будет полностью скопировано в новый файл: copy_to.write(chunk) # Обязательно считываем следующий фрагмент в этом цикле chunk = original_file.read(4096) После этой строки все отступы завершаются. Когда цикл заканчивается, оба файла автоматически закрываются, а последняя строка программы выводит сообщение: print('Done!') (что означает «Готово!»). На рисунке 1.8 показаны результаты выполнения кода. В панели терминала отображается всего лишь сообщение © Done!', но, как видно на рисунке, в папке теперь появился новый файл — happy_pickle_copy.jpg. Если открыть этот файл, можно убедиться, что он является точной копией исходного файла. 344 РАЗДЕЛ 3 Работа с библиотеками
Рисунок 1.8. Запуск binarycopy.py добавил файл happy_pickle_ copy.jpg в папку Работа с CSV-файлами CSV (от Comma-Separated Values — значения, разделенные запятыми) — это широко используемый формат для хранения и обмена табличными данными. Такие данные можно представить в виде таблицы, состоящей из строк и столбцов. В табличных редакторах, таких как Microsoft Excel, Apple Numbers или Google Таблицы, эти данные отображаются в привычном табличном виде, как показано на рисунке 1.9. Рисунок 1.9. CSV-файл в Microsoft Excel ГЛАВА 1 Работа с внешними файлами 345
Однако без специальной программы, которая визуализирует таблицы, каждая строка CSV-файла представляет собой обычную строку текста, а отдельные значения в ней разделяются запятыми. Например, если открыть файл, показанный на рисунке 1.9, в простом текстовом редакторе — таком как Блокнот или TextEdit, можно увидеть, как данные действительно хранятся внутри файла (рисунок 1.10). Рисунок 1.10. CSV-файл в текстовом редакторе В текстовом редакторе первая строка, часто называемая заголовком, содержит названия столбцов или имена полей, которые отображаются в первой строке электронной таблицы. Если обратить внимание на имена во втором примере, то видно, что в исходном CSV-файле они заключены в кавычки, например: "Angst, Annie" Кавычки указывают, что содержимое между ними следует рассматривать как единое целое. Иными словами, запятая между фамилией и именем является частью самого имени, а не разделителем, обозначающим начало нового столбца. Таким образом, первые два столбца этой строки выглядят так: "Angst, Annie", 1982 а не так: Angst, Annie То же правило действует и для остальных строк: имя, заключенное в кавычки (включая запятые), считается одним полем данных, а не двумя отдельными столбцами. Если строка содержит апостроф (тот же символ, что и одинарная кавычка), необходимо заключать ее в двойные кавычки. Иначе Python неправильно интерпретирует строку. Например, если написать: 346 РАЗДЕЛ 3 Работа с библиотеками
'O'Henry, Harry' то первой частью строки будет 'O', а оставшийся текст после второй одинарной кавычки вызовет ошибку синтаксиса. Чтобы избежать путаницы, используйте двойные кавычки, так как внутри имени нет других двойных кавычек: "O'Henry, Harry" На рисунке 1.10 показаны и другие особенности структуры CSV-файлов. Например, имя Bónañas, Barry содержит символы, выходящие за пределы ASCII-кодировки. Предпоследняя строка состоит только из набора запятых — в CSV это означает, что ячейки пусты, и каждая запятая просто разделяет отсутствующие значения. Кроме того, в столбце Balance числа содержат знаки доллара и запятые, что делает их несовместимыми с типом float в Python. О том, как решать все эти проблемы, мы подробно поговорим в следующих разделах. Хотя вы можете работать с CSV-файлами, используя только уже изученные методы, эта задача будет выполняться намного быстрее и проще, если использовать встроенный модуль csv, который уже доступен в Python. Чтобы воспользоваться им, просто добавьте следующую строку в начало вашей программы: import csv. Помните, эта строка не импортирует сам CSV-файл. Она импортирует готовый модуль, содержащий инструменты, которые упрощают чтение, запись и обработку CSV-файлов в вашем коде на Python. Открытие CSV-файла Процесс открытия CSV-файла ничем не отличается от открытия любого другого файла. Однако стоит помнить, что, если файл содержит специальные символы, необходимо добавить параметр encoding='utf-8', чтобы избежать ошибок при чтении. Кроме того, при импорте данных вы, скорее всего, не захотите считывать символ новой строки в конце каждой строки файла. Чтобы этого избежать, в функцию open() можно добавить параметр newline=''. Вот пример, как можно прокомментировать и записать этот код (только замените sample.csv на путь к нужному CSV-файлу): ГЛАВА 1 Работа с внешними файлами 347
# Открываем CSV-файл в кодировке UTF-8, не считываем символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: Для перебора строк CSV-файла можно использовать встроенную функцию reader(), которая возвращает итерируемый объект, проходящий по строкам файла. Опять же синтаксис предельно прост: reader = csv.reader(f) Здесь f вы можете заменить использованное в конце оператора open (обратите внимание, что двоеточие в конце строки with open(…) as f: не включается). Если вы хотите подсчитывать строки во время перебора, можно обернуть вызов csv.reader() в функцию enumerate(), чтобы получить счетчик строк вместе с их содержимым. Вот пример кода с поясняющим комментарием: # Создаем счетчик строк CSV и средство чтения строк reader = enumerate(csv.reader(f)) Теперь вы можете настроить цикл для построчного чтения CSV-файла. Поскольку в коде используется нумерация строк, в цикле for можно задать две переменные. • • Первая переменная (в примере — i) отслеживает номер строки — она начинается с 0 и увеличивается на 1 при каждом проходе цикла . Вторая переменная (row) содержит всю строку данных из CSVфайла . Пример: # Проходим по одной строке за раз, i - счетчик, row - вся строка for i, row in reader: Чтобы увидеть, что происходит на каждом шаге, можно использовать функцию print() для вывода значений i и row: import csv # Открываем CSV-файл в кодировке UTF-8, не считываем символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: # Создаем счетчик строк CSV и средство чтения строк reader = enumerate(csv.reader(f)) 348 РАЗДЕЛ 3 Работа с библиотеками
# Проходим по одной строке за раз, i - счетчик, row - вся строка for i, row in reader: print(i, row) print('Done') При запуске этого кода с файлом sample.csv, описанным ранее, на экране появится следующий вывод: 0 ['Full Name', 'Birth Year', 'Date Joined', 'Is Active', 'Balance'] 1 ['Angst, Annie', '1982', '1/11/2011', 'TRUE', '$300.00'] 2 ['Bónañas, Barry', '1973', '2/11/2012', 'FALSE', '-$123.45'] 3 ['Schadenfreude, Sandy', '2004', '3/3/2003', 'TRUE', '$0.00'] 4 ['Weltschmerz, Wanda', '1995', '4/24/1994', 'FALSE', '$999,999.99'] 5 ['Malaise, Mindy', '2006', '5/5/2005', 'TRUE', '$454.01'] 6 ["O'Possum, Ollie", '1987', '7/27/1997', 'FALSE', '-$1,000.00'] 7 ['', '', '', '', ''] 8 ['Pusillanimity, Pamela', '1979', '8/8/2008', 'TRUE', '$12,345.67'] Обратите внимание, что первая строка, содержащая названия столбцов, имеет номер 0. Если вы вдруг увидите в начале этой строки странные символы вроде \ufeff перед Full Name, не пугайтесь — это маркер последовательности байтов (BOM), который иногда автоматически добавляется Excel или другими программами. Обычно содержимое первой строки не играет ключевой роли, так как реальные данные начинаются со второй строки. Поэтому не стоит беспокоиться о маркере BOM — он не несет практической пользы, но и не вызывает ошибок. Как видно из вывода, каждая строка представляет собой список из пяти элементов, разделенных запятыми. В вашем коде вы можете обращаться к отдельным значениям строки по их позиции (индексу): • • row[0] — первый столбец, содержит имя человека; row[1] — год рождения; ГЛАВА 1 Работа с внешними файлами 349
• • • row[2] — дату регистрации; row[3] — показывает, активен ли человек; row[4] — содержит баланс . Такой способ доступа позволяет удобно работать с отдельными полями CSV-файла. Все данные, содержащиеся в CSV-файле, представляют собой строки — даже если на первый взгляд это не так. Любые данные, полученные из CSV-файла, будут строками, поскольку CSV — это разновидность текстового файла, а текстовые файлы содержат только текст, то есть строковые значения, без целых чисел, дат, логических значений или чисел с плавающей точкой. В большинстве приложений вам, вероятно, потребуется преобразовать эти строки в соответствующие типы данных Python, чтобы можно было работать с ними эффективнее или, например, загружать их в базу данных. В следующих разделах мы рассмотрим, как выполнять такое преобразование для различных типов данных. Преобразование строк Технически преобразовывать строки из CSV-файла не требуется, поскольку все данные уже являются строками. Однако нередко возникает необходимость разбить строку на части или обработать пустые строки в файле. Прежде всего, как уже отмечалось, нас интересуют только реальные данные, а не первая строка, которая содержит заголовки столбцов. Поэтому внутри цикла можно добавить условие if, которое пропускает первую строку. Для этого замените строку print(i, row) на следующий код: # Строка 0 - это заголовки столбцов, игнорируем ее if i > 0: full_name = row[0].split(',') last_name = full_name[0].strip() first_name = full_name[1].strip() Когда условие в операторе if истинно (True), выполняются строки кода с отступом, расположенные ниже. Первая из них создает список с именем full_name, где элемент с индексом 0 хранит фамилию человека, а элемент с индексом 1 — его имя. Затем код записывает фамилию в переменную last_name, а имя — в переменную first_name. Метод .strip() удаляет лишние пробелы в начале и в конце строки, чтобы данные сохранялись аккуратно. 350 РАЗДЕЛ 3 Работа с библиотеками
Однако если запустить код в таком виде, возникнет ошибка. Это происходит потому, что строка 7 не содержит имени, и Python не может разделить пустую строку по запятой — ведь в ней просто нечего делить. Чтобы избежать этой ошибки, можно указать Python выполнять разделение имени по запятой только тогда, когда строка действительно содержит данные. Если при попытке разделить строку возникает ошибка, можно просто сохранить пустую строку в переменных full_name, last_name и first_name. Ниже приведен код с дополнительными комментариями, поясняющими, что именно в нем происходит. Вместо вывода номера строки и ее полного содержимого программа теперь выводит только имя и фамилию. Для строк, где данные отсутствуют, ничего не выводится. import csv # Открываем CSV-файл в кодировке UTF-8, не учитываем символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: # Создаем счетчик строк CSV и средство чтения строк reader = enumerate(csv.reader(f)) # Проходим по одной строке за раз, i - счетчик, row - вся строка целиком for i, row in reader: # Строка 0 содержит только заголовки столбцов, пропускаем ее if i > 0: # Разделяем полное имя на две части по запятой try: full_name = row[0].split(',') # Фамилия, удаляем лишние пробелы last_name = full_name[0].strip() # Имя, удаляем лишние пробелы first_name = full_name[1].strip() except IndexError: full_name = last_name = first_name = "" print(first_name, last_name) print('Done!') Annie Barry Sandy Wanda Mindy Ollie Angst Bónañas Schadenfreude Weltschmerz Malaise O'Possum Pamela Pusillanimity Done! ГЛАВА 1 Работа с внешними файлами 351
Преобразование в целые числа Второй столбец каждой строки, row[1], содержит год рождения. Если это значение представляет собой число, его можно легко преобразовать в целое число с помощью встроенной функции int(). Однако в нашем примере есть строка 7, где это поле пустое. В таком случае Python не сможет автоматически преобразовать пустую строку в число и выдаст ошибку. Чтобы избежать этой проблемы, можно подсказать Python, что нужно использовать 0, если значение отсутствует. Для этого используется выражение row[1] or 0, которое подставляет 0 вместо пустой строки. Тогда строка кода будет выглядеть так: # Год рождения как целое число, ноль для пустой строки birth_year = int(row[1] or 0) Код выглядит удивительно просто, и в этом проявляется одно из главных преимуществ Python — его лаконичность и понятность. Эта строка говорит программе следующее: создай переменную с именем birth_year и запиши в нее значение из второго столбца, если оно существует. А если в этой ячейке ничего нет и преобразование в целое число невозможно, просто используй значение 0. Преобразование в дату В третьем столбце нашего CSV-файла — row[2] — хранится дата регистрации. Почти во всех строках это корректные даты, за исключением строки, где данные отсутствуют. Чтобы преобразовать такую текстовую дату в настоящий объект даты Python, нужно сначала подключить модуль datetime. Для удобства обычно используют короткое имя, добавив в начало программы строку: import datetime as dt. После этого преобразование выполняется с помощью метода strptime(), который считывает строку и преобразует ее в дату по заданному формату. Пример кода: date_joined = dt.datetime.strptime(row[2], "%m/%d/%Y").date() В этом выражении происходит несколько важных вещей. Сначала создается переменная с именем date_joined. Затем используется метод strptime, что расшифровывается как string parse time — «разобрать строку как дату и время». 352 РАЗДЕЛ 3 Работа с библиотеками
Аргумент row[2] указывает на третий столбец CSV-файла (напомним, что нумерация в Python начинается с нуля). Формат "%m/%d/%Y" сообщает методу, как именно записана дата в файле: сначала идет месяц (%m), затем день (%d), а потом четырехзначный год (%Y), и части даты разделены косыми чертами. Наконец, добавление метода .date() в конце говорит Python взять только часть, относящуюся к дате, без информации о времени. В результате переменная date_joined будет содержать корректный объект даты, с которым можно выполнять любые операции, связанные с датами. Здесь есть небольшая проблема. Когда программа дойдет до строки, в которой дата отсутствует, она завершится с ошибкой. Чтобы этого избежать, можно использовать блок try, который попытается выполнить преобразование. Если преобразовать дату не удастся, программа перейдет к блоку except и установит значение переменной date_joined равным None. Значение None в Python обозначает пустой объект и используется в тех случаях, когда данных просто нет. Такой подход позволяет программе продолжить выполнение без ошибок, даже если в некоторых строках CSV-файла отсутствуют даты. ЗАПОМНИТЕ В Python datetime — это класс, поэтому любая созданная дата или время представляют собой объект этого типа. Пустая строка обозначается как '', а отсутствие объекта — как None. Вот как выглядит код на этом этапе: в начале добавлен импорт модуля datetime, а для преобразования текстовой даты в формат даты Python используется конструкция try…except, чтобы программа не завершалась с ошибкой, если встречает пустую или некорректную строку: import csv import datetime as dt # Открываем CSV-файл с кодировкой UTF-8, не считываем символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: # Создаем счетчик строк CSV и средство чтения строк reader = enumerate(csv.reader(f)) # Перебираем по одной строке, i - счетчик, row - вся строка for i, row in reader: # Строка 0 содержит только заголовки столбцов, пропускаем ее if i > 0: ГЛАВА 1 Работа с внешними файлами 353
# Полное имя разбивается на две части по запятой try: full_name = row[0].split(',') # Фамилия, удаляем лишние пробелы last_name = full_name[0].strip() # Имя, удаляем лишние пробелы first_name = full_name[1].strip() except IndexError: full_name = last_name = first_name = "" # Год рождения - целое число, 0 для пустой строки birth_year = int(row[1] or 0) # date_joined - это дата try: date_joined = dt.datetime.strptime(row[2], "%m/%d/%Y").date() except ValueError: date_joined = None print(first_name, last_name, birth_year, date_ joined) print('Готово!') Вот как выглядит результат выполнения программы. Она выводит имя, фамилию, год рождения и дату регистрации (first_name, last_name, birth_year, and date_joined) для каждой строки, где есть данные: Annie Angst 1982 2011.01.11 Barry Bónañas 1973 2012.02.11 Sandy Schadenfreude 2004 2003.03.03 Wanda Weltschmerz 1995 1994.04.24 Mindy Malaise 2006 2005.05.05 Ollie O'Possum 1987 1997.07.27 0 None Pamela Pusillanimity 1979 2008-08-08 Готово! 354 РАЗДЕЛ 3 Работа с библиотеками
Преобразование в логический тип Четвертый столбец (row[3]) в каждой строке CSV-файла содержит значения TRUE или FALSE, которые Excel записывает заглавными буквами при сохранении файла в формате CSV. Иногда этот столбец может быть и пустым. Чтобы преобразовать данные этого столбца в логический тип Python, можно проверить, равняется ли значение строке © TRUE© . Если да — установить переменную is_active в True; во всех остальных случаях — в False. Вот как это выглядит в коде: # is_active - логическое значение. Преобразуем TRUE в True, все остальное в False is_active = True if row[3] == 'TRUE' else False После выполнения этой строки кода переменная is_active в Python будет содержать логическое значение True, если в соответствующей ячейке CSV-файла указано TRUE. Во всех остальных случаях — если значение равно FALSE или ячейка пуста — переменная получит значение False. Такой способ преобразования позволяет корректно обрабатывать данные даже при неполных записях и делает работу программы более надежной. Преобразование в числа с плавающей точкой Пятый столбец CSV-файла содержит значения баланса — денежные суммы, которые нужно преобразовать в числовой формат, чтобы можно было выполнять с ними математические операции. В Python для этого используется тип данных float, позволяющий работать с десятичными числами. Однако перед преобразованием нужно очистить строку от лишних символов: знака доллара $, запятых и пробелов. Такие символы недопустимы для чисел с плавающей точкой и вызовут ошибку при попытке преобразования. Для этого используется метод replace(), который заменяет указанные символы на пустую строку, а также метод strip(), убирающий пробелы в начале и конце. В результате получается строка, содержащая только цифры и точку — готовая для преобразования в число: ГЛАВА 1 Работа с внешними файлами 355
# Удаляем $, запятые, начальные и конечные пробелы str_balance = (row[4].replace('$', '').replace(',', '')).strip() Эту строку кода можно понимать так: «Создай переменную с именем balance и помести в нее число с плавающей точкой, преобразованное из str_balance. Если строка str_balance пуста или не содержит данных, установи значение 0». Функция float() выполняет преобразование строки в число с плавающей точкой, которое Python может использовать в арифметических операциях. Благодаря конструкции or 0 программа не завершится с ошибкой даже в случае пустого значения — вместо этого в переменную будет записан нуль. Таким образом, строка balance = float(str_balance or 0) создает переменную balance, которая всегда содержит корректное числовое значение и готова для дальнейших вычислений. ИСПОЛЬЗОВАНИЕ РЕГУЛЯРНЫХ ВЫРАЖЕНИЙ В PYTHON Хотя мы предполагаем, что вы не знакомы с другими языками программирования, некоторые читатели могут их знать и, возможно, задаются вопросом, почему мы не использовали регулярное выражение вместо метода replace() для удаления знака доллара и запятой из balance . Регулярные выражения представляют собой мощный инструмент для поиска и замены шаблонов в тексте . В Python они не встроены по умолчанию, поэтому, если вы хотите их использовать, нужно импортировать соответствующий модуль . Для этого в начало программы добавляется строка from re import sub Модуль re содержит различные функции для работы с шаблонами, а sub() — это функция, которая выполняет замену найденных символов или последовательностей на другие . После импорта можно заменить более простую строку с методом replace() на вариант с регулярным выражением: str_balance = (row[4].replace('$', '').replace(',', '')). strip() 356 РАЗДЕЛ 3 Работа с библиотеками
и в результате будет такая упрощенная строка: str_balance = (sub(r'[\s\$,]', '', row[4])).strip() Результат работы этого кода будет тем же, что и при использовании цепочки replace(): из значения пятого столбца удаляются лишние символы — пробелы, символ валюты и запятые — чтобы подготовить строку к безопасному преобразованию в число . Код на рисунке 1.11 показывает все элементы на своих местах, включая функцию print(), которая отображает значения всех пяти столбцов после преобразования. Рисунок 1.11. Чтение файла CSV и преобразование его в типы данных Python ГЛАВА 1 Работа с внешними файлами 357
Преобразование данных из CSV в объекты и словари Ранее вы уже познакомились с тем, как считывать данные из CSV-файла и приводить их из строкового типа по умолчанию к подходящим типам данных Python. Однако нередко возникает необходимость не просто прочитать данные, а организовать их более удобно — например, сгруппировать в набор объектов, созданных на основе одного класса, или сформировать структуру из словарей, вложенных друг в друга. Все приемы, изученные ранее, пригодятся и здесь, так как они необходимы для выполнения этой задачи. Чтобы код оставался понятным и не перегруженным, операции преобразования данных удобно выносить в отдельные функции. Такой подход делает программу более аккуратной и позволяет использовать преобразование в любом месте кода, просто вызвав нужную функцию с аргументом в скобках. Например, если требуется преобразовать значение баланса, достаточно написать balance(row[4]). Это делает код компактным и наглядным, а работу с CSV-данными — более гибкой и понятной. Импорт CSV в объекты Python Если вы хотите организовать данные из CSV-файла в список объектов, можно воспользоваться следующим примером кода: import datetime as dt import csv # Используйте эти функции для преобразования любой строки # в соответствующий тип данных Python. # Получить только имя из полного имени. def fname(any): try: nm = any.split(',') return nm[1] except IndexError: return '' # Получить только фамилию из полного имени. def lname(any): try: nm = any.split(',') 358 РАЗДЕЛ 3 Работа с библиотеками
return nm[0] except IndexError: return '' # Преобразовать строку в целое число или ноль, если значение отсутствует. def integer(any): return int(any or 0) # Преобразует дату формата мм/дд/гггг в объект date или None, если дата некорректна def date(any): try: return dt.datetime.strptime(any,"%m/%d/%Y").date() except ValueError: return None # Преобразует "TRUE" в True, все остальное в False def boolean(any): return True if any == "TRUE" else False # Преобразует строку в число с плавающей точкой или в ноль при отсутствии значения def floatnum(any): s_float = any.replace("$", "").replace(",", "") return float(s_float or 0) # Создаем пустой список для хранения данных о людях people = [] # Определяем класс, где каждый человек представлен как объект class Person: def __init__(self, id, first_name, last_name, birth_year, date_joined, is_active, balance): self.id = id self.first_name = first_name self.last_name = last_name self.birth_year = birth_year self.date_joined = date_joined self.is_active = is_active self.balance = balance # Открываем CSV-файл в кодировке UTF-8, игнорируя символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: # Создаем счетчик для CSV-reader reader = enumerate(csv.reader(f)) # Пропускаем первую строку с названиями столбцов f.readline() ГЛАВА 1 Работа с внешними файлами 359
# Построчно обрабатываем оставшиеся данные, где i - счетчик, # row - текущая строка целиком for i, row in reader: # Для каждой строки данных из CSV-файла создаем объект Person с уникальным id и соответствующими типами данных, добавляем в список people people.append(Person(i, fname(row[0]), lname(row[0]), integer(row[1]), date(row[2]), boolean(row[3]), floatnum(row[4]))) # Когда цикл выше завершится, показать все объекты в списке people. for p in people: print(p.id, p.first_name, p.last_name, p.birth_year, p.date_ joined, p.is_active, p.balance) Вот как работает этот код. В первых строках выполняются импорты необходимых модулей, после чего определяются несколько функций, каждая из которых отвечает за преобразование входных строковых данных в соответствующие типы данных Python. Эти функции помогают подготовить значения для дальнейшей обработки и делают код более аккуратным. По своей логике пример похож на те, что уже рассматривались ранее в этой главе. Основное отличие заключается в том, что операции преобразования вынесены в отдельные функции. Это решение позволяет лучше структурировать программу, упростить чтение кода и сделать его более удобным для сопровождения. import datetime as dt import csv # Используйте эти функции для преобразования любой строки в соответствующий тип данных Python. # Получить только имя из полного имени. def fname(any): try: nm = any.split(',') return nm[1] except IndexError: return '' # Получить только фамилию из полного имени. def lname(any): try: nm = any.split(',') return nm[0] except IndexError: return '' 360 РАЗДЕЛ 3 Работа с библиотеками
# Преобразовать строку в целое число или ноль, если значение отсутствует. def integer(any): return int(any or 0) # Преобразовать дату в формате мм/дд/гггг в объект даты или None, если дата недействительна. def date(any): try: return dt.datetime.strptime(any,"%m/%d/%Y").date() except ValueError: return None # Преобразовать TRUE в True, все остальное в False. def boolean(any): return True if any == "TRUE" else False # Преобразовать строку в число с плавающей точкой или в ноль, если значение отсутствует. def floatnum(any): s_float = any.replace("$", "").replace(",", "").strip() return float(s_float or 0) Следующая строка создает пустой список с именем people. Этот список предназначен для хранения объектов, которые программа будет формировать при чтении данных из CSV-файла: # Создать пустой список людей. people = [] Далее код определяет класс, который будет использоваться для создания каждого объекта Person из CSV-файла: # Определить класс, где каждый человек является объектом. class Person: def __init__(self, id, first_name, last_name, birth_year, date_joined,is_active, balance): self.id = id self.first_name = first_name self.last_name = last_name self.birth_year = birth_year self.date_joined = date_joined self.is_active = is_active self.balance = balance ГЛАВА 1 Работа с внешними файлами 361
Чтение CSV-файла начинается с последующих строк программы. Сначала открывается файл sample.csv в кодировке UTF-8. Параметр newline='' используется для того, чтобы избежать добавления лишнего символа новой строки к последнему элементу каждой строки данных. Для чтения применяется нумератор, который ведет счет строк по мере их обработки. Метод f.readline() считывает первую строку файла, где находятся только заголовки столбцов, поэтому основной цикл for начинает работу со второй строки. В теле цикла переменная i служит счетчиком, а переменная row содержит всю строку данных, извлеченную из CSV-файла. Такой подход позволяет последовательно обрабатывать каждую запись и передавать значения в нужном порядке для создания объектов. # Открываем CSV-файл в кодировке UTF-8, не читаем символы новой строки with open('sample.csv', encoding='utf-8', newline='') as f: # Настраиваем считыватель csv со счетчиком reader = enumerate(csv.reader(f)) # Пропускаем первую строку с названиями столбцов f.readline() # Проходим по оставшимся строкам по одной, # i - счетчик, row - вся строка for i, row in reader: При каждом проходе цикла программа создает новый объект класса Person, используя значение счетчика i и данные из текущей строки. Для преобразования строковых значений в нужные типы данных применяются функции, определенные ранее в коде. Такой подход делает программу более аккуратной, сокращает количество повторяющихся выражений и упрощает чтение кода. # Из каждой строки данных в CSV-файле создаем объект Person с уникальным id и преобразованными в нужные типы данными, после чего этот объект добавляется в список people. people.append(Person(i, fname(row[0]), lname(row[0]), integer(row[1]), date(row[2]), boolean(row[3]), floatnum(row[4]))) 362 РАЗДЕЛ 3 Работа с библиотеками
После завершения цикла следующий фрагмент кода выводит каждый созданный объект на экран, чтобы убедиться, что программа выполняется корректно и данные были обработаны правильно. # Когда цикл выше завершен, показываем все объекты в списке people for p in people: print(p.id, p.first_name, p.last_name, p.birth_year, p.date_joined,p.is_active, p.balance) На рисунке 1.12 показан результат выполнения программы. В дальнейшем код может выполнять любые необходимые действия с каждым объектом, а вывод на экран используется лишь для проверки и подтверждения корректной работы программы. Рисунок 1.12. Чтение CSV-файла в список объектов ГЛАВА 1 Работа с внешними файлами 363
Импорт CSV в словари Python Если вы хотите хранить каждую строку данных из CSV-файла в отдельном словаре, можно использовать код, похожий на пример с созданием объектов. В этом случае определять класс не нужно, так как объекты создаваться не будут. Вместо списка people создается пустой словарь с тем же именем, который будет использоваться для хранения всех отдельных словарей person. # Создаем пустой словарь people people = {} В цикле также используется счетчик i, который подсчитывает количество обработанных строк и одновременно служит уникальным ключом для каждого создаваемого словаря. Строка, начинающаяся с newdict =, создает словарь с данными из одной строки CSV-файла с помощью встроенной функции dict(). Следом каждой новой записи присваивается ключ, равный i плюс 1, чтобы нумерация начиналась с единицы, а не с нуля. # Перебираем оставшиеся строки по одной, i - счетчик, # row - вся строка целиком. for i, row in reader: # Из каждой строки данных в CSV-файле создаем элемент словаря с уникальным идентификатором и соответствующими типами данных, добавляем в список people. newdict = dict({'first_name': fname(row[0]), 'last_name': lname(row[0]), 'birth_year': integer(row[1]), 'date_joined' : date(row[2]), 'is_active' : boolean(row[3]), 'balance' : floatnum(row[4])}) people[i + 1] = newdict Чтобы убедиться, что код работает правильно, можно пройтись по всем элементам словаря people и вывести пары ключ:значение для каждого набора данных. Такой вывод позволяет наглядно увидеть содержимое каждой строки, преобразованной в словарь. На рисунке 1.13 показан результат выполнения этого кода в среде VS Code. 364 РАЗДЕЛ 3 Работа с библиотеками
Рисунок 1.13. Чтение CSV-файла в словарь словарей Ниже приведен полный код, который считывает данные из CSV-файла и сохраняет их в виде словарей: import datetime as dt import csv # Используйте эти функции для преобразования любой строки # в соответствующий тип данных Python. # Получить только имя из полного имени. def fname(any): try: nm = any.split(',') return nm[1] ГЛАВА 1 Работа с внешними файлами 365
except IndexError: return '' # Получить только фамилию из полного имени. def lname(any): try: nm = any.split(',') return nm[0] except IndexError: return '' # Преобразовать строку в целое число или ноль, если значение отсутствует. def integer(any): return int(any or 0) # Преобразовать дату в формате мм/дд/гггг в объект даты или None, если дата недействительна. def date(any): try: return dt.datetime.strptime(any, "%m/%d/%Y").date() except ValueError: return None # Преобразовать TRUE в True, все остальное в False. def boolean(any): return True if any == "TRUE" else False # Преобразовать строку в число с плавающей точкой или в ноль, если значение отсутствует. def floatnum(any): s_float = (any.replace('$', '').replace(',', '')).strip() return float(s_float or 0) # Создать пустой словарь для хранения данных people. people = {} # Открыть CSV-файл в кодировке UTF-8, не считывать символы новой строки. with open('sample.csv', encoding='utf-8', newline='') as f: # Настроить считыватель csv с счетчиком. reader = enumerate(csv.reader(f)) # Пропустить первую строку с названиями столбцов. f.readline() # Построчно пройти по оставшимся строкам, i - счетчик, # row - вся строка целиком. for i, row in reader: # Из каждой строки данных в CSV-файле создать запись о человеке с уникальным id и преобразованными в нужные типы данными, после чего этот объект добавляется в список people newdict = dict({'first_name': fname(row[0]), 'last_name': lname(row[0]), 'birth_year': integer(row[1]), 366 РАЗДЕЛ 3 Работа с библиотеками
'date_joined' : date(row[2]), 'is_active' : boolean(row[3]), 'balance' : floatnum(row[4])}) people[i + 1] = newdict # Когда цикл выше завершен, показываем все объекты в списке people for person in people.keys(): id = person print(id, people[person]['first_name'], people[person]['last_name'], people[person]['birth_year'], people[person]['date_joined'], people[person]['is_active'], people[person]['balance']) CSV-файлы получили широкое распространение, так как данные из электронных таблиц и баз данных можно легко экспортировать в этот формат. Извлечение информации из таких файлов порой бывает непростым, однако модуль csv в Python значительно упрощает эту задачу. Он берет на себя обработку множества технических деталей, позволяя без лишних усилий считывать данные построчно и использовать их в своем приложении так, как это необходимо. Существует и другой популярный формат хранения и обмена данными в текстовом виде — JSON. О работе с форматом JSON и его возможностях рассказывается в следующей главе.
В этой главе вы узнаете: » Как структурировать данные в формате JSON » Что такое сериализация и как она используется » Как импортировать данные из файлов JSON » Как экспортировать данные из Python в формат JSON Гл а в а 2 Работа с данными JSON J SON (JavaScript Object Notation) — это широко используемый формат маршалинга, предназначенный для представления и передачи объектно ориентированных данных. Под маршалингом обычно понимают процесс преобразования данных в формат, удобный для пересылки между компьютерами или системами. Однако JSON применяется не только для обмена данными: некоторые базы данных, например бесплатная облачная база данных Firebase от Google, также хранят информацию именно в этом формате. Название может показаться немного запутанным из-за слова «JavaScript», особенно если вы работаете с Python, а не с JavaScript. На самом деле поводов для беспокойства нет: формат просто возник в экосистеме JavaScript, но со временем стал универсальным стандартом, который используется практически на всех языках программирования и платформах. В этой главе вы познакомитесь с самим форматом JSON, узнаете, как сохранять данные в этом формате и как загружать их обратно в программу. Если терминология вокруг JSON кажется сложной, не переживайте — все станет понятнее по мере изучения. Формат JSON во многом напоминает структуру словарей в Python, поэтому для его освоения не потребуется много новых знаний. Кроме того, в Python уже есть встроенный модуль json, который значительно упрощает работу с такими данными. 368 РАЗДЕЛ 3 Работа с библиотеками
Организация данных JSON Данные в формате JSON во многом похожи на словари в Python, поэтому работать с JSON-файлами достаточно просто. Лучше всего этот формат понять, если сравнить его с табличными данными. Например, на рисунке 2.1 приведена таблица Excel с несколькими строками данных, а на рисунке 2.2 показано, как эти же данные выглядят после преобразования в формат JSON. Каждая строка таблицы Excel превращается в словарь, состоящий из пар ключ:значение, что делает структуру данных более гибкой и понятной. Большое количество фигурных скобок в JSON указывает на то, что данные представлены в виде объектов, аналогичных словарям Python. Рисунок 2.1. Данные в электронной таблице Excel Рисунок 2.2. Данные электронной таблицы Excel, преобразованные в формат JSON ГЛАВА 2 Работа с данными JSON 369
ПРЕОБРАЗОВАНИЕ EXCEL В JSON Чтобы преобразовать данные из таблицы Excel в формат JSON, сначала необходимо сохранить их в формате CSV (значения, разделенные запятыми) . Для этого выберите в меню Excel пункт «Файл»  «Экспорт» или «Файл»  «Сохранить как» и укажите формат CSV . Аналогичным образом можно экспортировать данные из Google Таблиц или приложения Numbers от Apple . После того как CSV-файл будет создан, его можно преобразовать в JSON с помощью онлайн-сервиса, например на сайте convertcsv .com . На этом сайте выберите пункт «Конвертировать CSV в JSON» и выполните шаги, предложенные на экране . В результате вы получите данные в формате JSON, готовые к использованию в вашем Python-проекте . Экспорт данных из Excel — это лишь один из способов создания JSONфайла. Такой файл можно сформировать и вручную, добавив к каждому набору данных уникальный ключ. Каждый блок информации в этом случае будет представлен парой «ключ — данные», где ключ служит идентификатором элемента. Он может быть числовым или текстовым — это не имеет значения, главное, чтобы ключ оставался уникальным в пределах одного файла. При работе с JSON-файлами, созданными другими разработчиками, нередко можно встретить подобные структуры с ключами. Например, на личном сайте Алана используется бесплатная база данных реального времени Firebase от Google, которая хранит информацию о просмотрах страниц и другую связанную с ними статистику. В такой базе данные организованы в виде набора записей с автоматически создаваемыми ключами. Эти необычные значения, например -LAOqOxg6kmP4jhnjQXS, представляют собой уникальные идентификаторы, которые Firebase генерирует самостоятельно, чтобы избежать совпадений. Знак «плюс» рядом с каждым ключом в интерфейсе позволяет при необходимости раскрывать или сворачивать связанные с ним данные. Рисунок 2.3. Данные в базе данных реального времени Firebase от Google 370 РАЗДЕЛ 3 Работа с библиотеками
Как видно на рисунке, в Firebase предусмотрена функция Export JSON, с помощью которой можно сохранить данные в виде JSON-файла на свой компьютер. На рисунке 2.4 показано, как выглядят данные в таком файле. Легко заметить, что это JSON-файл с ключами: каждому набору данных предшествует уникальный идентификатор, например -LAOqAyxxHrPw6pGXBMZ, после которого стоит двоеточие. В Python можно без труда работать как с JSON-файлами, содержащими ключи, так и с теми, где они отсутствуют. Некоторые могли обратить внимание на поле lastvisit, которое не выглядит как привычная дата в формате мм/дд/гггг. На самом деле это поле хранит дату и время, но в виде числового значения, принятого в базе данных Firebase. Такие значения называют серийными датами. Не стоит беспокоиться о необычном виде этих данных — в дальнейшем в этой главе будет показано, как преобразовать их в понятный человеку формат даты и времени. Рисунок 2.4. База данных реального времени Firebase от Google, экспортированная в JSON-файл с ключами ГЛАВА 2 Работа с данными JSON 371
Понимание сериализации Когда речь заходит о JSON, первое важное понятие, с которым стоит познакомиться, — это сериализация. Под сериализацией понимается процесс преобразования объекта, например словаря Python, в поток байтов или символов. Такой поток можно передать по сети, сохранить в файле, базе данных или просто в памяти программы. Главная цель сериализации — сохранить всю информацию об объекте так, чтобы ее можно было легко восстановить на другом компьютере или в другой программе. Обратный процесс называется десериализацией — это преобразование сохраненных данных обратно в объект. Проще говоря: » » сериализация — это преобразование объекта в строку; десериализация — преобразование строки обратно в объект . В стандартную библиотеку Python входит модуль json, который предназначен для работы с JSON-файлами. Чтобы использовать его возможности, достаточно добавить в начало программы строку import json. Модуль предоставляет четыре основных метода для сериализации и десериализации данных, которые приведены в таблице 2.1. Типы данных в JSON во многом похожи на типы данных в Python, но полностью им не соответствуют. В таблице 2.2 показано, как именно происходит преобразование между типами данных двух языков во время сериализации и десериализации. Таблица 2.1. Методы Python для сериализации и десериализации данных JSON 372 Метод Назначение json.dump() Записывает (сериализует) данные Python в JSON-файл (или поток) json.dumps() Преобразует (сериализует) объект Python в строку JSON json.load() Считывает (десериализует) данные JSON из файла или подобного объекта json.loads() Считывает (десериализует) данные JSON из строки РАЗДЕЛ 3 Работа с библиотеками
Таблица 2.2. Преобразования типов данных между Python и JSON Python JSON Dict Object List, tuple Array Str String Int и float Number True True False False None Null Загрузка данных из JSON-файлов Загрузка данных из JSON-файлов начинается с подключения модуля json в начале программы. После этого можно воспользоваться стандартным методом open() для открытия нужного файла. Как и при работе с другими типами файлов, при необходимости можно указать параметр encoding="utf-8", чтобы корректно обрабатывались символы, не входящие в стандартную ASCII-кодировку. Кроме того, иногда полезно добавить параметр newline="", чтобы избежать добавления лишнего символа новой строки в конец каждой строки данных. Этот символ не является частью содержимого файла — он используется только для перехода на новую строку при выводе информации на экран. ОСТОРОЖНО При работе с этим кодом не называйте свой файл json.py. Если дать программе такое имя, команда import json в начале кода будет ссылаться не на встроенный модуль Python, а на ваш собственный файл. Это приведет к ошибкам при запуске и может вызвать непредсказуемое поведение программы. Чтобы загрузить данные из JSON-файла в программу на Python, сначала нужно придумать имя переменной, в которой будут храниться загруженные данные (в примере используется переменная people). Затем вызовите функцию json.load(), чтобы прочитать содержимое файла и сохранить его в этой переменной. Пример кода выглядит так: ГЛАВА 2 Работа с данными JSON 373
import json # Это данные из Excel (без ключей) filename = 'people_from_excel.json' # Открываем файл (стандартное открытие файла) with open(filename, 'r', encoding='utf-8', newline='') as f: # Загружаем весь json-файл в объект с именем people people = json.load(f) Выполнение этого кода не выводит результат на экран. Тем не менее вы можете исследовать объект people разными способами, добавив команды print() без отступов после последней строки программы. Например, следующая команда выведет все содержимое переменной people. print(people) Вывод начинается и заканчивается квадратными скобками ([]), что указывает на то, что переменная people представляет собой список. Чтобы убедиться в этом, можно выполнить следующую строку кода: print(type(people)) Python выводит: <class 'list'> Эта строка показывает, что объект является экземпляром класса list. Иными словами, это объект списка, хотя чаще его просто называют списком. Поскольку переменная представляет собой список, по нему можно выполнить итерацию с помощью цикла. Внутри цикла можно вывести тип каждого элемента, чтобы убедиться, какие данные он содержит. for p in people: print(type(p)) И результат будет таким: <class <class <class <class <class <class <class 'dict'> 'dict'> 'dict'> 'dict'> 'dict'> 'dict'> 'dict'> Эта информация полезна, так как она показывает, что каждый элемент переменной people (в коде он обозначен как p) представляет собой словарь Python. Это означает, что внутри цикла можно обращаться к от374 РАЗДЕЛ 3 Работа с библиотеками
дельным значениям, используя их ключи. Например, следующий код демонстрирует, как это сделать: for p in people: print(p['Full Name'], p['Birth Year'], p['Date Joined'], p['Is Active'], p['Balance']) При выполнении этого кода на экран выводятся все данные из JSONфайла. Они соответствуют тем же сведениям, которые ранее были показаны в таблице Excel на рисунке 2.1. Angst, Annie 1982 40554 True 300 Bónañas, Barry 1973 40950 False -123.45 Schadenfreude, Sandy 2004 37683 True 0 Weltschmerz, Wanda 1995 34448 False 999999.99 Malaise, Mindy 2006 38477 True 454.01 O'Possum, Ollie 1987 35638 True -1000 Pusillanimity, Pamela 1979 39668 True 12345.67 СОВЕТ Если весь процесс открытия и обработки JSON-файла кажется вам слишком сложным, не стоит переживать. В любой момент можно обратиться за помощью к инструментам генеративного искусственного интеллекта. Например, вы можете попросить систему вроде Claude.ai или Copilot создать код на Python, который откроет файл с именем whatever.json, выполнит перебор его содержимого и обработает возможные исключения. После этого останется лишь немного изменить предложенный код, чтобы он подходил для вашей конкретной задачи. Преобразование даты Excel в дату JSON Вы могли заметить, что в столбце Date Joined вместо привычных дат указаны числа вроде 40554, 40950 или 37683. Это так называемые серийные даты — числовое представление дат, которое используется в Excel. Такие значения можно преобразовать в обычные даты Python. Для этого нужно импортировать модули xlrd (средство для чтения файлов Excel) и datetime. Прежде чем использовать xlrd, его необходимо установить. Сделать это можно, выполнив в терминале VS Code следующую команду: pip install xlrd О модулях и команде pip install мы подробно поговорим в четвертом разделе. А пока эта команда позволит вам приступить к работе с данным приложением. ГЛАВА 2 Работа с данными JSON 375
В вашем Python-приложении для преобразования целого числа в поле p['Date Joined'] в объект даты Python используйте следующий код: y, m, d, h, i, s = xlrd.xldate_as_tuple(p['Date Joined'],0) joined = dt.date(y, m, d) Чтобы вывести эту дату в привычном для чтения формате, используйте форматированную строку следующим образом: print(f"{joined:%m/%d/%Y}") Вот полный код, включая необходимые импорты в начале файла: import json, xlrd import datetime as dt # Это данные из Excel (без ключей) filename = 'people_from_excel.json' # Открываем файл (стандартное открытие файла) with open(filename, 'r', encoding='utf-8', newline='') as f: # Загружаем весь json-файл в объект с именем people people = json.load(f) # Словари находятся в списке, перебираем и отображаем каждый словарь. for p in people: name = p['Full Name'] byear = p['Birth Year'] # Дата Excel довольно сложная, используем модуль xlrd. y, m, d, h, i, s = xlrd.xldate_as_tuple(p['Date Joined'], 0) joined = dt.date(y, m, d) balance = '$' + f"{p['Balance']:,.2f}" print(f"{name:<22} {byear} {joined:%m/%d/%Y} {balance:>12}") Вот результат, аккуратно отформатированный и более похожий на исходные данные Excel, чем на исходные данные JSON. Если вы хотите вывести дату в формате дд/мм/гггг, измените шаблон в последней строке кода на %d/%m/%Y. Angst, Annie Bónañas, Barry Schadenfreude, Sandy Weltschmerz, Wanda Malaise, Mindy O'Possum, Ollie Pusillanimity, Pamela 376 1982 1973 2004 1995 2006 1987 1979 РАЗДЕЛ 3 Работа с библиотеками 01/11/2011 02/11/2012 03/03/2003 04/24/1994 05/05/2005 07/27/1997 08/08/2008 $300.00 $-123.45 $0.00 $999,999.99 $454.01 $-1,000.00 $12,345.67
Перебор JSON-файла с ключами Перебор JSON-файла с ключами выполняется почти так же, как и работа с файлом без ключей. Разница заключается в том, что после загрузки данных вы получаете не список словарей, а словарь, в котором каждый элемент имеет свой уникальный ключ и собственный вложенный словарь. Ниже приведен пример кода, который открывает и загружает данные, экспортированные из Firebase (их исходный вид показан на рисунке 2.4). В этих данных хранится информация о количестве посещений страниц сайта: названии страницы, общем числе визитов, последнем источнике перехода (реферере) и времени последнего посещения. Как видно, код для открытия и загрузки JSON-файла почти не отличается от примеров, рассмотренных ранее. Вся информация загружается в объект, названный hits. import json import datetime as dt # Это данные Firebase JSON (с ключами) filename = 'firebase_hitcounts.json' # Открываем файл (стандартное открытие файла) with open(filename, 'r', encoding='utf-8', newline='') as f: # Загружаем весь json-файл в объект с именем people hits = json.load(f) print(type(hits)) При выполнении этого кода последняя строка выводит тип данных объекта hits, в который были загружены JSON-данные: <class 'dict'>. Это означает, что hits представляет собой словарь, где каждый элемент является вложенным словарем, а не отдельным элементом списка. Такой объект можно перебрать с помощью обычного цикла, так же как и при работе с JSON-файлом без ключей. for p in hits: print(p) Однако при выполнении этого цикла на экран будет выведено не так много информации. Отобразятся только ключи каждого вложенного словаря, находящегося внутри основного словаря hits: -LAOqAyxxHrPw6pGXBMZ -LAOqOxg6kmP4jhnjQXS -LAOrwciIQJZvuCAcyLO -LAOs2nsVVxbjAwxUXxE -LAOwqJsjfuoQx8WISlX -LAQ7ShbQPqOANbDmm3O ГЛАВА 2 Работа с данными JSON 377
-LAQrS6avlv0PuJGNm6P -LI0iPwZ7nu3IUgiQORH -LI2DFNAxVnT-cxYzWR- Это не ошибка и не проблема — именно так устроены вложенные словари. Каждый ключ в основном словаре указывает на отдельный подсловарь с собственными данными. Получить доступ к этим данным очень просто: можно использовать две переменные цикла, например k для ключа и v для значения, и перебрать элементы словаря с помощью метода hits.items(): or k, v in hits.items(): print(k, v) Использование двух переменных цикла позволяет получить другое представление данных: вы видите каждый ключ, за которым следует соответствующий ему словарь, заключенный в фигурные скобки, как показано на рисунке 2.5. Фигурные скобки указывают на то, что данные внутри представляют собой словарь. Рисунок 2.5. Вывод при переборе и отображении ключей и значений из подсловарей Значения каждого подсловаря хранятся в объекте v внутри цикла. Чтобы обратиться к отдельным элементам данных, используйте переменную v и укажите имя нужного поля в квадратных скобках. Например, выражение v['count'] возвращает значение, связанное с ключом count в данном подсловаре. Ниже приведен пример кода, который выводит данные, но не отображает сами ключи. for k, v in hits.items(): # Сохраняем элементы в переменных key = k hits = v['count'] last_visit = v['lastvisit'] 378 РАЗДЕЛ 3 Работа с библиотеками
page = v['page'] came_from = v['lastreferrer'] print(f"{hits} {last_visit} {page:<28} {came_from}") Результатом выполнения этого кода будут данные из каждого вложенного словаря, выведенные в более удобном для чтения виде. Такой формат позволяет легко просматривать значения полей и соответствует тому, что показано на рисунке 2.6. Рисунок 2.6. Вывод, показывающий по одному значению за раз из каждого словаря Возможно, вы заметили, что мы столкнулись с еще одной странной ситуацией со столбцом lastvisit. Дата отображается в формате 545316328750, а не в более привычном формате мм/дд/гггг. На этот раз мы не можем винить Excel, поскольку эти даты никогда не были в Excel. То, что вы видите — это временная метка Firebase, показывающая, когда элемент данных был последний раз записан в базу данных. Эта дата выражена как время UTC, включая время с точностью до наносекунды, поэтому число такое длинное. Очевидно, чтобы люди могли понимать эти даты, их нужно преобразовать в даты Python, что мы обсудим далее. Преобразование временных меток Firebase в даты Python Как и при любой работе с датами и временем в Python, сначала нужно убедиться, что в программе подключен модуль datetime. Обычно это делается с помощью команды import datetime as dt, где dt — это удобный псевдоним, сокращающий запись. Так как временные метки Firebase основаны на времени UTC, для их преобразования можно использовать метод datetime.utcfromtimestamp(). Он позволяет превратить числовое значение временной метки в объект даты и времени Python. Однако здесь есть одна особенность: если следовать документации, может показаться, что следующий код должен работать без проблем: last_visit = dt.datetime.utcfromtimestamp(v['lastvisit']) ГЛАВА 2 Работа с данными JSON 379
Однако, как оказалось, упомянутое ранее наносекундное разрешение слишком велико для Windows, и при выполнении такого кода возникает исключение OS Error. К счастью, это легко исправить. Если разделить значение lastvisit на 1000, лишние цифры будут отброшены, и временная метка станет менее точной — в таком виде ее уже можно корректно обработать в Windows. В этом примере нас интересует только дата последнего посещения, а не точное время. Поэтому можно получить только дату, избежав ошибки, используя следующий код: last_visit = dt.datetime.utcfromtimestamp(v['lastvisit'] / 1000). date() В результате в переменной last_visit будет храниться обычный объект даты Python, с которым можно работать так же, как с любыми другими датами. Это значит, что вы можете применить стандартную форматную строку и вывести дату в любом удобном виде. Например, используйте следующую конструкцию в форматированной строке для отображения значения last_visit: {last_visit: %m/%d/%Y} Даты в выводе будут отображаться в формате мм/дд/гггг, например: 12/20/2018 12/19/2018 12/17/2018 12/20/2018 11/30/2018 12/16/2018 12/20/2018 12/20/2018 12/19/2018 Загрузка JSON без ключей из строки Python Ранее мы использовали метод load(), который загружает данные JSON из файла. Однако, поскольку данные JSON представляют собой обычный текст, их можно разместить прямо в программе, заключив в строку Python. Для этого создается переменная, которой присваивается многострочная строка в тройных кавычках, — так же, как оформляются строки документации в Python. 380 РАЗДЕЛ 3 Работа с библиотеками
Внутрь тройных кавычек помещаются все данные JSON. Такой способ особенно удобен для экспериментов или небольших примеров, когда нет необходимости читать данные из отдельного файла. В приведенном ниже примере для краткости показаны данные только для нескольких человек, чтобы можно было увидеть общую структуру JSON. import json # Здесь данные JSON находятся в большой строке с именем json_ string. # Она начинается с первых тройных кавычек и продолжается # до последних тройных кавычек. json_string = """ { "people": [ { "Full Name": "Angst, Annie", "Birth Year": 1982, "Date Joined": "01/11/2011", "Is Active": true, "Balance": 300 }, { "Full Name": "Schadenfreude, Sandy", "Birth Year": 2004, "Date Joined": "03/03/2003", "Is Active": true, "Balance": 0 } ] } """ Возможность видеть все данные прямо в коде может быть удобной, однако у такого подхода есть важный недостаток — строку невозможно перебрать для доступа к отдельным элементам. Если требуется обрабатывать данные поэлементно, их нужно преобразовать из строкового представления в объект Python. Для этого используется метод json.loads(), где буква s указывает, что данные загружаются из строки (string). Пример кода ниже показывает, как это сделать. Имя переменной peep_data выбрано, чтобы отличить загруженные данные JSON от исходной строки, содержащей текстовые данные. # Загружаем JSON-данные из большой строки json_string peep_data = json.loads(json_string) ГЛАВА 2 Работа с данными JSON 381
Теперь, когда у вас есть объект peep_data, можно выполнять перебор его элементов и обрабатывать данные по частям. Например, это можно сделать следующим образом: # Теперь можно перебрать коллекцию peep_data for p in peep_data['people']: print(p["Full Name"], p["Birth Year"], p["Date Joined"], p['Is Active'], p['Balance']) На рисунке 2.7 представлен полный код вместе с результатом его выполнения в среде VS Code. Рисунок 2.7. Вывод, показывающий значения по одному из каждого словаря (см . нижнюю часть изображения) Загрузка ключевых JSON-данных из строки Python В строке Python можно хранить не только данные без ключей, но и данные, где каждый элемент имеет собственный ключ. В следующем примере снова используется переменная json_string, но ее содержимое организовано по-другому: первый элемент имеет ключ 1, а второй — 382 РАЗДЕЛ 3 Работа с библиотеками
ключ 2. Как и раньше, чтобы загрузить эти данные из строки в объект JSON, применяется метод json.loads(json_string). После этого можно работать с данными так же, как и с любым другим словарем в Python. import json # Здесь JSON-данные находятся в большой строке с именем json_ string. # Она начинается с первых тройных кавычек и продолжается # до последних тройных кавычек. json_string = """ { "1": { "count": 9061, "lastreferrer": "https://difference-engine.com/Courses/ tml-5.1118/", "lastvisit": "12/20/2018", "page": "/etg/downloadpdf.html" }, "2": { "count": 3342, "lastreferrer": "https://alansimpson.me/", "lastvisit": "12/19/2018", "page": "/html_css/index.html" } } """ # Загружаем JSON-данные из большой строки json_string. hits_data = json.loads(json_string) # Теперь можно перебрать коллекцию hits_data. for k, v in hits_data.items(): print(f"{k}. {v['count']:>5} - {v['page']}") Цикл в конце программы использует две переменные — k и v — для перебора hits_data.items(), что является стандартным способом обхода словаря словарей в Python. В ходе выполнения цикла выводятся ключ, количество посещений и название страницы из каждого элемента данных в следующем формате: 1. 9061 - /etg/downloadpdf.html 2. 3342 - /html_css/index.html ГЛАВА 2 Работа с данными JSON 383
Изменение данных JSON Когда JSON-данные загружены в словарь, вы можете работать с ними, применяя обычные операции со словарями, рассмотренные ранее в главе 4 раздела 2. Это означает, что данные можно изменять, добавлять или удалять так же, как и в обычном словаре Python. Во время перебора словаря с использованием переменных для пар ключ: значение вы можете изменить любое значение, применив следующий простой синтаксис: value['key'] = newdata Например, предположим, что вы перебираете словарь, созданный из базы данных Firebase, в котором поле lastvisit представлено в виде метки времени UTC. Если вы хотите преобразовать эту метку в привычный формат даты Python, можно настроить цикл следующим образом. В первой строке внутри цикла создается новая переменная pydate, в которую записывается дата в формате Python. Затем во второй строке значение поля v['lastvisit'] заменяется этой датой, отформатированной в виде мм/дд/гг: for k, v in hits_data.items(): # Преобразуем дату Firebase в дату Python pydate = dt.datetime.utcfromtimestamp(v['lastvisit'] / 1000).date() # В словаре заменяем дату Firebase на дату Python v['lastvisit'] = f"{pydate:%m/%d/%Y})" Когда выполнение цикла завершится, все значения в поле lastvisit будут преобразованы и будут отображаться в формате мм/дд/гггг вместо исходных временных меток Firebase. Удаление данных из словаря Чтобы удалить данные из словаря во время перебора, используйте следующий синтаксис: pop('keyname', None) Тут 'keyname' означает имя столбца, который вы хотите удалить. Например, чтобы удалить все ключи lastreferrer и связанные с ними данные из словаря, созданного в примере с JSON из базы данных Firebase, добавьте в цикл следующую строку: 384 РАЗДЕЛ 3 Работа с библиотеками
v.pop('lastreferrer', None) Параметр None в этом коде не является обязательным, но его использование предотвращает возникновение ошибки Python, если указанный ключ отсутствует в словаре. На рисунке 2.8 приведен пример, в котором строки 1–8 импортируют данные из Firebase в объект Python с именем hits_data. В строке 10 начинается цикл, перебирающий каждый ключ (k) и значение (v) внутри словаря. В строке 12 временная метка преобразуется в объект даты Python с именем pydate. Затем в строке 14 значение поля lastvisit заменяется этой датой, представленной в строковом формате мм/дд/гггг. В строке 16 выражение v.pop('lastreferrer', None) удаляет из каждого словаря пару ключ:значение lastreferrer. Завершающий цикл выводит обновленное содержимое словаря после всех внесенных изменений. Рисунок 2.8. Изменение значения одного ключа в каждом словаре и удаление целой пары ключ: значение из словаря Имейте в виду, что изменения, внесенные в словарь внутри программы на Python, не изменяют исходный файл или строку, из которых были загружены данные JSON. Если необходимо сохранить обновленные данные, можно создать новую JSON-строку или файл с помощью методов json.dumps() или json.dump(). Эти методы будут рассмотрены далее. ГЛАВА 2 Работа с данными JSON 385
Выгрузка данных Python в JSON До этого момента рассматривалось, как импортировать JSON-данные из внешних источников в приложение, чтобы Python мог их использовать. Однако нередко требуется выполнить обратную операцию — экспортировать данные, уже находящиеся в программе (например, словарь), обратно в формат JSON. Это может понадобиться для передачи данных другому приложению, публикации или сохранения в удобном текстовом виде. Для этого используются методы json.dump() и json.dumps(). Метод dumps() создает JSON-строку из данных, которые находятся в памяти, поэтому ее можно сразу вывести на экран с помощью print(). Например, в предыдущих примерах мы импортировали данные из базы Firebase в словарь Python, затем преобразовали временные метки в формат даты мм/дд/гггг и удалили пары ключ:значение lastreferrer. Если теперь нужно создать JSON-строку из обновленного словаря, можно воспользоваться методом dumps(). В результате получится строка с именем new_dict, которую можно вывести в консоль. Последние две строки кода после цикла будут выглядеть следующим образом: # Цикл завершен, копируем новый словарь в JSON-строку new_dict = json.dumps(hits) print(new_dict) Строка new_dict будет выведена в своем исходном, компактном и не слишком удобном для чтения виде. Примерно это будет выглядеть так: {"-LAOqAyxxHrPw6pGXBMZ": {"count": 9061, "lastvisit": "12/20/2018)", "page": "/etg/downloadpdf.html"}, "-LAOqOxg6kmP4jhnjQXS": {"count": 3896, "lastvisit": "12/20/2018)", "page": "/"}, "-LAOrwciIQJZvuCAcyLO": {"count": 3342, "lastvisit": "12/20/2018)", "page": "/html_css/index.html"}, ... }} Мы сократили часть данных, обозначив ее многоточием (…), так как нет необходимости просматривать весь вывод, чтобы понять, насколько трудно читать такой результат. К счастью, метод dumps() поддерживает параметр indent=, который позволяет добавить отступы в структуру JSON и сделать данные гораздо более наглядными. Обычно достаточно установить отступ в два пробела. Например, добавьте indent=2 к предыдущему коду: 386 РАЗДЕЛ 3 Работа с библиотеками
# Цикл завершен, копируем новый словарь в JSON-строку new_dict = json.dumps(hits, indent=2) print(new_dict) Вывод отображает данные JSON в значительно более удобном и читаемом виде: { "-LAOqAyxxHrPw6pGXBMZ": { "count": 9061, "lastvisit": "12/20/2018)", "page": "/etg/downloadpdf.html" }, "-LAOqOxg6kmP4jhnjQXS": { "count": 3896, "lastvisit": "12/20/2018)", "page": "/" }, ... } Если в вашем словаре содержатся символы, выходящие за пределы ASCII, и вы хотите сохранить их без преобразования, добавьте параметр ensure_ascii=False в код следующим образом: new_dict = json.dumps(hits, indent=2, ensure_ascii=False) В нашем примере имена ключей в каждом словаре уже расположены в алфавитном порядке (count, lastvisit, page). Однако если вы хотите быть уверены, что ключи в вашем собственном JSON всегда будут выведены в алфавитном порядке, добавьте параметр sort_keys=True к методу dumps следующим образом: new_dict = json.dumps(hits, indent=2, ensure_ascii=False, sort_ keys=True) Если вы хотите сохранить данные JSON в файл, используйте метод json.dump() вместо json.dumps(). При необходимости можно добавить параметр ensure_ascii=False, чтобы сохранить символы, не входящие в ASCII, а также sort_keys=True, чтобы ключи в словарях были расположены в алфавитном порядке. Дополнительно можно указать параметр indent=, который создаст отступы и сделает содержимое файла более читаемым. ГЛАВА 2 Работа с данными JSON 387
Однако стоит помнить, что JSON редко используется как формат для чтения человеком — его основное назначение заключается в обмене данными между приложениями и базами данных. Например, если нужно создать новый файл с именем hitcounts_new.json (или перезаписать существующий), и при этом сохранить все символы, не входящие в ASCII, можно использовать следующий код. Параметр © w© в функции open() необходим для открытия файла в режиме записи: with open('hitcounts_new.json', 'w', encoding='utf-8') as out_ file: Затем, чтобы записать словарь с именем hits в файл в формате JSON, используйте имя переменной, указанное при открытии файла в предыдущей строке кода. Чтобы сохранить символы, не входящие в ASCII, и при необходимости расположить ключи в каждом словаре в алфавитном порядке, добавьте следующую строку с отступом внутрь блока with: json.dump(hits, out_file, ensure_ascii=False, sort_keys=True) На рисунке 2.9 показан полный код, который начинается с данных, экспортированных из Firebase, затем выполняет перебор импортированного словаря, изменение и удаление части его содержимого, а в завершение записывает обновленный словарь в JSON-файл под именем hitcounts_new.json. Рисунок 2.9. Запись модифицированных данных Firebase в новый JSON-файл с именем hitcounts_ new.json 388 РАЗДЕЛ 3 Работа с библиотеками
На рисунке 2.10 показано содержимое файла hitcounts_new.json после выполнения программы. Данные JSON не были специально отформатированы с отступами, так как такие файлы предназначены в первую очередь для хранения и обмена данными, а не для чтения человеком. Тем не менее можно заметить, что значения поля lastvisit теперь представлены в формате мм/дд/гггг, а пара ключ:значение lastreferrer отсутствует, поскольку была удалена предыдущим фрагментом кода. JSON является одним из самых распространенных форматов для хранения и обмена данными. К счастью, Python предоставляет множество встроенных инструментов для работы с JSON — как для чтения, так и для создания таких структур. В этом разделе были рассмотрены основные возможности, но при желании вы можете узнать больше, изучив документацию или поискать дополнительные примеры по запросу «python json». Рисунок 2.10. Запись модифицированных данных Firebase в новый JSONфайл с именем hitcounts_ new.json
В этой главе вы узнаете: » Как работает веб » Как открывать веб-страницы с помощью Python » Как отправлять данные на веб-сайты с использованием Python » Как выполнять веб-скрейпинг с помощью Python Гл а в а 3 Взаимодействие с интернетом К ак вы, вероятно, знаете, интернет является хранилищем практически всех знаний человечества. Большинство людей ежедневно пользуются сетью для поиска информации. Обычно это делается через веб-браузеры, такие как Safari, Google Chrome, Firefox, Opera, Internet Explorer или Edge. Чтобы открыть веб-сайт, достаточно ввести в адресную строку браузера унифицированный указатель ресурса (URL) и нажать Enter либо перейти по ссылке, которая перенаправит вас на нужную страницу. Однако получать доступ к веб-контенту можно не только через браузер, но и программным способом. Это значит, что вы можете использовать язык программирования, например Python, чтобы не только публиковать информацию в интернете, но и извлекать нужные данные из веб-источников. Таким образом, интернет можно рассматривать как огромную базу знаний, из которой ваши программы могут получать сведения по запросу. В этой главе рассматриваются два основных модуля, предназначенных для программного доступа к веб-ресурсам с помощью Python: urllib и BeautifulSoup. 390 РАЗДЕЛ 3 Работа с библиотеками
Как работает интернет Когда вы открываете веб-браузер и вводите URL-адрес или переходите по ссылке, ваш компьютер отправляет запрос в интернет. Этот запрос перенаправляется к соответствующему веб-серверу, который затем возвращает ответ. Чаще всего ответом является веб-страница, однако сервер может отправить практически любой тип файла — изображение, документ, видео или сообщение об ошибке, если запрашиваемый ресурс недоступен. Главное, что стоит понимать: вы, как пользователь, и программа, с помощью которой вы обращаетесь к интернету (браузер или другое приложение), находитесь на стороне клиента. Сервер же — это просто компьютер, обрабатывающий запросы и отправляющий ответы. Взаимодействие клиента и сервера показано на рисунке 3.1. δòðυôö ùυòχëυñ Рисунок 3.1. Клиент отправляет запрос, а сервер отправляет ответ ρυψëυψ óτúðôτ ïτõψχω χöëυö Разбираемся в загадочном URL URL — это ключевой элемент доступа к веб-страницам, ведь именно по нему интернет определяет, какой ресурс вы хотите получить. Большинство ресурсов в Сети используют протокол передачи гипертекста (HTTP), поэтому их адреса начинаются с http:// или https://. Разница между ними заключается в уровне безопасности. Протокол http передает данные в открытом виде, что делает их уязвимыми для перехвата злоумышленниками. В то же время https обеспечивает шифрование данных, превращая их в зашифрованный код, который невозможно просто так прочитать. Обычно любые сайты, на которых вы вводите личные данные — например, пароли или номера банковских карт, — используют именно https, чтобы защитить вашу информацию. ГЛАВА 3 Взаимодействие с интернетом 391
URL-адрес может быть простым, например https://AlanSimpson.me, или более сложным, если в нем нужно передать дополнительную информацию для запроса. На рисунке 3.2 показаны основные части URL-адреса и то, как они устроены. οψχöχñχò Ùχóυôôχυðóā σχωöðòðõχííχóυô οϊöþñɹõτõñυððòðωöψτôðøυ ρöψχñτïτõψχωτ Рисунок 3.2. Различные части URL-адресов ðóā!ïôτùυôðυ ðóā!ïôτùυôðυ Обратите внимание, что порядок элементов в URL имеет значение. Например, адрес может включать путь к определенной папке или странице, который начинается со слеша сразу после доменного имени. Кроме того, URL может содержать поисковую строку, которая всегда располагается в конце адреса и начинается со знака вопроса (?). После знака вопроса следуют одна или несколько пар имя=значение — по сути, это тот же синтаксис, что используется в словарях Python или в данных JSON. Если таких пар несколько, они разделяются символом амперсанда (&). ПРИМЕЧАНИЕ 392 Символ решетки (#), за которым в конце URL следует имя, называется фрагментом. Он указывает браузеру на конкретное место внутри веб-страницы, к которому нужно перейти после ее загрузки. В коде страницы для этого используется тег вида <a id="name"></a>, который служит меткой. Когда браузер открывает страницу, он автоматически прокручивает ее до элемента с таким идентификатором. РАЗДЕЛ 3 Работа с библиотеками
Исследование HTTP-заголовков Когда вы пользуетесь интернетом, вы обычно видите только содержимое веб-страницы. Однако на более глубоком уровне между вашим компьютером (клиентом) и веб-сервером происходит обмен специальными сообщениями — HTTP-заголовками. Эти заголовки невидимы для пользователя, но они играют важную роль в передаче данных: в них содержится информация о запросе, браузере, типе контента, кодировке, статусе ответа и многом другом. Хотя заголовки обычно скрыты, при необходимости их можно просмотреть. Это бывает полезно при написании кода, связанного с сетевыми запросами или взаимодействием с веб-ресурсами. Один из удобных инструментов для изучения заголовков — расширение HTTP Headers для браузера Google Chrome. Если у вас установлен Chrome, вы можете загрузить и установить это расширение из Chrome Web Store по адресу: chromewebstore.google.com. После установки следуйте инструкциям на экране. Чтобы просмотреть заголовки при посещении сайта, нажмите на значок HTTP Headers на панели инструментов Chrome (он выглядит как облачко). После этого откроется окно, показывающее все HTTP-заголовки, участвующие в передаче данных между вашим браузером и сервером ( рисунок 3.3). Рисунок 3.3. Просмотр HTTP-заголовков в Google Chrome Два самых важных элемента в HTTP-заголовках находятся в верхней части — это строка, начинающаяся с GET, за которой следует URL. Она указывает, что был отправлен GET-запрос, то есть запрос информации ГЛАВА 3 Взаимодействие с интернетом 393
с сервера. В этом случае никакие данные на сервер не отправляются — клиент просто запрашивает ресурс, указанный в URL. Существует и другой тип запроса — POST. Он используется, когда вы отправляете данные на сервер, например при публикации сообщения в социальной сети или при заполнении формы на сайте. Строка под GET показывает статус ответа сервера. Первая часть указывает используемый протокол — например, HTTP/1.1, что означает, что обмен данными осуществляется по правилам версии 1.1 протокола HTTP. Следующее число — это код состояния. В примере на рисунке 3.4 код 200 обозначает, что запрос был выполнен успешно. Наиболее распространенные коды состояния HTTP приведены в таблице 3.1. Рисунок 3.4. HTTP-заголовки Таблица 3.1. Наиболее распространенные коды состояния HTTP 394 Код Значение Описание 200 ОК (Okay) Проблем не обнаружено . Запрос выполнен успешно 400 Неверный запрос (Bad Request) Сервер доступен, но не может обработать ваш запрос — чаще всего из-за ошибки в URL или неправильного формата данных 403 Доступ запрещен (Forbidden) Сайт обнаружил, что вы обращаетесь к нему программно (например, через скрипт или парсер), и запрещает такой доступ РАЗДЕЛ 3 Работа с библиотеками
Код Значение Описание 404 Не найдено (Not Found) Либо URL указан неверно, либо страница (контент) была удалена 406 Неприемлемо (Not Acceptable) Запрос не может быть выполнен из-за несовместимости форматов данных . Часто решается с помощью добавления заголовков, например через urllib. request.add_header() Все, о чем говорилось выше, имеет большое значение, поскольку напрямую связано с программным доступом к веб-ресурсам с помощью Python. В следующих разделах вы увидите, как эти принципы используются на практике. Открытие URL из Python Для того чтобы программа на Python могла получать доступ к веб-ресурсам, используется пакет urllib — библиотека, предназначенная для работы с URL-адресами и сетевыми запросами. Этот пакет включает несколько модулей, каждый из которых отвечает за определенные функции, связанные с программным взаимодействием с интернетом. В таблице 3.2 приведено краткое описание модулей пакета urllib и их назначения. Таблица 3.2. Пакеты библиотеки urllib в Python Модуль Назначение request Открывает URL-адреса . Используется для отправки HTTPзапросов (GET, POST и т .д .) . response Обрабатывает ответ, полученный от сервера . Обычно с ним напрямую работать не нужно — он используется внутренне при вызове urlopen() . error Обрабатывает исключения, возникающие при выполнении запросов (например, HTTPError и URLError) . parse Разбивает URL на части (схему, домен, путь, параметры и т .д .) и может собирать их обратно . robotparser Анализирует файл robots.txt сайта, который задает разрешения для ботов, обращающихся к сайту программно . ГЛАВА 3 Взаимодействие с интернетом 395
В большинстве случаев вы будете использовать модуль request, так как именно он позволяет открывать и получать данные из интернета. Синтаксис для обращения к отдельному модулю внутри библиотеки выглядит следующим образом: from library import module Здесь library — это имя основной библиотеки, а module — имя модуля, который вы хотите использовать. Чтобы получить доступ к функциям модуля request из библиотеки urllib, добавьте в начало вашего кода следующую строку (комментарий указывать необязательно): # импортируем модуль request из библиотеки urllib from urllib import request Для открытия веб-страницы используется следующий синтаксис: variablename = request.urlopen(url) • • • Здесь variablename вы можете заменить на любое удобное имя переменной, в которой будет храниться результат. Вместо url укажите адрес ресурса, который нужно открыть, заключив его в одинарные или двойные кавычки, если это строка . Если URL уже сохранен в переменной, просто используйте имя этой переменной без кавычек . При выполнении кода результатом будет объект HTTPResponse. Например, вы можете выполнить следующий код в Jupyter Notebook или в любом файле с расширением .py, чтобы получить доступ к тестовой HTML-странице, которую Алан разместил на своем сайте специально для этого примера: # Импортируем модуль request из библиотеки urllib from urllib import request # URL (адрес) нужной страницы sample_url = 'https://freeeasy.ai/sample.html' # Запрашиваем страницу и помещаем ее в переменную the_page the_page = request.urlopen(sample_url) # Помещаем код ответа в переменную status status = the_page.code # Какой тип данных у страницы? print(type(the_page)) # Какой код состояния? print(status) 396 РАЗДЕЛ 3 Работа с библиотеками
При выполнении этого кода отображается следующий результат: <class 'http.client.HTTPResponse'> 200 Переменная the_page содержит объект типа http.client.HTTPResponse, в котором хранится все, что сервер отправил в ответ на запрос. Код состояния 200 указывает на то, что запрос был успешно выполнен и сервер вернул корректный ответ. Отправка веб-запросов с помощью Python Не все запросы к веб-ресурсам выполняются так просто, как в предыдущем примере. Например, если вы введите в адресную строку браузера следующий URL и нажмете Enter: https://www.google.com/search?q=python web scraping tutorial Google вернет страницу с результатами поиска, содержащими слова «python web scraping tutorial». Однако если вы посмотрите на адресную строку после загрузки страницы, заметите, что адрес немного изменился. Все пробелы были заменены на последовательность %20, и URL теперь выглядит так: https://www.google.com/search?q=python%20web%20scraping%20 tutorial Код %20 представляет собой шестнадцатеричное значение символа пробела в кодировке ASCII. Это связано с тем, что протокол HTTP не допускает использование обычных пробелов в URL-адресах. При необходимости после передачи такой код снова преобразуется в пробел. Теперь посмотрим, что произойдет, если выполнить тот же самый код, но указать в нем URL Google вместо исходного адреса. Пример кода выглядит так: from urllib import request # URL (адрес) нужной страницы sample_url = \ 'https://www.google.com/search?q=python%20 web%20scraping%20tutorial' # Запрашиваем страницу и помещаем ее в переменную the_page the_page = request.urlopen(sample_url) # Помещаем код ответа в переменную status status = the_page.code # Какой тип данных у страницы? ГЛАВА 3 Взаимодействие с интернетом 397
print(type(the_page)) # Какой код состояния? print(status) При запуске этого кода все проходит не так гладко. Вы можете увидеть несколько сообщений об ошибках, но ключевое из них такое: HTTPError: HTTP Error 403: Forbidden Проблема не в вашем коде, а в самом HTTP-ответе. Код 403 означает «Доступ запрещен». Запрос действительно был отправлен на сервер Google, но в ответ пришло сообщение: «Искать у нас можно через браузер, а не программно из Python». Google, как и многие другие крупные сайты, блокирует попытки программного доступа к своему контенту — частично для защиты прав на материалы, частично для контроля входящего трафика. Хорошая новость в том, что сайты, которые не позволяют напрямую отправлять запросы через Python или другой язык программирования, часто предоставляют возможность публиковать и получать данные через свой программный интерфейс приложения (API). Это значит, что вы все равно можете использовать Python, просто нужно работать в рамках установленных правил. Самый простой способ узнать, есть ли у сайта API, — поискать информацию в интернете или задать вопрос системе искусственного интеллекта. Например, можно ввести запрос «использование API с ChatGPT» и посмотреть, какие возможности доступны. Мы не приводим здесь конкретных примеров, потому что крупные платформы часто обновляют свои правила и протоколы, и любая инструкция может устареть. Поэтому лучше выполнять поиск самостоятельно и ориентироваться на самые свежие результаты. Веб-скрейпинг с помощью Python Когда вы запрашиваете страницу из интернета, она приходит на ваш компьютер в виде веб-документа, который обычно состоит из HTML-кода и содержимого. HTML — это язык разметки, который вместе с другим языком, CSS, сообщает браузеру, как отображать элементы страницы: какого они размера, где расположены, какие шрифты и цвета используются, какие изображения нужно вставить и т.д. При обычном просмотре сайта вы не видите HTML- или CSS-код — браузер показывает вам только готовый результат, то есть сам контент, который находится внутри HTML-тегов. 398 РАЗДЕЛ 3 Работа с библиотеками
Хотя большая часть информации, представленной на веб-страницах, поступает из баз данных, напрямую получить к ней доступ нельзя. Зато существует техника, называемая веб-скрейпингом, которая позволяет извлекать данные непосредственно со страниц сайта. Python предоставляет мощные инструменты для веб-скрейпинга, и эта тема особенно популярна среди начинающих программистов. В дальнейшем мы рассмотрим основы веб-скрейпинга на примере страницы, изображенной на рисунке 3.5. Код, который определяет, как браузер должен отображать содержимое страницы, скрыт от пользователя, если специально не открыть его. В большинстве браузеров это можно сделать, нажав клавишу F12 или выбрав пункт «Просмотр исходного кода» либо «Исследовать», щелкнув правой кнопкой мыши по странице. Как правило, основное содержимое веб-страницы — то, что вы видите на экране, — располагается между тегами <body> … </body>. Внутри этого блока обычно находятся разделы для заголовка, панели навигации, нижнего колонтитула, рекламы и других элементов. В рассматриваемом примере основная информация страницы размещена внутри тегов <article> … </article>. Каждый прямоугольный блок, который вы видите на веб-странице, изображенной на рисунке 3.5, называется карточкой. Каждая карточка представлена как ссылка, заключенная в теги <a> … </a>. На рисунке 3.6 показана часть HTML-кода страницы с рисунка 3.5. Здесь приведен код только для первых двух ссылок, однако все остальные имеют такую же структуру и находятся внутри секции, ограниченной тегами <article> … </article>. Рисунок 3.5. Пример страницы для веб-скрейпинга ГЛАВА 3 Взаимодействие с интернетом 399
Рисунок 3.6. Часть кода с примера страницы для веб-скрейпинга Обратите внимание, что каждая ссылка состоит из нескольких HTML-тегов, выполняющих разные функции: » <a> … </a> — теги привязки (anchor) указывают, куда браузер должен перенаправить пользователя при щелчке на ссылку . Атрибут href= внутри тега <a> содержит URL-адрес страницы, на которую осуществляется переход . » <img> — тег изображения . Он определяет картинку, отображаемую внутри ссылки . Атрибут src= указывает источник изображения, то есть путь и имя файла, который нужно загрузить и показать . » ПРИМЕЧАНИЕ <span> … </span> — тег, внутри которого размещается текст . В данном случае этот текст отображается в нижней части каждой ссылки белым цветом на черном фоне . Термин «скрейпинг экрана » иногда используют как синоним веб-скрейпинга, хотя на самом деле данные извлекаются не с экрана компьютера. Контент берется из файла, который веб-сервер отправляет вашему браузеру. Браузер затем использует этот файл, чтобы отобразить информацию на экране. В Python-коде для выполнения веб-скрейпинга вам понадобятся три модуля. Первый из них — модуль request из библиотеки urllib. Он позволяет отправлять запросы в интернет, получать доступ к веб-страницам и считывать ответы, которые возвращает сервер. Этот модуль входит в стандартную библиотеку Python, поэтому устанавливать его отдельно не нужно. 400 РАЗДЕЛ 3 Работа с библиотеками
Для анализа полученной веб-страницы используется модуль BeautifulSoup (его название взято из песни из книги «Алиса в Стране чудес»). Этот инструмент предназначен для парсинга HTML-кода и поиска нужных элементов данных внутри страницы. Поскольку BeautifulSoup не входит в стандартную поставку Python, его нужно установить вручную с помощью менеджера пакетов pip. В VS Code откройте панель терминала и, если вы работаете в Windows, введите следующую команду: pip install beautifulsoup4 Если при выполнении этой команды возникает ошибка и вы используете macOS, попробуйте вместо этого: pip3 install beautifulsoup4 После этого вы должны увидеть сообщение, подтверждающее успешную установку BeautifulSoup. Если модуль уже был установлен ранее, появится уведомление о том, что требование уже выполнено — в этом случае ничего делать не нужно. Кроме того, для работы BeautifulSoup потребуется HTML5 .парсер. Можно использовать модуль html5lib, который также устанавливается через pip. Если вы работаете в Windows, выполните следующую команду в терминале VS Code: pip install html5lib Если вы используете Mac и получаете сообщение об ошибке, введите: pip3 install html5lib Более подробно о pip и установке модулей Python мы поговорим в разделе 4 нашей книги. Пока же этих шагов достаточно, чтобы приступить к примеру веб-скрейпинга. Чтобы опробовать код, откройте блокнот Jupyter или создайте файл с расширением .py в VS Code и введите следующий код: # Получаем модуль request из библиотеки url from urllib import request # Этот модуль содержит инструменты для парсинга веб-страниц from bs4 import BeautifulSoup Теперь нужно указать Python, где находится страница, с которой вы хотите работать. В данном примере используйте следующий URL: https://freeeasy.ai/scrape_sample.html ГЛАВА 3 Взаимодействие с интернетом 401
Чтобы просмотреть страницу, просто введите этот URL в адресную строку браузера — она откроется как обычная веб-страница с изображениями. Это происходит потому, что браузер отображает уже обработанный результат, а не исходный HTML- и CSS-код. Для парсинга страницы в Python нужно указать этот URL внутри программы. Можно присвоить ему короткое и понятное имя, например page_url: # Пример страницы для практики page_url = 'https://freeeasy.ai/scrape_sample.html' Затем загрузите содержимое веб-страницы по этому адресу в ваше приложение Python. Для этого создайте переменную, например raw_page, и воспользуйтесь методом urlopen из модуля request: # Открываем страницу raw_page = request.urlopen(page_url) Чтобы упростить парсинг страницы в дальнейшем, создайте объект BeautifulSoup, в который будет загружено содержимое веб-страницы. В коде этот объект можно назвать, например, soup. Также необходимо указать, какой парсер использовать — в данном случае это html5lib. Пример кода выглядит так: # Создаем объект BeautifulSoup из html-страницы soup = BeautifulSoup(raw_page, 'html5lib') СОВЕТ Генеративный искусственный интеллект может значительно упростить процесс написания кода для веб-скрейпинга. Вам не нужно запоминать весь синтаксис — достаточно сформулировать задачу. Например, можно попросить Copilot или Claude .ai написать код на Python для парсинга веб-сайта с помощью BeautifulSoup. В результате вы получите базовый шаблон кода, который уже умеет загружать страницу и анализировать ее структуру. После этого останется лишь указать нужный URL и немного адаптировать код под вашу задачу, чтобы извлекать именно те данные, которые вам необходимы. Парсинг части страницы Большинство веб-страниц содержат множество элементов — заголовки, подвал, меню, рекламу и другие секции, которые не всегда нужны при извлечении данных. Основная информация обычно находится только 402 РАЗДЕЛ 3 Работа с библиотеками
в одном определенном разделе страницы. Если заранее выделить этот раздел, парсинг станет быстрее и точнее. В нашем примере вся основная часть страницы заключена между тегами <article> … </article>. Поэтому мы можем извлечь именно этот блок и сохранить его в переменную content, чтобы работать только с ним: # Выделяем основной блок контента content = soup.article Сохранение обработанного контента При веб-скрейпинге основная цель заключается в том, чтобы извлечь только нужные данные, а не весь код страницы. В нашем случае интерес представляют три элемента: URL, источник изображения и текст, связанный с каждой ссылкой. Поскольку таких ссылок несколько, удобнее всего хранить их данные в виде списка. Для этого сначала создадим пустой список под названием links_list: # Создаем пустой список для хранения словаря для каждого элемента links_list = [] Затем нужно пройти по каждой ссылке на странице. Ссылки начинаются с тега <a> и заканчиваются </a>. Чтобы перебрать их все, используем метод find_all из библиотеки BeautifulSoup внутри цикла for. При каждой итерации текущая ссылка будет сохраняться в переменной link: # Проходим по всем ссылкам в статье for link in content.find_all('a'): Каждая ссылка на странице имеет примерно такую структуру, хотя у каждой будут свои уникальные значения атрибутов — собственный URL, путь к изображению и текст: <a href="https://alansimpson.me/datascience/python/lists/"> <img src="images/lists256.jpg" alt="Python lists"> <span>Lists</span> </a> ГЛАВА 3 Взаимодействие с интернетом 403
Нам нужно извлечь три элемента данных: » URL ссылки — он находится в кавычках после href= внутри тега <a>. » Источник изображения — значение в кавычках после src= внутри тега <img>. » Текст ссылки — содержимое между тегами <span> … </span>. Для извлечения этих данных можно использовать метод .get() объекта BeautifulSoup, который позволяет получать значения атрибутов тегов. Сначала получим сам URL из тега <a> и сохраним его в переменной url: url = link.get('href') Чтобы код выполнялся для каждой ссылки на странице, нужно добавить отступ и разместить его внутри цикла for. В результате он будет выглядеть так: img = link.img.get('src') Текст, который находится между тегами <span> … </span>, можно извлечь внутри цикла, добавив еще одну строку с отступом. Код будет выглядеть так: text = link.span.text Вам не нужно использовать метод .get() для извлечения текста, так как он не является атрибутом HTML-тега вроде href= или src=. Это просто содержимое, находящееся между тегами <span> … </span>. После того как вы получили все три элемента — URL, источник изображения и текст, — их нужно сохранить, прежде чем цикл перейдет к следующей ссылке. Самый простой способ сделать это — добавить их в список links_list в виде словаря: links_list.append({'url': url, 'img': img, 'text': text}) Это все, что нужно для сохранения данных одной ссылки на каждом шаге цикла. Однако стоит помнить, что веб-страницы не всегда имеют идеально написанный HTML-код. Браузеры обычно «терпимо» относятся к ошибкам и могут корректно отображать страницу даже при наличии опечаток, пропущенных тегов или неверных атрибутов. Но при веб-скрейпинге такие ошибки могут вызвать сбой программы — чаще всего это проявляется как ошибка атрибута, когда Python не может найти нужное свойство элемента. 404 РАЗДЕЛ 3 Работа с библиотеками
Чтобы избежать остановки программы из-за подобных ошибок, полезно обернуть весь процесс извлечения данных в блок try:. Если какая-то ссылка окажется некорректной, Python просто пропустит ее и перейдет к следующей: # Пытаемся получить href, URL изображения и текст try: url = link.get('href') img = link.img.get('src') text = link.span.text links_list.append({'url': url, 'img': img, 'text': text}) # Если в строке что-то отсутствует... except AttributeError: #... пропускаем ее, не вызывая сбой pass На рисунке 3.7 показан весь код в его текущем виде. Если запустить его сейчас, список links_list будет успешно заполнен всеми извлеченными данными со страницы. Однако просто хранить их в памяти мало полезно, обычно возникает необходимость сохранить эти данные для дальнейшего использования. Рисунок 3.7. Полный код веб-скрейпинга ГЛАВА 3 Взаимодействие с интернетом 405
Сохранение собранных данных в JSON-файл Чтобы сохранить собранные данные в JSON-файл, сначала нужно импортировать модуль json в начале программы: # Если вы хотите сохранить данные в json-файл import json После того как цикл обработки ссылок завершится (код должен стоять без отступа, чтобы не выполняться на каждой итерации), откройте новый файл для записи. Имя файла может быть любым — в этом примере используется links.json: # Сохранить как JSON-файл with open('links.json', 'w', encoding='utf-8') as links_file: Затем, с отступом внутри блока with, используйте метод json.dump() для записи содержимого списка links_list в файл. Параметр ensure_ ascii=False позволяет сохранить символы за пределами ASCII, например кириллицу: json.dump(links_list, links_file, ensure_ascii=False) Вот и все! После выполнения этого кода у вас появится файл links. json, в котором будут сохранены все собранные данные в формате JSON. Если открыть этот файл в VS Code, вы увидите, что весь его содержимый текст записан в одну длинную строку. Это нормально — просто мы не добавляли параметры форматирования, такие как переносы строк или отступы. Чтобы сделать файл более удобным для чтения, можно скопировать его содержимое и вставить на сайт, например jsonformatter.org. Этот инструмент автоматически выведет JSON-данные в аккуратном и структурированном виде, не меняя при этом самих данных. На рисунке 3.8 показано, как это выглядит после форматирования. 406 РАЗДЕЛ 3 Работа с библиотеками
Рисунок 3.8. Данные, собранные с веб-страницы, в JSONфайле Сохранение собранных данных в CSV-файл Если вы хотите сохранить собранные данные сразу в CSV-файл, начните с импорта модуля csv в начале программы: # Для сохранения в CSV import csv После завершения цикла, в котором формируется список links_list, откройте новый файл для записи. В этом примере файл называется links.csv. Параметр newline='' предотвращает добавление пустых строк между записями: # Сохраняем в CSV with open('links.csv', 'w', newline='') as csv_out: Далее, с отступом внутри блока with, создайте объект для записи данных в CSV. Этот объект будет связан с файлом, который вы только что открыли: csv_writer = csv.writer(csv_out) Обычно первая строка в CSV-файле содержит заголовки столбцов, которые помогают понять, какие данные хранятся в каждой колонке. Добавьте такую строку, выбрав понятные имена для заголовков: ГЛАВА 3 Взаимодействие с интернетом 407
# Создаем строку заголовков csv_writer.writerow(['url', 'img', 'text']) Теперь, когда структура файла и заголовки готовы, можно записать все собранные данные из списка links_list в CSV-файл. Для этого используйте цикл, который перебирает каждый элемент списка и записывает значения в новые строки: for row in links_list: csv_writer.writerow([str(row['url']), str(row['img']), str(row['text'])]) После выполнения этого кода будет создан файл links.csv.Если открыть его в Excel или любом другом приложении для работы с таблицами, вы увидите аккуратную таблицу, где данные распределены по трем столбцам — url, img и text, как показано на рисунке 3.9. На рисунке 3.10 представлен весь код, который собирает данные и сохраняет их одновременно в форматах JSON и CSV. Просмотр полной версии программы поможет вам сверить порядок выполнения команд и устранить возможные ошибки. В примере некоторые комментарии в начале кода были убраны, чтобы весь код поместился в одном скриншоте. Рисунок 3.9. Данные, полученные с помощью веб-скрейпинга, в Excel 408 РАЗДЕЛ 3 Работа с библиотеками
Рисунок 3.10. Полный код программы scraper.py для извлечения данных в CSV-файл Программное взаимодействие с интернетом открывает широкие возможности для поиска, анализа и систематизации информации. Этот подход лежит в основе такого направления, как data science или наука о данных, где получение и обработка данных из различных источников играют ключевую роль. В дальнейшем вы познакомитесь со множеством специализированных инструментов, подробно рассмотренных в разделе 5. Прежде чем перейти к более сложным и специализированным способам применения Python в разделе 4 и далее, стоит обсудить еще одну важную концепцию. В первых трех главах этого раздела вы уже познакомились с различными библиотеками и модулями, но потенциал Python этим не исчерпывается. В следующей главе мы рассмотрим, какие еще библиотеки, пакеты и модули доступны в Python и как использовать их для решения самых разных задач.
В этой главе вы узнаете: » Что представляет собой стандартная библиотека Python » Какие существуют популярные пакеты Python » Как импортировать модули в Python » Как создавать собственные модули Python Гл а в а 4 Библиотеки, пакеты и модули Все предыдущие главы в основном были посвящены основам языка Python — тем элементам, которые необходимы каждому, кто работает с ним, независимо от области применения. Однако, как вы уже могли заметить, практически любая программа на Python начинается с импорта одного или нескольких модулей. Модуль — это, по сути, набор заранее написанного кода, который можно подключить к своей программе, чтобы не писать все с нуля. Такой подход позволяет использовать уже готовые решения для самых разных задач. Главным источником этих модулей является стандартная библиотека Python (Python Standard Library) — огромная коллекция встроенных инструментов, которые расширяют возможности языка и делают разработку быстрее и удобнее. Понимание стандартной библиотеки Python Стандартная библиотека Python — это все, что входит в состав самого языка программирования. Она включает не только инструменты и функции, но и все базовые типы данных, такие как строки, целые числа, числа с плавающей точкой и логические значения. 410 РАЗДЕЛ 3 Работа с библиотеками
Каждый объект этих типов данных является экземпляром определенного класса, который входит в стандартную библиотеку. Именно поэтому термины тип, экземпляр и объект часто употребляются как синонимы. Например, целое число в Python — это не просто число, а объект определенного типа данных. Этот тип существует потому, что в стандартной библиотеке есть класс, описывающий целые числа. Когда вы создаете любое число, Python создает экземпляр этого класса — объект, основанный на этом шаблоне. Таким образом, каждый тип данных — это не просто категория значений, а полноценная структура, построенная на объектно ориентированных принципах. Функция type() в Python используется для определения типа данных объекта. Например, вы можете выполнить следующие строки кода в интерактивной оболочке Python, в Jupyter Notebook или в обычном .py файле: x = 3 print(type(x)) Результат будет таким: <class 'int'> Это означает, что переменная x является целым числом и экземпляром класса int, который входит в стандартную библиотеку Python. Если теперь присвоить переменной строковое значение и снова проверить ее тип: x = 'howdy' print(type(x)) то вывод будет таким: <class 'str'> Иными словами, переменная x содержит данные строкового типа, созданные на основе класса str в Python. Функция type() работает аналогичным образом и с другими типами данных. Ее можно использовать для проверки чисел с плавающей точкой, то есть чисел, содержащих десятичную точку, например 3.14, а также логических значений True и False. ГЛАВА 4 Библиотеки, пакеты и модули 411
Использование функции dir() Стандартная библиотека Python содержит встроенный метод dir(), который позволяет просмотреть все атрибуты и методы, доступные для определенного типа данных или объекта. В предыдущем примере вывод <class 'str'> показал, что переменная содержит данные строкового типа, то есть экземпляр класса str (от слова string). Если теперь ввести следующую команду: dir(str) то Python выведет список всех атрибутов и методов, определенных для строкового типа. Примерный результат будет таким: ['__add__', '__class__', '__contains__', '__delattr__', '__ dir__', '__doc__', '__eq__', '__format__', '__ge__', '__ getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__ init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__ lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__ reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__ sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] 412 РАЗДЕЛ 3 Работа с библиотеками
Имена, окруженные двойными подчеркиваниями, например __add__ или __class__, называются методами с двойным подчеркиванием (dunder-named items), где слово dunder образовано от выражения double underscores — «двойное подчеркивание». Такие методы также известны как специальные или магические методы. Каждый метод с двойным подчеркиванием представляет встроенную функциональность Python, которую обычно не нужно вызывать напрямую. Вместо этого Python использует их автоматически, когда вы выполняете определенные операции. Например, метод __add__ срабатывает, когда вы применяете оператор + — будь то сложение чисел или объединение строк. Обычные функции и методы не содержат двойных подчеркиваний и при их вызове всегда используются круглые скобки. Рассмотрим пример: x = "Howdy" print(type(x), x.isalpha(), x.upper()) Результат выполнения будет следующим: <class 'str'> True HOWDY Первая часть вывода, <class 'str'>, показывает, что переменная x содержит строку. Это означает, что к ней можно применять все методы и атрибуты, перечисленные в результате выполнения команды dir(str). Значение True является результатом вызова x.isalpha(), так как строка действительно состоит только из буквенных символов. А HOWDY — это результат выполнения метода x.upper(), который преобразует все буквы в строке в заглавные. СОВЕТ Начинающие программисты нередко задаются вопросом, зачем нужен список имен вроде 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find' и 'format', который отображается при использовании функции dir(), если непонятно, что именно они делают и как их применять. И действительно, одного лишь списка имен недостаточно, чтобы понять их назначение. Чтобы узнать подробнее, для чего нужен тот или иной метод и как его использовать, можно воспользоваться функцией help(). Она предоставляет подробное описание каждого метода, включая его назначение, параметры и примеры использования. ГЛАВА 4 Библиотеки, пакеты и модули 413
Использование функции help() В командной строке Python доступна встроенная функция help(), которая используется для получения справки о любом объекте, модуле, классе или функции. Синтаксис этой команды выглядит так: help(object) Чтобы воспользоваться этой функцией, нужно заменить слово object на тип объекта, о котором вы хотите получить информацию. Например, чтобы получить справку по объектам типа str (строкам, создаваемым на основе класса str), введите в командной строке Python следующую команду: help(str) В результате выполнения команды вы получите подробную справку по объекту, указанному в скобках. Например, если dir(str) просто перечисляет имена всех атрибутов и методов строкового типа, то help(str) предоставляет расширенную информацию о каждом из них. Так, команда dir(str) покажет, что в классе str существует метод capitalize, а при вводе help(str.capitalize) Python выведет описание этого метода: capitalize(self, /) Возвращает версию строки с первой заглавной буквой. Если говорить точнее, эта функция делает первый символ заглавным, а остальные - строчными. Слово self означает, что метод capitalize применяется к самому объекту, для которого он вызывается. Другими словами, когда вы пишете x.capitalize(), именно значение переменной x и будет преобразовано. Косая черта /, указанная в описании метода, показывает, что все параметры, расположенные перед ней, являются позиционными аргументами. Это значит, что при вызове метода вы не можете передавать имена этих аргументов явно — их нужно указывать только по порядку. СОВЕТ 414 Когда вы закончите просмотр справочной информации, не нужно пролистывать ее до конца, чтобы вернуться в командную строку. Достаточно нажать сочетание клавиш Ctrl+C, и вы сразу выйдете из режима справки обратно к приглашению Python. РАЗДЕЛ 3 Работа с библиотеками
Когда вам нужна дополнительная информация или примеры, проще всего обратиться к интернету. Вы можете воспользоваться любым поисковиком — например, Google или Bing — и ввести слово python вместе с интересующей вас темой или функцией. Например, по запросу python capitalize вы получите множество ссылок на ресурсы, где подробно объясняется, как работает метод capitalize у строковых объектов, с примерами его использования. Если вы используете искусственный интеллект, такой как Copilot, Bard, ChatGPT или любой другой, вы также можете попросить объяснение и примеры. Например, можно задать запрос: расскажи мне о Python str.capitalize() или что-то подобное — и вы получите понятное описание вместе с демонстрацией работы метода. Хорошим, хотя и довольно техническим источником информации о стандартной библиотеке Python является ее официальная документация. Она всегда доступна по адресу: https://docs.python.org/, обычно под ссылкой Library Reference. Однако название раздела может со временем измениться, поэтому, если вы не можете его найти, просто выполните поиск в интернете по запросу python standard library. Следует помнить, что стандартная библиотека Python очень обширна и довольно сложна, поэтому не стоит ожидать, что вы сразу разберетесь во всем. Лучше использовать документацию как постоянный справочник и возвращаться к ней, когда хотите глубже понять ту или иную тему по мере освоения языка. ПРИМЕЧАНИЕ Документация, размещенная на сайте https://docs.python.org/, обычно относится к текущей стабильной версии Python. Если вам нужно обратиться к материалам по более старым версиям или посмотреть информацию о новых версиях, которые могут находиться в разработке, соответствующие ссылки можно найти в меню в левой части страницы. Изучение встроенных функций Функции dir() и help() — это примеры встроенных функций Python, которые всегда доступны как при создании программ, так и при работе в командной строке. Эти функции, как и многие другие, входят в стандартную библиотеку Python. ГЛАВА 4 Библиотеки, пакеты и модули 415
Если выполнить поиск в интернете по запросу «встроенные функции Python», вы найдете ссылки на официальную документацию, где приведен полный список таких функций. Перейдя по одной из ссылок, вы откроете раздел стандартной библиотеки с таблицей всех встроенных функций, как показано на рисунке 4.1. На этой странице можно нажать на название любой функции, чтобы получить подробное описание и примеры ее использования. Рисунок 4.1. Встроенные функции Python Изучение пакетов Python Python поддерживает модульный подход к программированию, при котором программа разбивается на небольшие, понятные и легко управляемые части — модули. Многие из таких модулей уже созданы другими разработчиками и доступны для повторного использования. Любой крупный проект, будь то индивидуальная работа или командная разработка, можно значительно упростить, если использовать уже готовый, протестированный и надежный код, созданный сообществом Python-разработчиков. Именно таким кодом и являются пакеты Python — тщательно проработанные и универсальные решения, которые можно применять как строительные блоки при создании собственных программ. Для Python существует множество пакетов, и отличным местом для их поиска является сайт PyPi — Python Package Index, доступный по адресу: 416 РАЗДЕЛ 3 Работа с библиотеками
https://pypi.org/. Здесь можно найти и установить практически любой пакет, необходимый для вашей работы. Для установки и управления пакетами используется инструмент pip (сокращение от Pip Installs Packages — «Pip устанавливает пакеты»). С его помощью можно устанавливать, обновлять и удалять пакеты прямо из командной строки операционной системы: Terminal на macOS или cmd.exe и PowerShell в Windows. Если вы работаете в VS Code, проще всего открыть встроенный терминал, выбрав пункт меню View  Terminal. Если pip уже установлен, вы можете проверить его версию, введя в командной строке следующую команду: pip --version Эта команда покажет текущую установленную версию pip. Если вы используете macOS и видите сообщение об ошибке «command not found» (команда не найдена), попробуйте использовать pip3 вместо pip во всех командах. Например: pip3 --version Результат выполнения команды будет выглядеть примерно так (у вас могут быть другие номера версий и путь к Python вместо многоточия): pip 23.1.2 from ... Чтобы посмотреть, какие пакеты уже установлены в вашей системе, введите в командной строке или в панели Terminal VS Code следующую команду: pip list Если вы используете macOS и при вводе этой команды появляется ошибка, замените pip на pip3: pip3 list ГЛАВА 4 Библиотеки, пакеты и модули 417
Импорт модулей Python СОВЕТ Слово «модуль» вы будете часто встречать в контексте Python. Чтобы понять разницу между библиотекой, пакетом и модулем, можно представить аналогию с обычной библиотекой. Стандартная библиотека Python — это как большая библиотека книг. Каждая книга в ней — это пакет, а отдельная глава в книге — это модуль. Таким образом, пакет может включать в себя несколько модулей, а сама библиотека состоит из множества пакетов. Модуль — это основная единица организации кода в Python, благодаря чему язык и называется модульным. Код группируется по функциональности, и вам не нужно импортировать все подряд, чтобы использовать лишь одну конкретную возможность. Если же вам нужны несколько связанных функций, например, для работы с датами и временем, нет необходимости импортировать каждую функцию по отдельности — достаточно импортировать соответствующий модуль, и у вас будет доступ ко всем нужным инструментам сразу. Импортировать функциональность из модулей можно разными способами. Один из самых распространенных — импортировать весь модуль целиком. Это удобно, когда вы хотите использовать несколько его функций и не хотите перечислять каждую из них по отдельности. Для этого достаточно после команды import указать название нужного модуля. Например, следующая строка импортирует весь модуль math, который содержит множество математических функций: import math После импорта модуля вы можете использовать функции dir() и help() для изучения его содержимого. Например, если попытаться выполнить dir(math) или help(math) до того, как модуль math был импортирован, Python выдаст ошибку, потому что он еще не знает, что это за модуль. Однако если сначала выполнить import math, а затем вызвать help(math), вы получите список функций и подробное описание их работы. Иногда нет необходимости загружать весь модуль целиком, особенно если вам нужна только одна или две функции. В этом случае можно импортировать лишь конкретные элементы, используя следующий синтаксис: from math import pi 418 РАЗДЕЛ 3 Работа с библиотеками
В этом примере вы импортируете только один элемент — pi, поэтому не загружаете весь модуль math. Такой способ делает код более легким и понятным. Кроме того, после такого импорта вы можете обращаться к числу pi напрямую, просто написав pi, без необходимости использовать полное имя math.pi. Чтобы проверить это на практике, откройте командную строку Python (например, терминал в Visual Studio Code) и введите команду print(pi), затем нажмите Enter. Скорее всего, появится сообщение об ошибке: NameError: name 'pi' is not defined Это означает, что имя pi неизвестно Python по умолчанию и его нужно импортировать из соответствующего модуля, прежде чем использовать. Сделать это можно двумя способами. Первый способ — импортировать весь модуль, выполнив в командной строке Python следующую команду: import math Однако если после этого вы попробуете ввести команду print(pi) вы снова получите ту же ошибку. Это происходит потому, что при таком импорте число pi все еще находится внутри модуля math, и для обращения к нему нужно указывать полное имя — math.pi. Когда вы импортируете весь модуль, но хотите использовать только отдельный элемент из него, необходимо перед именем элемента указывать имя модуля, за которым следует точка. Например, если ввести следующую команду: print(math.pi) вы получите правильный ответ: 3.141592653589793 Помните, что если вы импортируете только часть модуля, функции help() и dir() для всего модуля работать не будут. Например, если в вашем коде есть только строка from math import pi, а затем вы попробуете выполнить dir(math) или help(math), Python выдаст ошибку. Это происходит потому, что он знает только о pi, но не обо всем модуле math. Обычно функции help() и dir() применяются в командной строке Python, когда нужно быстро получить справку или список доступных ГЛАВА 4 Библиотеки, пакеты и модули 419
методов. В реальных программах чаще используется конструкция from, потому что она позволяет импортировать только те элементы, которые действительно необходимы, не загружая лишний код. Если вам нужно импортировать несколько функций или переменных из одного модуля, просто перечислите их через запятую в команде from. Например, если вы хотите использовать число pi и функцию квадратного корня, можно написать так: from math import pi, sqrt Поскольку в этом случае используется синтаксис from для импорта, вы можете обращаться к переменным pi и функции sqrt() напрямую, без указания имени модуля. Например, после выполнения этой команды from следующий код: print(sqrt(pi)) появится результат: 1.7724538509055159 и этот результат, как несложно догадаться, представляет собой квадратный корень числа π. Вы, вероятно, встречали и такой способ импорта: from math import * Звездочка в команде означает «все». По сути, такая запись делает то же самое, что и import math, то есть импортирует весь модуль, но с одним важным отличием. При использовании from math import * все имена функций и констант из модуля становятся доступны напрямую — без необходимости указывать префикс math. Это значит, что после выполнения команды from math import * вы можете выполнить команду вроде print(pi), и она сработает даже без указания math.pi. На первый взгляд это кажется удобным и упрощает написание кода. Однако многие опытные программисты считают такой подход не совсем «питоничным». Если в программе используется несколько модулей, а их функции и константы вызываются без указания имен модулей, код становится труднее читать и понимать другим разработчикам. Поэтому обычно рекомендуется явно указывать имя модуля перед функцией или константой, чтобы код оставался понятным и однозначным. 420 РАЗДЕЛ 3 Работа с библиотеками
Создание собственных модулей На самом деле, в создании модулей нет ничего сложного. Модуль — это просто файл с кодом Python, сохраненный с расширением .py. Каждый раз, когда вы пишете программу и сохраняете ее, вы создаете модуль, даже если не планируете использовать его повторно. Разница лишь в назначении: вы можете рассматривать такой файл как самостоятельное приложение или использовать его как модуль, если хотите повторно применять написанный код в других программах. Это особенно удобно, когда определенные функции или фрагменты кода нужны вам часто — вы можете просто подключить их с помощью команды import, а не копировать вручную. Имя модуля всегда совпадает с именем файла без расширения .py. Предположим, вы хотите создать три функции, которые помогут при работе с форматированием дат и денежных значений. Вы можете назвать их как угодно, но для примера возьмем следующие: » to_date( any_str) — принимает строку с датой в формате мм/дд/гг или мм/дд/гггг и возвращает объект datetime.date, который можно использовать для вычислений с датами . » mdy( any_date) — принимает объект даты или даты/времени и возвращает строку с датой в формате мм/дд/гггг, удобную для вывода на экран . » to_curr( any_num, len) — принимает число (целое или с плавающей точкой) и возвращает строку в денежном формате: со знаком доллара, запятыми между тысячами и двумя знаками после запятой для центов . Параметр len является необязательным — если он указан, возвращаемое значение будет дополнено пробелами слева до нужной длины . Вот полный код для трех функций, объединенных в одном модуле: # Содержит пользовательские функции для работы с датами и денежными значениями import datetime as dt def to_date(any_str): """ Преобразует строку в формате мм/дд/гг или мм/дд/гггг в datetime.date, или None если дата недействительна. """ try: ГЛАВА 4 Библиотеки, пакеты и модули 421
if len(any_str) == 10: the_date = dt.datetime.strptime(any_ str,'%m/%d/%Y').date() else: the_date = dt.datetime.strptime(any_ str,'%m/%d/%y').date() except (ValueError, TypeError): the_date = None return the_date def mdy(any_date): """ Возвращает строку с датой в формате мм/дд/гггг. Принимает дату Python или строку с датой в формате мм/дд/гггг """ if type(any_date) == str: any_date = to_date(any_date) # Убедимся, что передается datetime if isinstance(any_date,dt.date): s_date = f"{any_date:'%m/%d/%Y'}" else: s_date = "Недействительная дата" return s_date def to_curr(anynum, len=0): """ Возвращает число в виде строки со знаком $ и запятыми. Длина необязательна """ s = "Недействительная сумма" try: x = float(anynum) except ValueError: x = None if isinstance(x,float): s = '$' + f"{x:,.2f}" if len > 0: s = s.rjust(len) return s Вы можете создать такой же файл и назвать его myfunc.py, если хотите повторить эти шаги. Обратите внимание, что этот файл содержит только функции. Поэтому при его запуске на экране ничего не появится, так как в нем нет кода, который вызывает эти функции. Чтобы использовать функции из этого файла в своем Pythonприложении, сначала убедитесь, что файл myfunc.py находится в той же папке, где расположен ваш основной код. Затем в начале программы импортируйте myfunc как модуль, так же как вы импортировали бы любой другой модуль, созданный другими разработчиками: 422 РАЗДЕЛ 3 Работа с библиотеками
import myfunc Перед каждой функцией, которую вы вызываете из этого модуля, нужно указывать имя модуля. Чтобы сделать код короче и удобнее для чтения, можно задать псевдоним. Например: import myfunc as my После этого вы сможете обращаться к функциям модуля, используя префикс my. Так, чтобы вызвать функцию to_date, нужно написать my.to_date(). Вот пример программы, которая импортирует модуль и проверяет работу всех трех функций, используя сокращенный префикс my: # Импортируем весь код из myfunc.py как my import myfunc as my # Для работы с датами нужен этот модуль from datetime import datetime as dt # Простые тестовые данные string_date = "12/31/2024" # Преобразуем строковую дату в datetime.date print(my.to_date(string_date)) today = dt.today() # Показываем сегодняшнюю дату в формате мм/дд/гггг print(my.mdy(today)) dollar_amt = 12345.678 # Отображаем число в денежном формате print(my.to_curr(dollar_amt)) При запуске кода результат должен выглядеть так: 2024.12.31 '12/27/2024' $12,345.68 Ранее мы говорили, что префикс можно не использовать, если импортировать нужные элементы по имени. В этом случае функции можно вызывать напрямую — без указания модуля. Для этого в начале программы нужно написать: from myfunc import to_date, mdy, to_curr Остальная часть программы остается такой же, как и раньше, только теперь вызовы функций выполняются без префикса my., как показано в следующем примере: ГЛАВА 4 Библиотеки, пакеты и модули 423
# Импортируем весь код из myfunc.py по имени from myfunc import to_date, mdy, to_curr # Для работы с датами нужен этот модуль from datetime import datetime as dt # Простые тестовые данные string_date = "12/31/2024" # Преобразуем строковую дату в datetime.date print(to_date(string_date)) today = dt.today() # Показываем сегодняшнюю дату в формате мм/дд/гггг print(mdy(today)) dollar_amt = 12345.678 # Отображаем число в денежном формате print(to_curr(dollar_amt)) Управление виртуальными окружениями В предыдущей главе мы устанавливали несколько пакетов Python с помощью команды pip install, чтобы использовать их для веб-скрейпинга. Эти пакеты были установлены глобально. то есть теперь они доступны для любого проекта, который вы создадите, без необходимости повторной установки. Это удобно, но у такого подхода есть недостатки. Разные приложения могут требовать разные версии библиотек, и, если все они установлены в одной системе, легко запутаться или столкнуться с конфликтами между версиями пакетов. Виртуальные окружения позволяют изолировать зависимости каждого приложения. Внутри такого окружения хранятся только те пакеты, которые нужны конкретному проекту. Это особенно полезно при совместной разработке: если вы делитесь своим кодом через GitHub или другой сервис, другие разработчики смогут легко воспроизвести вашу рабочую среду. Им будет достаточно установить те же библиотеки, чтобы их окружение стало идентичным вашему, и программа заработала так же, как у вас. Можно представить виртуальное окружение как отдельное пространство, которое «путешествует» вместе с вашим проектом. Каждое приложение Python должно находиться в своей отдельной папке, а виртуальное окружение — быть подпапкой внутри нее. Назвать эту подпапку можно как угодно, но чаще всего ей дают имя venv, чтобы было понятно, что в ней находится и как она была создана (с помощью модуля venv). В следующих разделах мы научимся создавать и активировать виртуальное окружение с именем venv в новой папке Example. 424 РАЗДЕЛ 3 Работа с библиотеками
Создание виртуального окружения Прежде чем начать, убедитесь, что у вас открыт VS Code и папка проекта, в которой вы будете хранить свой код на Python. Именно в этой папке будет создано виртуальное окружение. Чтобы открыть терминал в VS Code, выберите пункт меню View  Terminal. В нижней части окна появится панель терминала, где вы сможете вводить команды. На операционной системе Windows виртуальное окружение создается с помощью следующей команды: python -m venv venv Если при создании виртуального окружения появилось сообщение об ошибке, попробуйте использовать другую команду. На Windows вместо python можно ввести: py -m venv venv А на macOS чаще используется команда: python3 -m venv venv ПРИМЕЧАНИЕ После создания виртуального окружения VS Code может вывести сообщение с предложением выбрать его для папки рабочей области. Рабочая область (workspace) — это набор настроек, файлов и параметров, которые можно сохранить для конкретного проекта, чтобы быстро возвращаться к нужной конфигурации. Однако использование рабочих областей не является обязательным и не связано напрямую с Python. Поэтому, если вы только начинаете изучать язык и осваивать VS Code, можете спокойно игнорировать подобные предложения. Позже, когда вы разберетесь с основами работы в редакторе, вы сможете при желании использовать рабочие области для удобной организации своих проектов. Если после выполнения команды не появилось сообщение об ошибке, значит, виртуальное окружение создано успешно. В папке вашего проекта теперь должна быть подпапка с именем venv, как показано на рисунке 4.2. В ней будут храниться все файлы, связанные с окружением. Иногда в окне VS Code структура папок может отображаться в одну строку, например EXAMPLE\venv. Это связано с настройкой отображения, которую можно изменить в любое время. ГЛАВА 4 Библиотеки, пакеты и модули 425
В Windows для этого откройте меню File  Preferences  Settings, а на macOS — Code  Settings. В поле поиска введите слово folders и снимите флажок с параметра Compact Folders, если хотите, чтобы папки отображались в виде развернутого списка, как в примере. Если вы предпочитаете работать напрямую с файлом настроек, откройте файл settings.json и добавьте в него строку: "explorer.compactFolders": false. Рисунок 4.2. Подпапка venv представляет собой виртуальное окружение Во всех трех предыдущих командах первое появление слова venv (сразу после параметра -m) обозначает название модуля, который запускается для создания виртуального окружения. Последнее слово в команде — это имя подпапки, в которой это окружение будет размещено. В приведенных примерах подпапка называется venv, как показано на рисунке 4.2. Если вы хотите выбрать другое имя, просто замените последнее слово в команде на любое удобное вам. На этом этапе виртуальное окружение уже создано, и внутри него появились несколько служебных файлов, которые обеспечивают его работу. Эти файлы не нужно изменять вручную. Однако важно помнить, что окружение пока не активировано. Если начать устанавливать пакеты прямо сейчас, они будут установлены глобально, а не внутри вашего проекта. Поэтому перед установкой любых библиотек необходимо активировать виртуальное окружение, как будет показано в следующем разделе. Активация виртуального окружения Перед установкой пакетов для нового проекта нужно убедиться, что виртуальное окружение активировано. Это делается через панель терминала в VS Code, но команды для активации зависят от используемой операционной системы. 426 РАЗДЕЛ 3 Работа с библиотеками
Если вы работаете в Windows, введите в терминале следующую команду: venv/scripts/activate На macOS для активации виртуального окружения используйте следующую команду: source venv/bin/activate После активации виртуального окружения в начале командной строки терминала появится его название, заключенное в круглые скобки, как показано на рисунке 4.3. Это единственный визуальный признак того, что окружение действительно активно. Теперь можно использовать команду pip install (или pip3 install на macOS), чтобы установить нужные пакеты для вашего приложения. Устанавливать их все сразу необязательно — на этом этапе вы, скорее всего, еще не знаете точно, какие библиотеки понадобятся. Это не проблема: вы можете добавить любой пакет позже, когда он действительно потребуется. Главное правило — всегда проверяйте, что виртуальное окружение активно, прежде чем устанавливать новые пакеты. В этом случае они будут установлены именно в ваше окружение и не повлияют на другие проекты или системную установку Python. Рисунок 4.3. Имя в скобках в начале приглашения командной строки указывает на активное виртуальное окружение Создание файла requirements.txt Когда вы планируете поделиться своим приложением с другими разработчиками, полезно создать файл requirements.txt. Этот файл содержит перечень всех библиотек и их версий, установленных в вашем виртуальном окружении. Благодаря этому другие смогут установить все необходимые пакеты одной командой, не разбираясь, какие именно зависимости нужны для работы программы. ГЛАВА 4 Библиотеки, пакеты и модули 427
Чтобы создать файл requirements.txt, убедитесь, что вы находитесь в папке своего проекта и что виртуальное окружение активно. Затем выполните в терминале следующую команду: pip freeze > requirements.txt Если команда не сработала, попробуйте заменить слово pip на pip3 и выполнить ее снова. После успешного выполнения в окне проводника VS Code появится новый файл с именем requirements.txt, как показано на рисунке 4.4. Рисунок 4.4. Файл requirements. txt добавлен в Pythonприложение Любой разработчик, получивший ваш проект, может установить все нужные пакеты с помощью файла requirements.txt. Для этого достаточно открыть терминал, перейти в папку проекта и выполнить команду: pip install -r requirements.txt Эта команда автоматически установит все перечисленные в файле библиотеки и сохранит их копии в виртуальном окружении. Благодаря этому программа сможет использовать необходимые пакеты во время работы, а окружение других разработчиков будет полностью соответствовать вашему. Деактивация виртуального окружения Когда вы закрываете VS Code, виртуальное окружение автоматически завершается. Поэтому при следующем открытии проекта всегда стоит проверить, активно ли оно. Взгляните на командную строку терминала: если в начале строки нет названия окружения (например, venv), значит, оно неактивно. В этом случае нужно снова активировать его перед установкой пакетов или запуском программы. 428 РАЗДЕЛ 3 Работа с библиотеками
Иногда может потребоваться отключить виртуальное окружение, не закрывая сам проект. Это бывает нужно редко, но полезно знать, как это сделать. Для деактивации достаточно ввести в терминале простую команду: deactivate После деактивации виртуального окружения его имя исчезнет из начала командной строки. Это означает, что окружение больше не активно. Если теперь установить какие-либо пакеты, они будут установлены глобально, то есть вне вашего проекта и не будут связаны с конкретным виртуальным окружением. Не стоит переживать, если вы не запомнили все команды для работы с виртуальными окружениями сразу. Достаточно понимать основные принципы и терминологию. При необходимости вы всегда можете найти нужные команды с помощью поиска в интернете или обратиться к инструментам вроде ChatGPT или Claude. Например, запрос «python venv virtual environments» быстро напомнит вам все основные команды для создания, активации и управления окружениями. На этом раздел о библиотеках, пакетах и модулях Python можно считать завершенным. Все эти элементы представляют собой код, созданный другими разработчиками и доступный для использования в ваших собственных проектах. Главное различие между ними заключается в масштабе: библиотека может включать несколько пакетов, пакет — несколько модулей, а модуль, как правило, содержит функции, классы и другие готовые части кода, которые можно применять в своих программах. В следующих главах вы познакомитесь с множеством модулей и классов, благодаря которым Python считается гибким, мощным и применимым к самым разным задачам — от анализа данных до искусственного интеллекта и робототехники. Однако при этом важно помнить: все те базовые принципы Python, которые вы изучили ранее, остаются основой любой работы с кодом, независимо от того, в какой области вы будете применять язык.

4 ИСПОЛЬЗОВАНИЕ ИСКУССТВЕННОГО ИНТЕЛЛЕКТА
В ЭТОМ РАЗДЕЛЕ Глава 1: Демистификация искусственного интеллекта Глава 2: Создание нейронной сети Глава 3: Практическое машинное обучение Глава 4: Игра с искусственным интеллектом — на картах, микроконтроллерах и в облаке
В этой главе вы узнаете: » Какие существуют методы искусственного интеллекта: нейронные сети и машинное обучение » Что представляет собой библиотека TensorFlow » В чем заключаются ограничения искусственного интеллекта Гл а в а 1 Демистификация искусственного интеллекта И скусственный интеллект (ИИ) в последние годы стал одним из самых обсуждаемых и при этом часто неправильно понимаемых терминов. Средства массовой информации нередко преувеличивают значение даже небольших достижений в этой области, создавая впечатление, будто «умные компьютеры уже среди нас». Например, в 2017 году инженеры одной крупной компании провели эксперимент: они создали две программы, каждая из которых по-разному оценивала определенные объекты — мячи, кубики и другие предметы. Программы должны были вести между собой переговоры, используя заданные правила и язык, напоминающий английский, чтобы получить как можно больше ценных для них объектов. Однако у этих программ не было полноценного синтаксического анализатора языка. В процессе обучения их способ общения постепенно утратил грамматическую структуру, превратившись в искаженный английский. Один из характерных примеров: если программа хотела что-то получить, она говорила: «I want». Логика обучения привела к тому, что повторение этой фразы — «I want I want I want» — стало для программы более «успешным» поведением. Некоторые СМИ восприняли это как создание нового языка общения между ИИ, хотя на деле это было просто результатом особенностей ГЛАВА 1 Демистификация искусственного интеллекта 433
обучения. Когда эксперимент завершился, программы отключили, но один из «экспертов» заявил, будто инженеры сделали это, чтобы остановить искусственный интеллект, который якобы вот-вот должен был обрести сознание. Разумеется, это не соответствовало действительности. Позже, в 2023 году, мир снова столкнулся с волной внимания к ИИ — на этот раз из-за появления ChatGPT и других генеративных моделей. На первый взгляд ситуация напомнила прежнюю шумиху, но в действительности она отличается. Генеративный искусственный интеллект действительно представляет собой полезный инструмент и является частью реальной технологической революции, тогда как эксперимент, который описывался выше, был скорее любопытным примером поведения обучающихся программ без практического значения. В этой главе мы познакомимся с основными идеями искусственного интеллекта, разберем, чем он является на самом деле и какие ограничения имеет. Мы рассмотрим ключевые подходы, используемые в ИИ, а также обратимся к его истории, чтобы лучше понять, как современные достижения вписываются в общий контекст развития этой области. ИИ — это набор методов ЗАПОМНИТЕ Цель этой главы — развеять мифы об искусственном интеллекте и показать, насколько полезными и впечатляющими могут быть современные методы ИИ. Здесь важно подчеркнуть: мы говорим именно о методах искусственного интеллекта, а не об «ИИ» как о некой разумной сущности. Так называемый универсальный ИИ — роботы или программы, способные мыслить и действовать как человек, — пока не создан и, вероятно, еще долго останется предметом научных исследований. Однако уже сегодня существует множество алгоритмов, статистических подходов и инструментов, которые позволяют машинам выполнять задачи, требующие элементов обучения, анализа и адаптации к изменениям окружающей среды. В этой главе мы познакомимся с тремя основными методами искусственного интеллекта: » » » 434 нейронными сетями; машинным обучением; и примером программы-классификатора на основе TensorFlow, написанной на Python . РАЗДЕЛ 4 Использование искусственного интеллекта
После изучения данного раздела вы не «обретете разум» (в отличие от ИИ), но точно приобретете четкое понимание того, как работают эти технологии и как их можно применять на практике. В следующих разделах мы кратко рассмотрим каждый из перечисленных методов и познакомимся с примерами программ, которые будут подробно разобраны в последующих главах. Кроме того, обратите внимание на раздел «Искусственный интеллект — технология будущего», где дается общее представление о развитии этой сферы и направлениях, в которых ИИ продолжает активно эволюционировать. Нейронные сети Как подсказывает само название, нейронные сети основаны на идее, вдохновленной работой человеческого мозга. В мозге человека насчитываются десятки миллиардов нейронов — по оценкам, около 80 миллиардов, — а также примерно в десять раз больше глиальных клеток, которые обеспечивают поддержку нейронов, помогают им обмениваться сигналами и участвуют в организации их взаимодействия. Главный принцип, определяющий работу мозга, — это связи между нейронами. Каждый нейрон может быть соединен с тысячами других, а у некоторых — до двухсот тысяч таких связей. Именно структура и характер этих соединений, а не просто количество клеток, определяют уровень интеллекта и сложность мыслительных процессов. Искусственные нейронные сети, применяемые в ИИ, были созданы как попытка воспроизвести этот принцип в цифровом виде. Они не являются точной копией человеческого мозга, но работают по схожей логике: отдельные «искусственные нейроны» принимают входные данные, обрабатывают их и передают результат дальше по Cети. ИСКУССТВЕННЫЙ ИНТЕЛЛЕКТ — ТЕХНОЛОГИЯ БУДУЩЕГО «Какой волшебный трюк делает нас умными? Трюк в том, что никакого трюка нет. Сила интеллекта проистекает из нашего огромного разнообразия, а не из какого‑то единственного со‑ вершенного принципа». — Марвин Минский, «Общество разума» (1987) ГЛАВА 1 Демистификация искусственного интеллекта 435
Джон Шовик, соавтор книги, делится воспоминаниями: «В 1983 году мне посчастливилось провести целый день с доктором Марвином Минским в Покателло, штат Айдахо . В то время я работал инженером по проектированию сверхбольших интегральных схем в компании American Micro Systems и только начинал писать диссертацию в Университете Айдахо на заочном отделении . Я как раз окончил свой первый курс по искусственному интеллекту и был невероятно взволнован, когда узнал, что сам доктор Минский из лаборатории ИИ Массачусетского технологического института приезжает в Покателло . После нескольких звонков мне удалось договориться о встрече — мы с ним пообедали и провели целый день в беседе . Оглядываясь назад, я до сих пор удивляюсь, как он смог уделить нам столько времени, но таким он и был — открытым и доброжелательным человеком . Для меня это было сродни тому, как если бы поклонники Rolling Stones вдруг получили возможность провести день с Миком Джаггером . И честно говоря, как бы мне ни хотелось встретиться с Джаггером, день, проведенный с доктором Минским, был для меня гораздо важнее . На тот момент я только что прочитал его книгу "Искусственный интеллект" и был переполнен вопросами о его идеях и исследованиях . Минскому, в свою очередь, было интересно узнать, чему нас учили на курсе по ИИ в Университете Айдахо . Он тепло отзывался о нашем преподавателе, докторе Джоне Дикинсоне, и хвалил его за удачный выбор тем . Я был полон энтузиазма и всерьез подумывал посвятить свою карьеру этой области . Из всего, что он сказал, я особенно запомнил одну фразу (передаю по смыслу): "Я начал заниматься искусственным интеллектом около двадцати лет назад и тогда был уверен, что еще через двадцать лет у нас будет настоящий ИИ . Сейчас я думаю, что до его появления нам нужно еще лет двадцать". Прошло время, и, вспоминая тот разговор в 2003 году, я осознал, что предсказание Минского не сбылось — общего искусственного интеллекта все еще не существовало . Тогда я впервые задумался: не останется ли ИИ навсегда "технологией будущего"? И вот теперь, в 2024 году, спустя еще двадцать лет, у нас по-прежнему нет универсального ИИ . Однако многое изменилось . Сегодня у нас есть целый арсенал технологий искусственного интеллекта, которые умеют обучаться, анализировать и выполнять поразительно сложные задачи . Возможно, именно эти достижения однажды приведут нас к созданию того самого общего искусственного интеллекта, о котором мечтал доктор Минский . А может быть, на этот раз ждать придется меньше двадцати лет» . 436 РАЗДЕЛ 4 Использование искусственного интеллекта
ЗАПОМНИТЕ У слонов насчитывается более 270 миллиардов нейронов, однако их связи не такие плотные, как у человеческих нейронов. Роберт Меткалф, основатель компании 3Com и профессор Университета Техаса, известен своим утверждением, что «ценность телекоммуникационной сети пропорциональна квадрату числа подключенных пользователей». Этот принцип, получивший название закон Меткалфа, применим и к мозгу: именно плотность связей между нейронами определяет его эффективность. Таким образом, хотя у человека значительно меньше нейронов, чем у слона, мы компенсируем это гораздо более плотной и сложной сетью соединений между ними. Возможно, именно благодаря этому наш мозг способен на такие формы мышления, творчества и самоосознания. И все же неудивительно, что слоны, обладая огромным количеством нейронов, отличаются хорошей памятью и развитым социальным поведением. Исследователи искусственного интеллекта предположили — и оказались правы, — что если удастся воспроизвести принципы работы нейронов и их связей, то эту идею можно будет использовать для создания компьютерных моделей, способных сделать программы более «умными». Однако первая популярная попытка реализовать эту идею — модель под названием перцептрон — имела серьезные ограничения. Когда стало ясно, что перцептроны не справляются со многими задачами, энтузиазм вокруг нейронных сетей резко угас, и это привело к периоду, который позже назвали первой «зимой ИИ» (см. врезку «Зима искусственного интеллекта»). В последующие десятилетия, особенно за последние сорок лет, исследования в этой области значительно продвинулись. Ученые подробно изучили устройство и принципы работы нейронных сетей, разработали сверточные нейронные сети и архитектуры глубокого обучения. Эти открытия легли в основу современных программных библиотек и инструментов, которые сегодня применяются для решения практических задач — от распознавания изображений до анализа речи и данных. ЗИМА ИСКУССТВЕННОГО ИНТЕЛЛЕКТА Термин «зима ИИ » впервые появился в 1984 году на встрече Американской ассоциации искусственного интеллекта . Тогда доктор Марвин Минский предупреждал представителей бизнеса, что чрезмерный энтузиазм вокруг ИИ в 1980-х годах может привести к неизбежному разочарованию . Его предсказание сбылось: всего через три года многомиллиардная индустрия искусственного интеллекта фактически рухнула . Под «зимой ИИ» принято понимать период, когда резко сокращаются инвестиции и финансирование научных исследований в этой области . ГЛАВА 1 Демистификация искусственного интеллекта 437
Подобные явления хорошо известны экономистам и называются «пузырями» — как, например, «пузырь доткомов» 1999 года, крах рынка недвижимости в 2007 году или знаменитый тюльпановый пузырь в Голландии 1637 года . Когда ожидания от технологии становятся слишком высокими, они неизбежно сменяются спадом и разочарованием . Подобная ситуация повторилась и в 2000-х годах, частично из-за кризиса доткомов . В то время исследователи старались избегать самих слов «искусственный интеллект» в названиях своих проектов, чтобы не отпугнуть потенциальных инвесторов . Сегодня можно заметить схожие признаки нового «пузыря» . Однако есть важное отличие: теперь технологии искусственного интеллекта прочно вошли в повседневную жизнь . Примеры — голосовые помощники вроде Amazon Alexa и Google Home, а также рекомендательные системы и интеллектуальные поисковые сервисы . Даже если нынешний ажиотаж и приведет к очередному спаду, у нас останется множество практических инструментов, которые продолжат использоваться и после того, как «пузырь» лопнет . Судя по тому вниманию, которое вызвал ChatGPT и другие генеративные модели в 2023 году, можно сказать, что мы вновь находимся на пике интереса к ИИ — и, возможно, перед новой зимой . Нейронные сети хорошо моделируют то, как мозг обучается и классифицирует информацию. Поэтому неудивительно, что они стали основой для машинного обучения и других направлений искусственного интеллекта. В их основе лежат искусственные нейроны и связи между ними — именно эти соединения и образуют нейронную сеть. При создании нейронной сети важно определить ее архитектуру и структуру, то есть задать, как нейроны будут соединены между собой. От выбранной архитектуры зависит то, как сеть будет воспринимать, обрабатывать и интерпретировать данные. Существует направление исследований, называемое эволюционными вычислениями, в котором архитектура сети изменяется и оптимизируется автоматически. В некоторых случаях используются гибридные подходы, сочетающие ручное проектирование и автоматическую настройку структуры сети. Одним из важнейших этапов при создании нейронной сети является выбор функции активации. Она определяет, когда нейрон будет «включаться», то есть реагировать на поступающие сигналы. Активация происходит, когда значение входного сигнала превышает заданный порог. Выбор подходящей функции активации сильно влияет на то, как сеть обучается и как она справляется с задачами распознавания или классификации. 438 РАЗДЕЛ 4 Использование искусственного интеллекта
В следующей главе вы создадите собственную модель нейронной сети на Python и зададите функцию активации как один из ключевых элементов ее работы. ЭВОЛЮЦИОННЫЕ ВЫЧИСЛЕНИЯ Эволюционные вычисления — это набор вычислительных алгоритмов и эвристических методов (или, проще говоря, практических правил), предназначенных для поиска оптимальных решений сложных задач . Эти методы вдохновлены процессами биологической эволюции — естественным отбором, мутацией и наследованием . Хотя эволюционные вычисления имеют общие корни с машинным обучением, между ними есть важное различие: если алгоритмы машинного обучения обучаются находить заранее известные закономерности, то эволюционные методы ищут решения там, где заранее не известно, каким оно должно быть . С помощью таких алгоритмов можно находить решения в так называемом «пространстве проектирования» или «пространстве задач», исследуя множество возможных вариантов и постепенно улучшая их . Эволюционные вычисления применяются в самых разных областях — от проектирования космических антенн и промышленных конструкций до оптимизации транспортных маршрутов . Их особенность заключается в том, что поиск решений осуществляется итеративно: алгоритм создает «популяцию» возможных решений, оценивает их эффективность, отбирает лучшие и формирует на их основе новое поколение, постепенно приближаясь к оптимальному результату . Такие методы требуют больших вычислительных ресурсов, поскольку необходимо одновременно анализировать множество вариантов . Оценка целых популяций решений и постоянное улучшение каждой итерации делают этот процесс крайне ресурсоемким . Поэтому для эффективной работы эволюционные алгоритмы часто выполняются на системах с параллельными вычислениями . Джон Шовик вспоминает: «Один из студентов моего курса “Продвинутая робототехника” в Университете Айдахо использовал эволюционные вычисления, чтобы научить робота по имени Бакстер перекладывать красные и желтые блоки из одной коробки в другую . Он использовал изображения с камер робота и пытался с помощью эволюционных алгоритмов найти способ распознавания и определения местоположения блоков . Однако ни студенту, ни самому Бакстеру не удалось найти устойчивое решение . Почему? Потому что использование эволюционных методов для плохо определенных и неограниченных задач ГЛАВА 1 Демистификация искусственного интеллекта 439
требует экспоненциально растущих вычислительных мощностей — и даже они не всегда гарантируют успех . Эволюционные вычисления лучше всего подходят для небольших, четко очерченных задач» . «Сейчас, в 2024 году, один из моих постдокторантов успешно применяет генетические алгоритмы и другие методы эволюционных вычислений для анализа производственных данных . Он ищет причины дефектов и узкие места в миллионах записей, и результаты его работы действительно впечатляют . Эти методы, при правильной постановке задачи, демонстрируют по-настоящему выдающуюся эффективность» . 440 РАЗДЕЛ 4 Использование искусственного интеллекта
Машинное обучение Обучение — одно из ключевых свойств, которые делают человека разумным существом. Мы способны воспринимать информацию, анализировать ее и адаптироваться к новым обстоятельствам. Машинное обучение основано на этом же принципе, но примененном к компьютерам. Это направление искусственного интеллекта изучает методы и алгоритмы, которые позволяют программам самостоятельно улучшать свою работу, накапливая опыт и извлекая закономерности из данных. По мнению Джона Шовика: «Люди обладают удивительной способностью быстро учиться и переносить знания из одной ситуации в другую. Пока я пишу эти строки, наблюдаю, как моя двухлетняя внучка Хейзел ест свою первую кесадилью. Я вижу, как она уже умеет находить еду на тарелке, говорить маме, что ей нравится брать кусочек, подносить его ко рту и есть. Все это — результат обучения и опыта, накопленного всего за два года жизни. Мне бы очень хотелось научить своих роботов хотя бы половине той сообразительности, которой обладает Хейзел. А пока что остается только одно — вытереть сливочный сыр с клавиатуры моего ноутбука». Сегодня исследователи в области искусственного интеллекта смогли довести способность компьютеров к обучению почти до человеческого уровня в решении конкретных задач — например, в распознавании изображений или обработке речи. Именно благодаря этому мы можем говорить такие фразы, как: «Алекса, найди мне новое телешоу». Подобные подходы позволяют достигать сопоставимого уровня обучения и в других сферах, например, в системах управления автомобилем. Однако машинное обучение нельзя назвать полностью автоматизированным процессом. Компьютер не способен просто «прочитать книгу» и осознать ее содержание. Современные методы машинного обучения требуют большого количества тщательно отобранных и размеченных человеком данных, а также анализа и тренировки. Только при таком подходе алгоритмы могут действительно обучаться и выполнять поставленные задачи эффективно. В машинном обучении применяется широкий спектр методов, среди которых: » » » » статистический анализ; нейронные сети; деревья решений; эволюционные алгоритмы . ГЛАВА 1 Демистификация искусственного интеллекта 441
Машинное обучение объединяет различные подходы, позволяющие использовать методы обучения для моделирования и даже улучшения экспертных систем, которые были основным направлением развития искусственного интеллекта в 1980-х годах. В конце этого десятилетия Джон написал докторскую диссертацию, посвященную созданию глубоко конвейеризованной вычислительной машины, предназначенной для ускорения работы таких систем. Аппаратное ускорение алгоритмов машинного обучения остается актуальной областью исследований и сегодня. Разработчики продолжают искать способы повышения скорости и эффективности вычислений, применяя специализированные процессоры и архитектуры. В следующей главе мы рассмотрим, как использовать язык Python для создания программ, которые демонстрируют основные принципы и ключевые задачи машинного обучения. ИСКУССТВЕННЫЙ ИНТЕЛЛЕКТ В СМАРТФОНАХ Современная революция в области искусственного интеллекта активно проникает в мир мобильных устройств . Крупнейшие компании, такие как Samsung и Apple, стремятся внедрить технологии ИИ прямо в свои смартфоны, делая их умнее и функциональнее . На момент написания этой книги обработка данных на последних моделях Samsung выполняется не на самом устройстве, а в облаке, что позволяет использовать более мощные вычислительные ресурсы для работы интеллектуальных функций . Смартфоны Apple оснащаются чипом A16 Bionic, в котором используется шестиядерный центральный процессор и четырехъядерный графический процессор, оптимизированный для задач искусственного интеллекта и графики . Этот чип специально разработан для работы с ИИ-приложениями, например, системами распознавания лиц . В его составе есть отдельный нейронный модуль, предназначенный для выполнения операций машинного обучения . Аппаратное и программное обеспечение ИИ в таких устройствах способны выполнять более 17 триллионов операций в секунду . Вот почему даже такие, казалось бы, простые функции, как анимодзи в текстовых сообщениях, на самом деле демонстрируют работу сложнейших технологий искусственного интеллекта . 442 РАЗДЕЛ 4 Использование искусственного интеллекта
TensorFlow — фреймворк для глубокого обучения Глубокое обучение — это направление искусственного интеллекта, в котором компьютеры обучаются обрабатывать данные по принципам, вдохновленным биологическими процессами и работой человеческого мозга. TensorFlow представляет собой набор открытых многомашинных программных интерфейсов (API), предназначенных для создания, обучения и исследования систем глубокого обучения. Этот фреймворк скрывает большую часть внутренней сложности подобных вычислений, делая технологию более понятной и доступной для разработчиков. TensorFlow был создан в 2011 году как внутренний инструмент группы Google Brain и изначально использовался только внутри компании. Позднее, после выпуска с открытым исходным кодом, он быстро завоевал признание в сообществе разработчиков и исследователей искусственного интеллекта. Сегодня TensorFlow применяют тысячи специалистов по всему миру: из более чем 95 000 репозиториев с кодом на GitHub, использующих этот фреймворк, лишь 32 принадлежат Google Research. GitHub — это популярная онлайн-платформа, где разработчики делятся исходным кодом своих проектов, чтобы другие могли использовать его и учиться на реальных примерах. Название TensorFlow отражает принцип работы системы. Слово тензор обозначает многомерную матрицу данных, которая последовательно преобразуется при прохождении через различные слои нейронной сети. Таким образом, данные «текут» (flow) через слои, изменяясь на каждом этапе обучения. Основы работы с тензорами и матричными преобразованиями изучаются в курсах линейной алгебры — например, в книге Мэри Джейн Стерлинг «Линейная алгебра для чайников», которая может послужить хорошим введением в эту тему. СОВЕТ TensorFlow основан на работе с матрицами. Поскольку другое название матрицы — тензор, фреймворк и получил свое имя: TensorFlow, что в переводе означает «поток тензоров». TensorFlow отлично интегрируется с языком Python и может использоваться как на локальных компьютерах, так и в облачных системах. Благодаря этому фреймворк подходит как для индивидуальных проектов, так и для масштабных распределенных вычислений. Он относительно прост в освоении и использовании, поэтому вы можете быстро начать разрабатывать и экспериментировать с приложениями искусственного интеллекта, не погружаясь в излишние технические детали. ГЛАВА 1 Демистификация искусственного интеллекта 443
Текущие ограничения ИИ Ключевое слово, определяющее все направление искусственного интеллекта, — это понимание. Алгоритмы машинного обучения можно обучить управлять автомобилем, распознавать речь или классифицировать изображения, но при этом они не понимают, что именно делают. Программа способна анализировать данные и находить закономерности, однако интерпретировать результаты и оценивать их правильность по-прежнему должен человек. Интерпретация выходит далеко за рамки простого анализа. Люди могут чувствовать, верен ли вывод, даже если не способны точно объяснить, почему. Искусственный интеллект, напротив, просто принимает данные как истину. Если в исходных данных содержится ошибка, алгоритм примет ее за факт и будет использовать в дальнейших расчетах. Это особенно заметно на примере генеративных систем, таких как ChatGP T. Программа способна формировать текст, но не оценивает его достоверность. Если алгоритм нашел в сети ошибочную информацию или пользователи сами подтвердили неверное утверждение, он может воспроизводить это как правильный ответ. В августе 2023 года доктор Терри Соул, заведующий кафедрой информатики Университета Айдахо, продемонстрировал это на практике: он намеренно сообщил ChatGPT неверное решение задачи как верное, а позже, задав тот же вопрос снова, получил тот же ошибочный ответ. Пока мы сами до конца не понимаем многие аспекты человеческого мышления и поведения, разработка математических моделей, точно отражающих эти процессы, остается маловероятной. А именно такие модели нужны, чтобы приблизить искусственный интеллект к настоящему человеческому пониманию. Тем не менее, несмотря на свои ограничения, ИИ остается мощным инструментом для анализа больших объемов данных и поиска эффективных решений в сложных задачах. Теперь самое время перейти к следующим главам и начать применять искусственный интеллект на практике, используя Python.
В этой главе вы узнаете: » Основы работы нейронных сетей » Как создать свою первую нейронную сеть » Как построить нейронную сеть с помощью Python Гл а в а 2 Создание нейронной сети Н ейронные сети и различные другие модели, основанные на принципах работы мозга, существуют столько же, сколько и сама идея искусственного интеллекта. Доктор Марвин Минский, о котором говорилось в первой главе данного раздела, сыграл ключевую роль в развитии интереса к моделированию нейронов благодаря своей новаторской работе с перцептронами, опубликованной в 1969 году. В то время во многих кругах возник бурный и зачастую чрезмерный энтузиазм по поводу того, что перцептроны вскоре сделают искусственный интеллект практической технологией. Это привлекло значительные инвестиции и внимание венчурного капитала. Однако, как это часто бывает, когда ожидания оказываются завышенными, большинство проектов не оправдали надежд, и финансирование исследований в области нейронных сетей резко сократилось. Перенесемся на сорок лет вперед от моей встречи с доктором Минским в 1983 году. Сегодня мы наблюдаем новое возрождение интереса к нейронным сетям. Современные модели стали гораздо более сложными и эффективными, а главное — получили реальные, экономически оправданные области применения. От распознавания изображений и обработки речи до прогнозирования и автономного управления — нейронные сети прочно вошли в жизнь. Можно ожидать, что нынешний ажиотаж вокруг ИИ со временем приведет к очередному «пузырю», как это уже бывало в прошлом. Однако ГЛАВА 2 Создание нейронной сети 445
в этот раз ситуация отличается: созданные технологии и практические приложения настолько полезны, что интерес к искусственному интеллекту вряд ли исчезнет полностью, даже после спада инвестиций. В этой главе вы познакомитесь с концепцией нейронных сетей и научитесь реализовывать их с помощью языка Python. Чтобы загрузить примеры кода, перейдите на сайт www.dummies.com/go/pythonaiofd3e, пролистайте страницу до раздела Downloads и выберите ссылку для загрузки материалов к разделу 4 книги. Существует множество типов компьютеров, на которых можно запускать TensorFlow. Если вы хотите использовать обычный персональный компьютер, это вполне возможно — фреймворк поддерживается как в системах Linux, так и в Windows. Каждая операционная система имеет свои особенности установки и настройки, с которыми стоит ознакомиться перед началом работы. В примерах этой книги используется одноплатный компьютер Raspberry Pi 4B с 8 ГБ оперативной памяти. Для него можно загрузить готовый образ системы с уже установленным TensorFlow 2.10. Чтобы сделать это, перейдите на сайт www.dummies.com/go/pythonaiofd3e, пролистайте страницу до раздела Downloads и скачайте файл 2023.09–23. PAIOSDCardV2.zip. Этот образ предназначен для 64-битной версии системы Bullseye и устанавливается на SD-карту Raspberry Pi 4B. СОВЕТ Используйте качественную SD-карту объемом не менее 32 ГБ. Это обеспечит стабильную работу системы и достаточное место для установки всех необходимых компонентов. Имя пользователя по умолчанию — pi, пароль по умолчанию — 3.14. ЗАПОМНИТЕ Мы предоставляем готовый образ системы с уже установленным TensorFlow. Если же вы предпочитаете установить его самостоятельно, вы можете найти множество подробных инструкций в интернете. Однако стоит быть осторожным: некоторые из них могут содержать ошибки или оказаться несовместимыми с определенными версиями TensorFlow. После завершения установки запустите файлы testTF.py и testML.py, входящие в код этой главы, чтобы проверить корректность работы системы. Если программы выполнятся без исключений (предупреждения можно не учитывать), значит установка прошла успешно, и TensorFlow работает правильно. 446 РАЗДЕЛ 4 Использование искусственного интеллекта
Понимание нейронных сетей Нейронная сеть обладает следующими шестью основными характеристиками: » входным слоем нейронов, который соединяется с внешним миром; » » одним или несколькими скрытыми слоями нейронов; » набором весов и смещений, связывающих нейроны между уровнями; » функцией активации, выбираемой для каждого скрытого слоя; » функцией потерь, используемой для предотвращения переобучения сети . выходным слоем нейронов, который передает результаты наружу; На рисунке 2.1 представлена архитектура двухслойной нейронной сети. Следует отметить, что при подсчете числа слоев обычно не учитывается входной слой, поэтому сеть с входным, скрытым и выходным слоями считается двухслойной. Нейрон, в контексте искусственного интеллекта, — это программная модель клетки нервной системы, которая функционирует примерно так же, как нейрон в человеческом мозге. В этой модели используются числовые параметры, называемые весами. Они определяют, насколько сильно конкретный нейрон влияет на итоговый результат: чем выше вес, тем больше значение сигнала, передаваемого этим нейроном, и наоборот. ГЛАВА 2 Создание нейронной сети 447
Рисунок 2.1. Двухслойная нейронная сеть Рассматривая эту диаграмму, можно заметить, что нейроны каждого слоя соединены со всеми нейронами следующего слоя. Каждой из этих связей назначается определенный вес, который определяет, насколько сильно сигнал от одного нейрона будет влиять на активацию нейрона в следующем слое. Слои нейронов На рисунке 2.1 показаны три основных компонента нейронной сети: входной слой, скрытый слой (названный так потому, что он не имеет прямого контакта с внешним миром) и выходной слой. Это пример простой сети, но в реальных приложениях нейронные сети могут быть гораздо сложнее и включать десятки или даже сотни скрытых слоев. Именно из-за большого количества таких слоев возник термин «глубокое обучение» — глубина сети определяется числом скрытых уровней обработки данных. Движение информации в этой сети происходит слева направо. Каждый слой принимает данные от предыдущего и передает результаты следующему. Такой тип сети называется сетью прямого распространения, потому что данные проходят только в одном направлении — от входа к выходу, без обратных связей. Теперь возникает вопрос: как такая сеть обучается? Процесс начинается с того, что нейронная сеть получает пример данных и делает прогноз, используя свои текущие веса и смещения. Если ответ оказывается 448 РАЗДЕЛ 4 Использование искусственного интеллекта
неверным, сеть «возвращается назад» и корректирует значения весов и смещений, уменьшая ошибку. Этот механизм называется обратным распространением ошибки. Он повторяется многократно, постепенно улучшая точность сети. По сути, это аналог того, как человек учится чему-то новому — методом последовательных проб и ошибок, для наглядности это показано на рисунке 2.2. Рисунок 2.2. Прямое распространение и обратное распространение ошибки После многократного повторения цикла обучения нейронная сеть постепенно совершенствуется: с каждой итерацией, называемой эпохой, она все точнее выполняет поставленные задачи. Для сложных вычислений этот процесс может занимать дни или даже недели и требовать значительных вычислительных ресурсов. Однако после завершения обучения обученную нейронную сеть можно легко воспроизвести. Достаточно сохранить ее топологию, веса и смещения — и модель можно использовать повторно без необходимости заново проходить процесс обучения. Это делает нейронные сети чрезвычайно удобными для внедрения в различные продукты: обучение требует больших ресурсов, но использование уже обученной сети — нет. Благодаря этому подходу инженеры могут интегрировать ИИ в приложения и устройства с минимальными затратами. ГЛАВА 2 Создание нейронной сети 449
Нейронные сети действительно моделируют некоторые аспекты человеческого обучения, однако человеческий мозг обладает гораздо более сложной системой восприятия и категоризации информации. Например, человеку не требуется дополнительное обучение, чтобы понять, что лошади и свиньи принадлежат к категории животных. В то время как нейронным сетям для переноса знаний из одной задачи в другую обычно нужно повторное обучение. Именно это ограничение остается одной из ключевых проблем глубокого обучения сегодня. ОБРАТНОЕ РАСПРОСТРАНЕНИЕ В человеческом мозге обучение происходит за счет образования новых соединений между нейронами — синапсов — и изменения уже существующих связей под воздействием внешних стимулов . Именно благодаря этому мозг способен адаптироваться и запоминать новую информацию . В области искусственного интеллекта аналогом такого процесса является обратное распространение ошибки — метод, при котором результаты работы сети передаются обратно на предыдущие слои, чтобы скорректировать значения весов и смещений . Этот процесс позволяет сети «учиться» на собственных ошибках и постепенно улучшать точность своих прогнозов . Подходы к реализации обратного распространения развивались с течением времени . Многие исследователи считают, что именно усовершенствованные алгоритмы обратного распространения сыграли ключевую роль в новом подъеме интереса к искусственному интеллекту и помогли выйти из последней «зимы ИИ» (подробнее об этом см . в главе 1 раздела 4) . Обратное распространение ошибки — достаточно сложная с математической точки зрения тема . Тем, кто хочет глубже разобраться в ее принципах и практическом применении, можно порекомендовать книгу Джона Пола Мюллера и Луки Массарона «Машинное обучение для чайников» . 450 РАЗДЕЛ 4 Использование искусственного интеллекта
Веса и смещения На рисунке 2.1 каждый круг обозначает нейрон, а линии между ними — это синапсы, то есть соединения, по которым передаются сигналы. Каждый нейрон имеет два основных параметра — вес и смещение, которые влияют на его работу и выходной сигнал. Вес можно рассматривать как показатель важности входного сигнала для данного нейрона: чем выше вес, тем больше влияние соответствующего входа на результат. Смещение (или bias) служит для корректировки активации нейрона, позволяя сместить порог срабатывания вверх или вниз. Если рассмотреть всю сеть, можно увидеть, что ее итоговый выход определяется исключительно значениями весов соединений и смещений отдельных нейронов. Вес влияет на крутизну кривой функции активации (насколько быстро нейрон «включается» при изменении входных данных), а смещение сдвигает эту кривую вправо или влево, изменяя момент активации. Процесс обучения нейронной сети заключается именно в подборе оптимальных весов и смещений. Сеть получает входные данные, анализирует ошибки своих предсказаний и постепенно корректирует эти параметры, добиваясь все более точных результатов. Функция активации Одним из ключевых элементов при создании нейронной сети является функция активации. Это математическая функция, которая определяет, будет ли нейрон активирован, то есть пропустит ли он сигнал дальше, или останется неактивным. По сути, функция активации выполняет роль фильтра, решающего, насколько сильно входной сигнал повлияет на выход нейрона. Однако ее задача не ограничивается простым решением «включить или выключить». Функция активации также преобразует входной сигнал особым образом, что позволяет нейронной сети работать с нелинейными зависимостями и обрабатывать более сложные данные. Существует множество разновидностей функций активации, каждая из которых подходит для своих типов задач. В примере простой нейронной сети, который рассматривается в этой главе, используется одна из самых распространенных — сигмоидная функция. Ее график имеет характерную S-образную форму, как показано на рисунке 2.3. ГЛАВА 2 Создание нейронной сети 451
Рисунок 2.3. Пример сигмоидной функции Как уже упоминалось ранее, каждый нейрон имеет параметр смещения. Если применить смещение со значением 1.0 к сигмоидной кривой, показанной на рисунке 2.3, график функции сдвинется вправо — точка (0, 0.5) переместится в положение (1, 0.5). Таким образом, смещение влияет на положение функции активации на графике, сдвигая ее влево или вправо. Это позволяет регулировать момент, при котором нейрон начинает активироваться, и тем самым точнее управлять его поведением в процессе обучения. Функция потерь Функция потерь — это последний важный элемент, который необходимо рассмотреть при создании нейронной сети. Ее основная задача — сравнивать полученные сетью результаты с ожидаемыми и показывать, насколько они близки к правильным. Иными словами, функция потерь измеряет, насколько хорошо обучена сеть на данный момент. Использование функции потерь помогает избежать переобучения — ситуации, когда нейронная сеть слишком точно запоминает обучающие данные и перестает правильно работать с новыми примерами. Помимо этого, результат функции потерь используется в процессе обратного распространения ошибки: именно на основе значения функции потерь сеть корректирует свои веса и смещения, улучшая качество работы. В нашей примерной нейронной сети будет применяться функция, которая вычисляет производную функции потерь, то есть показывает наклон кривой. Это значение помогает определить направление и величину изменения весов нейронов, чтобы сеть могла постепенно уменьшать ошибку и давать все более точные результаты. 452 РАЗДЕЛ 4 Использование искусственного интеллекта
Теперь, когда вы познакомились со всеми ключевыми элементами нейронной сети — слоями, весами, функцией активации и функцией потерь, — пришло время перейти к созданию собственного примера. Построение простой нейронной сети на Python Прежде чем создавать нейронную сеть, нужно определить, какую задачу она будет решать. В нашем примере цель проста: реализовать трехвходовой логический элемент XNOR (исключающее ИЛИ-НЕ) с входами X1, X2, X3 и выходом Y1. Элемент XNOR выполняет проверку логического равенства: его выход равен 1 тогда и только тогда, когда все входы одинаковы — либо все три равны 1, либо все три равны 0. Таблица 2.1 представляет функцию XNOR в виде таблицы истинности и задает пары «входы  желаемый выход», которые мы будем использовать как обучающие примеры для сети с рисунка 2.1. Для ясности отличим XNOR от XOR. Функция XOR (исключающее ИЛИ) возвращает 1, когда входы не все одинаковы — эквивалентно, когда число единиц на входе нечетно. Функция XNOR, напротив, возвращает 1, когда все входы совпадают. Таблица 2.1. Таблица истинности (для трехвходового XNOR-элемента) в нейронной сети X1 X2 X3 Y1 0 0 0 1 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 0 1 0 1 0 1 1 0 0 1 1 1 1 ГЛАВА 2 Создание нейронной сети 453
Программный код нейронной сети на Python Для построения нашей нейронной сети мы будем использовать библиотеку Python под названием NumPy. Она предоставляет удобные инструменты для работы с многомерными массивами данных и упрощает выполнение математических операций, необходимых для реализации нейронных сетей. Хотя NumPy изначально создавалась для работы с матричной математикой (матрица, напомним, также называется тензором) и используется в курсах линейной алгебры, она включает множество готовых математических функций высокого уровня, которые делают ее особенно полезной при создании и обучении моделей искусственного интеллекта. В настоящее время NumPy считается стандартной библиотекой для выполнения матричных операций в Python, то есть основой для большинства приложений в области ИИ. Она также является частью более крупных научных пакетов — SciPy и Matplotlib, которые используются для анализа и визуализации данных. ПРИМЕНЕНИЕ ФУНКЦИЙ «ИСКЛЮЧАЮЩЕЕ ИЛИ-НЕ-» Логические элементы XNOR находят широкое применение как в программных, так и в аппаратных системах . Их можно использовать, например, в одноразрядных сумматорах, которые складывают два бита и формируют бит переноса . Такие сумматоры можно объединять, создавая многобитные арифметические устройства . Кроме того, элементы XNOR применяются при создании генераторов псевдослучайных чисел и в системах цифрового кодирования . Особенно интересным примером использования XNOR является применение в алгоритмах кодирования и коррекции ошибок, таких как алгоритм Рида — Соломона . Этот метод использует комбинации элементов XNOR и дополнительных (избыточных) данных для повышения надежности передачи информации на большие расстояния . При передаче сигналов могут возникать помехи, которые искажают отдельные биты или байты данных . Код Рида — Соломона позволяет исправлять такие ошибки, восстанавливая исходные данные с высокой точностью . Когда данные принимаются, элементы XNOR снова используются для их декодирования и исправления ошибок (в пределах допустимого диапазона), что позволяет получать корректную информацию . 454 РАЗДЕЛ 4 Использование искусственного интеллекта
Благодаря этому коду передача данных может осуществляться на гораздо большие расстояния с меньшей мощностью сигнала, сохраняя высокую устойчивость к помехам . Джон вспоминает, что в 1980-х годах он работал в Университете Айдахо с командой, разрабатывавшей микросхемы для реализации кодов Рида — Соломона по заказу NASA . Эти микросхемы и их последующие версии применялись в ряде знаковых проектов, включая телескоп «Хаббл» и космический зонд New Horizons, который отправился к Плутону и далее, к объекту Ultima Thule в облаке Оорта . Удивительно осознавать, что потрясающие снимки, переданные с этих аппаратов, прошли через миллионы крошечных логических элементов XNOR . Ниже приведен пример на Python с использованием NumPy для простой двухслойной нейронной сети (входной слой  скрытый слой  выходной слой). Создайте файл 2LayerNeuralNetwork.py и вставьте код: # Двухслойная нейронная сеть на NumPy import numpy as np # X = входные данные для нашего трехвходового элемента XOR # настраиваем входы нейронной сети (прямо из таблицы) X = np.array(([0,0,0], [0,0,1], [0,1,0], \ [0,1,1], [1,0,0], [1,0,1], [1,1,0], [1,1,1]), dtype=float) # y = выход нашей нейронной сети y = np.array(([1], [0], [0], [0], [0], \ [0], [0], [1]), dtype=float) # значение, которое мы хотим предсказать xPredicted = np.array(([0,0,1]), dtype=float) X = X / np.amax(X, axis=0) # максимум входного массива X # максимум xPredicted (наших входных данных для прогнозирования) xPredicted = xPredicted / np.amax(xPredicted, axis=0) # создаем файл для записи потерь (для построения графика) lossFile = open("SumSquaredLossList.csv", "w") class Neural_Network (object): def __init__(self): # параметры self.inputLayerSize = 3 self.outputLayerSize = 1 self.hiddenLayerSize = 4 # формируем веса каждого # X1,X2,X3 # Y1 # Размер скрытого слоя слоя ГЛАВА 2 Создание нейронной сети 455
# устанавливаем случайные значения # посмотрите на схему соединений, чтобы понять логику # матрица 3x4 для входного слоя к скрытому self.W1 = \ np.random.randn(self.inputLayerSize, self. hiddenLayerSize) # матрица 4x1 для скрытого слоя к выходному self.W2 = \ np.random.randn(self.hiddenLayerSize, self. outputLayerSize) def feedForward(self, X): # прямое распространение через нашу сеть # скалярное произведение X (входных данных) и первого набора весов 3x4 self.z = np.dot(X, self.W1) # сигмоидная функция активации - нейронная магия self.z2 = self.activationSigmoid(self.z) # скалярное произведение скрытого слоя (z2) и второго набора весов 4x1 self.z3 = np.dot(self.z2, self.W2) # финальная функция активации - еще немного нейронной магии o = self.activationSigmoid(self.z3) return o def backwardPropagate(self, X, y, o): # обратное распространение через сеть # вычисляем ошибку на выходе self.o_error = y - o # применяем производную сигмоидной функции к ошибке self.o_delta = self.o_error*self. activationSigmoidPrime(o) # ошибка z2: насколько веса скрытого слоя повлияли на выходную ошибку self.z2_error = self.o_delta.dot(self.W2.T) # применяем производную сигмоидной функции к ошибке z2 self.z2_delta = self.z2_error * self. activationSigmoidPrime(self.z2) # корректировка весов первого набора (входной слой --> 456 РАЗДЕЛ 4 Использование искусственного интеллекта
скрытый слой) self.W1 += X.T.dot(self.z2_delta) # корректировка весов второго набора (скрытый слой --> выходной слой) self.W2 += self.z2.T.dot(self.o_delta) def trainNetwork(self, X, y): # прямое распространение в цикле o = self.feedForward(X) # а затем обратное распространение значений (обратная связь) self.backwardPropagate(X, y, o) def activationSigmoid(self, s): # функция активации # простая сигмоидная кривая, как в книге "Питон для чайников" return 1 / (1 + np.exp(-s)) def activationSigmoidPrime(self, s): # Первая производная сигмоидной функции # время для математического анализа! return s * (1 - s) def saveSumSquaredLossList(self,i,error): lossFile.write(str(i) + "," + str(error.tolist()) + '\n') def saveWeights(self): # сохраняем веса, чтобы воспроизвести нашу крутую сеть np.savetxt("weightsLayer1.txt", self.W1, fmt="%s") np.savetxt("weightsLayer2.txt", self.W2, fmt="%s") def predictOutput(self): print ("Predicted XOR output data based on trained weights:") print ("Expected (X1.X3): \n" + str(xPredicted)) print ("Output (Y1): \n" + str(self. feedForward(xPredicted))) myNeuralNetwork = Neural_Network() trainingEpochs = 1000 #trainingEpochs = 100000 for i in range(trainingEpochs): # тренируем нейросеть 1000 раз print ("Epoch # " + str(i) + "\n") ГЛАВА 2 Создание нейронной сети 457
print ("Network Input: \n" + str(X)) print ("Expected Output of XOR Gate Neural Network: \n" + str(y)) print ("Actual Output from XOR Gate Neural Network: \n" + \ str(myNeuralNetwork.feedForward(X))) # среднеквадратичная ошибка Loss = np.mean(np.square(y - myNeuralNetwork.feedForward(X))) myNeuralNetwork.saveSumSquaredLossList(i,Loss) print ("Sum Squared Loss: \n" + str(Loss)) print ("\n") myNeuralNetwork.trainNetwork(X, y) myNeuralNetwork.saveWeights() myNeuralNetwork.predictOutput() Разбор кода Некоторые части этого кода могут показаться непонятными при первом знакомстве, поэтому давайте разберем их подробнее. # Двухслойная нейронная сеть на NumPy import numpy as np Эта строка подключает библиотеку NumPy, которая используется для выполнения математических операций, особенно работы с многомерными массивами и матрицами. Без нее реализация нейронной сети была бы гораздо сложнее и занимала больше кода. Если при запуске программы вы получаете сообщение об ошибке, связанное с отсутствием библиотеки NumPy, ее нужно установить вручную. Чтобы установить NumPy на Raspberry Pi (или в системе Ubuntu), введите в терминале команду: sudo apt-get install python3.numpy Здесь мы задаем исходные данные, с которыми будет работать нейронная сеть, а также пример входа, для которого хотим получить предсказание. Для этого обращаемся к таблице 2.1: # X = входные данные для нашего 3.входового вентиля XNOR # настраиваем входы нейронной сети (прямо из таблицы) X = np.array(([0,0,0], [0,0,1], [0,1,0], \ [0,1,1], [1,0,0], [1,0,1], [1,1,0], [1,1,1]), dtype=float) # y = выходные данные нашей нейронной сети 458 РАЗДЕЛ 4 Использование искусственного интеллекта
y = np.array(([1], [0], [0], [0], [0], \ [0], [0], [1]), dtype=float)) Далее мы задаем пример входных данных, для которых сеть должна сделать предсказание: # значение, которое мы хотим предсказать xPredicted = np.array(([0,0,1]), dtype=float) X = X / np.amax(X, axis=0) # максимум входного массива X # максимум xPredicted (наши входные данные для прогнозирования) xPredicted = xPredicted / np.amax(xPredicted, axis=0) Сохраняем значения среднеквадратичной ошибки в файл, чтобы потом построить график, например, в Excel: # настраиваем файл для записи ошибок и построения графиков lossFile = open("SumSquaredLossList.csv", "w") Далее объявляем класс Neural_Network для нашей задачи. Он соответствует схеме на рисунке 2.2: входной слой, один скрытый слой и выходной слой. Размеры слоев задаются в конструкторе — по одной строке на слой: class Neural_Network (object): def __init__(self): # параметры self.inputLayerSize = 3 # X1,X2,X3 self.outputLayerSize = 1 # Y1 self.hiddenLayerSize = 4 # Размер скрытого слоя Теперь задаем начальные веса сети случайным образом. Это важно, потому что все веса должны иметь разные начальные значения — если они будут одинаковыми, нейроны в сети станут обучаться одинаково и сеть не сможет развиваться: # создаем веса для каждого слоя # устанавливаем случайные значения # посмотрите на схему соединений, чтобы понять логику # матрица 3x4 для входного слоя к скрытому self.W1 = \ np.random.randn(self.inputLayerSize, self. hiddenLayerSize) # матрица 4x1 для скрытого слоя к выходному self.W2 = \ np.random.randn(self.hiddenLayerSize, self. outputLayerSize) ГЛАВА 2 Создание нейронной сети 459
Функция rand() создает массив случайных чисел в диапазоне от 0 до 1. Такие значения часто используют для инициализации весов, чтобы задать нейронной сети начальные параметры перед обучением. Далее определим функцию feedforward(), которая реализует прямое распространение данных по сети — от входного слоя к выходному. Эта функция выполняет матричное умножение весов и входных значений, а затем применяет сигмоидную функцию активации, чтобы определить, активируется ли нейрон: def feedForward(self, X): # прямое распространение через нашу сеть # скалярное произведение X (входных данных) и первого набора весов 3x4 self.z = np.dot(X, self.W1) # сигмоидная функция активации - нейронная магия self.z2 = self.activationSigmoid(self.z) # скалярное произведение скрытого слоя (z2) и второго набора весов 4x1 self.z3 = np.dot(self.z2, self.W2) # финальная функция активации - еще немного нейронной магии o = self.activationSigmoid(self.z3) return o Добавим в класс функции активации и метод обратного распространения ошибки, который корректирует веса на основе градиентного спуска. Метод возвращает текущее значение ошибки, чтобы при желании записывать его в файл: def backwardPropagate(self, X, y, o): # обратное распространение через сеть # вычисляем ошибку на выходе self.o_error = y - o # применяем производную activationSigmoid к ошибке self.o_delta = self.o_error * self. activationSigmoidPrime(o) # ошибка z2: насколько веса скрытого слоя повлияли на ошибку # выхода self.z2_error = self.o_delta.dot(self.W2.T) # применяем производную activationSigmoid к ошибке z2 self.z2_delta = self.z2_error * self. activationSigmoidPrime(self.z2) # корректируем первый набор весов (входной слой --> скрытый слой) self.W1 += X.T.dot(self.z2_delta) 460 РАЗДЕЛ 4 Использование искусственного интеллекта
# корректируем второй набор весов (скрытый слой --> выходной слой) self.W2 += self.z2.T.dot(self.o_delta) Для обучения сети в течение одного периода, когда все обучающие данные один раз проходят через сеть, создается цикл, который многократно вызывает функции backwardPropagate() и feedforward(). Вот как это выглядит в коде: def trainNetwork(self, X, y): # прямое распространение в цикле o = self.feedForward(X) # а затем обратное распространение значений (обратная связь) self.backwardPropagate(X, y, o) После определения архитектуры сети добавляем сигмоидную функцию активации и ее первую производную. Эта функция определяет, насколько сильно активируется каждый нейрон в зависимости от входных данных. В данном примере смещение (bias) не задается явно — предполагается, что оно равно 0. Это упрощает код и позволяет сосредоточиться на принципе работы сети. Вот как эти функции выглядят в коде: def activationSigmoid(self, s): # функция активации # простая сигмоидная кривая активации, как в книге "Питон для чайников" return 1 / (1 + np.exp(-s)) def activationSigmoidPrime(self, s): # Первая производная от activationSigmoid # время для математического анализа! return s * (1 - s) После каждого цикла обучения полезно сохранять значение функции потерь, чтобы потом можно было визуализировать процесс обучения, например, построить график в Excel. Также стоит сохранить текущие веса сети, чтобы при необходимости использовать их позже без повторного обучения. def saveSumSquaredLossList(self,i,error): lossFile.write(str(i) + "," + str(error.tolist()) + '\n') ГЛАВА 2 Создание нейронной сети 461
def saveWeights(self): # сохраняем это, чтобы воспроизвести нашу классную сеть np.savetxt("weightsLayer1.txt", self.W1, fmt="%s") np.savetxt("weightsLayer2.txt", self.W2, fmt="%s") После завершения обучения можно запустить нейронную сеть, чтобы она предсказала выходные данные на основе уже обученных весов. Этот шаг показывает, насколько точно сеть усвоила закономерности из обучающих данных. Пример кода: def predictOutput(self): print ("Predicted XOR output data based on trained weights: ") print ("Expected (X1.X3): \n" + str(xPredicted)) print ("Output (Y1): \n" + str(self. feedForward(xPredicted))) myNeuralNetwork = Neural_Network() trainingEpochs = 1000 #trainingEpochs = 100000 Ниже приведен основной цикл обучения, который проходит через заданное количество периодов (эпох). Количество эпох задается переменной trainingEpochs — вы можете изменять ее значение, чтобы наблюдать, как это влияет на точность и скорость обучения сети: for i in range(trainingEpochs): # обучаем myNeuralNetwork 1000 раз print ("Epoch # " + str(i) + "\n") print ("Network Input: \n" + str(X)) print ("Expected Output of XOR Gate Neural Network: \n" + str(y)) print ("Actual Output from XOR Gate Neural Network: \n" + \ str(myNeuralNetwork.feedForward(X))) # средняя сумма квадратов ошибок Loss = np.mean(np.square(y - myNeuralNetwork. feedForward(X))) myNeuralNetwork.saveSumSquaredLossList(i,Loss) print ("Sum Squared Loss: \n" + str(Loss)) print ("\n") myNeuralNetwork.trainNetwork(X, y) 462 РАЗДЕЛ 4 Использование искусственного интеллекта
После завершения обучения можно сохранить результаты — обученные веса нейронной сети — чтобы повторно использовать их без необходимости заново проходить процесс обучения. Затем можно применить сеть для предсказания выходных значений по заданным входным данным: myNeuralNetwork.saveWeights() myNeuralNetwork.predictOutput() Запуск кода нейронной сети Теперь, когда весь код готов, можно запустить обучение нейронной сети и проследить, как она учится решать задачу. Откройте терминал (или окно командной строки) и перейдите в папку, где находится ваш файл 2LayerNeuralNetworkCode.py. Затем выполните команду: python3 2LayerNeuralNetworkCode.py Программа выполняет 1000 циклов обучения, печатает результаты каждого цикла, а затем выводит итоговые входные и выходные данные. По завершении создаются такие файлы: » weightsLayer1.txt — финальные обученные веса связей между входным и скрытым слоями (матрица 4×3) . » weightsLayer2.txt — финальные обученные веса связей между скрытым и выходным слоями (матрица 1×4) . » SumSquaredLossList.csv — файл с запятыми в качестве разделителей, содержащий номер цикла обучения и значение функции потерь в конце каждого цикла; используется для построения графика динамики обучения . Финальный вывод программы после последнего (999-го) цикла обучения выглядит примерно так: Epoch # Network [[0. 0. [0. 0. [0. 1. [0. 1. [1. 0. [1. 0. 999 Input: 0.] 1.] 0.] 1.] 0.] 1.] ГЛАВА 2 Создание нейронной сети 463
[1. 1. 0.] [1. 1. 1.]] Expected Output of XOR Gate Neural Network: [[1.] [0.] [0.] [0.] [0.] [0.] [0.] [1.]] Actual Output from XOR Gate Neural Network [[0.90512978] [0.04523854] [0.04389803] [0.01780811] [0.02403575] [0.04422157] [0.04438973] [0.94597526]] Sum Squared Loss: 0.0025891793785875906 Predicted XOR output data based on trained weights: Expected (X1.X3): [0. 0. 1.] Output (Y1): [0.04520511] Ожидаемый результат функции исключающее ИЛИ-НЕ (XNOR) равен 0. На графике видно, что фактический выход сети равен примерно 0.04422615, что довольно близко к нулю, хотя и не идеально точно. Это нормальное явление — небольшие отклонения возникают из-за случайной инициализации весов и особенностей вычислений с плавающей точкой. Если сравнить все ожидаемые и фактические значения, можно заметить, что сеть воспроизводит закономерность функции XNOR с высокой точностью. Каждый раз при запуске программы результаты немного отличаются, поскольку начальные веса нейронов задаются случайными числами, что приводит к небольшим различиям в процессе обучения. Цель обучения нейронной сети состоит не в том, чтобы получить абсолютно точные значения, а в достижении результатов в пределах допустимого отклонения от правильного ответа. Например, если считать, что любое значение выше 0.9 соответствует логической единице, а любое значение ниже 0.1 — логическому нулю, то сеть в нашем примере показала бы идеальные результаты. 464 РАЗДЕЛ 4 Использование искусственного интеллекта
Среднеквадратичная ошибка (Mean Squared Error, MSE) используется как показатель того, насколько сильно все выходы сети отклоняются от ожидаемых значений. Чем меньше это значение, тем точнее обучена сеть. На графике (рисунок 2.4) показано изменение среднеквадратичной ошибки по мере увеличения количества эпох обучения. Видно, что сеть быстро улучшает результаты в начале, а затем скорость снижения ошибки постепенно уменьшается, когда сеть приближается к оптимальным весам. Можно провести дополнительный эксперимент, увеличив количество эпох обучения, например, до 100 000. В этом случае сеть будет иметь больше возможностей для уточнения весов, но процесс обучения займет больше времени. Если вы решите попробовать это, рекомендуется закомментировать часть операторов print(), чтобы избежать чрезмерного вывода в консоль и ускорить выполнение программы. Рисунок 2.4. Функция потерь в процессе обучения Epoch # 99999 Network Input: [[0. 0. 0.] [0. 0. 1.] [0. 1. 0.] [0. 1. 1.] [1. 0. 0.] [1. 0. 1.] [1. 1. 0.] [1. 1. 1.]] Expected Output of XOR Gate Neural Network: [[1.] [0.] [0.] [0.] ГЛАВА 2 Создание нейронной сети 465
[0.] [0.] [0.] [1.]] Actual Output from XOR Gate Neural Network: [[0.99056677] [0.00395547] [0.00126813] [0.00501385] [0.00256704] [0.00347987] [0.0029181 ] [0.99727885]] Sum Squared Loss: 2.0749704741406824e-05 Predicted XOR output data based on trained weights: Expected (X1.X3): [0. 0. 1.] Output (Y1): [0.00395545] Численные показатели стали лучше, но и при 1000 эпох результаты уже соответствовали нашим критериям точности: значения выше 0.9 считаем единицей, ниже 0.1 — нулем. Этого количества эпох достаточно для решения поставленной задачи. Увеличение обучения до 100 000 эпох дает лишь незначительное улучшение и практически не влияет на итоговую пригодность модели. Использование TensorFlow для той же нейронной сети TensorFlow — это библиотека Python, предназначенная для работы с нейронными сетями на основе матриц и графов потоков данных, похожая по своей структуре на NumPy. Однако, в отличие от NumPy, TensorFlow специально создан для задач машинного обучения и искусственного интеллекта, поэтому содержит множество специализированных функций и инструментов, упрощающих создание, обучение и исследование моделей. Тензор в TensorFlow — это многомерная матрица данных, которая преобразуется при прохождении через каждый слой сети. TensorFlow хорошо интегрируется с Python и может работать на различных устройствах — от персональных компьютеров до серверов и облачных платформ. Благодаря этому TensorFlow можно использовать для самых разных проектов, от учебных примеров до промышленных решений. 466 РАЗДЕЛ 4 Использование искусственного интеллекта
Название TensorFlow отражает сам принцип работы библиотеки: данные (в виде тензоров) «текут» через граф вычислений, проходя преобразования в каждом узле. Поскольку нейронные сети активно используют матричные операции, тензоры в TensorFlow лежат в основе всех вычислений. TensorFlow — один из самых поддерживаемых фреймворков в мире. Он имеет интерфейсы (API) для таких языков, как Python, C++, Haskell, Java, Go, Rust, а также сторонний пакет для языка R. Благодаря этому его можно применять в широком спектре задач и интегрировать с другими системами и технологиями. Установка библиотеки TensorFlow для Python Для установки библиотеки TensorFlow в Python можно воспользоваться стандартным способом через менеджер пакетов pip. Подробные инструкции для разных операционных систем приведены на официальном сайте TensorFlow: www.tensorflow.org/install/pip. Там подробно описан процесс установки для Windows, Linux и Raspberry Pi, а также возможные варианты настройки. Кроме того, для изучения и экспериментов с TensorFlow вы можете использовать Google Colab или Jupyter Notebook — это удобные интерактивные среды, позволяющие запускать код без необходимости установки TensorFlow на свой компьютер. TensorFlow можно запускать на множестве различных типов устройств. В этих главах рассматривается пример с Raspberry Pi 4B, оснащенным 8 ГБ оперативной памяти. ЗНАКОМСТВО С ТЕНЗОРАМИ Как уже упоминалось в главе 1 данного раздела, тензоры — это многомерные матрицы данных . Однако полезно рассмотреть этот термин подробнее и разобраться в его разновидностях . • Скаляр — это единичный элемент данных, представляющий одно значение . Примерами скаляров могут быть 5 метров, 5 метров в секунду, 45 градусов по Фаренгейту или 21 градус по Цельсию . Скаляр можно представить как точку на числовой оси . ГЛАВА 2 Создание нейронной сети 467
• Вектор отличается от скаляра тем, что содержит несколько элементов данных . Например, выражение «5 метров на восток» включает не только величину (5 метров), но и направление (на восток) . Вектор можно рассматривать как одномерную матрицу (например, размером 2×1) или как стрелку, направленную в определенную сторону на плоскости . Вектор размером 3×1 задает координаты в трехмерном пространстве: x, y и z . Таким образом, векторы являются простейшей формой тензоров . • Тензор — это более общий объект, который описывает данные с величинами и направлениями в нескольких измерениях . Если скаляры — это отдельные числа, а векторы — упорядоченные наборы чисел, то тензоры представляют собой многомерные массивы чисел, которые могут включать два, три и более измерений . Вектор можно считать частным случаем тензора, имеющего только одно измерение . Используйте качественную SD-карту объемом 32 ГБ. Имя пользователя по умолчанию — «pi», пароль по умолчанию — «3.14». TensorFlow — это стандартный модуль и программный интерфейс приложения (API) для использования с Python 3. При установке TensorFlow автоматически устанавливается множество зависимостей, то есть программ, от которых сам TensorFlow зависит для своей работы. Построение нейронной сети на Python с использованием TensorFlow Для примера построения нейронной сети с использованием TensorFlow мы возьмем модель, немного более сложную, чем двухслойная сеть на NumPy (см. рисунок 2.1), реализующую операцию, исключающую ИЛИ-НЕ (XNOR). На рисунке 2.5 представлена новая трехслойная нейронная сеть. Увеличение глубины сети позволяет достичь более точных результатов по сравнению с предыдущим примером. Таблицу истинности для обеих сетей см. в таблице 2.1. Таблица 2.1 содержит логическую таблицу, общую для обеих сетей, и показывает, какие выходные значения соответствуют каждому набору входных данных. 468 РАЗДЕЛ 4 Использование искусственного интеллекта
Рисунок 2.5. Наша трехслойная нейронная сеть TensorFlow Keras — это библиотека нейронных сетей с открытым исходным кодом, предназначенная для быстрого и удобного экспериментирования с нейронными сетями, глубоким обучением и методами машинного обучения. Она является неотъемлемой частью TensorFlow. В 2017 году компания Google сделала Keras основным и официально поддерживаемым интерфейсом для работы с TensorFlow. Keras предоставляет простой и интуитивно понятный набор инструментов и абстракций для создания моделей, тогда как TensorFlow отвечает за эффективную реализацию вычислений. В основе TensorFlow лежат матричные операции, которые реализуются с помощью библиотеки NumPy. Создание и обучение нейронной сети с использованием Keras и TensorFlow обычно включает пять основных шагов: 1. Загрузка и подготовка данных . 2. Определение архитектуры модели и ее слоев . 3. Компиляция модели . 4. Настройка параметров и обучение модели . 5. Оценка результатов работы модели . ГЛАВА 2 Создание нейронной сети 469
Загрузка данных Первый шаг — загрузка данных — в нашем примере прост, но в реальных проектах часто становится самой трудной частью. Необходимо изучить доступные данные (будь то таблица истинности для XOR или база факторов риска сердечных заболеваний у диабетиков) и решить, как связать входы с целевыми результатами, чтобы модель могла извлекать нужную информацию и строить корректные прогнозы. Определение модели нейронной сети и ее слоев На втором этапе, при определении модели, можно увидеть одно из главных преимуществ Keras перед другими фреймворками. Здесь вы просто создаете последовательность слоев, через которые проходят данные. Такой подход делает процесс построения нейронной сети интуитивно понятным и гибким. Помните, что TensorFlow работает с матрицами данных, которые последовательно проходят через каждый слой нейронной сети. На этом этапе вы задаете конфигурацию слоев — количество нейронов, типы слоев, функции активации и другие параметры, определяющие поведение сети и то, как она будет обрабатывать входные данные. Компиляция модели На третьем этапе выполняется компиляция модели, то есть связывание определенной вами архитектуры нейронной сети Keras с базовым программным обеспечением, которое обеспечивает выполнение вычислений на вашем оборудовании. Этот слой взаимодействия называется бэкендом. Во время компиляции также выбирается функция потерь — она определяет, как сеть будет оценивать различие между предсказанными и фактическими результатами. Кроме того, на этом этапе можно указать оптимизатор и метрики, которые помогут контролировать процесс обучения и оценивать качество модели. 470 РАЗДЕЛ 4 Использование искусственного интеллекта
Настройка и обучение модели На четвертом этапе происходит основная работа по обучению нейронной сети. Здесь вы задаете количество эпох, то есть сколько раз программа должна пройти через все обучающие данные. В процессе обучения сеть постепенно корректирует свои веса, уменьшая ошибку между предсказанными и реальными значениями. Кроме того, в ходе обучения можно сохранять историю изменений параметров и метрик, таких как функция потерь или точность. Эти данные впоследствии используют для построения графиков, которые помогают визуально оценить процесс обучения и понять, насколько эффективно сеть усваивает информацию. Оценка модели После завершения обучения модели необходимо провести ее оценку. Оценка заключается в том, чтобы протестировать обученную модель на новых данных и проверить, насколько хорошо она справляется с примерами, которые не использовались во время обучения. Это помогает определить, действительно ли модель способна делать точные предсказания, а не просто запомнила обучающие данные. В нашем случае этого шага выполнять не нужно, потому что все возможные комбинации входных значений (три бита) уже входят в обучающий набор. Ниже приведен пример кода на Python, использующего TensorFlow, NumPy и Keras для создания двухслойной нейронной сети (см. рисунок 2.1). Сначала мы построим простую двухслойную сеть, а затем в следующем примере добавим третий слой, чтобы улучшить ее работу. Откройте nano (или другой текстовый редактор) и создайте новый файл с именем TensorFlowKeras.py. Введите в него код, приведенный в следующем разделе: import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import tensorflow as tf from tensorflow.keras import layers from tensorflow.keras.layers import Activation, Dense import numpy as np # X = входы трехвходового XOR-вентиля (исключающее ИЛИ) # настраиваем входы нейронной сети (прямо из таблицы) ГЛАВА 2 Создание нейронной сети 471
X = np.array(([0,0,0], [0,0,1], [0,1,0], [0,1,1], [1,0,0], [1,0,1], [1,1,0], [1,1,1]), dtype=float) # y = выходные данные нашей нейронной сети y = np.array(([1], [0], [0], [0], [0], [0], [0], [1]), dtype=float) model = tf.keras.Sequential() model.add(Dense(4, input_dim=3, activation='relu', use_bias=True)) #model.add(Dense(4, activation='relu', use_bias=True)) model.add(Dense(1, activation='sigmoid', use_bias=True)) model.compile(loss='mean_squared_error', optimizer='adam', metrics=['binary_accuracy']) print (model.get_weights()) history = model.fit(X, y, epochs=2000, validation_data = (X, y)) model.summary() # вывод в файл loss_history = history.history["loss"] numpy_loss_history = np.array(loss_history) np.savetxt("loss_history.txt", numpy_loss_history, delimiter="\n") binary_accuracy_history = history.history["binary_accuracy"] numpy_binary_accuracy = np.array(binary_accuracy_history) np.savetxt("binary_accuracy.txt", numpy_binary_accuracy, delimiter="\n") print(np.mean(history.history["binary_accuracy"])) result = model.predict(X ).round() print (result) После того как вы ознакомились с кодом, можно запустить нейронную сеть и выполнить ее обучение. После завершения обучения следует провести оценку модели, чтобы проверить, насколько точно она выполняет поставленную задачу и насколько хорошо предсказывает результаты на основе входных данных. Такой анализ поможет понять, эффективно ли сеть усвоила закономерности и соответствует ли ее работа ожидаемым результатам. Разбор кода Первое, на что стоит обратить внимание в нашем коде с использованием TensorFlow и Keras, — это то, что он значительно проще и нагляднее, чем 472 РАЗДЕЛ 4 Использование искусственного интеллекта
двухслойная модель, которую вы создавали ранее в этой главе с помощью Python и библиотеки NumPy. В коде присутствует оператор os.environ, который устраняет проблему несовместимости с библиотекой protobuf, возникающую в некоторых версиях TensorFlow. Библиотека protobuf используется для сериализации типизированных структурированных данных, лежащих в основе многих внутренних механизмов TensorFlow. Иногда несовместимости между версиями этой библиотеки могут создавать трудности при установке TensorFlow на отдельных системах. Для вашего компьютера этот шаг может оказаться необязательным — все зависит от способа установки TensorFlow. После этого выполняется импорт всех необходимых библиотек для запуска примера двухслойной модели. Обратите внимание, что TensorFlow уже содержит Keras по умолчанию, поэтому нет необходимости устанавливать ее отдельно. Здесь снова используется знакомая вам библиотека NumPy, которая остается предпочтительным инструментом для работы с матрицами и числовыми данными. import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import tensorflow as tf from tensorflow.keras import layers from tensorflow.keras.layers import Activation, Dense import numpy as np Шаг 1. Загрузка и форматирование данных В этом случае вы просто создаете таблицу истинности для логического элемента, исключающего ИЛИ, используя массивы NumPy. Такой подход прост и нагляден, но в реальных задачах процесс загрузки и подготовки данных может быть гораздо сложнее. Когда вы работаете с большими, разнородными и взаимосвязанными источниками данных, их форматирование и организация требуют гораздо больше усилий. # X = входы трехвходового XOR-вентиля (исключающее ИЛИ) # настраиваем входы нейронной сети (прямо из таблицы) X = np.array(([0,0,0], [0,0,1], [0,1,0], [0,1,1], [1,0,0], [1,0,1], [1,1,0], [1,1,1]), dtype=float) # y = выходные данные нашей нейронной сети y = np.array(([1], [0], [0], [0], [0], [0], [0], [1]), dtype=float) ГЛАВА 2 Создание нейронной сети 473
Шаг 2. Определение модели нейронной сети и ее слоев На этом этапе становятся очевидны преимущества использования Keras. Добавлять новые нейронные слои, изменять их размер или выбирать другие функции активации здесь намного проще. В этом примере используется функция активации ReLU — простая и универсальная линейная функция. Мы также применяем смещение, которое сдвигает кривую активации. В нашем случае для скрытых слоев используется ReLU, а для выходного слоя — уже знакомая сигмоидная функция. В отличие от предыдущей реализации на чистом Python, здесь смещение применяется автоматически. Функция линейной ректификации (ReLU) — это кусочно-линейная функция, которая возвращает входное значение, если оно положительно, и нуль — если отрицательно. Она широко применяется в нейронных сетях, так как часто обеспечивает более высокую производительность и ускоряет обучение по сравнению с другими функциями активации. Также обратите внимание на закомментированную строку model.add. В следующем примере, когда мы перейдем к трехслойной нейронной сети, эту строку нужно будет раскомментировать, чтобы добавить в модель дополнительный слой. model = tf.keras.Sequential() model.add(Dense(4, input_dim=3, activation='relu', use_bias=True)) #model.add(Dense(4, activation='relu', use_bias=True)) model.add(Dense(1, activation='sigmoid', use_bias=True)) Шаг 3. Компиляция модели Мы используем ту же функцию потерь, что и в нашей чистой Pythonреализации — mean_squared_error (среднеквадратичная ошибка). Новым является оптимизатор ADAM, который представляет собой метод стохастической оптимизации. Стохастическая оптимизация относится к набору методов минимизации или максимизации значения при наличии случайности в программе. Теперь перейдем к метрикам, которые мы хотим получить на выходе программы . binary_accuracy сравнивает выходные значения нашей сети с 1 или 0. Вы увидите значения, например, 0.75, что означает шесть правильных ответов из восьми (поскольку у нас есть восемь возможных выходов). binary_accuracy означает именно то, что можно предположить из названия: 474 РАЗДЕЛ 4 Использование искусственного интеллекта
model.compile(loss='mean_squared_error', optimizer='adam', metrics=['binary_accuracy']) После компиляции модели можно вывести начальные веса нейронной сети. По умолчанию при инициализации используется случайное распределение весов. Это означает, что при каждом новом запуске программы значения весов будут отличаться, а значит, и результаты обучения могут немного меняться. Если вы хотите, чтобы при каждом запуске результаты были одинаковыми, можно задать фиксированное начальное значение для генератора случайных чисел. В этом случае веса будут инициализироваться одинаково при каждом запуске. Кроме того, при необходимости можно изменить сам метод генерации весов, выбрав другой способ их инициализации, который лучше подходит для вашей задачи или архитектуры сети. print (model.get_weights()) Шаг 4. Настройка и обучение модели На этом этапе задается количество эпох, достаточное для того, чтобы в большинстве случаев достичь бинарной точности, близкой к 1.0. Здесь мы загружаем массивы NumPy с входными данными (X) и ожидаемыми выходными значениями (y). Параметр validation_data используется для сравнения выходных данных обученной сети после каждой эпохи. Он вычисляет значения val_acc и val_loss, которые показывают точность и потери модели на проверочных данных. Эти значения сохраняются в переменной history, чтобы можно было отслеживать, как изменялись результаты обучения от эпохи к эпохе: history = model.fit(X, y, epochs=2000, validation_data = (X, y)) Затем мы выводим сводку модели, чтобы убедиться, что она построена именно так, как мы ожидали. В сводке отображаются все слои сети, количество нейронов в каждом из них, типы слоев и общее число параметров, участвующих в обучении. Это помогает проверить, что структура модели определена правильно: ГЛАВА 2 Создание нейронной сети 475
model.summary() Далее мы выводим значения, сохраненные в переменной history. Эти данные показывают, как менялись значения функции потерь и точности на протяжении всех эпох обучения. Впоследствии мы используем их для построения графика, чтобы наглядно увидеть, как модель обучалась и насколько стабильно улучшались ее результаты.: # вывод в файл loss_history = history.history["loss"] numpy_loss_history = np.array(loss_history) np.savetxt("loss_history.txt", numpy_loss_history, delimiter="\n") binary_accuracy_history = history.history["binary_accuracy"] numpy_binary_accuracy = np.array(binary_accuracy_history) np.savetxt("binary_accuracy.txt", numpy_binary_accuracy, delimiter="\n") Шаг 5. Оценка модели Мы запускаем модель, чтобы получить предсказанные выходные значения для всех входных данных X. При этом используется функция round, которая преобразует результаты в 0 или 1. Это упрощает интерпретацию выходов и заменяет критерий, применявшийся в нашей предыдущей реализации на чистом Python, где значения меньше 0.1 считались равными 0, а больше 0.9 — равными 1. Кроме того, мы вычисляем среднее значение всех показателей binary_ accuracy за все эпохи обучения. Это значение не имеет практического значения для работы модели, но служит полезным показателем того, насколько быстро сеть достигла хорошей точности: чем ближе результат к 1.0, тем меньше эпох потребовалось для успешного обучения: print(np.mean(history.history["binary_accuracy"])) result = model.predict(X).round() print(result) Теперь можно перейти к результатам. На этом этапе мы анализируем, насколько хорошо обученная модель справляется с поставленной задачей и насколько точно она воспроизводит ожидаемые выходные значения. Результаты покажут, достигла ли сеть нужного уровня точности и насколько эффективно прошел процесс обучения. 476 РАЗДЕЛ 4 Использование искусственного интеллекта
Проверка результатов Чтобы запустить двухслойную модель, введите в панели терминала команду: python3 TensorFlowKeras.py После запуска программы вы сможете наблюдать процесс обучения — смену эпох и изменение показателей точности и функции потерь. Объем выводимой информации можно регулировать с помощью параметра Verbose в команде model.fit: значение Verbose=0 отключает вывод, Verbose=1 отображает краткий прогресс обучения, а Verbose=2 показывает результаты по каждой эпохе. После завершения обучения вы получите итоговые результаты работы модели: ... Epoch 1999/2000 1/1 [==============================] – 0s 115ms/step - loss: 0.0382 binary_accuracy: 1.0000 - val_loss: 0.0381 - val_binary_ accuracy: 1.0000 Эпоха 2000/2000 1/1 [==============================] – 0s 113ms/step - loss: 0.0381 binary_accuracy: 1.0000 - val_loss: 0.0381 - val_binary_ accuracy: 1.0000 Model: "sequential" Layer (type) Output Shape Param # ============================================================= dense (Dense) (None, 4) 16 dense_1 (Dense) (None, 1) 5 ============================================================= Total params: 21 Trainable params: 21 Non-trainable params: 0 _____________________________________________________________ 0.8128125 1/1 [==============================] - 0с 436мс/шаг [[1.] [0.] [0.] [0.] [0.] [0.] [0.] [1.]] ГЛАВА 2 Создание нейронной сети 477
К 2000-й эпохе модель достигает желаемой бинарной точности 1.0, а результаты, полученные при вызове функции model.predict, совпадают с логической таблицей, которую мы ожидали. На рисунке 2.6 показаны графики функции потерь и бинарной точности в зависимости от номера эпохи. Можно заметить, что кривая функции потерь в новой сети выглядит гораздо более плавной по сравнению с предыдущей (см. рисунок 2.4). Такая плавность объясняется использованием функции активации ReLU и оптимизатора ADAM. Алгоритм ADAM (адаптивная оценка момента) применяет стохастический подход, то есть использует случайные числа для поиска минимального значения функции потерь и ускорения обучения. Из-за этого при каждом запуске программы график обучения может немного отличаться, так как начальные веса нейронов задаются случайно. Если вы хотите получать одинаковые результаты при каждом запуске, можно установить фиксированное начальное значение для генератора случайных чисел. Это позволит воспроизводить одни и те же «случайные» значения и упростит процесс оптимизации модели. Рисунок 2.6. Результаты обучения двухслойной сети Когда мы создавали первую нейронную сеть в этой главе, мы подробно рассматривали процесс обратного распространения ошибки и отмечали, что он является основным механизмом обучения нейронных сетей. В Keras этот процесс выполняется автоматически, что значительно упрощает работу с моделями. 478 РАЗДЕЛ 4 Использование искусственного интеллекта
Если вы хотите изменить способ, которым Keras выполняет обратное распространение, это можно сделать, указав другой метод оптимизации в команде module.compile (в нашем случае использовался оптимизатор ADAM). Однако подобные изменения требуют более глубокого понимания внутренней работы алгоритмов и обычно не нужны на начальных этапах обучения. Когда вы запускаете обучение сети, Keras автоматически использует алгоритм обратного распространения ошибки и оптимизирует его согласно выбранным функциям — оптимизации и потерь, заданным при компиляции модели. Стоит отметить, что когда показатель бинарной точности достигает значения 1.00 (в нашем примере это произошло на эпохе 1556), сеть можно считать полностью обученной — она успешно воспроизводит все ожидаемые результаты. ПОЧЕМУ СТОИТ ИСПОЛЬЗОВАТЬ ГРАФИЧЕСКИЙ ИНТЕРФЕЙС ДЛЯ ЗАПУСКА TENSORFLOW При работе с нейронными сетями вы будете много времени проводить в текстовом редакторе, создавая и изменяя код своих моделей . Для простоты мы экспортировали данные в Excel, чтобы построить графики, приведенные в этой главе . Обычно достаточно использовать панель терминала, однако у работы в полноценной графической среде есть важное преимущество — возможность использовать TensorBoard . TensorBoard — это инструмент визуализации, входящий в состав TensorFlow . Он запускается прямо в браузере (например, Chrome или Firefox) и позволяет анализировать процесс обучения нейронных сетей . Достаточно указать TensorBoard путь к директории, где хранятся ваши проекты, и вы сможете в наглядной форме отслеживать изменения функции потерь, точности, структуры модели и другие параметры . Такой способ анализа делает работу с TensorFlow гораздо удобнее и эффективнее . ГЛАВА 2 Создание нейронной сети 479
Изменение на трехслойную нейронную сеть в TensorFlow и Keras В этом разделе мы добавим в нашу нейронную сеть еще один слой. Новая структура показана на рисунке 2.5, а результаты ее работы — на рисунке 2.7. Чтобы внести изменения, откройте файл TensorFlowKeras. py в вашем любимом текстовом редакторе. В код нужно будет внести лишь небольшое дополнение, которое превратит двухслойную модель в трехслойную: model.add(Dense(4, input_dim=3, activation='relu', use_bias=True)) #model.add(Dense(4, activation='relu', use_bias=True)) model.add(Dense(1, activation='sigmoid', use_bias=True)) Рисунок 2.7. Результаты обучения трехслойной сети Если убрать символ комментария перед строкой, отвечающей за добавление среднего слоя, мы получим трехслойную нейронную сеть, в которой каждый слой содержит по четыре нейрона. Средний слой, который не имеет прямой связи с входными или выходными данными, называется скрытым слоем. Настроить это очень просто — достаточно раскомментировать одну строку. После изменения код должен выглядеть следующим образом: model.add(Dense(4, input_dim=3, activation='relu', use_bias=True)) model.add(Dense(4, activation='relu', use_bias=True)) model.add(Dense(1, activation='sigmoid', use_bias=True)) 480 РАЗДЕЛ 4 Использование искусственного интеллекта
Когда вы запускаете программу, она начинает обучение уже трехслойной нейронной сети. После завершения процесса вы увидите результаты, которые будут выглядеть примерно так же, как в предыдущем примере, но с улучшенными показателями точности и более плавным поведением функции потерь: 8/8 [==============================] - 0s 2ms/step - loss: 0.0153 binary_accuracy: 1.0000 - val_loss: 0.0153 - val_binary_accuracy: 1.0000 Epoch 2000/2000 8/8 [==============================] - 0s 2ms/step - loss: 0.0153 binary_accuracy: 1.0000 - val_loss: 0.0153 - val_binary_accuracy: 1.0000 _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 4) 16 _________________________________________________________________ dense_1 (Dense) (None, 4) 20 _________________________________________________________________ dense_2 (Dense) (None, 1) 5 ================================================================= Total params: 41 Trainable params: 41 Non-trainable params: 0 _________________________________________________________________ 0.930375 [[1.] [0.] [0.] [0.] [0.] [0.] [0.] [1.]] Как видно из результатов, в нашей нейронной сети теперь три слоя. Возможность быстро и просто изменять структуру модели — одна из причин, по которым TensorFlow и Keras считаются такими мощными инструментами. Эти библиотеки позволяют легко экспериментировать с различными параметрами, пробовать разные функции активации и архитектуры, не переписывая программу с нуля. ГЛАВА 2 Создание нейронной сети 481
Трехслойная сеть достигла бинарной точности 1.00 примерно на эпохе 1170, что заметно быстрее, чем двухслойная сеть, которая вышла на тот же уровень на эпохе 1520. Кроме того, график функции потерь показывает более крутой наклон, что указывает на более эффективное обучение (см. рисунок 2.6). Из любопытства мы повторили эксперимент, увеличив количество нейронов в каждом скрытом слое до 100. Как и ожидалось, сеть достигла точности 1.00 значительно быстрее — уже на 65-й эпохе вместо 416-й. Попробуйте провести собственные эксперименты: измените количество слоев, нейронов или эпох и посмотрите, как это влияет на результаты. Теперь вы достаточно хорошо понимаете принципы работы нейронных сетей и основы машинного обучения. Пришло время применить эти знания на практике — вперед, тренировать нейроны!
В этой главе вы узнаете: » Основы машинного обучения » Как использовать TensorFlow для решения задач машинного обучения » Как работать с библиотекой Matplotlib Гл а в а 3 Практическое машинное обучение Ч то значит «учиться»? Одно из определений обучения — это процесс приобретения и усвоения новых знаний, а также понимание смысла изучаемого. Другое, более точное в контексте искусственного интеллекта, определение — это устойчивое изменение знаний или поведения в результате накопленного опыта. Именно второе определение лучше всего отражает текущее состояние дел в области искусственного интеллекта. Инженеры и исследователи создали алгоритмы и программы, которые способны анализировать данные и сигналы от различных датчиков, а затем применять полученные знания для решения новых задач. Однако стоит понимать, что машины не осознают то, чему они «научились». Они просто преобразуют входные данные в определенные выходные результаты, которые люди могут интерпретировать и использовать. Тем не менее, несмотря на отсутствие у машин понимания, методы машинного обучения позволяют решать весьма сложные и впечатляющие задачи. Возможно, эти технологии со временем станут основой для появления более продвинутых форм искусственного интеллекта. Что же значит для машины «научиться»? В рамках этой книги мы будем считать, что машина чему-то научилась, если она способна преобразовывать входные данные в полезные выходные результаты. Например, ГЛАВА 3 Практическое машинное обучение 483
если вы написали простую программу, которая складывает два числа, можно сказать, что вы научили ее выполнять определенную задачу — в данном случае, сложение. В этой главе мы сосредоточимся на машинном обучении — области, изучающей алгоритмы и статистические модели, которые постепенно улучшают свою работу при многократном выполнении конкретных задач. Если это напоминает вам эксперименты с нейронными сетями из предыдущей главы, то вы правы. Машинное обучение включает не только нейронные сети, но и другие мощные методы анализа данных и статистики. Чтобы скачать примеры кода, используемые в этой главе, посетите сайт www.dummies.com/go/pythonaiofd3e. Поиск решений там, где их нет Одна из главных задач машинного обучения и искусственного интеллекта в целом заключается в поиске наилучшего решения. Ключевое слово здесь — «наилучшее». Но как определить, что найденное решение действительно лучшее из возможных? Для этого необходимо заранее задать конкретные цели и стремиться к их достижению. При этом решение не всегда должно быть идеальным — иногда достаточно, чтобы оно было просто достаточно хорошим для поставленной задачи. Часто эту проблему сравнивают с попыткой найти самую высокую гору в туманную погоду. Вы поднимаетесь на вершину, которая кажется самой высокой, и утверждаете: «Я на вершине». На самом деле вы просто достигли самой высокой точки, которую могли увидеть. Но если ваша цель — подняться на высоту выше 1000 футов, и вы оказались на уровне 1250 футов, вы успешно достигли цели. Такая точка называется локальным максимумом — она удовлетворяет заданным условиям, но может не быть самой высокой из всех возможных. Именно поэтому постановка цели — один из ключевых этапов любого проекта машинного обучения. От того, насколько правильно вы определите цель и критерии успеха, зависит качество работы модели и ее способность решать задачу. В этой главе мы рассмотрим, как ставить цели и обучать модель на основе уже известных решений. Сначала вы научите машину находить закономерности в данных, а затем примените полученные знания для решения новых, похожих задач. 484 РАЗДЕЛ 4 Использование искусственного интеллекта
В машинном обучении выделяют три основных типа алгоритмов, каждый из которых решает разные задачи и требует разного подхода к обучающим данным. » Обучение с учителем — это наиболее распространенный и понятный тип машинного обучения . В этом случае у вас есть набор данных, который содержит как входные значения, так и ожидаемые выходные результаты . Такой набор называют обучающими данными . Алгоритм анализирует эти примеры, находит зависимости между входами и выходами и затем использует полученные знания для прогнозирования результатов на новых данных . Именно этот тип обучения мы рассмотрим в этой главе, поскольку он лучше всего подходит для начального знакомства с машинным обучением . » Обучение без учителя отличается тем, что в исходных данных содержатся только входные значения . Модель не знает, какой результат считается правильным, и самостоятельно ищет в данных закономерности, кластеры и структуры . Такой подход часто применяется в задачах кластеризации, анализа связей или сокращения размерности данных . Эти алгоритмы нередко преследуют более сложные статистические или математические цели, чем просто поиск «наивысшей горы» . » Обучение с подкреплением — это тип обучения, при котором программа взаимодействует со средой и учится на основе системы вознаграждений . Алгоритм выполняет действия, получает обратную связь в виде поощрения или наказания и накапливает опыт, чтобы со временем принимать все более оптимальные решения . Этот подход особенно полезен там, где точные модели заранее неизвестны . Обучение с подкреплением — одна из самых сложных и интересных областей машинного обучения, и, вероятно, именно в ней скрыт большой потенциал для будущего развития искусственного интеллекта . Соавтор Джон Шовик, например, проводил эксперименты с обучением с подкреплением в области компьютерного зрения для роботов . Теперь, когда вы понимаете основные подходы, мы перейдем к практической части и рассмотрим, как применить машинное обучение на Python. ГЛАВА 3 Практическое машинное обучение 485
Создание нейронной сети для распознавания типов одежды В этом разделе мы будем использовать TensorFlow и Keras вместе с открытой обучающей базой данных Fashion-MNIST (Модифицированный Национальный институт стандартов и технологий), чтобы создать примеры машинного обучения и проанализировать их результаты. Подробнее о работе с TensorFlow и Keras можно вспомнить в главе 2 этого раздела. База данных Fashion-MNIST содержит 60 000 изображений предметов одежды, разделенных на десять категорий. Каждое изображение представлено в формате 28×28 пикселей, а в каждой категории находится по 6 000 примеров. На рисунке 3.1 показаны примеры изображений из этой базы данных. Fashion-MNIST основана на оригинальной базе MNIST, которая использовалась для обучения моделей распознавания рукописных цифр. Однако вместо цифр здесь представлены предметы одежды — кроссовки, рубашки, платья, пальто и другие элементы гардероба. Все изображения выполнены в оттенках серого и имеют одинаковое разрешение 28×28 пикселей, что делает набор данных удобным для работы с нейронными сетями и алгоритмами машинного обучения. 486 » » » » 0: футболка/топ » » » » » » 4: пальто 1: брюки 2: пуловер 3: платье 5: сандалии 6: рубашка 7: кроссовки 8: сумка 9: ботильон РАЗДЕЛ 4 Использование искусственного интеллекта
Рисунок 3.1. Небольшая часть базы данных FashionMNIST Для работы с Keras мы будем использовать тот же пятиэтапный подход к построению многослойных нейронных сетей, который уже применяли в главе 2 этого раздела. Такой подход помогает структурировать процесс и шаг за шагом создать, обучить и оценить модель машинного обучения. 1. Загрузка и форматирование данных . 2. Определение модели нейронной сети и ее слоев . 3. Компиляция модели . 4. Настройка и обучение модели . 5. Оценка модели . Настройка программной среды Как и в предыдущих главах, основная работа здесь выполняется через командную строку, поскольку вам предстоит вводить код и запускать программы. Однако теперь мы будем визуализировать результаты машинного обучения — для этого понадобятся графические возможности системы. Мы будем использовать библиотеку Matplotlib для отображения ГЛАВА 3 Практическое машинное обучение 487
графиков и анализа работы программы, поэтому убедитесь, что графический интерфейс пользователя запущен. Если вы работаете на Raspberry Pi без подключенного монитора, у вас есть два варианта: • • подключить клавиатуру, мышь и монитор напрямую к устройству; или настроить VNC (виртуальный сетевой компьютер), чтобы использовать экран основного компьютера в качестве дисплея для Raspberry Pi . Подробные инструкции по настройке VNC и работе с графическим интерфейсом можно найти в многочисленных руководствах в интернете. Хорошим источником информации является официальный сайт www.raspberrypi.org, где описаны шаги по установке и настройке необходимого программного обеспечения. Вы также можете загрузить готовый образ SD-карты для Raspberry Pi 4B с операционной системой Bullseye и уже установленными нужными компонентами, перейдя на сайт www.dummies.com/go/pythonaiofd3e. В разделе Downloads доступен файл: 2023.09–23.PAIOSDCardV2.zip. В этой главе мы будем использовать VNC, чтобы работать с Raspberry Pi в безмониторном режиме. Это удобный способ удаленно управлять устройством, видеть его рабочий стол и выполнять команды, не подключая отдельный монитор. На рисунке 3.2 показан графический интерфейс, запущенный на Raspberry Pi. Хотя визуально это выглядит как обычная работа за компьютером, на самом деле интерфейс отображается через VNC-подключение — удаленно, с вашего основного компьютера. Это позволяет писать код, запускать программы и просматривать результаты прямо на экране, даже если сам Raspberry Pi находится в другой комнате или работает без внешнего дисплея. 488 РАЗДЕЛ 4 Использование искусственного интеллекта
Рисунок 3.2. Полноценный графический интерфейс на Raspberry Pi СОВЕТ Если на вашем компьютере отсутствуют какие-либо библиотеки Python, используемые в этом примере, вы можете легко найти инструкции по их установке в интернете. Каждая операционная система и конфигурация немного отличаются, поэтому важно искать именно для своей платформы. Например, если у вас не установлена библиотека seaborn, введите в поисковой системе запрос вроде «installing seaborn python library on [название вашей системы]». Для Raspberry Pi обычно используется команда: "sudo pip3 install seaborn". Иногда источники в сети дают более точные и адаптированные под вашу систему инструкции, чем универсальные советы. Забавно, но, когда я однажды решил спросить у ChatGPT, как установить seaborn, то получил довольно общий ответ, в котором даже присутствовало предупреждение о возможной неточности. ГЛАВА 3 Практическое машинное обучение 489
Получение данных из набора Fashion-MNIST Получить данные из набора Fashion-MNIST довольно просто, хотя при первом запуске программы загрузка может занять некоторое время. Данные автоматически скачиваются с удаленного сервера с помощью команд, которые вы увидите немного позже в коде. После первой загрузки файлы сохраняются локально на вашем компьютере, и при последующих запусках программы данные будут использоваться уже из этой локальной копии, что значительно ускорит процесс. Обучение сети Во время обучения нейронная сеть будет обрабатывать все 60 000 изображений из набора данных Fashion-MNIS T. Каждая из десяти категорий одежды представлена 6 000 примерами, которые используются для того, чтобы сеть могла «увидеть» разнообразие форм, контуров и оттенков каждого типа одежды. Тестирование сети После завершения обучения ваша нейронная сеть пройдет проверку на трех различных наборах данных, чтобы оценить ее способность распознавать образы и обобщать полученные знания. 490 » Во-первых, тестирование проводится на 10 000 изображениях из набора данных Fashion-MNIST, которые не использовались при обучении . Это позволяет понять, насколько хорошо сеть справляется с новыми, ранее невиданными примерами . » Во-вторых, вы протестируете сеть на конкретном выбранном изображении из набора Fashion-MNIST, чтобы наглядно увидеть процесс классификации и результат работы модели . » И наконец, третьим испытанием станет фотография женского платья, не входящая в исходный набор данных . Это проверит, насколько эффективно обученная сеть способна распознавать реальные изображения, отличающиеся от тех, на которых она обучалась . РАЗДЕЛ 4 Использование искусственного интеллекта
Первая версия программы проведет тестирование на 10 000 изображениях из набора данных Fashion-MNIST, которые не использовались при обучении. Эти изображения позволяют оценить, насколько хорошо обученная модель способна классифицировать новые, ранее невиданные примеры. Ниже приведен пример кода на Python, использующий библиотеки TensorFlow, NumPy и Keras для анализа и тестирования сети FashionMNIS T. Откройте текстовый редактор — например, nano или любой другой, который вам удобен, — создайте новый файл с именем FMTensorFlowStart.py и введите следующий код: #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image # Импорт Fashion MNIST fashion_mnist = tf.keras.datasets.fashion_mnist (train_images, train_labels), (test_images, test_labels) \ = fashion_mnist.load_data() class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] train_images = train_images / 255.0 test_images = test_images / 255.0 model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(input_shape=(28,28))) model.add(tf.keras.layers.Dense(128, activation='relu')) model.add(tf.keras.layers.Dense(10, activation='softmax')) model.compile(optimizer=tf.optimizers.Adam(), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(train_images, train_labels, epochs=5) # тестирование на 10 000 изображений test_loss, test_acc = model.evaluate(test_images, test_labels) print('10,000 image Test accuracy:', test_acc) ГЛАВА 3 Практическое машинное обучение 491
Разбор кода Если вы уже знакомы с примером программы на TensorFlow и Keras из главы 2, этот код покажется вам во многом похожим. Здесь мы снова используем тот же пятиэтапный подход к построению модели с помощью Keras. В начале программы используется оператор os.environ, который устраняет возможные проблемы совместимости с библиотекой protobuf в некоторых версиях TensorFlow. В зависимости от вашей системы этот шаг может оказаться необязательным. После этого подключаются все необходимые библиотеки для выполнения примера. TensorFlow по умолчанию включает Keras, поэтому его не нужно устанавливать отдельно. Как и прежде, библиотека NumPy используется для работы с матрицами и выполнения математических операций — она остается базовым инструментом для большинства задач в области искусственного интеллекта и машинного обучения. #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image Шаг 1. Загрузка и форматирование данных В отличие от примера из главы 2, где данные создавались вручную, здесь используется встроенная функция для загрузки готового набора данных. TensorFlow уже содержит модуль для работы с базой Fashion-MNIST, поэтому нет необходимости загружать или распаковывать файлы самостоятельно. С помощью оператора импорта from tensorflow.examples.tutorials.mnist программа получает доступ к встроенному модулю, который автоматически скачивает данные при первом запуске, а затем использует локальную копию. Код выглядит так: # Импорт Fashion MNIST fashion_mnist = tf.keras.datasets.fashion_mnist (train_images, train_labels), (test_images, test_labels) \ 492 РАЗДЕЛ 4 Использование искусственного интеллекта
= fashion_mnist.load_data() Далее мы даем описания десяти классам в данных Fashion_MNIST: class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] После того как классы названы, следующим шагом является нормализация данных изображений. Дело в том, что исходные пиксели в наборе Fashion-MNIST имеют значения от 0 до 255, где 0 — это черный, а 255 — белый цвет. Нейронные сети лучше обучаются, когда входные данные находятся в небольшом и ограниченном диапазоне — например, от 0.0 до 1.0. Поэтому каждое значение пикселя делится на 255, чтобы привести данные к нужному масштабу: train_images = train_images / 255.0 test_images = test_images / 255.0 Шаг 2. Определение нейронной сети и ее слоев На этом этапе мы определяем нейронную сеть и добавляем в нее несколько слоев. Keras делает этот процесс простым и интуитивно понятным. Мы используем функцию активации (relu) и применяем смещение к ней. Для финального выходного слоя используется функция активации softmax: model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(input_shape=(28,28))) model.add(tf.keras.layers.Dense(128, activation='relu')) model.add(tf.keras.layers.Dense(10, activation='softmax')) Шаг 3. Компиляция модели Функция потерь sparse_categorical_crossentropy используется в тех случаях, когда каждой категории данных присваивается отдельное целое число, как в нашем примере с элементами одежды. Оптимизатор ADAM, основанный на методе стохастической оптимизации, является хорошим выбором по умолчанию, так как он эффективно работает с задачами, в которых используется большой объем данных, множество параметров или оба этих фактора одновременно. model.compile(optimizer=tf.train.AdamOptimizer(), loss='sparse_categorical_crossentropy', metrics=['accuracy']) ГЛАВА 3 Практическое машинное обучение 493
ПРИМЕЧАНИЕ Функция потерь sparse_categorical_crossentropy вычисляет ошибку между предсказанными и реальными категориями по всему набору данных. Слово «categorical» означает, что в задаче участвует несколько категорий, а не две, как в бинарной классификации. Термин «sparse» указывает на то, что для обозначения каждого класса используется одно целое число — от 0 до 9 в нашем случае. Энтропия, в данном контексте, представляет собой меру беспорядка или неопределенности в распределении данных между категориями. Шаг 4. Настройка и обучение модели Мы выбрали пять эпох обучения, чтобы сократить время выполнения программы в наших примерах. Однако вы можете изменить это число, если хотите улучшить точность модели или поэкспериментировать с процессом обучения. На этом этапе мы загружаем массивы NumPy, содержащие входные данные нашей сети — изображения из обучающей выборки train_images, которые будут использоваться для обучения модели. model.fit(train_images, train_labels, epochs=5) Шаг 5. Оценка модели Функция model.evaluate() выполняет проверку обученной сети, сравнивая предсказанные значения с реальными результатами. В процессе она вычисляет показатели точности и ошибки — test_acc и test_loss, которые позволяют оценить, насколько хорошо сеть справилась с обработкой тестовых данных. Эти значения сохраняются в переменной history, чтобы их можно было проанализировать или использовать для построения графиков. # тестирование на 10 000 изображениях test_loss, test_acc = model.evaluate(test_images, test_ labels) print('10,000 image Test accuracy:', test_acc) Проверка результатов обучения и оценки Мы запустили программу на Raspberry Pi 3B+. На Raspberry Pi 4B выполнение заняло около 90 секунд, а на новом Raspberry Pi 5 этот процесс должен сократиться примерно до 30 секунд — весьма впечатляюще для устройства стоимостью около 50 долларов. Предупреждения о несовместимости кода и уведомления о возможном устаревании функций можно 494 РАЗДЕЛ 4 Использование искусственного интеллекта
не учитывать на данном этапе, они не влияют на работу программы. Ниже приведены результаты выполнения программы. Epoch 1/5 60000/60000 [==============================] - loss: 0.5009 - acc: 0.8244 Epoch 2/5 60000/60000 [==============================] - loss: 0.3751 - acc: 0.8652 Epoch 3/5 60000/60000 [==============================] - loss: 0.3359 - acc: 0.8767 Epoch 4/5 60000/60000 [==============================] - loss: 0.3124 - acc: 0.8839 Epoch 5/5 60000/60000 [==============================] - loss: 0.2960 - acc: 0.8915 10000/10000 [==============================] 10,000 image Test accuracy: 0.873 – 44s 726us/step – 42s 703us/step – 42s 703us/step – 42s 701us/step – 42s 703us/step – 4s 404us/step По сути, результаты тестирования показывают, что наша двухслойная нейронная сеть машинного обучения смогла правильно классифицировать 87% из 10 000 тестовых изображений. При увеличении числа эпох до 50 точность выросла лишь до 88,7%, то есть значительное увеличение времени обучения привело лишь к незначительному улучшению результата. Тестирование отдельного тестового изображения После того как вы проанализировали результаты обучения и оценили работу своей нейронной сети, полезно протестировать одно изображение из набора данных Fashion-MNIST, как показано на рисунке 3.3. Такой тест поможет лучше понять, насколько хорошо сеть распознает отдельные примеры. Для этого добавьте следующий фрагмент кода в конец вашей программы и выполните ее снова. #запуск тестового изображения из данных Fashion_MNIST img = test_images[15] img = (np.expand_dims(img, 0)) singlePrediction = model.predict(img,steps=1) print ("Prediction Output") print(singlePrediction) ГЛАВА 3 Практическое машинное обучение 495
print() NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print ("Our Network has concluded that the image number '15' – is a " +class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") Рисунок 3.3. Изображение 15 из тестовой базы данных FashionMNIST После пяти циклов обучения программа выдала следующий результат: Prediction Output [[1.2835168e-05 9.9964070e-01 6.2637120e-08 3.4126092e-04 4.4297972e-06 7.8450663e-10 6.2759432e-07 9.8717527e-12 1.2729484e-08 1.1002166e-09]] Our Network has concluded that the image number '15' is a Trouser 99% Confidence Level Отличный результат — сеть правильно распознала изображение. Однако стоит помнить, что общая точность тестовых данных составляет всего около 87%, поэтому данное изображение просто хорошо совпало с обученными параметрами сети. Процент уверенности может быть обманчив: иногда сеть уверена в неправильном ответе. Позже вы увидите пример, когда сеть ошибается при распознавании платья, но при этом демонстрирует высокую уверенность в своем решении. Добро пожаловать в увлекательный и непредсказуемый мир машинного обучения. 496 РАЗДЕЛ 4 Использование искусственного интеллекта
Тестирование на внешних изображениях Другой интересный способ проверить работу программы — использовать одно из собственных изображений. Мы сделали тестовую фотографию на iPhone, чтобы оценить, как обученная нейронная сеть справится с распознаванием нового изображения. На рисунке 3.4 показан пример такой фотографии. Рисунок 3.4. Неклассифицированное платье, висящее на стене Используя приложение Preview на Mac, мы уменьшили разрешение исходного изображения с 3024×3024 пикселей до 28×28 пикселей, как показано на рисунке 3.5. Конечно, при таком низком разрешении изображение становится размытым и теряет детали, но оно вполне сопоставимо по четкости с примерами из базы данных Fashion-MNIST, показанными на рисунке 3.1, только немного крупнее. Рисунок 3.5. Платье в разрешении 28x28 пикселей ГЛАВА 3 Практическое машинное обучение 497
Большая часть следующего кода предназначена для подготовки данных из исходного JPEG-файла к формату, который понимает TensorFlow. Используя этот пример, вы сможете без труда добавлять собственные изображения и проводить дополнительные эксперименты с обученной моделью: # чтение тестового изображения платья imageName = "Dress28x28.JPG" testImg = Image.open(imageName) testImg.load() data = np.asarray( testImg, dtype="float" ) data = tf.image.rgb_to_grayscale(data) data = data/255.0 data = tf.transpose(data, perm=[2,0,1]) singlePrediction = model.predict(data,steps=1) print ("Prediction Output") print(singlePrediction) print() NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print ("Our Network has concluded that the file '" +imageName+ "' – is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") Результаты, раунд 1 Результаты оказались далеки от ожидаемых. Мы поместили файл Dress28x28.JPG в ту же папку, что и программу, и запустили обучение на пять эпох. После завершения работы получили следующий результат: Prediction Output [[1.2717753e-06 1.3373902e-08 1.0487850e-06 3.3525557e-11 8.8031484e-09 7.1847245e-10 1.1177938e-04 8.8322977e-12 9.9988592e-01 3.2957085e-12]] Our Network has concluded that the file 'Dress28x28.JPG' is a Bag 99% Confidence Level Наша программа машинного обучения на основе нейронной сети, проанализировав 60 000 изображений, включая 6 000 фотографий платьев, 498 РАЗДЕЛ 4 Использование искусственного интеллекта
с 99% уверенностью определила, что файл Dress28x28.JPG — это сумка. Да, именно так — сумка. Обычно мы не склонны смеяться над результатами вычислений, но в этот раз сдержаться было невозможно. Мы решили увеличить количество эпох обучения до 50 и снова запустили программу. Вот результаты нового запуска: Prediction Output [[3.4407502e-33 0.0000000e+00 2.5598763e-33 0.0000000e+00 0.0000000e+00 0.0000000e+00 2.9322060e-17 0.0000000e+00 1.0000000e+00 1.5202169e-39]] Our Network has concluded that the file 'Dress28x28.JPG' is a Bag 100% Confidence Level Платье все еще определяется как сумка, но теперь программа абсолютно уверена в своем решении — на все 100%. Это хорошо демонстрирует одну из типичных проблем машинного обучения. Даже если модель полностью уверена в своем ответе, это не гарантирует, что ответ правильный. В нашем случае сеть на 100% уверена в том, что платье — это сумка, и при этом ошибается на все 100%. Почему это происходит? Вероятнее всего, текущая архитектура нейронной сети недостаточно сложна, чтобы различать тонкие визуальные различия между платьем и сумкой. Увеличение числа эпох обучения не улучшило точность, поскольку сама модель не имеет достаточной выразительности. Возможные причины включают размер сети, количество и качество данных, а также особенности изображения. Например, на тестовом фото могли повлиять освещение, угол съемки или фон. Наш технический редактор Род Стивенс предположил, что основной причиной может быть именно фон изображения. Все изображения из набора NIST имеют темный фон, тогда как на тестовом снимке фон светлый. Когда Род затемнил фон, результаты значительно улучшились: после пяти эпох обучения сеть оценивала вероятность того, что это платье или рубашка, примерно в 50% в (в зависимости от запуска), а после 50 эпох уверенность в том, что это платье, достигла 91%. Это впечатляющий прогресс и наглядный пример того, насколько важны контекст и условия данных для работы моделей машинного обучения. Эта ситуация подчеркивает один из ключевых принципов машинного обучения: качество модели напрямую зависит от качества обучающих данных. Если данные неполные, несбалансированные или недостаточно разнообразные, сеть не сможет хорошо обобщать и будет ошибаться в ситуациях, отличных от тех, на которых ее обучали. Хотите, чтобы модель корректно работала с изображениями на разных фонах? Значит, ГЛАВА 3 Практическое машинное обучение 499
нужно расширить набор обучающих данных, добавив примеры с различными вариантами освещения и фона. При этом необязательно делать новые снимки — достаточно искусственно изменить существующие изображения, например, отредактировать фон или немного трансформировать объекты. Такой подход называется аугментацией данных и часто используется для повышения устойчивости и точности моделей. После этого мы модифицировали нашу программу, добавив сверточные слои и превратив сеть в четырехуровневую модель. Одним из достоинств Keras и TensorFlow является возможность легко изменять архитектуру нейронной сети, экспериментируя с глубиной, количеством слоев и типами нейронов. Сверточная нейронная сеть (СНС) анализирует изображение постепенно, рассматривая его небольшими фрагментами — например, окнами размером 5×5 пикселей, которые последовательно перемещаются по всему изображению. Такой метод напоминает исследование объекта под микроскопом: вы видите отдельные детали, но постепенно складываете полное представление о всей картине. Однако за точность приходится платить скоростью. При запуске сверточной сети на Raspberry Pi время обработки одного периода обучения увеличилось с 10 секунд до примерно полутора часов, то есть почти в 540 раз. Это хороший пример того, насколько ресурсоемкими являются сверточные нейронные сети и почему для их работы обычно используются мощные графические процессоры (GPU) или облачные вычислительные платформы. Какие еще методы можно использовать, чтобы повысить точность этой программы машинного обучения? Один из наиболее распространенных подходов — аугментация данных, то есть увеличение объема обучающей выборки за счет искусственного изменения изображений: их можно поворачивать, сдвигать, отражать или изменять масштаб. Это помогает сети лучше распознавать объекты в разных условиях. Существуют и другие методы повышения точности, но они выходят за рамки данного введения в машинное обучение. Код модели СНС Следующий код имеет ту же общую структуру, что и предыдущая программа. Единственное существенное различие — добавлены новые слои сверточной нейронной сети (FMTensorFlow.py): #import libraries import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" 500 РАЗДЕЛ 4 Использование искусственного интеллекта
import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image # Import Fashion MNIST fashion_mnist = tf.keras.datasets.fashion_mnist (train_images, train_labels), (test_images, test_labels) \ = fashion_mnist.load_data() class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] train_images = train_images / 255.0 test_images = test_images / 255.0 # Prepare the training images train_images = train_images.reshape(train_images.shape[0], 28, 28, 1) # Prepare the test images test_images = test_images.reshape(test_images.shape[0], 28, 28, 1) model = tf.keras.Sequential() input_shape = (28, 28, 1) model.add(tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=input_shape)) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation='relu')) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2))) model.add(tf.keras.layers.Dropout(0.25)) model.add(tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation='relu')) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.Dropout(0.25)) ГЛАВА 3 Практическое машинное обучение 501
model.add(tf.keras.layers.Conv2D(128, kernel_size=(3, 3), activation='relu')) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2))) model.add(tf.keras.layers.Dropout(0.25)) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dense(512, activation='relu')) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.Dropout(0.5)) model.add(tf.keras.layers.Dense(128, activation='relu')) model.add(tf.keras.layers.BatchNormalization()) model.add(tf.keras.layers.Dropout(0.5)) model.add(tf.keras.layers.Dense(10, activation='softmax')) model.compile(optimizer=tf.optimizers.Adam(), loss='sparse_categorical_crossentropy', metrics=['accuracy']) model.fit(train_images, train_labels, epochs=5) # test with 10,000 images test_loss, test_acc = model.evaluate(test_images, test_labels) print('10,000 image Test accuracy:', test_acc) #run test image from Fashion_MNIST data img = test_images[15] img = (np.expand_dims(img,0)) singlePrediction = model.predict(img,steps=1) print ("Prediction Output") print(singlePrediction) print() NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print ("Our Network has concluded that the image number '15' is a " +class_names[NumberElement]) print (str(int(Element*100)) 502 + "% Confidence Level") РАЗДЕЛ 4 Использование искусственного интеллекта
Результаты, раунд 2 Запуск на Raspberry Pi 3B+ занял около семи часов, на Raspberry Pi 4B+ — примерно полтора часа, а на Raspberry Pi 5 — около сорока минут. Ниже приведены полученные результаты: 10,000 image Test accuracy: 0.8601 Prediction Output [[5.9128129e-06 9.9997270e-01 1.5681641e-06 8.1393973e06 1.5611777e-06 7.0504888e-07 5.5174642e-06 2.2484977e-07 3.0045830e-06 5.6888598e-07]] Our Network has concluded that the image number '15' is a Trouser Ключевой показатель здесь — точность тестирования на 10 000 изображениях, которая составила 86%, то есть немного ниже, чем у предыдущей, более простой модели машинного обучения с результатом 87%. Почему так произошло? Сначала я подумал, что снижение точности могло быть связано с переобучением на тренировочных данных. Такая ситуация возникает, когда сеть слишком хорошо запоминает обучающую выборку, но при этом теряет способность корректно распознавать новые данные. Этот пример еще раз показывает, что машинное обучение сочетает в себе как науку, так и искусство. Чтобы проверить свою гипотезу, я запустил дополнительный тест с 20 циклами обучения, который занял около четырех часов на Raspberry Pi 4B+. В результате точность повысилась до 93,5%, что подтвердило: при пяти циклах переобучения не происходило. СОВЕТ Выбор правильной нейронной сети машинного обучения для работы с вашими данными — одно из самых важных решений, которое вы принимаете при проектировании. Но не менее значимы понимание функций активации, настройка отсева и выбор функции потерь, поскольку все эти элементы напрямую влияют на производительность модели. Особенно слои отсева играют важную роль в борьбе с переобучением: они случайным образом "отключают" отдельные узлы сети, временно изменяя ее структуру и повышая обобщающую способность. Оптимизация всех этих параметров одновременно — задача непростая и требует как исследований, так и практического опыта. В каком-то смысле это действительно похоже на ракетостроение. Визуализация с помощью Matplotlib Теперь, когда мы работаем в среде с графическим интерфейсом, снова запустим базовый код и проанализируем результаты с помощью Matplotlib. В примерах используется Raspberry Pi, но подойдет и Mac, и ПК, и любая ГЛАВА 3 Практическое машинное обучение 503
другая система Linux. Если на вашем компьютере установлены TensorFlow, Matplotlib и Python, вы сможете выполнить те же эксперименты. Чтобы установить Matplotlib на Raspberry Pi, введите в терминале: pip3 install matplotlib. К выводу model.fit добавим переменную history, чтобы собирать данные обучения. Затем подключим команды Matplotlib для построения графиков потерь и точности по эпохам и для отображения двух отдельных тестовых изображений. Откройте nano (или другой редактор), создайте файл FashionMLPlot.py и введите следующий код: #import libraries import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image # Import Fashion MNIST fashion_mnist = tf.keras.datasets.fashion_mnist (train_images, train_labels), (test_images, test_labels) = \ fashion_mnist.load_data() class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] train_images = train_images / 255.0 test_images = test_images / 255.0 model = tf.keras.Sequential() model.add(tf.keras.layers.Flatten(input_shape=(28,28))) model.add(tf.keras.layers.Dense(128, activation='relu' )) model.add(tf.keras.layers.Dense(10, activation='softmax' )) model.compile(optimizer=tf.optimizers.Adam(), loss='sparse_categorical_crossentropy', 504 РАЗДЕЛ 4 Использование искусственного интеллекта
metrics=['accuracy']) history = model.fit(train_images, train_labels, epochs=2) # Get training and test loss histories training_loss = history.history['loss'] accuracy = history.history['accuracy'] # Create count of the number of epochs epoch_count = range(1, len(training_loss) + 1) # Visualize loss history plt.figure(0) plt.plot(epoch_count, training_loss, 'r--') plt.plot(epoch_count, accuracy, 'b--') plt.legend(['Training Loss', 'Accuracy']) plt.xlabel('Epoch') plt.ylabel('History') plt.show(block=False); plt.pause(0.001) test_loss, test_acc = model.evaluate(test_images, test_labels) #run test image from Fashion_MNIST data img = test_images[15] plt.figure(1) plt.imshow(img) plt.show(block=False) plt.pause(0.001) img = (np.expand_dims(img,0)) singlePrediction = model.predict(img,steps=1) print ("Prediction Output") print(singlePrediction) print() NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print ("Our Network has concluded that the image number '15' is a " +class_names[NumberElement]) ГЛАВА 3 Практическое машинное обучение 505
print (str(int(Element*100)) + "% Confidence Level") print('Test accuracy:', test_acc) # read test dress image imageName = "Dress28x28.JPG" testImg = Image.open(imageName) plt.figure(2) plt.imshow(testImg) plt.show(block=False) plt.pause(0.001) testImg.load() data = np.asarray( testImg, dtype="float" ) data = tf.image.rgb_to_grayscale(data) data = data/255.0 data = tf.transpose(data, perm=[2,0,1]) singlePrediction = model.predict(data,steps=1) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print(NumberElement) print(Element) print(singlePrediction) print ("Our Network has concluded that the file '"+imageName+ "' is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") plt.show() Результаты работы программы показаны на рисунке 3.6. В окне с названием «Figure 0» отображаются данные о точности для каждого из пяти циклов обучения модели машинного обучения. Хорошо видно, как точность постепенно увеличивается с каждой эпохой. В окне «Figure 1» показано тестовое изображение для первой проверки распознавания — программа определила брюки, что является правильным результатом. В окне «Figure 2» показано изображение платья, которое все еще ошибочно классифицируется как сумка. При распознавании платья на чер506 РАЗДЕЛ 4 Использование искусственного интеллекта
ном фоне результаты были немного лучше: примерно 44% уверенности, что это рубашка, после пяти циклов — 39%, что это футболка или топ, а после десяти циклов — снова 39% уверенности, что это рубашка. Результаты, конечно, не идеальные, но все же лучше, чем «сумка». Рисунок 3.6. Графический интерфейс Raspberry Pi с визуализацией через Matplotlib Изучаем машинное обучение глубже Вы уже создали собственную модель машинного обучения и нейронную сеть, поэкспериментировали с ними и убедились, насколько мощным инструментом может быть Python. Что делать дальше? Отличным продолжением станет изучение более специализированной литературы. Рекомендуем книги Джона Пола Мюллера и Луки Массарона «Machine Learning For Dummies, 2nd Edition» и «Artificial Intelligence For Dummies, 2nd Edition», которые дают понятное и системное введение в машинное обучение и искусственный интеллект. Также стоит обратить внимание на книгу Франсуа Шолле «Deep Learning with Python, 2nd Edition» (издательство Manning Publications), где подробно рассматриваются методы глубокого обучения с практическими примерами. В следующей главе вы узнаете, как использовать Python для решения других задач в области искусственного интеллекта.
В этой главе вы узнаете: » Какие существуют ограничения искусственного интеллекта на Raspberry Pi » Как использовать облачные сервисы для работы с AI » Как применять искусственный интеллект с использованием графических карт Гл а в а 4 Игра с искусственным интеллектом — на картах, микроконтроллерах и в облаке Е сли вы прочитали предыдущие три главы, то уже успели познакомиться с основами искусственного интеллекта — нейронными сетями и машинным обучением. Но ИИ охватывает гораздо более широкую область. Если бы у нас было больше места, мы могли бы рассмотреть, например, методы продвинутого поиска — не такие, как в Google, а те, что используются для анализа больших пространств задач и нахождения решений с помощью ИИ. Мы также могли бы поговорить об автономной робототехнике, которая частично рассматривается в седьмом разделе, хотя эта тема требует более глубокого изучения. В этой главе мы сосредоточимся на других способах работы с программами искусственного интеллекта, помимо использования Raspberry Pi. Помните, как выполнение пяти циклов обучения нашей большой нейронной сети заняло семь часов? Очевидно, что для более сложных экспериментов потребуется больше вычислительной мощности, чтобы обучать модели быстрее и эффективнее. Именно этому будет посвящена текущая глава. 508 РАЗДЕЛ 4 Использование искусственного интеллекта
Чтобы загрузить код для этой главы, перейдите на сайт www.dummies.com/ go/pythonaiofd3e, откройте раздел Downloads и найдите код для соответствующего раздела (в данном случае Book 4). Изучаем ограничения Raspberry Pi и ИИ Raspberry Pi — это недорогое, но полноценное вычислительное устройство, которое по своим возможностям может соперничать с настольными компьютерами начального уровня. В этой книге мы используем модель Raspberry Pi 4, обладающую следующими основными характеристиками: » Центральный процессор (ЦПУ): четырехъядерный 64-битный процессор Broadcom с тактовой частотой 1,8 ГГц . » » » Графический процессор (ГПУ): Broadcom Videocore-V I . » Хранение данных: используется SD-карта . Оперативная память: 8 ГБ SDRAM . Сетевые возможности: поддержка Gigabit Ethernet и Wi-Fi стандартов 802 .11b/g/n/ac . Эти характеристики действительно впечатляют, если учитывать, что стоимость Raspberry Pi составляет около 70 долларов. Однако для специализированных задач, связанных с искусственным интеллектом, этого все же недостаточно. У Raspberry Pi ограниченный объем оперативной памяти и не самый мощный графический процессор (GPU), что сказывается на производительности при работе с большими нейронными сетями и сложными моделями. На рисунке 4.1 показан процессор Raspberry Pi 3B+, который внешне очень похож на модель Raspberry Pi 4B. Новый Raspberry Pi 4B с 8 ГБ оперативной памяти стоит примерно 75 долларов. Он оснащен в восемь раз большим объемом памяти и примерно в два раза быстрее предыдущих моделей при выполнении большинства задач. Графический процессор Videocore-IV, установленный в Raspberry Pi, имеет четыре вычислительных ядра GPU и может использоваться для ускорения вычислений при выполнении приложений машинного обучения, хотя его возможностей явно недостаточно для серьезных ИИ-задач. ГЛАВА 4 Игра с искусственным интеллектом 509
Рисунок 4.1. Процессор Raspberry Pi 3B+ со встроенным Videocore-IV BROADCOM VIDEOCORE-IV НА RASPBERRY PI 4 Videocore-VI — это энергоэффективный мобильный графический процессор, который используется в Raspberry Pi 4 . Он представляет собой двумерный цифровой сигнальный процессор (DSP), состоящий из четырех графических ядер, называемых сегментами . Эти сегменты можно условно сравнить с вычислительными блоками графических процессоров от AMD или Nvidia, но их значительно меньше: современные GPU содержат сотни, а иногда и тысячи таких блоков (256, 512 и более), что обеспечивает им гораздо большую производительность, особенно в задачах искусственного интеллекта и глубокого обучения . Для сравнения, недавно исследовательский центр, в котором работает соавтор Джон, приобрел два новых ИИ-компьютера, каждый из которых оснащен 21 000 графических процессоров, то есть в сумме 42 000 GPU . На этом фоне четыре ядра Videocore-VI кажутся скромными . Процессор Videocore-VI изначально создавался для задач кодирования и декодирования видео, а не для вычислений, связанных с ИИ . Однако некоторым исследователям удалось использовать все четыре его секции для ускорения работы нейронных сетей на Raspberry Pi, добившись примерно трехкратного увеличения 510 РАЗДЕЛ 4 Использование искусственного интеллекта
производительности по сравнению с использованием только четырехъядерного центрального процессора . Главным ограничением в применении Videocore для задач искусственного интеллекта долгое время оставалась закрытость технической документации, инструментов разработки и спецификаций, которые предоставлялись только по соглашениям о неразглашении . Но теперь полная документация и исходный код графического стека Raspberry Pi 4B стали доступны под открытой BSD-лицензией . Это открывает новые возможности для разработчиков и исследователей, желающих использовать графический процессор Raspberry Pi в приложениях машинного обучения и ИИ . Существуют два фактора, которые позволяют Raspberry Pi оставаться актуальной платформой для экспериментов с искусственным интеллектом. Во-первых, к ней можно подключить внешний ускоритель ИИ через USB-порт, что значительно повышает производительность при работе с моделями машинного обучения. Во-вторых, Raspberry Pi можно использовать как управляющее устройство, которое взаимодействует с облачными вычислительными системами и специализированным аппаратным обеспечением для ИИ, выполняющим основную часть сложных вычислений. СОВЕТ Как вы, возможно, помните из главы 3 этого раздела, большая часть вычислительных ресурсов при создании системы искусственного интеллекта с машинным обучением расходуется на этап обучения модели. После завершения обучения для выполнения классификации нового, неизвестного изображения требуется значительно меньше вычислительной мощности. Поэтому нередко обучение проводят на мощных компьютерах или серверах, а затем готовую модель развертывают на менее производительных устройствах, таких как Raspberry Pi, где она используется в составе готового продукта на основе ИИ. Этот подход не всегда возможен — например, если требуется, чтобы программа продолжала обучаться в процессе работы, как в случае с обучением с подкреплением. Однако, когда он применим, он позволяет использовать сложные модели машинного обучения на доступных и недорогих устройствах. Такой способ реализации ИИ, при котором вычисления выполняются на периферийных устройствах, называется граничными вычислениями и становится все более распространенным. Выполнение анализа или обучения систем искусственного интеллекта на небольших устройствах, подключенных к сети, называется граничными вычислениями, или вычислениями на краю сети. Этот подход позволяет обрабатывать данные ближе к месту их получения, снижая нагрузку на центральные серверы и уменьшая задержки при передаче информации. ГЛАВА 4 Игра с искусственным интеллектом 511
Добавление аппаратного ИИ к Raspberry Pi Несколько компаний начали выпускать специализированные вычислительные стики для искусственного интеллекта — небольшие USB-донглы, которые содержат полноценные процессоры, а не только память. Такие устройства можно подключать к Raspberry Pi, существенно повышая его вычислительные возможности при работе с нейронными сетями и задачами машинного обучения. Как правило, производители предоставляют библиотеки Python, обеспечивающие поддержку TensorFlow и других фреймворков. Ниже приведены два из наиболее интересных и популярных вычислительных стиков: » Нейровычислительный стик Intel (NCS) подключается к USB-порту Raspberry Pi или другого компьютера и обеспечивает аппаратное ускорение анализа на основе глубокого обучения . Например, с его помощью можно использовать Raspberry Pi для сбора и передачи данных в облако Amazon, где выполняется анализ и классификация изображений . Однако использование NCS позволяет выполнять эти операции локально, без подключения к интернету . Стик обеспечивает скорость обработки примерно в 60 раз выше, чем стандартный Raspberry Pi, при этом стоит менее 100 долларов . На рисунке 4 .2 показан Intel Neural Compute Stick 2 . С помощью этого устройства можно выполнять задачи распознавания лиц, анализа текста, мониторинга и технического обслуживания . ОСТОРОЖНО Однако важно помнить, что NCS выполняет только анализ и логический вывод, но не обучение моделей — обучение по‑прежнему проводится на более мощных компьютерах. Устройство хорошо интегрируется с Keras и TensorFlow и может стать отличным ускорителем для уже готовых проектов, где этап обучения завершен. » 512 Ускоритель Google Edge TPU (Tensor Processing Unit) представляет собой устройство с разъемом USB Type-C, предназначенное для систем на базе Linux . Оно ускоряет выполнение задач анализа и вывода данных в моделях машинного обучения . TPU, как следует из названия, работает с тензорами — многомерными матрицами, которые используются при построении нейронных сетей . На рисунке 4 .3 изображен Google Edge TPU . РАЗДЕЛ 4 Использование искусственного интеллекта
ОСТОРОЖНО Подобно стику Intel, этот ускоритель не выполняет обучение моделей, а только обеспечивает их работу после обучения на более мощных системах . Такой подход относится к области граничных вычислений, когда обработка данных выполняется на устройствах, находящихся ближе к источникам информации . Рисунок 4.2. Нейровычислительный стик Intel Neural Compute Stick 2 Рисунок 4.3. Ускоритель Google Edge TPU В ближайшие годы ожидается стремительный рост специализированного оборудования, предназначенного для запуска моделей машинного обучения. Появятся самые разные архитектуры и решения от таких компаний, как Google, Intel, Nvidia, AMD, Qualcomm, а также множества небольших разработчиков по всему миру. Все они стремятся занять свое место в быстро развивающейся сфере ускорителей искусственного интеллекта. ГЛАВА 4 Игра с искусственным интеллектом 513
Исследуем способы использования ИИ в облаке Когда специалисты из технологической отрасли говорят об «облаке», они часто используют этот термин как модное слово, не вдаваясь в детали. Из-за этого понятие «облако» становится расплывчатым и вызывает путаницу — как у обычных пользователей, так и у опытных инженеров. На самом деле, когда говорят, что ваши данные или программы работают «в облаке», это просто означает, что они выполняются на удаленных серверах — мощных компьютерах, находящихся в центрах обработки данных. Иногда облако определяют как программное обеспечение или сервисы, работающие в интернете, а не на вашем локальном устройстве. Это частично верно, однако важно понимать, что само по себе ничего не работает «в интернете». Все данные и вычисления выполняются на конкретных машинах, подключенных к сети. Таким образом, осознание того, что «облачные» программы и сервисы на самом деле работают на физических серверах, помогает развеять иллюзию о том, что облако — это некое абстрактное место «где-то там». Это просто удобный способ доступа к удаленным вычислительным ресурсам и хранения данных через интернет. Если у вас есть два компьютера, соединенных в сеть, и один из них используется как сервер данных, то вы уже фактически обладаете собственным облаком. Некоторые компании, например Western Digital и Dell, предлагают решения, которые помогают пользователям создавать персональные облачные системы. Как отметил Билл Ваксмут, ранее работавший в Western Digital: «Облачные вычисления — это стремительно растущий рынок хранения данных. Мы можем использовать уже существующие накопители, изменить управляющее программное обеспечение и переупаковать то же оборудование, чтобы оно поддерживало облачные вычисления и хранение информации». Использование облака дает возможность получать доступ к сервисам и хранилищам, которые недоступны в локальной сети. Одним из ключевых преимуществ является гибкость: вы можете увеличивать или уменьшать объем используемых ресурсов в зависимости от текущих вычислительных задач. Для работы с облаком необходим доступ в интернет, но не всегда требуется постоянное соединение. Например, можно запустить облачный процесс, выйти из системы и позже вернуться к его результатам. Однако в некоторых приложениях, таких как беспилот- 514 РАЗДЕЛ 4 Использование искусственного интеллекта
ные автомобили, где требуется постоянный поток данных, зависимость от подключения к интернету становится ограничивающим фактором. Режим «запустил и забыл» особенно полезен для устройств интернета вещей, которым важно экономить энергию, например устройств на батарейном питании. Хороший пример — удаленная метеостанция, использующая дорогостоящее сотовое соединение. В таком случае устройство большую часть времени находится в спящем режиме и активируется только для передачи данных. Использование облака зависит от выбранного сервиса и поставщика. В контексте машинного обучения чаще всего применяется настройка Python на локальном компьютере, который вызывает облачные функции и приложения. Все крупные облачные провайдеры предоставляют примеры кода и готовые инструменты для интеграции. Отличными примерами облачных технологий для конечных пользователей являются Amazon Echo и Alexa. Эти устройства записывают вашу речь, сжимают данные, отправляют их в облачные сервисы Amazon (AWS), где происходит распознавание и обработка, после чего возвращают готовый ответ или выполняют команду — например, включают свет. Вся обработка данных выполняется именно в облаке. Если же интернет-соединение пропадает, устройства могут лишь сообщить: «У меня проблемы с подключением к интернету». Существует множество компаний, предоставляющих услуги облачных вычислений и хранилищ данных, однако на момент написания книги основными лидерами в области облачных решений для искусственного интеллекта считаются четыре провайдера. » Облако Google . » » » Облачные сервисы Amazon . Облако IBM . Microsoft Azure . Google Cloud Google Cloud, пожалуй, является облачным провайдером, который в наибольшей степени ориентирован на искусственный интеллект. В этой платформе пользователям доступны TPU — процессоры тензорной обработки, предназначенные для ускорения вычислений, связанных с обучением и выполнением моделей ИИ. Подобно описанному ранее Google ГЛАВА 4 Игра с искусственным интеллектом 515
TPU Stick, эти процессоры обеспечивают высокую производительность, но размещены в облаке и позволяют не только выполнять обученные модели, но и проводить их обучение. Многие возможности Google Cloud отражают ключевые сильные стороны компании, прежде всего связанные с поисковыми технологиями и обработкой данных. Например, Cloud Vision API способен распознавать объекты, логотипы и даже достопримечательности на изображениях. Один из проектов, разработанных студентами Университета Айдахо, — система «умного города» под названием ParkMyRide — использует камеру на солнечных батареях, подключенную к Raspberry Pi, чтобы фотографировать улицы и определять наличие свободных парковочных мест. Снимки отправляются в Google Cloud Vision API, где анализируются, после чего программа возвращает количество автомобилей и их расположение. Эти данные отображаются в мобильном приложении, помогая водителям быстро находить парковку. Кроме того, Google Cloud предлагает мощные инструменты для анализа видео, а также сервисы преобразования речи в текст и текста в речь, которые применяются, например, в устройствах Google Home. Подобно Amazon и Microsoft, Google активно использует собственные ИИ-технологии не только для развития своих продуктов, но и для создания коммерческих сервисов, доступных пользователям и компаниям по всему миру. Amazon Web Services Облачные сервисы Amazon (AWS) ориентированы на то, чтобы предоставить бизнесу накопленный компанией опыт в области потребительского искусственного интеллекта. Многие из решений AWS основаны на технологиях, используемых в популярных продуктах Amazon. Например, развитие голосового помощника Alexa напрямую способствует совершенствованию облачных ИИ-сервисов — миллионы пользователей ежедневно взаимодействуют с системой, фактически помогая обучать и улучшать ее алгоритмы. Amazon предлагает широкий спектр инструментов для работы с искусственным интеллектом и машинным обучением. Среди них — сервисы для обработки естественного языка и анализа текста, системы распознавания изображений, средства построения и обучения моделей машинного обучения, а также мощные аналитические инструменты. Эти сервисы позволяют компаниям автоматизировать рутинные процессы, анализировать большие объемы данных и создавать интеллектуальные приложения, не требующие собственной сложной инфраструктуры. 516 РАЗДЕЛ 4 Использование искусственного интеллекта
IBM Cloud Облачная платформа IBM в прошлом нередко подвергалась критике за сложность использования. Ранее пользователи сталкивались с множеством разрозненных опций и инструментов, из-за чего было трудно понять, с чего начать работу. Однако в последние годы IBM Cloud значительно изменилась и стала гораздо удобнее. Компания объединила три своих крупных направления — облачные сервисы IBM BlueMix, платформу SoftLayer и систему искусственного интеллекта Watson — под единым брендом Watson. Теперь пользователям доступно более 170 сервисов, и хотя процесс первоначальной настройки все еще может показаться сложным, он стал гораздо более логичным и единообразным. При этом IBM предоставляет пользователям высокий уровень контроля над своими приложениями. Watson Studio — это среда IBM для работы с машинным обучением, где можно создавать, обучать и тестировать модели искусственного интеллекта в едином интегрированном пространстве. Помимо этого, IBM предлагает обширные базы знаний с возможностью поиска и обладает одной из лучших систем управления для интернета вещей (IoT). Среди любопытных возможностей Watson Studio выделяется сервис Watson Personality Insights. Он анализирует письменный текст и пытается определить личностные черты, мотивы и ценности автора. Было бы забавно пропустить через этот анализ текст самой книги и посмотреть, каким видит нас Watson. Microsoft Azure Microsoft Azure ориентирована в первую очередь на разработчиков. Подобно Amazon и Google, компания строит свои решения в области искусственного интеллекта на основе уже существующих потребительских продуктов. Все инструменты и сервисы Azure в сфере ИИ разделены на три основные категории: » » » сервисы искусственного интеллекта; инструменты и фреймворки для ИИ; а также инфраструктура для поддержки ИИ-приложений . Azure поддерживает специализированные FPGA — программируемые логические интегральные схемы, которые можно перепрограммировать для решения разных задач без замены оборудования. Кроме того, Microsoft создала инфраструктуру, рассчитанную на использование ГЛАВА 4 Игра с искусственным интеллектом 517
множества типов ускорителей и является одним из крупнейших заказчиков нейросетевых чипов Intel. Платформа Microsoft Azure предоставляет широкий набор продуктов для машинного обучения, инструментов для интернета вещей и средств управления, а также обширные возможности работы с данными. Среди них — поддержка графических процессоров, специализированных кремниевых решений для ИИ, контейнерные сервисы, позволяющие переносить внутренние приложения в облако, и гибкие базы данных. За развитием Azure определенно стоит следить: Microsoft активно инвестирует в развитие своих облачных технологий, и многие эксперты ожидают от нее серьезных инноваций в ближайшие годы. Кстати, стоит помнить, что ChatGPT работает именно на платформе Microsoft Azure. Хотя история Microsoft показывает, что даже лидерские позиции не всегда гарантируют долгосрочное превосходство — примером может служить их упущенный рынок мобильных устройств. ИИ на графической карте Графические карты, подобные чипу Nvidia, показанному на рисунке 4.4, уже много лет являются важной частью персональных компьютеров. Пользователи стремятся приобрести самые современные и мощные видеокарты, чтобы улучшить производительность своих систем, особенно в играх. Хотя тактовая частота центрального процессора имеет значение, именно архитектура и мощность графической карты чаще всего оказывают решающее влияние на качество визуализации и общую производительность. Причина в том, что обработка графики высокого разрешения требует колоссальных вычислительных ресурсов. Для решения этой задачи разработчики создали специализированные вычислительные устройства — графические процессоры (GPU), предназначенные для параллельной обработки большого количества данных, характерной для графических вычислений. Со временем стало ясно, что те же принципы параллельной обработки отлично подходят и для машинного обучения. GPU способны выполнять огромное количество однотипных математических операций одновременно, что делает их идеальными для тренировки нейронных сетей. Именно это открыло путь к новому направлению — использованию графических карт в задачах искусственного интеллекта и развитию GPU, оптимизированных для вычислений в области ИИ. 518 РАЗДЕЛ 4 Использование искусственного интеллекта
Рисунок 4.4. 256-ядерный графический процессор Nvidia Компания Nvidia и другие производители начали выпускать графические карты, оснащенные несколькими GPU, что значительно повысило качество изображения и частоту кадров в современных играх. Это стало возможным благодаря архитектуре, ориентированной на параллельную обработку данных. При этом важно помнить, что графические алгоритмы основаны на работе со структурами данных, называемыми матрицами, или, в более общем виде, тензорами. Именно они проходят через вычислительные конвейеры графического процессора, обеспечивая быструю и эффективную обработку изображений. «Тензоры» и «матрицы»… знакомые термины, не правда ли? Они лежат в основе не только графической обработки, но и искусственного интеллекта. Алгоритмы машинного и глубокого обучения используют те же самые типы структур данных и аналогичные методы вычислений. Поэтому GPU оказались столь полезны для задач ИИ: их архитектура позволяет выполнять сложные матричные операции с высокой скоростью, что идеально подходит для обучения и работы нейронных сетей. Независимо от типа нейронной сети, все методы программирования и используемые модули основываются на выполнении большого количества статистических операций. Во время обучения сеть получает множество изображений или наборов данных, которые сопоставляются с правильными ответами или категориями. Чтобы создать обученную сеть, способную давать корректные результаты, нужно обработать миллионы тензоров (матриц) и их элементов, что требует значительных вычислительных ресурсов. ГЛАВА 4 Игра с искусственным интеллектом 519
Для ускорения процесса обучения эти операции можно выполнять параллельно, распределяя вычисления между множеством ядер графического процессора. Такая параллельная обработка делает использование GPU особенно эффективным для задач машинного обучения. Каждое ядро графического процессора проще по структуре, чем ядро центрального процессора, так как оно рассчитано на выполнение одной конкретной задачи, а не множества разных операций. Благодаря этой специализации графические процессоры с большим количеством ядер производить дешевле, чем многоядерные CPU. Именно это делает видеокарты с множеством GPU-ядер идеальным выбором для приложений машинного обучения. Комбинация мощного центрального процессора и высокопроизводительной видеокарты позволяет значительно ускорить обучение нейронных сетей. Например, TensorFlow имеет версии, специально оптимизированные для работы с графическими картами, что позволяет легко использовать всю эту вычислительную мощность. Для сравнения: Raspberry Pi 3B+ оснащен четырьмя ядрами CPU и примерно четырьмя ядрами GPU, тогда как одна из современных видеокарт Nvidia может содержать более 3500 ядер. Это колоссальная разница, которая объясняет, почему обучение на GPU проходит гораздо быстрее. Однако стоит помнить, что графические процессоры не ускоряют операции общего назначения — такие, как работа с базами данных или ввод-вывод. Эти задачи требуют более сложных вычислительных блоков центрального процессора и часто ограничены скоростью обращения к хранилищу данных. Для работы с GPU можно даже писать собственные программы, используя язык и интерфейс CUDA, разработанный Nvidia. С помощью CUDA разработчики получают прямой доступ к ресурсам графической карты, создавая оптимизированный код для параллельных вычислений. Подробнее об этом можно узнать на сайте https://www.turing.com/kb/ understanding-nvidia-cuda. Тем не менее графические карты — это не вершина эволюции аппаратных средств для искусственного интеллекта. Сегодня более 50 компаний разрабатывают специализированные микросхемы, предназначенные для ускорения вычислений, связанных с ИИ. В частности, Microsoft, как упоминалось ранее, создала в Azure инфраструктуру для поддержки таких аппаратных ускорителей. Все указывает на то, что будущее принадлежит все более специализированным аппаратным решениям, использование которых становится все проще благодаря современному программному обеспечению. 520 РАЗДЕЛ 4 Использование искусственного интеллекта
Где найти больше интересного про ИИ на Python Если вы хотите углубить свои знания и получить больше практического опыта в области машинного обучения и искусственного интеллекта, начните с небольших, но интересных проектов. » "Is Santa Claus Real?", Varun Vohra, https:// » "Keras and deep learning on the Raspberry Pi", Adrian Rosebrock, www.pyimagesearch.com/2017/12/18/keras-deeplearning-raspberry-pi/ . » "MouseAir – Using AI on the Raspberry Pi to Entertain your Cat", John Shovic, www.switchdoc.com/2019/11/mouseairraspberry-pi-cat-toy/ . » "How to easily Detect Objects with Deep Learning on Raspberry Pi", Sarthak Jain, https://medium.com/nanonets/ towardsdatascience.com/is-santa-claus-real-9b7b9839776c . how-to-easily-detect-objects-with-deep-learning-onraspberrypi-225f29635c74 . » "Robotic AI Bear using ChatGPT", M . LeBlanc-Williams, » "Использование API ChatGPT на Raspberry Pi", https:// pimylifeup.com/raspberry-pi-chatgpt/ . https://learn.adafruit.com/robotic-ai-bear-using-chatgpt/ raspberry-pi-setup . В духе серии «Для чайников» вы сможете значительно ускорить свое обучение, если будете не только читать эти материалы, но и самостоятельно создавать программы, экспериментировать с ними и модифицировать проекты других разработчиков. ГЛАВА 4 Игра с искусственным интеллектом 521

5 DATA SCIENCE ИЛИ НАУКА О ДАННЫХ
В ЭТОМ РАЗДЕЛЕ Глава 1: Понимание пяти областей науки о данных Глава 2: Исследование больших данных Глава 3: Использование больших данных из Google Cloud
В этой главе вы узнаете: » Что такое наука о данных и в чем его суть » Что представляет собой работа с большими данными » Какие существуют пять основных этапов процесса анализа данных Гл а в а 1 Понимание пяти областей науки о данных Н аука о данных, или data science, играет гораздо большую роль в нашей повседневной жизни, чем мы привыкли думать. Каждый раз, когда вы пользуетесь поисковыми системами вроде Google, Bing, DuckDuckGo или даже ChatGPT, вы взаимодействуете с мощными инструментами науки о данных. Автоматические подсказки поисковых запросов, которые появляются, когда вы начинаете набирать текст, — это тоже результат работы алгоритмов анализа данных. Медицинская диагностика, распознавание изображений и интерпретация симптомов — все это примеры применения методов науки о данных в реальной жизни. Сегодня врачи все чаще полагаются на интеллектуальные системы, которые помогают анализировать огромные массивы медицинской информации. Как и многие темы, обсуждаемые в этой книге, наука о данных может поначалу показаться сложной и пугающей. Графики, статистика, аналитические выводы — все это выглядит непросто. Однако, как и в случае с искусственным интеллектом, стоит немного погрузиться в практику, рассмотреть несколько примеров, и все становится гораздо понятнее. В этой главе мы покажем, как использовать Python для решения задач науки о данных, и дадим столько теории, сколько нужно для уверенного старта. Цель — помочь вам понять общий процесс анализа данных и осознать, что скрывается за результатами исследований, о которых вы часто слышите. Например, почему одно исследование утверждает, что ГЛАВА 1 Понимание пяти областей науки о данных 525
кофе вреден, а другое — что полезен, хотя оба могут использовать одни и те же исходные данные. Самая сложная часть науки о данных заключается в правильной интерпретации результатов и в том, чтобы отличать реальные закономерности от случайных совпадений. К концу этой главы вы получите общее представление о процессе анализа данных и поймете, как интерпретировать результаты исследований. Наука о данных и большие данные могут казаться чем-то загадочным, но, обладая базовыми знаниями Python, вы сможете разобраться в них и научитесь проводить собственный анализ. Python и его многочисленные библиотеки делают работу с данными доступной даже для тех, кто не является специалистом в программировании. Важно помнить, что большинство исследователей и специалистов по данным — это не инженеры, а ученые, которые используют удобные инструменты, чтобы сосредоточиться на сути задачи — получении знаний из данных, а не на сложностях программирования. Работа с по-настоящему большими данными Термин «большие данные » часто звучит в новостях и технологических статьях, особенно когда речь идет о прогнозировании поведения пользователей, потребителей или даже целых рынков. Но что же он на самом деле означает? Под большими данными понимаются огромные, сложные и разнородные наборы информации, которые невозможно эффективно обработать с помощью традиционных инструментов — таких как обычные базы данных, электронные таблицы или стандартные статистические программы вроде SPSS. Для их анализа требуются специальные методы, алгоритмы и вычислительные мощности. В индустрии принято описывать большие данные через три ключевые характеристики, известные как «три V»: объем (volume), многообразие (variety) и скорость (velocity). Объем Объем — это первая и, пожалуй, самая очевидная характеристика больших данных. Он отражает sheer масштаб информации, с которой приходится работать. И этот масштаб действительно поражает воображение. Например, у в одной крупной социальной сети пользователей больше, 526 РАЗДЕЛ 5 Data science или наука о данных
чем население Китая. На платформу загружено свыше 500 миллиардов изображений (а по некоторым оценкам — уже больше триллиона) и около 2,5 триллиона постов. Каждый день добавляется более миллиарда новых публикаций — текстов, фотографий, видео и комментариев. И это только начало. В скором времени нас ждет эпоха интернета вещей (IoT), когда к сети будет подключено все — от бытовой техники до автомобилей. По прогнозам аналитической компании Gartner, к 2025 году количество таких устройств превысит 30 миллиардов. И каждое из них будет генерировать тысячи единиц данных. Даже простое устройство, измеряющее температуру на кухне каждую минуту в течение года, создает более полумиллиона точек данных. Если добавить к этому измерение влажности — получится уже миллион. Теперь умножьте эти значения на количество помещений в доме. Пусть это пять комнат и гараж, где в каждой установлено одно IoT-устройство, собирающее температуру и влажность. В таком случае ваш дом будет производить примерно 6 миллионов точек данных в год — только на основе этих простых измерений. А теперь подумайте о данных, которые создает ваш смартфон: местоположение, использование приложений, уровень заряда, подключение к сети — все это непрерывно передается в базы данных и аналитические системы. Именно здесь вступает в игру наука о данных. Ее задача — не просто собирать этот океан информации, но и находить в нем смысл, выявлять закономерности и превращать необработанные данные в полезные знания. Многообразие Фотографии и изображения представляют собой совершенно иной тип данных по сравнению с такими параметрами, как температура, влажность или геолокация. Можно сказать, что данные о температуре содержат более концентрированную, структурированную информацию, тогда как изображения — это гораздо более сложные и «размытые» данные. Фотографии состоят из огромного количества взаимосвязанных элементов, и интерпретировать их значительно труднее, чем числовые значения. Еще сложнее научить компьютер правильно классифицировать такие данные. Как обсуждается в разделе 7, даже простая задача — например, различить кошку и собаку — требует сложной модели и большого объема обучающих данных. Если же на изображении вдруг появится, скажем, малыш Йода, компьютер не сможет понять, что это такое, поскольку у него нет контекста, выходящего за пределы уже известных категорий. Машина ГЛАВА 1 Понимание пяти областей науки о данных 527
способна различать только то, чему ее обучили. Поэтому задачи компьютерного зрения и распознавания изображений остаются одними из самых сложных направлений науки о данных и искусственного интеллекта. ПРИМЕЧАНИЕ Аналогичные трудности возникают и при работе с голосом. Голосовой помощник Alexa от Amazon, например, прекрасно справляется с преобразованием речи в текст, но гораздо хуже — с пониманием смысла сказанного. Частично это объясняется отсутствием контекста: Alexa не видит собеседника, не воспринимает выражение лица, жесты или интонации. Кроме того, люди выражают свои мысли множеством разных способов, и это создает дополнительные сложности для анализа. Чтобы улучшить качество распознавания, Amazon собирает огромные объемы данных о пользовательских запросах и применяет методы науки о данных для выявления закономерностей — например, чтобы понять, как люди формулируют просьбы или команды. Такой сбор данных может вызывать опасения в плане конфиденциальности, но также помогает создавать более точные и «умные» системы. Стоит отметить, что наука о данных работает эффективнее, когда голос преобразован в текст, поскольку текстовые данные проще анализировать, искать и классифицировать. Однако при этом теряется часть информации, связанная с эмоциональной окраской, акцентами и другими нюансами речи — теми самыми деталями, которые делают человеческое общение живым и многозначным. Скорость Скорость в контексте больших данных относится к тому, насколько быстро информация добавляется к уже существующим наборам данных. Например, пользователи крупных социальных сетей ежедневно загружают около миллиарда фотографий. Если эта тенденция сохранится, то всего за несколько лет количество изображений на платформе превысит триллион. Это яркий пример высокоскоростного набора данных, который постоянно обновляется в реальном времени. Для сравнения, данные о температуре и влажности в вашем доме, собираемые на протяжении пяти лет, меняются значительно медленнее. Такие данные считаются низкоскоростными, поскольку их объем и частота обновления относительно невелики. Важно понимать, что высокоскоростные наборы данных требуют принципиально других подходов к хранению и обработке. Их невозможно эффективно анализировать методами, которые применяются для статичных или медленно изменяющихся данных. Поэтому для работы с потоками 528 РАЗДЕЛ 5 Data science или наука о данных
информации в реальном времени используются специальные технологии и инструменты, разработанные именно для обработки больших скоростей поступления данных. Управление объемом, многообразием и скоростью данных Управление объемом, многообразием и скоростью данных — задача не из легких. Именно поэтому специалисты в области науки о данных разработали множество методов и подходов, позволяющих эффективно обрабатывать, хранить и анализировать информацию. Понятие «трех V» — объема (volume), многообразия (variety) и скорости (velocity) — помогает описать характеристики любого набора данных и понять, с какими трудностями предстоит столкнуться при его анализе. Процесс извлечения из данных полезных знаний и закономерностей называется аналитикой данных. В следующих главах этого раздела мы сосредоточимся на изучении аналитики и освоим практические методы применения Python для постановки и решения аналитических задач. По мере накопления опыта вы научитесь уверенно управлять всем процессом анализа данных и постепенно станете настоящим мастером в области науки о данных. Полный вперед: пятиэтапный процесс науки о данных Как правило, процесс научной работы с данными, особенно если речь идет о больших данных, можно разделить на пять основных этапов: 1. Сбор данных . 2. Обработка данных . 3. Анализ данных . 4. Представление результатов . 5. Поддержка данных . ГЛАВА 1 Понимание пяти областей науки о данных 529
Эти этапы образуют полный цикл работы с информацией — от момента ее получения до долгосрочного хранения и использования. Каждый из них играет важную роль, и успех всего проекта во многом зависит от того, насколько качественно выполнен каждый шаг. Сбор данных Чтобы начать анализ, сначала нужно собрать данные. В реальных проектах данные редко поступают из одного источника — обычно их несколько: внутренние корпоративные записи, открытые базы данных, данные, собранные самостоятельно или полученные от партнеров. На этом этапе важно составить перечень всех возможных источников и решить, какие из них будут использоваться в вашем проекте. Прежде чем приступить к сбору информации, необходимо четко определить цели и задачи — какие именно бизнес-вопросы вы хотите решить. Ясное понимание целей помогает не только выбрать нужные данные, но и впоследствии оценить, удалось ли их достичь. Если данные поступают из разных источников, полезно объединить их в единую структуру или хранилище. Это позволит удобнее проводить анализ, находить закономерности и подготавливать наглядные отчеты, которые можно будет представить руководству или заинтересованным сторонам. Обработка данных Обработка данных — это этап науки о данных, который кажется простым, но на деле часто оказывается самым трудоемким. Нередко специалисты по данным проводят месяцы, подготавливая и преобразуя информацию, чтобы сделать ее пригодной для анализа и заслуживающей доверия. На этом этапе необходимо выявлять аномалии и выбросы, устранять дубликаты, заполнять или удалять записи с пропущенными значениями и приводить к единому виду противоречивые данные. При этом важно не удалить полезную информацию и не исказить результаты, чтобы анализ впоследствии был точным и осмысленным. Наука о данных по-прежнему требует не только техники, но и творческого подхода. 530 РАЗДЕЛ 5 Data science или наука о данных
ОСТОРОЖНО Еще одна проблема, с которой сталкиваются специалисты при обработке данных, — это их достоверность. Согласно данным Marketing Week, в 2015 году около 60% пользователей намеренно вводили ложную информацию при заполнении онлайн-форм. Честно говоря, многие из нас тоже грешат этим, особенно когда сталкиваются с навязчивыми маркетинговыми опросами или анкетами с политическим уклоном. Даже опытных специалистов по данным нередко обвиняют в избирательной очистке данных, когда она проводится таким образом, чтобы подтвердить заранее выдвинутую гипотезу. Поэтому при обработке данных важно сохранять объективность и тщательно следить за их целостностью и достоверностью. Этот этап требует времени и внимательности. Имейте в виду, что даже небольшое количество ошибочных данных способно значительно исказить результаты анализа и обесценить всю базу. ПРИМЕЧАНИЕ Еще немного пищи для размышлений. Сегодня часто говорят о ChatGPT и многих других программах, которые относятся к категории генеративного искусственного интеллекта. Эти системы способны создавать тексты, изображения и другие виды медиаконтента, изучая закономерности и структуры обучающих данных. Такие алгоритмы стали еще одним ценным инструментом в арсенале технологий ИИ. Два замечания о ChatGPT с точки зрения науки о данных: » Во-первых, ChatGPT использует интернет и веб как основной источник обучающих данных . В этих данных содержится немало неточной и недостоверной информации, и долгосрочные последствия использования таких ошибочных данных пока неясны . » Во-вторых, стоит задуматься: поскольку ChatGPT и другие ИИ-системы уже создают значительную часть контента в интернете, который затем используется для обучения новых моделей, не приведет ли это к тому, что сеть станет постепенно заполняться все более посредственным и однообразным материалом? Когда искусственный интеллект обучается на данных, созданных другими ИИ, качество создаваемого контента может со временем снижаться . ГЛАВА 1 Понимание пяти областей науки о данных 531
Анализ данных К тому моменту, когда вы проделали весь путь до этапа анализа данных, может показаться, что формулировка вопросов должна быть самой простой частью работы. Однако на деле это один из самых трудных шагов. Анализ больших наборов данных, поиск закономерностей и формулирование правильных вопросов требуют высокой степени человеческой интуиции — именно здесь проявляется искусство науки о данных. Некоторые вопросы, например «Какова средняя сумма, потраченная на хлопья в 2020 году?», легко сформулировать и рассчитать, даже при огромных объемах информации. Но действительно ценные вопросы, такие как «Как увеличить продажи сахарных хлопьев Frosted?», требуют гораздо более глубокого анализа и понимания контекста. Такие задачи выходят за рамки простой статистики и требуют сочетания аналитического мышления, креативности и знания предметной области. Такой вопрос имеет множество уровней сложности. Прежде всего, необходимо определить исходный показатель — сколько сейчас продается сахарных хлопьев Frosted. Этот базовый уровень легко вычислить из имеющихся данных. Затем важно уточнить, что именно подразумевается под «увеличением продаж». Речь идет о росте количества покупателей или о повышении общей выручки? Можно снизить цену до одного цента за коробку и получить гораздо больше покупателей, но при этом прибыль снизится. На самом деле цель состоит не просто в увеличении продаж, а в росте прибыли, которая определяется как разница между ценой и себестоимостью. Однако самая сложная часть вопроса заключается в том, как побудить людей покупать больше сахарных хлопьев Frosted. И здесь возникает главный вызов — содержится ли ответ на этот вопрос в тех данных, которые вы собрали, или для этого нужно искать новую, недостающую информацию. Сложность анализа состоит в том, чтобы убедиться, что вопрос сформулирован правильно, задан в нужной форме и обращен к подходящему типу данных. Анализ данных требует знаний и опыта в использовании статистических методов, таких как линейная и логистическая регрессия, а также в выявлении корреляций между различными типами данных с помощью вероятностных алгоритмов и формул, например наивного байесовского классификатора (звучит впечатляюще, не правда ли?). Подробное рассмотрение этих методов выходит за рамки этой книги, однако позже в этом разделе мы приведем несколько практических примеров их применения. 532 РАЗДЕЛ 5 Data science или наука о данных
НАУКА О ДАННЫХ В СРАВНЕНИИ С АНАЛИТИКОЙ ДАННЫХ Наука о данных и аналитика данных тесно связаны, но не тождественны . В современном понимании наука о данных представляет собой технический процесс извлечения полезной информации из больших объемов неструктурированных данных . Она опирается на прогнозную аналитику, статистические методы и машинное обучение для обработки и интерпретации огромных массивов информации . Аналитика данных, в свою очередь, сосредоточена на применении статистического анализа к уже существующим наборам данных с целью получения конкретных выводов и рекомендаций . Из-за размытых границ между этими понятиями и постоянного появления новых инструментов анализа больших данных (в том числе аналитики больших данных) эти две области все больше пересекаются . Если говорить просто, можно сказать, что этапы анализа данных, представления результатов и поддержки данных — то есть шаги с третьего по пятый в общем процессе аналитики — являются подмножеством более широкой области науки о данных . Представление результатов После того как данные собраны и проанализированы, наступает не менее важный этап — представление результатов. На этом этапе цель заключается в том, чтобы донести выводы анализа до руководства, заказчика или других заинтересованных сторон в понятной и наглядной форме. Люди, как правило, быстрее воспринимают и лучше понимают информацию, когда видят ее в визуальном виде — на графиках, диаграммах или интерактивных панелях, а не в виде длинных таблиц и текстовых отчетов. Для визуализации данных специалисты по науке о данных чаще всего используют два основных инструмента — язык R и библиотеку MatPlotLib для Python. Мы будем использовать MatPlotLib, поскольку она отлично интегрируется с Python и подходит для создания как простых, так и сложных графиков. Если вы уже знакомы с материалами раздела 4, то знаете, как работать с MatPlotLib и строить наглядные графики на практике. ГЛАВА 1 Понимание пяти областей науки о данных 533
Поддержка данных Этап поддержки данных — один из самых недооцененных в процессе науки о данных. После того как специалисты завершили первый цикл анализа, получили ответы и представили результаты, часто возникает соблазн просто перейти к следующему проекту. Однако важно понимать, что к тем же данным, моделям и результатам, скорее всего, придется вернуться снова — возможно, через месяцы или даже годы, чтобы задать новые вопросы или уточнить старые выводы. ЗАПОМНИТЕ Чтобы это было возможно, необходимо тщательно архивировать и документировать весь процесс. Следует сохранять сами данные и их источники, модели и методы обработки (включая любые исключения и критерии отбраковки), а также запросы и результаты анализа. Такая документация позволит быстро восстановить ход работы и повторно использовать ее части в будущем. Кроме того, в ряде отраслей — особенно в медицине и финансах — существует юридическая обязанность сохранять данные, а также описания используемых методов и процедур, чтобы при необходимости подтвердить достоверность и прозрачность полученных выводов.
В этой главе вы узнаете: » Как использовать библиотеку NumPy для задач дата-сайенса » Как применять pandas для быстрого анализа данных » Какие выводы можно сделать из вашего первого проекта по анализу данных » Как визуализировать информацию с помощью библиотеки Matplotlib в Python Гл а в а 2 Исследование больших данных В этой главе вы познакомитесь с инструментами и процессами, которые специалисты по данным используют для форматирования, обработки и анализа информации. Хотя для работы с большими данными существует множество инструментов и библиотек на Python, а также на таких языках, как R, мы будем использовать NumPy по трем причинам. Во-первых, это один из двух наиболее популярных инструментов для науки о данных на Python (второй по популярности — Keras). Во-вторых, NumPy лежит в основе многих проектов в области искусственного интеллекта. И в-третьих, еще один важный пакет для работы с данными — pandas — построен на базе NumPy. Пакет pandas стал неотъемлемым инструментом в науке о данных, поскольку предоставляет более абстрактное представление данных, упрощая их обработку, документирование и отслеживание преобразований. Для визуализации результатов анализа мы будем использовать Matplotlib — мощный инструмент для построения графиков на Python. Он требует некоторого времени на освоение, но благодаря дополнительным библиотекам, таким как seaborn и Plotly, процесс визуализации стал проще и нагляднее. ГЛАВА 2 Исследование больших данных 535
В этой главе мы будем использовать три основных пакета — NumPy, seaborn и Plotly, чтобы шаг за шагом познакомить вас с практическими аспектами науки о данных. Чтобы скачать код для этой главы, перейдите на сайт www.dummies.com/go/pythonaiofd3e, найдите раздел Downloads и выберите ссылку для раздела 5 (Book 5 code). СОВЕТ Существует множество типов компьютеров, способных выполнять программы для обработки данных. В этих главах мы использовали Raspberry Pi 4B с 8 ГБ оперативной памяти. Вы можете загрузить готовый образ системы (в который уже входят NumPy, pandas и Matplotlib) для 64-битной версии Bullseye, установленной на SD-карту Raspberry Pi 4B. Для этого перейдите на сайт www.dummies.com/go/pythonaiofd3e, пролистайте страницу до раздела Downloads и скачайте файл 2023.09–23.PAIOSDCardV2.zip. Используйте качественную SD-карту объемом не менее 32 ГБ. Имя пользователя по умолчанию — «pi», пароль — «3.14». Знакомство с NumPy, pandas и Matplotlib При изучении научных вычислений и науки о данных вы снова и снова будете сталкиваться с тремя основными пакетами Python: NumPy, pandas и Matplotlib. В этом разделе мы подробно рассмотрим каждый из них. NumPy NumPy добавляет в Python мощные инструменты для работы с большими объемами данных, включая функции для обработки массивов и выполнения сложных математических операций. Он особенно эффективен при выполнении базовых числовых вычислений, таких как вычисление среднего значения, а также при создании и обработке многомерных массивов — тензоров или матриц. В разделе 4 NumPy активно используется для работы с данными и тензорами в нейронных сетях и задачах машинного обучения. Это один из ключевых инструментов для приложений в области искусственного интеллекта. СОВЕТ 536 В интернете доступно множество качественных руководств по установке и использованию NumPy. Вот несколько полезных пошаговых материалов: РАЗДЕЛ 5 Data science или наука о данных
» Python Numpy — Введение в ndarray [Часть 1] (www.machinelearningplus.com/python/numpy-tutorialpart1.array-python-examples): отличное введение в работу с матрицами (или тензорами) и их роль в NumPy . » Руководство по NumPy (www.tutorialspoint.com/numpy): подробный обзор библиотеки, ее происхождения и основных способов применения . » Учебник по NumPy: (www.guru99.com/numpy-tutorial.html): содержит меньше теории, но множество практических примеров, которые помогут закрепить материал и восполнить пробелы после изучения предыдущих двух руководств . Вот пример простой программы на Python с использованием библиотеки NumPy. Она создает матрицу 2×2 и выполняет несколько базовых операций с ее элементами: import numpy as np x = np.array([[1,2], [3,4]]) print(np.sum(x)) # Вычисляет сумму всех элементов; выводит "10" print(np.sum(x, axis=0)) # Вычисляет сумму по каждому столбцу; выводит "[4 6]" print(np.sum(x, axis=1)) # Вычисляет сумму по каждой строке; выводит "[3 7]" Если вы устанавливаете NumPy на Raspberry Pi, выполните команду: sudo apt-get install python3.numpy СОВЕТ pandas Python хорошо подходит для подготовки и обработки данных, но не так эффективен в их анализе и моделировании. Эту задачу решает библиотека pandas. Она предоставляет быстрые, гибкие и понятные структуры данных, которые делают работу с реляционными и размеченными данными более естественной и интуитивной. pandas — это один из ключевых инструментов для реального анализа данных на Python. Библиотека особенно эффективна при работе с табличными данными, такими как таблицы SQL или листы Excel, и прекрасно справляется с временными рядами, например, с почасовыми измерениями температуры. pandas наглядно демонстрирует, насколько мощные возможности предоставляют библиотеки Python для упрощения работы с данными. ГЛАВА 2 Исследование больших данных 537
Если вспомнить обсуждение этапа предобработки данных в прошлой главе, где речь шла об обработке отсутствующих или некорректных значений, то pandas идеально подходит именно для таких задач. Она также поддерживает сложные иерархические структуры данных, к которым можно обращаться с помощью простых и понятных функций. pandas позволяет объединять и соединять наборы данных, а также преобразовывать различные типы данных в универсальный формат DataFrame. ПРИМЕЧАНИЕ pandas основан на библиотеке NumPy и унаследовал ее высокую производительность. Он обеспечивает значительное ускорение обработки по сравнению с обычным Python-кодом, особенно при работе с большими объемами данных и повторяющимися вычислительными циклами. DataFrame в pandas представляет собой структуру для хранения данных в виде прямоугольной таблицы, которую удобно анализировать и обрабатывать. По сути, это аналог электронной таблицы, где строки соответствуют записям, а столбцы — переменным или признакам. Внутри DataFrame могут находиться другие DataFrame, одномерные ряды данных, тензоры NumPy (массивы — здесь снова проявляется связь с материалом из раздела 4, посвященного нейронным сетям и машинному обучению), а также словари, содержащие тензоры и матрицы. Помимо задания самих данных в структуре DataFrame, вы можете указывать индексы и имена столбцов, что делает код более наглядным и упрощает анализ. DataFrame позволяет обращаться к отдельным элементам, строкам или столбцам, а также удалять, переименовывать или добавлять новые компоненты. Кроме того, можно объединять несколько связанных наборов данных в одну структуру DataFrame, создавая целостную и удобную для обработки таблицу. Matplotlib Matplotlib — это библиотека, которая добавляет в Python средства визуализации данных. Она отлично дополняет NumPy и используется в программах для анализа данных и научных вычислений. Matplotlib предоставляет объектно ориентированный интерфейс (API) на Python, позволяющий встраивать графики в приложения с графическим интерфейсом. С помощью Matplotlib можно создавать сложные, профессионально оформленные графики, а также интерактивные визуализации, которые обновляются во время выполнения программы. Это особенно полезно при машинном обучении и анализе данных, когда важно наблюдать, как система развивается в процессе обучения. Отображение графиков 538 РАЗДЕЛ 5 Data science или наука о данных
в реальном времени помогает понять, когда достигнуты оптимальные результаты или когда дальнейшее обучение уже не приносит заметных улучшений. Выполнение вашего первого проекта по науке о данных Теперь пришло время применить NumPy и pandas в небольшом практическом проекте по науке о данных. В качестве источника данных мы будем использовать сайт Kaggle (www.kaggle.com). Kaggle — это принадлежащее Google онлайн-сообщество специалистов по данным, аналитиков и исследователей. Платформа позволяет пользователям находить, загружать и использовать наборы данных, которые в большинстве случаев распространяются по открытым лицензиям. Кроме того, Kaggle проводит многочисленные соревнования по задачам машинного обучения, в которых компании публикуют реальные задачи и предоставляют призы за лучшие решения. Это отличная возможность не только попрактиковаться, но и получить признание в профессиональном сообществе. Помимо соревнований, на Kaggle есть активные форумы и разделы для обсуждений (www.kaggle.com/discussions), где можно найти советы, примеры кода и поддержку от более опытных специалистов — отличный ресурс для тех, кто только начинает знакомство с наукой о данных. Бриллианты — лучшие друзья специалиста по анализу данных Для первой задачи мы выбрали базу данных бриллиантов, поскольку она имеет простую структуру и сравнительно небольшой объем — около 54 000 записей. Такой набор данных без труда обрабатывается даже на Raspberry Pi. Скачать его можно на странице https://www.kaggle.com/datasets/ shivam2503/diamonds. Использование Kaggle бесплатно, однако для доступа к данным потребуется регистрация и вход в систему. Метаданные этого набора, то есть данные, описывающие сами данные, — включают десять переменных. Их можно рассматривать как заголовки столбцов в таблице. Полный список представлен в таблице 2.1. ГЛАВА 2 Исследование больших данных 539
Таблица 2.1. Столбцы базы данных о бриллиантах Название столбца Тип данных Описание Index Числовой (Numeric) Уникальный индекс (порядковый номер) записи carat Числовой (Numeric) Вес бриллианта в каратах cut Текст (Text) Качество огранки бриллианта (в порядке улучшения): fair, good, very good, premium, ideal color Текст (Text) Цвет бриллианта: от D (лучший, бесцветный) до J (наименее качественный) clarity Текст (Text) Чистота бриллианта, т .е . насколько заметны включения (дефекты) . От лучшего к худшему: FL — без изъянов (flawless), IF, VVS1, VVS2, VS1, VS2, SI1, SI2, I1, I2, I3 — уровень включений увеличивается depth Числовой (Numeric) Глубина (в процентах): высота бриллианта (от нижней точки — culet — до верхней плоской грани — table), деленная на средний диаметр по поясу (girdle) table Числовой (Numeric) Размер верхней плоской грани (table) в процентах от среднего диаметра бриллианта Price Числовой (Numeric) Цена бриллианта X Числовой (Numeric) Длина бриллианта (в миллиметрах) Y Числовой (Numeric) Ширина бриллианта (в миллиметрах) Z Числовой (Numeric) Глубина бриллианта (в миллиметрах) Если бы вы использовали этот набор данных как обучающую выборку для программы машинного обучения, вы могли бы построить модель с помощью NumPy и TensorFlow — примерно так же, как описано в главе 2 раздела 4. Однако в этой главе мы сосредоточимся на создании простых программ для анализа данных с использованием библиотеки pandas. Эти программы будут считывать набор данных и помогать формулировать аналитические вопросы. Для работы мы будем использовать структуру данных DataFrame — двумерную таблицу с метками и столбцами различных типов. Она значительно упрощает визуализацию и обработку данных. 540 РАЗДЕЛ 5 Data science или наука о данных
СОВЕТ Чтобы установить библиотеки NumPy и pandas на Raspberry Pi, выполните следующие команды (на других компьютерах команды будут аналогичными): sudo apt-get install python3.numpy sudo apt-get install python3.pandas Теперь с помощью nano — стандартного текстового редактора в Raspberry Pi (хотя вы можете использовать и любой другой, например Thonny или Visual Studio Code; соавтор Джон, кстати, предпочитает vi!) — откройте новый файл под названием FirstDiamonds.py и введите следующий код: # Бриллианты - лучшие друзья специалиста по анализу данных # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd # читаем CSV-файл с данными о бриллиантах # создаем структуру данных DataFrame из данных df = pd.read_csv('diamonds.csv') print (df.head(10)) print() # вычисляем общую стоимость бриллиантов sum = df.price.sum() print ("Total $ Value of Diamonds: ${:0,.2f}".format( sum)) # вычисляем среднюю цену бриллиантов mean = df.price.mean() print ("Mean $ Value of Diamonds: ${:0,.2f}".format(mean)) # делаем сводку данных descrip = df.carat.describe() print() print (descrip) descrip = df.describe(include='object') print() print (descrip) Убедившись, что файл diamonds.csv находится в вашей текущей директории, запустите следующую команду в терминале: python3 FirstDiamonds.py После запуска программы вы должны увидеть следующий результат на экране: Unnamed: 0 x y 0 carat z 1 0.23 cut color clarity Ideal E depth SI2 table 61.5 price 55.0 ГЛАВА 2 Исследование больших данных 541
326 3.95 3.98 2.43 1 2 0.21 Premium E SI1 326 3.89 3.84 2.31 2 3 0.23 Good E VS1 327 4.05 4.07 2.31 3 4 0.29 Premium I VS2 334 4.20 4.23 2.63 4 5 0.31 Good J SI2 335 4.34 4.35 2.75 5 6 0.24 Very Good J VVS2 336 3.94 3.96 2.48 6 7 0.24 Very Good I VVS1 336 3.95 3.98 2.47 7 8 0.26 Very Good H SI1 337 4.07 4.11 2.53 8 9 0.22 Fair E VS2 337 3.87 3.78 2.49 9 10 0.23 Very Good H VS1 338 4.00 4.05 2.39 Общая стоимость бриллиантов: $212,135,217.00 Средняя стоимость бриллиантов: $3,932.80 count 53940.000000 mean 0.797940 std 0.474011 min 0.200000 25% 0.400000 50% 0.700000 75% 1.040000 max 5.010000 Name: carat, dtype: float64 cut color clarity count 53940 53940 53940 unique 5 7 8 top Ideal G SI1 freq 21551 11292 13065 542 РАЗДЕЛ 5 Data science или наука о данных 59.8 61.0 56.9 65.0 62.4 58.0 63.3 58.0 62.8 57.0 62.3 57.0 61.9 55.0 65.1 61.0 59.4 61.0
Это впечатляющий объем данных для такого небольшого фрагмента кода! Разбор кода Теперь разберем программу по шагам, чтобы видеть, как части кода работают вместе: # Бриллианты - лучшие друзья специалиста по анализу данных Сначала подключим библиотеки, которые понадобятся для анализа: # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd Теперь загрузим файл с данными о бриллиантах в структуру DataFrame. В нашем случае данные уже аккуратно оформлены, поэтому дополнительная предобработка не требуется, но в реальных проектах это скорее исключение: чаще всего значительная часть времени уходит на очистку и приведение данных к нужному виду: # читаем CSV-файл с данными о бриллиантах # создаем DataFrame из данных df = pd.read_csv('diamonds.csv') Для проверки выводим первые десять строк из DataFrame, чтобы убедиться, что данные загружены корректно: print (df.head(10)) print() Затем вычислим несколько показателей из столбца price. Обратите внимание, что можно напрямую обращаться к имени столбца как к части объекта DataFrame — это одно из удобных и мощных свойств Python. # вычисляем общую стоимость бриллиантов sum = df.price.sum() print ("Total $ Value of Diamonds: ${:0,.2f}".format( sum)) # вычисляем среднюю цену бриллиантов mean = df.price.mean() print ("Mean $ Value of Diamonds: ${:0,.2f}".format(mean)) ГЛАВА 2 Исследование больших данных 543
Далее вызываем встроенную функцию describe, чтобы получить описание и сводные статистические данные по элементу DataFrame carat: # обобщаем данные descrip = df.carat.describe() print() Следующий оператор выводит сводную информацию для всех нечисловых столбцов DataFrame — cut, color и clarity. descrip = df.describe(include='object') print() print(descrip) Визуализация данных с помощью Matplotlib Теперь мы можем приступить к визуализации данных с помощью Matplotlib. В разделе 4 мы использовали эту библиотеку для построения графиков, демонстрирующих, как программа машинного обучения повышала точность в процессе обучения. В данном случае Matplotlib поможет нам отобразить некоторые любопытные особенности набора данных. Чтобы установить Matplotlib на Raspberry Pi, введите sudo pip3 install matplotlib. СОВЕТ ОСТОРОЖНО Когда программа использует Matplotlib, ее следует запускать из терминала, открытого в графическом интерфейсе Raspberry Pi. Если ваш Raspberry Pi работает без подключенного монитора (в так называемом безмониторном режиме), можно использовать VNC, чтобы получить удаленный доступ к графическому интерфейсу и управлять системой с основного компьютера. Одно из преимуществ использования pandas и Matplotlib состоит в том, что типы данных NumPy и структуры DataFrame совместимы с популярными графическими форматами, такими как JPEG и GIF. Это позволяет легко работать с графическими данными и использовать их в визуализациях или анализе. Чистота бриллианта в зависимости от размера в каратах Наш первый график представляет собой точечную диаграмму, отображающую взаимосвязь между чистотой бриллианта и его весом в каратах. Чтобы создать этот график, откройте nano (или любой другой предпо- 544 РАЗДЕЛ 5 Data science или наука о данных
читаемый текстовый редактор), создайте новый файл с именем Plot_ ClarityVSCarat.py и введите следующий код: # Изучаем блестящие бриллианты # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd import matplotlib.pyplot as plt # читаем CSV-файл с данными о бриллиантах # создаем структуру данных DataFrame из данных df = pd.read_csv('diamonds.csv') import matplotlib.pyplot as plt carat = df.carat clarity = df.clarity plt.scatter(clarity, carat) plt.show() # или plt.savefig("name.png") Запустите программу, и вы увидите результат, аналогичный изображению на рисунке 2.1. Вот что такое настоящая простота визуализации! pandas и Matplotlib отлично взаимодействуют, позволяя быстро и наглядно представлять данные. Рисунок 2.1. Чистота бриллиантов (по горизонтали) относительно размера в каратах (по вертикали) Напомним, что чистота бриллианта определяется степенью видимости включений в камне: от наивысшего качества (FL — безупречный) до наименьшего (I3 — включения третьего уровня). Полный диапазон категорий выглядит так: FL, IF, VVS1, VVS2, VS1, VS2, SI1, SI2, I1, I2, I3. В нашей базе данных безупречных бриллиантов (FL) не оказалось. ГЛАВА 2 Исследование больших данных 545
Важно понимать, что порядок значений и порядок их отображения на графике определяются тем, как они расположены в исходном наборе данных. Например, если первая запись имеет чистоту SI2, следующая — SI1, а потом VS1, то именно в таком порядке они будут показаны на диаграмме. Чтобы убедиться в этом, выполните следующий код: for i in range(20): print(df.iloc[i].clarity) Можно было бы отсортировать данные по столбцу чистоты, но результат окажется алфавитным, так как этот столбец содержит текстовые, а не числовые значения, а категории чистоты не следуют алфавитному порядку по степени качества. Настоящее решение состоит в предварительной обработке данных — преобразовании категорий чистоты в числовые значения, отражающие их реальную иерархию. Это хороший пример того, как этап предобработки данных может оказаться сложнее, чем сам анализ. Может показаться, что из графика следует: самые крупные бриллианты относятся к категории I1. Однако делать подобные обобщения нельзя, потому что мы не знаем, каким образом собирался набор данных. Корректнее будет сказать: в этом конкретном наборе данных бриллианты с чистотой I1 имеют наибольший средний размер. Количество бриллиантов каждого типа чистоты Теперь построим график, который покажет, сколько бриллиантов в нашем наборе данных соответствует каждому уровню чистоты. Это поможет визуально оценить распределение категорий по численности. Используя nano (или ваш любимый текстовый редактор), откройте файл с именем Plot_CountClarity.py и введите следующий код: # Изучаем блестящие бриллианты # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd import matplotlib.pyplot as plt # читаем CSV-файл с данными о бриллиантах # создаем DataFrame из данных df = pd.read_csv('diamonds.csv') import matplotlib.pyplot as plt # подсчитываем количество бриллиантов каждого типа чистоты clarityindexes = df['clarity'].value_counts().index.tolist() claritycount= df['clarity'].value_counts().values.tolist() print(clarityindexes) print(claritycount) 546 РАЗДЕЛ 5 Data science или наука о данных
plt.bar(clarityindexes, claritycount) plt.show() # или plt.savefig("name.png") Когда вы запустите программу Plot_CountClarity.py, на экране появится столбчатая диаграмма, аналогичная изображению на рисунке 2.2. На графике показано, сколько бриллиантов в наборе данных относится к каждой категории чистоты. Рисунок 2.2. Количество бриллиантов каждого типа чистоты Чистота бриллианта определяется степенью заметности включений внутри камня: FL, IF, VVS1, VVS2, VS1, VS2, SI1, SI2, I1, I2, I3 — от лучшего качества (FL, безупречный) к худшему (I3, включения 3-го уровня). В нашей базе данных безупречных бриллиантов не оказалось. График показывает, что наиболее распространенными в наборе данных являются бриллианты среднего качества — категории SI1, VS2 и SI2. Количество бриллиантов каждого цвета Теперь построим график распределения бриллиантов по цветам. Откройте файл Plot_CountColor.py и введите следующий код: # Изучаем блестящие бриллианты # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd import matplotlib.pyplot as plt # читаем CSV-файл с данными о бриллиантах # создаем DataFrame из данных df = pd.read_csv('diamonds.csv') ГЛАВА 2 Исследование больших данных 547
import matplotlib.pyplot as plt # подсчитываем количество бриллиантов каждого цвета colorindexes = df['color'].value_counts().index.tolist() colorcount= df['color'].value_counts().values.tolist() print(colorindexes) print(colorcount) plt.bar(colorindexes, colorcount) plt.show() # или plt.savefig("name.png") Запустите вашу программу и результат вы можете увидеть на рисунке 2.3. Цвет G, который почти бесцветный, составляет примерно 20% выборки. В целом правило простое: чем меньше окрашен бриллиант, тем выше его стоимость. Исключение составляют редкие цветные камни — розовые и голубые бриллианты, которые не входят в эту классификацию и не представлены в данном наборе данных. Рисунок 2.3. Количество бриллиантов каждого цвета Использование тепловых карт pandas для поиска корреляций Наш заключительный график представляет собой тепловую карту, показывающую корреляции между числовыми значениями в базе данных. Мы используем все числовые поля и формируем матрицу, которая отражает степень взаимосвязи между ними. Для быстрой и удобной визуализации такого графика применяется дополнительная библиотека для Python — seaborn. Она построена поверх Matplotlib и тесно интегрирована с pandas DataFrame, что делает ее особенно удобной для задач науки о данных. 548 РАЗДЕЛ 5 Data science или наука о данных
С помощью nano (или любого другого текстового редактора) создайте файл Plot_Heat.py и введите следующий код: # Изучаем блестящие бриллианты # импортируем библиотеки pandas и numpy import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # читаем CSV-файл с данными о бриллиантах # создаем DataFrame из данных df = pd.read_csv('diamonds.csv') # удаляем столбцы с индексами и текстом df = df.drop('Unnamed: 0', axis=1) df = df.drop('cut', axis=1) df = df.drop('color', axis=1) df = df.drop('clarity', axis=1) f, ax = plt.subplots(figsize=(10, 8)) corr = df.corr() print (corr) #оттенки серого cmap = sns.cubehelix_palette(50, hue=0.05, rot=0, light=0.95, dark=0.05, as_cmap=True) sns.heatmap(corr, mask=np.zeros_like(corr, dtype=bool), cmap=cmap, square=True, ax=ax) plt.show() Запустите пример программы Plot_Heat.py, чтобы убедиться, что библиотека seaborn установлена на вашем Raspberry Pi. Если при выполнении программы Python выдаст ошибку импорта, это означает, что библиотека отсутствует. В таком случае установите ее с помощью следующей команды: sudo apt-get install python3.seaborn Запустите программу и насладитесь настоящей визуализацией данных, как показано на рисунке 2.4. ГЛАВА 2 Исследование больших данных 549
Рисунок 2.4. Корреляционная тепловая карта Первое, на что следует обратить внимание: чем темнее цвет на тепловой карте, тем сильнее корреляция между двумя переменными. Диагональная линия, проходящая от верхнего левого до нижнего правого угла, показывает, что параметр карат имеет стопроцентную корреляцию сам с собой, что вполне ожидаемо. Также можно заметить, что переменные x, y и z сильно коррелируют между собой, что означает: когда размер бриллианта увеличивается по одному измерению, он, как правило, увеличивается и по двум другим. Что касается цены, то по мере увеличения карата и размеров бриллианта она также растет — это вполне логично. Интересно, что показатель глубины (высота бриллианта, измеряемая от калетты — нижнего кончика — до площадки, деленная на средний диаметр рундиста) не демонстрирует сильной корреляции с ценой и даже показывает небольшую отрицательную зависимость. Из одной такой тепловой карты можно сделать множество наблюдений. Тепловые карты отлично подходят для выявления общих взаимосвязей и перекрестных корреляций в данных. Было бы интересно проанализировать корреляцию между цветом, чистотой и ценой, но эти параметры не отражены на диаграмме, поскольку столбцы с цветом и чистотой представлены в текстовом формате, а корреляции можно вычислять только между числовыми переменными. 550 РАЗДЕЛ 5 Data science или наука о данных
Отсутствие таких данных можно устранить, если заменить буквенные обозначения цвета числовыми кодами (например, от 1 до 8), а затем снова построить тепловую карту. Тот же подход можно применить и к показателю чистоты. Это еще раз показывает, что этап предварительной обработки данных является трудоемким и требует тщательности — без него невозможно провести корректный анализ.
В этой главе вы узнаете: » Как работать с действительно большими данными » Как использовать Google Cloud BigQuery » Как создавать свои первые запросы » Как визуализировать результаты с помощью библиотеки Matplotlib Гл а в а 3 Использование больших данных из Google Cloud До этого момента вы работали с относительно небольшими наборами данных. В этой главе вы познакомитесь с гораздо более масштабными источниками — базой данных Medicare и обширными данными NOAA (Национального управления океанических и атмосферных исследований), которые обновляются каждый час. Даже если ваш компьютер достаточно мощный, чтобы обработать большие наборы данных, это не всегда возможно сделать локально. Некоторые базы данных невозможно скачать по юридическим причинам, а другие — из-за частого обновления. Например, база данных о качестве воздуха NOAA обновляется каждый час, и скачивать ее целиком постоянно было бы крайне неэффективно. В таких случаях разумнее оставить данные на удаленном сервере и использовать облачные ресурсы для анализа. Этот подход имеет и еще одно преимущество — обработка выполняется в облаке, а значит, ваш локальный компьютер не обязан быть мощным или иметь большой объем памяти. Чтобы загрузить код для этой главы, перейдите на сайт www.dummies.com/ go/pythonaiofd3e, пролистайте страницу до раздела Downloads и выберите ссылку с кодом для раздела 5 (Book 5). 552 РАЗДЕЛ 5 Data science или наука о данных
Что такое большие данные? Большие данные — это наборы данных, которые слишком объемны или сложны для обработки традиционными инструментами и методами. Они содержат огромное количество наблюдений и строк, что дает возможность применять более сложные статистические методы и снижает вероятность ложных открытий. Под ложными открытиями понимается доля ошибок, возникающих, когда гипотеза отклоняется ошибочно, то есть когда результат оказывается ложноположительным. Как уже упоминалось в главе 1 этого раздела, большие данные становятся все более привычным явлением в современном мире. По мере распространения компьютеров, сенсоров и интеллектуальных устройств объем генерируемой информации постоянно растет, а скорость ее поступления увеличивается. В этой главе мы рассмотрим, как можно использовать облачные технологии для доступа к большим наборам данных с помощью Python и библиотеки pandas, а затем визуализировать полученные результаты на Raspberry Pi. Понимание Google Cloud и BigQuery В первой программе вы будете работать с данными, хранящимися в облаке, с помощью сервисов Google Cloud и инструмента анализа данных Google BigQuery. Это важный шаг к пониманию того, как использовать облачные технологии не только для хранения, но и для обработки больших объемов информации. Суть работы заключается в том, что ваш локальный компьютер выступает лишь в роли клиента, который отправляет инструкции серверам в облаке. Именно облачные вычислительные мощности выполняют основную работу по обработке и анализу данных. Таким образом, ваш компьютер лишь управляет процессом, а все вычисления и манипуляции происходят в инфраструктуре Google Cloud. ГЛАВА 3 Использование больших данных из Google Cloud 553
Платформа Google Cloud Платформа Google Cloud представляет собой набор облачных вычислительных сервисов, работающих на той же инфраструктуре, что и популярные продукты компании, такие как Google Поиск. Этот подход аналогичен стратегиям Amazon и Microsoft, которые также используют собственные технологии для предоставления облачных решений. Такой метод выгоден как для пользователей, так и для самой компании: улучшения, вносимые в продукты Google, напрямую отражаются на производительности и надежности облачных сервисов. Google Cloud включает более сотни программных интерфейсов (API) и инструментов, ориентированных на работу с данными, машинное обучение и искусственный интеллект. В этой главе основное внимание уделяется одному из ключевых сервисов платформы — BigQuery, мощному инструменту для анализа и обработки больших объемов данных с помощью SQL-запросов в облаке. BigQuery от Google Система программного обеспечения с передачей состояния представления (REST) определяет набор правил взаимодействия, которые используются для создания веб-сервисов. Как правило, REST-сервисы работают с помощью запросов HTTP или HTTPS, что позволяет обмениваться данными между различными устройствами и операционными системами. RESTful-веб-сервис использует URL-адреса для выполнения стандартных запросов и получения ответов — подобно тому, как браузер загружает веб-страницы. Чтобы скрыть технические детали запросов и ответов, в Python применяются специальные библиотеки. BigQuery как раз и построен на основе RESTful-веб-сервиса. В программной инженерии существует понятие абстракции — это метод, который помогает управлять сложностью систем. Он создает упрощенный слой в программном обеспечении (например, модули Python), скрывающий сложные внутренние процессы. Благодаря абстракции программисту не нужно разбираться в деталях работы нижних уровней, таких как передача IP-пакетов или манипуляция битами — достаточно работать с понятными инструментами. BigQuery представляет собой бессерверную платформу, что означает высокий уровень абстракции: пользователю не нужно заботиться о выделении виртуальных машин, объеме оперативной памяти или количестве 554 РАЗДЕЛ 5 Data science или наука о данных
процессоров. Система автоматически масштабируется — от одного до тысяч вычислительных ядер — в зависимости от задачи, а вы оплачиваете только реально использованные ресурсы. Передача данных в BigQuery может достигать миллионов строк в секунду, что позволяет практически мгновенно приступать к анализу. Google Cloud предоставляет бесплатный 90-дневный пробный доступ с кредитом в $300, так что вы можете выполнять примеры из этой книги без затрат. BigQuery также предлагает множество публичных наборов данных, включая базы Medicare и NOAA, к которым мы будем обращаться. Для работы с BigQuery в Python используется библиотека google .cloud, которая преобразует результаты запросов в структуры данных pandas. Это обеспечивает удобную интеграцию BigQuery с инструментами анализа данных, описанными ранее в этом разделе. Компьютерная безопасность в облаке При работе с облачными сервисами крайне важно обеспечить надлежащий уровень безопасности. Google реализует защиту данных с помощью системы IAM (Identity and Access Management — управление идентификацией и доступом), применяемой ко всем своим облачным продуктам. IAM предоставляет владельцу учетной записи — администратору — полный контроль над тем, кто имеет доступ к ресурсам и какие действия может выполнять каждый пользователь. Система позволяет управлять правами как на уровне отдельных проектов, так и в масштабах всей организации, обеспечивая прозрачность и гибкость настройки доступа. В последующих разделах мы рассмотрим, как настроить аутентификацию IAM и обеспечить безопасное взаимодействие вашего приложения с облаком Google. ПУБЛИЧНАЯ БАЗА ДАННЫХ MEDICARE Medicare — это национальная программа медицинского страхования США с единым плательщиком, управляемая Центрами услуг Medicare и Medicaid (CMS) . Она предоставляет медицинскую страховку гражданам старше 65 лет, а также более молодым людям с определенными формами инвалидности или хроническими заболеваниями . В 2023 году участниками программы являлись более 65 миллионов человек . ГЛАВА 3 Использование больших данных из Google Cloud 555
Такой масштаб делает Medicare одним из крупнейших источников медицинских данных в мире . Ежегодно программа генерирует огромные объемы информации о медицинских услугах, расходах и пациентах . Чтобы сделать эти данные доступными для исследователей и аналитиков, Google и CMS объединили усилия, разместив значительную часть информации в публичной базе данных BigQuery . Это позволяет просматривать, анализировать и визуализировать данные прямо в облаке, без необходимости загружать их на локальный компьютер, что делает работу с большими медицинскими данными быстрой, безопасной и эффективной . Регистрация в BigQuery Перейдите на сайт https://cloud.google.com и зарегистрируйтесь для получения бесплатного пробного периода. Google попросит указать данные кредитной карты — это необходимо, чтобы подтвердить, что вы не робот. Однако никакие средства списаны не будут, если вы сами не решите перейти на платный тариф. Даже если во время пробного периода вы превысите лимит в $300 (что крайне маловероятно), Google просто уведомит вас об этом, но не произведет списание. Этого лимита вполне достаточно для выполнения большого количества запросов и знакомства с возможностями облачной платформы BigQuery. После регистрации установите необходимые библиотеки для работы с BigQuery, выполнив следующие команды: sudo pip install google-cloud-bigquery sudo pip install db-dtypes Чтение больших данных Medicare В этом разделе, чтобы начать использовать BigQuery в своих программах на Python, нужно создать проект и загрузить файл аутентификации .json, содержащий ключи для доступа к вашей учетной записи и данным. База данных Medicare является крупнейшим легкодоступным медицинским набором данных в мире. Мы зададим этой базе данных несколько конкретных медицинских запросов. 556 РАЗДЕЛ 5 Data science или наука о данных
Настройка проекта и аутентификации Чтобы получить доступ к облаку Google, необходимо создать проект и получить учетные данные для аутентификации. ОСТОРОЖНО Следуйте инструкциям внимательно, чтобы все настроить правильно с первого раза. После загрузки файла аутентификации не удаляйте его — повторно скачать этот файл будет невозможно, и в случае потери придется заново проходить процесс аутентификации. Если у вас еще нет аккаунта Google, сначала создайте его. Вот шаги по настройке проекта и получению учетных данных: 1. Перейдите на сайт https://console.developers.google.com/ и войдите под своей учетной записью . Нажмите кнопку «Начать бесплатно» в правом верхнем углу и введите данные своей кредитной карты. После этого вернитесь на ту же страницу . Появится экран, показанный на рисунке 3 .1 . 2. В левом верхнем углу нажмите «Мой первый проект» . Рисунок 3.1. Страница консоли разработчика Google Cloud 3. В появившемся окне нажмите New Project (Новый проект) в правом верхнем углу . 4. Введите MedicareProject в качестве имени проекта и нажмите Create (Создать). После создания проекта вы вернетесь к экрану, показанному на рисунке 3 .1 . ГЛАВА 3 Использование больших данных из Google Cloud 557
5. В выпадающем меню в левом верхнем углу выберите MedicareProject (а не My First Project). Убедитесь, что выбран именно MedicareProject, иначе вы настроите API и аутентификацию не для того проекта — это частая ошибка . ЗАПОМНИТЕ 6. Нажмите +Enable APIs and Services (Включить API и сервисы) в верхней части экрана, чтобы активировать BigQuery API. Появится экран выбора API . Если кнопка Enable (Включить) недоступна, проверьте, включен ли API (должна быть зеленая галочка) . 7. В строке поиска введите BigQuery, выберите BigQuery API, затем нажмите Manage (Управление).Теперь создадим учетные данные для аутентификации . 8. Перейдите на вкладку Credentials (Учетные данные), затем в правом верхнем углу нажмите Create Credentials (Создать учетные данные) и выберите Service Account (Сервисный аккаунт). Появится экран, показанный на рисунке 3 .2 . 9. В поле Service Account Name (Имя сервисного аккаунта) введите MedicareProject и нажмите Create and Continue (Создать и продолжить). 10. На следующем экране в меню Role (Роль) выберите Owner (Владелец) и нажмите Done (Готово). После этого появится список ваших сервисных аккаунтов . 11. Нажмите на созданный сервисный аккаунт, перейдите на вкладку KEYS (Ключи), затем выберите ADD KEY (Добавить ключ)  JSON. После этого появится уведомление о создании сервисного аккаунта и ключа, а на ваш компьютер загрузится файл с именем вроде MedicareProject-1223xxxxx413.json . ОСТОРОЖНО Не потеряйте этот файл! Он содержит вашу аутентификационную информацию и не может быть скачан повторно . В случае утери придется создавать новый сервисный аккаунт . 12. Скопируйте загруженный файл в ту директорию, где вы будете создавать ваш Python-скрипт. Теперь можно перейти к первому примеру. 558 РАЗДЕЛ 5 Data science или наука о данных
Рисунок 3.2. Первый экран учетных данных Первый код для работы с большими данными Программа MedicareQuery1.py обращается к одному из десятков общедоступных наборов данных Medicare и извлекает из него сведения для последующего анализа. В примере используется таблица inpatient_ charges_2015 и выполняется SQL-запрос, который выбирает те поля, с которыми мы планируем работать и которые хотим изучить. Если вы еще не знакомы с этим широко распространенным языком запросов, загляните во врезку «Изучаем SQL» — там дано краткое введение. В таблице 3.1 перечислены все столбцы набора данных inpatient_ charges_2015. ГЛАВА 3 Использование больших данных из Google Cloud 559
Таблица 3.1. Столбцы, типы данных и описания набора данных inpatient_charges_2015 Столбец Тип данных Описание provider_id Строка (STRING) Номер сертификации поставщика медицинских услуг (CMS Certification Number, CCN), выставляющего счета за стационарные услуги provider_name Строка (STRING) Название медицинского учреждения (поставщика услуг) provider_ street_address Строка (STRING) Улица и номер здания, где физически расположен поставщик услуг provider_city Строка (STRING) Город, в котором находится поставщик услуг provider_state Строка (STRING) Штат, в котором находится поставщик услуг provider_zipcode Целое число (INTEGER) Почтовый индекс (ZIP-код) местоположения поставщика услуг drg_definition Строка (STRING) Код и описание группы MS-DRG (Medicare Severity — Diagnosis Related Group) — классификационной системы, объединяющей схожие клинические состояния (диагнозы) и процедуры, выполненные в ходе госпитализации hospital_ referral_ region_ description Строка (STRING) Описание региона медицинской направленности (HRR — Hospital Referral Region), в котором расположен поставщик услуг total_discharges Целое число (INTEGER) Общее количество выписанных пациентов (discharges), за которых поставщик выставил счета за стационарные услуги average_ covered_charges Число с плавающей точкой (FLOAT Средняя сумма, выставляемая поставщиком за услуги, покрытые Medicare, для всех выписанных пациентов по данной MS-DRG . Эти суммы различаются по больницам из-за различий в структуре тарифов average_ total_payments Число с плавающей точкой (FLOAT Средняя общая сумма выплат всем поставщикам по данной MS-DRG . Включает оплату MS-DRG, надбавки за обучение, капитал, коэффициент сложных случаев (outlier payments) и выплаты за участие в программах (например, disproportionate share) . Также включает сумму, которую оплачивает пациент (ко-платежи и франшизы), и любые дополнительные выплаты третьих сторон 560 РАЗДЕЛ 5 Data science или наука о данных
Столбец Тип данных Описание average_ medicare_payments Число с плавающей точкой (FLOAT Средняя сумма, которую Medicare выплачивает поставщику за его долю в MS-DRG . Включает стандартные выплаты, надбавки за обучение, капитал и сложные случаи . Не включает платежи пациента (ко-платежи, франшизы) и сторонние выплаты ИЗУЧЕНИЕ SQL SQL (язык структурированных запросов) — это язык, созданный для работы с базами данных и извлечения информации из них . Изначально он предназначался для управления реляционными базами данных, но со временем его применение распространилось и на другие типы баз данных, включая те, что доступны через BigQuery и Google Cloud . Вот несколько полезных учебных материалов, которые помогут вам научиться обращаться к данным с помощью SQL: • www .w3schools .com/sql • www .sql-tutorial .net • «SQL для чайников», 9-е издание • «SQL . Полное руководство для чайников», 3-е издание • «SQL за 10 минут», 4-е издание (издательство Sams) Используя nano (или любой другой текстовый редактор), создайте новый файл, введите следующий код и сохраните его под именем MedicareQuery1.py: import pandas as pd from google.cloud import bigquery # настройка запроса QUERY = """ SELECT provider_city, provider_state, drg_definition, average_total_payments, average_medicare_payments FROM `bigquery-public-data.cms_medicare.inpatient_ charges_2015` WHERE provider_city = "GREAT FALLS" AND provider_ state = "MT" ГЛАВА 3 Использование больших данных из Google Cloud 561
ORDER BY provider_city ASC LIMIT 1000 """ client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxf413.json') query_job = client.query(QUERY) df = query_job.to_dataframe() print ("Records Returned:", df.shape ) print () print ("First 3 Records") print (df.head(3)) После создания файла замените в коде имя MedicareProject2.122xxxxxf413. json на имя вашего собственного файла аутентификации (того самого, который вы ранее скопировали в рабочую директорию программы). СОВЕТ Если при запуске программы MedicareQuery1.py появится ошибка импорта, это значит, что библиотека google.cloud не установлена. Чтобы установить ее, выполните в терминале следующую команду: sudo pip3 install google-cloud-bigquery Если при запуске программы на Raspberry Pi появляется сообщение об ошибке «библиотека db-dtypes не установлена», выполните в терминале следующую команду: sudo pip install db-dtypes Разбор кода Для начала импортируем необходимые библиотеки. Обратите внимание, что здесь используется библиотека google.cloud с модулем bigquery, который обеспечивает доступ к базе данных BigQuery из программы на Python: import pandas as pd from google.cloud import bigquery Затем создаем SQL-запрос, который будет использоваться для извлечения данных из BigQuery и загрузки их в структуру данных pandas DataFrame для последующего анализа: # формируем запрос QUERY = """ SELECT provider_city, provider_state, drg_definition, 562 РАЗДЕЛ 5 Data science или наука о данных
average_total_payments, average_medicare_payments FROM `bigquery-public-data.cms_medicare.inpatient_ charges_2015` WHERE provider_city = "GREAT FALLS" AND provider_ state = "MT" ORDER BY provider_city ASC LIMIT 1000 """ Видите, как устроен SQL-запрос? Мы выбираем (SELECT) нужные столбцы (указанные в таблице 3.1) из (FROM) базы данных bigquerypublic-data.cms_medicare.inpatient_charges_2015, но только те записи, где город поставщика (provider_city) равен GREAT FALLS, а штат (provider_state) — MT. В завершение мы добавляем команду ORDER BY, чтобы отсортировать результаты по названию города (provider_city) в алфавитно-цифровом порядке. Не забудьте заменить имя json-файла на собственный файл аутентификации: client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxef413.json') Теперь отправляем запрос к BigQuery: query_job = client.query(QUERY) Затем преобразуем полученные данные в знакомую структуру pandas DataFrame: df = query_job.to_dataframe() Добавим несколько строк для вывода результатов запроса, которые мы получили от BigQuery: print print print print ("Records Returned: ", df.shape ) () ("First 3 Records") (df.head(3)) Запустите программу MedicareQuery1.py, и вы увидите результаты выполнения запроса, как показано ниже. Если появится ошибка аутентификации, убедитесь, что в директории программы находится правильный файл аутентификации. При необходимости создайте новый файл, внимательно проверив, чтобы имя проекта соответствовало вашему проекту в Google Cloud. ГЛАВА 3 Использование больших данных из Google Cloud 563
Records Returned: (112, 5) First 3 Records provider_city provider_state average_medicare_payments 0 GREAT FALLS MT 5709.0 1 GREAT FALLS MT 6145.0 2 GREAT FALLS MT 3976.0 ... average_total_payments ... 6820.0 ... 7141.0 ... 5059.0 Мы получили 112 записей из города Грейт-Фолс. Вы можете изменить запрос в программе, указав свой город и штат, чтобы получить данные для вашего региона. МКБ КОДЫ МКБ-10 — это международная система кодирования медицинских диагнозов, применяемая для учета, анализа и выставления счетов . Последняя версия этой классификации стала обязательной в 2015 году и вызвала немало беспокойства среди медицинского сообщества . В ней содержится более 155 000 кодов — от M79 .603 («Боль в руке, неуточненная») до S92 .4 («Перелом большого пальца стопы») . Эти коды объединяются в категории MS-DRG, которые используются в базах данных Medicare, рассматриваемых в этом разделе . Когда у соавтора Джона Шовика был собственный медицинский стартап, он нередко сталкивался с трудностями, связанными с кодами МКБ-10 . Вот несколько его «любимых» примеров: • V97 .33XD — затягивание в реактивный двигатель, повторный случай . • Z63 .1 — проблемы в отношениях с родственниками супруга . • V95 .43XS — травма пассажира при столкновении космического корабля, последствия . • R46 .1 — странный внешний вид . • Y93 .D1 — деятельность: вязание спицами и крючком . 564 РАЗДЕЛ 5 Data science или наука о данных
Проведение анализа данных К этому моменту у нас есть подключение к облачной базе данных и рабочая среда для запросов к большим данным. Сформулируем еще один запрос: выберем из полного набора inpatient_charges_2015 пациентов с кодом MS-DRG 554, который относится к заболеваниям костей и артропатиям без серьезных осложнений или сопутствующих заболеваний. В практике это означает случаи с проблемами костной системы без выраженных внешних факторов риска. Такой диагноз находится через систему кодирования МКБ-10 и сопоставляется в Medicare с группами MS-DRG, где каждой группе соответствует свой код. Создайте файл MedicareQuery2.py с помощью nano или любого другого редактора и вставьте в него код ниже. Не забудьте заменить путь к файлу аутентификации на имя вашего json-файла: import pandas as pd from google.cloud import bigquery # настройка запроса QUERY = """ SELECT provider_city, provider_state, drg_definition, average_total_payments, average_medicare_payments FROM `bigquery-public-data.cms_medicare.inpatient_ charges_2015` WHERE drg_definition LIKE '554 %' ORDER BY provider_city ASC LIMIT 1000 """ client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxf413.json') query_job = client.query(QUERY) df = query_job.to_dataframe() print("Records Returned:", df.shape) print() print("First 3 Records") pd.set_option('display.max_columns', None) print(df.head(3)) Единственное отличие этой программы от предыдущей в том, что мы , который находит любой DRG, начинающийся добавили LIKE '554%© с 554. Когда мы запустили программу, мы получили следующие результаты: Records Returned: (286, 5) First 3 Records ГЛАВА 3 Использование больших данных из Google Cloud 565
provider_city provider_state drg_definition\ 0 ABINGTON PA 554 - BONE DISEASES & ARTHROPATHIES W/O MCC 1 AKRON OH 554 - BONE DISEASES & ARTHROPATHIES W/O MCC 2 ALBANY NY 554 - BONE DISEASES & ARTHROPATHIES W/O MCC average_total_payments average_medicare_payments 0 5444.0 3993.0 1 5581.0 4292.0 2 4985.0 3834.0 Теперь у нас есть данные, подходящие для небольшого анализа. Вопрос, который стоит рассмотреть: какой процент общих расходов на это заболевание покрывается Medicare? Оставшаяся часть, как правило, оплачивается самим пациентом. В данном случае мы не будем учитывать сторонние источники платежей. Создайте новый файл под названием MedicareQuery3.py с помощью nano (или любого другого текстового редактора) и вставьте в него следующий код: import pandas as pd from google.cloud import bigquery # настройка запроса QUERY = """ SELECT provider_city, provider_state, drg_ definition, average_total_payments, average_medicare_ payments FROM `bigquery-public-data.cms_medicare. inpatient_charges_2015` WHERE drg_definition LIKE '554 %' ORDER BY provider_city ASC LIMIT 1000 """ client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxf413.json') query_job = client.query(QUERY) df = query_job.to_dataframe() print ("Records Returned:", df.shape ) print () total_payment = df.average_total_payments.sum() medicare_payment = df.average_medicare_payments.sum() percent_paid = ((medicare_payment/total_payment)) * 100 566 РАЗДЕЛ 5 Data science или наука о данных
print ("Medicare pays {:4.2f}% of Total for DRG 554". format(percent_paid)) print ("Patient pays {:4.2f}% of Total for DRG 554". format(100-percent_paid)) На экране будут выведены следующие результаты: Records Returned: (286, 5) Medicare pays 77.06% of Total for 554 DRG Patient pays 22.94% of Total for 554 DRG Процент оплаты по штатам Продолжая анализ из предыдущего раздела, выберем по очереди каждый штат, представленный в нашей выборке, и вычислим долю покрытия Medicare по штатам для всех записей с DRG, начинающимся на 554. Для этого создайте файл MedicareQuery4.py: import pandas as pd from google.cloud import bigquery # настройка запроса QUERY = """ SELECT provider_city, provider_state, drg_ definition, average_total_payments, average_medicare_payments FROM `bigquery-public-data.cms_medicare.inpatient_ charges_2015` WHERE drg_definition LIKE '554 %' ORDER BY provider_city ASC LIMIT 1000 """ client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxf413.json') query_job = client.query(QUERY) df = query_job.to_dataframe() print ("Records Returned: ", df.shape ) print () # находим уникальные значения штатов states = df.provider_state.unique() states.sort() total_payment = df.average_total_payments.sum() medicare_payment = df.average_medicare_payments.sum() percent_paid = ((medicare_payment/total_payment))*100 print("Overall:") ГЛАВА 3 Использование больших данных из Google Cloud 567
print ("Medicare pays {:4.2f}% of Total for DRG 554". format(percent_paid)) print ("Patient pays {:4.2f}% of Total for DRG 554". format(100-percent_paid)) print ("Per State:") # теперь выполним итерацию по штатам print(df.head(5)) state_percent = [] for current_state in states: state_df = df[df.provider_state == current_state] state_total_payment = state_df.average_total_payments. sum() state_medicare_payment = state_df.average_medicare_ payments.sum() state_percent_paid = ((state_medicare_payment/state_ total_payment))*100 state_percent.append(state_percent_paid) print ("{:s} Medicare pays {:4.2f}% of Total for DRG 554".format (current_state,state_percent_paid)) Теперь перейдем к визуализации В заключительном эксперименте этой главы мы визуализируем результаты по штатам с помощью Matplotlib, а программа MedicareQuery4.py строит график. СОВЕТ Чтобы проверить, установлена ли библиотека seaborn на вашем Raspberry Pi, запустите пример программы MedicareQuery4.py. Если появится ошибка импорта, установите библиотеку, выполнив следующую команду в терминале: sudo apt-get install python3.seaborn Переходя к программе VNC для получения графического интерфейса на Raspberry Pi, добавьте следующий код в конец программы MedicareQuery4.py: # Мы могли бы построить график, используя Matplotlib с двумя списками, # но для этого примера мы хотим использовать DataFrame data_array = {'State': states, 'Percent': state_percent} df_states = pd.DataFrame.from_dict(data_array) # Теперь вернемся к работе с DataFrame 568 РАЗДЕЛ 5 Data science или наука о данных
import matplotlib.pyplot as plt import seaborn as sb print (df_states) df_states.plot(kind='bar', x='State', y= 'Percent') plt.show() На рисунке 3.3 показан график, который появляется после запуска программы MedicareQuery4.py. Рисунок 3.3. Столбчатая диаграмма процентных выплат Medicare по штатам для кода 554 Поиск самого загрязненного города в мире в режиме реального времени Здесь мы предложим вам еще один пример работы с большим объемом данных. В BigQuery вы можете получить доступ к еще одной публичной базе данных, известной как OpenAQ, где хранятся измерения содержания различных загрязняющих веществ в атмосфере из 47 стран. Обновление этой базы данных было приостановлено в 2022 году. Вот пример кода, который выбирает три самых загрязненных города мира по качеству воздуха: import pandas as pd from google.cloud import bigquery ГЛАВА 3 Использование больших данных из Google Cloud 569
# пример запроса: QUERY = """ SELECT location, city, country, value, timestamp FROM `bigquery-public-data.openaq.global_air_quality` WHERE pollutant = "pm10" AND timestamp > "2017-04-01" ORDER BY value DESC LIMIT 1000 """ client = bigquery.Client.from_service_account_json( 'MedicareProject2.122xxxxxf413.json') query_job = client.query(QUERY) df = query_job.to_dataframe() print (df.head(3)) При запуске программы PollutedCity.py вы получите результаты, показывающие три города с наибольшим уровнем загрязнения воздуха по данным базы OpenA Q. На момент написания книги результаты выглядели примерно так: location city country value timestamp 0 Albury South-west Slopes AU 26647612.40 2022.04.17 23:00:00+00:00 1 Gölcük-MTHM Kocaeli TR 65984.84 2022.04.13 05:00:00+00:00 2 Mono Shore MONO US 25301.00 2022.05.07 02:00:00+00:00 Судя по полученным данным, Албери на юго-западных склонах Австралии выглядит крайне неблагоприятным по качеству воздуха. Но быстрый поиск показывает обратное: там воздух, как правило, чистый. Это указывает на возможные проблемы в самой базе — ошибочные значения, неполные записи или некорректную агрегацию. Второе место, Гельджюк в Турции, кажется более правдоподобным: этот район действительно известен высоким уровнем загрязнения и часто упоминается как часть так называемой Токсичной долины.
6 ВЗАИМОДЕЙСТВИЕ С ОБОРУДОВАНИЕМ
В ЭТОМ РАЗДЕЛЕ Глава 1: Введение в физические вычисления Глава 2: Никакой пайки! Использование разъемов Grove для конструирования Глава 3: Считывание данных об окружающем мире Глава 4: Приводим механизмы в движение
В этой главе вы узнаете: » Как использовать одноплатные миникомпьютеры » Как применять Raspberry Pi для считывания данных об окружающей среде » Как заставить компьютер выполнять физические действия Гл а в а 1 Введение в физические вычисления М ы считаем, что освоение программного обеспечения (в частности, Python) представляет собой более сложную задачу, чем работа с аппаратной частью. Поэтому в первых главах книги мы сосредоточились на изучении Python. Однако теперь пришло время перейти к практическому применению, научиться управлять оборудованием с помощью кода. Когда вы заставляете компьютер взаимодействовать с физическим миром, выполняя механические действия, это называется физическими вычислениями. ОСТОРОЖНО Чтобы скачать код для этой главы, перейдите на сайт www.dummies.com/ go/pythonaiofd3e.В этой главе мы подключим различные датчики и двигатели к компьютеру Raspberry Pi, чтобы управлять ими и считывать информацию из окружающей среды — например, включать светодиоды или измерять температуру — с помощью Python. Хотя используемые напряжения (3,3 В и 5 В) безопасны для человека, неправильное подключение может повредить компьютер или оборудование. Поэтому всегда придерживайтесь двух правил: » Всегда отключайте питание перед подключением или заменой проводов . » Всегда проверяйте соединения дважды — особенно провода питания и заземления . Почему это так важно, вы узнаете в следующей главе . ГЛАВА 1 Введение в физические вычисления 573
Физические вычисления — это действительно увлекательно Одна из причин, по которой мы хотим познакомить вас с этой областью, заключается в том, что маленькие компьютеры, выполняющие физические действия (часто называемые встраиваемыми системами), окружают нас буквально повсюду. Оглянитесь вокруг своей кухни. В вашем холодильнике есть компьютер, а может быть, даже несколько, если у него есть дисплей. В блендере, духовке и микроволновке тоже есть компьютеры. Если вы используете лампы Philips Hue, то даже в ваших лампочках есть встроенные микроконтроллеры. А в вашем автомобиле может быть более двадцати таких компьютеров. Вот еще пример — обычный тостер. Если на нем есть кнопка для бейглов или небольшой экран, значит, внутри тоже спрятан компьютер. Почему их так много? Потому что управлять устройствами с помощью компьютеров гораздо дешевле, чем с помощью специализированных электронных схем. Современные микроконтроллеры можно производить массово по цене около 15 центов за штуку. Конечно, большинство таких компьютеров значительно проще, медленнее и имеют гораздо меньше оперативной памяти, чем персональные компьютеры. Если у настольного ПК обычно от 4 до 128 ГБ ОЗУ (а 1 ГБ — это примерно миллиард байтов), то у микроконтроллера в вашем тостере может быть всего около 100 байтов памяти. Тем не менее все эти миниатюрные компьютеры выполняют физические вычисления, то есть измеряют параметры окружающей среды и реагируют на них. Компьютер в холодильнике следит за температурой и включает охлаждение только при необходимости, экономя энергию. Плита обновляет показания на панели, отслеживает состояние кнопок и регуляторов и поддерживает оптимальную температуру, чтобы вы получили идеально приготовленный ужин. Что такое Raspberry Pi? Функциональность большинства миниатюрных встроенных компьютеров слишком ограничена, чтобы использовать их в примерах этой книги. Поэтому мы выбрали компромиссный вариант — Raspberry Pi, 574 РАЗДЕЛ 6 Взаимодействие с оборудованием
одноплатный компьютер стоимостью от 35 до 70 долларов, для которого доступно огромное количество аппаратного и программного обеспечения, особенно для работы с Python. Raspberry Pi — это устройство, которое значительно мощнее тостера, но при этом проще, чем компьютер внутри современного телевизора. Оно представляет собой популярный одноплатный компьютер, существующий с 2012 года. Проект был создан организацией Raspberry Pi Foundation с целью обучения основам информатики и инженерии в школах по всему миру. Raspberry Pi быстро стал невероятно популярным: по всему миру продано более 30 миллионов таких устройств. Существует множество моделей — от компактного Raspberry Pi Zero стоимостью около 5 долларов до производительной Raspberry Pi 4B, цена которой достигает примерно 70 долларов. В примерах этой книги мы используем модель Raspberry Pi 4B, хотя для большинства проектов подойдет и Raspberry Pi 3B+. Чтобы лучше понять технологии, с которыми мы сталкиваемся каждый день, рассмотрим основные аппаратные компоненты Raspberry Pi 4B, показанные на рисунке 1.1: » Разъем GPIO — порты общего назначения для ввода-вывода . Мы будем часто использовать их в проектах из этой книги . » ЦПУ/ГПУ — центральный и графический процессоры, которые выполняют все вычисления и управляют другими компонентами . Именно здесь исполняются ваши программы на Python . » USB-порты — стандартные разъемы универсальной последовательной шины (USB), аналогичные тем, что есть в обычных компьютерах . К ним подключаются клавиатура, мышь и другие устройства . » Порт Ethernet — обеспечивает проводное подключение к сети, как на настольных ПК . » Wi-Fi — встроенный модуль беспроводной связи, находящийся внутри процессора . Благодаря ему можно подключаться к интернету без кабелей Ethernet . » Выход HDMI 0 — используется для подключения основного монитора или телевизора . » » Выход HDMI 1 — позволяет подключить второй дисплей . Композитный видео- и аудиоразъем — служит для передачи аналогового видео и звука . ГЛАВА 1 Введение в физические вычисления 575
» Слот для microSD-карты — в него вставляется карта памяти, на которой установлена операционная система Raspberry Pi . » Светодиод питания PWR — красный индикатор, показывающий, что питание подано (он не всегда горит постоянно) . » Светодиод активности ACT — желтый индикатор, мигающий при обращении к SD-карте . » Другие порты: Порт питания USB Type-C — используется для подачи питания 5 В . Интерфейс камеры (CSI) — для подключения камеры Raspberry Pi через шлейф . Интерфейс дисплея (DSI) — предназначен для подключения специализированных дисплеев, что выходит за рамки этой книги . àοÞιοÞ ρöτêðòðïτöχψôτõψāîυôðā ,(1-ɹ ,(1-ɹ θýϋχí θýϋχí δχóõχïðöôýφ ëðíυχðɹ τϊíðχψτïüυó Рисунок 1.1. Основные компоненты Raspberry Pi 4B Üôíðñτöχψý ωχωöχāôðā 4;6%'8 ρòχöíòāQMGVS7(ñτψöý ôτɹôðîôυφωöχψχôυ • • • ƀñχôöτñöôýφψτïüυó Чтобы компьютер мог не только выполнять вычисления, но и взаимодействовать с окружающим миром, помимо самого компьютера понадобятся два типа устройств — датчики и актуаторы. 576 РАЗДЕЛ 6 Взаимодействие с оборудованием
Датчик — это небольшое электронное устройство, которое измеряет параметры окружающей среды, например температуру, влажность, освещенность или расстояние. Актуатор — это устройство, которое выполняет физические действия: вращает мотор, открывает клапан, зажигает светодиод и т.д. В этой главе вы познакомитесь с основными компонентами вашего первого проекта по физическим вычислениям — мигающим светодиодом. Этот проект можно считать аналогом классической программы «Hello, World!» для мира физического программирования: простая, но наглядная демонстрация того, как код управляет реальными устройствами. СОВЕТ Перед началом работы приобретите стартовый набор Raspberry Pi, который обычно включает блок питания, корпус и предустановленную операционную систему. Если вы новичок, подготовьте также мышь, клавиатуру и монитор — так будет проще выполнить начальную настройку. Опытные пользователи могут подключиться к Raspberry Pi удаленно через SSH (Secure Shell) — это удобно, если устройство работает без монитора. Полезно ознакомиться с официальными руководствами на сайте www.raspberrypi.org. Еще один удобный способ управления устройством — использование VNC (Virtual Network Computing). С помощью VNC вы можете открыть графический рабочий стол Raspberry Pi на своем основном компьютере и работать с ним так, будто он подключен напрямую. Создание проектов, которые двигаются и взаимодействуют с окружающим миром В начале этой главы мы уже упоминали бытовые примеры: компьютеры, встроенные в духовку или холодильник, постоянно отслеживают состояние окружающей среды — температуру, влажность, время работы — и при необходимости воздействуют на нее. Точно так же, как блендер измельчает лед по команде, вы сможете создавать свои собственные проекты, которые будут измерять, реагировать и выполнять физические действия. ГЛАВА 1 Введение в физические вычисления 577
После знакомства с аппаратной частью Raspberry Pi вы увидите, что такие проекты — не просто теоретические упражнения. С помощью недорогих датчиков и актуаторов можно создавать удивительные устройства: умные лампы, системы автоматического полива, роботов или погодные станции. Начните с простого проекта, чтобы понять основы взаимодействия между кодом и электроникой. В последующих главах вы перейдете к более сложным устройствам — все так же управляемым с помощью Python, который станет вашим главным инструментом для программирования физического мира. КАКИЕ БЫВАЮТ МАЛЫЕ КОМПЬЮТЕРЫ? Существует огромное разнообразие малых компьютеров и микроконтроллерных плат, которые используются для создания собственных проектов — от простых датчиков до полноценных систем автоматизации . Мы выбрали Raspberry Pi, потому что он полностью поддерживает язык Python, имеет сотни специализированных библиотек и огромное сообщество пользователей . На сайтах вроде www.raspberrypi.org можно найти пошаговые инструкции, примеры кода и готовые проекты . Raspberry Pi работает под управлением Raspbian — разновидности Linux, что делает его полноценным мини-компьютером с графическим интерфейсом и многозадачностью . Другой популярный вариант — Arduino . Это небольшие микроконтроллерные платы, которые стоят примерно столько же, сколько Raspberry Pi, но имеют более простое устройство и меньше памяти . Их основное преимущество — большое количество портов ввода-вывода и встроенный аналого-цифровой преобразователь, которого нет у Raspberry Pi . Благодаря этому Arduino отлично подходит для взаимодействия с датчиками, которые измеряют температуру, свет, давление и другие аналоговые сигналы . Если покупать микроконтроллеры Arduino оптом, они обходятся значительно дешевле, чем Raspberry Pi, поэтому часто используются в серийных проектах . В одной из следующих глав мы создадим проект с использованием внешнего аналого-цифрового преобразователя и Raspberry Pi . Этот проект будет включать работу с пламенем — и, поверьте, это будет особенно интересно . Существует и другой класс небольших компьютеров, похожих на Arduino, — это платы SAMD21, ESP8266 и ESP32 . Их можно программировать в той же среде разработки (IDE), что и устройства 578 РАЗДЕЛ 6 Взаимодействие с оборудованием
Arduino . Эти микроконтроллеры имеют значительно меньший объем оперативной памяти по сравнению с Raspberry Pi, но при этом обладают встроенными модулями Wi-Fi (а иногда и Bluetooth) . Благодаря этому они отлично подходят для создания проектов, которые нужно подключить к интернету, например, устройств интернета вещей (IoT) . Тем не менее, хотя работа с такими платами действительно увлекательна, Raspberry Pi обеспечивает гораздо более комфортную и мощную среду для разработки на Python и обучения . Восприятие окружающей среды с помощью Raspberry Pi К этому моменту ваш Raspberry Pi должен быть полностью настроен и готов к работе — с подключенным монитором, клавиатурой и мышью. Если вы еще этого не сделали, самое время заняться настройкой. Подробные инструкции можно найти на сайте www.raspberrypi.org — это отличный источник информации для начинающих. Когда ваш Raspberry Pi готов, можно переходить к более интересной части — физическим вычислениям. Raspberry Pi идеально подходит для таких проектов, потому что сочетает в себе мощность полноценного компьютера с возможностью напрямую взаимодействовать с внешним миром. У него есть достаточный объем оперативной памяти, хранилище для ваших экспериментов и множество портов для подключения датчиков, двигателей и других устройств. Все это делает Raspberry Pi превосходной платформой для реализации идей на Python, будь то умный дом, робот или автоматизированная система сбора данных. Порты GPIO Одной из самых мощных возможностей Raspberry Pi является наличие порта ввода-вывода общего назначения — GPIO (General Purpose Input/Output). Этот 40-контактный разъем расположен вдоль верхнего края платы и позволяет подключать различные внешние устройства — от простых светодиодов и кнопок до датчиков, двигателей и других микроконтроллеров. Именно через GPIO Raspberry Pi «взаимодей- ГЛАВА 1 Введение в физические вычисления 579
ствует» с физическим миром, превращаясь из просто компьютера в контроллер для умных устройств. С помощью Python вы можете программно управлять каждым контактом GPIO, настраивая его как вход (для считывания данных с датчиков) или как выход (для управления светодиодами, реле и т.д.). При этом несколько контактов имеют фиксированные функции — два подают питание 5 В, два выдают 3.3 В, а несколько других используются для заземления (GND). На рисунке 1.2 приведено назначение каждого из 40 контактов GPIO. Во всех версиях Raspberry Pi этот разъем одинаковый, поэтому примеры из книги подойдут для любой модели. Каждый вывод GPIO может находиться в одном из двух состояний — высоком (1) или низком (0). В физическом смысле это соответствует напряжениям 3.3 В и 0 В, но в программировании проще думать о них как о логических значениях 1 и 0. Эти сигналы и составляют основу управления физическими устройствами с помощью Raspberry Pi. Рисунок 1.2. Назначение контактов GPIO Raspberry Pi ð ñχôöτñöý öχòþñχíòāχõýöôýϋ õχòþïχëτöυòυφ Библиотеки GPIO Для работы с портами GPIO в Python существует несколько библиотек, каждая из которых предлагает свои подходы и возможности. В этой книге мы будем использовать библиотеку gpiozero — простую, интуитивно понятную и специально разработанную для начинающих разработчиков. Документацию и инструкции по установке можно найти на официальном сайте: https://gpiozero.readthedocs.io/en/stable/. Теперь, когда у нас есть все необходимое, перейдем к созданию проекта «Hello World» в мире физических вычислений с использованием Raspberry Pi. 580 РАЗДЕЛ 6 Взаимодействие с оборудованием
Покупка и сборка оборудования для «Hello World» Для реализации проекта «Hello World» в области физических вычислений вам понадобятся всего два недорогих компонента из экосистемы Grove: » Плата Pi2Grover. Это переходная плата, которая подключается к 40-контактному разъему GPIO Raspberry Pi и преобразует его в стандартные разъемы Grove . Используя такие разъемы, вы избавляетесь от риска неправильно подключить провода или повредить контакты питания . Плату Pi2Grover можно приобрести, например, на Amazon (около $20) или на сайте Tindie: https://www.tindie.com/ products/31862/ . Более подробно о возможностях этой платы будет рассказано в следующей главе . » Модуль Grove с синим (или белым) светодиодом. Этот модуль комплектуется кабелем Grove и стоит примерно $3. Его можно купить на сайте seeedstudio.com. Светодиод подключается к плате Pi2Grover (рисунок 1.3) и управляется напрямую через Raspberry Pi. Цвет светодиода — синий, белый или фиолетовый — значения не имеет: все варианты работают одинаково (рисунок 1.4). В следующей главе мы подробнее рассмотрим, как устроены разъемы Grove и почему они делают работу с физическими проектами на Raspberry Pi такой удобной и безопасной. Рисунок 1.3. Плата Pi2Grover ГЛАВА 1 Введение в физические вычисления 581
Рисунок 1.4. Синий светодиод Grove Если вы впервые собираете проект на базе физического компьютера, выполните следующие действия, чтобы упростить процесс. 1. Совместите плату Pi2Grover с 40-контактным разъемом GPIO на Raspberry Pi (рис . 1 .5) . 2. Аккуратно установите Pi2Grover на гребенку GPIO, убедившись, что все пины совмещены и не погнуты, контакты не выступают ни с одной стороны (рис . 1 .6) . 3. Подключите один конец кабеля Grove к модулю светодиода Grove (рис . 1 .7) . Рисунок 1.5. Выравнивание платы Pi2Grover относительно Raspberry Pi 582 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 1.6. Установленная плата Pi2Grover 4. Если светодиод поставляется отдельно, вставьте его в плату Grove LED, совместив плоскую грань корпуса с плоской меткой на плате (рис . 1 .8) . Рисунок 1.7. Кабель Grove, подключенный к плате светодиода Grove ГЛАВА 1 Введение в физические вычисления 583
Рисунок 1.8. Светодиод, выровненный по контуру на плате 5. Подключите второй конец кабеля Grove в разъем D12/D13 на плате Pi2Grover . Аппаратная часть собрана, можно переходить к написанию программы на Python. Рисунок 1.9. Готовый проект «Hello World» 584 РАЗДЕЛ 6 Взаимодействие с оборудованием
Управление светодиодом с помощью Python Теперь, когда оборудование собрано, можно подать питание на Raspberry Pi. Если все подключено правильно, вы увидите: • • • • загорается светодиод модуля Grove; синий индикатор питания на плате Pi2Grover светится постоянно; желтый светодиод на Raspberry Pi мигает во время загрузки; красный светодиод питания Raspberry Pi загорается (иногда может погаснуть в зависимости от используемого блока питания) . Это означает, что система работает корректно и готова к дальнейшей настройке. ПРИМЕЧАНИЕ Светодиод Grove загорается при подаче питания на Raspberry Pi, потому что при запуске все порты GPIO по умолчанию настроены как входы. В этом состоянии вывод GPIO ничем не управляется, а для того чтобы контролировать светодиод, требуется именно выход. Поэтому контакт находится в неопределенном, так называемом плавающем состоянии, когда напряжение не фиксировано (оно не равно ни 0, ни 3.3 В). Из-за особенностей схемы платы Pi2Grover это плавающее напряжение стремится к высокому уровню (1), и светодиод загорается. Когда в коде вы измените режим GPIO на выход, вывод начнет управляться программно, и светодиод погаснет, пока вы не включите его явно. Для начала выполните следующие шаги: 1. Используя клавиатуру, откройте панель терминала на вашем Raspberry Pi . СОВЕТ Если вы не знаете, как открыть и использовать терминал или командную строку, посетите сайт https://maker.pro/linux/tutorial/how-to-uselinux-shell-terminal-for-raspberry-pi. Там вы найдете простое и понятное руководство по работе с оболочкой Linux и терминалом Raspberry Pi. 2. Введите следующий код Python в текстовый редактор nano (или любой другой редактор по вашему выбору), а затем сохраните файл под именем HelloWorld.py . ГЛАВА 1 Введение в физические вычисления 585
from gpiozero import LED from time import sleep blue = LED(12) while True: blue.on() print( "Светодиод включен") sleep(1) blue.off() print( "Светодиод выключен") sleep(1) 3. Теперь настал важный момент — запуск программы . СОВЕТ В командной строке терминала введите следующую команду и нажмите Enter: sudo python3 HelloWorld.py Светодиод начнет мигать с интервалом в одну секунду, а в окне терминала вы увидите примерно такой вывод: Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод Светодиод включен выключен включен выключен включен выключен включен выключен включен выключен включен Чтобы остановить выполнение программы, нажмите сочетание клавиш Ctrl+C (в программистском жаргоне обозначается как ^C). СОВЕТ Ключевое слово sudo означает «выполнить от имени суперпользователя». Мы используем его перед командой python3, потому что в некоторых версиях операционной системы Raspberry Pi доступ к отдельным портам и функциям ограничен для обычных пользователей. Запуск с sudo предоставляет программе повышенные права, гарантируя, что она сможет работать корректно. В последних версиях Raspberry Pi OS это обычно не требуется — достаточно ввести: python3 HelloWorld.py. Если программа не запускается или выдает ошибку доступа, попробуйте выполнить ее с правами суперпользователя: sudo python3 HelloWorld.py Следующий оператор импортирует функцию LED из библиотеки Python gpiozero: 586 РАЗДЕЛ 6 Взаимодействие с оборудованием
from gpiozero import LED Затем импортируется функция sleep из библиотеки Python time: from time import sleep Далее вы назначаете светодиод на GPIO 12 (помните D12/D13 на плате Pi2Grover из пятого шага предыдущего раздела?): blue = LED(12) Теперь запускается бесконечный цикл: while True: Включаете светодиод: blue.on() print( "Светодиод включен") Затем программа ждет одну секунду: sleep(1) После этого светодиод выключается: blue.off() print( "Светодиод выключен") sleep(1) Программа продолжает выполняться в бесконечном цикле, пока вы ее не остановите. Поздравляем! Вы только что сделали свой первый шаг в мир физических вычислений. В следующей части книги вы научитесь управлять роботами с помощью Python. Но это еще не все Раз все оборудование уже настроено, сделаем следующий небольшой проект: изменим яркость светодиода с помощью широтно-импульсной модуляции. ГЛАВА 1 Введение в физические вычисления 587
ПРИМЕЧАНИЕ ШИМ — это способ управления сигналом, при котором регулируется доля времени, когда он находится в состоянии 1, по сравнению со временем в состоянии 0. Наш светодиод светится при уровне 1 и гаснет при уровне 0, поэтому, изменяя отношение времени включения к времени выключения, можно управлять видимой яркостью для человеческого глаза. Это отношение называют рабочим циклом и его удобно представить как на рисунке 1.10: при рабочем цикле 100% светодиод все время включен, при 0% — постоянно выключен. Меняя длительность включенного состояния, мы соответствующим образом меняем воспринимаемую яркость. По умолчанию светодиод включается и выключается со скоростью 100 раз в секунду (100 Гц), но при необходимости эту частоту можно настроить (рисунок 1.10).  ñχÿ÷÷ðøðυôöïτõχòôυôðā   ëñòĀùυôχ   ëýñòĀùυôχ  ñχÿ÷÷ðøðυôöïτõχòôυôðā   ëñòĀùυôχ   ëýñò  ñχÿ÷÷ðøðυôöïτõχòôυôðā   Рисунок 1.10. Рабочие циклы ëñò   ëýñòĀùυôχ Введите следующий код Python в редакторе nano и сохраните файл под именем HelloWorld2.py. В этой программе используется функция PWMLED(12), которая подключает светодиод к GPIO-выводу 12 и позволяет управлять его яркостью с помощью широтно-импульсной модуляции (ШИМ): from gpiozero import PWMLED from time import sleep led = PWMLED(12) while True: led.value = 0 # выключен sleep(1) led.value = 0.5 # половинная яркость sleep(1) 588 РАЗДЕЛ 6 Взаимодействие с оборудованием
led.value = 1 sleep(1) # полная яркость Запустите программу, введя в терминале команду: sudo python3 HelloWorld2.py После запуска вы увидите, как яркость светодиода будет изменяться каждую секунду. Чтобы сделать эффект более плавным — с непрерывным увеличением и уменьшением яркости, — используйте следующий код: from gpiozero import PWMLED from signal import pause led = PWMLED(12) led.pulse() pause() Теперь светодиод будет мягко «дышать», постепенно усиливая и ослабляя свет. Отличная работа! В этой главе вы сделали важный шаг в освоении физических вычислений. У вас уже есть ваш первый работающий проект — мигающий и пульсирующий светодиод. ПОЧЕМУ СВЕТОДИОД КАК БУДТО МЕРЦАЕТ? Когда вы запускаете свою программу на Python, она работает не в одиночку: на Raspberry Pi параллельно выполняются десятки процессов . Убедиться в этом легко — введите в терминале команду ps xaf . Поскольку операционная система Raspberry Pi многозадачная, вашей программе, реализующей программный ШИМ, не всегда достается процессор в нужный момент . Из-за этих задержек период включения и выключения слегка «плывет», и глаз воспринимает это как едва заметное мерцание светодиода . На плате есть и аппаратные ШИМ-выводы GPIO, которые можно использовать для управления светодиодами (при условии, что вы не задействуете аудиовыход Raspberry Pi) . На Raspberry Pi 3B+ мерцание обычно почти не видно, если система не сильно загружена другими задачами .
В этой главе вы узнаете: » Как подключать аппаратные компоненты друг к другу » Как выбрать основную управляющую плату » Какие существуют четыре типа датчиков и как с ними работать » Как использовать различные кабели Grove Гл а в а 2 Никакой пайки! Использование разъемов Grove для конструирования G rove — это модульная, стандартизированная система разъемов, предназначенная для быстрого прототипирования электронных устройств. Она использует блочный подход, который значительно упрощает процесс сборки, экспериментов и обучения по сравнению с традиционными способами подключения — такими как пайка или использование перемычек. Главная особенность Grove заключается в простоте и безопасности подключения: модули соединяются с помощью универсальных кабелей, что исключает ошибки с полярностью питания и делает процесс сборки интуитивным даже для начинающих. Однако, несмотря на простоту использования, работа с Grove все же требует базовых знаний в электронике и понимания принципов работы датчиков и исполнительных устройств. В этой главе вы узнаете, что такое разъемы Grove, как они устроены, каких типов бывают, а также рассмотрите примеры модулей Grove для каждого из этих соединений. 590 РАЗДЕЛ 6 Взаимодействие с оборудованием
Работа с системой Grove Система Grove представляет собой модульную платформу, состоящую из базового блока и множества модулей со стандартизированными разъемами. Базовый блок служит промежуточным звеном между микроконтроллером или одноплатным компьютером (таким как Raspberry Pi или Arduino) и подключаемыми устройствами. Он обеспечивает простое и надежное соединение без необходимости пайки или использования макетных плат. Каждый модуль Grove выполняет одну конкретную функцию — например, может быть кнопкой, светодиодом, дисплеем, двигателем или датчиком для измерения температуры, влажности, уровня освещенности, расстояния или даже частоты сердечных сокращений. Выбор базового блока Grove Базовый блок Grove представляет собой контроллер или плату расширения (иногда называемую шилдом), к которой подключаются различные модули Grove. Он выполняет роль центрального узла системы: обеспечивает вычислительную мощность и координирует работу подключенных устройств. Модули же предоставляют данные с датчиков или управляют исполнительными устройствами — например, светодиодами, двигателями или реле. Хотя использование базового блока значительно упрощает сборку системы, его применение не является обязательным. Модули Grove можно подключать напрямую к Raspberry Pi или Arduino с помощью специальных кабелей-преобразователей (с разъема Grove на стандартные штыри GPIO). Такой способ мы рассмотрим позже в разделе «Подключение с помощью кабелей Grove». Однако прямое соединение через штыревые разъемы требует большей аккуратности и опыта — неправильное подключение может привести к ошибкам или повреждению оборудования. Поэтому для начала и для учебных проектов лучше использовать базовый блок Grove: он надежнее, проще и снижает вероятность ошибок при подключении. ГЛАВА 2 Никакой пайки! Использование разъемов Grove 591
Базовый блок Arduino В этой книге основное внимание уделяется работе с Raspberry Pi, однако, как уже упоминалось ранее, существует множество других платформ для создания проектов — например, широко распространенный Arduino. Для микроконтроллеров Arduino разработано несколько удобных базовых плат расширения, которые обеспечивают простое и надежное подключение множества модулей Grove. Эти платы устанавливаются поверх Arduino и предоставляют стандартные порты для подключения различных сенсоров и актуаторов без необходимости пайки или сложных соединений. На рисунке 2.1 показан пример базового блока, разработанного специально для Arduino Uno. Существуют также версии базовых блоков Grove, совместимые с другими моделями, такими как Arduino Mega, Due и др. Рисунок 2.1. Базовая плата Grove для Arduino Uno Некоторые модели Arduino, например Mini Pro LP (рисунок 2.2), уже оснащены встроенными разъемами Grove. Благодаря этому такие платы можно использовать без дополнительного базового блока. 592 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 2.2. Плата Arduino Mini Pro LB с разъемами Grove Базовый блок Raspberry Pi Базовый блок Raspberry Pi имеет меньший выбор по сравнению с платформой Arduino. Многие доступные базовые устройства для Pi оказываются слишком «умными», они содержат собственные контроллеры и программное обеспечение, которые скрывают от пользователя доступ к реальной аппаратной части Raspberry Pi. Такой дополнительный уровень абстракции становится проблемой, когда требуется напрямую управлять оборудованием через Python. Для обучения и разработки проектов, где важно полное понимание взаимодействия с железом, лучше использовать более простые решения. Именно поэтому в этой книге применяется базовый блок Pi2Grover для ГЛАВА 2 Никакой пайки! Использование разъемов Grove 593
Raspberry Pi, изображенный на рисунке 2.3. Эта плата выполняет функцию преобразователя уровней напряжения, она согласует 3,3 В Raspberry Pi с 5 В, необходимыми для большинства модулей Grove. При этом Pi2Grover не ограничивает доступ к портам GPIO, что позволяет свободно писать и использовать собственные драйверы на Python. Рисунок 2.3. Плата Pi2Grover в работе на Raspberry Pi Защита от ошибок с помощью разъема Grove Разъем Grove — это стандартный четырехконтактный интерфейс, предназначенный для безопасного и простого подключения модулей к базовым платам и контроллерам. Его ключевая особенность заключается в том, что конструкция разъема предотвращает неправильное подключение: штекер вставляется только в одном положении. Существуют четыре типа разъемов Grove, каждый из которых предназначен для определенного типа сигналов. Если случайно подключить модуль к неподходящему порту, ничего критичного не произойдет — устройство просто не будет работать, но при этом не повредится. 594 РАЗДЕЛ 6 Взаимодействие с оборудованием
Это важное преимущество системы Grove: стандартные разъемы защищают оборудование от ошибок подключения, исключая риск перепутать линии питания и заземления, что часто приводит к поломке микросхем и датчиков при работе с обычными штыревыми разъемами. ОСТОРОЖНО Есть одно важное исключение. Если вы подключите 3,3-вольтовый модуль I2C, который не рассчитан на работу с 5 В, к 5-вольтовому разъему I2C Grove, вы можете повредить устройство. Чтобы избежать подобных проблем, в этой книге мы используем только модули, рассчитанные на 5 В. Существуют четыре основных типа разъемов Grove. На рисунке 2.4 показан штекерный разъем Grove. Все кабели Grove имеют одинаковую конструкцию и могут использоваться взаимозаменяемо, различие заключается лишь в типе передаваемого сигнала. Как уже отмечалось, даже если вы подключите кабель Grove к неправильному типу разъема, ничего не выйдет из строя, питание и земля защищены от короткого замыкания. Рисунок 2.4. Разъем Grove Разводка всех разъемов Grove одинакова: » » » » Контакт 1 — сигнал 1 . Контакт 2 — сигнал 2 . Контакт 3 — питание . Контакт 4 — заземление . Цветовая схема проводов также всегда одинакова (рисунок 2.5): ГЛАВА 2 Никакой пайки! Использование разъемов Grove 595
» » » » Желтый провод — контакт 1 . Белый провод — контакт 2 . Красный провод — питание (контакт 3) . Черный провод — земля (контакт 4) . Рисунок 2.5. Кабели Grove длиной 5 см ЧТО ТАКОЕ НАПРЯЖЕНИЕ? Разница между 3,3 В и 5 В — это разница в уровне электрического «давления» . Буква «В» обозначает вольты, и напряжение можно сравнить с давлением воды в трубе: чем выше давление, тем сильнее поток . Аналогично, чем выше напряжение, тем больше тока протекает по цепи . Но если давление становится слишком высоким, труба может лопнуть, так же и слишком высокое напряжение может повредить входы Raspberry Pi, рассчитанные только на 3,3 В . Поэтому мы используем плату Pi2Grover, которая преобразует и буферизует все сигнальные линии, приводя 3,3 В от Raspberry Pi к уровню 5 В, подходящему для большинства модулей Grove . Напряжение всегда измеряется относительно определенной точки — обычно это земля . Общее заземление важно, чтобы все устройства в системе «видели» одинаковые уровни напряжения . Отсутствие общего заземления может вызвать сбои и нестабильность . Как гласит второй закон Шовика: «Матери можно доверять всегда, а вот заземлению — никогда» . (А первый закон Шовика звучит так: «Оно работает лучше, если включено в розетку!») 596 РАЗДЕЛ 6 Взаимодействие с оборудованием
Разъемы Grove Теперь пришло время подробнее рассмотреть четыре типа разъемов Grove, которые применяются для подключения датчиков и исполнительных устройств. Каждый тип разъема предназначен для передачи определенных сигналов и играет свою роль в работе системы. Если вы случайно подключите модуль к неправильному разъему, ничего страшного не произойдет — оборудование не повредится. Однако ваш проект просто не будет функционировать так, как задумано. Цифровой Grove — все о единицах и нулях Компьютеры обмениваются данными между собой и с внешними устройствами с помощью цифровых битов. Информацию из этих битов можно получить двумя способами: 1. По их значению (1 или 0) . 2. По временным характеристикам — например, по тому, как долго бит остается равным 1 . Именно временные параметры приводят нас к последовательным портам Grove, о которых речь пойдет позже в этой главе. ПРИМЕЧАНИЕ Многие датчики работают всего с одним или двумя битами. Бит — это фундамент цифровых вычислений, и он может принимать только одно из двух значений: 1 или 0. Хотя физически биты реализованы через уровни напряжения, на логическом уровне они интерпретируются как два состояния — «включено» (1) и «выключено» (0). Цифровой разъем Grove состоит из четырех стандартных линий. Две из них — сигнальные (контакты 1 и 2), обычно обозначаемые как D0 и D1. Большинство модулей использует только D0, но некоторые устройства (например, светодиодная линейка Grove) работают с обеими линиями. На базовых блоках Grove разъемы часто маркируются как D0/D1, D1/D2, D2/D3 и т.д. Это означает, что контакт 1 соединен с первым цифровым выводом (например, D0), а контакт 2 — со следующим (D1). В таблице 2.1 приведено описание контактов цифрового разъема Grove. ГЛАВА 2 Никакой пайки! Использование разъемов Grove 597
Таблица 2.1. Цифровой разъем Grove Номер контакта и цвет провода Имя Описание 1 (желтый) D0 Основной цифровой вход/выход (Primary digital input/output 2 (белый) D1 Вторичный цифровой вход/выход (Secondary digital input/output 3 (красный) VCC Питание для модуля Grove (5В или 3,3В) 4 (черный) GND Земля (Ground) Некоторые цифровые модули Grove отличаются простотой, а другие обладают довольно сложной внутренней схемой. К распространенным примерам цифровых модулей относятся: • • • Модули переключателей — обычные кнопки или тумблеры, которые передают компьютеру сигнал 1 или 0 в зависимости от положения . Модуль вентилятора — управляет включением и выключением небольшого вентилятора по цифровому сигналу . Светодиодный модуль — включает или выключает светодиод в ответ на цифровое значение . На рисунке 2.6 показан разъем Grove, изображенный на принципиальной схеме светодиодного модуля Grove из предыдущей главы. Этот пример помогает понять, как реализуется простейшее цифровое соединение. Таким образом, цифровые модули Grove могут быть как очень простыми (например, обычная кнопка), так и достаточно продвинутыми — с микроконтроллерами, управляющими несколькими компонентами и сигналами. Рисунок 2.6. Простой цифровой модуль Grove со светодиодом 598 РАЗДЕЛ 6 Взаимодействие с оборудованием
Grove аналоговый: когда единиц и нулей недостаточно ПРИМЕЧАНИЕ Аналоговый разъем Grove используется тогда, когда простого переключения между 1 и 0 недостаточно, и требуется измерять значения, которые могут изменяться постепенно — например, температуру, освещенность или уровень звука. Аналоговый разъем Grove, как и другие, имеет четыре стандартные линии. Две из них — сигнальные, обычно обозначаемые как A0 и A1. Большинство модулей использует только A0. На базовых блоках разъемы часто пронумерованы последовательно: A0/A1, затем A1/A2 и т.д. Принцип работы аналогового разъема можно понять на примере простого делителя напряжения, показанного на рисунке 2.7. Этот делитель снижает входное напряжение (например, с 5 В до 2,5 В), создавая аналоговый сигнал, величина которого зависит от положения переключателя или силы внешнего воздействия. Такой сигнал затем считывается через A0 и преобразуется в цифровое значение. В таблице 2.2 приведено описание каждого контакта аналогового разъема Grove. Рисунок 2.7. Простой делитель напряжения Grove для аналогового входа ГЛАВА 2 Никакой пайки! Использование разъемов Grove 599
Таблица 2.2. Аналоговый разъем Grove Номер контакта и цвет провода Имя Описание 1 (желтый) A0 Основной аналоговый вход (Primary analog input) 2 (белый) A1 Вторичный аналоговый вход (Secondary analog input) 3 (красный) VCC Питание для модуля Grove (5В или 3,3В) 4 (черный) GND Земля (Ground) . Примеры аналоговых модулей Grove: • • • Потенциометр — регулируемый резистор, создающий переменное напряжение . Делитель напряжения — используется для измерения разницы потенциалов . Датчик качества воздуха Grove — измеряет концентрацию загрязняющих веществ и выдает аналоговый сигнал, соответствующий уровню загрязнения . Grove UART (или последовательный интерфейс) — побитовая передача ПРИМЕЧАНИЕ В предыдущих разделах этой главы мы говорили о цифровых сигналах и о том, как можно передавать информацию не только через значение сигнала (1 или 0), но и через время, в течение которого сигнал находится в одном из этих состояний (временные характеристики). Эта временная информация лежит в основе передачи последовательного сигнала. Например, восемь отдельных битов, таких как 01000001, отправленных с определенной скоростью, могут представлять букву A. Скорость передачи битов называется скоростью передачи данных или бодрейтом. Термин «бод» происходит от имени Эмиля Бодо — изобретателя и ученого, который в конце 1800-х годов внес значительный вклад в развитие телеграфа. Модуль Grove UART (универсальный асинхронный приемопередатчик) — это разновидность цифрового модуля Grove, которая использует не только цифровые уровни сигнала (1 и 0), но и временные характеристики для передачи и приема данных. 600 РАЗДЕЛ 6 Взаимодействие с оборудованием
Он задействует контакт 1 и контакт 2 для последовательного обмена информацией — первый для приема, второй для передачи. Разъем Grove UART (или последовательный интерфейс) маркируется со стороны базового блока: контакт 1 обозначается как RX (прием данных, вход), а контакт 2 — как TX (передача данных, выход). Описание каждого контакта разъема Grove UART приведено в таблице 2.3. Таблица 2.3. Последовательный разъем Grove UART Номер контакта и цвет провода Имя Описание 1 (желтый) RX Последовательный прием данных (Serial Receive) — со стороны базового устройства (а не платы Grove) 2 (белый) TX Последовательная передача данных (Serial Transmit) — со стороны базового устройства (а не платы Grove) 3 (красный) VCC Питание для модуля Grove (5В или 3,3В) 4 (черный) GND Земля (Ground) К примерам модулей Grove UART относятся беспроводные модули XBee и считыватель RFID, работающий на частоте 125 кГц, показанный на рисунке 2.8. Рисунок 2.8. Считыватель RFID Grove UART ГЛАВА 2 Никакой пайки! Использование разъемов Grove 601
Grove I2C — использование I2C для понимания окружающего мира Наши любимые устройства для подключения к небольшим компьютерам — это датчики I2C (межмикросхемная шина). На рынке доступны сотни типов недорогих датчиков I2C. И множество типов датчиков Grove I2C готовы к подключению и работе! АНАЛОГОВЫЙ ПРОТИВ ЦИФРОВОГО: ДИСКУССИЯ ПРОДОЛЖАЕТСЯ Различия между аналоговыми и цифровыми сигналами одновременно просты и сложны . Цифровой сигнал может принимать только два значения — 1 или 0 . Аналоговые же сигналы могут иметь любые значения, например 1,2 В, 3,14198 В или любое другое число с плавающей точкой . Проще говоря, аналоговое напряжение может изменяться в пределах диапазона, а цифровое — нет . В устройствах, о которых идет речь, значение 1 обычно соответствует 5 В, а значение 0–0 В . Однако на практике все немного сложнее: любой сигнал выше примерно 2,5 В может считаться единицей, а любой сигнал ниже 0,7 В — нулем при считывании цифровым портом . В этой книге мы не будем углубляться в эти тонкости и будем считать сигналы либо цифровыми, либо аналоговыми . Аналоговые сигналы применяются, когда важно знать точное значение напряжения . Например, если напряжение 1,420 В от датчика влажности указывает на пересушенное растение, а 3,342 В — на достаточно увлажненное, то значения между ними позволяют определить степень сухости почвы . Чтобы компьютер мог обработать такую информацию, аналоговое напряжение преобразуется в цифровое число с помощью аналого-цифрового преобразователя (АЦП) . После этого система может «понять», что растению нужна вода . Датчик, показанный на рисунке 2.9, — это датчик солнечного света SI1145 с интерфейсом I2C. Он способен измерять не только интенсивность видимого света, но также инфракрасное (ИК) и ультрафиолетовое (УФ) излучение. Этот доступный по цене датчик может определить, насколько высок риск солнечного ожога для человека, а также оценить, достаточно ли света получают ваши растения. Сам сенсор — это небольшой 602 РАЗДЕЛ 6 Взаимодействие с оборудованием
цветной чип с маркировкой U1 и прозрачным верхом, через который проходит свет. Рисунок 2.9. Датчик солнечного света Grove I2C Невозможно не восхищаться тем, какие возможности дают современные компьютеры. ОСТОРОЖНО Большинство датчиков с интерфейсом I2C совместимы как с 3,3-вольтовыми, так и с 5-вольтовыми базовыми блоками, однако некоторые устройства рассчитаны только на одно из этих напряжений. Поэтому перед подключением необходимо внимательно проверять технические характеристики. Если подключить 3,3.вольтовый I2C-датчик к 5.вольтовому разъему Grove, есть большая вероятность повредить устройство. В таблице 2.4 приведено описание контактов разъема Grove. Таблица2.4. Разъем Grove I2C Номер контакта и цвет провода Имя Описание 1 (желтый) SCL Линия синхронизации I2C (I2C clock line) — управляет передачей данных между устройствами 2 (белый) SDA Линия данных I2C (I2C data line) — передает данные между устройствами 3 (красный) VCC Питание для модуля Grove (5В или 3 .3В) 4 (черный) GND Земля (Ground) ГЛАВА 2 Никакой пайки! Использование разъемов Grove 603
Разъем Grove I2C имеет стандартную схему подключения. Контакт 1 используется для сигнала тактирования I2C (SCL), а контакт 2 — для сигнала данных I2C (SDA). Питание и заземление остаются такими же, как у других разъемов Grove. Во многих контроллерах, таких как ESP8266, Raspberry Pi или Arduino, шина I2C реализуется через обычные цифровые порты ввода-вывода. Однако Raspberry Pi и Arduino имеют аппаратную поддержку интерфейса I2C, что делает работу с ним более стабильной и быстрой. В то время как в ESP8266 I2C реализован программно — методом, который в 90-х называли «программной эмуляцией». Подключение с помощью кабелей Grove Существуют кабели Grove различной длины — от 5 до 50 см (рисунок 2.10). На каждом их конце расположен стандартный разъем Grove, благодаря чему такие кабели можно свободно взаимозаменять. Они используются для подключения различных датчиков и модулей к Raspberry Pi. Если вам нужно подключить устройство или датчик, не оснащенный разъемом Grove, можно воспользоваться соединительным или адаптерным кабелем Grove. Такой кабель позволяет преобразовать штыревые контакты в разъем Grove. Различают два типа адаптерных кабелей. Первый тип — это кабель с разъемом Grove на одном конце и гнездовыми контактами на другом, как показано на рисунке 2.11. Рисунок 2.10. 20-сантиметровые кабели Grove 604 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 2.11. Кабели Grove с гнездовыми разъемами Второй тип адаптерного кабеля — это кабель с разъемом Grove на одном конце и штыревыми контактами на другом (рисунок 2.12). Для сравнения, на рисунке 2.10 показаны стандартные кабели Grove с разъемами на обоих концах. СОВЕТ Главное преимущество такого кабеля заключается в возможности подключения к Raspberry Pi датчиков и устройств, которые изначально не оснащены разъемами Grove. Рисунок 2.12. Кабели Grove со штыревыми контактами ГЛАВА 2 Никакой пайки! Использование разъемов Grove 605
ОСТОРОЖНО Чтобы воспользоваться соединительным кабелем, необходимо точно определить, какой контакт к какому выводу следует подключить — это зависит от типа используемого датчика и интерфейса. Разъемы Grove поддерживают четыре типа интерфейсов, которые были описаны ранее в этой главе. Перед подачей питания обязательно внимательно проверьте правильность всех подключений. Ошибка в подключении может привести к неправильной работе устройства или повреждению компонентов. GPS-модуль Adafruit Ultimate Примером использования соединительного кабеля Grove может служить подключение GPS-модуля, который работает через последовательный интерфейс UART. Например, модуль Adafruit Ultimate GPS подключается к Raspberry Pi или Arduino именно таким способом. Чтобы использовать его с разъемами Grove, нужно соединить кабель следующим образом: • • • • СОВЕТ Контакт 1 — Желтый провод (TX) Контакт 2 — Белый провод (RX) Контакт 3 — Красный провод (VIN, питание) Контакт 4 — Черный провод (GND, земля) Важно помнить, что при использовании последовательных соединений есть одна особенность: линии передачи и приема данных должны быть перекрещены. Это означает, что RX на разъеме Grove нужно подключить к TX на датчике, а TX на разъеме Grove — к RX на датчике. Если перепутать эти линии, связь между устройствами не установится — это самая распространенная ошибка при подключении устройств через последовательный интерфейс (cм. рисунок 2.13) 606 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 2.13. Крупный план GPS-модуля Adafruit с подключенным соединительным кабелем Grove Этот пример показывает, насколько легко подключать платы, не входящие в систему Grove, к Raspberry Pi. Используя простой переходник и упомянутую ранее плату Adafruit, вы можете добавить GPS-функциональность — ваш Raspberry Pi сможет получать координаты и определять свое текущее местоположение. И это только один из множества вариантов: к Raspberry Pi можно подключить самые разные датчики и модули с помощью соединительных кабелей Grove. Теперь, когда вы разобрались с принципами подключения модулей, кабелей и разъемов Grove, можно переходить к созданию более сложных и интересных проектов в следующей главе.
В этой главе вы узнаете: » Как использовать датчики I2C » Как измерять параметры окружающей среды с помощью Raspberry Pi » Как собирать и сохранять данные » Как подключить Python к вашему смартфону Гл а в а 3 Считывание данных об окружающем мире С уществует огромное количество датчиков, которые можно использовать с Raspberry Pi и другими небольшими компьютерами. Они позволяют решать самые разные задачи — от определения присутствия человека с помощью пассивного инфракрасного (ПИК) датчика до измерения параметров окружающей среды, таких как температура, влажность, давление, качество воздуха и многое другое. Эти устройства позволяют вашему компьютеру недорого и эффективно взаимодействовать с физическим миром. В этой главе вы узнаете, как с помощью Python научить ваш компьютер обмениваться данными с такими датчиками, считывать показания и использовать их для анализа или управления внешними устройствами. Чтобы загрузить код для этой главы, перейдите на сайт www.dummies.com/ go/pythonaiofd3e, прокрутите страницу до раздела Downloads и выберите ссылку для скачивания кода из раздела 6 (Book 6). 608 РАЗДЕЛ 6 Взаимодействие с оборудованием
Понимание I2C Чтобы компьютер мог взаимодействовать с датчиками, ему необходим интерфейс. Он состоит из двух частей: аппаратной, включающей контакты, типы и уровни напряжения, и программной, обычно называемой драйвером или программным интерфейсом приложения (API). В контексте этой книги роль программного интерфейса выполняет модуль Python. Существуют четыре основных способа получения данных от внешних датчиков: » Цифровой ввод — использование портов ввода-вывода общего назначения, настроенных как входные линии . » Аналоговый ввод — получение аналоговых сигналов, которые проходят через аналого-цифровой преобразователь (АЦП), чтобы компьютер мог их считать . » Цифровой I2C — универсальный интерфейс для обмена данными с датчиками . Его название произносится как «айквадрат-си расшифровывается как межмикросхемная шина (Inter-Integrated Circuit) . » Цифровой SPI — интерфейс, использующий единицы, нули и временные характеристики для последовательного обмена данными между устройствами . В этой книге мы работаем с датчиками, использующими цифровые входы, аналоговые входы и интерфейс I2C. Интерфейс SPI во многом схож с I2C, но I2C проще в использовании. Большинство устройств, поддерживающих SPI, также имеют встроенный интерфейс I2C. Чтобы понять, как работает интерфейс I2C, нужно знать, что такое шина I2C. Она используется для связи с микросхемами или датчиками, которые расположены на одной плате или физически близко к процессору. Шина I2C была разработана компанией Phillips (ныне NXP Semiconductors). Чтобы избежать проблем с лицензированием, ее также называют TWI — двухпроводным интерфейсом (Two Wire Interface). Существует также SMBus, разработанная компанией Intel, являющаяся более строгим подмножеством I2C. И Arduino, и Raspberry Pi поддерживают эту шину. Каждый датчик, подключенный к шине I2C, имеет уникальный адрес. Например, адрес датчика температуры и влажности HDC1080, используемого в этой главе, равен 0x40. Префикс 0x указывает на то, что это число записано в шестнадцатеричной системе счисления. ГЛАВА 3 Считывание данных об окружающем мире 609
Интерфейс I2C идеально подходит для медленных и редко опрашиваемых периферийных устройств — таких как температурные датчики, показания которых меняются постепенно. Напротив, устройства, генерирующие большие объемы данных (например, камеры), обычно используют другие интерфейсы. Шина I2C использует всего две двунаправленные линии с открытым стоком: SCL — линия тактового сигнала; SDA — линия передачи данных. Термин «открытый сток » означает, что устройство может опустить уровень сигнала до земли (0), но не может поднять его до высокого уровня (1). Поэтому обе линии должны быть подключены к питанию (Vdd) через подтягивающие резисторы. Отсутствие этих резисторов — самая распространенная ошибка при работе с I2C. Плата Pi2Grover, используемая в этой книге, уже содержит встроенные подтягивающие резисторы на 10 кОм, поэтому вам не нужно устанавливать их вручную. На шине I2C различают два типа устройств: • • ведущее устройство (master) — управляет обменом данными (в нашем случае это Raspberry Pi); ведомые устройства (slaves) — датчики, каждый из которых имеет уникальный 7-битный адрес . Иллюстрация на рисунке 3.1 показывает типичную структуру подключения таких устройств к шине I2C. âðôτ-' Þωöψχφωöëχ Рисунок 3.1. Шина I2C 610 Þωöψχφωöëχ Þωöψχφωöëχ νυωñχòþñχϊωöψχφωöëôτɹχêûυφúðôυ-' РАЗДЕЛ 6 Взаимодействие с оборудованием
Протокол определяет поведение и порядок взаимодействия устройств, подключенных к шине. Протокол I2C включает три основных типа сообщений: » Одиночное цифровое сообщение при записи — когда управляющее устройство (master) передает данные подчиненному устройству (slave) . » Одиночное цифровое сообщение при чтении — когда управляющее устройство запрашивает и получает данные от подчиненного устройства . » Комбинированное цифровое сообщение — когда управляющее устройство выполняет несколько операций чтения и/или записи подряд, обращаясь к одному или нескольким подчиненным устройствам . К счастью, вся сложная работа с низкоуровневым протоколом I2C уже реализована в драйверах и библиотеках Python. Благодаря этому вы можете использовать шину I2С без необходимости вручную управлять сигналами или синхронизацией — все это выполняется автоматически. Включение I2C на Raspberry Pi Чтобы использовать шину I2C на Raspberry Pi, необходимо убедиться, что она активирована в операционной системе. Отличное пошаговое руководство по настройке I2C представлено на сайте Adafruit: https://learn. adafruit.com/adafruits-raspberry-pi-lesson-4.gpio-setup/configuringi2c После включения I2C можно легко проверить, работает ли она корректно. Для этого откройте панель терминала и введите следующую команду: sudo i2cdetect -y 1 Если в ответ на экране появляется сообщения: -bash: i2cdetect: command not found это означает, что на Raspberry Pi не установлена утилита для работы с I2C. В этом случае нужно установить пакет i2c-tools, так что обратитесь к секции с обучением. Если шина I2C активирована и подключено устройство, вы увидите таблицу вроде этой: ГЛАВА 3 Считывание данных об окружающем мире 611
00: 10: 20: 30: 40: 50: 60: 70: 0 1 2 -------- -------- -------- 3 --------- 4 --------- 5 --------- 6 --------- 7 --------- 8 --------- 9 -------- a -------- b -------- c -------- d -------- e -------- f -------- значит, все прошло успешно! Тире означают, что на шине I2C нет подключенных датчиков. Оборудование для измерения температуры и влажности Чтобы начать работу с устройством I2C, его необходимо физически подключить к шине I2C вашего Raspberry Pi. В качестве примера используем датчик температуры и влажности HDC1080, изображенный на рисунке 3.2. Это популярный, недорогой и точный датчик, который можно приобрести, например, на Amazon (https://amzn.to/3S5solv). Для его работы нужно подключить всего четыре провода: • • • • VCC (или VDD) — питание (5 В или 3,3 В, в зависимости от версии датчика); GND — заземление; SCL — линия тактирования I2C; SDA — линия данных I2C . Подключение можно выполнить с помощью платы Pi2Grover, если вы используете систему Grove, или напрямую к разъему GPIO Raspberry Pi. Если у вашего датчика HDC1080 контакты не припаяны, их потребуется припаять. Это простая операция, особенно если вы используете паяльник малой мощности. В интернете доступно множество коротких видео и текстовых руководств по пайке — они помогут вам сделать это аккуратно и безопасно. 612 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 3.2. Датчик температуры и влажности HDC1080 ДАТЧИК ТЕМПЕРАТУРЫ И ВЛАЖНОСТИ HDC1080 — это компактное и высокоточное устройство, особенно впечатляющее своей функциональностью при стоимости около 9 долларов . Он работает по интерфейсу I2C и имеет фиксированный адрес 0x40 . Модуль Grove с датчиком HDC1080 основан на сенсорном чипе Texas Instruments HDC1080, который обеспечивает превосходную точность измерений при очень низком энергопотреблении . Датчик использует емкостной принцип измерения влажности, что позволяет получать стабильные и точные результаты даже при изменении внешних условий . Оба сенсора — температуры и влажности — проходят заводскую калибровку, что избавляет от необходимости настраивать их вручную . HDC1080 выпускается в корпусе WLCSP (Wafer Level Chip Scale Package) — это сверхкомпактная форма упаковки, позволяющая легко интегрировать устройство в различные проекты . Основные характеристики HDC1080: • Диапазон рабочих температур: от –40 °C до +125 °C . • Диапазон измерения относительной влажности: от 0 до 100% . • Точность измерения температуры: ±0,2 °C . • Точность измерения влажности: ±3% . Этот датчик отлично подходит для проектов мониторинга окружающей среды, домашних метеостанций и систем умного дома . ГЛАВА 3 Считывание данных об окружающем мире 613
Примечание: если вы приобрели датчик не из линейки Grove на Amazon, вам понадобится соединительный кабель с гнездовыми разъемами для подключения к системе Grove — подробно это описано в главе 2 данного раздела. В любом случае для подключения Raspberry Pi к системе Grove потребуется преобразователь Pi2Grover, рассмотренный в главах 1 и 2, который можно приобрести на сайте https://shop.switchdoc.com. Теперь установите датчик HDC1080 с интерфейсом I2C на Raspberry Pi, выполнив следующие шаги: 1. ЗАПОМНИТЕ Выключите Raspberry Pi, введя команду sudo halt в командной строке (или выбрав пункт «Shutdown» в графическом меню). После того как желтый индикатор перестанет мигать, отключите питание Raspberry Pi. Команда sudo halt подготавливает SD-карту к безопасному завершению работы . Простое отключение питания может привести к повреждению карты, а восстановление ее — процесс долгий и трудоемкий . Поэтому всегда выключайте Raspberry Pi корректно перед подключением или отключением любых устройств . Исключение составляют только USB-порты, аудиокабели и кабели Ethernet, поддерживающие «горячее» подключение . 2. Подключите кабель Grove к датчику HDC1080, как показано на рисунке 3.3. ЗАПОМНИТЕ В примере используется HDC1080 от SwitchDoc Labs . Если ваш датчик не относится к серии Grove, воспользуйтесь соединительным кабелем Grove, описанным в главе 2 . Рисунок 3.3. HDC1080 с подключенным кабелем Grove 614 РАЗДЕЛ 6 Взаимодействие с оборудованием
3. Подключите второй конец кабеля Grove к одному из разъемов Grove с маркировкой I2C на плате Pi2Grover, установленной на Raspberry Pi (рисунок 3.4). Примечание: шина I2C объединяет все соответствующие разъемы, поэтому можно использовать любой из четырех I2C-разъемов . Рисунок 3.4. HDC1080, подключенный к Raspberry Pi 4. Включите питание Raspberry Pi и откройте окно Терминала. 5. В Терминале введите команду sudo появится таблица: 00: 10: 20: 30: 40: 50: 60: 70: 0 1 2 ---40 ---- -------- -------- 3 --------- 4 --------- 5 --------- 6 --------- 7 --------- 8 -------- 9 -------- i2cdetect -y 1 . На экране a -------- b -------- c -------- d -------- e -------- f -------- Адрес 0x40, по которому работает HDC1080, будет отображен в таблице — это подтверждает, что Raspberry Pi успешно распознал устройство. Теперь можно переходить к программной части и использовать Python для считывания данных о температуре и влажности с датчика. ГЛАВА 3 Считывание данных об окружающем мире 615
Считывание температуры и влажности с I2C-устройства с помощью Python Использование библиотек Python является ключевым элементом продуктивной разработки приложений. Для этого примера мы воспользуемся библиотекой SDL_Pi_HDC1080_Python3, доступной на GitHub по адресу: https://github.com/switchdoclabs/SDL_Pi_HDC1080_Python3 Чтобы считать температуру и влажность, выполните следующие шаги: 1. Создайте новую директорию в вашей домашней папке. В Терминале введите команду: cd mkdir I2CTemperature cd I2CTemperature Теперь вы находитесь в директории I2CTemperature . 2. Клонируйте библиотеку SDL_Pi_HDC1080_Python3 на Raspberry Pi. Введите в панели Терминала следующую команду: git clone https://github.com/switchdoclabs/SDL_Pi_HDC1080_ Python3.git Команда git clone копирует удаленный репозиторий GitHub на ваш Raspberry Pi . После завершения клонирования введите команду: pi@RPi3.60:~/I2CTemperature $ ls SDL_Pi_HDC1080_Python3 pi@RPi3.60:~/I2CTemperature $ 3. Используя nano (или любой текстовый редактор), создайте файл с именем temperatureTest.py и введите следующий код: import sys sys.path.append('./SDL_Pi_HDC1080_Python3') import time import SDL_Pi_HDC1080 # Основная программа print print ("") print ("Read Temperature and Humidity from HDC1080 using I2C 616 РАЗДЕЛ 6 Взаимодействие с оборудованием
bus") print ("") hdc1080 = SDL_Pi_HDC1080.SDL_Pi_HDC1080() while True: print ("-----------------") print ("Temperature = %3.1f C" % hdc1080.readTemperature ()) print ("Humidity = %3.1f %%" % hdc1080.readHumidity ()) print ("-----------------") time.sleep(3.0) 4. Запустите программу, введя в терминале команду: sudo python3 temperatureTest.py Если все подключено правильно, на экране появятся обновляющиеся данные примерно каждые три секунды, например: Read Temperature and Humidity from HDC1080 using I2C bus ----------------Temperature = 24.2 °C Humidity = 32.9 % --------------------------------Temperature = 24.2 °C Humidity = 32.9 % --------------------------------Temperature = 24.2 °C Humidity = 32.9 % ----------------- Теперь ваш Raspberry Pi действительно взаимодействует с окружающим миром — вы считываете реальные данные с устройства I2C. Этот проект стоит сохранить, поскольку он пригодится в дальнейшем. СОВЕТ Попробуйте простой эксперимент: подуйте на датчик HDC1080 и понаблюдайте за изменениями показаний. Влажность заметно повысится, а температура слегка изменится. Пример вывода может выглядеть так: ----------------Temperature = 24.2 °C Humidity = 32.9 % --------------------------------Temperature = 24.1 °C ГЛАВА 3 Считывание данных об окружающем мире 617
Humidity = 33.6 % --------------------------------Temperature = 24.1 °C Humidity = 33.9 % --------------------------------Temperature = 24.1 °C Humidity = 36.3 % --------------------------------Temperature = 24.1 °C Humidity = 36.5 % --------------------------------- GITHUB — ХРАНИЛИЩЕ ВСЕГО ХОРОШЕГО GitHub — это онлайн-платформа, которая служит хранилищем и системой управления версиями для программного кода . Она построена на основе Git — популярной системы контроля версий, которая позволяет отслеживать изменения в исходных файлах, возвращаться к предыдущим версиям и работать над проектами совместно с другими разработчиками . GitHub предоставляет удобные инструменты для организации командной работы: можно создавать репозитории, отслеживать ошибки, оставлять комментарии к изменениям, предлагать улучшения и управлять задачами . Все это делает платформу незаменимой для разработчиков программного обеспечения . В 2018 году GitHub был приобретен компанией Microsoft, которая сохранила независимость сервиса . На сегодняшний день GitHub продолжает развиваться как крупнейшая в мире площадка для размещения и совместной работы с исходным кодом . По данным на сентябрь 2023 года, GitHub насчитывает более 100 миллионов пользователей и около 300 миллионов репозиториев — это делает его крупнейшим хостингом исходного кода и центром мирового сообщества разработчиков . 618 РАЗДЕЛ 6 Взаимодействие с оборудованием
Разбор программы Теперь разберем, как работает программа для измерения температуры и влажности с помощью датчика HDC1080. Первая строка кода импортирует стандартную библиотеку sys, которая используется для взаимодействия с системными функциями Python: import sys Следующая строка сообщает Python, где искать библиотеку, чтобы программа могла ее найти и использовать: sys.path.append('./SDL_Pi_HDC1080_Python3') Эта команда добавляет папку SDL_Pi_HDC1080_Python3, расположенную в текущем каталоге, в список путей поиска модулей Python. Когда вы импортируете библиотеку, Python просматривает все каталоги в списке sys.path. Если нужная библиотека находится не в стандартных местах, ее нужно добавить вручную — как сделано здесь. Далее следуют дополнительные импорты: import time import SDL_Pi_HDC1080 Следующие несколько строк кода выводят заголовок, который помогает пользователю понять, какие данные будут показаны в терминале: # Основная программа print print ("") print ("Read Temperature and Humidity from HDC1080 using I2C bus ") print ("") Следующая строка кода создает объект hdc1080 и выполняет его инициализацию: hdc1080 = SDL_Pi_HDC1080.SDL_Pi_HDC1080() Эти строки кода выполняют считывание температуры и влажности с датчика и выводят результаты в панель терминала: while True: print ("-----------------") ГЛАВА 3 Считывание данных об окружающем мире 619
print ("Temperature = %3.1f C" % hdc1080. readTemperature()) print ("Humidity = %3.1f %%" % hdc1080.readHumidity()) Пауза в три секунды и повтор выполняются следующими строками: print ("-----------------") time.sleep(3.0) Вы можете расширить программу, добавив дополнительные функции. Например: • • • включать красный светодиод, если температура превышает определенное значение; включать синий светодиод, если температура становится слишком низкой; или даже отправлять уведомления при выходе температуры за заданные пределы . ЧТО ПРОИСХОДИТ ВНУТРИ ДРАЙВЕРА I2C Устройства, работающие по шине I2C, имеют не только собственный адрес (например, 0x40 — адрес датчика HDC1080), но и внутренние регистры . Каждый регистр — это определенное место в памяти устройства, куда можно записывать команды или откуда можно считывать данные . У HDC1080 таких регистров восемь, и каждый отвечает за разные функции: измерение температуры, влажности, конфигурацию и т .д . Драйвер I2C выполняет операции чтения и записи этих регистров, чтобы управлять устройством и получать с него данные . Например, регистр температуры расположен по адресу указателя 0x00 . Именно из него считываются данные при измерении температуры . 620 РАЗДЕЛ 6 Взаимодействие с оборудованием
В библиотеке Python SDL_Pi_HDC1080 измерение температуры реализовано в функции readTemperature() . Ниже приведен ее код: def readTemperature(self): s = [HDC1080_TEMPERATURE_REGISTER] # температура s2 = bytearray( s ) HDC1080_fw.write( s2 ) time.sleep(0.0625) # согласно технической документации #читаем 2 байта температурных данных data = HDC1080_fr.read(2) buf = array.array('B', data) # преобразование данных temp = (buf[0] * 256) + buf[1] cTemp = (temp / 65536.0) * 165.0 - 40 return cTemp Код может показаться сложным, но идея проста . Сначала объявляем функцию: def readTemperature(self): Затем форматируем адрес указателя (в данном случае 0x00) в байтовый массив: s = [HDC1080_TEMPERATURE_REGISTER] # температура s2 = bytearray( s ) Сначала записываем адрес регистра: HDC1080_fw.write( s2 ) Выполняется небольшая задержка в 62,5 миллисекунды — именно столько времени требуется датчику HDC1080 для подготовки результата измерения, как указано в его технической документации (документацию можно найти, выполнив поиск HDC1080 на сайте www .ti .com) . time.sleep(0.0625) # согласно технической документации Считываем два байта: ГЛАВА 3 Считывание данных об окружающем мире 621
# читаем 2 байта температурных данных data = HDC1080_fr.read(2) И помещаем эти два байта в байтовый массив: buf = array.array('B', data) Затем мы преобразуем байты данных, используя формулу из технической документации: # Преобразование данных temp = (buf[0] * 256) + buf[1] cTemp = (temp / 65536.0) * 165.0 – 40 И отправляем значение температуры обратно в вызывающую программу: return cTemp Для работы с устройствами, подключенными через интерфейс I2C, существует множество низкоуровневых драйверов и программ, которые позволяют считывать и записывать данные на Raspberry Pi . Рассматриваемый драйвер является одним из самых распространенных и надежных решений . Помимо него, часто применяются такие библиотеки, как Adafruit_i2c, SMBUS, PyComms и Quick2Wire . Наиболее популярной считается библиотека SMBUS, так как она проста в использовании и подходит для большинства задач . Однако некоторые устройства требуют более широких возможностей, чем те, что предоставляет SMBUS, и в таких случаях используются альтернативные драйверы с дополнительной функциональностью . Измерение кислорода и пламени Этот эксперимент немного сложнее предыдущих. В нем используется датчик кислорода Grove, который помещается под частично герметичную стеклянную банку с зажженной свечой. Во время опыта измеряется уровень кислорода внутри банки и наблюдается, как он постепенно снижается, пока пламя не погаснет. Свеча потребляет кислород, и его содержание в воздухе уменьшается. Согласно данным из открытых источников, уровень кислорода должен снизиться примерно на 30% — с 21% до около 14,7%, после чего пламя обычно гаснет. Все изменения можно отслеживать в окне браузера на Raspberry Pi. 622 РАЗДЕЛ 6 Взаимодействие с оборудованием
Полученные данные будут сохраняться в файле формата CSV (файл с разделителями-запятыми). Позже их можно будет использовать для построения графика, например, в программе Excel. Для проведения эксперимента понадобятся следующие компоненты: » Аналого-цифровой преобразователь, который превращает аналоговый сигнал с датчика кислорода в цифровые данные, понятные Raspberry Pi . » Датчик кислорода Grove, измеряющий процентное содержание кислорода в воздухе и выдающий соответствующее аналоговое напряжение в диапазоне от 0 до 5 В . » Свеча, которая будет сгорать под стеклянной чашей, расходуя кислород . Подойдет любая свеча, помещающаяся под чашу . » Большая стеклянная чаша, служащая крышкой, создающей герметичное пространство для наблюдения за изменением концентрации кислорода . Аналого-цифровые преобразователи (АЦП) Аналого-цифровой преобразователь (АЦП) служит для преобразования аналогового сигнала в цифровой, который можно обрабатывать с помощью компьютера. В данном случае используется 16-битный АЦП. Разница между аналоговыми и цифровыми сигналами подробно объясняется в главе 2 этого раздела. После того как цифровое значение поступает в компьютер, его можно перевести обратно в напряжение, умножив полученное число на 5.0/32768.0. В результате получится число с плавающей точкой, выражающее напряжение в вольтах. Значение 32768 соответствует 2 в 15-й степени, так как 15 битов используются для представления величины, а шестнадцатый бит — для знака. Такой расчет связан с тем, что 16-битный преобразователь работает при напряжении 5,0 В, и этот диапазон делится на 32768 очень малых шагов, обеспечивая высокую точность измерений. В этом эксперименте применяется четырехканальный 16-битный аналого-цифровой преобразователь Grove, изображенный на рисунке 3.5. Его можно приобрести на сайте https://tindie.com (www.tindie.com/ products/31863/). Существуют и другие модули АЦП семейства Grove, однако для эксперимента выбран именно 16-битный вариант, так как он обеспечивает более точные результаты измерений. ГЛАВА 3 Считывание данных об окружающем мире 623
Рисунок 3.5. Четырехканальный 16-битный АЦП Grove Датчик кислорода Grove Газовый датчик Grove (O2), изображенный на рисунке 3.6, предназначен для измерения концентрации кислорода в воздухе. Он определяет долю кислорода и выдает сигнал в виде напряжения, величина которого пропорциональна его содержанию. Такой датчик можно использовать, например, в теплице для контроля состава воздуха — неплохая идея для нового проекта. Датчик кислорода Grove продается примерно за 40 долларов. Его можно приобрести на сайте www.seeedstudio.com по ссылке Grove-OxygenSensor-MIX8410 или найти на сайте mouser.com, выполнив поиск по запросу «grove oxygen sensor». Рисунок 3.6. Датчик кислорода Grove 624 РАЗДЕЛ 6 Взаимодействие с оборудованием
Следует учитывать, что показания датчика отражают общую тенденцию изменения концентрации кислорода и могут иметь некоторую погрешность. Кроме того, перед использованием датчику необходимо около 30 минут для прогрева. Если требуется высокая точность измерений, стоит рассмотреть использование более точного и дорогого прибора. Подключаем оборудование для эксперимента с кислородом К этому моменту вы уже приобрели достаточный опыт подключения модулей Grove к Raspberry Pi. Теперь можно перейти к настройке датчика кислорода. Для этого выполните следующие шаги: 1. Выключите Raspberry Pi. Это можно сделать, введя в командной строке команду sudo halt или выбрав пункт «Shutdown» в меню графического интерфейса. После того как желтый светодиод перестанет мигать, отключите питание устройства. 2. Подключите один конец кабеля Grove к датчику кислорода Grove, а другой — к разъему с маркировкой A1 на четырехканальном 16-битном аналого-цифровом преобразователе Grove. 3. Возьмите еще один кабель Grove и соедините разъем I2C на плате АЦП с одним из разъемов I2C на плате Pi2Grover, установленной на Raspberry Pi. На рисунке 3.7 показано, как должно выглядеть правильное подключение. Рисунок 3.7. Полная схема подключения Raspberry Pi / АЦП / датчика кислорода ГЛАВА 3 Считывание данных об окружающем мире 625
4. Подайте питание на Raspberry Pi. 5. Откройте терминал и выполните команду i2cdetect -y 1, чтобы убедиться, что плата АЦП корректно подключена и распознается системой. Вы должны 0 1 00: 10: -- -20: -- -30: -- -40: -- -50: -- -60: -- -70: -- -- увидеть следующий 2 3 4 5 6 7 -- -- -- -- --- -- -- -- -- --- -- -- -- -- --- -- -- -- -- --- -- -- -- -- --- -- -- -- -- --- -- -- -- -- --- -- -- -- -- -- вывод: 8 9 a -- -- --- -- --- -- --- -- -48 -- --- -- --- -- -- b -------- c -------- d -------- e -------- f -------- Адрес 0x48 соответствует четырехканальной 16-битной плате АЦП Grove (Grove 4.Channel 16.bit ADC). Если вы не видите такого адреса (например, при выполнении команды i2cdetect), вернитесь назад и проверьте подключение проводов — возможно, нарушен контакт на шине I2C. Теперь вы можете протестировать установку, запустив простую программу на Python. Сначала создайте новую директорию для проекта: cd mkdir oxygenProject cd oxygenProject git clone https://github.com/switchdoclabs/SDL_Pi_ Grove4Ch16BitADC После этого используйте редактор nano, чтобы создать новый файл senseOxygen.py: import time, sys sys.path.append('./SDL_Pi_Grove4Ch16BitADC/SDL_Adafruit_ ADS1x15') import SDL_Adafruit_ADS1x15 ADS1115 = 0x01 # 16.битный АЦП # Выбор коэффициента усиления gain = 6144 # +/- 6.144В # Выбор частоты дискретизации sps = 250 # 250 отсчетов в секунду 626 РАЗДЕЛ 6 Взаимодействие с оборудованием
# Инициализация АЦП в режиме по умолчанию (используется адрес I2C по умолчанию) adc = SDL_Adafruit_ADS1x15.ADS1x15(ic=ADS1115) dataFile = open("oxygenData.csv",'w') totalSeconds = 0 while (1): # Чтение канала кислорода в однополярном режиме с использованием указанных выше настроек print ("--------------------") voltsCh1 = adc.readADCSingleEnded(1, gain, sps) / 1000 rawCh1 = adc.readRaw(1, gain, sps) # Датчик кислорода sensorVoltage = voltsCh1 * (5.0/6.144) AMP = 121 K_O2 = 7.43 sensorVoltage = sensorVoltage / AMP*10000.0 Value_O2 = sensorVoltage / K_O2 - 1.05 print ("Channel 1 =%.6fВ raw=0x%4X Percent O2=%.2f" % (voltsCh1, rawCh1, Value_O2 )) print ("--------------------") dataFile.write("%d,%.2f\n" % (totalSeconds, Value_O2)) totalSeconds = totalSeconds + 1 dataFile.flush() time.sleep(1.0) ОСТОРОЖНО После завершения работы с датчиком кислорода обязательно поместите его обратно в контейнер с крышкой, который поставляется в комплекте, и герметично закройте его. В противном случае из-за воздействия влажности датчик со временем выйдет из строя. В прошлом уже были случаи, когда такие датчики портились именно потому, что их оставляли открытыми. После запуска программы были получены следующие результаты: -------------------Channel 1 =2.436375В raw=0x32C2 Percent O2= 22.05 --------------------------------------Channel 1 =2.436375В raw=0x32C2 Percent O2= 22.05 --------------------------------------Channel 1 =2.436375В raw=0x32C1 Percent O2= 22.05 --------------------------------------Channel 1 =2.436375В raw=0x32C2 Percent O2= 22.05 ГЛАВА 3 Считывание данных об окружающем мире 627
--------------------------------------Channel 1 =2.436187В raw=0x32C1 Percent O2= 22.05 -------------------- Разбор кода В этом разделе рассматривается код эксперимента с датчиком кислорода из предыдущей части. В следующих операторах задаются параметры для работы модуля АЦП: import time, sys sys.path.append('./SDL_Pi_Grove4Ch16BitADC/SDL_Adafruit_ ADS1x15') import SDL_Adafruit_ADS1x15 Сначала выполняется импорт стандартных библиотек Python, а также специальный импорт библиотеки для работы с АЦП. Обратите внимание, что путь к этой библиотеке указывает на поддиректорию внутри вашего проектного каталога. ADS1115 = 0x01 # 16.битный АЦП # Выбираем коэффициент усиления gain = 6144 # +/- 6.144В # Выбираем частоту дискретизации sps = 250 # 250 отсчетов в секунду Далее открывается текстовый файл для сохранения данных, которые впоследствии можно будет отобразить в виде графика с помощью Excel или любого другого инструмента визуализации: # Инициализация АЦП в режиме по умолчанию (используется стандартный I2C-адрес) adc = SDL_Adafruit_ADS1x15.ADS1x15(ic=ADS1115) dataFile = open("oxygenData.csv",'w') Производится считывание данных с модуля АЦП. В этом же фрагменте можно увидеть исходные значения, на основе которых выполняется вычисление напряжения: totalSeconds = 0 while (1): # Считывание канала кислорода в однополярном режиме с 628 РАЗДЕЛ 6 Взаимодействие с оборудованием
использованием указанных настроек print ("--------------------") voltsCh1 = adc.readADCSingleEnded(1, gain, sps) / 1000 rawCh1 = adc.readRaw(1, gain, sps) Формула voltsCh1 взята из спецификации датчика кислорода O2 (www.seeedstudio.com/Grove-Oxygen-Sensor-ME2.O2.f20.html). Эта формула используется для вычисления процентного содержания кислорода в воздухе на основе 16-битных данных, получаемых с АЦП: # Датчик O2 sensorVoltage = voltsCh1 *(5.0/6.144) AMP = 121 K_O2 = 7.43 sensorVoltage = sensorVoltage / AMP*10000.0 Value_O2 = sensorVoltage / K_O2 - 1.05 Здесь мы записываем данные в файл: print ("Channel 1 =%.6fV raw=0x%4X O2 Percent=%.2f" % (voltsCh1, rawCh1, Value_O2 )) print ("--------------------") dataFile.write("%d,%.2f\n" % (totalSeconds, Value_O2)) Тут выполняется принудительная запись буфера файла, чтобы убедиться, что последнее считанное значение сохранено. Завершение работы программы выполняется нажатием комбинации клавиш Ctrl+C: totalSeconds = totalSeconds + 1 dataFile.flush() Интерпретация результатов После того как программное обеспечение создано, можно переходить к эксперименту. Поместите свечу под чашу с датчиком кислорода, запустите программу и зажгите свечу (рисунок 3.8). Через некоторое время пламя погаснет. После этого остановите выполнение программы, нажав комбинацию клавиш Ctrl+C. time.sleep(1.0) ГЛАВА 3 Считывание данных об окружающем мире 629
Рисунок 3.8. Начало эксперимента по измерению O2 Данные сохраняются в файле oxygenData.csv в той же директории, где находится ваша программа. Для построения графика можно использовать Excel. Если вам нужна инструкция по созданию графиков, перейдите по адресу: www.workzone. com/blog/how-to-make-a-graph-in-excel/. Анализ данных, представленных на рисунке 3.9, показал, что начальное содержание кислорода составляло около 21%, а свеча погасла при уровне примерно 15,8%. Это означает снижение примерно на 25% от исходного значения. Конечное значение 15,8% оказалось немного выше ожидаемого уровня 14,7%, который соответствовал бы снижению кислорода на 30%. Предполагается, что это расхождение связано с точностью датчика и особенностями используемой свечи. Также стоит обратить внимание на участок графика сразу после того, как пламя погасло: уровень кислорода начал постепенно повышаться. Это свидетельствует о том, что чаша, накрывающая свечу, была не полностью герметична. 630 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 3.9. График данных нашего эксперимента с кислородом Что дальше? В этой главе вы узнали, как с помощью Raspberry Pi можно взаимодействовать с окружающим миром и подключать различные электронные устройства. Чтобы закрепить полученные знания и попробовать что-то новое, можно внести несколько улучшений в свой проект. » Попробуйте подключить к Raspberry Pi дополнительные датчики, работающие по интерфейсу I2C, их существует огромное множество, и вы сможете выбрать те, что подходят именно для вашего проекта . » Можно добавить и цифровые датчики, например, ПИК-датчик, способный обнаруживать теплые объекты, такие как люди, перед вашим Raspberry Pi . » Еще один интересный вариант — установить I2C-компас и акселерометр, чтобы устройство могло определять направление и движение . » Попробуйте также создавать более сложные и наглядные информационные панели и делиться ими с другими, предоставляя код доступа к вашему проекту . » А если хотите добавить динамику — подождите до следующей главы . Там вы узнаете, как подключить двигатель и заставить ваше устройство двигаться .
В этой главе вы узнаете: » Как заставить объекты двигаться с помощью Python » Как работают двигатели постоянного тока и как управлять ими с помощью программного обеспечения » Как использовать сервопривод » Как заставить шаговый двигатель выполнять шаги Гл а в а 4 Приводим механизмы в движение З аставлять предметы двигаться с помощью Python — это действительно впечатляюще. Когда в проект добавляются двигатели, возможности физических вычислений значительно расширяются и выходят на новый уровень. Электродвигатели используются повсюду: в роботах, микроволновых печах, холодильниках и даже в электромобилях. Они обеспечивают движение, циркуляцию воздуха, перекачивание жидкостей и транспортировку людей со скоростью около 60 миль в час. Электродвигатели играют огромную роль в современной технике и потребляют более половины всей электроэнергии, производимой в США. По сути, электродвигатель — это устройство, преобразующее электрическую энергию в механическую. В этой главе рассматриваются двигатели постоянного тока. Такие двигатели работают от источников с фиксированным напряжением, например 9 В, 5 В или 3,3 В. В отличие от этого, переменный ток — это тип электричества, подающийся из обычных бытовых розеток. 632 РАЗДЕЛ 6 Взаимодействие с оборудованием
Чтобы загрузить программный код, использованный в примерах этой главы, перейдите на сайт www.dummies.com/go/pythonaiofd3e, прокрутите страницу до раздела Downloads и выберите ссылку для раздела 6 (Book 6). Изучаем электродвигатели Все электродвигатели основаны на действии магнитных сил. Каждый магнит имеет два полюса — северный и южный. Полюса одного типа отталкиваются, а противоположные притягиваются. Именно это свойство и используется для создания движения. Мы хорошо знакомы с постоянными магнитами — например, теми, что удерживают записку на дверце холодильника. Но магнитное поле можно создать и искусственно, если пропустить электрический ток через катушку провода. Такой магнит называется электромагнитом. Если изменять направление тока, то будет меняться и направление магнитного поля. На этом принципе и основана работа двигателей: изменяя ток, можно создавать чередующиеся силы притяжения и отталкивания, которые превращаются в вращательное движение. Существует множество видов электродвигателей, но электромагнит является основой любого из них. В этой главе мы познакомимся с тремя наиболее распространенными типами двигателей, которые применяются в небольших проектах и робототехнике: » » малые двигатели постоянного тока; » шаговые двигатели . сервоприводы; Малые двигатели постоянного тока Двигатель постоянного тока имеет всего два провода — один для питания и другой для заземления (рисунок 4.1). При подаче напряжения, например 5 В, на линию питания двигатель начинает вращаться. Если поменять местами провода питания и заземления, направление вращения изменится на противоположное. ГЛАВА 4 Приводим механизмы в движение 633
Рисунок 4.1. Двигатель постоянного тока на маленьком роботе Управление скоростью двигателя постоянного тока осуществляется с помощью широтно-импульсной модуляции (ШИМ) — метода, который мы рассматривали в главе 1 этой раздела при управлении яркостью светодиода. Если мощность подается циклически с коэффициентом заполнения 50% (половину времени включено/половину выключено), двигатель будет вращаться с половинной скоростью. Такие двигатели постоянного тока недороги и отлично подходят для привода колес. Иногда на вал двигателя устанавливают датчик положения, чтобы определять, насколько повернулся вал, что дает компьютеру полезную обратную связь. Двигатель постоянного тока следует использовать всякий раз, когда нужно обеспечить вращение с определенной скоростью (об/мин), например, для вентилятора или колеса автомобиля. Сервоприводы Сервопривод представляет собой устройство, состоящее из трех основных частей: двигателя постоянного тока, управляющей схемы и системы шестерен. В некоторых моделях также используется потенциометр — переменный резистор, который выполняет функцию датчика положения и обеспечивает обратную связь, позволяя точно отслеживать угол поворота вала. 634 РАЗДЕЛ 6 Взаимодействие с оборудованием
Работа сервопривода основана на принципе широтно-импульсной модуляции (ШИМ), о которой уже шла речь в главе 1 этой раздела. Однако здесь ШИМ-сигнал используется не для регулирования скорости, а для задания конкретного положения. Определенная форма импульса удерживает вал двигателя в заданной точке и противодействует внешним воздействиям, стремящимся его сдвинуть. Максимальное усилие, которое сервопривод способен развить для удержания положения, называется номинальным крутящим моментом. Сервоприводы находятся под напряжением постоянно и, как правило, могут вращаться лишь в пределах около 180 градусов. Они широко применяются в моделировании и робототехнике — например, для управления закрылками радиоуправляемых самолетов, рулем лодок или движением роботизированных манипуляторов. Если требуется не просто переместить объект, а зафиксировать его в определенном положении, сервопривод является наиболее подходящим выбором. Шаговые двигатели Шаговый двигатель во многом похож на сервопривод, но принцип его работы отличается. Как видно на рисунке 4.2, вместо одного двигателя постоянного тока здесь используется несколько электромагнитов, расположенных вокруг центрального зубчатого вала. Каждый электромагнит управляется отдельно, и их последовательное включение заставляет вал поворачиваться небольшими фиксированными углами — шагами. Именно из-за такого способа работы двигатель получил название «шаговый». В отличие от сервопривода, шаговый двигатель способен удерживать свое положение благодаря постоянному удерживающему крутящему моменту и не нуждается в системе обратной связи для определения позиции. Если нагрузка не превышает допустимый крутящий момент, двигатель сохраняет точность без ошибок позиционирования. ГЛАВА 4 Приводим механизмы в движение 635
Рисунок 4.2. Солнечные панели с отслеживанием положения солнца, использующие шаговый двигатель Шаговые двигатели применяются там, где требуется медленное и точное вращение. Хороший пример — 3D-принтеры, где важно точное перемещение печатающей головки и стола. В следующих разделах будет показано, как управлять всеми тремя типами двигателей — двигателями постоянного тока, сервоприводами и шаговыми — с помощью Python, используя порты ввода-вывода общего назначения (GPIO) и контроллер I2C. Управление двигателем постоянного тока Существует множество способов управлять двигателями постоянного тока с помощью Raspberry Pi. Для этой цели можно использовать разные платы управления и контроллеры роботов — их действительно десятки. В данном проекте будет применяться плата с интерфейсом I2C, что удобно и логично, поскольку этот протокол уже использовался в предыдущих главах. Такая плата позволяет управлять сразу двумя двигателями, задавая для каждого направление вращения и скорость. Это довольно мощное и гибкое решение. Для сборки проекта понадобятся следующие компоненты: 636 » Плата сопряжения Pi2Grover Grove, которую можно приобрести на сайте shop.switchdoc.com; » Драйвер двигателя Grove I2C, доступный вместе с кабелем Grove на сайте seeedstudio.com; » Два небольших двигателя постоянного тока, например те, что представлены на adafruit.com/product/711 . РАЗДЕЛ 6 Взаимодействие с оборудованием
Подробнее о плате Pi2Grover и принципах ее работы можно узнать в главах 1–3 этого раздела. В следующем разделе будет рассмотрен драйвер двигателя Grove I2C, поскольку он использует особый способ управления двигателями, который стоит изучить подробнее. Драйвер двигателя Grove I2C Драйвер двигателя Grove I2C, показанный на рисунке 4.3, позволяет управлять сразу двумя двигателями постоянного тока через шину I2C, подключенную к Raspberry Pi. Он берет на себя всю сложность, связанную с управлением двигателями, и делает процесс максимально простым для пользователя. Благодаря этому вы можете управлять направлением и скоростью вращения двигателей напрямую из программы на Python, используя несколько простых команд. Рисунок 4.3. Драйвер двигателя Grove I2C Ампер (А) — это единица измерения силы электрического тока, то есть количества электричества, проходящего по проводнику за единицу времени. Драйвер Grove I2C способен подавать ток до 2 ампер на каждый двигатель, что вполне достаточно для большинства небольших проектов. Он поддерживает питание двигателей в диапазоне от 6 до 15 В. В нашем случае используются маломощные двигатели, поэтому их можно питать напрямую от Raspberry Pi. Однако если вы решите использовать более мощные моторы, можно подключить внешний источник питания, сохранив при этом ту же схему управления. ГЛАВА 4 Приводим механизмы в движение 637
На рисунке 4.4 показано устройство платы драйвера I2C. На ней установлен микроконтроллер Atmega 8L — небольшой встроенный компьютер, который эмулирует интерфейс I2C, принимает команды от Raspberry Pi и управляет двигателями. Это отличный пример того, как современные платы могут содержать собственные «мини-компьютеры», выполняющие вспомогательные задачи. Два разъема для подключения двигателей находятся на левой стороне платы. Светодиоды, расположенные на плате, показывают текущее состояние работы — они указывают, какие двигатели активны и подается ли питание. На рисунке 4.5 изображен двигатель постоянного тока от Adafruit, который используется в данном проекте. Рисунок 4.4. Схема платы драйвера I2C с пояснениями Рисунок 4.5. Двигатель постоянного тока Adafruit 638 РАЗДЕЛ 6 Взаимодействие с оборудованием
Теперь можно переходить к практической части — подключению двигателей и контроллера, чтобы запустить моторы. Перед началом работы обязательно выключите питание Raspberry Pi. Это важно для безопасности и предотвращения возможных повреждений оборудования. 1. Ослабьте два винтовых зажима, расположенные на краю платы I2C-контроллера двигателей, и аккуратно вставьте зачищенные концы проводов от моторов в соответствующие отверстия (рисунок 4.6). Для двигателей постоянного тока не имеет значения, какой из проводов — красный или черный — подключен к тому или иному зажиму . Если поменять провода местами, двигатель будет вращаться в противоположном направлении . Главное — соблюдать одинаковый порядок цветов при подключении обоих двигателей, чтобы они работали синхронно и вращались в одном направлении . При необходимости провода можно удлинить, хотя это необязательно . На рисунке 4 .7 показано, как двигатель устанавливается и подключается к плате драйвера Grove I2C . Рисунок 4.6. Провода в винтовых клеммах I2C-модуля управления двигателем Рисунок 4.7. Двигатели, установленные на модуле управления двигателем ГЛАВА 4 Приводим механизмы в движение 639
2. Убедитесь, что на плате установлена перемычка (см. рисунок 4.4). Она обозначена как “Output for external MCS selector” — «Выход для внешнего селектора MCS» . Обычно перемычка уже установлена при поставке, однако стоит обязательно проверить ее наличие . Без нее плата в текущей конфигурации работать не будет . Подключите один конец кабеля Grove к разъему Grove на плате драйвера двигателей Grove I2C, а другой — к разъему I2C Grove на плате Pi2Grover, установленной на Raspberry Pi . 3. После этого включите Raspberry Pi, чтобы приступить к программированию на Python. При подаче питания все пять светодиодов на плате драйвера Grove I2C должны загореться — это означает, что питание подано и контроллер готов к работе . Если светодиоды не загорелись, выключите Raspberry Pi и проверьте все подключения, особенно правильность установки кабеля и наличие перемычки . Программное обеспечение для двигателей постоянного тока на Python Двигатели постоянного тока часто применяются для приведения в движение колес роботов, поэтому команды «вперед» и «назад» будут встречаться и дальше, когда вы перейдете к созданию собственного робота. Программное обеспечение, которое используется для эксперимента с двигателем постоянного тока на Python, покажется вам знакомым, если вы уже изучали главы 2 и 3 этого раздела. Его структура похожа на программы для предыдущих I2C-проектов, где мы также использовали аналогичные подходы к подключению и управлению устройствами. Одним из ключевых преимуществ Python является возможность использовать готовые библиотеки, что значительно упрощает разработку. Для этого проекта применяется библиотека SDL_Pi_GroveI2CMotorDrive, которую можно найти на GitHub по адресу: https://github.com/switchdoclabs. Она обеспечивает удобный интерфейс для управления платой драйвера двигателей Grove I2C. СОВЕТ 640 Если после подключения платы и двигателя вы не замечаете реакции, попробуйте нажать кнопку Reset на плате драйвера I2C. Эта кнопка расположена в правом нижнем углу платы (см. рисунок 4.4). На рисунке 4.8 показано, как выглядят собранные двигатель и плата драйвера I2C в готовом виде. РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 4.8. Установка двигателя постоянного тока Для подготовки программного окружения и настройки программного обеспечения выполните следующие действия: 1. Откройте терминал на вашем Raspberry Pi и создайте новую директорию в основной папке, введя команду: cd mkdir dcMotor cd dcMotor Теперь вы находитесь в директории dcMotor . 2. Чтобы установить библиотеку SDL_Pi_GroveI2CMotorDrive для Python 3, выполните в терминале следующую команду: git clone https://github.com/switchdoclabs/SDL_Pi_ GroveI2CMotorDrive.git Команда git clone загружает репозиторий Git, расположенный по указанному адресу, и создает его локальную копию на вашем Raspberry Pi . Это позволяет вам использовать все файлы проекта, включая примеры кода и библиотеки . Если ввести ls в панели терминала, вы увидите следующий результат: pi@RPi3.60:~/dcMotor $ ls SDL_Pi_GroveI2CMotorDrive 3. Откройте текстовый редактор nano (или любой другой редактор по вашему выбору) и создайте новый файл под названием dcmotorTest.py . Для этого введите в терминале: ГЛАВА 4 Приводим механизмы в движение 641
import sys sys.path.append("./SDL_Pi_GroveI2CMotorDrive") import SDL_Pi_GroveI2CMotorDrive import time #"0b1010" определяет полярность выхода #"10" означает, что M+ "положительный", а M- "отрицательный" MOTOR_FORWARD = 0b1010 MOTOR_BACKWARD = 0b0101 try: m= SDL_Pi_GroveI2CMotorDrive.motor_drive() #ВПЕРЕД print("Forward") #задает скорость двигателя 1 и двигателя 2 m.MotorSpeedSetAB(100,100) m.MotorDirectionSet(MOTOR_FORWARD) time.sleep(2) #НАЗАД print("Back") m.MotorSpeedSetAB(100,100) #0b0101 Вращение в противоположном направлении m.MotorDirectionSet(MOTOR_BACKWARD) time.sleep(2) #СТОП print("Stop") m.MotorSpeedSetAB(0,0) time.sleep(1) #Увеличение скорости for i in range (100): print("Speed:",i) m.MotorSpeedSetAB(i,i) time.sleep(.02) print("Stop") m.MotorSpeedSetAB(0,0) except IOError: print("Unable to find the I2C motor drive") print("Hit Reset Button on I2C Motor Drive and Try Again") 642 РАЗДЕЛ 6 Взаимодействие с оборудованием
Эта программа запускает оба двигателя вперед на полной скорости (100), затем запускает их назад на полной скорости, останавливает двигатели, затем снова запускает их назад на малой скорости с постепенным увеличением до полной скорости, после чего останавливает двигатели. Ключевыми элементами этого программного обеспечения являются вызовы библиотеки SDL_Pi_GroveI2CMotorDrive. Библиотека поддерживает следующие функции: » MotorSpeedSetAB(MotorSpeedA, MotorSpeedB): эта функция устанавливает скорость для двигателя A (M1) и двигателя B (M2) . Диапазон скорости двигателя составляет от 0 до 100 . » MotorDirectionSet(Direction): эта функция управляет направлением вращения двигателей (вперед или назад) с помощью констант, определенных в программе: MOTOR_FORWARD = 0b1010 и MOTOR_BACKWARD = 0b0101 . Библиотека SDL_Pi_GroveI2CMotorDrive использует библиотеку smbus — одну из множества библиотек Python, предназначенных для работы с интерфейсом I2C. В библиотеке smbus команды передаются на плату I2C в виде блочной записи, которая включает I2C-адрес устройства, командный байт и набор аргументов. Пример вызова smbus для установки направления вращения двигателя выглядит так: #Установка направления двигателя def MotorDirectionSet(self,Direction): bus.write_i2c_block_data(self.I2CMotorDriveAdd, self. DirectionSet, [Direction,0]) time.sleep(.02) Пора запустить двигатели постоянного тока. Для этого в окне терминала введите команду: sudo python3 testMotor.py В награду вы увидите, как загораются и меняют состояние светодиоды, а двигатели выполняют запрограммированную вами в Python последовательность действий. Наблюдая за этим, вы сможете убедиться, что программа работает правильно, и при желании изменить ее, создавая собственные комбинации движений и скоростей. ОСТОРОЖНО Помните, что во время работы все двигатели получают питание от Raspberry Pi. Поэтому, когда эксперимент будет завершен и вы захотите перейти к следующему разделу, обязательно выключите Raspberry Pi и отсоедините двигатели постоянного тока от платы, чтобы избежать лишней нагрузки на систему и продлить срок службы оборудования. ГЛАВА 4 Приводим механизмы в движение 643
Управление сервоприводом Сервопривод значительно отличается от обычного двигателя постоянного тока. Хотя внутри него тоже находится двигатель постоянного тока, дополнительная схема управления позволяет точно задавать положение вала и удерживать его в нужной точке. Благодаря этому сервоприводы идеально подходят для задач, где требуется перемещать объект в определенное положение, например, в роботизированных манипуляторах. Однако они не подходят для приложений, требующих непрерывного вращения, например, для колес роботов. Двигатели постоянного тока при этом способны вращаться значительно быстрее, особенно недорогие модели. Управление сервоприводом осуществляется с помощью широтноимпульсной модуляции (ШИМ). Существуют специальные платы, которые позволяют выполнять ШИМ и управлять более мощными сервоприводами под контролем компьютера, но в этом эксперименте используется встроенная функция ШИМ, доступная на портах ввода-вывода общего назначения (GPIO) Raspberry Pi. Для сборки понадобятся следующие компоненты: » Плата сопряжения Pi2Grover Grove. Ее можно приобрести на сайте Tindie: https://www .tindie .com/products/switchdoc/ pi2grover-raspberry-pi-to-grove-connector/ . » Микросервопривод SG90. Эти небольшие и недорогие приводы легко найти на сайтах eBay или Amazon . За менее чем 10 долларов можно приобрести сразу несколько штук . » Набор соединительных проводов Grove с четырьмя штыревыми разъемами. Такие кабели позволяют подключить сервопривод к системе Grove и доступны по адресу https:// www .seeedstudio .com/Grove-4 .pin-Male-Jumper-toGrove-4 .pin-Conversion-Cable-5 .PCs-per-Pack .html . О плате Pi2Grover уже рассказывалось в главах 1 -3 этого раздела. Микросервопривод SG90, показанный на рисунке 4.9, представляет собой компактный и недорогой вариант, который можно приобрести у разных производителей. Он способен поворачиваться примерно на 90 градусов в каждую сторону, обеспечивая общий угол поворота около 180 градусов. Рабочее напряжение привода находится в диапазоне от 3,0 В до 7,0 В, а максимальный ток потребления составляет около 40 мА (миллиампер — это одна тысячная ампера). Таким образом, напряжения 5 В, подаваемого от Raspberry Pi, вполне достаточно для питания и стабильной работы этого сервопривода. 644 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 4.9. Микросервопривод SG90 с проводами Большинство сервоприводов, включая модель SG90, имеют три управляющих провода: » желтый — передает ШИМ-сигнал управления, который задает положение привода; » красный — используется для подачи питания (в данном случае 5 В от Raspberry Pi); » коричневый — служит проводом заземления (общим проводом схемы) . КАКОЙ ТОК МОЖЕТ ПОДАВАТЬ RASPBERRY PI НА ВЫВОДЫ 5В? Если вы хотите узнать, какой ток может подаваться на выводы 5В Raspberry Pi 4B, то точное значение зависит от нескольких факторов — в частности, от мощности вашего блока питания и того, какие устройства уже подключены к плате . В целом можно считать, что безопасное значение составляет около 250 мА, если питание подается от стандартного источника . Однако при использовании мощного USB-блока питания (например, на 5 В и 2,5 А) Raspberry Pi способен выдавать через выводы 5В значительно больший ток — до 1000 мА и даже выше, если нагрузка на саму плату невелика . ГЛАВА 4 Приводим механизмы в движение 645
Следует учитывать, что в Raspberry Pi 4B установлен самовосстанавливающийся предохранитель на 2,5 А в цепи 5-вольтового питания . Если нагрузка превысит допустимый предел и предохранитель сработает, достаточно просто отключить питание и дать ему остыть, после этого он восстановит работу . Для подключения сервопривода используйте соединительные кабели Grove, у которых на одном конце расположены штыревые контакты, а на другом — стандартный разъем Grove (рисунок 4.10). Подсоедините три провода сервопривода к соответствующим штырям кабеля: сигнальный (желтый) — к выводу ШИМ, красный — к питанию 5 В и коричневый — к земле. После этого подключите конец кабеля с разъемом Grove к разъему на плате Pi2Grover, установленной на вашем Raspberry Pi. Рисунок 4.10. Соединительный кабель Grove со штыревыми контактами на одном конце и разъемом Grove на другом Теперь можно приступить к подключению сервопривода и его проверке в работе. 1. Выключите Raspberry Pi и полностью отключите питание. Это важно, чтобы избежать короткого замыкания при подключении . 2. Подключите один конец соединительного кабеля Grove ОСТОРОЖНО к сервоприводу SG90 (рисунок 4.11), ориентируясь на схему подключения проводов, которая приведена в таблице 4 .1 . Проверьте, чтобы каждый провод был подключен к правильному контакту — ошибка в подключении может привести к повреждению как Raspberry Pi, так и самого сервопривода . 3. Подсоедините другой конец кабеля Grove к разъему с маркировкой D4/D5 на плате Pi2Grover. 646 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 4.11. Правильно подключенный к соединительному кабелю сервопривод 4. Неиспользуемый белый контакт на кабеле Grove заклейте изолентой или синей лентой, чтобы исключить возможность короткого замыкания. 5. Установите один из прилагаемых пластиковых балансиров (рычагов) на шестерню сервопривода. Это позволит наглядно видеть диапазон движения сервопривода, как показано на рисунке 4.12. После завершения подключения можно переходить к программному обеспечению на Python, чтобы протестировать работу сервопривода. Таблица 4.1. Подключение сервопривода к кабелю Grove Patch Cable SG90 Servo Провод Назначение Желтый провод Желтый провод Сигнал PWM (управление положением сервопривода) Красный провод Красный провод Питание Коричневый провод Коричневый провод Земля Рисунок 4.12. Полностью подключенный Raspberry Pi и сервопривод ГЛАВА 4 Приводим механизмы в движение 647
Программное обеспечение сервопривода на Python Для управления сервоприводом в данном примере не используется библиотека высокого уровня, хотя для Raspberry Pi существует множество подобных библиотек на Python. Вместо этого применяется работа напрямую с выводами GPIO и используется функция широтно-импульсной модуляции (ШИМ, PWM) из встроенной библиотеки RPi .GPIO. Да, библиотека RPi .GPIO все же используется, но без дополнительных уровней вызовов API, как это обычно делается. Таким образом, управление сервоприводом выполняется напрямую через выводы Raspberry Pi. Выполните следующие шаги: 1. Создайте директорию в вашей основной папке, введя следующие команды: cd mkdir Servo cd Servo 2. Используя nano (или другой текстовый редактор), создайте файл с именем servoTest.py и введите следующий код Python: import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) ServoPin = 4 GPIO.setup(ServoPin, GPIO.OUT) p = GPIO.PWM(ServoPin, 50) p.start(7.5) try: while True: p.ChangeDutyCycle(7.5) print ("90 degrees") time.sleep(1) # пауза 1 print ("0 degrees") p.ChangeDutyCycle(2.5) time.sleep(1) # пауза 1 print ("180 degrees") p.ChangeDutyCycle(12.5) time.sleep(1) # пауза 1 648 # поворот на 90 градусов секунда # поворот на 0 градусов секунда # поворот на 180 градусов секунда РАЗДЕЛ 6 Взаимодействие с оборудованием
except KeyboardInterrupt: p.stop() GPIO.cleanup() Разбор кода В этом разделе код будет разобран построчно, чтобы пояснить, как он работает. Следующий фрагмент кода задает номер контакта, соответствующий разъему Grove D4/5 на плате Pi2Grover: import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) ServoPin = 4 Далее используется параметр GPIO .OUT, чтобы перевести выбранный контакт GPIO в режим вывода: GPIO.setup(ServoPin, GPIO.OUT) Эта команда создает объект p для ServoPin (4) с частотой 50 Гц: p = GPIO.PWM(ServoPin, 50) Различные значения рабочего цикла ШИМ перемещают сервопривод от одного края диапазона движения до другого. В ходе экспериментов было установлено, что для используемых двигателей рабочий цикл 2,5% а поворачивает ось сервопривода в начальное положение (0 градусов), 7,5% соответствует положению 90 градусов, а 12,5%перемещает ось в крайнее положение (180 градусов). Эти значения примерно одинаковы для большинства небольших сервоприводов. Хорошее практическое руководство по работе с сервоприводами можно найти по ссылке: www.raspberrypi.com/news/how-to-use-a-servo-motor-with-raspberry-pi/ p.start(7.5) Числа, используемые для управления сервоприводом в следующем фрагменте кода, зависят от конкретной модели сервопривода. ГЛАВА 4 Приводим механизмы в движение 649
В данном случае для нашего сервопривода применяются значения 7.5, 2.5 и 12.5: while True: p.ChangeDutyCycle(7.5) print ("90 degrees") time.sleep(1) # пауза 1 print ("0 degrees") p.ChangeDutyCycle(2.5) time.sleep(1) # пауза 1 print ("180 degrees") p.ChangeDutyCycle(12.5) time.sleep(1) # пауза 1 # поворот к 90 градусам секунда # поворот к 0 градусам секунда # поворот к 180 градусам секунда Обратите внимание, что все тело программы помещено в блок try: except:. Это сделано для того, чтобы при нажатии Ctrl+C программа завершалась корректно — сервопривод останавливался, а порты GPIO освобождались и возвращались операционной системе. Такая очистка портов считается хорошей практикой при прямой работе с GPIO. except KeyboardInterrupt: p.stop() GPIO.cleanup() Настало время запустить программу. Введите в окне терминала следующую команду: sudo python3 servoTest.py На экране появятся приведенные ниже строки, а сервопривод начнет двигаться, последовательно выполняя запрограммированные команды: 90 degrees 0 degrees 180 degrees 90 degrees 0 degrees 180 degrees 90 degrees 0 degrees Попробуйте поэкспериментировать с разными углами поворота и последовательностями, изменяя код и значения констант в вызовах p.ChangeDutyCycle — сервоприводу это не повредит. Такие изменения помогут вам лучше понять, как работает широтно-импульсное управление и как можно точно задавать положение привода. 650 РАЗДЕЛ 6 Взаимодействие с оборудованием
Если вы все сделали правильно, сервопривод на вашем Raspberry Pi должен работать корректно. Нетрудно заметить, почему для колес роботов чаще используют двигатели постоянного тока — они обеспечивают непрерывное вращение. Сервоприводы же идеально подходят для тех случаев, когда требуется движение в пределах определенного угла, без полного оборота. В разделе 7 книги будет рассмотрен пример робота-автомобиля: задние колеса будут вращаться с помощью двигателей постоянного тока, а передние управляться отдельно. В начале главы уже упоминалось, что сервоприводы можно использовать для движения руки робота, управления закрылками радиоуправляемого самолета или рулем лодки. Наблюдая за тем, как сервопривод выполняет запрограммированные движения, вы, вероятно, сможете придумать и другие интересные варианты применения. Экспериментируйте, создавайте собственные проекты и не бойтесь пробовать новое. А теперь перейдем к последнему основному типу двигателей — шаговому двигателю. Заставляем шаговый двигатель шагать Шаговые двигатели значительно отличаются от двигателей постоянного тока и сервоприводов. Их основное преимущество заключается в способности точно позиционировать объекты с помощью цифрового интерфейса. Хотя сервоприводы тоже могут обеспечивать точное управление положением, для этого им требуется более сложная электроника и система обратной связи, отслеживающая текущее положение вала. Шаговый двигатель решает задачу точного позиционирования программным способом. Он перемещается строго на один шаг при подаче определенной последовательности электрических сигналов на две электромагнитные катушки. Каждый такой шаг соответствует фиксированному углу поворота, что позволяет точно управлять положением без необходимости дополнительной обратной связи. На рисунке 4.13 показан принцип работы шагового двигателя. Эта последовательность шагов реализуется в программе на Python, которая управляет включением катушек и определяет направление и скорость вращения ротора. ГЛАВА 4 Приводим механизмы в движение 651
âτìχëχφíëðìτöυòþ Рисунок 4.13. Схема шагового двигателя Шаговый двигатель обычно состоит из двух катушек, которые включаются поочередно, заставляя ротор двигаться от одного шага к следующему. Такая поэтапная активация катушек обеспечивает точное и контролируемое вращение. В таблицах 4.2 и 4.3 показаны последовательности сигналов, которые необходимо подавать, чтобы переместить шаговый двигатель на один шаг вперед или на один шаг назад. Эти шаблоны чередования сигналов несложно отследить и воспроизвести в программе на Python, управляющей шаговым двигателем. Таблица 4.2. Движение шагового двигателя вперед Coil_A_1 (пин 12) Coil_A_2 (пин 20) Coil_B_1 (пин 13) Coil_B_2 (пин 21) 1 0 1 0 0 1 1 0 0 1 0 1 1 0 0 1 Таблица 4.3. Движение шагового двигателя назад Coil_A_1 (пин 12) Coil_A_2 (пин 20) Coil_B_1 (пин 13) Coil_B_2 (пин 21) 1 0 0 1 0 1 0 1 0 1 1 0 1 0 1 0 652 РАЗДЕЛ 6 Взаимодействие с оборудованием
На рисунке 4.14 показано графическое отображение цифровой последовательности сигналов, управляющих шаговым двигателем. Для этого использовался логический анализатор, подключенный к контактам GPIO Raspberry Pi. Рисунок 4.14. Логический анализатор, показывающий последовательность шагов двигателя ОБРАТНАЯ СВЯЗЬ: КАКАЯ ПОЛЕЗНАЯ ВЕЩЬ! Обратная связь — это очень полезное явление, которое возникает, когда выходной сигнал системы частично возвращается на ее вход, образуя замкнутый контур . На первый взгляд это может показаться сложным, но идея проста . Представьте, что вы написали статью и попросили читателей оставить комментарии . После получения отзывов вы исправляете или улучшаете текст — это и есть пример обратной связи, только в человеческом, а не техническом смысле . В электрических схемах обратная связь используется для повышения точности и стабильности работы систем, например, для более точного позиционирования сервопривода . Если на валу установлен энкодер — устройство, которое выдает электрический сигнал, соответствующий текущему положению, — программа может использовать этот сигнал для корректировки рабочего цикла ШИМ, чтобы достичь нужного угла поворота с высокой точностью . ГЛАВА 4 Приводим механизмы в движение 653
Существуют два типа обратной связи — положительная и отрицательная . В приведенной выше аналогии положительная обратная связь — это конструктивные комментарии, которые помогают улучшить статью, а отрицательная — это просто негативные отзывы без пользы . Однако в электронике все наоборот: здесь чаще используется отрицательная обратная связь, потому что она помогает системе стабилизироваться и удерживать нужное значение параметра . Положительная же обратная связь, напротив, приводит к нарастанию ошибок и нестабильности . Хороший пример — неприятный визг динамика, когда микрофон находится слишком близко: микрофон улавливает звук из динамика, усиливает его и снова подает в динамик — так возникает самоподдерживающийся цикл, типичный пример положительной обратной связи . Теперь вы знаете все необходимое, чтобы создать свой первый проект с шаговым двигателем. Для этого понадобятся следующие компоненты: » Плата сопряжения Pi2Grover Grove. Приобретайте на сайте shop.switchdoc.com . Подробнее о ней рассказано в главах 1,- -3 этого раздела . » Шаговый двигатель 28BYJ-48 ULN2003 5V. Его можно купить на www.ebay.com или по ссылке https://amzn. to/4aLFonW . Этот двигатель недорогой, поэтому имеет смысл приобрести несколько штук . Убедитесь, что в комплект входит плата драйвера, как в примере по ссылке на Amazon . » Набор кабелей Grove с гнездовыми разъемами (Grove-tofemale-pins). Они доступны на https://www.seeedstudio. com/Grove-4.pin-Female-Jumper-to-Grove-4.pin-ConversionCable-5.PCs-per-Pack.html или на сайте mouser.com (воспользуйтесь поиском по запросу «Grove female patch cables») . Двигатель 28BYJ-48, изображенный на рисунке 4.15, работает от напряжения 5 В и имеет шаг 5.625 × 1/64 градуса, что примерно соответствует 0.088 градуса на один шаг. В комплект входит плата драйвера с микросхемой ULN2003 и четырьмя светодиодами, которые показывают, как программа изменяет состояние GPIO-выводов Raspberry Pi при управлении двигателем. 654 РАЗДЕЛ 6 Взаимодействие с оборудованием
Рисунок 4.15. Шаговый двигатель 28BYJ-48 и плата драйвера UNL2003 Кабели Grove с гнездовыми разъемами, показанные на рисунке 4.16, используются для подключения платы драйвера шагового двигателя к Raspberry Pi. Рисунок 4.16. Соединительный кабель с разъемом Grove и гнездовыми контактами Пора приступить к сборке проекта с шаговым двигателем. Выполните следующие шаги: ГЛАВА 4 Приводим механизмы в движение 655
1. Выключите Raspberry Pi и отсоедините питание. 2. Подключите кабель Grove с гнездовыми разъемами к плате драйвера ULN2003 в соответствии со схемой подключения, приведенной в таблице 4.4. Обратите внимание, что для аккуратного размещения проводов можно использовать кабельную стяжку . Проверьте правильность подключения красного и черного проводов кабеля Grove — они должны располагаться так, как показано на рисунке 4 .17 . Таблица 4.4. Первый кабель Grove с гнездовыми разъемами к плате драйвера ULN2003 Кабель Grove Плата драйвера ULN2003 Назначение Желтый провод IN1 Катушка A_1 Белый провод IN2 Катушка B_1 Красный провод + Питание Черный провод - Земля Рисунок 4.17. Крупный план подключения питания на плате драйвера UNL2003 656 РАЗДЕЛ 6 Взаимодействие с оборудованием
3. Подключите второй кабель Grove с гнездовыми разъемами к плате драйвера ULN2003 в соответствии со схемой подключения, указанной в таблице 4.5. Закрепите неиспользуемые красный и черный провода с помощью кабельной стяжки или небольшого кусочка изоленты, как показано на рисунке 4 .18 . Таблица 4.5. Второй кабель Grove с гнездовыми разъемами к плате драйвера ULN2003 Кабель Grove Плата драйвера ULN2003 Назначение Желтый провод IN3 Катушка A_2 Белый провод IN4 Катушка B_3 Красный провод - Не подключен Черный провод - Не подключен Рисунок 4.18. Подключенный второй кабель Grove ГЛАВА 4 Приводим механизмы в движение 657
Рисунок 4.19. Все соединительные провода установлены на плате драйвера UNL2003 4. Еще раз проверьте все соединения и убедитесь, что они выполнены так же, как на рисунке 4.19. 5. Подключите кабель шагового двигателя 28BYJ-48 к разъему на плате драйвера ULN2003, как показано на рисунке 4.20. Кабель имеет ключ, поэтому вставить его можно только в одном положении. Рисунок 4.20. Подключенные шаговый двигатель и плата драйвера 658 РАЗДЕЛ 6 Взаимодействие с оборудованием
6. Подключите первый кабель Grove (тот, у которого все четыре провода подключены к драйверу ULN2003) к разъему Grove на плате Pi2Grover, обозначенному как D12/13. 7. Подключите второй кабель Grove (тот, у которого используются только желтый и белый провода) к разъему Grove на плате Pi2Grover с маркировкой D20/21. После этого подключение проводов будет завершено . Полностью собранная система показана на рисунке 4 .21 . Рисунок 4.21. Полностью подключенный проект с Raspberry Pi и шаговым двигателем 8. Прикрепите к валу шагового двигателя картонную стрелку, чтобы удобнее было наблюдать за его вращением, как показано на рисунке 4.22. Рисунок 4.22. Шаговый двигатель, готовый к работе ГЛАВА 4 Приводим механизмы в движение 659
Программное обеспечение для шагового двигателя на Python Как и в случае с сервоприводом, библиотеку высокого уровня использовать не будем: управление шаговым двигателем выполним напрямую через порты GPIO. Для этого выполним следующие шаги.: 1. Создайте директорию в домашней папке и введите в ней следующие команды: cd mkdir Stepper cd Servo 2. Откройте редактор (например, nano или любой другой), создайте файл с именем stepperTest.py со следующим кодом: import sys import RPi.GPIO as GPIO import time GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) coil_A_1_pin = 12 coil_B_1_pin = 13 coil_A_2_pin = 20 coil_B_2_pin = 21 GPIO.setup(coil_A_1_pin, GPIO.setup(coil_A_2_pin, GPIO.setup(coil_B_1_pin, GPIO.setup(coil_B_2_pin, GPIO.OUT) GPIO.OUT) GPIO.OUT) GPIO.OUT) def forward(delay, steps): for i in range(0, steps): setStep(1, 0, 1, 0) time.sleep(delay) setStep(0, 1, 1, 0) time.sleep(delay) setStep(0, 1, 0, 1) time.sleep(delay) setStep(1, 0, 0, 1) time.sleep(delay) def backwards(delay, steps): for i in range(0, steps): setStep(1, 0, 0, 1) 660 РАЗДЕЛ 6 Взаимодействие с оборудованием
time.sleep(delay) setStep(0, 1, 0, 1) time.sleep(delay) setStep(0, 1, 1, 0) time.sleep(delay) setStep(1, 0, 1, 0) time.sleep(delay) def setStep(w1, w2, w3, w4): GPIO.output(coil_A_1_pin, w1) GPIO.output(coil_A_2_pin, w2) GPIO.output(coil_B_1_pin, w3) GPIO.output(coil_B_2_pin, w4) while True: try: # Задержка между шагами (миллисекунды) delay = 10 # Количество шагов вперед steps = 50 forward(int(delay) / 1000.0, int(steps)) # Количество шагов назад steps = 50 backwards(int(delay) / 1000.0, int(steps)) except KeyboardInterrupt: # выключаем все катушки setStep(0,0,0,0) sys.exit() Блок try: ... except: в конце программы используется для безопасного завершения работы и очистки состояния портов ввода-вывода GPIO. Когда вы нажимаете Ctrl+C, программа прерывается, и выполнение переходит в раздел except или finally. В этом блоке выполняется отключение всех выводов GPIO и возврат их операционной системе, чтобы другие программы могли использовать эти порты в будущем. Разбор кода Вы написали свою первую программу управления шаговым двигателем, и теперь пришло время разобраться, как она работает. Код stepperTest. py довольно прост. Вы устанавливаете выходы GPIO в значения 1 и 0 в соответствии с последовательностями для шагового двигателя, показанными в таблицах 4.2 и 4.3. Точные последовательности можно увидеть в коде функций forward() и backwards(). ГЛАВА 4 Приводим механизмы в движение 661
Теперь давайте запустим код и начнем делать шаги. Включите свой Raspberry Pi и откройте панель терминала. Обратите внимание, что при включении питания загораются все четыре светодиода, как показано на рисунке 4.23, но они будут мигать при запуске вашей программы на Python. Шаговый двигатель делает 50 шагов влево, а затем 50 шагов вправо. Попробуйте изменить эти переменные в программе, чтобы переместить шаговый двигатель в другие положения. Видите, как эти двигатели можно использовать в 3D-принтерах и роботах для точного позиционирования печатающих головок, высоты платформы и манипуляторов? Со всеми знаниями, полученными в этом разделе, вы готовы создать робота, которым можно управлять с помощью Python. Рисунок 4.23. Raspberry Pi управляет шаговым двигателем
7 СОЗДАНИЕ РОБОТОВ
В ЭТОМ РАЗДЕЛЕ Глава 1: Введение в робототехнику Глава 2: Создание вашего первого робота на Python Глава 3: Программирование вашего роботаровера Глава 4: Использование искусственного интеллекта в робототехнике
В этой главе вы » Поймете, что из себя представляет робот » Научитесь отличать различные типы роботов » Познакомитесь с составными частями робота Гл а в а 1 Введение в робототехнику Р оботы. Это слово используется уже более ста лет. Оно происходит от чешского слова «robota», что означает «принудительный труд». Впервые термин появился в пьесе Карела Чапека «R.U.R. — Россумские Универсальные Роботы», написанной в 1920 году. Однако само слово придумал не Карел, а его брат Йозеф Чапек. Интересно, что еще раньше, в 1907 году, в произведении Фрэнка Баума о стране Оз появился персонаж, который, по сути, был роботом, хотя сам автор так его не называл. Сегодня роботы встречаются повсюду. Возможно, вы даже не осознаете, насколько они близко присутствуют в вашей повседневной жизни. Чтобы понять это, нужно рассматривать понятие «робот» не с точки зрения Голливуда, где это часто гуманоидные машины, а с позиции компьютерных наук. Робот не всегда похож на человека На самом деле, есть две ключевые вещи, которые нужно знать о роботах: » У любого робота должны быть как минимум два компонента — компьютер и актуатор . » Роботы глупы . Это правило № 1 . Роботы — не люди . В каждом роботе есть некий компьютер, который можно представить как мозг машины. Его мощность может сильно различаться: от суперГЛАВА 1 Введение в робототехнику 665
компьютера IBM Watson до простого 8-битного микроконтроллера с несколькими тысячами байт оперативной памяти. Например, роботбариста по имени Бакстер имеет внутри себя 16 компьютеров — о нем речь пойдет позже. Важно понимать, что компьютер не всегда находится внутри самого робота. Иногда робот подключается к управляющему компьютеру по сети, и тот выполняет обработку данных и управление. Однако даже при сетевом подключении внутри самого робота часто есть отдельные встроенные микрокомпьютеры, каждый из которых выполняет свою задачу: один управляет сервоприводом или конкретным сочленением, другие контролируют группы движений. Центральный управляющий компьютер можно считать мозгом робота, а распределенное управление — аналогом человеческих рефлексов, которые выполняются без участия сознания. Актуатор — это устройство, которое выполняет физическое действие, воздействуя на окружающий мир. Согласно этому определению, даже сложное устройство интернета вещей (IoT), подключенное к датчику и базе данных, не считается роботом, поскольку оно не оказывает физического воздействия на окружающую среду. А вот компьютер, который управляет механизмом тостера и заставляет подниматься поджаренный хлеб, можно назвать роботом — ведь он взаимодействует с физическим миром, выполняя конкретное действие. Это определение не идеально, и вокруг него можно вести долгие споры, особенно в пограничных случаях. Тем не менее оно помогает понять основную идею — робот может принимать самые разные формы, от промышленного манипулятора до бытового устройства, выполняющего простое действие. Не у каждого робота есть руки или колеса Классическое представление о роботе обычно подразумевает хотя бы некоторое сходство с человеком. Например, промышленные роботы на автомобильных заводах имеют огромные механические руки, которые поднимают двери, сваривают кузов, устанавливают лобовые стекла и выполняют множество других операций сборки. Манипуляторы меньшего размера используются на производственных линиях по всему миру для создания самых разных товаров — от мелких деталей до крупного оборудования. Относительно недавно появилась новая категория роботов — коботы, или коллаборативные роботы. Они созданы для совместной работы с людьми 666 РАЗДЕЛ 7 Создание роботов
на производстве. В отличие от промышленных роботов, которые могут быть опасны при непосредственном контакте, коботы останавливаются, если сталкиваются с человеком. Их задача — помогать людям выполнять работу быстрее и эффективнее, разделяя задачи между человеком и машиной. Пример такого робота — Бакстер, робот-бариста, способный безопасно взаимодействовать с людьми. При этом не все роботы имеют руки или вообще похожи на людей. Вспомните пример с тостером — это тоже робот, хоть и не имеет ничего общего с гуманоидом. Роботы могут выглядеть как микроволновые печи, автомобили или даже бытовые приборы. Современные автомобили, например, можно считать сложными роботизированными системами. В них множество встроенных компьютеров, которые управляют различными функциями — от подачи топлива до стабилизации подвески. Компьютер автомобиля считывает положение педали газа и регулирует подачу топливно-воздушной смеси, чтобы достичь нужной мощности. Между педалью и механизмом двигателя нет прямой механической связи — все контролируется электроникой. В некоторых моделях, например в BMW X3 2019 года, водитель может менять режимы движения — комфортный, спортивный или экономичный. При этом изменяется чувствительность руля, реакция педали газа и даже поведение подвески. В современном автомобиле может быть более двадцати компьютеров, которые выполняют самые разные функции и постоянно обмениваются данными друг с другом. Таким образом, ваш автомобиль — это не просто средство передвижения, а настоящий робот на колесах. Рисунок 1.1. Входные данные для роботизированной системы управления автомобилем BMW ГЛАВА 1 Введение в робототехнику 667
Когда мы говорим о роботах, многие сразу представляют огромные механические руки на заводских линиях или впечатляющих роботовсобак, созданных компанией Boston Dynamics. Но на самом деле мир робототехники гораздо шире. Существует множество других типов роботов — от простых домашних устройств до сложных автономных систем, используемых в медицине, сельском хозяйстве и космосе. В следующих разделах вы познакомитесь с разными видами роботов и узнаете, как они работают и чем отличаются друг от друга. Робот-пекарь от Wilkinson Робот-пекарь от компании Wilkinson Baking (www.wilkinsonbaking.com) — это интересный пример того, как робототехника может изменить традиционные отрасли. Компания из города Уолла-Уолла, штат Вашингтон, создала роботизированную систему выпечки хлеба (рисунок 1.2), которая способна вернуть процесс производства хлеба из крупных хлебозаводов обратно в местные магазины. Такая система не только позволяет выпекать хлеб прямо на месте, но и делает это гораздо эффективнее — по словам разработчиков, для доставки их продукции конечному потребителю требуется в шесть раз меньше ископаемого топлива. Подобные технологические сдвиги уже происходили в прошлом. Вспомните времена, когда фотопленку нужно было отправлять в большие лаборатории для проявки. Позже появились автоматы — по сути, тоже роботы, — которые позволили небольшим семейным магазинам снова заниматься проявкой пленки и печатью фотографий на месте. Конечно, с распространением цифровых камер эта отрасль практически исчезла, но пример остается показателем того, как автоматизация и роботизация могут изменить целую индустрию. Бакстер, робот-бариста Бакстер — это универсальный коллаборативный робот, или кобот, созданный компанией Rethink Robotics в 2011 году (рисунок 1.3). Несмотря на то что он появился уже довольно давно, его возможности до сих пор впечатляют. Робот оснащен множеством датчиков и камер, включая по одной камере в каждой руке, что позволяет ему точно воспринимать окружающее пространство. Каждая рука Бакстера имеет собственный инструмент: левая оборудована захватом, а правая — присоской. Такая комбинация делает его идеальным для выполнения разнообразных задач и экспериментов. 668 РАЗДЕЛ 7 Создание роботов
Рисунок 1.2. Робот, выпекающий хлеб Рисунок 1.3. Бакстер готовит кофе Внутри Бакстера установлено более шестнадцати отдельных компьютеров. Главный из них — обычный настольный компьютер Dell, закрепленный на корпусе робота. Остальные микрокомпьютеры управляют его суставами, камерами и датчиками в обеих руках. Интересно, что «мозг» Бакстера с программой приготовления кофе, полностью написанной на Python, находится не внутри самого робота, а в другом конце комнаты и подключен к нему через сеть. Основная программа использует распределенную операционную систему для роботов (ROS), которая управляет взаимодействием между центральным компьютером и вычислительными узлами в теле робота, обеспечивая обмен данными и передачу изображений. Проект с Бакстером был реализован тремя студентами курса «Проектирование роботизированных систем» в Университете Айдахо. На своем выпускном проекте они поставили задачу — научить Бакстера варить ГЛАВА 1 Введение в робототехнику 669
кофе. После трех месяцев работы и написания нескольких тысяч строк кода им удалось добиться успеха. Команда использовала методы компьютерного зрения, чтобы определять готовность кофе, а также подключила робота к облачной платформе Amazon AWS, что позволило управлять им через голосового помощника Алексу. Например, можно было сказать: «Алекса, скажи роботу Бакстеру сварить кофе». Студенты также разработали программы для захвата и размещения предметов, чтобы робот мог выбирать капсулу Keurig, заваривать кофе и аккуратно подавать готовую чашку клиенту. Теперь, когда Бакстер научился делать все это, он кажется почти разумным, но правило № 1 все еще в силе: роботы глупы. Он умеет варить кофе, но до приготовления слоеных пирожков ему пока далеко — возможно, это задание для следующего семестра. Стоит отметить, что программные ошибки в робототехнике особенно занимательны, потому что их последствия проявляются физически. Если в обычной программе ошибка просто выдает неправильный результат, то здесь она может привести, например, к пролитому кофе. Тостер Griffin с поддержкой Bluetooth Вы же не думали, что мы закончим этот список примеров роботов, не упомянув тостер? Тостер Griffin с поддержкой Bluetooth был представлен на выставке потребительской электроники CES как часть линейки подключенной кухонной техники. Он позволял через приложение задавать желаемый уровень поджаристости и даже отправлял уведомление на телефон, когда тост был готов. К сожалению, похоже, что этот тостер так и не поступил в массовое производство, что сильно расстроило Джона — он очень хотел иметь подключенный тостер. Действительно очень хотел. В 2024 году на рынке появился тостер Revolution InstaGLO R270 стоимостью около 400 долларов. Он оснащен отличным сенсорным экраном и предлагает сотни вариантов настроек. Однако, несмотря на внушительный функционал, он все еще не способен выжигать на хлебе портрет любимого человека и не подключается к интернету. Так что до мечты об идеальном «умном» тостере еще немного не дотянули. Достаточно примеров. Пора перейти к главному — разберемся, что именно делает робота роботом и как программировать роботов на Python. 670 РАЗДЕЛ 7 Создание роботов
Понимание основных составляющих робота Как уже упоминалось ранее, минимальный набор компонентов, необходимых для того, чтобы устройство можно было назвать роботом, включает компьютер и актуатор. Однако промышленные и сервисные роботы, используемые на практике, обычно состоят из большего числа элементов. Типичный робот включает четыре основных типа компонентов: » » » » Компьютеры . Двигатели и актуаторы . Средства связи . Датчики . В этом разделе мы подробно рассмотрим каждый из этих компонентов и разберем, какую роль он играет в работе робота. Компьютеры Компьютеры играют ключевую роль в робототехнике. Они управляют камерами и двигателями, обрабатывают изображения, получаемые с камер, а также считывают данные с датчиков, которые отслеживают состояние окружающей среды робота. В большинстве случаев используются небольшие вычислительные устройства, называемые встраиваемыми системами. Такие компьютеры могут выполнять одну конкретную задачу — например, контролировать потребление тока определенного двигателя — или координировать работу целой группы приводов в роботизированном манипуляторе, передавая команды другим контроллерам. Компьютеры более высокого уровня в составе системы отвечают за планирование действий, обмен данными с другими роботами, сборочными линиями или операторами. Именно на этом уровне происходит принятие решений и взаимодействие с пользователем. Примечательно, что многие такие управляющие компьютеры программируются на языке Python, благодаря его простоте, гибкости и большому количеству библиотек, используемых в робототехнике. ГЛАВА 1 Введение в робототехнику 671
Двигатели и актуаторы Добавление двигателей и актуаторов превращает робота в нечто большее, чем просто компьютер, например ваш ноутбук. Именно двигатели обеспечивают движение и делают возможным выполнение физических действий. Они бывают самых разных типов и размеров — от крошечных микродвигателей до мощных промышленных агрегатов. (Подробнее о них можно было узнать в разделе 6, где рассматривались основы работы двигателей.) Термин «актуатор» имеет более широкое значение, чем «двигатель». Двигатель — это один из видов актуаторов, но к актуаторам относятся и другие устройства, способные преобразовывать энергию в движение. Например, проволока с памятью формы — это особый тип металлического сплава, который при нагревании изменяет форму, а при охлаждении возвращается в исходное состояние. Такой материал может сокращаться и удлиняться, напоминая работу мышечных волокон. Проволока с памятью формы находит широкое применение в робототехнике: с ее помощью открывают вентиляционные заслонки, регулируют направление микрофонов и датчиков, а также создают миниатюрные приводы, где обычные двигатели слишком громоздки. В главе 6 этого раздела вы узнаете, как использовать Python для управления движением двигателей и других актуаторов, чтобы ваш робот мог выполнять реальные действия. Коммуникации Коммуникации играют важнейшую роль в работе роботов. Им нужно уметь обмениваться данными — не только с людьми через экран или голосовые интерфейсы, но и с другими компьютерами и роботами, чтобы согласовывать действия и реагировать на изменения в окружающей среде. Роботы используют системы связи для передачи информации, команд и даже распределения вычислительных задач. Например, сложные операции, такие как обработка изображений или анализ данных с датчиков, могут выполняться не самим роботом, а внешним компьютером или облачным сервером. Такой подход позволяет разгрузить встроенные системы и повысить эффективность работы. Современные роботы применяют различные технологии связи: Bluetooth — для обмена данными на небольших расстояниях, Wi-Fi — для беспроводного подключения к сети, и TCP/IP — для взаимодействия 672 РАЗДЕЛ 7 Создание роботов
с другими устройствами через интернет или локальные сети. В большинстве случаев эти методы связи реализуются с помощью встроенных микрокомпьютеров, которые управляют передачей данных между компонентами робота и внешними системами. Датчики Признаемся честно: датчики — это одна из самых захватывающих частей робототехники, и мы всегда стремимся узнать о новых, более точных и функциональных моделях. Сегодня существуют датчики практически для всего: электричества, температуры, влажности, давления, касания, движения, присутствия человека, а также камеры и модули компьютерного зрения. С каждым годом такие устройства становятся все доступнее, компактнее и точнее. Многие из этих датчиков можно программировать на Python — как для получения и обработки данных, так и для управления внешними устройствами. Это делает Python идеальным языком для работы с современными роботами и встраиваемыми системами. В современных роботах границы между компонентами все больше размываются. Один модуль может совмещать в себе функции сразу нескольких систем. Например, двигатель часто содержит встроенный микрокомпьютер для управления, датчики для контроля положения и скорости, а также средства связи для передачи данных. Все это размещается на одной плате или внутри единого корпуса, что делает современные робототехнические системы компактными, эффективными и умными. Программирование роботов Роботы могут программироваться на множестве разных языков. Некоторые модели обучаются с помощью прямого взаимодействия: оператор вручную перемещает манипулятор в нужные положения, фактически «показывая» роботу, что нужно делать. Другие роботы программируются традиционным способом — с помощью процедурных команд, которые описывают движение и действия шаг за шагом. Одним из самых популярных языков высокого уровня для программирования роботов является Python. Он используется для управления поведением робота, обработки данных с датчиков и камер, планирования движений и взаимодействия между различными системами. Например, ГЛАВА 1 Введение в робототехнику 673
робот Бакстер программируется через программный интерфейс приложения (API), специально разработанный для Python. Хотя многие производители создают собственное программное обеспечение для своих роботов, почти все из них предоставляют возможность интеграции с Python через специальные библиотеки и модули. Это делает Python универсальным инструментом для робототехники — от простых проектов на Raspberry Pi до сложных промышленных систем. Лучший способ по-настоящему понять, как связаны Python и робототехника, — это собрать собственного робота и написать программу, которая будет считывать показания датчиков, анализировать окружающую среду и принимать решения о движении и действиях. К концу этого раздела вы увидите, как все компоненты — двигатели, датчики и программное обеспечение — объединяются в единую систему, благодаря которой ваш робот оживает и начинает двигаться.
В этой главе вы: » Соберете робота » Начнете понимать его компоненты » Изучите как программировать компоненты Гл а в а 2 Создание вашего первого робота на Python В этой главе мы начнем с самого интересного — сборки собственного робота, а затем разберем, как использовать Python для управления его компонентами. Почему именно сборка? Есть две основные причины. Первая — экономия: робот в виде набора для самостоятельной сборки обычно стоит дешевле готового устройства. Вторая — обучение. Собирая робота своими руками, вы узнаете, как устроены его детали, как они взаимодействуют между собой и как можно использовать Python для управления двигателями, датчиками и другими элементами. В качестве основы мы выбрали роботизированную платформу на базе одноплатного компьютера Raspberry Pi. Существуют и другие популярные варианты — например, роботы на базе Arduino, — однако их вычислительные возможности ограничены. На таких микроконтроллерах сложно выполнять сложную обработку данных или применять методы искусственного интеллекта, которые доступны на Raspberry Pi. Поскольку эта книга посвящена Python, логично использовать устройство, позволяющее в полной мере раскрыть потенциал этого языка. Raspberry Pi идеально подходит для таких целей: он достаточно мощный, чтобы запускать алгоритмы машинного обучения, работать с изображениями и управлять несколькими датчиками одновременно. Хотя существуют адаптации Python для микроконтроллеров, такие как CircuitPython для Arduino или ESP32, их производительность и объем памяти ограничены. Поэтому, если вы хотите создавать действительно умных и функциональных роботов, Raspberry Pi — это оптимальный выбор. ГЛАВА 2 Создание вашего первого робота на Python 675
Знакомство с марсоходом PiCar-B Выбирая подходящего робота для сборки в этой книге, мы протестировали четыре различных модели небольших роботов-автомобилей. Все они находились примерно в одной ценовой категории, но у каждой нашлись свои сильные и слабые стороны. После анализа мы остановились на роботе Adeept PiCar-B (см. рисунок 2.1) — и вот почему: » Руководство по сборке написано ясно и подробно, содержит множество иллюстраций и схем, что делает процесс сборки понятным даже для новичков . » В комплекте поставляется программное обеспечение, полностью совместимое с Python 3 и версией Stretch операционной системы Raspberry Pi . » Для сборки PiCar-B не требуется пайка — все компоненты соединяются с помощью разъемов и проводов, что делает проект простым и безопасным . » Робот имеет разумную цену, легко доступен и хорошо подходит для учебных целей . Рисунок 2.1. Собранный робот PiCar-B 676 РАЗДЕЛ 7 Создание роботов
Что нужно для сборки СОВЕТ Для сборки робота, описанного в этой главе, вам понадобятся три основных компонента, а также несколько простых инструментов. Большинство необходимых инструментов уже включено в комплект — в наборе вы найдете шестигранные ключи и отвертки. Дополнительно стоит подготовить пластиковые кабельные стяжки, чтобы аккуратно зафиксировать и упорядочить провода после сборки: » Raspberry Pi 4B. Хотя можно использовать более простые версии, такие как Raspberry Pi Zero или Pi 3B+, мы рекомендуем именно 4B . Она обеспечивает более высокую производительность и позволяет выполнять сложные вычисления прямо на борту робота . Однако высокая мощность сопровождается повышенным энергопотреблением и сокращенным временем работы от батареи — это разумный компромисс для наших целей . Учтите, что с некоторыми аккумуляторами робот может не суметь запустить Pi 4B из-за пускового тока . Подробнее о старте и энергопотреблении можно прочитать в статье Джона: https://switchdoc.com/2019/10/raspberry-pi-3b-4bstartup-currents-examined/ Купить Raspberry Pi 3B+ или 4B можно на Amazon.com, Newark.com и Adafruit.com . » Adeept Raspberry Pi PiCar- B. Этот робот не так широко доступен, как сам Raspberry Pi, поэтому важно убедиться, что вы покупаете именно модель PiCar-B, а не PiCar- A . В каталоге производителя устройство может называться Adeept Mars Rover PiCar-B — ищите именно это название . Приобрести PiCar-B можно на Amazon https://amzn.to/36dukPU), eBay и на официальном сайте Adeept.com . » Литий-полимерные аккумуляторы 18650. Для питания PiCar-B используются два аккумулятора типа 18650 с номинальным напряжением 3,7 В и емкостью 5000 мА·ч . При желании можно запитать робота напрямую через разъем micro USB Raspberry Pi, отключив батареи — в этом случае питание будет подаваться как на саму плату, так и на робота . Мы выбрали комплект, включающий два аккумулятора и настенное зарядное устройство . Такие батареи и зарядные устройства легко найти на Amazon и других торговых площадках . ГЛАВА 2 Создание вашего первого робота на Python 677
Знакомство с компонентами робота Теперь перейдем к рассмотрению компонентов робота PiCar- B. Мы не будем подробно останавливаться на его механической конструкции, а сосредоточимся на активных элементах, таких как датчики, двигатели и управляющие модули. Также мы обсудим программное обеспечение на Python, которое используется для взаимодействия с этими компонентами как в тестовых программах, приведенных далее в этой главе, так и в нашем собственном программном обеспечении для робота, которое будет рассмотрено в главе 3 этого раздела. В процессе изучения мы приведем примеры кода на Python, показывающие, как управлять каждым из датчиков и двигателей. Полные версии программ и их подробное объяснение вы сможете найти в разделе «Подготовка к запуску тестов вашего ровера на Python» в конце этой главы. Плата контроллера двигателей Контроллер двигателей служит связующим звеном между Raspberry Pi и компонентами робота PiCar-B, включая датчики и моторы (см. рисунок 2.2). В основе этой платы лежат две ключевые микросхемы. Первая — PCA9685, устройство с интерфейсом I2C, способное управлять до 16 сервоприводами. В нашем случае задействованы только три, поэтому остается значительный запас для дальнейшего расширения системы. Вторая микросхема — L289P, отвечающая за питание и управление основным приводным двигателем робота. Остальные элементы платы обеспечивают подключение портов вводавывода Raspberry Pi (GPIO) к различным датчикам и исполнительным устройствам. Кроме того, на плате контроллера установлен источник питания на 5,0 В, который распределяет энергию от литий-полимерных аккумуляторов, питая как Raspberry Pi, так и двигатели робота. 678 РАЗДЕЛ 7 Создание роботов
ρëυöχíðχíý ϊψχëôāïτψāíτ êτöτψυð οðöτôðυ χöɹêτöτψυð πτïüυó χωôχëôχìχ íëðìτöυòā πτïüυóý ωυψëχõψðëχíχë πτïüόó χωôχëôχìχíëðìτöυòā õυψυëόψôϊöýφ χêψτöôχφ õχòāψôχωöð Þòþöψτïëϊñχëχφ íτöùðñ πτïüυóõυψυíôðϋ ωëυöχíðχíχë πτïüόó+4-3 6EWTFIVV]4M Рисунок 2.2. Плата контроллера двигателей PiCar-B πτïüόóíτöùðñτ òðôðð PMRIXVEGOMRK θýñòĀùτöυòþ õðöτôðāχöɹêτöτψυð πτïüυóõυψυíôðϋ ωëυöχíðχíχë ƀõðñωυòþôýυ õψχìψτóóðψϊυóýυ 6+&ωëυöχíðχíý Сервоприводы Сервоприводы обычно состоят из трех основных элементов: двигателя постоянного тока, простой управляющей схемы и набора редукторных шестерен. В некоторых моделях также установлен потенциометр (переменный резистор), который обеспечивает обратную связь по положению вала сервопривода. Подробное описание принципа обратной связи и ее применения приведено в главе 4 раздела 6. Положение сервопривода контролируется с помощью широтноимпульсной модуляции (ШИМ) — метода, который подробно рассматривается в главе 1 раздела 6 при объяснении управления яркостью светодиода. Микросервопривод SG90, входящий в комплект робота, представляет собой компактный и недорогой мотор (см. рисунок 2.3). Он работает при напряжении от 3 до 7 В и потребляет ток до 40 мА (миллиампер — это одна тысячная ампера). Напряжения 5 В, подаваемого от Raspberry Pi, вполне достаточно для его питания. Сервопривод способен поворачиваться примерно на 90 градусов в каждую сторону, что дает общий диапазон движения около 180 градусов. ГЛАВА 2 Создание вашего первого робота на Python 679
Рисунок 2.3. Микросервопривод SG90 Большинство сервоприводов, включая модель SG90, имеют три управляющих провода: » » » Желтый — передает управляющий ШИМ-сигнал . Красный — подает питание (в нашем случае 5 В) . Коричневый — соединен с землей (GND) . Сервоприводы находятся под постоянным напряжением и обычно имеют ограниченный диапазон движения — от 180 до 270 градусов. В роботе PiCar-B используются три микросервопривода SC-90 9g, отличающиеся компактными размерами и низким энергопотреблением. Все сервоприводы управляются с помощью микросхемы PCA9685, которая работает через шину I2C. Это означает, что они не занимают GPIO-выводы Raspberry Pi, что оставляет больше свободных портов для подключения других компонентов. Подробное описание работы шины I2C приведено в главе 3 раздела 6. Для управления сервоприводом достаточно задать значение широтноимпульсной модуляции (ШИМ), соответствующее нужному положению, на одной из линий ШИМ микросхемы PCA9685: print ("-------------------") print ("Servo Test - Head Left") print ("-------------------") pwm.set_pwm(HEAD_TURN_SERVO, 0, calValues.look_left_max) time.sleep(1.0) 680 РАЗДЕЛ 7 Создание роботов
Значение позиции, передаваемое сервоприводу (calValues.look_left_ max), подбирается опытным путем и определяется позже в этой главе. Его устанавливают, наблюдая за движением сервопривода при выполнении команд поворота влево и вправо, чтобы определить предельные углы безопасного отклонения. Подробное описание этого процесса приведено в разделе «Калибровка сервоприводов» далее в этой главе. Приводной двигатель Основной приводной двигатель представляет собой двигатель постоянного тока с двумя проводами — питание и земля (см. рисунок 2.4). При подаче питания, например 5 В на линию питания, двигатель начинает вращаться. Если поменять местами провода питания и земли, направление вращения изменится на противоположное. Рисунок 2.4. Основной приводной двигатель Скорость вращения двигателя постоянного тока регулируется с помощью широтно-импульсной модуляции (ШИМ) — того же метода, который используется для управления яркостью светодиодов (глава 1 раздел 6) и положением сервоприводов. При подаче питания с коэффициентом заполнения 50%, то есть, когда половину времени сигнал включен, а половину выключен, двигатель будет вращаться примерно с половиной своей максимальной скорости. Этот двигатель управляется с помощью шести линий GPIO, подключенных к Raspberry Pi, которые отвечают за регулировку скорости и направления вращения. Однако все сложности управления скрыты от пользователя — взаимодействие с двигателем осуществляется через простые команды на Python. ГЛАВА 2 Создание вашего первого робота на Python 681
Вот пример кода, который заставляет робот двигаться вперед: motor.motor_left(MOTOR_START, forward, left_spd * spd_ad) motor.motor_right(MOTOR_START, backward, right_spd * spd_ad) Почему в программе активируются оба двигателя, если у PiCar-B фактически только один приводной мотор? Дело в том, что при сборке невозможно заранее предсказать, как именно будет подключен двигатель — в одном случае он может вращаться в правильном направлении, а в другом — в обратном. (В нашем экземпляре он был подключен наоборот.) СОВЕТ На плате контроллера предусмотрено два разъема для подключения двигателя. Если команда движения вперед вызывает у робота движение назад, достаточно просто переставить провод в другой разъем — и направление исправится. Использование в коде обеих линий управления (для левого и правого двигателя) делает программу универсальной: она корректно работает независимо от того, в какой разъем подключен двигатель. RGB-светодиод На передней панели робота расположены два отдельных RGB-светодиода — по одному с каждой стороны (см. рисунок 2.5). Каждый такой светодиод представляет собой корпус, внутри которого находятся три миниатюрных светодиода: красный, зеленый и синий. Каждый из этих светодиодов управляется отдельно через линии GPIO, подключенные к Raspberry Pi. Для управления используется программная широтно-импульсная модуляция (ШИМ), которая позволяет изменять интенсивность каждого цвета и тем самым создавать практически любые оттенки путем смешения красного, зеленого и синего света. Рисунок 2.5. Одиночный RGB-светодиод 682 РАЗДЕЛ 7 Создание роботов
print() print("-------------------") print("Left Front LED Test - Red") print ("-------------------") led.side_on(led.left_R) time.sleep(1.0) led.side_off(led.left_R) Светодиоды можно включать по отдельности, управляя каждым цветовым каналом отдельно. В дальнейшем, с помощью другого программного обеспечения, описанного позже в этой книге, вы сможете изменять яркость каждого светодиода, добиваясь плавных переходов и различных цветовых эффектов. Программируемые RGB-светодиоды Робот оснащен двенадцатью программируемыми RGB-светодиодами, соединенными в одну цепочку. Две группы по три светодиода расположены на нижней части корпуса, а еще две такие же группы направлены назад (см. рисунок 2.6). Все двенадцать светодиодов соединены последовательно, как звенья новогодней гирлянды. Как и в подобных гирляндах, выход из строя одного светодиода приводит к остановке всей цепочки. Это связано с тем, что управление светодиодами осуществляется через последовательный поток данных, проходящий через каждый из них. Raspberry Pi передает этот поток, и для корректной работы требуется точная синхронизация сигналов, поэтому управление цепочкой реализуется с помощью специального программного кода. Рисунок 2.6. 12 программируемых RGB-светодиодов ГЛАВА 2 Создание вашего первого робота на Python 683
Цепочка из двенадцати RGB-светодиодов управляется всего одним GPIO-выводом Raspberry Pi: print () print ("-------------------") print ("12 RGB LED Test - On ") print ("-------------------") rainbowCycle(strip, wait_ms=20, iterations=3) ПРОГРАММИРУЕМЫЕ RGB-ЛЕНТЫ НА RASPBERRY PI Raspberry Pi работает под управлением сложной многозадачной операционной системы Linux . Эта система использует вытесняющую многозадачность, то есть любая выполняемая задача, включая пользовательские процессы, может быть временно приостановлена для выполнения другой . Из-за этого последовательный поток данных, управляющий программируемыми RGB-светодиодами, может прерываться или задерживаться, что приводит к искажению сигнала . В результате некоторые светодиоды на ленте начинают отображать неправильные цвета . Используемая библиотека решает эту проблему, применяя аппаратные модули ШИМ и ПДП процессора Raspberry Pi . Модуль широтно-импульсной модуляции (ШИМ) генерирует сигналы с заданным рабочим циклом, которые могут использоваться для управления сервоприводами или регулировки яркости светодиодов . Модуль прямого доступа к памяти (ПДП) позволяет передавать данные между частями процессора без участия центрального процессора . Благодаря этому можно отправлять последовательность байтов в модуль ШИМ напрямую, формируя стабильный поток данных для светодиодов, не зависящий от работы операционной системы . Такой подход обеспечивает стабильное управление подсветкой и предотвращает искажения . На микроконтроллерах вроде Arduino, где отсутствует операционная система, подобные сигналы генерируются гораздо проще . Однако на устройствах вроде ESP8266 и ESP32, где в фоновом режиме выполняются задачи, связанные, например, с Wi-Fi, требуются специальные драйверы, компенсирующие влияние этих процессов и устраняющие мерцание светодиодов . Следует помнить, что технология прямого доступа к памяти наиболее эффективно работает на моделях Raspberry Pi 3B+ и 4B, тогда как на более старых или компактных версиях — таких как A+, 3B, Pi Zero и Pi Zero W — RGB-светодиоды могут работать нестабильно . 684 РАЗДЕЛ 7 Создание роботов
Эта команда запускает циклическую радужную подсветку на всех двенадцати RGB-светодиодах, расположенных на задней и нижней частях робота. Драйвер, управляющий этой светодиодной лентой, довольно сложен, однако вам не нужно писать его с нуля — в комплекте уже есть готовый код, который позволяет легко управлять светодиодами и изменять их цвета. Кроме того, приведены примеры использования этих светодиодов для других проектов и эффектов. Мы особенно любим работать с такими светодиодами — они универсальны, эффектно выглядят и часто применяются в самых разных разработках. Камера Pi Камера, входящая в комплект PiCar-B, — это классическая камера Raspberry Pi версии 2.1 (см. рисунок 2.7). Она оснащена 8-мегапиксельным сенсором Sony и поддерживает съемку с максимальным разрешением 3280×2464 пикселей при частоте 15 кадров в секунду. Последние модели камер от Raspberry Pi Foundation имеют сенсор с разрешением 12,3 мегапикселя и крепление для сменных объективов, однако они слишком громоздки для использования в небольшом роботе, таком как PiCar- B. Рисунок 2.7. Камера Raspberry Pi и кабель Камера обладает хорошим цветовым откликом и полностью поддерживается в Python, а также в OpenCV — популярной библиотеке компьютерного зрения с открытым исходным кодом, которую мы будем использовать позже в главе 4 этого раздела. Камера подключается к Raspberry Pi через параллельный шлейф, соединенный с разъемом на плате. ГЛАВА 2 Создание вашего первого робота на Python 685
Пример ниже демонстрирует работу с камерой: программа открывает небольшое окно в графическом интерфейсе Raspberry Pi, отображает изображение с камеры, ожидает 20 секунд, затем изменяет положение и размер окна, делает короткую паузу, снова перемещает окно и закрывает его: print () print ("—————————-") print ("Open Video Window") print ("—————————-") camera.resolution = (1024, 768) camera.start_preview(fullscreen=False, window=(100,100,256,192)) time.sleep(20) camera.preview.window=(200,200,256,192) time.sleep(2) camera.preview.window=(0,0,512,384) time.sleep(2) camera.close() СОВЕТ Чтобы просматривать видео с камеры робота, необходимо запустить графический интерфейс Raspberry Pi. Если вы работаете без подключенного монитора, у вас есть два варианта: подключить клавиатуру, мышь и дисплей напрямую к Raspberry Pi или настроить удаленный доступ с помощью VNC. Использование VNC позволяет управлять Raspberry Pi через экран вашего основного компьютера, как если бы вы использовали обычный монитор. В интернете доступно множество инструкций по настройке этой функции, включая официальное руководство Raspberry Pi Foundation, опубликованное по адресу: https://www.raspberrypi.com/ documentation/computers/remote-access.html. В этой главе мы будем использовать именно VNC, поскольку это удобнее при работе с мобильным роботом. Однако, если вам проще подключить периферию напрямую, вы можете использовать стандартную схему с монитором, мышью и клавиатурой. Если вы работаете через VNC, выполните следующие шаги для правильной настройки отображения видео. Щелкните правой кнопкой мыши по значку VNC-сервера на панели задач и выберите пункт «Параметры» (Options). В открывшемся окне выберите вкладку «Troubleshooting» на левой панели и установите флажок «Enable Direct Capture Mode». Эта настройка необходима для корректного отображения видеопотока с камеры. Более подробные инструкции по этому процессу приведены в разделе «Сборка робота» далее в этой главе. 686 РАЗДЕЛ 7 Создание роботов
Ультразвуковой датчик Ультразвуковой датчик, установленный в PiCar-B, предназначен для бесконтактного измерения расстояния и работает на частоте 40 кГц (рисунок 2.8). Рисунок 2.8. Ультразвуковой датчик расстояния Когда на сигнальный вывод подается импульсный триггерный сигнал длительностью более 10 микросекунд, ультразвуковой датчик генерирует восемь циклов звука с частотой 40 кГц и ожидает отраженный эхо-сигнал. Время, в течение которого сохраняется высокий уровень эхо-сигнала, прямо пропорционально расстоянию до объекта. Принцип работы прост, но при этом очень эффективен. Расстояние вычисляется по формуле: Расстояние = (Время высокого уровня эхо-сигнала × Скорость звука) / 2, где скорость звука принимается равной 340 м/с. В приведенном ниже коде вызов ultra.checkdist() инициирует измерение: программа устанавливает выходной сигнал GPIO для отправки ультразвукового импульса, затем ожидает отраженное эхо, фиксируя момент его возвращения. После этого она вычисляет время прохождения сигнала туда и обратно и определяет расстояние в метрах, которое далее преобразуется в сантиметры: print () print ("-------------------") print ("Ultrasonic Distance Test") print ("-------------------") average_dist = 0.0 for i in range(0,10): distance = ultra.checkdist() ГЛАВА 2 Создание вашего первого робота на Python 687
average_dist = average_distance + distance print ("Distance = {:6.3f}см ".format( distance*100)) time.sleep(1.0) average_distance = average_distance / 10 print ("average_dist={:6.3f}см".format(average_dist*100)) Сборка робота Набор PiCar-B включает подробное руководство по сборке с наглядными иллюстрациями (см. рисунок 2.9). На полную сборку и подготовку робота к тестированию у нас ушло примерно четыре часа. ОСТОРОЖНО Важно: не переходите дальше главы 2 в официальном руководстве по сборке PiCar- B. Третья глава посвящена установке программного обеспечения Adeept на Raspberry Pi, однако перед этим рекомендуется протестировать робота с помощью программ, приведенных в этой книге. Это позволит лучше понять работу отдельных компонентов и принцип взаимодействия между ними. Перед началом сборки ознакомьтесь с приведенными далее полезными советами, затем соберите робота и вернитесь к этому разделу, чтобы приступить к тестированию. Первым этапом будет калибровка сервоприводов, за которой последует полное тестирование системы. На рисунке 2.10 показан полностью собранный робот PiCar- B. Рисунок 2.9. Пример схем из руководства по сборке 688 РАЗДЕЛ 7 Создание роботов
Рисунок 2.10. Собранный PiCar-B с подключенной проводкой ПРАВИЛЬНОЕ ВЫКЛЮЧЕНИЕ RASPBERRY PI В отличие от большинства компьютеров, Raspberry Pi не имеет отдельного выключателя питания . Однако, как и у других устройств, простое отключение питания может привести к повреждению SD-карты, на которой хранятся операционная система, программы и данные . Поэтому перед выключением Raspberry Pi необходимо выполнить безопасное завершение работы . Чтобы корректно выключить устройство, введите в окне терминала команду: sudo halt После этого зеленый индикатор активности (ACT) мигнет десять раз с интервалом около полсекунды, а затем погаснет . Это будет означать, что все процессы завершены, и питание можно безопасно отключить — выдернуть шнур из розетки или выключить блок питания . Красный индикатор питания останется включенным до тех пор, пока питание действительно подается на Raspberry Pi . Вот несколько полезных советов по сборке робота: СОВЕТ » На левой стороне платы управления двигателями (если смотреть спереди робота) расположен выключатель питания . Он отключает подачу энергии от аккумуляторов, хотя в руководстве этот выключатель не упоминается . ГЛАВА 2 Создание вашего первого робота на Python 689
» Для подключения камеры Raspberry Pi используйте более длинный из двух прилагаемых шлейфов — короткий просто не дотянется . » Проложите провода так, как показано на рисунке 2 .10, чтобы избежать заклинивания двигателей . Зафиксируйте провода пластиковыми кабельными стяжками, оставив достаточно свободного пространства для движения сервоприводов и корпуса . В комплект также входит кабельный канал, в который можно уложить часть проводов . » При сборке внимательно следите за ориентацией пластиковых деталей и сервоприводов . Многие элементы имеют различия между верхней и нижней частями, поэтому важно правильно их расположить . » Постарайтесь не уронить мелкие винты — их бывает очень трудно найти! Тестирование робота После того как сборка робота завершена, наступает следующий важный этап — калибровка. Теперь нужно убедиться, что все компоненты работают корректно: двигатели вращаются в правильном направлении, сервоприводы двигаются в нужных пределах, датчики корректно считывают информацию, а светодиоды загораются так, как задумано. Калибровка сервоприводов Перед началом тестирования первой задачей будет калибровка сервоприводов. Она необходима потому, что, даже если при сборке вы оставили сервопривод в среднем положении, фактический центр и крайние точки могут отличаться. Кроме того, в процессе сборки положение сервопривода нередко слегка смещается. Калибровка позволяет определить корректные минимальные, максимальные и центральные значения для каждого сервопривода, чтобы дальнейшее управление было точным. Программа calibrateServo.py служит для проверки диапазона движения всех трех сервоприводов робота. Она последовательно перемещает каждый сервопривод от одного крайнего положения к другому, чтобы вы могли визуально наблюдать за его работой и записать минимальные, максимальные и средние значения углов поворота. Эти значения выводятся прямо в терминал. После того как вы определите границы движения, 690 РАЗДЕЛ 7 Создание роботов
их следует вручную внести в файл calValues.py, который используется всеми остальными программами управления роботом. Хотя значения, предоставленные в книге, подходят для авторского экземпляра PiCar-B, у вашего робота они могут немного отличаться из-за допусков механических деталей или незначительных смещений при сборке. Поэтому обязательно выполните калибровку самостоятельно, чтобы убедиться, что сервоприводы двигаются в полном, но безопасном диапазоне. Далее следует сам код программы calibrateServo.py, который выполняет описанную процедуру: #!/usr/bin/python3 # калибровка сервоприводов import time import Adafruit_PCA9685 import calValues #импорт настроек для сервоприводов pwm = Adafruit_PCA9685.PCA9685() pwm.set_pwm_freq(60) #привязка сервоприводов # pmw 0 наклон головы HEAD_TILT_SERVO = 0 # pwm 1 поворот головы HEAD_TURN_SERVO = 1 # pwm 2 поворот колес WHEELS_TURN_SERVO = 2 if __name__ == '__main__': print("--------------------") print("calibrate wheel turn") print("--------------------") for i in range(calValues.turn_right_max, calValues.turn_left_max,10): pwm.set_pwm(WHEELS_TURN_SERVO,0, i) print("servoValue= ", i) time.sleep(0.5) print("--------------------") print("калибровка поворота головы") print("--------------------") for i in range(calValues.look_right_max, ГЛАВА 2 Создание вашего первого робота на Python 691
calValues.look_left_max,10): pwm.set_pwm(HEAD_TURN_SERVO,0, i) print("servoValue = ", i) time.sleep(0.5) print("--------------------") print("calibrate head turn") print("--------------------") for i in range(calValues.look_down_max, calValues.look_up_max,-10): pwm.set_pwm(HEAD_TILT_SERVO,0, i) print("servoValue = ", i) time.sleep(0.5) Код программы достаточно прост, но цикл, управляющий движением сервопривода, заслуживает отдельного внимания: for i in range(calValues.turn_right_max, calValues.turn_left_max,10): pwm.set_pwm(WHEELS_TURN_SERVO,0, i) print("servoValue = ", i) time.sleep(0.5) Цикл последовательно проходит по диапазону сервопривода, заданному в calValues.py, перемещая «голову» справа налево с шагом 10. Это позволяет наглядно определить крайние правое и левое положения, а также центр (с учетом конструктивных ограничений робота) и внести эти значения в calValues.py. Файл calValues.py содержит калибровочные параметры сервоприводов. После измерений вы заменяете в нем исходные значения на свои, полученные из программы calibrateServos.py: # Калибровочные значения # голова look_up_max = 150 # look_down_max = 420 # look_tilt_middle = 330 # # поворот головы look_right_max = 200 look_left_max = 450 look_turn_middle = 310 # колеса turn_right_max = 180 turn_left_max = 460 turn_middle = 320 692 РАЗДЕЛ 7 Создание роботов сервоприводов максимальный подъем максимальный наклон вниз среднее положение наклона
# скорость поворота look_turn_speed = 5 # скорость двигателя left_spd = 100 #Скорость машины right_spd = 100 #Скорость машины Подготовка к запуску тестов вашего ровера на Python Теперь, когда вы собрали PiCar-B и откалибровали сервоприводы, можно переходить к тестированию. Ранее мы рекомендовали не устанавливать программное обеспечение Adeept, однако если вы все же это сделали, необходимо отключить его автозапуск. Для этого откройте файл ~/.config/autostart/car.desktop, найдите и измените строку: Exec=sudo python3 /home/Adeept_PiCar-B/server/server.py на: #Exec=sudo python3 /home/Adeept_PiCar-B/server/server.py Чтобы изменения вступили в силу, перезагрузите Raspberry Pi с помощью команды sudo reboot. Установка программного обеспечения для тестирования PiCar-B на Python Теперь необходимо загрузить и запустить программу, предназначенную для тестирования всех функций робота PiCar- B. Чтобы загрузить программное обеспечение, выполните следующие шаги: 1. Перейдите на сайт www.dummies.com/go/pythonaiofd3e . 2. Найдите раздел Downloads . 3. Выберите ссылку для скачивания кода, соответствующую Book 7 (раздел 7) . Перейдите в каталог Book 7 Testing и выполните следующие действия, чтобы обеспечить корректную работу 12 программируемых RGB-светодиодов на вашем Raspberry Pi. ГЛАВА 2 Создание вашего первого робота на Python 693
1. В окне терминала введите команду: sudo apt-get install build-essential python3.dev git scons swig Эта команда установит несколько библиотек разработчика, необходимых для компиляции программного обеспечения, управляющего программируемыми RGB-светодиодами. 2. Загрузите исходный код драйвера RGB-светодиодов с GitHub. Используйте команду git clone, чтобы скопировать весь репозиторий на ваш Raspberry Pi: git clone https://github.com/jgarff/rpi_ws281x.git 3. Перейдите в каталог rpi_ws281x и выполните компиляцию. Введите последовательно: cd rpi_ws281x scons 4. Установите библиотеку Python 3: sudo pip install rpi_ws281x 5. Установите библиотеку PCA9685: sudo pip install adafruit-pca9685 Тестовый код PiCar-B на Python Тестовый файл Python содержит примерно 370 строк кода, поэтому здесь приводятся только ключевые части, которые уже были подробно рассмотрены ранее при описании отдельных компонентов и датчиков робота. В каталоге Testing вы также найдете несколько вспомогательных библиотек и файлов, необходимых для корректной работы тестовой программы. Чтобы запустить тест всего робота PiCar-B, выполните следующую команду в терминале Raspberry Pi: sudo python3 PiCar-B-Test.py Вы сразу увидите, как ваш робот начнет выполнять тестовую последовательность. 694 РАЗДЕЛ 7 Создание роботов
Тестирование видео с камеры Pi Далее необходимо протестировать камеру Raspberry Pi, установленную на вашем роботе. Перед запуском убедитесь, что вы работаете в графическом интерфейсе Raspberry Pi или используете удаленное подключение через VNC. Тестовая программа для проверки камеры Pi (PiCar-B-Video-Test.py) находится в каталоге Testing и выглядит следующим образом: #!/usr/bin/python3 DEBUG = True VIDEOTEST = True # выполняет видеотесты для PiCar-B import RPi.GPIO as GPIO import motor import ultra import socket import time import threading import led import os from picamera2 import Picamera2, Preview #from Picamera2.array import PiRGBArray import cv2 import calValues if __name__ == '__main__': camera = Picamera2() #Инициализация камеры camera.configure(camera.create_preview_configuration( main={"format": 'XRGB8888', "size": (640, 480)})) camera.framerate = 7 try: print ("-------------------") print ("-------------------") print (" PiCar2. Video Test") print (" Must be run from a GUI") print ("-------------------") print ("-------------------") print () print () if (VIDEOTEST): print () print ("-------------------") print ("Open Video Window") print ("-------------------") ГЛАВА 2 Создание вашего первого робота на Python 695
camera.preview_configuration.main.size = (640,480) camera.preview_configuration.main.format = "RGB888" camera.preview_configuration.align() camera.configure("preview") camera.start() t_end = time.time() + 20 while time.time() < t_end: im = camera.capture_array() cv2.imshow("Video Test", im) if cv2.waitKey(1)==ord('q'): break cv2.destroyAllWindows() camera.close() except KeyboardInterrupt: print("done") destroy() Запустите данную программу с помощью следующей команды: python3 PiCar-B-Video-Test.py Если при запуске кода возникают проблемы с правами доступа, добавьте sudo перед командой. Сообщение QStandardPaths: XDG_RUNTIME_DIR not set можно игнорировать. Программа открывает небольшое окно с изображением с камеры в графическом интерфейсе Raspberry Pi (или на вашем ноутбуке при работе через VNC), ждет 20 секунд и закрывает окно. На этом сборка и базовое тестирование робота завершены. В следующей главе вы дадите роботу «мозг» на Python и даже повеселитесь!
В этой главе вы узнаете: » Как заставить вашего робота двигаться и реагировать на окружающую среду » Что такое автономные транспортные средства и как они работают Гл а в а 3 Программирование вашего робота-ровера Первые две главы этого раздела дали общее представление о том, как устроены роботы и из каких компонентов они состоят. Еще важнее то, что они показали, каким образом язык Python может управлять этими компонентами, заставляя их работать согласованно для выполнения конкретных роботизированных задач. Это действительно большой объем информации. В этой главе вы узнаете, как объединить аппаратные компоненты и программное обеспечение, чтобы создать простой «мозг» для робота — систему, позволяющую вашему роботу-роверу двигаться самостоятельно. Такой ровер, конечно, не станет полноценным беспилотным автомобилем, но после изучения этой главы вы получите представление о том, как программируются большие автономные машины. Создание простого высокоуровневого интерфейс а на Python В этом разделе создается небольшой модуль Python, предназначенный для упрощения работы с роботом. Он позволяет писать более сложные программы, скрывая при этом всю техническую сложность управления аппаратными компонентами. ГЛАВА 3 Программирование вашего робота-ровера 697
Этот высокоуровневый интерфейс реализован в виде Python-файла класса с именем RobotInterface.py. Полный код слишком объемен, чтобы приводить его целиком в книге, но его можно скачать с сайта www.dummies.com/go/pythonaiofd3e. В книге будут подробно рассмотрены некоторые функции из этого модуля, а затем показано, как использовать оставшуюся часть программы. Функция motorForward() Функция motorForward() служит типичным примером функции, используемой для управления двигателями в классе RobotInterface: def motorForward(self, speed, delay): motor.motor_left(self.MOTOR_START, self.forward,speed) motor.motor_right(self.MOTOR_START, self.backward,speed) time.sleep(delay) motor.motor_left(self.MOTOR_STOP, self.forward,speed) motor.motor_right(self.MOTOR_STOP, self.backward,speed) Эта функция заставляет робота двигаться вперед в течение количества секунд, указанного в аргументе delay. При вызове функции следует передавать время именно в секундах (например, 2.3), а не в миллисекундах. Функция motorForward() по сути выполняет три действия: 1. Включает двигатели, обеспечивая движение вперед . 2. Удерживает это состояние в течение заданного времени в параметре delay . 3. Останавливает двигатели после истечения этого времени . Стоит отметить, что для управления каждым двигателем используется по две команды — две для включения и две для остановки. Как уже упоминалось в предыдущей главе, на плате управления установлены два контроллера двигателей. Благодаря этому код будет работать корректно, даже если двигатели будут подключены в обратном порядке во время сборки. 698 РАЗДЕЛ 7 Создание роботов
Функция wheelsLeft() Эта функция поворачивает передние колеса робота полностью влево, а затем делает паузу продолжительностью 50 миллисекунд: def wheelsLeft(self): pwm.set_pwm(self.WHEELS_TURN_SERVO, 0, calValues.turn_left_ max) time.sleep(0.05) Функция wheelsLeft() устанавливает сервопривод WHEELS_TURN_SERVO в крайнее левое положение колес, а затем делает задержку 50 миллисекунд. Эта задержка необходима для предотвращения скачков тока при подаче нескольких команд на сервоприводы подряд. Когда сервопривод получает команду на движение, он кратковременно потребляет повышенный ток. Если сразу после этого дать следующую команду, суммарная нагрузка на источник питания может оказаться слишком высокой, что приведет к нестабильной работе или даже перезапуску Raspberry Pi. Задержка в 50 мс позволяет току стабилизироваться после выполнения первой команды, прежде чем отправляется следующая. Это простое, но эффективное решение для обеспечения надежной работы всех сервоприводов и предотвращения перегрузки питания. Функция wheelsPercent() Функция wheelsPercent() позволяет пользователю устанавливать сервопривод на определенный процент от полного диапазона движения сервомотора. Значение изменяется от крайнего левого положения (0) до крайнего правого (100) для колес; значение 50 примерно соответствует среднему положению. Если диапазон движения вашего сервопривода асимметричен, значение 50 может не совпадать с точным центром — в этом случае используйте функцию wheelsMiddle() для выравнивания колес. Следующий код вычисляет полный диапазон движения сервомотора, умножает его на заданный процент и устанавливает сервомотор в соответствующее положение: def wheelsPercent(self,percent): adder = calValues.turn_left_max + (calValues.turn_left_max – calValues.turn_right_max)*(percent/100.0) ГЛАВА 3 Программирование вашего робота-ровера 699
pwm.set_pwm(self.WHEELS_TURN_SERVO, 0,int(calValues.turn_ right_max + adder)) time.sleep(0.05) Выполнение одиночного движения с помощью Python В этом разделе вы объедините функции, рассмотренные ранее, чтобы выполнить простое движение робота с помощью Python. Сначала перейдите на страницу поддержки книги по адресу: www.dummies.com/go/pythonaiofd3e и скачайте программное обеспечение для главы 3 раздела 7 (Book 7). Приведенный ниже код заставляет робота проехать небольшое расстояние вперед с помощью функции motorForward(), а затем вернуться назад с помощью motorBackward(). Это хороший пример того, как использование высокоуровневых функций Python упрощает управление действиями робота. Ниже показан код Python, который выполняет это движение: #!/usr/bin/python3 # Тест интерфейса робота import RobotInterface import time RI = RobotInterface.RobotInterface() print ("Short Move Test") RI.wheelsMiddle() RI.motorForward(100,1.0) time.sleep(1.0) RI.motorBackward(100,1.0) Сначала импортируйте библиотеку класса RobotInterface, а также библиотеку time, которая используется для функции sleep(). Обратите внимание, насколько короткий получается код — все сложные операции, связанные с управлением роботизированным интерфейсом, скрыты внутри модуля RobotInterface: 700 РАЗДЕЛ 7 Создание роботов
import RobotInterface import time Затем инициализируйте модуль RobotInterface и присвойте его экземпляр переменной объекта RI: RI = RobotInterface.RobotInterface() print ("Short Move Test") Далее установите колеса в центральное положение, вызвав функцию wheelsMiddle(): RI.wheelsMiddle() Теперь переходим к самому интересному — заставим робота двигаться. Сначала он поедет вперед в течение одной секунды, затем сделает паузу, после чего проедет назад и вернется в исходное положение: RI.motorForward(100,1.0) time.sleep(1.0) RI.motorBackward(100,1.0) Довольно просто, правда? Сохраните файл под именем singleMove.py. Запускайте все программы робота в этой главе с использованием команды sudo (например, sudo python3 singleMove.py). Это необходимо, потому что управление программируемыми светодиодами требует доступа к системному устройству Linux /dev/mem, который доступен только с правами администратора. А теперь, когда робот успешно движется, пришло время заняться документацией — опишем функции класса RobotInterface. Функции класса RobotInterface В этом разделе разберем функции класса RobotInterface. Сначала вы увидите тестовую программу Robot Interface, после чего перейдем к созданию программного обеспечения для робота. Класс RobotInterface опирается как на исходный код соавтора Джона, так и на решения Adeept. Все необходимое уже включено в код этой главы, поэтому устанавливать программное обеспечение Adeept не требуется. ГЛАВА 3 Программирование вашего робота-ровера 701
Функции управления передними светодиодами Следующие функции управляют двумя светодиодами на передней части робота. set_Front_LED_On() Функция set_Front_LED_On() включает передний светодиод: set_Front_LED_On(colorLED) Помните, что оба передних светодиода являются трехцветными — в каждом находятся отдельные красный, зеленый и синий светодиоды, которыми можно управлять независимо. Параметр colorLED определяет, с какой стороны робота включить светодиод и какого он будет цвета. Цвет и сторону задают с помощью следующих констант из класса RobotInterface: RobotInterface.left_R RobotInterface.left_G RobotInterface.left_B RobotInterface.right_R RobotInterface.right_G RobotInterface.right_B Например, команда RobotInterface.left_R включает красный светодиод на левой стороне робота (если смотреть сзади). Можно вызывать несколько таких команд подряд, чтобы активировать все три светодиода. Повторное включение уже работающего светодиода не вызывает проблем — команда просто игнорируется. Можно также написать более продвинутый драйвер для управления светодиодами, использующий широтно-импульсную модуляцию (ШИМ) на выводах ввода-вывода. Такой подход позволяет плавно регулировать яркость и создавать смешанные цвета. Однако если не использовать аппаратные ШИМ-выводы Raspberry Pi, светодиоды могут мерцать, так как операционная система Raspberry Pi является многозадачной и может прерывать выполнение программы. Более подробно процесс ШИМ и причины появления мерцания описаны в главе 1 раздела 6. Чтобы устранить эффект мерцания, можно написать программу на Python, использующую аппаратную ШИМ-генерацию платы управления сервоприводами PCA9685. 702 РАЗДЕЛ 7 Создание роботов
set_Front_LED_Off() Функция set_Front_LED_Off() выключает передний светодиод: set_Front_LED_Off(colorLED) Как упоминалось в предыдущем разделе, эти два передних светодиода являются RGB-светодиодами с красным, зеленым и синим компонентами, каждый из которых можно контролировать независимо. Параметр colorLED определяет сторону робота и цвет светодиода, который нужно выключить. Для выбора цвета и стороны используются следующие константы из класса RobotInterface: RobotInterface.left_R RobotInterface.left_G RobotInterface.left_B RobotInterface.right_R RobotInterface.right_G RobotInterface.right_B Например, команда set_Front_LED_Off(RobotInterface.left_R) выключает красный светодиод на левой стороне робота (если смотреть сзади). Можно выполнить несколько таких вызовов подряд, чтобы отключить все три цвета светодиода. Повторное выключение уже отключенного светодиода не вызывает ошибок и просто игнорируется. Функции для управления светодиодной лентой У робота установлены 12 программируемых RGB-светодиодов, размещенных в четырех различных местах. Все они соединены последовательно и управляются через одну общую линию данных, проходящую через всю цепочку светодиодов. (Подробнее об этом см. во врезке «Что такое программируемый RGB».) Для управления такими светодиодами требуется строго выверенная последовательность импульсов, синхронизированных по времени. Однако из-за особенностей операционной системы Raspberry Pi (Linux) генерировать эти импульсы с нужной точностью с помощью Python и обычных GPIO невозможно. Разработчик под ником jgarff создал специальный драйвер, использующий интерфейс прямого доступа к памяти (DMA) на Raspberry Pi. Этот драйвер позволяет передавать данные светодиодам с высокой ГЛАВА 3 Программирование вашего робота-ровера 703
точностью и без сбоев. Библиотека, реализующая этот метод, называется rpi_ws281x и включена в комплект загрузки с сайта www.dummies.com/go/ pythonaiofd3e. Мы используем этот драйвер для генерации управляющих импульсов. При этом все взаимодействие с ним полностью скрыто внутри программного обеспечения Robot Interface, поэтому пользователю не нужно беспокоиться о низкоуровневых деталях. ЧТО ТАКОЕ RGB? RGB расшифровывается как red, green, blue (красный, зеленый, синий) . RGB-светодиод содержит три отдельных светодиода — красный, зеленый и синий, которые, комбинируясь, могут воспроизводить практически любой цвет . RGB-модель является аддитивной цветовой моделью, то есть цвета создаются путем добавления света разных цветов в различных пропорциях . Например: • Чтобы получить красный цвет, устанавливают красный канал на 100%, а зеленый и синий — на 0% . • Чтобы получить белый цвет, все три канала устанавливаются на 100% . Обычно шкала значений каналов варьируется от 0 до 255, где 0% соответствует 0, а 100% — 255 . Например, светло-розовый цвет задается как (R=255, G=204, B=229) . Для подбора цветов можно воспользоваться удобным онлайнинструментом: https://www.w3schools.com/colors/colors_rgb.asp rainbowCycle() Вызов функции rainbowCycle() запускает анимацию «радужного цикла», которая использует все 12 программируемых RGB-светодиодов и последовательно изменяет их цвета, создавая эффект плавного переливания оттенков. Несмотря на то, что каждый модуль имеет только три светодиода — красный, зеленый и синий, — драйвер изменяет яркость каждого из них, чтобы получить широкую палитру цветов. 704 РАЗДЕЛ 7 Создание роботов
Пример вызова: rainbowCycle(wait_ms = 20, iterations = 3) Параметры: • • wait_ms — задержка между сменой цветов в миллисекундах . По умолчанию значение составляет 20 мс; iterations — количество полных цветовых циклов (каждый цикл содержит 256 различных оттенков), выполняемых перед завершением функции . По умолчанию выполняются 3 цикла . colorWipe() Функция colorWipe() устанавливает одинаковый цвет для всех 12 программируемых светодиодов или полностью их выключает: colorWipe(color) Например, colorWipe(color(0,0,0)) выключает все светодиоды. Параметр color задает значения RGB для нужного цвета и передается в функцию colorWipe(). Подробнее о функции color() рассказывается далее в этой главе. theaterChaseRainbow() Функция theaterChaseRainbow() создает эффект «бегущих огней» с плавным переходом цветов на всех 12 программируемых светодиодах: theaterChaseRainbow(wait_ms = 50) Параметр wait_ms определяет временную задержку между шагами анимации, выраженную в миллисекундах. Он контролирует скорость эффекта «бегущих огней»: чем меньше значение, тем быстрее происходит смена светодиодов. По умолчанию используется задержка 50 миллисекунд. setPixelColor() Функция setPixelColor() задает цвет для конкретного программируемого RGB-светодиода, нумерация которых начинается с 0 и заканчивается 11: setPixelColor(pixel, color, brightness) Параметр color определяет цвет с помощью функции color(), например color(255,255,32). ГЛАВА 3 Программирование вашего робота-ровера 705
Параметр brightness задает уровень яркости в диапазоне от 0 до 255 для всей цепочки RGB-светодиодов. Указанный цвет автоматически масштабируется в соответствии с выбранной максимальной яркостью. color() Функция color() — это вспомогательная функция, преобразующая значения R, G и B в одно 24-битное целое число, которое используется внутренним драйвером программируемых RGB-светодиодов: color(red, green, blue, white = 0) Параметры red, green и blue задаются целыми числами в диапазоне от 0 (выключено) до 255 (максимальная яркость). Параметр white = 0 предназначен для программируемых RGB-светодиодов и обычно не используется в стандартных RGB-модулях. allLEDSOff() Функция allLEDSOff() выключает все светодиоды на роботе — как два передних RGB-светодиода, так и всю 12-светодиодную цепочку программируемых светодиодов: allLEDSOff() Функция ультразвукового датчика расстояния Ультразвуковой датчик расстояния определяет расстояние до объекта, отправляя короткий звуковой импульс высокой частоты и измеряя время, за которое отраженный сигнал возвращается к приемнику. Зная скорость звука, можно рассчитать расстояние до препятствия перед датчиком. Этот метод прост и эффективен, хотя и менее точен, чем лазерные дальномеры. Ультразвуковые датчики работают медленнее, чем лазеры, и имеют более широкий угол рассеивания сигнала, что может снижать точность измерений при обнаружении неровных или маленьких объектов. fetchUltraDistance() Функция fetchUltraDistance() выполняет измерение расстояния с помощью ультразвукового датчика, установленного в «голове» робота, и возвращает результат в сантиметрах (см): fetchUltraDistance() 706 РАЗДЕЛ 7 Создание роботов
Основные функции управления двигателем Основной двигатель робота отвечает за перемещение всей конструкции. Функции управления двигателем позволяют задавать скорость вращения и длительность работы мотора. motorForward() Функция motorForward() запускает движение робота вперед с определенной скоростью на заданное время, после чего автоматически останавливает двигатель: motorForward(speed, delay) Параметр speed определяет коэффициент заполнения ШИМ-сигнала, который подается на GPIO-вывод, управляющий мотором. Значение может быть от 0 (двигатель выключен) до 100 (максимальная скорость). Параметр delay задает длительность работы двигателя в секундах — по истечении этого времени питание автоматически отключается. motorBackward() Функция motorBackward() заставляет робота двигаться назад с заданной скоростью в течение определенного времени, после чего двигатель автоматически выключается: motorBackward(speed, delay) Параметр speed задает коэффициент заполнения ШИМ-сигнала, подаваемого на GPIO-вывод, управляющий мотором. Значение может быть от 0 (двигатель выключен) до 100 (максимальная скорость). Параметр delay определяет, сколько секунд двигатель будет работать, прежде чем питание будет отключено. stopMotor() Функция stopMotor() останавливает основной двигатель мгновенно: stopMotor() Эта функция особенно полезна при управлении двигателем в отдельном потоке. Поток можно рассматривать как независимую минипрограмму, работающую параллельно с основной. В главе 4 этого раздела будет показано, как создавать многопоточные программы для робота. Такой подход к программированию открывает множество возГЛАВА 3 Программирование вашего робота-ровера 707
можностей — он позволяет выполнять несколько задач одновременно и делает управление роботом более плавным и отзывчивым. Функции сервоприводов Функции сервоприводов управляют тремя сервоприводами робота: поворотом головы, наклоном головы и передними колесами. ЗАПОМНИТЕ Все используемые в этих функциях константы, например «Полностью влево» или «Полностью вверх», заданы в файле calValues.py. Подробная информация о калибровке сервоприводов и настройке этих значений с помощью программы calibrateServos.py приведена в главе 2 этого раздела. headTurnLeft() Функция headTurnLeft() перемещает сервопривод поворота головы в крайнее левое положение: headTurnLeft() headTurnRight() Функция headTurnRight() аналогичным образом поворачивает голову робота в крайнее правое положение: headTurnRight() headTurnMiddle() Функция headTurnMiddle() устанавливает сервопривод поворота головы робота в центральное положение, направляя камеру и датчик прямо вперед: headTurnMiddle() headTurnPercent() Функция headTurnPercent() позволяет задать положение головы робота с точностью до процента от полного диапазона движения сервопривода. Значение параметра percent варьируется от 0 (крайнее левое положение) до 100 (крайнее правое положение): headTurnPercent(percent) 708 РАЗДЕЛ 7 Создание роботов
Функция headTurnPercent() используется для более точного позиционирования головы робота. Положения «крайнее левое» и «крайнее правое» определяются в файле calValues.py. Подробности о калибровочных значениях и способах их настройки с помощью программы calibrateServos.py приведены в главе 2 еэтого раздела. Параметр percent принимает значения от 0 до 100 и задает линейное положение головы в процентах от крайнего левого до крайнего правого угла. Следует учитывать, что значение 50 может не совпадать с идеальным центром, так как сервоприводы могут быть установлены с небольшим смещением относительно среднего положения. headTiltDown() Функция headTiltDown() опускает голову робота до нижнего предела движения: headTiltDown() headTiltUp() Функция headTiltUp() поднимает голову робота до верхнего предела движения: headTiltUp() headTiltMiddle() Функция headTiltMiddle() устанавливает голову робота в нейтральное положение, при котором она направлена прямо вперед: headTiltMiddle() headTiltPercent() Функция headTiltPercent() используется для точного управления наклоном головы робота в пределах от 0 (полностью вниз) до 100 (полностью вверх). headTiltPercent(percent) Она позволяет плавно изменять угол наклона головы. Положения «полностью вниз» и «полностью вверх» задаются в файле calValues.py. Подробнее о настройке этих значений можно узнать в главе 2 этого раздела, где описана программа calibrateServos.py. ГЛАВА 3 Программирование вашего робота-ровера 709
Параметр percent принимает значения от 0 до 100 и определяет линейную долю между нижним и верхним положением головы. Значение 50 не всегда соответствует точному центру из-за индивидуальных особенностей установки сервоприводов и механической конструкции робота. wheelsLeft() Функция wheelsLeft() поворачивает передние колеса робота до упора влево. wheelsLeft() wheelsRight() Функция wheelsRight() поворачивает передние колеса робота до упора вправо. wheelsRight() wheelsMiddle() Функция wheelsMiddle() устанавливает передние колеса робота в среднее положение. wheelsMiddle() wheelsPercent() Функция wheelsPercent() поворачивает колеса от 0 (крайнее левое положение) до 100 (крайнее правое положение) wheelsPercent(percent) Эта функция полезна для более точной настройки направления передних колес робота. Положения «до упора влево» и «до упора вправо» определены в файле calValues.py. Подробности о калибровочных значениях и способах их настройки с помощью программы calibrateServos.py приведены в главе 2 данного раздела. Параметр percent принимает значения от 0 до 100 и представляет собой линейный процент поворота от левого до правого положения. Учтите, что значение 50 может не соответствовать точно середине, поскольку сервоприводы могут быть установлены не идеально по центру своего диапазона (как указано при калибровке), а также из-за особенностей конструкции вашего робота. 710 РАЗДЕЛ 7 Создание роботов
Общая функция для сервоприводов Мы добавили универсальную функцию для одновременного управления всеми сервоприводами. Вызов этой функции перемещает все сервоприводы в центральное положение. centerAllServos() Функция centerAllServos() устанавливает все сервоприводы в середину их диапазона, как это определено в файле calValues.py: centerAllServos() Тестирование интерфейса робота на Python Теперь, когда вы определили API (программный интерфейс приложения) робота, можно запустить системное тестирование с использованием класса RobotInterface на Python. Эта программа полезна по двум причинам: • • Во-первых, она проверяет работу всех функций класса RobotInterface. Во-вторых, она демонстрирует, как использовать каждую функцию в программе на Python. Следующая программа RITest.py предназначена для тестирования всех функций робота, управляемых классом RobotInterface: #!/usr/bin/python3 # Тест интерфейса робота import RobotInterface import time RI = RobotInterface.RobotInterface() print ("Robot Interface Test") print ("LED tests") RI.set_Front_LED_On(RI.left_R) time.sleep(0.1) RI.set_Front_LED_On(RI.left_G) ГЛАВА 3 Программирование вашего робота-ровера 711
time.sleep(0.1) RI.set_Front_LED_On(RI.left_B) time.sleep(1.0) RI.set_Front_LED_On(RI.right_R) time.sleep(0.1) RI.set_Front_LED_On(RI.right_G) time.sleep(0.1) RI.set_Front_LED_On(RI.right_B) time.sleep(1.0) RI.set_Front_LED_Off(RI.left_R) time.sleep(0.1) RI.set_Front_LED_Off(RI.left_G) time.sleep(0.1) RI.set_Front_LED_Off(RI.left_B) time.sleep(1.0) RI.set_Front_LED_Off(RI.right_R) time.sleep(0.1) RI.set_Front_LED_Off(RI.right_G) time.sleep(0.1) RI.set_Front_LED_Off(RI.right_B) time.sleep(1.0) RI.rainbowCycle(20, 1) time.sleep(0.5) # Выполняется 40 секунд RI.theaterChaseRainbow(50) time.sleep(0.5) print ("RI.Color(0,0,0)=", RI.Color(0,0,0)) RI.colorWipe(RI.Color(0,0,0)) time.sleep(1.0) for pixel in range (0,12): RI.setPixelColor(pixel,RI.Color(100,200,50),50) time.sleep(0.5) print (""Servo Tests") RI.headTurnLeft() time.sleep(1.0) RI.headTurnRight() time.sleep(1.0) RI.headTurnMiddle() time.sleep(1.0) RI.headTiltDown() 712 РАЗДЕЛ 7 Создание роботов
time.sleep(1.0) RI.headTiltUp() time.sleep(1.0) RI.headTiltMiddle() time.sleep(1.0) RI.wheelsLeft() time.sleep(1.0) RI.wheelsRight() time.sleep(1.0) RI.wheelsMiddle() time.sleep(1.0) print("servo scan tests") for percent in range (0,100): RI.headTurnPercent(percent) for percent in range (0,100): RI.headTiltPercent(percent) for percent in range (0,100): RI.wheelsPercent(percent) print("motor test") RI.motorForward(100,1.0) time.sleep(1.0) RI.motorBackward(100,1.0) print("ultrasonic test") print ("distance in cm =", RI.fetchUltraDistance()) print("general function test") RI.allLEDSOff() RI.centerAllServos() Запустите программу, введя в терминале команду: sudo python3 RITest.py Обратите внимание, что необходимо использовать sudo, поскольку для корректной работы светодиодов требуются права администратора (root), предоставляемые этой командой. Во время выполнения программы в окне терминала вы увидите примерно следующий вывод: Robot Interface Test LED tests RI.Color(0,0,0)= 0 Servo Tests ГЛАВА 3 Программирование вашего робота-ровера 713
servo scan tests motor test ultrasonic test distance in cm= 16.87312126159668 general function test ROS: ОПЕРАЦИОННАЯ СИСТЕМА ДЛЯ РОБОТОВ Мы создали довольно простой интерфейсный класс для робота PiCar- B . Модуль RobotInterface позволяет управлять роботом с помощью программы на Python . Если бы в этой книге было больше места, мы бы показали, как подключить вашего робота к операционной системе для роботов — ROS (произносится как «Росс») . На самом деле использование ROS для такого робота заслуживает отдельной книги . ROS предназначена для управления роботами в распределенной системе . Несмотря на название, ROS не является операционной системой в привычном смысле . Это программное промежуточное обеспечение, упрощающее разработку программ для сложной и разнородной среды, где задействованы различные типы роботов и датчиков . Благодаря ROS можно работать с разными роботами по единому принципу . ROS использует модель публикации и подписки, аналогичную газетной . Газета публикует статьи, но видят их только подписчики . Аналогично робот может публиковать данные — например, текущие показания ультразвукового датчика или поток с камеры, — а другие роботы или компьютеры могут подписываться на эти данные . При этом ваш робот может сам подписываться на чужие источники — например, получать температуру воздуха в помещении или изображения от других роботов . Преимущество такого подхода в том, что ваш робот становится частью экосистемы, состоящей из множества устройств, сенсоров и людей, обменивающихся информацией . Можно создать интерфейс ROS для вашего робота и управлять им удаленно, передавая данные с его датчиков на другие компьютеры . ROS и ее обновленная версия ROS2 — мощные системы, способные управлять как небольшими роботами, вроде вашего PiCar-B, так и крупными промышленными машинами . Более того, они позволяют координировать взаимодействие между ними, чтобы выполнять общие задачи . Подробнее о ROS можно узнать на официальном сайте: https://www.ros.org . 714 РАЗДЕЛ 7 Создание роботов
На фото Джон Шовик изображен рядом со своим промышленным роботом под управлением ROS . Координация движений моторов с датчиками Способность изменять и согласовывать движения моторов с показаниями датчиков играет решающую роль в перемещении в пространстве. Датчики обеспечивают не только информацию для выполнения действий, но и обратную связь о результате этих действий. Представьте, что вы ловите бейсбольный мяч перчаткой. Ваши датчики — это глаза и осязание. Глаза фиксируют движение мяча, после чего вы направляете руку, чтобы поймать его. Таким образом, вы координируете движение, используя зрение как датчик. Обратная связь возникает, когда вы ощущаете удар мяча о перчатку и одновременно видите, что он оказался в вашей руке, благодаря чему понимаете, когда нужно сжать пальцы. При этом мозг обновляет внутренние модели, помогая вам ловить мяч все лучше. ГЛАВА 3 Программирование вашего робота-ровера 715
PiCar-B оснащен двумя датчиками, которые позволяют роботу получать информацию из окружающей среды и анализировать ее. Ультразвуковой датчик определяет наличие объектов перед роботом, а камера фиксирует изображение мира вокруг. Однако создание программ, которые позволяют роботу распознавать увиденное, — достаточно сложная задача. В главе 4 мы рассмотрим, как использовать искусственный интеллект, чтобы роботы могли интерпретировать данные с камеры, и создадим пример на основе машинного обучения. Также в этой главе будет показано, как применять изображения, полученные с камеры, для анализа. В следующем примере внимание будет сосредоточено на более простом устройстве — ультразвуковом датчике расстояния. Программа на Python с именем simpleFeedback.py заставляет робота двигаться вперед или назад в зависимости от того, насколько близко находится объект перед ним. #!/usr/bin/python3 # Тест интерфейса робота import RobotInterface import time DEBUG = True RI = RobotInterface.RobotInterface() print ("Simple Feedback Test") RI.centerAllServos() RI.allLEDSOff() # Игнорировать расстояния больше одного метра DISTANCE_TO_IGNORE = 1000.0 # Приближение примерно к 10 см короткими движениями DISTANCE_TO_MOVE_TO = 10.0 # Сколько попыток сделает робот, прежде чем сдаться REPEAT_MOVE = 10 def bothFrontLEDSOn(color): RI.allLEDSOff() if (color == "RED"): RI.set_Front_LED_On(RI.right_R) RI.set_Front_LED_On(RI.left_R) return if (color == "GREEN"): RI.set_Front_LED_On(RI.right_G) 716 РАЗДЕЛ 7 Создание роботов
RI.set_Front_LED_On(RI.left_G) return if (color == "BLUE"): RI.set_Front_LED_On(RI.right_B) RI.set_Front_LED_On(RI.left_B) return try: Quit = False moveCount = 0 bothFrontLEDSOn("BLUE") while (Quit == False): current_distance = RI.fetchUltraDistance() if (current_distance >= DISTANCE_TO_IGNORE): bothFrontLEDSOn("BLUE") if (DEBUG): print("distance too far={:6.2f}см" .format(current_ distance)) elif (current_distance <= 10.0): # сброс счетчика движений # Робот достаточно близко bothFrontLEDSOn("GREEN") moveCount = 0 if (DEBUG): print("distance close enough ={:6.2f}см" .format(current_ distance)) time.sleep(5.0) # отъезд назад и повтор RI.motorBackward(100,1.0) else: if (DEBUG): print("moving forward={:6.2f}см" .format(current_distance)) # Короткий шаг вперед bothFrontLEDSOn("RED") RI.motorForward(90,0.50) moveCount = moveCount + 1 # Проверка условия завершения программы time.sleep(1.0) if (moveCount > REPEAT_MOVE): Quit = True except KeyboardInterrupt: print("program interrupted") ГЛАВА 3 Программирование вашего робота-ровера 717
print ("program finished") Программа simpleFeedback.py — наглядный пример применения принципа обратной связи в робототехнике. Сначала робот проверяет, находится ли он на расстоянии менее одного метра (1000 см) от стены. Если это так, он начинает медленно приближаться к ней короткими шагами. Когда расстояние сокращается до менее чем 10 см, робот останавливается, делает паузу на пять секунд, а затем отъезжает назад примерно на метр. Точное расстояние может немного меняться в зависимости от типа поверхности и уровня заряда аккумуляторов. После этого процесс повторяется. В коде предусмотрено условие: if (moveCount > REPEAT MOVE): Quit = True Особое внимание стоит обратить на использование светодиодов, которые сигнализируют окружающим о действиях робота. Такая визуальная обратная связь играет важную роль в обеспечении понятного, безопасного и эффективного взаимодействия между человеком и машиной. Основная логика программы построена на цикле while в языке Python. Пока программа не прервана вручную или не выполнено одно из условий выхода, робот продолжает выполнять цикл вплоть до разрядки аккумулятора. Чтобы запустить программу, скопируйте код в файл simpleFeedback.py и выполните команду sudo python3 simpleFeedback.py в каталоге главы 3 раздела 7 (Book 7, Chapter 3). После запуска на экране появятся результаты выполнения программы: Simple Feedback Test moving forward = 55.67cm moving forward = 44.48cm moving forward = 34.22cm moving forward = 26.50cm moving forward = 17.53 cm distance close enough = 9.67 cm moving forward = 66.64 cm moving forward = 54.25 cm moving forward = 43.55 cm moving forward = 36.27 cm moving forward = 28.44 cm moving forward = 21.08 cm moving forward = 13.55 cm distance close enough = 6.30 cm 718 РАЗДЕЛ 7 Создание роботов
moving forward = 64.51 moving forward = 52.89 moving forward = 43.75 moving forward = 33.95 moving forward = 26.79 ^Cprogram interrupted program finished cm cm cm cm cm Поэкспериментируйте с этим кодом: пробуйте свои идеи и изменяйте константы, чтобы увидеть, как меняется поведение робота, например, увеличьте скорость двигателей или уменьшите минимальное расстояние с 10 см до 5 см и проследите, какие эффекты это даст. Создание Python-мозга для нашего робота Теперь вы будете создавать простой самоуправляемый автомобиль: по сути, вы примените идеи из программы simpleFeedback.py, чтобы получить автономное транспортное средство, которое, пусть и не слишком умное, но наглядно демонстрирует использование обратной связи при принятии решений. «Мозг» на Python, который вы напишете, по сути представляет собой сочетание кода для обнаружения стены (рассмотренного ранее в этой главе) и кода, генерирующего случайные движения на основе данных от ультразвукового датчика. Во время тестирования мы заметили, что робот иногда застревает, поэтому добавили фрагмент, распознающий такие ситуации и выполняющий маневр отъезда назад. Примечание: перед запуском убедитесь, что аккумуляторы полностью заряжены. При частичном разряде скорость вращения моторов заметно падает; альтернативный способ снизить влияние этой проблемы — использовать аккумуляторы большей емкости. Ниже приведен код для «мозга» робота: #!/usr/bin/python3 # Мозг робота import RobotInterface import time ГЛАВА 3 Программирование вашего робота-ровера 719
from random import randint DEBUG = True RI = RobotInterface.RobotInterface() print(""Simple Robot Brain") RI.centerAllServos() RI.allLEDSOff() # Примерно 20 см CLOSE_DISTANCE = 20.0 # Сколько раз робот будет пытаться повернуть, прежде чем сдастся REPEAT_TURN = 10 def bothFrontLEDSOn(color): RI.allLEDSOff() if (color == "RED"): RI.set_Front_LED_On(RI.right_R) RI.set_Front_LED_On(RI.left_R) return if (color == "GREEN"): RI.set_Front_LED_On(RI.right_G) RI.set_Front_LED_On(RI.left_G) return if (color == "BLUE"): RI.set_Front_LED_On(RI.right_B) RI.set_Front_LED_On(RI.left_B) return STUCKBAND = 2.0 # проверка застрявшего робота по неизменному расстоянию def checkForStuckCar(cd,p1,p2): if (abs(p1.cd) < STUCKBAND): if (abs(p2.cd) < STUCKBAND): return True return False try: Quit = False turnCount = 0 bothFrontLEDSOn("BLUE") previous2distance = 0 previous1distance = 0 720 РАЗДЕЛ 7 Создание роботов
while (Quit == False): current_distance = RI.fetchUltraDistance() if (current_distance >= CLOSE_DISTANCE): bothFrontLEDSOn("BLUE") if (DEBUG): print("Continue straight ={:6.2f}см" .format(current_distance)) if (current_distance > 300): # проверяем расстояние current_distance = RI.fetchUltraDistance() if (current_distance > 300): # увеличиваем скорость RI.motorForward(90,1.0) else: RI.motorForward(90,0.50) turnCount = 0 else: if (DEBUG): print("distance close enough so turn={:6.2f}см" .format(current_distance)) bothFrontLEDSOn("RED") # определяем направление поворота # turn = 0 поворот налево # turn = 1 поворот направо turn = randint(0,1) if (turn == 0): # поворот налево # поворачиваем колеса вправо, так как # движемся задним ходом RI.wheelsRight() else: # поворот направо # поворачиваем колеса влево, так как # движемся задним ходом RI.wheelsLeft() time.sleep(0.5) RI.motorBackward(100,1.00) time.sleep(0.5) RI.wheelsMiddle() turnCount = turnCount+1 print("Turn Count=", turnCount) # проверка застрявшего автомобиля if (checkForStuckCar(current_distance, previous1distance, previous2distance)): ГЛАВА 3 Программирование вашего робота-ровера 721
# если застрял, пробуем сдать назад и выполнить случайный поворот bothFrontLEDSOn("RED") if (DEBUG): print("Stuck - Recovering={:6.2f}см" .format(current_distance)) RI.wheelsMiddle() RI.motorBackward(100,1.00) # определяем направление поворота # turn = 0 поворот налево # turn = 1 поворот направо turn = randint(0,1) if (turn == 0): # поворот налево # поворачиваем колеса вправо, так как # движемся задним ходом RI.wheelsRight() else: # поворот направо # поворачиваем колеса влево, так как # движемся задним ходом RI.wheelsLeft() time.sleep(0.5) RI.motorBackward(100,2.00) time.sleep(0.5) RI.wheelsMiddle() # сохраняем состояние для расстояний previous2distance = previous1distance previous1distance = current_distance # Теперь проверяем условие для остановки программы time.sleep(0.1) if (turnCount > REPEAT_TURN-1): bothFrontLEDSOn("RED") if (DEBUG): print("too many turns in a row") Quit = True except KeyboardInterrupt: print("program interrupted") print ("program finished") 722 РАЗДЕЛ 7 Создание роботов
Эта программа может показаться сложнее предыдущего примера с ультразвуковым датчиком из этой главы, но на самом деле она лишь немного расширяет тот же подход: мы сохранили базовую структуру на цикле while и добавили несколько новых функций. Во-первых, мы ввели условие увеличения скорости, когда робот находится на значительном расстоянии от препятствия (более 300 см), а затем повторно считываем показание датчика, чтобы убедиться, что дистанция по-прежнему не меньше 300 см, на случай если обстановка изменилась: if (current_distance >= CLOSE_DISTANCE ): bothFrontLEDSOn("BLUE") if (DEBUG): print("Continue straight={:6.2f}см" .format(current_distance)) if (current_distance > 300): # проверяем расстояние current_distance = RI.fetchUltraDistance() if (current_distance > 300): # увеличиваем скорость RI.motorForward(90,1.0) else: RI.motorForward(90,0.50) turnCount = 0 Робот продолжает двигаться короткими рывками, постепенно приближаясь к стене. Когда расстояние сокращается примерно до 10 см, программа принимает решение повернуть передние колеса в случайном направлении и немного отъезжает назад. Это позволяет роботу выбрать новое направление и продолжить исследование окружающего пространства, избегая столкновения с препятствием и демонстрируя принцип адаптивного поведения на основе обратной связи. if (DEBUG): print("distance close enough so turn={:6.2f}см" .format(current_distance)) bothFrontLEDSOn("RED") # определяем направление поворота # turn = 0 поворот налево # turn = 1 поворот направо turn = randint(0,1) if (turn == 0): # поворот налево # поворачиваем колеса направо, так как # движемся задним ходом RI.wheelsRight() else: ГЛАВА 3 Программирование вашего робота-ровера 723
# поворот направо # поворачиваем колеса налево, так как # движемся задним ходом RI.wheelsLeft() time.sleep(0.5) RI.motorBackward(100,1.00) time.sleep(0.5) RI.wheelsMiddle() turnCount = turnCount+1 print("Turn Count =", turnCount) Мы долго испытывали робота с этой логикой и заметили, что он иногда застревал, даже если ультразвуковой датчик продолжал показывать расстояние более 10 см. Это происходило, когда часть пути была заблокирована, но датчик не мог корректно распознать препятствие. Чтобы решить эту проблему, мы добавили в программу анализ трех последних измерений ультразвукового датчика. Если три последовательных значения отличались не более чем на ±2,0 см, робот делал вывод, что застрял. В ответ он начинал движение назад, поворачивался в случайном направлении и затем продолжал свое движение, исследуя пространство дальше. Такой подход оказался очень эффективным и позволил роботу успешно избегать ситуаций, в которых он ранее останавливался. if (checkForStuckCar(current_distance, previous1distance, previous2distance)): # если застрял, пробуем сдать назад и выполнить случайный поворот bothFrontLEDSOn("RED") if (DEBUG): print("Stuck - Recovering ={:6.2f}cm" .format(current_distance)) RI.wheelsMiddle() RI.motorBackward(100,1.00) # теперь определяем направление поворота # turn = 0 поворот налево # turn = 1 поворот направо turn = randint(0,1) if (turn == 0): # поворот налево # поворачиваем колеса вправо, так как # движемся задним ходом RI.wheelsRight() else: # поворот направо # поворачиваем колеса влево, так как # движемся задним ходом 724 РАЗДЕЛ 7 Создание роботов
RI.wheelsLeft() time.sleep(0.5) RI.motorBackward(100,2.00) time.sleep(0.5) RI.wheelsMiddle() Мы поместили робота в комнату с мебелью и сложной планировкой стен и запустили его, введя в окне терминала команду sudo python3 robotBrain.py, после чего в консоли появился отчет о ходе работы программы и ее действиях: Simple Robot Brain Continue straight =115.44 cm Continue straight =108.21 cm Continue straight =101.67 cm Continue straight = 95.67 cm Continue straight = 88.13 cm Continue straight = 79.85 cm Continue straight = 70.58 cm Continue straight = 63.89 cm Continue straight = 54.36 cm Continue straight = 44.65 cm Continue straight = 36.88 cm Continue straight = 28.32 cm Continue straight = 21.10 cm distance close enough so turn Turn Count = 1 Continue straight = 33.75 cm Continue straight = 25.12 cm distance close enough so turn Turn Count = 1 Continue straight = 40.51 cm Continue straight = 33.45 cm Continue straight = 24.73 cm distance close enough so turn Turn Count = 1 Continue straight = 35.72 cm Continue straight = 26.13 cm distance close enough so turn Turn Count = 1 Continue straight = 43.63 cm Continue straight = 37.74 cm Continue straight = 27.33 cm Continue straight = 84.01 cm = 11.33 cm = 18.20 cm = 14.83 cm = 18.56 cm Робот подъезжает к стене, выполняет несколько поворотов, пытаясь найти путь, а затем продолжает движение, обходя препятствия. ГЛАВА 3 Программирование вашего робота-ровера 725
УЛУЧШЕННАЯ АРХИТЕКТУРА МОЗГА РОБОТА Если рассмотреть программу robotBrain.py с точки зрения архитектуры, можно заметить, что основная ее часть представляет собой один цикл while, который последовательно опрашивает ультразвуковой датчик, выполняет действие (движение, поворот и т .д .), затем снова делает измерение . Такое чередование приводит к прерывистому поведению робота — небольшое движение, затем остановка для измерения, потом снова движение . Хотя этот подход является самым простым и подходит для базового примера, существуют более эффективные, но и более сложные архитектуры, выходящие за рамки текущего проекта . Для улучшения работы программы можно использовать потоки . Потоки — это независимые процессы, выполняющиеся одновременно и обменивающиеся данными . Так как несколько потоков работают параллельно, важно следить за тем, как именно они взаимодействуют, чтобы избежать ошибок . Один поток может, например, отправить другому устаревшие данные или изменить переменную, используемую в другом процессе . Также нужно не допустить, чтобы два потока одновременно обращались к одному и тому же оборудованию . При соблюдении определенных правил организация взаимодействия между потоками не представляет сложности . Подробнее о потоках рассказано в главе 4 данного раздела . Оптимальная архитектура для нашего робота могла бы выглядеть следующим образом: • поток управления двигателями — отвечает за запуск и остановку двигателей по поступающим командам; • поток датчиков — регулярно считывает данные с ультразвукового датчика (а также других установленных), обеспечивая постоянную доступность актуальной информации о расстоянии; • поток управления головой — управляет сервоприводами головы, выполняя команды, полученные из потока команд; • поток команд — выполняет роль «мозга» программы: получает данные из потока датчиков и передает инструкции потокам управления двигателями и головой . Такая архитектура делает работу робота более плавной и естественной, позволяя одновременно управлять движением, считывать показания датчиков и обрабатывать команды . Подобный подход реализован в файле server.py программного обеспечения Adeept, который поставляется вместе с набором PiCar- B . 726 РАЗДЕЛ 7 Создание роботов
Обзор программного обеспечения Adeept Программное обеспечение Adeept, поставляемое вместе с роботом (рисунок 3.1), построено в основном по клиент-серверной модели. В этой системе клиентом выступает панель управления, запущенная на другом компьютере, а сервер работает на Raspberry Pi, установленном на PiCar- B. Панель управления позволяет дистанционно управлять роботом и предоставляет множество полезных возможностей, таких как отслеживание объектов с помощью OpenCV и построение радарной карты с использованием ультразвукового датчика. Кроме того, через нее можно просматривать видеопоток с камеры робота и использовать его для ручного управления. Однако устанавливать программное обеспечение Adeept не рекомендуется, так как оно может мешать выполнению вашего собственного Python-кода. Рисунок 3.1. Программное обеспечение удаленного управления Adeept Если вы все же решите установить программное обеспечение Adeept, имейте в виду, что процесс достаточно трудоемкий, поэтому важно внимательно следовать инструкциям, шаг за шагом и без спешки. Работать с этим ПО интересно, даже несмотря на то, что для начала не требуется писать собственный код, а поскольку проект открытый, вы можете просмотреть исходники и понять, как устроены разные функции Adeept. ГЛАВА 3 Программирование вашего робота-ровера 727
Обязательно загляните в файл server.py в каталоге Server и обратите внимание на то, как организована многопоточность, благодаря которой робот движется плавно и при этом регулярно считывает данные с датчиков. Что дальше? Если вы выполнили проект из этой главы, то теперь у вас есть небольшой робот, способный демонстрировать довольно сложное поведение благодаря встроенному ультразвуковому датчику. Вы можете продолжить развивать этот проект, добавляя к Raspberry Pi новые сенсоры — например, лазерный дальномер, датчики бампера или освещенности. Кроме того, можно подключить дополнительные двигатели, с которыми вы работали в разделе 6. Возможности для экспериментов практически безграничны — каждая новая деталь и строка кода делает вашего робота умнее и интереснее.
В этой главе вы узнаете » Как используется искусственный интеллект (AI) в робототехнике » Как искусственный интеллект помогает роботам выполнять задачи » Как применять машинное обучение в вашем роботе Гл а в а 4 Использование искусственного интеллекта в робототехнике С огласно определению Dictionary.com, искусственный интеллект (ИИ) — это «теория и разработка компьютерных систем, способных выполнять задачи, которые обычно требуют человеческого интеллекта, такие как визуальное восприятие, распознавание речи, принятие решений и перевод между языками». Означает ли это, что ИИ создан для того, чтобы заменить человека? На самом деле, нет. Современный искусственный интеллект направлен на расширение возможностей машин в выполнении определенных задач, которые обычно выполняют люди. Даже выражение «машинный интеллект» не совсем точное, ведь трудно утверждать, что машины действительно обладают интеллектом — по крайней мере, в том смысле, как мы понимаем человеческий разум. Вместо философских рассуждений эта глава сосредоточена на практическом проекте, в котором используется ИИ и машинное обучение. Мы применим методы нейронных сетей для реализации машинного зрения в нашем роботе-автомобиле. Это позволит роботу использовать камеру Raspberry Pi для анализа и восприятия окружающей среды. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 729
Научить робота «видеть» — несложно, но научить его понимать то, что он видит, — чрезвычайно сложная задача. Если вы хотите подробнее изучить компьютерное зрение с помощью Python, рекомендуется книга «Проекты компьютерного зрения с OpenCV и Python 3» (автор Мэтью Ревер, издательство Packt). СОВЕТ Для лучшего понимания методов ИИ, рассматриваемых в этой главе, полезно также перечитать главы 1 и 2 раздела 4. ЗАПОМНИТЕ Проекты этой главы: все для собак В этой главе мы рассмотрим, как создать нейронную сеть машинного обучения и обучить ее различать изображения кошек и собак — навык, который пригодится любому роботу. Для обучения сети будет использован фреймворк TensorFlow и выборка из 1000 изображений (по 500 кошек и собак), взятая из базы данных Kaggle Cats and Dogs, содержащей в общей сложности около 25 000 изображений. Подробнее о платформе Kaggle можно узнать в главе 2 раздела 5. TensorFlow — это библиотека Python, предназначенная для создания и обучения нейронных сетей, основанных на работе с матрицами и графами потоков данных. Она во многом похожа на NumPy, библиотеку, обеспечивающую работу с многомерными массивами и математическими операциями над ними. Главное отличие заключается в том, что TensorFlow изначально разработан для задач машинного обучения и искусственного интеллекта, поэтому включает специализированные инструменты и функции для этих областей. Во втором проекте этой главы мы применим компьютерное зрение, чтобы обучить робота искать мяч и двигаться к нему — примерно, как медлительная, но настойчивая собака. Несмотря на простоту, этот пример хорошо демонстрирует возможности компьютерного зрения и машинного обучения на Python. Подробную информацию о работе с TensorFlow в Python и на Raspberry Pi, а также примеры его использования, вы найдете в главе 4. СОВЕТ 730 РАЗДЕЛ 7 Создание роботов
Настройка первого проекта Для установки TensorFlow на Raspberry Pi перейдите на сайт www.tensorflow.org/install/pip, скачайте подходящую версию и выполните установку, следуя официальным инструкциям. СОВЕТ TensorFlow можно запускать на множестве различных платформ, но в этой главе используется Raspberry Pi 4B с 8 ГБ оперативной памяти. Для удобства вы можете загрузить готовый образ системы (TensorFlow 2.10) для Raspberry Pi 4B с Bullseye 64.bit. Для этого перейдите по адресу www.dummies.com/go/pythonaiofd3e, прокрутите страницу до раздела Downloads и скачайте файл 2023.09–23.PAIOSDCardV2.zip. Рекомендуется использовать качественную SD-карту объемом не менее 32 ГБ .После установки система будет готова к работе с TensorFlow. Имя пользователя по умолчанию — pi, пароль — 3.14. Когда установка завершена, запустите файлы testTF.py и testML.py, приведенные в главе 2 раздела 4, чтобы убедиться, что все работает корректно. Если программы выполняются без ошибок (предупреждения можно не учитывать), значит TensorFlow установлен и готов к использованию. СОВЕТ Если вы захотите продолжить эксперименты с машинным обучением, вы можете загрузить полный набор данных «Кошки и собаки» с сайта www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data. Этот набор содержит тысячи изображений, которые подойдут для более серьезных тренировок нейронной сети. Альтернативный источник тех же данных доступен по адресу: https://www.microsoft.com/en-us/download/ details.aspx?id=54765. Сокращенная версия набора данных «Кошки и собаки» доступна по адресу: https://github.com/switchdoclabs/CatsAndDogsTruncatedData. Ее размер составляет около 65 МБ, и она уже включена в состав программного обеспечения, доступного на сайте www.dummies.com/go/ pythonaiofd3e. Чтобы убедиться, что данные организованы правильно и подходят для использования в примерах из этой главы, выполните следующую команду в директории вашей программы. Она загрузит сокращенный набор и автоматически разместит его в нужной структуре каталогов, необходимой для корректной работы примеров. Production: The link in the following line should not be live.git clone https://github.com/switchdoclabs/ CatsAndDogsTruncatedData.git ГЛАВА 4 Использование искусственного интеллекта в робототехнике 731
После выполнения команды в директории CatsAndDogsTruncatedData появятся два подкаталога. Каталог train содержит изображения кошек и собак, которые будут использоваться для обучения нейронной сети. Каталог validation включает изображения, не участвующие в обучении, — они предназначены для проверки работы сети после завершения процесса обучения. Когда структура данных подготовлена и изображения распределены по каталогам, можно переходить к следующему этапу — обучению нейронной сети. Машинное обучение с использованием TensorFlow Цель этого раздела — полностью обучить нейронную сеть машинного обучения распознавать кошек и собак, затем проверить ее работу на тестовых данных и сохранить готовую модель. После этого вы сможете использовать обученную нейронную сеть на своем роботе — и вот тогда начнется самое интересное! Если при запуске следующей программы появится сообщение об ошибке ImportError: No module named 'seaborn', выполните команду: СОВЕТ sudo pip3 install seaborn Для создания этого примера была использована простая двухслойная нейронная сеть, предназначенная для обучения распознаванию кошек и собак. Существуют гораздо более сложные архитектуры нейронных сетей, способные обеспечивать более высокую точность, однако для наших целей такая упрощенная модель вполне подходит. Подробное объяснение принципов работы нейронных сетей и особенностей использования TensorFlow приведено в главе 2 раздела 4. СВЕРТОЧНЫЕ НЕЙРОННЫЕ СЕТИ Сверточные нейронные сети (СНС) обрабатывают изображения, анализируя их по частям . Для этого используется небольшое окно, например 5x5 пикселей, которое последовательно перемещается по изображению с шагом в два пикселя, пока не будет охвачено все изображение . Этот процесс можно сравнить с рассмотрением 732 РАЗДЕЛ 7 Создание роботов
фотографии под микроскопом: в каждый момент видна лишь малая часть, но постепенно анализируется вся картинка . После завершения обработки одного изображения сеть переходит к следующему из набора данных . Полный проход по всем изображениям называется одним периодом обучения . Применение сверточной нейронной сети на Raspberry Pi значительно увеличивает время обучения — один период занимает около 1000 секунд, тогда как у простой двухслойной сети этот процесс занимает примерно 10 секунд, то есть в сто раз быстрее . Во время обучения анализ производительности показывает загрузку процессора на уровне 352%, что соответствует использованию примерно трех с половиной ядер из четырех, имеющихся у Raspberry Pi 4B . Это свидетельствует о высокой вычислительной нагрузке . При этом энергопотребление устройства возрастает до примерно 3,8 Вт, тогда как в обычном режиме работы оно составляет около 1,5 Вт . Использование простой двухслойной нейронной сети для набора данных «Кошки и собаки» не дало удовлетворительных результатов, точность распознавания составила всего около 51%, что практически совпадает со случайным угадыванием (50%). Чтобы улучшить качество классификации и научить сеть распознавать более сложные изображения, потребовалась более развитая архитектура. Поэтому мы перешли к стандартной шестислойной сверточной нейронной сети (СНС). В программе были изменены слои модели, чтобы реализовать шестиуровневую сверточную архитектуру. Примечательно, насколько просто в Keras и TensorFlow можно перестроить структуру сети, добавляя новые уровни и функции без необходимости переписывать весь код. Чтобы освежить память о Keras, просмотрите главу 2 раздела 4, где рассказывалось о нейронной сети в Python. Сложность и структуру новой модели можно оценить, выполнив команду model.summary(), которая выводит подробное описание всех слоев нейронной сети, количества параметров и связей между ними: _____________________________________________________________ ____ Layer (type) Output shape Param # ==== ============================================================= conv2d (Conv2D) (None, 150, 150, 32) 896 _____________________________________________________________ ____ ГЛАВА 4 Использование искусственного интеллекта в робототехнике 733
max_pooling2d (MaxPooling2D) (None, 75, 75, 32) 0 _____________________________________________________________ ____ conv2d_1 (Conv2D) (None, 75, 75, 32) 9248 _____________________________________________________________ ____ max_pooling2d_1 (MaxPooling2 (None, 37, 37, 32) 0 _____________________________________________________________ ____ conv2d_2 (Conv2D) (None, 37, 37, 64) 18496 _____________________________________________________________ ____ max_pooling2d_2 (MaxPooling2 (None, 18, 18, 64) 0 _____________________________________________________________ ____ dropout (Dropout) (None, 18, 18, 64) 0 _____________________________________________________________ ____ flatten (Flatten) (None, 20736) 0 _____________________________________________________________ ____ dense (Dense) (None, 64) 1327168 _____________________________________________________________ ____ dropout_1 (Dropout) (None, 64) 0 _____________________________________________________________ ____ dense_1 (Dense) (None, 2) 130 ============================================================= ==== Total params: 1,355,938 Trainable params: 1,355,938 Non-trainable params: 0 Особое внимание стоит обратить на последние строки вывода. Они показывают, что наша сверточная нейронная сеть содержит более 1,3 миллиона обучаемых параметров. Каждый из этих параметров изменяется в ходе обучения, постепенно подстраиваясь под данные, чтобы сформировать готовую обученную модель машинного обучения. Именно эти параметры определяют, насколько точно сеть сможет распознавать изображения после завершения обучения. 734 РАЗДЕЛ 7 Создание роботов
Код Код, реализующий обучение нейронной сети с помощью TensorFlow, довольно компактный. Это наглядно показывает, как мощные библиотеки Python позволяют упростить работу с нейронными сетями, скрывая за несколькими строками кода сложные математические вычисления и настройку миллионов параметров модели: #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.layers import * # загрузка данных img_width = 150 img_height = 150 train_data_dir = 'data/train' valid_data_dir = 'data/validation' datagen = ImageDataGenerator(rescale = 1./255) train_generator = datagen.flow_from_directory( directory=train_data_dir, target_size=(img_width,img_height), classes=['dogs','cats'], class_mode='binary', batch_size=16) validation_generator = datagen.flow_from_directory( directory=valid_data_dir, target_size=(img_width,img_height), classes=['dogs','cats'], class_mode='binary', batch_size=32) ГЛАВА 4 Использование искусственного интеллекта в робототехнике 735
# создаем модель model = tf.keras.Sequential() model.add(Conv2D(32, (3, 3), input_shape=(150, 150, 3), padding='same',activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(32, (3, 3), padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(64, (3, 3), activation='relu', padding='same')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(2, activation='softmax')) model.compile(optimizer='adam', loss= sparse_categorical_crossentropy', metrics=['accuracy']) print (model.summary()) # обучаем модель print('starting training...') history = model.fit_generator(generator=train_generator, steps_per_epoch=2048 // 16,epochs=20, validation_data=validation_generator,validation_ steps=832//16) print('training finished!!') # сохраняем коэффициенты model.save("CatsVersusDogs.trained") # Получаем историю потерь при обучении и тестировании training_loss = history.history['loss'] accuracy = history.history['acc'] # Создаем счетчик количества эпох epoch_count = range(1, len(training_loss) + 1) # Визуализация истории потерь plt.figure(0) plt.plot(epoch_count, training_loss, 'r--') plt.plot(epoch_count, accuracy, 'b--') plt.legend(['Training Loss', 'Accuracy']) 736 РАЗДЕЛ 7 Создание роботов
plt.xlabel('Epoch') plt.ylabel('History') plt.grid(True) plt.show(block=True); Сохраните этот код в файле CatsVersusDogsTrain.py. Как работает код Код CatsVersusDogsTrain.py занимает 93 строки, и удивительно, насколько многого можно добиться с таким компактным источником. Разберем основные блоки программы по порядку. Сначала подключаются необходимые библиотеки: #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import seaborn as sns import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.layers import * Далее выполняется предобработка данных: изображения преобразуются в тензоры — числовые матрицы, которые используются нейронной сетью для обучения. Файлы с изображениями кошек и собак расположены в отдельных подкаталогах внутри директории data. На этом этапе все изображения изменяются до единого формата — 150×150 пикселей. Это необходимо, чтобы все входные данные имели одинаковый размер, ведь нейронная сеть ожидает фиксированные размеры входного слоя: # загрузка данных img_width = 150 img_height = 150 train_data_dir = 'data/train' valid_data_dir = 'data/validation' ГЛАВА 4 Использование искусственного интеллекта в робототехнике 737
datagen = ImageDataGenerator(rescale = 1./255) train_generator = datagen.flow_from_directory( directory=train_data_dir, target_size=(img_width,img_height), classes=['dogs','cats'], class_mode='binary', batch_size=16) validation_generator = datagen.flow_from_ directory(directory=valid_data_dir, target_size=(img_width,img_height), classes=['dogs','cats'], class_mode='binary', batch_size=32) Теперь создадим шестислойную нейронную сеть, которая станет основой нашей модели: # создаем модель model = tf.keras.Sequential() Первый слой — это двумерная сверточная сеть (Conv2D), которая начинает извлекать из изображения базовые признаки вроде краев и простых текстур; в качестве функции активации используется ReLU (прямолинейная линейная ректификация), она заменяет все отрицательные значения на нуль и тем самым вносит нелинейность в модель, позволяя сети выделять более выразительные шаблоны и ускоряя обучение за счет простой и устойчивой к затухающему градиенту формы; подробнее о ReLU и других функциях активации вы можете прочитать здесь: https://machinelearningmastery.com/rectified-linear-activationfunction-for-deep-learning-neural-networks/. model.add(Conv2D(32, (3, 3), input_shape=(150, 150, 3), padding='same',activation='relu')) Следующий слой нашей сверточной нейронной сети — MaxPooling2D — выполняет объединение (пулинг) результатов, полученных после работы предыдущего сверточного слоя: model.add(MaxPooling2D(pool_size=(2, 2))) Затем в модель добавляются еще два сверточных слоя, каждый из которых сопровождается слоем пулинга. Эти дополнительные уровни позволяют сети извлекать более сложные и абстрактные признаки из изо- 738 РАЗДЕЛ 7 Создание роботов
бражений — например, формы, текстуры и характерные элементы объектов. model.add(Conv2D(32, (3, 3), padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(64, (3, 3), activation='relu', padding='same')) model.add(MaxPooling2D(pool_size=(2, 2))) Одна из проблем нейронных сетей — переобучение, при котором модель слишком точно подстраивается под обучающие данные и перестает распознавать новые, немного отличающиеся изображения. В таких случаях помогают слои прореживания (Dropout), которые случайным образом обнуляют часть данных во время обучения. Это предотвращает чрезмерную зависимость сети от отдельных нейронов и делает ее более устойчивой. В данном случае слой удаляет 25% данных: model.add(Dropout(0.25)) Теперь данные преобразуются в одномерный массив, чтобы их можно было одновременно подать на 64 нейрона следующего уровня сети. После этого используется финальный полносвязный слой, состоящий из 64 нейронов, который объединяет извлеченные признаки и подготавливает данные для выходного слоя: model.add(Flatten()) model.add(Dense(64, activation='relu')) Далее отбрасывается еще 50% входных элементов, чтобы снизить риск переобучения. На первый взгляд может показаться, что удаление половины данных после обучения не имеет смысла, однако такой прием помогает нейронной сети лучше обобщать информацию и корректно классифицировать изображения, которых не было в обучающем наборе: model.add(Dropout(0.5)) Выходной слой «Кошка или собака» (последний model.add в коде) содержит два выхода. Если сверточная нейронная сеть распознает на изображении кошку, первый выход принимает значение 1, а второй — 0. Если сеть определяет, что на изображении собака, первый выход равен 0, а второй — 1: model.add(Dense(2, activation='softmax')) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) ГЛАВА 4 Использование искусственного интеллекта в робототехнике 739
print (model.summary()) Обучение нейронной сети на наборе из 1000 изображений кошек и собак занимает примерно один час на Raspberry Pi 4B. Для сравнения, на модели Raspberry Pi 3B+ тот же процесс занимает более пяти часов: # обучаем модель print('starting training...') history = model.fit_generator(generator=train_generator, steps_per_epoch=2048 // 16,epochs=20, validation_data=validation_generator,validation_ steps=832//16) print('training finished!!') Следующая строка кода сохраняет обученную модель нейронной сети, чтобы можно было использовать ее позже без повторного обучения — это позволяет сэкономить около пяти часов при каждом последующем запуске. Сохранение результатов обучения является стандартной практикой в системах машинного обучения: процесс обучения занимает значительное время и ресурсы, тогда как использование уже готовой модели для классификации новых изображений выполняется быстро и эффективно. # сохраняем коэффициенты model.save("CatsVersusDogs.trained") Мы используем библиотеку Matplotlib для построения графика, который наглядно показывает, как точность модели постепенно повышается с каждой эпохой обучения: # Получаем историю потерь при обучении и тестировании training_loss = history.history['loss'] accuracy = history.history['acc'] # Создаем счетчик количества эпох epoch_count = range(1, len(training_loss) + 1) # Визуализируем историю потерь plt.figure(0) plt.plot(epoch_count, training_loss, 'r--') plt.plot(epoch_count, accuracy, 'b--') plt.legend(['Training Loss', 'Accuracy']) plt.xlabel('Epoch') plt.ylabel('History') plt.grid(True) 740 РАЗДЕЛ 7 Создание роботов
Результаты Теперь, когда обучение нейронной сети завершено, можно проверить результаты ее работы и убедиться, насколько хорошо она научилась различать изображения кошек и собак. Наступает время испытаний! Выполните следующую команду в окне терминала, чтобы запустить проверку и увидеть, как работает ваша полностью обученная модель. python3 CatsVersusDogsTrain.py Обучение занимает примерно час на Raspberry Pi 4B (по сравнению с более чем пятью часами на Raspberry Pi 3B+) для выполнения 20 циклов и сохранения результатов в директории CatsVersusDogs.training. Ниже приведены итоги последнего цикла обучения.: Epoch 20/20 128/128 [==============================] – 894s 7s/step - loss: 0.0996 acc: 0.9609 – val_loss: 1.1069 – val_acc: 0.7356 training finished!! Можно спокойно игнорировать следующие предупреждения, выводимые TensorFlow во время работы программы: СОВЕТ UserWarning: `Model.fit_generator` is deprecated and will be removed in a future version. Please use `Model.fit`, which supports generators. history = model.fit_generator(generator=train_generator, WARNING:absl:Found untraced functions such as _jit_compiled_ convolution_op, _jit_compiled_convolution_op, _jit_compiled_ convolution_op while saving (showing 3 of 3). These functions will not be directly callable after loading. Эти предупреждения будут устранены в следующей версии TensorFlow. В нашем примере после примерно часа обучения нейронная сеть достигла точности 96% — отличный результат для такой компактной модели и ограниченного набора данных. На рисунке 4.1 показано, как постепенно повышалась точность сети по мере прохождения эпох обучения. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 741
Рисунок 4.1. Точность распознавания кошек и собак по циклам обучения Форма кривой точности показывает, что можно было бы немного повысить результат, увеличив количество эпох и продолжив обучение еще несколько часов, однако для нашего примера достигнутого уровня вполне достаточно. Показатель потерь во время обучения отражает количество ошибок, которые нейронная сеть совершает при попытке распознать изображения. При этом важно помнить, что простое увеличение числа эпох не гарантирует улучшения точности. После определенного момента сеть может начать переобучаться, то есть слишком точно подстраиваться под обучающие данные и терять способность распознавать новые изображения. Тестирование обученной сети Теперь пришло время протестировать нейронную сеть на изображениях, которых не было ни в обучающем, ни в валидационном наборе. Мы воспользуемся обученной моделью, чтобы определить, кто изображен на новых фотографиях — кошка или собака. Для проверки выбраны два изображения. Первое, показанное на рисунке 4.2, — это фотография кошки с низким контрастом и невысоким качеством, что делает задачу классификации более сложной. Второе, изображенное на рисунке 4.3, — это снимок собаки высокого качества, с четкими деталями и хорошим освещением. Такое сочетание позволяет оценить, насколько хорошо обученная сеть справляется с изображениями различного уровня сложности. 742 РАЗДЕЛ 7 Создание роботов
Рисунок 4.2. Кот Пантера Рисунок 4.3. Пес Уинстон Код Код, который используется для применения обученной нейронной сети к новым изображениям, оказывается еще проще, чем программа для ее обучения. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 743
Создайте новый файл под названием singleTestImage.py и вставьте в него код: #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image print("import complete") # загрузка модели img_width = 150 img_height = 150 class_names = ["Dog", "Cat"] model = tf.keras.models.load_model("CatsVersusDogs. trained",compile=True) print (model.summary()) # обработка одного изображения кота #imageName = "Cat150x150.jpeg" imageName = "Dress150x150.JPG" testImg = Image.open(imageName) testImg.load() data = np.asarray( testImg, dtype="float" ) data = np.expand_dims(data, axis=0) singlePrediction = model.predict(data, steps=1) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print(NumberElement) print(Element) print(singlePrediction) print ("Our Network has concluded that the file'" +imageName+"' is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") # обработка одного изображения собаки imageName = "Dog150x150.JPG" testImg = Image.open(imageName) testImg.load() data = np.asarray( testImg, dtype="float" ) data = np.expand_dims(data, axis=0) 744 РАЗДЕЛ 7 Создание роботов
singlePrediction = model.predict(data, steps=1) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print(NumberElement) print(Element) print(singlePrediction) print ("Our Network has concluded that the file'" +imageName+"' is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") Как работает код Код singleTestImage.py использует обученную нейронную сеть, созданную во время часового обучения на наборе данных «Кошки и собаки» из этой главы. Сначала импортируем необходимые библиотеки, как в программе CatsVersusDogsTrain.py: #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image print("import complete") # загрузка модели img_width = 150 img_height = 150 Затем создается массив имен классов, в котором хранятся текстовые обозначения категорий, распознаваемых нейронной сетью: class_names = ["Dog", "Cat"] В следующем фрагменте кода выполняется загрузка обученной модели нейронной сети, созданной ранее во время обучения. При этом восстанавливается не только архитектура модели, но и все ее слои, параметры и веса, полученные в процессе тренировки: ГЛАВА 4 Использование искусственного интеллекта в робототехнике 745
model = tf.keras.models.load_model( "CatsVersusDogs.trained",compile=True) print (model.summary()) Теперь вы тестируете одно изображение кошки, чтобы убедиться, что обученная нейронная сеть работает корректно: # обработка одиночного изображения кошки imageName = "Cat150x150.jpeg" testImg = Image.open(imageName) testImg.load() Затем вы преобразуете тестовое изображение в массив NumPy, чтобы подготовить его к анализу нейронной сетью: data = np.asarray( testImg, dtype="float" ) Далее вы расширяете размерности массива, потому что функция predict() ожидает на вход не одно изображение, а целый набор изображений, даже если в нем всего один элемент. Это делается с помощью следующей команды: data = np.expand_dims(data, axis=0) Теперь вы определяете, кто изображен на фотографии — кошка или собака — с помощью обученной модели: singlePrediction = model.predict(data, steps=1) Вы выводите необработанные данные, чтобы увидеть исходные результаты работы нейронной сети и понять, какие значения она присвоила каждому классу: NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print(NumberElement) print(Element) print(singlePrediction) Затем интерпретируете предсказание, преобразуя полученные числовые результаты в понятный человеку вывод: print ("Our Network has concluded that the file'" +imageName+"' is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") 746 РАЗДЕЛ 7 Создание роботов
Далее вы выполняете те же действия, но уже с изображением собаки: # обработка одиночного изображения собаки imageName = "Dog150x150.JPG" testImg = Image.open(imageName) testImg.load() data = np.asarray( testImg, dtype="float" ) data = np.expand_dims(data, axis=0) singlePrediction = model.predict(data, steps=1) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print(NumberElement) print(Element) print(singlePrediction) print ("Our Network has concluded that the file'" +imageName+"' is a "+class_names[NumberElement]) print (str(int(Element*100)) + "% Confidence Level") Результаты Сохраните весь приведенный код в файл с именем singleTestImage.py и запустите его: sudo python3 singleTestImage.py После запуска программы singleTestImage.py в терминале вы увидите примерно такие результаты: import complete WARNING:tensorflow:No training configuration found in save file: the model was *not* compiled. Compile it manually. _____________________________________________________________ Layer (type) Output Shape Param # ============================================================= conv2d (Conv2D) (None, 150, 150, 32) 896 _____________________________________________________________ max_pooling2d (MaxPooling2D) (None, 75, 75, 32) 0 _____________________________________________________________ conv2d_1 (Conv2D) (None, 75, 75, 32) 9248 _____________________________________________________________ ГЛАВА 4 Использование искусственного интеллекта в робототехнике 747
max_pooling2d_1 (MaxPooling2 (None, 37, 37, 32) 0 _____________________________________________________________ conv2d_2 (Conv2D) (None, 37, 37, 64) 18496 _____________________________________________________________ max_pooling2d_2 (MaxPooling2 (None, 18, 18, 64) 0 _____________________________________________________________ dropout (Dropout) (None, 18, 18, 64) 0 _____________________________________________________________ flatten (Flatten) (None, 20736) 0 _____________________________________________________________ dense (Dense) (None, 64) 1327168 _____________________________________________________________ dropout_1 (Dropout) (None, 64) 0 _____________________________________________________________ dense_1 (Dense) (None, 2) 130 ============================================================= Total params: 1,355,938 Trainable params: 1,355,938 Non-trainable params: 0 _____________________________________________________________ None 1 1.0 [[ 0. 1.]] Our Network has concluded that the file 'Cat150x150.jpeg' is a Cat 100% Confidence Level 0 1.0 [[ 1. 0.]] Our Network has concluded that the file 'Dog150x150.JPG' is a Dog 100% Confidence Level Что ж, сеть показала отличные результаты — она корректно распознала и кошку, и собаку как представителей своих видов. Обратите внимание на стопроцентные значения доверительных интервалов, отображаемых программой. 748 РАЗДЕЛ 7 Создание роботов
Доверительный интервал отражает степень уверенности нейронной сети в своем предсказании. В нашем случае показатели уверенности фактически составляют около 99,99%, но при выводе на экран округляются до 100%. Это означает, что сеть почти полностью уверена в своем решении, что подтверждает успешное обучение модели. Теперь, когда у вас есть обученная модель и она успешно прошла проверку на новых изображениях, можно переходить к следующему шагу — установке нейронной сети в вашего робота. Подключив камеру Raspberry Pi, робот сможет самостоятельно распознавать кошек и собак в реальном времени, анализируя изображения с камеры и классифицируя их с помощью обученной модели. ОСТОРОЖНО Ограничение созданной нами нейронной сети состоит в том, что она обучена различать только два класса изображений — кошек и собак. Поэтому любая другая картинка, поданная на вход, будет отнесена сетью к одному из этих двух классов, даже если на ней изображен совершенно другой объект. Чтобы сделать сеть более универсальной и способной распознавать больше типов объектов, необходимо добавить в обучающий набор данные дополнительных категорий и провести повторное обучение. Например, можно расширить модель, чтобы она различала три класса — кошку, собаку и платье (рисунок 4.4) Мы протестировали сеть, используя известное изображение платья из раздела 4, и результаты показали, как ограниченность обучающих данных влияет на способность сети правильно интерпретировать новые, неожиданные объекты. Рисунок 4.4. Изображение платья? ГЛАВА 4 Использование искусственного интеллекта в робототехнике 749
Как и следовало ожидать, сеть ошиблась при распознавании, ведь она никогда не обучалась понимать, что такое платье: Our Network has concluded that the file 'Dress150x150.JPG' is a Cat 100% Confidence Level Обратите внимание, что программа выводит уровень уверенности 100%; фактически он, вероятнее всего, составляет около 99,99% а, однако, поскольку сеть обучена различать только кошек и собак, любое изображение будет отнесено лишь к одной из этих категорий, поэтому численное значение уверенности в таком случае малоинформативно, а в задаче общего распознавания объектов остается еще много нюансов, которые требуется учитывать при обучении. СОВЕТ Если при запуске программы появляется ошибка вида undefined symbol: cblas_sgemm, попробуйте выполнить ее командой sudo python3 singleTestImage.py. Распознавание кошек и собак нашим роботом Пора добавить роботу из предыдущей главы новую функцию. Вы установите обученную нейронную сеть для распознавания кошек и собак на PiCar-B и задействуете встроенные светодиоды, чтобы показывать, кого видит бортовая камера Raspberry Pi, при этом программное обеспечение для этой части главы находится в подкаталоге Robot каталога главы 4, раздела 7, что, согласитесь, довольно изящное решение, начнем с программы robotVision.py. Как уже отмечалось, эта нейронная сеть относит любой объект либо к категории «кошка», либо к категории «собака», а запуск классификации изображения с камеры будет привязан к срабатыванию ультразвукового датчика, то есть к моменту, когда перед роботом проходит кошка или собака. Поскольку кот соавтора Джона не пожелал участвовать в эксперименте, мы использовали презентацию PowerPoint с разными изображениями кошек и собак (см. рисунок 4.5), робот смотрит на экран, ультразвуковой датчик срабатывает, и начинается процесс распознавания. 750 РАЗДЕЛ 7 Создание роботов
Рисунок 4.5. Тестовая установка для нейросетевого компьютерного зрения робота Код В программе robotVision.py используется обученная нейронная сеть, которая запускается каждый раз, когда ультразвуковой датчик фиксирует объект перед роботом — предполагается, что это кошка или собака: #!/usr/bin/python3 #использование нейронной сети с роботом #импорт библиотек import os os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"]="python" import numpy as np import tensorflow as tf from tensorflow.python.framework import ops from PIL import Image import RobotInterface import time from picamera2 import Picamera2, Preview print("import complete") RI = RobotInterface.RobotInterface() # загрузка модели нейронной сети ГЛАВА 4 Использование искусственного интеллекта в робототехнике 751
img_width = 150 img_height = 150 class_names = ["Dog", "Cat"] model = tf.keras.models.load_model("CatsVersusDogs.trained", compile=True) RI.centerAllServos() RI.allLEDSOff() # Игнорировать расстояния больше одного метра DISTANCE_TO_IGNORE = 1000.0 # Сколько попыток сделает робот, прежде чем сдаться DETECT_DISTANCE = 60 def bothFrontLEDSOn(color): RI.allLEDSOff() if (color == "RED"): RI.set_Front_LED_On(RI.right_R) RI.set_Front_LED_On(RI.left_R) return if (color == "GREEN"): RI.set_Front_LED_On(RI.right_G) RI.set_Front_LED_On(RI.left_G) return if (color == "BLUE"): RI.set_Front_LED_On(RI.right_B) RI.set_Front_LED_On(RI.left_B) return def checkImageForCat(testImg): # проверка одиночного изображения собаки data = np.asarray(testImg, dtype="float") data = np.expand_dims(data, axis=0) singlePrediction = model.predict(data, steps=1) print("single Prediction=", singlePrediction) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print("Our Network has concluded that the file'" + imageName + "' is a " + class_ names[NumberElement]) 752 РАЗДЕЛ 7 Создание роботов
return class_names[NumberElement] try: print("starting sensing") Quit = False trigger_count = 0 bothFrontLEDSOn("RED") #RI.headTiltPercent(70) camera = Picamera2() camera.configure(camera.create_preview_configuration(main={ "format": 'XRGB8888', "size": (1024, 1024)})) camera.start_preview(Preview.QTGL, x=150, y=150, width=100, height=100) camera.start() # Время на прогрев камеры time.sleep(2) while (Quit == False): current_distance = RI.fetchUltraDistance() print ("current_distance = ", current_distance) if (current_distance < DETECT_DISTANCE): trigger_count = trigger_count + 1 print("classifying image") camera.capture_file('FrontView.jpg') imageName = "FrontView.jpg" testImg = Image.open(imageName) new_image = testImg.resize((150, 150)) new_image.save("FrontView150x150.jpg") if (checkImageForCat(new_image) == "Cat"): bothFrontLEDSOn("GREEN") else: bothFrontLEDSOn("BLUE") time.sleep(2.0) bothFrontLEDSOn("RED") time.sleep(7.0) except KeyboardInterrupt: print("program interrupted") print ("program finished") ГЛАВА 4 Использование искусственного интеллекта в робототехнике 753
Как это работает Большая часть кода программы действительно напоминает то, что уже использовалось ранее для управления роботом — настройка датчиков, работа с моторами и основной цикл управления. Однако ключевая часть, отвечающая за интеллектуальное поведение робота, заключается в функции классификации checkImageForCat, которая выполняет анализ изображения, полученного с камеры. Эта функция работает следующим образом: def checkImageForCat(testImg): # проверка одиночного изображения собаки data = np.asarray( testImg, dtype="float" ) data = np.expand_dims(data, axis=0) singlePrediction = model.predict(data, steps=1) print ("single Prediction =", singlePrediction) NumberElement = singlePrediction.argmax() Element = np.amax(singlePrediction) print ("Our Network has concluded that the file'" +imageName+"' is a "+class_names[NumberElement]) return class_names[NumberElement] Функция checkImageForCat принимает входное тестовое изображение, сделанное камерой Raspberry Pi, после чего изображение уменьшается до размера 150×150 пикселей — формата, необходимого для работы этой нейронной сети (рисунок 4.5). Результаты Сохраните программу в файл robotVision.py. Затем, чтобы запустить поиск кошек, выполните следующую команду в окне терминала: sudo python3 robotVision.py Эту программу лучше запускать из графического интерфейса Raspberry Pi (с подключенным монитором или через VNC), чтобы на экране видеть небольшое превью с камеры — так гораздо интереснее наблюдать, что «видит» робот. Вот результаты нашей тестовой установки (см. рисунок 4.5): 754 РАЗДЕЛ 7 Создание роботов
current_distance = 20.05481719970703 classifying image single Prediction = [[ 0. 1.]] Our Network has concluded that the file 'FrontView.jpg' is a Cat 100.00% Confidence Level current_distance = 20.038604736328125 classifying image single Prediction = [[ 1. 0.]] Our Network has concluded that the file 'FrontView.jpg' is a Dog 100.00% Confidence Level current_distance = 19.977807998657227 В целом результаты оказались довольно хорошими. Мы заметили некоторые различия в распознавании, связанные с условиями освещения, что не стало неожиданностью — освещение почти всегда вызывает проблемы при работе с системами компьютерного зрения. Тем не менее сеть стабильно определяла большинство изображений правильно, за исключением одного случая: она приняла фотографию кошки за изображение собаки, как показано на рисунке 4.6. Рисунок 4.6. Кошка, которая, по-видимому, собака ГЛАВА 4 Использование искусственного интеллекта в робототехнике 755
Мы предполагаем, что нейронную сеть сбили с толку сложенные уши кошки, однако, учитывая, что модель содержит около 1,3 миллиона параметров, невозможно точно определить, на каком этапе возникает ошибка. Это по сути огромная статистическая система, и ее внутренние процессы трудно проследить напрямую. Со временем вы начинаете интуитивно понимать, какие приемы работают, а какие нет в машинном обучении, но точное объяснение того, почему сеть ошибается в конкретных случаях, остается недостижимым. У нас был хитрый план для варианта этой нейронной сети: мы использовали ее для создания нового проекта — MouseAir на базе Raspberry Pi. Этот проект запускает игрушечных мышей, когда камера обнаруживает кошку, но не собаку. Камера Raspberry Pi, использовавшаяся в этом проекте, показана в правом верхнем углу рисунка 4.7. Рисунок 4.7. MouseAir — игрушка для кошек с ИИ, запускающая мышей Этот проект доставил массу удовольствия и включал использование сервоприводов, камеры Raspberry Pi и вращающихся двигателей постоянного тока, о которых говорилось в разделе 6. Здесь не хватает места, чтобы подробно описать все технические детали проекта, но более сорока энтузиастов собрали этот набор и добились отличных результатов. Наш подопытный кот по кличке Пантер, однако, оказался весьма невозмутимым, он предпочитал просто сидеть перед пусковым устройством и не проявлял никакого интереса к запущенным мышам, если только одна из них случайно не попадала в него. 756 РАЗДЕЛ 7 Создание роботов
Настройка второго проекта Второй проект этой главы использует OpenCV и библиотеку RobotInterface на Python для создания новой программы управления роботом, которая ищет мяч определенного цвета и движется к нему, пока не приблизится примерно на 20 сантиметров — примерно, как не слишком сообразительная, но настойчивая собака. СОВЕТ Если во время эксперимента робот внезапно зависнет или начнет издавать странные звуки, просто выполните команду: sudo python3 calibrateServos.py. Она вернет сервоприводы и систему управления в нормальное состояние. В этом проекте используется OpenCV для обработки изображений, однако существует важное ограничение: OpenCV, 64-битная операционная система Bullseye и TensorFlow несовместимы между собой. Поэтому для корректной работы проекта рекомендуется использовать версию Buster операционной системы Raspberry Pi. Для вашего удобства предоставлен готовый образ 32-гигабайтной SDкарты с 32-битной версией системы Raspberry Pi Buster. Эта версия хорошо работает со всем необходимым программным обеспечением. Используйте качественную SD-карту на 32 ГБ, чтобы избежать ошибок и сбоев. Имя пользователя по умолчанию — pi, пароль — raspberry. Загрузить файл 2023.09–23.BusterPAIOSDcardV1.zip можно на сайте www.dummies.com/go/pythonaiofd3e, прокрутив страницу до раздела Downloads. Если у вас уже установлена 32-битная версия операционной системы Raspberry Pi Buster (что маловероятно), вы можете установить OpenCV вручную, введя в терминале следующие команды: sudo pip install imutils sudo pip install opencv-python sudo pip install zmq sudo apt-get install libhdf5.dev libhdf5.serial-dev libhdf5.100 sudo apt-get install libqtgui4 libqtwebkit4 libqt4.test python3.pyqt5 sudo apt-get install libatlas-base-dev sudo apt-get install libjasper-dev wget https://bootstrap.pypa.io/get-pip.py sudo python3 get-pip.py sudo pip install opencv sudo pip install opencv-contrib-python==4.1.0.25 ГЛАВА 4 Использование искусственного интеллекта в робототехнике 757
ОСТОРОЖНО Использование 64-битной версии Bullseye операционной системы Raspberry Pi в этом проекте не подойдет — она несовместима с необходимыми библиотеками! Для успешной работы вы должны использовать 32-битную версию операционной системы Raspberry Pi Buster. Теперь все готово, и вы можете приступать к выполнению проекта! Программа FindAndChaseTheBall.py на Python Во многих отношениях программа FindAndChaseTheBall является самой сложной в этой книге. В ней используется продвинутый метод программирования — многопоточность, позволяющий выполнять несколько действий одновременно. Потоки дают возможность параллельно работать различным частям программы, что делает ее более гибкой и эффективной. В любой современной операционной системе, включая Raspberry Pi OS, постоянно работает множество потоков (или процессов), выполняющих различные задачи — от управления экраном и диском до распределения памяти. Когда нужно, чтобы определенный участок кода (например, функция на Python) выполнялся параллельно с другими, создается отдельный поток. Такой подход позволяет сделать структуру программы значительно проще, чем при попытке объединить весь код в одну длинную последовательность команд. В нашем роботе мы применяем потоки для одновременной обработки видеокадров, управления движением, вывода информации на экран Raspberry Pi и считывания расстояний с датчиков. Чтобы лучше понять основы многопоточности, рекомендуем ознакомиться с кратким и понятным введением по ссылке: www.tutorialspoint.com/python3/ python_multithreading.htm. ПРИМЕЧАНИЕ 758 Взаимодействие между потоками и основной программой — одна из самых сложных частей при работе с многопоточностью. Когда несколько потоков выполняются одновременно, они могут вступать в конкуренцию за одни и те же ресурсы, что приводит к так называемым состояниям гонки и непредсказуемому поведению программы. РАЗДЕЛ 7 Создание роботов
Поскольку каждый поток работает с собственной скоростью и выполняет задачи независимо, возникают ситуации, когда один поток пытается обратиться к данным, которые в этот момент изменяются другим. Такие конфликты можно предотвратить с помощью специальных механизмов синхронизации, называемых семафорами и очередями. Подробное изучение этих структур выходит за рамки данной книги, но важно помнить, что такие инструменты существуют и позволяют надежно координировать взаимодействие между потоками, обеспечивая стабильную и предсказуемую работу программы. Структура программы Файл FindAndChaseTheBall.py содержит около 320 строк, поэтому полный листинг здесь не приводится, вместо этого перейдите на сайт www. dummies.com/go/pythonaiofd3e, прокрутите страницу до раздела Downloads и скачайте код для раздела 7 (Book 7), программа FindAndChaseTheBall.py выполняет такие шаги: 1. Запуск потоков . 2. Отображение видеорезультатов . 3. Считывание показаний ультразвукового датчика расстояния . 4. Поиск синего мяча . 5. Определение способа движения к мячу . 6. Приближение к мячу на расстояние 20 см (±2 см) . 7. Вывод информации о ходе работы на экран . 8. Удержание позиции рядом с мячом . Основная программа по сути отвечает за шаг 1 и делает совсем немного, основная логика распределена по трем потокам: потоку ультразвукового датчика, потоку отображения видео и потоку анализа кадров OpenCV, далее каждый из этих потоков рассматривается отдельно. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 759
Поток ультразвукового датчика Поток ультразвукового датчика dis_scan_thread() — самый простой поток в программе: def dis_scan_thread(): #Получение данных о расстоянии с ультразвукового сканера global dis_data, dis_scan while 1: while dis_scan: dis_data = RI.fetchUltraDistance() #print("dis_data=", dis_data) time.sleep(0.2) time.sleep(0.2) Обратите внимание, что этот поток никогда не завершается. Это не является обязательным требованием — некоторые потоки выполняют определенную задачу и завершаются, но для рабочих потоков, подобных этому, вполне нормально работать бесконечно. Поток использует библиотеку RobotInterface (RI) для получения данных с ультразвукового датчика, установленного на передней части робота. В начале программы объявлены две глобальные переменные: dis_data, которая хранит текущее значение расстояния, и dis_scan, управляющая включением и выключением процесса сканирования, что позволяет другим частям программы контролировать работу датчика. Команда time.sleep(0.2) приостанавливает выполнение потока на 200 миллисекунд, давая операционной системе возможность выполнять другие задачи. Без этой паузы поток непрерывно занимал бы одно ядро процессора (а у Raspberry Pi 4B их всего четыре) и тратил бы вычислительные ресурсы впустую, считывая данные чаще, чем это нужно. Робот не успевает существенно переместиться за 200 мс, поэтому получать измерения чаще просто нет необходимости. Поток отображения видео Поток отображения видео video_thread() — это еще один сравнительно простой поток. Он отвечает за вывод видеопотока и взаимодействует с потоком OpenCV через специальный канал связи — сокет. Сокет представляет собой механизм обмена данными между различными потоками или даже между программами, работающими на разных устройствах. По сути, это универсальный способ передачи информации, 760 РАЗДЕЛ 7 Создание роботов
который используется и в сетевом взаимодействии — например, когда вы открываете сайт в браузере, связь с сервером осуществляется именно через сокеты. В данном случае сокет (порт 5555) создается в первых четырех строках кода и служит для связи между потоком OpenCV, который анализирует изображения, и потоком отображения видео, который показывает результаты обработки на экране Raspberry Pi: def video_thread(): context = zmq.Context() my_footage_socket = context.socket(zmq.SUB) my_footage_socket.bind('tcp://127.0.0.1:5555') my_footage_socket.setsockopt_string(zmq.SUBSCRIBE, np.unicode('')) while True: newframe = my_footage_socket.recv_string() img = base64.b64decode(newframe) npimg = np.frombuffer(img, dtype=np.uint8) source = cv2.imdecode(npimg, 1) cv2.imshow("Stream", source) cv2.waitKey(1) В этом потоке используется библиотека zmq, которая отвечает за упаковку данных, поступающих из потока OpenCV, в пакет, содержащий изображение в формате JPEG. После этого поток отображает полученный видеокадр на экране и ждет следующий кадр. Обратите внимание на оператор while True:, который указывает, что поток работает непрерывно и никогда не завершится самостоятельно. Цикл выполняется бесконечно, обеспечивая постоянное получение и отображение новых кадров с камеры, что позволяет видеть живое видео с робота в реальном времени. Поток анализа кадров OpenCV Поток анализа кадров OpenCV opencv_thread() выполняет основную часть работы программы. Он анализирует каждый кадр, поступающий с камеры Raspberry Pi. Скорость обработки составляет около семи кадров в секунду, хотя на более производительном устройстве, например Raspberry Pi 4B, этот показатель может быть выше. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 761
Для каждого полученного видеокадра поток выполняет следующие действия: 1. Ищет синий мяч . 2. Определяет, в каком направлении нужно двигаться к мячу . 3. Двигается вперед, пока не приблизится к мячу на расстояние около 20 см (±2 см) . 4. Отображает информацию о текущих действиях на экране . 5. Остается рядом с мячом, удерживая позицию . Вот так — именно в этом потоке сосредоточена основная логика программы. Так как функция довольно объемная, дальше будут рассмотрены только самые интересные и важные ее фрагменты. Анализ кадров с помощью OpenCV Поток обработки OpenCV анализирует каждый новый кадр, поступающий с камеры, выполняя обработку изображений в реальном времени. Его основная задача — находить на видеопотоке синий мяч, определяя его положение в кадре: for frame in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True): image = frame.array cv2.line(image,(300,240),(340,240),(128,255,128),1) cv2.line(image,(320,220),(320,260),(128,255,128),1) if True: hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, colorLower, colorUpper) mask = cv2.erode(mask, None, iterations=2) mask = cv2.dilate(mask, None, iterations=2) cv2.imshow("OpenCV Mask", mask) cnts = cv2.findContours(mask.copy(), cv2.RETR_ EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2] Цикл for последовательно считывает кадры с камеры Raspberry Pi. Пока камера активна, этот цикл не завершается, обеспечивая непрерывную обработку видеопотока. 762 РАЗДЕЛ 7 Создание роботов
С помощью операторов cv2.line на каждом кадре рисуется визирная сетка. Этот процесс называется наложением (overlay) и используется несколько раз в потоке для вывода вспомогательной информации на экран. В частности, он применяется для отображения текста, отладочных данных и ограничивающей рамки вокруг обнаруженного объекта — в нашем случае, синего мяча (рисунок 4.8). Рисунок 4.8. Кадр, обработанный OpenCV Далее вы создаете маску для поиска мяча (рисунок 4.9). Команды OpenCV cv2.cvtColor(), cv2.inRange(), cv2.erode() и cv2.dilate() ищут цвета в диапазоне оттенков мяча (в нашем случае синего), удаляют очевидные шумы для улучшения качества маски, а затем отображают ее на экране для пользователя. Маска показывает, какие области изображения OpenCV определил как соответствующие нужному цветовому диапазону. Затем маска используется функцией findContours() для поиска объектов, похожих на мяч. Программа анализирует найденные контуры, выбирает самый большой синий объект, определяет его положение в кадре и направляет робота к нему. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 763
Рисунок 4.9. Кадр маски OpenCV Перемещение робота Теперь, когда мяч найден на экране (а если нет — продолжается поиск), программа определяет его координаты X и Y. При этом верхний левый угол изображения имеет координаты 0,0, а нижний правый — 640×480. После вычисления положения мяча робот поворачивает голову в его сторону. Если ультразвуковой датчик показывает расстояние больше 20 см, робот немного продвигается вперед в направлении, где находится мяч, после чего программа возвращается к обработке следующего кадра. На первый взгляд это звучит просто, и логика действительно понятна, однако сам код получается довольно громоздким, потому что одновременно выполняются несколько действий — повороты головы в разные стороны, движение колес и обработка изображений. if radius > 10: cv2.rectangle(image,(int(x-radius),int(y+radius)),(int(x+ radius), int(y-radius)),(255,255,255),1) if X < 310: 764 РАЗДЕЛ 7 Создание роботов
mu1 = int((320-X)/3) # масштабируем mu1 до 50 (50 - это середина) mu1 = int((mu1/320.0)*50.0) hoz_mid_orig-=mu1 if hoz_mid_orig < look_left_max: pass else: hoz_mid_orig = look_left_max #print("mu1 = ", mu1) #print("hoz_mid_orig=", hoz_mid_orig) RI.headTurnPercent(hoz_mid_orig) Предыдущий фрагмент кода проверяет, превышает ли радиус найденного мяча 10 пикселей на экране. Если это так, робот поворачивает голову в сторону мяча. Координата X положения мяча определяется с помощью функций OpenCV cv2.minEnclosingCircle() и cv2. moments(). Центр найденного объекта на маске называется моментом изображения. Термин «момент» пришел из механики и, по сути, обозначает центр тяжести маскированного изображения. Если бы изображение было идеальным кругом, момент находился бы точно в центре этого круга. Поиск мяча Последний фрагмент кода, который мы рассмотрим, описывает действия робота в случае, если он не обнаружил ничего, похожего на целевой синий мяч. В этом случае выполняется небольшой поиск, чтобы проверить, не находится ли мяч где-то вне поля зрения камеры. Простой алгоритм, который используется в программе, вполне подходит для нашего примера (хотя при желании процесс поиска можно было бы значительно усовершенствовать): # это часть кода, отвечающая за поиск цели #print("scaning for target") RI.stopMotor() #motor.motorStop() # ничего не найдено, начинаем сканирование RI.headTurnPercent(random.randint(0,100)) RI.headTiltPercent(50) RI.wheelsPercent(100) # периодически отъезжаем назад для осмотра if (random.randint(0,3) == 1): RI.motorBackward(motor_speed,scan_motor_duration) ГЛАВА 4 Использование искусственного интеллекта в робототехнике 765
Если у вас нет синего мяча в качестве цели, голова робота начинает случайным образом сканировать пространство, поворачиваясь в разные стороны. Каждые три таких сканирования робот немного отъезжает назад и выбирает новое направление обзора. Наблюдать за его действиями довольно интересно, вы можете увидеть, как робот словно «ищет» цель, двигая головой и корпусом. Попробуйте поэкспериментировать с кодом и придумать более эффективные стратегии поиска, чтобы сделать вашего робота еще умнее! Основная программа Основная часть программы FindAndChaseTheBall.py выполняет начальную настройку системы. Она инициализирует камеру, запускает необходимые потоки и задает конфигурационные переменные, которые будут описаны далее. Кроме того, на этом этапе робот приводится в исходное состояние: все сервоприводы и колеса обнуляются, чтобы обеспечить правильное начало работы программы. Конфигурация программы Следующий раздел основной программы задает константы, определяющие поведение робота: ################################## # конфигурационные переменные ################################## distance_stay = 20 # дистанция до цели (см) distance_range = 2 # допустимая погрешность в distance_stay (см) dis_data = 0 # текущее показание ультразвукового дальномера (см) # средние значения поворота (hoz) и наклона (vtr) головы - в процентах vtr_mid = 50 hoz_mid = 50 # начальные позиции (поворот и наклон головы) hoz_mid_orig = 50 vtr_mid_orig = 50 # задает максимальные и минимальные значения (в процентах) 766 РАЗДЕЛ 7 Создание роботов
для наклона и поворота головы look_up_max = 100 look_down_max = 0 look_right_max = 0 look_left_max = 100 # переменные скорости двигателя - возможно, потребуется их подстройка # в зависимости от типа поверхности, по которой движется робот motor_speed = 100 motor_duration = 0.40 # стандартная длительность приближения к цели turn_motor_duration = 0.50 # длительность поворота (должна быть немного больше) scan_motor_duration = 0.50 # длительность отъезда назад во время # сканирования цели Сначала задаются параметры расстояния. Переменная distance_stay установлена на 20 см, а допустимое отклонение ±2 см задается переменной distance_range. Погрешность в 2 см важна, потому что приводы робота не отличаются высокой точностью и могут слегка переезжать или не достигать ровно 20 см. Параметры hoz_mid_orig и vt_mid_orig определяют средние положения сервоприводов (50%), причем все значения указываются в процентах, так как используются процентные функции управления из библиотеки RobotInterface. Значения motor_speed и motor_duration подбираются в зависимости от типа поверхности и уровня заряда аккумулятора: указанные в примере параметры рассчитаны на обычные, невысоковорсистые ковры; если робот движется по твердому полу без коврового покрытия, имеет смысл уменьшить длительность работы двигателя. Настройка цвета мяча Чтобы указать OpenCV цвет мяча, используется цветовая модель HSV (тон, насыщенность, значение). Это удобный способ представления цветов, отличающийся от привычной модели RGB. ГЛАВА 4 Использование искусственного интеллекта в робототехнике 767
В программе задаются две переменные — colorLower и colorUpper. Они определяют диапазон оттенков вокруг выбранного цвета мяча (в нашем примере синего). Например, если мяч имеет определенный оттенок синего, то эти значения задают границы диапазона — чуть светлее и чуть темнее основного цвета: colorLower = (65, 66, 97) искать OpenCV colorUpper = (131, 255, 255) #HSV-цвет, который будет #границы поиска для OpenCV Правильная настройка значений HSV требует экспериментов и зависит как от конкретного цвета вашего мяча, так и от условий освещения. Освещение — настоящая головная боль для любого робототехнического проекта, использующего компьютерное зрение: поддерживать стабильное и достаточно яркое освещение непросто. Люди быстро адаптируются к изменениям света, а вот роботы — нет. Чтобы точно определить параметры цвета вашего мяча, поместите мяч перед роботом и запустите следующую программу, которая находится в каталоге Robot в составе загруженного программного обеспечения: sudo python3 testColor.py На экране, показанном на рисунке 4.10, вы увидите три области: слева отображается маска, в центре — видеокадр с камеры Raspberry Pi, а справа — наложение маски на изображение с камеры. Используйте две верхние полосы прокрутки для настройки нижней границы диапазона цвета. Регулируйте их до тех пор, пока на экране не появится ваш мяч. Затем установите верхнюю границу на значения 131, 255, 255. Экспериментируйте с параметрами, добиваясь того, чтобы маска была четкой и имела ровную круглую форму. Не удивляйтесь, если вам придется повторить настройку после того, как вы запустите робота в реальных условиях — освещение может значительно повлиять на распознавание цвета. После этого можно переходить к самому интересному этапу — началу «преследования» мяча. 768 РАЗДЕЛ 7 Создание роботов
Рисунок 4.10. Экран настройки цвета синего мяча Погоня за мячом Поместите мяч примерно в одном метре перед роботом, как показано на рисунке 4.11. Затем запустите программу погони за мячом, введя в окне терминала следующую команду: sudo python3 FindAndChaseTheBall.py Робот начнет осматриваться в поисках мяча. Спустя несколько секунд на экране вашего Raspberry Pi появятся два окна: в одном будет показан видеопоток с камеры, а в другом — маска OpenCV, с помощью которой выполняется поиск мяча. Вы будете удивлены, насколько впечатляюще выглядит работа программы — до тех пор, пока робот не начнет вести себя немного непредсказуемо. Мы продолжаем улучшать этот проект и будем публиковать обновления в архиве Python All-in-One по адресу: https://github.com/switchdoclabs/Python-All-In-One-3rd-Edition/tree/ main ГЛАВА 4 Использование искусственного интеллекта в робототехнике 769
Рисунок 4.11. Raspberry Pi пытается догнать мяч Примечания к программе Вот несколько замечаний, которыми мы хотим поделиться: 770 » Синий мяч не всегда является лучшим выбором для всех условий . Наш робот уверенно распознавал синий мяч, но также направлялся к окну, пропускающему синеватый свет, к двери лаборатории SwitchDoc Labs, где фитолампа Raspberry Pi SmartGardenSystem создает отраженное синее свечение, и даже к потолочному освещению . Не самый умный робот-пес в мире . В зависимости от вашего окружения лучше может подойти ярко-зеленый или желтый мяч . В нашей тестовой среде, вероятно, оптимальным был бы желтый цвет . » Алгоритм сканирования для поиска мяча довольно примитивен . Более продвинутый алгоритм мог бы распознавать ситуации, когда робот сталкивается с препятствием и не может двигаться дальше . РАЗДЕЛ 7 Создание роботов
» Было бы полезно добавить механизм, позволяющий определить случаи, когда робот фиксируется на объектах, не являющихся мячом (например, на окне) . Эта проблема показана на рисунке 4 .12 . Для улучшения распознавания можно выполнить дополнительный геометрический анализ и фильтрацию по размеру и форме, чтобы отбрасывать объекты, не похожие на мяч . Рисунок 4.12. Пропуск мячей из-за других целей ДРУГИЕ ЗАДАЧИ ДЛЯ РОБОТА В рамках более продвинутого программного управления на Python для PiCar-B можно реализовать множество интересных задач с использованием нейронных сетей и других методов искусственного интеллекта . Вот несколько примеров: • Фокус только на кошках. Переобучите сеть распознавания кошек и собак так, чтобы она определяла только кошек, а все остальное относила к категории «не кошка» . Для этого можно использовать уже обученную нейронную сеть из этой главы — она классифицирует все, что не похоже на кошку, как собаку, поэтому адаптация под новую задачу будет несложной . • Проверка появления Санты. Периодически делайте снимки с видеопотока Raspberry Pi и с помощью нейронной сети, обученной ГЛАВА 4 Использование искусственного интеллекта в робототехнике 771
на изображениях Санты, проверяйте, не появился ли он на Рождество . Звучит невероятно, но такой проект уже реализован и подробно описан в серии статей на сайте https://www.pyimagesearch. com/2017/12/18/keras-deep-learning-raspberry-pi/. • Управление роботом через Alexa. Можно использовать возможности голосового помощника Amazon Alexa для управления вашим роботом . Если вы хотите узнать, как подключить Raspberry Pi к Alexa и создать подобный проект, обратитесь к электронной книге Джона Шовика «Voice Time: Connecting Your Raspberry Pi to the Amazon Alexa» . Заставить робота обнаруживать мяч и двигаться к нему оказалось увлекательным занятием. То, что робот выполняет эту задачу не идеально, хорошо показывает, насколько сложно работать с реальной средой и использовать компьютерное зрение в практических условиях. Поведение робота можно было бы значительно улучшить, добавив дополнительные датчики, несколько камер или даже лазерный дальномер (LIDAR), хотя это сделало бы программу гораздо сложнее. Теперь наш робот умеет преследовать мяч — и это прекрасный способ завершить нашу историю о роботе-собаке. Гав! ИИ и будущее робототехники Мы убеждены, что искусственный интеллект — это будущее робототехники. По мере того как оборудование становится доступнее, а программные инструменты — проще в использовании, роботы будут находить все больше применений не только в промышленности, но и в повседневной жизни, а также в транспортных системах. Мы ожидаем, что в ближайшие годы технологии ИИ появятся во множестве потребительских устройств. И остается только один вопрос, который нас действительно интересует: «Когда же искусственный интеллект появится в моем тостере?» Как только это случится, мы непременно купим такой.
Предметный указатель Symbols B + (оператор сложения) 74 <= (оператор меньше или равно) 75 <a> . . . </a>, тег 399 <article>, тег 399 <body>, тег 399 <img>, тег 400 <span>, тег 400 < (оператор меньше) 75 == (оператор равенства) 75 = (знак равно) 86 != (оператор неравенства) 75 >= (оператор больше или равно) 75 > (оператор больше) 75 3Com 437 backwardPropagate(), функция 461 backwards(), функция 661 BigQuery от Google 553 bin(), функция 108 Boston Dynamic 668 break, оператор 155, 162 Broadcom Videocore-IV 510 b (двоичный режим) 325 A abs(), функция 92 ADAM (адаптивная оценка момента) 478 Adeept Raspberry Pi PiCar-B 677 Alexa (Amazon) 438 allLEDSOff(), функция 706 alphabetize(), функция 236 Amazon Alexa 438 and, оператор 76 append(), метод 170, 184 Apple 370, 442 archive(), метод 254 Arduino 578 ASCII-кодировка (Американский стандартный код для обмена информацией) 115 astimezone(), функция 137 a (добавление) 324 C camelCase (верблюжий регистр) 78 centerAllServos(), функция 711 ChatGPT 33, 107, 398, 434, 444, 531 chr(), функция 116, 159 Claude 33 clear(), метод 184, 209 close(), метод 325 colorWipe(), функция 705 color(), функция 706 complex(), функция 110 continue, оператор 157, 161 copy(), метод 184, 209 count(), метод 184 CSI (интерфейс камеры) 576 CSS 26, 398 csv, расширение 302 CSV-файлы общие сведения 345 открытие 347 преобразование в дату 352 в логический тип 355 в целые числа 352 в числа с плавающей точкой 355 строк 350 преобразование данных из CSV в объекты и словари импорт CSV в объекты Python 358 Предметный указатель 773
импорт CSV в словари Python 364 общие сведения 358 сохранение собранных данных в CSV-файл 407 CUDA 520 currency(), функция 249 current_time(), метод 278 cохранение обработанного контента 403 D DataFrame 538 datetime, модуль 119 dateutil, модуль 135 date(), метод 128 Dell 514 describe(), функция 544 dict(), функция 364 dir(), функция 412 dis_scan_thread() 760 DSI (интерфейс дисплея) 576 DST (летнее время) 136 dumps(), метод 386 E EDT (Североамериканское восточное летнее время) 137 elif 146 else 144 else if 144 error, пакет 395 extend(), метод 184 format(), функция 94 forward(), функция 661 FPGA (программируемые логические интегральные схемы) 517 fromkeys(), метод 209, 217 f-строки (форматные строки) 98 G Gartner 527 get_status(), метод 293 GET-запрос 393 get(), метод 202, 209 GitHub 33, 618 GitHub Copilot 33 GMT (среднее время по Гринвичу) 131 Google Brain 443 Google Chrome 390 Google Cloud использование больших данных из BigQuery от Google 553 компьютерная безопасность в облаке 555 общие сведения 552 платформа Google Cloud 554 регистрация в BigQuery 556 общие сведения 515 Google Home 438 Google Research 443 GPIO (порты ввода-вывода общего назначения) 579 GPS-модуль Adafruit Ultimate 606 GPT-4 (OpenAI) 33 GPU (графический процессор) 500 Griffin, тостер с поддержкой Bluetooth 670 F Fashion-MNIST, база данных 486 feedforward(), функция 460 fetchUltraDistance(), функция 706 FindAndChaseTheBall .py, программа 758 findContours(), функция 763 float(), функция 356 774 H headTiltDown(), функция 709 headTiltMiddle(), функция 709 headTiltPercent(), функция 709 headTiltUp(), функция 709 headTurnLeft(), функция 708 headTurnMiddle(), функция 708 Python все в одном для чайников
headTurnPercent(), функция 708 headTurnRight(), функция 708 help(), функция 295 hex(), функция 108 HTML 26 HTTP (протокол передачи гипертекста) 391 I IAM (управление идентификацией и доступом) 555 if, оператор 140 import, оператор 63 index(), метод 184 init, метод 257 insert(), метод 184 IntelliSense 48 int(), функция 95, 162, 352 is not (оператор отрицания совпадения) 75 is (оператор совпадения) 75 items(), метод 208, 209 J JavaScript 26 Jupyter Notebook выполнение кода 54 общие сведения 35 создание 53 создание папки 52 K Kaggle 539 keys(), метод 209 L len(), функция 113 Linux 30 ljust(), функция 249 load(), метод 380 lower(), метод 244 M Matplotlib визуализация данных с помощью 503 общие сведения 487 max(), функция 94 mdy(), функция 421 Microsoft Azure 517 min(), функция 94 model .evaluate(), функция 494 motorBackward(), функция 700 motorForward(), функция 698 N NCS (нейровычислительный стик Intel) 512 not, оператор 76 now(), метод 125 NumPy 454, 536 O oct(), функция 108 OpenAQ, база данных 569 OpenCV 685 opencv_thread() 761 open(), метод 325 ord(), функция 116 or, оператор 76 P pandas 537 parse, пакет 395 PEP 8 (Руководство по стилю написания кода Python) 57 PEP (предложения по улучшению Python) 57 Pi2Grover Grove, плата сопряжения 636 Pi2Grover, плата 19 PiCar-B, марсоход 676 pip (система управления пакетами Python) 417 Предметный указатель 775
popitem(), метод 209 pop(), метод 184, 209 POSIX 101 POST-запрос 394 print(), функция 102, 111, 115, 141, 198, 219, 240 PyPi 416 R r+ (чтение/запись) 324 rainbowCycle(), функция 704 range(), функция 150 Raspberry Pi включение I2C 611 выключение 689 добавление аппаратного ИИ 512 общие сведения 488, 574 ограничения 509 программируемые RGB-ленты 684 Raspberry Pi 4B 19 Raspberry Pi Foundation 575 Raspbian 578 readline() общие сведения 332 циклическое использование 337 readlines() общие сведения 332 циклическое использование 334 read(), метод 332 ReLU (линейная ректификация) 474 remove(), метод 184 replace(), метод 355 request, пакет 395 response, пакет 395 restore(), методы 254 REST (программная система передачи репрезентативного состояния) 554 reverse(), метод 184 RGB-светодиоды 683 rjust(), метод 249 RobotInterface, функции основной двигатель 707 передние светодиоды 702 776 светодиодная лента 703 сервопривод 708 ультразвуковой датчик расстояния 706 robotparser, пакет 395 ROS (операционная система для роботов) 669 round(), функция 93 r (чтение) 324 S Samsung 442 SciPy 454 SCL (линия тактового сигнала) 610 SDA (линия передачи данных) 610 seek(), метод 341 set_active(), метод 270 setdefault(), метод 209 set_Front_LED_Off(), функция 702 set_Front_LED_On(), функция 702 setPixelColor(), функция 705 SG90, микросервопривод 20 show_date_joined(), метод 268 show_expiry(), метод 292 sleep(), функция 700 SMBus, шина 609 sort(reverse=True), метод 184 sort(), метод 184 SPI (последовательный периферийный интерфейс) 609 SQL (язык структурированных запросов) 561 sqrt(), функция 96 stopMotor(), функция 707 strip(), метод 350 str(), функция 178 sub(), функция 356 super(), метод 287 T tell() для определения позиции указателя 340 TensorBoard 479 Python все в одном для чайников
TensorFlow изменение на трехслойную нейронную сеть 480 общие сведения 443 построение нейронной сети 468 установка 467 theaterChaseRainbow(), функция 705 timedelta, объект 127 time(), метод 133 to_curr(), функция 421 to_date(), функция 421 today(), метод 120 try 304 TWI (двухпроводной интерфейс) 609 type(), функция 127 t (текстовый режим) 324 U Unicode 116 update(), метод 190, 209 URL Library (urllib) 395 USB-порты 575 utcnow(), метод 134 UTC (универсальное координированное время) 121 UTF-8 330 V values(), метод 209 video_thread() 760 VNC (виртуальный сетевой компьютер) 488 W Watson Personality Insights 517 Watson Studio 517 Western Digital 514 wheelsLeft(), функция 699, 710 wheelsMiddle(), функция 699, 710 wheelsPercent(), функция 699, 710 wheelsRight(), функция 710 Wi-Fi 575 Wilkinson Baking, хлебопекарный робот 668 w (запись) 324 X x (создание) 324 Z Zulu-time 131 А абсолютное значение 92 автосохранение 49 адаптивная оценка момента (ADAM) 478 активация виртуального окружения 426 актуаторы как компонент роботов 665 определение 577 алгоритм коррекции ошибок Рида Соломона 454 американская ассоциация искусственного интеллекта 437 & (амперсанд) 392 амперсанд (&) 392 анализ данных 532, 672 аналитика данных 533 аналитика данных в сравнении с наукой о данных 533 аналоговые разъёмы Grove 599 аналоговые сигналы 602 аналоговый ввод 609 аналого-цифровые преобразователи (АЦП) 602, 623 анонимные функции 242 апостроф 71 арифметические операторы 74 атрибуты добавление 258 Предметный указатель 777
значения по умолчанию 264 изменение значения 263 общие сведения 252 определение 255 Б база данных Олсона 132 база данных реального времени Firebase от Google 370 базовый блок Raspberry Pi 593 Бакстер, робот 439 библиотеки общие сведения 410 стандартная библиотека Python встроенные функции 415 общие сведения 410 функция dir() 412 функция help() 414 библиотеки GPIO 580 блоки кода 64 Бодо, Эмиль 600 больше или равно, оператор (>=) 75 больше, оператор (>) 75 большие данные Matplotlib 538 Medicare анализ 565 визуализация 568 коды МКБ 564 настройка проекта и аутентификации 557 общие сведения 555 первый код для работы с большими данными 559 процент оплаты по штатам 567 разбор кода 562 NumPy 536 pandas 537 база данных бриллиантов визуализация 544 общие сведения 539 разбор кода 543 исользование Google Cloud BigQuery от Google 553 778 использование Google Cloud компьютерная безопасность в облаке 555 общие сведения 552 платформа Google Cloud 554 работа с BigQuery 555 многообразие 527 общие сведения 526, 553 объем 526 поиск самого загрязненного города в мире 569 скорость 528 большие данные Medicare 556 Буль, Джордж 72 В ввод команд 41 веб-скрейпинг с помощью Python 398 вектор 468 верблюжий регистр (camelCase) 78 вероятность ложных открытий 553 веса и смещения 451 видеокарты 518 виртуальные окружения активация 426 деактивация 428 общие сведения 424 виртуальный сетевой компьютер (VNC) 488 вложенность 59 вложенные словари 220 внешние файлы CSV общие сведения 345 открытие 347 преобразование в дату 352 преобразование в логический тип 355 преобразование в целые числа 352 преобразование в числа с плавающей точкой 355 преобразование строк 350 UTF-8 331 двоичные 322 Python все в одном для чайников
закрытие 323 общие сведения 321 открытие 323 преобразование из CSV в объекты и словари импорт в объекты Python 358 импорт в словари Python 364 общие сведения 358 регулярные выражения 356 текстовые 322 циклическая обработка seek() для перемещения указателя 341 tell() для определения позиции указателя 340 добавление в сравнении с перезаписью 338 общие сведения 334 с помощью readline() 336 с помощью readlines() 334 чтение содержимого 332 вопросительный знак (?) 392 восьмеричные числа 108 Вохра, Варун 521 время вычисление временных интервалов 126 общие сведения 119 работа со временем 124 системное время 121 часовые пояса 131 время Zulu 131 вставка элемента в список 171 встраиваемые системы 574, 671 встроенная справка 42 встроенные функции 94, 416 выбор базовых блоков Grove 592 вызов метода базового класса 292 выключение Raspberry Pi 689 выравнивание 104, 105 выражение 99 выход HDMI 0 575 выход HDMI 1 575 вычисление временных интервалов 126 чисел с помощью функций 92 вычислительные стики 512 вычитание, оператор (-) 74 Г генеративный ИИ 30, 32 глубокое обучение 443 граничные вычисления 511 графический процессор (GPU) 500 Д данные анализ 532 визуализация 538 загрузка из JSON-файлов загрузка JSON без ключей из строки 380 загрузка JSON с ключами из строки 382 изменение данных JSON 384 общие сведения 373 перебор JSON-файла с ключами 377 преобразование временных меток Firebase в даты Python 379 преобразование даты Excel в дату JSON 375 удаление данных из словаря 384 обработка 530 поддержка 534 сбор 530 сохранение изменений 267 данные в формате JSON (JavaScript Object Notation) выгрузка данных Python в JSON 386 загрузка безключевых данных из строк 380 загрузка данных из JSONфайлов 373 загрузка ключевых данных из строк 382 изменение 384 общие сведения 368 организация данных 369 Предметный указатель 779
перебор JSON с ключами 377 преобразование временных меток Firebase в даты Python 379 преобразование даты Excel в дату JSON 375 преобразовение Excel в JSON 370 сериализация 372 удаление данных из словаря 384 датчики межмикросхемной шины (I2C) включение на Raspberry Pi 611 драйверы 620 оборудование для измерения температуры и влажности 612 общие сведения 602, 609 разбор программ 619 датчик кислорода Grove 622, 624 датчик температуры и влажности Grove HDC 1080 I2C 19 датчик температуры и влажности HDC1080 612 даты на компьютере 121 общие сведения 120 двигатели как компонент роботов 672 координация движений с датчиками 715 общие сведения 633 управение двигателями постоянного тока драйвер двигателя Grove I2C 637 общие сведения 636 программное обеспечение 640 управение сервоприводом общие сведения 644 программное обеспечение 648 разбор кода 649 шаговый двигатель общие сведения 651 программное обеспечение 659 разбор кода 661 электродвигатели малые двигатели постоянного тока 633 общие сведения 633 сервопривод 634 780 шаговый двигатель 635 двигатели и актуаторы 672 двигатели постоянного тока общие сведения 633 управление драйвер двигателя Grove I2C 637 общие сведения 636 программное обеспечение 640 (двоеточие) 197, 325 двоеточие ( ) 197, 325 двоичные файлы 322, 342 двоичные числа 108 двоичный режим (b) 325 "" (двойные кавычки) 71, 166, 334, 346, 396 двойные кавычки ("") 71, 166, 334, 346, 396 двухпроводной интерфейс (TWI) 609 деактивация виртуального окружения 428 действия общие сведения 138 принятие решений elif 145 else if 144 if 140 тернарные операции 148 цикл for break, оператор 155 continue,оператор 157 вложенность 157 диапазоны 150 общие сведения 150 списки 153 строки 152 цикл while break, оператор 162 continue, оператор 161 общие сведения 158 декоратор 276 деление, оператор (/) 74 десериализация 372 дзен Python (PEP 20) 56 диапазоны 150 Дикинсон, Джон 436 Python все в одном для чайников
длина словаря 200 списка 170 строки 113 добавление аппаратного ИИ к Raspberry Pi 512 данных в словари 204 новых параметров в подкласс 287 элементов в списки 170 добавление (a) 324 добавление в конец файла или перезапись 338 доверительный интервал 749 доступ к данным словаря 198 драйвер двигателя Grove I2C 637 З заземление 612 запись (w) 324 заполнить 258 , (запятые) 70 запятые (,) 70 зима ИИ 437 ? (знак вопроса) 392 -(знак минус) 68 _ (знак подчеркивания) 78 значения возвращение из функций 240 передача в списке 235 передача в функции 231 значок аватара 34 измерение кислорода и пламени датчик кислорода Grove 624 интерпретация результатов 629 общие сведения 622 подключение оборудования для эксперимента с кислородом 625 разбор кода 628 И имена использование одного и того же имени дважды 293 создание переменных с допустимыми именами 78 имена полей 346 именованные аргументы (kwargs) 233 инструменты 29 интерактивный режим ввод команд 41 выход из справки 44 интерпретатор 41 использование встроенной справки 42 общие сведения 39 определение версии 40 открытие терминала 39 поиск справки онлайн 45 шпаргалки 46 интернационализация 101 интерпретатор Python 29 интерпретация результатов 629 интерфейс дисплея (DSI) 576 интерфейс камеры (CSI) 576 информированные значения даты и времени 132 исключающее ИЛИ-НЕ 454 искусственные нейроны 435 искусственный интеллект в облаке Amazon Web Services 516 Google Cloud 515 IBM Cloud 517 Microsoft Azure 517 общие сведения 514 в робототехнике 729, 772 в смартфонах 442 добавление аппаратного ИИ к Raspberry Pi 512 зима ИИ 437 методы TensorFlow 443 машинное обучение 441 нейронные сети 435 общие сведения 434 эволюционные вычисления 439 на графической карте 518 написание кода 338 общие сведения 433 Предметный указатель 781
ограничения 444, 509 процессор Broadcom Videocore-IV 510 ресурсы 521 технология будущего 435 искусственный интеллект в смартфонах 442 использование статических методов 277 использование тепловых карт pandas для поиска корреляций 548 исходный код 21 К кабели с гнездовыми разъемами Grove 19 калибровка сервоприводов 690 камера Pi, как компонент роботов 685 [ ] (квадратные скобки) 62, 154, 189 квадратные скобки ([ ]) 62, 154, 189 классы добавление методов вызов метода через имя класса 271 использование методов класса 275 использование переменных класса 273 использование статических методов 277 общие сведения 267 передача параметров в методы 269 наследование вызов метода базового класса 292 добавление новых параметров в подкласс 287 использование одного имени дважды 293 общие сведения 279 определение подкласса 284 переопределение значения по умолчанию в подклассе 286 создание базового (основного) класса 282 общие сведения 251 пустые классы 256 782 изменение значений атрибутов 263 определение атрибутов со значениями по умолчанию 264 присваивание атрибутов 258 создание экземпляров 260 создание 255 сохранение изменений данных 267 кобот (коллаборативный робот) 666 код модели СНС 500 код нейронной сети на Python 454 коды состояния HTTP 394 комментарии 66 комментирование функции 226 компиляция модели 470 комплексные числа 70, 110 композитный видеои аудиоразъём 575 компьютер, как компонент роботов 671 компьютерная безопасность в облаке 555 конкатенация строк 112 конструктор 257 контекстное программирование 325 координация движений моторов с датчиками 715 копирование двоичного файла 342 словаря 209 списка 183 кортежи 188, 192, 264 / (косая черта) 414 косая черта (/) 414 ( ) (круглые скобки) 189 круглые скобки ( ( ) ) 189 курсив 62 Л Леблан-Уильямс, М . 521 летнее время (DST) 136 линейная ректификация (ReLU) 474 литеральная часть 99 литий-полимерные аккумуляторы 18650 677 логические значения Python все в одном для чайников
как тип данных 72 преобразование в логический тип 355 логические операторы 76, 139 логическое равенство 453 локальный максимум 484 М магические методы 413 малые двигатели постоянного тока 633 марсоход PiCar-B 676 Массарон, Лука 450 математические функции 95 машинное обучение TensorFlow 443 визуализация с помощью Matplotlib 503 общие сведения 441, 483 поиск решений 484 создание нейронной сети для распознавания типов одежды код модели СНС 500 настройка программной среды 487 обучение сети 490 общие сведения 486 получение данных из набора Fashion-MNIST 490 проверка результатов обучения и оценки 494 разбор кода 492 результаты 498, 503 тестирование на внешних изображениях 497 тестирование отдельного изображения 495 тестирование сети 490 менеджер пакетов 467 меньше или равно, оператор (<=) 75 меньше, оператор (<) 75 метаданные 539 Меткалф, Роберт 437 методы append() 184 archive() 254 clear() 184, 209 close() 325 copy() 184, 209 count() 184 current_time() 278 date() 128 dumps() 386 extend() 184 fromkeys() 209, 217 get() 202, 209 get_status() 293 index() 184 insert() 184 items() 208, 209 keys() 209 load() 380 lower() 244 now() 125 open() 325 pop() 184, 209 popitem() 209 read() 332 remove() 184 replace() 355 restore() 254 reverse() 184 rjust() 249 seek() 341 set_active() 270 setdefault() 209 show_date_joined() 268 show_expiry() 292 sort() 184 sort(reverse=True) 184 strip() 350 super() 287 time() 133 today() 120 update() 190, 209 utcnow() 134 values() 209 добавление в класс вызов метода через имя класса 271 использование методов класса 275 использование переменных класса 273 Предметный указатель 783
использование статических методов 277 общие сведения 267 передача параметров 269 магические 413 общие сведения 255 работа со словарями 209 работа со списками 184 методы искусственного интеллекта TensorFlow 443 машинное обучение 441 нейронные сети 435 общие сведения 433 эволюционные вычисления 439 методы с двойным подчёркиванием 413 микросервопривод SG90 20 Минский, Марвин 435 минус, знак (-) 68 МКБ коды 564 мнимая часть 70 многомерные массивы 468 многострочные форматные строки, создание 103 . . . (многоточие) 197 многоточие ( . . .) 197 множества 189 модули импорт 418 использование псевдонима 63 общие сведения 61, 410, 416 понимание синттаксиса импорта 62 создание 421 модуль Grove с синим (или белым) светодиодом 581 мозг для робота, создание 719 Мюллер, Джон Пол 450 Н наивные значения даты и времени 132 наложение 762 напряжения 573 наследование вызов метода базового класса 292 784 добавление новых параметров в подкласс 287 использование одного имени дважды 293 общие сведения 279 определение подкласса 284 переопределение значения по умолчанию в подклассе 286 создание базового класса 282 настройка аутентификации 557 настройка программной среды 487 наука о данных большие данные многообразие 527 общие сведения 526 объем 526 скорость 528 в сравнении с аналитикой данных 533 общие сведения 525 поддержка данных 534 представление результатов 533 пятиэтапный процесс анализ данных 532 обработка данных 530 общие сведения 529 сбор данных 530 нейровычислительный стик Intel (NCS) 512 нейронные сети веса и смещения 451 обратное распространение 450 общие сведения 435, 445 построение в TensorFlow графический интерфейс 479 загрузка данных 470 изменение на трехслойную сеть 480 компиляция модели 470 настройка и обучение модели 471 общие сведения 468 определение модели и ее слоев 470 оценка модели 471 проверка результатов 477 разбор кода 472 построение на Python Python все в одном для чайников
использование TensorFlow 466 общие сведения 453 программный код 454 установка TensorFlow 467 сверточная 500 слои нейронов 448 функция активации 451 функция потерь 452 необычные числа 108 необязательные параметры со значениями по умолчанию 230 неравенство, оператор (!=) 75 О облако IBM 515 облачные сервисы Amazon (AWS) 515 оборудование для измерения температуры и влажности 612 обработка данных 530 обратная связь 653 обратное распространение ошибки 450 обращение к элементам списка по позиции 166 обучение без учителя 485 общие сведения 483 с подкреплением 485 с учителем 485 общая функция для сервоприводов 711 объединение списков 173 объектно-ориентированное программирование (ООП) 58 объекты добавление атрибутов изменение значений 263 общие сведения 258 определение атрибутов со значениями по умолчанию 264 создание экземпляров 260 классы и объекты 252 онлайн-справка 45 ООП (объектно-ориентированное программирование) 58 ** (оператор возведения в степень) 74 - (оператор вычитания) 74 / (оператор деления) 74 % (оператор остатка от деления) 74 * (оператор умножения) 74 // (оператор целочисленного деления) 74 операторы break 155 continue 157 import 63 арифметические 74 логические 76 реляционные 138 сравнения 75, 139 строковые 114 операционная система для роботов (ROS) 669 определение позиции указателя с помощью tell() 340 организация данных JSON 369 основные функции управления двигателем 707 остаток от деления, оператор (%) 74 открытый сток 610 отладка кода 46 отображение денежных сумм 100 отправка веб-запросов с помощью Python 397 отрицание совпадения, оператор (is not) 75 отступы и их важность 59 оценка модели 471 очистка списка 176 ошибка аутентификации 563 ошибки else 309 try 312 исключения 299 обработка 302 общие сведения 298 предотвращение сбоев 307 создание исключений 314 Предметный указатель 785
П пакеты 410, 424 папка AIO Python 46 папки создание для Jupyter Notebook 52 создание для кода 46 пасхалка 56 передача данных (SDA) 610 переменные определение 77 переменные класса 273 создание запуск в VS Code 81 манипуляции переменными 79 общие сведения 79 с допустимыми именами 78 сохранение работы 81 перемещение робота 764 переобучение 503 плата Pi2Grover 19 плата сопряжения Pi2Grover Grove 636 платформа Google Cloud 554 поддержка данных 534 подклассы добавление новых параметров 287 определение 284 переопределение значений по умолчанию 286 подключение оборудование для эксперимента с кислородом 625 подчеркивание, знак (_) 78 порт Ethernet 575 порты 576 порты ввода-вывода общего назначения (GPIO) 579 последовательный интерфейс 600 последовательный периферийный интерфейс (SPI) 609 поток анализа кадров OpenCV 761 потоки 726 поток отображения видео 760 поток ультразвукового датчика 760 предложения по улучшению Python (PEP) 57 786 предотвращение сбоев приложения 307 преобразование Excel в JSON 370 в дату 352 в логический тип 355 временных меток Firebase в даты Python 379 в целые числа 352 в числа с плавающей точкой 355 данных из CSV в объекты и словари импорт CSV в объекты Python 358 импорт CSV в словари Python 364 общие сведения 358 даты Excel в дату JSON 375 строк 350 преобразование Excel в JSON 370 преобразование в нижний регистр 244 преобразование временных меток Firebase в даты Python 379 преобразование в числа с плавающей точкой 355 приводной двигатель 681 приложения написание комментариев 66 общие сведения 64 открытие файла приложения 65 работа с операторами арифметические 74 логические 76 общие сведения 73 сравнения 75 синтаксис 83 создание 64 создание переменных в коде 79 запуск в VS Code 81 манипуляции 79 общие сведения 77 с допустимыми именами 78 сохранение работы 81 типы данных логические значения 72 общие сведения 67 слова (строки) 70 числа 68 Python все в одном для чайников
программирование роботов 673 программируемые RGB-ленты 684 программируемые RGBсветодиоды 683 программируемые логические интегральные схемы (FPGA) 517 программная система передачи репрезентативного состояния (REST) 554 программное обеспечение Adeept 693 прореживание 739 протокол 121, 391 протокол передачи гипертекста (HTTP) 391 проценты 101 процессор Broadcom Videocore-IV 510 псевдоним, использование с модулями 63 пузыри 438 пузырь доткомов 438 пустая строка 113 пустые классы 256 Р рабочий цикл 649 равенство, оператор (==) 75 равно, знак (=) 86 разъём GPIO 575 расширение Python 31 регистрация в BigQuery 556 регулярные выражения 356 редактирование кода 46 редактор кода 29 реляционные операторы 138 репозиторий GitHub 443 # (решетка) 66, 392 решетка (#) 66, 392 робот координация движений моторов с датчиками 715 общие сведения 697 операционная система для роботов 714 перемещение 700 программное обеспечение Adeept 727 создание Python-мозга 719 создание интерфейса общие сведения 697 функция motorForward() 698 функция wheelsLeft() 699 функция wheelsPercent() 699 тестирование интерфейса 711 функции класса RobotInterface общие сведения 701 основные функции управления двигателем 707 передний светодиод 702 светодиодная лента 703 сервопривод 708 ультразвуковой датчик расстояния 706 робототехника компоненты робота 671 марсоход PiCar-B 676 общие сведения 665 программирование 673 робот Бакстер 667, 668 робот от Wilkinson Baking 668 сборка робота 675 тестирование видео с камеры 695 калибровка сервоприводов 690 подготовка к проведению тестов 693 тестовый код 694 установка программного обеспечения для тестирования 693 тостер Griffin 670 робот-пекарь от Wilkinson 668 Роузброк, Адриан 521 руководство по стилю написания кода Python (PEP 8) 57 С Сартак, Джейн 521 сверточная нейронная сеть (СНС) 500 Предметный указатель 787
светодиод активности (Status ACT) 576 светодиод питания (Status PWR) 576 североамериканское восточное летнее время (EDT) 137 сериализация 372 сеть прямого распространения 448 сеть, тестирование 490 (символ новой строки) 334 символ новой строки 334 система Grove GPS-модуль Adafruit Ultimate 606 выбор базовых блоков 591 датчики межмикросхемной шины I2C 602 защита от ошибок 594 общие сведения 590 разъемы Grove аналоговые 599 общие сведения 597 цифровые 597 система управления пакетами Python (pip) 417 скаляр 467 скорость передачи 600 скрейпинг экрана 400 словари данных методы словарей 209 многоключевые словари вложенные словари 220 метод fromkeys() 217 метод setdefaulf() 217 общие сведения 214 создание с помощью включений 223 о словарях 193 перебор элементов 206 создание добавление данных 204 доступ к данным 198 изменение данных 204 изменение значения ключа 203 метод get() 202 общие сведения 196 определение длины 200 поиск ключей 201 удаление элементов 210 788 сложение, оператор (+) 74 слот для microSD-карты 576 смещения и веса 451 совпадение, оператор (is) 75 соглашение об именовании 86 соединительные кабели Grove 646 создание базового (основного) класса 282 виртуального окружения 425 интерфейса для роботов motorForward(), функция 698 wheelsLeft(), функция 699 wheelsPercent(), функция 699 общие сведения 697 класса 255 многострочные форматные строки 103 модулей 421 нейронных сетей веса и смещения 451 обратное распространение 450 общие сведения 445 тензоры 467 функция активации 451 функция потерь 452 нейронных сетей в TensorFlow графический интерфейс 479 загрузка данных 470 компиляция модели 470, 474 настройка и обучение модели 471 общие сведения 466 определение модели нейронной сети и ее слоев 474 оценка модели 471, 476 переход к трехслойной нейроннойсети в TensorFlow и Keras 480 проверка результатов 477 разбор кода 472 нейронных сетей на Python использование TensorFlow 466 код нейронной сети 454 общие сведения 453 применение исключающего ИЛИ-НЕ 454 папки Python все в одном для чайников
для Jupyter Notebooks 52 общие сведения 46 переменных в коде 79 запуск в VS Code 81 манипуляции с переменными 79 общие сведения 77 с допустимыми именами 78 сохранение работы 81 приложений 38 программы управления роботами на Python 719 роботов 673 словарей данных добавление данных 204 доступ к данным 198 изменение данных 204 изменение значения ключа 203 общие сведения 196 определение длины словаря 200 получение данных с помощью get() 202 проверка наличия ключа 201 с помощью включений 223 экземпляра класса 257, 260 создание (x) 324 создание базового (основного) класса 282 создание мозга для робота 719 создание файла requirements .txt 427 сортировка списков по алфавиту 180 сохранение блокнота Jupyter 53 кода 49 собранных данных в CSV -файл 407 в JSON-файл 406 специализированные инструменты 27 списки вставка элемента 171 длина 166, 170 добавление элемента в конец 170 изменение элемента 172 использование включений 184 копирование списка 183 обращение к элементам по позиции 166 обращение порядка элементов 183 общие сведения 165 объединение списков 173 очистка списка 176 перебор элементов 168 подсчет количества вхождений элемента 177 поиск индекса элемента 178 проверка наличия элемента 169 сортировка по алфавиту 180 уладение элемента 174 списковые включения 184 справка встроенная 42 выход 44 онлайн 45 сравнение, операторы 75 среднее время по Гринвичу (GMT) 131 стартовый набор Raspberry Pi 577 степень, оператор (**) 74 Стивенс, Род 499 стохастическая оптимизация 474 строка форматирования 125 строки длина 113 как тип данных 70 конкатенация 112 манипуляции с помощью методов 117 общие сведения 91 операторы 114 строки документации 67 Т табличные данные 345 тактовая линия (SCL) 610 теги привязки 400 Тейлор, Аллен Г . 561 текстовые файлы 322 текстовый режим (t) 324 тензор 468 терминал, запуск 39 тернарные операции 148 Предметный указатель 789
тестирование интерфейса робота 711 на внешних изображениях 497 обученной сети 742 отдельного тестового изображения 495 роботов видео с камеры 695 калибровка сервоприводов 690 подготовка к запуску тестов 693 тестовый код PiCar-B на Python 694 установка программного обеспечения для тестирования 693 сети 490 тестирование видео с камеры Pi 695 тестовый код PiCar-B на Python 694 типы данных логические значения 72 общие сведения 67 строки 70 числа 68 тостер Griffin с поддержкой Bluetooth 670 ; (точка с запятой) 59, 85 точка с запятой (;) 59, 85 ''' (тройные кавычки) 66 """ (тройные кавычки) 66 тройные кавычки (""" или ''') 66 тюльпановый пузырь 438 У удаление элементов словаря 210 элементов списка 174 удаление данных из словаря 384 указатели использование tell() для определения позиции указателя 340 перемещение с помощью seek() 341 ультразвуковой датчик, как компонент роботов 687 790 ультразвуковой датчик расстояния, функции 706 умножение, оператор (*) 74 универсальная последовательная шина типа C (USB-C) 512 универсальное координированное время (UTC) 121 универсальный асинхронный приёмопередатчик (модуль UART) 606 управление виртуальными окружениями активация 426 деактивация 428 общие сведения 424 создание 425 создание файла requirements .txt 427 управление идентификацией и доступом (IAM) 555 управление светодиодной лентой 703 ускоритель Google Edge TPU 512 установка Python 31 TensorFlow в Python 467 TensorFlow на Raspberry Pi 731 VS Code 30 программного обеспечения для тестирования PiCar-B 693 расширения Python 31 установка программного обеспечения для тестирования PiCar-B на Python 693 учебник по NumPy 537 Ф файлы CSV общие сведения 345 открытие 347 преобразование данных в объекты и словари 358 сохранение собранных данных в 407 внешние Python все в одном для чайников
UTF-8 331 двоичные 322 закрытие 323 общие сведения 321 открытие 323 перебор содержимого 334 текстовые 322 чтение и копирование двоичного файла 342 чтение содержимого 332 закрытие 52 { } (фигурные скобки) 99, 189, 378 фигурные скобки ( { } ) 99, 189, 378 физические вычисления Raspberry Pi 574 общие сведения 574 создание проектов для считывания параметров окружающей среды 577 управление светодиодом 585 широтно-импульсная модуляция 682 форматирование чисел выравнивание 105 интернационализация 101 общие сведения 98 отображение денежных сумм 100 проценты 101 создание многострочных форматных строк 103 с помощью f-строк 98 Форта, Бен 561 функции abs() 92 allLEDSOff() 706 alphabetize() 236 astimezone() 137 backwardPropagate() 461 backwards() 661 bin() 108 centerAllServos() 711 chr() 116, 159 color() 706 colorWipe() 705 complex() 110 currency() 249 describe() 544 dict() 364 dir() 412 feedforward() 460 fetchUltraDistance() 706 findContours() 763 float() 356 format() 94 forward() 661 headTiltDown() 709 headTiltMiddle() 709 headTiltPercent() 709 headTiltUp() 709 headTurnLeft() 708 headTurnMiddle() 708 headTurnPercent() 708 headTurnRight() 708 help() 295 hex() 108 int() 95, 162, 352 len() 113 ljust() 249 max() 94 mdy() 421 min() 94 model .evaluate() 494 motorBackward() 700 motorForward() 698 oct() 108 ord() 116 print() 102, 111, 115, 141, 198, 219, 240 rainbowCycle() 704 range() 150 round() 93 set_Front_LED_Off() 702 set_Front_LED_On() 702 setPixelColor() 705 sleep() 700 sqrt() 96 stopMotor() 707 str() 178 sub() 356 theaterChaseRainbow() 705 to_curr() 421 to_date() 421 type() 127 Предметный указатель 791
wheelsLeft() 699, 710 wheelsMiddle() 699, 710 wheelsPercent() 699, 710 wheelsRight() 710 активации 451 анонимные 242 возвращение значений 240 встроенные 94 вычисление чисел 92 исключающее ИЛИ-НЕ 454 класса RobotInterface основной двигатель 707 передние светодиоды 702 светодиодная лента 703 сервопривод 708 ультразвуковой датчик расстояния 706 комментирование 226 математические 95 основной двигатель 707 передача информации использование именованных аргументов (kwargs) 233 необязательные параметры со значениями по умолчанию 230 общие сведения 227 передача нескольких значений 231 передача нескольких значений в списке 235 передача произвольного количества аргументов 239 потерь 452 создание 225 сортировка по алфавиту 180 ультразвукового датчика расстояния 706 ультразвуковой датчик расстояния 706 управление светодиодной лентой 703 управления передними светодиодами 702 функция активации 451 функция потерь 452 792 Х хлебопекарный робот Wilkinson Baking 668 Ц целочисленное деление, оператор (//) 74 центральный процессор (ЦПУ) 509 циклический перебор по JSON-файлу с ключами 377 по внешним файлам определение позиции указателя с помощью tell() 340 перемещение указателя с помощью seek() 341 с помощью readline() 336 с помощью readlines() 334 по словарю 206 по списку 153 цифровой I2C 609 цифровой SPI 609 цифровой ввод 609 цифровые разъемы Grove 597 ЦПУ (центральный процессор) 509 Ч часовые пояса 120 четырёхканальный 16-битный аналогоцифровой преобразователь Grove 623 числа восьмеричные 108 вычисления с помощью функций 92 двоичные 108 как тип данных 68 комплексные 70, 110 математические функции 94 общие сведения 67, 91 форматирование выравнивание 105 интернационализация 101 общие сведения 98 Python все в одном для чайников
отображение денежных сумм 100 проценты 101 создание многострочных форматных строк 103 с помощью f-строк 98 шестнадцатеричные 108 числа с плавающей точкой 69 чтение (r) 324 чтение/запись (r+) 324 чувствительность к регистру 51 Ш шаговые двигатели общие сведения 635 программное обеспечение 640 шаговый двигатель 28BYJ-48 ULN2003 5V 654 шестнадцатеричные числа 108 ШИМ (широтно-импульсная модуляция) 588 широтно-импульсная модуляция (ШИМ) 588 Шовик, Джон 436 Шолле, Франсуа 507 шпаргалки 46 Э эволюционные вычисления 439 экранирующие символы 72 электродвигатели малые двигатели постоянного тока 633 общие сведения 633 сервоприводы 634 шаговые двигатели 635 энтропия 494 Я язык структурированных запросов (SQL) 561
Об авторах Джон Шовик начал работать с программным обеспечением после того, как уговорил администрацию средней школы разрешить ему использовать компьютер IBM 1130 все лето 1973 года. Это стало началом его пожизненной любви к программированию. Доктор Шовик основал несколько компаний, включая Advance Hardware Architectures, TriGeo Network Security, Blue Water Technologies, InstiComm, LLC, SwitchDoc Labs и bankCDA. Он выступил с более чем 80 докладами по приглашению и опубликовал свыше 50 статей на темы, связанные с искусственным интеллектом, автоматизацией производства, Arduino, Raspberry Pi, iBeacons, HIPAA, GLB, компьютерной безопасностью, криминалистикой, встраиваемыми системами и многим другим. В 2019 году он был назначен директором исследовательской группы «Центр интеллектуальной промышленной робототехники». В настоящее время Джон преподает информатику в Университете Айдахо в городе Кер-д© Ален, штат Айдахо, где специализируется на робототехнике и искусственном интеллекте, работая со студентами, разделяющими его страсть к технологиям. Алан Симпсон — автор более сотни компьютерных книг по базам данных, программированию и веб-разработке. Его работы издавались по всему миру более чем на десяти языках и разошлись миллионными тиражами. Несколько лет назад Алан решил оставить писательскую деятельность и перейти в практическую разработку. За годы работы его называли «мастером коммуникации», а его онлайн-курсы и видео продолжают получать положительные отзывы от множества учеников и последователей. Благодарности авторов Джон Шовик: Я хочу поблагодарить замечательную команду издательства Wiley и выразить особую признательность моему соавтору Алану. Наш технический рецензент Род Стивенс проделал отличную и вдумчивую работу. Также благодарю моего литературного агента Кэрол Джелен за то, что вдохновила меня на написание этой книги.
Отдельное спасибо Университету Айдахо за поддержку и советы, особенно доктору Бобу Ринкеру и доктору Мэри Эверетт. Замечательные люди, замечательный университет. И, конечно, я благодарен моим студентам — Хантеру, Николаю, Гарретту, Лейси и Дагу — за ежедневное вдохновение. Алан Симпсон. Огромная благодарность Элизабет Стиллвелл, Сьюзан Кристоферсен и всей команде Wiley за возможность работать над этой книгой. Спасибо моим литературным агентам Кэрол Джелен и Марго Мейли Хатчинсон из Waterside Productions. И, конечно, спасибо моей жене Сьюзан за терпение, особенно в те моменты, когда я работал в самое неподходящее время. Посвящение Джон Шовик. Моей жене Лори — за поддержку во всем, за проверку того, что носки утром подобраны правильно, и за заботу о моем фикусе, который появился во время написания диссертации. Спасибо! Алан Симпсон. Сьюзан, Эшли и Алеку. Изображение на обложке: © Дамиан Собчик/Adobe Stock Phot
УЖЕ В ПРОДАЖЕ! Джон Пол Мюллер, Лука Массарон АЛГОРИТМЫ для чайников Секретное оружие для эффективного использования алгоритмов От ленты новостей до расчета страховых взносов — алгоритмы пронизывают практически все сферы нашей жизни, формируя современное общество и личную реальность. На первый взгляд они могут показаться непостижимо сложными, но на самом деле каждый способен освоить — и творчески применять — эти мощные инструменты для решения любых задач! Книга «Алгоритмы для чайников» расскажет, что такое алгоритмы, как они функционируют, где встречаются (спойлер: повсюду!), кто стоит за созданием ключевых современных алгоритмов (это удивительно, но их корни уходят к древнегреческому философу!) и как вы можете начать создавать их самостоятельно. Вас ждут: • Наглядные материалы: десятки графиков и диаграмм, помогающие интуитивно понять внутреннюю логику алгоритмов. • Практический код: ссылки на онлайн-репозиторий GitHub для доступа к актуальным примерам кода. • Легкий старт в программировании: пошаговые инструкции по работе с Google Colaboratory — удобной средой программирования, не требующей установки и запускающейся прямо в браузере.
УЖЕ В ПРОДАЖЕ! Пол Макфедрис HTML, CSS и JAVASCRIPT все в одном для чайников Хотите научиться создавать привлекательные, функциональные веб-сайты, но боитесь сложности? Эта книга специально создана для тех, кто хочет овладеть веб-разработкой с нуля, без необходимости покупать несколько разных учебников. Здесь собрано все самое важное: от основ до практических навыков создания реальных проектов. Пять книг под одной обложкой — это полный путь веб-разработчика. Вы начнете с введения в веб-интерфейсы, затем освоите язык разметки HTML для создания структуры страниц, изучите CSS для стилизации и дизайна, научитесь добавлять интерактивность с помощью JavaScript и узнаете, как создавать оптимальные макеты, которые одинаково хорошо выглядят на любых устройствах. В этой книге вы найдете: • современные методы создания привлекательных и функциональных веб-сайтов с нуля; • пошаговые инструкции по верстке и добавлению анимации; • практические советы по настройке интерфейса, внедрению виджетов и оптимизации производительности сайта; • доступ к бонусной 6-й книге по специальной ссылке. Структурированный обучающий самоучитель с практическими примерами и понятными объяснениями сложных концепций делают обучение простым даже для полных новичков. Вы не только поймете теорию, но и сразу сможете применить знания на практике, создавая реальные проекты.
Все права защищены. Книга или любая ее часть не может быть скопирована, воспроизведена в электронной или механической форме, в виде фотокопии, записи в память ЭВМ, репродукции или каким-либо иным способом, а также использована в любой информационной системе без получения разрешения от издателя. Копирование, воспроизведение и иное использование книги или ее части без согласия издателя является незаконным и влечет уголовную, административную и гражданскую ответственность. Издание для досуга ДЛЯ ЧАЙНИКОВ Шовик Джон Симпсон Алан PYTHON ВСЕ В ОДНОМ ДЛЯ ЧАЙНИКОВ Главный редактор Р. Фасхутдинов Руководитель направления В. Обручев Ответственный редактор Л. Салихова Научный редактор Н. Фидченко Литературный редактор Е. Ерошкина Младший редактор М. Назаренко Художественный редактор К. Доброслов Компьютерная верстка Т. Лукина Корректор Л. Макарова Страна происхождения: Российская Федерация Шы0арушы ел: Ресей Федерациясы ООО «Издательство «Эксмо» 123308, Россия, г. Москва, ул. Зорге, д. 1, стр. 1, эт. 20, каб. 2013. Тел.: 8 (495) 411-68-86. Home page: www.eksmo.ru E-mail: info@eksmo.ru Lндіруші: «Издательство «Эксмо» ЖШP 123308, Ресей, МQскеу Rаласы, Зорге кSшесі, 1-Tй, 1-RUрылыс, 20 Rабат, 2013-каб. Тел.: 8 (495) 411-68-86. Home page: www.eksmo.ru E-mail: info@eksmo.ru. Тауар белгісі: «Эксмо» Интернет-магазин : www.book24.ru Интернет-магазин : www.book24.kz Интернет-дкен : www.book24.kz 12+ Импортёр в Республику Казахстан ТОО «РДЦ-Алматы». PазаRстан Республикасына импорттаушы «РДЦ-Алматы» ЖШС. Дистрибьютор и представитель по приему претензий на продукцию в Республике Казахстан: ТОО «РДЦ-Алматы» ТОО РДЦ Алматы, Алматы, ул. Домбровского, 3«а», литер Б, офис 1. Дистрибьютор жQне PазаRстан Республикасында Sнімге ша0ымдар Rабылдау жSніндегі Sкіл: «РДЦ-Алматы» ЖШС. Алматы R., Домбровский кSш., 3 «а», литер Б, офис 1. Тел.: 8 (727) 251-59-90/91/92. E-mail: RDC-Almaty@eksmo.kz Сведения о подтверждении соответствия издания согласно законодательству РФ о техническом регулировании можно получить на сайте Издательства «Эксмо»: www.eksmo.ru/certification ТехникалыR реттеу туралы РФ за]намасына сай басылымны] сQйкестігін растау туралы мQліметтерді мына адрес бойынша алу0а болады: http://eksmo.ru/certification/ Произведено в Российской Федерации Ресей Федерациясында Sндірілген Сертификаттау0а жатпайды Дата изготовления / Подписано в печать 05.05.2026. Формат 70x1001/16. Печать офсетная. Усл. печ. л. 64,81. Тираж экз. Заказ
Москва. ООО «Торговый Дом «Эксмо» Адрес: 123308, г. Москва, ул. Зорге, д.1, строение 1. Телефон: +7 (495) 411-50-74. E-mail: reception@eksmo-sale.ru По вопросам приобретения книг «Эксмо» зарубежными оптовыми покупателями обращаться в отдел зарубежных продаж ТД «Эксмо» E-mail: international@eksmo-sale.ru International Sales: International wholesale customers should contact Foreign Sales Department of Trading House «Eksmo» for their orders. international@eksmo-sale.ru По вопросам заказа книг корпоративным клиентам, в том числе в специальном оформлении, обращаться по тел.: +7 (495) 411-68-59, доб. 2151. E-mail: borodkin.da@eksmo.ru Оптовая торговля бумажно-беловыми и канцелярскими товарами для школы и офиса «Канц-Эксмо»: Компания «Канц-Эксмо»: 142702, Московская обл., Ленинский р-н, г. Видное-2, Белокаменное ш., д. 1, а/я 5. Тел./факс: +7 (495) 745-28-87 (многоканальный). e-mail: kanc@eksmo-sale.ru, сайт: www.kanc-eksmo.ru Филиал «Торгового Дома «Эксмо» в Нижнем Новгороде Адрес: 603094, г. Нижний Новгород, улица Карпинского, д. 29, бизнес-парк «Грин Плаза» Телефон: +7 (831) 216-15-91 (92, 93, 94). E-mail: reception@eksmonn.ru Филиал OOO «Издательство «Эксмо» в г. Санкт-Петербурге Адрес: 192029, г. Санкт-Петербург, пр. Обуховской обороны, д. 84, лит. «Е» Телефон: +7 (812) 365-46-03 / 04. E-mail: server@szko.ru Филиал ООО «Издательство «Эксмо» в г. Екатеринбурге Адрес: 620024, г. Екатеринбург, ул. Новинская, д. 2щ Телефон: +7 (343) 272-72-01 (02/03/04/05/06/08) Филиал ООО «Издательство «Эксмо» в г. Самаре Адрес: 443052, г. Самара, пр-т Кирова, д. 75/1, лит. «Е» Телефон: +7 (846) 207-55-50. E-mail: RDC-samara@mail.ru Филиал ООО «Издательство «Эксмо» в г. Ростове-на-Дону Адрес: 344023, г. Ростов-на-Дону, ул. Страны Советов, 44А Телефон: +7(863) 303-62-10. E-mail: info@rnd.eksmo.ru Филиал ООО «Издательство «Эксмо» в г. Новосибирске Адрес: 630015, г. Новосибирск, Комбинатский пер., д. 3 Телефон: +7(383) 289-91-42. E-mail: eksmo-nsk@yandex.ru Обособленное подразделение в г. Хабаровске Фактический адрес: 680000, г. Хабаровск, ул. Фрунзе, 22, оф. 703 Почтовый адрес: 680020, г. Хабаровск, А/Я 1006 Телефон: (4212) 910-120, 910-211. E-mail: eksmo-khv@mail.ru Республика Беларусь: ООО «ЭКСМО АСТ Си энд Си» Центр оптово-розничных продаж Cash&Carry в г. Минске Адрес: 220014, Республика Беларусь, г. Минск, проспект Жукова, 44, пом. 1-17, ТЦ «Outleto» Телефон: +375 17 251-40-23; +375 44 581-81-92 Режим работы: с 10.00 до 22.00. E-mail: exmoast@yandex.by Казахстан: «РДЦ Алматы» Адрес: 050039, г. Алматы, ул. Домбровского, 3А Телефон: +7 (727) 251-58-12, 251-59-90 (91,92,99). E-mail: RDC-Almaty@eksmo.kz Полный ассортимент продукции ООО «Издательство «Эксмо» можно приобрести в книжных магазинах «Читай-город» и заказать в интернет-магазине: www.chitai-gorod.ru. Телефон единой справочной службы: 8 (800) 444-8-444. Звонок по России бесплатный. Интернет-магазин ООО «Издательство «Эксмо» www.eksmo.ru Розничная продажа книг с доставкой по всему миру. Тел.: +7 (495) 745-89-14. E-mail: imarket@eksmo-sale.ru