Вы уже овладели основными навыками, необходимыми для создания хорошо структурированных и удобных в использовании программ; теперь пора подумать о том, как сделать ваши программы еще более удобными и полезными. В этой главе вы научитесь работать с файлами, чтобы ваши программы могли быстро анализировать большие объемы данных.
Вы научитесь обрабатывать ошибки, чтобы возникновение аномальных ситуаций не приводило к аварийному завершению ваших программ. Мы рассмотрим исключения (exceptions) — специальные объекты, которые создаются для управления ошибками, возникающими во время выполнения программ Python. Вдобавок будет описан модуль json, позволяющий сохранять пользовательские данные, чтобы они не терялись при завершении работы программы.
Работа с файлами и сохранение данных упрощают использование ваших программ. Пользователь сам выбирает, какие данные и когда нужно вводить. Он может запустить программу, выполнить некую работу, потом закрыть программу и позднее продолжить работу с того момента, на котором он остановился. Умея обрабатывать исключения, вы сможете справиться с такими ситуациями, как отсутствие нужных файлов, а также другими проблемами, приводящими к сбою программ. Обработка исключений повысит устойчивость ваших программ при работе с некорректными данными — как появившимися из-за случайных ошибок, так и злонамеренными попытками взлома ваших программ. Используя материал, представленный в этой главе, вы сделаете ваши программы более практичными, удобными и надежными.
Гигантские объемы данных доступны в текстовых файлах. В них могут храниться погодные данные, социально-экономическая информация, литературные произведения и многое другое. Чтение из файла особенно актуально для приложений, предназначенных для анализа данных, но может пригодиться и в любой другой ситуации, требующей анализа или изменения информации, хранящейся в файле. Например, программа может читать содержимое текстового файла и переписывать его, используя форматирование, рассчитанное на отображение информации в браузере.
Работа с информацией в текстовом файле начинается с чтения данных в память. Вы можете прочитать все содержимое файла или же читать данные по строкам.
Для начала нам понадобится файл с несколькими строками текста. Пусть это будет файл, содержащий число пи с точностью до 30 знаков, по 10 знаков на строку:
pi_digits.txt
3.1415926535
8979323846
2643383279
Чтобы опробовать эти примеры, либо введите данные в редакторе и сохраните файл pi_digits.txt, либо скачайте файл из дополнительных материалов на https://ehmatthes.github.io/pcc_3e. Сохраните файл в каталоге, в котором будут располагаться программы этой главы.
Следующая программа открывает данный файл, читает его и выводит содержимое на экран:
file_reader.py
from pathlib import Path
❶ path = Path('pi_digits.txt')
❷ contents = path.read_text()
print(contents)
Чтобы получить доступ к содержимому файла, нам нужно указать Python путь к нему. Путь (path) указывает на точное местоположение файла или папки в системе. В Python доступен модуль pathlib, упрощающий работу с файлами и каталогами, независимо от того, в какой операционной системе работаете вы или пользователи вашей программы. Модуль, предоставляющий ту или иную функциональность, часто называют библиотекой (library), отсюда и название pathlib.
Начнем с импорта класса Path из pathlib. Объект Path, указывающий на файл, позволяет вам выполнить многие действия. Например, прежде чем работать с файлом, вы можете проверить, существует ли он, прочитать его содержимое или записать в него новые данные. В нашем случае мы создаем объект Path, представляющий файл pi_digits.txt, который мы присваиваем переменной path ❶. Данный файл сохранен в том же каталоге, что и файл .py, который мы пишем, поэтому имя файла — все, что нужно Path для доступа к нему.
ПРИМЕЧАНИЕ
Программа VS Code ищет файлы в папке, которая была открыта последней. Если вы пользуетесь этим редактором, то начните с открытия папки, в которой хранятся программы из данной главы. Например, если вы храните файлы программ в папке chapter_10, то нажмите сочетание клавиш Ctrl+O (+O в macOS) и откройте эту папку.
После того как в программе появится объект Path, представляющий файл pi_digits.txt, используется метод read_text(), который читает все содержимое файла ❷ и сохраняет это содержимое в одной длинной строке в переменной contents. При выводе значения contents на экране появляется все содержимое файла:
3.1415926535
8979323846
2643383279
Единственное различие между выводом и исходным файлом — лишняя пустая строка в конце вывода. Откуда она взялась? Метод read_text() возвращает ее при чтении, если достигнут конец файла. Если вы хотите удалить лишнюю пустую строку, то примените функцию rstrip() к строке, хранящейся в переменной contents:
from pathlib import Path
path = Path('pi_digits.txt')
contents = path.read_text()
contents = contents.rstrip()
print(contents)
Напомним (из главы 2), что метод rstrip() удаляет все пробельные символы, начиная от правого края строки. Теперь вывод точно соответствует содержимому исходного файла:
3.1415926535
8979323846
2643383279
Мы можем удалить символ новой строки при чтении содержимого файла, применив метод rstrip() сразу после вызова read_text():
contents = path.read_text().rstrip()
Код дает Python указание применить метод read_text() к обрабатываемому файлу. Затем интерпретатор применяет метод rstrip() к строке, возвращаемой методом read_text(). Итоговая строка присваивается переменной contents. Такой подход называется цепочкой методов (method chaining) и используется в программировании довольно часто.
Если передать Path простое имя файла, такое как pi_digits.txt, то Python ищет файл в том каталоге, в котором находится файл, выполняемый в настоящий момент (то есть файл программы .py).
В некоторых случаях (в зависимости от того, как организованы ваши рабочие файлы) открываемый файл может и не находиться в одном каталоге с файлом программы. Например, файл программы может располагаться в папке python_work; в ней создается папка text_files для текстовых файлов, с которыми работает программа. И хотя она находится в папке python_work, простая передача объекту Path имени файла из text_files не подойдет, поскольку Python проводит поиск файла в python_work и на этом остановится; поиск не будет продолжен во вложенной папке text_files. Чтобы открыть файлы из каталога, отличного от того, в котором хранится файл программы, необходимо указать корректный путь — то есть дать Python указание искать файлы в конкретном месте файловой системы.
В программировании применяются два основных способа указания путей. С помощью относительного пути (relative file path) вы можете дать Python указание искать файлы в каталоге, который задается относительно каталога, содержащего текущий файл программы. Папка text_files расположена в папке python_work, поэтому для открытия файла из text_files нужно создать путь, который начинается с text_files и заканчивается именем файла. Вот как создать этот путь:
path = Path('text_files/имя_файла.txt')
Кроме того, можно точно определить местонахождение файла в вашей системе независимо от того, где хранится выполняемая программа. Такие пути называются абсолютными (absolute file path) и используются в том случае, если относительный путь не работает. Например, если папка text_files находится не в python_work, а в другой папке (скажем, в other_files), то передать объекту Path путь 'text_files/имя_файла.txt' не получится, поскольку Python будет искать указанную папку только внутри python_work. Чтобы объяснить Python, где следует искать файл, необходимо записать полный путь.
Абсолютные пути обычно длиннее относительных, поскольку начинаются с корневого каталога системы:
path = Path('/home/eric/data_files/text_files/имя_файла.txt')
Используя абсолютные пути, вы сможете читать файлы из любого каталога вашей системы. Пока будет проще хранить файлы в одном каталоге с файлами программ или в папках, вложенных в каталог с файлами программ (таких как text_files из рассмотренного примера).
ПРИМЕЧАНИЕ
В операционной системе Windows для отображения путей к файлам применяется обратный слеш (\) вместо прямого (/), но вы должны использовать прямые слеши в своем коде, даже в Windows. Библиотека pathlib автоматически выберет правильное представление пути при работе в вашей системе или системе стороннего пользователя.
В процессе чтения файла часто бывает нужно обработать каждую строку. Возможно, вы ищете некую информацию в файле или собираетесь каким-то образом изменить текст. Например, при чтении файла с метеорологическими данными вы обрабатываете каждую строку, у которой в описании погоды встречается слово «солнечно». Или, допустим, в новостях ищете каждую строку с тегом заголовка и заменяете ее специальными элементами форматирования.
Вы можете использовать метод splitlines(), чтобы преобразовать длинную строку в группу, а затем добавить цикл for для обработки каждой строки по очереди:
file_reader.py
from pathlib import Path
path = Path('pi_digits.txt')
❶ contents = path.read_text()
❷ lines = contents.splitlines()
for line in lines:
print(line)
Начнем со считывания всего содержимого файла, как мы делали это ранее ❶. Если вы планируете обрабатывать отдельные строки в файле, то вам не нужно удалять пробельные символы при чтении файла. Метод splitlines() возвращает список всех строк в файле, и мы присваиваем этот список переменной lines ❷. Затем перебираем эти строки и выводим каждую из них:
3.1415926535
8979323846
2643383279
Поскольку мы не изменили ни одной строки, то вывод полностью совпадает с исходным текстовым файлом.
После того как файл будет прочитан в память, вы сможете обрабатывать данные так, как посчитаете нужным. Вкратце изучим цифры числа пи. Для начала попробуем создать одну строку со всеми цифрами из файла без промежуточных пробельных символов:
pi_string.py
from pathlib import Path
path = Path('pi_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
❶ for line in lines:
pi_string += line
print(pi_string)
print(len(pi_string))
Сначала интерпретатор считывает содержимое файла и сохраняет каждую строку цифр в списке — точно так же, как это делалось в предыдущем примере. Затем создается переменная pi_string для хранения цифр числа пи. Далее следует цикл, который добавляет к pi_string каждую серию цифр ❶. Затем программа выводит строку и ее длину:
3.1415926535 8979323846 2643383279
36
Переменная pi_string содержит пробельные символы, которые присутствовали в начале каждой строки цифр. Чтобы удалить их, достаточно использовать функцию lstrip() на каждой строке:
--пропуск--
for line in lines:
pi_string += line.strip()
print(pi_string)
print(len(pi_string))
В итоге мы получаем строку, содержащую значение пи с точностью до 30 знаков. Длина строки равна 32 символам, поскольку в нее также добавляются начальная цифра 3 и десятичная точка:
3.141592653589793238462643383279
32
ПРИМЕЧАНИЕ
Считывая содержимое текстового файла, Python интерпретирует весь текст в файле как строку. Если вы считываете из текстового файла число и хотите работать с ним в числовом контексте, то преобразуйте его в целое или вещественное число с помощью функций int() или float() соответственно.
До настоящего момента мы ограничивались анализом текстового файла, который состоял всего из трех строк, но код этих примеров будет работать и с куда бо́льшими файлами. Начиная с текстового файла, содержащего значение пи до 1 000 000 знаков (вместо 30), вы сможете создать одну строку, которая содержит все эти цифры. Изменять программу вообще не придется — достаточно передать ей другой файл. Кроме того, мы ограничимся выводом первых 50 цифр, чтобы не пришлось ждать, пока в терминале прокрутится миллион знаков:
pi_string.py
from pathlib import Path
path = Path('pi_million_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
for line in lines:
pi_string += line.lstrip()
print(f"{pi_string[:52]}...")
print(len(pi_string))
Из выходных данных видно, что строка действительно содержит значение числа пи с точностью до 1 000 000 знаков:
3.14159265358979323846264338327950288419716939937510...
1000002
Python не устанавливает никаких ограничений на длину данных, с которыми вы можете работать. Она ограничивается разве что объемом памяти вашей системы.
ПРИМЕЧАНИЕ
Чтобы запустить эту программу (и многие другие примеры, приведенные ниже), необходимо скачать дополнительные материалы с сайта https://ehmatthes.github.io/pcc_3e.
Меня всегда интересовало, не встречается ли мой день рождения среди цифр числа пи. Воспользуемся только что созданной программой и проверим, есть ли цифры дня рождения пользователя в первом миллионе цифр. Для этого можно записать день рождения в виде строки из цифр и посмотреть, имеется ли эта строка в pi_string:
pi_birthday.py
--пропуск--
for line in lines:
pi_string += line.lstrip()
birthday = input("Enter your birthday, in the form mmddyy: ")
if birthday in pi_string:
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appear in the first million digits of pi.")
Сначала программа запрашивает день рождения пользователя, а затем проверяет вхождение этой строки в pi_string. Пробуем:
Enter your birthdate, in the form mmddyy: 120372
Your birthday appears in the first million digits of pi!
Оказывается, мой день рождения встречается среди цифр числа пи! После того как данные будут прочитаны из файла, вы сможете делать с ними все, что сочтете нужным.
Упражнения
10.1. Изучение Python. Откройте пустой файл в текстовом редакторе и напишите несколько строк текста о возможностях Python. Каждая строка должна начинаться с фразы «В Python вы можете…» Сохраните файл под именем learning_python.txt в каталоге, использованном для примеров этой главы. Напишите программу, которая читает файл и выводит текст два раза: читая весь файл и сохраняя строки в списке, проходя по каждой строке.
10.2. Изучение C. Метод replace() может использоваться для замены любого слова в строке другим словом. В следующем примере слово 'dog' заменяется словом 'cat':
>>> message = "I really like dogs."
>>> message.replace('dog', 'cat')
'I really like cats.'
Прочитайте каждую строку из только что созданного файла learning_python.txt и замените слово Python названием другого языка — например, C. Выведите каждую измененную строку на экран.
10.3. Более простой код. В программе file_reader.py в этом разделе используется временная переменная, lines, демонстрирующая работу метода splitlines(). Вы можете опустить временную переменную и выполнить цикл непосредственно над списком, который возвращает метод splitlines(), следующим образом:
for line in contents.splitlines():
Удалите временную переменную из всех программ в этом разделе, чтобы сделать их код более лаконичным.
Один из простейших способов сохранения данных — запись в файл. Текст, записанный в файл, останется доступным и после того, как терминал с выводом вашей программы будет закрыт. Вы сможете проанализировать результаты после завершения программы или передать свои файлы другим. Вы также сможете написать программы, которые снова читают сохраненный текст в память и работают с ним.
Определив путь, вы можете записать текст в файл с помощью метода write_text(). Для наглядности напишем простое сообщение и сохраним его в файл вместо вывода на экран:
write_message.py
from pathlib import Path
path = Path('programming.txt')
path.write_text("I love programming.")
Метод write_text() принимает единственный аргумент: строку, которую вы хотите записать в файл. Эта программа не имеет терминального вывода, но если вы откроете файл programming.txt, то увидите следующую строку:
programming.txt
I love programming.
Этот файл ничем не отличается от любого другого текстового файла на вашем компьютере. Его можно открыть, записать в него новый текст, скопировать/вставить текст и т.д.
ПРИМЕЧАНИЕ
Python может записывать в текстовые файлы только строковые данные. Если вы захотите сохранить в текстовом файле числовую информацию, то данные придется предварительно преобразовать в строки с помощью функции str().
Метод write_text() «за кадром» выполняет несколько действий. Если файла, на который указывает путь, не существует, то метод создает его. Кроме того, после записи строки в файл метод проверяет, закрыт ли файл должным образом. Незакрытые файлы могут привести к потере или повреждению данных.
Чтобы записать в файл несколько строк, необходимо создать строку, содержащую все содержимое файла, а затем вызвать функцию write_text() и передать ей эту строку. Запишем несколько строк в файл programming.txt:
from pathlib import Path
contents = "I love programming.\n"
contents += "I love creating new games.\n"
contents += "I also love working with data.\n"
path = Path('programming.txt')
path.write_text(contents)
Мы определяем переменную contents, в которой будет храниться содержимое файла. В следующей строке используется оператор +=, позволяющий добавлять что-либо к этой строке. Вы можете применить его столько раз, сколько нужно, формируя строки любой длины. В данном случае мы добавляем символы новой строки в конце каждой строки, чтобы каждая фраза выводилась на отдельной строке.
Если вы выполните этот код, а затем откроете файл programming.txt, то увидите в нем следующие строки:
programming.txt
I love programming.
I love creating new games.
I also love working with data.
Вы можете форматировать вывод с помощью символов пробелов, табуляции и пустых строк так же, как и в терминале. Длина строк неограниченна, и именно так формируются многие документы, генерируемые компьютером.
ПРИМЕЧАНИЕ
Будьте осторожны при вызове функции write_text() на объекте Path. Если файл уже существует, то функция write_text() перезапишет текущее содержимое файла. Позже в этой главе вы научитесь с помощью модуля pathlib проверять, существует ли файл.
Упражнения
10.4. Гость. Напишите программу, которая запрашивает у пользователя его имя. Введенный ответ сохраняется в файле guest.txt.
10.5. Гостевая книга. Напишите цикл while, который в цикле запрашивает у пользователей имена. При вводе каждого имени выведите на экран приветствие и добавьте строку с сообщением в файл guest_book.txt. Проследите за тем, чтобы каждое сообщение размещалось в отдельной строке файла.
Для управления ошибками, возникающими в ходе выполнения программы, в Python используются специальные объекты, называемые исключениями (exceptions). Если при возникновении ошибки Python не знает, что делать дальше, то создается объект исключения. Если в программу добавлен код обработки исключения, то выполнение программы продолжится, а если нет — программа останавливается и выводит трассировку с отчетом об исключении.
Исключения обрабатываются в блоках try-except. С помощью такого блока можно дать Python указание выполнить некие действия, но при этом сообщить, что делать при возникновении исключения. Используя блоки try-except, ваши программы будут работать даже в том случае, если что-то пошло не так. Вместо невразумительной трассировки выводится понятное сообщение об ошибке, которое вы определяете в программе.
Рассмотрим простую ошибку, при которой Python инициирует исключение. Конечно, вы знаете, что деление на ноль невозможно, но мы все же прикажем Python выполнить эту операцию:
division_calculator.py
print(5/0)
Конечно, из этого ничего не выйдет, поэтому на экран выводятся данные трассировки:
Traceback (most recent call last):
File "division.py", line 1, in <module>
print(5/0)
~^~
❶ ZeroDivisionError: division by zero
Ошибка, упоминаемая в трассировке — ZeroDivisionError, — является объектом исключения ❶. Такие объекты создаются в том случае, если Python не может выполнить ваши указания. Обычно в таких случаях Python прерывает выполнение программы и сообщает тип обнаруженного исключения. Эта информация может использоваться в программе; по сути, вы даете Python указание, как следует поступить при возникновении исключения данного типа. В таком случае ваша программа будет подготовлена к его появлению.
Если вы предполагаете, что в программе может произойти ошибка, то напишите блок try-except для обработки возникающего исключения. С помощью такого блока вы даете Python указание выполнить некий код, а также сообщаете, что нужно делать, если при его выполнении произойдет исключение конкретного типа.
Вот как выглядит блок try-except для обработки исключений ZeroDivisionError:
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")
Команда print(5/0), порождающая ошибку, находится в блоке try. Если код в этом блоке выполнен успешно, то Python пропускает блок except. Если код в блоке try порождает ошибку, то Python ищет блок except с соответствующей ошибкой и выпускает код в нем.
В этом примере код блока try порождает ошибку ZeroDivisionError, поэтому Python ищет блок except, содержащий описание того, как следует действовать в такой ситуации. При выполнении кода этого блока пользователь видит понятное сообщение об ошибке вместо данных трассировки:
You can't divide by zero!
Если бы за кодом try-except следовал другой код, то выполнение программы продолжилось бы, поскольку мы объяснили Python, как обрабатывать эту ошибку. В следующем примере обработка ошибки позволяет программе продолжить выполнение.
Правильная обработка ошибок особенно важна в том случае, если программа должна продолжить работу после возникновения ошибки. Такая ситуация часто встречается в программах, запрашивающих данные у пользователя. Если программа правильно среагировала на некорректный ввод, то может запросить новые данные после сбоя.
Создадим простой калькулятор, который выполняет только операцию деления:
division_calculator.py
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
❶ first_number = input("\nFirst number: ")
if first_number == 'q':
break
❷ second_number = input("Second number: ")
if second_number == 'q':
break
❸ answer = int(first_number) / int(second_number)
print(answer)
Программа запрашивает у пользователя первое число first_number ❶, а затем, если он не ввел q для завершения работы, запрашивает второе число second_number ❷. Далее одно число делится на другое для получения результата answer ❸. Программа никак не пытается обрабатывать ошибки, так что попытка деления на ноль приводит к ее аварийному завершению:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division_calculator.py", line 11, in <module>
answer = int(first_number) / int(second_number)
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
ZeroDivisionError: division by zero
Конечно, аварийное завершение — это плохо, но еще хуже, что пользователь увидит данные трассировки. Неопытного пользователя они собьют с толку, а при сознательной попытке взлома злоумышленник сможет получить из них куда больше информации, чем вам хотелось бы. Например, он узнает имя файла программы и увидит некорректно работающую часть кода. На основании этой информации опытный хакер иногда может определить, какие атаки следует применять против вашего кода.
Для повышения устойчивости программы к ошибкам можно поместить строку, выдающую ошибки, в блок try-except. Ошибка происходит в строке, выполняющей деление; следовательно, именно эту строку следует поместить в блок try-except. Данный пример также содержит блок else. Любой код, зависящий от успешного выполнения блока try, размещается в блоке else:
--пропуск--
while True:
--пропуск--
if second_number == 'q':
break
❶ try:
answer = int(first_number) / int(second_number)
❷ except ZeroDivisionError:
print("You can't divide by 0!")
❸ else:
print(answer)
Программа пытается выполнить операцию деления в блоке try ❶, который содержит только код, способный породить ошибку. Любой код, зависящий от успешного выполнения блока try, добавляется в блок else. В данном случае если операция деления выполняется успешно, то блок else используется для вывода результата ❸.
Блок except сообщает Python, как следует поступать при возникновении ошибки ZeroDivisionError ❷. Если при выполнении команды из блока try происходит ошибка, связанная с делением на ноль, то программа выводит понятное сообщение, которое объясняет пользователю, как избежать подобных ошибок. Выполнение программы продолжается, и пользователь не увидит трассировку:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q
В блоках try следует размещать только тот код, при работе которого может возникнуть исключение. Иногда некий код должен выполняться только в том случае, если выполнение try прошло успешно; такой код размещается в блоке else. Блок except сообщает Python, что делать, если при выполнении кода try произошло исключение.
Заранее определяя вероятные источники ошибок, вы повышаете надежность своих программ, которые продолжают работать даже при вводе некорректных данных или при недоступности ресурсов. Ваш код оказывается защищенным от случайных ошибок пользователей и сознательных атак.
Одна из стандартных проблем при работе с файлами — отсутствие необходимых файлов. Тот файл, который вам нужен, может находиться в другом месте, в имени файла может быть допущена ошибка, или файл может вообще не существовать. Все эти ситуации обрабатываются в блоках try-except.
Попробуем прочитать данные из несуществующего файла. Следующая программа пытается прочитать содержимое файла с текстом «Алисы в Стране чудес», но я не сохранил файл alice.txt в одном каталоге с файлом alice.py:
alice.py
from pathlib import Path
path = Path('alice.txt')
contents = path.read_text(encoding='utf-8')
Обратите внимание, что здесь мы используем метод read_text() несколько иначе, чем в предыдущих случаях. Аргумент encoding необходим в тех случаях, когда кодировка вашей системы по умолчанию не совпадает с кодировкой читаемого файла. Обычно так происходит при чтении файла, который был создан не в вашей системе.
Прочитать данные из несуществующего файла нельзя, поэтому Python выдает исключение:
Traceback (most recent call last):
❶ File "alice.py", line 4, in <module>
❷ contents = path.read_text(encoding='utf-8')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../pathlib.py", line 1056, in read_text
with self.open(mode='r', encoding=encoding, errors=errors) as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../pathlib.py", line 1042, in open
return io.open(self, mode, buffering, encoding, errors, newline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
❸ FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
Это более длинная трассировка, чем встречавшиеся вам ранее, поэтому посмотрим, как разобраться в более сложных результатах. Зачастую лучше всего читать трассировки с конца. В последней строке показано, что было выброшено исключение FileNotFoundError ❸. Это важно, поскольку так мы можем определить, какой тип исключения использовать в блоке except, который мы напишем.
В начале трассировки ❶ мы видим, что ошибка произошла в строке 4 файла alice.py. Далее приведена строка кода, вызвавшая ошибку ❷. В остальной части трассировки показан код библиотек, участвовавших в открытии и чтении файлов. Обычно эта часть трассировки при анализе не нужна.
Чтобы решить возникшую проблему, начнем блок try с проблемной строки, указанной в трассировке. В нашем примере это строка, содержащая вызов read_text():
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
❶ except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
В этом примере код блока try выдает исключение FileNotFoundError, поэтому Python ищет блок except для этой ошибки ❶. Затем выполняется код данного блока, в результате чего вместо трассировки выдается более привычное сообщение об ошибке:
Sorry, the file alice.txt does not exist.
Если файл не существует, то программе больше нечего делать, поэтому код обработки ошибок почти ничего в нее не добавляет. Доработаем этот пример и посмотрим, как обработка исключений помогает при работе с несколькими файлами.
Программа может анализировать текстовые файлы, содержащие целые книги. Многие классические произведения, ставшие общественным достоянием, доступны в виде простых текстовых файлов. Тексты, использованные в этом подразделе, взяты с сайта проекта «Гутенберг» (http://gutenberg.org/). На нем хранится подборка литературных произведений, не защищенных авторским правом; это превосходный ресурс для разработчиков, которые собираются использовать литературные тексты в своих программных проектах.
Прочитаем текст «Алисы в Стране чудес» и попробуем подсчитать количество слов в тексте. Мы воспользуемся методом split(), предназначенным для создания списка слов на основе пробельных символов:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Подсчет приблизительного количества строк в файле.
❶ words = contents.split()
❷ num_words = len(words)
print(f"The file {path} has about {num_words} words.")
Я переместил файл alice.txt в правильный каталог, чтобы код в блоке try был выполнен без ошибок. Программа загружает текст в переменную contents, которая теперь содержит весь текст «Алисы в Стране чудес» в виде одной длинной строки, и использует метод split() для получения списка всех слов в книге ❶. Запрашивая длину этого списка ❷ с помощью функции len(), мы получаем неплохое приближенное значение количества слов в исходной строке. Напоследок выводится сообщение с количеством слов, найденных в файле. Этот код помещен в блок else, поскольку он должен выводиться только в случае успешного выполнения блока try.
Выходные данные программы сообщают, сколько слов содержит файл alice.txt:
The file alice.txt has about 29594 words.
Количество слов немного завышено, поскольку в нем учитывается дополнительная информация, добавленная в текстовый файл издателем, но в целом оно довольно точно оценивает длину текста «Алисы в Стране чудес».
Добавим еще несколько файлов с книгами для анализа. Но для начала переместим основной код программы в функцию count_words(). Это упростит проведение анализа для нескольких книг:
word_count.py
from pathlib import Path
def count_words(path):
❶ """Подсчитывает приблизительное количество строк в файле."""
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
path = Path('alice.txt')
count_words(path)
Бо́льшая часть кода не изменилась. Мы просто добавили в код отступ и переместили в тело count_words(). При внесении изменений в программу желательно обновлять комментарии, поэтому мы преобразовали комментарий в строку документации и слегка переформулировали его ❶.
Теперь мы можем написать простой цикл для подсчета слов в любом тексте, который нужно проанализировать. Для этого имена анализируемых файлов сохраняются в списке, после чего для каждого файла в списке вызывается функция count_words(). Мы попробуем подсчитать слова в «Алисе в Стране чудес», «Сиддхартхе», «Моби Дике» и «Маленьких женщинах» — все эти книги находятся в свободном доступе. Я намеренно не стал копировать файл siddhartha.txt в каталог с программой word_count.py, чтобы выяснить, насколько хорошо наша программа справляется с отсутствием файла:
from pathlib import Path
def count_words(filename):
--пропуск--
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
❶ path = Path(filename)
count_words(path)
Имена файлов хранятся в виде простых строк. Перед вызовом метода count_words() каждая строка преобразуется в объект Path ❶. Отсутствие файла siddhartha.txt не влияет на дальнейшее выполнение программы:
The file alice.txt has about 29594 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215864 words.
The file little_women.txt has about 189142 words.
Использование блока try-except в данном примере предоставляет два важных преимущества: программа ограждает пользователя от получения данных трассировки и продолжает выполнение, анализируя тексты, которые ей удается найти. Если бы в программе не перехватывалось исключение FileNotFoundError, инициированное из-за отсутствия siddhartha.txt, то пользователь увидел бы полную трассировку, а работа программы прервалась бы после попытки подсчитать слова в тексте «Сиддхартхи»; до анализа «Моби Дика» или «Маленьких женщин» дело не дошло бы.
В предыдущем примере мы сообщили пользователю о том, что один из файлов оказался недоступен. Тем не менее вы не обязаны сообщать о каждом обнаруженном исключении. Иногда при возникновении исключения программа должна просто проигнорировать сбой и продолжать работу, словно ничего не произошло. Для этого блок try пишется так же, как обычно, но в блоке except вы даете Python явное указание не предпринимать никаких особых действий в случае ошибки. В языке Python существует оператор pass, который сообщает, что в блоке ничего не надо делать:
def count_words(path):
"""Подсчитывает приблизительное количество строк в файле."""
try:
--пропуск--
except FileNotFoundError:
pass
else:
--пропуск--
Единственное отличие этого листинга от предыдущего — оператор pass находится в блоке except. Теперь при возникновении ошибки FileNotFoundError выполняется код в блоке except, но при этом ничего не происходит. Программа не выдает данные трассировки и вообще никакие результаты, указывающие на возникновение ошибки. Пользователи получают данные о количестве слов во всех существующих файлах, однако ничего не знают о том, что какой-то файл не был найден:
The file alice.txt has about 29594 words.
The file moby_dick.txt has about 215864 words.
The file little_women.txt has about 189142 words.
Оператор pass также может служить временным заполнителем. Он напоминает, что в этот конкретный момент выполнения вашей программы вы решили ничего не предпринимать, хотя возможно, что решите сделать что-то позднее. Например, эта программа может записать все имена отсутствующих файлов в файл missing_files.txt. Пользователи не увидят его, но создатель программы сможет прочитать данный файл и разобраться с отсутствующими текстами.
Как определить, в каком случае следует сообщить об ошибке пользователю, а когда можно просто проигнорировать ее незаметно для него? Если пользователь знает, с какими текстами должна работать программа, то, вероятно, предпочтет получить сообщение, объясняющее, почему некоторые тексты были пропущены при анализе. Пользователь ожидает увидеть какие-то результаты, но не знает, какие книги должны быть проанализированы. Возможно, ему и не нужно знать о недоступности каких-то файлов. Лишняя информация сделает вашу программу менее удобной для пользователя. Средства обработки ошибок Python позволяют достаточно точно управлять тем, какой объем информации следует предоставить пользователю.
Хорошо написанный, правильно протестированный код редко содержит внутренние ошибки (например, синтаксические или логические). Но в любой ситуации, в которой ваша программа зависит от внешних факторов (пользовательского ввода, существования файла, доступности сетевого подключения), существует риск возникновения исключения. По мере накопления практического опыта вы начнете видеть, в каких местах программы следует разместить блоки обработки исключений и какой объем информации о возникающих ошибках предоставлять пользователям.
Упражнения
10.6. Сложение. При вводе числовых данных часто встречается типичная проблема: пользователь вводит текст вместо чисел. При попытке преобразовать данные в int происходит исключение ValueError. Напишите программу, которая запрашивает два числа, складывает их и выводит результат. Перехватите исключение ValueError, если какое-либо из входных значений не является числом, и выведите удобное сообщение об ошибке. Протестируйте свою программу: сначала введите два числа, а затем текст вместо одного из чисел.
10.7. Калькулятор. Поместите код из упражнения 10.5 в цикл while, чтобы пользователь мог продолжать вводить числа, даже если допустил ошибку и ввел текст вместо числа.
10.8. Кошки и собаки. Создайте два файла с именами cats.txt и dogs.txt. Сохраните по крайней мере три клички кошек в первом файле и три клички собак во втором. Напишите программу, которая пытается прочитать эти файлы и выводит их содержимое на экран. Поместите свой код в блок try-except в целях перехвата исключения FileNotFoundError и вывода понятного сообщения об отсутствии файла. Переместите один из файлов в другое место файловой системы; убедитесь в том, что код блока except выполняется как положено.
10.9. Ошибки без уведомления. Измените блок except из упражнения 10.7 так, чтобы при отсутствии файла программа продолжала работу, не уведомляя пользователя о проблеме.
10.10. Распространенные слова. Зайдите на сайт проекта «Гутенберг» (http://gutenberg.org/) и найдите несколько книг для анализа. Скачайте текстовые файлы этих произведений или скопируйте текст из браузера в текстовый файл на вашем компьютере.
Чтобы узнать, сколько раз слово или фраза встречается в строке, можно воспользоваться методом count(). Например, следующий код подсчитывает количество вхождений 'row' в строке:
>>> line = "Row, row, row your boat"
>>> line.count('row')
2
>>> line.lower().count('row')
3
Обратите внимание: преобразование строки в нижний регистр с помощью функции lower() позволяет найти все вхождения искомого слова независимо от регистра.
Напишите программу, которая читает файлы из проекта «Гутенберг» и определяет количество вхождений слова 'the' в каждом тексте. Результат будет приближенным, поскольку программа будет учитывать такие слова, как 'then' и 'there'. Попробуйте повторить поиск для строки 'the ' (с пробелом в строке) и посмотрите, насколько уменьшится количество найденных результатов.
Многие ваши программы будут запрашивать у пользователя информацию. Например, он может вводить настройки для компьютерной игры или данные для визуального представления. Чем бы ни занималась ваша программа, информация, введенная пользователем, будет сохраняться в структурах данных (таких как списки или словари). Когда пользователь закрывает программу, введенную им информацию почти всегда следует сохранять на будущее. Простейший способ сохранения данных основан на использовании модуля json.
Модуль json позволяет записывать простые структуры данных Python в строки в формате JSON и загружать данные из файла при следующем запуске программы. Этот модуль также может использоваться для обмена данными между программами Python. Более того, формат данных JSON не привязан к Python, поэтому данные в этом формате можно передавать программам, написанным на многих других языках программирования. Это полезный и универсальный формат, который к тому же легко изучать.
ПРИМЕЧАНИЕ
Формат JSON (JavaScript Object Notation) был изначально разработан для JavaScript. Однако с того времени стал использоваться во многих языках, в том числе Python.
Напишем две программы: одну короткую, сохраняющую набор чисел, и вторую, которая будет считывать эти числа обратно в память. Первая программа использует функцию json.dumps(), а вторая — функцию json.loads().
Функция json.dumps() получает два аргумента: сохраняемые данные и объект файла, используемый для сохранения. В следующем примере json.dumps() используется для сохранения списка чисел:
number_writer.py
from pathlib import Path
import json
numbers = [2, 3, 5, 7, 11, 13]
❶ path = Path('numbers.json')
❷ contents = json.dumps(numbers)
path.write_text(contents)
Программа импортирует модуль json и создает список чисел для работы. Далее мы указываем имя файла, в котором будет храниться список ❶. Обычно для таких файлов принято задавать расширение .json, указывающее, что данные в файле хранятся в формате JSON. Функция json.dumps() ❷ используется для генерации строки, содержащей JSON-представление обрабатываемых данных. Получив эту строку, мы записываем ее в файл с помощью уже известного нам метода write_text().
Программа ничего не выводит, но давайте откроем файл numbers.json и посмотрим на его содержимое. Данные хранятся в формате, очень похожем на код Python:
[2, 3, 5, 7, 11, 13]
А теперь напишем следующую программу, которая использует json.loads() для чтения списка обратно в память:
number_reader.py
from pathlib import Path
import json
❶ path = Path('numbers.json')
❷ contents = path.read_text()
❸ numbers = json.loads(contents)
print(numbers)
Для чтения данных используется тот же файл, в который они были записаны ❶. Поскольку считывается обычный текстовый файл с определенным форматированием, мы можем прочитать его с помощью метода read_text() ❷. Затем мы передаем содержимое файла функции json.loads() ❸. Она принимает строку в формате JSON и возвращает Python-объект (в данном случае список), который мы присваиваем переменной numbers. Наконец, программа выводит прочитанный список. Как видите, это тот же список, который был создан в программе number_writer.py:
[2, 3, 5, 7, 11, 13]
Модуль json позволяет организовать простейший обмен данными между программами.
Сохранение данных с помощью модуля json особенно полезно при работе с данными, сгенерированными пользователем, поскольку при отсутствии сохранения эта информация будет потеряна в случае остановки программы. В следующем примере программа запрашивает у пользователя имя при первом запуске программы и «вспоминает» его при повторных запусках.
Начнем с сохранения имени пользователя:
remember_me.py
from pathlib import Path
import json
❶ username = input("What is your name? ")
❷ path = Path('username.json')
contents = json.dumps(username)
path.write_text(contents)
❸ print(f"We'll remember you when you come back, {username}!")
Программа запрашивает имя пользователя, чтобы сохранить его ❶. Далее собранные данные записываются в файл username.json ❷. Затем выводится сообщение о том, что имя пользователя было сохранено ❸:
What is your name? Eric
We'll remember you when you come back, Eric!
А теперь напишем другую программу, которая приветствует пользователя, имя которого уже было сохранено ранее:
greet_user.py
from pathlib import Path
import json
❶ path = Path('username.json')
contents = path.read_text()
❷ username = json.loads(contents)
print(f"Welcome back, {username}!")
Мы считываем содержимое файла ❶, а затем с помощью функции json.loads() присваиваем извлеченные данные переменной username ❷. После того как данные будут успешно прочитаны, мы можем поприветствовать пользователя по имени:
Welcome back, Eric!
Теперь эти две программы необходимо объединить в файл. Когда пользователь запускает remember_me.py, программа должна извлечь имя пользователя из памяти, если это возможно. В противном случае программа запрашивает имя пользователя и сохраняет его в файле username.json, чтобы вывести в следующий раз. Здесь можно использовать конструкцию try-except, чтобы соответствующим образом среагировать, если файла username.json не существует, но лучше воспользуемся удобным методом из модуля pathlib:
remember_me.py
from pathlib import Path
import json
path = Path('username.json')
❶ if path.exists():
contents = path.read_text()
username = json.loads(contents)
print(f"Welcome back, {username}!")
❷ else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
Для работы с объектами Path доступно множество полезных методов. Метод exists() возвращает True, если файл или папка существует, и False, если нет. Здесь мы используем метод path.exists(), чтобы узнать, сохранено ли имя пользователя ❶. Если файл username.json существует, то мы загружаем имя пользователя и выводим персональное приветствие.
Если файла username.json не существует ❷, то мы запрашиваем имя пользователя и сохраняем введенное им значение. Мы также выводим сообщение о том, что вспомним пользователя в следующий раз, когда он вернется.
Какой бы блок ни выполнялся, результатом является имя пользователя и соответствующее сообщение. При первом запуске программы результат выглядит так:
What is your name? Eric
We'll remember you when you come back, Eric!
Или же так:
Welcome back, Eric!
Такой результат вы увидите, если программа запускается не первый раз. В этом подразделе мы передавали программе одну строку, но программа будет работать с любыми данными, которые можно преобразовать в строку в формате JSON.
Часто возникает типичная ситуация: код работает, но вы понимаете, что его структуру можно усовершенствовать, разбив на функции, каждая из которых решает свою конкретную задачу. Этот процесс называется рефакторингом (или переработкой). Рефакторинг делает ваш код более чистым, понятным и простым для расширения.
В процессе рефакторинга программы remember_me.py мы можем переместить основную часть логики в одну или несколько функций. Главной задачей remember_me.py является вывод приветствия для пользователя, поэтому весь существующий код будет перемещен в функцию greet_user():
remember_me.py
from pathlib import Path
import json
def greet_user():
❶ """Приветствует пользователя по имени."""
path = Path('username.json')
if path.exists():
contents = path.read_text()
username = json.loads(contents)
print(f"Welcome back, {username}!")
else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
greet_user()
Мы используем функцию, поэтому комментарии заменяются строкой документации, которая описывает работу кода в текущей версии ❶. Код становится немного чище, но функция greet_user() не только приветствует пользователя — она загружает хранимое имя пользователя, если оно существует, и запрашивает новое, если имя не было сохранено ранее.
Переработаем функцию greet_user(), чтобы она не решала столько разных задач. Начнем с перемещения кода загрузки хранимого имени пользователя в отдельную функцию:
from pathlib import Path
import json
def get_stored_username(path):
❶ """Получает хранимое имя пользователя, если оно существует."""
if path.exists():
contents = path.read_text()
username = json.loads(contents)
return username
else:
❷ return None
def greet_user():
"""Приветствует пользователя по имени."""
path = Path('username.json')
username = get_stored_username(path)
❸ if username:
print(f"Welcome back, {username}!")
else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
greet_user()
Новая функция get_stored_username() ❶ имеет четкое предназначение, изложенное в строке документации. Она считывает и возвращает сохраненное имя пользователя, если его удается найти. Если пути, переданного функции get_stored_username(), не существует, то функция возвращает None ❷. И это правильно: функция должна возвращать либо ожидаемое значение, либо None. Это позволяет провести простую проверку возвращаемого значения функции. Программа выводит приветствие для пользователя, если попытка получения имени пользователя была успешной ❸; в противном случае программа запрашивает новое имя пользователя.
Из функции greet_user() стоит вынести еще один блок кода. Если имени пользователя не существует, то код запроса нового имени должен размещаться в функции, специализирующейся на решении этой задачи:
from pathlib import Path
import json
def get_stored_username(path):
"""Получает хранимое имя пользователя, если оно существует."""
--пропуск--
def get_new_username(path):
"""Запрашивает новое имя пользователя."""
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
return username
def greet_user():
"""Приветствует пользователя по имени."""
path = Path('username.json')
❶ username = get_stored_username(path)
if username:
print(f"Welcome back, {username}!")
else:
❷ username = get_new_username(path)
print(f"We'll remember you when you come back, {username}!")
greet_user()
Каждая функция в окончательной версии remember_me.py имеет конкретное предназначение. Мы вызываем greet_user(), и эта функция выводит нужное приветствие: либо для уже знакомого, либо для нового пользователя. Для этого интерпретатор вызывает функцию get_stored_username() ❶, которая отвечает только за чтение хранимого имени пользователя (если оно есть). Наконец, функция greet_user() при необходимости вызывает функцию get_new_username() ❷, которая отвечает только за получение нового имени пользователя и его сохранение. Такое «разделение обязанностей» является важнейшим аспектом написания чистого кода, простого в сопровождении и расширении.
Упражнения
10.11. Любимое число. Напишите программу, которая запрашивает у пользователя его любимое число. Воспользуйтесь функцией json.dumps() для сохранения этого числа в файле. Напишите другую программу, которая читает это значение и выводит сообщение: «Я знаю ваше любимое число! Это _____».
10.12. Сохраненное любимое число. Объедините две программы из упражнения 10.11 в файл. Если число уже сохранено, то сообщите его пользователю, а если нет — запросите любимое число пользователя и сохраните в файле. Выполните программу дважды, чтобы убедиться в том, что она работает.
10.13. Словарь пользователя. В программе remember_me.py хранится только один вид данных — имя пользователя. Дополните этот пример, запросив еще два вида информации о пользователе, а затем сохраните все собранные данные в словарь. Запишите его в файл с помощью функции json.dumps() и прочитайте данные с помощью функции json.loads(). Выведите сводку, какие именно данные о пользователе сохранила ваша программа.
10.14. Проверка пользователя. Последняя версия remember_me.py предполагает, что пользователь либо уже ввел свое имя, либо программа выполняется впервые. Ее нужно изменить на тот случай, если текущий пользователь не является тем человеком, который использовал программу последним.
Прежде чем выводить приветствие в greet_user(), спросите пользователя, правильно ли определено его имя. Если ответ будет отрицательным, то вызовите get_new_username() для получения правильного имени пользователя.
В этой главе вы научились работать с файлами. Вы узнали, как прочитать сразу весь файл и как обрабатывать его построчно. Вы научились записывать в файл столько текста, сколько захотите, а также познакомились с исключениями, возникающими в программе, и средствами их обработки. Кроме того, вы узнали, как использовать структуры данных Python для сохранения введенной информации, чтобы пользователю не приходилось раз вводить данные заново при каждом запуске программы.
В главе 11 вы познакомитесь с эффективными способами тестирования вашего кода. Тестирование поможет убедиться в том, что написанный код работает правильно, а также выявит ошибки, внесенные в процессе расширения уже написанных программ.