Extended sequence unpacking in python3 - python

I create a list as:
>>> seq = [1, 2, 3, 4]
Now i assign the variables as follows:
>>> a, b, c, d, *e = seq
>>> print(a, b, c, d, e)
I get output as:
>>> 1 2 3 4 []
Now i change the sequence in which i assign variables as:
>>> a, b, *e, c, d = seq
>>> print(a, b, c, d, e)
I get output as:
>>> 1, 2, 3, 4, []
So my question is Why *e variable above is always assigned an empty list regardless of where it appears?

It was a design choice, according to PEP 3132 (with my bold):
A tuple (or list) on the left side of a simple assignment (unpacking is not defined for augmented assignment) may contain at most one expression prepended with a single asterisk (which is henceforth called a "starred" expression, while the other expressions in the list are called "mandatory"). This designates a subexpression that will be assigned a list of all items from the iterable being unpacked that are not assigned to any of the mandatory expressions, or an empty list if there are no such items.
Indeed, the first example in the PEP illustrates your point:
>>> a, *b, c = range(5)
>>> a
0
>>> c
4
>>> b
[1, 2, 3]

In the first case
a, b, c, d, *e = seq
since the sequence has only four elements, a, b, c and d get all of them and the rest of them will be stored in e. As nothing is left in the sequence, e is an empty list.
In the second case,
a, b, *e, c, d = seq
First two elements will be assigned to a and b respectively. But then we have two elements after the *e. So, the last two elements will be assigned to them. Now, there is nothing left in the seq. That is why it again gets an empty list.

a, b, *e, c, d = [1, 2, 3, 4]
*e says "what's left put into e". Because you have 4 elements, empty sequence is put into e.

Related

Unpacking a list of lists in a for loop

Let's say I create a list in the following way:
a = [[0, 1], [2, 3]]
If I execute the following for loop:
for b in a:
print(b)
I get
[0, 1]
[2, 3]
which makes sense to me because I am looking for elements of a which, in that case, are sub-lists. However, if I execute:
for b, c in a:
print(b, c)
I get
0 1
2 3
I would have expected to get
[0, 1] [2, 3]
Is there an obvious reason why Python is unpacking the list this way? I mean, why Python would suddenly give me elements of sub-elements of my list when I am requesting objects in the list?
You're unpacking to the integers here:
>>> for b, c in a:
... print(b, c)
... print(type(b), type(c))
...
0 1
<class 'int'> <class 'int'>
2 3
<class 'int'> <class 'int'>
Put another way, compare the following two:
>>> b, c = a; print(b, c)
[0, 1] [2, 3]
>>> b, c = a[0]; print(b,c)
0 1
When you specified:
for b, c in a:
print(b, c)
You could read that as: for each element in a, unpack into the tuple "b, c"
It's helpful to understand the Assignment Statement being used here. Relevant part here:
If the target list is a single target with no trailing comma,
optionally in parentheses, the object is assigned to that target.
Else: The object must be an iterable with the same number of items as
there are targets in the target list, and the items are assigned, from
left to right, to the corresponding targets.
The for loop essentially boils down to this:
for (object) in (iterable):
Now let's see what objects are in your iterable:
[0, 1] # object1: list
[2, 3] # object2: list
But when you use the Assignment Statement, the "Else" clause applies.
for (obj1, obj2 of object) in (iterable):
Which treats the object as an inner iterable, and unpack each to the inner objects, i.e. the ints within:
0 1 # obj1 of object1: int, obj2 of object1: int
2 3 # obj1 of object2: int, obj2 of object2: int
What you're experiencing is unpacking assignment, which assigns the elements of an iterable to multiple variables. To get your expected behavior, do the following:
b, c = a
print(b, c)
for b, c in a: iterates through a the same way as for b in a:, but since each element is a 2-element iterable, its own elements get unpacked to b and c. See what happens when you do this:
a = [[0, 1], [2, 3, 4]]
for b, c in a:
print(b, c)
You would get an error, because a[1] has more elements than there are variables to assign to.

ValueError: too many values to unpack - Is it possible to ignore one value?

The following line returns an error:
self.m, self.userCodeToUserNameList, self.itemsList, self.userToKeyHash, self.fileToKeyHash = readUserFileMatrixFromFile(x,True)
The function actually returns 6 values. But in this case, the last one is useless (its None). So i want to store only 5.
Is it possible to ignore the last value?
You can use *rest from Python 3.
>>> x, y, z, *rest = 1, 2, 3, 4, 5, 6, 7
>>> x
1
>>> y
2
>>> rest
[4, 5, 6, 7]
This way you can always be sure to not encounter unpacking issues.
It's common to use _ to denote unneeded variables.
a, b, c, d, e, _ = my_func_that_gives_6_values()
This is also often used when iterating a certain number of times.
[random.random() for _ in range(10)] # gives 10 random values
Python 3 also introduced the * for assignment, similar to how *args takes an arbitrary number of parameters. To ignore an arbitrary number of arguments, just assign them to *_:
a, b, c, d, e, *_ = my_func_that_gives_5_or_more_values()
This can be used at any point in your assignment; you can fetch the first and last values and ignore padding in the middle:
>>> a, b, c, *_, x, y, z = range(10)
>>> print(a, b, c, '...', x, y, z)
0 1 2 ... 7 8 9
Just, use the throw-away variable '_'
self.m,
self.userCodeToUserNameList,
self.itemsList,
self.userToKeyHash,
self.fileToKeyHash,
_ = readUserFileMatrixFromFile(x,True)
here '_' is deliberately ignored.
Just slice the last one out:
self.m, self.userCodeToUserNameList, \
self.itemsList, self.userToKeyHash, \
self.fileToKeyHash = readUserFileMatrixFromFile(x,True)[:-1]
EDIT after TigerhawkT3's comment :
Note that this works only if the return object supports slicing.

