Изучаем Python Эрик Метиз. Crash course2 n d e d i t i o na h a n d s o n, p r o j e c t b a s e d i n t r o d u c t i o n t o p r o g r a m m i n g
Скачать 6.19 Mb.
|
216 Глава 10 • Файлы и исключения ПРИМЕЧАНИЕ Формат JSON (JavaScript Object Notation) был изначально разработан для JavaScript . Впрочем, с того времени он стал использоваться во многих языках, вклю- чая Python . Функции json .dump() и json .load() Напишем короткую программу для сохранения набора чисел и другую программу, которая будет читать эти числа обратно в память. Первая программа использует функцию json.dump() , а вторая — функцию json.load() Функция json.dump() получает два аргумента: сохраняемые данные и объект фай- ла, используемый для сохранения. В следующем примере json.dump() используется для сохранения списка чисел: number_writer.py import json numbers = [2, 3, 5, 7, 11, 13] ❶ filename = 'numbers.json' ❷ with open(filename, 'w') as f: ❸ json.dump(numbers, f) Программа импортирует модуль json и создает список чисел для работы. В точ- ке выбирается имя файла для хранения списка. Обычно для таких файлов при- нято использовать расширение .json , указывающее, что данные в файле хранятся в формате JSON. Затем файл открывается в режиме записи, чтобы модуль json мог записать в него данные . В точке функция json.dump() используется для сохранения списка numbers в файле numbers .json Программа ничего не выводит, но давайте откроем файл numbers .json и посмотрим на его содержимое. Данные хранятся в формате, очень похожем на код Python: [2, 3, 5, 7, 11, 13] А теперь напишем следующую программу, которая использует json.load() для чтения списка обратно в память: number_reader.py import json ❶ filename = 'numbers.json' ❷ with open(filename) as f: ❸ numbers = json.load(f) print(numbers) В точке для чтения данных используется тот же файл, в который эти данные были записаны. На этот раз файл открывается в режиме чтения, потому что Python Сохранение данных 217 нужно только прочитать данные из файла . В точке функция json.load() ис- пользуется для загрузки информации из numbers .json ; эта информация сохраняется в переменной numbers . Наконец, программа выводит прочитанный список. Как видите, это тот же список, который был создан в программе number_writer .py : [2, 3, 5, 7, 11, 13] Модуль json позволяет организовать простейший обмен данными между про- граммами. Сохранение и чтение данных, сгенерированных пользователем Сохранение с использованием модуля json особенно полезно при работе с данны- ми, сгенерированными пользователем, потому что без сохранения эта информа- ция будет потеряна при остановке программы. В следующем примере программа запрашивает у пользователя имя при первом запуске программы и «вспоминает» его при повторных запусках. Начнем с сохранения имени пользователя: remember_me.py import json ❶ username = input("What is your name? ") filename = 'username.json' with open(filename, 'w') as f: ❷ json.dump(username, f) ❸ print(f"We'll remember you when you come back, {username}!") В точке программа запрашивает имя пользователя для сохранения. Затем вы- зывается функция json.dump() , которой передается имя пользователя и объект файла; функция сохраняет имя пользователя в файле . Далее выводится сообще- ние о том, что имя пользователя было сохранено : What is your name? Eric We'll remember you when you come back, Eric! А теперь напишем другую программу, которая приветствует пользователя по ранее сохраненному имени: greet_user.py import json filename = 'username.json' with open(filename) as f: ❶ username = json.load(f) ❷ print(f"Welcome back, {username}!") 218 Глава 10 • Файлы и исключения В точке вызов json.load() читает информацию из файла username .json в пере- менную username . После того как данные будут успешно прочитаны, мы можем поприветствовать пользователя по имени : Welcome back, Eric! Теперь эти две программы необходимо объединить в один файл. Когда пользова- тель запускает remember_me .py , программа должна взять имя пользователя из памя- ти, если это возможно; соответственно, программа начинается с блока try , который пытается прочитать имя пользователя. Если файл username .json не существует, блок except запросит имя пользователя и сохранит его в username .json на будущее: remember_me.py import json # Программа загружает имя пользователя, если оно было сохранено ранее. # В противном случае она запрашивает имя пользователя и сохраняет его. filename = 'username.json' try: ❶ with open(filename) as f: ❷ username = json.load(f) ❸ except FileNotFoundError: ❹ username = input("What is your name? ") ❺ with open(filename, 'w') as f: json.dump(username, f) print(f"We'll remember you when you come back, {username}!") else: print(f"Welcome back, {username}!") Никакого нового кода здесь нет; просто блоки кода из двух предыдущих приме- ров были объединены в один файл. В точке программа пытается открыть файл username .json . Если файл существует, программа читает имя пользователя в па- мять и выводит сообщение, приветствующее пользователя, в блоке else . Если программа запускается впервые, то файл username .json не существует и происходит исключение FileNotFoundError . Python переходит к блоку except , в котором пользователю предлагается ввести имя . Затем программа вызывает json.dump() для сохранения имени пользователя и выводит приветствие . Какой бы блок ни выполнялся, результатом является имя пользователя и соот- ветствующее сообщение. При первом запуске программы результат выглядит так: What is your name? Eric We'll remember you when you come back, Eric! Если же программа уже была выполнена хотя бы один раз, то результат будет таким: Welcome back, Eric! Сохранение данных 219 Рефакторинг Часто возникает типичная ситуация: код работает, но вы понимаете, что его струк- туру можно усовершенствовать, разбив его на функции, каждая из которых решает свою конкретную задачу. Этот процесс называется рефакторингом (или перера- боткой). Рефакторинг делает ваш код более чистым, понятным и простым в рас- ширении. В процессе рефакторинга remember_me .py мы можем переместить основную часть логики в одну или несколько функций. Основной задачей remember_me .py является вывод приветствия для пользователя, поэтому весь существующий код будет пере- мещен в функцию greet_user() : remember_me.py import json def greet_user(): ❶ """Приветствует пользователя по имени.""" filename = 'username.json' try: with open(filename) as f: username = json.load(f) except FileNotFoundError: username = input("What is your name? ") with open(filename, 'w') as f: json.dump(username, f) print(f"We'll remember you when you come back, {username}!") else: print(f"Welcome back, {username}!") greet_user() С переходом на функцию комментарии дополняются строкой документации, кото- рая описывает работу кода в текущей версии . Код становится немного чище, но функция greet_user() не только приветствует пользователя — она также загружает хранимое имя пользователя, если оно существует, и запрашивает новое имя, если оно не было сохранено ранее. Переработаем функцию greet_user() , чтобы она не решала столько разных задач. Начнем с перемещения кода загрузки хранимого имени пользователя в отдельную функцию: import json def get_stored_username(): ❶ """Получает хранимое имя пользователя, если оно существует.""" filename = 'username.json' try: with open(filename) as f: username = json.load(f) except FileNotFoundError: 220 Глава 10 • Файлы и исключения ❷ return None else: return username def greet_user(): """Приветствует пользователя по имени.""" username = get_stored_username() ❸ if username: print(f"Welcome back, {username}!") else: username = input("What is your name? ") filename = 'username.json' with open(filename, 'w') as f: json.dump(username, f) print(f"We'll remember you when you come back, {username}!") greet_user() Новая функция get_stored_username() имеет четкое предназначение, изложенное в строке документации . Эта функция читает и возвращает сохраненное имя пользователя, если его удается найти. Если файл username .json не существует, то функция возвращает None . И это правильно: функция должна возвращать либо ожидаемое значение, либо None . Это позволяет провести простую проверку воз- вращаемого значения функции. В точке программа выводит приветствие для пользователя, если попытка получения имени пользователя была успешной; в про- тивном случае программа запрашивает новое имя пользователя. Из функции greet_user() стоит вынести еще один блок кода. Если имя пользова- теля не существует, то код запроса нового имени должен размещаться в функции, специализирующейся на решении этой задачи: import json def get_stored_username(): """Получает хранимое имя пользователя, если оно существует.""" def get_new_username(): """Запрашивает новое имя пользователя.""" username = input("What is your name? ") filename = 'username.json' with open(filename, 'w') as f: json.dump(username, f) return username def greet_user(): """Приветствует пользователя по имени.""" username = get_stored_username() if username: print(f"Welcome back, {username}!") else: username = get_new_username() print(f"We'll remember you when you come back, {username}!") greet_user() Итоги 221 Каждая функция в окончательной версии remember_me .py имеет четкое, конкретное предназначение. Мы вызываем greet_user() , и эта функция выводит нужное при- ветствие: либо для уже знакомого, либо для нового пользователя. Для этого функ- ция вызывает функцию get_stored_username() , которая отвечает только за чтение хранимого имени пользователя (если оно есть). Наконец, функция greet_user() при необходимости вызывает функцию get_new_username() , которая отвечает толь- ко за получение нового имени пользователя и его сохранение. Такое «разделение обязанностей» является важнейшим аспектом написания чистого кода, простого в сопровождении и расширении. УПРАЖНЕНИЯ 10.11. Любимое число: напишите программу, которая запрашивает у пользователя его лю- бимое число. Воспользуйтесь функцией json.dump() для сохранения этого числа в файле. Напишите другую программу, которая читает это значение и выводит сообщение: «Я знаю ваше любимое число! Это _____». 10.12. Сохраненное любимое число: объедините две программы из упражнения 10.11 в один файл. Если число уже сохранено, сообщите его пользователю, а если нет — запро- сите любимое число пользователя и сохраните в файле. Выполните программу дважды, чтобы убедиться в том, что она работает. 10.13. Проверка пользователя: последняя версия remember_me .py предполагает, что поль- зователь либо уже ввел свое имя, либо программа выполняется впервые. Ее нужно изме- нить на тот случай, если текущий пользователь не является тем человеком, который по- следним использовал программу. Прежде чем выводить приветствие в greet_user() , спросите, правильно ли определено имя пользователя. Если ответ будет отрицательным, вызовите get_new_username() для получе- ния правильного имени пользователя. Итоги В этой главе вы научились работать с файлами. Вы узнали, как прочитать сразу весь файл и как читать его содержимое по строкам. Вы научились записывать в файл и присоединять текст в конец файла, познакомились с исключениями и средствами обработки исключений, возникающих в программе. В завершающей части главы рассматриваются структуры данных Python для сохранения введенной информа- ции, чтобы пользователю не приходилось каждый раз вводить данные заново при каждом запуске программы. В главе 11 мы займемся эффективной организацией тестирования вашего кода. Тестирование поможет убедиться в том, что написанный код работает правильно, а также выявит ошибки, внесенные в процессе расширения уже написанных про- грамм. 11 Тестирование Вместе с функциями и классами вы также можете написать тесты для своего кода. Тестирование доказывает, что код работает так, как положено, для любых раз- новидностей входных данных, которые он может получать. Тесты позволят вам быть уверенными в том, что код будет работать правильно и тогда, когда вашими программами начнут пользоваться другие люди. Тестирование при добавлении нового кода гарантирует, что внесенные изменения не изменят текущее поведение программы. Все программисты допускают ошибки, поэтому каждый программист должен часто тестировать свой код и выявлять ошибки до того, как с ними столк- нутся другие пользователи. В этой главе вы научитесь тестировать код средствами модуля Python unittest Вы узнаете, как построить тестовые сценарии, как проверить, что для конкретных входных данных программа выдает ожидаемый результат. Вы поймете, как выгля- дят успешно проходящие или сбойные тесты, и узнаете, как сбойный тест помогает усовершенствовать код. Также вы научитесь тестировать функции и классы и оце- нивать примерное количество необходимых тестов для проекта. Тестирование функции Чтобы потренироваться в тестировании, нам понадобится код. Ниже приведена простая функция, которая получает имя и фамилию и возвращает отформатиро- ванное полное имя: name_function.py def get_formatted_name(first, last): """Строит отформатированное полное имя.""" full_name = f"{first} {last}" return full_name.title() Функция get_formatted_name() строит полное имя из имени и фамилии, разде- лив их пробелом, преобразует первый символ каждого слова к верхнему регистру и возвращает полученный результат. Чтобы убедиться в том, что функция get_ formatted_name() работает правильно, мы напишем программу, использующую Тестирование функции 223 эту функцию. Программа names .py запрашивает у пользователя имя и фамилию и выдает отформатированное полное имя: names.py from name_function import get_formatted_name print("Enter 'q' at any time to quit.") while True: first = input("\nPlease give me a first name: ") if first == 'q': break last = input("Please give me a last name: ") if last == 'q': break formatted_name = get_formatted_name(first, last) print(f"\tNeatly formatted name: {formatted_name}.") Программа импортирует функцию get_formatted_name() из модуля name_function . py . Пользователь вводит последовательность имен и фамилий и видит, что про- грамма сгенерировала отформатированные полные имена: Enter 'q' at any time to quit. Please give me a first name: janis Please give me a last name: joplin Neatly formatted name: Janis Joplin. Please give me a first name: bob Please give me a last name: dylan Neatly formatted name: Bob Dylan. Please give me a first name: q Как видно из листинга, имена сгенерированы правильно. Но допустим, вы решили изменить функцию get_formatted_name() , чтобы она также работала со вторыми именами. При этом необходимо проследить за тем, чтобы функция не перестала правильно работать для имен, состоящих только из имени и фамилии. Чтобы про- тестировать код, можно запустить names .py и для проверки вводить имя из двух компонентов (скажем, Janis Joplin ) при каждом изменении get_formatted_name() , но это довольно утомительно. К счастью, Python предоставляет эффективный механизм автоматизации тестирования вывода функций. При автоматизации те- стирования get_formatted_name() вы будете уверены в том, что функция успешно работает для всех видов имен, для которых написаны тесты. Модульные тесты и тестовые сценарии Модуль unittest из стандартной библиотеки Python предоставляет функциональ- ность для тестирования вашего кода. Модульный тест проверяет правильность ра- 224 Глава 11 • Тестирование боты одного конкретного аспекта поведения функции. Тестовый сценарий представ- ляет собой совокупность модульных тестов, которые совместно доказывают, что функция ведет себя так, как положено, во всем диапазоне ситуаций, которые она должна обрабатывать. Хороший тестовый сценарий учитывает все возможные виды ввода, которые может получать функция, и включает тесты для представления всех таких ситуаций. Тестовый сценарий с полным покрытием включает полный спектр модульных тестов, покрывающих все возможные варианты использования функции. Обеспечение полного покрытия для крупного проекта может быть весьма непростой задачей. Часто бывает достаточно написать модульные тесты для кри- тичных аспектов поведения вашего кода, а затем стремиться к полному покрытию только в том случае, если проект перейдет в фазу масштабного использования. Прохождение теста Вы не сразу привыкнете к синтаксису создания тестовых сценариев, но после того, как тестовый сценарий будет создан, вы сможете легко добавить новые модульные тесты для своих функций. Чтобы написать тестовый сценарий для функции, импор- тируйте модуль unittest и функцию, которую необходимо протестировать. Затем создайте класс, наследующий от unittest.TestCase , и напишите серию методов для тестирования различных аспектов поведения своей функции. Ниже приведен тестовый сценарий с одним методом, который проверяет, что функ- ция get_formatted_name() правильно работает при передаче имени и фамилии: test_name_function.py import unittest from name_function import get_formatted_name ❶ class NamesTestCase(unittest.TestCase): """Тесты для 'name_function.py'.""" def test_first_last_name(self): """Имена вида 'Janis Joplin' работают правильно?""" ❷ formatted_name = get_formatted_name('janis', 'joplin') ❸ self.assertEqual(formatted_name, 'Janis Joplin') ❹ if __name__ == '__main__': unittest.main() Сначала мы импортируем unittest и тестируемую функцию get_formatted_name() В точке создается класс NamesTestCase , который содержит серию модульных тестов для get_formatted_name() . Имя класса выбирается произвольно, но лучше выбрать имя, связанное с функцией, которую вы собираетесь тестировать, и вклю- чить в имя класса слово Test . Этот класс должен наследовать от класса unittest. TestCase , чтобы Python знал, как запустить написанные вами тесты. Класс NamesTestCase содержит один метод, который тестирует всего один аспект get_formatted_name() — правильность форматирования имен, состоящих только из |