Algorithms¶
As discussed in PEP 572, assignment expression is a way to assign to variables within
an expression using the notation NAME := expr
. It is roughly equivalent to first assigning
expr
to the variable NAME
, then referencing such variable NAME
at the current scope.
Basic Concepts¶
To convert, walrus
will need to evaluate the expression, assign to the variable,
make sure such variable is available from the current scope and replace the original
assignment expression with code using pre-3.8 syntax.
For example, with the samples from PEP 572:
# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
it should be converted to
# Handle a matched regex
match = pattern.search(data)
if (match) is not None:
# Do something with match
However, such implementation is NOT generic for assignment expressions in more complex grammars and contexts, such as comprehensions per Appendix B:
a = [TARGET := EXPR for VAR in ITERABLE]
For a more generic implementation, walrus
wraps the assignment operation as a
function, and utilises global
and nonlocal
keywords to inject such assigned variable back into the original scope.
For instance, it should convert the first example to
# make sure to define ``match`` in this scope
if False:
match = NotImplemented
def wraps(expr):
"""Wrapper function."""
global match # assume we're at module level
match = expr
return match
# Handle a matched regex
if (wraps(pattern.search(data))) is not None:
# Do something with match
The original assignment expression is replaced with a wrapper function call, which
takes the original expression part as a parameter. And in the wrapper function, it
assigns the value of the expression to the original variable, and then injects such variable
into outer scope (namespace) with global
and/or
nonlocal
keyword depending on current context, and finally
returns the assigned variable so that the wrapper function call works exactly as if
we were using an assignment expression.
See also
variable declaration –
walrus.NAME_TEMPLATE
wrapper function call –
walrus.CALL_TEMPLATE
wrapper function definition –
walrus.FUNC_TEMPLATE
Scope Keyword Selection¶
Python provides global
and nonlocal
keywords for interacting with variables not in the current namespace. Following the Python
grammar definitions, walrus
selects the scope keyword in the mechanism described below:
If current context is at module level, i.e. neither inside a function nor a class definition, then
global
should be used.If current context is at function level and the variable is not declared in any
global
statements, thennonlocal
should be used; otherwiseglobal
should be used.If current context is at class level and not in its method definition, i.e. in the class body, it shall be treated as a special case.
For assignment expression in lambda functions, it shall be treated as another special case.
Formatted String Literals¶
Since Python 3.6, formatted string literals (f-string) were introduced in
PEP 498. And since Python 3.8, f-string debugging syntax were added to the grammar. However,
when walrus
performs the conversion on assignment expressions inside f-string,
it may break the lexical grammar and/or the original context.
Therefore, we utilise f2format
to first expand such f-string into str.format()
calls,
then rely on walrus
to perform the conversion and processing. Basically, there are
two cases as below:
When an assignment expression is in a debug f-string. (To prevent the converted code from changing the original expression for self-documenting and debugging.)
When an assignment expression is in an f-string inside a class scope. (To prevent the converted code from breaking the quotes of the original string.)
Lambda Functions¶
lambda functions can always be transformed into a regular function. This is the foundation of converting assignment expressions in lambda functions.
For a sample lambda function as follows:
>>> foo = lambda: [x := i ** 2 for i in range(10)]
>>> foo()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
walrus
will transform the original lambda function into a regular function first:
def foo():
return [x := i ** 2 for i in range(10)]
And now, walrus
can simply apply the generic conversion strategies to replace the
assignment expression with a wrapper function:
def foo():
if False:
x = NotImplemented
def wraps(expr):
"""Wrapper function."""
nonlocal x # assume that ``x`` was not declared as ``global``
x = expr
return x
return [wraps(i ** 2) for i in range(10)]
Class Definitions¶
As the class context is slightly different from regular module and/or function contexts, the generic conversion strategies are NOT applicable to such scenarios.
Note
For method context in the class body, the generic conversion strategies are still applicable. In this section, we are generally discussing conversion related to class variables.
Given a class definition as following:
class A:
bar = (foo := x ** 2)
walrus
will rewrite all class variables in the
current context:
class A:
bar = ((__import__('builtins').locals().__setitem__('foo', x ** 2), foo)[1])
The major reason of doing so is that locals()
dictionary can (and may only)
be edited directly in the class declaration. Therefore, we can use this
one-liner to rewrite the original assignment expression.
However, if a variable is declared in global
and/or
nonlocal
statements, it is NOT supposed to be assigned
to the class context, rather it should go to the outer scope (namespace),
which will then be applicable to the regular conversion templates as discussed above.
See also
In some corner cases, the first argument (variable name) to the __setitem__
call
must be mangled and normalised instead of used directly. See
bpc_utils.BaseContext.mangle()
for more information.