Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding *x ,= lst

I'm going through some old code trying to understand what it does, and I came across this odd statement:

*x ,= p

p is a list in this context. I've been trying to figure out what this statement does. As far as I can tell, it just sets x to the value of p. For example:

p = [1,2]
*x ,= p    
print(x)

Just gives

[1, 2]

So is this any different than x = p? Any idea what this syntax is doing?

like image 938
Kewl Avatar asked Apr 03 '17 17:04

Kewl


3 Answers

*x ,= p is basically an obfuscated version of x = list(p) using extended iterable unpacking. The comma after x is required to make the assignment target a tuple (it could also be a list though).

*x, = p is different from x = p because the former creates a copy of p (i.e. a new list) while the latter creates a reference to the original list. To illustrate:

>>> p = [1, 2] >>> *x, = p  >>> x == p True >>> x is p False >>> x = p >>> x == p True >>> x is p True 
like image 71
Eugene Yarmash Avatar answered Sep 22 '22 08:09

Eugene Yarmash


It's a feature that was introduced in Python 3.0 (PEP 3132). In Python 2, you could do something like this:

>>> p = [1, 2, 3]
>>> q, r, s = p
>>> q
1
>>> r
2
>>> s
3

Python 3 extended this so that one variable could hold multiple values:

>>> p = [1, 2, 3]
>>> q, *r = p
>>> q
1
>>> r
[2, 3]

This, therefore, is what is being used here. Instead of two variables to hold three values, however, it is just one variable that takes each value in the list. This is different from x = p because x = p just means that x is another name for p. In this case, however, it is a new list that just happens to have the same values in it. (You may be interested in "Least Astonishment" and the Mutable Default Argument)

Two other common ways of producing this effect are:

>>> x = list(p)

and

>>> x = p[:]

Since Python 3.3, the list object actually has a method intended for copying:

x = p.copy()

The slice is actually a very similar concept. As nneonneo pointed out, however, that works only with objects such as lists and tuples that support slices. The method you mention, however, works with any iterable: dictionaries, sets, generators, etc.

like image 34
zondo Avatar answered Sep 23 '22 08:09

zondo


You should always throw these to dis and see what it throws back at you; you'll see how *x, = p is actually different from x = p:

dis('*x, = p')
  1           0 LOAD_NAME                0 (p)
              2 UNPACK_EX                0
              4 STORE_NAME               1 (x)

While, the simple assignment statement:

dis('x = p')
  1           0 LOAD_NAME                0 (p)
              2 STORE_NAME               1 (x)

(Stripping off unrelated None returns)

As you can see UNPACK_EX is the different op-code between these; it's documented as:

Implements assignment with a starred target: Unpacks an iterable in TOS (top of stack) into individual values, where the total number of values can be smaller than the number of items in the iterable: one of the new values will be a list of all leftover items.

Which is why, as Eugene noted, you get a new object that's referred to by the name x and not a reference to an already existing object (as is the case with x = p).


*x, does seem very odd (the extra comma there and all) but it is required here. The left hand side must either be a tuple or a list and, due to the quirkiness of creating a single element tuple in Python, you need to use a trailing ,:

i = 1, # one element tuple

If you like confusing people, you can always use the list version of this:

[*x] = p

which does exactly the same thing but doesn't have that extra comma hanging around there.

like image 45
Dimitris Fasarakis Hilliard Avatar answered Sep 25 '22 08:09

Dimitris Fasarakis Hilliard