Consider the following Python3 program:
a = [0, 0]
i = 0
a[i] = i = 1
print(a, i)
a = [0, 0]
i = 0
i = a[i] = 1
print(a, i)
I expected the output to be:
[0, 1] 1
[1, 0] 1
But instead I got:
[1, 0] 1
[0, 1] 1
My question is: is there anything in the Python language specification about associativity of the assignment operator, or is the behavior for the above example undefined?
All I was able to find is that expressions are evaluated form left to right, except that r-value is evaluated first in case of assignment, but that doesn't help.
Short answer: the code is well defined; the order is left-to-right.
Long answer:
First of all, let's get the terminology right. Unlike in some other languages, assignment in Python is a statement, not an operator. This means that you can't use assignment as part of another expression: for example i = (j = 0) is not valid Python code.
The assignment statement is defined to explicitly permit multiple assignment targets (in your example, these are i and a[i]). Each target can be a list, but let's leave that aside.
Where there are multiple assignment targets, the value is assigned from left to right. To quote the documentation:
An assignment statement evaluates the expression list (remember that
this can be a single expression or a comma-separated list, the latter
yielding a tuple) and assigns the single resulting object to each of
the target lists, from left to right.
Just to make it clear, since I struggled myself to understand this. The statement:
a = b = c = ... = E
is equivalent to
a = E
b = E
c = E
...
Related
PEP 572 introduces assignment expressions (colloquially known as the Walrus Operator), implemented for Python 3.8. This seems like a really substantial new feature since it will allow this form of assignment within comprehensions and lambda functions.
What exactly are the syntax, semantics, and grammar specification of assignment expressions?
Why is this new (and seemingly quite radical concept) being introduced, when a similar idea in PEP 379 on "Adding an assignment expression" was rejected before?
PEP 572 contains many of the details, especially for the first question. I'll try to summarise/quote concisely arguably some of the most important parts of the PEP:
Rationale
Allowing this form of assignment within comprehensions, such as list comprehensions, and lambda functions where traditional assignments are forbidden. This can also facilitate interactive debugging without the need for code refactoring.
Recommended use-case examples
a) Getting conditional values
for example (in Python 3):
command = input("> ")
while command != "quit":
print("You entered:", command)
command = input("> ")
can become:
while (command := input("> ")) != "quit":
print("You entered:", command)
Similarly, from the docs:
In this example, the assignment expression helps avoid calling len()
twice:
if (n := len(a)) > 10:
print(f"List is too long ({n} elements, expected <= 10)")
b) Simplifying list comprehensions
for example:
stuff = [(lambda y: [y,x/y])(f(x)) for x in range(5)]
can become:
stuff = [[y := f(x), x/y] for x in range(5)]
Syntax and semantics
In any context 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, and name is an identifier.
The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned that value
Differences from regular assignment statements
In addition to being an expression rather than statement, there are several differences mentioned in the PEP: expression assignments go right-to-left, have different priority around commas, and do not support:
Multiple targets
x = y = z = 0 # Equivalent: (z := (y := (x := 0)))
Assignments not to a single name:
# No equivalent
a[i] = x
self.rest = []
Iterable packing/unpacking
# Equivalent needs extra parentheses
loc = x, y # Use (loc := (x, y))
info = name, phone, *rest # Use (info := (name, phone, *rest))
# No equivalent
px, py, pz = position
name, phone, email, *other_info = contact
Inline type annotations:
# Closest equivalent is "p: Optional[int]" as a separate declaration
p: Optional[int] = None
Augmented assignment is not supported:
total += tax # Equivalent: (total := total + tax)
A couple of my favorite examples of where assignment expressions can make code more concise and easier to read:
if statement
Before:
match = pattern.match(line)
if match:
return match.group(1)
After:
if match := pattern.match(line):
return match.group(1)
Infinite while statement
Before:
while True:
data = f.read(1024)
if not data:
break
use(data)
After:
while data := f.read(1024):
use(data)
There are other good examples in the PEP.
A few more examples and rationales now that 3.8 has been officially released.
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 expression contexts.
Source: LicensedProfessional's reddit comment
Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
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]
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]
What is := operator?
In simple terms := is a expression + assignment operator.
it executes an expression and assigns the result of that expression in a single variable.
Why is := operator needed?
simple useful case will be to reduce function calls in comprehensions while maintaining the redability.
lets consider a list comprehension to add one and filter if result is grater than 0 without a := operator. Here we need to call the add_one function twice.
[add_one(num) for num in numbers if add_one(num) > 0]
Case 1:
def add_one(num):
return num + 1
numbers = [1,2,3,4,-2,45,6]
result1 = [value for num in numbers if (value := add_one(num)) > 0]
>>> result1
[2, 3, 4, 5, 46, 7]
The result is as expected and we don't need to call the add_one function to call twice which shows the advantage of := operator
be cautious with walarus := operator while using list comprehension
below cases might help you better understand the use of := operator
Case 2:
def add_one(num):
return num + 1
numbers = [1,2,3,4,-2,45,6]
>>> result2 = [(value := add_one(num)) for num in numbers if value > 0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <listcomp>
NameError: name 'value' is not defined
Case 3: when a global variable is set to positive
def add_one(num):
return num + 1
numbers = [1,2,3,4,-2,45,6]
value = 1
result3 = [(value := add_one(num)) for num in numbers if value > 0]
>>> result3
[2, 3, 4, 5, -1]
Case 4: when a global variable is set to negitive
def add_one(num):
return num + 1
numbers = [1,2,3,4,-2,45,6]
value = -1
result4 = [(value := add_one(num)) for num in numbers if value > 0]
>>> result4
[]
Here is a code snippet.
x = {}
x[1] = len(x)
print x
{1: 0}
Is this well defined? That is, could x == {1: 1} instead?
Because I remember that an equivalent program in C++ '98 (if we use std::map) has undefined behaviour. The output of the program was different when compiled with VS compiler and G++.
As I mentioned in a comment, this test case can be reduced to:
x = {}
x[1] = len(x)
The question then becomes, is x[1] == 0, or is x[1] == 1?
Let's look at the relevant 2.x documentation and 3.x documentation:
Python evaluates expressions from left to right. Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.
In the following lines, expressions will be evaluated in the arithmetic order of their suffixes:
expr3, expr4 = expr1, expr2
Therefore...
len(x) will be fully computed before we do x[1], so x[1] == 0 and this is well defined.
Yes, it's defined. len() is called before the assignment. However, dict's are not ordered in Python, which is why you sometimes see 0, 1 and 1, 0 in the output.
I know about tuple unpacking but what is this assignment called where you have multiple equals signs on a single line? a la a = b = True
It always trips me up a bit especially when the RHS is mutable, but I'm having real trouble finding the right keywords to search for in the docs.
It's a chain of assignments and the term used to describe it is...
- Could I get a drumroll please?
Chained Assignment.
I just gave it a quite google run and found that there isn't that much to read on the topic, probably since most people find it very straight-forward to use (and only the true geeks would like to know more about the topic).
In the previous expression the order of evaluation can be viewed as starting at the right-most = and then working towards the left, which would be equivalent of writing:
b = True
a = b
The above order is what most language describe an assignment-chain, but python does it differently. In python the expression is evaluated as this below equivalent, though it won't result in any other result than what is previously described.
temporary_expr_result = True
a = temporary_expr_result
b = temporary_expr_result
Further reading available here on stackoverflow:
How do chained assignments work? python
#refp's answer is further supported with this output using the dis (disassembly) module:
>>> def a(x):
... g = h = x
...
>>> import dis
>>> dis.dis(a)
2 0 LOAD_FAST 0 (x)
3 DUP_TOP
4 STORE_FAST 1 (g)
7 STORE_FAST 2 (h)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
The RHS is retrieved and duplicated, then stored into the destination variables left-to-right (try this yourself with e = f = g = h = x).
Some other posters have been confused if the RHS is a function call, like a = b = fn() - the RHS is only evaluated once, and then the result assigned to each successive variable. This may cause unwanted sharing if the returned value is a mutable, like a list or dict.
For those using threading, it is useful to note that there is no "atomicity" implied by the chained assignment form over multiple explicit assignment statements - a thread switch could occur between the assignments to g and h, and another thread looking at the two of them could see different values in the two variables.
From the documentation, 7.2. Assignment statements, g and h being two target lists, x being the expression list:
assignment_stmt ::= (target_list "=")+ (expression_list | yield_expression)
An assignment statement evaluates the expression list (remember that this can be a single expression or a comma-separated list, the latter yielding a tuple) and assigns the single resulting object to each of the target lists, from left to right.
OK, "chained assignment" was the search term I was after, but after a bit more digging I think it's not strictly correct. but it is easier to search for than "a special case of the assignment statement".
The Wikipedia article senderle linked to says:
In Python, assignment statements are not expressions and thus do not
return a value. Instead, chained assignments are a series of
statements with multiple targets for a single expression. The
assignments are executed left-to-right so that i = arr[i] = f()
evaluates the expression f(), then assigns the result to the leftmost
target, i, and then assigns the same result to the next target,
arr[i], using the new value of i.
Another blog post says:
In Python, assignment statements do not return a value. Chained
assignment (or more precisely, code that looks like chained assignment
statements) is recognized and supported as a special case of the
assignment statement.
This seems the most correct to me, on a closer reading of the docs - in particular (target_list "=")+ - which also say
An assignment statement evaluates the expression list ... and assigns
the single resulting object to each of the target lists, from left to
right.
So it's not really "evaluated from right-most to left" - the RHS is evaluated and then assigned from left-most target to right - not that I can think of any real-world (or even contrived) examples where it would make a difference.
got bitten by python's Chained Assignment today, due to my ignorance. in code
if l1.val <= l2.val:
tail = tail.next = l1 # this line
l1 = l1.next
what I expected was
tail.next = l1
tail = tail.next
# or equivalently
# tail = l1
whereas I got below, which produce a self loop in the list, leave me in a endless loop, whoops...
tail = l1
tail.next = l1 # now l1.next is changed to l1 itself
since for a = b = c,
one way (python, for example) equivalent to
tmp = evaluate(c)
evaluate(a) = tmp
evaluate(b) = tmp
and have equal right operand for two assignment.
the other (C++, for example) equivalent to
evaluate(b) = evaluate(c)
evaluate(a) = evaluate(b)
since in this case a = b = c is basically
b = c
a = b
and two right hand operand could be different.
That why similar code works well in C++.
I know the conditional expression in Python is X if C else Y, but I got some problems in using it.
I have two codes to be compared.
Code 1:
def fun(p):
if len(p) >= 2:
p[1] = 'Ok'
else:
p.append('Ok')
p = [1]
fun(p)
print p
Output of code 1:
[1, 'Ok']
Code 2:
def fun(p):
(p[1] = 'Ok') if (len(p) >= 2) else p.append('OK')
p = [1]
fun(p)
print p
Output of code 2:
(p[1] = 'Ok') if (len(p) >= 2) else p.append('OK')
^
SyntaxError: invalid syntax
I know in code 1, whose format is "if C : X else: Y", the evaluation order is:
C
X
Y
Code 2 throws a syntax error, the reason may be p[1] doesn't exist. So I guess the format "X if C else Y" is evaluated as follows:
X
C
Y
But that is only my guess. does anyone know the real reason why code 2 is wrong while code 1 is right?
The reason you have a SyntaxError is because Python differentiates between statements and expressions.
Assignments, like
p[1] = 'Ok'
are statements and can't be part of an expression, including the conditional expression. See What is the difference between an expression and a statement in Python? for more info.
Order of evaluation doesn't come into it -- SyntaxErrors happen before any code is evaluated, when its being parsed.
In both if statements and conditional expressions, The order of evaluations is ether
Condition
True Statement
or
Condition
False Statement
So, in
if condition:
true_statement
else:
false_statement
or
true_statement if condition else false_statement
only the true or the fase statement is evaluated, depending on the truthiness of the condition.
As Dan D. points out, you can't have statements in expression contexts.
In the interest of learning -- I do not recommend doing it this way -- I'll show you how you could use the inline X if C else Y to accomplish what you're attempting to do:
def fun(p):
p.__setitem__(1, 'Ok') if len(p) >= 2 else p.append('OK')
As you can see, your fist version is much more readable.
Assignment is no expression (others pointed that out). Hence the syntax error.
Regarding your question concerning evaluation order:
In if-else expressions(!) first the condition operand is evaluated, then either the then-operand (leftmost) or the else-operand (rightmost) is evaluated. The other operand is not evaluated.
In or-expressions the operands are evaluated from leftmost to rightmost (a or b or c or d or ...) but only until one is found whose Boolean value is True. The remaining expressions are not evaluated.
In and-expressions the operands are evaluated from leftmost to rightmost (a and b and c and d and ...) but only until one is found whose Boolean value is False. The remaining expressions are not evaluated.
Inner expressions are evaluated before outer expressions (of course): third(second(first()))
In all other cases (a() + b(), f(g(), h()), a[b[c]] = d() + e) the order is defined as specified. Typically it's left-to-right except for assignment (where it is right before left).
What does it mean when it's like this:
self.something += ('somethin',)
What does "+=" mean and what does the comma mean?
The expression a += b is shorthand for a = a + b, where a and b can be numbers, or strings, or tuples, or lists (but both must be of the same type).
The comma in ('x',) means that this is a tuple of a single element, 'x' . If the comma is absent, is just an 'x' between parenthesis.
+= is addition and assignment into one (sometimes referred to as iadd or in-place add). It is the same as a = a + x
a = 4
a += 5 # add 5 to a, and assign the result into a
b = 4
b = b + 5 # this does the same thing as +=
print a # prints out 9
print b # prints out 9
You can also do other operations in this style, such as -=, *=, /=, &= (bitwise and), |= (bitwise or), ^= (bitwise xor), %= (mod), **= (exponent).
('something',) is a tuple. ('something') (without the comma) is using the parenthesis in grouping, kind of like ('some' + 'thing') or (a + b). In order to differentiate between the one-member tuple and the grouping syntactically, Python uses a comma.
Python has an operator to assign value to a name and it's =.
The language also support many other operators like +, -, ** for operations defined in special methods of your objects.
Although + is the math sign to add things, it can be customized to do whatever you want.
Sometimes you want to make an operation and store it using the same name. For these situations you have in-place operators that are just the normal operators you're use to plus the = sign.
For immutable objects (numbers, strings, tuples,...) you can't have in-place changes because... they're immutable. So, the in-place methods do exactly the same thing the normal method followed by an assignment.
For mutable objects the difference is much clear:
In-place add:
>>> a = []
>>> b = a
>>> b += [1,2]
>>> a
[1, 2]
Add and assign:
>>> a = []
>>> b = a
>>> b = b + [1,2]
>>> a
[]
See? The object itself was transformed with the in-place add for lists, but, on the other case, a new object was created.
For your other question, the comma is the tuple separator.
a = (1) # Just number 1 inside parenthesis
a = (1,) # A tuple with one element
results=[]
for x in range(5):
results += '$'
print(results)
output : ['$', '$', '$', '$', '$']
this code behaves differently from typical += operator , as you can see here it has generated list with $ sign inside it.
its is not like we think results = results+ '$' this code will throw you an error.
What actually happens is += operator works like .extend() in lists.