2018-02-27 17:18:52 -05:00
|
|
|
|
PEP: 572
|
2018-04-08 21:40:59 -04:00
|
|
|
|
Title: Assignment Expressions
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Author: Chris Angelico <rosuav@gmail.com>, Tim Peters <tim.peters@gmail.com>,
|
|
|
|
|
Guido van Rossum <guido@python.org>
|
2018-07-11 20:54:47 -04:00
|
|
|
|
Status: Accepted
|
2018-02-27 17:18:52 -05:00
|
|
|
|
Type: Standards Track
|
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
|
Created: 28-Feb-2018
|
|
|
|
|
Python-Version: 3.8
|
2018-04-24 11:29:49 -04:00
|
|
|
|
Post-History: 28-Feb-2018, 02-Mar-2018, 23-Mar-2018, 04-Apr-2018, 17-Apr-2018,
|
2019-08-13 22:30:53 -04:00
|
|
|
|
25-Apr-2018, 09-Jul-2018, 05-Aug-2019
|
2018-07-11 20:54:47 -04:00
|
|
|
|
Resolution: https://mail.python.org/pipermail/python-dev/2018-July/154601.html
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
|
========
|
|
|
|
|
|
2018-04-18 09:05:10 -04:00
|
|
|
|
This is a proposal for creating a way to assign to variables within an
|
2019-08-13 22:30:53 -04:00
|
|
|
|
expression using the notation ``NAME := expr``.
|
|
|
|
|
|
|
|
|
|
As part of this change, there is also an update to dictionary comprehension
|
|
|
|
|
evaluation order to ensure key expressions are executed before value
|
|
|
|
|
expressions (allowing the key to be bound to a name and then re-used as part of
|
|
|
|
|
calculating the corresponding value).
|
|
|
|
|
|
|
|
|
|
During discussion of this PEP, the operator became informally known as
|
|
|
|
|
"the walrus operator". The construct's formal name is "Assignment Expressions"
|
|
|
|
|
(as per the PEP title), but they may also be referred to as "Named Expressions"
|
|
|
|
|
(e.g. the CPython reference implementation uses that name internally).
|
|
|
|
|
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
|
|
|
|
Rationale
|
|
|
|
|
=========
|
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
Naming the result of an expression is an important part of programming,
|
|
|
|
|
allowing a descriptive name to be used in place of a longer expression,
|
|
|
|
|
and permitting reuse. Currently, this feature is available only in
|
|
|
|
|
statement form, making it unavailable in list comprehensions and other
|
2018-05-22 15:49:00 -04:00
|
|
|
|
expression contexts.
|
2018-03-02 06:26:35 -05:00
|
|
|
|
|
2018-04-25 01:32:22 -04:00
|
|
|
|
Additionally, naming sub-parts of a large expression can assist an interactive
|
|
|
|
|
debugger, providing useful display hooks and partial results. Without a way to
|
|
|
|
|
capture sub-expressions inline, this would require refactoring of the original
|
|
|
|
|
code; with assignment expressions, this merely requires the insertion of a few
|
|
|
|
|
``name :=`` markers. Removing the need to refactor reduces the likelihood that
|
|
|
|
|
the code be inadvertently changed as part of debugging (a common cause of
|
2018-04-26 08:39:09 -04:00
|
|
|
|
Heisenbugs), and is easier to dictate to another programmer.
|
2018-04-25 01:32:22 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
The importance of real code
|
|
|
|
|
---------------------------
|
|
|
|
|
|
|
|
|
|
During the development of this PEP many people (supporters and critics
|
|
|
|
|
both) have had a tendency to focus on toy examples on the one hand,
|
|
|
|
|
and on overly complex examples on the other.
|
|
|
|
|
|
|
|
|
|
The danger of toy examples is twofold: they are often too abstract to
|
|
|
|
|
make anyone go "ooh, that's compelling", and they are easily refuted
|
|
|
|
|
with "I would never write it that way anyway".
|
|
|
|
|
|
|
|
|
|
The danger of overly complex examples is that they provide a
|
|
|
|
|
convenient strawman for critics of the proposal to shoot down ("that's
|
|
|
|
|
obfuscated").
|
|
|
|
|
|
|
|
|
|
Yet there is some use for both extremely simple and extremely complex
|
|
|
|
|
examples: they are helpful to clarify the intended semantics.
|
2021-02-03 09:06:23 -05:00
|
|
|
|
Therefore, there will be some of each below.
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
However, in order to be *compelling*, examples should be rooted in
|
|
|
|
|
real code, i.e. code that was written without any thought of this PEP,
|
|
|
|
|
as part of a useful application, however large or small. Tim Peters
|
|
|
|
|
has been extremely helpful by going over his own personal code
|
|
|
|
|
repository and picking examples of code he had written that (in his
|
|
|
|
|
view) would have been *clearer* if rewritten with (sparing) use of
|
|
|
|
|
assignment expressions. His conclusion: the current proposal would
|
|
|
|
|
have allowed a modest but clear improvement in quite a few bits of
|
|
|
|
|
code.
|
|
|
|
|
|
|
|
|
|
Another use of real code is to observe indirectly how much value
|
|
|
|
|
programmers place on compactness. Guido van Rossum searched through a
|
|
|
|
|
Dropbox code base and discovered some evidence that programmers value
|
|
|
|
|
writing fewer lines over shorter lines.
|
|
|
|
|
|
|
|
|
|
Case in point: Guido found several examples where a programmer
|
|
|
|
|
repeated a subexpression, slowing down the program, in order to save
|
|
|
|
|
one line of code, e.g. instead of writing::
|
|
|
|
|
|
|
|
|
|
match = re.match(data)
|
|
|
|
|
group = match.group(1) if match else None
|
|
|
|
|
|
|
|
|
|
they would write::
|
|
|
|
|
|
|
|
|
|
group = re.match(data).group(1) if re.match(data) else None
|
|
|
|
|
|
|
|
|
|
Another example illustrates that programmers sometimes do more work to
|
|
|
|
|
save an extra level of indentation::
|
|
|
|
|
|
|
|
|
|
match1 = pattern1.match(data)
|
|
|
|
|
match2 = pattern2.match(data)
|
|
|
|
|
if match1:
|
2018-07-10 14:19:10 -04:00
|
|
|
|
result = match1.group(1)
|
2018-05-22 15:49:00 -04:00
|
|
|
|
elif match2:
|
2018-07-10 14:19:10 -04:00
|
|
|
|
result = match2.group(2)
|
|
|
|
|
else:
|
|
|
|
|
result = None
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
This code tries to match ``pattern2`` even if ``pattern1`` has a match
|
|
|
|
|
(in which case the match on ``pattern2`` is never used). The more
|
|
|
|
|
efficient rewrite would have been::
|
|
|
|
|
|
|
|
|
|
match1 = pattern1.match(data)
|
|
|
|
|
if match1:
|
2018-07-10 14:19:10 -04:00
|
|
|
|
result = match1.group(1)
|
2018-05-22 15:49:00 -04:00
|
|
|
|
else:
|
|
|
|
|
match2 = pattern2.match(data)
|
|
|
|
|
if match2:
|
2018-07-10 14:19:10 -04:00
|
|
|
|
result = match2.group(2)
|
|
|
|
|
else:
|
|
|
|
|
result = None
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
|
|
|
|
Syntax and semantics
|
|
|
|
|
====================
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
In most contexts where arbitrary Python expressions can be used, a
|
|
|
|
|
**named expression** can appear. This is of the form ``NAME := expr``
|
|
|
|
|
where ``expr`` is any valid Python expression other than an
|
|
|
|
|
unparenthesized tuple, and ``NAME`` is an identifier.
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
|
|
|
|
The value of such a named expression is the same as the incorporated
|
2018-04-08 21:40:59 -04:00
|
|
|
|
expression, with the additional side-effect that the target is assigned
|
2018-04-12 09:42:53 -04:00
|
|
|
|
that value::
|
2018-03-23 14:10:41 -04:00
|
|
|
|
|
2018-04-12 09:39:05 -04:00
|
|
|
|
# Handle a matched regex
|
|
|
|
|
if (match := pattern.search(data)) is not None:
|
2018-07-10 14:19:10 -04:00
|
|
|
|
# Do something with match
|
2018-03-23 14:10:41 -04:00
|
|
|
|
|
2018-07-10 14:19:10 -04:00
|
|
|
|
# A loop that can't be trivially rewritten using 2-arg iter()
|
|
|
|
|
while chunk := file.read(8192):
|
|
|
|
|
process(chunk)
|
|
|
|
|
|
|
|
|
|
# Reuse a value that's expensive to compute
|
|
|
|
|
[y := f(x), y**2, y**3]
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
|
|
|
|
# Share a subexpression between a comprehension filter clause and its output
|
|
|
|
|
filtered_data = [y for x in data if (y := f(x)) is not None]
|
2018-03-02 04:05:31 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Exceptional cases
|
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
|
|
There are a few places where assignment expressions are not allowed,
|
|
|
|
|
in order to avoid ambiguities or user confusion:
|
|
|
|
|
|
|
|
|
|
- Unparenthesized assignment expressions are prohibited at the top
|
2018-07-09 00:35:44 -04:00
|
|
|
|
level of an expression statement. Example::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
y := f(x) # INVALID
|
2018-07-09 00:35:44 -04:00
|
|
|
|
(y := f(x)) # Valid, though not recommended
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
This rule is included to simplify the choice for the user between an
|
2018-07-10 14:02:30 -04:00
|
|
|
|
assignment statement and an assignment expression -- there is no
|
2018-05-22 15:49:00 -04:00
|
|
|
|
syntactic position where both are valid.
|
|
|
|
|
|
|
|
|
|
- Unparenthesized assignment expressions are prohibited at the top
|
2018-07-09 00:35:44 -04:00
|
|
|
|
level of the right hand side of an assignment statement. Example::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
y0 = y1 := f(x) # INVALID
|
2018-07-09 00:35:44 -04:00
|
|
|
|
y0 = (y1 := f(x)) # Valid, though discouraged
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
Again, this rule is included to avoid two visually similar ways of
|
|
|
|
|
saying the same thing.
|
|
|
|
|
|
|
|
|
|
- Unparenthesized assignment expressions are prohibited for the value
|
2018-07-09 00:35:44 -04:00
|
|
|
|
of a keyword argument in a call. Example::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
foo(x = y := f(x)) # INVALID
|
2018-07-09 00:35:44 -04:00
|
|
|
|
foo(x=(y := f(x))) # Valid, though probably confusing
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
This rule is included to disallow excessively confusing code, and
|
|
|
|
|
because parsing keyword arguments is complex enough already.
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-07-09 00:35:44 -04:00
|
|
|
|
- Unparenthesized assignment expressions are prohibited at the top
|
|
|
|
|
level of a function default value. Example::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
def foo(answer = p := 42): # INVALID
|
|
|
|
|
...
|
2018-07-09 00:35:44 -04:00
|
|
|
|
def foo(answer=(p := 42)): # Valid, though not great style
|
2018-05-22 15:49:00 -04:00
|
|
|
|
...
|
|
|
|
|
|
2018-07-09 00:35:44 -04:00
|
|
|
|
This rule is included to discourage side effects in a position whose
|
2018-05-22 15:49:00 -04:00
|
|
|
|
exact semantics are already confusing to many users (cf. the common
|
2018-07-09 00:35:44 -04:00
|
|
|
|
style recommendation against mutable default values), and also to
|
|
|
|
|
echo the similar prohibition in calls (the previous bullet).
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
2018-07-10 11:05:11 -04:00
|
|
|
|
- Unparenthesized assignment expressions are prohibited as annotations
|
|
|
|
|
for arguments, return values and assignments. Example::
|
|
|
|
|
|
|
|
|
|
def foo(answer: p := 42 = 5): # INVALID
|
|
|
|
|
...
|
|
|
|
|
def foo(answer: (p := 42) = 5): # Valid, but probably never useful
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
The reasoning here is similar to the two previous cases; this
|
|
|
|
|
ungrouped assortment of symbols and operators composed of ``:`` and
|
|
|
|
|
``=`` is hard to read correctly.
|
|
|
|
|
|
2018-07-10 19:38:31 -04:00
|
|
|
|
- Unparenthesized assignment expressions are prohibited in lambda functions.
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
(lambda: x := 1) # INVALID
|
|
|
|
|
lambda: (x := 1) # Valid, but unlikely to be useful
|
|
|
|
|
(x := lambda: 1) # Valid
|
|
|
|
|
lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
|
|
|
|
|
|
|
|
|
|
This allows ``lambda`` to always bind less tightly than ``:=``; having a
|
|
|
|
|
name binding at the top level inside a lambda function is unlikely to be of
|
|
|
|
|
value, as there is no way to make use of it. In cases where the name will be
|
|
|
|
|
used more than once, the expression is likely to need parenthesizing anyway,
|
|
|
|
|
so this prohibition will rarely affect code.
|
|
|
|
|
|
2019-05-06 12:30:23 -04:00
|
|
|
|
- Assignment expressions inside of f-strings require parentheses. Example::
|
|
|
|
|
|
|
|
|
|
>>> f'{(x:=10)}' # Valid, uses assignment expression
|
|
|
|
|
'10'
|
|
|
|
|
>>> x = 10
|
|
|
|
|
>>> f'{x:=10}' # Valid, passes '=10' to formatter
|
|
|
|
|
' 10'
|
|
|
|
|
|
|
|
|
|
This shows that what looks like an assignment operator in an f-string is
|
|
|
|
|
not always an assignment operator. The f-string parser uses ``:`` to
|
|
|
|
|
indicate formatting options. To preserve backwards compatibility,
|
|
|
|
|
assignment operator usage inside of f-strings must be parenthesized.
|
|
|
|
|
As noted above, this usage of the assignment operator is not recommended.
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Scope of the target
|
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
|
|
An assignment expression does not introduce a new scope. In most
|
|
|
|
|
cases the scope in which the target will be bound is self-explanatory:
|
|
|
|
|
it is the current scope. If this scope contains a ``nonlocal`` or
|
|
|
|
|
``global`` declaration for the target, the assignment expression
|
2018-07-09 00:35:44 -04:00
|
|
|
|
honors that. A lambda (being an explicit, if anonymous, function
|
|
|
|
|
definition) counts as a scope for this purpose.
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
There is one special case: an assignment expression occurring in a
|
|
|
|
|
list, set or dict comprehension or in a generator expression (below
|
|
|
|
|
collectively referred to as "comprehensions") binds the target in the
|
|
|
|
|
containing scope, honoring a ``nonlocal`` or ``global`` declaration
|
|
|
|
|
for the target in that scope, if one exists. For the purpose of this
|
|
|
|
|
rule the containing scope of a nested comprehension is the scope that
|
|
|
|
|
contains the outermost comprehension. A lambda counts as a containing
|
|
|
|
|
scope.
|
|
|
|
|
|
|
|
|
|
The motivation for this special case is twofold. First, it allows us
|
|
|
|
|
to conveniently capture a "witness" for an ``any()`` expression, or a
|
|
|
|
|
counterexample for ``all()``, for example::
|
|
|
|
|
|
|
|
|
|
if any((comment := line).startswith('#') for line in lines):
|
|
|
|
|
print("First comment:", comment)
|
|
|
|
|
else:
|
|
|
|
|
print("There are no comments")
|
|
|
|
|
|
|
|
|
|
if all((nonblank := line).strip() == '' for line in lines):
|
|
|
|
|
print("All lines are blank")
|
|
|
|
|
else:
|
|
|
|
|
print("First non-blank line:", nonblank)
|
|
|
|
|
|
|
|
|
|
Second, it allows a compact way of updating mutable state from a
|
|
|
|
|
comprehension, for example::
|
|
|
|
|
|
|
|
|
|
# Compute partial sums in a list comprehension
|
|
|
|
|
total = 0
|
|
|
|
|
partial_sums = [total := total + v for v in values]
|
|
|
|
|
print("Total:", total)
|
|
|
|
|
|
2018-07-10 14:02:30 -04:00
|
|
|
|
However, an assignment expression target name cannot be the same as a
|
|
|
|
|
``for``-target name appearing in any comprehension containing the
|
|
|
|
|
assignment expression. The latter names are local to the
|
|
|
|
|
comprehension in which they appear, so it would be contradictory for a
|
|
|
|
|
contained use of the same name to refer to the scope containing the
|
|
|
|
|
outermost comprehension instead.
|
|
|
|
|
|
|
|
|
|
For example, ``[i := i+1 for i in range(5)]`` is invalid: the ``for
|
|
|
|
|
i`` part establishes that ``i`` is local to the comprehension, but the
|
|
|
|
|
``i :=`` part insists that ``i`` is not local to the comprehension.
|
|
|
|
|
The same reason makes these examples invalid too::
|
|
|
|
|
|
2019-08-13 22:30:53 -04:00
|
|
|
|
[[(j := j) for i in range(5)] for j in range(5)] # INVALID
|
|
|
|
|
[i := 0 for i, j in stuff] # INVALID
|
|
|
|
|
[i+1 for i in (i := stuff)] # INVALID
|
|
|
|
|
|
|
|
|
|
While it's technically possible to assign consistent semantics to these cases,
|
|
|
|
|
it's difficult to determine whether those semantics actually make *sense* in the
|
|
|
|
|
absence of real use cases. Accordingly, the reference implementation will ensure
|
|
|
|
|
that such cases raise ``SyntaxError``, rather than executing with implementation
|
|
|
|
|
defined behaviour.
|
|
|
|
|
|
|
|
|
|
This restriction applies even if the assignment expression is never executed::
|
|
|
|
|
|
|
|
|
|
[False and (i := 0) for i, j in stuff] # INVALID
|
|
|
|
|
[i for i, j in stuff if True or (j := 1)] # INVALID
|
|
|
|
|
|
|
|
|
|
For the comprehension body (the part before the first "for" keyword) and the
|
|
|
|
|
filter expression (the part after "if" and before any nested "for"), this
|
|
|
|
|
restriction applies solely to target names that are also used as iteration
|
|
|
|
|
variables in the comprehension. Lambda expressions appearing in these
|
|
|
|
|
positions introduce a new explicit function scope, and hence may use assignment
|
|
|
|
|
expressions with no additional restrictions.
|
|
|
|
|
|
|
|
|
|
Due to design constraints in the reference implementation (the symbol table
|
|
|
|
|
analyser cannot easily detect when names are re-used between the leftmost
|
|
|
|
|
comprehension iterable expression and the rest of the comprehension), named
|
|
|
|
|
expressions are disallowed entirely as part of comprehension iterable
|
|
|
|
|
expressions (the part after each "in", and before any subsequent "if" or
|
|
|
|
|
"for" keyword)::
|
|
|
|
|
|
|
|
|
|
[i+1 for i in (j := stuff)] # INVALID
|
|
|
|
|
[i+1 for i in range(2) for j in (k := stuff)] # INVALID
|
|
|
|
|
[i+1 for i in [j for j in (k := stuff)]] # INVALID
|
|
|
|
|
[i+1 for i in (lambda: (j := stuff))()] # INVALID
|
2018-07-05 20:21:28 -04:00
|
|
|
|
|
2018-07-10 12:29:03 -04:00
|
|
|
|
A further exception applies when an assignment expression occurs in a
|
2018-05-22 15:49:00 -04:00
|
|
|
|
comprehension whose containing scope is a class scope. If the rules
|
|
|
|
|
above were to result in the target being assigned in that class's
|
2019-08-13 22:30:53 -04:00
|
|
|
|
scope, the assignment expression is expressly invalid. This case also raises
|
|
|
|
|
``SyntaxError``::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
2019-08-13 22:30:53 -04:00
|
|
|
|
class Example:
|
|
|
|
|
[(j := i) for i in range(5)] # INVALID
|
|
|
|
|
|
|
|
|
|
(The reason for the latter exception is the implicit function scope created
|
2018-05-22 15:49:00 -04:00
|
|
|
|
for comprehensions -- there is currently no runtime mechanism for a
|
|
|
|
|
function to refer to a variable in the containing class scope, and we
|
|
|
|
|
do not want to add such a mechanism. If this issue ever gets resolved
|
|
|
|
|
this special case may be removed from the specification of assignment
|
|
|
|
|
expressions. Note that the problem already exists for *using* a
|
|
|
|
|
variable defined in the class scope from a comprehension.)
|
|
|
|
|
|
2018-07-08 11:28:47 -04:00
|
|
|
|
See Appendix B for some examples of how the rules for targets in
|
|
|
|
|
comprehensions translate to equivalent code.
|
2018-07-05 20:21:28 -04:00
|
|
|
|
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Relative precedence of ``:=``
|
|
|
|
|
-----------------------------
|
|
|
|
|
|
|
|
|
|
The ``:=`` operator groups more tightly than a comma in all syntactic
|
2018-07-10 14:02:30 -04:00
|
|
|
|
positions where it is legal, but less tightly than all other operators,
|
2018-07-10 14:19:10 -04:00
|
|
|
|
including ``or``, ``and``, ``not``, and conditional expressions
|
|
|
|
|
(``A if C else B``). As follows from section
|
2018-05-22 15:49:00 -04:00
|
|
|
|
"Exceptional cases" above, it is never allowed at the same level as
|
|
|
|
|
``=``. In case a different grouping is desired, parentheses should be
|
|
|
|
|
used.
|
|
|
|
|
|
|
|
|
|
The ``:=`` operator may be used directly in a positional function call
|
|
|
|
|
argument; however it is invalid directly in a keyword argument.
|
|
|
|
|
|
|
|
|
|
Some examples to clarify what's technically valid or invalid::
|
|
|
|
|
|
|
|
|
|
# INVALID
|
|
|
|
|
x := 0
|
|
|
|
|
|
|
|
|
|
# Valid alternative
|
|
|
|
|
(x := 0)
|
|
|
|
|
|
|
|
|
|
# INVALID
|
|
|
|
|
x = y := 0
|
|
|
|
|
|
|
|
|
|
# Valid alternative
|
|
|
|
|
x = (y := 0)
|
|
|
|
|
|
|
|
|
|
# Valid
|
|
|
|
|
len(lines := f.readlines())
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
# Valid
|
2018-05-22 15:49:00 -04:00
|
|
|
|
foo(x := 3, cat='vector')
|
|
|
|
|
|
|
|
|
|
# INVALID
|
|
|
|
|
foo(cat=category := 'vector')
|
|
|
|
|
|
|
|
|
|
# Valid alternative
|
|
|
|
|
foo(cat=(category := 'vector'))
|
|
|
|
|
|
|
|
|
|
Most of the "valid" examples above are not recommended, since human
|
|
|
|
|
readers of Python source code who are quickly glancing at some code
|
|
|
|
|
may miss the distinction. But simple cases are not objectionable::
|
|
|
|
|
|
|
|
|
|
# Valid
|
|
|
|
|
if any(len(longline := line) >= 100 for line in lines):
|
|
|
|
|
print("Extremely long line:", longline)
|
|
|
|
|
|
|
|
|
|
This PEP recommends always putting spaces around ``:=``, similar to
|
2022-01-21 06:03:51 -05:00
|
|
|
|
:pep:`8`'s recommendation for ``=`` when used for assignment, whereas the
|
2018-05-22 15:49:00 -04:00
|
|
|
|
latter disallows spaces around ``=`` used for keyword arguments.)
|
|
|
|
|
|
2018-07-09 00:36:28 -04:00
|
|
|
|
Change to evaluation order
|
|
|
|
|
--------------------------
|
|
|
|
|
|
|
|
|
|
In order to have precisely defined semantics, the proposal requires
|
|
|
|
|
evaluation order to be well-defined. This is technically not a new
|
|
|
|
|
requirement, as function calls may already have side effects. Python
|
|
|
|
|
already has a rule that subexpressions are generally evaluated from
|
|
|
|
|
left to right. However, assignment expressions make these side
|
|
|
|
|
effects more visible, and we propose a single change to the current
|
|
|
|
|
evaluation order:
|
|
|
|
|
|
|
|
|
|
- In a dict comprehension ``{X: Y for ...}``, ``Y`` is currently
|
|
|
|
|
evaluated before ``X``. We propose to change this so that ``X`` is
|
2018-07-10 17:05:30 -04:00
|
|
|
|
evaluated before ``Y``. (In a dict display like ``{X: Y}`` this is
|
2018-07-09 00:36:28 -04:00
|
|
|
|
already the case, and also in ``dict((X, Y) for ...)`` which should
|
|
|
|
|
clearly be equivalent to the dict comprehension.)
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
Differences between assignment expressions and assignment statements
|
|
|
|
|
---------------------------------------------------------------------
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-04-13 07:34:33 -04:00
|
|
|
|
Most importantly, since ``:=`` is an expression, it can be used in contexts
|
|
|
|
|
where statements are illegal, including lambda functions and comprehensions.
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Conversely, assignment expressions don't support the advanced features
|
|
|
|
|
found in assignment statements:
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
- Multiple targets are not directly supported::
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-07-09 00:21:30 -04:00
|
|
|
|
x = y = z = 0 # Equivalent: (z := (y := (x := 0)))
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-07-10 20:43:29 -04:00
|
|
|
|
- Single assignment targets other than a single ``NAME`` are
|
2018-05-22 15:49:00 -04:00
|
|
|
|
not supported::
|
2018-03-01 01:12:08 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# No equivalent
|
|
|
|
|
a[i] = x
|
|
|
|
|
self.rest = []
|
2018-04-24 02:58:40 -04:00
|
|
|
|
|
2018-07-09 00:21:30 -04:00
|
|
|
|
- Priority around commas is different::
|
|
|
|
|
|
|
|
|
|
x = 1, 2 # Sets x to (1, 2)
|
|
|
|
|
(x := 1, 2) # Sets x to 1
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
- Iterable packing and unpacking (both regular or extended forms) are
|
|
|
|
|
not supported::
|
2018-04-24 02:58:40 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# Equivalent needs extra parentheses
|
|
|
|
|
loc = x, y # Use (loc := (x, y))
|
|
|
|
|
info = name, phone, *rest # Use (info := (name, phone, *rest))
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# No equivalent
|
|
|
|
|
px, py, pz = position
|
|
|
|
|
name, phone, email, *other_info = contact
|
2018-03-02 11:00:05 -05:00
|
|
|
|
|
2018-07-10 14:08:09 -04:00
|
|
|
|
- Inline type annotations are not supported::
|
2018-04-24 02:58:40 -04:00
|
|
|
|
|
2018-07-10 14:08:09 -04:00
|
|
|
|
# Closest equivalent is "p: Optional[int]" as a separate declaration
|
2018-05-22 15:49:00 -04:00
|
|
|
|
p: Optional[int] = None
|
2018-03-23 05:57:38 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
- Augmented assignment is not supported::
|
2018-03-02 11:00:05 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
total += tax # Equivalent: (total := total + tax)
|
2018-04-10 21:07:42 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
2019-08-13 22:30:53 -04:00
|
|
|
|
Specification changes during implementation
|
|
|
|
|
===========================================
|
|
|
|
|
|
|
|
|
|
The following changes have been made based on implementation experience and
|
|
|
|
|
additional review after the PEP was first accepted and before Python 3.8 was
|
|
|
|
|
released:
|
|
|
|
|
|
|
|
|
|
* for consistency with other similar exceptions, and to avoid locking in an
|
|
|
|
|
exception name that is not necessarily going to improve clarity for end users,
|
|
|
|
|
the originally proposed ``TargetScopeError`` subclass of ``SyntaxError`` was
|
|
|
|
|
dropped in favour of just raising ``SyntaxError`` directly. [3]_
|
|
|
|
|
* due to a limitation in CPython's symbol table analysis process, the reference
|
|
|
|
|
implementation raises ``SyntaxError`` for all uses of named expressions inside
|
|
|
|
|
comprehension iterable expressions, rather than only raising them when the
|
|
|
|
|
named expression target conflicts with one of the iteration variables in the
|
|
|
|
|
comprehension. This could be revisited given sufficiently compelling examples,
|
|
|
|
|
but the extra complexity needed to implement the more selective restriction
|
|
|
|
|
doesn't seem worthwhile for purely hypothetical use cases.
|
|
|
|
|
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Examples
|
|
|
|
|
========
|
2018-03-25 13:07:50 -04:00
|
|
|
|
|
2018-07-07 17:05:56 -04:00
|
|
|
|
Examples from the Python standard library
|
|
|
|
|
-----------------------------------------
|
|
|
|
|
|
|
|
|
|
site.py
|
|
|
|
|
^^^^^^^
|
|
|
|
|
|
|
|
|
|
*env_base* is only used on these lines, putting its assignment on the if
|
|
|
|
|
moves it as the "header" of the block.
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
env_base = os.environ.get("PYTHONUSERBASE", None)
|
|
|
|
|
if env_base:
|
|
|
|
|
return env_base
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
if env_base := os.environ.get("PYTHONUSERBASE", None):
|
|
|
|
|
return env_base
|
|
|
|
|
|
2018-07-07 17:05:56 -04:00
|
|
|
|
_pydecimal.py
|
|
|
|
|
^^^^^^^^^^^^^
|
|
|
|
|
|
2018-07-10 12:53:48 -04:00
|
|
|
|
Avoid nested ``if`` and remove one indentation level.
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
if self._is_special:
|
|
|
|
|
ans = self._check_nans(context=context)
|
|
|
|
|
if ans:
|
|
|
|
|
return ans
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
if self._is_special and (ans := self._check_nans(context=context)):
|
|
|
|
|
return ans
|
|
|
|
|
|
2018-07-07 17:05:56 -04:00
|
|
|
|
copy.py
|
|
|
|
|
^^^^^^^
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
Code looks more regular and avoid multiple nested if.
|
2018-07-07 17:05:56 -04:00
|
|
|
|
(See Appendix A for the origin of this example.)
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
- Current::
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
reductor = dispatch_table.get(cls)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor(x)
|
|
|
|
|
else:
|
|
|
|
|
reductor = getattr(x, "__reduce_ex__", None)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor(4)
|
|
|
|
|
else:
|
|
|
|
|
reductor = getattr(x, "__reduce__", None)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor()
|
|
|
|
|
else:
|
|
|
|
|
raise Error(
|
|
|
|
|
"un(deep)copyable object of type %s" % cls)
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
if reductor := dispatch_table.get(cls):
|
|
|
|
|
rv = reductor(x)
|
|
|
|
|
elif reductor := getattr(x, "__reduce_ex__", None):
|
|
|
|
|
rv = reductor(4)
|
|
|
|
|
elif reductor := getattr(x, "__reduce__", None):
|
|
|
|
|
rv = reductor()
|
|
|
|
|
else:
|
|
|
|
|
raise Error("un(deep)copyable object of type %s" % cls)
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
|
|
|
|
datetime.py
|
|
|
|
|
^^^^^^^^^^^
|
|
|
|
|
|
|
|
|
|
*tz* is only used for ``s += tz``, moving its assignment inside the if
|
|
|
|
|
helps to show its scope.
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
s = _format_time(self._hour, self._minute,
|
|
|
|
|
self._second, self._microsecond,
|
|
|
|
|
timespec)
|
|
|
|
|
tz = self._tzstr()
|
|
|
|
|
if tz:
|
|
|
|
|
s += tz
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
s = _format_time(self._hour, self._minute,
|
|
|
|
|
self._second, self._microsecond,
|
|
|
|
|
timespec)
|
|
|
|
|
if tz := self._tzstr():
|
|
|
|
|
s += tz
|
|
|
|
|
return s
|
|
|
|
|
|
2018-07-07 17:05:56 -04:00
|
|
|
|
sysconfig.py
|
|
|
|
|
^^^^^^^^^^^^
|
|
|
|
|
|
|
|
|
|
Calling ``fp.readline()`` in the ``while`` condition and calling
|
|
|
|
|
``.match()`` on the if lines make the code more compact without making
|
|
|
|
|
it harder to understand.
|
|
|
|
|
|
2018-07-08 18:27:52 -04:00
|
|
|
|
- Current::
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
line = fp.readline()
|
|
|
|
|
if not line:
|
|
|
|
|
break
|
|
|
|
|
m = define_rx.match(line)
|
|
|
|
|
if m:
|
|
|
|
|
n, v = m.group(1, 2)
|
|
|
|
|
try:
|
|
|
|
|
v = int(v)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
vars[n] = v
|
|
|
|
|
else:
|
|
|
|
|
m = undef_rx.match(line)
|
|
|
|
|
if m:
|
|
|
|
|
vars[m.group(1)] = 0
|
|
|
|
|
|
|
|
|
|
- Improved::
|
|
|
|
|
|
|
|
|
|
while line := fp.readline():
|
|
|
|
|
if m := define_rx.match(line):
|
|
|
|
|
n, v = m.group(1, 2)
|
|
|
|
|
try:
|
|
|
|
|
v = int(v)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
vars[n] = v
|
|
|
|
|
elif m := undef_rx.match(line):
|
|
|
|
|
vars[m.group(1)] = 0
|
|
|
|
|
|
|
|
|
|
|
2018-03-25 13:07:50 -04:00
|
|
|
|
Simplifying list comprehensions
|
|
|
|
|
-------------------------------
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
2018-04-19 22:53:15 -04:00
|
|
|
|
A list comprehension can map and filter efficiently by capturing
|
|
|
|
|
the condition::
|
|
|
|
|
|
|
|
|
|
results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]
|
|
|
|
|
|
|
|
|
|
Similarly, a subexpression can be reused within the main expression, by
|
|
|
|
|
giving it a name on first use::
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
2018-04-12 09:39:05 -04:00
|
|
|
|
stuff = [[y := f(x), x/y] for x in range(5)]
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Note that in both cases the variable ``y`` is bound in the containing
|
|
|
|
|
scope (i.e. at the same level as ``results`` or ``stuff``).
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-03-25 13:07:50 -04:00
|
|
|
|
|
|
|
|
|
Capturing condition values
|
|
|
|
|
--------------------------
|
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
Assignment expressions can be used to good effect in the header of
|
|
|
|
|
an ``if`` or ``while`` statement::
|
2018-02-28 08:43:53 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# Loop-and-a-half
|
2018-04-08 21:40:59 -04:00
|
|
|
|
while (command := input("> ")) != "quit":
|
2018-02-28 08:43:53 -05:00
|
|
|
|
print("You entered:", command)
|
|
|
|
|
|
2018-03-25 18:59:47 -04:00
|
|
|
|
# Capturing regular expression match objects
|
|
|
|
|
# See, for instance, Lib/pydoc.py, which uses a multiline spelling
|
|
|
|
|
# of this effect
|
2018-04-08 21:40:59 -04:00
|
|
|
|
if match := re.search(pat, text):
|
2018-02-28 08:43:53 -05:00
|
|
|
|
print("Found:", match.group(0))
|
2018-04-25 01:32:22 -04:00
|
|
|
|
# The same syntax chains nicely into 'elif' statements, unlike the
|
|
|
|
|
# equivalent using assignment statements.
|
|
|
|
|
elif match := re.search(otherpat, text):
|
|
|
|
|
print("Alternate found:", match.group(0))
|
|
|
|
|
elif match := re.search(third, text):
|
|
|
|
|
print("Fallback found:", match.group(0))
|
2018-02-28 08:43:53 -05:00
|
|
|
|
|
2018-03-25 18:59:47 -04:00
|
|
|
|
# Reading socket data until an empty string is returned
|
2018-07-10 19:07:06 -04:00
|
|
|
|
while data := sock.recv(8192):
|
2018-02-28 08:43:53 -05:00
|
|
|
|
print("Received data:", data)
|
|
|
|
|
|
|
|
|
|
Particularly with the ``while`` loop, this can remove the need to have an
|
|
|
|
|
infinite loop, an assignment, and a condition. It also creates a smooth
|
|
|
|
|
parallel between a loop which simply uses a function call as its condition,
|
|
|
|
|
and one which uses that as its condition but also uses the actual value.
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Fork
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
An example from the low-level UNIX world::
|
|
|
|
|
|
|
|
|
|
if pid := os.fork():
|
|
|
|
|
# Parent code
|
|
|
|
|
else:
|
|
|
|
|
# Child code
|
|
|
|
|
|
|
|
|
|
|
2018-03-23 22:35:26 -04:00
|
|
|
|
Rejected alternative proposals
|
|
|
|
|
==============================
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-23 05:57:38 -04:00
|
|
|
|
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.
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Changing the scope rules for comprehensions
|
|
|
|
|
-------------------------------------------
|
|
|
|
|
|
|
|
|
|
A previous version of this PEP proposed subtle changes to the scope
|
|
|
|
|
rules for comprehensions, to make them more usable in class scope and
|
|
|
|
|
to unify the scope of the "outermost iterable" and the rest of the
|
|
|
|
|
comprehension. However, this part of the proposal would have caused
|
|
|
|
|
backwards incompatibilities, and has been withdrawn so the PEP can
|
|
|
|
|
focus on assignment expressions.
|
|
|
|
|
|
|
|
|
|
|
2018-03-23 22:35:26 -04:00
|
|
|
|
Alternative spellings
|
|
|
|
|
---------------------
|
|
|
|
|
|
|
|
|
|
Broadly the same semantics as the current proposal, but spelled differently.
|
|
|
|
|
|
2018-04-19 22:53:15 -04:00
|
|
|
|
1. ``EXPR as NAME``::
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
|
|
|
|
stuff = [[f(x) as y, x/y] for x in range(5)]
|
|
|
|
|
|
2018-07-09 11:16:01 -04:00
|
|
|
|
Since ``EXPR as NAME`` already has meaning in ``import``,
|
|
|
|
|
``except`` and ``with`` statements (with different semantics), this
|
|
|
|
|
would create unnecessary confusion or require special-casing
|
|
|
|
|
(e.g. to forbid assignment within the headers of these statements).
|
2018-04-08 21:40:59 -04:00
|
|
|
|
|
2018-07-09 22:32:24 -04:00
|
|
|
|
(Note that ``with EXPR as VAR`` does *not* simply assign the value
|
2018-07-09 11:16:01 -04:00
|
|
|
|
of ``EXPR`` to ``VAR`` -- it calls ``EXPR.__enter__()`` and assigns
|
|
|
|
|
the result of *that* to ``VAR``.)
|
|
|
|
|
|
|
|
|
|
Additional reasons to prefer ``:=`` over this spelling include:
|
|
|
|
|
|
|
|
|
|
- In ``if f(x) as y`` the assignment target doesn't jump out at you
|
|
|
|
|
-- it just reads like ``if f x blah blah`` and it is too similar
|
|
|
|
|
visually to ``if f(x) and y``.
|
|
|
|
|
|
|
|
|
|
- In all other situations where an ``as`` clause is allowed, even
|
|
|
|
|
readers with intermediary skills are led to anticipate that
|
|
|
|
|
clause (however optional) by the keyword that starts the line,
|
|
|
|
|
and the grammar ties that keyword closely to the as clause:
|
|
|
|
|
|
|
|
|
|
- ``import foo as bar``
|
|
|
|
|
- ``except Exc as var``
|
|
|
|
|
- ``with ctxmgr() as var``
|
|
|
|
|
|
|
|
|
|
To the contrary, the assignment expression does not belong to the
|
|
|
|
|
``if`` or ``while`` that starts the line, and we intentionally
|
|
|
|
|
allow assignment expressions in other contexts as well.
|
|
|
|
|
|
|
|
|
|
- The parallel cadence between
|
|
|
|
|
|
|
|
|
|
- ``NAME = EXPR``
|
|
|
|
|
- ``if NAME := EXPR``
|
|
|
|
|
|
|
|
|
|
reinforces the visual recognition of assignment expressions.
|
2018-07-08 18:27:52 -04:00
|
|
|
|
|
2018-04-13 08:40:36 -04:00
|
|
|
|
2. ``EXPR -> NAME``::
|
|
|
|
|
|
|
|
|
|
stuff = [[f(x) -> y, x/y] for x in range(5)]
|
|
|
|
|
|
|
|
|
|
This syntax is inspired by languages such as R and Haskell, and some
|
|
|
|
|
programmable calculators. (Note that a left-facing arrow ``y <- f(x)`` is
|
|
|
|
|
not possible in Python, as it would be interpreted as less-than and unary
|
|
|
|
|
minus.) This syntax has a slight advantage over 'as' in that it does not
|
2018-07-09 11:16:01 -04:00
|
|
|
|
conflict with ``with``, ``except`` and ``import``, but otherwise is
|
|
|
|
|
equivalent. But it is entirely unrelated to Python's other use of
|
|
|
|
|
``->`` (function return type annotations), and compared to ``:=``
|
|
|
|
|
(which dates back to Algol-58) it has a much weaker tradition.
|
2018-04-13 08:40:36 -04:00
|
|
|
|
|
|
|
|
|
3. Adorning statement-local names with a leading dot::
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as"
|
|
|
|
|
stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
|
|
|
|
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
|
2018-04-19 22:53:15 -04:00
|
|
|
|
refactoring harder.
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
2018-04-13 08:40:36 -04:00
|
|
|
|
4. Adding a ``where:`` to any statement to create local name bindings::
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
|
|
|
|
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
|
2022-01-21 06:03:51 -05:00
|
|
|
|
is repurposed (most likely ``with:``). See :pep:`3150` for prior discussion
|
2018-04-08 21:40:59 -04:00
|
|
|
|
on this subject (with the proposed keyword being ``given:``).
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
2018-04-15 16:42:37 -04:00
|
|
|
|
5. ``TARGET from EXPR``::
|
|
|
|
|
|
|
|
|
|
stuff = [[y from f(x), x/y] for x in range(5)]
|
|
|
|
|
|
|
|
|
|
This syntax has fewer conflicts than ``as`` does (conflicting only with the
|
|
|
|
|
``raise Exc from Exc`` notation), but is otherwise comparable to it. Instead
|
|
|
|
|
of paralleling ``with expr as target:`` (which can be useful but can also be
|
|
|
|
|
confusing), this has no parallels, but is evocative.
|
|
|
|
|
|
2018-03-23 22:35:26 -04:00
|
|
|
|
|
|
|
|
|
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
|
2020-08-07 12:35:20 -04:00
|
|
|
|
use-cases (regex matches, socket reads that return ``''`` when done), and
|
2018-07-10 14:19:10 -04:00
|
|
|
|
completely useless in more complicated cases (e.g. where the condition is
|
2018-03-23 22:35:26 -04:00
|
|
|
|
``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``::
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-01 01:33:47 -05:00
|
|
|
|
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)]
|
|
|
|
|
stuff = [(y, x/y) given y = f(x) for x in range(5)]
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
|
|
|
|
This brings the subexpression to a location in between the 'for' loop and
|
|
|
|
|
the expression. It introduces an additional language keyword, which creates
|
|
|
|
|
conflicts. Of the three, ``where`` reads the most cleanly, but also has the
|
2018-07-10 14:19:10 -04:00
|
|
|
|
greatest potential for conflict (e.g. SQLAlchemy and numpy have ``where``
|
2018-02-28 01:50:32 -05:00
|
|
|
|
methods, as does ``tkinter.dnd.Icon`` in the standard library).
|
|
|
|
|
|
2018-03-23 05:57:38 -04:00
|
|
|
|
2. ``with NAME = EXPR``::
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-01 01:33:47 -05:00
|
|
|
|
stuff = [(y, x/y) with y = f(x) for x in range(5)]
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-07-09 23:31:21 -04:00
|
|
|
|
As above, but reusing the ``with`` keyword. Doesn't read too badly, and needs
|
2018-02-28 01:50:32 -05:00
|
|
|
|
no additional language keyword. Is restricted to comprehensions, though,
|
|
|
|
|
and cannot as easily be transformed into "longhand" for-loop syntax. Has
|
|
|
|
|
the C problem that an equals sign in an expression can now create a name
|
2018-03-23 05:57:38 -04:00
|
|
|
|
binding, rather than performing a comparison. Would raise the question of
|
|
|
|
|
why "with NAME = EXPR:" cannot be used as a statement on its own.
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-23 05:57:38 -04:00
|
|
|
|
3. ``with EXPR as NAME``::
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-01 01:33:47 -05:00
|
|
|
|
stuff = [(y, x/y) with f(x) as y for x in range(5)]
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-23 05:57:38 -04:00
|
|
|
|
As per option 2, but using ``as`` rather than an equals sign. Aligns
|
2018-02-28 01:50:32 -05:00
|
|
|
|
syntactically with other uses of ``as`` for name binding, but a simple
|
|
|
|
|
transformation to for-loop longhand would create drastically different
|
|
|
|
|
semantics; the meaning of ``with`` inside a comprehension would be
|
2018-03-23 05:57:38 -04:00
|
|
|
|
completely different from the meaning as a stand-alone statement, while
|
|
|
|
|
retaining identical syntax.
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
2018-03-23 22:35:26 -04:00
|
|
|
|
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.
|
2018-02-28 01:50:32 -05:00
|
|
|
|
|
|
|
|
|
|
2018-04-17 03:40:51 -04:00
|
|
|
|
Lowering operator precedence
|
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
|
There are two logical precedences for the ``:=`` operator. Either it should
|
|
|
|
|
bind as loosely as possible, as does statement-assignment; or it should bind
|
|
|
|
|
more tightly than comparison operators. Placing its precedence between the
|
|
|
|
|
comparison and arithmetic operators (to be precise: just lower than bitwise
|
|
|
|
|
OR) allows most uses inside ``while`` and ``if`` conditions to be spelled
|
|
|
|
|
without parentheses, as it is most likely that you wish to capture the value
|
|
|
|
|
of something, then perform a comparison on it::
|
|
|
|
|
|
|
|
|
|
pos = -1
|
|
|
|
|
while pos := buffer.find(search_term, pos + 1) >= 0:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
Once find() returns -1, the loop terminates. If ``:=`` binds as loosely as
|
|
|
|
|
``=`` does, this would capture the result of the comparison (generally either
|
|
|
|
|
``True`` or ``False``), which is less useful.
|
|
|
|
|
|
|
|
|
|
While this behaviour would be convenient in many situations, it is also harder
|
|
|
|
|
to explain than "the := operator behaves just like the assignment statement",
|
|
|
|
|
and as such, the precedence for ``:=`` has been made as close as possible to
|
2018-05-22 15:49:00 -04:00
|
|
|
|
that of ``=`` (with the exception that it binds tighter than comma).
|
2018-04-11 01:16:15 -04:00
|
|
|
|
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Allowing commas to the right
|
|
|
|
|
----------------------------
|
2018-04-11 01:16:15 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Some critics have claimed that the assignment expressions should allow
|
|
|
|
|
unparenthesized tuples on the right, so that these two would be equivalent::
|
2018-04-11 01:30:34 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
(point := (x, y))
|
|
|
|
|
(point := x, y)
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
(With the current version of the proposal, the latter would be
|
|
|
|
|
equivalent to ``((point := x), y)``.)
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
However, adopting this stance would logically lead to the conclusion
|
|
|
|
|
that when used in a function call, assignment expressions also bind
|
|
|
|
|
less tight than comma, so we'd have the following confusing equivalence::
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
foo(x := 1, y)
|
|
|
|
|
foo(x := (1, y))
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
The less confusing option is to make ``:=`` bind more tightly than comma.
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Always requiring parentheses
|
|
|
|
|
----------------------------
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2022-02-27 19:21:02 -05:00
|
|
|
|
It's been proposed to just always require parentheses around an
|
2018-05-22 15:49:00 -04:00
|
|
|
|
assignment expression. This would resolve many ambiguities, and
|
|
|
|
|
indeed parentheses will frequently be needed to extract the desired
|
|
|
|
|
subexpression. But in the following cases the extra parentheses feel
|
|
|
|
|
redundant::
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# Top level in if
|
|
|
|
|
if match := pattern.match(line):
|
|
|
|
|
return match.group(1)
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
# Short call
|
|
|
|
|
len(lines := f.readlines())
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
Frequently Raised Objections
|
|
|
|
|
============================
|
2018-03-01 01:33:47 -05:00
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
Why not just turn existing assignment into an expression?
|
|
|
|
|
---------------------------------------------------------
|
2018-03-01 01:33:47 -05:00
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
C and its derivatives define the ``=`` operator as an expression, rather than
|
|
|
|
|
a statement as is Python's way. This allows assignments in more contexts,
|
|
|
|
|
including contexts where comparisons are more common. The syntactic similarity
|
2018-03-23 22:35:26 -04:00
|
|
|
|
between ``if (x == y)`` and ``if (x = y)`` belies their drastically different
|
2018-04-08 21:40:59 -04:00
|
|
|
|
semantics. Thus this proposal uses ``:=`` to clarify the distinction.
|
2018-03-25 18:59:47 -04:00
|
|
|
|
|
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
With assignment expressions, why bother with assignment statements?
|
|
|
|
|
-------------------------------------------------------------------
|
2018-03-01 10:25:02 -05:00
|
|
|
|
|
2018-04-08 21:40:59 -04:00
|
|
|
|
The two forms have different flexibilities. The ``:=`` operator can be used
|
2018-04-17 03:40:51 -04:00
|
|
|
|
inside a larger expression; the ``=`` statement can be augmented to ``+=`` and
|
2018-04-24 02:58:40 -04:00
|
|
|
|
its friends, can be chained, and can assign to attributes and subscripts.
|
2018-03-23 05:57:38 -04:00
|
|
|
|
|
2018-02-28 08:43:53 -05:00
|
|
|
|
|
2018-04-12 09:39:05 -04:00
|
|
|
|
Why not use a sublocal scope and prevent namespace pollution?
|
|
|
|
|
-------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
Previous revisions of this proposal involved sublocal scope (restricted to a
|
|
|
|
|
single statement), preventing name leakage and namespace pollution. While a
|
|
|
|
|
definite advantage in a number of situations, this increases complexity in
|
|
|
|
|
many others, and the costs are not justified by the benefits. In the interests
|
|
|
|
|
of language simplicity, the name bindings created here are exactly equivalent
|
|
|
|
|
to any other name bindings, including that usage at class or module scope will
|
|
|
|
|
create externally-visible names. This is no different from ``for`` loops or
|
|
|
|
|
other constructs, and can be solved the same way: ``del`` the name once it is
|
|
|
|
|
no longer needed, or prefix it with an underscore.
|
|
|
|
|
|
2018-04-18 09:15:42 -04:00
|
|
|
|
(The author wishes to thank Guido van Rossum and Christoph Groth for their
|
|
|
|
|
suggestions to move the proposal in this direction. [2]_)
|
|
|
|
|
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
|
|
|
|
Style guide recommendations
|
|
|
|
|
===========================
|
|
|
|
|
|
2018-04-24 03:22:41 -04:00
|
|
|
|
As expression assignments can sometimes be used equivalently to statement
|
2018-04-24 02:58:40 -04:00
|
|
|
|
assignments, the question of which should be preferred will arise. For the
|
2022-01-21 06:03:51 -05:00
|
|
|
|
benefit of style guides such as :pep:`8`, two recommendations are suggested.
|
2018-04-12 09:39:05 -04:00
|
|
|
|
|
|
|
|
|
1. If either assignment statements or assignment expressions can be
|
|
|
|
|
used, prefer statements; they are a clear declaration of intent.
|
|
|
|
|
|
|
|
|
|
2. If using assignment expressions would lead to ambiguity about
|
|
|
|
|
execution order, restructure it to use statements instead.
|
|
|
|
|
|
|
|
|
|
|
2018-04-10 21:07:42 -04:00
|
|
|
|
Acknowledgements
|
|
|
|
|
================
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
The authors wish to thank Nick Coghlan and Steven D'Aprano for their
|
|
|
|
|
considerable contributions to this proposal, and members of the
|
2018-04-10 21:07:42 -04:00
|
|
|
|
core-mentorship mailing list for assistance with implementation.
|
|
|
|
|
|
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
Appendix A: Tim Peters's findings
|
|
|
|
|
=================================
|
|
|
|
|
|
|
|
|
|
Here's a brief essay Tim Peters wrote on the topic.
|
|
|
|
|
|
|
|
|
|
I dislike "busy" lines of code, and also dislike putting conceptually
|
|
|
|
|
unrelated logic on a single line. So, for example, instead of::
|
|
|
|
|
|
|
|
|
|
i = j = count = nerrors = 0
|
|
|
|
|
|
|
|
|
|
I prefer::
|
|
|
|
|
|
|
|
|
|
i = j = 0
|
|
|
|
|
count = 0
|
|
|
|
|
nerrors = 0
|
|
|
|
|
|
|
|
|
|
instead. So I suspected I'd find few places I'd want to use
|
|
|
|
|
assignment expressions. I didn't even consider them for lines already
|
|
|
|
|
stretching halfway across the screen. In other cases, "unrelated"
|
|
|
|
|
ruled::
|
|
|
|
|
|
|
|
|
|
mylast = mylast[1]
|
|
|
|
|
yield mylast[0]
|
|
|
|
|
|
2018-07-10 14:19:10 -04:00
|
|
|
|
is a vast improvement over the briefer::
|
2018-05-22 15:49:00 -04:00
|
|
|
|
|
|
|
|
|
yield (mylast := mylast[1])[0]
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
The original two statements are doing entirely different conceptual
|
|
|
|
|
things, and slamming them together is conceptually insane.
|
|
|
|
|
|
|
|
|
|
In other cases, combining related logic made it harder to understand,
|
|
|
|
|
such as rewriting::
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
old = total
|
|
|
|
|
total += term
|
|
|
|
|
if old == total:
|
|
|
|
|
return total
|
|
|
|
|
term *= mx2 / (i*(i+1))
|
|
|
|
|
i += 2
|
|
|
|
|
|
|
|
|
|
as the briefer::
|
|
|
|
|
|
|
|
|
|
while total != (total := total + term):
|
|
|
|
|
term *= mx2 / (i*(i+1))
|
|
|
|
|
i += 2
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
The ``while`` test there is too subtle, crucially relying on strict
|
|
|
|
|
left-to-right evaluation in a non-short-circuiting or method-chaining
|
|
|
|
|
context. My brain isn't wired that way.
|
|
|
|
|
|
|
|
|
|
But cases like that were rare. Name binding is very frequent, and
|
|
|
|
|
"sparse is better than dense" does not mean "almost empty is better
|
|
|
|
|
than sparse". For example, I have many functions that return ``None``
|
|
|
|
|
or ``0`` to communicate "I have nothing useful to return in this case,
|
|
|
|
|
but since that's expected often I'm not going to annoy you with an
|
|
|
|
|
exception". This is essentially the same as regular expression search
|
|
|
|
|
functions returning ``None`` when there is no match. So there was lots
|
|
|
|
|
of code of the form::
|
|
|
|
|
|
|
|
|
|
result = solution(xs, n)
|
|
|
|
|
if result:
|
|
|
|
|
# use result
|
|
|
|
|
|
|
|
|
|
I find that clearer, and certainly a bit less typing and
|
|
|
|
|
pattern-matching reading, as::
|
|
|
|
|
|
|
|
|
|
if result := solution(xs, n):
|
|
|
|
|
# use result
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
It's also nice to trade away a small amount of horizontal whitespace
|
|
|
|
|
to get another _line_ of surrounding code on screen. I didn't give
|
|
|
|
|
much weight to this at first, but it was so very frequent it added up,
|
|
|
|
|
and I soon enough became annoyed that I couldn't actually run the
|
|
|
|
|
briefer code. That surprised me!
|
|
|
|
|
|
|
|
|
|
There are other cases where assignment expressions really shine.
|
|
|
|
|
Rather than pick another from my code, Kirill Balunov gave a lovely
|
|
|
|
|
example from the standard library's ``copy()`` function in ``copy.py``::
|
|
|
|
|
|
|
|
|
|
reductor = dispatch_table.get(cls)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor(x)
|
|
|
|
|
else:
|
|
|
|
|
reductor = getattr(x, "__reduce_ex__", None)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor(4)
|
|
|
|
|
else:
|
|
|
|
|
reductor = getattr(x, "__reduce__", None)
|
|
|
|
|
if reductor:
|
|
|
|
|
rv = reductor()
|
|
|
|
|
else:
|
|
|
|
|
raise Error("un(shallow)copyable object of type %s" % cls)
|
|
|
|
|
|
|
|
|
|
The ever-increasing indentation is semantically misleading: the logic
|
|
|
|
|
is conceptually flat, "the first test that succeeds wins"::
|
|
|
|
|
|
|
|
|
|
if reductor := dispatch_table.get(cls):
|
|
|
|
|
rv = reductor(x)
|
|
|
|
|
elif reductor := getattr(x, "__reduce_ex__", None):
|
2018-07-07 17:05:56 -04:00
|
|
|
|
rv = reductor(4)
|
2018-05-22 15:49:00 -04:00
|
|
|
|
elif reductor := getattr(x, "__reduce__", None):
|
|
|
|
|
rv = reductor()
|
|
|
|
|
else:
|
|
|
|
|
raise Error("un(shallow)copyable object of type %s" % cls)
|
|
|
|
|
|
|
|
|
|
Using easy assignment expressions allows the visual structure of the
|
|
|
|
|
code to emphasize the conceptual flatness of the logic;
|
|
|
|
|
ever-increasing indentation obscured it.
|
|
|
|
|
|
|
|
|
|
A smaller example from my code delighted me, both allowing to put
|
|
|
|
|
inherently related logic in a single line, and allowing to remove an
|
|
|
|
|
annoying "artificial" indentation level::
|
|
|
|
|
|
|
|
|
|
diff = x - x_base
|
|
|
|
|
if diff:
|
|
|
|
|
g = gcd(diff, n)
|
|
|
|
|
if g > 1:
|
|
|
|
|
return g
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
became::
|
2018-07-07 17:05:56 -04:00
|
|
|
|
|
2018-05-22 15:49:00 -04:00
|
|
|
|
if (diff := x - x_base) and (g := gcd(diff, n)) > 1:
|
|
|
|
|
return g
|
|
|
|
|
|
2018-07-07 18:37:55 -04:00
|
|
|
|
That ``if`` is about as long as I want my lines to get, but remains easy
|
2018-05-22 15:49:00 -04:00
|
|
|
|
to follow.
|
|
|
|
|
|
|
|
|
|
So, in all, in most lines binding a name, I wouldn't use assignment
|
|
|
|
|
expressions, but because that construct is so very frequent, that
|
|
|
|
|
leaves many places I would. In most of the latter, I found a small
|
|
|
|
|
win that adds up due to how often it occurs, and in the rest I found a
|
|
|
|
|
moderate to major win. I'd certainly use it more often than ternary
|
|
|
|
|
``if``, but significantly less often than augmented assignment.
|
|
|
|
|
|
|
|
|
|
A numeric example
|
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
|
|
I have another example that quite impressed me at the time.
|
|
|
|
|
|
|
|
|
|
Where all variables are positive integers, and a is at least as large
|
|
|
|
|
as the n'th root of x, this algorithm returns the floor of the n'th
|
|
|
|
|
root of x (and roughly doubling the number of accurate bits per
|
|
|
|
|
iteration)::
|
|
|
|
|
|
|
|
|
|
while a > (d := x // a**(n-1)):
|
|
|
|
|
a = ((n-1)*a + d) // n
|
|
|
|
|
return a
|
|
|
|
|
|
|
|
|
|
It's not obvious why that works, but is no more obvious in the "loop
|
|
|
|
|
and a half" form. It's hard to prove correctness without building on
|
|
|
|
|
the right insight (the "arithmetic mean - geometric mean inequality"),
|
|
|
|
|
and knowing some non-trivial things about how nested floor functions
|
|
|
|
|
behave. That is, the challenges are in the math, not really in the
|
|
|
|
|
coding.
|
|
|
|
|
|
|
|
|
|
If you do know all that, then the assignment-expression form is easily
|
|
|
|
|
read as "while the current guess is too large, get a smaller guess",
|
|
|
|
|
where the "too large?" test and the new guess share an expensive
|
|
|
|
|
sub-expression.
|
|
|
|
|
|
|
|
|
|
To my eyes, the original form is harder to understand::
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
d = x // a**(n-1)
|
|
|
|
|
if a <= d:
|
|
|
|
|
break
|
|
|
|
|
a = ((n-1)*a + d) // n
|
|
|
|
|
return a
|
|
|
|
|
|
|
|
|
|
|
2018-07-08 11:28:47 -04:00
|
|
|
|
Appendix B: Rough code translations for comprehensions
|
|
|
|
|
======================================================
|
|
|
|
|
|
|
|
|
|
This appendix attempts to clarify (though not specify) the rules when
|
|
|
|
|
a target occurs in a comprehension or in a generator expression.
|
|
|
|
|
For a number of illustrative examples we show the original code,
|
|
|
|
|
containing a comprehension, and the translation, where the
|
|
|
|
|
comprehension has been replaced by an equivalent generator function
|
|
|
|
|
plus some scaffolding.
|
|
|
|
|
|
|
|
|
|
Since ``[x for ...]`` is equivalent to ``list(x for ...)`` these
|
|
|
|
|
examples all use list comprehensions without loss of generality.
|
|
|
|
|
And since these examples are meant to clarify edge cases of the rules,
|
|
|
|
|
they aren't trying to look like real code.
|
|
|
|
|
|
|
|
|
|
Note: comprehensions are already implemented via synthesizing nested
|
|
|
|
|
generator functions like those in this appendix. The new part is
|
|
|
|
|
adding appropriate declarations to establish the intended scope of
|
|
|
|
|
assignment expression targets (the same scope they resolve to as if
|
|
|
|
|
the assignment were performed in the block containing the outermost
|
|
|
|
|
comprehension). For type inference purposes, these illustrative
|
|
|
|
|
expansions do not imply that assignment expression targets are always
|
|
|
|
|
Optional (but they do indicate the target binding scope).
|
|
|
|
|
|
|
|
|
|
Let's start with a reminder of what code is generated for a generator
|
|
|
|
|
expression without assignment expression.
|
|
|
|
|
|
|
|
|
|
- Original code (EXPR usually references VAR)::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
a = [EXPR for VAR in ITERABLE]
|
|
|
|
|
|
|
|
|
|
- Translation (let's not worry about name conflicts)::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
def genexpr(iterator):
|
|
|
|
|
for VAR in iterator:
|
|
|
|
|
yield EXPR
|
|
|
|
|
a = list(genexpr(iter(ITERABLE)))
|
|
|
|
|
|
|
|
|
|
Let's add a simple assignment expression.
|
|
|
|
|
|
|
|
|
|
- Original code::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
a = [TARGET := EXPR for VAR in ITERABLE]
|
|
|
|
|
|
|
|
|
|
- Translation::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
if False:
|
|
|
|
|
TARGET = None # Dead code to ensure TARGET is a local variable
|
|
|
|
|
def genexpr(iterator):
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
for VAR in iterator:
|
|
|
|
|
TARGET = EXPR
|
|
|
|
|
yield TARGET
|
|
|
|
|
a = list(genexpr(iter(ITERABLE)))
|
|
|
|
|
|
|
|
|
|
Let's add a ``global TARGET`` declaration in ``f()``.
|
|
|
|
|
|
|
|
|
|
- Original code::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
global TARGET
|
|
|
|
|
a = [TARGET := EXPR for VAR in ITERABLE]
|
|
|
|
|
|
|
|
|
|
- Translation::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
global TARGET
|
|
|
|
|
def genexpr(iterator):
|
|
|
|
|
global TARGET
|
|
|
|
|
for VAR in iterator:
|
|
|
|
|
TARGET = EXPR
|
|
|
|
|
yield TARGET
|
|
|
|
|
a = list(genexpr(iter(ITERABLE)))
|
|
|
|
|
|
|
|
|
|
Or instead let's add a ``nonlocal TARGET`` declaration in ``f()``.
|
|
|
|
|
|
|
|
|
|
- Original code::
|
|
|
|
|
|
|
|
|
|
def g():
|
|
|
|
|
TARGET = ...
|
|
|
|
|
def f():
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
a = [TARGET := EXPR for VAR in ITERABLE]
|
|
|
|
|
|
|
|
|
|
- Translation::
|
|
|
|
|
|
|
|
|
|
def g():
|
|
|
|
|
TARGET = ...
|
|
|
|
|
def f():
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
def genexpr(iterator):
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
for VAR in iterator:
|
|
|
|
|
TARGET = EXPR
|
|
|
|
|
yield TARGET
|
|
|
|
|
a = list(genexpr(iter(ITERABLE)))
|
|
|
|
|
|
|
|
|
|
Finally, let's nest two comprehensions.
|
|
|
|
|
|
|
|
|
|
- Original code::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
a = [[TARGET := i for i in range(3)] for j in range(2)]
|
|
|
|
|
# I.e., a = [[0, 1, 2], [0, 1, 2]]
|
|
|
|
|
print(TARGET) # prints 2
|
|
|
|
|
|
|
|
|
|
- Translation::
|
|
|
|
|
|
|
|
|
|
def f():
|
|
|
|
|
if False:
|
|
|
|
|
TARGET = None
|
|
|
|
|
def outer_genexpr(outer_iterator):
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
def inner_generator(inner_iterator):
|
|
|
|
|
nonlocal TARGET
|
|
|
|
|
for i in inner_iterator:
|
|
|
|
|
TARGET = i
|
|
|
|
|
yield i
|
|
|
|
|
for j in outer_iterator:
|
|
|
|
|
yield list(inner_generator(range(3)))
|
|
|
|
|
a = list(outer_genexpr(range(2)))
|
|
|
|
|
print(TARGET)
|
|
|
|
|
|
|
|
|
|
|
2018-07-09 15:24:47 -04:00
|
|
|
|
Appendix C: No Changes to Scope Semantics
|
|
|
|
|
=========================================
|
|
|
|
|
|
|
|
|
|
Because it has been a point of confusion, note that nothing about Python's
|
|
|
|
|
scoping semantics is changed. Function-local scopes continue to be resolved
|
|
|
|
|
at compile time, and to have indefinite temporal extent at run time ("full
|
|
|
|
|
closures"). Example::
|
|
|
|
|
|
|
|
|
|
a = 42
|
|
|
|
|
def f():
|
2018-07-10 18:17:34 -04:00
|
|
|
|
# `a` is local to `f`, but remains unbound
|
|
|
|
|
# until the caller executes this genexp:
|
2018-07-09 15:24:47 -04:00
|
|
|
|
yield ((a := i) for i in range(3))
|
|
|
|
|
yield lambda: a + 100
|
|
|
|
|
print("done")
|
2018-07-10 18:17:34 -04:00
|
|
|
|
try:
|
|
|
|
|
print(f"`a` is bound to {a}")
|
|
|
|
|
assert False
|
|
|
|
|
except UnboundLocalError:
|
|
|
|
|
print("`a` is not yet bound")
|
2018-07-09 15:24:47 -04:00
|
|
|
|
|
|
|
|
|
Then::
|
|
|
|
|
|
|
|
|
|
>>> results = list(f()) # [genexp, lambda]
|
|
|
|
|
done
|
2018-07-10 18:17:34 -04:00
|
|
|
|
`a` is not yet bound
|
2018-07-09 15:24:47 -04:00
|
|
|
|
# The execution frame for f no longer exists in CPython,
|
|
|
|
|
# but f's locals live so long as they can still be referenced.
|
|
|
|
|
>>> list(map(type, results))
|
|
|
|
|
[<class 'generator'>, <class 'function'>]
|
|
|
|
|
>>> list(results[0])
|
|
|
|
|
[0, 1, 2]
|
|
|
|
|
>>> results[1]()
|
|
|
|
|
102
|
|
|
|
|
>>> a
|
|
|
|
|
42
|
|
|
|
|
|
|
|
|
|
|
2018-02-27 17:18:52 -05:00
|
|
|
|
References
|
|
|
|
|
==========
|
|
|
|
|
|
2018-07-11 20:54:47 -04:00
|
|
|
|
.. [1] Proof of concept implementation
|
2018-04-08 21:40:59 -04:00
|
|
|
|
(https://github.com/Rosuav/cpython/tree/assignment-expressions)
|
2018-04-18 09:15:42 -04:00
|
|
|
|
.. [2] Pivotal post regarding inline assignment semantics
|
|
|
|
|
(https://mail.python.org/pipermail/python-ideas/2018-March/049409.html)
|
2019-08-13 22:30:53 -04:00
|
|
|
|
.. [3] Discussion of PEP 572 TargetScopeError
|
|
|
|
|
(https://mail.python.org/archives/list/python-dev@python.org/thread/FXVSYCTQOTT7JCFACKPGPXKULBCGEPQY/)
|
2018-02-27 17:18:52 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Copyright
|
|
|
|
|
=========
|
|
|
|
|
|
|
|
|
|
This document has been placed in the public domain.
|
|
|
|
|
|
2018-07-11 20:54:47 -04:00
|
|
|
|
|
|
|
|
|
|
2018-02-27 17:18:52 -05:00
|
|
|
|
..
|
|
|
|
|
Local Variables:
|
|
|
|
|
mode: indented-text
|
|
|
|
|
indent-tabs-mode: nil
|
|
|
|
|
sentence-end-double-space: t
|
|
|
|
|
fill-column: 70
|
|
|
|
|
coding: utf-8
|
|
|
|
|
End:
|