Sublocal scoping at its simplest

There's been a lot of talk about sublocal scopes, within and without the context of PEP 572. I'd like to propose what I believe is the simplest form of sublocal scopes, and use it to simplify one specific special case in Python. There are no syntactic changes, and only a very slight semantic change. def f(): e = 2.71828 try: 1/0 except Exception as e: print(e) print(e) f() The current behaviour of the 'except... as' statement is as follows: 1) Bind the caught exception to the name 'e', replacing 2.71828 2) Execute the suite (printing "Division by zero") 3) Set e to None 4) Unbind e Consequently, the final print call raises UnboundLocalError. I propose to change the semantics as follows: 1) Bind the caught exception to a sublocal 'e' 2) Execute the suite, with the reference to 'e' seeing the sublocal 3) Set the sublocal e to None 4) Unbind the sublocal e At the unindent, the sublocal name will vanish, and the original 'e' will reappear. Thus the final print will display 2.71828, just as it would if no exception had been raised. The above definitions would become language-level specifications. For CPython specifically, my proposed implementation would be for the name 'e' to be renamed inside the block, creating a separate slot with the same name. With no debates about whether "expr as name" or "name := expr" or "local(name=expr)" is better, hopefully we can figure out whether sublocal scopes are themselves a useful feature :) ChrisA

On Sun, Apr 29, 2018 at 01:14:54PM +1000, Chris Angelico wrote: [...]
What problem does this solve? The current behaviour where 'e' is unbound when the except clause finishes is a neccessary but ugly hack that forces you to do bind 'e' to another variable if you want to inspect it after the exception: try: something() except Exception as e: err = e # defeat the automatic deletion of e print(e) For example, in the interactive interpreter, where I do this very frequently. I understand and accept the reasons for deleting e in Python 3, and don't wish to re-debate those. But regardless of whether we have Python 3 behaviour or Python 2 behaviour, binding to e has always replaced the value of e. Just as if I had written: except Exception as some_other_name: e = some_other_name It has never been the case that binding to e in the except clause won't replace any existing binding to e, and I see no reason why anyone would desire that. If you don't want to replace e, then don't use e as the name for the exception. Your proposal doesn't solve any known problem that I can see. For people like me who want to inspect the error object outside the except clause, we still have to defeat the compiler, so you're not solving anything for me. You're not solving any problems for those people who desire (for some reason) that except clauses are their own scope. It is only the exception variable itself which is treated as a special case. The one use-case you give is awfully dubious: if I wanted 'e' to keep its value from before the exception, why on earth would I rebind 'e' when there are approximately a zillion alternative names I could use? Opportunities for confusion should be obvious: e = 2.71 x = 1 try: ... except Exception as e: assert isinstance(e, Exception) x = 2 assert isinstance(e, Exception) # why does this fail? assert x != 2 # why does this fail? Conceptually, this is even more more complex than the idea of giving the except block its own scope. The block shares the local scope, it's just the block header (the except ... as ... line itself) which introduces a new scope. -- Steve

On 29 April 2018 at 13:14, Chris Angelico <rosuav@gmail.com> wrote:
The challenge with doing this implicitly is that there's no indication whatsoever that the two "e"'s are different, especially given the longstanding precedent that the try/except level one will overwrite any existing reference in the local namespace. By contrast, if the sublocal marker could be put on the *name itself*, then: 1. Sublocal names are kept clearly distinct from ordinary names 2. Appropriate sublocal semantics can be defined for any name binding operation, not just exception handlers 3. When looking up a sublocal for code compiled in exec or eval mode, missing names can be identified and reported at compile time (just as they can be for nonlocal declarations) (Such a check likely wouldn't be possible for code compiled in "single" mode, although working out a suitable relationship between sublocal scoping and the interactive prompt is likely to prove tricky no matter what) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Apr 29, 2018 at 6:03 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
My intention is that the "except" statement IS the indication that they're different. Now that the name gets unbound at the exit of the clause, the only indication that it overwrites is that, after "except Exception as e:", any previous e has been disposed of. I'd hardly call that a feature. Can you show me code that actually DEPENDS on this behaviour?
I'm aware of this, but that gets us right back to debating syntax, and I'm pretty sure death and syntaxes are the two things that we can truly debate forever. :) ChrisA

On 29 April 2018 at 21:24, Chris Angelico <rosuav@gmail.com> wrote:
That's not the bar the proposal needs to meet, though: it needs to meet the bar of being *better* than the status quo of injecting an implicit "del e" at the end of the suite. While the status quo isn't always convenient, it has two main virtues: 1. It's easily explained in terms of the equivalent "del" statement 2. Given that equivalence, it's straightforward to avoid the unwanted side effects by either adjusting your exact choices of names (if you want to avoid overwriting an existing name), or else by rebinding the caught exception to a different name (if you want to avoid the exception reference getting dropped). I do agree that *if* sublocal scopes existed, *then* they would offer a reasonable implementation mechanism for block-scoped name binding in exception handlers. However, exception handlers don't offer a good motivation for *adding* sublocal scopes, simply because the simpler "implicitly unbind the name at the end of the block" approach works well enough in practice. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sat, Apr 28, 2018, 20:16 Chris Angelico <rosuav@gmail.com> wrote:
Does this mean indentation is now a scope, or colons are a scope, or is that over simplifying? either seems to be more consistent with the patterns set by class and function defs, barring keywords. not sure if relevant but curious. I think with sublocal scope, reuse of a name makes more sense. Currently, if using sensible, descriptive names, it really doesn't make sense to go from food = apple to food = car as the value between scopes, but it happens. And if from fruit = apple to fruit = orange (eg appending a msg to a base string) it _could_ be nice to restore to apple once finished. Obviously that's simple enough to do now, I am only illustrating my point. I know bad code can be written with anything, this is not my point. It can be seen as enforcing that, what every nonsense someone writes like fruit=car, there is at least some continuity of information represented by the name... till they do it again once out of the sublocal scope of course. as for the value of this use case, I do not know.

