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
|
PEP: 577
|
||||||
Title: Augmented Assignment Expressions
|
Title: Augmented Assignment Expressions
|
||||||
Author: Nick Coghlan <ncoghlan@gmail.com>
|
Author: Nick Coghlan <ncoghlan@gmail.com>
|
||||||
Status: Draft
|
Status: Withdrawn
|
||||||
Type: Standards Track
|
Type: Standards Track
|
||||||
Content-Type: text/x-rst
|
Content-Type: text/x-rst
|
||||||
Created: 14-May-2018
|
Created: 14-May-2018
|
||||||
|
@ -9,71 +9,62 @@ Python-Version: 3.8
|
||||||
Post-History: 22-May-2018
|
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
|
Abstract
|
||||||
========
|
========
|
||||||
|
|
||||||
This is a proposal to allow augmented assignment statements such as
|
This is a proposal to allow augmented assignments such as ``x += 1`` to be
|
||||||
``x += 1`` to be used as expressions when the assignment target is a
|
used as expressions, not just statements.
|
||||||
simple name.
|
|
||||||
|
|
||||||
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
|
This PEP is a direct competitor to PEP 572 (although it borrows heavily from that
|
||||||
while remaining_attempts -= 1:
|
PEP's motivation, and even shares the proposed syntax for inline assignments).
|
||||||
try:
|
See `Relationship with PEP 572`_ for more details on the connections between
|
||||||
result = attempt_operation()
|
the two PEPs.
|
||||||
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
|
|
||||||
|
|
||||||
It is a direct competitor to PEP 572 (although it borrows heavily from that
|
To improve the usability of the new expressions, a semantic split is proposed
|
||||||
PEP's motivation, and even borrows its proposed syntax for a slightly
|
between the handling of augmented assignments in regular block scopes (modules,
|
||||||
different purpose).
|
classes, and functions), and the handling of augmented assignments in scoped
|
||||||
|
expressions (lambda expressions, generator expressions, and comprehensions),
|
||||||
As part of this, a semantic split is proposed between the handling of augmented
|
such that all inline assignments default to targeting the nearest containing
|
||||||
assignments in regular block scopes (modules, classes, and functions), and the
|
block scope.
|
||||||
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.
|
|
||||||
|
|
||||||
A new compile time ``TargetNameError`` is added as a subclass of ``SyntaxError``
|
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
|
to handle cases where it is deemed to be currently unclear which target is
|
||||||
expected to be rebound by an augmented assignment, or else the augmented
|
expected to be rebound by an inline assignment, or else the target scope
|
||||||
assignment target scope is invalid for another reason.
|
for the inline assignment is considered 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).
|
|
||||||
|
|
||||||
|
|
||||||
Syntax and semantics
|
Syntax and semantics
|
||||||
|
@ -82,10 +73,10 @@ Syntax and semantics
|
||||||
Augmented assignment expressions
|
Augmented assignment expressions
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
The language grammar would be adjusted to allow augmented assignments that
|
The language grammar would be adjusted to allow augmented assignments to
|
||||||
target simple names to appear as expressions, where the result of the
|
appear as expressions, where the result of the augmented assignment
|
||||||
augmented assignment expression is the same post-calculation reference as is
|
expression is the same post-calculation reference as is being bound to the
|
||||||
being bound to the given target.
|
given target.
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
|
@ -99,25 +90,96 @@ For example::
|
||||||
>>> n
|
>>> n
|
||||||
9
|
9
|
||||||
|
|
||||||
Augmented assignments to attributes and container subscripts will continue to
|
For mutable targets, this means the result is always just the original object::
|
||||||
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.
|
|
||||||
|
|
||||||
(Note: as an implementation detail, the language grammar itself may allow all
|
>>> seq = []
|
||||||
existing permitted targets for all augmented assignments, regardless of whether
|
>>> seq_id = id(seq)
|
||||||
they're appearing as an expression or a statement, with the restriction to
|
>>> seq += range(3)
|
||||||
statement level usage for more complex targets being implemented at the AST
|
[0, 1, 2]
|
||||||
generation step).
|
>>> 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
|
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
|
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
|
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
|
n += 1 # Raises UnboundLocalError at runtime
|
||||||
|
|
||||||
def local_declaration_without_initial_assignment():
|
def local_declaration_without_initial_assignment():
|
||||||
n : typing.Any
|
n: typing.Any
|
||||||
n += 1 # Raises UnboundLocalError at runtime
|
n += 1 # Raises UnboundLocalError at runtime
|
||||||
|
|
||||||
Whereas the following would raise a compile time ``DeprecationWarning``
|
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
|
When augmented assignments are used as expressions in function scope (rather
|
||||||
than as standalone statements), there aren't any backwards compatibility
|
than as standalone statements), there aren't any backwards compatibility
|
||||||
concerns, so the compile time name binding checks would be enforced immediately
|
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
|
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
|
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``
|
binding or declaration can be found in that scope, then ``TargetNameError``
|
||||||
will be raised at compile time (rather than creating a new binding within
|
will be raised at compile time (rather than creating a new binding within
|
||||||
the scoped expression).
|
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
|
* if the containing block scope is a class scope, than ``TargetNameError`` will
|
||||||
always be raised, with a dedicated message indicating that combining class
|
always be raised, with a dedicated message indicating that combining class
|
||||||
scopes with augmented assignments in scoped expressions is not currently
|
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.
|
backwards incompatible change.
|
||||||
|
|
||||||
|
|
||||||
Promoting nonlocal references to global references
|
Design discussion
|
||||||
--------------------------------------------------
|
=================
|
||||||
|
|
||||||
As part of the above changes, all ``nonlocal NAME`` declarations (including
|
Allowing complex assignment targets
|
||||||
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::
|
|
||||||
|
|
||||||
>>> def f():
|
The initial drafts of this PEP kept PEP 572's restriction to single name targets
|
||||||
... global x
|
when augmented assignments were used as expressions, allowing attribute and
|
||||||
... def g():
|
subscript targets solely for the statement form.
|
||||||
... nonlocal x
|
|
||||||
... x = 1
|
However, enforcing that required varying the permitted targets based on whether
|
||||||
... g()
|
or not the augmented assignment was a top level expression or not, as well as
|
||||||
>>> f()
|
explaining why ``n += 1``, ``(n += 1)``, and ``self.n += 1`` were all legal,
|
||||||
>>> x
|
but ``(self.n += 1)`` was prohibited, so the proposal was simplified to allow
|
||||||
1
|
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
|
PEP 572 makes a reasonable case that the potential use cases for inline
|
||||||
``|=`` as a general purpose assignment operator by defining a ``Target`` wrapper
|
augmented assignment are notably weaker than those for inline assignment in
|
||||||
type that worked as follows::
|
general, so it's acceptable to require that they be spelled as ``x := x + 1``,
|
||||||
|
bypassing any in-place augmented assignment methods.
|
||||||
|
|
||||||
>>> class Target:
|
While this is at least arguably true for the builtin types (where potential
|
||||||
... def __init__(self, value):
|
counterexamples would probably need to focus on set manipulation use cases
|
||||||
... self.value = value
|
that the PEP author doesn't personally have), it would also rule out more
|
||||||
... def __or__(self, other):
|
memory intensive use cases like manipulation of NumPy arrays, where the data
|
||||||
... return Target(other)
|
copying involved in out-of-place operations can make them impractical as
|
||||||
...
|
alternatives to their in-place counterparts.
|
||||||
>>> x = Target(10)
|
|
||||||
>>> x.value
|
|
||||||
10
|
|
||||||
>>> x |= 42
|
|
||||||
<__main__.Target object at 0x7f608caa8048>
|
|
||||||
>>> x.value
|
|
||||||
42
|
|
||||||
|
|
||||||
Rather than requiring such workarounds, this PEP instead proposes that
|
That said, this PEP mainly exists because the PEP author found the inline
|
||||||
PEP 572's "NAME := EXPR" syntax be adopted as a new inline assignment
|
assignment proposal much easier to grasp as "It's like ``+=``, only skipping
|
||||||
expression that uses the augmented assignment scoping rules described above.
|
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
|
That difference in target scoping behaviour means that the ``NAME := EXPR``
|
||||||
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``
|
|
||||||
syntax would be expected to have two primary use cases:
|
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
|
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
|
declared or bound in the current function scope
|
||||||
|
|
||||||
At module or class scope, ``NAME = EXPR`` and ``NAME := EXPR`` would be
|
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
|
required before use" behaviour for ``NAME := EXPR`` as the compiler would
|
||||||
enforce at function scope.
|
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
|
1. Can an expression rebind a name in the current scope?
|
||||||
assignments are used as expressions, restricting attribute and subscript
|
2. Can an expression declare a new name in the current scope?
|
||||||
targets to the statement form.
|
|
||||||
|
|
||||||
While the case could be made that it would be more consistent to allow
|
For module global scopes, the answer to both of those questions is unequivocally
|
||||||
those in the expression form as well, the rationale for excluding them is
|
"Yes", because it's a language level guarantee that mutating the ``globals()``
|
||||||
that it's inherently ambiguous as to whether or not the expression form would
|
dict will immediately impact the runtime module scope, and ``global NAME``
|
||||||
return the expression being bound, or the result of evaluating the LHS as
|
declarations inside a function can have the same effect (as can importing the
|
||||||
an expression (rather than as an assignment target).
|
currently executing module and modifying its attributes).
|
||||||
|
|
||||||
If this restriction was deemed unduly confusing, then the simplest resolution
|
For class scopes, the answer to both questions is also "Yes" in practice,
|
||||||
would be to retain the current semantics of augmented assignment statements
|
although less unequivocally so, since the semantics of ``locals()`` are
|
||||||
and have the expression result be the reference bound to the target (i.e.
|
currently formally unspecified. However, if the current behaviour of ``locals()``
|
||||||
``__getitem__`` and ``__getattribute__`` would *not* be called after the
|
at class scope is taken as normative (as PEP 558 proposes), then this is
|
||||||
assignment had already taken place)
|
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
|
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
|
the name already be defined to the new name binding operator (raising
|
||||||
``TargetNameError`` rather than implicitly declaring new local variables at
|
``TargetNameError`` rather than implicitly declaring new local variables at
|
||||||
function scope)
|
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
|
target name binding purposes, making this transparency to assignments common
|
||||||
to all of the scoped expressions rather than being specific to comprehensions
|
to all of the scoped expressions rather than being specific to comprehensions
|
||||||
and generator expressions
|
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
|
``NAME := EXPR`` are entirely semantically equivalent at every scope. This
|
||||||
makes the two forms hard to teach, since there's no inherent nudge towards
|
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
|
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
|
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
|
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
|
indicating that the targeted name has already been bound or declared and hence
|
||||||
should already be known to the compiler).
|
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
|
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).
|
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
|
Examples
|
||||||
========
|
========
|
||||||
|
|
||||||
|
@ -468,29 +615,29 @@ Simplifying retry loops
|
||||||
There are currently a few different options for writing retry loops, including::
|
There are currently a few different options for writing retry loops, including::
|
||||||
|
|
||||||
# Post-decrementing a counter
|
# Post-decrementing a counter
|
||||||
remaining_attempts = 9
|
remaining_attempts = MAX_ATTEMPTS
|
||||||
while remaining_attempts:
|
while remaining_attempts:
|
||||||
|
remaining_attempts -= 1
|
||||||
try:
|
try:
|
||||||
result = attempt_operation()
|
result = attempt_operation()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
remaining_attempts -= 1
|
|
||||||
continue # Failed, so try again
|
continue # Failed, so try again
|
||||||
|
log.debug(f"Succeeded after {attempts} attempts")
|
||||||
break # Success!
|
break # Success!
|
||||||
else:
|
else:
|
||||||
# Ran out of attempts before succeeding
|
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||||
raise OperationFailed("No more attempts remaining") from exc
|
|
||||||
|
|
||||||
# Loop-and-a-half with a pre-decremented counter
|
# Loop-and-a-half with a pre-incremented counter
|
||||||
remaining_attempts = 10
|
attempt = 0
|
||||||
while True:
|
while True:
|
||||||
remaining_attempts -= 1
|
attempts += 1
|
||||||
if not remaining_attempts:
|
if attempts > MAX_ATTEMPTS:
|
||||||
# Ran out of attempts before succeeding
|
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||||
raise OperationFailed("No more attempts remaining") from exc
|
|
||||||
try:
|
try:
|
||||||
result = attempt_operation()
|
result = attempt_operation()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
continue # Failed, so try again
|
continue # Failed, so try again
|
||||||
|
log.debug(f"Succeeded after {attempts} attempts")
|
||||||
break # Success!
|
break # Success!
|
||||||
|
|
||||||
Each of the available options hides some aspect of the intended loop structure
|
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
|
The proposal in this PEP allows both the state modification and the exit
|
||||||
condition to be included directly in the loop header::
|
condition to be included directly in the loop header::
|
||||||
|
|
||||||
remaining_attempts = 10
|
attempt = 0
|
||||||
while remaining_attempts -= 1:
|
while (attempt += 1) <= MAX_ATTEMPTS:
|
||||||
try:
|
try:
|
||||||
result = attempt_operation()
|
result = attempt_operation()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
continue # Failed, so try again
|
continue # Failed, so try again
|
||||||
|
log.debug(f"Succeeded after {attempts} attempts")
|
||||||
break # Success!
|
break # Success!
|
||||||
else:
|
else:
|
||||||
# Ran out of attempts before succeeding
|
raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
|
||||||
raise OperationFailed("No more attempts remaining") from exc
|
|
||||||
|
|
||||||
|
|
||||||
Simplifying if-elif chains
|
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
|
binding it to a value (as shown above), or else by including an appropriate
|
||||||
explicit type declaration::
|
explicit type declaration::
|
||||||
|
|
||||||
m : typing.re.Match
|
m: typing.re.Match
|
||||||
if m := pattern.match(data):
|
if m := pattern.match(data):
|
||||||
...
|
...
|
||||||
elif m := other_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
|
intermediate values in comprehensions and generator expressions by
|
||||||
exporting them to the containing block scope::
|
exporting them to the containing block scope::
|
||||||
|
|
||||||
factor = 1
|
factor: int
|
||||||
while any(n % (factor := p) == 0 for p in small_primes):
|
while any(n % (factor := p) == 0 for p in small_primes):
|
||||||
n //= factor
|
n //= factor
|
||||||
|
|
||||||
def cumulative_sums(data, start=0)
|
total = 0
|
||||||
total = start
|
partial_sums = [total += value for value in data]
|
||||||
yield from (total += value for value in data)
|
|
||||||
return total
|
|
||||||
|
|
||||||
|
|
||||||
Allowing lambda expressions to act more like re-usable code thunks
|
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):
|
def make_counter(start=0):
|
||||||
x = start
|
x = start
|
||||||
|
@ -597,7 +742,7 @@ To be abbreviated as::
|
||||||
|
|
||||||
def make_counter(start=0):
|
def make_counter(start=0):
|
||||||
x = start
|
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
|
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",
|
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.
|
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
|
Acknowledgements
|
||||||
================
|
================
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue