Вы уже овладели основными навыками, необходимыми для создания хорошо структурированных и удобных в использовании программ; теперь пора подумать о том, как сделать ваши программы еще более удобными и полезными. В этой главе вы научитесь работать с файлами, чтобы ваши программы могли быстро анализировать большие объемы данных.
В этой главе вы научитесь обрабатывать ошибки, чтобы возникновение аномальных ситуаций не приводило к аварийному завершению ваших программ. Мы рассмотрим исключения — специальные объекты, которые создаются для управления ошибками, возникающими во время выполнения программ Python. Также будет описан модуль json, позволяющий сохранять пользовательские данные, чтобы они не терялись при завершении работы программы.
Работа с файлами и сохранение данных упрощают использование ваших программ. Пользователь сам выбирает, какие данные и когда нужно вводить. Он может запустить вашу программу, выполнить некоторую работу, потом закрыть программу и позднее продолжить работу с того момента, на котором он прервался. Умение обрабатывать исключения поможет справиться с такими ситуациями, как отсутствие нужных файлов, а также с другими проблемами, приводящими к сбою программ. Обработка исключений повысит устойчивость ваших программ при работе с некорректными данными — появившимися как из-за случайных ошибок, так и из-за злонамеренных попыток взлома ваших программ. Материал, представленный в этой главе, сделает ваши программы более практичными, удобными и надежными.
Гигантские объемы данных доступны в текстовых файлах. В них могут храниться погодные данные, социально-экономическая информация, литературные произведения и многое другое. Чтение из файла особенно актуально для приложений, предназначенных для анализа данных, но оно также может пригодиться в любой ситуации, требующей анализа или изменения информации, хранящейся в файле. Например, программа может читать содержимое текстового файла и переписывать его с форматированием, рассчитанным на отображение информации в браузере.
Работа с информацией в текстовом файле начинается с чтения данных в память. Вы можете прочитать все содержимое файла или же читать данные по строкам.
Для начала нам понадобится файл с несколькими строками текста. Пусть это будет файл с числом «пи» с точностью до 30 знаков, по 10 знаков на строку:
pi_digits.txt
3.1415926535
8979323846
2643383279
Чтобы опробовать эти примеры, либо введите данные в редакторе и сохраните файл с именем pi_digits.txt, либо загрузите файл из ресурсов книги на странице https://www.nostarch.com/pythoncrashcourse/. Сохраните файл в каталоге, в котором будут храниться программы этой главы.
Следующая программа открывает этот файл, читает его и выводит содержимое на экран:
file_reader.py
with open('pi_digits.txt') as file_object:
. .contents = file_object.read()
. .print(contents)
В первой строке этой программы многое заслуживает вашего внимания. Начнем с функции open(). Чтобы выполнить любые операции с файлом — даже просто вывести его содержимое, — сначала необходимо открыть файл. Функция open() получает один аргумент: имя открываемого файла. Python ищет файл с указанным именем в каталоге, в котором находится файл текущей программы. В данном примере выполняется программа file_reader.py, поэтому Python ищет файл pi_digits.txt в каталоге, в котором хранится file_reader.py. Функция open() возвращает объект, представляющий файл. В данном случае open('pi_digits.txt') возвращает объект, представляющий файл pi_digits.txt. Python сохраняет этот объект в переменной file_object, с которой мы будем работать позднее в программе.
Конструкция с ключевым словом with закрывает файл после того, как надобность в нем отпадет. Обратите внимание: в этой программе есть вызов open(), но нет вызова close(). Файлы можно открывать и закрывать явными вызовами open() и close(); но если из-за ошибки в программе команда close() останется невыполненной, то файл не будет закрыт. На первый взгляд это не страшно, но некорректное закрытие файлов может привести к потере или порче данных. А если функция close() будет вызвана слишком рано, программа попытается работать с закрытым (то есть недоступным) файлом, что приведет к новым ошибкам. Не всегда можно заранее определить, когда нужно закрывать файл, но с приведенной конструкцией Python сделает это за вас. Вам остается лишь открыть файл и работать с ним так, как требуется, надеясь на то, что Python закроет его автоматически в правильный момент.
После того как в программе появится объект, представляющий файл pi_digits.txt, во второй строке программы используется метод read(), который читает все содержимое файла и сохраняет его содержимое в одной длинной строке в переменной contents. При выводе значения contents на экране появляется все содержимое файла:
3.1415926535
8979323846
2643383279
Единственное различие между выводом и исходным файлом — лишняя пустая строка в конце вывода. Откуда она взялась? Метод read() возвращает ее при чтении, если достигнут конец файла. Если вы хотите удалить лишнюю пустую строку, включите вызов rstrip() в команду print:
with open('pi_digits.txt') as file_object:
contents = file_object.read()
. .print(contents.rstrip())
Напомним, что метод rstrip() удаляет все пропуски в конце строки. Теперь вывод точно соответствует содержимому исходного файла:
3.1415926535
8979323846
2643383279
Если передать функции open() простое имя файла, такое как pi_digits.txt, Python ищет файл в том каталоге, в котором находится файл, выполняемый в настоящий момент (то есть файл программы .py).
В некоторых случаях (в зависимости от того, как организованы ваши рабочие файлы) открываемый файл может и не находиться в одном каталоге с файлом программы. Например, файл программы может находиться в каталоге python_work; в каталоге python_work создается другой каталог с именем text_files для текстовых файлов, с которыми работает программа. И хотя папка text_files находится в python_work, простая передача open() имени файла из text_files не подойдет, потому что Python проведет поиск файла в python_work и на этом остановится; поиск не будет продолжен во вложенном каталоге text_files. Чтобы открыть файлы из каталога, отличного от того, в котором хранится файл программы, необходимо указать путь — то есть приказать Python искать файлы в конкретном месте файловой системы.
Так как каталог text_files находится в python_work, для открытия файла из text_files можно воспользоваться относительным путем. Относительный путь приказывает Python искать файлы в каталоге, который задается относительно каталога, в котором находится текущий файл программы. В системе Linux и OS X это выглядит так:
with open('text_files/имя_файла.txt') as file_object:
Эта строка означает, что файл .txt следует искать в каталоге text_files; она предполагает, что каталог text_files находится в python_work (так оно и есть). В системах Windows в путях к файлам вместо слеша (/) используется обратный слеш (\):
with open('text_files\имя_файла.txt') as file_object:
Также можно точно определить местонахождение файла в вашей системе независимо от того, где хранится выполняемая программа. Такие пути называются абсолютными и используются в том случае, если относительный путь не работает. Например, если каталог text_files находится не в python_work, а в другом каталоге (скажем, в каталоге с именем other_files), то передать open() путь 'text_files/filename.txt' не получится, потому что Python будет искать указанный каталог только внутри python_work. Чтобы объяснить Python, где следует искать файл, необходимо записать полный путь.
Абсолютные пути обычно длиннее относительных, поэтому их лучше сохранять в переменных, которые затем передаются open(). В Linux и OS X абсолютные пути выглядят так:
file_path = '/home/ehmatthes/other_files/text_files/имя_файла.txt'
with open(file_path) as file_object:
В Windows они выглядят так:
file_path = 'C:\Users\ehmatthes\other_files\text_files\имя_файла.txt'
with open(file_path) as file_object:
С абсолютными путями вы сможете читать файлы из любого каталога вашей системы. Пока будет проще хранить файлы в одном каталоге с файлами программ или в каталогах, вложенных в каталог с файлами программ (таких как text_files из рассмотренного примера).
примечание
Иногда в системах семейства Windows слеш в пути к файлам интерпретируется правильно. Если вы используете Windows, но не получаете ожидаемых результатов, попробуйте использовать символы обратного слеша.
В процессе чтения файла часто бывает нужно обработать каждую строку. Возможно, вы ищете некую информацию в файле или собираетесь каким-то образом изменить текст, например при чтении файла с метеорологическими данными вы обрабатываете каждую строку, у которой в описании погоды встречается слово «солнечно». Или, допустим, в новостях вы ищете каждую строку с тегом заголовка и заменяете ее специальными элементами форматирования.
Для последовательной обработки каждой строки в файле можно воспользоваться циклом for:
file_reader.py
(1) filename = 'pi_digits.txt'
(2)with open(filename) as file_object:
(3) . .for line in file_object:
. . print(line)
В точке (1) имя файла, из которого читается информация, сохраняется в переменной filename. Это стандартный прием при работе с файлами: так как переменная filename не представляет конкретный файл (это всего лишь строка, которая сообщает Python, где найти файл), вы сможете легко заменить 'pi_digits.txt' именем другого файла, с которым вы собираетесь работать. После вызова open() объект, представляющий файл и его содержимое, сохраняется в переменной file_object (2). Мы снова используем синтаксис with, чтобы поручить Python открывать и закрывать файл в нужный момент. Для просмотра содержимого все строки файла перебираются в цикле for по объекту файла (3).
На этот раз пустых строк оказывается еще больше:
3.1415926535
8979323846
2643383279
Пустые строки появляются из-за того, что каждая строка в текстовом файле завершается невидимым символом новой строки. Команда print добавляет свой символ новой строки при каждом вызове, поэтому в результате каждая строка завершается двумя символами новой строки: один прочитан из файла, а другой добавлен командой print. Вызов rstrip() в команде print удаляет лишние пустые строки:
filename = 'pi_digits.txt'
with open(filename) as file_object:
for line in file_object:
. . . .print(line.rstrip())
Теперь вывод снова соответствует содержимому файла:
3.1415926535
8979323846
2643383279
При использовании with объект файла, возвращаемый вызовом open(), доступен только в пределах содержащего его блока with. Если вы хотите, чтобы содержимое файла оставалось доступным за пределами блока with, сохраните строки файла в списке внутри блока и в дальнейшем работайте с полученным списком. Одни части файла можно обработать немедленно и отложить другие для обработки в будущем.
В следующем примере строки pi_digits.txt сохраняются в списке в блоке with, после чего выводятся за пределами этого блока:
filename = 'pi_digits.txt'
with open(filename) as file_object:
(1) . .lines = file_object.readlines()
(2)for line in lines:
. .print(line.rstrip())
В точке (1) метод readlines() последовательно читает каждую строку из файла и сохраняет ее в списке. Список сохраняется в переменной lines, с которой можно продолжить работу после завершения блока with. В точке (2) в простом цикле for выводятся все элементы списка lines. Так как каждый элемент lines соответствует ровно одной строке файла, вывод точно соответствует его содержимому.
После того как файл будет прочитан в память, вы сможете обрабатывать данные так, как считаете нужным. Для начала попробуем построить одну строку со всеми цифрами из файла без промежуточных пропусков:
pi_string.py
filename = 'pi_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
(1) pi_string = ''
(2)for line in lines:
. .pi_string += line.rstrip()
. .
(3)print(pi_string)
print(len(pi_string))
Сначала программа открывает файл и сохраняет каждую строку цифр в списке — точно так же, как это делалось в предыдущем примере. В точке (1) создается переменная pi_string для хранения цифр числа «пи». Далее следует цикл, который добавляет к pi_string каждую серию цифр, из которой удаляется символ новой строки (2). В точке (3) программа выводит строку и ее длину:
3.1415926535 8979323846 2643383279
36
Переменная pi_string содержит пропуски, которые присутствовали в начале каждой строки цифр. Чтобы удалить их, достаточно использовать strip() вместо rstrip():
filename = 'pi_30_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
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
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.strip()
. .
print(pi_string[:52] + "...")
print(len(pi_string))
Из выходных данных видно, что строка действительно содержит значение «пи» с точностью до 1 000 000 знаков:
3.14159265358979323846264338327950288419716939937510...
1000002
Python не устанавливает никаких ограничений на длину данных, с которыми вы можете работать. Она ограничивается разве что объемом памяти вашей системы.
примечание
Для запуска этой программы (и многих других примеров, приведенных ниже) необходимо загрузить ресурсы по адресу https://www.nostarch.com/pythoncrashcourse/.
Меня всегда интересовало, не встречается ли мой день рождения среди цифр числа «пи»? Воспользуемся только что созданной программой для проверки того, входит ли запись дня рождения пользователя в первый миллион цифр. Для этого можно записать день рождения в виде строки из цифр и посмотреть, присутствует ли эта строка в pi_string:
filename = 'pi_million_digits.txt'
with open(filename) as file_object:
lines = file_object.readlines()
pi_string = ''
for line in lines:
pi_string += line.rstrip()
(1) birthday = input("Enter your birthday, in the form mmddyy: ")
(2)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.")
В точке (1) программа запрашивает день рождения пользователя, а затем в точке (2) проверяет вхождение этой строки в pi_string. Пробуем:
Enter your birthdate, in the form mmddyy: 120372
Your birthday appears in the first million digits of pi!
Оказывается, мой день рождения встречается среди цифр «пи»! После того как данные будут прочитаны из файла, вы сможете делать с ними все, что сочтете нужным.
Упражнения
10-1. Изучение Python: откройте пустой файл в текстовом редакторе и напишите несколько строк текста о возможностях Python. Каждая строка должна начинаться с фразы: «In Python you can…» Сохраните файл под именем learning_python.txt в каталоге, использованном для примеров этой главы. Напишите программу, которая читает файл и выводит текст три раза: с чтением всего файла, с перебором строк объекта файла и с сохранением строк в списке с последующим выводом списка вне блока with.
10-2. Изучение C: метод replace() может использоваться для замены любого слова в строке другим словом. В следующем примере слово ‘dog’ заменяется словом ‘cat’:
>>> message = "I really like dogs."
>>> message.replace('dog', 'cat')
'I really like cats.'
Прочитайте каждую строку из только что созданного файла learning_python.txt и замените слово Python названием другого языка, например C. Выведите каждую измененную строку на экран.
Один из простейших способов сохранения данных — запись в файл. Текст, записанный в файл, останется доступным и после закрытия терминала с выводом вашей программы. Вы сможете проанализировать результаты после завершения программы или передать свои файлы другим. Вы также сможете написать программы, которые снова читают сохраненный текст в память и работают с ним.
Чтобы записать текст в файл, необходимо вызвать open() со вторым аргументом, который сообщает Python, что вы собираетесь записывать данные в файл. Чтобы увидеть, как это делается, напишем простое сообщение и сохраним его в файле (вместо того чтобы просто вывести на экран):
write_message.py
filename = 'programming.txt'
(1) with open(filename, 'w') as file_object:
(2) . .file_object.write("I love programming.")
При вызове open() в этом примере передаются два аргумента (1) . Первый аргумент, как и прежде, содержит имя открываемого файла. Второй аргумент 'w' сообщает Python, что файл должен быть открыт в режиме записи. Файлы можно открывать в режиме чтения ('r'), записи ('w'), присоединения ('a') или в режиме, допускающем как чтение, так и запись в файл ('r+'). Если аргумент режима не указан, Python по умолчанию открывает файл в режиме только для чтения.
Если файл, открываемый для записи, еще не существует, функция open() автоматически создает его. Будьте внимательны, открывая файл в режиме записи ('w'): если файл существует, то Python уничтожит его данные перед возвращением объекта файла.
В точке (2) метод write() используется с объектом файла для записи строки в файл. Программа не выводит данные на терминал, но, открыв файл programming.txt, вы увидите в нем одну строку:
programming.txt
I love programming.
Этот файл ничем не отличается от любого другого текстового файла на вашем компьютере. Его можно открыть, записать в него новый текст, скопировать/вставить текст и т.д.
примечание
Python может записывать в текстовые файлы только строковые данные. Если вы захотите сохранить в текстовом файле числовую информацию, данные придется предварительно преобразовать в строки функцией str().
Функция write() не добавляет символы новой строки в записываемый текст. А это означает, что если вы записываете сразу несколько строк без включения символов новой строки, полученный файл может выглядеть не так, как вы рассчитывали:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
file_object.write("I love programming.")
. .file_object.write("I love creating new games.")
Открыв файл programming.txt, вы увидите, что две строки «склеились»:
I love programming.I love creating new games.
Если включить символы новой строки в команды write(), текст будет состоять из двух строк:
filename = 'programming.txt'
with open(filename, 'w') as file_object:
. .file_object.write("I love programming.\n")
. .file_object.write("I love creating new games.\n")
Результат выглядит так:
I love programming.
I love creating new games.
Для форматирования вывода также можно использовать пробелы, символы табуляции и пустые строки по аналогии с тем, как это делалось с выводом на терминал.
Если вы хотите добавить в файл новые данные вместо того, чтобы перезаписывать существующее содержимое, откройте файл в режиме присоединения. В этом случае Python не уничтожает содержимое файла перед возвращением объекта файла. Все строки, выводимые в файл, будут добавлены в конец файла. Если файл еще не существует, то Python автоматически создаст пустой файл.
Изменим программу write_message.py и дополним существующий файл programming.txt новыми данными:
write_message.py
filename = 'programming.txt'
(1) with open(filename, 'a') as file_object:
(2) . .file_object.write("I also love finding meaning in large datasets.\n")
. .file_object.write("I love creating apps that can run in a browser.\n")
В точке (1) аргумент 'a' используется для открытия файла в режиме присоединения (вместо перезаписи существующего файла). В точке (2) записываются две новые строки, которые добавляются к содержимому programming.txt:
programming.txt
I love programming.
I love creating new games.
I also love finding meaning in large datasets.
I love creating apps that can run in a browser.
В результате к исходному содержимому файла добавляется новый текст.
Упражнения
10-3. Гость: напишите программу, которая запрашивает у пользователя его имя. Введенный ответ сохраняется в файле с именем guest.txt.
10-4. Гостевая книга: напишите цикл while, который в цикле запрашивает у пользователей имена. При вводе каждого имени выведите на экран приветствие и добавьте строку с сообщением в файл с именем guest_book.txt. Проследите за тем, чтобы каждое сообщение размещалось в отдельной строке файла.
10-5. Опрос: напишите цикл while, в котором программа спрашивает у пользователя, почему ему нравится программировать. Каждый раз, когда пользователь вводит очередную причину, сохраните текст его ответа в файле.
Для управления ошибками, возникающими в ходе выполнения программы, в Python используются специальные объекты, называемые исключениями. Если при возникновении ошибки Python не знает, что делать дальше, создается объект исключения. Если в программу включен код обработки исключения, то выполнение программы продолжится, а если нет — программа останавливается и выводит трассировку с отчетом об исключении.
Исключения обрабатываются в блоках try-except. Блок try-except приказывает Python выполнить некоторые действия, но при этом также сообщает, что нужно делать при возникновении исключения. С блоками try-except ваши программы будут работать даже в том случае, если что-то пошло не так. Вместо невразумительной трассировки выводится понятное сообщение об ошибке, которое вы определяете в программе.
Рассмотрим простую ошибку, при которой Python инициирует исключение. Конечно, вы знаете, что деление на ноль невозможно, но мы все же прикажем Python выполнить эту операцию:
division.py
print(5/0)
Из этого ничего не выйдет, поэтому на экран выводятся данные трассировки:
Traceback (most recent call last):
File "division.py", line 1, in
. .print(5/0)
(1) ZeroDivisionError: division by zero
Ошибка, упоминаемая в трассировке (1) — 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. Если код в блоке try выполнен успешно, то Python пропускает блок except. Если код в блоке try порождает ошибку, то Python ищет блок except с соответствующей ошибкой и выпускает код в этом блоке.
В этом примере код блока try порождает ошибку ZeroDivisionError, поэтому Python ищет блок except с описанием того, как следует действовать в такой ситуации. При выполнении кода этого блока пользователь видит понятное сообщение об ошибке вместо данных трассировки:
You can't divide by zero!
Если бы за кодом try-except следовал другой код, то выполнение программы продолжилось, потому что мы объяснили Python, как обрабатывать эту ошибку. В следующем примере обработка ошибки позволяет программе продолжить выполнение.
Правильная обработка ошибок особенно важна в том случае, если программа должна продолжить работу после возникновения ошибки. Такая ситуация часто встречается в программах, запрашивающих данные у пользователя. Если программа правильно среагировала на некорректный ввод, она может запросить новые данные после сбоя.
Создадим простой калькулятор, который выполняет только операцию деления:
division.py
print("Give me two numbers, and I'll divide them.")
print("Enter 'q' to quit.")
while True:
(1) . .first_number = input("\nFirst number: ")
. .if first_number == 'q':
. . . .break
(2) . .second_number = input("Second number: ")
. .if second_number == 'q':
. . . .break
(3) . .answer = int(first_number) / int(second_number)
. .print(answer)
Программа запрашивает у пользователя первое число first_number (1) , а затем, если пользователь не ввел q для завершения работы, запрашивает второе число second_number (2). Далее одно число делится на другое для получения результата answer (3). Программа никак не обрабатывает ошибки, так что попытка деления на ноль приводит к ее аварийному завершению:
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.py", line 9, in
. .answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
Конечно, аварийное завершение — это плохо, но еще хуже, если пользователь увидит данные трассировки. Неопытного пользователя они собьют с толку, а при сознательной попытке взлома злоумышленник сможет получить из них больше информации, чем вам хотелось бы. Например, он узнает имя файла программы и увидит некорректно работающую часть кода. На основании этой информации опытный хакер иногда может определить, какие атаки следует применять против вашего кода.
Для повышения устойчивости программы к ошибкам можно заключить строку, выдающую ошибки, в блок try-except. Ошибка происходит в строке, выполняющей деление; следовательно, именно эту строку следует заключить в блок try-except.
Данный пример также включает блок else. Любой код, зависящий от успешного выполнения блока try, размещается в блоке else:
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: ")
(1) . .try:
answer = int(first_number) / int(second_number)
(2) . .except ZeroDivisionError:
. . . .print("You can't divide by 0!")
(3) . .else:
print(answer)
Программа пытается выполнить операцию деления в блоке try (1) , который включает только код, способный породить ошибку. Любой код, зависящий от успешного выполнения блока try, добавляется в блок else. В данном случае, если операция деления выполняется успешно, блок else используется для вывода результата (2).
Блок except сообщает Python, как следует поступать при возникновении ошибки ZeroDivisionError (3). Если при выполнении команды из блока try происходит ошибка, связанная с делением на 0, программа выводит понятное сообщение, которое объясняет пользователю, как избежать подобных ошибок. Выполнение программы продолжается, и пользователь не сталкивается с трассировкой:
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-except-else работает так: Python пытается выполнить код в блоке try. В блоках try следует размещать только тот код, при выполнении которого может возникнуть исключение. Иногда некоторый код должен выполняться только в том случае, если выполнение try прошло успешно; такой код размещается в блоке else. Блок except сообщает Python, что делать, если при выполнении кода try произошло определенное исключение. Заранее определяя вероятные источники ошибок, вы повышаете надежность своих программ, которые продолжают работать даже при вводе некорректных данных или при недоступности ресурсов. Ваш код оказывается защищенным от случайных ошибок пользователей и сознательных атак.
Одна из стандартных проблем при работе с файлами — отсутствие необходимых файлов. Тот файл, который вам нужен, может находиться в другом месте, в имени файла может быть допущена ошибка, или файл может вообще не существовать. Все эти ситуации достаточно прямолинейно обрабатываются в блоках try-except.
Попробуем прочитать данные из несуществующего файла. Следующая программа пытается прочитать содержимое файла с текстом «Алисы в Стране чудес», но я не сохранил файл alice.txt в одном каталоге с файлом alice.py:
alice.py
filename = 'alice.txt'
with open(filename) as f_obj:
. .contents = f_obj.read()
Прочитать данные из несуществующего файла нельзя, поэтому Python выдает исключение:
Traceback (most recent call last):
File "alice.py", line 3, in
. .with open(filename) as f_obj:
FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
В последней строке трассировки упоминается FileNotFoundError: это исключение выдается в том случае, если Python не может найти открываемый файл. В данном примере функция open() порождает ошибку, и, чтобы обработать ее, блок try начинается перед строкой с вызовом open():
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
. .msg = "Sorry, the file " + filename + " does not exist."
. .print(msg)
В этом примере код блока try выдает исключение FileNotFoundError, поэтому Python ищет блок except для этой ошибки. Затем выполняется код этого блока, в результате чего вместо трассировки выдается доступное сообщение об ошибке:
Sorry, the file alice.txt does not exist.
Если файл не существует, программе больше делать нечего, поэтому код обработки ошибок почти ничего не добавляет в эту программу. Доработаем этот пример и посмотрим, как обработка исключений помогает при работе с несколькими файлами.
Программа может анализировать текстовые файлы, содержащие целые книги. Многие классические произведения, ставшие общественным достоянием, доступны в виде простых текстовых файлов. Тексты, использованные в этом разделе, взяты с сайта проекта «Гутенберг» (http://gutenberg.org/). На этом сайте хранится подборка литературных произведений, не защищенных авторским правом; это превосходный ресурс для разработчиков, которые собираются работать с литературными текстами в своих программных проектах.
Прочитаем текст «Алисы в Стране чудес» и попробуем подсчитать количество слов в тексте. Мы воспользуемся методом split(), предназначенным для построения списка слов на основе строки. Вот как метод split() работает со строкой, содержащей только название книги:
>>> title = "Alice in Wonderland"
>>> title.split()
['Alice', 'in', 'Wonderland']
Метод split() разделяет строку на части по всем позициям, в которых обнаружит пробел, и сохраняет все части строки в элементах списка. В результате создается список слов, входящих в строку (впрочем, вместе с некоторыми словами могут храниться знаки препинания). Для подсчета слов в книге мы воспользуемся вызовом split() для всего текста, а затем подсчитаем элементы списка, чтобы получить примерное количество слов в тексте:
filename = 'alice.txt'
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
. .# Подсчет приблизительного количества строк в файле.
(1) . .words = contents.split()
(2) . .num_words = len(words)
(3) . .print("The file " + filename + " has about " + str(num_words) + " words.")
Затем я переместил файл alice.txt в правильный каталог, чтобы код в блоке try был выполнен без ошибок. В точке (1) программа загружает текст в переменную contents, которая теперь содержит весь текст в виде одной длинной строки и использует метод split() для получения списка всех слов в книге. Запрашивая длину этого списка при помощи функции len(), мы получаем неплохое приближенное значение количества слов в исходной строке (2). В точке (3) выводится сообщение с количеством слов, найденных в файле. Этот код помещен в блок else, потому что он должен выводиться только в случае успешного выполнения блока try. Выходные данные программы сообщают, сколько слов содержит файл alice.txt:
The file alice.txt has about 29461 words.
Количество слов немного завышено, потому что в нем учитывается дополнительная информация, включенная в текстовый файл издателем, но в целом оно довольно точно оценивает длину «Алисы в Стране чудес».
Добавим еще несколько файлов с книгами для анализа. Но для начала переместим основной код программы в функцию с именем count_words(). Это упростит проведение анализа для нескольких книг:
word_count.py
def count_words(filename):
(1) . ."""Подсчет приблизительного количества строк в файле."""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# Подсчет приблизительного количества строк в файле.
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) +
" words.")
filename = 'alice.txt'
count_words(filename)
Бульшая часть кода не изменилась. Мы просто снабдили код отступом и переместили его в тело count_words(). Кроме того, комментарий был преобразован в строку документации (1) .
Теперь мы можем написать простой цикл для подсчета слов в любом тексте, который нужно проанализировать. Для этого имена анализируемых файлов сохраняются в списке, после чего для каждого файла в списке вызывается функция count_words(). Мы попробуем подсчитать слова в «Алисе в Стране чудес», «Сиддхартхе», «Моби Дике» и «Маленьких женщинах» — все эти книги есть в свободном доступе. Я намеренно не стал копировать файл siddhartha.txt в каталог с программой word_count.py, чтобы выяснить, насколько хорошо наша программа справляется с отсутствием файла:
def count_words(filename):
...
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
. .count_words(filename)
Отсутствие файла siddhartha.txt не влияет на выполнение программы:
The file alice.txt has about 29461 words.
Sorry, the file siddhartha.txt does not exist.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
Использование блока try-except в этом примере дает два важных преимущества: программа ограждает пользователя от вывода трассировки и продолжает выполнение, анализируя тексты, которые ей удается найти. Если бы в программе не перехватывалось исключение FileNotFoundError, инициированное из-за отсутствия siddhartha.txt, то пользователь увидел бы полную трассировку, а работа программы прервалась бы после попытки подсчитать слова в тексте «Сиддхартхи»; до анализа «Моби Дика» или «Маленьких женщин» дело не дошло бы.
В предыдущем примере мы сообщили пользователю о том, что один из файлов оказался недоступен. Тем не менее вы не обязаны сообщать о каждом обнаруженном исключении. Иногда при возникновении исключения программа должна просто проигнорировать сбой и продолжать работу, словно ничего не произошло. Для этого блок try пишется так же, как обычно, но в блоке except вы явно приказываете Python не предпринимать никаких особых действий в случае ошибки. В языке Python существует команда pass, с которой блок ничего не делает:
def count_words(filename):
"""Подсчет приблизительного количества строк в файле."""
try:
...
except FileNotFoundError:
(1) . . . .pass
else:
...
filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt']
for filename in filenames:
count_words(filename)
Единственное отличие этого листинга от предыдущего — команда pass в точке (1) . Теперь при возникновении ошибки FileNotFoundError выполняется код в блоке except, но при этом ничего не происходит. Программа не выдает данные трассировки и вообще никакие результаты, указывающие на возникновение ошибки. Пользователи получают данные о количестве слов во всех существующих файлах, однако ничто не сообщает о том, что какой-то файл не был найден:
The file alice.txt has about 29461 words.
The file moby_dick.txt has about 215136 words.
The file little_women.txt has about 189079 words.
Команда pass также может служить временным заполнителем. Она напоминает, что в этот конкретный момент выполнения вашей программы вы решили ничего не предпринимать, хотя, возможно, сделаете что-то позднее. Например, эта программа может записать все имена отсутствующих файлов в файл с именем missing_files.txt. Пользователи этот файл не увидят, но создатель программы сможет прочитать его и разобраться с отсутствующими текстами.
Как определить, в каком случае следует сообщить об ошибке пользователю, а когда можно просто проигнорировать ее незаметно для пользователя? Если пользователь знает, с какими текстами должна работать программа, вероятно, он предпочтет получить сообщение, объясняющее, почему некоторые тексты были пропущены при анализе. Пользователь ожидает увидеть какие-то результаты, но не знает, какие книги должны быть проанализированы? Возможно, ему и не нужно знать о недоступности каких-то файлов. Лишняя информация только сделает вашу программу менее удобной для пользователя. Средства обработки ошибок Python позволяют достаточно точно управлять тем, какой объем информации следует предоставить пользователю.
Хорошо написанный, правильно протестированный код редко содержит внутренние ошибки (например, синтаксические или логические). Но в любой ситуации, в которой ваша программа зависит от внешних факторов (пользовательского ввода, существования файла, доступности сетевого подключения), существует риск возникновения исключения. С накоплением практического опыта вы начнете видеть, в каких местах программы следует разместить блоки обработки исключений и сколько информации предоставлять пользователям о возникающих ошибках.
Упражнения
10-6. Сложение: при вводе числовых данных часто встречается типичная проблема: пользователь вводит текст вместо чисел. При попытке преобразовать данные в int происходит исключение TypeError. Напишите программу, которая запрашивает два числа, складывает их и выводит результат. Перехватите исключение TypeError, если какое-либо из входных значений не является числом, и выведите удобное сообщение об ошибке. Протестируйте свою программу: сначала введите два числа, а потом введите текст вместо одного из чисел.
10-7. Калькулятор: заключите код из упражнения 10-6 в цикл while, чтобы пользователь мог продолжать вводить числа, даже если он допустил ошибку и ввел текст вместо числа.
10-8. Кошки и собаки: создайте два файла с именами cats.txt и dogs.txt. Сохраните минимум три клички кошек в первом файле и три клички собак во втором. Напишите программу, которая пытается прочитать эти файлы и выводит их содержимое на экран. Заключите свой код в блок try-except для перехвата исключения FileNotFoundError и вывода понятного сообщения об отсутствии файла. Переместите один из файлов в другое место файловой системы; убедитесь в том, что код блока except выполняется, как и положено.
10-9. Ошибки без уведомления: измените блок except из упражнения 10-8 так, чтобы при отсутствии файла программа продолжала работу, не уведомляя пользователя о проблеме.
10-10. Частые слова: зайдите на сайт проекта «Гутенберг» (http://gutenberg.org/) и найдите несколько книг для анализа. Загрузите текстовые файлы этих произведений или скопируйте текст из браузера в текстовый файл на вашем компьютере.
Для подсчета количества вхождений слова или выражения в строку можно воспользоваться методом count(). Например, следующий код подсчитывает количество вхождений ‘row’ в строке:
>>> line = "Row, row, row your boat"
>>> line.count('row')
2
>>> line.lower().count('row')
3
Обратите внимание: преобразование строки к нижнему регистру функцией lower() позволяет найти все вхождения искомого слова независимо от регистра.
Напишите программу, которая читает файлы из проекта «Гутенберг» и определяет количество вхождений слова ‘the’ в каждом тексте.
Многие ваши программы будут запрашивать у пользователя информацию. Например, пользователь может вводить настройки для компьютерной игры или данные для визуального представления. Чем бы ни занималась ваша программа, информация, введенная пользователем, будет сохраняться в структурах данных (таких, как списки или словари). Когда пользователь закрывает программу, введенную им информацию почти всегда следует сохранять на будущее. Простейший способ сохранения данных основан на использовании модуля json.
Модуль json обеспечивает запись простых структур данных Python в файл и загрузку данных из файла при следующем запуске программы. Модуль json также может использоваться для обмена данными между программами Python. Более того, формат данных JSON не привязан к Python, поэтому данные в этом формате можно передавать программам, написанным на многих других языках программирования. Это полезный и универсальный формат, который к тому же легко изучается.
примечание
Формат JSON (JavaScript Object Notation) был изначально разработан для JavaScript. Впрочем, с того времени он стал использоваться во многих языках, включая Python.
Напишем короткую программу для сохранения набора чисел и другую программу, которая будет читать эти числа обратно в память. Первая программа использует функцию json.dump(), а вторая — функцию json.load().
Функция json.dump() получает два аргумента: сохраняемые данные и объект файла, используемый для сохранения. В следующем примере json.dump() используется для сохранения списка чисел:
number_writer.py
import json
numbers = [2, 3, 5, 7, 11, 13]
(1) filename = 'numbers.json'
(2)with open(filename, 'w') as f_obj:
(3) . .json.dump(numbers, f_obj)
Программа импортирует модуль json и создает список чисел для работы. В точке (1) выбирается имя файла для хранения списка. Обычно для таких файлов принято использовать расширение .json, указывающее, что данные в файле хранятся в формате JSON. Затем файл открывается в режиме записи, чтобы модуль json мог записать в него данные (2). В точке (3) функция json.dump() используется для сохранения списка numbers в файле numbers.json.
Программа ничего не выводит, но давайте откроем файл numbers.json и посмотрим на его содержимое. Данные хранятся в формате, очень похожем на код Python:
[2, 3, 5, 7, 11, 13]
А теперь напишем следующую программу, которая использует json.load() для чтения списка обратно в память:
number_reader.py
import json
(1) filename = 'numbers.json'
(2)with open(filename) as f_obj:
(3) . .numbers = json.load(f_obj)
. .
print(numbers)
В точке (1) для чтения данных используется тот же файл, в который эти данные были записаны. На этот раз файл открывается в режиме чтения, потому что Python нужно только прочитать данные из файла (2). В точке (3) функция json.load() используется для загрузки информации из numbers.json; эта информация сохраняется в переменной numbers.
Наконец, программа выводит прочитанный список. Как видите, этот тот же список, который был создан в программе number_writer.py:
[2, 3, 5, 7, 11, 13]
Модуль json позволяет организовать простейший обмен данными между программами.
Сохранение с использованием модуля json особенно полезно при работе с данными, сгенерированными пользователем, потому что без сохранения эта информация будет потеряна при остановке программы. В следующем примере программа запрашивает у пользователя имя при первом запуске программы и «вспоминает» его при повторных запусках.
Начнем с сохранения имени пользователя:
remember_me.py
import json
(1) username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
(2) . .json.dump(username, f_obj)
(3) . .print("We'll remember you when you come back, " + username + "!")
В точке (1) программа запрашивает имя пользователя для сохранения. Затем вызывается функция json.dump(), которой передается имя пользователя и объект файла; функция сохраняет имя пользователя в файле (2). Далее выводится сообщение о том, что имя пользователя было сохранено (3):
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_obj:
(1) . .username = json.load(f_obj)
(2) . .print("Welcome back, " + username + "!")
В точке (1) вызов json.load() читает информацию из файла username.json в переменную username. После того как данные будут успешно прочитаны, мы можем поприветствовать пользователя по имени (2):
Welcome back, Eric!
Теперь эти две программы необходимо объединить в один файл. Когда пользователь запускает remember_me.py, программа должна взять имя пользователя из памяти, если это возможно; соответственно, программа начинается с блока try, который пытается прочитать имя пользователя. Если файл username.json не существует, блок except запросит имя пользователя и сохранит его в username.json на будущее:
remember_me.py
import json
# Программа загружает имя пользователя, если оно было сохранено ранее.
# В противном случае она запрашивает имя пользователя и сохраняет его.
filename = 'username.json'
try:
(1) . .with open(filename) as f_obj:
(2) . . . .username = json.load(f_obj)
(3)except FileNotFoundError:
(4) . .username = input("What is your name? ")
(5) . .with open(filename, 'w') as f_obj:
. . . .json.dump(username, f_obj)
. . . .print("We'll remember you when you come back, " + username + "!")
else:
. .print("Welcome back, " + username + "!")
Никакого нового кода здесь нет; просто блоки кода из двух предыдущих примеров были объединены в один файл. В точке (1) программа пытается открыть файл username.json. Если файл существует, программа читает имя пользователя в память (2) и выводит сообщение, приветствующее пользователя, в блоке else. Если программа запускается впервые, то файл username.json не существует, и происходит исключение FileNotFoundError (3). Python переходит к блоку except, в котором пользователю предлагается ввести имя (4). Затем программа вызывает json.dump() для сохранения имени пользователя и выводит приветствие (5).
Какой бы блок ни выполнялся, результатом является имя пользователя и соответствующее сообщение. При первом запуске программы результат выглядит так:
What is your name? Eric
We'll remember you when you come back, Eric!
Если же программа уже была выполнена хотя бы один раз, результат будет таким:
Welcome back, Eric!
Часто возникает типичная ситуация: код работает, но вы понимаете, что его структуру можно усовершенствовать, разбив его на функции, каждая из которых решает свою конкретную задачу. Этот процесс называется рефакторингом (или переработкой). Рефакторинг делает ваш код более чистым, понятным и простым в расширении.
В процессе рефакторинга remember_me.py мы можем переместить основную часть логики в одну или несколько функций. Основной задачей remember_me.py является вывод приветствия для пользователя, поэтому весь существующий код будет перемещен в функцию greet_user():
remember_me.py
import json
def greet_user():
(1) . ."""Приветствует пользователя по имени."""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")
greet_user()
С переходом на функцию комментарии дополняются строкой документации, которая описывает работу кода в текущей версии (1) . Код становится немного чище, но функция greet_user() не только приветствует пользователя — она также загружает хранимое имя пользователя, если оно существует, и запрашивает новое имя, если оно не было сохранено ранее.
Переработаем функцию greet_user(), чтобы она не решала столько разных задач. Начнем с перемещения кода загрузки хранимого имени пользователя в отдельную функцию:
import json
def get_stored_username():
(1) . ."""Получает хранимое имя пользователя, если оно существует."""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
(2) . . . .return None
. .else:
. . . .return username
def greet_user():
"""Приветствует пользователя по имени."""
. .username = get_stored_username()
(3) . .if username:
. . . .print("Welcome back, " + username + "!")
. .else:
username = input("What is your name? ")
filename = 'username.json'
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
greet_user()
Новая функция get_stored_username() имеет четкое предназначение, изложенное в строке документации (1) . Эта функция читает и возвращает сохраненное имя пользователя, если его удается найти. Если файл username.json не существует, то функция возвращает None (2). И это правильно: функция должна возвращать либо ожидаемое значение, либо None. Это позволяет провести простую проверку возвращаемого значения функции. В точке (3) программа выводит приветствие для пользователя, если попытка получения имени пользователя была успешной; в противном случае программа запрашивает новое имя пользователя.
Из функции 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_obj:
json.dump(username, f_obj)
. .return username
def greet_user():
"""Приветствует пользователя по имени."""
username = get_stored_username()
if username:
print("Welcome back, " + username + "!")
else:
. . . .username = get_new_username()
print("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.dump() для сохранения этого числа в файле. Напишите другую программу, которая читает это значение и выводит сообщение: «Я знаю ваше любимое число! Это _____».
10-12. Сохраненное любимое число: объедините две программы из упражнения 10-11 в один файл. Если число уже сохранено, сообщите его пользователю, а если нет — запросите любимое число пользователя и сохраните в файле. Выполните программу дважды, чтобы убедиться в том, что она работает.
10-13. Проверка пользователя: последняя версия remember_me.py предполагает, что пользователь либо уже ввел свое имя, либо программа выполняется впервые. Ее нужно изменить на тот случай, если текущий пользователь не является тем человеком, который последним использовал программу.
Прежде чем выводить приветствие в greet_user(), спросите пользователя, правильно ли определено имя пользователя. Если ответ будет отрицательным, вызовите get_new_username() для получения правильного имени пользователя.
В этой главе вы научились работать с файлами. Вы узнали, как прочитать сразу весь файл и как читать его содержимое по строкам. Вы научились записывать в файл и присоединять текст в конец файла, познакомились с исключениями и средствами обработки исключений, возникающих в программе. В завершающей части главы рассматриваются структуры данных Python для сохранения введенной информации, чтобы пользователю не приходилось каждый раз вводить данные заново при каждом запуске программы.
В главе 11 мы займемся эффективной организацией тестирования вашего кода. Тестирование поможет убедиться в том, что написанный код работает правильно, а также выявит ошибки, внесенные в процессе расширения уже написанных программ.