On Mon, Apr 30, 2018 at 6:20 PM, Matt Arcidy <marcidy@gmail.com> wrote:
Does this mean indentation is now a scope, or colons are a scope, or is that over simplifying?
No, no, and yes. This is JUST about the 'except' statement, which currently has the weird effect of unbinding the name it just bound. ChrisA

On Sun, Apr 29, 2018 at 01:14:54PM +1000, Chris Angelico wrote: [...]
What problem does this solve? The current behaviour where 'e' is unbound when the except clause finishes is a neccessary but ugly hack that forces you to do bind 'e' to another variable if you want to inspect it after the exception: try: something() except Exception as e: err = e # defeat the automatic deletion of e print(e) For example, in the interactive interpreter, where I do this very frequently. I understand and accept the reasons for deleting e in Python 3, and don't wish to re-debate those. But regardless of whether we have Python 3 behaviour or Python 2 behaviour, binding to e has always replaced the value of e. Just as if I had written: except Exception as some_other_name: e = some_other_name It has never been the case that binding to e in the except clause won't replace any existing binding to e, and I see no reason why anyone would desire that. If you don't want to replace e, then don't use e as the name for the exception. Your proposal doesn't solve any known problem that I can see. For people like me who want to inspect the error object outside the except clause, we still have to defeat the compiler, so you're not solving anything for me. You're not solving any problems for those people who desire (for some reason) that except clauses are their own scope. It is only the exception variable itself which is treated as a special case. The one use-case you give is awfully dubious: if I wanted 'e' to keep its value from before the exception, why on earth would I rebind 'e' when there are approximately a zillion alternative names I could use? Opportunities for confusion should be obvious: e = 2.71 x = 1 try: ... except Exception as e: assert isinstance(e, Exception) x = 2 assert isinstance(e, Exception) # why does this fail? assert x != 2 # why does this fail? Conceptually, this is even more more complex than the idea of giving the except block its own scope. The block shares the local scope, it's just the block header (the except ... as ... line itself) which introduces a new scope. -- Steve

On 29 April 2018 at 13:14, Chris Angelico <rosuav@gmail.com> wrote:
The challenge with doing this implicitly is that there's no indication whatsoever that the two "e"'s are different, especially given the longstanding precedent that the try/except level one will overwrite any existing reference in the local namespace. By contrast, if the sublocal marker could be put on the *name itself*, then: 1. Sublocal names are kept clearly distinct from ordinary names 2. Appropriate sublocal semantics can be defined for any name binding operation, not just exception handlers 3. When looking up a sublocal for code compiled in exec or eval mode, missing names can be identified and reported at compile time (just as they can be for nonlocal declarations) (Such a check likely wouldn't be possible for code compiled in "single" mode, although working out a suitable relationship between sublocal scoping and the interactive prompt is likely to prove tricky no matter what) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Apr 29, 2018 at 6:03 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
My intention is that the "except" statement IS the indication that they're different. Now that the name gets unbound at the exit of the clause, the only indication that it overwrites is that, after "except Exception as e:", any previous e has been disposed of. I'd hardly call that a feature. Can you show me code that actually DEPENDS on this behaviour?
I'm aware of this, but that gets us right back to debating syntax, and I'm pretty sure death and syntaxes are the two things that we can truly debate forever. :) ChrisA

On 29 April 2018 at 21:24, Chris Angelico <rosuav@gmail.com> wrote:
That's not the bar the proposal needs to meet, though: it needs to meet the bar of being *better* than the status quo of injecting an implicit "del e" at the end of the suite. While the status quo isn't always convenient, it has two main virtues: 1. It's easily explained in terms of the equivalent "del" statement 2. Given that equivalence, it's straightforward to avoid the unwanted side effects by either adjusting your exact choices of names (if you want to avoid overwriting an existing name), or else by rebinding the caught exception to a different name (if you want to avoid the exception reference getting dropped). I do agree that *if* sublocal scopes existed, *then* they would offer a reasonable implementation mechanism for block-scoped name binding in exception handlers. However, exception handlers don't offer a good motivation for *adding* sublocal scopes, simply because the simpler "implicitly unbind the name at the end of the block" approach works well enough in practice. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sat, Apr 28, 2018, 20:16 Chris Angelico <rosuav@gmail.com> wrote:
Does this mean indentation is now a scope, or colons are a scope, or is that over simplifying? either seems to be more consistent with the patterns set by class and function defs, barring keywords. not sure if relevant but curious. I think with sublocal scope, reuse of a name makes more sense. Currently, if using sensible, descriptive names, it really doesn't make sense to go from food = apple to food = car as the value between scopes, but it happens. And if from fruit = apple to fruit = orange (eg appending a msg to a base string) it _could_ be nice to restore to apple once finished. Obviously that's simple enough to do now, I am only illustrating my point. I know bad code can be written with anything, this is not my point. It can be seen as enforcing that, what every nonsense someone writes like fruit=car, there is at least some continuity of information represented by the name... till they do it again once out of the sublocal scope of course. as for the value of this use case, I do not know.

On Mon, Apr 30, 2018 at 6:20 PM, Matt Arcidy <marcidy@gmail.com> wrote:
Does this mean indentation is now a scope, or colons are a scope, or is that over simplifying?
No, no, and yes. This is JUST about the 'except' statement, which currently has the weird effect of unbinding the name it just bound. ChrisA
participants (5)
-
Chris Angelico
-
Greg Ewing
-
Matt Arcidy
-
Nick Coghlan
-
Steven D'Aprano