Перейти к содержанию

432 уроков, 26 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 19 из 20 Начальный 16 мин 100 XP

except или if в Python: граница применимости проверки и исключения

Отсутствие ключа: проверить if-ом или поймать KeyError? Разбираем честную границу: обычные варианты — if, редкие сбои — except, и случай, где проверка проигрывает исключению всухую.

Редакция Питоники

Урок 14 свёл два стиля в формулу: EAFP — сначала действие, потом ловля; LBYL — сначала проверка, потом действие. Но формула не отвечает на главный практический вопрос: где проходит граница? Отсутствие ключа в словаре — проверять if-ом или ловить KeyError? Битое число — прогонять через isdigit или верить int с ValueError? Урок разбирает задачу за задачей и выводит правило, которое работает без оговорок.

Одна задача — два стиля

Настройка может отсутствовать — тогда берём умолчание. Вот эта задача в обеих формах, и результаты совпадают:

два стиля, один результат
config = {'host': 'localhost', 'port': '8080'}

# стиль if: проверяем до действия
if 'port' in config:
    port = int(config['port'])
else:
    port = 80
print('if-стиль:', port)

# стиль except: действуем и ловим
try:
    port = int(config['port'])
except KeyError:
    port = 80
print('except-стиль:', port)
Вывод
if-стиль: 8080
except-стиль: 8080

Обе ветки напечатали одно и то же — техники равносильны. Выбор делается не по технике, а по смыслу: считается ли отсутствие порта нормой или сбоем. Если конфиг без порта — обычное дело (умолчание прописано в документации), то if читается честнее: отсутствие — одна из ожидаемых веток программы. Но в этом конкретном случае у Python есть и третий, самый короткий вариант — get из урока про KeyError:

get: умолчание без ветвления
config = {'host': 'localhost'}
port = int(config.get('port', 80))
print('порт:', port)
print('host:', config.get('host', 'не задан'))
Вывод
порт: 80
host: localhost

Когда отсутствие ключа обрабатывается подстановкой умолчания, отдельное ветвление и исключение одинаково избыточны — get выражает это одной строкой. Запомните порядок предпочтений для словарей: get — если есть умолчание; if in — если отсутствие требует разных действий; try/except KeyError — если отсутствие — редкий сбой, а не вариант.

Где проверка проигрывает: разбор числа

Классическая попытка проверить строку перед int — метод isdigit. Он отвечает «только цифры?» — и кажется идеальным стражем. Сравним его с честным разбором через int и ValueError из урока 12 на наборе кандидатов:

isdigit против int
candidates = ['42', '-7', ' 5 ', '3.14', 'семь']

print('проверка isdigit:')
for text in candidates:
    if text.isdigit():
        print(' ', repr(text), '->', int(text))
    else:
        print(' ', repr(text), '-> отклонено')

print('разбор int-ом:')
for text in candidates:
    try:
        print(' ', repr(text), '->', int(text))
    except ValueError:
        print(' ', repr(text), '-> не число')
Вывод
проверка isdigit:
  '42' -> 42
  '-7' -> отклонено
  ' 5 ' -> отклонено
  '3.14' -> отклонено
  'семь' -> отклонено
разбор int-ом:
  '42' -> 42
  '-7' -> -7
  ' 5 ' -> 5
  '3.14' -> не число
  'семь' -> не число

Страж отсеял всё, что не чистые цифры, — включая числа, которые int разбирает прекрасно: минус семь и пятёрку с пробелами по краям. Данные потеряны не из-за ошибки, а из-за неточной проверки. Чтобы if-вариант догнал int, пришлось бы написать text.lstrip('-').isdigit() — и всё равно что-то упустить. Проверка сложнее действия — значит, действие с обработкой честнее.

if-вариант, догоняющий int
# LBYL-цепочка: проверки повторяют действие
if text.isdigit() or (text.startswith('-') and text[1:].isdigit()):
    number = int(text)
else:
    number = None
Минус уже разобран руками — а пробелы по краям и научная запись всё ещё потеряны. Как только проверка начинает догонять действие, это сигнал писать try с except ValueError и доверять разбор.

if для обычных веток

Обратная сторона: если оба исхода частые и оба — норма, ветвление через исключение странно. Счётчик голосов с двумя типами ответов: половина «да», половина «нет» — оба случая встречаются в каждой итерации:

оба исхода частые — значит, if
votes = ['да', 'нет', 'да', 'нет', 'да']
counts = {}

for v in votes:
    if v in counts:
        counts[v] += 1
    else:
        counts[v] = 1

print(counts)
Вывод
{'да': 3, 'нет': 2}

Писать здесь try: counts[v] += 1 except KeyError: counts[v] = 1 можно — будет работать. Но KeyError здесь ловился бы в каждой второй итерации: исключение перестало быть сигналом о сбое и стало обычной веткой логики. Исключение дорого именно в момент возникновения: Python строит объект, раскручивает стек. Частые сбои — довод в пользу if.

Спорный случай: пустая строка в списке слов

Есть случаи, где оба инструмента работают, и выбор — вопрос вкуса. Список слов, среди которых затесалась пустая:

граница двух инструментов
words = ['кот', 'собака', '', 'ёж']

for word in words:
    try:
        print('первая буква:', word[0])
    except IndexError:
        print('пустая строка — букв нет')
Вывод
первая буква: к
первая буква: с
пустая строка — букв нет
первая буква: ё

