PEP 572: Rework the alternatives section into separate categories

This commit is contained in:
Chris Angelico 2018-03-24 13:35:26 +11:00
parent d9d878ee6e
commit af7b76f545
1 changed files with 107 additions and 43 deletions

View File

@ -263,14 +263,78 @@ Both of these are forbidden; creating SLNBs in the headers of these statements
will result in a SyntaxError.
Alternative proposals and variants
==================================
Rejected alternative proposals
==============================
Proposals broadly similar to this one have come up frequently on python-ideas.
Below are a number of alternative syntaxes, some of them specific to
comprehensions, which have been rejected in favour of the one given above.
1. ``where``, ``let``, or ``given``, in comprehensions only::
Alternative spellings
---------------------
Broadly the same semantics as the current proposal, but spelled differently.
1. ``EXPR as NAME`` without parentheses::
stuff = [[f(x) as y, x/y] for x in range(5)]
Omitting the parentheses from this PEP's proposed syntax introduces many
syntactic ambiguities. Requiring them in all contexts leaves open the
option to make them optional in specific situations where the syntax is
unambiguous (cf generator expressions as sole parameters in function
calls), but there is no plausible way to make them optional everywhere.
2. Adorning statement-local names with a leading dot::
stuff = [[(f(x) as .y), x/.y] for x in range(5)]
This has the advantage that leaked usage can be readily detected, removing
some forms of syntactic ambiguity. However, this would be the only place
in Python where a variable's scope is encoded into its name, making
refactoring harder. This syntax is quite viable, and could be promoted to
become the current recommendation if its advantages are found to outweigh
its cost.
3. Adding a ``where:`` to any statement to create local name bindings::
value = x**2 + 2*x where:
x = spam(1, 4, 7, q)
Execution order is inverted (the indented body is performed first, followed
by the "header"). This requires a new keyword, unless an existing keyword
is repurposed (most likely ``with:``).
Special-casing conditional statements
-------------------------------------
One of the most popular use-cases is ``if`` and ``while`` statements. Instead
of a more general solution, this proposal enhances the syntax of these two
statements to add a means of capturing the compared value::
if re.search(pat, text) as match:
print("Found:", match.group(0))
This works beautifully if and ONLY if the desired condition is based on the
truthiness of the captured value. It is thus effective for specific
use-cases (regex matches, socket reads that return `''` when done), and
completely useless in more complicated cases (eg where the condition is
``f(x) < 0`` and you want to capture the value of ``f(x)``). It also has
no benefit to list comprehensions.
Advantages: No syntactic ambiguities. Disadvantages: Answers only a fraction
of possible use-cases, even in ``if``/``while`` statements.
Special-casing comprehensions
-----------------------------
Another common use-case is comprehensions (list/set/dict, and genexps). As
above, proposals have been made for comprehension-specific solutions.
1. ``where``, ``let``, or ``given``::
stuff = [(y, x/y) where y = f(x) for x in range(5)]
stuff = [(y, x/y) let y = f(x) for x in range(5)]
@ -304,58 +368,58 @@ comprehensions, which have been rejected in favour of the one given above.
completely different from the meaning as a stand-alone statement, while
retaining identical syntax.
4. ``EXPR as NAME`` without parentheses::
Regardless of the spelling chosen, this introduces a stark difference between
comprehensions and the equivalent unrolled long-hand form of the loop. It is
no longer possible to unwrap the loop into statement form without reworking
any name bindings. The only keyword that can be repurposed to this task is
``with``, thus giving it sneakily different semantics in a comprehension than
in a statement; alternatively, a new keyword is needed, with all the costs
therein.
stuff = [[f(x) as y, x/y] for x in range(5)]
Assignment expressions
----------------------
Omitting the parentheses from this PEP's proposed syntax introduces many
syntactic ambiguities. Requiring them in all contexts leaves open the
option to make them optional in specific situations where the syntax is
unambiguous (cf generator expressions as sole parameters in function
calls), but there is no plausible way to make them optional everywhere.
Rather than creating a statement-local name, these forms of name binding have
the exact same semantics as regular assignment: bind to a local name unless
there's a ``global`` or ``nonlocal`` declaration.
5. Adorning statement-local names with a leading dot::
Syntax options:
stuff = [[(f(x) as .y), x/.y] for x in range(5)]
1. ``(EXPR as NAME)`` as per the promoted proposal
This has the advantage that leaked usage can be readily detected, removing
some forms of syntactic ambiguity. However, this would be the only place
in Python where a variable's scope is encoded into its name, making
refactoring harder. This syntax is quite viable, and could be promoted to
become the current recommendation if its advantages are found to outweigh
its cost.
2. C-style ``NAME = EXPR`` in any context
6. Allowing ``(EXPR as NAME)`` to assign to any form of name.
3. A new and dedicated operator with C-like semantics ``NAME := EXPR``
This is exactly the same as the promoted proposal, save that the name is
bound in the same scope that it would otherwise have. Any expression can
assign to any name, just as it would if the ``=`` operator had been used.
Such variables would leak out of the statement into the enclosing function,
subject to the regular behaviour of comprehensions (since they implicitly
create a nested function, the name binding would be restricted to the
comprehension itself, just as with the names bound by ``for`` loops).
The C syntax has been long known to be a bug magnet. The syntactic similarity
between ``if (x == y)`` and ``if (x = y)`` belies their drastically different
semantics. While this can be mitigated with good tools, such tools would need
to be deployed for Python, and even with perfect tooling, one-character bugs
will still happen. Creating a new operator mitigates this, but creates a
disconnect between regular assignment statements and these new assignment
expressions, or would result in the old syntax being a short-hand usable in
certain situations only.
7. Enhancing ``if`` and ``while`` syntax to permit the capture of their
conditions::
Regardless of the syntax, all of these have the problem that wide-scope names
can be assigned to from an expression. This creates strange edge cases and
unexpected behaviour, such as::
if re.search(pat, text) as match:
print("Found:", match.group(0))
# Name bindings inside list comprehensions usually won't leak
x = [(y as local) for z in iter]
# But occasionally they will!
x = [y for z in (iter as leaky)]
This works beautifully if and ONLY if the desired condition is based on the
truthiness of the captured value. It is thus effective for specific
use-cases (regex matches, socket reads that return `''` when done), and
completely useless in more complicated cases (eg where the condition is
``f(x) < 0`` and you want to capture the value of ``f(x)``). It also has
no benefit to list comprehensions.
# Function default arguments are evaluated in the surrounding scope,
# not the enclosing scope
def x(y = (1 as z)):
# z here is closing over the outer variable
# z is a regular variable here
8. Adding a ``where:`` to any statement to create local name bindings::
# Assignment targets are evaluated after the values to be assigned
x[y] = f((1 as y))
value = x**2 + 2*x where:
x = spam(1, 4, 7, q)
Execution order is inverted (the indented body is performed first, followed
by the "header"). This requires a new keyword, unless an existing keyword
is repurposed (most likely ``with:``).
The same peculiarities can be seen with function calls and global/nonlocal
declarations, but will become considerably more likely to occur.
Discrepancies in the current implementation