Это продолжение статьи Исключения в Python. Введение
Следующая задача. Допустим, данные необходимо получить не с помощью консольного ввода, а построчно считать из файла и добавить обработанный результат в тот же файл. И если в процессе чтения / обработки возникнет ошибка, то, помимо стандартного сообщения об ошибке, необходимо также в любом случае закрыть этот текстовой файл.
Для тестирования этого кода понадобится добавить в текущий каталог программы текстовой файл tmp.txt следующего содержания:
5
7
В коде, представленом ниже, происходит открытие файла, обработка строк (в данном случае, это деление числа 10 на целое число, взятое из соответствющей строки файла), накопление результата для выгрузки в файл в переменной output и, наконец, добавление полученного результата в конец текстового файла:
try:
f = open('tmp.txt', 'r+')
output = ''
for line in f:
num = int(line)
result = 10 / num
output += f'{result}\n'
f.write(output)
except Exception as e:
print(e.__class__, e)
finally:
print('f.closed = ', f.closed)
f.close()
print('f.closed = ', f.closed)
Если мы сейчас запустим этот код, то он отработает без ошибок, и на печать будет выведено:
>>>
f.closed = False
f.closed = True
А текстовой файл tmp.txt увеличится на две новых строки:
>>>
5
7
2.0
1.4285714285714286
Эти добавленные строки - дробные числа в строковом формате. Поэтому, если мы ещё раз запустим скрипт, то на этот раз получим ошибку и сообщение:
>>>
<class 'ValueError'> invalid literal for int() with base 10: '2.0\n'
f.closed = False
f.closed = True
Однако, как мы видим, не смотря наличие или отсутствие ошибок исполнения кода, и в первом и во втором случае был выполнен блок finally: файл был закрыт с выводом соответствующего сообщения: f.closed = True.
Продолжить в статье Исключения в Python. Перехват специфических ошибок
Вернуться в начало, на статью Исключения в Python. Введение
Это продолжение статьи Исключения в Python. Введение
Изменим условие задачи. Допустим, бесконечный цикл нам нужен только случае ошибки ввода. Если же пользователь ввёл валидное число, то программа на этом должна успешно завершиться.
Для подобных случаев существует конструкция try-except-else, когда ветвление программы зависит от наличия или отсутствия ошибки в блоке try. Если ошибка найдена, то программа выполняет блок except. Если нет — выполняется блок else:
while True:
line = input('Please enter an integer: ')
if line == 'end':
break
try:
result = 100 / int(line)
print(result)
except Exception as e:
print(e.__class__, e)
print('Please enter another number! ')
else:
print('Successful input!')
break
Продолжить в статье Исключения в Python. Конструкция try-except-finally
Вернуться в начало, на статью Исключения в Python. Введение
Это продолжение статьи Исключения в Python. Введение
Перехватить все возможные исключения (Exceptions) в этой ситуации мы сможем с помощью конструкции try-except:
while True:
line = input('Please enter an integer: ')
if line == 'end':
break
try:
result = 100 / int(line)
print(result)
except Exception as e:
print(e.__class__, e)
print('Please enter another number! ')
Как видим, код не стал больше, скорее даже наоборот — он стал понятнее.
Однако помимо этого мы получили возможность перехватывать абсолютно все ошибки, которые могут возникнуть в блоке try при вычислении переменной result:
1.) Ошибка деления на ноль:
>>>
Please enter an integer: 0
<class 'ZeroDivisionError'> division by zero
Please enter another number!
2.) Ошибка ввода дробного значения:
>>>
Please enter an integer: 1.1
<class 'ValueError'> invalid literal for int() with base 10: '1.1'
Please enter another number!
3.) Ошибка ввода нечисловых символов:
>>>
Please enter an integer: asd
<class 'ValueError'> invalid literal for int() with base 10: 'asd'
Please enter another number!
И самое главное, благодаря этой конструкции будут перехвачены вообще все мыслимые и немыслимые ошибки ввода. Даже те, которые могут возникнуть в новых версиях интерпретатора Python, программа не будет завершена аварийно и пользователь в конце концов сможет ввести валидное значение делителя!
Продолжить в статье Исключения в Python. Конструкция try-except-else
Вернуться в начало, на статью Исключения в Python. Введение
Не ошибается только тот, кто ничего не делает. Поэтому ошибки в коде — естественны и неизбежны.
Однако ошибка ошибке рознь. Есть обычные опечатки, которые сразу же обнаружит и подчеркнёт редактор IDE, либо выявит сам интерпретатор при первом же прогоне. Но есть ошибки, заметить которые не так просто. Которые обязательно дождутся самого неподходящего момента и громко дадут о себе знать.
В первом случае это Syntax Errors (синтаксические ошибки), а во втором — Exceptions (исключения).
Синтаксическая ошибка — это ошибка в синтаксисе последовательности символов или токенов. Иными словами, синтаксическая ошибка — это несоответствие правилам записи кода, без исправления которой код не может быть запущен.
Исключения возникают, если программа синтаксически верна, но в некоторых случаях (при некоторых значениях переменных) исполнение кода приводит к ошибке. То есть исключения возникают в тех случаях, когда программист не учёл или не предусмотрел какое-либо событие, которое в итоге привело в ошибке и остановке программы.
Классический пример — ошибка деления на 0:
>>> 5 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
Главное, что всегда следует знать программисту при появлении сообщения об ошибке:
Всё это так — “сервис исключений” (если его можно так назвать) в Python, позволяет эффективно бороться с ошибками после остановки программы. А может ли он помочь ещё до остановки программы, в процессе её выполнения?
Давайте рассмотрим пример. Предположим, в бесконечном цикле пользователь должен вводить значение делителя и получать: либо результат деления на введённое число (при правильном вводе), либо сообщение, что введённое число даст ошибку и просьбу ввести вместо него правильный вариант.
while True
line = input('Please enter an integer: ')
if line == 'end':
break
if line == '0':
print('Zero gives an error! Please enter a different number!')
continue
result = 100 / int(line)
print(result)
Программа будет работать до тех пор, пока пользователь не введёт end.
В этом примере учтена возможность ввода пользователем нулевого значения. Для этого каждое введённое значение прежде сравнивается с нулём. И если пользователь ввёл 0, то программа его не примет и попросит ввести другое значение.
Это действительно решает проблему. Но что будет, если число окажется дробным? А если пользователь по ошибке введёт букву и другой символ? В этом случае всё равно будет аварийное завершение программы. Мы конечно же может предусмотреть и эти два варианта ошибочного ввода и поставить точно такие же “заглушки” как и в случае ввода число 0. Но где гарантия, что не может возникнуть других ошибок ввода помимо этих трёх?
Один из вариантов решения — не сравнивать введённый результат со списком ошибочных значений, а сразу же обработать все возможные исключений с помощью конструкции try-except (переход на эту статью по ссылке ниже).
Конструкция try-except — далеко не единственный способ использования исключений в Python. Более подробно этот вопрос рассмотриваются в следующих статьях:
Исключения в Python. Конструкция try-except-else
Исключения в Python. Конструкция try-except-finally
Исключения в Python. Перехват специфических ошибок
Исключения в Python. Операторы вызова исключений raise и assert
Более подробно эта тема рассматривается в курсе Python Fundamentals
В теме анкоров (или якорей) была рассмотрена возможность использования паттерна (шаблона) относительно начала или конца строки, или начала или конца слова.
С помощью regex lookaround, тоже самое можно сделать в любой части строки или слова. Причём в конечном итоге, и что ОЧЕНЬ важно, символы на которые ссылается lookaround НЕ БУДУТ добавлены в итоговый результат.
Анализ символов может быть как непосредственно перед шаблоном - метод lookahead(смотреть вперёд), так и за шаблоном lookbehind (посмотреть назад).
Кстати, в русскоязычной печати и блогосфере lookahead иногда называется “опережающая проверка” и lookbehind - “ретроспективная проверка”. Однако пользоваться аутентичными английскими терминами lookahead и lookbehind в данном случае будет более логично, и, что самое главное, более понятно.
Итак в чём же здесь суть? Попробуем разобрать это на примере.
Задача:
Допустим, нам необходимо найти имена, которые начинаются с _ и выбрать эти имена, отбросив нижнее подчёркивание.
Пример:
_id name list _name
На первый взгляд, самым простым решением будет: _\w+
Но ведь в задаче сказано, что имена нужны без подчёркиваний!
Другим относительно приемлемым решением будет выделить имя после нижнего подчёркивания в группу: _(\w+) И в Match information мы видим, что хотя и match не совпало с нужным нам результатом, но появились дополнительные группы с правильным ответом.
Однако, для того, чтобы получить правильный ответ в match, нам надо использовать lookaround. И, в частности, в этом конкретном случае - lookbehind (взгляд назад, поскольку поисковая машина проверяет символы слева-направо, поэтому переход в начало слова будет с точки зрения поискового алгоритма возвратом назад).
Для этого условие начала нижнего подчёркивания мы перенесём в конструкцию lookbehind:
(?<=_)\w+
Как видим, теперь нижнее подчёркивание вообще пропало из выделения, но зато в match появилось именно то, что нам нужно!
Это был пример positive lookbehind, когда наличие указанного элемента (или группы элементов) - истина.
Предположим, что условия изменились и теперь нам нужны слова, которые НЕ начинаются с нижнего подчёркивания.
В этом случае знак = мы меняем на !:
(?<!_)\w+
Как видим, это не дало ожидаемого результата: все слова выделились и стали валидными. Это произошло из-за того, что знак нижнего подчёркивания входит в \w, поэтому, для выполнения условия, выделение сместилось на знак влево. Изменим \w на [a-z]:
(?<!_)[a-z]+
Теперь выделение сместилось на символ вправо!
Чтобы исправить ситуацию добавим знак границы слова \b:
(?<:!_)\b[a-z]+
Теперь, благодаря negative lookbehind, всё работает как надо!
Аналогичную проверку можно выполнить для знака нижнего подчёркивания в КОНЦЕ слова. Для этого сначала изменим исходный текст:
Изменённый пример к предыдущей задаче:
id_ name list name_
Сначала попробуем выделить все слова: [a-z]+
Далее, с помощью positive lookahead (позитивного взгляда вперёд или более благозвучной, но менее понятной “позитивной упреждающей проверки”) оставим только те, что заканчиваются на нижнее подчёркивание:
[a-z]+(?=_)
Теперь, c помощью negative lookahead (негативный взгляд вперёд), попробуем наоборот, оставить только слова БЕЗ нижнего подчёркивания. И для этого знак (=) поменяем на (!).
Как видим, это помогло лишь отчасти: слова с подчёркиванием тоже пока выделены, правда, в них теперь выделен на один символ меньше. Чтобы их совсем убрать, жёстко ограничим конец слов границей \b:
[a-z]+\b(?!_)
Ура, получилось!
И ещё раз выведем выведем вместе все четыре рассмотренных варианта lookaround:
Pattern | Type of Lookaround |
---|---|
(?<=_)\w+ | positive lookbehind |
(?<!_)\w+ | negative lookbehind |
[a-z]+(?=_) | positive lookahead |
[a-z]+(?!_) | negative lookahead |
Более подробно эта тема рассматривается в курсе Python Fundamentals
Если мы что-то ищем в строке с помощью регулярного выражения, то по-умолчанию производится широкий или “жадный” (greedy) поиск.
Рассмотрим пример. Допустим, мы хотим получить в тексте слова, ограниченные двойными кавычками:
Without "economic freedom", there can be no other "freedom".
Margaret Thatcher.
По идее, нам нужен паттерн, который начинается с первой кавычки, далее показывает любой символ (точка) и далее идёт квантификатор (плюс), который вмещает в себя один или более символов. И заканчивается всё это снова кавычками:
r ='\".+\"'
Однако, на деле оказывается, что это регулярное выражение выделяет значительно больше, чем предполагалось:
"economic freedom", there can be no other "freedom"
Почему? Потому что так работает алгоритм поиска совпадения в регулярном выражении.
Если не указано иное (то есть по умолчанию) алгоритм поиска работает от первого совпадения до конца текста. А затем, идёт посимвольно назад, до выполнения первого совпадения по условию поиска.
Поэтому, если мы хотим выделить два варианта, то нам нужен ленивый (lazy) поиск, где варианты перебираются до первого совпадения:
"economic freedom", "freedom"
Для этого после квантификатора + необходимо поставить знак вопроса ?:
r ='\".+?\"'
Это относится не только к знаку “плюс”, но и к другим квантификаторам:
Symbols | Their meaning: |
---|---|
+ | 1 or > characters |
* | 0 or > characters |
{min, } | nmin or > times |
{,max} | from 0 to nmax times |
{min, max} | from nmin to nmax times |
Более подробно эта тема рассматривается в курсе Python Fundamentals
Часть паттерна, окружённая круглыми скобками считается Группой.
Рассмотрим пример. Из текста необходимо извлечь телефонные номера. Важное ограничение: валидными считаются номер в формате 000 00 00 c блоками цифр, разделёнными одновременно либо двумя пробелами, либо двумя дефисами, либо двумя бэкслешами. Смешанное разделение в любой комбинации считается невалидным. Пример текста:
111 22 33-aad 232-15-11 ff 15 15 555-22-78 223/99-37 223/25/987 123-35 26 258-85-45
Исходя из того, что мы уже знаем, единственным вариантом решением будет объединение всех трёх вариантов с помощью ИЛИ (|):
\d{3}-\d{2}-\d{2}|\d{3} \d{2} \d{2}|\d{3}\/\d{2}\/\d{2}
Однако, если мы выделим разделители в отдельную группу (-| |\/) в начале шаблона, то в случае, если эта группа встречается в шаблоне несколько раз, мы можем не вставлять в это место всю группу целиком, а всего лишь сослаться на неё с помощью бэкслеш+порядковый номер группы (в нашем случае это будет \1, поскольку в шаблоне присутствует только одна группа):
\d{3}(-| |\/)\d{2}\1\d{2}
Важно ещё раз отметить, что ссылка на группу \1 означает, допустимость только того символа, что встретился в группе. Иными словами, если в самой группе совпал дефис, то в ссылке на группу \1 допустим ТОЛЬКО дефис и больше никакой другой символ. Аналогично, если в группе совпал пробел, то пробел должен быть и в ссылке на группу. То же самое и для бэкслеша.
Более подробно эта тема рассматривается в курсе Python Fundamentals
В предыдущих примерах мы уже познакомились с несколькими метасимволами (то есть символами, объединяющими определённую группу символов). Теперь самое время свести их в одну таблицу и заодно рассказать о тех, которые нам ещё не встречались.
Symbols | Their meaning: |
---|---|
. (dot) | Any character except \n |
\d | Any digit |
\w | Any word character: [A-Za-z0-9_] |
\s | Any whitespace characters: [ \n\r\t\f] |
\D | Any NON-digit |
\W | Any NON-word character: [A-Za-z0-9_] |
\S | Any NON-whitespace characters: [ \n\r\t\f] |
Более подробно эта тема рассматривается в курсе Python Fundamentals
Бывают ситуации, когда нам надо не просто найти совпадение символов в тексте, но также быть уверенными в том, что эта найденная комбинация символов выполняет определённые условия относительно другой части текста. Например, при валидации пароля, важно не просто получить совпадение символов пароля My_Pa$$worD, но также быть уверенными в том, что пароль полностью занимает всю строку от начала и до конца.
Например, все эти три строки содержат комбинацию символов, удовлетворяющих паттерну My_Pa$$worD:
My_Pa$$worD___
My_Pa$$worD
aaa_asdf_ My_Pa$$worDllfff
Однако единственно правильным вариантом будет только пароль во второй строке.
Для выполнение этого условия в начало и конец паттерна пароля добавить символы (якоря) начала ^ и конца строки $: ^My_Pa\$\$worD$
Таблица анкоров: <\p>
Symbols | Their meaning: |
---|---|
^ | Start of line |
$ | End of line |
\b | Boundary of word (start or end of word) |
\B | Inverse of \b (non-start or non-end of word) |
Более подробно эта тема рассматривается в курсе Python Fundamentals
Очевидно, что все показанные ранее примеры поиска точного совпадения слов или символов на практике случаются крайне редко (имеются в виду совпадения, показанные в предыдущем разделе Quantification (Квантификация или количественная оценка)).
Значительно чаще требуется более сложный поиск, например поиск текста, удовлетворяющего определённым критериям. Например, поиск телефонных номеров в формате 000-00-00.
Очевидно, что в начале нашего паттерна, соответствующего поиску указанного формата телефонного номера, потребуется указать комбинацию чисел от 0 до 9, повторяющуюся ровно три раза. Ни больше и не меньше.
В том случае, когда на месте указанного символа может быть любой символ из выбранного диапазона, удобнее всего использовать Character class (Класс символов).
Например, если нам нужна любая буква из набора a, b, c или d, то класс символов мы запишем как [abcd]. И поскольку указанные символы расположены в таблице кодировки по соседству, то запись [abcd] можно сократить до вида [a-d].
Пример класса символов [0-9] - означает, что нам подойдёт любой символ от 0 до 9. И если мы теперь добавим {3}, то это будет означать, что нам нужна группа из трёх чисел подряд: [0-9]{3}.
Далее добавляем дефис и ещё 2 последовательности по два символа. И в результате получаем итоговый паттерн, удовлетворяющий заданному условию:
Кстати, забегая вперёд, следует отметить, что есть один способ записать тоже самое:
\d{3}-\d{2}-\d{2}
В этом случае, для записи последовательности чисел используется специальный символ \d (бэкслеш-ди), называемый Metacharacter (Метасимвол) .