EAFP вместо LBYL: почему Python поощряет 'сделать и обработать ошибку', а не 'проверить всё заранее'
core переписать код с предварительными проверками (if os.path.exists, if key in dict) в try/except там, где это идиоматичнее и безопаснее от race condition
C-программист приучен думать в терминах предохранителей: перед strcpy проверить длину, перед разыменованием указателя проверить на NULL, перед open() проверить access(). Это разумно в языке, где ошибка — это undefined behavior или падение процесса. Такой стиль называется LBYL — Look Before You Leap: сначала выясняем, безопасно ли действие, потом действуем.
Python поощряет обратную модель — EAFP, Easier to Ask Forgiveness than Permission: сначала выполняем действие, а если оно невозможно, интерпретатор сообщает об этом исключением, которое мы перехватываем. Исключения в Python — стандартный и основной механизм сообщения об ошибках, и EAFP опирается на две особенности языка, которых нет в C:
- Динамическая/утиная типизация. Типы в Python привязаны к объектам и проверяются во время выполнения, а пригодность объекта для операции определяется наличием нужных методов, а не классом. Проверить это заранее в общем случае невозможно —
isinstance-проверки на каждый шаг превращают Python в плохую копию C с ручной типизацией. - TOCTOU (time-of-check to time-of-use). Между проверкой и действием проходит время, за которое состояние мира могло измениться — файл удалили, ключ словаря убрал другой поток, сокет отвалился. Проверка создаёт ложное чувство безопасности, а не устраняет проблему.
Классическая ошибка си-программиста:
import os
if os.path.exists(path):
with open(path) as f:
data = f.read()
Между os.path.exists и open файл может быть удалён другим процессом — это ровно тот же класс гонки, из-за которого проверку прав через access() перед open() в C считают небезопасной. Идиоматичный Python:
try:
with open(path) as f:
data = f.read()
except FileNotFoundError:
data = None
Здесь нет окна гонки: попытка и обработка её результата — одно действие с точки зрения кода.
То же со словарями. Инстинкт C-программиста — «проверить границы перед доступом»:
if key in d:
value = d[key]
else:
value = default
Это работает, но делает два обращения к хеш-таблице вместо одного и остаётся LBYL-мышлением. Идиоматичнее:
value = d.get(key, default)
а если нужна ветка с побочным эффектом — try/except KeyError. dict.get — не «трюк», а прямое следствие EAFP: спросить объект напрямую надёжнее, чем гадать заранее.
Разобранный пример
Задача: получить конфигурацию пользователя из вложенного словаря, где часть ключей может отсутствовать, а часть — быть не тем типом.
LBYL-версия в стиле C:
def get_timeout(config):
if "network" in config:
network = config["network"]
if isinstance(network, dict) and "timeout" in network:
timeout = network["timeout"]
if isinstance(timeout, (int, float)):
return timeout
return 30
Четыре уровня проверок повторяют структуру данных вручную — ровно то, что в C вы делали бы, проверяя каждый указатель перед разыменованием. EAFP-версия:
def get_timeout(config):
try:
return config["network"]["timeout"]
except (KeyError, TypeError):
return 30
TypeError здесь перехватывает случай, когда config["network"] — не словарь (например, строка: индексация строки по строковому ключу даёт TypeError), а KeyError — случай отсутствующего ключа. Одна строка вместо дерева проверок, и она устойчива к любой форме «не такой структуры», а не только к предусмотренным случаям.
Попробуй сейчас
Возьмите функцию, читающую значение по цепочке из списка (по индексу) внутри словаря:
def get_first_score(data, key):
if key in data and isinstance(data[key], list) and len(data[key]) > 0:
return data[key][0]
return None
Перепишите её в стиле EAFP одним try/except, перехватывая ровно те исключения, которые реально могут возникнуть (KeyError, IndexError, TypeError). Проверьте на data = {"scores": []}, data = {"scores": [10, 20]} и data = {}.
Получилось, если…
Вы поняли идею, если новая версия перехватывает исключения по конкретным типам (не except Exception), не делает предварительных проверок структуры и даёт тот же результат на всех трёх тестовых входах, что и LBYL-версия.
flowchart TB
subgraph LBYL["LBYL: проверка заранее"]
A1[Проверить: файл существует?] --> A2{Существует}
A2 -->|окно гонки| A3[Другой процесс удаляет файл]
A3 --> A4[open всё равно падает с ошибкой]
end
subgraph EAFP["EAFP: сделать и обработать"]
B1[Попытаться открыть файл] --> B2{Успех?}
B2 -->|да| B3[Работать с файлом]
B2 -->|нет| B4[except FileNotFoundError]
endВывод
Проверка «на всякий случай» перед действием в Python обычно не устраняет ошибку, а дублирует работу интерпретатора и оставляет гонку. Действие — это и есть проверка; исключение — это её отрицательный результат. dict.get/try-except — не сахар, а прямое выражение EAFP.
AI-generated · source-grounded review
🛡 Fact-checked: 1 risky claim verified · 2 removed · confidence: high · figures: 1
A second model re-checked this lesson's claims and corroborated 1 of 7 , and could not confirm 6 either way. This is incomplete corroboration, not a disagreement.
On your own course these buttons answer instantly, quizzes track what you've mastered, and lessons adapt to your gaps. Write my course