Текст
                    PYTHON

объектно-ориентированное
программирование с нуля

PYTHON объектно-ориентированное программирование с нуля Издательство ACT г. Москва
УДК 1)04.43 ББК 32.973.432.2 0-29 В книге использованы материалы издания «Object-Oriented Programming in Python Documentation» Кейптаунского университета, которое распространяется по лицензии Creative Commons Attribution-ShareAlike 4.0 international (https://creativecornmons.Org/licenses/by-sa/4.0/j и доступно по ссылке https://python-textbok.readthedocs.io/en/latest/ Объектно-ориентированное программирование на Python 0-29 с нуля — Москва : Издательство ACT, 2026. — 464 с.: ил. — (Програм- мирование от Адо C++). ISBN 978-5 17-182311-5. Данное руководство предлагает системный подход к освоению прин- ципов объектно-ориентированного программирования (ООП) с исполь- зованием языка Python, широко применяемого в промышленной разработке и научных исследованиях. В книге представлена комплексная теоретическая и практическая база, адаптированная под современные версии Python. Обучение строится по принципу последовательного усложнения материала: от базовых элементов языка (переменных, функций, коллекций) к проектированию и реализации собственных классов и объектов Подробно рассматриваются ключевые парадигмы ООП: инкапсуляция, наследование и полиморфизм, а также работа с магическими методами, модулями, пакетами и организация тестирования. Особое внимание уделяется практическому применению знаний. На примере создания ряда приложений, включая графический интерфейс на основе tkinter, демонстрируется использование ООП для решения реальных задач. Все примеры логически взаимосвязаны, что способствует глубокому и последовательному усвоению материала УДК 004.43 ББК 32.973.432.2 ISBN 978-5-17-182311-5 Перевод на русский язык: ООО -сИнтеджер». Издание на русском языке. ООО «Издательство АСТ».
Содержание Введение........................................ 15 Что такое компьютер?..............................15 Компьютерные инструкции.........................15 Основные компоненты компьютера..................16 Процессор.......................................18 Память..........................................19 Типы компьютеров................................20 История компьютеров...............................23 Компьютеры первого поколения (1950-е годы)......25 Компьютеры второго поколения (конец 1950-х — середина 1960-х годов)..........................26 Компьютеры третьего поколения (середина 1960-х — начало 1970-х годов)............................26 Компьютеры четвертого поколения (начало 1970-х годов и далее)........................................26 Программирование компьютера.......................27 Алгоритмы.......................................27 Рецепты как алгоритмы (структурированный подход к программированию).........................27 Сценарии для пьес как алгоритмы (объектно- ориентированный подход к программированию)..28 Языки программирования..........................28 Компиляторы, интерпретаторы и язык программирования Python..........................................30 Разработка программы на Python..................31 Понимание проблемы..........................32 Создание алгоритма..........................33
Написание программы.........................34 Тестирование npoj-раммы................... 35 Языки программирования............................35 Процедурные языки...............................35 Функциональные и логические языки...............38 Объектно-ориентированные языки..................40 Основы Python.......................................43 Введение..........................................43 Сравнение Python 2 и Python 3...................43 Начало работы с Python............................44 Использование интерактивного интерпретатора.....44 Запуск программ из файлов.......................46 Установка новых пакетов.........................47 Дополнительные материалы........................47 Основные элементы программы на Python.............48 Ключевые слова..................................49 Идентификаторы..................................50 Упражнение 1....................................52 Поток управления................................53 Отступы и отсутствие точек с запятой............53 Упражнение 2....................................54 Упражнение 3....................................55 Чувствительность к регистру символов............55 Подробнее о комментариях...................... 56 Ввод и вывод....................................57 Файлы...........................................59 Встроенные типы.................................60 Целые числа.......................................61 Целочисленные операции..........................62 Приоритет операторов............................64 Упражнение 4....................................65 Числа с плавающей точкой..........................66
Операции с плавающей точкой и приоритет.........67 Упражнение 5....................................68 Строки............................................69 Форматирование строк............................70 Экранирование...................................70 « Сы рые » стр ок и.............................72 Тройные кавычки.................................73 Операции со строками............................73 Упражнение 6....................................74 Ответы на упражнения..............................76 Ответ на упражнение 1...........................76 Ответ на упражнение 2...........................76 Ответ на упражнение 3...........................76 Ответ на упражнение 4...........................77 Ответ на упражнение 5...........................77 Ответ на упражнение 6...........................79 Переменные и область видимости.........................81 Переменные...........................................81 Объявление переменных..............................81 Область видимости и время жизни переменных.........82 Оператор присваивания..............................84 Составные операторы присваивания...................86 Подробнее об области видимости: пересечение границ.87 Упражнение 1.......................................90 Изменение значений...................................91 Константы..........................................91 Изменяемые и неизменяемые типы (mutable vs immutable)..........................94 Подробнее о вводе данных...........................96 Пример: вычисление расхода топлива автомобилем.....98 Упражнение 2.......................................99 Преобразование типов.................................100 Неявное преобразование.............................100
Явное преобразование.............................101 Преобразование строк.............................103 Дополнительные сведения о преобразованиях........104 Упражнение 3.....................................106 Ответы на упражнения...............................107 Ответ на упражнение 1............................107 Ответ на упражнение 2............................108 Ответ па упражнение 3............................108 Операторы ветвления..................................110 Введение...........................................110 Оператор выбора: if................................110 Операторы проверки равенства.....................111 Сравнение значений и оператор тождественности....113 Использование отступов...........................114 Условная конструкция else..........................115 Упражнение 1.....................................116 Дополнительные сведения об операторе if............117 Вложенные операторы if.............................117 Конструкция el if и «лестница условий»...........119 Значения типа bool и логические операторы..........122 Тип bool.........................................122 Логические операторы.............................123 Оператор and.....................................124 Вычисление по ускоренной схеме...................125 Оператор о г.....................................127 Оператор not.....................................129 Приоритет операторов для логических выражений....130 Закон Де Моргана и особенности применения оператора not....................................131 Упражнение 2.....................................133 Значение None......................................134 Замена оператора switch в Python на основе словарей.136
Тернарный оператор (условное выражение).........137 Упражнение 3....................................138 Ответы на упражнения..............................141 Ответ на упражнение 1.......................... 141 Ответ на упражнение 2...........................142 Ответ на упражнение 3...........................143 Коллекции...........................................145 Списки............................................145 Встроенные функции для списков..................149 Использование арифметических операторов со списками.152 Списки и массивы................................152 Упражнение 1....................................154 Кортежи...........................................154 Упражнение 2....................................157 Множества (sets)..................................157 Упражнение 3....................................159 Диапазоны.........................................160 Упражнение 4....................................161 Словари...........................................161 Упражнение 5....................................165 Преобразование между коллекциями..................166 Неявные преобразования..........................166 Явные преобразования............................167 Подробно о строках..............................168 Упражнение 6....................................171 Двумерные последовательности......................172 Упражнение 7....................................176 Ответы на упражнения..............................178 Ответ на упражнение 1...........................178 Ответ на упражнение 2...........................178 Ответ на упражнение 3...........................179 Ответ на упражнение 4...........................179
Ответ на упражнение 5..........................179 Ответ на упражнение 6..........................180 Ответ на упражнение 7..........................181 Операторы управления циклом........................182 Введение.........................................182 Оператор while...................................184 Упражнение 1...................................187 Оператор for.....................................188 Упражнение 2...................................191 Вложенные циклы..................................192 Упражнение 3...................................193 Итерируемые объекты, итераторы и генераторы......194 Упражнение 4...................................198 Выражения-генераторы.............................198 Упражнение 5...................................202 Операторы break и continue.......................202 Оператор break.................................202 Оператор continue..............................203 Использование оператора break для имитации цикла do-while.................................204 Упражнение 6...................................206 Использование циклов для упрощения кода........207 Упражнение 7...................................208 Ответы на упражнения.............................209 Ответ на упражнение 1..........................209 Ответ на упражнение 2..........................210 Ответ на упражнение 3..........................212 Ответ на упражнение 4..........................212 Ответ на упражнение 5..........................213 Ответ на упражнение 6..........................214 Ответ на упражнение 7..........................216
Ошибки и исключения.................................218 Ошибки............................................218 Синтаксические ошибки...........................218 Ошибки времени выполнения.......................220 Логические ошибки...............................221 Упражнение 1....................................222 Обработка исключений..............................223 Операторы tгу и except..........................224 Как Python обрабатывает исключения..............228 Проверка на ошибки и обработка исключений.......229 Операторы else и finally........................230 Упражнение 2....................................232 Оператор with...................................233 Использование объекта исключения................233 Генерация исключений вручную....................235 Упражнение 3....................................236 Отладка программ..................................237 Инструменты отладки.............................239 Pyflakes, pylint, PyChecker и рер8..............239 pdb.............................................240 Легирование (Logging).............................241 Упражнение 4....................................244 Ответы на упражнения..............................246 Ответ на упражнение 1...........................246 Ответ на упражнение 2...........................247 Ответ на упражнение 3...........................249 Ответ на упражнение 4...........................250 Функции.............................................252 Введение..........................................252 Упражнение 1....................................255 Входные параметры.................................256
Возвращаемые значения.............................258 Упражнение 3....................................262 Стек вызовов......................................262 Рекурсия........................................263 Упражнение 4....................................265 Параметры по умолчанию............................265 Изменяемые типы и параметры по умолчанию........268 Упражнение 5....................................269 *args и **kwargs..................................270 Упражнение 6....................................273 Декораторы........................................274 Упражнение 7....................................277 Лямбды............................................277 Упражнение 8....................................279 Функции-генераторы и yield........................279 Упражнение 9....................................281 Ответы на упражнения..............................282 Ответ на упражнение 1...........................282 Ответ на упражнение 2...........................282 Ответ на упражнение 3...........................283 Ответ на упражнение 4...........................283 Ответ на упражнение 5...........................284 Ответ на упражнение 6...........................285 Ответ на упражнение 7...........................286 Ответ на упражнение 8...........................287 Ответ на упражнение 9...........................288 Классы..............................................289 Определение и использование класса................290 Упражнение 1....................................293 Атрибуты экземпляра...............................294 getattr, setattr и hasattr......................296 Упражнение 2....................................297
Атрибуты класса....................................298 Упражнение 3.....................................301 Декораторы классов.................................302 @classmethod.....................................302 @sta tiemethod...................................304 @property........................................306 Упражнение 4.....................................309 Исследование объекта...............................310 Упражнение 5.....................................312 Переопределение магических методов.................313 Упражнение 6.....................................316 Ответы на упражнения............................. 318 Ответ на упражнение 1............................318 Ответ на упражнение 2............................319 Ответ на упражнение 3............................320 Ответ на упражнение 4............................320 Ответ на упражнение 5............................322 Ответ на упражнение 6............................322 Объектно-ориентированное программирование............323 Введение...........................................323 Основные принципы ООП............................325 Отношения между объектами........................326 Композиция.........................................327 Упражнение 1.....................................331 Наследование.......................................332 Упражнение 2.....................................335 Подробнее о наследовании...........................337 Множественное наследование.......................337 Миксины (Mix-ins)................................338 Абстрактные классы и интерфейсы........ ........340 Упражнение 3.....................................342 Как запретить наследования.........................342
Замена наследования композицией..................344 Упражнение 4.....................................345 Ответы на упражнения...............................346 Ответ па упражнение 1............................346 Ответ на упражнение 2............................349 Ответ па упражнение 3............................351 Ответ на упражнение 4............................353 Работа с пакетами и тестирование.....................355 Модули.............................................355 Пакеты.............................................357 Создание дистрибутива программы с помощью Distribute.357 Установка и импорт модулей.......................359 Документация.......................................360 Docstrings..................................... 360 Примеры документирования.........................361 Тестирование.......................................363 Выбор тестовых случаев...........................365 Написание модульных тестов.......................368 Проверка покрытия тестами........................373 Упражнение 1.....................................375 Ответы на упражнения...............................378 Ответ на упражнение 1............................378 Полезные модули стандартной библиотеки...............388 Дата и время: модуль datetime......................388 Упражнение 1.....................................390 Математические функции: модуль math................390 Упражнение 2.....................................391 Псевдослучайные числа: модуль random...............391 Упражнение 3.....................................394 Сопоставление строк по шаблону: модуль те..........394 Основы регулярных выражений......................395
Использование модуля ге.........................398 Жадность........................................401 Использование функции в re. sub.................402 Флаги...........................................403 Упражнение 4....................................404 Разбор CSV-файлов: модуль csv.....................405 Упражнение 5....................................406 Написание скриптов: модули sys и argparse.........406 Скрипты против библиотек........................407 Простые параметры командной строки..............407 Сложные параметры командной строки..............408 Упражнение 6....................................410 Ответы на упражнения..............................412 Ответ на упражнение 1...........................412 Ответ на упражнение 2...........................412 Ответ на упражнение 3...........................413 Ответ на упражнение 4...........................413 Ответ на упражнение 5...........................414 Ответ на упражнение 6...........................414 Введение в программирование графического интерфейса с помощью tkinter...................................417 Событийно-ориентированное программирование........418 Основы tkinter....................................418 Классы виджетов.................................421 Варианты компоновки...............................422 Пользовательские события..........................425 Собираем все вместе...............................428 Проверка правильности ввода текста..............431 Обновление общего результата....................432 Упражнение 1....................................433 Ответы на упражнения..............................434
Сортировка, поиск и анализ алгоритмов...............438 Введение..........................................438 Алгоритмы сортировки..............................439 Сортировка выбором..............................440 Упражнение 1....................................444 Упражнение 2....................................444 Сортировка слиянием.............................445 Упражнение 3....................................449 Алгоритм сортировки в Python....................449 Алгоритмы поиска..................................450 Линейный поиск..................................451 Упражнение 4....................................452 Бинарный поиск..................................453 Упражнение 5....................................454 Сложность алгоритма и нотация «О-большое».........455 Упражнение 6....................................457 Ответы на упражнения..............................459 Ответ па упражнение 1...........................459 Ответ на упражнение 2...........................459 Ответ на упражнение 3...........................460 Ответ на упражнение 4...........................462 Ответ па упражнение 5...........................462 Ответ на упражнение 6...........................463
Введение Полезность компьютеров отчасти обусловлена их универ- сальностью в решении разнообразных задач и выполнении различных операций. Однако, чтобы воспользоваться ско- ростью и вычислительной мощностью компьютеров, необ- ходимо уметь программировать. Эта книга посвящена про- граммированию и тому, как с его помощью можно решать практические задачи и автоматизировать полезные действия. Мы выбрали язык Python — современный язык, который считается мощным и относительно простым в изучении. В то же время, несмотря на свою простоту, он способен обеспе- чить платформу для дальнейшего освоения продвинутого про- граммирования. В этом разделе вы научитесь анализировать поставленную задачу и разрабатывать для нее эффективное решение с использованием языка Python. Что такое компьютер? Компьютер — это устройство общего назначения, которое ведет себя в соответствии с набором инструкций и данных, которые ему предоставляются. Компьютеры выполняют ин- струкции для обработки данных. В основе каждого компьюте- ра лежит центральный процессор (ЦП) — современные ЦП вы- полнены в виде единого микропроцессорного чипа. Компьютерные инструкции Компьютер принимает на вход ряд инструкций, обрабаты- вает их одну за другой и, как правило, выводит на экран не-
кии результат, чтобы показать, что он сделал. Это похоже на то, как человек следует инструкциям или рецептам. Однако если человек способен понимать сложные инструкции на есте- ственных языках (например английском), то компьютер пони- мает только очень простые команды, выраженные в языках программирования — формально определенных, строго огра- ниченных языках, значительно менее выразительных, чем естественные. Хотя некоторые языки (например Python) позволя- ют записывать более высокоуровневые инструкции, чем другие (например, язык ассемблера), но и у них есть су- щественные ограничения. Компьютер может легко по- нять инструкцию типа «сложи эти два числа вместе», но не способен напрямую выполнить команду вроде «вычис- ли баланс моей чековой книжки». Такой запрос придется раз- бить на множество более простых пошаговых инструкций. Списки инструкций, которые указывают компьютеру, как вы- полнять сложные задачи, называются программами. Вот несколько примеров простых компьютерных инструкций: • Арифметические инструкции: сложение, вычитание, ум- ножение или деление чисел. • Инструкции сравнения: сравнение двух чисел, чтобы определить, какое из них больше, или равны ли они. Та- кие операции часто называют логическими. • Инструкции ветвления: переход к другой инструкции в программе и продолжение работы с этого места. Современные компьютеры могут выполнять миллионы та- ких инструкций в секунду. Основные компоненты компьютера Компьютер состоит из четырех основных типов компонен- тов:
• Устройства ввода: все, что позволяет компьютеру полу- чать информацию от пользователя. Сюда входят клавиа- туры, мыши, сканеры и микрофоны. • Вычислительные устройства: компоненты компьюте- ра, которые обрабатывают информацию. Основным вычислительным компонентом компьютера является центральный процессор, или ЦП (англ. CPU), но в совре- менном компьютере, скорее всего, есть и другие процес- соры. Например, многие видеокарты оснащены графи- ческими процессорами, которые раньше использовались только для обработки графики, а сегодня могут приме- няться и для программ общего назначения (например, для решения задач искусственного интеллекта (ИИ)). • Устройства хранения информации: компоненты, в кото- рых хранится информация. Сюда входит как основная па- мять — оперативное запоминающее устройство ОЗУ (то, что мы в просторечии называем «памятью»), так и долго- временная память (то, что нам известно как устройства хранения данных, например жесткие диски, компакт-ди- ски или флеш-диски). • Устройства вывода: все, что компьютер использует для отображения информации пользователю. Сюда вхо- дят мониторы, колонки и принтеры. Чтобы понять, как эти компоненты сочетаются друг с дру- гом, рассмотрим, как работает торговый автомат. Торговый ав- томат, строго говоря, не является компьютером, но его можно разделить на очень похожие компоненты: • Устройства ввода: чтобы воспользоваться торговым ав- томатом, вы опускаете в него деньги и делаете выбор. Мо- нетоприемники и кнопки выбора — это устройства ввода торгового автомата.
• Вычислительные устройства: при выборе товара авто- мат выполняет несколько тагов: проверяет, достаточно ли внесено денег, вычисляет сдачу и убеждается, что вы- бранный товар есть в наличии. Часть автомата, которая выполняет все эти действия, можно назвать процессором. • Устройства вывода: торговый автомат выдает вашу по- купку и сдачу в специальные отсеки, из которых вы мо- жете их забрать. Он также оснащен простым электрон- ным дисплеем, на котором могут отображаться буквы и цифры, с помощью которого он передает информацию пользователю. • Устройства хранения информации: для выполнения опе- раций обработки торговый автомат должен хранить ин- формацию о том, какие товары есть на складе, их ценах и доступной сдаче. Эта информация должна где-то хра- ниться. Процессор Центральный процессор — самая важная часть компьюте- ра, поскольку он обрабатывает инструкции. Остальные части компьютера выполняют команды процессора. Процессор име- ет две ключевые характеристики: • Тактовая частота: процессор содержит генератор такто- вых импульсов, который вырабатывает регулярный сиг- нал. Все низкоуровневые операции (переключения), ко- торые выполняет процессор для обработки инструкций, синхронизируются с этим сигналом. Чем выше тактовая частота, тем быстрее (теоретически) может работать про- цессор, но есть и другие ограничения. Современные про- цессоры имеют частоту более 1 ГГц, или 1 000 000 000 пе- реключений в секунду.
• Архитектура процессора: частью архитектуры ЦП явля- ется набор инструкций (точнее, инструкций машинного языка), которые понимает процессор. Разные типы про- цессоров понимают разные наборы инструкций: напри- мер, Intel IA-32 и х86-64, IBM PowerPC или ARM. Центральный процессор состоит из нескольких важных компонентов: • Арифметико-логическое устройство (ALU) выполняет арифметические операции и операции сравнения. • Блок управления определяет, какую инструкцию выпол- нить следующей. • Регистры образуют высокоскоростную область памяти для хранения временных результатов. Память Компьютер хранит информацию в своей памяти для после- дующего использования. Существует два типа памяти: опера- тивная и долговременная. Оперативная память подключается непосредственно к про- цессору (или другим процессорам) и обычно называется ОЗУ (оперативное запоминающее устройство). Большинство типов оперативной памяти теряет свое содержимое при выключе- нии компьютера (т. е. она является энергозависимой). Мы можем представить себе оперативную память как длин- ную последовательность ячеек памяти, каждая из которых имеет свой адрес. Эти адреса начинаются с нуля для первой ячейки, а адрес каждой последующей ячейки на единицу боль- ше адреса предыдущей. Каждая ячейка может содержать толь- ко одно число, но процессор может в любой момент заменить
ее содержимое новым числом. Содержимое может быть извле- чено без удаления из ячейки. Долговременная память дешевле оперативной, поэтому мо- жет иметь гораздо больший объем. Хотя она работает гораздо медленнее, эта память энергонезависима — то есть ее содер- жимое сохраняется даже после выключения компьютера. Ти- пичные примеры такого типа памяти: жесткие диски и твер- дотельные накопители (флеш-диски). Операционная система компьютера предоставляет высоко- уровневые интерфейсы для работы с долговременной памя- тью. Эти интерфейсы позволяют работать с файлами — сово- купностями связанных данных, организованных в иерархию каталогов (директорий). Совокупность интерфейсов и иерар- хии каталогов часто называют файловыми системами. Мы можем представить себе каталоги как коробки (кото- рые могут содержать другие коробки). Хотя с помощью этой метафоры легко представить содержимое жесткого диска или флеш-памяти, важно отметить, что это всего лишь мета- фора — на более низком уровне жесткий диск имеет ряд адре- сов памяти, как и оперативная память, и все данные в конеч- ном итоге хранятся именно в этой форме. Части одного и того же файла не обязательно хранятся в соседних адресах памяти. Типы компьютеров Исторически сложилось так, что компьютеры делятся на специализированные подтипы. Это различие не всегда чет- ко прослеживается в современных компьютерах, которые мо- гут выполнять более широкий спектр функций и часто совме- щают несколько ролей: • Персональные компьютеры: эти компьютеры предназна- чены для домашнего использования одним человеком (од-
нопользовательский режим). Они достаточно компактны и могут полностью поместиться на столе, что стало насто- ящим прорывом в то время, когда они только появились. Современные персональные компьютеры достаточно мощные, и их можно использовать для многих задач, ко- торые раньше выполняли более специализированные компьютеры. • Компьютерные системы пакетной обработки: большин- ство компьютеров интерактивны — пользователь вводит команду и сразу получает отклик. Однако раньше некото- рые устаревшие компьютерные системы были разработа- ны для обработки больших наборов инструкций в неин- терактивном режиме — то есть большие объемы работы планировались к выполнению без возможности последу- ющего ввода данных пользователем в процессе выполне- ния. Это позволяло компьютерам использовать свои ре- сурсы более эффективно. Некоторые крупные компании до сих пор используют та- кие устаревшие компьютерные системы для выполнения повторяющихся задач, как, напимер, начисление зарпла- ты или ведение бухгалтерского учета. Большинство со- временных интерактивных компьютерных систем мож- но использовать и для выполнения неинтерактивных пакетных операций, т. е. они поддерживают процесс па- кетной обработки. Запуск выполнения таких операций часто планируется производить в нерабочее время, что- бы не конкурировать с пользователями за ресурсы. • Компьютерные системы с разделением времени: эти ком- пьютерные системы были усовершенствованы по сравне- нию с системами пакетной обработки данных и позво- лили нескольким пользователям получать удаленный доступ к одному и тому же центральному компьютеру
в одно и то же время. Центральный компьютер обычно располагался в кондиционируемом помещении, которое физически находилось далеко от пользователей. Поль- зователи подключались к центральному компьютеру через терминалы, которые не обладали большой вычис- лительной мощностью — обычно у них в наличии были только монитор и клавиатура. В отличие от компьютера с пакетной обработкой дан- ных, компьютер с разделенимем времени мог переклю- чаться между состояниями программ разных пользовате- лей, опрашивая разные терминалы на предмет наличия нового ввода от конкретного пользователя. По мере ро- ста скорости компьютеров это переключение происходи- ло настолько быстро, что создавалась иллюзия, будто все пользователи работают одновременно. Сегодня несколько пользователей могут подключаться к центральному компьютеру с помощью обычной ком- пьютерной сети. В роли центрального компьютера может выступать обычный персональный компьютер (хотя за- частую он имеет гораздо более совершенное аппаратное обеспечение), который выполняет специализированную роль. Большинство современных компьютеров способны переключаться между несколькими запущенными про- граммами достаточно быстро, чтобы создать видимость их одновременной работы. Роль терминала обычно выполня- ет обычный персональный компьютер пользователя. Существуют также мощные суперкомпьютеры, специали- зированное оборудование которых позволяет значитель- но превосходить вычислительную мощность любого пер- сонального компьютера. Пользователи получают доступ к таким компьютерам, когда им нужно решить задачу,
требующую использования большого количества вычис- лительных ресурсов. • Компьютерные сети: это несколько компьютеров, соеди- ненных между собой цифровыми либо аналоговыми ка- белями или с помощью беспроводной связи, которые спо- собны взаимодействовать друг с другом. Сегодня почти все компьютеры могут быть подключены к сети. В боль- шинстве сетей есть специализированные компьютеры, называемые серверами, которые предоставляют услуги другим компьютерам в сети (клиентам). Например, сер- вер хранения данных, скорее всего, содержит много бы- стрых дисковых накопителей большой емкости для хра- нения данных и резервного копирования. Сервер печати может быть оптимизирован для управления заданиями печати. Использование серверов позволяет сократить расходы, давая пользователям возможность эффективно совместно использовать ресурсы. Интернет — это очень большая международная компью- терная сеть. Многие компьютеры в Интернете являются серверами. Когда вы используете веб-браузер, то посыла- ете запросы на веб-серверы, которые в ответ отправляют вам веб-страницы. История компьютеров Современные компьютеры — электронные. Ранее компью- теры были в основном механическими и электромеханиче- скими. Со временем развитие компьютеров происходило в ге- ометрической прогрессии — от дорогих машин, занимавших целые комнаты, до современных доступных и компактных устройств.
Самое древнее механическое счетное устройство — абак (счеты). Он был изобретен в Вавилоне более 3000 лет назад и также использовался китайцами. С его помощью можно вы- полнять сложение, вычитание, умножение и деление. Позже, в 1642 году, Блез Паскаль изобрел первый механический каль- кулятор. Машина Паскаля могла выполнять операции сложе- ния и вычитания. В 1671 году Готфрид фон Лейбниц расширил возможности машины Паскаля, добавив в нее умножение, де- ление и вы числение квадратных корней. В1801 году Жозеф-Мари Жаккард изобрел ткацкий станок, ко- торый управлялся с помощью перфокарт. Он мог ткать ткань в соответствии с отверстиями на перфокартах. Это был первый станок, который можно было перепрограммировать. В середине 1800-х годов Чарльз Бэббидж разработал раз- ностную машину, которая должна была вычислять и печа- тать математические таблицы. Однако работа над ней так и не была завершена, поскольку он увлекся созданием своей аналитической машины. Она была спроектирована для вы- полнения инструкций программы и, таким образом, могла выполнять любые вычисления. В машине Бэббиджа также предполагалось использовать перфокарты, но, к сожалению, английское правительство прекратило финансирование проекта, и машина так и не была завершена. Сомнительно, что эта машина могла бы работать, поскольку для ее создания требовались навыки металлообработки, превосходящие воз- можности того времени. Ада Лавлейс помогала Бэббиджу в некоторых его работах. В 1842 году она перевела одну из работ Бэббиджа об аналити- ческой машине с французского на английский и на полях на- писала примеры того, как использовать машину, став, по сути, первым в истории программистом.
Американец Герман Холлерит изобрел метод использования перфокарт для автоматизированной обработки данных. Его ма- шины использовались правительством США для составления таблиц переписи населения в 1890 году. Позднее фирма Холле- рита объединилась с тремя другими и образовала компанию International Business Machines (IBM). Система перфокарт про- должала использоваться вплоть до 1960-х годов. В 1944 году Говард Эйкен и его команда завершили работу над компьютером Mark I — первым в мире автоматическим компьютером. Он работал с помощью электромеханических пе- реключателей и мог перемножать два числа за шесть секунд. В 1946 году Джон В. Маучли и Дж. Преспер Экерт разработали первый электронный компьютер общего назначения под назва- нием ENIAC ((E)lectronic (N)umerical (I)ntegrator (A)nd (C)omputer). Он был в сотни раз быстрее любых электромеханических вы- числительных устройств и мог быть запрограммирован путем подключения проводов к отверстиям на его внешней стороне. Начиная с 1950-х годов компьютерные системы стали до- ступны коммерчески. С тех пор их принято разделять по поко- лениям. Компьютеры первого поколения (1950-е годы) Компания Sperry-Rand представила коммерческий элек- тронный компьютер UNIVAC I. Другие компании вскоре по- следовали ее примеру, но по сегодняшним стандартам эти компьютеры были громоздкими и ненадежными. В качестве электронных переключателей в них использовались вакуум- ные лампы, которые сильно нагревались и часто перегорали. Большинство программ было написано на машинном языке, а для хранения данных использовались перфокарты.
Компьютеры второго поколения (конец 1950-х — середина 1960-х годов) В конце 1950-х годов были созданы компьютеры второго по- коления, в которых вместо вакуумных ламп использовались транзисторы, что сделало их более надежными. Примерно в это же время были разработаны языки программирования высо- кого уровня, такие как FORTRAN, Algol и COBOL, и многие про- граммисты стали использовать их вместо ассемблера или ма- шинных языков. Это сделало программы более независимыми от конкретных компьютерных систем. Кроме того, произво- дители увеличили объем оперативной памяти, а также ввели магнитные ленты для долговременного хранения данных. Компьютеры третьего поколения (середина 1960-х — начало 1970-х годов) В 1964 году компания IBM представила линейку компью- теров System/360 — каждая машина этой линейки могла вы- полнять одни и те же программы, но с разной скоростью. В компьютерах этого поколения стали использоваться инте- гральные схемы с большим количеством транзисторов. Боль- шинство из них работали в режиме пакетной обработки, а не- которые — в режиме разделения времени. Компьютеры четвертого поколения (начало 1970-х годов и далее) С начала 1970-х годов разработчики компьютеров сосредо- точились на создании все более миниатюрных компьютерных компонентов. Сегодня компьютеры собираются с использова- нием интегральных схем очень большой степени интеграции (VLSI), каждая из которых содержит миллионы транзисторов
на одном чипе. Этот процесс привел к созданию более деше- вых и надежных компьютеров. В конце 1960-х — начале 1970-х годов компьютеры начали использовать средние организа- ции, включая колледжи и университеты. К началу 1980-х го- дов их примеру последовали малые предприятия. В тот период большинство из них были системами с разделением времени. Сегодняшние компьютеры, как правило, являются однополь- зовательскими или многопользовательскими персональными компьютерами, многие из которых подключены к более круп- ным сетям, таким как Интернет. Программирование компьютера Алгоритмы Алгоритм — это ряд шагов, которые необходимо выполнить для решения определенной задачи или завершения какого-ли- бо действия. Возможно, вы уже использовали алгоритм — на- пример, собирая модель игрушки по инструкции или готовя блюдо по рецепту. Шаги в инструкции или рецепте представля- ют своего рода алгоритм. Они объясняют вам. как превратить составные компоненты в готовую игрушку или ингредиенты в печенье. В этом случае ингредиенты выступают в роли вход- ных данных алгоритма, а сам алгоритм преобразует их в конеч- ный результат— выходные данные. Рецепты как алгоритмы (структурированный подход к программированию) Типичный рецепт содержит список ингредиентов и пере- чень действий для приготовления блюда. Обычно в рецептах не указывается, кто именно будет выполнять действия, но под-
разумевается, что это делает повар — то есть вы. Многие ал- горитмы записываются аналогичным образом, предполагая, что исполнитель указанных инструкций — это центральный процессор компьютера (CPU). Такой подход называют структу- рированным подходом к программированию, и он достаточ- но хорошо работает для простых программ. Однако он может усложняться, когда вы пытаетесь решить реальную задачу, где редко только один исполнитель контролирует весь про- цесс. Объектно-ориентированный подход к программирова- нию — это попытка смоделировать реальный мир, включив в алгоритм сразу несколько исполнителей. Сценарии для пьес как алгоритмы (объектно-ориенти- рованный подход к программированию) Сценарий театральной пьесы — хорошая аналогия для по- яснения объектно-ориентированного (ОО) подхода. В начале сценария указывается список актеров и сцен (подобно списку ингредиентов в рецепте). В каждой сцене сценарий содержит инструкции для каждого актера: что говорить и как действо- вать. Каждый актер волен интерпретировать эти инструкции (при условии одобрения режиссером) так, как считает нуж- ным. При этом каждый актер волен интерпретировать свои реплики и действия (в пределах, одобренных режиссером) по-своему. В объектно-ориентированном программировании несколько объектов взаимодействуют для достижения общей цели. Алгоритм, подобно сценарию, указывает каждому объек- ту, что и в какой последовательности нужно выполнять. Языки программирования Чтобы использовать алгоритм в компьютере, мы должны сначала преобразовать его в программу. Это делается с по- мощью языка программирования (очень формальный язык
со строгими правилами оформления и грамматики), кото- рый компьютер способен однозначно преобразовать в ком- пьютерные команды или машинный код — инструкции, понятные компьютеру. В рамках этого курса мы будем ис- пользовать язык программирования Python, но существует множество других языков, о которых мы расскажем далее в этой главе. Причина, по которой мы не пишем программы сразу на ма- шинном языке, заключается в том, что такие инструкции слож- ны для восприятия и понимания человеком. Например, вот как выглядят машинные инструкции для процессора Intel 8086 (одного из вариантов архитектуры Pentium), необходимые для сложения чисел 17 и 20: 1011 0800 0001 0001 0000 0100 0001 0100 1010 0010 0100 1000 0000 0000 Первая строка указывает компьютеру скопировать число 17 в регистр AL: первые четыре бита (1011) означают команду ко- пирования данных в регистр, следующие четыре бита (0000) указывают, что компьютер должен использовать регистр с именем AL, а последние восемь бит (0001 0001, что соответ- ствует 17 в двоичной системе) задают копируемое значение. Как видите, писать программы на машинном языке доволь- но сложно. В 1940-е годы программистам первых компьютеров приходилось так делать, потому что других вариантов попро- сту не существовало. Чтобы упростить процесс программиро- вания, был создан язык ассемблера. Каждой инструкции ассемблера соответствует одна машин- ная команда, но ассемблер легче понимается человеком. При- мером может служить эквивалентная программа сложения на языке ассемблера для процессора 8086:
MOV AL, 17D ADD AL, 20D MOV [SUM], AL Однако программы на языке ассемблера не могут быть на- прямую выполнены компьютером, поэтому требуется этап трансляции. Для этого используется ассемблер — программа, которая переводит инструкции ассемблера в машинный код. Несмотря на то, что язык ассемблера стал шагом вперед по сравнению с машинным кодом, он все еще остается доста- точно трудоемким и низкоуровневым: даже простейшие опе- рации требуют множества инструкций. Чтобы еще больше упростить разработку программ, были созданы языки высоко- го уровня. В языках высокого уровня одной инструкции может соответ- ствовать несколько инструкций машинного языка, что делает программы более читаемыми и удобными для написания. Вот пример на на языке Python, который эквивалентен приведен- ному выше коду: sum =17+20 Компиляторы, интерпретаторы и язык программирования Python Программы, написанные на языках высокого уровня, так- же должны быть преобразованы в машинный код, прежде чем компьютер сможет их выполнить. Некоторые языки про- граммирования компилируются, т. е. переводят сразу весь ис- ходный код программы в исполняемый файл. Другие языки интерпретируют каждую строку программы, т. е. переводят и выполняют программы построчно. Такие языки называют- ся соответственно компилируемыми и интерпретируемыми. Python — это интерпретируемый язык.
Компилируемый язык поставляется с компилятором — программой, которая компилирует исходные файлы в испол- няемые бинарные файлы. Интерпретируемый язык поставля- ется с интерпретатором, который выполняет исходный код напрямую. Интерпретация, как правило, менее эффективна по производительности, поэтому за интерпретируемыми язы- ками закрепилась репутация медленных. Программы, требующие высокой производительности и эф- фективного использования ресурсов, часто пишут на языке С. Это компилируемый язык, который во многих аспектах бли- же к низкоуровневому программированию, чем Python. На- пример, программист на С должен явно управлять памятью, в то время как в Python это происходит автоматически. Такой тонкий контроль позволяет значительно оптимизи- ровать производительность, но может отнимать много вре- мени и приводить к ошибкам. Для задач, не требующих мак- симальной эффективности, Python позволяет разрабатывать программы быстрее и проще, а разница в скорости на совре- менных компьютерах зачастую незначительна. Разработка программы на Python Предположим, мы хотим разработать программу, вычисля- ющую среднее значение последовательности чисел. Первый шаг — понять суть задачи, но сначала уточним детали: • Как программа будет получать числа? Допустим, что пользователь будет вводить их с клавиатуры. • Как программа узнает, сколько чисел нужно ввести? Предположим, пользователь будет вводить 0 в качестве признака окончания списка. • Где отображать результат? На экране (в консоли). Для создания алгоритма можно попробовать решить задачу вручную и записать последовательность шагов:
1. Установить накопительную сумму равной 0. 2. Установить счетчик введенных чисел равным 0. 3. Повторять следующие действия: • Принять введенное пользователем значение. • Проверить, равно ли это значение 0 (если да, то остано- вить цикл и выйти из него). • Прибавить значение к накопительной сумме. • Увеличить счетчик на 1. 4. Вычислить среднее значение: разделить сумму на коли- чество чисел. 5. Вывести результат на экран. Следующий этап разработки — тестирование алгоритма. Для этого можно опробовать алгоритм на различных набо- рах чисел и посмотреть, удастся ли получить правильный ре- зультат. Для простых алгоритмов, таких как этот, вы можете провести тест, проведя вычисления на бумаге или с помощью калькулятора. Попробуйте сделать это с простым набором чи- сел, например 1,2, 3,4 и 5. Затем протестируйте на более слож- ных вариантах. У вас может сложиться впечатление, что этот алгоритм работает во всех случаях. Однако стоит проверить и пограничные случаи— например, набор чисел, содержащий только 0. Среднее значение списка с единственным 0 равно 0. Алгоритм должен корректно обрабатывать такую ситуацию (например, не делить на 0). Подумайте, как можно изменить алгоритм для этого. Теперь мы рассмотрим более подробно четыре этапа разра- ботки программы. Понимание проблемы Прежде чем приступить к написанию программы, необхо- димо досконально разобраться в задаче, которую вы пытаетесь решить:
• Начиная работу, подумайте над проблемой самостоятель- но, не прикасаясь к клавиатуре. Определите, что именно должен делать алгоритм. • Если программа создается для других пользователей, воз- можно, будет полезно поговорить с теми, кто будет ис- пользовать программу, чтобы выяснить, что именно нуж- но. Может оказаться, что не хватает некоторых данных или существуют такие ограничения, как скорость, надеж- ность или простота использования. • Определите, какие типы входных и выходных данных по- требуются программе, способы их ввода и отображения. • Полезно составить план программы на бумаге, записав список требований или набросав несколько схем алгорит- ма. Это поможет вам собраться с мыслями и заметить воз- можные ошибки или недостающую информацию. Создание алгоритма Ранее мы описывали алгоритм как последовательность ша- гов, но есть несколько более формальных требований. Алго- ритм должен удовлетворять следующим условиям: • Последовательность шагов должна быть конечной: бес- конечный список шагов — это не алгоритм. Компьютер имеет ограниченные ресурсы (например память) и не мо- жет обрабатывать бесконечные последовательности ша- гов. • Он должен быть однозначным: каждый шаг должен представлять собой четко определенную инструкцию. На- пример, у вас не может быть шага, который говорит: «Ум- ножьте х либо на 1, либо на -1». Если у вас есть подобная инструкция, вы должны указать, как именно должен быть
сделан выбор — например, «если у меньше 0, то необхо- димо умножить х на 1, иначе умножить х на -1». • Он должен быть эффективным: алгоритм должен делать именно то, что от него требуется. • Он должен завершиться: алгоритм не должен выпол- няться бесконечно. Обратите внимание, что это отлича- ется от требования, чтобы он был конечным. Рассмотрим следующий конечный список инструкций: 1. Установите х равным 1. 2. Установите у равным 2. 3. Повторяйте следующие шаги: #. Сложить х и у, чтобы получить z #. Вывести z на экран. 4. Вывести на экран сообщение «Конец работы». Если вы попытаетесь следовать этим инструкциям, то на- всегда застрянете на шаге 3 — поэтому данный набор инструк- ций не является алгоритмом. Написание программы После разработки алгоритма переходите к его реализации на языке Python. Некоторые шаги можно перевести напрямую в инструкции языка, другие — адаптировать под синтаксис Python. Со временем вы научитесь писать код более лаконич- но и эффективно, используя возможности языка. Это будет ста- новиться тем проще, чем больше опыта вы приобретете в ра- боте на языке Python. Чтобы избежать ошибок, нужно продумывать свою програм- му по мере ее написания, раздел за разделом. Это называется ручной проверкой. В последующих главах мы также обсудим другие инструменты для проверки кода, например програм- мы, которые автоматически проверяют код на наличие опре- деленных видов ошибок.
Тестирование программы После тщательной проверки программы в процессе написа- ния вы должны запустить ее и протестировать с нескольки- ми различными входными данными. Позже в этом разделе мы рассмотрим, как писать автоматические тесты — неболь- шие программы, которые проверяют корректность работы ос- новной программы. Автоматические тесты позволяют легко запускать одни и те же тесты несколько раз, чтобы убедиться, что ваша программа по-прежнему работает правильно. Языки программирования Существуют сотни языков программирования. Некоторые предназначены для специфических задач, другие — универ- сальны. Языки можно разделить на четыре основные груп- пы — процедурные, функциональные, логические и объек- тно-ориентированные. Некоторые языки содержат черты, характерные более чем для одной группы. Процедурные языки Процедурные (или императивные) языки — наиболее мно- гочисленная группа. Программа представляет собой последо- вательность инструкций, выполняемых одна за другой. Части программы взаимодействуют через переменные — это имено- ванные участки оперативной памяти, значения которых мо- гут изменяться в процессе выполнения программы. В неко- торых других группах языков программирования (например, в логических языках) переменные действуют скорее как пере- менные, используемые в математике, и их значения не могут быть изменены.
BASIC — пример процедурного языка программирования. У него очень простой синтаксис (изначально было всего 14 ти- пов операторов). Вот код BASIC для сложения чисел 17 и 20: 10 REM ЭТА БАЗОВАЯ ПРОГРАММА ВЫЧИСЛЯЕТ СУММУ 20 REM 17 И 26, А ЗАТЕМ ВЫВОДИТ РЕЗУЛЬТАТ НА ЭКРАН. 30 LET RESULT =17+20 40 PRINT "Сумма 17 и 20 равна ", RESULT 50 END В начале 1990-х годов компания Microsoft расширила язык BASIC, выпустив Visual Basic — систему разработки для Microsoft Windows. COBOL (COinmon Business Oriented Language) широко исполь- зуется в бизнесе. Он был разработан для удобной работы с дан- ными. Ниже приведен пример программы сложения, написан- ный на языке COBOL: IDENTIFICATION DIVISION. PROGRAM-ID. ADDING. DATE-WRITTEN. JUL 11,1997. DATE-COMPILED. JUL 12,1997. * THIS COBOL PROGRAM CALCULATE THE SUM * OF 17 AND 20. IT THEN DISPLAYS THE RESULT. ENVIRONMENT DIVISION. CONFIGURATION SECTION. SOURCE-COMPUTER. IBM-370. OBJECT-COMPUTER. IBM-370. DATA DIVISION. WORKING-STORAGE SECTION. 77 TOTAL PICTURE 99. PROCEDURE DIVISION. ADD 17, 20 GIVING TOTAL. DISPLAY 'СУММА 17 И 20 РАВНА ' , TOTAL UPON CONSOLE.
STOP RUN. END PROGRAM FORTRAN (FORmula TRANslator) был популярен среди уче- ных и инженеров, но со временем многие из них перешли на С и C++. Это был первый язык высокого уровня. Вот программа сложения, написанная на FORTRAN: PROGRAM ADDNUMS С ЭТА ПРОГРАММА НА ЯЗЫКЕ ФОРТРАН НАХОДИТ ОБЩЕЕ КО- ЛИЧЕСТВО С 17 И 20, ЗАТЕМ ВЫВЕДИТЕ РЕЗУЛЬТАТ НА ЭКРАН. INTEGER RESULT RESULT =17+20 WRITE (*, 10) RESULT 10 FORMAT ('СУММА 17 И 20 РАВНА ',12) STOP END Многие программисты используют С для написания кода системного уровня (операционные системы, компиляторы). Вот код программы добавления, написанный на языке С: /* Эта программа на языке С вычисляет сумму чисел 17 и 20. Затем она выводит полученный результат на экран. ★/ #include <stdio.h> void main(void) { int result; результат = 17 + 20; printf("Сумма 17 и 20 равна 70d\n", result); }
Паскаль (названный в честь Блеза Паскаля) раньше широко использовался для обучения программированию, но к началу 1990-х годов многие школы перешли на C++ или Java. Вот про- грамма сложения на Pascal: PROGRAM AddNums (Input, Output); { Эта программа на языке Pascal вычисляет сумму 17 и 20. Затем она выводит результат на экран } BEGIN Result := 17 + 20; writeLn ('Сумма 17 и 20 равна ', Result) END Функциональные и логические языки Функциональный язык основан на математических функ- циях. Обычно он состоит из определений функций и их вы- зовов. В чистом виде переменных в языке не существует: вместо этого части программы общаются между собой с помо- щью параметров функций. Один из самых популярных функ- циональных языков — Common Lisp, широко применяемый в области искусственного интеллекта, особенно в США. Так- же к функциональным языкам относятся ML и Miranda. Вот пример программы сложения в функциональном стиле на Common Lisp: ;; Эта Lisp-программа вычисляет ;; сумма 20 и 17. Затем выводит результат. (format t "Сумма 20 и 17 равна ~D~%" (+ 20 17))
Логический язык основан на формальных правилах логи- ки и умозаключений. Примером такого языка является Prolog. В Prolog значения переменных нельзя изменить после их опре- деления — это типично для логических языков. Вот программа сложения, написанная на языке Prolog: /* Эта программа на языке Prolog вычисляет сумму 17 и 20. /★ Затем выводит результат. */ run Result is 17 + 20, write(“Сумма 17 и 20 равна ", Result), nl. Приведенная выше программа не демонстрирует дедуктив- ную мощь языка Prolog. Программы на Prolog могут состоять из набора известных фактов, а также правил вывода новых фактов из существующих. В примере ниже в первых семи стро- ках перечислены факты о том, какой напиток нравится опре- деленным людям. Последняя строка — это правило вывода, ко- торое гласит, что Мэтт любит напиток только тогда, когда этот напиток нравится и Майку, и Мэри: /* Показывает способность Prolog делать выводы */ likes (dave, cola). likes(dave, orange_juice). likes (mary, lemonade). likes(mary, orange.juice). likes(mike, sparkling.water). likes(mike, orange.juice). likes(matt, Drink) :- likes(mary, Drink), likes(mike, Drink).
Программа на языке Prolog отвечает на запрос. Папртгмер, чтобы узнать, какие напитки нравятся Мэри, можно сделать за- прос: likes(mary, Drink). И Prolog выдаст возможные ответы: Drink = lemonade Drink = orange_juice Чтобы продемонстрировать правило умозаключения, мы можем спросить, какой напиток любит Мэтт. Система мо- жет найти решение, проверив, что нравится Мэри и Майку: likes(matt, Drink) Drink = orange.juice Объектно-ориентированные языки В последние десятилетия разработчики все активнее исполь- зуют новый подход к программированию — объектно-ориенти- рованное программирование (ООП). В этом подходе каждая сущ- ность моделируется как объект, обладающий своими данными и поведением. Это делает объект активной сущностью, в то вре- мя как переменная в процедурном языке пассивна. Язык Simula (Simulation Language), созданный в 1967 году, был первым языком, в котором был применен объектно- ориентированный подход. Однако именно язык Smalltalk (на- чало 1980-х) был первым чисто объектно-ориентированным языком — все в нем является объектом. C++ гибридный язык, сочетающий черты С и ООП. Программа на C++ может быть как процедурной, так и объектно-ориентированной, или ком-
бинированной. В большинстве ОО-языков переменные ис- пользуются так же, как и в процедурных языках. Пример программы сложения на C++ (обратите внимание на сходство с программой, написанной на языке С): // Эта программа на C++ находит результат сложения // чисел 17 и 20. // Затем выводит результат на экран. #include <iostream> int main(void) { int result = 17 + 20; std::cout<< "Сумма 17 и 20 равна "<< result<< std: :endl; return 0; } Язык Java был представлен в 1995 году компанией Sun Microsystems, которая в 2009-2010 годах была куплена корпора- цией Oracle. Java — объектно-ориентированный язык, но не та- кой «чистый», как Smalltalk. Например, в Java примитивные значения (числа и символы) не являются объектами — это просто значения, в то время как в Smalltalk все является объек- тами. Изначально язык разрабатывался для бытовой электро- ники, но первая выпущенная версия предназначалась для Ин- тернета. Синтаксис Java очень похож на С и C++, но функции не могут быть определены вне классов. Вот пример програм- мы сложения двух чисел, написанный на языке Java: // Эта Java-программа вычисляет сумму чисел // 17 и 20. Затем выводит результат. public class Add {
public static void main(Stnng[ ] args) { int result; result =17+20; System.out.println("сумма 17 и 20 равна " + result); } } Python — это интерпретируемый язык общего назначения, который был создан в конце 1980-х годов, но получил широ- кое распространение только в 2000-е годы после выхода версии 2.0, Он известен своим понятным, простым синтаксисом и ди- намической типизацией — одни и те же переменные в Python можно повторно использовать для хранения значений разных типов, что невозможно в статически типизированных языках, таких как С или Java. В Python все является объектом, но его можно использовать для написания кода в различных сти- лях — процедурном, объектно ориентированном и даже функ- циональном. Вот программа сложения двух чисел, написан- ная на языке Python: # Эта программа на Python складывает 17 и 20 и выводит # результат на экран. result =17+20 print("Сумма 17 и 20 равна %d." % result)
Основы Python Введение В этой главе мы познакомимся с основами языка програм- мирования Python. К этому моменту вы уже должны были на- строить среду разработки и окружение для написания и вы- полнения кода на языке Python. Далее в тексте предполагается, что это так. В ходе изучения материала настоятельно реко- мендуется практиковаться, реализуя изученные концепции в виде реальных программ. Программированию невозможно научиться, просто читая — это навык, приобретаемый исклю- чительно на практике. Каждая глава содержит несколько упражнений, которые по- могут вам попрактиковаться для закрепления материала. От- веты к ним приведены в конце главы. Сравнение Python 2 и Python 3 Недавно Python претерпел серьезное обновление: переход от версии 2 к версии 3. Поскольку Python 3 к настоящему вре- мени приобрел наибольшую популярность, в дальнейшем из- ложении мы будем использовать Python 3. Версия Python 2 все еще широко применяется, и, несмотря на то, что Python 3 об- ратно несовместим с Python 2, различия между ними не кри- тичны — если вам встретится код на Python 2, он покажется вполне знакомым. На протяжении всей книги мы будем упо- минать важные различия между двумя версиями. При этом,
говоря о Python 2, мы будем иметь в виду его последнюю ста- бильную версию — 2.7 (актуальную на момент написания). Начало работы с Python Использование интерактивного интерпретатора Если ввести команду python в командной строке без пара- метров, запустится интерпретатор Python. Это текстовая кон- соль, в которой вы можете вводить команды Python построчно одну за другой — они будут сразу же выполняться. Примечание В этой книге мы предполагаем, что команда python запускает Python 3, но если на =ашем компьютере установлены обе версии - и Python 2, и Python 3 - возможно, вам придется использовать команду python3, чтобы явно запустить нужную версию При запуске интерпретатора на экране по- является информация, включающая номер версии - убедитесь, что он начи- нается с цифры 3! Запомните, какую именно команду вы используете для за- пуска. Пример приглашения интерактивного интерпретатора: Python 3.2.3 (default, Oct 19 2012, 20:10:41) [GCC 4.6.3| на linux2 Type "help", "copyright", "credits" or "license" for more information. >>> Если вы введете в интерпретатор число, строку или любую переменную, их значение автоматически отобразится на эк- ране: »> "привет' 'привет'
»> з 3 Это означает, что при использовании интерпретатора вам не нужно использовать команду print для вывода значения переменной — достаточно просто ввести ее имя: »> х = 2 >» х 2 Однако такой подход не работает при запуске программы из файла — если вы запишете эти две строки в файл и запусти- те его как программу, вывод не появится. Чтобы вывести зна- чение х, придется использовать функцию print: х = 2 print(х) В большинстве примеров кода в этой книге использует- ся явный вызов print, чтобы вы получали одинаковый ре- зультат независимо от способа запуска — из интерпретатора или из файла. Интерактивный интерпретатор удобен для быстрого тести- рования небольших фрагментов кода, изучения работы функ- ций или проверки синтаксиса. Существуют и другие интерактивные оболочки для Python, ко- торые обладают более широкими возможностями, чем встроен- ный интерпретатор, например, такими, как просмотр содержимо- го объектов или документации к модулям, классам и функциям: • IPython, который изначально был разработан для науч- ных вычислений. • bpython — современный проект с поддержкой автодопол- нения и подсветки.
Запуск программ из файлов Интерпретатор удобен для отладки небольших фрагментов кода, но для сохранения программы необходимо записать ее в файл. Расширение файлов с программами на Python — . ру. После сохранения вы можете запустить файл на выполнение командой python с именем файла в качестве параметра: python myprogram.py Эта команда заставит Python выполнить программу. Файл с исходным кодом — это обычный текстовый файл. Вы можете редактировать его в любом понравившемся вам тек- стовом редакторе. Желательно использовать редактор с под- держкой подсветки синтаксиса (разное цветовое оформление элементов кода в зависимости от их роли), а также функция- ми автоматического отступа и группового изменения отступов (программа сама определяет нужный уровень отступа, когда вы начинаете вводить новую строку). Некоторые программисты предпочитают использовать ин- тегрированные среды разработки, или IDE. IDE — это програм- ма, которая сочетает в себе текстовый редактор с дополни- тельными функциями, такими как справка по документации, отладчик, компилятор (в случае компилируемого языка), за- пуск программ и прочее. Некоторые IDE поддерживают не- сколько языков, а некоторые предназначены для конкретно- го языка. Существует множество бесплатных и коммерческих IDE, ко- торые можно использовать с Python. Python также поставляет- ся с простой встроенной IDE под названием IDLE (иногда требу- ется установить ее из отдельного пакета).
Установка новых пакетов Способ установки новых пакетов Python зависит от вашей операционной системы. Дистрибутивы Linux имеют свои соб- ственные менеджеры пакетов, и вы можете установить паке- ты с помощью этих менеджеров, чтобы они были интегриро- ваны с другими пакетами в вашей системе. Однако некоторые малоизвестные пакеты Python могут быть недоступны в каче- стве системных пакетов, а те пакеты, которые доступны, часто могут быть устаревшими. Поэтому иногда приходится уста- навливать их напрямую из Python Package Index (PyPI). Python Package Index (PyPI) — это крупное хранилище сторонних Python-библиотек. Вы можете установить паке- ты из этого репозитория с помощью таких инструментов, как easy_install или pip (последний является более со- временным и предпочтительным). Обе эти утилиты являют- ся кроссплатформенными. Вот как можно установить пакет sqlob j ect с помощью pip: pip install sqlobject Эта команда найдет пакет sqlobject на PyPI, загрузит и установит его на вашу систему. Дополнительные материалы В курсе, изложенном в этой книге, мы познакомимся со мно- гими встроенными функциями, типами и модулями из стан- дартной библиотеки Python. Однако эти материалы являют- ся лишь обзором и не охватывают всех возможностей языка. При выполнении упражнений в этом модуле в качестве спра- вочника следует использовать официальную документацию по Python, которую можно найти по адресу (http://docs.python. org/3.3/index.html).
Каждый модуль стандартной библиотеки имеет в доку- ментации раздел, описывающий его интерфейс прикладного программирования, или API, — функциональность, доступ- ную вам при использовании модуля в коде. Ознакомившись с API, вы сможете узнать, какие функции предоставляет мо- дуль, какие входные данные он требует, какие выходные дан- ные возвращает и так далее. Документация часто включает полезные примеры, показывающие, как следует использо- вать модуль. Документация доступна онлайн, но вы также можете уста- новить ее локально — в виде HTML-файлов или с помощью утилиты pydoc, которая выводит справочную информацию прямо в терминал: pydoc re Основные элементы программы на Python В большинстве современных письменных языков сло- ва сами по себе не имеют смысла, если они не расположены в определенном порядке и не окружены правильными знака- ми препинания. Так же обстоит дело и с языком программи- рования Python. Интерпретатор Python способен интерпре- тировать и запускать лишь правильно структурированные программы. Пример корректной программы: prlnt("Hello, world!") Во многих языках программирования даже такая простая программа требует значительного объема кода, но в Python до- статочно этой единственной строки, которая выводит на экран короткое сообщение. Однако это не очень информативный
пример синтаксиса Python — вот чуть более сложная програм- ма, которая делает (почти) то же самое: # Здесь находится основная функция. def my^function(): print("Hello, World!") my^function() Такого рода программу часто называют программой-карка- сом, потому что на ее основе можно создать более сложную программу. Примечание Первая строка программы - это комментарий. Знак решетки (#) обозна- чает начало комментария Интерпретатор будет игнорировать все, что сле- дует после # и до конца строки Комментарии будут рассмотрены далее в этой главе Ключевые слова В приведенном выше коде слова def и if являются клю- чевыми, или зарезервированными, словами. Это значит, что они зарезервированы для использования в самом языке и не могут использоваться для других целей, например, в ка- честве имен переменных. Ниже приведен список ключевых слов Python: False class finally is return None continue for lambda try Т rue def f rom nonlocal while
and del global not whith as elif if or yield assert else import pass break except. in raise Идентификаторы Когда мы пишем программу на языке Python, то создаем множество сущностей — переменные, которые хранят значе- ния, например, числа или строки, а также функции и классы. Каждая из них должна иметь уникальное имя — идентифи- катор. Например, в нашем коде-каркасе, приведенном выше, имя функции — my .function. Это конкретное имя не име- ет особого значения — мы могли бы назвать функцию main или print_hello_world. Важно то, что мы должны использо- вать это же имя для обращения к функции при ее вызове даль- ше в тексте программы. В Python есть несколько правил, которые необходимо соблю- дать при создании идентификатора: • разрешены только буквы, цифры и символ подчеркива- ния _ (пробелы запрещены!). • имя не может начинаться с цифры. • имя не должно совпадать с ключевым словом. Если мы нарушим любое из этих правил, наша програм- ма завершится с ошибкой синтаксиса (SyntaxError). Однако не все идентификаторы с правильным синтаксисом имеют смысл для человека. Есть несколько рекомендаций, которым
мы должны следовать при именовании переменных, что- бы сделать наш код более понятным (и для других людей, и для нас!) — это важная часть соблюдения хорошего стиля: • Имена должны быть понятными — переменная должна отражать свое содержимое, а функция — свое назначе- ние, и т. д. • Не используйте непонятные аббревиатуры — они могут быть двусмысленными и сложными для восприятия. Следуйте принятой схеме именования и придерживай- тесь ее: • Имена классов должны быть в формате CamelCase (вер- блюжий стиль) (слова пишутся с прописной буквы без пробела). • Имена констант должны быть написаны в формате CAPITAL_LETTERS_WITH_UNDERSCORES (все буквы про- писные, слова отделяются друг от друга знаками подчер- кивания). • Имена всех остальных переменных должны быть в нижнем регистре, отдельные слова отделяются друг от друга знаками подчеркивания. Пример: lowercase. wilh_underscores. В некоторых других языках, например, в Java, стандартом является использование названий пе- ременных в виде camelCase (со строчной начальной бук- вой), но этот стиль менее популярен в Python. • Имена атрибутов и методов класса, которые должны быть «приватными» и недоступными за пределами клас- са, должны начинаться со знака подчеркивания. Конечно, всегда есть исключения — например, многие рас- пространенные математические символы имеют очень корот- кие названия, которые, тем не менее, широко понятны.
Вот несколько примеров идентификаторов: Ошибка Плохая Хорошая синтаксиса практика практика Person Record PRcrd PersonRecord DEFAULT-HEIGHT Default.Ht DEFAULTS EIGHT class Class AlgebraCourse 2totalweight пшп2 total_weight Примечание Будьте осторожны, чтобы случайно не переопределить существующие пере- менные, повторно используя их имена Это относится не только к вашим соб- ственным переменным, но и к «строенным функциям Python, таким как 1еп, max или sort эти имена не являются ключевыми словами, и вы не полу- чите синтаксическую ошибку при их переопределении, но столкнетесь с не- ожиданными результатами, если попытаетесь использовать оригинальные функции позже в своей программе Переопределение переменных (случай- ное и намеренное) более подробно будет рассмотрено в разделе об области видимости. Упражнение 1 Объясните, почему каждая из записей в левом столбце вызовет синтаксическую ошибку, если будет использо- ваться в качестве идентификатора.
Поток управления В Python команды записываются в виде списка, как если бы человек написал список дел, которые ему нужно сделать. Компьютер исполняет их сверху вниз, в том порядке, в кото- ром они появляются в программе. Он прекращает выполнение программы только после выполнения последней инструкции. Порядок, в котором компьютер выполняет инструкции, назы- вается потоком управления. Когда исполняется конкретная инструкция — говорят, что управление находится на ней. Отступы и отсутствие точек с запятой Многие языки организуют код в блоки с помощью фигур- ных скобок ({ и }) или операторов BEGIN и END — эти язы- ки поощряют нас делать отступы в блоках с целью повыше- ния читабельности программ, при этом отступы не являются обязательными. Python использует отступы (знаки пробела или табуляцию) для группировки кода, поэтому мы должны делать отступы в коде: # это определение функции начинает новый блок def add_numbers(а, b): # эта инструкция находится внутри блока, потому # что перед ней присутствует отступ с = а + b # так же как и эта инструкция return с # эта инструкция if начинает новый блок if it_is_tuesday: # эта инструкция находится внутри блока print( 'Сегодня вторник!").
# а эта уже в блок не входит! print("Будет выведено на экран в любом случае.") Во многих языках используется специальный символ для обозначения конца каждой инструкции — обычно это точка с запятой. Python использует символ перевода строки, чтобы определить, где заканчивается инструкция (за исклю- чением некоторых особых случаев, когда последний символ в строке дает Python знать, что инструкция будет занимать не- сколько строк). По желанию мы можем использовать точки с запятой — это может понадобиться, если мы хотим разме- стить в строке более одной инструкции (но обычно это счита- ется плохим стилем): # Это все отдельные инструкции - точка с запятой не # требуется! print("Hello!") print('BoT новая инструкция") а= 2 # Эта инструкция занимает более одной строки Ь= [1. 2, 3, 4, 5, 6] # Так тоже можно, но это плохой стиль. с= 1; d= 5 Упражнение 2 Выпишите две инструкции, входящие в блок функции append_chickens:
no_chickens= "Здесь нет кур..." def append_chickens(text): text = text + ' Rawwwk!" return text print(append_chickens(no_chickens)) Упражнение 3 Исправьте отступы в этой программе: def happy.day(day): if day == "понедельник": return ":(" if day != "понедельник": return ":D" print(happy_day("воскресенье")) print(happy_day("понедельник")) Чувствительность к регистру символов В отличие от некоторых языков (например, языка Pascal), Python чувствителен к регистру. Это означает, что интерпре- татор воспринимает прописные и строчные буквы как от- личные друг от друга. Например, А отличается от a, a def main() отличается от DEF MAIN(). Также помните, что все ключевые слова, кроме Т rue, False и None, пишутся в ниж- нем регистре.
Подробнее о комментариях Напомним, что комментарии начинаются со знака # и про- должаются до конца строки, например: # Это комментарий print("Hello!") # сообщает компьютеру, что нужно па- пе ча тать "Hellо!". Комментарии игнорируются интерпретатором и должны использоваться программистом, чтобы: • помочь объяснить, что делает программа; • объяснить логику работы всей программы, а не каждой отдельной строки. Не обязательно комментировать каждую строку. Мы долж- ны комментировать в соответствующих местах, где у чело- века, пытающегося понять логику работы программы, могут возникнуть вопросы. Можно также добавить короткий ком- ментарий, описывающий, что происходит в нескольких следу- ющих инструкциях. Некоторые языки также поддерживают комментарии, со- стоящие из нескольких строк, но Python этого не делает. Если мы хотим набрать очень длинный комментарий в Python, нам нужно разбить его на несколько более коротких строк и поста- вить # в начале каждой строки. Примечание В код можно вставить многострочный строковый литерал, заключил его в тройные кавычки Обычно это не используется для комментариев, за исключением особого случая с docstrings: строк, которые исгавляются в верхнюю часть таких структур, как функции и классы, и документируют их в соответствии со стандартным форматом Аннотировать код таким обра- зом - хорошая практика, потому что автоматизированные инструменты мо-
гут затем анализировать его и автоматически генерировать документацию. Подробнее о docstrings мы поговорим в одной из следующих глав Примечание Вы можете временно отключить часть программы, закомментировав не- сколько строк Добавление или удаление множества знаков # вручную мо- жет быть утомительно В хороших редакторах кода обычно есть сочетание клавиш для быстрого комментирования или раскомментирования выделен- ного блока текста Ввод и вывод Многие программы выводят текст на экран либо для того, чтобы сообщить какую-то информацию, либо для того, чтобы попросить пользователя ее предоставить. Например, мы мо- жем просто рассказать пользователю, что делает наша про- грамма: Добро пожаловать в Вычислительную машину Джона". Возможно, нам понадобится попросить пользователя вве- сти число: Введите первую цифру: Самый простой способ вывести информацию на экран — вос- пользоваться для этого встроенной функцией print со строко- вым литералом в качестве аргумента. Строковый литерал — это текст, заключенный в кавычки. Мы можем использовать как одинарные кавычки ('), так и двойные ("") — но началь- ная и конечная кавычки должны совпадать! Вот примеры строковых литералов: "Добро пожаловать в Вычислительную машину Джона". ‘Введите первое число:'
Мы можем заставить компьютер вывести «Hello!» с помо- щью следующей инструкции: print("Hello!") Как видите, функция print принимает в качестве аргумен- та строку. Она выводит строку на экран, а также ставит в конце символ новой строки — именно поэтому курсор консоли появ- ляется на новой строке после того, как мы что-то напечатали. Чтобы запросить у пользователя информацию и считать ее из консоли, мы используем функцию input: first_number= input('Введите первое число: ') Следует отметить несколько моментов. Во-первых, в отличие от функции print, функция input не печатает символ перево- да на новую строку автоматически — ответ пользователя будет введен сразу после текста приглашения. Именно поэтому мы до- бавили пробел после двоеточия. Во-вторых, функция ввода всег- да возвращает переменную типа «строка» (st г), и поэтому нам придется самим преобразовывать ее в число. Строка приглашения в функции ввода не обязательна — мы могли бы просто использовать функцию input без пара- метра: second_number= lnput() Примечание Обратите внимание на то, что в Python 2 есть функция raw_input, которая де- лает то же самое, что и функция input в Pylhon 3. то есть считывает г вод от пользователя и возвращает его в виде значения типа «строка». В Python 2 функция input делает нечто другое’ она считывает ввод от пользователя и пытается выполнить его как команду В Python 3 такой функции нет, но вы можете достичь того же результата, используя функцию eval на строке, воз- вращаемой функцией ввода input. Использование функции eval - не самая хорошая идея, и ьам следует избегать ее использования, особенно для произ-
вольного пользовательского ввода, который вы предварительно не провери' ли в своей программе Это может быть очень опасно - пользователь может ввести абсолютно все что угодно, включая вредоносный код! Файлы Хотя по умолчанию функция print выводит текст в кон- соль, ее также можно использовать и для записи в файл. Вот простой пример: with ореп('myfile.txt', 'w') as myfile: print("Hello!", file= myfile) При выполнении этих двух строк программы происходит довольно много событий. В операторе with (который мы рас- смотрим более подробно в главе об ошибках и исключениях) файл myfile.txt открывается для записи и присваивается переменной myfile. Внутри блока with в файл записывает- ся строка Hello!, за которой следует символ перевода на но- вую строку. Символ w, переданный в функцию open, указывает на то, что файл должен быть открыт для записи. В качестве альтернативы функции print для записи дан- ных в файл мы можем использовать метод write следующим образом: with open('myfile.txt', 'w') as myfile: myfile.write("Hello!") Метод — это функция, принадлежащая объекту (в данном случае объекту «файл»). Более подробно методы рассматрива- ются в главе про классы. В отличие от метода print, метод write не добавляет ав- томатически символ перевода строки в конец записываемой в файл строки.
Чтобы считать данные из файла, откройте его для чтения (флаг 1 г ') и используйте метод read: with ореп('myfile.txt’, 'г') as myfile: data = myfile.read() Содержимое файла будет считано в переменную data. Обра- тите внимание, что на этот раз мы передали флаг ' г ' в функ- цию open. Это указывает на то, что файл должен быть открыт для чтения. Примечание Если попытаться открыть несуществующий файл в режиме г’, Python вы- зовет ошибку (FileN otFoundError). Примечание Оператор with автоматически закрывает файл в конце блока, даже если внутри блока произошла ошибка. В старых версиях Python файлы нужно было закрывать явно, используя метод . close (), но сейчас всегда следует использовать оператор with. Встроенные типы Компьютер может обрабатывать много видов информации, например числа и символы. В Python (и других языках програм- мирования) типы информации, с которыми может работать язык, называются типами. В Python встроены многие распро- страненные типы — например, целые числа, числа с плаваю- щей точкой и строки. Пользователи также могут определять свои собственные типы с помощью классов. Во многих языках существует различие между встроенны- ми типами (которые по этой причине часто называют «прими- тивными типами») и классами, но в Python они неразличимы.
В Python все является объектом (то есть экземпляром како- го-либо класса) — даже списки и функции. Тип состоит из двух частей: области возможных значений и набора возможных операций, которые можно выполнять над этими значениями. Например, область целочисленного типа (int) содержит все целые числа, а общие целочислен- ные операции — это сложение, вычитание, умножение и де- ление. Python — это язык с динамической (а не статической) типи- зацией. Это означает, что нам не нужно указывать тип пере- менной при ее создании — мы можем использовать одну и ту же переменную для хранения значений разных типов. Одна- ко Python также является сильно (а не слабо) типизированным языком — в любой момент времени переменная имеет опреде- ленный тип. Если мы попытаемся выполнить операции с пе- ременными, которые имеют несовместимые типы (например, если мы пытаемся сложить число со строкой), Python завер- шит работу с ошибкой типа вместо того, чтобы пытаться уга- дать, что мы имеем в виду. Функция type может быть использована для определения типа объекта. Например: print(type(1)) print(type("а")) Целые числа Целое число (тип int) — это целое число, такое как 1, 5, 1350 или -34. Число 1 .5 не является целым числом, потому что в нем есть десятичная точка. Числа с десятичной точкой — это числа с плавающей точкой. Даже 1.0 — это число с плава- ющей точкой, а не целое число.
Целочисленные операции Python может вывести на экран целое число с помощью функции print, но только если оно является единственным аргументом: print(3) # Мы можем сложить два числа вместе print(1 + 2) Мы не можем объединить при выводе строку и целое число напрямую, потому что Python сильно типизирован: »> print("Moe число "+ 3) Traceback (most recent call last): File "<stdin>", line 1, в <module> TypeError: Cant convert 'int' object to str implicitly Если мы хотим вывести число и строку вместе, нам придет- ся каким-то образом преобразовать число в строку: # Функция str преобразует данные в строки. # Затем мы можем соединить две строки с помощью +. print("Moe число - "+ str(3)) # Форматирование строки выполняет преобразование авто- матически. print('Мое число - %d" % 3) Другие операции с целыми числами: Операция 1 Символ Пример Результат Сложение + 28 + 10 38 Вычитание 28 - 10 18
Операция Символ Пример Результат Умножение * 28 * 10 280 Целочисленное деление // 28 // 10 2 Остаток от деления % 28 % 10 8 Возведение ** 28**10 296196766695424 в степень Обратите внимание, что все эти операции являются цело- численными. Поэтому ответ на 2 8 / / 10 — не 2.8, а 2. Цело- численная операция дает целочисленный результат. Примечание В Python 2 оператор / выполняет целочисленное деление, если делимое и делитель были целыми числами, и деление с плавающей точкой, если хотя бы одно из них было значением с плавающей точкой. В Python 3 опе- ратор / всегда выполняет деление с плавающей точкой и возвращает дроб- ное число (тип float), а оператор // всегда выполняет целочисленное де- ление — даже если делимое и делитель являются числами с плавающей точкой! Примечание Некоторые другие языки (например, С, Java) хранят каждое целое число в небольшом фиксированном объеме памяти. Это ограничивает размер це- лого числа, которое может быть сохранено Обычно это 2**8, 2**16. 2**32 и 2**64 Python не имеет фиксированного ограничения и может хранить уди- вительно большие целые числа, такие как 2 **1900000, при условии, что ком- пьютер, на котором он установлен, обладает достаточной памятью и вычис- лительной мощностью
Приоритет операторов Еще один важный момент, о котором следует помнить, — это приоритет выполнения операторов. Например, выражение 1 + 2 // 3 равнозначно какому из выражений — (1 +2) // 3 или 1 + (2 // 3)? Python имеет особый и предсказуемый способ определения порядка выполнения операций. Для операций с целыми числами система сначала обрабатывает скобки (), затем возведение в степень **, затем умножение, деление и деление с остатком (*, / / и %) и, нако- нец, операции сложения + и вычитания Если выражение содержит несколько операций, находящих- ся на одном уровне старшинства, например *, / / и %, они будут выполняться по порядку, либо слева направо (для левоассоци- ативных операторов), либо справа налево (для правоассоциа- тивных операторов). Все эти арифметические операторы яв- ляются левоассоциативными, за исключением **, который является правоассоциативным: # все арифметические операторы, кроме **, являются # левоассоциативными, поэтому 2*3/4 # вычисление производится слева направо: (2 * 3) / 4 # является правоассоциативным, поэтому 2 ** з ** 4 # вычисление производится справа налево: 2 ** (з ** 4) В следующей таблице приведено еще несколько примеров очередности выполнения операторов: Выражение Как вычисляет Python Результат 20 + 10 // 2 20 + (10 // 2) 25
Выражение Как вычисляет Python Результат 20 + 10-2 (20 + 10)-2 28 20-10 + 2 (20 -10) + 2 12 20-10*2 20-(10 *2) 0 20 //10*2 (20 //10) * 2 4 20 * 10 // 2 (20 *10) Ц2 100 20 * 10 ** 2 20 *(10** 2) 2000 Иногда в арифметических выражениях стоит использовать скобки, даже если они не обязательны, потому7 что это делает код более понятным. Упражнение 4 1. Какие из следующих чисел являются допустимыми це- лыми числами в языке Python? 110,1 . 0,17.5, -39, -2.3. 2. Каковы результаты следующих операций? Объясните, почему: #. 15 + 20 * 3 #. 13 // 2 + 3 #. 31 + 10 // 3 #. 20 % 7 // 3 # 2 ** 3 ** 2 3. Что произойдет при вычислении выражения 1 / / 0 в консоли Python? Почему это происходит?
Числа с плавающей точкой Числа с плавающей точкой (тип float) — это числа с деся- тичной точкой или экспонентой (или и тем и другим). Приме- рами являются 5.0, 10.24, 0.0, 12 и .3. Для обозначения очень больших или очень маленьких чисел с плавающей точ- кой можно использовать научную запись, например 3.8 * 1015. Первая часть числа, 3.8. называется мантиссой, а 15 — это по- казатель степени. Можно представить себе показатель степе- ни как количество раз, на которое нужно сдвинуть десятич- ную точку вправо, чтобы получить действительное значение числа. В Pyihon число 3.8 х 1015 можно записать как 3.8е15 или 3.8е+1 5. Также допустимы записи 38е14 или . 038е17 — они все представляют одно и то же значение. Отрицательная степень указывает на малые числа, например, 2.5е-3 равно 0.0025. Отрицательную степень можно представить как коли- чество раз, на которое мы должны сдвинуть десятичную точку влево. Отрицательная мантисса указывает на то, что само чис- ло является отрицательным, например, -2.5еЗ равно -2500, а -2.5е З равно - 0.0025. Функция print выводит числа с плавающей точкой в обыч- ной десятичной записи, если они находятся в диапазоне от 1 е-4 до 1 el 6. Для значений вне этого диапазона использу- ется научная запись: # На экран будет выведено: 10000000000.0 print(1e10) # На экран будет выведено: 1е+100 ргint(1е100) # На экран будет выведено: Те-10
67 print(0.0000000001) При отображении чисел с плавающей точкой (тип float) мы обычно указываем формат отображения, используя форма- тирование строки: # На экран будет выведено:12.35 print("%.2f" % 12.3456) # На экран будет выведено:1.234560е+01 print("%e" % 12.3456) Обратите внимание, что любое округление влияет только на то, в каком виде число будет выведено на экран, но не на его точность. Операции с плавающей точкой и приоритет Арифметические операции для чисел с плавающей точкой такие же, как и для целых чисел: сложение, вычитание, умно- жение, деление, возведение в степень и деление с остатком. Они также используют те же операторы, за исключением де- ления — оператором деления для чисел с плавающей точкой является /. Результатом операции с плавающей точкой всег- да является результат с плавающей точкой. Приоритет у этих операторов такой же, как у целочисленных. Обычно в программе нужно решить, какой тип числа ис- пользовать: целые числа — для подсчета и измерения дискрет- ных целых величин, числа с плавающей точкой — для непре- рывных величин. Мы можем комбинировать в арифметических выражениях целые числа и числа с плавающей точкой без необходимости их преобразовывать — это Python сделает за нас автоматиче-
ски. Если мы выполним арифметическую операцию над це- лым числом и числом с плавающей точкой, результатом всег- да будет число с плавающей точкой. Также возможно использовать оператор целочисленного деления (//) с float. Оба оператора деления (/ и //) имеют одинаковый приоритет. Примечание Числа с плавающей точкой в Python соответствуют стандарту IEEE 754. Это значит, что они хранятся в фиксированном объеме памяти и имеют ограни- ченный диапазон значений. Максимальное значение float в Python - это 2**1023. Примечание Python также поддерживает три других числовых типа • complex - комплексные числа, например. 1 + 5j; • F raction - рациональные дроби (из модуля fractions); • Decimal - десятичная арифметика с плавающей точкой (из модуля decimal). Их использование выходит за рамки данной книги, но стоит знать об их су- ществовании на случай, если они пригодятся вам позже Упражнение 5 1. Какие из следующих чисел являются числами с плава- ющей точкой в Python? 1, 1 .0, 1 .12е4, -3.141759, 735, 0.57721566, 7.5е-3. 2. В чем разница между целочисленным делением и деле- нием с плавающей точкой? Какой оператор используется для целочисленного деления? Какой оператор использу- ется для деления с плавающей точкой?
3. Каковы результаты следующих операций? Объясните, почему: #. 1.5+2 # . 1.5 // 2.0 # . 1 .5 / 2.0 # . 1.5 ** 2 #.1/2 # . -3 // 2 4. Что произойдет, когда вы запустите на выполнение вы- ражение 1 / 0 в консоли Python? 5. Что произойдет при вычислении 1 е1000? А как насчет -1 el 000? И type(1el000)? Строки Строка — это последовательность символов. Вы уже должны быть знакомы со строковыми литералами, поскольку работали с ними в предыдущем разделе. В Python строки (тип st г) — это особый тип последовательностей. Во многом строки ведут себя также, как списки (тип list), о которых мы поговорим в следу- ющей главе, но при этом обладают функциональностью, специ- фичной для работы с текстом. Во многих языках программирования существует отдель- ный тип данных для одиночных символов, но в Python одиноч- ный символ — это просто строка длиной 1. Примечание В Python 2 тип str использовал кодировку ASCII. Чтобы использовать строки с символами Unicode (например, символы других алфавитов или специаль-
ные знаки), нужно было применять тип unicode В Python 3 тип st г по умол- чанию поддерживает Unicode Форматирование строк Часто нам нужно вывести сообщение, которое не являет- ся фиксированной строкой — возможно, мы хотим включить в него числа или другие значения, которые хранятся в перемен- ных. Рекомендуемый способ включить эти переменные в сооб- щение — использовать синтаксис форматирования строк: name = "Джейн" аде = 23 print("Привет! Меня зовут %s." % name) print("Привет! Меня зовут %s и мне %d лет." % (name, аде)) Символы в строке, начинающиеся со знака процента (%), — это спецификаторы (placeholders), а переменные, которые нужно вставить в эти позиции, указываются после операто- ра форматирования строки, %, в том же порядке, в котором они появляются в строке. Если переменная только одна, то она не требует никакого спецификатора, но если их несколько, они передаются в кортеже (в круглых скобках). Символы- спецификаторы представляют собой разные буквы в зависимо- сти от типа переменной: name — это строка, а аде — целое чис- ло. Все переменные будут преобразованы в строки перед тем, как будут объединены с остальной частью текста. Экранирование Экранирующая (управляющая) последовательность (сим- волов) (от англ, escape sequence) позволяет вставлять в стро-
ку специальные символы, которые сложно ввести напрямую (нельзя легко набрать на клавиатуре) или которые зарезерви- рованы для других целей. Например, чтобы вставить символ перевода строки: print('Это одна строка.\пЭто другая строка.') Если строка заключена в одинарные кавычки, апострофы В1гутри строки нужно экранировать, и наоборот — двойные ка- вычки экранируются внутри строк в двойных кавычках. Экра- нирование осуществляется с помощью обратного слеша \: print(1 "Hi! IVm Jane," she said.'). print("\"Hi! I'm Jane,\" she said.") Если не экранировать кавычки, Python посчитает, что стро- ка закончилась раньше времени, и выдаст ошибку синтаксиса: »> print('"Hi! I'm Jane," she said.'). File "<stdin>", line 1 print(print('"Hi! I'm Jane," she said.')). Л SyntaxError: invalid syntax Некоторые распространенные экранированные последова- тельности: Последовательность Значение \\ обратный слеш V одинарная кавычка \" двойная кавычка
г Последовательность _____________________I Значение \п перевод строки \t символ табуляции Также с помощью экранированных последовательностей можно вставлять символы Unicode. «Сырые» строки Если строка содержит множество обратных слешей (напри- мер, в путях файлов или регулярных выражениях), экрани- ровать каждый из них неудобно. В таких случаях можно ис- пользовать «сырые» строки. Для этого достаточно поставить префикс г перед открывающей кавычкой — тогда все симво- лы в строке будут интерпретироваться буквально, без экрани- рования. Добавляя символ г перед открывающей строку ка- вычкой, мы указываем, что содержимое строки — это именно то, что мы написали, и что обратные косые черты не имеют специального значения. Например: # Эта строка заканчивается новой строкой "Привет! \п" # Эта строка заканчивается обратной косой чертой, за # которой следует 'п‘. г"3дравствуйте! \п" Сырые строки часто используют при передаче строк в дру- гие программы, которые сами обрабатывают езсаре-последо- вательности. В таком случае нам не надо, чтобы Python что- либо интерпретировал.
Тройные кавычки В тех случаях, когда нам нужно задать длинную строку, охватывающую несколько строк или содержащую много ка- вычек, проще всего заключить ее в тройные кавычки (' ' ' или """, но открывающая и закрывающая кавычки должны совпадать). Внутри тройных кавычек все пробельные сим- волы воспринимаются как есть — если мы выполним пере- вод строки, он будет отражен в нашей строке. Экранирование кавычек также не требуется. Следует помнить, что даже от- ступы попадут в строку! Эти строковые литералы при выводе на экран будут выгля- деть одинаково: string,one = '''"Hello," said Jane. "Hi," said Bob.''' string_two = '"Hello," said Jane.\n"Hi," said Bob.' Операции co строками Мы уже познакомились со строковой операцией конкатена- ции ( + ), которая объединяет строки. Помимо этого, в Python есть множество встроенных функций и методов, доступных для строковых объектов: name = "Джейн Смит" # Выводит на экран длину строки, определенную с помощью # встроенной функции 1еп print(len(name))
# Выводит на экран строку, преобразованную к нижнему # регистру print(name.lower()) # Выводит на экран исходную строку print(name) Почему последняя строка выводит исходное значение пере- менной name? Потому что метод lower не изменяет значение name. Он возвращает измененную копию значения, т. е. воз- вращает новую строку. Если необходимо изменить значение name, нужно переменной присвоить новое значение, как это показано здесь: # Преобразуем строку в нижний регистр name = name.lower() print(name) В Python строки неизменяемы (immutable)—то есть мы не мо- жем изменить строку после ее создания. Но мы можем присво- ить переменной новое значение строки. Упражнение 6 1. Если заданы переменные х и у, используйте формати- рование строк, чтобы вывести на экран значения х и у и их сумму Например, если х = 5, а у = 3, то должно быть выведено: 5 + 3 = 8. 2. Перепишите следующие строки, используя одинарные кавычки вместо двойных. При необходимости используй- те экранирующие последовательности: #. "Привет! Я Илай." #. "Название книги \"Благие знамениях"."
#. "Привет! Я Д\'Артаньян." 3. С помощью экранирующих последовательностей напи- шите строку, которая представляет собой буквы а, b и с, разделенные табуляцией. 4. С помощью экранирующих последовательностей за- пишите строку, содержащую следующее хайку (с при- менением символов перевода строки), заключенную в одинарные кавычки. Затем то же самое, но с использо- ванием тройных кавычек: первый холодный душ даже обезьяна, кажется, хочет небольшое пальто из соломы 5. Имея переменную name, содержащую строку, напиши- те оператор print, который выведет на экран строку, со- держащую имя и количество символов в нем. Например, если name = "Джон", то ваш оператор должен вывести: Имя Джон состоит из 4 букв. 6. Что выведет следующий код? name = Джон Смит" print(name.lower()) print(name) Почему при выводе во второй раз строка остается без из- менений?
Ответь» на упражнения Ответ на упражнение 1 Синтаксическая ошибка Причина Person Record Идентификатор содержит пробел. DEFAULT-HEIGHT Идентификатор содержит тире. class Идентификатор является ключевым сло- вом. 2totalweight Идентификатор начинается с цифры. Ответ на упражнение 2 Две инструкции, находящиеся внутри блока функции appencLchickens, это: text = text + " Rawwwkr return text Ответ на упражнение 3 Правильный код с отступами выглядит так: def happy_day(day): if day == "понедельник":
return if day != "понедельник": return " :D" print(happy_day("воскресенье")) print(happy_day("понедельник’ )) Ответ на упражнение 4 1. Допустимыми целыми числами в Python являются: 110 и-39. 1.15 + 20 * 3 : 75 — оператор * имеет более высокий приоритет, чем +. 2.13// 2+3 : 9 — оператор//имеет более высокий приоритет, чем +. 3.31 + 10 // 3 : 34 — сначала 10 // 3 3, затем 31 + 3. 4 . 20 % 7 // 3 : 2 — операторы//и % имеют одинако- вый приоритет, но являются левоассоциативными (поэто- му выполняются слева направо). 5 2 ** 3 ** 2 : 512—оператор ** является правоассо- циативным, то есть сначала вычисляется 3 ** 2 = 9. за- тем 2 ** 9 = 512. 3. Возникает ошибка ZeroDivisionError — деление на ноль. Ответ на упражнение 5 1. Только 1 и 73 5 не являются числами с плавающей точкой (это целые числа).
2. При целочисленном делении дробная часть (остаток) от- брасывается (хотя результат всегда будет с плавающей точкой, если хотя бы один из операндов содержал значение с плаваю- щей точкой). Оператором Python для целочисленного деления является / /. Оператором обычного деления для чисел с плава- ющей точкой является /. 3. 1.1.5 + 2 : 3.5 — целое число 2 преобразуется в число с плавающей точкой, а затем прибавляется к 1 .5. 2. 1.5 // 2.0 : 0.0 — выполняется целочисленное де- ление для двух чисел с плавающей точкой, после чего воз- вращается результат (также в виде числа с плавающей точкой). 3. 1.5 / 2.0 : 0.75 — выполняется обычное деление двух чисел с плавающей точкой. 4.1 .5 ** 2 : 2.25 5.1 / 2 : 0.5 — обычное деление двух целых чисел воз- вращает число с плавающей точкой. 6.-3 / / 2 : -2 — целочисленное деление округляет ре- зультат в меньшую сторону даже для отрицательных чи- сел. 4. При делении на ноль возникает ошибка ZeroDivisionError. Обратите внимание, что сообщение об ошибке немного отличается от того, которое возвращается при делении 1 / / 0. 5.1 el 000 —слишком большое число, чтобы представить его в виде числа с плавающей точкой. Вместо этого возвращается специальное значение inf с плавающей точкой (inf — сокра- щение от inf in it у (бесконечность)). Как вы заметили, изучив его тип, inf в Python — это действительно число с плавающей точкой (а не строка "inf"). -1 el 000 дает другое специальное значение с плавающей точкой, - inf, что означает минус бес-
конечность). Эти специальные значения определены стандар- том IEEE 754, который используется в реализации чисел с пла- вающей точкой в Python. Ответ на упражнение 6 1. Один из возможных вариантов вывода: print("%s + %s = %s" % (х, у, х + у)) 2. Эквивалентными строками с одинарными кавычками яв- ляются: #. 'Привет! Я Илай . ' #. 'Название книги было "Благие знамения" . #. 'Привет! Я Д'Артаньян.' 3. "a\tb\tc" 4. Использование двойных кавычек: "первый холодный душ\п даже обезьяна, кажется, хочетХп небольшое пальто из соломы" То же хайку с использованием тройных кавычек: """первый холодный душ даже обезьяна, кажется, хочет небольшое пальто из соломы""" 5. print("Имя %s состоит из %s букв." % (name, len(name))) 6. Результат выполнения: ДЖОН СМИТ Джон Смит
При выводе во второй раз строка осталась неизменен- ной, потому что строки Python неизменяемы, а метод name.lower() возвращает новую строку содержащую имя в нижнем регистре.
Переменные и область видимости Переменные Напомним, что переменная — это метка для области в па- мяти, которую можно использовать для хранения значения. В языках со статической типизацией переменные имеют зара- нее определенные типы, и переменная может использоваться только для хранения значений этого типа. В Python мы можем использовать одну и ту же переменную для хранения значе- ний любых типов. Переменная похожа на функцию памяти, присутствующую в большинстве калькуляторов: она хранит одно значение, к ко- торому можно многократно обращаться, и при записи ново- го значения старое стирается. Однако переменные отличают- ся от памяти калькулятора тем, что в программе можно иметь множество таких «ячеек», и каждая из них имеет свое имя. Объявление переменных Чтобы объявить новую переменную в Python, достаточно просто присвоить значение некоторой метке. Например, соз- дадим переменную count, которая будет содержать целое зна- чение, равное нулю: count = 0 У этой инструкции такой же синтаксис, как в выражении, в котором существующей переменной count присваивается
новое значение. Позже в этой главе мы обсудим, при каких об- стоятельствах эта операция приведет к созданию новой пере- менной. Если попытаться обратиться к переменной, которая еще нигде не была определена, интерпретатор завершит работу с ошибкой NameE г го г (ошибка имени). Можно определить несколько переменных в одной строке, однако это считается плохим стилем: # Объявление трех переменных сразу: count, result, total = 0, 0, 0 # Это эквивалентно: count = 0 result = 0 total = 0 В соответствии с хорошим стилем программирования сле- дует давать переменным осмысленные имена. Область видимости и время жизни переменных Не все переменные доступны из всех частей нашей програм- мы, и не все они живут одинаковое время. Доступность перемен- ной определяется тем, как она была объявлена. Мы называем областью видимости (scope) ту часть программы, из которой доступна переменная, а временем жизни (lifetime) — период, в течение которого переменная существует в памяти. Переменная, которая определена в основном теле файла, называется глобальной переменной. Она будет видна во всем файле, а также в любом файле, который импортирует этот. Гло- бальные переменные могут иметь побочные эффекты из-за
своей широкой доступности — именно поэтому их следует из- бегать. Глобальную область видимости стоит использовать только для таких объектов, как функции и классы. Переменная, объявленная внутри функции, называется ло- кальной. Она доступна начиная с места объявления и до конца тела функции и существует, пока функция выполняется. Имена параметров в объявлении функции ведут себя как локальные переменные, но они содержат значения, которые мы передаем в функцию при ее вызове. Когда мы используем оператор при- сваивания (=) внутри функции, по умолчанию создается новая локальная переменная — если только переменная с таким же именем уже не существует в текущей области видимости. Вот пример переменных с разными областями видимости: # Это глобальная переменная а = 0 if а == 0: # Это все еще глобальная переменная b = 1 def my_function(c): # Это локальная переменная d = 3 print(с) print(d) # Теперь мы вызываем функцию, передавая значение 7 в ка- честве первого и единственного параметра my_function(7) # Переменные а и b все еще существуют print(а)
print(b) # Переменные end больше не существуют - вызов этих ин- струкций приведет к ошибке! print(c) р rlnt(d) Примечание Тело класса также создает собственную локальную область видимости Пе- ременные, объявленные в теле класса (вне методов), называются атрибутами класса Они доступны но имени внутри класса, а также извне через оператор доступа к атрибутам (.) от имени класса или его экземпляра Атрибуты, уста- новленные внутри метода в экземпляре, называются атрибутами экземпляра. Атрибуты класса разделяются между всеми экземплярами, а атрибуты экзем- пляра уникальны для каждого объекта Мы подробнее рассмотрим это в глэ' ве, посвященной классам Оператор присваивания Как мы видели в предыдущих разделах, оператор присваи- вания в Python — это одиночный знак равенства (=). Данный оператор присваивает переменной слева значение, вычислен- ное из выражения справа. Если справа указано выражение (например арифметическое), оно сначала вычисляется, а за- тем результат присваивается переменной. Вот несколько при- меров: a„number=5 # переменная a„number получает зна- # чение 5 a.number = total # переменная a.number получает зна- # чение переменной total а_number = total + 5 # переменная a„number содержит ре- # зультат сложения total + 5
a_number = a_number + 1 # увеличение значения переменной # а_number на 1 Последнее выражение может показаться странным, если вос- принимать = как математический знак равенства — очевидно, что число не может быть равно самому себе плюс один! Пом- ните, что знак = является оператором присваивания — этот оператор присваивает переменной a_number новое значение, которое равно старому значению a_number плюс один. Первое присваивание переменной значения называется инициализацией. В некоторых языках программирования объ- явление переменной возможно отдельно от присваивания, что может приводить к ошибкам или неожиданному пове- дению при обращении к переменной до ее инициализации. В Python переменная создается и инициализируется одновре- менно, поэтому подобных ошибок практически не возникает. Левая часть оператора присваивания должна быть допусти- мой целью (переменной): # эго корректно: а = 3 # а это - недопустимо: 3 = 4 3 = а а + b = 3 Оператор присваивания может содержать несколько це- лей (переменных), разделенных знаком =. В этом случае одно и то же значение будет присвоено всем переменным: # обе переменные получат значение 0: а = b = 0
# недопустимо, так как нельзя присваивать переменную числу: а - й - о Составные операторы присваивания Мы уже видели, что результат арифметического выражения можно присвоить переменной: total =a+b+c+50 Счетчики — очень частый элемент программ. Например, нам может понадобиться вычислить, сколько раз происходит определенное событие. Для этого создается переменная count, которая инициализируется нулем и увеличивается на 1 при на- сзуплении каждого события. Сложение выполняется с помо- щью следующего оператора: count = count + 1 На самом деле это очень распространенная операция. В Python существует сокращенная форма записи — составной оператор присваивания +=, позволяющий избегать дублиро- вания имени переменной: # Эти инструкции эквивалентны: count - count + 1 count += 1 # Мы можем прибавлять любое значение: count += 2 count += 7 count += а + b
Аналогично для уменьшения значения используется опера- тор -=: # Эти инструкции эквивалентны: count = count - 3 count -- 3 Другие распространенные составные операторы присваива- ния приведены в таблице ниже: Оператор Пример Эквивалент Подробнее об области видимости: пересечение границ Что делать, если мы хотим получить доступ к глобальной переменной внутри функции? Это возможно, но с некоторыми оговорками: а = 0 def my_function(): print(а) my_function()
Оператор print выведет 0, то есть значение глобальной пе- ременной а, как и ожидалось. А что произойдет в следующем примере? а = 0 def my_function(): а = 3 print(a) my_function() print(a) При вызове функции будет выведено значение 3, но почему оператор prints конце программы выводит О? По умолчанию оператор присваивания создает переменные в локальной области видимости. Поэтому присваивание вну- три функции не изменяет глобальную переменную а — оно создает новую локальную переменную с именем а и присваи- вает ей значение 3. Первый оператор print выводит значение новой локальной переменной — потому что если локальная переменная имеет то же имя, что и глобальная, то локальная переменная всегда будет иметь приоритет. Последний опера- тор print выводит значение глобальной переменной, которое осталось неизменным. Что делать, если мы действительно хотим изменить гло- бальную переменную внутри функции? Для этого нужно ис- пользовать ключевое слово global: а = 0 def my.function():
global а а = 3 print(a) my_function() print(а) Мы не можем ссылаться на глобальную и локальную пере- менную с одним и тем же именем внутри одной и той же функ- ции. Программа выдаст ошибку: а = 0 def my_function(): print(a) а = 3 print(а) my_function() Поскольку мы не объявили а глобальной, присваивание во второй строке внутри тела функции создаст локальную пе- ременную а. Это означает, что мы не можем ссылаться на гло- бальную переменную а в других местах функции, даже до этой строки! Теперь первый оператор print обращается к локаль- ной переменной а — нов первой строке у этой переменной еще нет значения, потому что мы его еще не присвоили! Обратите внимание, что обычно очень плохой практи- кой является обращение к глобальным переменным изнутри функций, а еще хуже — изменять их. Это нарушает модуль- ность и делает поведение программы трудно предсказуемым. Если функции требуется внешнее значение — передавайте
его через аргументы. В объектно-ориентированных програм- мах часто используется другой подход: значение сохраняется в атрибуте объекта, и метод работает с ним напрямую. Мы об- судим это подробнее в главе об объектной модели Python. Примечание В Python существует также ключевое слово nonlocal Оно позволяет функ- ции, вложенной в другую функцию, изменять переменные гнешней (но не глобальной!) функции. В отличие от global, nonlocal требует, чтобы пе- ременная уже существовала - Python не сможет угадать, в какой из обла- стей ее создавать Упражнение 1 1. Опишите области видимости переменных а, Ь, с и d в этом примере: def my_function(a): b = а - 2 return b с = 3 if о 2: d = my_function(5) print(d) 2. Каково время жизни этих переменных? Когда они будут создаваться и уничтожаться? 3. Что произойдет, если присвоить переменной с значе- ние 1, а не 3?
4. Почему это может стать проблемой? Можете ли вы при- думать способ избежать этого? Изменение значений Константы В некоторых языках можно определить специальные пе- ременные, которым можно присвоить значение только один раз — после того как их значение установлено, оно не может быть изменено. Мы называем такие переменные константами. Python не позволяет нам установить такое ограничение на пе- ременные, но существует широко распространенная конвен- ция для обозначения таких переменных, чтобы указать, что их значения не должны изменяться: мы пишем их имена заглав- ными буквами, разделяя слова подчеркиванием: # Эти переменные по умолчанию являются "константами": NUMBER_OF_DAYS_IN_A_WEEK = 7 NUMBER_OF_MONTHS_IN_A_YEAR = 12 # Ничто не мешает нам переопределить их... NUMBER_OF_DAYS_IN_A_WEEK = 8 # ...но это, вероятно, не очень хорошая идея. Зачем нам вообще определять переменные, которые мы не собираемся менять? Рассмотрим этот пример: MAXIMUM_MARK = 80
tom_mark = 58 print((('Отметка Тома составляет %.2f%%n % (t onuma г к / MAXIMUM-MARK * 100))) # % % - это способ экранирования литерала % внутри строки Есть несколько веских причин определить переменную константу MAXIMUM-MARK вместо того, чтобы просто написать 80 внутри оператора print. Во-первых, это дает числу описа- тельное имя, объясняющее, что оно означает — это делает код более понятным. Во-вторых, в конечном итоге нам может по- надобиться ссылаться на это число в нашей программе более одного раза. Если нам когда-нибудь понадобится обновить наш код, добавив новое значение максимальной оценки, нам при- дется изменить его только в одном месте программы, а не ис- кать каждое место, где оно используется — такие замены часто приводят к ошибкам. Численные значения, разбросанные беспорядочно по всей программе, известны как «магические числа» — их исполь- зование считается плохим стилем кодирования. Это не от- носится к числам со значениями 0 или 1, которые считаются самоочевидными — легко понять, почему сумма инициализи- руется нулем или увеличивается на единицу. Если переменная может принимать одно из нескольких значений, полезно обозначать значения этих вариантов при помощи констант, а не использовать их напрямую, если сами значения не имеют внутреннего смысла: # Определяем некоторые константы LOWER, UPPER, CAPITAL = 1, 2, 3 name = "джейн"
# Мы используем наши константы при присвоении значений... print_style = UPPER .и при их проверке: if print.style == LOWER: print(name.lower()) elif print_style == UPPER: print(name.upper()) elif print_style == CAPITAL: print(name.capitalize()) else: # Ничто не мешает нам случайно установить print_style # в значение, равное 4, 90 или "ложка", поэтому # на этот случай мы добавили этот запасной вариант: print('Неизвестный вариант стиля!') В данном примере значения 1, 2, 3 не имеют смысла сами по себе — только как условные обозначения. С таким же успе- хом можно использовать 4, 5 и 6 или строки ' lower1, ' upper ' и 'capital1. Единственное, что важно, — эти три значения должны быть разными. Если бы мы использовали непосред- ственно числа вместо констант, программа была бы гораздо более запутанной для чтения. Использование осмысленных строк сделает код более читабельным, но мы можем случай- но допустить орфографическую ошибку при установке одного из значений и не заметить ее, если неправильно введем имя одной из констант. При этом, скорее всего, сразу же получим ошибку. Некоторые библиотеки Python определяют для нашего удоб- ства общераспространенные константы, например: # нам нужно импортировать эти библиотеки, прежде чем ис- tf пользовать их
import string import math import re # Все строчные буквы ASCII: 'abcdefghijklmnopqrstuvwxyz' print(st ring.ascii_lowercase) # Математические константы pi и e, оба числа с плаваю- # щей точкой. print(math.pi) # отношение окружности круга к его диа- # метру print(math.e) # натуральное основание логарифмов # флаг для игнорирования регистра в регулярных выраже- # НИЯХ print(re.IGNORECASE) Обратите внимание, что многие встроенные констан- ты не соответствуют общепринятым правилам именования (в том, что касается использования заглавных букв). Изменяемые и неизменяемые типы (mutable vs immutable) Некоторые значения в Python можно изменять, а некото- рые — нет. Это не означает, что мы не можем изменить зна- чение переменной — но если переменная содержит значение неизменяемого типа, мы можем только присвоить ей новое значение. Мы не можем изменить существующее значение ни- каким образом.
Целые числа, числа с плавающей точкой и строки являют- ся неизменяемыми типами — во всех предыдущих примерах, когда мы изменяли значения существующих переменных, мы использовали оператор присваивания, чтобы присвоить им новые значения: а = 3 а = 2 # новое значение b = “джейн" b = боб'' # новое значение Даже этот оператор не изменяет значение total, вместо этого он присваивает новое значение: total += 4 До этого момента мы еще не сталкивались с изменяемы- ми (мутабельными) типами данных, но в последующих гла- вах мы будем их активно использовать. Списки и словари яв- ляются изменяемыми, как и большинство объектов, которые мы, скорее всего, будем писать сами: # это список чисел my_list = [1, 2, 3] my_list[0] = 5 # мы можем изменить первый элемент # списка print(my_list) class MyClass(object): pass # это примитивный класс # Теперь мы создадим очень простой объект, используя наш класс в качестве типа
my_ob]ect = MyClass() # Мы можем добавлять и изменять атрибуты объекта my.object.some_property = 42 Подробнее о вводе данных Ранее мы узнали, как заставить программу вывести сообще- ние на экран с помощью функции print или считать из кон- соли строку, введенную пользователем с помощью функции input. А что если мы хотим, чтобы пользователь имел воз- можность вводить числа или другие типы переменных? Мы по-прежнему используем функцию input, но преобразуем строки, возвращаемые функцией input, в нужные нам типы данных. Вот простой пример: height = int(input(' Введите высоту прямоугольника: ")) width = int(input("BBeflMTe ширину прямоугольника: ")) print("Площадь прямоугольника составляет %d" % (height * width)) Здесь int — это функция, которая преобразует значения раз- личных типов в тип int. Более подробно о преобразовании ти- пов мы поговорим в следующем разделе, а пока важно знать, что функция int не сможет преобразовать строку в целое чис- ло, если она содержит что-либо, кроме символов цифр. Приве- денная выше программа завершится с ошибкой ValueError, если пользователь введет "ааа", "zzz10" или даже "7.5". Когда мы пишем программу которая предполагает, что пользо- вательский ввод может быть неверным, нам нужно добавить некоторые меры предосторожности, чтобы ее работа не завер-
шилась аварийно, если пользователь допустит ошибку. Напри- мер, мы можем определить, что пользователь ввел неверные данные, и выйти из программы с более приятным сообщени- ем об ошибке: try: height = int(input('Введите высоту прямоугольника: ")) width = int(input('Введите ширину прямоугольника: ")) except ValueError as e: # если произойдет ошибочный ввод, # программа продолжит работу с этого места print("Ошибка при чтении значений для высоты и ширины: %s" % е) Эта программа попытается считать введенные данные как минимум один раз и завершит работу, если они окажут- ся неверными. Если мы хотим продолжать запрашивать ввод у пользователя до тех пор, пока он не будет правильным, мы можем сделать что-то вроде этого: correct_input = False # это логическое значение - оно # может быть либо истинным, либо ложным. while not correct_input: # это цикл while try: height = int(input( Введите высоту прямоуголь- ника : ")) width = int(input("Введите ширину прямоугольни- ка: ")) except ValueError: print('Пожалуйста, введите правильные целые чис- ла для высоты и ширины.") else: # эта инструкция будет выполнена, если нет зна- # чения error correct_input = True
Мы подробнее изучим логические значения, циклы и ис- ключения в следующих разделах. Пример: вычисление расхода топлива автомобилем В этом примере мы напишем простую программу, кото- рая запрашивает у пользователя расстояние, пройденное ав- томобилем, и сумму, потраченную на топливо, которое было использовано для преодоления этого расстояния. На основе этой информации, а также стоимости литра топлива програм- ма рассчитает эффективность автомобиля, как с точки зрения расхода топлива в литрах на 100 километров, так и в киломе- трах на литр. В начале программы мы определим стоимость топлива как константу. Это позволит нам легко обновлять стоимость при ее изменении в первую среду каждого месяца: PETROL_PRICE_PER_LITRE =4.50 При запуске программы мы хотим вывести приветственное сообщение: print("*** Добро пожаловать в калькулятор топливной эф- фективности1 ***\п') # мы добавляем дополнительную пустую строку после сооб- щения с помощью управляющего символа перевода на новую строку \п Ввод имени пользователя: name = input('Введите ваше имя: ") Ввод расстояния: # float - это функция, которая преобразует строку в чис- ла с плавающей точкой.
distance-travelled = float(input("Введите пройденное расстояние в км: ")) Ввод суммы оплаты: amount_paid = float(input("Введите сумму, потраченную на топливо (в рублях): ")) Теперь займемся расчетами: fuel_consumed = amount_paid / PETROL_PRICE_PER_LITRE efficiency_l_per_100_km = fuel_consumed / distance- travelled * 100 efficiency_km_per_l = distance-travelled / fuel_consumed Наконец, выведем результаты: print("Привет, %s!" % name) print("Эффективность вашего автомобиля составляет %.2f литров на 100 км." % efficiency_l_per_100_km) print( "Это означает, что вы можете проехать %.2f км на одном литре топлива." % efficiency_km_per_l) # мы добавляем дополнительную пустую строку после сооб- # щения с помощью управляющего символа перевода на но- # вую строку 1/7 print('"ХпСпасибо за использование программы.") Упражнение 2 1. Напишите программу на Python для преобразования температуры, заданной в градусах Фаренгейта, в ее экви-
валент в градусах Цельсия. Вы можете считать, что Т_с = (5/9) х (T_f - 32),гдеТ_с — температура в граду- сах Цельсия, a T_f — температура в градусах Фаренгейта. Ваша программа должна запрашивать значение у пользо- вателя и выводить результат. Входные и выходные значе- ния должны быть числами с плавающей точкой. 2. При каком вводе программа может завершиться с ошиб- кой? Что нужно сделать, чтобы обработать эту ситуацию корректно? Преобразование типов По мере написания программ мы часто сталкиваемся с необ- ходимостью преобразования данных из одного типа в другой, например, из строки в целое число или из целого числа в чис- ло с плавающей точкой. В Python есть два вида преобразова- ния типов: неявное и явное. Неявное преобразование Как было сказано ранее в разделе об операторах с плаваю- щей точкой, в арифметическом выражении мы можем про- извольно комбинировать целые числа и числа с плавающей точкой. В этом случае результатом любого такого выражения всегда будет число с плавающей точкой. Это связано с тем, что прежде чем вычислить выражение, Python преобразует це- лые числа в числа с плавающей точкой. Это неявное преобразо- вание — нам не нужно ничего преобразовывать самим. Обыч- но при преобразовании целого числа в число с плавающей точкой точность не теряется.
Например, в следующем примере целое число 2 будет авто- матически преобразовано в число с плавающей точкой: result = 8.5 * 2 Здесь число 8.5 — это тип float, а 2 — тип int. Python ав- томатически преобразует операнды так, чтобы они были од- ного типа. В данном случае это достигается, если целое число 2 будет преобразовано в эквивалент 2.0 с плавающей точ- кой. Затем эти два числа с плавающей точкой можно перем- ножить. Давайте рассмотрим более сложный пример: result = 8.5 + 7 // 3 - 2.5 Python выполняет операции в соответствии с приоритетом и решает, нужно ли отдельное преобразование для каждой опе- рации. В нашем примере операция целочисленного деления // имеет наивысший приоритет, поэтому она будет выполне- на первой. Числа 7 и 3 имеют тип int, а // — оператор це- лочисленного деления, следовательно, результатом этой опе- рации будет целое число 2. Теперь выражение выглядит так: 8.5 + 2 - 2.5. Операции сложения и вычитания имеют одинаковый приоритет, поэтому они вычисляются слева на- право, начиная со сложения. Сначала 2 преобразуется в число с плавающей точкой 2.0, а затем два числа с плавающей точ- кой складываются, в результате чего мы получим 10.5 - 2.5. Результатом вычитания с плавающей точкой является число 2.0, которое присваивается переменной result. Явное преобразование Преобразование чисел из float в Int приводит к потере точности. Например, попробуйте преобразовать число 5.834 в int — это невозможно сделать без потери точности. Что-
бы этого не произошло, мы должны явно сообщить Python, что знаем о потере точности. Например, мы должны указать компилятору преобразовать float в int следующим образом: i = int(5.834) Функция int преобразует число float в число int, отбра- сывая дробную часть — при этом всегда округляя в меньшую сторону! Если мы хотим получить больше контроля над тем, как округляется число, нам надо использовать другую функ- цию: # функции floor и ceil находятся в модуле math import math # функция ceil возвращает ближайшее целое число, большее # или равное числу # (чтобы всегда округлять в большую сторону) i = math.ceil(5.834) # floor возвращает ближайшее целое число, меньшее или рав- # ное числу # (чтобы всегда округлять в меньшую сторону) i = math.floor(5.834) # round возвращает ближайшее целое к числу # (чтобы округлить в большую или меньшую сторону) # Обратите внимание, что это встроенная функция - нам # не нужно импортировать math, чтобы использовать ее. 1 = round(5.834) Явное преобразование иногда также называют приведени- ем — можно встретить выражения вроде приведения float к int или наоборот.
Преобразование строк Как мы видели в предыдущих разделах, Python редко выпол- няет неявные преобразования в тип str и из типа str в дру- гой тип данных. Обычно в этом случае нам приходится пре- образовывать значения явно. Если мы передадим в функцию print число (или любое другое значение), то оно автоматиче- ски будет преобразовано в строку. Однако если мы попытаемся сложить число и строку, то получим ошибку: # Это работает print(5) print(6.7) # А это - нет print("3" + 4) # Поскольку непонятно, имелось в виду это... print("3%d" % 4) # объединить "3" и "4", чтобы получить "34" # или это? pnnt(int( '3' ) + 4) # сложить 3 и 4, чтобы получить 7 Для преобразования чисел в строки чаще всего использует- ся форматирование строк — это обычно самый правильный и понятный способ вставить несколько численных значений в сообщение. Если нужно преобразовать одно число в строку, можно также воспользоваться функцией st г в явном виде: # Обе инструкции дадут одинаковый результат. print("3%d" % 4) print("3" + str(4))
Дополнительные сведения о преобразованиях В Python такие функции как st г, int и float стараются пре- образовать переданные значения к соответствующему типу. Например, мы можем использовать функцию int для преоб- разования строк в целые числа или для преобразования чи- сел с плавающей точкой в целые числа. Обратите внимание, что хотя функция int может преобразовать число с плаваю- щей точкой в целое число, она не может напрямую преобра- зовать строку, содержащую число с плавающей точкой, в це- лое число! # Это работает int("3") # Это тоже работает int(3.7) # результат: 3 # А это вызовет ошибку int("3.7") # здесь строка представляет не целое число, # а число с плавающей точкой # Сначала нужно преобразовать строку в число с плавающей точкой int(float("3.7")) # результат: 3 Значения типа bool могут принимать только два значения: True и False. Они часто используются в условных операторах, которые определяют, выполнять ли тот или иной участок кода: my_flag = True if my_flag: print("Привет!")
Условие часто представляет собой выражение, результат ко- торого — логическое значение: if 3 > 4: print('Эта строка не будет выведена никогда.") Однако практически любое значение может быть неявно преобразовано в тип bool, если оно используется в условии: my_number = 3 if my_number: print("Число не равно нулю!") Это поведение обычно интуитивно понятно: ненулевые числа считаются True, а ноль — False. Однако при рабо- те со строками нужно быть особенно внимательным: пустая строка ("") воспринимается как False, но любая другая строка будет True—даже "0" и "False"! # Функция bool() возвращает логическое значение boo1(34) # True bool(0) # False bool(1) # True bool("") # False bool("Jane") # True bool("0") # True! bool("False") # Также True1
Упражнение 3 1. Преобразуйте строку "8.8" в число с плавающей точ- кой.. 2. Преобразуйте число 8.8 в целое число (с округлением). 3. Преобразуйте строку '8.8" в целое число (с округлени- ем). 4. Преобразуйте число с плавающей точкой 8.8 в строку. 5. Преобразуйте целое число 8 в строку. 6. Преобразуйте целое число 8 в число с плавающей точкой. 7. Преобразуйте целое число 8 в логическое значение.
Ответы на упражнения Ответ на упражнение 1 1. Переменная а — локальная, так как она является аргумен- том функции my .function. Переменная b также является ло- кальной, поскольку ей присваивается значение внутри функ- ции. Переменные с и d — глобальные. При этом неважно, что d создается внутри блока if, потому что внутренняя часть блока if не является новой областью видимости — все, что находит- ся внутри блока, является частью той же области видимости, что и снаружи (в данном случае глобальной). Только определе- ния функций (которые начинаются с def) и определения клас- сов (которые начинаются с class) указывают на начало ново- го уровня области видимости. 2. Переменные а и b создаются каждый раз при вызове функ- ции my.function и уничтожаются после завершения ее вы- полнения. Переменная с создается, когда ей присваивается значение 3, и существует до конца выполнения программы. Переменная d создается внутри блока if (когда ей присваива- ется значение, возвращаемое функцией) и также существует до конца выполнения программы. 3. Как мы узнаем в следующей главе, блоки if выполняются на основе условия. Если условие if не выполнится (например, с не больше 3), переменная d не будет создана. 4. Это может привести к ошибке, если позже в программе предполагается, что переменная существует. В программиро- вании считается плохой практикой определять переменные
только при выполнении определенного условия. Лучше зара- нее задать переменной значение по умолчанию — это упро- щает проверку и делает поведение программы предсказуе- мым. Ведь проще проверить, имеет ли переменная значение по умолчанию, чем проверять, существует ли она вообще. Ответ на упражнение 2 1. Вот пример программы: T_f = float(input("Пожалуйста, введите температуру в °F: ")) Т_с = (5/9) * (T_f - 32) print("%g°F = %g°C" % (T_f, T_c)) Примечание Символ форматирования %g используется для чисел с плавающей точкой и позволяет Python автоматически выбрать наиболее удобный для прочте- ния формат отображения числа с плавающей точкой 2. Программа может аварийно завершиться, если пользо- ватель введет значение, которое невозможно преобразовать в число с плавающей точкой. Чтобы избежать этого, необхо- димо добавить какую-то проверку некорректного ввода — на- пример, сохранять введенную строку и проверять ее содер- жимое. Если значение некорректно, можно либо вывести сообщение об ошибке и завершить программу, либо повтор- но запросить ввод у пользователя, пока не будет введено пра- вильное значение. Ответ на упражнение 3 Вот примеры ответов:
import math а_1 = float("8.8") а_2 = math.round(8.8) а_3 = math.round('8.8") a_4 = "%g" % 8.8 a_5 = "%d" % 8 a_6 = float(8) a_7 = bool(8).
Операторы ветвления Введение В предыдущей главе вы познакомились с понятием пото- ка управления: последовательностью инструкций, которые выполняет компьютер. В процедурно-ориентированном коде инструкции обычно выполняются в том порядке, в котором они записаны. Однако это не всегда так. Один из способов, с по- мощью которого программисты могут изменить поток управ- ления, — это использование операторов ветвления. В этой главе мы рассмотрим работу с операторами выбора, которые позволяют программе принимать решения — то есть выполнять те или иные действия в зависимости от условий. Например, программа может выбирать, как поступить, на ос- нове введенных пользователем данных. Как вы увидите, такие операторы делают программу более универсальной. Также мы познакомимся с основными типами ошибок в про- граммах и обсудим стратегии их поиска и устранения. Оператор выбора: if Каждый день человек принимает множество решений. Что съесть на обед? Чем запяться на выходных? Каждое реше- ние базируется на каком-либо критерии. Например, вы може- те выбрать блюдо в зависимости от своего настроения или ди- еты. Приняв решение, вы действуете в соответствии с ним. Таким образом, принятие решений — это двухступенчатый
процесс: сначала вы решаете, что именно делать, основываясь на каком-то критерии, а затем предпринимаете действие. Принятие решений компьютером основано на том же двух- ступенчатом процессе. В Python для этого используется опера- тор if, известный также как оператор выбора. При обработке оператора if компьютер сначала вычисляет некоторый кри- терий или условие. Если оно истинно, то выполняется указан- ный блок инструкций. Вот синтаксис оператора if: if условие: блок_кода Дойдя до оператора if, компьютер выполняет блок_кода только в том случае, если условие истинно. Вот пример на язы- ке Python, написанный в соответствии с приведенной выше блок-схемой: if age < 18: print("Голосовать нельзя") Как видно из фрагмента программы, инструкции в теле оператора if выполняются только в том случае, если условие выполняется (то есть если оно истинно). Если условие не вы- полняется (т. е. ложно), инструкции в теле оператора if про- пускаются. Операторы проверки равенства Часто условие в операторе if — это результат сравнения двух значений. В последнем примере мы сравнили перемен- ную аде с целым числом 18, чтобы проверить, меньше 18 лет или нет возраст избирателя. Для сравнения мы использова-
ли оператор меньше <. Это один из операторов сравнения в Python. В таблице ниже представлен полный список опера- торов сравнения: Оператор Описание Пример =— равно if age == 18 । = не равно if score != 10 > больше if num_people > 50 < меньше if price < 25 >- больше или рав- но if total >= 50 <= меньше или рав- но if value <= 30 Обратите внимание на оператор равенства ==. Один знак равенства (=) используется для присваивания. Ошибка в выбо- ре оператора приведет к синтаксической ошибке: »> if choice = 3: File "<stdin>", line 1 if choice = 3: A SyntaxError: invalid syntax Корректное использование оператора сравнения: if choice == 3: print("Спасибо за использование этой программы").
Примечание В некоторых языках оператор присваивания внутри условия считается до- пустимым он выполняет ту же роль, что и знак == в Python В таких языках проще всего случайно использовать неправильный оператор и не заметить этого1 В языке Python это сразу приведет к ошибке выполнения программы. Сравнение значений и оператор тождественности До сих пор в наших примерах мы сравнивали только целые числа. Но операторы сравнения работают и с другими типами данных: # мы можем сравнивать значения строк if name == "Джейн": print("Привет, Джейн!") # ... или числа с плавающей точкой if size < 10.5: print(size) При сравнении переменных с помощью оператора == мы проводим сравнение по значению: проверяем, имеют ли две переменные одинаковое значение. В отличие от этого, может быть интересно узнать, являются ли два объекта, та- кие как списки, словари или пользовательские объекты, кото- рые мы создали сами, одним и тем же объектом (ссылаются ли на один и тот же объект). Это проверка на тождествен- ность. Два объекта могут иметь одинаковое содержимое, но ссылаться на два разных объекта. Проверку на тождествен- ность можно выполнить с помощью оператора is: а = [1,2,3] b = [1,2,3]
if а == Ь: print("Эти списки имеют одинаковое значение. ') if a is b: print("Эти списки - один и тот же список"). Как правило (с некоторыми оговорками) считается, что если две переменные являются одним и тем же объектом, они так- же равны. Обратное неверно — две переменные могут быть равны по значению, но при этом ссылаться на разные объек- ты в памяти. Чтобы проверить, ссылаются ли две переменные на один и тот же объект, мы можем использовать оператор is not: if a is not b: print("a и b не ссылаются на один и тот же объект'). Примечание Во многих случаях переменные, содержащие одинаковые значения неизме- няемых встроенных типов (например, int, str), могут ссылаться на один и тот же объект. Это связано с тем, что интерпретатор Python для экономии памяти и ускорения операции сравнения может использовать один и тот же объект в нескольких местах. Однако не стоит полагаться на это поведение и использовать оператор is для сравнения значений — для таких целей всег- да следует применять оператор ==. Использование отступов В примерах, приведенных в этой главе, мы пока использо- вали конструкции if с одним оператором сравнения внутри блока. Однако Python допускает несколько операторов в теле if. Например:
if choice == 1: count += 1 print("Спасибо за использование этой программы'), print("Это сообщение будет выведено на экран в любом случае.".") # инструкция с функцией print расположена вне блока if Интерпретатор рассматривает все строки с одинаковым уровнем отступа после заголовка if как один блок. Все ин- струкции внутри блока выполняются до перехода к следующей части программы. Это позволяет нам указать в блоке любое ко- личество инструкций, которые будут выполняться при выпол- нении условия. В Python оператор if называют составным, поскольку он объединяет несколько других операторов. Составной опера- тор состоит из заголовка (например if) и тела (оно содержит последовательность инструкций). Границы блока определя- ются отступами: строки с одинаковыми отступами принадле- жат одному блоку. Условная конструкция else Конструкция if может иметь необязательный блок else, который определяет, что делать, если условие в if ложно: if условие: if_body # действия, если условие истинно else: else_body # действия, если условие ложно Другими словами, компьютер выполнит тело оператора if, если условие истинно, в противном случае он выполнит тело else. В приведенном ниже примере компьютер приба-
вит 1 к значению х, если оно равно нулю, в противном случае он вычтет 1 из х: if х == 0: х += 1 else: х -= 1 Эта конструкция соответствует следующей блок-схеме: если условие истинно — выполняется один путь, иначе — другой. Всегда выполняется только один из блоков. Упражнение 1 1. Какие из следующих строк содержат корректные выра- жения с if, а какие нет? Объясните, почему: 1. if (х > 4) 2. if х == 2 3. if (у =< 4) 4. if (у = 5) 5. if (3 <= а) 6. if (1 - 1) 7. if ((1 - 1) <= 0) 8. if (name == James") 2. Что выведет следующий фрагмент кода? Объясните, по- чему: х = 2 if х > 3: print("3TO число')
print("больше") print("чем 3.") 3. Как можно упростить эти фрагменты кода? 1. if bool(a) == True: print("а содержит значение True") 2. if х > 50: b += 1 a = 5 else: b - = 1 a - 5 Дополнительные сведения об операторе if Вложенные операторы if Иногда при написании программы возникает ситуация, ког- да одно решение может зависеть от результата предыдуще- го. Подобные зависимости нередки и в повседневной жизни. Сначала мы принимаем одно решение — например, пойти за покупками — и только после этого начинаем размышлять, в какой именно магазин отправиться. Или же мы сначала осоз- наем, что проголодались и пора бы поужинать, и лишь затем выбираем, что именно приготовить. Таким образом, выбор
конкретного действия (магазина или блюда) становится акту- альным только после того, как принято предшествующее ре- шение — отправиться за покупками или устроить ужин. В язы ке Python такая логика реализуется с помощью вложен- ных условных операторов — когда один оператор if распола- гается внутри блока if или else другого. Рассмотрим пример. Допустим, нужно рассчитать стоимость отправки небольшой посылки. Согласно условию, за первые 300 граммов почта взи- мает R5, а за каждые последующие 100 граммов — еще по R2. При этом расчет ведется с округлением в большую сторону, и максимально допустимый вес — 1000 граммов. Ниже пред- ставлен фрагмент кода, который выполняет этот расчет: if weight <= 1000: if weight <= 300: cost = 5 else: cost = 5 + 2 * round((weight - 360)/100) print("Стоимость посылки: R%d." % cost) else: print("Превышен максимальный вес.") print('Воспользуйтесь службой доставки крупных по- сылок" ). Обратите внимание на структуру отступов в примере: бло- ки внешних операторов if и else выровнены по одному уровню отступа, тогда как вложенные в них конструкции if и else имеют дополнительный отступ. Это не просто фор- мальность — отступы в Python играют ключевую роль, опре- деляя, какие инструкции к какому блоку относятся. Малей- шая ошибка в отступах может изменить логику программы
или привести к синтаксической ошибке. Стоит также отме- тить, что наличие пустой строки между последней инструк- цией внутреннего оператора if и следующей за ним строкой с функцией print не нарушает структуру кода. Обе инструк- ции остаются внутри тела внешнего оператора if, поскольку сохраняют одинаковый уровень отступа. Пустые строки при разумном использовании улучшают читабельность программы и помогают лучше воспринимать ее структуру — особенно в случаях, когда логика программы включает вложенные блоки. Конструкция el if и «лестница условий» Добавление ключевого слова else позволяет нам задать действия для случая, когда условие в if не выполняется. Од- нако бывают ситуации, когда нужно обрабатывать более двух альтернатив. Например, нам надо написать программу, кото- рая выставляет оценку в зависимости от полученного после прохождения теста количества баллов. Для наглядности логи- ку ее работы можно представить в виде блок-схемы. Можно было бы реализовать такой алгоритм с помощью вложенных операторов if. Такой фрагмент кода может выгля- деть примерно так: if mark >= 80: grade = А else: if mark >= 65: grade = В else: if mark >= 50: grade = C
else: grade = D Этот фрагмент программы работает корректно, но плохо воспринимается из-за большого количества уровней вложен- ности. Поэтому Python предлагает специальную конструкцию elif, которая позволяет легче воспринимать конструкцию из нескольких вложенных команд if-else более читаемой: if mark >= 80: grade = А elif тагк >= 65: grade = В elif mark >= 50: grade = C else: grade = D Теперь все альтернативные пути объединены в единую кон- струкцию — один оператор if, дополненный ветвями elif и, при необходимости, завершающим оператором else. Все эти ветви находятся на одном уровне отступа, поскольку при- надлежат к одной логической структуре. Такой подход часто называют лестницей условий (или if ladder)— он позволяет по- следовательно проверять несколько взаимоисключающих ва- риантов. Для наглядности такая логика часто сопровождается блок-схемой — она помогает визуально представить порядок проверок и разветвление исполнения программы. Последняя ветка else играет роль значения по умолча- нию — своеобразной «ловушки», в которую попадает ход вы- полнения программы, если ни одно из предыдущих условий не оказалось истинным, т. е. если ни одно из указанных ранее
условий не выполняется, будут выполнены инструкции в теле оператора else. Это важно: даже если кажется, что предусмо- трены все возможные случаи проверки, добавление else —хо- рошая практика. Оно служит своего рода страховочной сеткой, особенно в тех случаях, когда перечень условий со временем может измениться. Наличие оператора else поможет своевре- менно заметить и обработать неожиданные или ошибочные ситуации: if course_code == "CSC": department-name = "Компьютерные науки" elif course_code == "МАМ": department-name = 'Математика и прикладная матема- тика " elif course_code == "STA": department-name = "Статистические науки" else: department-name = None print("Неизвестный код курса: %s" % course.code) if department-name: print("Отдел: %s" % department-name) Допустим, мы неожиданно получили курс с кодом "INF" (информатика). В этом случае сработает блок else, и выведет- ся сообщение: Неизвестный код курса : INF. Если бы блока с оператором else не было, мы могли бы не заметить ошибку, пока программа не попыталась бы использовать переменную department_name, которой так и не было присвоено никако- го значения. Использование оператора else позволяет нам об- наруживать и обрабатывать исключительные случаи своевре- менно.
Значения типа bool и логические операторы Тип bool В Python существует специальный тип данных для логиче- ских значений: bool. Он может принимать два значения: ис- тина — Тrue, и ложь — False. Python автоматически приво- дит значения других типов к bool, если они используются в логических контекстах, например в условии if. Чаще всего нам не придется явно использовать приведение к bool. Так- же не нужно явно использовать оператор ==, чтобы проверить, равно ли значение переменной Тrue — мы можем использо- вать имя переменной само по себе в качестве условия: name = " Джейн" # Это правильный способ проверки равенства на True: if name: рrint("Hello, %s!" % name) # To же самое, но менее предпочтительно: if bool(name) == True: print("Hello, %s!" % name) # Неправильный способ: if name == 1 rue: printf"Hello, %s!" % name) Почему последний оператор if работает не так, как мы ожи- дали? Потому что строка "Джейн ' не равна Т rue и изначально не является логическим значением сама по себе. Ее необходи- мо прообразовать к типу bool (чтобы она стала равна True). Поэтому условие name == True будет равно False. Именно
поэтому мы всегда должны использовать сокращенный син- таксис, как показано в первой инструкции с оператором if. В этом случае Python сделает неявное приведение типа за нас. Примечание По историческим причинам числа 0 и 0.0 фактически равны False, а 1 и 1 0 - T rue Однако они не идентичны этим объектам - яы можете проверить это. сравнив их с помощью оператора is. В конце предыдущей главы мы обсудили, как Python неяв- но преобразует значения различных типов данных в тип bool. Все непустые строки и ненулевые числа считаются True, пу- стые строки "" и ноль (0, 0.0) считаются False. Это прави- ло распространяется и на другие типы данных: пустые спи- ски, множества, словари, кортежи также считаются равными False. Логические операторы Иногда при принятии решения нужно учитывать сразу не- сколько факторов. Такие ситуации возникают не только в про- граммировании, но и в повседневной жизни. Например, вы ре- шаете, что купите рубашку, только если она вам действительно нравится и стоит меньше 1000 рублей. Или вы отправитесь в ре- сторан при условии, что в холодильнике пусто или вам про- сто сегодня не хочется готовить. Или вы решаете, что пойдете на концерт завтра, но только при условии, что не будет дождя. Во всех этих случаях условие состоит не из одного, а из не- скольких критериев. Такие условия называют сложными ло- гическими выражениями (compound conditions), поскольку они объединяют несколько простых условий в одно. В Python для этого используются логические операции И, ИЛИ и НЕ.
Оператор and Операцию логическое И в Python выполняет оператор and. Составное выражение, состоящее из двух условий и операто- ра and, будет истинным только тогда, когда оба условия ис- тинны: if mark >= 50 and mark < 65: print('Оценка В") Составное условие верно только в том случае, если обе части условия верны: значение mark больше либо равно 50 и одновременно меньше 65. Оператор and работает так же, как и его речевой аналог. Мы можем формально определить работу оператора and с помощью таблицы истинности, при- веденной ниже. В таблице показано, на каких именно комби- нациях значений а и b из всех возможных оператор and воз- вращает значение True (Истина). Например, если значение а равно Тrue и b равно Тrue, то выражение a and b вернет значение 7 rue. а b a and b Т rue T rue True Т rue False False False T rue False False False False Оператор and — бинарный, поэтому для его работы требу- ются два операнда. Каждое условие должно быть полноцен- ным логическим выражением:
# Корректный пример: if (х > 3 and х < 300): х += 1 # В этом случае возникнет синтаксическая ошибка: if (х > 3 and < 30G): # < 300 не является допустимым вы- # ражением! х += 1 С помощью оператора and мы можем объединить больше двух условий. Они будут проверяться слева направо: условие_1 and условие_2 and условие.З and условие_4 # То же самое, что: ((условие_1 and условие.?) and условие.З) and условие_4 Примечание В python проверку принадлежности значения диапазону можно выполнить более компактно: вместо mark >= 50 and mark < 65 следует использо- вать выражение 50 <= mark < 65 Такая запись не работает для большин- ства других языков программирования, но язык Python предоставляет такую полезную особенность Вычисление по ускоренной схеме Обратите внимание: если первая часть условия с операто- ром and ложна, Python не будет вычислять вторую часть, так как итог уже ясен — все условие будет ложным независимо от того, истинно b или нет. Такое поведение мы называем уско- ренной схемой вычисления, и, соответственно, оператор and — оператором, поддерживающим такую схему вычисления.
Такое поведение можно и нужно использовать в своих инте- ресах при написании программ, поскольку оно ускоряет быст- родействие. Рассмотрим следующий пример: if х > 0 and 1/х < 0,5: print("x равно %f" % х) Что произойдет, если х равен нулю? Если бы интерпре- татор обработал два условия, мы бы получили ошибку деле- ния на ноль. Но поскольку and — оператор, поддерживающий ускоренную схему вычисления, второе условие будет прове- рено только в том случае, если первое вернет значение True (Истина). Если х равен нулю, его значение будет равно False, и проверка второго условия выполняться не будет. Похожим образом мы могли бы использовать и вложенные операторы if, например, вот так: if х > 0: if 1/х < 0,5: print("x равно %f" % х) Использование оператора and делает текст программы бо- лее компактным и понятным — особенно в ситуации, когда требуется проверить более двух условий. Эти два фрагмента программы эквивалентны: if х != 0: if у != 0: if z != 0: р rint(1/(x*y*z)) if x ‘=0 and у != 0 and z != 0: print(1Z(x*y*z))
Также оператор and часто используется при доступе к атри- бутам объектов или элементам контейнеров: if hasattr(my_person, name") and len(my_person.name) > 30: print("Длинное имя - %s!" % my_person.name) if i < len(mylist) and mylist[i] == 3: print('Число 3 находится в этом списке’") if key in mydict and mydictfkey] == 3: print("Обнаружил число 3, выполнив поиск по слова- рю!") Оператор ог Операцию логическое ИЛИ в Python выполняет оператор о г. Сложное логическое выражение, в состав которого входят два условия и оператор or, вернет значение True (Истина), если хотя бы одно из условий имеет значение Т rue. Это означа- ет, что оно ложно только в том случае, если оба условия име- ют значение False (Ложь) одновременно, и истинно во всех остальных случаях. Наглядно это можно увидеть в таблице ис- тинности для оператора or: - а b a or b Т rue T rue T rue Т rue False T rue False T rue T rue False False False
Следующий фрагмент программы выведет сообщение о не- верно введенных данных, если заданный возраст меньше О или больше 120: if age < 0 or age > 120: print( "Некорректный р.озраст: %d" % age) Оператор о г так же, как и оператор and, поддерживает уско- ренную схему вычисления. Если результат проверки перво- го условия — Т rue, второе условие не проверяется, поскольку ясно, что теперь в любом случае общий результат будет Тrue. Оператор or так же, как и оператор and, бинарный: # Корректно: if х < 3 or х > 300: х += 1 # Приведет к синтаксической ошибке: if х < 3 or > 300: # > 300 не является допустимым выра- жением1 х += 1 # Может работь не так, как ожидается: if х == 2 ог 3: print("x равно 2 или 3") Результатом выполнения последней инструкции с if всег- да будет значение Тrue, поскольку 3 — это допустимый опе- ранд и поскольку это число, отличное от нуля, в логическом представлении оно равно Тrue. Поэтому тело последней ин- струкции if будет выполняться всегда, независимо от значе- ния х!
Оператор not Операцию логическое НЕТ в Python выполняет оператор not. Он является унарным оператором: ему требуется только один операнд. Оператор not используется для инвертирова- ния переменной типа bool или результата выполнения логи- ческого выражения, используемого вместе с ним. Ниже приве- дена таблица истинности оператора not: а not Т rue False False T rue Оператор not можно использовать для написания более по- нятного кода. Например, рассмотрим следующий пример, в ко- тором мы проверяем, начинается ли строка с буквы "А": if name.startswith("А"): pass # тело оператора не может быть пустым - это ин- # струкция, которая ничего не делает. else: print("' %s' не начинается с А!" % s) # Предыдущий фрагмент программы слегка избыточен - луч- # ше использовать оператор not1 if not name.startswith("А"): print("'%s' не начинается с A!" % s) Вот несколько других примеров: # Тело с оператором If будет выполнено, если флаг равен # False:
if not my.flag: print("Hel.lo!") # Эта инструкция c if... if not x == 5: x += 1 # ... эквивалентна этой: if x != 5: x += 1 Приоритет операторов для логических выражений Здесь приведена таблица приоритетов операторов для ло- гических выражений (от наивысшего к низшему), показыва- ющая относительный уровень старшинства для всех опера- торов: Операторы () (высший) ** is, is not not
Операторы and о г (самый низкий) Даже несмотря на то. что Python строго соблюдает порядок приоритетов операторов (см. таблицу приоритетов выше), всегда полезно использовать скобки, чтобы явно обозначить логику выражения. Скобки могут повысить читаемость кода, уменьшают вероятность появления ошибок, делают поведе- ние программы более предсказуемым. Закон Де Моргана и особенности применения оператора not Оператор not может усложнить понимание выражений, особенно если в выражении встречается двойное отрицание. Старайтесь использовать оператор not только там, где он име- ет смысл. Большинству людей легче понять положительные утверждения, чем отрицательные. Иногда лучше применить в тексте программы инверсию оператора сравнения вместо использования оператора not, например: if not mark < 50: print("Экзамен сдан") # То же самое, но легче для понимания: if mark >= 50: print('Экзамен сдан")
В этой таблице приведены операторы сравнения и их ин- версии: Оператор Инверсия == । - > <= < >= Законы, названные в честь английского логика Августа Де Моргана, были сформулированы еще в XIX веке, но остают- ся актуальными по сей день — особенно при работе с логиче- скими выражениями в программировании. Эти законы помо- гают упростить выражения с отрицаниями и сделать логику условий более наглядной и понятной. Пример из обычного языка. Фраза «Сегодня не одновремен- но и прохладно, и дождливо» означает, что сегодня не одновре- менно прохладно и дождливо. В какой-то момент может быть прохладно, но не дождливо, или наоборот. То есть это не то же самое, что сказать: «Сегодня и не прохладно, и не дождливо» — здесь отрицаются обе части, что уже совсем другое утверж- дение, поскольку в нем говорится, что оба условия неверны, а в первом варианте утверждения одно из условий может быть верным. Правильный эквивалент этой фразы — сегодня или не прохладно, или не дождливо. Мы только что применили закон Де Моргана, чтобы проде- монстрировать, как операция логическое НЕ (отрицание) мо- жет преобразовать первое утверждение. Формально законы Де Моргана гласят: 1. NOT (a AND b) = (NOT a) OR (NOT Ь) (НЕ (а И b) = (НЕ а) ИЛИ (НЕ Ь)) 2. NOT (a OR b) = (NOT a) AND (NOT b) (НЕ (а ИЛИ Ь) = (НЕ а) И(НЕЬ))
Мы можем использовать эти законы для применения опе- ратора пог к логическим выражениям в языке Python. На- пример: if not (age > 0 and age <= 120): print("Некорректный возраст") # эту конструкцию с if можно переписать в виде if age <= 0 or age > 120: print("Некорректный возраст") Здесь вместо использования оператора логического отрица- ния not можно добиться того же результата, инвертируя опе- раторы, входящие в состав выражения. Такой прием позволяет переписать условие без not, сделав его интуитивно понятным. Упражнение 2 1. При каких значениях, возвращаемых функцией input, программа выведет сообщение "Тrue"? if not input > 5: print("True") 2. При каких значениях absentee.rate и overall, mark эта программа выведет сообщение “Вы прошли курс"? if absentee.rate <= 5 and overall.mark >= 50: print("Вы прошли курс").
3. При каких значениях х эта программа выведет сооб- щение "True"? if х > 1 or х <= 8: print(”True‘') 4. Перепишите выражения, исключив оператор not: not total <= 2 not count > 40 not (value > 20.0 and total != 108.0) not (angle > 180 and width == 5) not (count == 5 and not (value != 10) or count > 5U) not (count == 5 and not (value != 10) or count > 50) Значение None Иногда мы инициализируем переменные «заглушками» (значениями по умолчанию): 0, "", [ ] и т. д., прежде чем при- своим им более осмысленные значения в теле программы. Все они считаются имеющими значение False при использова- нии в логических выражениях: if (my.variable): print(my_variable) Но что делать, если 0 или "" — это и есть уже осмыслен- ные значения, и мы хотим отличить их от значения по умол- чанию? Для этого в Python существует специальное значение None. В Python None — это специальное значение, которое оз- начает «ничего». Его тип называется NoneType, и одновремен- но существует только одно значение None, т. е. все значения
None, которые мы используем в программе, на самом деле яв- ляются одним и тем же объектом. Как мы можем указать, что переменная не установлена ни на что, если мы не можем использовать ноль или пустую строку? Вместо этого мы можем установить значение None. print(None is None) # True В логических (условных) выражениях None воспринима- ется как значение False. Если нам неважно, равна ли наша переменная значению None или содержит какое-то другое значение, равное False, мы можем проверить ее значение следующим образом: if my.stnng: print("Moa строка - это % my.string) В этом случае строка будет выведена, если она не пуста или не содержит значение None. Если нам надо точно опреде- лить, содержит ли переменная именно значение None, необхо- димо выполнить более конкретную проверку: if my_number is not None: print(my.number) # есть вероятность, что равно 0 if my_string is None: print("Строка еще не задана!") elif not my.string: # еще одно ложное значение, т. е. пус- # тая строка print("Строка задана, но она пустая") else: рг1пТ("Моя строка: ,%s'." % my.string)
Замена оператора switch в Python на основе словарей Во многих языках есть оператор switch, который упроща- ет длинные конструкции if...elif .. .else. Он проверяет зна- чение одной переменной и на основе этого значения прини- мает решение. Этот оператор напоминает лестницу условий if, но более нагляден и оптимизирован для ускорения работы программы. В языке Python нет оператора switch, но его функциональ- ность можно реализовать с помощью структуры данных сло- варя. Пример, приведенный ниже, станет понятнее после того, как вы ознакомитесь с разделом, посвященным использова- нию словаря, а пока нужно знать только то, что словарь — это структура данных, которая хранит пары «ключ-значение». Мы можем использовать значение переменной в качестве ключа и выполнять соответствующее действие, извлекая его из словаря подобно тому, как мы получаем элемент списка по индексу. Вот как мы можем переписать фрагмент програм- мы, определяющей полное название курса по его абревиатуре: DEPARTMENT_NAMES = { "CSC": "Компьютерные науки", "МАМ1: "Математика и прикладная математика", "STA": "Статистика ", # В Python допустима запятая # после последнего элемента if course.code in DEPARTMENT.NAMES: # здесь проверяется, # является ли переменная одним из ключей словаря print("Факультет: %s" % DEPARTMENT„NAMEScourse^ code]) else: print("Неизвестный код курса: %s" % course_code)
Функциональность словаря не ограничевается способно- стью хранения в нем простых значений вроде строк. В Python функции также Moiyr храниться в переменных, как и любые другие объекты, поэтому мы можем использовать эту реали- зацию оператора switch для выполнения совершенно разных операторов в ответ на разные значения: def reverse(string): print("%s' в обратном порядке: ^s'." % (string, string Г::-!])) def capitalise(string): print("'%s' в верхнем регистре '%s'." % (string, string.upper())) ACTIONS = { "r": reverse, # для вызова используеся имя функции # без скобок "с”: capitalise, } my_function= ACTIONS[my.action] # получаем функцию # no ключу my_function(my.st ring) # вызываем функцию Тернарный оператор (условное выражение) Python предоставляет краткую форму условного выраже- ния, называемую тернарным оператором. Результат его вы- полнения можно присвоить переменной (то есть он может быть вычислен) — в отличие от операторов if и if-else, ко- торые являются просто операторами, а не выражениями. Его часто называют тернарным оператором, поскольку он состо-
ит из трех операндов (у двоичных операторов — два, а у унар- ных — один). Синтаксис выглядит следующим образом: Инструкция, которая выполняется в случае, если усло- вие Тrue, if условие else инструкция, которая выполняет- ся в случае, если условие False. Например: result = "Сдал" if (score >= 50) else 'Не сдал Это означает, что если значение переменной score не ме- нее 50 баллов, то переменной result присваивается строка Сдал", в противном случае ей присваивается строка "Не сдал". Этот код эквивалентен следующей конструкции if: if score result = else: result = >= 50: " Сдал " "He сдал" Тернарный оператор следует использовать только для про- стых выражений — слишком сложные конструкции с ним ста- новятся трудно читаемыми. Между обычной конструкцией if-else и тернарный оператором нет никакой разницы в бы- стродействии. Упражнение 3 1. Перепишите следующий фрагмент с вложенными кон- струкциями if в виде «лестницы» условий на основе кон- струкции if. .elif..else: if temperature < 0: рг1гц("Ниже нуля')
else: if temperature < 10: print("Очень холодно") else: if temperature < 20: print("Прохладно") else: if temperature < 30: print("Тепло") else: if temperature < 40: print("Жарко") else: print("Слишком жарко) 2. Напишите на языке Python программу, которая выстав- ляет итоговую годовую оценку по успеваемости. Програм- ма должна выполнять следующее: 1. Запросить номер студента. 2. Запросить оценку студента за семинарские занятия. 3. Запросить оценку студента за тест. 4. Рассчитать, достаточно ли высок средний балл сту- дента на данный момент для допуска к экзамену. Если средний балл (среднее арифметическое) за семинар- ские занятия и тесты ниже 40%, студент должен ав- томатически получить оценку F, программа должна вывести эту оценку на экран и завершить работу, не вы- полняя следующие шаги. 5. Запросить оценку студента за экзамен. 6. Рассчитать итоговую оценку студента. Оценки за се- минарские занятия и тесты должны составлять по 25% от итоговой оценки, а итоговый экзамен — оставшие-
ся 50%. Итоговую оценку студента необходимо рассчи- тать и вывести результ в соответствии со следующей таблицей: Взвешенная итоговая Итоговая оцен- оценка ка 80 <= отметка <= 100 А 70 <= отметка <80 В 60 <= отметка <70 С 50 <= отметка <60 D отметка <50 Е
Ответы на упражнения Ответ на упражнение 1 1. if (х > 4)—корректно 2. if х == 2 — корректно (скобки не обязательны) 3. if (у =< 4) — недопустимо (=< не является допусти- мым оператором; должен быть <=) 4. if (у = 5) — недопустимо (= — это оператор присва- ивания, а не оператор сравнения) 5. if (3 <= а)—корректно 6. if (1 - 1) — корректно (результат — 0, логическое значение False) 7. if ((1 - 1) <= 0)—корректно 8. if (name == "James")—корректно 2. Программа выведет на экран: больше чем 3. Последние две строки print выполняются всегда, так как они не входят в блок if (не имеют отступов). 3. 1. В Python необязательно явно сравнивать значение пере- менной с I rue — переменная уже сама по себе интерпретиру- ется как логическое значение:
if а: print("a is true") 2. Если вы присваиваете переменной одинаковое значение в блоке if и else, вынесите это присваивание за пределы ус- ловия. Это позволит затратить на присвоение значения пере- менной одну инструкцию, а не две: if х > 50: b += 1 else: b -= 1 а = 5 Ответ на упражнение 2 1. Программа выведет сообщение "True", если вводимое значение меньше либо равно 5. 2. Программа выведет сообщение "Вы сдали курс.", если absentee.rate (процент пропусков) меньше или равен 5, а оve га1 l_ma г к (итоговая оценка) больше или равна 50. 3. Программа выведет "True" для любого значения х, так как любое значение х, кроме значений, интерпретируемых как False (0, None, False), считается истинным. 4. total > 2 count <= 40 value <= 20.0 or total == 108.0 angle <= 180 or width != 5 (count != 5 or value != 10) and count <= 50 value <= 200 and (value >= 0 or total == 0)
Ответ на упражнение 3 1. if temperature < 0: print('Hnxe нуля") elif temperature < 10: print("Очень холодно") elif temperature < 20: print("Прохладно") elif temperature < 30: print( 'Тепло") elif temperature < 40: print( 'Жарко") else: print( "Слишком жарко”) 2. Вот пример программы: student.number = input("Введите номер студента: ") tutorial_mark = float(input("Введите оценку за семинар: ")) test.mark = float(input('Введите оценку за тест: ")) # Расчет среднего значения семинарской и тестовой работы if (tutorial_mark + test_mark) / 2 < 40: grade = "F" else: exam.mark = float(input("Введите оценку за экзамен: ")) # Вклад в итоговую оценку: семинар - 25%, тест - 25%, # экзамен - 50%
final.mark = (tutorial_mark * 0,25) + (test_mark * 0.25) + (exam_mark * 0.5) if 80 <= final-mark <= 100: grade = "A" elif 70 <= final_mark < 80: grade = "B" elif 60 <= final_mark < 70: grade = "C" elif 50 <= final_niark < 60: grade = "D" else: grade = "E" print("%s получил итоговую оценку: %s‘ % (student_number, grade))
Коллекции Ранее мы уже сталкивались с простыми типами данных в Python — числами, строками и булевыми значениями. Те- перь мы рассмотрим, как можно сгруппировать несколько зна- чений в одну коллекцию — например, в список чисел или сло- варь, который можно использовать для хранения и извлечения пар «ключ-значение». Многие полезные коллекции являются встроенными типами данных в языке Python, и вы будете стал- киваться с ними довольно часто. Списки В Python тип для списков называется list. Это разновид- ность последовательности (sequence) — мы можем использо- вать список для хранения нескольких значений и получать к ним последовательный доступ по позиции (индексу). Спи- сок можно создать с помощью литерала, перечислив значения через запятую В1гутри квадратных скобок [ ]: # список строк animals = [кошка', собака’, 'рыба', 'бизон']. # список целых чисел numbers = [1, 7, 34, 20, 12} # пустой список my„list= []
# список переменных, определенных ранее things = [ one_variable, another.variable, third.variable, # завершающая запятая допустима Как видите, мы использовали существительные во множе- ственном числе для названия большинства идентификаторов списков. Это общепринятое соглашение, и в большинстве слу- чаев его полезно придерживаться. Чтобы получить доступ к элементу списка, используйте имя списка с индексом в квадратных скобках. Индексация в Python начинается с нуля: print(animals[0]) # кошка print(numbers[1 ]) # 7 # Это вызовет ошибку, так как в списке только четыре эле- мента . print(animals[6]) Можно также обращаться к элементам с конца списка: рrint(animals[-1]) # последний элемент - бизон рrint(numbers!-2 ]) # предпоследний элемент - 20 С помощью срезов можно получить подсписок (новый спи- сок, состоящий из части элементов исходного). Синтаксис по- хож на обращение к элементу, но вместо указания единствен- ного индекса в квадратных скобках нужно указать диапазон индексов через двоеточие. Обратите внимание, что наш под- список будет включать элемент на нижней границе, но ис- ключать элемент на верхней границе:
print(animals[1:3]) # ['собака', 'рыба'] print(animals[l:-1]) # ['собака', 'рыба'] Если граница среза совпадает с началом или концом списка, ее можно опустить. Срез, в котором границы не указаны, даст нам копию списка: print(animals[2:]) # ['рыба', 'бизон'] print(animals! :2]) # ['кошка', 'собака'] print(animals!:]) # копия всего списка Мы даже можем включить в срез третий параметр, чтобы указать размер шага: print(animals!::2]) # ['кошка', 'рыба'] Списки — изменяемые объекты (mutable). Это значит, что мы можем изменять их содержимое — заменять элемен- ты, добавлять и удалять их. Список будет динамически менять размер, когда мы добавляем или удаляем элементы — нам не нужно управлять этим самостоятельно: # заменим элемент по индексу animals!3] = 'хомяк" # добавить новый элемент в конец списка animals.append("белка1) # удалить элемент по его индексу del animals[2] Поскольку списки являются изменяемыми, мы можем мо- дифицировать переменную списка, не присваивая ей совер-
шенно нового значения. Помните, что если мы присваиваем одно и то же значение списка двум переменным, обе перемен- ные будут ссылаться на один и тот же список. Любые измене- ния отразятся на обеих этих переменных: animals = ['кошка’, 'собака', 'золотая рыбка', 'кана- рейка ' ] pets = animals # теперь обе переменные ссылаются на один # и тот же список animals.append('aardvark') print(pets) # pets - это все тот же список, что и animals, # и он тоже изменился animals= ['крыса', 'песчанка , 'хомяк'] # теперь мы при- # сваиваем списку animals новый список print(pets) # pets по-прежнему ссылается на старый спи- # сок pets = animals[:] # присваиваем переменной pets *копию* # списка animals animals.append)'aardvark') print(pets) # pets остается неизменным, потому что ссы- # лается на копию, а не на сам исходный список Python позволяет создавать списки с элементами разных типов: my.list = ['cat', 12, 35.8] Чтобы проверить, содержится ли значение в списке, исполь- зуйте операторы принадлежности in и not in: numbers = [34, 67, 12, 29] my_number = 67
if number in numbers: print('%d находится в списке!" % number) my.number = 90 if my.number not in numbers: print("%d нет в списке!" % number) Примечание Операторы in и not in по приоритету находятся между логическими опера- торами (and, or, not) и операторами тождественности (is, is not)- Встроенные функции для списков Существует множество встроенных функций, которые мы можем использовать для работы со списками и другими последовательностями: # длина списка len(animals) # сумма чисел в списке sum(numbers) # есть ли в списке хотя бы одно значение, приводимое # к True? any([1,8,1,е,1]) # все ли эти значения можно привести к значению True? all([1,0,1,0,1]) Объекты списков также содержат полезные методы, которые мы можем вызывать:
numbers = [1, 2, 3, 4, 5] # добавить элемент в конец списка numbers,append(5) # подсчитать, сколько раз значение появляется в списке numbers,count(5) # добавить в конец списка сразу несколько значений numbers.extend([56, 2, 12]) # помогает найти значение индекса элемента в последова- тельности numbers.index(3) # если значение встречается в списке несколько раз, # мы получим индекс первого вхождения numbers.index(2) # если значения нет в списке, мы получим ошибку ValueError! numbers.index(42) # вставить значение по определенному индексу numbers.insert(0, 45) # вставить 45 в начало списка # удалить элемент по его индексу и присвоить его пере- # менной my.number = numbers,рор(0) # удалить элемент по его значению numbers.remove(12) # если значение встречается в списке несколько раз, уда- # ляется только первое встреченное numbers.remove(5)
Если мы хотим отсортировать или изменить порядок сле- дования элементов в списке на противоположный, мы можем либо вызвать метод списка, чтобы изменить его на месте, либо использовать встроенные функции, чтобы вернуть изме- ненную копию списка, оставив оригинал нетронутым: numbers = [3, 2, 4, 1] # Эти функции создают новый список print(sorted(numbers)) print(list(reversed(numbers))) # Исходный список не изменился print(numbers) # Эти методы изменяют список на месте numbers.sort() numbers.reverse() print(numbers) На самом деле функция reversed возвращает генератор, а не список (генераторы мы рассмотрим в следующей гла- ве), поэтому перед тем как вывести его содержимое на экран мы должны преобразовать его в список. Для этого необходи- мо вызвать функцию list (названную ио имени типа дан- ных, к которому происходит преобразование), точно так же, как мы вызываем функции int или float для преобразования чисел. Мы также можем использовать функцию list как еще один способ создания копии списка: animals = ['кошка , 'собака', 'золотая рыбка", 'кана- рейка ' ].
pets = list(animals) animals.sort() pets.append('песчанка') print(animals) print(pets) Использование арифметических операторов co списками Некоторые из арифметических операторов, которые мы ра- нее использовали для чисел, можно применять и к спискам, но результат может быть не всегда таким, как мы ожидаем: # мы можем объединить два списка с помощью оператора + print([1, 2, 3] + [4, 5, 6J) # мы можем объединить список с самим собой, умножив его # на целое число print([1, 2, 3] * 3) # не все арифметические операторы можно использовать # в списках -это приведет к ошибке1 print([1, 2, 3] - [2, 3]). Списки и массивы Во многих других языках нет встроенного типа, который бы вел себя так же, как списки в Python. Для реализации похо- жего типа данных они либо используют сторонние библиоте- ки, либо программисты пишут свои собственные. Однако чаще программисты, использующие эти языки, применяют вместо
них массивы. Массивы — это более простые, низкоуровневые структуры данных, которые не обладают всей функционально- стью списков. Вот некоторые основные различия между спи- сками и массивами: • Массив имеет фиксированный размер, который задается при его создании. Если необходимо добавить или удалить элементы, надо создать новый массив. • Если язык статически типизирован, то при создании мас- сива вам также придется указать один тип для значений, которые будут размещены в массиве. • В языках с примитивными типами массивы обычно не являются объектами, поэтому у них нет методов — это просто контейнеры. Во многих отношениях массивы менее удобны в использо- вании, но у них есть и свои преимущества, поскольку они про- ще реализуются и обрабатываются компьютером значительно быстрее. Это означает, что зачастую использовать массив го- раздо проще, чем объект, который ведет себя как список. Мно- гие программисты применяют их в ситуациях, где важна про- изводительность. В Python также есть свой вариант массива — это встроен- ный тип array, но у него меньше ограничений, чем у масси- вов в С или Java. Несмотря на то, что здесь, как и для любого другого массива, вы также должны указать тип для содержи- мого массива и его можно использовать только для хранения числовых значений, вы можете изменять его размер динами- чески, как список. На практике вам, скорее всего, не придется использовать массивы в Python.
Упражнение 1 1. Создайте список а, содержащий первые три нечетных положительных целых числа, и список bs содержащий первые три четных положительных целых числа. 2. Создайте новый список с, в котором будут объединены числа из обоих списков (порядок неважен). 3. Создайте новый список d, который является отсортн рованной копией с, оставив сам оригинальный список с без изменений. 4. Поменяйте порядок следования элементов в списке d на противоположный без создания нового списка. 5. Установите четвертый элемент в списке с равным 42. 6. Добавьте число 10 в конец списка d. 7. Добавьте числа 7, 8 и 9 в конец списка с. 8. Выведите на экран первые три элемента списка с. 9. Выведите последний элемент списка d, не используя функцию 1еп. 10. Выведите длину списка d. Кортежи Кортеж (tuple) — еще один тип последовательности в Python. Этот тип данных похож на список, но является неиз- меняемым (immutable). Мы определяем литерал кортежа, по-
мещая в круглые скобки ( ) список значений, разделенных запятыми: WEEKDAYS = ('понедельник', 'вторник', среда', чет- верг', 'пятница', 'суббота', воскресенье’) Кортежи можно использовать точно так же, как и списки, за исключением того, что их нельзя изменять: Animals = ('кошка’, собака', 'рыба) # пустой кортеж my.tuple = () # доступ к элементу по индексу print(animals[0]) # срез (результат - тоже кортеж, а не список) print(animals[1:]) # мы можем подсчитать количество вхождений или найти ин- # деке animals.count('cat') animals.index('cat') # это недопустимо: animals.append(’canary’) animals 11] = 'песчанка' Когда использовать кортежи? С их помощью можно создать последовательность значений, которые мы не хотим изме- нять. Например, список названий дней недели никогда не из- менится. Если мы сохраним его в кортеже, то можем быть уве- рены, что он никогда не будет случайно изменен:
# Вот что может произойти, если мы поместим наши дни не- # дели в изменяемый список WEEKDAYS = ['понедельник1, 'вторник', 'среда', 'чет- верг', пятница', 'суббота', 'воскресенье ']. def print_funny_weekday_list(weekdays): weekdays[5] = ’зуббота # исходный список изменится! print(weekdays) ргint_funny_weekday_list(WEEKDAYS) prlnt(WEEKDAYS) # упс Мы уже использовали кортежи в форматировании строк: prlnt("%d %d %d" % (1, 2, 3)) Как определить кортеж с одним элементом? Мы не можем просто поставить круглые скобки вокруг значения, потому что круглые скобки также используются для изменения при- оритета операторов в выражении. Значение в скобках — это просто другой способ записи значения: р rint(3) print((3)) # это все то же число 3, только представлен- ное по-другому Чтобы дать языку Python понять, что мы хотим создать кор- теж, нужно в конце добавить запятую: print((3,))
Упражнение 2 1. Создайте кортеж а, содержащий первые четыре поло- жительных целых числа, и кортеж Ь, содержащий следу- ющие четыре положительных целых числа. 2. Создайте кортеж с, который содержит все числа из а и b в любом порядке. 3. Создайте отсортированную копию кортежа с и сохра- ните ее в d. Выведите третий элемент кортежа d. 4. Выведите на экран три последних элемента кортежа d, не используя функцию, вычисляющую его длину. 5. Выведите на экран длину кортежа d. Множества (sets) Тип множества в Python называется set. Множество — это неупорядоченная коллекция уникальных элементов. Если мы добавим в набор несколько копий одного и того же элемен- та, дубликаты будут автоматически удалены. Чтобы опреде- лить литерал множества, мы помещаем список значений, раз- деленных запятыми, внутрь фигурных скобок { }: animals = {'кошка1, 'собака', 'золотая рыбка', 'канарей- ка', 'кошка'}. print(animals) # 'кошка' встретится только один раз Мы можем выполнять различные операции над множе- ствами:
even_numoers = {2, 4, 6, 8, 10} big_nunibers = {6, 7, 8, 9, 10} # разность: элементы множества big_numbers, которые # не содержатся в множестве even_numbers print(big_numbers - even_numbers) # объединение: все элементы из обоих множеств print(big_numbers| even.numbers) # пересечение: элементы, которые одновременно принадле- # жат обоим множествам print(big_numbers & even_numbers) # симметрическая разность: элементы, присутствующие # ЛИБО в первом, ЛИБО во втором оригинальном множестве, # но не в обоих одновременно. print(big_numbersA even_numbers) Важно отметить, что, в отличие от списков и кортежей, эле- менты множества не упорядочены. Когда мы выводим содержи- мое множества на экран, порядок элементов будет случайным. Если надо обработать содержимое множества в определенном порядке, его сначала нужно преобразовать в список или кор- теж и затем отсортировать: print(animals) рrint(sorted(animals)) Функция sorted возвращает список (объекттипа list). Как создать пустое множество? Для этого необходимо ис- пользовать функцию set. Фигурные скобки (} были зарезер- вированы для создания пустого словаря (тип данных, о кото-
ром мы поговорим в следующем разделе) задолго до появления типа множество. Поэтому пустой набор фигурных скобок — это фактически пустой словарь: # это пустой словарь а = {} # вот так мы создаем пустое множество b = set() Таким же образом мы можем использовать функции list, tuple, diet и даже int, float или str —все они имеют зна- чения по умолчанию, — но, скорее всего, вам редко понадобит- ся это делать. Упражнение 3 1. Создайте множество а, содержащее первые четыре по- ложительных целых числа, и множество Ь, содержащее первые четыре нечетных положительных целых числа. 2. Создайте множество с, которое объединяет все числа, содержащее все элементы из множеств а и Ь. 3. Создайте множество d, которое содержит все числа из множества а, которые не содержатся в множестве Ь. 4. Создайте множество е, которое содержит все числа из множества Ь, которые не содержатся в множестве а. 5. Создайте множество f, которое содержит все числа, входящие как в множество а. так и в множество Ь.
6. Создайте множество д, содержащее все числа, которые входят либо в множество а, либо в множество Ь, но не вхо- дят в оба этих множества. 7. Выведите количество элементов в множестве с. Диапазоны Range — это неизменяемая последовательность целых чи- сел. Он очень специфичен — его используют для создания ди- апазонов целых чисел. Диапазоны также являются генера- торами. Подробнее о генераторах мы узнаем в следующей главе, а пока нам достаточно знать, что числа в диапазоне ге- нерируются по одному, по мере необходимости, а не все сразу и поэтому вся последовательность не хранится в памяти це- ликом. В приведенных ниже примерах мы преобразуем каж- дый диапазон в список, чтобы все числа были сгенерированы и мы смогли бы их вывести на экран: # выведите целые числа от в до 9 print(list( rartge(10))) # выведите последовательно целые числа в диапазоне от 7 # до 10 рrlnt(list(range(1, 11))) # выведите последовательно нечетные целые числа в диа- пазоне от 1 до 10 print(list(range(1, 11, 2)))
Здесь мы создаем диапазон чисел, вызывая функцию range. Как видите, если мы передаем функции range один параметр, то он используется в качестве верхней границы. Если мы используем два параметра, то первый — это нижняя граница, а второй — верхняя. Если мы используем три пара- метра, то третий параметр — это размер шага. По умолчанию нижняя граница равна нулю, а размер шага по умолчанию равен единице. Обратите внимание, что диапазон включает в последовательность чисел нижнюю границу и исключает из нее верхнюю. Упражнение 4 1. Создайте диапазон а, который начинается с 0 и содер- жит 20 чисел. 2. Создайте диапазон b от 3 до 12 включительно. 3. Создайте диапазон с, который содержит каждое третье целое число, начиная с 2 и заканчивая 50. Словари В Python тип данных для хранения пар «ключ-значение» называется diet (словарь). Чтобы объявить словарь, мы за- ключаем список пар «ключ-значение», разделенных запяты- ми, в фигурные скобки. Внутри каждой пары ключ отделяет- ся от значения с помощью двоеточия (:). Доступ к значениям в словаре осуществляется так же, как и в списках или корте- жах, но вместо числового индекса мы используем ключ:
marbles = {"красный1: 34, "зеленый': 30, 'коричневый": 31, "желтый": 29 } # marbles (русск. шарики) personal_details = { "name": "Jane Doe", "age": 38, # запятая в конце разрешена } print(marbles["зеленый"]) print(personal_details[ 'name ' ]) # Эта инструкция приведет к ошибке KeyЕг го, потому что # в словаре нет такого ключа. print(marblesf"синий"]) # изменить значение marbles!"красный"]+= 3 personal_details["name"]= "Jane Q. Doe" В словаре ключами могут быть не только строки. В качестве ключа допустимо использовать любой неизменяемый (хеши- руемый) тип данных — например, числа, или даже кортежи. Это делает словари особенно гибким инструментом. В слова- ре можно комбинировать любые типы даных как для ключей, так и для значений. Важно помнить: ключи в словаре уникаль- ны. Если при создании или изменении словаря вы указываете ключ, который уже существует, его значение будет перезапи- сано новым. Это не вызовет ошибку — Python просто заменит старое значение. Также обратите внимание: при добавлении новой пары «ключ-значение» вовсе не требуется заранее создавать ключ. Если указанный ключ отсутствует в словаре, он будет создан автоматически:
battleship.guesses = { (3, 4): False, (2, 6): I rue, (2, 5): True, } surnames = {} # это пустой словарь surnames [Джон] = "Смит" surnames!"Джон”] = "Доу" print(surnames) # мы переписали старую фамилию marbles - {"красный”: 34, 'зеленый': 30, "коричневый": 31, "желтый": 29 } marbles["синий']= 30 # корректно marbles["пурпурный"] += 2 # это приведет к ошибке - опе- # ратору инкремента нужно наличие хоть какого-то значе- # ния, чтобы можно было его изменить! Как и множества, элементы в словаре не упорядочены — если мы распечатаем словарь, порядок вывода будет случай- ным. Вот некоторые часто используемые методы для работы со словарями: marbles = {"красный": 34, "зеленый : 30, "коричневый": 31, "желтый": 29 } # Получение значения по его ключу. Вернет None, если оно # не существует marbles.get("orange") # Мы можем изменить значение по умолчанию. marbles.get("оранжевый", 0)
# Добавление в словарь сразу нескольких элементов marbles.update({'оранжевый": 34, "синий": 23, 'фиолето- вый": 36}) # выводим все ключи в словаре marbles.keys() # выводим все значения в словаре marbles.value() # выводим все элементы словаря marbles.ltems() Последние три метода словаря — keys(), values() и items () — возвращают специальные объекты, которые называются представлениями (views). Эти объекты похожи на последовательности, но не совсем являются ими. Главное, что нужно о них знать: они предназначены только для чте- ния — напрямую изменять их нельзя. Однако эти представления обладают важным свойством: они автоматически отражают все изменения, происходящие в словаре. Если вы добавите, удалите или измените элемент в словаре, соответствующее представление тут же обновит- ся — вручную ничего делать не нужно. Чаще всего методы keys(), values() и items() исполь- зуются, когда нужно выполнить обход (итерацию) по ключам, значениям или парам «ключ-значение». Мы подробно рассмо- трим это в следующей главе. При необходимости представле- ние можно преобразовать в обычный список или другой тип последовательности, если нам это необходимо. Мы можем проверить, содержится ли в словаре определен- ный ключ, используя операторы in и not in: print("фиолетовый" in marbles) print("белый ' not in marbles)
Мы также можем проверить, содержится ли искомое значе- ние в словаре, используя оператор in в сочетании с методом values: print('Смит" in surnames.values()) Однако не стоит использовать способ mykey in mydict .keys() для проверки принадлежности ключа словарю, поскольку это менее эффективно, чем mykey in mydict. Примечание Обратите внимание: в Python 2 методы keys(), values() и items() воз- вращают копии списков соответствующих последовательностей. Методы iterkeys(), itervalues() и iteritems() возвращают объекты-итерато- ры, a viewkeys(), viewvalues() и viewitems() - объекты-представления (views), которые по умолчанию используются в Python 3 Однако эти методы с префиксом view- доступны только начиная с Python 2 7. Кроме того, в Python 2 не рекомендуется использовать конструкцию mykey in mydict.keys() для проверки наличия ключа в словаре. Дело в том, что mydict .keys() возвращает список всех ключей, и оператор in будет выполнять последо- вательный перебор этого списка, что значительно медленнее, чем прямой поиск по словарю mykey in mydict, который выполняется за константное время (0(1)). Упражнение 5 1. Создайте словарь directory, который будет хранить телефонные номера (в виде строковых значений), и за- полните его следующими парами «ключ-значение»: Имя Номер телефона Джейн Доу +27 555 5367
Имя Номер телефона Джон Смит +27 555 6254 Боб Стоун +27 555 5689 2. Измените номер Джейн на +27 555 1024. 3. Добавьте новую запись для человека по имени Анна Ку- пер с номером телефона +27 555 3237. 4. Выведите на экран номер Боба. 5. Выведите на экран номер Боба таким образом, чтобы в случае отсутствия ключа в словаре было выведено None. 6. Выведите на экран все ключи. Формат вывода не име- ет значения, главное — чтобы были выведены все ключи. 7. Выведите все значения, содержащиеся в словаре. Преобразование между коллекциями Неявные преобразования Если мы пытаемся перебрать элементы коллекции в цикле for (об этом подробнее в следующей главе), Python попыта- ется автоматически преобразовать объект во что-то, по чему мы можем выполнить итерацию. Например, возвращаемые словарем представления (views), такие как diet.keys() или diet.items(), сами по себе не являются итераторами,
но Python знает, как превратить их в итераторы, так что мы мо- жем использовать их в цикле for напрямую. Иногда итератор, который мы получаем в результате ав- томатического преобразования, по умолчанию может быть не таким, как мы ожидали. Если мы выполняем проход по элементам словаря, по умолчанию мы получим только ключи. Если же нам нужны значения или пары «ключ-зна- чение», необходимо явно использовать функции values() или items(). Явные преобразования Мы можем легко преобразовывать одни последовательно- сти в другие с помощью функций list, set, tuple — анало- гично тому, как преобразуем числа с помощью int и float: animals = ['кошка', 'собака1, ’золотая рыбка', ’канарей- ка’, 'кот'] . animals.set = set(animals) animals_unique_list = list(animals_set) animals_unique„tuple = tuple(animals_unique_list) Словари требуют большего внимания при преобразовании в последовательности — нужно решить, хотим ли мы полу- чить ключи, значения или пары «ключ-значение»: marbles = {красный": 34, "зеленый": 30, 'коричневый": 31, "желтый": 29 } colours = list(marbles) # по умолчанию из словаря будут # взяты ключи
counts= tuple(marbles.values()) # используем представление # значений marbles_set= set(marbles.items()) # из словаря будут извле- # чены пары «ключ-значение» в виде кортежей В последнем случае мы преобразуем пары «ключ-значение» словаря в последовательность, где каждая пара будет преобра- зована в кортеж из двух элементов, содержащий ключ, за кото- рым следует значение. Мы также можем преобразовать последовательность в сло- варь, но только если это последовательность пар, где каждая пара должна быть последовательностью с двумя значениями: # Не сработает - элементы не являются парами dict([1, 2, 3, 4]) # 4 вот так - сработает dict([(1, 2), (3, 4)]) Мы вернемся к теме преобразований в следующей главе, когда изучим генераторы (comprehensions) — компактный синтаксис для фильтрации и преобразования коллекций. Подробно о строках Строки — это тоже последовательности, состоящие из сим- волов. Исходя из этого, они также обладают свойствами, при- сущими другим последователям. Например, мы можем найти длину строки или индекс символа в строке, а также получать отдельные символы или срезы:
s = "abracadabra" print(len(s)) print(s.index("а")) print(s[0]) print(s[3:5]) Помните, что строки неизменяемы — нельзя изменить сим- вол по индексу: # приведет к ошибке s[0] = "b" Оператор in имеет особое поведение при применении к строкам: позволяет искать как отдельные символы, так и под- строки: print('a1 in 'abed') # True print(1ab' in 'abed1) # также True # для списков это не работает print(['а', ' b'] in ['а', 'b', 'с', ' d']) # False Мы можем легко преобразовать строку в список символов: abc_list= list("abracadabra") Что если требуется преобразовать список символов в стро- ку? Если применить функцию str к списку, то мы получим строку списка в не очень приглядном виде, поскольку она бу- дет включать в себя запятые, кавычки и скобки. Чтобы объе- динить последовательность символов (или более длинные
строки) в одну строку и исключить лишние символы, нужно использовать функцию join. Несмотря на то, что join работает с последовательностями, сам он не является функцией или методом списка. Это метод объекта типа st г. Он вызывается для строки, которая будет ис- пользоваться как разделитель, и принимает в качестве аргу- мента последовательность строк, которые нужно объединить. Проще говоря, строка, на которой вызывается join, опреде- ляет, что будет вставляться между элементами при склеива- нии. А сама последовательность (обычно список строк) — это то, что будет склеено. Например, чтобы объединить в строку список одиночных символов без пробелов между ними, мы вы- зываем метод j о in на пустой строке: ’с', 'a', 'd', 'а', ’Ь‘, 'г', s = "".join(l) print(c) Если вместо этого мы хотим вставить, скажем, пробел или за- пятую, мы просто используем нужную строку в качестве разде- лителя: animals = (кошка’, 'собака , 'рыба') # список, разделенный пробелами print(" ".join(animals)) # список, разделенный запятыми print(",".joln(animals)) # список, разделенный запятыми, с пробелами print(", ".join(anlmals))
Противоположностью объединения является разбиение. Мы можем разбить строку на список подстрок с помощью метода split. Если метод split вызывать без параметров, он разделит строку на слова, используя в качестве раздели- теля любое количество последовательных символов пробела. Мы можем использовать дополнительные параметры, чтобы указать другой разделитель, а также ограничение на макси- мальное количество выполняемых разбиений: print("кошка собака рыба\п".split()) print("кошка|собака|рыба".split(" |")) print('кошка, собака, рыба".split(", ")) print("кошка, собака, рыба".split(", ", 1)) Упражнение 6 1. Преобразуйте список, содержащий числа 1,1, 2, 3 и 3, в кортеж а. 2. Преобразуйте кортеж а в список Ь. Выведите его длину. 3. Преобразуйте список b в множество с. Выведите его длину. 4. Преобразуйте множество с в список d. Выведите его длину. 5. Создайте диапазон от 1 до 10 и преобразуйте его в спи- сок е. 6. Создайте словарь directory из предыдущего примера. Создайте список t, содержащий пары «ключ-значение» в виде кортежей.
7. Создайте список v, содержащий все значения словаря. 8. Создайте список к, содержащий все ключи словаря. 9. Создайте строку s. которая должна содержать слово "antidisestablishmentarianism". Отсортируйте ее с помощью функции sorted. Какой тип данных получит- ся? Объедините символы в строку s2. 10. Разбейте строку "the quick brown fox jumped over the lazy dog" и преобразуйте результат в список отдельных слов w. Двумерные последовательности Большинство последовательностей, с которыми мы до сих пор работали, были одномерными: каждая последователь- ность — это строка элементов. Но что если нам понадобится использовать последовательность для представления двумер- ной структуры данных, в которой есть и строки, и столбцы? Самый простой способ — создать последовательность, в кото- рой каждый элемент также является последовательностью. Например, мы можем создать список списков: my_table = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], [10, И, 12],
Внешний список состоит из четырех элементов, в свою оче- редь, каждый из них представляет собой список из трех чисел. Чтобы получить доступ к одному из этих чисел, нам нужно ис- пользовать два индекса — один для внешнего списка, а другой для вложенного: print(my_table[0j[0]) # Можно менять значения, т. к. списки изменяемы my_table[0][0] = 42 Похожий пример мы уже видели в предыдущей главе, ког- да создавали список кортежей для преобразования в словарь. Когда мы используем двумерную последовательность для представления табличных данных, обычно предполага- ется, что каждая внутренняя последовательность (например список) имеет одинаковую длину. Это соответствует идее пря- моугольной таблицы, где каждая строка содержит одинако- вое количество столбцов. Однако в Python это не обязательно. Ничто не мешает вам создать двумерную структуру, в кото- рой вложенные последовательности имеют разную длину. Та- кая структура уже не будет «прямоугольной», но тем не менее останется допустимой с точки зрения языка: my_2d_list = [ [0]. [1, 2, 3, 41, [5, 6], I Мы можем пойти еще дальше и создать трехмерную по- следовательность, вложив списки внутрь списков, которые, в свою очередь, тоже вложены в другие списки:
my_3d_list = [ [[1. 2], [3, 4]], [[5, 6], [7, 8]], print(my_3d_list[0][0][0]) Конечно, мы можем создать список списков списков спи- сков и так далее — в Python допустимо сколько угодно уровней вложенности. Структуры можно вкладывать друг в друга прак- тически без ограничений. Если мы хотим создать двумерный список, чтобы пред- ставить, например, расписание на неделю, у нас есть два ва- рианта: внешний список может представлять дни недели, а внутренний — временные интервалы, либо наоборот — внешний список содержит интервалы, а внутренний — дни. Важно определиться, какое измерение будет строками, а ка- кое — столбцами, и придерживаться этого выбора при работе с таблицей. Предположим, мы хотим инициализировать таблицу так, чтобы в каждой ячейке была пустая строка "" (например, до того как мы внесем расписание). Если расписание должно охватывать 24 часа в каждом дне, то нам нужно создать семь списков по 24 элемента каждый. Это довольно громоздко, если все делать вручную, используя литералы — так, как мы созда- вали небольшие списки в предыдущих примерах. И здесь мы сталкиваемся с типичной ошибкой. Возмож- но, вы помните из предыдущего раздела, что в Python можно умножать списки на число. Это действительно удобно, когда нужно создать длинный список, все элементы которого оди- наковы:
my_long_list = [0] * 100 # длинный список из нулей print(my_long_list) Скорее всего, вы сейчас подумали, почему бы не использо- вать этот метод для построения расписания, например, создав список пустых строк, представляющих день: day = [""] * 24 print(day) Но что произойдет, если мы повторим день семь раз, чтобы получилась неделя? timetable = day * 7 print(timetable) Пока все выглядит хорошо, так в чем же проблема? Давайте посмотрим, что произойдет, если мы попытаемся запланиро- вать встречу на вторую половину дня в понедельник: timetable [0][15] = print(timetable) 'Встреча с Джейн' В результате у нас окажется запланирована одна и та же встреча после обеда на каждый день! Как это произошло? Ког- да мы умножили наш список дней на семь, мы заполнили наше расписание одним и тем же объектом списка, повто- ренным семь раз. Все элементы нашего расписания ссылаются на один и тот же объект day, поэтому независимо от того, ка- кой из них мы изменяем, мы изменяем их все сразу. Это один из подводных камней при работе со списками в Python: опера- тор умножения не создает копии вложенных списков, он копи- рует только ссылки на них.
Почему это не имело значения, когда мы составляли днев- ной список, умножая одну и ту же пустую строку 24 раза? Потому что строки неизменяемы. Мы можем изменить зна- чения строк в списке дней, только присвоив им новые значе- ния — мы не можем модифицировать их на месте, поэтому не имеет значения, что все они ссылаются на один и тот же строковый объект. Но списки обладают способностью к мо- дификации и мы можем их изменять, и поэтому то, что все наши списки дней ссылаются на один и тот же список, имеет большое значение. На самом деле в нашем расписании нам нужно создать семь копий списка day, каждый из которых со- держит 24 пустые строки: timetable = [[""] * 24 for day in range(7)] Теперь timetable — это настоящий двумерный список раз- мером 7 х 24, который мы создали с помощью генератора спи- сков, и все вложенные списки независимы. Мы можем безо- пасно менять отдельные ячейки расписания, не затрагивая другие строки или столбцы. Детальнее мы разберемся со спи- сками в следующей главе, а пока важно знать, что этот метод создает новый список пустых строк для каждого дня, в отличие от оператора умножения. Упражнение 7 1. Создайте список а, содержащий три кортежа. Первый кортеж должен содержать один элемент, второй — два элемента, а третий —три элемента. 2. Выведите второй элемент второго кортежа списка а.
3. Создайте список Ь, содержащий четыре списка, каж- дый из которых содержит четыре элемента. 4. Выведите последние два элемента первого вложенного списка из Ь.
Ответы на упражнения Ответ на упражнение 1 а = [1, 3, 5] b = [2, 4, 6] с = а + b d = sorted(c) d.reverse() c[3] = 42 d.append(l0) d.extend([7, 8, 9]) print(c[:2]) print(d[-1 ]) print(len(d)) Ответ на упражнение 2 a = (1, 2, 3, 4) b = (5, 6, 7, 8) c = a + b d = sorted(c) p r int(d[3])
print(d[-3:]) print(len(d)) Ответ на упражнение 3 а = {1, 2, 3, 4} b = {1, 3, 5, 7} с = а | b d = а - b е = b - а f = а & Ь g = а л b print(len(c)) Ответ на упражнение 4 а = range(20) b = гапде(3, 13) с = гапде(2, 51, 3) Ответ на упражнение 5 directory = { “Джейн Доу": "+27 555 5367", “Джон Смит": "+27 555 6254", “Боб Стоун": "+27 555 5689", } directory["Джейн Доу"] = "+27 555 1024" directory!"Анна Купер"] = "+27 555 3237" print(directoryI"Боб Стоун"])
print(directory.get("Боб Стоун", None)) ргint(di rectory.keys()) print(di rectory.values()) Ответ на упражнение 6 a = tuple([1, 1, 2, 3, 3]) b = list(a) print(len(b)) c = set(b) print(len(c)) d = list(c) print(len(d)) e = list(range(1, 11)) directory = ( "Джейн Доу": "+27 555 5367", "Джон Смит": "+27 555 6254", "Боб Стоун": "+27 555 5689", } t = list(directory.items()) v = llst(directory.values()) k = list(directory) s = "antidisestablishmentarianism". s2 = "".join(sorted(s)) w the quick brown fox jumped over the lazy dog".split()
Ответ на упражнение 7 Вог пример кода: а = [ (Ъ), (2, 2), (3, 3, 3), print(а[1 ] [ 1 ]) b = [ list(range(10)), list(range(10, 20)), list(range(20, 30)), list(range(30, 40)), print(bf 0][1 :-1 ])
Операторы управления циклом Введение В этой главе вы узнаете, как заставить компьютер выпол- нять группу операторов снова и снова, пока соблюдается опре- деленное условие. Группа операторов, выполняющихся много- кратно, называется циклом. В Python два оператора цикла: for и while. Разницу между ними мы рассмотрим позже, а пока давайте посмотрим на пример цикла из реальной жизни. При обслуживании клиента служащий автозаправочной станции выполняет следующие действия: • приветствует клиента • спрашивает, какой нужен тип топлива, и его количество • уточняет, требуются ли клиенту дополнительные услуги • просит требуемую сумму денег • передает деньги кассиру • ждет сдачу и чек • выдает сдачу и чек клиенту • благодарит и прощается Он выполняет эти шаги для каждого клиента, но не делает этого, если нет клиента, которого нужно обслужить или если сейчас не его смена. Если бы мы захотели смоделировать это поведение на компьютере, недостаточно было бы просто за- дать последовательность шагов и попросить компьютер по- вторять их снова и снова. Мы также должны указать, когда сле- дует прекратить выполнение цикла.
Существует два основных типа программных циклов: цикл со счетчиком (counting loop) и цикл с предусловием. В первом случае компьютер заранее знает, сколько раз нужно выпол- нить тело цикла. В языке Python такой цикл реализуется с по- мощью оператора for, который перебирает элементы некото- рой последовательности (например, списка или диапазона). В цикле с предусловием компьютер завершает выполне- ние цикла, когда условие перестает быть истинным. Для это- го в Python используется оператор while, который выполняет тело цикла, пока условие истинно. Оператор while проверя- ет условие перед каждой итерацией. В некоторых языках про- граммирования есть цикл, проверяющий условие после вы- полнения тела (такой цикл гарантированно выполняется хотя бы один раз). В Python такой конструкции нет, но позже мы по- кажем, как ее имитировать. На самом деле цикл со счетчиком — это частный случай цикла с предусловием, где условие зависит от количества уже выполненных итераций (цикл повторяется до тех пор, пока не будет достигнуто необходимое количество итераций). Если вы хотите добраться из Кейптауна в Кампс-Бей, какой алгоритм действий вы бы использовали? Можно восполь- зоваться разными стратегиями. Если бы вы начали с того, что подъехали на машине к дороге в Кампс-Бей, то дальше можно было бы действовать так: • Проехать по этой дороге ровно 15 минут, затем остановить- ся и выйти из машины. • Проехать по этой дороге 8 км. Проехав 8 км, остановиться и выйти из машины. • Ехать до тех пор, пока вы не окажетесь в Кампс-Бей, и толь- ко тогда остановиться.
Первые два случая — это алгоритмы со счетчиком: в первом вы считаете время, во втором — расстояние. Ни один из этих алгоритмов не гарантирует, что вы прибудете в Кампс-Бей. В первом случае вы можете либо попасть в пробку и не дое- хать, либо проскочить нужный пункт назначения. Во втором случае вы можете найти объездную дорогу и оказаться рядом с Кампс-Бей, но не в Кампс-Бей. Третий алгоритм — цикл с предусловием. Вы продолжаете ехать до тех пор. пока не окажетесь на пляже. Вы постоянно проверяете условие: «Яуже на пляже?». Многие действия в реальной жизни контролируются усло- виями. Например, вы будете пить воду до тех пор, пока испы- тываете жажду. Вы читаете газету до тех пор, пока вам это инте- ресно. Некоторые виды деятельности основаны на нескольких условиях — например, рабочий трудится до тех пор, пока есть работа и не наступил конец рабочего дня. Оператор while В Python оператор цикла с условием — это оператор while. Его следует использовать, когда вам заранее неизвестно, сколь- ко раз придется выполнить тело цикла. Тело цикла while по- вторяется до тех пор, пока условие истинно. Цикл состоит из трех важных частей: инициализации, усло- вия и обновления. На этапе инициализации вы задаете пере- менную, которая будет использоваться в условии. На этапе ус- ловия выполняется проверка переменной, чтобы определить, следует ли завершить цикл или следует выполнить тело цик- ла еще раз. Затем, после каждого успешно завершенного вы- полнения тела цикла, вы обновляете переменную.
Обратите внимание, что условие проверяется перед первым выполнением тела цикла — если условие ложно в начале, тело цикла вообще не будет выполнено. Вот простой пример на Python, который находит сумму пер- вых десяти целых чисел: total = 0 i = 1 while i <= 10: total += i i += 1 Переменная, используемая в условии цикла, — это число i, которое используется для подсчета целых чисел от 1 до 10. Сначала вы инициализируете переменную числом 1. В усло- вии вы проверяете значение переменной 1, меньше или рав- но оно 10, и если это так, то выполняете тело цикла. Затем, в конце тела цикла, вы обновляете переменную i, увеличи- вая ее значение на 1. Очень важно, чтобы вы увеличивали i именно в конце цик- ла. Если этого не сделать, значение i всегда будет равно 1, усло- вие всегда будет истинным, и программа никогда не завершит- ся — это называется бесконечным циклом. Всякий раз, когда вы пишете цикл while, убедитесь, что значение переменной, которую вы используете в условии, обновляется внутри тела цикла! Вот несколько типичных ошибок, которые могут привести к бесконечному выполнению цикла: х = 0 while х < 3: у += 1 # обновляется не та переменная
product = 1 count = 1 while count <= 10: product *= count # забыли обновить переменную count X = 0 while x < 5: print(x) x += 1 # инструкция с обновлением находится вне тела цик- # ла х = 0 while х != 5: Drint(x) х += 2 # х никогда не будет равен 5, потому что всег- # да будет четным числом! Возможно, вы задаетесь вопросом: почему интерпретатор Python не может обнаружить такие бесконечные циклы? В тео- рии алгоритмов такое явление известно как проблема останов- ки. Она состоит в том, что в общем случае невозможно опреде- лить, завершится ли произвольная программа. Таким образом, ответственность за предотвращение бесконечных циклов ле- жит на самом программисте. Во многих примерах, приведенных выше, мы заранее зна- ли, сколько итераций надо было выполнить — в таких случа- ях удобнее использовать цикл for, который будет рассмотрен в следующем разделе — именно эта конструкция цикла чаще всего используется в Python для циклов со счетчиком. Напри- мер:
# numbers - список чисел, значения нам заранее # неизвестны! total = 0 i = 0 while i < len(numbers) and total < 100: total += numbers[i] i += 1 Здесь мы суммируем элементы списка до тех пор. пока сум- ма не превысит значение 100. Количество необходимых ите- раций заранее неизвестно, ведь мы не знаем значения эле- ментов. Обратите внимание: если сумма не достигнет 100, но список закончится, и мы попытаемся обратиться к несуще- ствующему элементу, возникнет ошибка. Поэтому мы добави- ли проверку 1 < len(numbers), чтобы этого не произошло. Упражнение 1 1. Напишите программу, которая с помощью цикла while суммирует квадраты целых чисел, начиная с 1, до тех пор, пока сумма не превысит 200. Выведите итоговую сумму и число, квадрат которого был добавлен последним. 2. Напишите программу, которая запрашивает у пользо- вателя секретное слово, позволяя сделать не более десяти попыток. Напишите код так, чтобы можно было легко из- менить как загаданное слово, так и количество попыток. Программа должна обеспечить пользователю обратную связь после каждой попытки.
Оператор for Второй оператор циклов в Python — это for. Его следует ис- пользовать, когда вы заранее знаете, сколько итераций нужно выполнить. Прежде чем мы рассмотрим синтаксис цикла for в Python, вкратце рассмотрим, как работают циклы for в дру- гих языках. Вот пример цикла for в Java: for (int count = 1; count <= 8; count ++ ) { System.out.println(count); } Можно заметить, что этот тип цикла for имеет много обще- го с циклом while — фактически можно сказать, что это про- сто частный случай цикла while. Такой цикл включает три части — инициализацию, условие и обновление — аналогич- но циклу while, но все записывается в одной строке. Циклы for часто применяются, когда нужно выполнить од- нотипное действие для каждого элемента последовательно- сти — например, для каждого элемента списка, строки или ди- апазона чисел. В других языках программирования цикл for обычно требует от нас управления счетчиком вручную: мы должны задать начальное значение, условие завершения и шаг, а затем обращаемся к элементам по индексу. В Python все проще. Здесь цикл for чаще всего используется именно для прямого перебора элементов — без необходимости отслеживать индексы или явно управлять счетчиком. Python берет на себя всю «рутинную работу» — вы просто указываете, но чему хотите пройтись. Вот пример цикла for, который по- следовательно выводит числа от 1 до 8: for i in range(1, 9): print(i)
Как вы уже знаете, range — это неизменяемый тип после- довательности, который используется для получения диапазо- нов целых чисел. В приведенном выше примере range (1, 9) создает последовательность чисел от 1 до 8 включительно (поскольку верхняя граница не входит в диапазон). Цикл for в Python перебирает все значения из этой последовательности по одному, выполняя заданные действия для каждого из них. В нашем случае это вывод числа на экран. Как только все зна- чения будут перебраны, цикл завершится. Однако цикл for работает не только с range. Вы можете ис- пользовать его с любой последовательностью — например, со списками, строками, кортежами и даже словарями. Рассмотрим пример — перебор списка строк: pets = ["кошка", собака", "волнистый попугайчик"] for pet in pets: print(pet) Во время каждого прохода цикла очередной элемент списка pets присваивается переменной pet, к которой вы можете об- ратиться внутри тела цикла. Пример выше функционально идентичен этому: for г in range(len(pets)): # переменная i последовательно # принимает значения 0, 1,2 pet = pets[i] print(pet) Это похоже на то, как пишутся циклы for, например, в язы- ке Java. В Python такая организация циклов считается менее предпочтительной из-за снижения читаемости. Если по ка- кой-то причине вам нужно получать внутри цикла и индекс,
и значение элемента списка, вы можете использовать функ- цию enumerate: for i„ pet in enumerate(pets): pets[i] = pet.upper() # преобразуем содержимое эле- # мента списка к верхнему регистру Как и range, enumerate также возвращает итератор, кото- рый генерирует и возвращает кортежи, представляющие со- бой пару (индекс, элемент), где первое значение — это индекс элемента (начиная с нуля), а второе — сам элемент. В приве- денном выше цикле на каждой итерации значение индекса присваивается переменной 1, а сам элемент, как и в примере выше, присваивается переменной pet. Почему мы не можем просто написать pet = pet. upper ()? Потому что это изменит только локальную переменную pet, а не сам элемент списка. Это подводит нас к распространенной ошибке, связанной с циклом for: изменение списка во время его итерации. Удале- ние элементов во время обхода изменяет индексы оставших- ся элементов, из-за чего при дальнейшей работе цикла можно пропустить элементы или получить ошибку из-за попытки до- ступа к несуществующему элементу: numbers = [1, 2, 2, 3] for i, num in enumerate(numbers): if num == 2: del numbers[i] print(numbers) # Упс - один элемент пропущен, поскольку # во время итерации мы сдвинули элементы!
Иногда такой ситуации можно избежать, выполнив итера- цию по копии списка. Однако в нашем случае это не поможет, поскольку мы здесь изменяем исходный список. Б этом случае индексы копии перестают соответствовать длине оригиналь- ного списка, и мы получим ошибку. В общем случае, если вам понадобится выбрать подмноже- ство элементов из списка на основе какого-либо условия, сле- дует воспользоваться генератором списка. Мы рассмотрим их в конце этой главы. Упражнение 2 1. Напишите программу, которая находит сумму целых чисел от 1 до 10 с помощью цикла for (и в конце выводит результат). 2. Можете ли вы придумать способ, как это сделать без ис- пользования цикла? 3. Напишите программу, которая находит факториал за- данного числа. Например, факториал 3, или 3! равен 3x2 х 1; 5! равен 5х4хЗх2х1ит. д. Ваша программа должна содержать только один цикл. 4. Напишите программу которая запрашивает у пользо- вателя 10 чисел с плавающей точкой и вычисляет их сум- му, произведение и среднее значение. Используйте толь- ко один цикл. 5. Перепишите предыдущую программу так, чтобы в ней было два цикла — один собирает и хранит числа, а другой их обрабатывает.
Вложенные циклы В предыдущей главе мы видели, что можно создавать много- мерные последовательности — последовательности, в которых каждый элемент является еще одной последовательностью. Как перебрать все значения многомерной последовательно- сти? Для этого надо использовать циклы внутри других ци- клов. В этом случае мы имеем дело с вложенными циклами. Рассмотрим пример с расписанием из предыдущей главы — допустим, расписание состоит из семи дней, а каждый день в свою очередь содержит 24 временных интервала. Каждый тайм-слот — это строка, которая пуста, если ничего не запла- нировано. Как вывести все запланированные события? # сначала давайте определим названия дней недели WEEKDAYS = ('понедельник', 'вторник', среда', 'чет- верг', 'пятница', 'суббота1, 'воскресенье’) # теперь мы перебираем каждый день в расписании for day in timetable: # и в каждом дне перебираем тайм-слоты for 1, event in enumerate(day): if event: # если временной слот не является пус- # той строкой print("%s at %02d:00 -- %s" % (WEEKDAYS[day], i, event)) Обратите внимание, что у нас здесь два цикла for — внутренний цикл будет выполняться один раз на каждом шаге итерации внешнего цикла. Также обратите внима- ние, что мы используем функцию enumerate при переборе тайм-слотов — потому что нам нужен как индекс каждого вре- менного интервала (чтобы можно было вывести час), так и со- держимое этого интервала.
Вы, наверное, заметили, что мы ищем название дня недели один раз за каждую итерацию внутреннего цикла — но назва- ние меняется только один раз за каждую итерацию внешнего цикла. Мы можем сделать наш цикл немного эффективнее, пе- ренеся этот поиск из внутреннего цикла, чтобы он выполнялся только семь раз, а не 168! for day in timetable: day„name = WEEKDAYS[dayI for i, event in enumerate(day): if event: print("%s в %92d:09 -- %s" % (day.name, i, event)) Это может не иметь большого значения при доступе к зна- чению в коротком кортеже, но может существенно повлиять на производительность, если речь идет о ресурсоемких, про- должительных вычислениях, выполняемых многократно — например, при итерации по сотням или тысячам значений. Упражнение 3 1. Напишите программу, которая с помощью вложен- ных циклов for создает трехмерный список, представ- ляющий календарь. Верхний уровень должен быть спи- ском из 12 месяцев, каждый месяц — списком из 4 недель, а каждая неделя — пустым списком. 2. Модифицируйте программу так, чтобы доступ к ме- сяцам можно было осуществлять по названию месяца, а к каждой неделе — по номеру, начиная с 1. Добавьте со- бытие (строкой-описанием) во вторую неделю июля.
Итерируемые объекты, итераторы и генераторы В Python любой тип, элементы которого можно перебрать с помощью цикла f о г, является итерируемым. Списки, корте- жи, строки и массивы — все это часто используемые итериру- емые типы. Итерация по списку или кортежу просто означает поочередную обработку каждого элемента. Иногда нам нужно хранить последовательность значений, не подчиняющихся определенному правилу: каждое значе- ние заранее неизвестно и не может быть вычислено «на лету». В таких случаях у нас нет другого выбора, кроме как хранить каждое значение в списке или кортеже. Однако если таких зна- чений очень много, это может привести к высокому потребле- нию памяти. Если же значения можно вычислять по какому-либо прави- лу, гораздо эффективнее не хранить их заранее, а вычислять по мере необходимости. Мы можем сэкономить значительный объем памяти, вычисляя значения только тогда, когда они нам нужны, вместо того чтобы вычислять их все заранее: вместо того чтобы хранить большой список, мы можем хранить толь- ко ту информацию, которая нам нужна для вычисления в дан- ный момент. В Python множество встроенных типов поддерживают такой подход: они генерируют значения по требованию — их часто называют генераторами. Мы уже познакомились с некоторы- ми примерами, такими как range и enumerate. В основном вы можете относиться к генератору как к любой другой после- довательности, если вам нужно обращаться к его элементам только по одному за раз — например, в цикле for: # Эти два цикла делают одно и то же: for i in (1, 2, 3, 4, 5): print(i)
for 1 in range(1, 6): print(i) Разница становится заметной, если вы попытаетесь выве- сти содержимое генератора напрямую. Обычно вы увидите строковое представление объекта, включающее его тип и уни- кальный идентификатор, но не его значения. Чтобы вывести все значения генератора, нам нужно преобразовать его к типу какой-либо последовательности, например к списку, — это приведет к генерации всех его значений: # Не очень полезный вывод print (range(100)) # Выведет все значения от 0 до 99 print(list(range(100))) Все итерируемые объекты (iterable) в Python используют единый интерфейс, благодаря которому мы можем обходить их с помощью циклов. Каждый итерируемый объект предо- ставляет итератор, который «запоминает» текущую позицию в последовательности. Объект iterable и итератор вместе образуют согласованный интерфейс, который можно исполь- зовать для перебора последовательности значений — незави- симо от того, хранятся ли эти значения в памяти или вычисля- ются по мере необходимости: • У итерируемого объекта есть метод для доступа к элемен- ту по его индексу. Например, список просто возвращает элемент, который хранится в определенной позиции. Ди- апазон range, с другой стороны, вычисляет целое число, которое соответствует определенному индексу. • Итератор обеспечивает пошаговый доступ к значениям и хранит текущую позицию, отталкиваясь от которой
можно получить доступ к следующему элементу. Один и тот же итерируемый объект может иметь несколько не- зависимых итераторов — каждый в своем месте итерации. Например, вы можете выполнять итерацию над одним и тем же списком на обоих уровнях вложенного цикла — каждый цикл использует свой итератор, и они не мешают друг другу: animals - [кошка , 'собака , 'рыба']. for first_animal in animals: for seconcLanimal in animals: print(f'Вчера я купил {first_animal}. Сегодня купил {second-animal}.") Более подробно мы рассмотрим, как определяются эти мето- ды, в одной из следующих глав, когда будем обсуждать напи- сание собственных классов и создание объектов на их основе. Пока же приведем еще несколько примеров встроенных гене- раторов, определенных в модуле iter tools в Python: # импортируем модуль iter too 1s import itertools # в отличие от range, count не имеет верхней границы # и не ограничивается только целыми числами for 1 in itertools.count(1): print(i) # 1, 2, 3.... for i in itertools.count(1, 0.5): print(i) # 1.0, 1.5, 2.0....
# cycle - бесконечно повторяет значения из итерируемого # объекта for animal in itertools.cycle(['кошка1, ‘собака]): print (animal) # 'кошка', 'собака', 'кошка', 'соба- ка'... # repeat - многократно повторяет одно и то же значение for i in itertools.repeat(1): # ... бесконечно print(i) # 1, 1, 1.... for i in itertools.repeat(1, 3): # или заданное количе- # ст во раз print(i) # 1, 1, 1 # chain - объединяет несколько итерируемых объектов for i in itertools.chain(numbers, animals): print(i) # сначала выведет все числа, а затем всех # животных Некоторые генераторы (например, count или cycle) могут ра- ботать бесконечно. Если вы используете их в цикле for, вам по- надобится еще какая-нибудь проверка, чтобы цикл завершился. Существует также встроенная функция zip, которая по- зволяет объединить несколько итерируемых объектов поэле- ментно и тоже возвращает генератор: for i in zip((1, 2, 3), (4, 5, 6)): print(i) for i in zip(range(5), range(5, 10), range(W, 15)): print(i)
Объединенный результат будет такой длины, как самый ко- роткий из входных итерируемых объектов — лишние элемен- ты от других будут отброшены. Упражнение 4 1. Создайте кортеж с названиями месяцев и кортеж с количеством дней в каждом месяце (предположим, что в феврале 28 дней). С помощью одного цикла for соз- дайте словарь, в котором названия месяцев будут ключа- ми, а количество дней — значениями. 2. Теперь сделайте то же самое, не используя цикл for. Выражения-генераторы Предположим, у нас есть список чисел, и мы хотим по- строить новый список, удвоив все значения в первом списке. Или мы хотим извлечь из него все четные числа. Или, напри- мер. мы хотим найти в списке имен животных все имена, ко- торые начинаются с гласной буквы, и написать их с заглавной буквы. Мы можем решить каждую из этих задач, перебирая исходный список, выполняя по очереди какую-либо проверку каждого элемента и добавляя по мере выполнения значения в новый список: numbers = [1, 5, 2, 12, 14, 7, 18] doubles = [ ] for number in numbers: doubles.append(2 * number)
even_numbers = [] for number in numbers: if number % 2 == 0: even.numbers.append(number) animals = ['aardvark', 'cat', 'dog', 'opossum']. vowel.animals = [] for animal in animals: if animal[0] in aeiou': vowel_animals.append(animal.title()) Такой код работает, но он громоздкий. В Python существует более лаконичный и читабельный синтаксис — выражения- генераторы. Выражение-генератор — это своего рода фильтр, который мы можем определить для итерируемого элемента на осно- ве некоторого условия. Результатом является другой итери- руемый список. Вот несколько примеров выражений-гене- раторов: doubles = [2 * number for number in numbers] even_numbers = [number for number in numbers if number % 2 == 0] vowel_animals = [animal.title() for animal in animals if animal[0] in 'aeiou'] Выражение-генератор — это часть, заключенная в квадрат- ные скобки в каждой строке. Каждый из этих генераторов при- водит к созданию нового списка. Можно считать, что выражение-генератор — это компакт- ная форма цикла for, которая была немного изменена.
• Первая часть (2 * number или number или animal. title()) определяет, что именно будет вставлено в но- вый список па каждом шаге цикла. Обычно это некоторая функция от каждого элемента исходного списка во время его обработки. • Средняя часть (for number in numbers или for animal in a n ima Is) соответствует первой строке цикла f о г и опре- деляет, по какому объекту идет проход и какое имя пере- менной присваивается каждому элементу внутри цикла. • Последняя часть — это условие (if ...) (в первом выра- жении отсутствует, во втором if number % 2 == 0, в третьем — if animal[0] in ' aeiou'), которое от- фильтровывает некоторые из исходных элементов. Толь- ко те элементы, для которых условие истинно, будут об- работаны (в соответствии с выражением в первой части) и включены в новый список. Эту часть можно и не вклю- чать в выражение-генератор, как это продемонстрирова- но в первом примере — здесь мы просто хотим удвоить все числа, которые содержатся в исходном списке. Выражения-генераторы можно использовать для замены даже довольно сложных циклов — включая вложенные ци- клы. Однако чем сложнее цикл, тем запутаннее будет соответ- ствующее выражение-генератор. Длинные и сложные выра- жения могут быть трудны для восприятия — в таких случаях лучше просто написать цикл в обычной форме. Результатом выражения-генератора не обязательно дол- жен быть список. Python предоставляет схожие синтаксиче- ские конструкции для создания других коллекций — таких как множества, словари и генераторы. Все они строятся по по- хожему принципу, но используют разные скобки, чтобы ука- зать тип создаваемого объекта:
• Квадратные скобки [ ] — создают список. • Круглые скобки () — создают генератор. • Фигурные скобки { } — создают множество. • Фи1урные скобки с двоеточием { :} — создают словарь, где указывается пара «ключ-значение». numbers = [1, 5, 2, 12, 14, 7, 18] # результат выражения-генератора - генератор doubles.generator = (2 * number for number in numbers) # результат выражения - множество doubles_set = {2 * number for number in numbers} # результат выражения - словарь, в котором число исполь- # зуется в качестве ключа, а удвоенное число - в каче- # стве значения doubles_dict = -{number: 2 * number for number in numbers} Если выражение-генератор является параметром, передава- емым функции, например sum, круглые скобки можно не ста- вить: sum_doubles = sum(2 * number for number in numbers) Примечание Выражения-генераторы для создания множеств и словарей появились толь- ко в Python 3. В Python 2 придется сначала создать список или генератор и за- тем самостоятельно преобразовывать его в множество или словарь
Упражнение 5 1. Создайте строку, содержащую первые десять положи- тельных целых чисел, разделенных запятыми и пробе- лами. Учтите, что нельзя соединять числа напрямую — их нужно сначала преобразовать в строки. Выведите на экран полученную строку. 2. Перепишите программу-календарь из упражнения 3, используя вместо вложенных циклов вложенные выра- жения-генераторы. Попробуйте добавить строку в один из списков недель, чтобы убедиться, что вы действитель- но создаете отдельный список для каждой недели, а не пе- реиспользуете один и тот же. 3. Аналогичным образом создайте календарь в виде спи- ска из 52 пустых подсписков — по одному на каждую не- делю года. Подумайте, как бы вы переписали для этого вложенные циклы for. Операторы break и continue Оператор break Оператор break позволяет немедленно прервать выполне- ние цикла. Это может быть полезно, например, для обработки особых случаев. Пример: х = 1 while х <= 10:
if x == 5: break print(x) x += 1 В приведенном выше фрагменте программы на экран будут выведены только числа от 1 до 4. В случае, когда х станет рав- ным 5, будет выполнен оператор break и, соответственно, про- изойдет выход из цикла. Оператор continue Оператор continue похож на оператор break тем, что застав- ляет поток управления выйти из текущего тела цикла, но сам цикл при этом не завершается. Например: for х in range(1, 10+ 1): # перебор чисел от 1 до 10 if х == 5: continue print(х) Этот фрагмент программы выведет все числа от 1 до 10, кроме числа 5. В случае, когда х равен 5, встретится оператор continue, и поток управления покинет тело цикла — но затем цикл продолжится со следующим элементом диапазона. Обратите внимание, что если в первом примере заменить break на continue, то получится бесконечный цикл — по- тому что оператор continue будет срабатывать до того, как х успеет измениться, х останется равным 5, и вызов оператора continue продолжится до бесконечности!
Использование оператора break для имитации цикла do-while Вспомните, что цикл while — это цикл с предусловием, и он проверяет условие перед первым выполнением тела цик- ла. Иногда это удобно, а иногда нет. Что делать, если вам необ- ходимо, чтобы тело цикла выполнялось как минимум один раз? age = Input("Пожалуйста, введите ваш возраст: ") while not valid_number(age): # предположим, что мы опре- # делили valid_number в другом месте age = input(' Пожалуйста, введите ?.аш возраст: ") Здесь приходится повторять запрос вне и внутри цикла. Это неудобно — особенно если тело цикла большое. Что если поз- же мы захотим изменить сообщение пользователю и забудем изменить его в обоих местах? А если тело цикла содержит мно- го строк кода? Многие другие языки предлагают структуру, называемую циклом do-while или циклом repeat-until, которая проверяет условие после выполнения тела цикла. Это означает, что тело цикла всегда будет выполнено хотя бы один раз. В Python нет подобной структуры, но мы можем смоделировать ее с помо- щью оператора break: while True: age = input("Пожалуйста, введите ваш возраст: ") if valid_number(age): break В данной версии цикла мы перенесли условие внутрь тела цикла. Это значит, что теперь мы сначала выполняем все нужные действия внутри цикла — например, запрашиваем у пользователя ввод — и лишь после этого проверяем уело-
вие, нужно ли продолжать. Чтобы цикл выполнялся беско- нечно до тех пор, пока мы сами не решим его прервать, мы за- менили условие в заголовке while на True. Это выражение всегда истинно, поэтому само по себе оно не остановит цикл никогда — он может завершиться только в случае срабаты- вания оператора break. Этот трюк может помочь нам сделать данный конкрет- ный случай использования цикла более привлекательным, но у него есть и свои недостатки. Если мы случайно забудем вставить оператор break или ошибемся с логикой, то полу- чим бесконечный цикл! Такой код также может быть более сложным для понимания, поскольку фактическое условие, которое заставляет цикл завершиться, скрыто в теле цикла. Поэтому использовать эту конструкцию следует с осторож- ностью. Иногда лучше переписать цикл таким образом, что- бы условие проверялось до тела цикла, и повторения можно было бы избежать. Например, можно инициализировать пе- ременную заведомо некорректным значением и использо- вать обычный цикл while: age = None # мы можем инициализировать переменную аде заведомо некорректным значением while not valid_number(age): # теперь мы можем исполь- зовать условие, прежде чем просить пользователя ввести данные age = input("Пожалуйста, введите ваш возраст: ")
Упражнение 6 1. Напишите программу, которая многократно запраши- вает у пользователя целое число. Если целое число чет- ное, выведите его. Если целое число нечетное, ничего не выводите. Завершите выполнение, если пользователь введет целое число 99. 2. Некоторые программы позволяют вводить произволь- ное количество данных и завершаются, когда пользова- тель вводит специальный символ (называемый сенти- нелом). Например, вас могут попросить ввести PIN-код, за которым следует символ (#). Этот символ означает, что вы закончили вводить свой PIN-код. Напишите программу, которая запрашивает у пользова- теля целые положительные числа, пока пользователь не введет отрицательное целое число. После ввода отри- цательного числа программа должна вывести среднее значение всех ранее введенных положительных чисел (само отрицательное число игнорируется). 3. Реализуйте простой калькулятор с меню. Выведите до- ступные действия, запросите выбор пользователя и вы- полните соответствующую операцию (например, сложе- ние двух чисел). После каждого действия возвращайтесь к меню. После каждой операции возвращайте пользова- теля в меню. Выйдите из программы, когда пользователь выберет 0. Если введено некорректное значение — проиг- норируйте его и снова покажите меню. Можно считать, что пользователь всегда вводит корректные целые числа: -- Меню калькулятора... 0. Выйти из программы 1. Сложение двух чисел
2. Вычитание двух чисел 3. Умножение двух чисел 4. Деление двух чисел Использование циклов для упрощения кода Мы можем использовать наши знания о циклах, чтобы из- бежать повторяющегося кода. Рассмотрим пример, в котором программа запрашивает у пользователя некоторые личные данные: name = input( Пожалуйста, введите ваше имя: ") surname = input("Пожалуйста, введите вашу фамилию: ") # пока сохраняем как строки, потом преобразуем в числа age = input("Пожалуйста, введите ваш возраст: ") height = input("Пожалуйста, введите ваш рост: ") weight = input("Пожалуйста, введите ваш вес: ") В этом фрагменте видно много повторений: каждая стро- ка отличается только названием переменной и тем, какую ха- рактеристику мы запрашиваем. При написании такого кода мы часто копируем и вставляем строки — а значит, легко мо- жем допустить ошибку. Если впоследствии понадобится вне- сти изменения, придется править каждую строку отдельно. Как сделать код лучше? Нужно отделить изменяющиеся ча- сти строк от неизменных и воспользоваться циклом для по- вторяющихся операций. Вместо отдельных переменных ис- пользуем словарь: в нем удобно хранить значения, а ключом служит название характеристики:
person = {} for prop in [ "имя", "фамилия", 'возраст", 'рост", "вес"]: person[prop] = input("Введите ваш(у) %s: " % prop) Теперь нет ненужного дублирования. Мы можем легко из- менить строку, используемую в качестве приглашения к вводу, или добавить дополнительный код, выполняемый для каждо- го свойства, — нам придется редактировать код только в од- ном месте, а не в пяти. Чтобы добавить еще одну характери- стику, достаточно вписать ее в список. Упражнение 7 Измените пример выше, чтобы происходило преобразо- вание типов свойств: возраст должен быть целым чис- лом, рост и вес — числом с плавающей точкой, а имя и фа- милия — строками.
Ответы на упражнения Ответ на упражнение 1 1. Вот пример программы, вычисляющей сумму квадратов, пока результат не превысит 200: total = 0 number = 0 while total < 200: number += 1 total += number**2 print( "Сумма: %d" % total) print('Последнее число: %d“ % number) 2. Программа с угадыванием слов: GUESSES.ALLOWED = 10 SECRET-WORD = "caribou" guesses.left = GUESSES.ALLOWED guessed_word = None while guessed.word != SECRET.WORD and guesses.left: guessed.word = input("Угадайте слово: ") if guessed.word == SECRET_WORD: print("Вы угадали! Поздравляем1')
else: guesses.left -= 1 print("Неверно! Осталось попыток: %d,L % guesses. Ответ на упражнение 2 1. Текст программы, которая подсчитывает сумму чисел от 1 до 10: total = 0 for i in range(1, 10 + 1): total += i print(total) 2. To же с использованием встроенной функции sum(): print(sum(range(1, 10 + 1))) 3. Текст программы, которая вычисляет факториал числа: num = int(input("Пожалуйста, введите целое число: ")) num.fac = 1 for i in range(1, num+ 1): num.fac *= i print("%d! = %d" % (num, num.fac))
4. Текст программы, которая вычисляет сумму, произведе- ние и среднее 10 чисел: total = 0 product = 1 for i in range(1, 10+ 1): num = float(input("Пожалуйста, введите число %d: " % i)) total += num product *= num average = total/16 print("Сумма: %д\пПроизведение: %д\пСреднее: %g’ % (total, product, average)) 5. Вариант с использованием списка: calendar = [] for i in range(10): numoers[i] = float(input("Пожалуйста, введите число %d: " % (i+ 1)) total = 0 product = 1 for num in numbers: total += num product *= num average = total/10
print("Сумма: %д\пПроизведение: %д\пСреднее: %д % (total, product, average)) Ответ на упражнение 3 1. Программа, создающая вложенный список «месяц —> не- дели»: calendar = [] for m in range(12): month = [ ] for w in range(4): month.append([ ]) calendar.append(month) 2. Добавление события: (JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER) = range(12) (WEEK_1, WEEK_2, WEEK_3, WEEK.4) = range(4) calendar[JULY][WEEK_2].append(' В отпуск!") Ответ на упражнение 4 1. Пример программы, которая создает словарь: месяц —> число дней:
month = ("Январь”, "Февраль", "Март", Апрель", "Май", 'Июнь', "Июль", "Август", "Сентябрь', "Октябрь", "Но- ябрь", "Декабрь") num.days = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 30, 31) month.diet = {} for month, days in zip(months, days): month_dict[month] = days 2. Упрощенный вариант с использованием diet (zip ()): month = ("Январь", 'Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Но- ябрь", "Декабрь") num.days = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 30, 31) # результат работы функции zip представляет собой после- # довательность двухэлементных кортежей, поэтому мы мо- # жем сразу использовать преобразование в формат словаря. month_dict = dict(zip(months, days)) Ответ на упражнение 5 1. Программа, которая создает строку из первых 10 чисел: number_string = ", ".join(str(n) for n in range(1, 11)) print(number_string)
2. Программа, которая создает календарь событий с помо- щью выражений-генераторов: calendar = [[[] for w in range(4)] for m in range(12)] (JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER) = range(12) (WEEK_1, WEEK-2, WEEK.3, WEEK_4) = range(4) calendar[JULY][WEEK.2].append("B отпуск!") 3. Ошибочный вариант, все месяцы ссылаются на один спи- сок (не используйте): calendar = [[] for w in range(4) for m in range(!2)] Ответ на упражнение 6 1. Программа, которая выводит четные числа, пока не будет введено число 99: while (True): num = int(input("Введите целое число: ")) if num == 99: break if num % 2: continue print(num) 2. Программа, которая осуществляет ввод положительных чисел до первого отрицательного, выводит среднее значе- ние:
print("Введите положительные целые числа для вычисления среднего. Введите отрицательное число для завершения.") nums = [] while True: num = int(input("Введите число: ")) if num < 0: break nums.append(num) average = float(sum(nums))/len(nums)) print( "Среднее = %g' % average) 3. Программа — реализация калькулятора: menu = """— Меню калькулятора... 0. Выход из программы 1. Сложение двух чисел 2. Вычитание двух чисел 3. Умножение двух чисел 4. Деление двух чисел""" selection = None while selection != 0: print(menu) selection = int(input("Выберите вариант: ")) if selection not in range(5): print("Неверный вариант: °/od" % selection)
continue if selection == 0: continue a = float(input("Пожалуйста, введите первое число: )) b = float(input("Пожалуйста, введите второе число: )) if selection == 1: result = а + b elif selection == 2: result = a - b elif selection == 3: result = a * b elif selection == 4: result = a / b print("Результат равен %g.,r % result) Ответ на упражнение 7 Программа — модифицированный вариант ввода данных с преобразованием типов: person = {} properties = [ ("имя", str), ("фамилия", str), ("возраст", int), ("рост", float),
("вес", float), for prop, p.type in properties: person[prop] = p_type(input(' Введите ваш(у) %s: " % prop))
Ошибки и исключения Ошибки Ошибки в программе часто называют багами. Почти всегда они возникают по вине программиста. Процесс поиска и устра- нения ошибок называется отладкой. Ошибки можно разде- лить на три основные группы: • Синтаксические ошибки (syntax errors) • Ошибки времени выполнения (runtime errors) • Логические ошибки (logical errors) Синтаксические ошибки Python обнаруживает эти ошибки, когда пытается разобрать вашу программу. В случае обнаружения программа не запу- стится, а интерпретатор выведет сообщение об ошибке. Син- таксические ошибки — это нарушения правил языка, ана- логичные грамматическим или орфографическим ошибкам в тексте. К распространенным синтаксическим ошибкам Python от- носятся: • пропуск ключевого слова; • неверный порядок слов; • пропуск символа, например, двоеточия, запятой или ско- бок; • опечатка в написание ключевого слова;
• неправильный отступ; • пустые блоки (чтобы блок «ничего не делал», использует- ся ключевое слово pass). Примечание Запрещено оставлять любой блок (нап ример, тело в конструкции if или тело функции) полностью пустым. Если вы хотите, чтобы блок ничего не делал, вы можете использовать внутри него оператор pass. Python сделает все возможное, чтобы сообщить вам, где именно находится ошибка, но иногда его сообщения могут вводить в заблуждение: например, если вы забудете убрать ка- вычки внутри строки, то можете получить сообщение о син- таксической ошибке, ссылающееся на место, расположенное чуть ниже от нее в вашем коде, хотя это и не является реаль- ным источником проблемы. Если вы не видите ничего непра- вильного в строке, на которую указывает сообщение об ошиб- ке, проверьте несколько строк выше. По мере того как вы будете больше практиковаться в программировании, будет ра- сти и ваше умение определять и исправлять ошибки. Вот несколько примеров синтаксических ошибок в Python: myfunction(x, у): return х + у # отсутствует def else: print("Hello!") # else без предшествующего if if mark >= 50 print("Вы сдали!") # пропущено двоеточие
if arriving: рг!пГ("Привет!") esle: print("Bye!") # опечатка в else if flag: print("$nar установлен!") # неверный отступ Ошибки времени выполнения Если программа синтаксически корректна, то есть не содер- жит синтаксических ошибок, она будет запущена интерпрета- тором Python. Однако программа может во время выполнения неожиданно завершиться, если столкнется с ошибкой времени выполнения. Это ошибка, которая не была обнаружена на ста- дии проверки синтаксиса, а обнаружилась только при выпол- нении определенной инструкции. Когда программа заверша- ется из-за ошибки времени выполнения, мы говорим, что она потерпела крах. Рассмотрим инструкцию на разговорном языке взмахните руками и летите в Австралию. Хотя инструкция структурно правильная и вы прекрасно понимаете ее смысл, выполнить ее невозможно. Некоторые примеры ошибки времени выполнения Python: • деление на ноль; • выполнение операции над несовместимыми типами; • использование идентификатора, который не был опреде- лен; • обращение к несуществующему элементу списка, значе- нию в словаре или атрибуту объекта; • попытка получить доступ к несуществующему файлу.
Ошибки времени выполнения часто возникают, если вы не учитываете все возможные значения, которые может со- держать переменная, особенно при обработке пользователь- ского ввода. Вы всегда должны стараться добавлять проверки в свой код, чтобы убедиться, что он может изящно справляться с некеорректным вводом и потенциальными крайними случа- ями. Более подробно мы рассмотрим это в главе об обработке исключений. Логические ошибки Логические ошибки — самые сложные для исправления. Они возникают, когда программа работает без сбоев, но выда- ет неверный результат. Это следствие ошибки в логике про- граммы. Поскольку не возникла ни синтаксическая ошибка, ни ошибка времени выполнения, сообщения об ошибке вы не получите. Вам придется самостоятельно найти пробле- му, просмотрев все соответствующие части программы, хотя некоторые инструменты могут указать на подозрительный фрагмент, который, похоже, может вызвать неожиданное по- ведение. Иногда в реализации алгоритма на Python может быть абсо- лютно все в порядке — ошибка заключена в нем самом. Одна- ко чаще всего подобные ошибки возникают из-за небрежности программиста. Вот несколько примеров ошибок, которые при- водят к логическим ошибкам: • использование неправильного имени переменной; • отступ блока на неправильный уровень; • использование целочисленного деления вместо деления с плавающей точкой; • неправильное определение приоритета операторов;
• ошибка в логическом выражении (в условии для операто- ров сравнения); • ошибка диапазона и другие числовые ошибки. Если вы сделаете опечатку в имени переменной, то в зави- симости от того, существует ли в программе идентификатор с таким ошибочным именем, можете получить ошибку време- ни выполнения или логическую ошибку. Частым источником путаницы в именах переменных и не- правильных отступов является частое копирование и вставка больших блоков кода. Если у вас много дублирующихся строк с незначительными различиями, очень легко пропустить не- обходимое изменение при редактировании вставленных строк. Всегда старайтесь применять функции и циклы, чтобы исключить излишнее дублирование. Мы рассмотрим это бо- лее подробно чуть позже. Упражнение 1 1. Найдите все синтаксические ошибки в приведенном фрагменте кода и объясните, почему они являются ошиб- ками. 2. Найдите потенциальные источники ошибок времени выполнения в этом фрагменте программы: dividend = float(input("Пожалуйста, введите дели- мое : ")) divisor = float(input("Пожалуйста, введите дели- тель : ")) quotient = dividend / divisor quotient_rounded = math.round(quotient)
3. Найдите потенциальные источники возникновения оши- бок времени выполнения в этом фрагменте программе: for х in range(a, b): print("(%f, %f, %f)" % my_list[x]) 4. Найдите потенциальные источники логических оши- бок в этом фрагменте программы: product = 0 for i in range(10): product *= i sum_squares = 0 for i in range(10): i_sq = i**2 sum_squares += i_sq nums = 0 for num in range(10): num += num Обработка исключений До сих пор в программах, которые были представлены в этой книге, игнорировался тот факт, что что-то может пойти не так. Мы пытались предотвратить ошибки времени выпол- нения, проверяя данные на корректность, прежде чем их ис- пользовать. Однако нами еще не рассматривалось, как можно справиться с ошибками во время их возникновения — наши программы до сих пор, когда с ними сталкивались, просто вне- запно терпели крах.
Существует несколько ситуаций, в которых могут возник- нуть ошибки времени выполнения. Всякий раз, когда мы пы- таемся прочитать файл или получить ввод от пользователя, есть вероятность, что произойдет что-то неожиданное — файл может быть перемещен или удален, а пользователь может вве- сти данные не в том формате. Хорошие программисты долж- ны добавлять в свои программы средства защиты, чтобы по- добные ситуации разрешались без проблем — программа, которая аварийно завершается всякий раз, когда сталкивается с легко предсказуемой проблемой, не очень приятна в исполь- зовании. Большинство пользователей ожидают, что програм- мы будут достаточно надежными, чтобы уметь восстанавли- ваться после подобных сбоев. Если мы знаем, что определенный раздел нашей програм- мы может привести к ошибке, то можем указать Python, что делать, если она произойдет. Вместо того чтобы позво- лить ошибке разрушить нашу программу, мы можем перехва- тить ее, что-то сделать и позволить программе продолжить работу дальше. Все ошибки времени выполнения (и синтаксиса), с которы- ми мы столкнулись, в Python называются исключениями — Python использует их, чтобы указать на то, что произошло нечто исключительное, и ваша программа не сможет продол- жить работу, пока это событие не будет обработано. Все исклю- чения являются подклассами класса Exception — мы узнаем больше о классах и о том, как создавать собственные типы ис- ключений, в последующих главах. Операторы try и except Для обработки возможных исключений мы используем кон- струкцию try-except:
try: age = int(input(’Введите ваш возраст: ")) pnnt( "Ваш возраст %d лет." % age) except ValueError: рг1п!(г'Эй, вы ввели не число!") Python попытается обработать все инструкции внутри бло- ка try. Если в процессе их выполнения произойдет ошиб- ка ValueError (неверное значение для данного типа), поток управления немедленно перейдет к блоку except, а все остав- шиеся инструкции в блоке try будут пропущены. В этом примере мы знаем, что ошибка, скорее всего, воз- никнет при попытке преобразовать введенные пользовате- лем данные в целое число. Если введенная строка не являет- ся числом, эта строка вызовет ошибку ValueError — именно поэтому мы указали ее в качестве типа ошибки, которую бу- дем обрабатывать. Мы могли бы указать более общий тип ошибки — или даже не указывать тип вообще, что привело бы к тому, что предложение except могло бы соответствовать лю- бому типу исключения, — но это не очень хорошая идея. Что если бы мы получили совершенно другую ошибку, ко- торую мы не предусмотрели? Она также будет обработана, и мы даже не заметим, что произошло что-то необычное. Также нам может понадобиться по-разному реагировать на разные виды ошибок. Поэтому мы всегда должны ста- раться выбирать конкретные, а не общие типы ошибок для наших инструкций except. В одном предложении except можно обрабатывать более одного типа ошибок. Для этого вместо одного типа мы можем предоставить кортеж, содержащий различные типы исклю- чении:
try: dividend = int(input("Пожалуйста, введите делимое: ")) divisor = int(input("Пожалуйста, введите делитель: ")) print("%d / %d = %f" % (dividend, divisor, dividend/ divisor)) except(ValueError, ZeroDivisionError): print("Ync, что-то пошло не так!") Блок try-except также может содержать несколько блоков except. При возникновении исключения Python проверит по- следовательно сверху вниз каждую инструкцию except на со- ответствие типу исключения. Если соответствие не будет най- дено, исключение будет считаться необработанным, и ваша программа завершится аварийно: try: dividend = int(input("Пожалуйста, введите делимое: ")) divisor = int(input('Пожалуйста, введите делитель: ")) print("%d / %d = %f" % (dividend, divisor, dividend/ divisor)) except ValueError: print(“Делитель и делимое должны быть числами!") except ZeroDivisionError: print("Делимое не может быть равно нулю!") Обратите внимание, что в приведенном выше примере, если возникнет ошибка ValueError, мы не будем знать точ- но, вызвана ли она тем, что делимое или делитель не явля- ются целыми числами — любая из строк ввода может приве-
сти к этой ошибке. Если мы хотим дать пользователю более конкретную информацию о том, какой ввод был неправиль- ным, нам придется обернуть каждую строку ввода в отдель- ный блок try-except: try:: dividend = int(input("Пожалуйста, введите делимое: “)) except ValueError: print("Делимое должно быть числом!") try: divisor = int(input("Пожалуйста, введите делитель: ")) except ValueError: print("Делитель должен быть числом!") try: print("%d / %d = %f" % (dividend, divisor, dividend/ divisor)) except ZeroDivisionError: print("Делимое не может быть равно нулю!") В целом лучше использовать обработчики исключений для защиты небольших блоков кода от конкретных ошибок, чем оборачивать ими большие фрагменты программы и пи- сать расплывчатый, общий код восстановления в случае воз- никновения ошибок. Возможно, поначалу кажется, что ис- пользование множества небольших блоков try/except — это избыточно и неэффективно. Однако эту кажущуюся громозд- кость можно уменьшить, если использовать функции и ци- клы, которые помогают избежать дублирования кода.
Как Python обрабатывает исключения Когда возникает исключение, обычный порядок исполне- ния программы прерывается. Python проверяет, находится ли строка кода, вызвавшая исключение, внутри блока try. Если да, то проверяется, может ли какой-либо из блоков except, свя- занных с блоком try, обрабатывать этот тип исключения. Если соответствующий обработчик найден, исключение обрабаты- вается, и программа продолжает свою работу с инструкции, следующей сразу после конца блока try-except. Если такой обработчик отсутствует или строка кода с ошиб- кой находится не в блоке try, Python поднимется на один уро- вень выше внутрь функции, из которой был вызван проблем- ный код. Эта функция немедленно завершится, а строка, в которой находится вызов этой функции, будет рассматри- ваться так, как если бы именно она выбросила исключение. Python проверит, находится ли эта строка внутри блока try, и так далее. При вызове функции она помещается в стек вы- зовов Python, о котором мы поговорим в главе, посвященной функциям. При обработке исключения Python проходит по это- му стеку, пока не найдет соответствующий возникшему ис- ключению блок except. Если ошибка возникла в основном теле программы, вне функций, и не была обработана, программа аварийно за- вершится. При этом Python выведет сообщение об исключе- нии (тип исключения и его описание). Также будет выведен traceback — список, который показывает «обратный путь», пройденный исключением, от места завершения до строки, вызвавшей ошибку.
Проверка на ошибки и обработка исключений Обработка исключений дает нам альтернативный способ решения проблем, связанных с ошибками в программе. Вме- сто того чтобы выполнять множество проверок заранее, что- бы предотвратить ошибку, мы просто пробуем выполнить действие, и в случае неудачи обрабатываем исключение. Это может позволить нам писать более простой и понятный код. Давайте рассмотрим более сложный пример ввода — тот, в ко- тором мы продолжаем просить пользователя вводить данные до тех пор, пока они не будут введены правильно. Мы попро- буем написать этот пример, используя два разных подхода: # вариант с проверками n = None while n is None: s = input("Пожалуйста, введите целое число: ") if s.lstrip('- 1),isdigit(): n = int(s) else: print("%s - не целое число." % s) # вариант с использованием исключений n = None while n is None: try: s = input("Пожалуйста, введите целое число: ") n = int(s) except ValueError: print("°/oS - не целое число." % s)
В первом фрагменте кода нам приходится писать довольно запутанную проверку, чтобы проверить, являются ли вводи- мые пользователем данные целым числом — сначала мы уби- раем возможный знак минуса, а затем проверяем, состоит ли оставшаяся часть строки только из цифр. Однако гораздо проще выяснить, вызовет ли эта строка ошибку ValueError, если мы попытаемся преобразовать ее в целое число. Во вто- ром фрагменте мы как раз проверяем это условие, а не пыта- емся выполнить все возможные проверки самостоятельно, что не всегда просто сделать. Например, легко забыть, что це- лые числа могут быть также и отрицательными, и написать проверку, представленную в первом фрагменте неправильно. Вот еще несколько преимуществ обработки с применением механизма исключений: • Четкое разделение: основной код и код обработки оши- бок находятся в отдельных блоках. • Гибкость: исключения могут передаваться вверх по стеку функций до того уровня, где их можно обработать. • Исключения содержат много полезной информации об ошибке — например, они могут выводить «обратный путь» (traceback), который помогает нам увидеть, где именно произошла ошибка. Операторы else и finally Python позволяет использовать в конструкции try-except два дополнительных блока: else и finally. Блок else будет вы- полнен только в том случае, если в блоке tiy не возникнет ис- ключение: try: age = int(input("Пожалуйста, введите ваш возраст: "))
except ValueError: print("3T0 не число!1") else: print("Вам %d лет." % age) Требуется вывести сообщение о возрасте пользователя только в том случае, если преобразование в целое число про- шло успешно. В первом примере программы для обработчика исключений мы поместили этот оператор вывода непосред- ственно после преобразования внутри блока try. В обоих случаях оператор будет выполнен только в том случае, если преобразование не вызовет исключения, однако лучшая практика — разместить его в блоке else. Таким образом, весь код внутри блока try состоит из единственной строки, кото- рая и является потенциальным источником ошибки, кото- рую мы хотим обработать. При редактировании этой программы в будущем мы смо- жем ввести дополнительные операторы, которые будут вы- полнятся в случае, если преобразование входных данных о возрасте в число пройдет без ошибок. Предположительно, некоторые из этих операторов также могут вызывать ошибку ValueError. Если мы не учтем это и поместим их внутрь бло- ка t г у, то инструкция с except также будет обрабатывать и эти ошибки, если они возникнут. Это может привести к неуправ- ляемому поведению программы. Поместив же весь этот допол- нительный код в блок else, мы избежим такого риска. Блок finally будет выполнен в конце конструкции try- except в любом случае, независимо от того, что произойдет — возникнет исключение или нет, будет возникшее исключение обработано или нет и даже если мы выйдем из блока с помо- щью операторов break, continue или return. Блок finally можно использовать, например, для очистки ресурсов, закры- тия файлов, освобождения соединений и т. п.:
try: age = int(input(* 1 2 3 Пожалуйста, введите ваш возраст: ")) except ValueError: printf'Bbi ввели не число!") else: print("Я вижу, что вам %d лет." % аде) finally: print("Было очень приятно с вами пообщаться. До сви- дания 1") Упражнение 2 1. Добавьте дополнительную функциональность про- грамме из упражнения 7 главы «Операторы управления циклом», включив в нее обработку исключений. Каж- дый раз, когда пользователь вводит данные неверного типа, программа должна повторно запрашивать это зна- чение до тех пор, пока оно не будет введено корректно. Предусмотрите понятную обратную связь для пользова- теля. 2. Добавьте конструкцию try-except в тело функции, при- веденной ниже, чтобы обрабатывать возможное исклю- чение IndexError, которое может возникнуть, если пе- реданный индекс выходит за пределы списка. Если это случится, выведите сообщение об ошибке: def print_list_element(thelist, index): print(thelist[index]) 3. Следующая функция добавляет элемент в список, на- ходящийся внутри словаря списков. Перепишите ее
с использованием конструкции try-except, которая будет обрабатывать исключение КеуЕггог, если список с задан- ным именем еще не существует в словаре, вместо пред- варительной проверки. Используйте также блоки else и finally: def add_to_list_in_dict(thedict, listname, element): ]f listname in thedict: 1 = thedictflistname] print("%s уже содержит %d элементов." % (listname, len(l))) else: thedictflistname] = [] print("Создан список %s." % listname) thedict[listname].append(element) print("Добавлено %s в %s." % (element, listname)) Оператор with Использование объекта исключения Объекты исключений Python содержат больше информа- ции, чем просто тип ошибки. Они также сопровождаются ка- ким-либо сообщением — мы уже видели некоторые из этих сообщений, выводимых при сбоях в наших программах. Од- нако такие сообщения редко бывают понятными для пользо- вателя — если мы хотим сообщить пользователю об ошибке, то лучше написать более содержательное сообщение, которое
объясняет, как ошибка связана с тем, что сделал пользователь. Например, если ошибка была вызвана некорректным вводом, полезно сообщить пользователю, какое из введенных значе- ний было введено с ошибкой. Иногда само уже встроенное сообщение об исключении бы- вает полезным — тогда его можно отобразить напрямую. Что- бы получить доступ к сообщению, нам нужно иметь доступ к объекту исключения. Чтобы получить доступ к сообщению, нужно сохранить объект исключения в переменную, доступ- ную внутри блока except: t гу: age = int(input('Пожалуйста, введите ваш возраст: ")) except ValueError as err: print(err) Переменная err — не строка, но Python умеет преобразо- вывать объект исключения в строку — она будет содержать текст ошибки, то есть именно то, что нам нужно. Можно так- же объединить сообщение исключения с нашим собствен- ным сообщением: try: age = int(input("Пожалуйста, введите ваш возраст: ")) except ValueError as err: print("Вы ввели некорректный возраст: %s” % err) Обратите внимание, что вставка переменной в строку с фор- матированием через %s автоматически вызывает ее преобра- зование в строку.
Генерация исключений вручную Можно вызвать исключение самостоятельно с помощью оператора raise: try: age = int(input("Пожалуйста, введите ваш возраст: ")) if age < 0: raise ValueError("%d недопустимый возраст. Воз- раст должен быть положительным или рален нулю. ) except ValueError as err: pnnt("Bbi ввели некорректный возраст: %s" % err) else: print("fl вижу, что вам %d лет." % age) Здесь мы вручную выбрасываем исключение ValueError, если введенный возраст— отрицательное число. Такое исклю- чение работает точно так же, как любое другое — поток управ- ления немедленно покидает блок try и переходит к блоку except, который также соответствует нашему исключению, поскольку оно имеет тип ValueError. Мы выбрали ValueError в качестве типа исключения, по- тому что он наиболее подходит для такого рода ошибок. Ничто нс мешает нам использовать здесь совершенно неподходящий класс исключений, но мы должны стараться быть последова- тельными. Ниже перечислены наиболее распространенные типы исключений, которые можно использовать в собствен- ных программах: • ТуреЕггог: ошибка типа — переменная имеет непод- ходящий тип. Например, если функция получила строку вместо числа. • ValueError: эта ошибка используется для указания, что переменная имеет правильный тип, но неправиль-
ное значение. Например, мы использовали ее, когда значе- ние переменной аде хоть и было целым числом, но содер- жало знак минус (то есть ее значение не соответствовало требуемому типу). • NotlmplementedError: используется, чтобы указать, что метод должен быть реализован в подклассе (в следую- щей главе мы увидим, как использовать это исключение). Мы также можем написать свои собственные классы исклю- чений, которые будут унаследованы от существующих классов исключений — мы увидим несколько примеров этого в следу- ющей главе. Иногда нужно повторно выбросить уже перехваченное ис- ключение — например, чтобы обработать его частично в теку- щей функции, но позволить внешнему коду тоже отреагиро- вать на него: t гу: age = int(input('Пожалуйста, введите ваш возраст: ")) except ValueError as err: print("Bw ввели некорректный возраст: %s" % err) raise err Упражнение 3 1. Перепишите программу из первого задания упражне- ния 2 так, чтобы вместо пользовательского сообщения в блоке except печатался текст оригинального исключе- ния Python. 2. Перепишите программу из второго задания упраж- нения 2 так. чтобы исключение, перехваченное в блоке
except, было повторно выбрашено после вывода сообще- ния об ошибке. Отладка программ Синтаксические ошибки обычно отлаживаются довольно легко: сообщение об ошибке, которое Python выводит на экран, содержит строку с ошибкой и тип ошибки. Ее легко найти и ис- править. Ошибки времени выполнения отлаживать немного слож- нее: сообщение об ошибке и стек вызовов могут подсказать нам, где именно произошла ошибка, но это не обязательно го- ворит о том, в чем заключается проблема. Иногда они вызваны чем-то очевидным, например, опечаткой в имени идентифи- катора, но иногда причиной является непредвиденное состоя- ние переменных — не всегда понятно, какая из множества пе- ременных имеет неожиданное значение. Логические ошибки сложнее всего исправить, потому что они не вызывают никаких ошибок, которые можно было бы отследить до конкретной строки в коде. Все, что мы знаем, это то, что программа ведет себя не так, как должна — иногда отслеживание участка кода, вызывающего неправильное по- ведение, может занять много времени. Важно тестировать свой код, чтобы убедиться, что он ведет себя так, как вы ожидаете. Например, быстрый и простой спо- соб проверить, что функция делает все правильно, — это вста- вить после каждой строки оператор print, чтобы выводить промежуточные значения, вычисленные на этом этапе. Боль- шинство программистов интуитивно делают это в процессе
написания функции, или, возможно, если им нужно выяснить, почему она работает неправильно: def hypotenusefx, у): print("x равно %f, а у равно %f’d % (х, у)) х_2 = х**2 print(x_2) у_2 = у**2 print(y_2) z_2 = х_2+ у„2 print(z_2) z = math.sqrt(z_2) prlnt(z) return z Такой способ удобен и доступен, и даже опытные разработ- чики периодически к нему прибегают. Но у этого подхода есть несколько недостатков: • Как только функция начинает работать правильно, мы, скорее всего, удалим все операторы print, чтобы программа не засоряла вывод отладочной информацией. Проблема в том, что код часто меняется — в следующий раз, когда мы захотим протестировать эту функцию, нам придется добавлять операторы вывода заново. • Чтобы не писать операторы print заново, мы можем за- комментировать их вместо удаления. В результате код захламляется, а устаревшие сообщения могут запутать и вовсе потерять смысл. • Чтобы вывести промежуточные значения, мы были вы- нуждены разбить вычисление на несколько строк. Иногда это оправдано — если выражение длинное и его сложно читать в одной строке. Но чаще это делает код избыточ- ным. В идеале функция выглядела бы так:
def hypotenuse(x, у): return math.sqrt(x**2+ y**2) Как мы можем сделать этот код лучше? Если вы хотите про- сматривать значения переменных на разных этапах выпол- нения программы, можно использовать отладчик, например, модуль pdb. Если нужно, чтобы программа выводила инфор- мативные сообщения, возможно — в файл, и при этом мож- но было управлять уровнем детализации без изменения кода, стоит использовать логирование. Самое главное: чтобы убедиться, что наш код работает пра- вильно сейчас и будет работать так же и дальше, нам следует написать постоянный набор тестов, которые мы сможем регу- лярно выполнять. Более подробно о тестировании мы погово- рим в одной из следующих глав. Инструменты отладки Существуют автоматизированные инструменты, кото- рые помогают нам отлаживать ошибки, а также поддержи- вать код в максимально корректном состоянии, чтобы свести к минимуму вероятность появления новых ошибок. Некото- рые из этих инструментов анализируют синтаксис програм- мы, сообщая об ошибках и плохом стиле программирования, а другие позволяют анализировать программу в процессе ее выполнения. Ру flakes, pylint, Ру Checker и рер8 Эти четыре утилиты анализируют код на предмет синтак- сических ошибок, а также некоторых видов ошибок време- ни выполнения. Они также выдают предупреждения о нару- шениях стиля программирования, а также о неэффективном
и потенциально некорректном коде — например, о перемен- ных и импортированных модулях, которые никогда не ис- пользуются. Pytlakes разбирает код, а не импортирует его, а это означа- ет, что он не может обнаружить столько ошибок, сколько дру- гие инструменты, — но он также более безопасен в использо- вании, поскольку нет риска, что он выполнит поврежденный код, который нанесет непоправимый ущерб нашей системе. Это особенно актуально, когда мы используем его как часть ав- томатизированной системы. Это также означает, что Pyflakes работает быстрее других инструментов. Pylint и PyChecker импортируют проверяемый код и выда- ют более обширные списки ошибок и предупреждений. Их ис- пользуют программисты, которым функциональность Pytlakes кажется недостаточной. Рер8 проверяет стиль оформления кода на соответствие стандарту РЕР 8 — документу спецификации хорошего стиля кодирования. Вот как мы используем эти программы в командной строке: pyflakes myprog гam.ру pylint myprogram.py pychecker myprogram.py pep8 myprogram.py pdb pdb — это встроенный модуль Python, который мы можем использовать для отладки программы во время ее выполне- ния. Этот модуль можно либо импортировать и использовать его функции внутри нашего кода, либо вызывать как скрипт при запуске нашей программы. С помощью pdb мы можем вы- полнять пошаговый переход от одной строки кода к другой
и проверять состояние на каждом шаге, а также проводить анализ программы в случае ее сбоя. Вот как мы будем использовать pdb в нашем коде: import pdb def our_function(): bad_idea = 3 + "4" pdb.run('our_function()') Пример запуска pdb в виде скрипта сценария: python3 -m pdb ourprogram.py Более подробную документацию, включая полный список команд, которые можно использовать в отладчике, можно найти по ссылке http://docs.python.Org/3.3/library/pdb.htmL Легирование (Logging) Иногда полезно, чтобы по мере выполнения программа вы- водила сообщения в консоль или файл. Эти сообщения мож- но использовать как запись выполнения программы, которая поможет нам найти ошибки. Иногда ошибка возникает пери- одически, и мы не знаем, что ее вызывает — если мы добавим отладочный вывод в нашу программу только тогда, когда за- хотим начать активный поиск ошибки, то можем оказаться не в состоянии ее воспроизвести. Однако если наша програм- ма постоянно записывает сообщения в файл, то, проверив журнал после возникновения ошибки, мы можем обнару- жить, что в нем записана информация, которая поможет ее выявить.
Некоторые виды сообщений важнее других: ошибки — это заслуживающие внимания события, которые почти всегда следует регистрировать. Сообщения, фиксирующие успеш- ное завершение операции, иногда могут быть полезны, но не так важны, как ошибки. Подробные отладочные сооб- щения, фиксирующие каждый шаг выполнения программы, могут быть полезны только в процессе отладки. В обыч- ных условиях их постоянный вывод превратил бы консоль или лог-файл в сплошной информационный шум — неудоб- ный и объемный. Поэтому при проектировании системы вывода сообщений важно уметь разграничивать уровни важности и использо- вать их осмысленно — об этом и пойдет речь далее. Мы мо- жем использовать модуль легирования Python logging, что- бы добавить протоколирование в нашу программу простым и последовательным способом. Операторы логирования ведут себя почти как операторы print, но всякий раз, когда мы за- писываем сообщение в журнал, мы указываем его уровень. Ког- да мы запускаем нашу программу, мы устанавливаем для нее желаемый уровень логирования. В журнале будут появляться только те сообщения, уровень которых больше или равен за- данному нами уровню. Это означает, что мы можем времен- но включить подробное логирование и снова выключить его, просто изменив уровень в одном месте. Существует последовательный набор имен уровней логиро- вания, который используется в большинстве языков. В поряд- ке от наибольшего значения (наиболее важный) до наимень- шего (наименее важный) они следующие: • CRITICAL — для очень серьезных ошибок; • ERROR — для менее серьезных ошибок;
• WARNING — для предупреждений; • INFO — для важных информационных сообщений; • DEBUG — для получения подробных отладочных сообще- ний. Эти имена используются для целочисленных констант, опре- деленных в модуле logging. Модуль также предоставляет ме- тоды, которые мы можем использовать для записи сообщений в журнал. По умолчанию эти сообщения выводятся в консоль, а уровень журнала по умолчанию — WARNING. Мы можем на- строить модуль, чтобы изменить его поведение — например, записывать сообщения в файл, повышать или понижать уро- вень логирования и изменять формат сообщений. Вот простой пример логирования; import logging # запись логов в файл, игнорируя сообщения менее важные, # чем ERROR logging.basicConfig(filename = 1 тургод гam.log', level = logging.ERROR) # эти сообщения попадут в лог logging.error("Стиральная машина протекает!") logging.critical(1 Дом горит!") # А эти - нет logging.warning("У нас почти закончилось молоко"). logging.infо("Сегодня солнечно"). logging,debug("Я ел яйца на завтрак"). Также существует специальный метод exception, который используется для логирования исключений. Уровень, исполь- зуемый для этих сообщений, равен ERROR, но к ним добавляет-
ся дополнительная информация об исключении. Он автомати- чески добавляет информацию об исключении и применяется внутри блоков except вместо error: try: age = int(input("Сколько вам лет? ")) except ValueError as err: logging.exception(err) В больших проектах может потребоваться более гибкая система легирования: например, нам может понадобить- ся по-разному форматировать сообщения, направлять раз- ные типы сообщений в разные файлы или одновременно ле- гировать в несколько мест. Для этого модуль легирования предоставляет нам объекты логгеров (logger) и обработчи- ков (handler). Мы можем использовать несколько логгеров для создания наших сообщений, настраивая каждый из них независимо. Разные обработчики могут быть связаны с раз- ными местами хранения логов (файлы, консоль, сеть и т, д.). Мы можем соединять наши логгеры и обработчики любым способом — один логгер может использовать множество об- работчиков, а несколько логгеров могут использовать один и тот же обработчик. Упражнение 4 1. Напишите конфигурацию легирования для программы, которая ведет запись лога в файл с именем log. txt и иг- норирует все сообщения ниже уровня INFO. 2. Перепишите вторую программу из упражнения 2 так, чтобы она использовала эту конфигурацию логирования вместо вывода сообщений в консоль (за исключением
первого print — он должен остаться, так как является ча- стью назначения функции). 3. То же самое проделайте с третьей программой из упражнения 2.
Ответы на упражнения Ответ на упражнение 1 1. В коде обнаружено пять синтаксических ошибок: 1. Отсутствует ключевое слово def в определении функ- ции 2. Использован else без if 3. Отсугствует двоеточие после условия if 4. Опечатка: esle вместо else. 5. Блок if пуст, потому что оператор print имеет непра- вильный отступ 2. Также возможны следующие ошибки времени выполне- ния: 1. Значения, введенные пользователем, могут не быть действительными целыми числами или числами с плаваю- щей точкой. 2. Пользователь может ввести ноль в качестве делителя. 3. Если модуль math не был импортирован, вызов math, round вызовет ошибку. 3. 1. Переменные а, b и my_list должны быть определены до этого фрагмента. 2. Попытка обратиться к элементу списка с индексом х может закончиться неудачей во время одной из итераций цикла, если диапазон от а до b превышает размер списка my_list. 3. Операция форматирования строки в операторе print ожидает, что элемент my_list[x] будет кортежем с тремя
числами. Если в нем слишком много или слишком мало эле- ментов. или он вообще не является кортежем, попытка фор- матирования строки будет неудачной. 4. 1. При накоплении произведения переменная product должна быть инициализирована как 1, а не 0, иначе произ- ведение всегда будет равно нулю! 2. Строка, которая добавляет i_sq к sum., squares, выров- нена неправильно и добавляет последнее значение i_sq только после завершения цикла. 3. Используется неправильная переменная: на каждой итерации цикла текущее число в диапазоне прибавляется к самому себе, а переменная nums не изменяется — исполь- зуется неверное имя переменной. Ответ на упражнение 2 1. Пример программы с валидацией пользовательского вво- да: person - {} properties = [ ("name", str), ("фамилия", str), ("возраст", int), ("рост", float), ("вес', float), for property, p.type in properties: valid_value = None
while valid.value is None: try: value = input("Пожалуйста, введите ваш(-у) %s: " % property) valid.value = p_type(value) except ValueError: print("He удалось преобразовать %s '%s' в тип %s. Пожалуйста, попробуйте еще раз." % (property, value, р_ type.__name__)) personfproperty] = valid_value 2. Пример функции с обработкой исключения IndexError: def print_list_element(thelist, index): try: print(thelist[index]) except IndexError: print("В списке нет элемента с индексом %d." % index) 3. Функция добавления элемента в список внутри словаря: def add_to_list_in_dict(thedict, listname, element): try: 1 = thedictflistname] except KeyError: thedict[listname] = [] print("Создано %s." % listname) else: print("%s уже содержит %d элементов." % (listname, len(l))) finally:
thedict[listname].append(element) print("Добавлено %s в %s." % (element, listname)) Ответ на упражнение 3 1. Модифицированная версия первой программы с выводом текста исключения: person = {} properties = [ ("имя", str), ("фамилия , str), ("возраст’, int), ("рост", float), ("вес", float), for property, p.type in properties: valid_value = None while valid_value is None: try: value = input("Пожалуйста, введите ваш(-у) %s: " % property) valid_value = p_type(value) except ValueError as ve: printfve) person[property] = valid_value 2. Функция с повторной генерацией исключения после ло- гирования:
def print_list_element(thelist, index): try: print(thelist[index]) except IndexError as ie: print("В списке нет элемента с индексом %d." % index) raise ie Ответ на упражнение 4 1. Вот пример конфигурации ведения журнала: import logging logging.basicConfig(filename = 'log.txt', level = logging. INFO) 2. Функция с легированием ошибок вместо вывода: def print_list_element(thelist, index): try: print(thelist[index]) except IndexError: logging.error("В списке нет элемента с индексом %d." % index) 3. Функция добавления в список словаря с легированием: def add_to_list_in_dict(thedict, listname, element): try: 1 = thedict[listname] except KeyError: thedict[listname] = [] logging.info("Создано %s." % listname) else:
logging.info(“%s уже содержит %d элементов." % (listname, len(l))) finally: thedict[listname].append(element) logging.inf©(Добавлено %s в %s." % (element, listname))
Функции Введение Функция — это последовательность операторов, выполня- ющая определенную задачу. Мы используем функции, чтобы избежать дублирования кода — вместо того чтобы повторять один и тот же набор операторов в разных местах программы, мы определяем: их один раз в виде функции и вызываем ее по имени. Если нам понадобится изменить способ выполне- ния этой задачи, то придется изменить код в основном толь- ко в одном месте. Вот пример простейшей функции, которая не принимает аргументов и не возвращает значений: def print_a_message(): prlnt("Hello, world!") Ключевое слово def используется для начала определения функции. Следующей частью определения является имя функ- ции, в данном случае print_a_message, за которым следуют круглые скобки (внутри них при необходимости размещают- ся параметры) и двоеточие. Все, что отступает вправо на один уровень после этой строки, составляет тело функции. Функции выполняют определенные действия, поэтому всег- да следует выбирать имя функции, которое как можно про- ще и точнее объясняет, что она делает. Обычно это глагол или фраза с глаголом. Если вы существенно измените поведе-
ние функции и ее имя больше не будет отражать суть работы, его следует переименовать — даже если это не всегда удобно. Эта конкретная функция всегда делает одно и то же: выво- дит сообщение " Hello, world!". Определение функции не приводит к ее запуску. Когда ин- терпретатор Python доходит до определения функции, он про- сто «узнает» о ее существовании и о том, что она будет делать при вызове. Чтобы запустить функцию, мы должны ее вы- звать. Для вызова функции используются ее имя и круглые скобки с аргументами (если есть): print_a_message() Конечно, мы уже использовали многие встроенные функ- ции Python, такие как print и 1еп: print("Hello") 1еп([1, 2, 3]) Многие объекты в Python являются вызываемыми, то есть их можно вызывать как функции. V вызываемого объекта опреде- лен специальный метод, который срабатывает при вызове. На- пример, такие типы, как str, int или list, можно использо- вать как функции для создания новых объектов (иногда путем преобразования существующего объекта): num.str = str(3) num = int("3") people = list() # создает новый пустой список people = list((1, 2, 3)) # преобразует кортеж в новый список
В общем случае классы (а типы — это частный случай клас- сов) являются вызываемыми — когда мы вызываем класс, мы вызываем его метод-конструктор, который используется для создания нового объекта этого класса. Мы узнаем боль- ше о классах в следующей главе, но вы можете вспомнить, что мы уже вызывали некоторые классы для создания новых объектов, когда выбрасывали исключения: raise ValueError("С вашим номером что-то не так!') Поскольку функции являются объектами в Python, мы мо- жем обращаться с ними так же, как и с любыми другими объ- ектами: присваивать переменным, передавать в качестве ар- гументов и т. д. Чтобы сослаться на функцию без ее вызова, используйте ее имя без скобок: my_function = print_a_message # позже мы сможем вызвать Функцию, используя имя пере- # менной my_function() Поскольку определение функции не вызывает ее сразу, вы мо- жете использовать в теле одной функции другую, еще не опре- деленную функцию — если ко времени выполнения первая функция уже будет известна интерпретатору. Например, если мы определим несколько функций, которые вызывают друг друга, порядок их определения не имеет значения, если они все определены до того, как мы начнем их использовать: def my_function(): my_other_function() def my.other_function():
print("Hello!") # это нормально, потому что функция my_other_function # теперь определена my_function() Если мы переместим вызов этой функции вверх, то получим ошибку: def my_function(): my_other_function() # это некорректно, потому что функция my_other_function # еще не определена! my.function() def my_other_function(): print("Hello!") В связи с этим рекомендуется помещать все определения функций в начало программы, чтобы они выполнялись рань- ше всех остальных операторов. Упражнение 1 1. Создайте функцию f unc_a, которая выводит какое-ли- бо сообщение. 2. Вызовите эту функцию. 3. Присвойте объект функции в качестве значения пере- менной Ь, не вызывая функцию. 4. Теперь вызовите функцию, используя переменную Ь.
Входные параметры Очень редко бывает так, что задача, которую мы хотим вы- полнить с помощью функции, всегда абсолютно одинакова. Обычно есть небольшие различия в том, что нам нужно сде- лать при разных обстоятельствах. Мы не хотим писать отдель- ную функцию для каждой мелкой вариации — это нарушает принцип повторного использования кода! Вместо этого мы пе- редаем информацию в функцию через серию входных параме- тров и используем их внутри для настройки поведения функ- ции под конкретную задачу. Например, мы можем сделать функцию, которую мы опре- делили выше, более универсальной, позволяя задавать сооб- щение: def print_a_message(message): print(message) Будет гораздо удобней передать два числа и сложить их: def print_sum(а, b): print(a + b) Здесь а и b — это параметры функции. При вызове этой функ- ции нужно передать два аргумента, иначе возникнет ошибка: print_sum() # Ошибка: недостаточно аргументов print_sum(2, 3) # Правильный вызов В приведенном выше примере мы передаем значения 2 и 3 в качестве параметров в функцию при ее вызове. Это означает, что при выполнении функции переменной а будет присвоено значение 2, а переменной b — значение 3. Затем вы сможете
ссылаться на эти значения, используя имена переменных а и Ь внутри функции. В языках со статической типизацией мы должны объявлять типы параметров при определении функции, а при вызове функции можем использовать только переменные этих типов. Если мы хотим выполнить аналогичную задачу с переменны- ми других типов, мы должны определить отдельную функцию, принимающую эти типы. В Python параметры не имеют объявленных типов. В приве- денную выше функцию print_message мы можем передать любую переменную, а не только строку. С помощью функции print _sum можно сложить любые два числа: два целых, два с плавающей точкой, одно целое и одно вещественное, даже две строки (которые тоже складываются). Мы также можем пе- редать целое число и строку, но хотя они разрешены в качестве параметров, их нельзя сложить вместе, поэтому при попытке сложить их внутри функции мы получим ошибку. Преимущество здесь заключается в том, что нам не нужно писать множество различных функций print_sum, по од- ной для каждой пары типов, в то время как в противном слу- чае все они были бы идентичны. Недостаток заключается в том, что, поскольку Python не проверяет типы параметров в определении функции при ее вызове, мы не сразу можем заметить, что передан параметр неправильного типа — на- пример, если другой человек, взаимодействующий с напи- санным нами кодом, использует типы параметров, которые мы не предусмотрели, или если мы случайно получили па- раметры не в том порядке. Именно поэтому нам важно тщательно тестировать наш код, особенно если он будет использоваться другими людь- ми — об этом мы поговорим в одной из следующих глав. Для того чтобы написать надежный код, особенно если он бу-
дет использоваться другими людьми, часто бывает полезно проверять параметры функции в самом начале ее выполнения и давать пользователю обратную связь (выбрасывая исключе- ния), если они неверны. Упражнение 2 1. Создайте функцию hypotenuse, которая принимает два числа в качестве параметров и выводит на экран ко- рень квадратный из суммы их квадратов. 2. Вызовите эту функцию с двумя значениями с плаваю- щей точкой. 3. Вызовите эту функцию с двумя целыми числами. 4. Вызовите эту функцию с одним целым числом и одним значением с плавающей точкой. Возвращаемые значения Примеры функций, которые мы рассмотрели выше, не воз- вращают никаких значений — они просто выводят сообще- ние. Однако часто функции используются для вычисления какого-то значения с последующим возвратом результата вы- звавшему ее коду. Программа может сохранить это возвращен- ное значение в переменной и использовать позже. Вывод, ко- торый возвращается из функции, называется возвращаемым значением. Перепишем функцию print_sum, чтобы она воз- вращала результат сложения, а не выводила его на экран:
def add(a, b): return a + b Здесь используется ключевое слово return для возврата значения. Чтобы получить доступ к этому значению при вызо- ве функции, мы должны присвоить результат ее работы пере- менной: с = add(a, b) Теперь переменная с содержит результат выполнения функ- ции. Функция может иметь только одно возвращаемое значение, но это значение может быть списком или кортежем, поэтому на практике вы можете возвращать из функции любое коли- чество различных значений. Обычно возвращать несколько значений имеет смысл только в том случае, если они каким-то образом логически связаны друг с другом. Если вы поместите после оператора return несколько значений, разделенных за- пятыми, они будут автоматически преобразованы в кортеж. Принимая возвращаемое значение, вы можете присвоить кор- теж нескольким переменным, разделенным запятыми, и та- ким образом распаковать кортеж, возвращенный функцией, в несколько переменных: def divide(dividend, divisor): quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder # можно сделать так q, г = divide(35, 4) # а можно сделать немного по-другому result = divide(67, 9)
ql = result[0] q2 = result[1 ] # распаковка кортежа a, b = (1, 2) # распаковка списка c, d = [5, 6] Что произойдет, если вы попытаетесь присвоить перемен- ной функцию, которая не возвращает значение? Например, написанную нами ранее функцию print_message: mystery.output = print_message("Boo1") print(mystery_output) Все функции на самом деле что-тпо возвращают, даже если мы не определяем возвращаемое значение — по умолчанию возвращаемым значением является None, которое и будет при- своено переменной mystery_output. Когда в теле функции выполняется инструкция с операто- ром return, управление немедленно передается обратно вы- зывающему коду, и все оставшиеся в функции инструкции пропускаются. Иногда мы можем использовать это в своих ин- тересах, чтобы сократить количество условных операторов внутри функции: def divide(dividend, divisor): if not divisor: return Nonet None # защита от деления на ноль quotient= dividend // divisor remainder = dividend % divisor return quotient, remainder
Если условие if выполняется, срабатывает return, и выход из функции происходит сразу. Это означает, что оставшиеся инструкции можно писать без блока else, так как они выпол- нятся только при невыполнении условия if. Такой подход мо- жет быть полезен, когда мы хотим проверить параметры в на- чале функции — это позволяет писать основной код функции без лишней вложенности. Иногда, если с одним из параметров что-то не так, вместо возврата значения None целесообразнее выбросить исключение: def divide(dividend, divisor): if not divisor: raise ValueError('Делитель не может быть равен нулю!") quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder Наличие нескольких точек выхода, разбросанных по всей функции, может сделать ваш код трудночитаемым — боль- шинство людей ожидают единственного оператора return в конце функции. Поэтому такой прием следует использовать с осторожностью. Примечание В некоторых других языках программирования функциями называют только те подпрограммы, которые возвращают значение (по аналогии с математи- ческими функциями). Подпрограммы без возвращаемого значения там на зываются процедурами.
Упражнение 3 1. Перепишите функцию hypotenuse из упражнения 2 так, чтобы она возвращала значение, а не выводила его на экран. Добавьте обработку исключений: функция должна возвращать None, если ей переданы аргументы неправильного типа. 2. Вызовите функцию с двумя числами и выведите резуль- тат. 3. Вызовите функцию с двумя строками и выведите ре- зультат. 4. Вызовите функцию с числом и строкой и выведите ре- зультат. Стек вызовов Python использует структуру данных стек вызовов (call stack) для хранения информации о запущенных функциях. При вы- зове функции в стек добавляется новый кадр стека — в него добавляются все параметры функции, а по мере выполнения тела функции в нем создаются локальные переменные. Когда функция завершает выполнение, кадр стека удаляется из сте- ка, и управление возвращается в ту точку программы, откуда она была вызвана, на предыдущий уровень стека. Если вы вспомните главу, в которой мы рассказывали об об- ласти видимости переменных, в начале книги, то описание того, как работает стек вызовов, многое проясняет. При обра- щении к переменной Python сначала ищет ее на текущем уров-
не стека, а если не находит, то проверяет предыдущий уровень, и так далее — пока она либо не будет найдена, либо не будет найдена нигде, и вы получите ошибку. Вот почему локальная переменная всегда будет иметь приоритет перед глобальной переменной с тем же именем. Python также просматривает стек каждый раз, когда обраба- тывает исключение: сначала он проверяет, может ли исклю- чение быть обработано в текущей функции, и если нет, то за- вершает функцию и пробует следующую — до тех пор, пока либо исключение не будет обработано на каком-то уровне, либо программа сама не завершится. Именно этот путь вы ви- дите в t raceback, который показывает, как Python шел по сте- ку вызовов. Рекурсия Функция может вызывать саму себя — такой подход назы- вается рекурсией. Классический пример — вычисление чисел Фибоначчи: нулевое число равно 0, первое — 1, каждое следу- ющее — сумма двух предыдущих: def fibonacci(n): if n == 0: return 0 if n == 1: return 1 return fibonacci(n - 1) + fibonacci(n - 2) Всякий раз, когда мы пишем рекурсивную функцию, нам необходимо включить в нее некое условие, которое позволит ей прекратить рекурсию — базовый случай, когда функция
не будет вызывать сама себя, иначе произойдет бесконечная рекурсия. В данном примере это происходит в начале последо- вательности: первые два числа не вычисляются из каких-либо предыдущих чисел — они являются константами. Что произойдет, если убрать базовый случай? Когда мы дой- дем до л = 2, мы будем продолжать вызывать функцию, пыта- ясь вычислить fibonacci(0), fibonacci(-1) и так далее. Теоретически функция будет вызывать сама себя вечно и ни- когда не завершится, но на практике программа завершится с ошибкой RuntimeE г го г и сообщением о том, что мы превы- сили максимальную глубину рекурсии. Это происходит пото- му, что стек в Python имеет конечный размер — если мы будем продолжать размещать экземпляры функции в стеке, то в кон- це концов заполним его и вызовем переполнение стека. Python защищает себя от переполнения стека, устанавливая ограни- чение на количество рекурсивных вызовов. Писать отказоустойчивые рекурсивные функции довольно сложно. Что если бы мы вызвали функцию выше с параме- тром -1? В этом случае, поскольку не предусмотрено никакой проверки на ошибки, мы бы получили бесконечную рекурсию с попытками вычислить fibonacci(-2), fibonacci(-3) и так далее. Любая рекурсивная функция может быть переписана ите- рационным способом, который позволяет избежать рекурсии. Например: def fibonacci(n): current, next = 0, 1 for i in range(n): current, next = next, current + next return current
Эта функция использует итерацию для подсчета до желае- мого значения и, обновляя переменные, чтобы следить за вы- числениями. Вся итерация происходит внутри одного экзем- пляра функции. Особенность здесь в том. что обе переменные обновляются одновременно. Благодаря этому мы можем ис- пользовать старые значения обеих переменных при вычисле- нии новых, не теряя промежуточные данные. Упражнение 4 Напишите рекурсивную функцию, которая вычисляет факториал заданного числа. Используйте обработку ис- ключений, чтобы выбросить соответствующее исключе- ние, если входной параметр не является положительным целым числом, но разрешите пользователю вводить ве- щественные числа, если они являются целыми числами (например 5.0). Параметры по умолчанию Сочетание имени функции и количества принимаемых ею параметров называется сигнатурой функции. В языках со статической типизацией в одной области видимости мо- жет существовать несколько функций с одинаковым именем, но с разным числом или типом параметров (в этих языках типы параметров и возвращаемых параметров также явля- ются частью сигнатуры). В Python в области видимости может быть определена толь- ко одна функция с определенным именем — если вы опреде-
лите еще одну функцию с таким же именем, вы перезапишете первую функцию. Вы должны вызвать эту функцию с правиль- ным количеством параметров, иначе получите ошибку. Иногда появляются веские причины, чтобы иметь две вер- сии одной и той же функции с разными наборами параметров. Вы можете добиться чего-то похожего на это, сделав некото- рые параметры необязательными. Чтобы сделать параметр необязательным, нужно задать для него значение по умолча- нию. Необязательные параметры должны идти после всех обя- зательных параметров в определении функции: def make_greeting(title, name, surname, formal= True): if formal: return "Здравствуйте, %s %s!" % (title, surname) return Привет, %s!" % name print(make_greeting("г-н", Иван", Петров")) print(make_greeting ("г-н", "Иван", "Петров , False)) При вызове функции мы можем не указывать необязатель- ный параметр — в этом случае будет использоваться значение по умолчанию. Если мы включим параметр, наше значение за- менит значение по умолчанию. Можно задать несколько параметров по умолчанию: def make^greeting(title, name, surname, formal= True, time= None): if formal: fullname = "%s %s" % (title, surname) else: fullname = name if time is None: greeting = "Здравствуйте"
else: greeting = “Добрый %s1 % time return "%s, %s!" % (greeting, fullname) print(make_greeting(“Мистер", "Джон", "Смит")) print(make_greeting(1 Мистер", “Джон", "Смит", False)) print(make_greeting( Мистер", "Джон", "Смит", False, вечер )) Что делать, если мы хотим передать второй необязательный параметр, но не хотим передавать первый? До сих пор мы пе- редавали всем этим функциям позиционные параметры — кор- теж значений, которые сопоставляются с параметрами в сигна- туре функции на основе их позиций. Однако мы также можем передавать эти значения в виде параметров с ключевыми сло- вами — мы можем явно указать имена параметров вместе со значениями. Такие параметры получили название «имено- ванные параметры»: print(make_greeting(title = "г-н", name = "Иван1, surname = Петров")) print(make_greeting(“г-н", "Иван", surname = "Петров")) print(make_greeting("г-н", "Иван', "Петров1, time = 1 ве- мер")) Мы можем смешивать позиционные и именованные пара- метры, но последние должны идти после любых позиционных параметров: # это корректно print(make_greeting("г-н", "Иван", surname = "Петров")) # это приведет к ошибке print(make_greeting(title = "г-н", "Иван", "Петров"))
Мы можем указывать именованные параметры в любом по- рядке — они не обязательно должны совпадать с порядком в определении функции: print(make_greeting(surname = "Петров', name = "Иван', title = "г-н")) Теперь мы можем легко передать второй необязательный параметр, а не первый: print(make_greeting( "г-н", ’Иван", "Петров", time = зечер ) Изменяемые типы и параметры по умолчанию Следует быть осторожным при использовании в определе- ниях функций изменяемых типов в качестве значений пара- метров по умолчанию: def add_pet_to_list(pet, pets.append(pet) return pets pets = []): list_with_cat = add_pet_to_list("кошка") list_with_dog = add_pet_to_list("собака") print(list_with_cat) print(list_with_dog) # Упс. .. Помните, что хотя тело функции может выполняться много раз, определение функции выполняется только один раз — это означает, что пустой список, созданный в определении функ- ции, будет общим списком для всех экземпляров функции.
В данном случае мы хотим создать пустой список внутри тела функции (правильный способ): def add_pet_to_llst(pet, pets = None): if pets is None: pets = [] pets.append(pet) return pets Упражнение 5 1. Напишиге функцию с именем calculator. Она долж- на принимать следующие параметры: два числа, арифме- тическую операцию (это может быть сложение, вычита- ние, умножение или деление, по умолчанию — сложение) и формат вывода (это может быть целое число или число с плавающей точкой, по умолчанию — число с плавающей точкой). Деление должно быть делением с плавающей точ- кой. Функция должна выполнить требуемую операцию над двумя введенными числами и вернуть результат в требуемом формате (если формат целочисленный, ре- зультат должен быть округлен, а не просто усечен). Если какие-либо параметры, переданные в функцию, недей- ствительны, то следует выбросить исключение. 2. Вызовите функцию со следующими наборами параме- тров и проверьте, что ответ соответствует вашим ожида- ниям: 1. 2, 3.0 2. 2, 3.0, формат вывода — целое число 3. 2,3.0, операция — деление
4. 2, 3.0, операция — деление, формат вывода — целое число *args и **kwargs Иногда требуется передать в функцию список позицион- ных или именных параметров с ключами переменной дли- ны. Мы можем поставить символ * перед именем параметра, чтобы указать, что это кортеж позиционных параметров пере- менной длины, и использовать два таких символа **, чтобы указать, что параметр — это словарь с именованными пара- метрами переменной длины. По соглашению имя параметра, которое мы используем для кортежа, — args, а имя, которое мы используем для словаря, — kwa rgs: def print_args(*args): for arg in args: print(arg) def print_kwargs(**kwargs): for k, v in kwargs.items(): print("%s: %s" % (k, v)) Внутри функции мы можем обращаться Kargs как к обыч- ному кортежу, но * означает, что args не передается в функцию как единственный параметр, который является кортежем: вместо этого он передается как последовательность отдель- ных параметров. Аналогично ** означает, что kwargs переда- ется как последовательность отдельных именных параметров, а не как один параметр, который является словарем:
print_args("один", 'два", "три") print_args("один", "два", "три", "четыре') print„kwargs(name = "Джейн", surname = "Доу") print_kwargs(age = 10) Мы можем использовать * или ** при вызове функции, что- бы распаковать последовательность или словарь на ряд от- дельных параметров: my_list = ["один", "два", три"]. print_args(*my_list) my.dict = {"имя": "Джейн", "фамилия": "Доу"} print,kwargs(**my_dict) Это упрощает создание списков параметров программным способом. Обратите внимание, что мы можем использовать та- кой способ для любой функции, а не только для тех, которые используют *args или **kwargs: my.dict = { обращение": "Мистер", "имя": "Джон", "фамилия": "Смит", Формальный": False, "время": вечер", } print(make_greeting(**my_dict)) Мы можем смешивать обычные параметры *args и **kwargs в одном определении функции. При этом параме- тры *args и **kwargs должны идти после всех остальных,
a **kwargs — после *args. Нельзя иметь более одного пара- метра списка переменной длины или более одного параметра словаря переменной длины (напомним, что вы можете давать им любые имена): def print_everything(name, time= "утро1, *args, **kwargs): print("Доброе %s, %s." % ({time}, {name})) for arg in args: print(arg) for k, v in kwargs.items(): print("%s: %s" % (k, v)) Если мы используем выражение с * при вызове функции, оно должно идти после всех позиционных параметров, а если мы используем выражение с **, то оно должно идти в самом конце: def print_everything(*args, **kwargs): for arg in args: print(arg) for k, v in kwargs.items(): print("%s: %s" % (k, v)) # мы можем записать все параметры по отдельности print_everything("кошка", "собака", day = вторник") t = ("кошка", "собака") d = {"День": "вторник"} # мы можем распаковать кортеж и словарь print_everything(*t, **d)
# ИЛИ только один из них print_everything(*t, day = "вторник") print_everything("кошка", "собака", **d) # мы можем смешивать * и ** с обычными параметрами print_everything("Джейн", *t, **d) print_everything("Джейн", *t, time = "вечер", **d) print_everything(time = вечер", *t, **d) # ни один из этих вариантов не разрешен: print_everything(*t, " Джейн", **d) print.everything(*t, **d, time = "вечер") Если у функции только *а rgs и **kwa rgs, она сможет при- нимать любые параметры и работать корректно даже без ар- гументов. Один или оба параметра args и kwargs могут быть пустыми. В этом случае функция примет любую комбинацию позиционных и именованных параметров, включая ситуа- цию, когда параметры вообще отсутствуют. Это может быть по- лезно, если мы пишем очень общую функцию, как, например, print_everything в примере выше. Упражнение 6 Перепишите функцию калькулятора из упражнения 5 та- ким образом, чтобы она принимала любое количество численных параметров, а также те же необязательные именованные параметры. Функция должна применять операцию к первым двум числам, затем снова применять ее к результату и следующему числу, и так далее. Напри- мер, если на вход поступают числа 6,4,9 и 1, а операция —
вычитание, функция должна вернуть 6 - 4 - 9 - 1. Если введено только одно число, оно должно быть возвра- щено без изменений. Если не введено ни одного числа, должно быть выброшено исключение. Декораторы Иногда нам может понадобиться изменить несколько функ- ций одним и тем же способом — например, выполнить опреде- ленное действие до и после выполнения каждой из функций, передать дополнительный параметр или преобразовать вы- вод в другой формат. У нас также могут быть веские причины не записывать из- менения во все функции — возможно, это сделает определения функций очень громоздкими. В этом случае хотелось бы иметь возможность быстро и легко применять изменение к любой функции (и так же легко удалять его). Чтобы решить эту проблему, мы можем написать функцию, которая изменяет другие функции. Мы называем такую функ- цию декоратором. Наша функция принимает в качестве вход- ного параметра функцию, которую требуется изменить, и воз- вращает новый объект изменной функции. Теперь мы можем присвоить новое значение функции имени старой функции, чтобы заменить старую функцию новой. Например, вот деко- ратор, который записывает имя функции и ее аргументы в лог- файл при каждом использовании функции: # определяем декоратор def log(original_function): def new_function(*args, **kwargs):
with open("log.txt", "w") as logfile: logfile.write('Функция ’%s' вызвана с пози- ционными аргументами %s и именными аргументами %s.\n" % (original-function.__name__, args, kwargs)) return original_function(*args, **kwargs) return new_function # функция, которую нужно декорировать def my_function(message): print(message) # Применение декоратора my_function = log(my_function) Внутри нашего декоратора (внешней функции) мы опре- деляем функцию, которая заменит ту, что поступит на вход, и возвращаем ее. Заменяющая функция (внутренняя функция) записывает сообщение в журнал, а затем просто вызывает ис- ходную (входную) функцию и возвращает ее значение. Обратите внимание, что функция-декоратор вызывается только один раз, когда мы заменяем оригинальную функцию декорированной, но внутренняя функция будет вызываться каждый раз, когда мы используем вызов my_function. Вну- тренняя функция может обращаться как к переменным в своей собственной области видимости (например, args и kwargs), так и к переменным в области видимости декоратора (напри- мер, к переменной original-function). Поскольку внутренняя функция принимает в качестве пара- метров *args и **kwargs, мы можем использовать этот деко- ратор для изменения любой функции, независимо от того, ка- кой у нее список параметров. Внутренняя функция принимает
любые параметры и просто передает их в исходную (входную) функцию. При передаче неправильных параметров внутри ис- ходной функции мы все равно получим ошибку. Существует сокращенный синтаксис для применения деко- раторов к функциям: мы можем использовать символ @ вместе с именем декоратора перед определением каждой функции, которую мы хотим изменить: @1од def my_function(message): print(message) @1од перед определением функции означает ровно то же са- мое, что иmy_function = log(my_function). Мы можем передать декоратору дополнительные параме- тры. Например, мы можем указать пользовательский файл журнала, который будет использоваться в декораторе логиро- вания: def log(onginal_function, logfilename = "log.txt"): def new_function(*args, **kwargs): with open(logfilename, "w") as logfile: logfile.write("Функция J%s' вызвана с позици- онными аргументами %s и аргументами ключевых слов %s.\n" % (original-function.__name__, args, kwargs)) return orig]nal_function(*argsF **kwargs) return new_function @log("someotherfilename.txt") def my_function(message): print(message)
В Python есть несколько встроенных декораторов, которые обычно используются для декорирования методов классов. Мы познакомимся с ними в следующей главе. Примечание Декоратор не обязательно должен быть функцией - это может быть любой зызываемый объект Некоторые программисты предпочитают писать деко- раторы в виде классов. Упражнение 7 1. Перепишите пример декоратора log так, чтобы декора- тор записывал в журнал как имя и параметры функции, так и возвращаемый результат. 2. Протестируйте декоратор, применив его к функции, которая принимает два аргумента и возвращает их сум- му. Выведите результат функции и то, что было записано в файл. Лямбды Мы уже видели, что когда нам надо использовать число или строку в нашей программе, то можно либо записать ее в виде литерала в том месте, где требуется ее использовать, либо использовать переменную, которую мы заранее опреде- лили в нашем коде. Например, print("Привет! ") выводит строковый литерал "Привет!", который мы нигде не храни-
ли в переменной, тогда как print (message) выводит любую строку, хранящуюся в переменной message. Мы также знаем, что функцию можно хранить в перемен- ной, как любой другой объект, обращаясь к ней по имени (но не вызывая ее). Но существует ли такой же подход для самих функций, аналогичный литералам? Можем ли мы определить функцию на лету, когда нам надо передать ее в качестве аргу- мента или присвоить переменной, как мы это делали со стро- кой "Привет!"? Ответ — да, но только для очень простых функций. Мы мо- жем использовать ключевое слово lambda для определения анонимных однострочных функций прямо в нашей програм- ме: а = lambda: 3 # это то же самое, что и def а (): return 3 Lambda-функции могут принимать параметры — они указы- ваются между ключевым словом 1 ambda и двоеточием, без кру- глых скобок. Тело lambda-функции может содержать толь- ко одно выражение, результат которого неявно возвращается (мы не используем ключевое слово return): b = lambda х, у: х + у # это то же самое, что и def b(x, у): return х + у
Lambda-функции следует использовать только для действи- тельно простых операций. Если ваша лямбда выглядит слиш- ком сложной, чтобы ее можно было лето понять, лучше офор- мить ее как обычную именованную функцию. Упражнение 8 1. Определите следующие функции в виде lambda-функ- ций и присвойте их переменным: 1. Принимает один параметр; возвращает его квадрат. 2. Принимает два параметра; возвращает корень ква- дратный из суммы квадратов этих параметров. 3. Принимает произвольное количество параметров: возвращает их среднее арифметическое. 4. Принимает строку; возвращает строку, содержащую только уникальные символы из исходной строки (в лю- бом порядке). 2. Перепишите все эти функции в виде обычных имено- ванных функций. Функции-генераторы и yield Ранее мы уже сталкивались с генераторами — это такие по- следовательности, элементы которых создаются по мере необ- ходимости, а не заранее. Мы можем создать собственные ге- нераторы, написав функции, использующие ключевое слово yield.
Рассмотрим эту простую функцию, которая возвращает диа- пазон чисел в виде списка: def my_list(n): 1 = 0 while i < n: l.append(i) i += 1 return 1 Эта функция заранее формирует весь список чисел и возвра- щает его. Мы можем изменить эту функцию на функцию ге- нератор, сохранив при этом очень похожий синтаксис, напри- мер, так: def my_gen(n): i = 0 while i < n: yield i i += 1 Первое, что важно знать об операторе yield, — это то, что если он используется в функции, то эта функция вернет генератор. Мы можем проверить это, вызвав функцию type для возвращаемого значения my_gen. Также можно исполь- зовать полученный генератор в цикле for, как любую другую итерируемую последовательность: g = my_gen(3) print(type(g))
for х in g: print(x) Что делает оператор yield? Всякий раз, когда генератор за- прашивает новое значение, например в цикле for в приве- денном выше примере, он начинает выполнять инструкции, входящие в тело функции, пока не дойдет до оператора yield. В этот момент выполнение приостанавливается, и из генера- тора возвращается одно значение. После выполнения оператора yield выполнение функции не завершается — при следующем запросе значения выполне- ние продолжится с того места, где остановилось. Если генера- тор дойдет до конца функции, не встретив оператора yield, он выбросит исключение Stopiteration, указывающее на то, что значений больше нет. Цикл f о г автоматически обра- батывает это исключение. В нашей функции my_gen это про- изойдет, когда 1 станет равным п — когда это случится, цикл while завершится, и yield больше не будет вызван. Упражнение 9 Напишите функцию-генератор, принимающую в каче- стве параметра целое число п. Функция должна возвра- щать генератор, который выполняет обратный отсчет чисел от п до 0. Протестируйте свою функцию с помощью цикла for.
Ответы на упражнения Ответ на упражнение 1 Пример программы: def func_a(): print('3To моя потрясающая функция"). func.a() b = func_a b() Ответ на упражнение 2 Пример программы: import math def hypotenuse(x, у): print(math.sqrt(x**2+ y**2)) hypotenuse(12.3, 45.6) hypotenuse(12, 34) hypotenuse(12, 34.5)
Ответ на упражнение 3 Пример программы: import math def hypotenuse(x, у): try: return math.sqrt(x**2+ y**2) except TypeError: return None print(hypotenuse(12, 34)) print(hypotenuse("12", "34")) print(hypotenuse(12, "34")) Ответ на упражнение 4 Пример программы: def factorial(n): ni = int(n) if ni != n or ni <= 0: raise ValueError("%s не является положительным целым числом." % п) if ni == 1: return 1 return ni * factorial(ni - 1)
Ответ на упражнение 5 1. Пример программы: import math ADD, SUB, MUL, DIV = range(4) def calculator(a, b, operation = ADD, output.format = float): if operation == ADD: result = a + b elif operation == SUB: result = a - b elif operation == MUL: result = a * b elif operation == DIV: result = a / b else: raise ValueError("Операция должна быть ADD, SUB, MUL или DIV.") if output_format == float: result = float(result) elif output-format == int: result = math.round(result) else: raise ValueError("Формат должен быть float или int.") return result 2. Вы должны получить следующие результаты: 1. 5.0 2. 5
3.0.6666666666666666 4.1 Ответ на упражнение 6 Вот пример программы: import math ADD, SUB, MUL, DIV = range(4) def calculator(operation = ADD, output-format = float, *args): if not args: raise ValueError("Необходимо ввести хотя бы одно число.") result = args[0] for n in args[1:]: if operation == ADD: result += n elif operation == SUB: result -= n elif operation == MUL: result *= n elif operation == DIV: result /= n else: raise ValueError("Операция должна быть ADD, SUB, MUL или DIV.")
if output-format == float: result - float(result) elif output-format == int: result = math.round(result) else: raise ValueError("Формат должен быть float или int.’) return result Ответ на упражнение 7 1. Пример программы: def log(original_function, logfilename = "log.txt '): def new_function(*args, **kwargs): result = original_function(*args, **kwargs) with open(logfilename, "w") as logfile: logfile.write("Функция 1%s1 вызвана с позици- онными аргументами %s и именованными параметрами %s. Ре- зультат был %s.\n'' % (original-function___name__, args, kwargs, result)) return result return new.function 2. Пример программы: ©log def add(x,y): return x + у
print(add(3.5, 7)) with cpen("log.txt", " r") as logfile: print(logfile.read()) Ответ на упражнение 8 1. Пример программы: import math # 7. Квадрат числа а = lambda х: х**2 # 2. Корень из суммы квадратов b = lambda х, у: math.sqrt(x**2+ у**2) # 3. Среднее значение произвольного количества чисел с = lambda *args: sum(args)/len(args) # 4. Уникальные буквы в строке d = lambda s: "".join(set(s)) 2. Пример программы: # реализация на основе применения обычных именованных функций import math def а(х): return х**2 def b(x, у): return math.sqrt(x**2+ y**2)
def c(*args): return sum(args)/len(args) def d(s): return "".join(set(s)) Ответ на упражнение 9 Пример программы: def my..gen(n): i = n while i >= 0: yield i i -= 1 for x in my_gen(3): print(x)
Классы Ранее мы уже видели, как можно использовать словарь для группировки связанных данных, и как функции позволя- ют создавать сокращения для часто используемых групп опе- раторов. Функция выполняет некоторое действие, используя заданные входные параметры. Однако не все функции приме- нимы ко всем типам данных. Классы — это способ объедине- ния связанных данных и функций, которые с этими данными работают. Класс — это разновидность типа данных, подобно строкам, целым числам или спискам. Когда мы создаем объект этого типа данных, мы говорим, что создаем экземпляр класса. Как мы уже упоминали, в некоторых языках программи- рования одни сущности являются объектами, а другие — нет. В Python все является объектом — любой элемент представ- ляет собой экземпляр какого-либо класса. В ранних верси- ях Python различали встроенные типы и пользовательские классы, но теперь это различие полностью исчезло. Сами классы и типы являются объектами и имеют тип type. Опре- делить тип любого объекта можно с помощью встроенной функции type: type(nio6oH_o6heKT) Значения данных, которые мы храним внутри объекта, на- зываются атрибутами, а функции, связанные с объектом, — методами. Мы уже использовали методы некоторых встроен- ных объектов, таких как строки и списки.
Создавая собственные объекты, мы должны решить, какие данные нужно объединить, и что именно будет представлять наш объект. Иногда объекты напрямую отображаются на реальные сущ- ности. Например, при моделировании химических реакций можно создать объекты Atom и комбинировать их в объекты Molecule. Зачастую все же нет необходимости, или даже воз- можности, создать все программные объекты кода полностью соответствующими их реальным аналогам. Иногда мы создаем объекты, не имеющие аналогов в дей- ствительности, просто потому что это удобный способ группи- ровки связанных функций. Определение и использование класса Вот пример простого пользовательского класса, который хранит информацию о человеке: import datetime # будем использовать для работы с датами class Person: def __init__(self, name, surname, birthdate, address, telephone, email): self.name = name self.surname = surname self.birthdate = birthdate self.address = address self.phone = telephone self.email = email def age(self):
today = datetime.date.today() age = today.year - self.birthdate.year if today < datetime.date(today.year, self, birthdate.month, self.birthdate.day): age -= 1 return age person = Person( ’Джейн", "Доу", datetime.date(1992, 3, 12), # год, месяц, день "№ 12 Шорт-стрит, Гринвилл", "555 456 0987", "jane.doe@example.com" print(person.name) print(person.email) print(person.age()) Мы начинаем определение класса с ключевого слова class, за которым следуют имя класса и двоеточие. Если класс на- следует другие классы, они указываются в круглых скобках перед двоеточием — в этом случае таких родительских клас- сов нет, поэтому скобки можно опустить. В теле класса мы определяем две функции — это мето- ды нашего объекта. Первая называется_in it__, это специ- альный метод. Когда мы вызываем объект класса, создает- ся новый экземпляр класса, и в нем сразу вызывается метод
__in it__co всеми параметрами, которые мы передали объек- ту класса. Таким образом, цель этого метода — создать новый объект, используя данные, которые мы передали. Второй метод — это пользовательский метод, который вы- числяет возраст человека, используя дату рождения и теку- щую дату. Примечание _init__иногда называют конструктором, потому что он используется ана- логично тому, как конструкторы используются в других языках. Однако тех- нически это неправильно - лучше называть его инициализатором Суще- ствует и другой метод_new_, который ближе по смыслу к настоящему конструктору, но он используется крайне редко Вы, наверное, заметили, что оба метода принимают self в качестве первого параметра, и этот параметр используется внутри методов, хотя явно мы его не передаем. Это потому, что при вызове метода объекта сам объекта автоматически передается как первый аргумент. Это дает нам возможность получить доступ к свойствам объекта изнутри его методов. В некоторых языках этот параметр является неявным, то есть он не виден в сигнатуре функции, и мы обращаемся к нему с помощью специального ключевого слова. В Python он раскрывается явно и не обязательно должен называться self — так принято, но это не обязательно. Теперь вы должны видеть, что наш метод_______init___соз- дает атрибуты объекта и присваивает им значения, пере- данные в качестве параметров. Мы используем одни и те же имена для атрибутов и параметров, но это не является обя- зательным. Метод аде не принимает никаких параметров, кроме self — он использует только данные, хранящиеся в атрибу-
тах объекта, и текущую дату (которую он извлекает с помо- щью модуля datetime). Обратите внимание, что атрибут birthdate сам по себе тоже является объектом. Класс date определен в модуле datetime, и мы создаем новый экземпляр этого класса, чтобы использовать его в качестве параметра birthdate, когда соз- даем экземпляр класса Person. Нам не нужно присваивать его промежуточной переменной, прежде чем использовать в каче- стве параметра Person; мы можем просто создать его при вы- зове Person, точно так же, как мы создаем строковые литера- лы для других параметров. Важно помнить, что определение функции не вызывает ее исполнение. То же справедливо и для класса: при его определе- нии ничего не происходит, Python просто «запоминает» класс. Класс не будет определен, пока Python не выполнит весь блок класса, поэтому вы можете быть уверены, что сможете вызы- вать один метод внутри другого или даже ссылаться на сам класс внутри его методов — ко времени вызова класс уже точ- но будет определен. Упражнение 1 Объясните, на что ссылаются следующие переменные и какова их область видимости: 1. Person 2. person 3. surname 4. self 5. age (название функци)
6. age (переменная, используемая внутри функции) 7. self. email 8. person. email Атрибуты экземпляра Важно понимать, что атрибуты, установленные для объекта в функции__in it__, не являются исчерпывающим списком всех атрибутов, которые объект может когда-либо иметь. В некоторых языках вы должны указать список атрибутов объекта в определении класса. Для этих атрибутов при созда- нии объекта выделяются специальные места, и добавление новых атрибутов после этого запрещено. В Python вы можете динамически добавлять объекту новые атрибуты — и даже методы — во время выполнения. На самом деле функция __in it_не обладает никакими особыми привилегиями в плане задания атрибутов. Мы могли бы, например, сохра- нить рассчитанный возраст (в виде кешированного значения) прямо в методе аде: def age(self): if hasattr(self, "_age"): return self.„age today = datetime.date,today() age = today.year - self.birthdate.year
if today < datetime.date(today.year, self.birthdate, month, self.birthdate.day): age -= 1 self._age = age return age Примечание Начинать имя метода или атрибута с подчеркивания (_) - это соглашение, обозначающее, что это «внутреннее» или «приватное» свойство, к которому не следует обращаться напрямую. В реальных сценариях кеш может устаре- вать, и значение потребуется пересчитать заново - именно поэтому всегда следует использовать метод аде, чтобы получить актуальное значение Мы даже можем добавить совершенно не связанный с объ- ектом атрибут извне: person.pets = ['кошка , 'кошка', 'собака']. Очень часто методы объекта обновляют значения атри- бутов объекта, и это нормально. Однако считается плохой практикой создавать новые атрибуты внутри методов, если они не были инициализированы в____in it__. Еще менее же- лательна установка произвольных свойств извне объекта, по- скольку это нарушает объектно-ориентированную парадигму (о чем пойдет речь в следующей главе). Метод___in it__всегда выполняется первым при создании объекта — и именно в нем рекомендуется инициализировать все данные. Если атрибут создается вне_in it__, возника- ет риск обращения к нему до его появления. В приведенном выше примере с методом аде мы должны проверить, суще- ствует ли атрибут _аде у объекта, прежде чем попытаться его использовать, потому что если мы не запускали метод аде, то он еще создан не будет. Но лучше всего было бы вызвать
этот метод хотя бы один раз из_init___, чтобы гарантиро- вать наличие _аде сразу при создании объекта. Инициализация всех атрибутов в______init__, даже если мы просто установим для них пустые значения, снижает риск ошибок. Кроме того, его легче читать и понимать — мы сразу видим, какие атрибуты есть у нашего объекта. Метод____init__не должен принимать никаких параме- тров (кроме self), а может и вовсе отсутствовать. getattr, setattr и hasattr Что делать, если нам нужно получить или установить зна- чение атрибута объекта, имя которого хранится в виде строки? Например, при переборе имен атрибутов в цикле, аналогично работе со словарем: for key in ["a", "b", "с"]: print(mydict[key]) В случае объекта мы не можем использовать оператор . на- прямую, так как он требует жестко заданного имени атрибута. Если имя хранится в строке, используется встроенная функция getatt г: for key in ["a", "b", "с"]: print(getattr(myobject, key, None)) Обратите внимание, что getattr — это не метод объек- та, а встроенная функция: в качестве первого параметра она принимает сам объект, второй параметр — это имя атрибута в виде строки, а необязательный третий параметр — это зна- чение по умолчанию, которое будет возвращено, если атрибут не существует. Если не указать значение по умолчанию и атри- бут не найден — будет выброшено исключение.
Аналогично setattr позволяет задать значение атрибута. В следующем примере данные из словаря копируются в объ- ект: for key in ["a", "b", "с"]: setattr(myobject, key, mydict[key]) Первый аргумент setattr — объект, второй — имя атрибу- та, а третий — новое значение атрибута. Как было показано ранее в методе аде, функция hasattr позволяет проверить наличие атрибута у объекта. Хотя технически мы можем использовать функцию get at t г, даже если известно имя атрибута, делать так не рекомендует- ся — это неоправданно громоздкий способ: getattrfmyobject, "а") # означает то же самое, что и myobject.а Используйте getattr, setattr и hasattr только при необ- ходимости. Упражнение 2 Перепишите класс Person так, чтобы возраст человека рассчитывался сразу при создании нового экземпляра, а при последующих запросах пересчитывался только в том случае, если с момента последнего расчета изме- нилась дата.
Атрибуты класса Все атрибуты, которые определены для экземпляра Person, являются атрибутами экземпляра — они появляются при вы- зове метода in it . Однако мы также можем определить атрибуты, которые устанавливаются для класса. Эти атрибуты будут общими для всех экземпляров данного класса. Во мно- гом они ведут себя так же, как и атрибуты экземпляра, но есть некоторые оговорки, о которых вы должны знать. Мы определяем атрибуты класса в теле класса, на том же уровне отступа, что и определения методов (на один уровень выше, чем тело метода): class Person: TITLES = ('Dr', 'Mr', 'Mrs', 'Ms') def __init___(self, title, name, surname): if title not in self.TITLES: raise ValueError("%s не яьляется правильным названием." % title) self.title = title self.name = name self.surname = surname Как видите, мы обращаемся к атрибуту класса TITL ES точно так же, как и к атрибуту экземпляра — он доступен как свой- ство объекта экземпляра, к которому мы обращаемся внутри метода через переменную self. Все объекты Person, которые мы создадим, будут иметь один и тот же атрибут TITLES.
Атрибуты класса часто используются для определения кон- стант, которые тесно связаны с определенным классом. К ним можно обращаться как через экземпляр, так и через сам класс: # мы можем получить доступ к атрибуту класса из экзем- # пляра person.TITLES # но мы также можем получить доступ к нему из класса Person.TITLES Обратите внимание, что объект класса не имеет доступа к атрибутам экземпляра — они создаются только при созда- нии экземпляра! # Это приведет к ошибке Person.паше Person.surname Атрибуты класса также иногда могут использоваться для предоставления значений атрибутов по умолчанию: class Person: deceased = False def mark_as_deceased(self): self.deceased = True Когда мы устанавливаем для экземпляра атрибут, имя кото- рого совпадает с именем атрибута класса, мы переопределяем атрибут класса атрибутом экземпляра, который будет иметь приоритет над ним. Если мы создадим два объекта Person и вызовем метод mark_as_deceased на одном из них, второй объект не изменится. Однако следует быть осторожным, если
атрибут класса имеет изменяемый тип — ведь если мы изме- ним его, то одновременно затронем все объекты этого класса. Помните, что все экземпляры имеют одинаковые атрибуты класса: class Person: pets = [] def add_pet(self, pet): self.pets.append(pet) jane = Person() bob = Person() jane.add_pet("кошка") prlnt(jane.pets) print(bob.pets) # неожиданно тоже содержит "кошка" В подобных случаях нам следует инициализировать изме- няемый атрибут как атрибут экземпляра внутри_____init___. Тогда у каждого экземпляра будет своя отдельная копия: class Person: def___init__(self): self.pets = [ ] def add_pet(self, pet): self.pets.append(pet) jane = Person() bob = Person()
jane.add_pet( "кошка ') print(jane.pets) print(bob.pets) Обратите внимание, что определения методов находят- ся в той же области видимости, что и определения атрибу- тов класса, поэтому мы можем использовать имена атрибутов класса в качестве переменных в определениях методов (без self, который определяется только внутри методов): class Person: TITLES = ('Dr', Mr', 'Mrs', Ms') def __init__(self, title, name, surname, allowed, titles = TITLES): if title not in allowed.titles: raise ValueError("%s не является правильным названием." % title) self.title = title self.name = name self.surname = surname Может ли класс иметь методы? Да, может. В следующем разделе мы рассмотрим, как определить их с помощью деко- ратора. Упражнение 3 Объясните разницу между атрибутами name, surname и profession и то, какие значения они могут иметь в разных экземплярах этого класса:
class Smith: surname = "Smith’ profession = "smith" def __init__(self, name, profession = None): self.name - name if profession is not None: self.profession = profession Декораторы классов В предыдущей главе мы узнали о декораторах — функциях, которые используются для изменения поведения других функ- ций. Существует несколько встроенных декораторов, которые часто используются в определениях классов. @classmethod Точно так же, как мы можем определять атрибуты клас- са, которые являются общими для всех экземпляров класса, мы можем определять методы класса. Их создают с помощью декоратора ©classmethod. Метод класса по-прежнему принимает в качестве первого аргумента вызывающий его объект — не self, a cis — ссылку на сам класс. Если мы вызываем метод класса из экземпляра, этот параметр будет содержать объект экземпляра, но если мы вызываем его из класса, он будет содержать объект класса. Присваивая параметру имя cis, мы напоминаем себе, что ему не гарантировано наличие атрибутов экземпляра.
Иногда возникает необходимость выполнять действия, свя- занные не с конкретным экземпляром, а с самим классом. В таких случаях мы можем использовать методы класса — они позволяют обращаться к атрибутам и выполнять действия на уровне класса, без создания экземпляра. Если бы для этого приходилось использовать обычные методы экземпляра, нам пришлось бы создавать объект, даже если он нам не нужен. Это было бы весьма расточительно. Например, мы можем ис- пользовать класс просто как способ сгруппировать связанные между собой константы и функции. Такие классы могут вооб- ще не иметь экземпляров, но при этом оставаться полезными за счет логической организации кода. Иногда полезно написать метод класса, который создает эк- земпляр класса после предварительной обработки входных данных, чтобы они были в правильном формате для переда- чи в конструктор класса. Это позволяет сделать конструктор __in it__максимально простым и не реализовывать слож- ный код анализа входных параметров: class Person: def __init__(self, name, surname, birthdate, address, telephone, email): self.name = name # (...) ©classmethod def from_text_file(cls, filename): # извлекаем все параметры из текстового файла return cls(*params) # это то же самое, что и вы- # зов Person(*params)
@staticmethod Статический метод не имеет вызывающего объекта, переда- ваемого ему в качестве первого параметра, т. е. не получают автоматически ни self, ни cis. Это означает, что он не зна- ет, какой именно объект его вызвал (экземпляр или сам класс). Также он он не имеет доступа ни к атрибутам экземпляра (self), ни к атрибутам класса (cis). Мы можем вызывать их из экземпляра или объекта класса, но чаще всего они вызыва- ются из объектов класса, как и методы класса. Если мы используем класс для объединения связанных ме- тодов, которым не нужно обращаться друг к другу или к лю- бым другим данным класса, мы можем использовать эту тех- нику. Преимущество использования статических методов заключается в том, что мы исключаем ненужные параме- тры cis или self из определений наших методов. Недостат- ком является то, что если мы иногда хотим сослаться на метод или атрибут другого класса внутри статического метода, нам приходится писать имя класса полностью, что бывает доволь- но трудоемким, поскольку имена классов могут быть доволь- но длинными. И с этой точки зрения это менее удобно, чем ис- пользование переменной cis, которая доступна нам внутри метода класса. Вот краткий пример сравнения методов трех типов: class Person: TITLES = ('Dr', 'Mr', 'Mrs', 'Ms') def __init__(self, name, surname): self.name = name self.surname = surname def fullname(self): # метод экземпляра
# объект экземпляра, доступный через self return "%s %s" % (self.name, self.surname) ©classmethod def allowed_titles_starting_with(cls, startswith): # метод класса # класс или объект экземпляра, доступный че- # рез cis return [t for t in cis.TITLES if t .startswith(startswith)j ©staticmethod def allowed_titles_ending_with(endswith): # стати- # чес кий метод # нет параметра для класса или объекта экзем- # пляра # мы должны использовать Person напрямую return [t for t in Person.TITLES if t.endswith(endswith)] jane = Person("Jane", "Smith) print(jane.fullname()) print(jane.allowed_titles_starting_with(' M")) print(Person.allowed_titles_starting_with("M")) print (jane .allowed_titles_ending_with( "s")) print(Person.allowed_titles_ending_with(' s"))
^property Иногда мы используем метод, чтобы динамически вычис- лить некоторое свойство объекта на основе других его атри- бутов. В некоторых случаях можно просто использовать метод для доступа к одному атрибуту, чтобы возвратить его значения. Также можно определить отдельный метод, чтобы изменить значение этого атрибута вместо прямого обращения к нему. Подобные методы называются геттерами и сеттерами, по- скольку они «получают» и «устанавливают» значения атрибу- тов соответственно. В некоторых языках рекомендуется всегда использовать гет- теры и сеттеры для доступа к атрибутам и никогда не обра- щаться к ним напрямую. Более того, такие языки часто пре- доставляют механизмы, ограничивающие прямой доступ к атрибутам, позволяя работать с ними только через геттеры и сеттеры. В Python прямой доступ к простым атрибутам впол- не допустим и считается нормальной практикой, а написание геттеров и сеттеров для всех атрибутов считается избыточным. Кроме того, сеттеры могут быть неудобны, потому что не под- держивают составные операторы присваивания: class Person: def __init__(self, height): self.height = height def get_height(self): return self.height def set_height(self, height) : self.height = height jane = Person(153) # Рост Джейн составляет 153 см.
jane.height += 1 # Джейн подросла на сантиметр jane.set_height(jane.height + 1) # Джейн снова подросла Как мы видим, увеличение атрибута height через сеттер делает код более громоздким. Конечно, мы можем написать второй сеттер, который увеличивает атрибут на заданный параметр — но тогда придется писать такие методы для всех атрибутов и всех возможных вариантов их изменения. С по- хожими трудностями мы столкнемся и при работе с изме- няемыми объектами, например при добавлении элементов в список. Одним из преимуществ использования геттеров и сетте- ров является то, что мы можем изменить способ вычисления или хранения значения атрибута внутри класса, не затраги- вая остальной код, который использует этот класс. Например, предположим, что мы изначально создали класс Person, ко- торый имеет атрибут fullname, но позже мы хотим изменить класс так, чтобы он имел отдельные атрибуты name и surname, а полное имя формировалось на их основе. Если доступ к атри- буту fullname всегда осуществляется через геттер, мы можем просто изменить реализацию геттера — и весь остальной код продолжит работать без изменений. Но что если наш код обращается к атрибуту fullname на- прямую? Можно, конечно, написать метод fullname(), воз- вращающий нужное значение, но методы надо вызывать с круглыми скобками. К счастью, декоратор ^property позво- ляет нам заставить метод вести себя как атрибут: class Person: def __init__(self, name, surname): self.name = name self.surname = surname
©property def fullname(self): return "%s %s" % (self.name, self.surname) jane = Personf"Джейн", Смит") print(jane.fullname) # без скобок! Существуют также декораторы, с помощью которых мы мо- жем определить сеттер (setter) и делитер (delete) для нашего атрибута (делитер удалит атрибут из нашего объекта). Мето- ды getter, setter и deleter должны иметь одинаковое имя: class Person: def __init__(self, name, surname): self.name = name self.surname = surname ©property def fullname(self): return "%s %s" % (self.name, self.surname) ©fullname.setter def fullname(self, value): # В реальной жизни разбор может быть сложнее name, surname = value.split(" ", 1) self.name = name self.surname = surname ©fullname.deleter def fullname(self): del self.name
del self.surname jane = Person("Джейн", "Смит") print(jane.fullname) jane.fullname = "Джейн Доу" print(jane.fullname) printjjane.name) print(jane.surname) Упражнение 4 Создайте класс Numbers, который имеет единственный атрибут класса MULTIPLIER и конструктор, принимаю- щий параметры х и у (все они должны быть числами). 1. Напишите метод add, который возвращает сумму атри- бутов х и у. 2. Напишите метод класса multiply, который принимает один числовой параметр а и возвращает произведение а и MULTIPLIER. 3. Напишите статический метод subtract, который при- нимает два числовых параметра, b и с, и возвращает b - с. 4. Напишите метод value, который возвращает кортеж, содержащий значения х и у. Сделайте этот метод свой- ством и и добавьте к нему сеттер и делитер для работы со значениями х и у.
Исследование объекта Проверить, какие свойства и методы определены у объекта, можно с помощью функции dir: class Person: def __init__(self, name, surname): self.name = name self.surname = surname def fullnamefself): return "%s %s" % (self.name, self.surname) jane = Person(“Джейн", “Смит") print(dir(jane)) Теперь мы видим наши атрибуты и наш метод, но также множество других элементов. Что это за дополнительная ин- формация? Мы обсудим наследование в следующей главе, а пока вам нужно знать, что любой класс, который вы опре- деляете, имеет в качестве родительского класса object, даже если вы это явно не указываете — поэтому ваш класс содер- жит множество стандартных атрибутов и методов, которые есть у любого объекта Python. Примечание В Pylhon 2 требовалось явно наследовать object, иначе класс считался «классом старого стиля» и не имел большинства встроенных свойств Такие классы не рекомендуется использовать Если бы мы писали класс Person в Python 2, мы бы написали первую строку так class Person(object) Именно поэтому вы можете просто не указывать метод __init___в своем классе, если вам не нужно проводить ини- циализацию — вместо него будет использоваться метод
по умолчанию, унаследованный от класса object (который ничего не делает). Если вы напишете свой собственный метод __in it_, он будет переопределять метод по умолчанию. Та- кое поведение часто называют перегрузкой. Многие стандартные методы и атрибуты в Python имеют име- на с двойным подчеркиванием в начале и в конце, например __init__или___str___.Такие имена указывают на то, что эти свойства зарезервированы — вам не следует создавать соб- ственные методы или атрибуты с такими же именами, если вы не собираетесь их перегружать. Эти свойства обычно являют- ся методами, и их иногда называют магическими методами. Мы можем использовать функцию dir для любого объекта. Вы можете попробовать использовать его на всех видах объек- тов, с которыми мы уже встречались ранее, таких как числа, списки, строки и функции, чтобы посмотреть, какие встроен- ные свойства есть у этих объектов. Вот некоторые распространенные магические методы и атрибуты: •___init__: метод инициализации объекта, который вы- зывается при его создании. •___str_: метод представления объекта в виде строки, который вызывается, когда вы используете функцию str для преобразования объекта в строку. •___class__: атрибут, содержащий класс объекта (тип), возвращается функцией type, примененной к объекту. •___eq__: метод, определяющий, равен ли данный объект другому. Существуют также другие методы для определе- ния того, не равен ли он, меньше чем и т. д. Эти методы используются при сравнении объектов, например, когда мы используем оператор равенства ==, чтобы проверить, равны ли два объекта.
• __add_— это метод, который позволяет добавить дан- ный объект к другому объекту Существуют эквивалент- ные методы для всех остальных арифметических опера- торов. Не все объекты поддерживают все арифметические операции — у чисел определены все эти методы, но у дру- гих объектов может быть только их некоторое подмноже- ство. •___iter__: метод, который возвращает итератор для объ- екта — мы можем найти его в строках, списках и других итерируемых объектах. Он выполняется, когда мы приме- няем к объекту функцию iter. • __1еп_: метод, который вычисляет длину объекта — мы найдем его во всех последовательностях. Он выполня- ется, когда мы применяем к объекту функцию 1еп. •___diet__: словарь, содержащий все атрибуты экзем- пляра объекта, с их именами в качестве ключей. Он мо- жет быть полезен, если мы хотим перебрать все атрибу- ты объекта.__diet___не содержит методов, атрибутов класса или специальных атрибутов по умолчанию, таких как___class__. Упражнение 5 Создайте экземпляр класса Person из примера 2. Вызови- те функцию dir для экземпляра, затем для класса. 1. Что произойдет, если вызвать метод_str___из эк- земпляра? Убедитесь, что вы получите тот же резуль- тат, если вызовете функцию str с экземпляром в каче- стве параметра. 2. Какой тип у экземпляра?
3. Какой тип у класса? 4. Напишите функцию, которая принимает любой объект и выводит имена и значения всех его пользовательских атрибутов. Переопределение магических методов Мы уже рассмотрели, как перегрузить метод __init__, чтобы настроить его для инициализации нашего класса. Мы также можем перегружать другие специальные методы. Например, цель метода_str__— вывести полезное строко- вое представление нашего объекта. Но по умолчанию, если мы применим функцию st г (которая вызовет метод_str_) к объекту person, все, что мы получим, это имя класса и ID. Это не очень полезно! Давайте напишем собственный метод __str__, который выведет на экран значения всех свойств объекта: import datetime class Person: def __inlt__(self, name, surname, birthdate, address, telephone, email): self.name = name self.surname = surname self.birthdate = birthdate self.address = address self.phone = telephone self.email = email
def __str___(self): return "%s %s, родился %s\nAflpec: %5\пТелефон: %s\ nEmail:%s" % (self.name, self.surname, self.birthdate, self.address, self.telephone, self.email) jane = Person( "Джейн", "Доу", datetime.date(1992, 3, 12), # год, месяц, день "№ 12 Шорт-стрит, Гринвилл", "555 456 0987", "]ane.doe@example.com" pnnt(jane) Обратите внимание, что когда мы вставим объект даты рождения в строку вывода с помощью %s, он сам будет преоб- разован в строку, поэтому нам не нужно делать это самостоя- тельно (если только мы не хотим изменить формат вывода). Также бывает полезно выполнить перегрузку методов срав- нения, чтобы можно было использовать операторы сравнения с экземплярами класса Person. По умолчанию экземпляры это- го класса считаются равными только в том случае, если они яв- ляются одним и тем же объектом. Кроме того, нельзя сравни- вать их на болыпе/меньше, так как объекты Person не имеют встроенного порядка. Предположим, мы хотим, чтобы два объекта Person счита- лись равными, если все их атрибуты совпадают по значению, а порядок между ними определяется в алфавитном поряд- ке — сначала по фамилии, затем по имени. Все магические методы сравнения независимы друг от друга, поэтому, чтобы они работали, нам придется их все перегрузить. Но, к счастью,
если выполнить перегрузку метода проверки на равенство __eq___и один из базовых методов сравнения, остальные ре- ализуются достаточно просто. Каждый из этих методов при- нимает два параметра — self для текущего объекта и othe г для другого объекта: class Person: def __init__(self, name, surname): self.name = name self.surname = surname def __eq__(self, other): # self == other? return self.name == other.name and self.surname == other.surname def __gt__(self, other): # self > other9 if self.surname == other.surname: return self.name > other.name return self.surname > other.surname # теперь определим остальные методы на основе пер- вых двух def __ne__(self, other): # self != other? return not self == other # вызывает self.__eq____ (other) def __le__(self, other): # self <= other? return not self> other # вызывает self.__gt____ (other) def __It__(self, other): # self < other? return not (self> other or self == other)
def __ge___(self, other): # is self >= other? return not self < other Важно понимать: объект other не обязан быть экземпляром класса Person, и мы не ввели никаких проверок, чтобы убе- диться, что это так. Если у переданного объекта нет атрибутов name или surname, метод вызовет ошибку, но если они при- сутствуют, сравнение будет работать. Имеет ли это смысл или нет — об этом нам нужно будет подумать, если мы будем создавать подобные типы объектов. Иногда имеет смысл выдавать ошибку, если другой объект не имеет того же типа, что и наш объект, а иногда допустимо сравнивать совместимые объекты разных типов. Например, имеет смысл сравнить 1 и 2.5, потому что они оба являются числами, хотя одно из них — целое число, а другое — число с плавающей точкой. Примечание В Python 2 также существует метод_стр_, который был введен в язык до появления отдельных методов сравнения (называемых «расширенными сравнениями»), описанных выше Он используется, если эти «расширенные сравнения» не определены Вы должны перегрузить его таким образом, что- бы он соответствовал методам расширенных сравнений, иначе вы можете столкнуться с очень странным поведением Упражнение 6 Напишите класс для создания полностью универсальных объектов: его функция_init__должна принимать лю- бое количество именованных аргументов и устанавливать их в качестве атрибутов объекта (имя атрибута соответ- ствует ключу).
Напишите метод______str__для этого класса — воз- вращаемая им строка должна содержать имя клас- са и значения всех пользовательских атрибутов экземпляра.
Ответы на упражнения Ответ на упражнение 1 1. Person — это класс, определенный в глобальной области видимости. Это глобальная переменная. 2. person — это экземпляр класса Person. Это также гло- бальная переменная. 3. surname — это параметр, передаваемый в метод ____in it_— это локальная переменная в области видимости метода__init___. 4. self — это параметр, передаваемый в каждый метод эк- земпляра класса — он будет заменен на объект экземпляра, когда метод будет вызван на объекте с помощью оператора .. Это локальная переменная в каждом методе, но она всегда ссы- лается на текущий экземпляр и называется одинаково по со- глашению. 5. аде — это метод класса Person. Это локальная перемен- ная в области видимости класса. 6. аде (переменная, используемая внутри функции) являет- ся локальной переменной в области видимости метода аде. 7. На самом деле self .email не является отдельной пере- менной. Это способ обращения к атрибуту экземпляра через пе- ременную self с помощью оператора .. Мы используем пе- ременную self для ссылки на объект внутри одного из его собственных методов — везде, где определена переменная self, мы можем использовать self .email, self .age () ит.д.
8. person.email — аналогично, но в глобальной области видимости, person — имя переменной, ссылающейся на объ- ект, a .email — доступ к его атрибуту. Везде, где определена переменная person, мы можем использовать person .email, person . age() и т. д. Ответ на упражнение 2 Пример программы: import datetime class Person: def __init__(self, name, surname, birthdate, address, telephone, email): self.name = name self.surname = surname self.birthdate = birthdate self.address = address self.phone = telephone self.email = email # это не обязательно, но позволяет явно задать атрибуты self._age = None self._age_last_recalculated = None self._recalculate_age() def _recalculate_age(self): today = datetime.date.today()
age = today.year - self.birthdate.year if today < datetime.date(today.year, self, birthdate.month, self.birthdate.day): age -= 1 self.„age = age self._age_last_recalculated = today def age(self): if (datetime.date.today()> self._age_last recalculated): self._recalculate_age() return self._age Ответ на упражнение 3 Переменная name всегда является атрибутом экземпляра, который устанавливается в конструкторе, и каждый экзем- пляр класса может иметь разное значение name. Переменная surname всегда является атрибутом класса и не может быть пе- реопределена в конструкторе — каждый экземпляр будет иметь значение surname, равное Smith. Переменная profession является атрибутом класса, но по желанию может быть перео- пределена в конструкторе. Каждый экземпляр будет1 иметь зна- чение smith (т. е. «кузнец»), если только необязательный пара- метр р rof ession не передан в конструктор с другим значением. Ответ на упражнение 4 Пример программы:
class Numbers: MULTIPLIER =3.5 def __init__(self, x, y): self.x = x self.у = у def add(self): return self.x + self.у ©classmethod def multiply(cis, a): return cis.MULTIPLIER * a ©staticmethod def subtract(b, c): return b - c ©property def value(self): return (self.x, self.y) ©value.setter def value(self, xy.tuple): self.x, self.y = xy.tuple ©value.deleter def value(self): del self.x del self.y
Ответ на упражнение 5 1. Вы должны увидеть что-то вроде ' <_main_.Person object at 0x7fcb233301d0>'. 2. <class '_main___.Per son1> -___main__— это имя текущей выполняемой программы в Python. 3. <class ' type' > —любой класс имеет тип type. 4. Пример программы: def print_object_attrs(any_object): for k, v in any.object.__dict__.items(): print("%s: %s" % (k, v)) Ответ на упражнение 6 Пример программы: class AnyClass: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def __str___(self): attrs = ("%s= %s" % (k, v) for (k, v) in self., diet__.items()] classname = self.___class__.__name___ return "%s: %s" % (classname, " ".join(attrs))
Объектно-ориентированное программирование Введение Как вы видели в самых первых примерах кода в этой кни- ге, при программировании на Python не обязательно струк- турировать код в виде классов. Можно использовать функ- ции сами по себе — такой подход называется процедурным программированием. Однако, хотя процедурный стиль впол- не подходит для написания коротких и простых программ, по мере увеличения их размера и сложности объектно- ориентированный подход (ООП) становится все более вос- требованным. Чем больше данных и функций содержит ваш код, тем важ- нее организовать их в логически обоснованные подмодули, следя за тем, чтобы связанные между собой данные и функции были сгруппированы вместе, а несвязанные данные и функ- ции не мешали друг другу. Модульный код проще читать, те- стировать и изменять, а также легче использовать повторно — а повторное использование кода ценно, поскольку сокращает время разработки. В качестве наихудшего сценария представьте себе про- грамму с сотней функций и сотней отдельных глобальных переменных в одном файле. Такую программу будет очень сложно поддерживать. Все переменные потенциально мо- гут быть изменены всеми функциями, даже если это не пред- полагается. А чтобы избежать конфликтов имен для пере-
менных с похожим назначением, нам, возможно, пришлось бы прибегать к неудачным приемам назначения имен. В та- кой ситуации легко запутаться, какая переменная использу- ется в какой функции. Даже без объектной модели можно попытаться сделать та- кой код более модульным. Мы могли бы сгруппировать связан- ные переменные в составные структуры данных. В некоторых языках, например C++, для этого раньше использовали тип struct, который изначально не имел методов — только атри- буты, — но в итоге стал неотличим от класса. Это позволило программистам объединять отдельные переменные в состав- ные объекты и стало первым шагом к ООП. В Python мы часто используем словари для временного объединения связанных данных. Мы также можем разделить функции и данные по разным пространствам имен, вместо того чтобы определять их все в одном глобальном пространстве имен. Обычно это сопрово- ждается физическим разделением кода на несколько файлов. В Python мы делаем это, разделяя код на модули. Основное дополнительное преимущество объектно-ориен- тированного подхода, как мы уже обсуждали в предыдущей главе, — это объединение данных и функций, работающих с ними, в единую структуру Это облегчает поиск связанных ча- стей кода, поскольку они физически определены в непосред- ственной близости друг от друга, а также позволяет писать код таким образом, чтобы доступ к данным внутри каждого объек- та осуществлялся по возможности только через методы этого объекта. Этот принцип называется инкапсуляцией, и мы под- робно обсудим его в следующем разделе. Многие считают, что ООП — более интуитивно понят- ный стиль программирования, поскольку людям легко рас- суждать об объектах и отношениях между ними. Таким об-
разом, ООП иногда считается лучшим подходом, поскольку позволяет начинающим программистам быстрее овладеть навыками. Основные принципы ООП Важнейшим принципом объектно-ориентированного про- граммирования является инкапсуляция: идея о том, что до- ступ к данным внутри объекта должен осуществляться толь- ко через общедоступный интерфейс, то есть через методы объекта. Функция аде, которую мы рассматривали в предыдущей главе, — хороший пример этой философии. Если мы хотим ис- пользовать данные, хранящиеся в объекте, для выполнения какого-либо действия или вычисления производного значе- ния, мы создаем метод, связанный с этим объектом. Затем вся- кий раз, когда мы хотим выполнить это действие, мы просто вызываем метод. Считается плохой практикой извлекать дан- ные из объекта и выполнять над ними действия вне его. Инкапсуляция полезна по нескольким причинам: • функциональность определена в одном месте, а не раз- бросана по тексту программы. • она реализована логически — именно там, где хранятся соответствующие данные. • данные объекта защищены от непреднамеренных изме- нений извне. • используя метод, мы знаем лишь, какой результат он возвращает, но нам не нужно знать детали внутрен- него устройства объекта. Мы можем заменить объект другим с совершенно иной реализацией, если они име- ют одинаковый интерфейс — без необходимости менять остальной код.
Говоря образно, объект «знает», как работать со своими дан- ными, и нам не стоит вмешиваться в его внутреннюю структу- ру вручную и делать что-то с данными самостоятельно. Если объект не предоставляет нужного метода, правильнее доба- вить его или обновить существующий. В некоторых языках, таких как Java или C++, инкапсуляция может быть реализована жестко: можно задать уровень до- ступа к атрибутам и запретить их использование вне методов объекта. В Java также считается хорошей практикой писать сеттеры и геттеры для всех атрибутов, даже если геттер просто извлекает атрибут, а сеттер просто присваивает ему значение параметра, который вы передаете. В Python инкапсуляция не обеспечивается языком, но суще- ствует общепринятая договоренность: если атрибут должен считаться внутренним, его имя начинается с символа подчер- кивания. Также в Python принято напрямую обращаться к простым атрибутам и создавать геттеры/сеттеры лишь для тех из них, которые требуют дополнительных вычислений. В предыду- щей главе мы узнали, как использовать декоратор @р горе rty, чтобы заменить простой атрибут методом без изменения ин- терфейса объекта. Отношения между объектами В следующем разделе мы рассмотрим различные способы связи классов друг с другом. В Python существует два основных типа отношений между классами: композиция и наследование.
Композиция Композиция — это способ объединения объектов путем включения одних объектов в другие в качестве атрибутов. В предыдущей главе мы видели, как можно включить объ- ект datetime.date в качестве атрибута в объект Person и использовать его для хранения даты рождения человека. Мы можем сказать, что у человека есть дата рождения — если мы можем выразить отношение между двумя классами с по- мощью фразы «имеет» (has-a), то это отношение композиции. Подобные отношения могут быть один-к-одному, один-ко- многим или многие-ко-многим, а также однонаправленными или двунаправленными, в зависимости от специфики ролей, которые выполняют объекты. Согласно некоторым формальным определениям, термин «композиция» подразумевает, что два объекта достаточно силь- но связаны — один объект принадлежит другому. Если объ- ект-владелец удаляется, зависимый объект тоже должен быть удален. Если связь между двумя объектами слабее, и ни один из них не обладает исключительным правом собственности друг на друга, ее назвают агрегацией. Вот четыре класса, которые демонстрируют несколько при- меров композиции и агрегации: class Student: def __init__(self, name, student.number): self.name = name self.student_number = student_number self.classes = [] def enrol(self, course.running): self.classes.append(course-running) course, running.add.student(self)
class Department: def___init__(self, name, department_code): self.name = name self.department_code = department_code self.courses = {} def add_course(self, description, course_code, credits): self.courses[course_code] = Course(description, course_code, credits, self) return self.courses[course_code] class Course: def __init__(self, description, course_code, credits, department): self.description = description self.course_code = course_code self.credits = credits self.department = department self.department.add.cou rse(self) self.runnings = [] def add_running(self, year): self.runnings.append(CourseRunning(self, year)) return self.runnings[-1] class CourseRunning: def___init__(self, course, year): self.course = course
self.year = year self.students = [] def add_student(self, student): self.students.append(student) maths_dept = Department ("Математика и прикладная мате- матика', МАМ’) mam1000w = maths_dept.add_course("Математика 1000 , "MAM1000W", 1) mam1000w_2013 = mam1000w.add_running(2013) bob = Student("Боб", "Смит") bob.enrol(mam1000w_2013) Почему для описания курса используются два разных клас- са? Это пример того, как перенос реальных понятий в объ- ектную модель не всегда очевиден. Можно ли было обойтись одним классом Course, содержащим и описание, и список студентов? Нам нужно представить два разных понятия, которые мож- но назвать «курсом»: первое — это теоретическая идея курса, который предлагается факультетом каждый год и всегда име- ет одно и то же название и код, а второе — это курс, который проводится в конкретном году, каждый раз с разной группой студентов. Мы представили эти два понятия двумя отдельны- ми классами, которые связаны друг с другом. Course — это те- оретическое описание курса, а Сои rseRunning — конкретный экземпляр курса. Мы определили несколько отношений между этими клас- сами:
• Студент <--► Конкретный курс (CourseRunning): многие- ко-многим, двунаправленная связь. Студент может быть зачислен на несколько курсов (объекты CourseRunning), а курс (CourseRunning) может иметь несколько студен- тов, зачисленных на него в определенном году, вот поче- му это отношение «многие-ко-многим». Студент знает обо всех своих курсах, а курс имеет запись обо всех зачислен- ных студентах, так что это двунаправленная связь. Эти объекты не очень сильно связаны друг с другом — сту- дент может существовать независимо от курса, а курс — независимо от студента. • Кафедра Курс (Course): один-ко-многим, двунаправлен- ная связь. Факультет предлагает несколько курсов (объек- ты Course), но в нашей реализации у курса может быть только один факультет — это отношение «один-ко-мно- гим». Оно также является двунаправленным. Кроме того, эти объекты более тесно связаны друг с другом — можно сказать, что факультет владеет курсом. Курс не может су- ществовать без кафедры. • Курс <-> Конкретное проведение курса (CourseRunning): также один-ко-многим, двунаправленно и прочно связа- ны. Аналогичные отношения существуют между курсом и его «запусками»: они также двунаправленные, один- ко-многим и тесно связаны — нет смысла в том, чтобы «MAM1000W run in 2013» существовал сам по себе в отсут- ствие «MAM1000W». Термины вроде «существует» и «владеет» могут иметь раз- ное значение для программы. Объект, который «владеет» дру- гим объектом, может отвечать за создание этого объекта, когда он ему нужен, и за его уничтожение, когда он больше не ну- жен — но эти слова также могут использоваться для описания
логических отношений между понятиями, которые не обяза- тельно буквально реализованы таким образом в коде. Упражнение 1 1. Кратко опишите возможную структуру классов, которые можно использовать для представления музыкальной коллекции (например, в музыкальном плеере), сосредо- точив внимание на связях между классами через компо- зицию. Вы должны включить классы для песен, исполни- телей, альбомов и плейлистов. Подсказка: запишите названия этих четыре классов, на- рисуйте линии между каждой парой, в которой, по ваше- му мнению, должна быть связь, и определите, какой тип связи (композиция, агрегирование и т. п.) наиболее уме- стен. Для упрощения допустим, что каждая песня или альбом имеют одного исполнителя (значение artist), при этом это значение может представлять собой как одного чело- века, так и группу. Однако вы должны предусмотреть аль- бомы-компиляции (которые содержат песни разных ис- полнителей). В таком случае в поле artist может быть записано специальное значение, например «Various Artists» («Разные исполнители»). Также допустим, что ка- ждая песня относится к одному альбому, но при этом воз- можны дубликаты одной и той же песни, включенные в разные альбомы. 2. Напишите простую реализацию этой модели, которая наглядно показывает, в каких отношениях состоят раз- личные классы. Также напишите пример кода, в котором создается альбом и все его песни добавляются в плейлист.
Подсказка: если два объекта связаны друг с другом дву- направленно, как установить такую связь — один из объ- ектов должен быть создан раньше другого, так что вы не можете связать их друг с другом в обоих направлениях одновременно! Наследование Наследование — это способ расположения объектов в ие- рархии от наиболее общего к наиболее конкретному. Объект, который наследуется от другого объекта (часто называемо- го родительским или суперклассом), считается его подтипом и получает все его атрибуты и методы. Как мы видели в пре- дыдущей главе, все объекты в Python наследуются от базово- го класса object. Таким образом, строка, целое число или эк- земпляр класса Person — это все экземпляры объектов. Когда мы можем описать отношения между двумя объектами с по- мощью фразы «является» is а, то это отношение реализуется через наследование. Мы также часто говорим, что класс является подклассом или дочерним классом другого класса, от которого он наследу- ется, или что другой класс является его суперклассом или роди- тельским классом. Самый общий класс, находящийся в осно- вании иерархии, принято называть базовым классом. Наследование позволяет удобно представлять объекты, име- ющие как общие, так и отличающиеся свойства. Мы можем поместить всю общую функциональность объектов в базовый класс, а затем определить один или несколько подклассов с их собственной функциональностью.
Наследование способствует повторному использованию су- ществующего кода. Если у нас уже есть класс, который делает почти то, что нам нужно, мы можем создать подкласс, в ко- тором частично переопределим его поведение или, возможно, добавим новую функциональность. Вот простой пример наследования: class Person: def __init__(self, name, surname, number): self.name = name self.surname = surname self.number = number class Student(Person): UNDERGRADUATE, POSTGRADUATE = range(2) def __init__(self, student-type, *args, **kwargs): self.student_type = student_type self.classes = [] super(Student, self).___init__(*args, **kwargs) def enrol(self, course): self.classes.append(course) class StaffMember(Person): PERMANENT, TEMPORARY = range(2) def___init__(self, employment-type, *args, **kwargs): self.employment-type = employment-type super(StaffMember, self)init__(*args, **kwargs)
class Lecturer(StaffMember): def_______init__(self, *args, **kwargs): self.courses_taught = [] super(Lecturer, self).__init___(*args, **kwargs) def assign_teaching(self, course): self.courses.taught.append(course) jane= Student(Student.POSTGRADUATE, "Джейн", "Смит1, "SMTJNX045") jane,enrol(a_postgrad.course) bob = Lecturer(StaffMember.PERMANENT, “Боб11, "Джонс", "123456789") bob.assign-teaching(an_undergrad_course) Наш базовый класс Person представляет любого чело- века, связанного с университетом. Мы создаем от него под- класс Student для представления студентов и подкласс Lecturer для представления сотрудников, а затем подкласс Staff Member для людей, которые преподают курсы (в отли- чие от сотрудников, занимающих административные долж- ности). Атрибут number (идентификационный номер) определен в базовом классе, поскольку используется как общее средство идентификации и для студентов, и для сотрудников. В то же время тип студента UNDERGRADUATE, POSTGRADUATE (бака- лавр или аспирант) и тип занятости сотрудника PERMANENT, TEMPORARY (постоянный или временный работник) оформле- ны как разные атрибуты. Мы также добавили метод для класса Student, позво- ляющий зачислить студента на курс, и метод для класса
Lecturer, позволяющий назначить курс, который будет ве- сти преподаватель. Метод___init___базового класса инициализирует все пе- ременные экземпляра, которые являются общими для всех подклассов. В каждом подклассе мы переопределяем метод __init__, чтобы использовать его для инициализации атри- бутов этого класса — но мы хотим, чтобы атрибуты родитель- ского класса тоже были инициализированы, поэтому нам нужно из нашего класса вызвать метод__init__родитель- ского. Чтобы найти нужный метод, мы используем функцию super, куда передаем в качестве параметров текущий класс и объект. Она вернет прокси-объект с правильным методом __init__, который мы можем затем вызвать. В каждом из наших переопределенных методов___init__ мы используем параметры метода, характерные для нашего класса, а остальные параметры передаем в метод_init__ родительского класса. Часто используется соглашение: па- раметры, специфичные для подкласса, добавляются в нача- ло списка параметров, а остальные передаются как *args и **kwargs. Это позволяет создавать подклассы, не зная всех деталей инициализации родителя. Поэтому, если мы добавим новый параметр в___init___суперкласса, нам нужно будет добавить его только во все места, где мы создаем этот класс или один из его подклассов — нам не нужно будет обновлять все определения дочерних классов, чтобы включить новый параметр. Упражнение 2 Наследование часто используется для создания иерар- хий пользовательских исключений. Поскольку класс ис-
ключения определяет, будет ли оно перехвачено опреде- ленным блоком except, полезно создавать собственные классы исключений для разных ситуаций. Наследова- ние удобно тем, что блок except, перехватывающий ис- ключение родительского класса, также перехватывает и его подклассы. Это означает, что мы можем эффектив- но писать блоки except, которые обрабатывают группы связанных исключений, просто расположив их в логи- ческой иерархии. Наши классы исключений должны на- следоваться от встроенных классов исключений Python. Как правило, они не нуждаются в дополнительных атри- бутах или методах. Напишите простую программу, которая просматривает список данных пользователей (кортежи, содержащие имя пользователя, электронную почту и возраст) и добавляет каждого пользователя в справочник, если ему не менее 16 лет. Сохранять возраст не нужно. Напишите простую ие- рархию исключений, которая определяет различные ис- ключения для следующих ситуаций: 1. имя пользователя не является уникальным: 2. возраст не является целым положительным числом; 3. пользователю меньше 16 лет; 4. недопустимый адрес электронной почты (достаточно простой проверки на наличие имени, символа @ и до- мена). Генерируйте соответствующие исключения в нужных ме- стах. При возникновении исключения переходите к сле- дующему набору данных. Для каждого типа ошибки вы- ведите отдельное сообщение.
Подумайте, где еще уместно использовать пользователь- ские классы и какой тип коллекции лучше всего подхо- дит для хранения справочника. Допущение: адрес электронной почты считается кор- ректным, если содержит ровно один символ @ и непустые имя пользователя и домен. Предполагается, что возраст уже представлен как целое число. Подробнее о наследовании Множественное наследование На первый взгляд кажется, что приведенная выше модель студентов и сотрудников достаточно хороша. Но если попы- таться расширить ее до более реалистичного уровня, возник- нут сложности. В реальном университете разделение между сотрудниками и студентами, административным и препода- вательским составом не всегда однозначно. Студент, который преподает на курсе, также является своего рода временным сотрудником. Сотрудник может записаться на курс. Сотруд- ник может занимать как административную должность на фа- культете, так и преподавательскую. В Python класс может наследоваться от нескольких других классов. Например, мы можем создать класс Tutor, который наследуется от классов Student и Staff Member. Множествен- ное наследование нс слишком сложно понять, если класс на- следуется от нескольких классов, которые имеют совершенно разные свойства, но все усложняется, если два родительских класса реализуют один и тот же метод или атрибут..
Если классы В и С являются наследниками класса А, а класс D наследником от классов В и С, и оба класса В и С содержат метод do_something, какой вариант этого метода унаследу- ет класс D? Эта двусмысленность известна как проблема «ром- бовидной структуры», и разные языки решают ее по-разно- му. В нашем примере класс Tutor унаследует метод_init__ от двух родителей — и здесь возникнет та же проблема. К счастью, функция super в Python корректно работа- ет с множественным наследованием. Если мы использу- ем ее внутри метода___init__класса Tutor, то все методы __init___родительских классов будут вызываться в пра- вильном порядке. В итоге мы получим класс, который со- держит все атрибуты и методы, присутствующие в классах Student и StaffMember. Миксины (Mix-ins) Если мы используем множественное наследование, раз- умным решением будет проектировать наши классы та- ким образом, чтобы избежать неоднозначностей, описанных выше. Один из способов сделать это — разбить дополнитель- ный функционал на миксины. Миксин — это класс, который не предназначен для самостоятельного использования. Его за- дача — добавлять дополнительный функционал другому клас- су посредством множественного наследования. Например, давайте перепишем приведенный выше пример так, чтобы ка- ждая группа связанных действий, которые может выполнять человек в университете, была реализована в виде миксина: class Person: def __init__(self, name, surname, number): self.name = name self.surname = surname self.number = number
class LearnerMixin: def ______init__(self): self.classes = [] def enrol(self, course): self.classes.append(course) class leacherMixin: def __init__(self): self.courses_taught = [] def assign_teaching(self, course): self.courses^taught.append(course) class Tutor(Person, LearnerMixin, leacherMixin): def __init__(self, *args, **kwargs): super(Tutor, self).__init__(*args, **kwargs) jane = Tutor("Джейн", "Смит", "SMTJNX045") jane.enrol(a_postgrad_course) jane.assign_teaching(an.undergrad.course) Теперь класс Tutor наследуется от одного «основного» класса Person и двух миксинов. которые не связаны напря- мую с Person. Каждый миксин предоставляет отдельный фрагмент дополнительного функционала. У наших микси- нов по-прежнему есть методы___init__, потому что каждый из них должен инициализировать список курсов (в преды- дущей главе мы видели, что это нельзя сделать с помощью атрибута класса). Многие миксины просто предоставляют до-
полнительные методы и ничего не инициализируют. Это мо- жет означать, что они зависят от других свойств, уже суще- ствующих в классе-наследнике. Мы могли бы расширить этот пример, добавив миксины, представляющие возможность оплачивать обучение, полу- чать оплату за услуги и т. д. — тогда мы сможем создать отно- сительно плоскую иерархию классов для различных типов лю- дей, которые наследуются от Ре rson и нужных им миксинов. Абстрактные классы и интерфейсы В некоторых языках программирования можно создать класс, экземпляры которого нельзя создавать. Это означает, что мы не можем использовать этот класс напрямую для соз- дания объекта — мы можем только наследовать от него и соз- давать объекты на основе его подклассов. Зачем это нужно? Иногда мы хотим определить набор свойств и методов, которые должен содержать объект, чтобы быть подходящим для решения какой-то задачи. Например, мы можем написать функцию, один из параметров которой должен быть объектом с определенными методами, необхо- димыми для работы функции. Для этого мы можем создать класс-шаблон, содержащий перечень обязательных методов. Такой класс не предназначен для создания экземпляров, пото- му что его методы не реализованы — они должны быть пере- определены в подклассе. Таким образом, абстрактный класс фактически определяет интерфейс. В некоторых языках существует специальная кон- струкция interface, которая работает аналогично. Мы го- ворим, что класс реализует интерфейс, если он наследуется от класса, задающего этот интерфейс.
В Python нельзя напрямую запретить создание экземпляров класса, но мы можем создать нечто похожее на абстрактный класс, вызывая NotlmplementedError в определениях наших методов. Например, вот несколько «абстрактных» классов, ко- торые можно использовать в качестве шаблонов для фигур: class Shape2D: def area(self): raise NotImplementedError() class Shape3D: def volume(self): raise NotImplementedError() Любая двумерная фигура имеет площадь, а трехмерная — объем. Формулы для их вычисления зависят от конкретной фигуры, и объекты разных фигур могут иметь совершенно раз- ные атрибуты. Если подкласс наследуется от Shape2D, он получит реализа- цию метода а геа по умолчанию — но она вызывает исключе- ние, что ясно указывает пользователю на необходимость пере- определения этого метода в данном дочернем классе: class Square(Shape2D): def __init__(self, width): self.width = width def area(self): return self.width ** 2
Упражнение 3 1. Напишите «абстрактный» класс Box (коробка) и опре- делите в нем методы, которыми должен обладать любой объект Box: add — для добавления любого количества эле- ментов в коробку, empty — для извлечения всех элементов из коробки и возврата их в виде списка, count — для под- счета количества объектов в коробке. Напишите простой класс Item, у которого есть атрибуты name и value. Мож- но предположить, что все объекты, с которыми вы рабо- таете, — это экземпляры Item. Теперь напишите два под- класса класса Box, которые используют разные базовые коллекции для хранения элементов: Li st Box должен ис- пользовать список, a DictBox — словарь. 2. Напишите функцию repack_boxes, которая принима- ет в качестве параметров любое количество коробок, со- бирает все содержащиеся в них предметы и как можно равномернее распределяет их по всем коробкам. Порядок не имеет значения. Существует несколько способов реа- лизации. Протестируйте ваш код с List Box, содержащей 20 предметов, другой ListBox с 9 предметами и DictBox с 5 предметами. В результате должно получиться две ко- робки по 11 предметов в каждой и одна с 12. Как запретить наследования Наследование — полезный прием, но иногда оно услож- няет код без необходимости. Как мы уже обсуждали, множе- ственное наследование может вызвать путаницу, поэтому
иерархии, использующие множественное наследование, сле- дует проектировать осторожно, чтобы свести ошибки к ми- нимуму. Глубокая иерархия с множеством уровней наследования за- трудняет понимание кода. В нашем первом примере с насле- дованием, чтобы понять, как работает класс Lecturer, нужно проследить поведение трех разных классов. Если наши клас- сы объемные и разделены на несколько разных файлов, стано- вится сложно выяснить, какой класс за что отвечает. Следует избегать создания иерархий глубиной более одного-двух уров- ней. В некоторых статически типизированных языках насле- дование очень популярно, потому что позволяет программи- сту обойти некоторые ограничения статической типизации. Если Lecturer (преподаватель) и Student (студент) являют- ся подклассами класса Person, можно написать функцию, принимающую объект типа Person, и она будет работать как с объектами типа Lecturer, так и типа Student. Это из- вестно как полиморфизм. В Python наследование не требуется для реализации поли- морфизма, потому что язык не использует статическую типи- зацию. Функция может работать как с объектами lecturer, так и с объектами student, если они оба имеют соответствую- щие атрибуты и методы, даже в том случае, если эти объекты не имеют общего родительского класса и совершенно не связа- ны между собой. Рекомендуется не проверять тип объекта на- прямую, а вместо этого проверять наличие необходимых атри- бутов и методов — так вы не будете искусственно загонять объекты-параметры в иерархию наследования, если в этом нет необходимости.
Замена наследования композицией В некоторых случаях наследование можно заменить компо- зицией и добиться аналогичного результата — и такой подход зачастую предпочтительнее. В примере с миксинами мы раз- делили возможное поведение человека на логические груп- пы. Вместо того чтобы реализовывать эти группы поведения в виде миксинов и наследовать от них наш класс, мы можем добавить их в качестве атрибутов в класс Person: class Learner: def _init___(self): self.classes = [] def enrol(self, course): self.classes.append(course) class Teacher: def __init__(self): self.courses_taught = [] def assign_teaching(self, course): self.courses.taught.append(course) class Person: def __init___(self, name, name, number, learner = None, teacher= None): self.name = name self.surname = surname self.number = number
self.learner = learner self.teacher - teacher jane= Person("Джейн", "Смит", "SMTJNX045", LearnerQ, Teacher()) jane, learner.enrol(a_postgrad.course) jane.teacher.assign.teaching(an_undergrad_course) Теперь вместо вызова методов enrol и assign-teaching напрямую у объекта Person мы делегируем вызов соответ- ствующим объектам lea rne г и teache г. Упражнение 4 Перепишите класс Person из предыдущего примера, ре- ализовав дополнительные методы enrol и assign- teaching, которые будут скрывать делегирование. Эти методы должны вызывать соответствующий метод у атри- бутов learner и teacher. Если нужный атрибут не за- дан, метод должен выбрасывать исключение с понятным сообщением об ошибке.
Ответы на упражнения Ответ на упражнение 1 1. Между четырьмя классами должны быть реализованы следующие отношения: • отношение «один-ко-многим» между альбомами и песня- ми — скорее всего, оно будет двунаправленным, посколь- ку песни и альбомы довольно тесно связаны между собой. • отношение «один-ко-многим» между исполнителями и песнями. Оно может быть однонаправленным или дву- направленным. На самом деле нам не нужно хранить ссылки на все песни исполнителя в объекте artist, по- скольку для поиска песен по исполнителю достаточно ссылки на исполнителя из каждой песни, но если музы- кальная коллекция очень большая, то может быть хоро- шей идеей кешировать этот список. • отношение «один-ко-многим» между исполнителями и альбомами, которое может быть однонаправленным или двунаправленным по тем же причинам. • отношение «один-ко-многим» между плейлистами и пес- нями — скорее всего, оно будет однонаправленным, по- скольку редко требуется отслеживать, в каких плейли- стах содержится конкретная песня. 2. Пример программы: class Song: def __init__(self, title, artist, album, track-number): self.title = title
self.artist = artist self.album = album self.track.number = track.number artist.add_song(self) class Album: def __init__(self, title, artist, year): self.title = title self.artist = artist self.year = year self.tracks = [] artist.add_album(self) def add_track(self, title, artist= None): if artist is None: artist = self.artist track.number = len(self.tracks) song = Song(title, artist, self, track.number) self.tracks.append(song) class Artist: def ______init__(self, name): self.name = name
self.albums = [] self.songs = [] def add_album(self, album): self.alburns.append(album) def add_song(self, song): self.songs.append(song) class Playlist: def ______init___(self, name): self.name = name self.songs = [ ] def add_song(self, song): self.songs.append(song) band = Artist("Группа Боба") album = Album("Первый сингл Боба", группа, 2013) album.add_track("Баллада о сыре') album.add_track("Баллада о сыре (танцевальный ремикс)") album.add_track("Третья песня, чтобы заполнить осталь- ное место") playlist = Playlist("Мои любимые песни") for song in album.tracks: playlist.add_song(song)
Ответ на упражнение 2 Пример программы с обработкой исключений: # Исключения class DuplicateUsernameError(Exception): pass class InvalidAgeError(Exception): pass class UnderageError(Exception): pass class InvalidEmailError(Exception): pass # Класс для хранения данных пользователя class User: def __init__(self, username, email): self.username = username self.email = email example_list= [ ("jane", iane@example.com1', 21), ("bob", "bob@example", 19), ("jane", jane2@example.com", 25), (’steve", "steve@somewhere", 15), joe , joe , 23), ("anna", ‘anna@example.com', -3),
directory = {} for username, email, age in example_list: try: if username in directory: raise DuplicateUsernameError() if age < 0: raise InvalidAgeError() if age < 16: raise UnderageError() email_parts = email.split('©') if len(email_parts) != 2 or not email_parts[3] or not email_parts[1]: raise InvalidEmailError() except DuplicateUsernameError: print("Имя пользователя '%s' уже занято." % username) except InvalidAgeError: print("Недопустимый возраст: %d % age) except UnderageError: print("Пользователь %s слишком молод. 1 % username) except InvalidEmailError: print("'%s‘ - некорректный адрес электронной почты." % email) else: directory[username]= User(username, email)
Ответ на упражнение 3 1. Пример программы: class Box: def add(self, *items): raise NotImplementedError() def empty(self): raise NotlmplementedErrorf) def count(self): raise NorImplementedError() class Item: def ______init__(self, name, value): self.name = name self.value = value class ListBox(Box): def —init_______(self): self..items = [] def add(self, *items): self..items.extend(items) def empty(self): items = self..items self..items = |] return items
def count(self): return len(self..items) class DictBox(Box): def ______init__(self): self..items = {} def add(self, *items): self..items.update(dict((i.name, i) for i in items)) def empty(self): items = list(self..items.values()) self..items = {} return items def count(self): return len(self..items) 2. Пример программы: def repack_boxes(*boxes): items = (] for box in boxes: items.extend(box.empty()) while items: for box in boxes: try: box.add(items.pop())
except IndexError: break boxl = ListBox() for i in range(20): boxl.add(Item(str(i), i)) box2 = ListBox() for i in range(9): box2.add(Item(str(i), i)) box3 = DictBoxQ for i in range(5): box3,add(Item(str(i), i)) repack_boxes(box1, box2, box3) print(boxl.count()) print(box2.count()) print(box3.count()) Ответ на упражнение 4 Пример программы: class Person: def __init__(self, name, surname, number, learner = None, teacher = None): self.name = name self.surname = surname self.number = number self.learner = learner
self.teacher = teacher def enrolfself, course): if not hasattr(self, "learner"): raise NotImplementedError() self.learner.enrol(course) def assign_teaching(self, course): if not hasattr(self, "learner"): raise NotImplementedError() self.teacher.assign.teaching(course)
Работа с пакетами и тестирование Модули Все программные проекты начинаются с малого, и вы, ско- рее всего, сначала разместите весь код вашей программы в од- ном файле. Однако по мере роста проекта работать с одним файлом становится все менее удобно: трудно что-либо найти в большом объеме кода, и рано или поздно возникнет проб- лема, когда вам понадобится создать два класса с одинаковы- ми именами. В какой-то момент будет разумно привести про- ект в порядок, разбив его на несколько файлов, сгруппировав связанные классы и функции вместе. Иногда логическая структура программы изначально под- сказывает, как разделить код на модули — например, если ваша программа использует базу данных, бизнес-логику и гра- фический интерфейс, то с самого начала разумно вынести ка- ждую из этих частей в отдельный файл, не смешивая их. Как же получить доступ к коду из одного файла в другом? В Python для этого предусмотрен механизм модулей: каждый файл с исходным кодом представляет собой модуль. Чтобы ис- пользовать код, определенный в модуле, его нужно импорти- ровать с помощью ключевого слова import — мы уже делали это с некоторыми встроенными модулями в предыдущих при- мерах. Каждый модуль имеет свое собственное пространство имен, поэтому можно иметь два класса с одинаковыми имена- ми, если они находятся в разных модулях. Если мы импортиру-
ем весь модуль, мы можем получить доступ к свойствам, опре- деленным в этом модуле, с помощью оператора .: import datetime today = datetime.date.today() Мы также можем импортировать определенные классы или функции из модуля с помощью ключевого слова from и использовать их имена напрямую: from datetime import date today = date.today() Создать модуль очень просто — достаточно записать код в файл Python. Имя модуля будет совпадать с именем файла без расширения . ру. Такой модуль можно будет импортиро- вать, если запустить Python из каталога, в котором находится файл, или из скрипта, расположенного в той же директории. Если вы хотите, чтобы модуль можно было импортировать вне зависимости от текущей рабочей директории, его нужно упа- ковать и установить — как это сделать, мы рассмотрим в сле- дующей главе. С помощью ключевого слова as можно задать псевдоним для импортируемого модуля, класса или функции. Это удоб- но, если имя часто используется и его хочется сократить, либо если нужно избежать конфликта имен, поскольку импортиру- емый модуль или класс может иметь то же имя, что и класс, ко- торый уже находится в нашем пространстве имен: import datetime as dt today = dt.date.today() from mymodule import MyClass as FirstClass from myothermodule import MyClass as OtherClass
Можно также импортировать все содержимое модуля с по- мощью *, но это не рекомендуется, поскольку есть риск слу- чайно переопределить уже существующие имена в текущем пространстве имен, даже не заметив этого: from mymodule import * Пакеты Так же как модуль — это набор классов или функций, па- кет — это набор модулей. Мы можем организовать несколь- ко файлов модулей в структуру каталогов. Существуют раз- личные инструменты, которые могут преобразовать каталог в специальный формат (также называемый «пакетом»), ко- торый мы можем использовать для установки программы на свой компьютер или на компьютеры других пользователей. Этот процесс в Python связан с работой с пакетами (packaging). Создание дистрибутива программы с помощью Distribute На данный момент наиболее популярным инструментом для создания Python-пакетов является библиотека Distribute, и она рекомендуется к использованию с Python 3. Эта библи- отека не входит в стандартную поставку Python, но ее мож- но установить с помощью таких инструментов, как pip или easy_install. Distribute — это более современная вер- сия старой библиотеки для создания пакетов под названием Setuptools, и она была разработана, чтобы заменить ее. Ори- гинальная Setuptools не поддерживает Python 3, но вы можете столкнуться с ней, если работаете с кодом на Python 2. У этих двух библиотек практически идентичный интерфейс, а имя
модуля, которое нужно импортировать в коде, у Distribute та- кое же — setuptools. Допустим, мы разделили нашу большую программу, ко- торую назовем "оигргод", на три файла: db.py для работы с базой данных, rules.ру для бизнес-логики и gui.py для графического интерфейса пользователя. Сначала мы должны расположить наши файлы в типичной структуре ка- талогов, которую ожидают инструменты создания пакета: оигргод/ оигргод/ __init___.ру db.py gui.py rules.ру setup.ру Здесь добавлены два новых файла. Файл__init___.ру — это специальный файл, который помечает внутреннюю ди- ректорию оигргод как пакет, а также позволяет нам импор- тировать весь оигргод как модуль. Мы можем использовать этот файл для импорта классов или функций из наших моду- лей (db, gui и rules) в пространство имен пакета, чтобы их за- тем можно было импортировать непосредственно из оигргод, а не из ои гр год . db, и так далее — но пока оставим этот файл пустым. Другой файл, setup. ру, — это спецификация нашего паке- та. Вот минимальный пример содержимого файла setup. ру: from setuptools import setup setup(name = ’ourprog', version = '0.1',
description = 'Наша первая программа', url = 'http://example.coni , author = 'Jane Smith , author_email = 'jane.smith@example.com', license = 'GPL', packages = [ourprog'J, zip.safe = False, Мы создаем пакет одним вызовом функции setup, которую импортируем из модуля setuptools. В эту функцию мы пере- даем ряд параметров, описывающих наш пакет. Установка и импорт модулей Теперь, когда мы написали файл setup. ру, мы можем вы- полнить его, чтобы установить наш пакет в систему. Хотя это и не очевидно, setup. ру — это скрипт, который прини- мает различные параметры командной строки — все это по- ведение обеспечивается благодаря тому, что мы импорти- ровали setuptools. Чтобы установить пакет, мы должны передать скрипту параметр install. Команду необходимо ввести в командной строке, находясь в той же директории, что и setup.ру: python3 setup.ру install Если все прошло успешно, мы сможем импортировать мо- дуль ои гр год из любого каталога в системе.
Документация Документирование программы часто воспринимается как второстепенная задача. Пока мы пишем программу, может казаться, что все и так очевидно, и длинные пояснения к ка- ждой функции или классу — пустая трата времени. Однако спустя несколько месяцев, вернувшись к коду или столкнув- шись с чужим не документированным проектом, вы наверня- ка измените свое мнение. Мы уже видели, как можно вставлять комментарии в код Python с помощью символа #. Такие комментарии полезны для аннотирования отдельных строк, но они не очень подхо- дят для более длинных объяснений или систематического до- кументирования всех структур в нашем коде. Для этого мы ис- пользуем docstrings. Docstrings Docstring — это обычная строка, которая записывается меж- ду тройными кавычками, потому что тройные кавычки удоб- ны для определения многострочных строковых литералов. Главное в docstring — ее расположение в коде. Существуют ин- струменты, которые анализируют исходный код Python и из- влекают строки, расположенные сразу после определения мо- дуля, класса, функции или метода, формируя на их основе автоматически сгенерированную документацию. Такую документацию легче поддерживать в актуальном состоянии, чем отдельный документ, написанный вручную. Набор строк документации для каждого отдельного класса или функции определяется рядом с функцией в нашем коде, где мы, скорее всего, увидим его и заметим, если он не соответ- ствует действительности и нуждается в обновлении. Эти стро- ки также могут выполнять функцию комментариев — другие разработчики смогут видеть их при чтении исходного кода.
Интерактивные оболочки, использующие Python, также могут отображать набор строк документации, когда пользователь за- прашивает информацию функции или класса. Существует несколько различных инструментов, которые анализируют набор строк документации. Самый популярный из них — Sphinx. В этом курсе мы не будем подробно рассказы- вать о том, как использовать Sphinx для генерации докумен- тов, но мы посмотрим, как записывать строки документации в формате, совместимом с ним. Примеры документирования Язык разметки, используемый в Sphinx, основан на reStructuredText (reST) с добавлением некоторых специаль- ных ключевых слов. Универсального и строго обязательного формата docstring для Sphinx нет — вы можете использовать любой синтаксис Sphinx внутри строки документации. Однако docstring, как минимум, должна содержать общее описание до- кументируемого объекта. Если структура представляет собой функцию, полезно опи- сать все параметры и возвращаемое значение, а также ука- зать, может ли функция выбрасывать какие-либо исключения. Поскольку Python не является статически типизированным, важно предоставить информацию о параметрах, которые при- нимает функция. После краткого описания можно добавить более подробное объяснение — его длина ничем не ограничена. Вот несколько примеров формирования docstrings для раз- личных объектов: """ Модуль, содержащий класс Person. ... moduleauthor: Джейн Смит <jane.smith@example.com>. t! U U
import datetime class Person: """Это класс, который представляет человека. Создан скорее в учебных целях. Сохраняет некоторую личную ин- формацию и умеет рассчитывать возраст. def __init__(self, name, surname, birthdate, address, telephone, email) : """Этот метод создает новый объект Person. :param name: имя :type name: str :param surname: фамилия :type surname: str :param birthdate: дата рождения :type birthdate: datetime.date :param address: физический адрес :type address: str :param телефон: номер телефона :тип телефона: str :param email: адрес электронной почты :type email: str II II II self.name = name self.surname = surname self.birthdate = birthdate self.address = address self.phone = telephone self.email = email
def age(self): """Этот метод вычисляет возраст человека по дате рождения и текущей дате. :returns: int - возраст человека в годах :rtype: int II II II today = datetime.date.today() age = today.year - self.birthdate.year if today< datetime.date(today.year, self, birthdate.month, self.birthdate,day): age -= 1 return age Тестирование Автоматизированные тесты — полезное дополнение к любой программе. Они не только помогают обнаружить ошибки, но и облегчают модификацию кода — после вне- сения изменений можно запустить тесты, чтобы убедиться, что все работает корректно. Это очень важно в любом боль- шом проекте, особенно если над одним и тем же кодом ра- ботают несколько человек. Без тестов сложно определить, какие части системы могут быть затронуты изменением, и, таким образом, любое изменение становится потенциаль- ным риском. Из-за этого разработка проекта продвигается очень медленно, а изменения часто приводят к появлению ошибок.
На первый взгляд может показаться, что добавление те- стов к небольшому проекту — пустая трата времени. Одна- ко они становятся незаменимыми, если проект разрастает- ся или к нему приходится возвращаться через долгое время. Они также могут служить своего рода документацией — по ним легко понять, как программа должна себя вести. Неко- торые разработчики даже предлагают писать сначала тесты, тем самым создавая спецификацию программы, а уже затем написать саму программу. Такой подход может показаться нам немного экстремаль- ным, но не стоит бросаться и в другую крайность — если отло- жить написание тестов до завершения всей программы, ско- рее всего, их не напишут вовсе. Хорошей идеей будет писать части кода и тесты для них примерно в одно и то же время — тогда мы сможем тестировать наш код в процессе его разработ- ки. Большинство программистов пишут хотя бы временные тесты во время разработки, чтобы убедиться, что новая функ- ция работает правильно — ранее было показано, как можно использовать операторы print в качестве быстрой, но непо- стоянной формы отладки. Лучше сразу писать постоянные ав- томатические тесты — после настройки тестового фреймворка это требует совсем немного усилий. Чтобы код подходил для автоматического тестирования, его нужно организовать в логически обособленные части — функ- ции и классы — с минимальной зависимостью от глобально- го состояния. Это достигается с помощью функций и классов и отказом от глобальных переменных. Если функция полагает- ся только на свои входные параметры для получения какого-то результата, мы можем легко импортировать эту функцию в от- дельный модуль тестирования и проверить, что различные входные данные дают ожидаемые результаты. Каждая пара
входных и ожидаемых выходных данных называется тесто- вым случаем. Тесты, применяемые к отдельным компонентам наше- го кода, называются юнит-тестами — они проверяют пра- вильность работы каждого компонента. Тестирование взаи- модействия различных компонентов в системе называется интеграционным тестированием. Тест можно назвать функ- циональным, если он проверяет определенную возможность или функцию кода — обычно более высокий уровень специ- фикации, а не реальная отдельная функция. В этом разделе мы рассмотрим в основном модульное те- стирование, но аналогичные приемы применимы на любом уровне автоматизированных тестов. Когда мы пишем мо- дульные тесты, то у нас должен быть тест для каждой функ- ции в нашем коде (включая каждый метод каждого класса). Также хорошей практикой является написание нового те- ста каждый раз, когда мы исправляем ошибку — тест должен специально проверять наличие ошибки, которую мы только что исправили. Если ошибка была типичной, возможно, кто-то допустит ее снова — тест поможет этого избежать. Это разно- видность регрессионного тестирования, цель которого — пре- дотвратить повторное возникновение старых ошибок при из- менениях в коде. Выбор тестовых случаев Как мы выбираем тестовые случаи? Существует два основ- ных подхода, которых мы можем придерживаться: тестиро- вание «черного ящика» или «прозрачного ящика». Иногда ис- пользуется комбинация обоих.
При тестировании «черного ящика» мы относимся к нашей функции как к непрозрачному «черному ящику». Мы не ис- пользуем наши знания о том, как написана функция, для под- бора тестовых случаев — мы думаем только о том, что функция должна делать. Стратегия, часто используемая при тестиро- вании «черного ящика», — это использование классов эквива- лентности и анализ граничных значений. Класс эквивалентности — это множество входных значе- ний, при которых ожидается одинаковое поведение. Между соседними классами эквивалентности существуют границы. Входные значения, которые находятся вблизи этих границ, с наибольшей вероятностью дадут неверный результат, по- тому что программисту легко ошибиться и использовать опе- ратор < вместо оператора <= или начать отсчет от 1 вместо 0, что может привести к ошибке выхода из диапазона значений off-by-one. Если мы протестируем входное значение внутри каждого класса эквивалентности и дополнительно протести- руем значения до, после и на каждой границе, мы можем быть уверены, что охватили все диапазоны. Например, рассмотрим простую функцию, которая вычис- ляет оценку по проценту. Если бы мы использовали тестиро- вание на эквивалентность и анализ границ для этой функции, мы бы выбрали тестовые случаи следующим образом: Класс эквивалентности Обра- зец Нижняя граница Чуть выше границы Чуть ниже границы отметка > 100 150 100 101 99 80 <= отметка <= 100 90 80 81 79
Класс эквивалентности Обра- зец Нижняя граница Чуть выше границы Чуть ниже границы 70 <= отметка < 80 75 70 71 69 G0 <= отметка < 70 G5 60 61 59 50 <= отметка < 60 55 50 51 49 0 <= отметка < 50 25 0 1 -1 метка < 0 -50 При тестировании методом «прозрачного ящика» мы выби- раем тестовые случаи, анализируя код внутри функции. Наи- более часто применяемой формой этой стратегии является покрытие пути, цель которого — проверка всех возможных путей выполнения функции. Функция, не содержащая операторов выбора или цикла, име- ет только один путь. Проверить такую функцию относитель- но просто — если она выполнилась правильно хотя бы один раз, то, вероятно, будет выполняться правильно и каждый раз. Если функция содержит оператор выбора или цикла, то воз- можных путей становится больше: выполнение будет разным, если условие истинно или ложно, а цикл выполняется пере- менное число раз. Для такой функции один тест может не за- действовать все инструкции в программе. Мы могли бы построить отдельный тест для каждого воз- можного пути, но это быстро становится непрактичным. Каждый оператор if удваивает количество путей — если
бы в нашей функции было 10 операторов if, нам понадоби- лось бы более тысячи тестов, а если 20, то более миллиона! Более реалистичным подходом является стратегия покрытия операторов, которая требует от нас только выбора достаточ- ного количества тестов, чтобы каждый оператор внутри на- шей функции был выполнен хотя бы один раз. Написание модульных тестов В Python модульные тесты можно писать с использовани- ем встроенного модуля unittest. Обычно все тесты разме- щаются в иерархии файлов, отделенной от основного текста программы. Например, если мы хотим добавить тесты к наше- му примеру с модулями упаковки, мы, скорее всего, создадим по одному тестовому модулю на каждый модуль программы и поместим их в отдельный каталог test: оигргод/ оигргод/ __init__.ру db.py gui.py rules.ру test/ __init__.ру test.db.ру test.gui.py test.rules.ру setup.ру Предположим, что наш файл rules.ру содержит следую- щий класс:
class Person: TITLES = ('Dr', Mr', 'Mrs', Ms') def __init__(self, name, surname): self.name = name self.surname = surname def fullname(self, title): if title not in self.TITLES: raise ValueError("Unrecognised title: '%s"" 0/ 4) return "%s %s %s" % (title, self.name, self, surname) Наш файл test_rules. py должен выглядеть примерно так: import unittest from ourprog.rules import Person class TestPerson(unittest.TestCase): def setUp(self): self.person = Person("Jane", 'Smith") def test_init(self): self.assertEqual(self.person.name, "jane") self.assertEqual(self.person.surname, 'Smith") def test_fullname(self): self.assertEqual(self.person.fullname("Ms"), Ms Jane Smith")
self .assert Equal (self .person .fullname (' Mrs"), "Mrs Jane Smith") self.assertRaises(ValueError, self.person. fullname, "HRH") Мы импортируем модуль unittest, а также класс, кото- рый собираемся тестировать. В этом примере предполагает- ся, что код уже упакован и установлен в систему, чтобы Python мог найти ои гр год .rules. В модуле unittest класс TestCase служит контейнером для тестов, которые должны обмениваться некоторыми дан- ными. Для каждой коллекции тестов, которую мы хотим напи- сать, мы определяем класс, наследующий TestCase, и опреде- ляем все наши тесты как методы этого класса. В приведенном примере все тесты в TestCase тестируют один и тот же класс, и на каждый метод приходится по одно- му тесту (включая метод инициализации __init__) — но это не обязательное правило. Вы можете использовать несколько классов TestCase для тестирования каждого из ваших собствен- ных классов, или, возможно, иметь один TestCase для каждой группы связанных функций. Мы устанавливаем класс, который будем тестировать, в методе setup — этот специальный метод будет выполнять- ся перед каждым запуском теста. Есть также метод tearDown, который мы можем использовать, если нам нужно что-то сде- лать после каждого теста. Внутри каждого теста мы используем методы утвержде- ний (assert*), предоставляемые TestCase, чтобы прове- рить поведение программы. Если хоть одно утверждение не выполнится — тест считается проваленным. Чаще всего используется assert Equal, но доступны и другие методы, такие как assertNotEqual, assertTrue, assertln. Метод
assertRaises позволяет убедиться, что функция вызыва- ет исключение. Обратите внимание, что при использовании этого метода утверждения мы не вызываем функцию (потому что это вызвало бы исключение!) — мы просто передаем имя функции и ее параметры. Существует множество способов запуска тестов после их написания. Самый простой — добавить в конец файла test_rules.py следующую конструкцию: if___name__== '__main___' : unittest.main() Теперь, если запустить test.rules.ру с помощью Python, модуль unittest выполнит все тесты, определенные в TestCase. Условие if__name___== '„main_____'позволя- ет избежать выполнения кода при импорте модуля из друго- го файла. Подробнее о написании скриптов будет рассказано в следующей главе. Также можно запускать модуль unittest в командной строке и использовать его для импорта и запуска некото- рых или всех наших тестов. По умолчанию модуль попытает- ся обнаружить все тесты, которые могут быть импортирова- ны из текущей директории, но мы также можем указать один или несколько модулей, классов или методов тестирования: # Поиск и выполнение всех тестов из текущей директории python -m unittest python -m unittest discover # Запуск конкретных элементов python -m unittest ourprog.test.test.rules python -m unittest ourprog.test.test.rules.TestPerson
python -m unittest ourprog.test.test.rules.TestPerson. test„fullname # Включение подробного вывода с помощью флага -v python -m unittest -v test_rules Пакет unit test также позволяет нам группировать неко- торые или все тесты в наборы, чтобы можно было запускать несколько связанных тестов одновременно. Например, чтобы добавить все тесты из нашего класса Test Person в набор, сле- дует добавить эту функцию в файл test_rules.py: def suite(): suite = unittest.TestSuite() suite.addTest(Test Person) return suite Можно также создать набор в ou гр год/test/_init__.ру, объединив тесты из всех наших модулей, либо объединив на- боры из всех модулей, либо просто добавив все тесты напря- мую. Классы TestSuite и Test Loader позволяют создавать гибкие конфигурации тестов различными способами. Мы можем интегрировать наши тесты в код пакета, доба- вив параметр test.suite в вызов функции setup файла setup. ру. Несмотря на название, этот параметр не обязательно должен быть набором — мы можем просто указать полное имя нашего модуля с тестами, чтобы включить в него все наши тесты: setup(name = ’ourprog’, # (...) test_suite = ourprog.test'.
Теперь можно собрать наш пакет и запустить все тесты, пе- редав параметр test в программу setup . ру: python setup.py test # Указание, что именно запускать, с помощью флага -s # Например, мы можем запустить один модуль python setup.py test -s ourprog.test.test.test_rules В предыдущих версиях Python нам нужно было вручную определять набор тестов, чтобы запустить все наши тесты од- новременно. В новых версиях можно легко запустить все тесты или один модуль, класс или метод, просто используя утилиту unittest в командной строке или набрав setup.py test. Для более сложных задач нам все еще может пригодиться на- писание собственных наборов — можно сгруппировать тесты, которые распределены по нескольким модулям или классам и связаны с одной и той же функцией. Проверка покрытия тестами Как убедиться, что тесты покрывают весь код? Существует множество сторонних библиотек модульного тестирования, которые вкл ючают в себя способность проверки покрытия. Од- нако существует очень простая проверка, которая заключает- ся в том, чтобы запустить unit test вместе со встроенным мо- дулем trace. Можно модифицировать наш тестовый модуль следующим образом: import trace, sys # весь наш тестовый код
if___name__== "__main___": t = trace.Trace(ignoredirs = [sys.prefix, sys.exec_ prefixj, count = 1, trace = 0) t.runfunc(unittest.main) r = t.results() г.write_results(show_missing = True) Первая инструкция в блоке if создает объект Trace, ко- торый будет отслеживать выполнение нашей программы — по всем исходным файлам, в которых будет найден код. Мы используем параметр ignoredirs, который позволя- ет исключить из анализа любой код в установленных моду- лях Python — мы увидим результаты проверки только фай- ла нашей программы и нашего тестового файла. Установка параметра count в 1 включает подсчет количества выполне- ний каждой строки, а установка параметра trace в 0 отклю- чает пошаговый вывод кода во время исполнения — чтобы не захламлять консоль. Во второй строке указывается, что мы должны запустить главную функцию нашего тестового набора. Третья строка извлекает результаты из объекта — результаты представля- ют собой другой тип объекта, основанный на словаре. У этого объекта есть удобный метод write, results, который мы ис- пользуем в четвертой строке, чтобы сохранить в файл количе- ство строк кода для каждого исходного файла. По умолчанию файлы сохраняются в текущем каталоге, однако при необхо- димости можно указать другой каталог через необязатель- ный параметр. Параметр show.missing позволяет включить в результат строки, которые ни разу не выполнялись, и явно их отметить. Для того чтобы код внутри блока if выполнился, необ- ходимо запустить тестовый файл напрямую. После этого в текущем каталоге должны появиться два файла с расши-
рением .cover: один для основного программного файла, другой — для файла с тестами. Каждая строка должна быть аннотирована количеством раз, которое она была выполне- на в процессе тестирования. Упражнение 1 В этом упражнении вам предстоит написать программу, которая оценивает стоимость телефонного звонка, а так- же разработать и реализовать модульные тесты для этой программы. Телефонная компания применяет следующие правила для расчета стоимости звонка: • Минимальная плата до уплаты налогов в размере 59 400 центов распространяется на все звонки по любым направ- лениям на расстояние до 50 км и 89 000 центов — на лю- бые направления на расстояние более 50 км. • Звонки тарифицируются посекундно по цене 0,759 цента за секунду (<= 50 км) и 1,761 цента за секунду (> 50 км). • В непиковые секунды (с 19:00:00 до 06:59:59 следующе- го дня) предоставляется скидка в размере 40 % (<= 50 км) и 50 °/о (> 50 км) от вышеуказанного тарифа. • Если тип звонка — совместный вызов и пункт назначе- ния находится на расстоянии более 50 км, предоставляет- ся скидка 50 % после всех внепиковых скидок (минималь- ная плата остается в силе). Однако на звонки по акции на меньшие расстояния скидка не распространяется. • Наконец, к окончательной стоимости добавляется НДС в размере 14 %. Ваша программа должна запросить у пользователя следу- ющую информацию:
• Время начала вызова (должно быть разбито на часы, ми- нуты и секунды) • Продолжительность звонка (должна быть разбита на ми- нуты и секунды) • Находился ли объект на расстоянии более 50 км • Является ли вызов звонком по акциями Подсказка: вы можете предложить пользователю вве- сти сразу часы, минуты и секунды в формате, например, ЧЧ:ММ:СС, а затем разделить строку по символу Мож- но предположить, что пользователь введет корректные данные и что продолжительность звонка не превышает 59 минут и 59 секунд. • Ваша программа должна вывести следующую информа- цию: • Базовая стоимость • Скидка в непиковое время • Скидка по акции • Чистая стоимость (после всех скидок, но до налога) • НДС • Итоговая стоимость 1. Прежде чем писать программу, определите классы эк- вивалентности и граничные значения, которые вам при- дется использовать при тестировании эквивалентно- сти и анализе граничных условий при написании тестов «черного ящика». Это поможет вам не только в разработ- ке тестов, но и в проектировании самой программы! 2. Напишите программу. Помните, что вам нужно будет написать модульные тесты для этой программы, а значит, спроектируйте ее соответствующим образом — вычисле- ния, которые вам нужно протестировать, должны быть помещены в какую-нибудь функцию, которую можно им-
портировать и тестировать независимо от остального кода (например, пользовательского ввода). 3. Теперь реализуйте разработанные вами тесты «черно- го ящика», написав отдельный модуль с юнит-тестами для вашей программы. Запустите все тесты и убедитесь, что они прошли. Затем используйте модуль trace, что- бы проверить, насколько хорошо ваши тесты покрывают код.
Ответы на упражнения Ответ на упражнение 1 1. Звонок может начаться как в пиковое, так и в непиковое время, и завершиться также в пиковое или непиковое время (так как максимальная длительность звонка составляет чуть меньше одного часа, звонок может пересекать границу между пиковым и непиковым временем только один раз, но не дваж- ды). Вызов также может пересечь границу между сутками, и этот переход надо правильно обработать. Хорошими граничными значениями для начала звонка бу- дут: 00:00,06:00, 07:00,18:00 и 19:00. Для длительности звонка — минимальное и максимальное значения: 00:00 и 59:59. Нет не- обходимости тестировать каждую возможную комбинацию начала и продолжительности звонка — длительность важна лишь в случае, если звонок начинается в пределах одного часа до переключения между пиковым и непиковым временем. Остальные варианты начала можно протестировать с одной фиксированной длительностью. Другие значения, вводимые пользователем, являются буле- выми, поэтому достаточно протестировать только случаи с ис- тинным и ложным значением для каждого из них. И снова нет нужды проверять каждую булеву опцию в комбинации со все- ми возможными значениями предыдущих параметров — од- ного-двух случаев будет достаточно. 2. Вот пример программы: import datetime # Первое значение в каждой паре (кортеж) - для расстоя- # ний <= 50 км
# Второе значение - для расстояний > 50 км MIN_CHARGE = (59.400, 89.000) # минимальная стоимость CHARGE_PER_SEC = (0.759, 1.761) # стоимость за секунду OFFPEAK_DISCOUNT = (0.4, 0.5) # скидка в непиковое вре- # мя SHARECALL-DISCOUNT = (0.0, 0.5) # скидка по акции NEAR, FAR = 0, 1 # индексы для ближнего и дальнего звон- # ка OFF_PEAK_START = datetime.time(19, 0, 0) # начало не- # пикового времени HOUR_BEFORE_OFF_PEAK_START = datetime.time(18, 0, 0) # час до его начала OFF_PEAK_END = datetime.time(7, 0, 0) # конец непико- # вого времени HOUR_BEFORE_OFF_PEAK_END = datetime.time(6, 0, 0) # час # до его конца VAT-RATE =0.14 # ставка НДС def price_estimate(start_str, duration.str, destination, str, share_call_str): start = datetime.datetime.strptime(start_str, "%H:%M:%S").time() d_m, d_s = [int(p) for p in duration_str.split(":L )] duration = datetime.timedelta(minutes = d_m, seconds = d_s).total_seconds() # Определяем индекс назначения по расстоянию destination = FAR if destination.str.lower() == ' y’ else NEAR
share.call = True if share.call.str.lower() == 'у' else False peak_seconds = 0 off_peak_seconds = 9 if OFF.PEAK.END <= start <= HOUR.BEFORE.OFF.PEAK. START: # звонок полностью в пиковое время peak_seconds = duration elif start >= OFF.PEAK.START or start <= HOUR. BEFORE.OFF_PEAK.END: # звонок полностью в непиковое время off.peak.seconds = duration else: # звонок начинается в течение часа до переклю- чения secs_left_in_hour = 3609 - start.minute * 60 + start.second if start < OFF_PEAK_END: # звонок начинается в непиковое время if duration > secs.left.in.hour: peak.seconds = duration - secs_left_in_ hour off_peak_seconds = duration - peak.seconds else: # звонок начинается в пиковое время if duration > secs.left_in.hour: off.peak.seconds = duration - secs.left in.hour peak.seconds = duration - off.peak.seconds
basic = CHARGE_PER_SEC[destination] * duration offpeak_discount = OFFPEAK.DISCOUNT[destination]* CHARGE_PER_SEC[destination] * off_peak_seconds if share_call: share_call_discount = SHARECALL- DISCOUNT!destination] * (basic - offpeak_discount) else: share_call_discount = G net = basic - offpeak_discount - share_call_discount if net < MIN_CHARGE[destination]: net = MIN_CHARGE[destination] vat = VAT_RATE * net total = net + vat return basic, offpeak.discount, share_call_discount, net, vat, total if___name__== "___main__ start_str = input("Введите время начала звонка (HH:MM:SS): ") duration_str = input("Введите продолжительность звонка (MM:SS): ") destination.str = input("Было ли расстояние до пун- кта назначения больше 50 км? (Y/N): н) share_call_str = input("Bbin ли это совместный зво- нок? (Y/N): ”) results = price_estimate(start_str, duration.str, destination-Str, share-call„str)
print(...Базовая стоимость: %д Скидка за непиковое время: %д Скидка за совместный звонок: %д Итоговая стоимость: %д НДС: %д Общая стоимость: %д ... % results) 3. Пример программы, включающий тест на покрытие: import unittest import trace, sys from estimate import price_estimate class TestEstimate(unittest.TestCase): def test_off_peak(self): # все эти случаи должны попадать в непиковое вре- # мя и давать одинаковый результат test. .cases = [ ("23:59:59", "10:00" , "N", "N"), ("00:00:00", "10:00" , "N", "N"), ("00:00:01", "10:00" , "N", "N"), ("05:59:59", "10:00" , "N", "N"), ("06:00:00", "10:00" , "N", "N"), ("06:00:01", "10:00" , "N", "N"), ("19:00:00", "10:00" , "N", "N"), ("19:00:01", "10:00" , “N", "N"), for start, duration, far.away, share.call in test.cases:
basic, op_discount, sc.discount, net, vat, total = pnce_estimate(start, duration, far_away, share, call) self.assertAlmostEqual(basic, 455.4) self.assertAlmostEqual(op.discount, 182.16) self.assertAlmostEqual(sc.discount, 0) self.assertAlmostEqual(net, 273.24) self.assertAlmostEqual(vat, 38.2536) self.assertAlmostEqual(total, 311.4936) def test_peak(self): # все эти случаи должны попадать в пиковое вре- # мя и давать одинаковый результат test.cases = [ ("07:00:00", "10:00", "N", "N"), ("07:00:01", "10:00", "N", "N"), ("17:59:59", "10:00", "N", "N"), ("18:00:00", "10:00", "N", "N"), ("18:00:01", "10:00", "N", "N"), for start, duration, far.away, share.call in test.cases: basic, op.discount, sc.discount, net, vat, total - price_estimate(start, duration, far.away, share.call) self.assertAlmostEqual(basic, 455.4) self.assertAlmostEqual(op.discount, 0) self.assertAlmostEqual(sc.discount, 0) self.assertAlmostEqual(net, 455.4) self.assertAlmostEqual(vat, 63.756) self.assertAlmostEqual(total, 519.156)
def test_peak_and_off _peak(sel'f): # эти случаи пересекают границу между пиковым # и непиковым временем, и каждый дает разный # результат test_cases = [ ("06:59:59", "59:59", "N", "N"), ("07:00:00", "59:59", "N", "N"), ("07:00:01", "59:59", "N", "N"), ("18:59:59", "59:59", "N", "N"), ("19:00:00", "59:59", "N", "N"), ("19:00:01", "59:59", "N", "N"), ("06:30:00", "00:00", "N", "N"), ("06:30:00", "00:01", "N", "N"), ("06:30:00", "59:58", “N", "N"), ("06:30:00", "59:59", "N", "N"), expected_results = [ (2731.641, 36.128400000000006, 0, 2695.5126, 377.371764, 3072.884364), (2731.641, 0.0, 0, 2731.641, 382.42974, 3114.07074), (2731.641, 0.0, 0, 2731.641, 382.42974, 3114.07074), (2731.641, 1056.528, 0, 1675.113, 234.51582, 1909.62882), (2731.641, 1092.6564, 0, 1638.9846, 229.457844, 1868.442444),
(2731.641, 1092.6564, 0, 1638.9846, 229.457844, 1868.442444), (0.0, 0.0, 0, 59.4, 8.316, 67.716), # мини- # мальный тариф (0.759, 0.3036, 0, 59.4, 8.316, 67.716), # ми- # нимальный тариф (2730.882, 546.48, 0, 2184.402, 305.81628, 2490.21828), (2731.641, 546.48, 0, 2185.161, 305.92254, 2491.08354), for parameters, results in zip(test_cases, expected_results): basic, op.discount, sc.discount, net, vat, total = price_estimate(*parameters) exp_basic, exp.op.discount, exp_sc_discount, exp_net, exp.vat, exp_total = results self.assertAlmostEqual(basic, exp.basic) self.assertAlmostEqual(op_discount, exp_op_ discount) self.assertAlmostEqual(sc_discount, exp_sc_ discount) self.assertAlmostEqual(net, exp_net) self.assertAlmostEqual(vat, exp_vat) self.assertAlmostEqual(total, exp^total) def test_far_destination_share_call(self): # здесь мы повторяем некоторые базовые случаи # с дальним направлением и/или совместным звонком
test.cases = [ # непиковое время ("23:59:59", "10:00", "Y", "N"), ("23:59:59", "10:00", "Y", "Y"), ("23:59:59", "10:00", "N", "Y"), # пиковое время ("07:00:00", "10:00", "Y", "N"), ("07:00:00", "10:00", "Y", "Y"), ("07:00:00", "10:00", "N", "Y"), expected.results = [ (1056.6, 528.3, 0, 528.3, 73.962, 602.262), (1056.6, 528.3, 264.15, 264.15, 36.981, 301.131), (455.4, 182.16, 0.0, 273.24, 38.2536, 311.4936), (1056.6, 0.0, 0, 1056.6, 147.924, 1204.524), (1056.6, 0.0, 528.3, 528.3, 73.962, 602.262), (455.4, 0.0, 0.0, 455.4, 63.756, 519.156), for parameters, results in zip(test_cases, expectecLresults): basic, op.discount, sc_discount, net, vat, total = price_estimate(*parameters) exp_basic, exp_op_discount, exp_sc_discount, exp_net, exp.vat, exp.total = results self.assertAlmostEqual(basic, exp.basic)
discount) discount) self.assertAlmostEqual(op_discount, exp_op self.assertAlmostEqual(sc_discount, exp_sc self.assertAlmostEqual(net, exp_net) self.assertAlmostEqual(vat, exp.vat) self.assertAlmostEqual(total, exp_total) if___name__== "____main__": t = trace.Trace(ignoredirs = [sys.prefix, sys.exec prefix], count = 1, trace = 0) t. runfunc(unittest.main) r = t.resultsf) r.write_results(show_missing=True)
Полезные модули стандартной библиотеки Python поставляется со встроенным набором модулей, пре- доставляющих часто используемую функциональность. С не- которыми из этих модулей мы уже встречались в предыдущих главах — например, itertools, logging, pdb и unittest. В этой главе мы рассмотрим еще несколько примеров. Это лишь краткий обзор небольшого подмножества доступных модулей — полный список и подробную информацию по ка- ждому модулю вы можете найти в официальной документа- ции по стандартной библиотеке Python по адресу (http://docs. python.org/3.3/library/index.htnil). Дата и время: модуль datetime Модуль datetime предоставляет объекты, с помощью кото- рых можно хранить информацию о дате и времени: • datet ime. date используется для создания даты без при- вязки ко времени. • datetime .time используется для времени, не связанно- го с конкретной датой. • datetime . datetime предназначен для объектов, содер- жащих и дату, и время. • Объекты datetime. timedelta представляют собой раз- ницу между двумя датами или моментами времени — при вычитании одного datetime из другого получается объект timedelta, • Объекты datetime. timezone представляет времен- ные зоны как смещения относительно UTC. Этот класс
является подклассом datetime.tzinfо, который не предназначен для непосредственного использова- ния. С помощью этих объектов можно получать отдельные ком- поненты (например, год, месяц, час или минуту), выполнять арифметические операции и извлекать строковые представле- ния, удобные для вывода. Вот несколько примеров: import datetime # этот метод класса создает объект datetime с текущей # датой и временем now = datetime.datetime.today() print(now.year) print(now.hour) print(now.minute) print(now.weekday()) print(now.strftime("%a, %d %B %Y")) long.ago = datetime.datetlme(1999, 3, 14, 12, 30, 58) print(long_ago) # помните, что здесь автоматически вы- # зывается str print(long_ago < now) difference = now - long_ago print(type(difference)) print(difference) # автоматически вызывается str
Упражнение 1 Выведите десять дат, каждая из которых отстоит от преды- дущей на две недели, начиная с сегодняшнего дня, в фор- мате ГГГГ-ММ-ДД. Математические функции: модуль math Модуль math содержит набор математических функций. Они используются как с числами с плавающей точкой, так и с целыми числами, но в основном предназначены для ис- пользования с числами с плавающей точкой и обычно воз- вращают также числа с плавающей точкой. Вот несколько примеров: import math # Эти атрибуты являются константами, а не функциями math.pi math .е # Округление вверх и вниз math,ceil(3.3) math.floor(3.3) # натуральный логарифм math.log{5) # логарифм с основанием 10 math.log(5, 10) math.logl0(5) # эта функция немного точнее
# квадратный корень math.sqrt(10) # тригонометрические функции math.sin(math.pi/2) math.cos(Q) # преобразование между радианами и градусами math.degrees(math.pi/2) math.radians(90) Если вам нужны математические функции для работы с ком- плексными числами, используйте модуль cmath. Упражнение 2 Напишите программу реализующую объект, представ- ляющий сферу заданного радиуса. Напишите метод, ко- торый вычисляет объем сферы, и метод для вычисления площади ее поверхности. Псевдослучайные числа: модуль random Последовательность чисел называется псевдослучайной, если она выглядит случайной, но на самом деле таковой не яв- ляется. Такие последовательности генерируются с помощью предсказуемого алгоритма, но обладают достаточным количе- ством свойств истинно случайных последовательностей, что-
бы использоваться в большинстве практических задач, где требуются случайные числа. Генерация по-настоящему случайных чисел на компью- тере — непростая задача. Это возможно с использовани- ем аппаратных источников, например, нажатия клавиш пользователем или незначительных колебаний напряже- ния, но этот процесс сложнее и дороже, чем генерация псев- дослучайных чисел, которая может быть выполнена исклю- чительно программно. Поскольку псевдослучайные последовательности на самом деле не являются случайными, можно воспроизвести одну и ту же последовательность дважды. Это нежелательно в обычных условиях, но бывает полезно, например, при отладке или в ав- томатических тестах. В Python для генерации псевдослучайных чисел использу- ется модуль random. Его основная функция создает случайное число с плавающей точкой от 0 до 1, а остальные функции ба- зируются на ней. Вот несколько примеров: import random # случайное число с плавающей точкой от 0 до 1 (исклю- # чая 1) random.random() pets = ["кошка", "собака", "рыба"]. # случайный элемент из последовательности random.choice(pets) # перетасовать список (на месте) random.shuffle(pets) # случайное целое число от 1 до 10 (включительно) random.randint(1, 10)
Перед генерацией значений можно установить начальное значение с помощью метода seed. Это похоже на выбор места в псевдослучайной последовательности, с которого мы хотим начать. Обычно нам нужно получать разные последователь- ности, и по умолчанию модуль инициализируется значением, взятым из системных часов. Чтобы воспроизвести ту же после- довательность, например, при тестировании, следует исполь- зовать одно и то же целое число или строку в качестве параме- тра для seed: # Задать предсказуемый seed random.seed(3) random.random() random.random() random.random() # Повторить - будет та же последовательность random.seed(3) random.random() random.random() random.random() # Попробовать другой seed random.seed("что-то совершенно другое) random.random() random.random() random.random()
Упражнение 3 Напишите программу, которая случайным образом вы- бирает целое число от 1 до 100. Программа должна предлагать пользователю угадать число — если поль- зователь угадает неправильно, она должна вывести со- общение о том, является ли его значение слишком боль- шим или слишком малым. Если пользователь угадывает правильно, программа должна вывести, сколько попы- ток потребовалось пользователю, чтобы угадать пра- вильный ответ. Можно считать, что пользователь вво- дит корректные данные. Сопоставление строк по шаблону: модуль ге Модуль ге позволяет использовать регулярные выражения. Регулярные выражения — это мини-язык для сопоставления строк, который можно использовать для поиска (и, возможно, замены) текста. Если вы научитесь использовать регулярные выражения в Python, то обнаружите, что их синтаксис почти не отличается от других языков программирования. Полный спектр возможностей регулярных выражений до- вольно обширен, и их часто критикуют за излишнюю слож- ность, но, зная лишь несколько базовых конструкций, можно эффективно выполнять мощную обработку строк. Примечание Регулярные выражения хороши для работы с обычным текстом, но плохо справляются с разбором более структурированных форматов, та- ких как XML - в таких случаях следует использовать специализированные библиотеки разбора.
Документация Python по модулю ге не только объясняет, как использовать модуль, но и содержит ссылку на полный справочник по синтаксису регулярных выражений, поддержи- ваемому в Python. Основы регулярных выражений Регулярное выражение — это строка, которая описывает ша- блон. Этот шаблон сравнивается с другими строками, которые могут совпадать или не совпадать с ним. Регулярные выраже- ния могут содержать как обычные символы (буквы, цифры, знаки), так и специальные, которые имеют различные значе- ния в выражении. Поскольку многие специальные символы используют сим- вол обратной косой черты (\), в Python для представления регу- лярных выражений часто применяют «сырые» строки. Это из- бавляет от необходимости удваивать обратные слеши для их экранирования, что значительно усложнило бы чтение слож- ных регулярных выражений. Если регулярное выражение не содержит обратных слешей, то не имеет значения, исполь- зуем ли мы «сырую» или обычную строку. Вот несколько очень простых примеров: # обычная строка без спецсимволов # не будет соответствовать ничему, кроме строки 'кот' "кот" # . обозначает любой одиночный символ (кроме перевода строки). # это будет соответствовать 'кот', 'кбт', 'кЗт', ‘к!т‘
# * повторяет предыдущий символ 0 или более раз # можно использовать после обычного символа или специ- # аль ного символа, например . # это будет соответствовать 'кт', 'кот', 'коот', ‘кооо- ооооот' ... ко*т" # . * - произвольная последовательность любых символов "s.*c" # это будет соответствовать 'sc', 'sac', 'sic', # 'supercalif ragilistic' ... # + - как *, но символ должен встречаться хотя бы один # раз "ко+т" # должно быть хотя бы одно "о". # в более общем случае мы можем использовать фигурные # скобки {}, чтобы указать любое количество повторов # или минимум и максимум "к.{3}т"# будет соответствовать любому слову из пяти # букв, которое начинается с 'к' и заканчивается 'т' "к.{3,5}т"# будет соответствовать любому пяти-, шести- # или семибуквенному слову между 'к' и 'т' # ? - предыдущий символ может встретиться 0 или 7 раз "https?" # будет соответствовать 'http или https' # квадратные скобки [] определяют набор допустимых зна- # чений для символа
# они могут содержать обычные символы или диапазоны # если л - первый символ в скобках, он *отрицает* со- # держимое # символ между к' и 'т‘ должен быть гласной буквой "к[ауоыэяюеие]т" "[Л ауоыэяюеие]{3}"# это соответствует любому символу, # который *не* является гласным, три раза подряд согласные # Это соответствует номеру студента UCT в верхнем реги- # стре. "[B-DF-HJ-NP-TV-Z]{3}[A-Z]{3}[0-9]{3}" # мы используем \для экранирования любого специального # символа регулярного выражения г"к\*т"# соответствовует 'к*т' # обратите внимание, что мы использовали необработанную # строку, чтобы можно было написать обратный слеш # также существуют некоторые сокращенные символы для опре- # деленных разрешенных подмножеств символов: # \d соответствует любой цифре # Is соответствует любому пробельному символу, такому # как пробел, табуляция или символ новой строки # |w соответствует алфавитно-цифровым символам - буквам, # цифрам или символу подчеркивания # \D, \S и \W - обратные к \d, Is, Iи/ # можно использовать круглые скобки (), чтобы *захва- # тить* части шаблона # это полезно, если надо выполнить поиск и замену # можно получить содержимое захвата на шаге замены
# захватит все, что соответствует .* "к(.*)т" # л и $ обозначают начало или конец строки # соответствует строке, которая начинается с 'к' и за- # канчивается на 'т' "Лк.*т$" # / означает "или'' - он позволяет нам выбирать между не- # сколькими вариантами. “кошка!собака" Использование модуля ге Теперь, когда мы знаем, как создавать регулярные выраже- ния, давайте посмотрим, как использовать модуль ге. Он пре- доставляет функции для различных вариантов работы с шаб- лонами: • sea rch — ищет регулярное выражение внутри строки — считается совпавшим, если хотя бы часть строки соответ- ствует заданному шаблону. • match — сопоставляет регулярное выражение со всей строкой — ре1улярное выражение будет совпа- дать только в том случае, если совпадает вся строка. ге.match(‘something', some_string) эквивалентно re.search('Asomething$‘, some_string). • sub — ищет все участки строки, которые соответствуют регулярному выражению, и заменяет их на указанную подстроку • f indall — ищет все совпадения регулярного выражения в строке.
• split — разделяет строку, используя в качестве раздели- теля любое регулярное выражение. • compile — позволяет преобразовать нашу строку регу- лярного выражения в предварительно скомпилирован- ный объект регулярного выражения, который имеет мето- ды, аналогичные модулю ге. Такой подход чуть быстрее, особенно при многократных проверках одного и того же шаблона. Как видите, этот модуль предоставляет более мощные вер- сии некоторых простых операций со строками: например, мы можем разделить строку или заменить подстроку с по- мощью встроенных методов split и replace — но мы мо- жем использовать их только с фиксированными разделите- лями или шаблонами поиска и замены. С помощью re.sub и re. split мы можем указывать переменные шаблоны вме- сто фиксированных строк. Все функции принимают регулярное выражение в качестве первого аргумента, match, search, findall и split также принимают строку для поиска в качестве второго аргумента — но в функции sub это третий параметр, а второй — строка за- мены. Все функции также принимают параметр keyword, задающий необязательные флаги, которые мы рассмотрим в ближайшее время. match и search возвращают объекты match, которые хра- нят информацию, например, содержимое захваченных групп, sub возвращает измененную копию исходной строки, findall и split возвращают список строк, compile возвращает ском- пилированный объект регулярного выражения. Методы объекта регулярного выражения очень похожи на функции модуля, но первый параметр (строка регуляр- ного выражения) каждого метода отбрасывается — потому что он уже скомпилирован в объект.
Вот несколько примеров использования: import re # match проверяет начало строки, search ищет по всей # строке. print(re.match("c.*t", "cravat")) # совпадение print(re.match("c.*t", "I have a cravat )) # нет совпа- # дения print(re.search("c.*t", "I have a cravat")) # совпадение # Простая замена строки. .. print(re.sub("lamb", "squirrel", lamb..")) # Замена с использованием групп. print(re.sub("(.*) (BITES) (.*)", BITES MAN")) "Mary had a little r"\3 \2 \1", "DOG # count - это параметр ключевого слова, который мы можем # использовать для ограничения количества замен print(re.sub("а", "b", "аааааааа")) print(re.sub("a", "b", аааааааа", count = 1)) # Вот более подробный взгляд на объект match. my.match = re.match("(.*) (BITES) (.*)", "DOG BITES MAN") print(my_match.groups()) print(my.match.group(1)) # Мы можем давать группам имена. my.match = re.match("(?P<subject>.*) (?P<verb>BITES) (?P<object>.*)", "DOG BITES MAN") print(my_match.group("subject")) # 'DOG'
print(my_match.groupdict()) #{'subject’: 'DOG', 'verb': # 'BITES', 'object': 'MAN'} # Мы по-прежнему можем получить доступ к именованным # группам по их позициям. print(my_match.group(1)) # Иногда нам надо найти все совпадения в строке. Найти # все email print(re.findall('[Л ]+@[л ]+", "Bob <bob@example.com>, Jane <jane.doe@example.com>")) # Разделить строку по запятой с пробелом print(re.split(", *", "один, два, три, четыре')) # Мы можем скомпилировать регулярное выражение в объект my.regex = re.compile("(.*) (BITES) (.*)") # теперь мы можем использовать его аналогично модулю print(my_regex.sub(r"\3 \2 \1", "DOG BITES MAN")) Жадность Регулярные выражения по умолчанию являются жадны- ми — это означает, что если часть регулярного выражения может соответствовать переменному количеству символов, то она всегда будет стараться соответствовать как мож- но большему количеству символов (стремятся захватить как можно больше символов). Это означает, что иногда нам нужно особенно тщательно следить за тем, чтобы регуляр- ное выражение не совпадало с большим количеством симво- лов. Например: # Захватит все между первыми и последними кавычками'''. # но это не то, чего мы хотим!
print(re.findall('"один" "два” "три" "четыре"')) # Более точный способ - исключить закрывающую кавычку. print(re.findall( "'[А, '"один" "два" "три" "четы- ре"')) # Мы также можем использовать ? после * или других выра- # жении, чтобы сделать выражение *не жадным*. print(re.findall( , '"один" "два" "три" "четы- ре"')) Использование функции в ге. sub Мы также можем использовать метод ге. sub. Здесь вместо обычной строки замены мы передаем функцию. Она должна принимать объект match в качестве параметра и возвращать строку. Мы можем использовать эту функцию для выполне- ния сложных преобразований, которые невозможно выразить обычным шаблоном замены: def swap(m): subject = m.group("object").title() verb = m.group("verb 1) object = m.group("subject").lower() return "%s %s %s!" % (subject, verb, object) print(re.sub("(?P<subject>.*) (?P<verb>.*) (?P<object>.*)1", swap, "Собака кусает человека!"))
Флаги Исторически сложилось так, что регулярные выражения применяются к тексту построчно — символы перевода стро- ки обычно требовали специальной обработки. В Python текст по умолчанию рассматривается как единое целое, но это пове- дение (и другие параметры) можно изменить с помощью фла- гов. Ниже приведены наиболее часто используемые флаги: • re . IGNORECASE — делает регулярное выражение нечув- ствительным к регистру. По умолчанию оно чувствитель- но к регистру. • re . MULTILINE — позволяет символам Л и $ соответство- вать началу и концу каждой строки (исключая символ но- вой строки в конце), а не только началу и концу всего тек- ста, что является поведением по умолчанию. • re.DOTALL — позволяет символу . соответствовать лю- бому символу, включая символы новой строки (по умол- чанию они не включены). Ниже приведено несколько примеров: print(ге.match("cat", "Cat")) # Не совпадает print(re.match("cat', "Cat", re.IGNORECASE)) # Совпа- # дает text = """numbers = one, two, three1 numbers = 'four, five, six' not_numbers = 'cat, dog.... print(re.findall("Anumbers = text)) # это ниче- # то не найдет
# # Требуются одновременно флаги DOTALL и MULTILINE print(re.findall('Anumbers = ' text, re.DOTALL| re.MULTILINE)) Примечание Обратите внимание, что функции модуля ге принимают только один имено- ванный аргумент для флагов, однако несколько флагов можно объединить с помощью оператора | (побитовое ИЛИ), так как значения этих констант яв- ляются степенями двойки Упражнение 4 1. Напишите функцию, которая принимает в каче- стве аргумента строку и возвращает True, если стро- ка является допустимым именем переменной в Python, или False, если это не так. Вам не нужно проверять, яв- ляется ли строка зарезервированным ключевым словом в Python — просто проверьте, является ли она синтак- сически правильной. Проверьте ее, используя все при- меры правильных и неправильных имен переменных, описанные в первой главе. 2. Напишите функцию, которая принимает строку содер- жащую два слова, разделенных любым количеством и ти- пом пробельных символов, и возвращает строку, в кото- рой слова переставлены местами, а пробельные символы сохранены.
Разбор CSV-файлов: модуль csv CSV расшифровывается как «значения, разделенные запя- тыми» — это очень простой формат файлов для хранения табличных данных. Большинство электронных таблиц (на- пример из Excel) можно легко преобразовать в формат CSV и обратно. В типичном CSV-файле каждая строка представляет собой строку значений в таблице, а столбцы разделены запятыми. Значения полей часто заключаются в двойные кавычки, что- бы можно было экранировать запятые или переносы строк: "один1, "два", "три "четыре, пять", "шесть", "семь". Модуль csv в Python берет на себя всю рутинную работу по обработке формата и позволяет нам манипулировать данны- ми в CSV-файле простым способом, используя класс reader: import csv with open("numbers.csv") as f: r = csv.reader(f) for row in r: pnnt( row) Единого стандарта CSV не существует — в качестве раздели- теля может использоваться не только запятая (например табу- ляция), а в качестве кавычек — другой символ. Эти параметры можно задать через именованные параметры конструктора reader. Аналогичным образом мы можем записывать данные в CSV- файл, используя класс writer:
with ореп(1 pets.csv', 'w') as f: w = csv.writer(f) w. wnterow( [' Fluffy' , 'cat']) w.wnterowf [1 Max1, 'dog ]) Мы можем использовать необязательные параметры для writer, чтобы указать разделитель и символ кавычек, а также указать, нужно ли заключать в кавычки все поля или только поля с символами, которые необходимо экраниро- вать. Упражнение 5 Откройте CSV-файл, содержащий три столбца с числами. Запишите данные в новый CSV-файл, поменяв местами второй и третий столбцы и добавив четвертый столбец, который содержит сумму первых трех. Написание скриптов: модули sys и argparse Мы уже рассматривали несколько скриптов. С технической точки зрения любой файл с программой на языке Python — это скрипт, поскольку он может быть выполнен без предвари- тельной компиляции. Однако когда мы называем программу на Python скриптом, мы обычно имеем в виду, что она содер- жит инструкции, отличные от определений функций и клас- сов — скрипты делают что-то еще, кроме определения струк- тур для повторного использования.
Скрипты против библиотек Можно объединить в одном файле определения классов и функций с операторами, которые их используют, но в боль- шом проекте считается хорошей практикой держать их от- дельно: определять все наши классы в библиотечных файлах и импортировать их в основную программу. Если мы поместим классы и основную программу в один файл, то сможем гаран- тировать, что программа будет выполняться только при непо- средственном запуске файла как скрипта, а не при импорте из другого файла — пример этого мы видели ранее: class MyClass: pass class MyOtherClass: pass if___name__== '___main__': my.object = MyClass() # другие действия Если наш файл написан исключительно для использования в качестве скрипта и никогда не будет импортирован, включе- ние этого условного оператора считается излишним. Простые параметры командной строки При запуске программы из командной строки часто требу- ется передать аргументы — аналогично передаче параметров в функции. Например, при запуске файла с программой на язы- ке Python через интерпретатор имя файла передается как аргу- мент. В отличие от Python-функций, аргументы в командной строке отделяются пробелами и не заключаются в скобки.
Самый простой способ получить доступ к аргументам ко- мандной строки внутри скрипта — использовать модуль sys. Все аргументы по порядку хранятся в атрибуте a rgv этого мо- дуля. Нужно помнить, что первым аргументом всегда являет- ся имя файла скрипта, а все аргументы будут представлены в виде строк. Попробуйте сохранить этот простой скрипт и вы- звать его с различными аргументами после имени скрипта: import sys print(sys.argv) Сложные параметры командной строки Модуль sys хорошо подходит, когда у нас есть только не- сколько простых аргументов — имя файла, который нужно от- крыть, или число, которое говорит нам, сколько раз нужно вы- полнить цикл. Но если требуется гибкая обработка множества сложных аргументов, некоторые из которых необязательны, нам нужно лучшее решение. Модуль argparse позволяет определить широкий спектр обязательных и необязательных аргументов. Часто исполь- зуемым типом аргумента является флаг, который можно счи- тать эквивалентом именованных параметров в Python. Флаг необязателен, у него есть имя (может использоваться два ва- рианта — короткое или длинное) и значение. В программах для Linux и OSX имена флагов часто начинаются с тире (длин- ные имена обычно начинаются с двойного тире), и это согла- шение иногда соблюдается и в программах для Windows. Вот простой пример программы, в которой с помощью мо- дуля argparse определяются два позиционных аргумента, ко- торые должны быть целыми числами, флагом, задающим one-
рацию, которую нужно выполнить над этими двумя числами, и флагом для включения подробного вывода: import argparse import logging parser = argparse.ArgumentParser() # два целых аргумента parser.add_argument(1 num1", help = type = int) parser.add_argument("num2", help = type = int) первое число", второе число", # строка, ограниченная списком допустимых значений parser.add_argument('op", help = "желаемая арифметиче- ская операция , choices = [‘add’, 'sub', 'mul', div’]) # необязательный флаг, по умолчанию true, с коротким # и длинным именем parser.add_argument("-v ', "--verbose", help = "включить подробный вывод", action = "store.true") opts = parser.parse_args() if opts.verbose: logging.basicConfig(level = logging.DEBUG) logging.debug("Первое число: %d" % opts.numl) logging.debug("Второе число: %d" % opts.num2) logging.debug("Операция: %s" % opts.op) if opts.op == "add": result = opts.num1+ opts.num2 elif opts.op == "sub": result = opts.numl - opts.num2
elif opts,ор == "mul": result = opts.numl * opts.num2 elif opts.op == "div": result = opts.numl / opts.num2 print(result) Модуль argparse автоматически добавляет флаг -h или --help, который вызывает вывод справки, сформирован- ной на основе наших описаний аргументов. При ошибках в пе- реданных параметрах также выводятся понятные сообщения. Попробуйте вызвать приведенный выше скрипт с разными ар- гументами. Примечание В операционных системах Linux или OSX можно превратить наши скрипты в исполняемые файлы. Тогда их можно будет выполнять напрямую, а не пере- давать в Python в качестве аргументов. Чтобы сделать скрипт исполняемым, его надо пометить как исполняемый с помощью системной- инструмента (chmod). Также необходимо добавить строку в начало файла, чтобы сооб- щить операционной системе, что она должна использовать Python для его выполнения. Обычно это строка имеет вид #! /usr/bin/env python. Упражнение 6 Напишите программу, которая изменяет порядок столб- цов в CSV-файле. В качестве параметров она должна принимать путь к исходному CSV-файлу, строку с пере- числением индексов столбцов в том порядке, в котором они должны выводиться на экран, и, опционально, путь к выходному файлу (по умолчанию он должен иметь то же имя, что и исходный файл, но с добавлением суффикса).
Программа должна возвращать ошибку, если список ин- дексов не удается разобрать или если какой-либо из ин- дексов некорректен (либо слишком мал, либо превышает количество столбцов). Можно разрешить использование отрицательных индексов и повторяющихся значений. Также необходимо включить инструкции по использова- нию программы.
Ответы на упражнения Ответ на упражнение 1 Пример программы: import datetime today = datetime.datetime.today() for w in range(10): day = today + datetime.timedelta(weeks = w) print(day.strftime("%Y-%m-%d")) Ответ на упражнение 2 Пример программы: import math class Sphere: def ______init___(self, radius): self.radius = radius def volume(self): return (4/3) * math.pi * math.pow(self.radius, 3) def surface_area(self): return 4 * math.pi * self.radius ** 2
Ответ на упражнение 3 Пример программы: import random secret_number = random.randint(1, 100) guess = None num_guesses = 0 while not guess == secret_number: guess = int(input("Угадайте число от 1 до 100: ")) num_guesses += 1 if guess == secret-number: suffix = ' ки' if num.guesses == 1 else 'ok' print("Поздравляем! Вы угадали число после %d попыток." % (num.guesses, suffix)) break if guess < secret_number: print("Слишком мало!") else: print("Слишком много!") Ответ на упражнение 4 1. import re VALID-VARIABLE = re.compile('[a-zA-Z_][a-zA-Z0-9_]*')
def validate_variable_name(name): return bool(VALID-VARIABLE.match(name)) import re WORDS = re.compile('(\S+)(\s+)(\S+)') def swap_words(s): return WORDS.sub(r'\3\2\1', s) Ответ на упражнение 5 Пример программы: import csv with open("numbers.csv") as f_in: with open("numbers_new.csv", "w") as f_out: r = csv.reader(f_in) w = csv.writer(f_out) for row in r: w.writerow([row[0], row[2], row[l], sum(float(c) for c in row)]) Ответ на упражнение 6 Пример программы: import sys import argparse
import csv import ре parser = argparse.ArgumentParser() parser.add_argument("input", help = 'входной CSV-файл") parser.add_argument("order", help = "желаемый порядок столбцов; номера через запятую; начиная с нуля") parser.add_argument("-о", "--output", help = " файл на- значения (выходной CSV-файл") opts = parser.parse_args() output-file = opts.output if not output-file: output-file = re.sub("\.csv", ".reordered.csv", opts.input, re.IGNCRECASE) try: new.row. indices = [int(i) for i in opts.order. split(',’)] except ValueError: sys.exit('Ошибка: не удалось разобрать список столб- цов .") with open(opts.input) as f_in: with open(output_file, "w") as f_out: r = csv.reader(f.in) w = csv.writer(f_out) for row in r: new_row = [] for i in new_row_indices:
try: new_row.append(row[i]) except IndexError: sys.exit("Ошибка : недопустимый ин- декс столбца: %d" % i) w.writerow(new.row)
Введение в программирование графического интерфейса с помощью tkinter Ранее мы рассмотрели, как писать программы, работающие только с текстом и имеющие интерфейс командной строки (CLI). Теперь мы кратко рассмотрим создание программы с гра- фическим пользовательским интерфейсом, или GUI. В этой гла- ве мы будем использовать модуль tkinter, входящий в стан- дартную библиотеку Python и предоставляющий интерфейс к Тк — простому инструментарию для создания GUI. Существу- ет множество других библиотек для создания графических ин- терфейсов, однако они часто отличаются на разных платфор- мах. Освоив основы tkinter, вы обнаружите много общего при переходе к другим инструментам. Мы научимся создавать простой графический интерфейс, который обрабатывает ввод и вывод данных от пользовате- ля. В графических интерфейсах часто используется объек- тно-ориентированная модель программирования, которую мы называем событийно-ориентированной: программа ре- агирует на события, то есть действия, которые совершает пользователь. Примечание В некоторых дистрибутивах Linux, таких как Ubuntu и Debian, модуль tkinter поставляется отдельно от основного интерпретатора Python и требует от- дельной установки.
Событийно-ориентированное программирование Любое действие в пользовательском интерфейсе — это со- бытие. Мы говорим, что событие происходит всякий раз, ког- да пользователь совершает какое-либо действие — например, нажимает на кнопку или использует сочетание клавиш. Неко- торые события могут возникать и без участия пользователя — например, завершение фоновой задачи или изменение состоя- ния сетевого подключения. Наше приложение должно отслеживать, или прослуши- вать, все события, которые нам кажутся интересными, и как- то на них реагировать. Для этого мы обычно связываем опре- деленные функции с конкретными событиями. Мы называем функцию, которая выполняет действие в ответ на событие, об- работчиком события — и мы говорим, что привязываем обра- ботчики к событиям. Основы tkinter Модуль tkinter предоставляет нам богатый набор элемен- тов графического интерфейса — кнопки, текстовые поля, мет- ки, выпадающие списки, меню и многое другое. Все эти эле- менты называются виджетами (widgets), и они являются строительными блоками графического интерфейса. Мы соби- раемся построить дерево виджетов для нашего графического интерфейса — у каждого виджета будет родительский вид- жет, вплоть до корневого окна нашего приложения. Например, кнопка или текстовое поле должны находиться в каком-то со- держащем их окне. Классы виджетов предоставляют нам множество функций по умолчанию. В них есть методы для настройки внешне- го вида графического интерфейса — например, для располо- жения элементов в соответствии с каким-либо макетом —
и для обработки различных пользовательских событий. После построения основной структуры интерфейса мы интегрируем ее с логикой нашего приложения. Наш первый графический интерфейс будет представлять со- бой окно с текстовой меткой и двумя кнопками: from tkinter import Tk, Label, Button class MyFirstGUl: def ________(self, master): self.master = master master.title("Простой графический интерфейс") self.label = Label(master, text = "Это наш пер- вый графический интерфейс!") self.label.pack() self.greet.button = Button(master, text= "Попри- ветствовать", command= self.greet) self. greet.button.pack() self.close.button = Button(master, text = "За- крыть1, command = master.quit) self.close.button.pack() def greet(self): print("Приветстеую!") root = Tk() my_gui = MyFirstGUI(root) root.mainloop()
Попробуйте выполнить этот код самостоятельно. Вы долж- ны увидеть окно с заголовком, текстовой меткой и двумя кноп- ками — по нажатию одной из них выводится сообщение в кон- соль, а другая закрывает окно. Окно должно обладать всеми обычными свойствами любого другого окна, с которым вы сталкиваетесь в оконном менеджере — вы, вероятно, сможете перетаскивать его за панель заголовка, изменять размер, пе- ретаскивая рамку, а также управлять им через кнопки «свер- нуть», «развернуть» и «закрыть» на панели заголовка. Примечание Оконный менеджер - это часть вашей операционной системы, которая управляет окнами Все виджеты внутри окна, такие как кнопки и другие эле- менты управления, могут выглядеть по-разному в каждом наборе инстру- ментов GUI, но оформление рамок окна и заголовков определяется оконным менеджером и остается неизменным Мы используем три виджета: Тк — это класс, который мы ис- пользуем для создания корневого окна — главного окна наше- го приложения. В нашем приложении должно быть только одно корневое окно, но мы можем создавать другие окна, от- дельные от главного окна. Button и Label — кнопки и метки соответственно. У каж- дого из них есть родительский виджет, который мы передаем в качестве первого аргумента в конструктор — мы поместили метку и обе кнопки внутри главного окна, поэтому они явля- ются дочерними элементами главного окна в дереве. Мы ис- пользуем метод pack для каждого виджета, чтобы располо- жить его внутри родителя — о различных видах компоновки будет рассказано позже. Все три виджета могут отображать текст (мы также можем заставить их отображать изображения). Метка является ста- тичным элементом — по умолчанию он ничего не делает;
он просто что-то отображает. Кнопки, наоборот, предназна- чены для того, чтобы при нажатии на них что-то происходи- ло. При создании каждой кнопки мы использовали параметр command, чтобы связать каждую кнопку с соответствующим методом-обработчиком. Нам не пришлось писать никакого кода, чтобы явно связы- вать методы с событиями — все это уже реализовано в самих объектах кнопок. Мы просто передали функции-обработчики. Также не пришлось писать отдельную функцию для закры- тия окна — метод quit уже существует. Однако метод greet мы определили сами — он выводит сообщение в консоль. Существует множество способов организовать наш класс приложения. В этом примере наш класс не наследует ни от ка- ких объектов tkinter — мы используем композицию, чтобы связать дерево виджетов с нашим классом. Мы также можем использовать наследование, чтобы расширить один из видже- тов в дереве нашими пользовательскими функциями. Метод root .mainloop() запускает главное окно и пе- реходит в главный цикл обработки событий. Этот метод бу- дет циклически повторяться, ожидая событий от пользовате- ля, пока тот не завершит программу (например, закрыв окно или прервав выполнение в терминале). Классы виджетов В tkinter встроено множество различных классов видже- тов — они должны быть знакомы вам по другим графическим интерфейсам: • Frame — это виджет-контейнер, размещаемый вну- три окна, который может иметь собственные границы и фон — он используется для группировки связанных между собой виджетов в макете приложения.
• Toplevel — это виджет-контейнер, который отобража- ется в виде отдельного окна. • Canvas — это виджет для рисования графики. В расши- ренном варианте его можно использовать для создания пользовательских виджетов, ведь внутри него можно на- рисовать все что угодно и сделать это интерактивным. • Тext — многострочное текстовое поле с поддержкой фор- матирования и вставки изображений. • Button — кнопка; запускает действие при клике. • Label — это простой виджет, который отображает корот- кий текст или изображение, но обычно не является инте- рактивным. • Message похож на Label, но предназначен для длинных текстов с переносом строк. • Sc го 11 bar позволяет пользователю прокручивать содер- жимое, которое слишком велико, чтобы его можно было увидеть сразу на одном экране. • Checkbutton, Radiobutton, Listbox, Entry и Scale являются различными видами виджетов ввода — они по- зволяют пользователю вводить информацию в програм- му. • Menu и Menubutton используются для создания выпада- ющих меню. Варианты компоновки В предыдущем примере мы использовали простой ма- кет — три виджета, расположенных в одну колонку. Для этого мы использовали метод pack, который является одним из трех различных менеджеров геометрии, доступных в tkinter. Мы используем один из доступных менеджеров геометрии,
чтобы указать позицию для каждого из наших виджетов, ина- че виджет не появится в нашем окне. По умолчанию метод pack располагает виджеты вер- тикально сверху вниз внутри родительского контейнера, но мы можем изменить выравнивание с помощью необяза- тельного параметра side на нижнее, слева или справа (ТОР (по умолчанию), BOTTOM, LEFT, RIGHT). Можно также смеши- вать различные виды выравнивания в одном контейнере, но это не очень хорошо работает для сложных макетов. Одна- ко в нашем простом случае такой вариант оптимален: from tkinter import LEFT, RIGHT self.label.pack() self.greet_button.pack(side = LEFT) self.close_button.pack(side = RIGHT) С помощью метода pack можно создавать довольно слож- ные макеты, группируя виджеты во фреймы и выравнивая группы по своему усмотрению. Но для более гибкой компонов- ки лучше использовать вместо этого метод grid, который по- зволяет очень удобно размещать виджеты, используя макет сетки (организует интерфейс по строкам и столбцам). Именно этот менеджер геометрии рекомендуется для сложных интер- фейсов: from tkinter import W self.label,grid(coldmnspan = 2, sticky = W)
self.greet_button.grid(row = 1) self.close_button.grid(row - 1, column = 1) Мы помещаем каждый виджет в ячейку внутри таблицы, указывая строку (row) и столбец (column) — строкой по умол- чанию является первая доступная пустая строка, а столбцом но умолчанию — 0. Если виджет меньше своей ячейки, мы можем настроить его выравнивание с помощью параметра sticky — возможны- ми значениями являются координатные направления (N, S, Е и W), которые мы можем комбинировать с помощью сложения. По умолчанию виджет выравнивается как по вертикали, так и по горизонтали, но мы можем заставить его придерживаться определенной стороны, указав ее в параметре sticky. Напри- мер, sticky = W заставит виджет быть выровненным влево по горизонтали, a sticky = W + Е заставит его растягиваться, чтобы заполнить всю ячейку по горизонтали. Мы также можем указать углы, используя NE, SW и т. д. Чтобы вщ(жет охватывал несколько столбцов или строк, можно использовать параметры columnspan и rowspan. В примере выше мы сделали так, что метка занимает два столбца, поэтому по горизонтали он занимает столько же ме- ста, сколько и обе кнопки под ним. Примечание Никогда не используйте методы pack и grid в одном окне Алгоритмы, по которым они рассчитывают положение виджетов, несовместимы друг с другом, и ваша программа будет вечно зависать, пытаясь скомпоновать элементы. Третий менеджер геометрии — place, позволяющий за- давать явные размеры и позиции виджетов (т. е. он разме- щает виджеты по абсолютным координатам). Обычно его ис-
пользование не рекомендуется из-за сложности и негибкости, но он может пригодиться в специальных случаях. Пользовательские события До сих пор мы привязывали обработчики событий толь- ко к тем событиям, которые определены в tkinter по умол- чанию — класс Button уже знает о нажатиях на кнопки, по- скольку нажатие является ожидаемой частью нормального поведения кнопки. Однако мы не ограничены этими конкрет- ными событиями — мы можем заставить виджеты слушать другие события и привязать к ним обработчики, используя ме- тод bind, который можно найти в каждом классе виджетов. События однозначно идентифицируются именем последо- вательности в формате строки и описываются мини-языком, который не является специфическим для Python. Вот несколь- ко примеров распространенных событий: • "<Button-1 >", "<Button-2>" и "<Button-3>" — этосо- бытия, сигнализирующие о нажатии определенной кноп- ки мыши, когда курсор находится над соответствующим виджетом. Button 1 — это левая кнопка мыши, Button 3 — правая, a Button 2 — средняя, но помните, что не у всех мышей есть средняя кнопка. • '<ButtonRelease-1 >" указывает на то, что левая кноп- ка была отпущена. • "<В1-Motion^“ означает, что мышь была перемещена при нажатой левой кнопке (для других кнопок можно ис- пользовать В2 или ВЗ). • "<Enter>" и ' <Leave>" говорят нам о том. что курсор мыши вошел или вышел из области виджета. • "<Кеу>" означает, что была нажата любая клавиша на клавиатуре. Мы также можем прослушивать нажа- тия конкретных клавиш, например "<Return>" (клави-
ша ввода) или комбинации типа "<Shift-Up>" (стрелка вверх). Нажатие клавиш большинства печатных симво- лов выражается в виде их символьного представления, без скобок — например, буква а — это просто " а". • "<Conf igure>" означает, что виджет изменил размер. Теперь мы можем расширить наш простой пример, чтобы сделать метку интерактивной — давайте заставим текст мет- ки циклически повторять последовательность сообщений при каждом нажатии на него: from tkinter import Tk, Label, Button, StringVar class MyFirstGUI: LABEL-TEXT = [ "Это наш первый графический интерфейс!", "На самом деле это наш второй графический интер- фейс" , "Мы сделали его более интересным...', "...сделав этот ярлык интерактивным", Давай, нажми на него еще раз", def __init__(self, master): self.master = master master.title("Простой графический интерфейс") self.label-index = 0 self.label_text = StringVar() self.label-text.set(self.LABEL-TEXT[self.label- index] ) self.label = Label(master, textvariable = self, label-text)
self .label, bind (,h<Button-1>", self .eye le_label_ text) self.label.pack() self.greet.button = Button(master, text = "Greet", command = self.greet) self.greet.button.pack() self.close.button = Button(master, text = "Close", command = master.quit) self.close.button.pack() def greet(self): print("Приветствую!") def cycle_label_text(self, event): self.label.index += 1 self.label-index %= len(self.LABEL-TEXT) # цик- # лически прокручивает текст self.label-text.set(self.LABEL-TEXT[self.label, index]) root = Tk() my.gui = MyFirstGUI(root) root.mainloop() Обновление текста метки может вызвать некоторое за- труднение — мы не можем просто обновить текст с помощью обычной строки Python. Вместо этого мы должны предоста- вить метке специальный объект — строку и устанавливать но- вое значение для этого объекта всякий раз, когда хотим, чтобы текст метки изменился.
Мы определили обработчик, который циклически перехо- дит к следующей текстовой строке в последовательности, и ис- пользовали метод bind метки, чтобы привязать наш новый обработчик к щелчкам левой кнопкой мыши по метке. Важно отметить, что этот обработчик принимает дополнительный параметр — объект события, который содержит некоторую ин- формацию о нем. Мы можем использовать один и тот же об- работчик для множества разных событий (например, для не- скольких одинаковых событий, которые происходят в разных виджетах), и использовать этот параметр для того, чтобы их различать. Поскольку в данном случае мы используем наш об- работчик только для одного вида событий, мы просто проигно- рируем параметр event. Собираем все вместе Теперь мы воспользуемся полученной информациией для создания простого калькулятора. После того как пользова- тель введет число в текстовое поле, мы либо прибавим его к те- кущему результату, либо вычтем из него и выведем результат на экран. Мы также добавим возможность сброса значения ре- зультата: from tkinter import Ik, Label, Button, Entry, IntVar, END, W, E class Calculator: def __init__(self, master): self.master = master master.title("Калькулятор") self.total = 0
self.entered_number = б self.total_label_text = IntVarf) self.total.label-text.set(self.total) self.total-label = Labelfmaster, textvariable = self.total_label_text) self.label = Labelfmaster, text = "Total:") vcmd = master.registerfself.validate) # необходим # mo обернуть функцию self.entry = Entryfmaster, validate = "key", validatecommand = (vcmd, '%P')) self.add_button = Buttonfmaster, text = command = lambda: self.update("add")) self.subtract_button = Buttonfmaster, text = command = lambda: self.updatef'subtract’)) self.reset_button = Buttonfmaster, text = Reset", command = lambda: self.updatef"reset")) # РАЗМЕТКА ЭЛЕМЕНТОВ self.label.gridfrow = 0, column = 0, sticky = W) self.total-label.gridfrow = 0, column = 1, columnspan = 2, sticky = E) self.entry.gridfrow = 1, column = 0, columnspan = 3, sticky W=+ E) self.add-button.gridfrow = 2, column = 0) self.subtract_button.gridfrow = 2, column = 1)
self.reset.button,grid(row = 2, column = 2, sticky W =+ E) def validate(self, new.text): if not new_text: # поле очищается self,entered_number = 0 return True try: self.entered.number = int(new„text) return True except ValueError: return False def update(self, method): if method == add": self.total += self.entered.number elif method == subtract": self.total -= self.entered_number else: # сброс self.total = 0 self.total_label.text.set(self.total) self.entry.delete(0, END) root = Tk() my_gui = Calculator(root) root,mainloop() Мы определили два метода в нашем классе: один использу- ется для проверки содержимого поля ввода, а второй — для об- новления текущего результата.
Проверка правильности ввода текста Метод validate проверяет, является ли содержимое поля ввода действительным целым числом: при вводе текста поль- зователем поле будет изменяться только в случае, если новое значение является действительным числом. Мы также доба- вили специальное исключение для случаев, когда значение равно нулю, чтобы поле могло быть очищено (пользователем или нами). Каждый раз, когда значение поля меняется, мы со- храняем целочисленное значение содержимого в переменную self . ent е red_number. В любом случае нам придется выпол- нить преобразование, чтобы проверить, является ли оно дей- ствительным целым числом, и если оно действительно кор- ректное, мы сразу сохраняем его, чтобы не делать повторное преобразование при обновлении результата. Как связать эту функцию проверки с нашим полем ввода? Мы используем параметр validatecommand. Функция, ис- пользуемая в этом параметре, должна возвращать Тrue, если значение может быть принято, и False — в противном слу- чае. Эта функция должна быть обернута с помощью мето- да register соответствующего виджета (в данном случае — окна). Мы также можем указать аргументы, которые должны быть переданы функции. Для этого мы передаем кортеж, содержа- щий функцию и набор строк со специальными кодами. Эти коды будут автоматически заменяться при вызове функции на данные, связанные с изменением в поле ввода. В нашем примере нас интересует только одно значение — новое со- держимое поля. Для этого используется строка ' %Р', которую мы включили в кортеж. Еще один необязательный параметр, передаваемый в Ent г у, — validate, который указывает, когда должна про- исходить проверка. По умолчанию используется значение
' none1 (строка, а не питоновское None !), которое означает, что проверка не должна производиться. Мы установили зна- чение ' key1, чтобы проверка происходила при каждом на- жатии клавиши в поле — в том числе и при его очистке вну- три метода update. Обновление общего результата Мы написали один обработчик для всех вариантов обнов- ления результата, потому что логика во всех трех случаях почти одинаковая. Однако способ обновления значения зави- сит от того, какая кнопка была нажата — вот почему наше- му обработчику нужен параметр. Здесь возникает проблема: в tkinter нельзя напрямую передавать аргументы в обра- ботчики кнопок. Это решается с помощью оберток — мы соз- даем три разные функции-обертки, каждая из которых вызы- вает обработчик с нужным параметром. Для создания этих оберток мы использовали лямбда-функции, так как они пре- дельно просты и лаконичны. Внутри обработчика мы сначала обновляем итоговый ре- зультат, используя целочисленное значение поля ввода (ко- торое вычисляется и сохраняется в методе validate — обра- тите внимание, что мы инициализируем его нулем в методе __init__, чтобы пользователь мог нажимать на кнопки, ничего не вводя). Мы знаем, как обновить итоговый резуль- тат. благодаря параметру, который передается в обработчик. После того как мы обновили итоговый результат, необхо- димо обновить и текст, отображаемый меткой, чтобы он от- ражал новый результат. Для этого мы устанавливаем новое значение в объекте IntVar, связанном с меткой, через па- раметр textvariable. Механизм тот же, что и в предыду- щем примере со StringVar, за исключением того, что объ-
ект IntVar используется с целочисленными значениями, а не со строками. Наконец, после того как введенное пользователем число было использовано, мы очищаем поле ввода с помощью ме- тода delete виджета ввода, удаляя все символы от первого индекса (ноль) до конца (END — это константа, определенная в tkinter). Мы также должны очистить наше внутреннее зна- чение для последнего введенного числа — к счастью, удаление запускает метод проверки, который уже в свою очередь сбра- сывает это число в ноль при очистке поля. Упражнение *1 1. Объясните, зачем в последнем примере понадобилось использовать лямбда-функции для обертки вызовов. Пе- репишите определения кнопок, заменив лямбды на обыч- ные функции. 2. Создайте графический интерфейс для игры в угадайку из упражнения 3 в предыдущей главе.
Ответы на упражнения Ответ на упражнение 1 1. Лямбда-выражения необходимы, потому что мы передаем функции в конструкторы кнопок, чтобы объекты этих кнопок могли вызывать их позже. Если бы мы использовали обычные вызовы функций (без lambda), то эти функции были бы немедленно выполнены, и в качестве аргументов в конструкторы передавались бы их возвращаемые значения (в данном случае None). Вот пример того, как можно переписать этот фрагмент кода с определением полноценных функций: def update_add(): self.update("add") def update_subtract(): self.update("subtract") def update.reset(): self.update("reset") self.add.button = Button(master, text = " + ", command = update_add) self.subtract_button = Button(master, text = command = update.subtract) self.reset_button = Button(master, text = Reset", command = update.reset) 2. Пример программы: import random
from tkinter import Тк, Label, Button, Entry, StringVar, DISABLED, NORMAL, END, W, E class GuessingGame: def ______init__(self, master): self.master = master master.title(‘ Игра в угадайку") self.secret_number = random.randint(1, 100) self.guess = None self.num.guesses = 0 self.message = "Угадайте число от 1 до 100" self.label-text = StringVarQ self.label-text.set(self.message) self.label = Label(master, textvariable = self, label-text) vend = master.register(self.validate) # нам нуж- # но обернуть команду self.entry = Entry(master, validate = "key", validatecommand = (vend, *%P’)) self.guess_button = Button(master, text = "Уга- дай", command = self.guess.number) self.reset_button = Button(master, text = "По- пробуйте сыграть еще раз", command = self.reset, state= DISABLED) self.label.grid(row = 0, column = 0, columnspan = 2, sticky W=+ E)
self.entry,grid(row = 1, column = 0, columnspan = 2, sticky W=+ E) self .guess_button.gnd(row = 2, column = 0) self.reset_button.grld(row = 2, column = 1) def validate(self, new.text): if not new.text: # поле очищается self.guess = None return True try: guess = int(new.text) if 1 <= guess <= 100: self.guess = guess return True else: return False except ValueError: return False def guess_number(self): self.num_guesses += 1 if self.guess - None: self.message = "Угадайте число от 1 до 100" elif self.guess == self.secret_number: suffix = ‘ки if self.num_guesses == 1 else self.message = "Поздравляем! Вы угадали число после %d попыт%ок." % (self.num_guesses, suffix) self.guess.button.configure(state = DISABLED) self.reset_button.configure(state = NORMAL)
elif self.guess < self.secret-number: self.message = Слишком мало! Попробуйте еще Л Л Л I и раз! else: self.message = 'Слишком много! Попробуйте еще раз! self.label-text.set(self.message) def reset(self): self.entry.delete(0, END) self.secret_number = random.randint(1, 100) self.guess = 0 self.num_guesses = 0 self.message = "Угадайте число от 1 до 100 self.label-text.set(self.message) self.guess_button.configure(state = NORMAL) self.reset_button.conflgure(state = DISABLED) root= Tk() my-gui= GuessingGame(root) root.mainloop()
Сортировка, поиск и анализ алгоритмов Введение Мы уже узнали, что для того, чтобы написать компьютер- ную программу, выполняющую некоторую задачу, необходимо сконструировать соответствующий алгоритм. Однако любой разработанный нами алгоритм вряд ли будет единственным возможным решением — зачастую существует множество ал- горитмов, способных выполнить ту же задачу. Возникает во- прос: можно ли сказать, что одни алгоритмы лучше других? Ответ на этот вопрос изучается в рамках анализа алгоритмов. В этой главе мы рассмотрим четыре алгоритма, по два для ка- ждой из следующих распространенных задач: • Сортировка: упорядочивание списка значений • Поиск: определение позиции значения в списке Анализ алгоритмов должен начинаться с четкой формули- ровки задачи, которую необходимо решить. Это позволит нам проверить правильность алгоритма и убедиться, что сравни- ваемые алгоритмы выполняют одну и ту же задачу. Существует множество критериев для сравнения алгорит- мов, но в данной главе мы сосредоточимся на двух, которые имеют первостепенное значение для многих алгоритмов обра- ботки данных: • временная сложность: как количество необходимых ша- гов зависит от размера входных данных
• пространственная сложность: как объем требуемой до- полнительной памяти или хранилища зависит от разме- ра входных данных Примечание Распространенные алгоритмы сортировки и поиска уже реализованы и доступны в большинстве языков программирования Вне рамок учебных упражнений вам редко придется реализовывать их самостоятельно Тем не менее важно понимать, как работают базовые алгоритмы, поскольку вы, скорее всего, будете использовать их в своих собственных программах - их пространственная и временная сложность будет влиять на сложность ваших собственных алгоритмов. Кроме того, в ряде случае^ вам придется самосто- ятельно выбрать подходящий алгоритм сортировки или поиска - и для этого необходима хорошая теоретическая база Алгоритмы сортировки Сортировка списка значений — одна из наиболее часто встречающихся задач в программировании, которая получила серьезное теоретическое и практическое изучение. Классиче- ское описание задачи выглядит следующим образом. Пусть дан список значений и функция, которая сравнива- ет два значения. Необходимо упорядочить значения в списке от наименьшего к наибольшему. Значения могут быть целыми числами, строками или даже другими видами объектов. Мы рассмотрим два алгоритма: • Сортировка выбором, которая основана на многократ- ном выборе наименьшего оставшегося элемента. • Сортировка слиянием, которая основана на многократ- ном объединении уже отсортированных частей списка. Другими известными алгоритмами сортировки списков яв- ляются сортировка вставкой, пузырьковая сортировка, пира-
мидалъная сортировка, быстрая сортировка и сортировка Шелла. Существуют также различные алгоритмы, которые выпол- няют задачу сортировки для значений, обладающих опреде- ленными свойствами, например: • Сортировка подсчетом, подходит для небольшого фик- сированного множества значений. • Сортировка по корзинам, которая требует возможности отнести каждое значение к одной из нескольких катего- рий. • Поразрядная сортировка, которая предполагает, что зна- чения представлены как последовательности цифр. Если мы наложим ограничения на исходную задачу, то мо- жем использовать более широкий набор алгоритмов, некото- рые из которых обладают особыми преимуществами. Напри- мер, поразрядная сортировка может работать быстрее, чем любые универсальные алгоритмы сортировки. Сортировка выбором Для упорядочивания списка с помощью сортировки выбо- ром мы многократно выбираем наименьший оставшийся эле- мент и перемещаем его в конец растущего отсортированного сегмента списка. Чтобы проиллюстрировать сортировку выбором, давайте рассмотрим, как она работает с небольшим списком из четы- рех элементов: Неотсортированный список
Изначально весь список не отсортирован. Мы будем разме- щать отсортированные элементы в начале списка — без ис- пользования дополнительной памяти. На начальном этапе от- сортированная часть пуста. Сначала мы должны найти наименьший элемент в неот- сортированной части списка. Мы берем первый элемент не- сортированного списка в качестве минимального значения и сравниваем его с каждым из следующих элементов по оче- реди, заменяя на любой элемент, который окажется меньше. Для этого требуется три сравнения, и мы обнаруживаем, что элемент 1.5 в позиции 2 является наименьшим. Теперь мы поменяем первый элемент нашего неупорядо- ченного списка на наименьший. Он становится первым эле- ментом отсортированной части: Отсортированный список Неотсортированный список Повторяем процесс: находим наименьший из оставшихся элементов — это 2.7. Меняем его местами с первым элементом текущей неотсортированной части (3.8): Отсортированный список Неотсортированный список
Наконец, мы определяем, что 3.8 — наименьший из остав- шихся неупорядоченных элементов, и меняем его местами с 7.2: Отсортированный список Неотсортированный список В таблице ниже показано количество операций каждого типа, использованных при сортировке нашего списка: Длина отсортированного списка Сравнения Обменов Присваиваний нового минимума 0->1 3 1 3 1->2 2 1 2 2->3 1 1 2 Всего 6 3 7 Обратите внимание, что количество сравнений и количе- ство обменов не зависит от содержимого списка (это справед- ливо для сортировки выбором, но не обязательно для других алгоритмов сортировки), в то время как количество присваи- ваний нового минимального значения зависит от данных. В общем случае алгоритм сортировки выбором выглядит следующим образом:
1. Разделить список на две части: уже отсортированную (сначала она пустая) и оставшуюся неотсортированную (вначале это весь список). С каждым шагом отсортирован- ная часть будет увеличиваться, а неотсортированная — уменьшаться. 2. Найти наименьший элемент в несортированном списке: • Выбрать первый элемент несортированного списка в ка- честве минимального элемента. • Сравнить его с каждым элементом несортированного спи- ска по очереди, заменяя текущим элементом, если теку- щий элемент меньше. • В конце этого процесса у нас есть точное местоположение наименьшего элемента в оставшемся списке. 3. Поменять местами минимальный элемент и первый элемент неотсортированной части, тем самым расширив отсортированный список на один элемент. 4. Повторять шаги 2 и 3, пока в неотсортированной части не останется один элемент. Примечание Алгоритм сортировки выбором, описанный здесь, обладает двумя свойства- ми, которые часто желательны для алгоритмов сортировки Первое - это то, что алгоритм является in-place (работает «на месте»). Это означает, что он практически не использует дополнительной памяти, кроме той, которая требуется для хранения входных данных (в данном слу- чае несортированного списка). Допустимо использовать небольшое коли- чество дополнительной памяти (например, переменную для текущего ми- нимального значения) Важным свойством является то, что общий объем не должен увеличиваться с ростом размера входных данных. Второе - алгоритм сортировки устойчив Это означает, что если два элемен- та равны, то их относительный порядок сохраняется. Это особенно важно, когда значения имеют дополнительную связанную информацию (например, сортировка людей по дате рождения). Устойчивые алгоритмы сортировки гарантируют, что сортировка уже отсортированного списка оставит его поря- док неизменным, даже при наличии элементов, которые в результате срав нения считаются равными
Упражнение 1 Допишите следующий код, реализующий сортировку вы- бором на Python. Символ ... указывает на недостающий код, который необходимо вставить: def selection_sort(items): """Сортирует список элементов в порядке воз- растания с помощью алгоритма сортировки выбором. н П // for step in range(len(items)): # Находим позицию наименьшего элемента в # срезе items[step:]. location_of_smallest = step for location in rangefstep, len(items)): # TODO: определить позицию наименьшего # элемента # TODO: поменять местами items[step] # и items[location.of.smallest] Упражнение 2 Ранее в этом разделе мы подсчитывали количество срав- нений, обменов и присваиваний, использованных в нашем примере. 1. Сколько обменов выполняется при применении сорти- ровки выбором к списку из N элементов?
2. Сколько сравнений выполняется при сортировке выбо- ром списка из N элементов? • Сколько сравнений нужно выполнить, чтобы найти наименьший элемент, если неотсортированная часть списка состоит из М элементов? • Просуммируйте все значения М, встречающиеся при сор- тировке списка длины N, чтобы найти общее количе- ство сравнений. 3. Количество присваиваний (для хранения текуще- го наименьшего значения), выполняемых при поиске наименьшего элемента, не более чем на одно больше, чем количество сравнений. Используйте это, чтобы получить верхнюю границу общего количества при- сваиваний, выполняемых при сортировке списка дли- ны N. 4. Используйте результаты предыдущих пунктов, чтобы оценить верхнюю границу общего количества операций (обменов, сравнений и присваиваний), выполняемых при сортировке. Какой из типов операций будет домини- ровать при сортировке больших списков? Сортировка слиянием При использовании сортировки слиянием мы многократно объединяем отсортированные подсписки — начиная с подспи- сков, содержащих по одному элементу. Вскоре мы увидим, что сортировка слиянием требует значи- тельно меньше операций, чем сортировка выбором.
Давайте снова начнем с нашего небольшого списка из четы- рех элементов: Сначала мы объединим два подсписка слева во времен- ное хранилище. Представьте эти два подсписка как две от- сортированные колоды карт. Мы будем объединять их, по- вторно выбирая меныпую из верхних карт обеих колод и помещая ее в конец объединенного списка во временном хранилище. Как только одна из двух колод опустеет, оставшиеся в дру- гой колоде карты можно будет просто поместить в конец объ- единенного списка: Временное хранилище Затем мы копируем объединенный список из временного хранилища обратно в ту часть основного списка, где раньше находились подсписки:
Повторяем процедуру для объединения второй пары отсор- тированных подсписков: Когда мы дошли до конца исходного списка, то возвращаем- ся в начало и снова начинаем объединение отсортированных подсписков. Мы повторяем эту процедуру до тех пор, пока весь список не станет одним отсортированным подсписком. В на- шем примере потребуется еще одно объединение:
Обратите внимание, что размер отсортированных подспи- сков исходного списка удваивается после каждой итерации слияния. После М шагов размер отсортированных секций ра- вен 2м. Как только 2м станет больше N, весь список будет отсор- тирован. Таким образом, чтобы отсортировать список разме- ра N, нам потребуется М = log,N итераций слияния. Каждая итерация слияния требует полного прохода по спи- ску, причем каждый элемент копируется дважды — один раз во временное хранилище, затем обратно в основной список. Пока в обоих подсписках есть элементы, каждая операция ко- пирования требует сравнения, чтобы выбрать, какой элемент копировать. Когда один из списков опустеет, сравнения боль- ше не нужны. Таким образом, каждый проход требует 2N ко- пирований и примерно N сравнений (и уж точно не более N). Общее количество операций, необходимых для сортиров- ки слиянием, равно произведению количества операций в ка- ждом проходе на количество проходов — то есть 2Nlog.N копи- рований и примерно Nlog.N сравнений. Алгоритм сортировки слиянием можно записать в виде сле- дующей последовательности шагов: 1. Создать временный список (хранилище), размер кото- рого соответствует размеру сортируемого списка. 2. Начать с того, что каждый элемент рассматривается как отсортированный подсписок, состоящий из одного элемента. 3. Проходить по всем отсортированным подспискам, объ- единяя соседние пары, следующим образом: i. Использовать две переменные, указывающие на ин- дексы наименьших еще не скопированных элементов в двух отсортированных подсписках, и третью перемен- ную, которая содержит индекс на начало временного хранилища.
и. Скопировать меньший из двух проиндексированных элементов в указанную позицию во временном храни- лище. Увеличить индекс того подсписка, из которого был скопирован элемент, а также индекс временного храни- лища. iii. Если все элементы одного из подсписков скопирова- ны, скопировать оставшиеся элементы из другого под- списка в конец списка, представляющего временное хра- нилище. В противном случае вернуться к шагу 3 ii. iv. Скопировать отсортированный список из временного хранилища обратно в ту часть исходного списка, которая была занята двумя только что объединенными подспи- сками. 4. Если остался только один отсортированный подсписок, значит, весь список отсортирован. В противном случае вернуться к началу шага 3. Упражнение 3 Напишите функцию на Python, которая реализует сорти- ровку слиянием. Возможно, будет удобно реализовать от- дельную функцию для слияния подсписков и вызывать ее из основной программы, реализующей сортировку. Алгоритм сортировки в Python Алгоритм сортировки Python, который по умолчанию ис- пользуется встроенной функцией sorted, а также методом
sort объектов списка, называется Timsort. Это алгоритм, раз- работанный Тимом Питерсом в 2002 году для использования в Python. Timsort — это модифицированная версия сортировки слиянием, которая использует сортировку вставкой для упо- рядочивания участков списка, пригодных для последующего объединения. Примечание Тим Питерс также является автором книги The Zen of Python - попытки крат- ко выразить философию сообщества Python в виде набора канонов Вы мо- жете прочитать его, набрав import this в консоли Pylhon. Алгоритмы поиска Поиск — это также часто встречающаяся и хорошо изучен- ная задача. Формально эта задача может быть описана следу- ющим образом. Имеются список значений, функция сравнения двух значе- ний и искомое значение. Необходимо найти позицию искомо- го значения в списке. Рассмотрим два алгоритма, которые решают эту задачу: • линейный поиск — последовательно проверяет значения в списке, пока не найдет нужное; • бинарный поиск — требует, чтобы исходный список был отсортирован. Проверяет значение в середине списка, от- брасывая половину списка, в которой явно нет искомого значения (все элементы либо больше, либо меньше иско- мого). Существует множество других методов поиска. Часто они основаны на построении более сложных структур дан- ных для облегчения многократного поиска. Примерами таких
структур являются хеш-таблицы (например, словари Python) и префиксные деревья. Также важным направлением являют- ся неточные (inexact) поиски, которые находят элементы, по- хожие на искомый. Линейный поиск Линейный поиск — это самый простой метод поиска. Он предполагает поочередную проверку каждого элемента списка, пока не будет найден нужный элемент. Например, предположим, что мы хотим найти число 3.8 в следующем списке: Проверяется Мы начинаем с первого элемента и выполняем сравнение, чтобы проверить, равно ли его значение искомому. В данном случае 1.5 не равно 3.8, поэтому мы переходим к следующему элементу: Проверка выполнена Проверяется Выполняем еще одно сравнение и видим, что 2.7 также не равно 3.8, поэтому переходим к следующему элементу:
Проверка выполнена Проверяется Выполняем еще одно сравнение и определяем, что нашли правильный элемент. Теперь мы можем завершить поиск и вернуть позицию элемента (индекс 2). В этом случае потребовалось три сравнения, чтобы найти элемент в списке из четырех элементов. Количество сравне- ний зависит от общей длины списка, а также от того, где нахо- дится искомый элемент — в начале или в конце списка. В худ- шем случае, если наш элемент окажется последним в списке, нам придется пройти через весь список. Если мы ищем по этому же списку многократно, предпо- лагая, что вероятность поиска каждого элемента одинакова, то в среднем каждый раз нам придется просматривать полови- ну списка. Таким образом, стоимость (в количестве сравнений) линейного поиска линейно зависит от длины списка. Упражнение 4 Напишите функцию, реализующую линейный поиск. Она должна принимать в качестве параметров список и элемент и возвращать позицию элемента в списке. Если элемента нет в списке, функция должна выбрасы- вать исключение. Если элемент встречается в списке несколько раз, функция должна возвращать индекс его первого вхождения.
Бинарный поиск Бинарный поиск — это более эффективный алгоритм поис- ка, который опирается на то, что элементы списка отсортиро- ваны. Поиск осуществляется в постепенно сужающемся под- списке, начиная со всего списка и уменьшая область поиска вдвое на каждом шаге. Сначала мы проверяем средний элемент в списке. • Если это та величина, которая нам нужна, поиск завершен. • Если элемент больше искомого, мы повторяем процесс по- иска в первой половине списка (перед средним элементом). • Если элемент меньше искомого, мы повторяем процесс по- иска во второй половине списка (после среднего элемента). Например, предположим, что мы хотим найти значение 3.8 в следующем списке из семи элементов: Будет проверен следующим Проверяется Сначала мы сравниваем элемент в середине списка с нашим значением. Поскольку 7.2 больше, чем 3.8, нам нужно прове- рить первую половину списка. Проверяется Будет проверен следующим Проверка выполнена
Теперь первая половина списка — это наш новый список для поиска. Мы сравниваем элемент в середине этого списка с нашим значением. Поскольку 2.7 меньше, чем 3.8, нам нужно искать во второй половине этого подсписка. Проверка выполнена Проверка выполнена Проверяется Вторая половина последнего подсписка — это всего лишь один элемент, который также является средним элементом. Мы сравниваем этот элемент с нашим значением, и это тот элемент, который нам нужен. Всего мы выполнили три сравнения при поиске в списке из семи элементов. Количество сравнений зависит от размера списка, но растет намного медленнее, чем при линейном поис- ке — в худшем случае при длине списка N потребуется не бо- лее log2N сравнений. Упражнение 5 Напишите функцию, реализующую бинарный поиск. Можно считать, что входной список уже отсортирован. Подсказка: такую функцию часто реализуют рекурсивно.
Сложность алгоритма и нотация «О-большое» Обычно мы выражаем стоимость алгоритма как функцию от числа N элементов, к которым применяется алгоритм. Эта функция позволяет оценить, сколько операций потребует- ся для обработки N элементов, и, следовательно, дает воз- можность предсказать, как будет меняться число операций при увеличении N. При этом используется приближенная функция — мы максимально упрощаем выражение, чтобы со- хранить только наиболее важную информацию. Например, мы знаем, что при использовании линейного по- иска в списке из N элементов нам в среднем придется просмо- треть половину списка, прежде чем мы найдем наш элемент — таким образом, количество операций, которые нам придется выполнить, равно N/2. Однако самое главное, что алгоритм масштабируется линейно — при увеличении N стоимость алго- ритма растет пропорционально N, а не N2 или N3. Постоянный коэффициент 1/2 не имеет большого значения по сравнению с различиями между N и, скажем, N2, поэтому мы его опускаем. Таким образом, мы записываем стоимость алгоритма ли- нейного поиска как O(N) — читается как «порядка N» или про- сто «О от N». Такая запись называется нотацией «О-большое», потому что в ней используется большая латинская буква О (от слова order, «порядок»). Мы опускаем не только постоянные множители, но и сла- гаемые меньшего порядка: например, O(N3 + N2) упрощается до O(N3). В приведенном выше примере мы рассчитали среднюю сто- имость алгоритма, которая также известна как ожидаемая стоимость. Также полезно рассчитать стоимость в лучшем и худшем случае. Ниже представлена таблица, в которой пока- зана лучшая, средняя и худшая временная сложность рассмо- тренных нами алгоритмов сортировки и поиска:
Алгоритм Лучший случай Ожидаемый Худший случай Сортировка выбором O(N-) 0(№) 0(№) Сортировка слиянием 0(N log N) 0(N log N) 0(N log N) Линейный поиск 0(1) 0(N) 0(N) Бинарный поиск 0(1) O(log N) Ollog N) Что означает 0(1)? Это значит, что стоимость алгоритма по- стоянна и не зависит от значения N. Для обоих этих алгорит- мов поиска наилучший сценарий — когда первый проверя- емый элемент оказывается искомым, тогда для его нахождения нам нужно выполнить всего одну операцию. В таблице выше «0-большое» используется для описания временной сложности. Но также его можно использовать и для пространственной сложности, то есть оценки того, сколь- ко дополнительной памяти (единиц хранения) требуется алго- ритму помимо входных данных: Алгоритм 1 Сложность пространства Сортировка выбором 0(1) Сортировка слиянием 0(N) Линейный поиск 0(1) Бинарный поиск 0(1)
Ни один из этих алгоритмов не требует значительного объема памяти в дополнение к тому, который используется под входной список, за исключением сортировки слиянием, которая, как мы видели в предыдущем разделе, требует вре- менного хранилища того же размера, что и входные данные (и, следовательно, потребление ею памяти растет линейно с размером входных данных). Примечание В Python Wiki есть страница, которая содержит таблицу со сводкой по вре- менным характеристикам типовых операций с различными структурами данных. Также стоит изучить модуль collections, который содержит до- полнительные структуры данных, оптимизированные под различные за- дачи Примечание Теория вычислительной сложности изучает i нутреннюю сложность самих задач Иногда можно доказать, что любой алгоритм, способный выполнить заданную задачу, потребует некоторого минимального количества шагов или объема дополнительной памяти. Например, можно доказать, что при на- личии списка произвольных объектов и функции сравнения, с помощью ко- торой они сравниваются, ни один алгоритм сортировки не может использо- вать меньше O(N !og N) сравнений. Упражнение 6 1. Из приведенных выше сравнительных таблиц видно, что бинарный поиск эффективнее линейного. Зачем нам вообще использовать линейный поиск? Подсказка: каким свойством должен обладать список, чтобы мы могли ис- пользовать в нем бинарный поиск? 2. Предположим, что каждая из следующих функций по- казывает среднее количество операций, необходимых
для выполнения некоторого алгоритма над списком дли- ны N. Запишите сложность в виде нотации «О-большое» для каждого случая: 1.4N2 + 2N + 2 2. N + log N 3. N log N 4.3
Ответы на упражнения Ответ на упражнение 1 Реализация сортировки выбором (selection sort): def selection.sort(items): """Сортирует список элементов items по возрастанию с использованием алгоритма сортировки выбором. // и н for step in range(len(items)): # Находим позицию наименьшего элемента # в подсписке items[step:]. location_of_smallest = step for location in range(step, len(items)): # Определяем позицию минимального элемента if items[location] < items[location.of_ smallest]: location_of_smallest = location # Обмениваем items[step] и items]location^ оf^smallest] temporary_item = items[step] items[step] = items[location_of_smallest] items[location_of_smallest] = temporary_item Ответ на упражнение 2 1. Количество обменов значениями (N - 1). 2. Количество сравнений (N - 1) * N / 2.
• Чтобы найти наименьший элемент, требуется М - 1 срав- нений. • Суммируя значения М - 1 от 2 до N, получаем: 1+2+3+...+(N-1) = (N - 1) * N / 2 3. Максимальное число присваиваний: (N - 1) * N / 2 + (N - 1). 4. Общее максимальное число операций N**2 + N - 2. Для длинных списков количество операций растет как N**2. Ответ на упражнение 3 Пример реализации алгоритма сортировки слиянием: def merge(items, sections, temporary_storage): start-1, end-1), (start_2, end-2) = sections i_1 = start_1 i_2 = start_2 i_t = 0 while i_1 < end_1 or i_2 < end_2: if i_1 < end_l and i_2 < end_2: if items[i_1] < items[i_2] : temporary_storage[i_t] = items[i_1] i_1 += 1 else: # the_list[i_2] >= items[i_1] temporary_storage[i_t] = items[i_2] i_2 += 1 i_t += 1 elif i_1 < end_1: for i in range(i_1, end_1):
temporary_storage[i_t] = items[i_1] i_1 += 1 i_t += 1 else: # i_2 < end_2 for i in range(i_2, encL2): temporary_storage[i_t] = items[i_2] i_2 += 1 i_t += 1 for i in range(i_t): items[start_1+ i] = temporary_storage[i] def merge_sort(items): n = len(items) temporary-storage = [None] * n size_of_subsections = 1 while size_of_subsections < n: for i in range(0, n, size_of_subsections * 2): i1_start, i1_end = i, min(i + size_of_ subsections, n) i2_start, i2_end = i1_end, min(11_end + size_of_subsections, n) sections = (il.start, il.end), (i2_start, i2_end) merge(items, sections, temporary_storage) size_of_subsections *= 2 return items
Ответ на упражнение 4 Пример реализации линейного поиска; def linear_search(items, desired_item): for position, item in enumerate(items): if item == desired_item: return position raise ValueError("%s не найден в списке." % desired item) Ответ на упражнение 5 Пример реализации двоичного поиска: def binary_search(items, desired_item, start = 9, end = None): if end == None: end = len(items) if start == end: raise ValueError("%s не найден в списке. % desired_item) pos = (end - start) // 2 + start if desired_item == itemsfpos]: return pos elif desired_item > itemsfpos]: return binary_search(items, desired_item, start = (pos+ 1), end = end) else: # desired_item < items[pos]:
return binary_search(items, desired.item, start = start, end = pos) Ответ на упражнение 6 1. Преимущество линейного поиска в том, что его можно вы- полнять в несортированном списке — если мы собираемся про- сматривать все значения по очереди, их порядок не имеет зна- чения. Линейный поиск может быть более эффективным, чем двоичный, если нам нужно найти значение один раз в боль- шом несортированном списке, потому что просто сортировка списка при подготовке к двоичному поиску может оказаться более дорогостоящей. Однако если нам нужно найти значения в одном и том же большом списке несколько раз, то подготови- тельная сортировка списка и использование двоичного поис- ка становятся более целесообразными. 2. Мы отбрасываем все постоянные коэффициенты и менее значимые члены: 1. O(N2) 2. O(N) 3. O(N log N) 4. 0(1)
Справочное издание Аныкрпамальц Сасылым Серия «Программирование от А до C++» ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ НА PYTHON С НУЛЯ Ответственный за выпуск* И. Резько Ответственный редактор; А Ходякова Оформление обложки: С Дарсич Подписано в печать 13.02.2026. Формат 84x108/16. Бумага офсетная. Печать офсетная Гарнитура Noto Serif SennCondensed. Усл. печ. л. 37,7. Тираж 2000 эка Заказ Ms Общероссийский классификатор продукции Ок 034-2014 (КПЕС 2008); 58.11 1 — книги, брошюры печатные. Изготовлено в 2026 г. Произведено в Российской Федерации. Изготовитель: ООО «Издательство АСТ». 129085, Российская Федерация, г. Москва, Звёздный бульвар, дом 21, строение 1, комната 705, пом. I, 7 этаж. Адрес места осуществления деятельности по изготовлению продукции 123112. Российская Федерация, г. Москва, Пресненская набережная, д. 6. стр. 2, Деловой комплекс «Империя», 14,15 этаж. Наш электронный адрес ask@ast.ru Наш сайт: www.ast.ru Интернет-магазин, www.book24.ru 0нд1руш1: «Издательство АСТ» ЖШК, 129085, Ресей Федерациясы. Мэскеу к,. Звёздный бульвары, 21-уй, 1-курылыс, 705-белме, I уй-жай, 7-кабат. Ohim енд!ру к,ызмет1н жузеге асыру мекен-жайы: 123112, Ресей Федерациясы, Маскеук., Пресненская жаг., 6-уй, 2-к,ур., «Империя» ккерлж кешен!, 14 15 к,абат. Б1зд1ц электрондык, мекенжаймыз; www.ast.ru L mail: ask@ast.ru Интернет-магазин: www.buok24.ru Интернет-дукен: www book24.kz Импортер в Республику Казахстан, дистрибьютор и представитель по приёму претензий на продукцию в Республике Казахстан: ТОО «РДЦ Алматы». г. Алматы, ул. Домбровского, 3«а», литер Б, офис 1. Казахстан Республикасына импорттаушы дистрибьютор жэне ^азакстан Республикасында ежмге шагымдар кабылдау жежндеп екш: «РДЦ-Алмагы» ЖШС Алматы к., Домбровский кеш., 3«а», Б литер!, офис 1. Тел.: 8(727) 2 51 59 90,91. факс: 8 (727) 251 59 92 iiuxi 107; E-mail: RDC Almaty@eksmo.kz, www oook24.kz Онд|р<лген жылы: 2026- 0н1мнщ жарамдылык мерз1м, шектелмеген Сертификаттауга жатпайды. Ресей Федерациясында енд!р1лген.
Перед вами не просто руководство по обучению основам объектно- ориентированного программирования на языке Python — благодаря этой книге вы не только получите необходимые знания, но и научитесь думать в терминах ООП, как продвинутый Python-разработчик. И, что важно, с уверенностью выбирать, когда вам действительно понадобится ООП-архи- тектура, а где стоит довольствоваться процедурным программированием. После изучения материала вы легко сможете: создавать классы и объекты, работать с атрибутами и методами: переопределять специальные (магические) методы; выстраивать наследование, композицию и интерфейсы, не допуская «из- быточной» сложности; организовывать модули и пакеты, документировать код и писать модуль- ные тесты. Кроме того, вы подробно ознакомитесь с модулями стандартной би- блиотеки (datetime, re, csu и др.) и научитесь использовать их в своих прило- жениях. Но главное — вы напишете собственные программы, в том чис- ле графическое приложение на tkinter, и отработаете применение ООП в реальных задачах. Для кого эта книга: студентов, уже знакомых с базовым Python и готовых перейти к уровню архитектурного мышления; практикующих программистов, желающих углубить или систематизиро- вать свое понимание ООП; преподавателей как надежный источник теоретического материала и практических задач для курса по программированию. Освойте объектно-ориентированное программирование на Python, чтобы создавать не просто наборы скриптов, а структурированные, масштабируе- мые и легко поддерживаемые программные системы.