PEP 577: Revise & withdraw based on review feedback (#665)
This commit is contained in:
parent
1a43bed5ca
commit
8857369e2a
520
pep-0577.rst
520
pep-0577.rst
|
@ -1,7 +1,7 @@
|
|||
PEP: 577
|
||||
Title: Augmented Assignment Expressions
|
||||
Author: Nick Coghlan <ncoghlan@gmail.com>
|
||||
Status: Draft
|
||||
Status: Withdrawn
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 14-May-2018
|
||||
|
@ -9,71 +9,62 @@ Python-Version: 3.8
|
|||
Post-History: 22-May-2018
|
||||
|
||||
|
||||
PEP Withdrawal
|
||||
==============
|
||||
|
||||
While working on this PEP, I realised that it didn't really address what was
|
||||
actually bothering me about PEP 572's proposed scoping rules for previously
|
||||
unreferenced assignment targets, and also had some significant undesirable
|
||||
consequences (most notably, allowing ``>>=` and ``<<=`` as inline augmented
|
||||
assignment operators that meant something entirely different from the ``>=``
|
||||
and ``<=`` comparison operators).
|
||||
|
||||
I also realised that even without dedicated syntax of their own, PEP 572 allows
|
||||
inline augmented assignments to be written using the ``operator`` module::
|
||||
|
||||
from operator import iadd
|
||||
if (target := iadd(target, value)) < limit:
|
||||
...
|
||||
|
||||
(The restriction to simple names as inline assignment targets means that the
|
||||
target expession can always be repeated without side effects)
|
||||
|
||||
Accordingly, I'm withdrawing this PEP without submitting it for pronouncement,
|
||||
and will instead be writing a replacement PEP that focuses specifically on the
|
||||
handling of assignment targets which haven't already been declared as local
|
||||
variables in the current scope (for both regular block scopes, and for scoped
|
||||
expressions).
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
This is a proposal to allow augmented assignment statements such as
|
||||
``x += 1`` to be used as expressions when the assignment target is a
|
||||
simple name.
|
||||
This is a proposal to allow augmented assignments such as ``x += 1`` to be
|
||||
used as expressions, not just statements.
|
||||
|
||||
For example, this will allow operation retry loops to be written as::
|
||||
As part of this, ``NAME := EXPR`` is proposed as an inline assignment expression
|
||||
that uses the new augmented assignment scoping rules, rather than implicitly
|
||||
defining a new local variable name the way that existing name binding
|
||||
statements do. The question of allowing expression level local variable
|
||||
declarations at function scope is deliberately separated from the question of
|
||||
allowing expression level name bindings, and deferred to a later PEP.
|
||||
|
||||
remaining_attempts = 10
|
||||
while remaining_attempts -= 1:
|
||||
try:
|
||||
result = attempt_operation()
|
||||
except Exception as exc:
|
||||
continue # Failed, so try again
|
||||
break # Success!
|
||||
else:
|
||||
# Ran out of attempts before succeeding
|
||||
raise OperationFailed("No more attempts remaining") from exc
|
||||
This PEP is a direct competitor to PEP 572 (although it borrows heavily from that
|
||||
PEP's motivation, and even shares the proposed syntax for inline assignments).
|
||||
See `Relationship with PEP 572`_ for more details on the connections between
|
||||
the two PEPs.
|
||||
|
||||
It is a direct competitor to PEP 572 (although it borrows heavily from that
|
||||
PEP's motivation, and even borrows its proposed syntax for a slightly
|
||||
different purpose).
|
||||
|
||||
As part of this, a semantic split is proposed between the handling of augmented
|
||||
assignments in regular block scopes (modules, classes, and functions), and the
|
||||
handling of augmented assignments in scoped expressions (lambda expressions,
|
||||
generator expressions, and comprehensions), such that augmented assignments
|
||||
default to targeting the nearest containing block scope.
|
||||
To improve the usability of the new expressions, a semantic split is proposed
|
||||
between the handling of augmented assignments in regular block scopes (modules,
|
||||
classes, and functions), and the handling of augmented assignments in scoped
|
||||
expressions (lambda expressions, generator expressions, and comprehensions),
|
||||
such that all inline assignments default to targeting the nearest containing
|
||||
block scope.
|
||||
|
||||
A new compile time ``TargetNameError`` is added as a subclass of ``SyntaxError``
|
||||
to handle cases where it either isn't clear to the compiler which target is
|
||||
expected to be rebound by an augmented assignment, or else the augmented
|
||||
assignment target scope is invalid for another reason.
|
||||
|
||||
Finally, ``NAME := EXPR`` is proposed as a name rebinding expression that
|
||||
uses the new augmented assignment scoping rules, rather than implicitly
|
||||
defining a new local variable name the way that existing name binding
|
||||
statements do.
|
||||
|
||||
|
||||
Relationship with PEP 572
|
||||
=========================
|
||||
|
||||
The case for allowing inline assignments at all is made in PEP 572. This
|
||||
competing PEP was initially going to propose an alternate surface syntax
|
||||
(``EXPR given NAME = EXPR``), while retaining the expression semantics from
|
||||
PEP 572, but that changed when discussing one of the initial motivating use
|
||||
cases for allowing embedded assignments at all: making it possible to easily
|
||||
calculate cumulative sums in comprehensions and generator expressions.
|
||||
|
||||
As a result of that, and unlike PEP 572, this PEP focuses primarily on use
|
||||
cases for inline augmented assignment. It also has the effect of converting
|
||||
cases that currently inevitably raise ``UnboundLocalError`` at function call
|
||||
time to report a new compile time ``TargetNameError``.
|
||||
|
||||
New syntax for a name rebinding expression (``NAME := TARGET``) is then added
|
||||
primarily as a lower level primitive to help illustrate, implement and explain
|
||||
the new augmented assignment semantics, rather than being the sole change being
|
||||
proposed.
|
||||
|
||||
The author of this PEP believes that this approach makes the value of the new
|
||||
flexibility in name rebinding clearer, while also mitigating many of the
|
||||
potential concerns raised with PEP 572 around explaining when to use
|
||||
``NAME = EXPR`` over ``NAME := EXPR`` (and vice-versa).
|
||||
to handle cases where it is deemed to be currently unclear which target is
|
||||
expected to be rebound by an inline assignment, or else the target scope
|
||||
for the inline assignment is considered invalid for another reason.
|
||||
|
||||
|
||||
Syntax and semantics
|
||||
|
@ -82,10 +73,10 @@ Syntax and semantics
|
|||
Augmented assignment expressions
|
||||
--------------------------------
|
||||
|
||||
The language grammar would be adjusted to allow augmented assignments that
|
||||
target simple names to appear as expressions, where the result of the
|
||||
augmented assignment expression is the same post-calculation reference as is
|
||||
being bound to the given target.
|
||||
The language grammar would be adjusted to allow augmented assignments to
|
||||
appear as expressions, where the result of the augmented assignment
|
||||
expression is the same post-calculation reference as is being bound to the
|
||||
given target.
|
||||
|
||||
For example::
|
||||
|
||||
|
@ -99,25 +90,96 @@ For example::
|
|||
>>> n
|
||||
9
|
||||
|
||||
Augmented assignments to attributes and container subscripts will continue to
|
||||
be restricted to the standalone statement form, and will be defined as
|
||||
returning ``None`` for purposes of interactive use. While a future PEP could
|
||||
potentially make the case for allowing those more complex targets as expressions,
|
||||
this PEP doesn't attempt to do so due to the ambiguity around whether or not
|
||||
they should call ``__getitem__`` and/or ``__getattribute`` on the target
|
||||
expression after performing the assignment, or else just return a reference to
|
||||
the binary operation result being assigned.
|
||||
For mutable targets, this means the result is always just the original object::
|
||||
|
||||
(Note: as an implementation detail, the language grammar itself may allow all
|
||||
existing permitted targets for all augmented assignments, regardless of whether
|
||||
they're appearing as an expression or a statement, with the restriction to
|
||||
statement level usage for more complex targets being implemented at the AST
|
||||
generation step).
|
||||
>>> seq = []
|
||||
>>> seq_id = id(seq)
|
||||
>>> seq += range(3)
|
||||
[0, 1, 2]
|
||||
>>> seq_id == id(seq)
|
||||
True
|
||||
|
||||
Augmented assignments to attributes and container subscripts will be permitted,
|
||||
with the result being the post-calculation reference being bound to the target,
|
||||
just as it is for simple name targets::
|
||||
|
||||
def increment(self, step=1):
|
||||
return self._value += step
|
||||
|
||||
In these cases, ``__getitem__`` and ``__getattribute__`` will *not* be called
|
||||
after the assignment has already taken place (they will only be called as
|
||||
needed to evaluate the in-place operation).
|
||||
|
||||
|
||||
Augmented assignment in block scopes
|
||||
Adding an inline assignment operator
|
||||
------------------------------------
|
||||
|
||||
Given only the addition of augmented assignment expressions, it would be
|
||||
possible to abuse a symbol like ``|=`` as a general purpose assignment
|
||||
operator by defining a ``Target`` wrapper type that worked as follows::
|
||||
|
||||
>>> class Target:
|
||||
... def __init__(self, value):
|
||||
... self.value = value
|
||||
... def __or__(self, other):
|
||||
... return Target(other)
|
||||
...
|
||||
>>> x = Target(10)
|
||||
>>> x.value
|
||||
10
|
||||
>>> x |= 42
|
||||
<__main__.Target object at 0x7f608caa8048>
|
||||
>>> x.value
|
||||
42
|
||||
|
||||
This is similar to the way that storing a single reference in a list was long
|
||||
used as a workaround for the lack of a ``nonlocal`` keyword, and can still be
|
||||
used today (in combination with ``operator.itemsetter``) to work around the
|
||||
lack of expression level assignments.
|
||||
|
||||
Rather than requiring such workarounds, this PEP instead proposes that
|
||||
PEP 572's "NAME := EXPR" syntax be adopted as a new inline assignment
|
||||
expression that uses the augmented assignment scoping rules described below.
|
||||
|
||||
This cleanly handles cases where only the new value is of interest, and the
|
||||
previously bound value (if any) can just be discarded completely.
|
||||
|
||||
Note that for both simple names and complex assignment targets, the inline
|
||||
assignment operator does *not* read the previous reference before assigning
|
||||
the new one. However, when used at function scope (either directly or inside
|
||||
a scoped expression), it does *not* implicitly define a new local variable,
|
||||
and will instead raise ``TargetNameError`` (as described for augmented
|
||||
assignments below).
|
||||
|
||||
|
||||
Assignment operator precedence
|
||||
------------------------------
|
||||
|
||||
To preserve the existing semantics of augmented assignment statements,
|
||||
inline assignment operators will be defined as being of lower precedence
|
||||
than all other operators, include the comma pseudo-operator. This ensures
|
||||
that when used as a top level expression the entire right hand side of the
|
||||
expression is still interpreted as the value to be processed (even when that
|
||||
value is a tuple without parentheses).
|
||||
|
||||
The difference this introduces relative to PEP 572 is that where
|
||||
``(n := first, second)`` sets ``n = first`` in PEP 572, in this PEP it would set
|
||||
``n = (first, second)`, and getting the first meaning would require an extra
|
||||
set of parentheses (``((n := first), second)``).
|
||||
|
||||
PEP 572 quite reasonably notes that this results in ambiguity when assignment
|
||||
expressions are used as function call arguments. This PEP resolves that concern
|
||||
a different way by requiring that assignment expressions be parenthesised
|
||||
when used as arguments to a function call (unless they're the sole argument).
|
||||
|
||||
This is a more relaxed version of the restriction placed on generator
|
||||
expressions (which always require parentheses, except when they're the sole
|
||||
argument to a function call).
|
||||
|
||||
|
||||
Augmented assignment to names in block scopes
|
||||
---------------------------------------------
|
||||
|
||||
No target name binding changes are proposed for augmented assignments at module
|
||||
or class scope (this also includes code executed using "exec" or "eval"). These
|
||||
will continue to implicitly declare a new local variable as the binding target
|
||||
|
@ -180,7 +242,7 @@ as they do today::
|
|||
n += 1 # Raises UnboundLocalError at runtime
|
||||
|
||||
def local_declaration_without_initial_assignment():
|
||||
n : typing.Any
|
||||
n: typing.Any
|
||||
n += 1 # Raises UnboundLocalError at runtime
|
||||
|
||||
Whereas the following would raise a compile time ``DeprecationWarning``
|
||||
|
@ -202,11 +264,16 @@ as a backwards incompatible change that requires a deprecation period.
|
|||
When augmented assignments are used as expressions in function scope (rather
|
||||
than as standalone statements), there aren't any backwards compatibility
|
||||
concerns, so the compile time name binding checks would be enforced immediately
|
||||
in Python 3.9.
|
||||
in Python 3.8.
|
||||
|
||||
Similarly, the new inline assignment expressions would always require explicit
|
||||
predeclaration of their target scope when used as part of a function, at least
|
||||
for Python 3.8. (See the design discussion section for notes on potentially
|
||||
revisiting that restriction in the future).
|
||||
|
||||
|
||||
Augmented assignment in scoped expressions
|
||||
------------------------------------------
|
||||
Augmented assignment to names in scoped expressions
|
||||
---------------------------------------------------
|
||||
|
||||
Scoped expressions is a new collective term being proposed for expressions that
|
||||
introduce a new nested scope of execution, either as an intrinsic part of their
|
||||
|
@ -227,6 +294,12 @@ be defined as follows:
|
|||
binding or declaration can be found in that scope, then ``TargetNameError``
|
||||
will be raised at compile time (rather than creating a new binding within
|
||||
the scoped expression).
|
||||
* if the containing block scope is a function scope, and the target name is
|
||||
explicitly declared as ``global`` or ``nonlocal``, then it will be use the
|
||||
same scope declaration in the body of the scoped expression
|
||||
* if the containing block scope is a function scope, and the target name is
|
||||
a local variable in that function, then it will be implicitly declared as
|
||||
``nonlocal`` in the body of the scoped expression
|
||||
* if the containing block scope is a class scope, than ``TargetNameError`` will
|
||||
always be raised, with a dedicated message indicating that combining class
|
||||
scopes with augmented assignments in scoped expressions is not currently
|
||||
|
@ -282,66 +355,53 @@ initial implementation rather than needing to be phased in as a potentially
|
|||
backwards incompatible change.
|
||||
|
||||
|
||||
Promoting nonlocal references to global references
|
||||
--------------------------------------------------
|
||||
Design discussion
|
||||
=================
|
||||
|
||||
As part of the above changes, all ``nonlocal NAME`` declarations (including
|
||||
the implicit ones added for augmented assignment targets in scoped expressions
|
||||
at function scope) will be changed to take explicit ``global NAME`` declarations
|
||||
into account, such that the affected name is considered ``global`` in the inner
|
||||
scope as well. For example, the following code would work by binding ``x`` in
|
||||
the global scope instead of raising ``SyntaxError`` as it does today::
|
||||
Allowing complex assignment targets
|
||||
-----------------------------------
|
||||
|
||||
>>> def f():
|
||||
... global x
|
||||
... def g():
|
||||
... nonlocal x
|
||||
... x = 1
|
||||
... g()
|
||||
>>> f()
|
||||
>>> x
|
||||
1
|
||||
The initial drafts of this PEP kept PEP 572's restriction to single name targets
|
||||
when augmented assignments were used as expressions, allowing attribute and
|
||||
subscript targets solely for the statement form.
|
||||
|
||||
However, enforcing that required varying the permitted targets based on whether
|
||||
or not the augmented assignment was a top level expression or not, as well as
|
||||
explaining why ``n += 1``, ``(n += 1)``, and ``self.n += 1`` were all legal,
|
||||
but ``(self.n += 1)`` was prohibited, so the proposal was simplified to allow
|
||||
all existing augmented assignment targets for the expression form as well.
|
||||
|
||||
Since this PEP defines ``TARGET := EXPR`` as a variant on augmented assignment,
|
||||
that also gained support for assignment and subscript targets.
|
||||
|
||||
|
||||
Adding an inline assignment expression
|
||||
--------------------------------------
|
||||
Augmented assignment or name binding only?
|
||||
------------------------------------------
|
||||
|
||||
Given just the above changes, it would be possible to abuse a symbol like
|
||||
``|=`` as a general purpose assignment operator by defining a ``Target`` wrapper
|
||||
type that worked as follows::
|
||||
PEP 572 makes a reasonable case that the potential use cases for inline
|
||||
augmented assignment are notably weaker than those for inline assignment in
|
||||
general, so it's acceptable to require that they be spelled as ``x := x + 1``,
|
||||
bypassing any in-place augmented assignment methods.
|
||||
|
||||
>>> class Target:
|
||||
... def __init__(self, value):
|
||||
... self.value = value
|
||||
... def __or__(self, other):
|
||||
... return Target(other)
|
||||
...
|
||||
>>> x = Target(10)
|
||||
>>> x.value
|
||||
10
|
||||
>>> x |= 42
|
||||
<__main__.Target object at 0x7f608caa8048>
|
||||
>>> x.value
|
||||
42
|
||||
While this is at least arguably true for the builtin types (where potential
|
||||
counterexamples would probably need to focus on set manipulation use cases
|
||||
that the PEP author doesn't personally have), it would also rule out more
|
||||
memory intensive use cases like manipulation of NumPy arrays, where the data
|
||||
copying involved in out-of-place operations can make them impractical as
|
||||
alternatives to their in-place counterparts.
|
||||
|
||||
Rather than requiring such workarounds, this PEP instead proposes that
|
||||
PEP 572's "NAME := EXPR" syntax be adopted as a new inline assignment
|
||||
expression that uses the augmented assignment scoping rules described above.
|
||||
That said, this PEP mainly exists because the PEP author found the inline
|
||||
assignment proposal much easier to grasp as "It's like ``+=``, only skipping
|
||||
the addition step", and also liked the way that that framing provides an
|
||||
actual semantic difference between ``NAME = EXPR`` and ``NAME := EXPR`` at
|
||||
function scope.
|
||||
|
||||
This cleanly handles cases where only the new value is of interest, and the
|
||||
previously bound value (if any) can just be discarded completely.
|
||||
|
||||
As with other augmented assignment operators, function level usage would always
|
||||
require a preceding name binding or scope declaration to avoid getting
|
||||
``TargetNameError`` (as a new operator, there's no need for a
|
||||
``DeprecationWarning`` period).
|
||||
|
||||
This difference in target scoping behaviour means that the ``NAME := EXPR``
|
||||
That difference in target scoping behaviour means that the ``NAME := EXPR``
|
||||
syntax would be expected to have two primary use cases:
|
||||
|
||||
- as a way of allowing assignments to be embedded as an expression in an ``if``
|
||||
* as a way of allowing assignments to be embedded as an expression in an ``if``
|
||||
or ``while`` statement, or as part of a scoped expression
|
||||
- as a way of requesting a compile time check that the target name be previously
|
||||
* as a way of requesting a compile time check that the target name be previously
|
||||
declared or bound in the current function scope
|
||||
|
||||
At module or class scope, ``NAME = EXPR`` and ``NAME := EXPR`` would be
|
||||
|
@ -351,32 +411,72 @@ type checkers would be encouraged to enforce the same "declaration or assignment
|
|||
required before use" behaviour for ``NAME := EXPR`` as the compiler would
|
||||
enforce at function scope.
|
||||
|
||||
Unlike existing augmented assignment statements, inline assignment expressions
|
||||
would be restricted entirely to single name targets (even when used as a
|
||||
standalone statement).
|
||||
|
||||
Postponing a decision on expression level target declarations
|
||||
-------------------------------------------------------------
|
||||
|
||||
Design discussion
|
||||
=================
|
||||
At least for Python 3.8, usage of inline assignments (whether augmented or not)
|
||||
at function scope would always require a preceding name binding or scope
|
||||
declaration to avoid getting ``TargetNameError``, even when used outside a
|
||||
scoped expression.
|
||||
|
||||
Restriction to single name targets
|
||||
----------------------------------
|
||||
The intent behind this requirement is to clearly separate the following two
|
||||
language design questions:
|
||||
|
||||
This PEP keeps PEP 572's restriction to single name targets when augmented
|
||||
assignments are used as expressions, restricting attribute and subscript
|
||||
targets to the statement form.
|
||||
1. Can an expression rebind a name in the current scope?
|
||||
2. Can an expression declare a new name in the current scope?
|
||||
|
||||
While the case could be made that it would be more consistent to allow
|
||||
those in the expression form as well, the rationale for excluding them is
|
||||
that it's inherently ambiguous as to whether or not the expression form would
|
||||
return the expression being bound, or the result of evaluating the LHS as
|
||||
an expression (rather than as an assignment target).
|
||||
For module global scopes, the answer to both of those questions is unequivocally
|
||||
"Yes", because it's a language level guarantee that mutating the ``globals()``
|
||||
dict will immediately impact the runtime module scope, and ``global NAME``
|
||||
declarations inside a function can have the same effect (as can importing the
|
||||
currently executing module and modifying its attributes).
|
||||
|
||||
If this restriction was deemed unduly confusing, then the simplest resolution
|
||||
would be to retain the current semantics of augmented assignment statements
|
||||
and have the expression result be the reference bound to the target (i.e.
|
||||
``__getitem__`` and ``__getattribute__`` would *not* be called after the
|
||||
assignment had already taken place)
|
||||
For class scopes, the answer to both questions is also "Yes" in practice,
|
||||
although less unequivocally so, since the semantics of ``locals()`` are
|
||||
currently formally unspecified. However, if the current behaviour of ``locals()``
|
||||
at class scope is taken as normative (as PEP 558 proposes), then this is
|
||||
essentially the same scenario as manipulating the module globals, just using
|
||||
``locals()`` instead.
|
||||
|
||||
For function scopes, however, the current answers to these two questions are
|
||||
respectively "Yes" and "No". Expression level rebinding of function locals is
|
||||
already possible thanks to lexically nested scopes and explicit ``nonlocal NAME``
|
||||
expressions. While this PEP will likely make expression level rebinding more
|
||||
common than it is today, it isn't a fundamentally new concept for the language.
|
||||
|
||||
By contrast, declaring a *new* function local variable is currently a statement
|
||||
level action, involving one of:
|
||||
|
||||
* an assignment statement (``NAME = EXPR``, ``OTHER_TARGET = NAME = EXPR``, etc)
|
||||
* a variable declaration (``NAME : EXPR``)
|
||||
* a nested function definition
|
||||
* a nested class definition
|
||||
* a ``for`` loop
|
||||
* a ``with`` statement
|
||||
* an ``except`` clause (with limited scope of access)
|
||||
|
||||
The historical trend for the language has actually been to *remove* support for
|
||||
expression level declarations of function local names, first with the
|
||||
introduction of "fast locals" semantics (which made the introduction of names
|
||||
via ``locals()`` unsupported for function scopes), and again with the hiding
|
||||
of comprehension iteration variables in Python 3.0.
|
||||
|
||||
Now, it may be that in Python 3.9, we decide to revisit this question based on
|
||||
our experience with expression level name binding in Python 3.8, and decide that
|
||||
we really do want expression level function local variable declarations as well,
|
||||
and that we want ``NAME := EXPR`` to be the way we spell that (rather than,
|
||||
for example, spelling inline declarations more explicitly as
|
||||
``NAME := EXPR given NAME``, which would permit them to carry type annotations,
|
||||
and also permit them to declare new local variables in scoped expressions,
|
||||
rather than having to pollute the namespace in their containing scope).
|
||||
|
||||
But the proposal in this PEP is that we explicitly give ourselves a full
|
||||
release to decide how much we want that feature, and exactly where we find
|
||||
its absence irritating. Python has survived happily without expression level
|
||||
name bindings *or* declarations for decades, so we can afford to give ourselves
|
||||
a couple of years to decide if we really want *both* of those, or if expression
|
||||
level bindings are sufficient.
|
||||
|
||||
|
||||
Ignoring scoped expressions when determining augmented assignment targets
|
||||
|
@ -409,7 +509,7 @@ areas:
|
|||
the name already be defined to the new name binding operator (raising
|
||||
``TargetNameError`` rather than implicitly declaring new local variables at
|
||||
function scope)
|
||||
- it includes lambda expressions in the set of scopes that gets ignored for
|
||||
- it includes lambda expressions in the set of scopes that get ignored for
|
||||
target name binding purposes, making this transparency to assignments common
|
||||
to all of the scoped expressions rather than being specific to comprehensions
|
||||
and generator expressions
|
||||
|
@ -428,7 +528,9 @@ One of the challenges with PEP 572 is the fact that ``NAME = EXPR`` and
|
|||
``NAME := EXPR`` are entirely semantically equivalent at every scope. This
|
||||
makes the two forms hard to teach, since there's no inherent nudge towards
|
||||
choosing one over the other at the statement level, so you end up having to
|
||||
resort to "``NAME = EXPR`` is preferred because it's been around longer".
|
||||
resort to "``NAME = EXPR`` is preferred because it's been around longer"
|
||||
(and PEP 572 proposes to enfore that historical idiosyncrasy at the compiler
|
||||
level).
|
||||
|
||||
That semantic equivalence is difficult to avoid at module and class scope while
|
||||
still having ``if NAME := EXPR:`` and ``while NAME := EXPR:`` work sensibly, but
|
||||
|
@ -440,6 +542,14 @@ providing a reasonable incentive to continue to default to using the
|
|||
indicating that the targeted name has already been bound or declared and hence
|
||||
should already be known to the compiler).
|
||||
|
||||
If Guido were to declare that support for inline declarations was a hard
|
||||
design requirement, then this PEP would be updated to propose that
|
||||
``EXPR given NAME`` also be introduced as a way to support inline name declarations
|
||||
after arbitrary expressions (this would allow the inline name declarations to be
|
||||
deferred until the end of a complex expression rather than needing to be
|
||||
embedded in the middle of it, and PEP 8 would gain a recommendation encouraging
|
||||
that style).
|
||||
|
||||
|
||||
Disallowing augmented assignments in class level scoped expressions
|
||||
-------------------------------------------------------------------
|
||||
|
@ -459,6 +569,43 @@ other than directly inline in the class body (e.g. in a separate helper
|
|||
function).
|
||||
|
||||
|
||||
Comparison operators vs assignment operators
|
||||
--------------------------------------------
|
||||
|
||||
The ``OP=`` construct as an expression currently indicates a comparison
|
||||
operation::
|
||||
|
||||
x == y # Equals
|
||||
x >= y # Greater-than-or-equal-to
|
||||
x <= y # Less-than-or-equal-to
|
||||
|
||||
Both this PEP and PEP 572 propose adding at least one operator that's somewhat
|
||||
similar in appearance, but defines an assignment instead::
|
||||
|
||||
x := y # Becomes
|
||||
|
||||
This PEP then goes much further and allows all *13* augmented assignment symbols
|
||||
to be uses as binary operators::
|
||||
|
||||
x += y # In-place add
|
||||
x -= y # In-place minus
|
||||
x *= y # In-place multiply
|
||||
x @= y # In-place matrix multiply
|
||||
x /= y # In-place division
|
||||
x //= y # In-place int division
|
||||
x %= y # In-place mod
|
||||
x &= y # In-place bitwise and
|
||||
x |= y # In-place bitwise or
|
||||
x ^= y # In-place bitwise xor
|
||||
x <<= y # In-place left shift
|
||||
x >>= y # In-place right shift
|
||||
x **= y # In-place power
|
||||
|
||||
Of those additional binary operators, the most questionable would be the
|
||||
bitshift assignment operators, since they're each only one doubled character
|
||||
away from one of the inclusive ordered comparison operators.
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
|
@ -468,29 +615,29 @@ Simplifying retry loops
|
|||
There are currently a few different options for writing retry loops, including::
|
||||
|
||||
# Post-decrementing a counter
|
||||
remaining_attempts = 9
|
||||
remaining_attempts = MAX_ATTEMPTS
|
||||
while remaining_attempts:
|
||||
remaining_attempts -= 1
|
||||
try:
|
||||
result = attempt_operation()
|
||||
except Exception as exc:
|
||||
remaining_attempts -= 1
|
||||
continue # Failed, so try again
|
||||
log.debug(f"Succeeded after {attempts} attempts")
|
||||
break # Success!
|
||||
else:
|
||||
# Ran out of attempts before succeeding
|
||||
raise OperationFailed("No more attempts remaining") from exc
|
||||
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||
|
||||
# Loop-and-a-half with a pre-decremented counter
|
||||
remaining_attempts = 10
|
||||
# Loop-and-a-half with a pre-incremented counter
|
||||
attempt = 0
|
||||
while True:
|
||||
remaining_attempts -= 1
|
||||
if not remaining_attempts:
|
||||
# Ran out of attempts before succeeding
|
||||
raise OperationFailed("No more attempts remaining") from exc
|
||||
attempts += 1
|
||||
if attempts > MAX_ATTEMPTS:
|
||||
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||
try:
|
||||
result = attempt_operation()
|
||||
except Exception as exc:
|
||||
continue # Failed, so try again
|
||||
log.debug(f"Succeeded after {attempts} attempts")
|
||||
break # Success!
|
||||
|
||||
Each of the available options hides some aspect of the intended loop structure
|
||||
|
@ -500,16 +647,16 @@ or both.
|
|||
The proposal in this PEP allows both the state modification and the exit
|
||||
condition to be included directly in the loop header::
|
||||
|
||||
remaining_attempts = 10
|
||||
while remaining_attempts -= 1:
|
||||
attempt = 0
|
||||
while (attempt += 1) <= MAX_ATTEMPTS:
|
||||
try:
|
||||
result = attempt_operation()
|
||||
except Exception as exc:
|
||||
continue # Failed, so try again
|
||||
log.debug(f"Succeeded after {attempts} attempts")
|
||||
break # Success!
|
||||
else:
|
||||
# Ran out of attempts before succeeding
|
||||
raise OperationFailed("No more attempts remaining") from exc
|
||||
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||
|
||||
|
||||
Simplifying if-elif chains
|
||||
|
@ -552,7 +699,7 @@ indicated as local before the first use as a ``:=`` target, either by
|
|||
binding it to a value (as shown above), or else by including an appropriate
|
||||
explicit type declaration::
|
||||
|
||||
m : typing.re.Match
|
||||
m: typing.re.Match
|
||||
if m := pattern.match(data):
|
||||
...
|
||||
elif m := other_pattern.match(data):
|
||||
|
@ -570,20 +717,18 @@ The proposal in this PEP makes it straightforward to capture and reuse
|
|||
intermediate values in comprehensions and generator expressions by
|
||||
exporting them to the containing block scope::
|
||||
|
||||
factor = 1
|
||||
factor: int
|
||||
while any(n % (factor := p) == 0 for p in small_primes):
|
||||
n //= factor
|
||||
|
||||
def cumulative_sums(data, start=0)
|
||||
total = start
|
||||
yield from (total += value for value in data)
|
||||
return total
|
||||
total = 0
|
||||
partial_sums = [total += value for value in data]
|
||||
|
||||
|
||||
Allowing lambda expressions to act more like re-usable code thunks
|
||||
------------------------------------------------------------------
|
||||
|
||||
This PEP allows the closure-based counter example::
|
||||
This PEP allows the classic closure usage example::
|
||||
|
||||
def make_counter(start=0):
|
||||
x = start
|
||||
|
@ -597,7 +742,7 @@ To be abbreviated as::
|
|||
|
||||
def make_counter(start=0):
|
||||
x = start
|
||||
return (lambda step=1: x += step)
|
||||
return lambda step=1: x += step
|
||||
|
||||
While the latter form is still a conceptually dense piece of code, it can be
|
||||
reasonably argued that the lack of boilerplate (where the "def", "nonlocal",
|
||||
|
@ -606,6 +751,35 @@ have been replaced with the "lambda" keyword) may make it easier to read in
|
|||
practice.
|
||||
|
||||
|
||||
Relationship with PEP 572
|
||||
=========================
|
||||
|
||||
The case for allowing inline assignments at all is made in PEP 572. This
|
||||
competing PEP was initially going to propose an alternate surface syntax
|
||||
(``EXPR given NAME = EXPR``), while retaining the expression semantics from
|
||||
PEP 572, but that changed when discussing one of the initial motivating use
|
||||
cases for allowing embedded assignments at all: making it possible to easily
|
||||
calculate cumulative sums in comprehensions and generator expressions.
|
||||
|
||||
As a result of that, and unlike PEP 572, this PEP focuses primarily on use
|
||||
cases for inline augmented assignment. It also has the effect of converting
|
||||
cases that currently inevitably raise ``UnboundLocalError`` at function call
|
||||
time to report a new compile time ``TargetNameError``.
|
||||
|
||||
New syntax for a name rebinding expression (``NAME := TARGET``) is then added
|
||||
not only to handle the same use cases as are identified in PEP 572, but also
|
||||
as a lower level primitive to help illustrate, implement and explain
|
||||
the new augmented assignment semantics, rather than being the sole change being
|
||||
proposed.
|
||||
|
||||
The author of this PEP believes that this approach makes the value of the new
|
||||
flexibility in name rebinding clearer, while also mitigating many of the
|
||||
potential concerns raised with PEP 572 around explaining when to use
|
||||
``NAME = EXPR`` over ``NAME := EXPR`` (and vice-versa), without resorting to
|
||||
prohibiting the bare statement form of ``NAME := EXPR`` outright (such
|
||||
that ``NAME := EXPR`` is a compile error, but ``(NAME := EXPR)`` is permitted).
|
||||
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
|
|
Loading…
Reference in New Issue