[Python-ideas] A "local" pseudo-function

Tim Peters tim.peters at gmail.com
Fri Apr 27 22:37:53 EDT 2018


A brain dump, inspired by various use cases that came up during the
binding expression discussions.

Idea:  introduce a "local" pseudo-function to capture the idea of
initialized names with limited scope.

As an expression, it's

    "local" "(" arguments ")"

- Because it "looks like" a function call, nobody will expect the targets
  of named arguments to be fancier than plain names.

- `a=12` in the "argument" list will (& helpfully so) mean pretty much the
  same as "a=12" in a "def" statement.

- In a "local call" on its own, the scope of a named argument begins at the
  start of the next (if any) argument, and ends at the closing ")".  For the
  duration, any variable of the same name in an enclosing scope is shadowed.

- The parentheses allow for extending over multiple lines without needing
  to teach editors (etc) any new tricks (they already know how to format
  function calls with arglists spilling over multiple lines).

- The _value_ of a local "call" is the value of its last "argument".  In
  part, this is a way to sneak in C's comma operator without adding cryptic
  new line noise syntax.

Time for an example.  First a useless one:

a = 1
b = 2
c = local(a=3) * local(b=4)

Then `c` is 12, but `a` is still 1 and `b` is still 2.  Same thing in the end:

c = local(a=3, b=4, a*b)

And just to be obscure, also the same:

c = local(a=3, b=local(a=2, a*a), a*b)

There the inner `a=2` temporarily shadows the outer `a=3` just long
enough to compute `a*a` (4).

This is one that little else really handled nicely:

r1, r2 = local(D = b**2 - 4*a*c,
               sqrtD = math.sqrt(D),
               twoa = 2*a,
               ((-b + sqrtD)/twoa, (-b - sqrtD)/twoa))

Everyone's favorite:

if local(m = re.match(regexp, line)):
    print(m.group(0))

Here's where it's truly essential that the compiler know everything
about "local", because in _that_ context it's required that the new
scope extend through the end of the entire block construct (exactly
what that means TBD - certainly through the end of the `if` block, but
possibly also through the end of its associated (if any) `elif` and
`else` blocks - and similarly for while/else constructs).

Of course that example could also be written as:

if local(m = re.match(regexp, line), m):
    print(m.group(0))

or more specifically:

if local(m = re.match(regexp, line), m is not None):
    print(m.group(0))

or even:

if local(m = re.match(regexp, line)) is not None:
    print(m.group(0))

A listcomp example, building the squares of integers from an iterable
but only when the square is a multiple of 18:

squares18 = [i2 for i in iterable if local(i2=i*i) % 18 == 0]

That's a bit mind-bending, but becomes clear if you picture the
kinda-equivalent nest:

    for i in iterable:
        if local(i2=i*i) % 18 == 0:
            append i2 to the output list

That should also make clear that if `iterable` or `i` had been named
`i2` instead, no problem.  The `i2` created by `local()` is in a
wholly enclosed scope.

Drawbacks:  since this is just a brain dump, absolutely none ;-)

Q:  Some of those would be clearer if it were the more Haskell-like

    local(...) "in" expression

A: Yup, but for some of the others needing to add "in m" would be
annoyingly redundant noise.  Making an "in" clause optional doesn't
really fly either, because then

    local(a='z') in 'xyz'

would be ambiguous.  Is it meant to return `'xyz'`, or evaluate `'z'
in 'xyz'`?  And any connector other than "in" would make the loose
resemblance to Haskell purely imaginary ;-)

Q: Didn't you drone on about how assignment expressions with complex
targets seemed essentially useless without also introducing a "comma
operator" - and now you're sneaking the latter in but _still_ avoiding
complex targets?!

A. Yes, and yes :-)  The syntactic complexity of the fully general
assignment statement is just too crushing to _sanely_ shoehorn into
any "expression-like" context.

Q:  What's the value of this?   local(a=7, local(a=a+1, a*2))

A: 16.  Obviously.

Q:  Wow - that _is_ obvious!  OK, what about this, where there is no
`a` in any enclosing scope:  local(a)

A: I think it should raise NameError, just like a function call would.
There is no _intent_ here to allow merely declaring a local variable
without supplying an initial value.

Q: What about local(2, 4, 5)?

A: It should return 5, and introduce no names.  I don't see a point to
trying to outlaw stupidity ;-)  Then again, it would be consistent
with the _intent_ to require that all but the last "argument" be of
the `name=expression` form.

Q: Isn't changing the meaning of scope depending on context waaaay magical?

A: Yup!  But in a language with such a strong distinction between
statements and expressions, without a bit of deep magic there's no
single syntax I can dream up that could work well for both that didn't
require _some_ deep magic.  The gimmick here is something I expect
will be surprising the first time it's seen, less so the second, and
then you're never confused about it again.

Q: Are you trying to kill PEP 572?

A: Nope!  But since this largely subsumes the functionality of binding
expressions, I did want to put this out there before 572's fate is
history.  Binding expressions are certainly easier to implement, and I
like them just fine :-)


Note:  the thing I'm most interested in isn't debates, but in whether
this would be of real use in real code.


More information about the Python-ideas mailing list