Исключения и finally: одно слово, после которого функция перестаёт падать
Стоимость try, который не сработал, — около двух наносекунд, пять процентов, и байт-код объясняет почему. Дорого стоит другое: return в finally, который молча уничтожает исключение и превращает функцию в такую, из которой не выходит ни одна ошибка.
Полное техническое изложение
TL;DR
У инструкции try четыре части, и у каждой своя роль: try — попытка,
except — обработка ошибки, else — ветка «ошибки не было», finally —
выход в любом случае. Про finally важно одно: он выполняется и после удачи,
и после ошибки, и это ровно то, ради чего его пишут.
Отсюда главное следствие: finally, который сам передаёт управление наружу,
уничтожает летящее исключение. return в finally — справочник формулирует
это одним предложением: the saved exception is discarded
(сохранённое исключение отбрасывается), — и то же делают
break и continue. Отсюда тихая потеря данных: те же строки, что в уроке
про контекстные менеджеры, дают 100 вместо 175 — другой механизм, тот же
неверный ответ, и ни единого исключения в логе. Рядом стоит вторая ловушка:
голый except: ловит BaseException, то есть Ctrl+C и sys.exit().
Дальше — то, что отличает знающего от читавшего. Стоимость try/except,
который не сработал, около двух наносекунд: 40,5 нс против 38,7 у того же
кода без try (3.13.7). Это пять процентов, и разница различима — размах
прибора 0,4 нс, — но величина такая, что ради неё try не убирают. Сработавший — 201,1 нс, впятеро дороже. Причина структурная, а не случайная:
SETUP_FINALLY — псевдоинструкция, в байт-код она не попадает, и на счастливом
пути try добавляет один NOP. С 3.14 компилятор предупреждает о return в
finally (PEP 765) — но только предупреждает: поведение осталось прежним.
- когда в функции случается ошибка, оставшиеся строки не выполняются;
- ошибка «летит» наружу — из вызванной функции в вызывающую и дальше, пока её кто-нибудь не перехватит;
- у ошибки есть тип:
ValueError,KeyError, — и ловят их по типу.
BaseException,ExceptionGroup,except*,__context__,__cause__;- PEP 765,
SETUP_FINALLY, таблица исключений в байт-коде и наносекунды из замеров.
База: четыре части, и у каждой своя роль
Инструкция try состоит из четырёх частей. Первые две знают все, вторые две
путают чаще всего — а путаница в ролях и приводит к ошибкам, ради которых
написан весь урок.
try— попытка: код, который может не получиться;except— обработка: что делать, если не получилось, и с какой именно ошибкой;else— ветка «ошибки не было»: код, который имеет смысл только тогда, когда попытка удалась;finally— выход в любом случае: код, который выполняется и после удачи, и после ошибки.
Одним примером все четыре видно сразу:
try:
value = int(raw) # попытка
except ValueError:
value = 0 # сюда попадём, только если попытка не удалась
else:
audit(value) # а сюда — только если удалась
finally:
counter += 1 # а это выполнится в любом из двух случаевРоль finally стоит проговорить отдельно, потому что она шире, чем кажется.
Он выполняется не только после удачи и после пойманной ошибки, но и тогда,
когда управление уходит из блока наружу: при return из середины try и
при ошибке, которую никто не поймал. Именно поэтому в finally кладут
освобождение ресурсов — закрыть файл, отпустить блокировку: это единственное
место, про которое известно, что до него дойдут.
И здесь же главный вопрос урока. finally выполняется, пока исключение летит
наружу, — а что будет с самим исключением, если finally при этом сам решит
выйти наружу: сделает return, break или continue?
Обычно ничего необычного: finally отработал, исключение полетело дальше. Но
в этом одном случае — исключение исчезает. Не откладывается, не заменяется:
его больше нет, и функция возвращает значение так, будто ошибки не было.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования: try —
попытка, except — обработка, else — ветка удачи, finally — выход в любом
случае, и выход этот умеет проглотить ошибку. Всё дальнейшее — про то, как это
выглядит в коде, который никто не заподозрит; про то, чем голый except:
отличается от except Exception:; про цепочки исключений и про группы, в
которых ошибок сразу несколько.
Механизм 1: одно слово, после которого функция перестаёт падать
return в finally отбрасывает сохранённое исключение. Так записано в справочнике и так работает на всех версиях.Справочник формулирует это одним предложением: If the
(Если предложение finally clause
executes a return, break or continue statement, the saved exception is
discardedfinally выполняет return, break или continue, сохранённое исключение отбрасывается).
Слово «discarded» здесь надо читать буквально. Не «перекрывается», не «заменяется» — исчезает:
def never_fails():
try:
raise RuntimeError("что угодно")
finally:
return NoneЭта функция не может упасть. Никогда и ни от чего: любое исключение внутри
try будет отброшено на выходе. Проверено запуском: вызов возвращает None, исключения нет.
То же делает break, и заметить его труднее, потому что слова return рядом
нет:
for _ in range(1):
try:
raise KeyError("и этого")
finally:
break
finally, передающий управление наружу, глотает исключение так же полно, какexcept BaseException: pass. Разница только в том, что второе видно при чтении кода, а первое — нет.
Четвёртый случай на рисунке — тот же return в finally, но без исключения:
отбрасывается уже обычный результат.
Механизм 2: ошибка, которая не падает
Те же строки, что в уроке про контекстные менеджеры. Механизм другой — неверный ответ тот же.
ROWS = [
{"id": 1, "amount": "100"},
{"id": 2, "amount": "нет данных"}, # битая
{"id": 3, "amount": "50"},
{"id": 4, "amount": "25"},
]Правильный вариант: finally только закрывает ресурс, а что делать с битой
строкой, решает отдельный except.
def correct(rows):
total = 0
for row in rows:
handle = open("/dev/null", "w")
try:
total += int(row["amount"])
except ValueError:
continue
finally:
handle.close()
return totalНеправильный выглядит аккуратнее — «закрыли ресурс и честно вернули то, что успели посчитать»:
def broken(rows):
total = 0
for row in rows:
handle = open("/dev/null", "w")
try:
total += int(row["amount"])
finally:
handle.close()
return total # ← вот здесьПроверено запуском (одинаково на 3.13.7 и 3.14.7):
| вариант | результат |
|---|---|
finally закрывает, битую строку пропускает except | 175 |
finally c return | 100 |
| верная сумма | 175 |
Потеряно 75 из 175, ни одного исключения в логе. Разница между двумя функциями
— одна строка, и та выглядит как забота о ресурсе. Но return в finally
выходит из функции на первой же битой строке, отбросив исключение по дороге, —
а except ValueError: continue в правильном варианте просто идёт к следующей
записи.
Механизм 3: else — зачем он, если можно просто дописать в try
Разница видна не в успешном случае, а в том, что происходит с исключением из дописанного кода.
Справочник: The optional
(Необязательное предложение else clause is executed if the control flow leaves
the try suite, no exception was raised… Exceptions in the else clause are
not handled by the preceding except clauseselse выполняется, если управление вышло из блока try и исключение не возникло… Исключения в предложении else не обрабатываются предшествующими предложениями except).
То есть else — это способ сузить try до строки, которая действительно
может упасть:
try:
value = registry[key] # может бросить KeyError
except KeyError:
value = default
else:
audit(value) # KeyError отсюда НЕ будет перехваченЕсли audit внутри себя бросит KeyError, обработчик его не поймает — и
правильно: этот except написан для одной конкретной строки, а не для всего,
что случайно окажется рядом. Проверено запуском: исключение, брошенное в
else, уходит наружу мимо стоящего выше except.
Если к тому же блоку дописать finally, порядок получается такой (проверено
запуском):
без исключения: try -> else -> finally
с исключением: try -> except -> finally
Механизм 4: голый except: ловит больше, чем вы думаете
KeyboardInterrupt и SystemExit — не Exception. Проверяется одной
строкой: issubclass(SystemExit, Exception) даёт False, а MRO
KeyboardInterrupt — это KeyboardInterrupt → BaseException → object.
Документация объясняет, почему так сделано: The exception inherits from
(Исключение наследуется от BaseException so as to not be accidentally caught by code that catches
Exception and thus prevent the interpreter from exitingBaseException, чтобы его случайно не поймал код, ловящий Exception, и тем самым не помешал интерпретатору завершиться).
То есть иерархия построена специально, чтобы except Exception: пропускал
Ctrl+C. А голый except: эту защиту снимает: он ловит BaseException, и
программа перестаёт закрываться по Ctrl+C.
Механизм 5: цепочки — __context__, __cause__ и from None
Исключение, возникшее внутри except, само получает ссылку на предыдущее —
это PEP 3134, и делается это интерпретатором без участия автора кода.
| запись | __context__ | __cause__ | __suppress_context__ | в трассировке |
|---|---|---|---|---|
просто raise внутри except | есть | нет | False | During handling of the above exception… (При обработке вышеупомянутого исключения…) |
raise … from e | есть | есть | True | The above exception was the direct cause… (Вышеупомянутое исключение было прямой причиной…) |
raise … from None | есть | нет | True | связующей фразы нет |
Третья строка — самая полезная. from None не удаляет предыдущее
исключение: __context__ остаётся на месте, меняется только
__suppress_context__ — флаг, по которому трассировка предыдущее исключение
не печатает.
Практически это значит, что from None — про читаемость сообщения, а не про
сокрытие данных. Предыдущее исключение по-прежнему достижимо из объекта, и в
обработчике верхнего уровня его можно записать в лог.
Ещё одна деталь, и уже не из PEP 3134, а из справочника языка: имя из
except … as e удаляется в конце блока.
Справочник даёт точную расшифровку — это как если бы тело было обёрнуто в
try: … finally: del N. Обращение к e после блока даёт NameError; чтобы
сохранить объект, его надо присвоить другому имени.
Механизм 6: группы — чем except* отличается от except
PEP 654 (3.11) добавил тип ExceptionGroup и синтаксис except* для одной
задачи: бросить и обработать несколько независимых ошибок сразу. Типичный
источник — параллельные задачи, где каждая упала по-своему. Именно это и
отдаёт asyncio.TaskGroup, и цепочка тут прямая:
TaskGroupотменяет соседей и собирает их ошибки;- отдаёт он их одним объектом —
ExceptionGroup, внутри которого несколько независимых исключений; except*разбирает группу по типам, и каждая ветка выполняется не более одного раза.
Разбор TaskGroup — в уроке про async/await; здесь разбирается вторая
половина цепочки. Всё ниже проверено запуском.
Группа — это отдельный объект, а не её содержимое. ExceptionGroup не
«раскрывается» в свои ошибки: обычный except ValueError: не поймает группу,
даже если внутри ровно один ValueError.
try:
raise ExceptionGroup("одна", [ValueError("одинокая")])
except ValueError:
print("сюда не попадём")
except ExceptionGroup as e:
print("поймали группу:", type(e).__name__) # → поймали группу: ExceptionGroupОбычный except Exception группу поймает, но целиком — одним объектом, а не
по одной ошибке.
except* разбирает группу по типам и может сработать несколько раз за один
raise. Каждая ветка получает подгруппу — снова ExceptionGroup — со
всеми листьями своего типа. PEP: a single exception group can cause several
(одна группа исключений может привести к выполнению нескольких предложений except* clauses to execute, but each such clause executes at most onceexcept*, но каждое из них выполняется не более одного раза).
try:
raise ExceptionGroup("две беды", [ValueError("значение"), KeyError("ключ")])
except* ValueError as eg:
print("ValueError:", [str(x) for x in eg.exceptions]) # → ValueError: ['значение']
except* KeyError as eg:
print("KeyError:", [str(x) for x in eg.exceptions]) # → KeyError: ["'ключ'"]
# за один raise сработали ОБЕ веткиВажное: переменная ветки (eg) — не одно исключение, а группа; сами ошибки
лежат в eg.exceptions.
Необработанный остаток летит дальше — снова группой. Если ветки разобрали не
всё, оставшиеся листья продолжают распространяться как ExceptionGroup, а не как
отдельные исключения:
try:
raise ExceptionGroup("три", [ValueError(), KeyError(), TypeError()])
except* ValueError:
pass
# наружу летит ExceptionGroup([KeyError(), TypeError()])Смешивать except и except* в одном try нельзя — это синтаксическая
ошибка. Ветки одного try либо все обычные, либо все со звёздочкой.
Глубже: сколько стоит try
Теперь то, о чём спорят без чисел: во что обходится сама конструкция.
| форма | 3.13.7 | 3.14.7 |
|---|---|---|
без try | 38,7 нс | 34,5 нс |
try/except, не сработал | 40,5 нс | 36,9 нс |
try/except, сработал | 201,1 нс | 189,5 нс |
Колонки читаются по отдельности. Сборки 3.13.7 и 3.14.7 различаются не только компилятором, и разрыв между колонками — не разница между версиями языка.
Разницу между первыми двумя строками надо сперва сопоставить с разбросом самого прибора — и прогон это делает сам, повторяя весь замер пять раз:
РАЗМАХ ПО 5 ПОВТОРАМ ВСЕГО ЗАМЕРА
без try 38.7 .. 39.1 нс размах 0.4
try/except, не сработал 40.5 .. 40.9 нс размах 0.4
try/except, сработал 201.1 .. 203.4 нс размах 2.3
разница между первыми двумя строками 1.8 нс
наибольший размах этих же двух строк 0.4 нс
различима ли разница прибором да
Различима: 1,8 нс против размаха 0,4. Значит, утверждение здесь не
«неотличимо», а более узкое и более полезное: try, который не сработал,
стоит около двух наносекунд — пять процентов, — и знак этой разницы
устойчив, во всех пяти повторах вариант с try чуть медленнее.
Вопрос из-за этого меняется. Он уже не «есть ли разница» — есть, — а «стоит ли
она того», и это вопрос к величине. Пять процентов на пустом вызове не окупают
ни одной перестройки кода: чтобы они дали заметный выигрыш, try должен стоять
в цикле, который сам по себе и есть узкое место.
Почему так, видно в байт-коде. SETUP_FINALLY описана в документации dis
среди псевдоинструкций, и про них сказано прямо: They do not appear in
Python bytecode. They are used by the compiler but are replaced by real opcodes
or removed before bytecode is generated
(Их нет в байт-коде Python. Компилятор ими пользуется, но до создания байт-кода заменяет их настоящими инструкциями или убирает). Проверено дизассемблером на всех
четырёх версиях — SETUP_FINALLY нет ни в одной.
Вот что реально исполняется:
без try: RESUME LOAD_NAME PUSH_NULL CALL POP_TOP
в try: RESUME NOP LOAD_NAME PUSH_NULL CALL POP_TOP
Разница — один NOP. Куда делась обработка? В отдельную таблицу: дизассемблер
печатает её блоком ExceptionTable под кодом. Пока исключения нет, таблицу
никто не читает.
И здесь важно назвать границу наблюдения, а не только число. Замерялся один
пустой вызов в цикле, на двух сборках CPython, на одной машине. Отсюда следует
ровно одно: в этом замере на успешном пути try стоит около двух
наносекунд — а не «try бесплатен» вообще и не «try можно ставить куда
угодно без последствий». Вложенные обработчики, finally с работой
внутри, try внутри генератора замером не покрыты.
Поэтому практический вывод пишется от границы, а не от числа: аргумент
«уберём try, он тормозит» ничем не подкреплён — если он звучит, просят
замер, а не убирают конструкцию. Дорого стоит сработавшее исключение —
впятеро. Вот его в горячем цикле как штатный способ ветвления использовать и
не стоит.
Глубже: что изменилось в 3.14
Компилятор начал предупреждать о return и break, выходящих из finally.
Проверено компиляцией:
3.11.15 предупреждений: 0
3.12.3 предупреждений: 0
3.13.7 предупреждений: 0
3.14.7 SyntaxWarning: 'return' in a 'finally' block
Для break — своё сообщение, 'break' in a 'finally' block.
Поведение при этом не изменилось. PEP 765 (Ирит Катриэль, Алисса Кофлан)
говорит об этом прямо: we leave it open whether, and when, this will become a
(мы оставляем открытым вопрос, станет ли это когда-нибудь SyntaxErrorSyntaxError и когда именно). Код, написанный так, работает ровно как раньше — просто теперь
об этом сообщают. Причина осторожности названа в самом PEP: прогоны с -We
превращают предупреждения в ошибки, и немедленный запрет сломал бы их.
Глубже: история версий
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| 3.0 | PEP 3134 вводит __context__ и __cause__. Цепочка появляется сама, без участия автора кода; raise … from задаёт её явно. | |
| 3.11 | PEP 654: ExceptionGroup и except*. Проверено дизассемблером: на всех четырёх версиях, 3.11–3.14, SETUP_FINALLY в байт-коде уже нет, и стоимость не сработавшего try в замере не выходит за разброс. | |
| 3.14 | PEP 765: компилятор предупреждает о return, break и continue, выходящих из finally. Поведение прежнее — исключение по-прежнему отбрасывается молча. |
Как отвечать на собеседовании
Короткий ответ: try — попытка, except — обработка ошибки, else —
ветка «ошибки не было», finally — выход в любом случае. И главное про
finally: если он сам передаёт управление наружу — return, break,
continue, — летящее исключение исчезает. Справочник формулирует это одним
предложением: «сохранённое исключение отбрасывается».
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
Первое — показать, как это выглядит в настоящем коде: return в finally
выглядит как забота о ресурсе, а на деле выходит из функции на первой же
ошибке; те же строки, что в уроке про контекстные менеджеры, дают 100 вместо
175 без единого исключения в логе. С 3.14 компилятор об этом предупреждает
(PEP 765), но только предупреждает: поведение осталось прежним.
Второе — голый except: ловит BaseException, то есть Ctrl+C и
sys.exit(); иерархия сделана именно так, чтобы except Exception: их
пропускал.
Третье — если разговор ушёл в скорость, назвать замер вместе с его границей:
на успешном пути дополнительная стоимость try в этом замере — около двух
наносекунд (40,5 нс против 38,7 у того же кода без try — пять процентов, и
это больше размаха прибора, 0,4 нс), а сработавшее исключение стоит 201,1 нс — впятеро дороже. Причина
структурная: SETUP_FINALLY — псевдоинструкция, в байт-код она не попадает, и
на счастливом пути try добавляет один NOP. Вывод отсюда узкий: убирать
try ради скорости незачем, а использовать исключение как штатное ветвление
в горячем цикле — есть за что.
Дальше спросят
return внутри finally — что произойдёт с летящим исключением?
Оно исчезнет, и функция вернёт значение как при обычном выходе. Одно слово — и функция перестаёт падать вовсе, включая те исключения, которых автор не имел в виду.
Когда except*, а когда обычный except?
except* нужен там, где исключений может прилететь несколько сразу и потерять
любое из них нельзя, — то есть там же, откуда взялись группы. Обычный except
разбирает одно исключение и ветку выбирает одну.
Частые заблуждения
try/except замедляет код, лучше проверять условием
В этом замере на успешном пути дополнительная стоимость около двух наносекунд: 38,7 нс без try против 40,5 нс с ним (3.13.7) — пять процентов, устойчивых по знаку во всех пяти повторах замера, при размахе прибора 0,4 нс. Причина структурная: SETUP_FINALLY — псевдоинструкция и в байт-код не попадает, на счастливом пути добавляется один NOP. Это не значит «try бесплатен вообще»: значит, что аргумент «уберём try, он тормозит» надо подкреплять замером, а не интуицией. Дорого стоит СРАБОТАВШЕЕ исключение: 201,1 нс, впятеро.
return в finally просто перекрывает возвращаемое значение
Он ещё и уничтожает исключение. Справочник: the saved exception is discarded
(сохранённое исключение отбрасывается). Функция с return в finally не может упасть вообще — проверено запуском: raise RuntimeError внутри try наружу не выходит, вызов возвращает None. То же делают break и continue.
в 3.14 return в finally запретили
Предупредили, а не запретили. PEP 765: we leave it open whether, and when, this will become a
(мы оставляем открытым вопрос, станет ли это когда-нибудь SyntaxErrorSyntaxError и когда именно). Проверено компиляцией: на 3.14 появляется SyntaxWarning: 'return' in a 'finally' block, на 3.11–3.13 не появляется ничего, а работает код во всех четырёх одинаково.
else в try — синтаксический сахар, можно дописать в конец try
Нельзя без изменения смысла. Справочник: Exceptions in the
(исключения в предложении else clause are not handled by the preceding except clauseselse не обрабатываются предшествующими предложениями except). Перенеся строку из else в try, вы отдаёте её исключения тому же обработчику — и KeyError из вложенного вызова станет неотличим от KeyError, ради которого except писали.
голый except: — то же, что except Exception:
Он ловит BaseException, то есть KeyboardInterrupt и SystemExit. Иерархия сделана так намеренно — документация: inherits from
(наследуется от BaseException so as to not be accidentally caught by code that catches ExceptionBaseException, чтобы его случайно не поймал код, ловящий Exception). Голый except: снимает эту защиту, и программа перестаёт закрываться по Ctrl+C.
raise ... from None стирает предыдущее исключение
Не стирает. Проверено: после from None у объекта __context__ по-прежнему ZeroDivisionError, меняется только __suppress_context__ — то есть трассировка перестаёт его печатать. Предыдущее исключение остаётся достижимым и его можно записать в лог на верхнем уровне.
переменную из except ... as e можно использовать после блока
Нельзя: она удаляется. Справочник даёт точную расшифровку — тело except оборачивается в try: … finally: del N. Обращение после блока даёт NameError: name 'e' is not defined. Чтобы сохранить объект, присвойте его другому имени внутри блока.
ExceptionGroup из одного исключения ведёт себя как это исключение
Нет. ExceptionGroup("одна", [ValueError()]) обычным except ValueError: не ловится — проверено запуском, срабатывает except ExceptionGroup. Группа остаётся группой независимо от размера, и разбирать её нужно через except*.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
def swallow():
try:
raise ValueError("boom")
finally:
return "ok"
def keep():
try:
raise ValueError("boom")
finally:
pass
print(swallow())
try:
keep()
except ValueError as e:
print("ValueError:", e)Практика · оцените
Проверка знаний
Что вернёт функция, у которой в try стоит raise RuntimeError, а в finally — return None?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- У инструкции
tryчетыре части, и у каждой своя роль:try— попытка,except— обработка ошибки,else— ветка «ошибки не было»,finally— выход в любом случае. Проfinallyважно одно: он выполняется и после удачи, и после ошибки, и это ровно то, ради чего его пишут. - Отсюда главное следствие:
finally, который сам передаёт управление наружу, уничтожает летящее исключение.returnвfinally— справочник формулирует это одним предложением: the saved exception is discarded, — и то же делаютbreakиcontinue. Отсюда тихая потеря данных: те же строки, что в уроке про контекстные менеджеры, дают 100 вместо 175 — другой механизм, тот же неверный ответ, и ни единого исключения в логе. Рядом стоит вторая ловушка: голыйexcept:ловитBaseException, то естьCtrl+Cиsys.exit(). - Дальше — то, что отличает знающего от читавшего. Стоимость
try/except, который не сработал, около двух наносекунд: 40,5 нс против 38,7 у того же кода безtry(3.13.7). Это пять процентов, и разница различима — размах прибора 0,4 нс, — но величина такая, что ради неёtryне убирают. Сработавший — 201,1 нс, впятеро дороже. Причина структурная, а не случайная:SETUP_FINALLY— псевдоинструкция, в байт-код она не попадает, и на счастливом путиtryдобавляет одинNOP. С 3.14 компилятор предупреждает оreturnвfinally(PEP 765) — но только предупреждает: поведение осталось прежним.
На самом деле
- В этом замере на успешном пути дополнительная стоимость около двух наносекунд: 38,7 нс без
tryпротив 40,5 нс с ним (3.13.7) — пять процентов, устойчивых по знаку во всех пяти повторах замера, при размахе прибора 0,4 нс. Причина структурная:SETUP_FINALLY— псевдоинструкция и в байт-код не попадает, на счастливом пути добавляется одинNOP. Это не значит «tryбесплатен вообще»: значит, что аргумент «уберёмtry, он тормозит» надо подкреплять замером, а не интуицией. Дорого стоит СРАБОТАВШЕЕ исключение: 201,1 нс, впятеро. - Он ещё и уничтожает исключение. Справочник: the saved exception is discarded. Функция с
returnвfinallyне может упасть вообще — проверено запуском:raise RuntimeErrorвнутриtryнаружу не выходит, вызов возвращаетNone. То же делаютbreakиcontinue. - Предупредили, а не запретили. PEP 765: we leave it open whether, and when, this will become a
SyntaxError. Проверено компиляцией: на 3.14 появляетсяSyntaxWarning: 'return' in a 'finally' block, на 3.11–3.13 не появляется ничего, а работает код во всех четырёх одинаково. - Нельзя без изменения смысла. Справочник: Exceptions in the
elseclause are not handled by the precedingexceptclauses. Перенеся строку изelseвtry, вы отдаёте её исключения тому же обработчику — иKeyErrorиз вложенного вызова станет неотличим отKeyError, ради которогоexceptписали. - Он ловит
BaseException, то естьKeyboardInterruptиSystemExit. Иерархия сделана так намеренно — документация: inherits fromBaseExceptionso as to not be accidentally caught by code that catchesException. Голыйexcept:снимает эту защиту, и программа перестаёт закрываться поCtrl+C. - Не стирает. Проверено: после
from Noneу объекта__context__по-прежнемуZeroDivisionError, меняется только__suppress_context__— то есть трассировка перестаёт его печатать. Предыдущее исключение остаётся достижимым и его можно записать в лог на верхнем уровне. - Нельзя: она удаляется. Справочник даёт точную расшифровку — тело
exceptоборачивается вtry: … finally: del N. Обращение после блока даётNameError: name 'e' is not defined. Чтобы сохранить объект, присвойте его другому имени внутри блока. - Нет.
ExceptionGroup("одна", [ValueError()])обычнымexcept ValueError:не ловится — проверено запуском, срабатываетexcept ExceptionGroup. Группа остаётся группой независимо от размера, и разбирать её нужно черезexcept*.
По версиям
- 3.0
- PEP 3134 вводит
__context__и__cause__. Цепочка появляется сама, без участия автора кода;raise … fromзадаёт её явно.< - 3.11
- PEP 654:
ExceptionGroupиexcept*. Проверено дизассемблером: на всех четырёх версиях, 3.11–3.14,SETUP_FINALLYв байт-коде уже нет, и стоимость не сработавшегоtryв замере не выходит за разброс.< - 3.14
- PEP 765: компилятор предупреждает о
return,breakиcontinue, выходящих изfinally. Поведение прежнее — исключение по-прежнему отбрасывается молча.<
Что разобрано
- База: четыре части, и у каждой своя роль
- Механизм 1: одно слово, после которого функция перестаёт падать
- Механизм 2: ошибка, которая не падает
- Механизм 3: `else` — зачем он, если можно просто дописать в `try`
- Механизм 4: голый `except:` ловит больше, чем вы думаете
- Механизм 5: цепочки — `__context__`, `__cause__` и `from None`
- Механизм 6: группы — чем `except*` отличается от `except`
- Глубже: сколько стоит `try`
- Глубже: что изменилось в 3.14
- Глубже: история версий
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Справочник языка — инструкция tryОфициальная документация. Два нормативных предложения, на которых держится половина урока. Про finally: «If the finally clause executes a return, break or continue statement, the saved exception is discarded» (Если предложение finally выполняет return, break или continue, сохранённое исключение отбрасывается). Про else: «The optional else clause is executed if the control flow leaves the try suite, no exception was raised… Exceptions in the else clause are not handled by the preceding except clauses» (Необязательное предложение else выполняется, если управление вышло из блока try и исключение не возникло… Исключения в предложении else не обрабатываются предшествующими предложениями except). Там же — что имя из `except … as N` удаляется в конце блока, с явной расшифровкой через try/finally: del N.https://docs.python.org/3.14/reference/compound_stmts.html#the-try-statement
- PEP 765 — Disallow return/break/continue that exit a finally blockPEP. Ирит Катриэль и Алисса Кофлан, статус Final, Python 3.14. Компилятор с 3.14 предупреждает — но именно предупреждает: «we leave it open whether, and when, this will become a SyntaxError» (мы оставляем открытым вопрос, станет ли это когда-нибудь SyntaxError и когда именно). То есть код продолжает работать по-старому, просто теперь об этом сообщают.https://peps.python.org/pep-0765/
- PEP 654 — Exception Groups and except*PEP. Ирит Катриэль, Юрий Селиванов, Гвидо ван Россум; Final, Python 3.11. Отсюда два из трёх свойств раздела про группы: «a single exception group can cause several except* clauses to execute, but each such clause executes at most once» (одна группа исключений может привести к выполнению нескольких предложений except*, но каждое из них выполняется не более одного раза) и «the remaining part of the group is propagated on» (оставшаяся часть группы передаётся дальше наверх). Третье — что группа из одного исключения не ловится except-ом по типу листа (except ValueError) — проверено запуском (
bench/exceptions/groups.py); нормативной формулировки в PEP для него нет.https://peps.python.org/pep-0654/ - PEP 3134 — Exception Chaining and Embedded TracebacksPEP. Ка-Пинг Йи, Final, Python 3.0. Документ, которым введены `__context__` и `__cause__`: первый ставится интерпретатором сам, второй — записью `raise EXCEPTION from CAUSE`, равносильной `exc.__cause__ = CAUSE`.https://peps.python.org/pep-3134/
- dis — SETUP_FINALLY среди псевдоинструкцийОфициальная документация. Инструкция описана в разделе «Pseudo-instructions», про который сказано прямо: «They do not appear in Python bytecode. They are used by the compiler but are replaced by real opcodes or removed before bytecode is generated» (Их нет в байт-коде Python. Компилятор ими пользуется, но до создания байт-кода заменяет их настоящими инструкциями или убирает). Это и объясняет замер: на счастливом пути `try` не выполняется ничего, кроме NOP.https://docs.python.org/3.14/library/dis.html
- Встроенные исключения — Exception против BaseExceptionОфициальная документация. «All built-in, non-system-exiting exceptions are derived from this class [Exception]» (Все встроенные исключения, не завершающие работу системы, происходят от этого класса [Exception]). И про KeyboardInterrupt дословно: «The exception inherits from BaseException so as to not be accidentally caught by code that catches Exception and thus prevent the interpreter from exiting» (Исключение наследуется от BaseException, чтобы его случайно не поймал код, ловящий Exception, и тем самым не помешал интерпретатору завершиться).https://docs.python.org/3.14/library/exceptions.html