Пустая строка — сбой или вариант? Если слова приходят из формы ввода и пустое поле — нормальное событие, честнее if not word: из уроков про строки: условие короткое и говорит само за себя. Если пустая строка — аномалия данных, которую вы редко ждёте, except IndexError с обработкой тоже оправдан. Критерий один и тот же: частота события и его статус в глазах программы.

тот же случай в форме if
for word in words:
    if word:
        print('первая буква:', word[0])
    else:
        print('пустая строка - букв нет')
Тот же результат без исключения: условие короткое и честное, потому что пустое поле здесь — обычный вариант. Как только пустые строки станут аномалией, та же обработка переедет в except IndexError.
СитуацияИнструментПочему
У ключа словаря есть умолчаниеdict.getОдна строка, без ветвлений
Отсутствие ключа — обычный вариантif in / if notЧастый случай: исключение дорого
Отсутствие ключа — редкий сбойtry / except KeyErrorСигнал о поломке данных
Разбор числа с минусами и пробеламиint + except ValueErrorПроверка сложнее действия
Файл мог исчезнуть между проверкой и открытиемtry / except FileNotFoundErrorМежду if и open нет гарантий
Опечатка в кодеНикаких ловушекПусть падает и показывает строку

Строка про файл — самый коварный случай: урок 13 о файлах показывал проверку os.path.exists, но между проверкой и открытием файл могут удалить — проверка отвечает за прошлое, исключение ловит настоящее. Именно поэтому для внешнего мира try надёжнее любого if.

проверка, у которой нет гарантий
import os

if os.path.exists('report.csv'):
    f = open('report.csv')  # файл могли удалить между двумя строками
exists отвечает на вопрос о моменте проверки, а открывается файл позже: если между строками файл исчез, программа падает с FileNotFoundError, мимо любой проверки. Честная защита — try с except FileNotFoundError вокруг open. В песочнице файлов нет, поэтому блок не запускается.

Правило одной фразы

Если вариант обычный — if; если вариант — сбой — except. К нему два уточнения: когда проверка сложнее действия, выбирайте действие с исключением; когда сбои частые, исключение замените проверкой. Проверки и исключения при этом не конкуренты, а разные этажи одной защиты: if отсеивает ожидаемые варианты быстро и без шума, except обрабатывает то, что предвидеть проверкой невозможно, а assert из предыдущего урока следит, чтобы собственный код не соврал о себе. Осталось собрать всё в одну программу — финальный проект раздела: разбор файла с грязными данными, где try внутри цикла, счётчики и журнал работают как одна машина.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

config = {}
try:
    port = int(config['port'])
except KeyError:
    port = 80
print(port)
print('-7'.isdigit())
try:
    print(int('-7'))
except ValueError:
    print('отклонено')
counts = {'да': 1}
if 'нет' in counts:
    counts['нет'] += 1
else:
    counts['нет'] = 1
print(counts)
Проверь себя
0 / 7

1. Отсутствие ключа в словаре — обычный вариант работы. Что выбрать?

2. Почему isdigit перед int — плохой страж?

3. Когда исключение проигрывает проверке по цене?

4. Почему для файла надёжнее try, а не os.path.exists?

5. Пустая строка в списке слов — норма для программы. Что честнее?

6. if-проверка не спасает от чего?

7. Правило одной фразы из урока звучит так:

Карточки терминов
Запомнено: 0 / 7
Практика

Дан словарь stock с остатками: {'ручка': 12, 'тетрадь': 0}. Напишите функцию price_of(item), которая возвращает остаток товара, а при отсутствии — строку нет в наличии. Решите задачу через get с умолчанием и напечатайте результат для 'ручка' и для 'ластик'.

practice.py
Вопросы и ответы по уроку

Что лучше в Python: if-проверка или try-except?

Зависит от статуса события: частый, ожидаемый вариант — if-проверка (или dict.get для умолчаний), редкий сбой данных — try-except. Исключение дорого в момент срабатывания, поэтому половина проходов цикла, падающих в except, — знак, что нужна проверка.

Урок 14: EAFP против LBYL

Почему isdigit не подходит для проверки перед int?

isdigit возвращает True только для строк из цифр, а int разбирает и минусы, и пробелы по краям: '-7' и ' 5 ' отклоняются зря, данные теряются. Надёжнее вызывать int и ловить ValueError — он точно перечисляет, что разобрать не удалось.

Урок 12: ValueError

Как проверить, есть ли ключ в словаре, и не поймать KeyError?

Три инструмента: dict.get(key, default) — если есть умолчание; if key in dict — если отсутствию нужны свои действия; try-except KeyError — если отсутствие редкий сбой. Для частых случаев первые два дешевле исключения.

Урок 11: KeyError

Почему проверка файла os.path.exists ненадёжна?

Проверка отвечает за момент, когда её выполнили: между exists и open файл может удалить другой процесс. try с except FileNotFoundError проверяет настоящее состояние мира — для внешних ресурсов это единственная честная защита.

Урок 13: FileNotFoundError

Может ли if заменить обработку багов в коде?

Нет: if проверяет данные, а NameError от опечатки или TypeError от неверного типа — свойства кода. Они пройдут мимо любой проверки значений. За кодом следят assert-инварианты и тесты, за данные — try-except.

Урок 18: assert

Понравился урок? Сошлитесь на него

«Проверка сложнее действия — значит, действие с обработкой честнее.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.