Мэтиз. Изучаем Python. Crash course2 n d e d i t i o na h a n d s o n, p r o j e c t b a s e d i n t r o d u c t i o n t o p r o g r a m m i n g
Скачать 6.2 Mb.
|
208 Глава 10 • Файлы и исключения Использование исключений для предотвращения аварийного завершения программы Правильная обработка ошибок особенно важна в том случае, если программа должна продолжить работу после возникновения ошибки. Такая ситуация часто встречается в программах, запрашивающих данные у пользователя. Если програм- ма правильно среагировала на некорректный ввод, она может запросить новые данные после сбоя. Создадим простой калькулятор, который выполняет только операцию деления: 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.py", line 9, in ZeroDivisionError: division by zero Конечно, аварийное завершение — это плохо, но еще хуже, что пользователь увидит данные трассировки. Неопытного пользователя они собьют с толку, а при сознательной попытке взлома злоумышленник сможет получить из них больше информации, чем вам хотелось бы. Например, он узнает имя файла программы и увидит некорректно работающую часть кода. На основании этой информации опытный хакер иногда может определить, какие атаки следует применять против вашего кода. Исключения 209 Блок else Для повышения устойчивости программы к ошибкам можно заключить строку, вы- дающую ошибки, в блок 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 происходит ошибка, связанная с делением на 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 произо- шло определенное исключение. 210 Глава 10 • Файлы и исключения Заранее определяя вероятные источники ошибок, вы повышаете надежность своих программ, которые продолжают работать даже при вводе некорректных данных или при недоступности ресурсов. Ваш код оказывается защищенным от случайных ошибок пользователей и сознательных атак. Обработка исключения FileNotFoundError Одна из стандартных проблем при работе с файлами — отсутствие необходимых файлов. Тот файл, который вам нужен, может находиться в другом месте, в имени файла может быть допущена ошибка или файл может вообще не существовать. Все эти ситуации достаточно прямолинейно обрабатываются в блоках try - except Попробуем прочитать данные из несуществующего файла. Следующая программа пытается прочитать содержимое файла с текстом «Алисы в Стране чудес», но я не сохранил файл alice .txt в одном каталоге с файлом alice .py : alice.py filename = 'alice.txt' with open(filename, encoding='utf-8') as f: contents = f.read() В программе видны два изменения. Во-первых, объект файла представляется пере- менной с именем f — это общепринятое соглашение. Во-вторых, в программе ис- пользуется аргумент encoding . Он необходим в тех случаях, когда кодировка вашей системы по умолчанию не совпадает с кодировкой читаемого файла. Прочитать данные из несуществующего файла нельзя, поэтому Python выдает исключение: Traceback (most recent call last): File "alice.py", line 3, in FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt' В последней строке трассировки упоминается FileNotFoundError : это исключение выдается в том случае, если Python не может найти открываемый файл. В данном примере функция open() порождает ошибку, и чтобы обработать ее, блок try на- чинается перед строкой с вызовом open() : filename = 'alice.txt' try: with open(filename, encoding='utf-8') as f: contents = f.read() except FileNotFoundError: print(f"Sorry, the file {filename} does not exist.") В этом примере код блока try выдает исключение FileNotFoundError , поэто- му Python ищет блок except для этой ошибки. Затем выполняется код этого Исключения 211 блока, в результате чего вместо трассировки выдается доступное сообщение об ошибке: 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, encoding='utf-8') as f: contents = f.read() except FileNotFoundError: print(f"Sorry, the file {filename} does not exist.") else: # Подсчет приблизительного количества строк в файле. ❶ words = contents.split() ❷ num_words = len(words) ❸ print(f"The file {filename} has about {num_words} words.") Затем я переместил файл alice .txt в правильный каталог, чтобы код в блоке try был выполнен без ошибок. В точке программа загружает текст в переменную 212 Глава 10 • Файлы и исключения contents , которая теперь содержит весь текст в виде одной длинной строки и ис- пользует метод split() для получения списка всех слов в книге. Запрашивая длину этого списка при помощи функции len() , мы получаем неплохое приближенное значение количества слов в исходной строке . В точке выводится сообщение с количеством слов, найденных в файле. Этот код помещен в блок else , потому что он должен выводиться только в случае успешного выполнения блока try . Выход- ные данные программы сообщают, сколько слов содержит файл alice .txt : The file alice.txt has about 29465 words. Количество слов немного завышено, потому что в нем учитывается дополнительная информация, включенная в текстовый файл издателем, но в целом оно довольно точно оценивает объем «Алисы в Стране чудес». Работа с несколькими файлами Добавим еще несколько файлов с книгами для анализа. Но для начала переместим основной код программы в функцию с именем count_words() . Это упростит про- ведение анализа для нескольких книг: word_count.py def count_words(filename): ❶ """Подсчет приблизительного количества строк в файле.""" try: with open(filename,encoding='utf-8') as f: contents = f.read() except FileNotFoundError: print(f"Sorry, the file {filename} does not exist.") else: words = contents.split() num_words = len(words) print(f"The file {filename} has about {num_words} words.") filename = 'alice.txt' count_words(filename) Большая часть кода не изменилась. Мы просто снабдили код отступом и переме- стили его в тело count_words() . При внесении изменений в программу желательно обновлять комментарии, поэтому мы преобразовали комментарий в строку доку- ментации и слегка переформулировали его . Теперь мы можем написать простой цикл для подсчета слов в любом тексте, который нужно проанализировать. Для этого имена анализируемых файлов со- храняются в списке, после чего для каждого файла в списке вызывается функция count_words() . Мы попробуем подсчитать слова в «Алисе в Стране чудес», «Сидд- хартхе», «Моби Дике» и «Маленьких женщинах» — все эти книги распространя- ются в свободном доступе. Я намеренно не стал копировать файл siddhartha .txt в каталог с программой word_count .py , чтобы выяснить, насколько хорошо наша программа справляется с отсутствием файла: Исключения 213 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 29465 words. Sorry, the file siddhartha.txt does not exist. The file moby_dick.txt has about 215830 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: ❶ pass else: filenames = ['alice.txt', 'siddhartha.txt', 'moby_dick.txt', 'little_women.txt'] for filename in filenames: count_words(filename) Единственное отличие этого листинга от предыдущего — команда pass в точке . Теперь при возникновении ошибки FileNotFoundError выполняется код в блоке except , но при этом ничего не происходит. Программа не выдает данных трасси- ровки и вообще никаких результатов, указывающих на возникновение ошибки. 214 Глава 10 • Файлы и исключения Пользователи получают данные о количестве слов во всех существующих файлах, однако ничто не сообщает о том, что какой-то файл не был найден: The file alice.txt has about 29465 words. The file moby_dick.txt has about 215830 words. The file little_women.txt has about 189079 words. Команда pass также может служить временным заполнителем. Она напоминает, что в этот конкретный момент выполнения вашей программы вы решили ничего не предпринимать, хотя, возможно, решите сделать что-то позднее. Например, эта программа может записать все имена отсутствующих файлов в файл с именем missing_files .txt . Пользователи этот файл не увидят, но создатель программы сможет прочитать его и разобраться с отсутствующими текстами. О каких ошибках нужно сообщать? Как определить, в каком случае следует сообщить об ошибке пользователю, а когда можно просто проигнорировать ее незаметно для пользователя? Если пользователь знает, с какими текстами должна работать программа, вероятно, он предпочтет по- лучить сообщение, объясняющее, почему некоторые тексты были пропущены при анализе. Пользователь ожидает увидеть какие-то результаты, но не знает, какие книги должны быть проанализированы. Возможно, ему и не нужно знать о недо- ступности каких-то файлов. Лишняя информация только сделает вашу программу менее удобной для пользователя. Средства обработки ошибок Python позволяют достаточно точно управлять тем, какой объем информации следует предоставить пользователю. Хорошо написанный, правильно протестированный код редко содержит внутрен- ние ошибки (например, синтаксические или логические). Но в любой ситуации, в которой ваша программа зависит от внешних факторов (пользовательского вво- да, существования файла, доступности сетевого подключения), существует риск возникновения исключения. С накоплением практического опыта вы начнете ви- деть, в каких местах программы следует разместить блоки обработки исключений и сколько информации предоставлять пользователям о возникающих ошибках. УПРАЖНЕНИЯ 10.6. Сложение: при вводе числовых данных часто встречается типичная проблема: пользователь вводит текст вместо чисел. При попытке преобразовать данные в int про- исходит исключение ValueError . Напишите программу, которая запрашивает два числа, складывает их и выводит результат. Перехватите исключение ValueError , если какое-ли- бо из входных значений не является числом, и выведите удобное сообщение об ошибке. Протестируйте свою программу: сначала введите два числа, а потом введите текст вместо одного из чисел. 10.7. Калькулятор: заключите код из упражнения 10.6 в цикл while , чтобы пользователь мог продолжать вводить числа, даже если он допустил ошибку и ввел текст вместо числа. Сохранение данных 215 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' в каждом тексте. Результат будет приближенным, потому что программа также будет учитывать такие слова, как 'then' и 'there' . Попробуйте повторить поиск для строки 'the ' (с пробелом в строке) и посмотрите, насколько уменьшится коли- чество найденных результатов. Сохранение данных Многие ваши программы будут запрашивать у пользователя информацию. Напри- мер, пользователь может вводить настройки для компьютерной игры или данные для визуального представления. Чем бы ни занималась ваша программа, инфор- мация, введенная пользователем, будет сохраняться в структурах данных (таких, как списки или словари). Когда пользователь закрывает программу, введенную им информацию почти всегда следует сохранять на будущее. Простейший способ сохранения данных основан на использовании модуля json Модуль json обеспечивает запись простых структур данных Python в файл и за- грузку данных из файла при следующем запуске программы. Модуль json также может использоваться для обмена данными между программами Python. Более того, формат данных JSON не привязан к Python, поэтому данные в этом формате можно передавать программам, написанным на многих других языках програм- мирования. Это полезный и универсальный формат, который к тому же легко изучается. |