On Tue, Nov 17, 2020 at 9:44 PM Steven D'Aprano <[email protected]> wrote:
`try...except` is no different.
...
The only wrinkle in the case of `try...except` is that the error
variable is deleted, even if it wasn't actually used. If you look at the
byte-code generated, each compound try...except with an exception
variable is followed by the equivalent of:
err = None
del err
There really ought to be a FAQ about this, but it has something to do
with the exception object forming a long-lasting reference cycle. To
avoid that, the error variable is nuked on leaving the compound block.
That's a much bigger wrinkle than it might seem at first, though, and
I agree, this is a quite literal frequently-asked-question and should
be made clear somewhere. The except clause is special in that, if you
want the exception afterwards, you have to reassign it to another
variable; but it doesn't ACTUALLY introduce a subscope, despite kinda
looking like it does.
Interestingly, Python 3.10 has a very odd disassembly:
def f():
... try: g()
... except Exception as e:
... print(e)
...
import dis
dis.dis(f)
2 0 SETUP_FINALLY 10 (to 12)
2 LOAD_GLOBAL 0 (g)
4 CALL_FUNCTION 0
6 POP_TOP
8 POP_BLOCK
10 JUMP_FORWARD 44 (to 56)
3 >> 12 DUP_TOP
14 LOAD_GLOBAL 1 (Exception)
16 JUMP_IF_NOT_EXC_MATCH 54
18 POP_TOP
20 STORE_FAST 0 (e)
22 POP_TOP
24 SETUP_FINALLY 20 (to 46)
4 26 LOAD_GLOBAL 2 (print)
28 LOAD_FAST 0 (e)
30 CALL_FUNCTION 1
32 POP_TOP
34 POP_BLOCK
36 POP_EXCEPT
38 LOAD_CONST 0 (None)
40 STORE_FAST 0 (e)
42 DELETE_FAST 0 (e)
44 JUMP_FORWARD 10 (to 56)
>> 46 LOAD_CONST 0 (None)
48 STORE_FAST 0 (e)
50 DELETE_FAST 0 (e)
52 RERAISE
>> 54 RERAISE
>> 56 LOAD_CONST 0 (None)
58 RETURN_VALUE
Reconstructing approximately equivalent Python code, this would mean
it looks something like this:
def f():
try: g()
except Exception as e:
try:
print(e)
e = None
del e
raise
finally:
e = None
del e
except:
raise
return None