On Tue, Nov 17, 2020 at 9:44 PM Steven D'Aprano <st...@pearwood.info> 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

I don't understand why (a) the "e = None; del e" part is duplicated,
nor (b) why the RERAISE opcodes are there in two branches, but I guess
it works out best to be explicit in there?

Anyhow. You say that this can't come up very often because people can
go a long time without asking, but the trouble is that there are two
false interpretations that are both extremely close - either that
"except E as e:" is similar to "with E as e:", or that the except
clause creates its own scope. It's entirely possible to see supporting
evidence for your own wrong assumption and never actually know the
truth. Maybe this is going to be the next "Python has call-by-value"
vs "Python has call-by-reference" debate?

ChrisA
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/6NKGXWLRX3SD4JQDFCOR43TAXREC33GD/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to