Idiomatic way to unpack variable length list of maximum size n

I'm reading a file and unpacking each line like this:
for line in filter(fh):
a, b, c, d = line.split()
However, it's possible that line may have more or fewer columns than the variables I wish to unpack. In the case when there are fewer, I'd like to assign None to the dangling variables, and in the case where there are more, I'd like to ignore them. What's the idiomatic way to do this? I'm using python 2.7.
Fix the length of the list, padding with None.
def fixLength(lst, length):
return (lst + [None] * length)[:length]
In python 3 you can use this
a, b, c, d, *_unused_ = line.split() + [None]*4
Edit
For large strings I suggest to use maxsplit-argument for split (this argument also works in py2.7):
a, b, c, d, *_unused_ = line.split(None, 4) + [None]*4
Why 5? Otherwise the 4th element would consist the whole residual of the line.
Edit2 It is 4… It stops after 4 splits, not 4 elements
First of all, think about why you want to do this.
However, given that you want to (1) pad with None and (2) ignore extra variables, the code is easy:
a,b,c,d = (line.split() + [None]*4)[:4]
Obviously, the magic number has to be the same as the number of variables. This will extend what you have with the magic number, then trim back down to that length.
For an arbitrary iterable you can do:
import itertools
def padslice(seq,n):
return itertools.islice(itertools.chain(seq,itertools.repeat(None)), n)
This is the same pad-and-slice with itertools.
In Python 3, you can use itertools.zip_longest, like this:
from itertools import zip_longest
max_params = 4
lst = [1, 2, 3, 4]
a, b, c, d = next(zip(*zip_longest(lst, range(max_params))))
print(f'{a}, {b}, {c}, {d}') # 1, 2, 3, 4
lst = [1, 2, 3]
a, b, c, d = next(zip(*zip_longest(lst, range(max_params))))
print(f'{a}, {b}, {c}, {d}') # 1, 2, 3, None
For Python 2.x you can follow this answer.
Something like this, works for any iterable/iterator. If you're always going to pass a list then you can remove the islice part.
from itertools import islice
def solve(seq, n):
lis = list(islice(seq, n))
return lis + [None]*(n - len(lis))
...
>>> a, b, c, d = solve(range(2), 4)
>>> a, b, c, d
(0, 1, None, None)
>>> a, b, c, d = solve('qwe', 4)
>>> a, b, c, d
('q', 'w', 'e', None)
>>> a, b, c, d = solve(iter([1, 2, 3, 4, 5]), 4)
>>> a, b, c, d
(1, 2, 3, 4)

Summing tuples in python?

pt1 and pt2 are two tuple made up of ints.
pairs = zip(pt1, pt2)
sum_sq_diffs = sum((a - b)**2 for a, b in pairs)
return (sum_sq_diffs)
My question concerns the second line. What are a and b? If you print them by doing:
print list((a,b) for a, b in pairs))
you get [(pt1x,pt2x), (pt1y, pt2y)]
If I take two tuples and subtract them, you get an error. So how come sum_sq_diffs = sum((a - b)**2 for a, b in pairs) doesn't result in an error? It seems a is a tuple and b is a tuple.
You understand that pairs is a list of tuples.
Now, the second line is a list comprehension which is the equivalent of
sum_sq_diffs = 0
for a, b in pairs:
sum_sq_diffs += (a - b)**2
Now, while iterating through the individual elements, python would do a "tuple unpacking" for you , and extracts the (x, y) to local variables a and b respectively.
You can read more on tuple unpacking here
This is called, appropriately enough, tuple unpacking. Tuple unpacking requires that the list of variables on the left has the same number of elements as the length of the tuple. Note that multiple assignment is really just a combination of tuple packing and tuple unpacking!
Here is a quick demo which should demonstrate this:
>>> pt1 = [1, 2, 3]
>>> pt2 = [4, 5, 6]
>>> pairs = zip(pt1, pt2)
>>> pairs
[(1, 4), (2, 5), (3, 6)]
>>> sum_sq_diffs = sum((a - b)**2 for a, b in pairs)
>>> sum_sq_diffs
27
>>> sum_sq_diffs_2 = 0
>>> for a, b in pairs:
... print a, b
... sum_sq_diffs_2 += (a - b)**2
...
1 4
2 5
3 6
>>> sum_sq_diffs_2
27
>>>
pairs should be a list of tuples.
a, b should unpack those tuples, one at a time. So they should be scalars.
There is much to like about list comprehensions and generator expressions. One thing I don't like about them, is you tend to end up rewriting them as loops when you have a question about how they are behaving.
But I do suggest rewriting your comprehension as a loop, and inserting a print(a,b) or print a, b. It may help.

Trimming lists using a loop

I have few lists like:
a = [1, 2, 3, 4, 5]
b = [4, 6, 5, 9, 2]
c = [4, 7, 9, 1, 2]
I want to trim all of them using a loop, instead of doing as below:
a[-2:]
b[-2:]
c[-2:]
I tried but got confused with pass by value or pass by reference fundamentals, looked into other questions as well but no help.
Thanks
for l in [a, b, c]:
del l[-2:]
This removes the last two elements from each list. If you want to remove all but the last two elements only, do this:
for l in [a, b, c]:
del l[:-2]
There's no need to worry about references here; the list over which the for loop iterates contains references to a, b and c, and each list is mutated in-place by deleting a list slice.
x = [a, b, c]
x = map(lambda lst: lst[-2:], x)
Deleting the unwanted items from the existing lists, using a loop:
for list in [a, b, c]:
del a[:-2]
Or, creating new lists containing only the correct items, using a list comprehension:
(a, b, c) = [x[-2:] for x in (a, b, c)]

Categories