Chained Comparison with Loop Explanation in Python - python

Beginner Here! I came across some python code about the zip() function being combined with the sum() function, but the code does not make sense to me and I was wondering if I could get an explanation:
list_1 = ['a', 'a', 'a', 'b']
list_2 = ['a', 'b', 'b', 'b', 'c']
print(sum(a != b for a, b in zip(list_1, list_2)))
a and b are not defined, but are being compared? Is it also looping through "a" with b for a? What is a and b in this case? How are they being added together with sum()? What is being looped through? If I can have some help understanding this, it would be greatly appreciated.
Thanks in advance!

When confronted with code like this, it's helpful to break it into bite-sized pieces and see what each does. Here's an annotated version:
list_1 = ['a', 'a', 'a', 'b']
list_2 = ['a', 'b', 'b', 'b', 'c']
print(list(zip(list_1, list_2))) # you need to pass this to list() because zip is a lazy iterator
# correponding pairs from each list
# zip() trucates to the shortest list, so `c` is ignored
# [('a', 'a'), ('a', 'b'), ('a', 'b'), ('b', 'b')]
print([(a, b) for a, b in zip(list_1, list_2)])
# same thing as above using a list comprehension
# loops over each pair in the zip and makes a tuple of (a,b)
print([a != b for a, b in zip(list_1, list_2)])
# [False, True, True, False]
# compare each item in those pairs. Are they different?
print(sum(a != b for a, b in zip(list_1, list_2)))
# 2
# take advantage of the fact that True = 1 and False = 0
# and sum those up -> 0 + 1 + 1 + 0
It's also helpful for lookup things like zip(), and list comprehensions, although for many it makes more sense when you see them in action.

The for construct in the code is a generator. This form of generator is typically seen in a list comprehension, but it can also be passed directly to a function that wants an iterable, such as sum.
If you want to see what the generator actually produces, you can do:
x = [a != b for a, b in zip(list_1, list_2)]
This is equivalent to:
x = list(a != b for a, b in zip(list_1, list_2))
The values in the list x are bool values that are True where values in the two lists compare unequal and False where they compare equal. If one list is longer than the other, the values past the shorter list are skipped.
Back to your code, instead of creating a list, it's left as a generator and passed directly to sum, which will operate on any iterable. Since bool values are just integers (with False = 0 and True = 1), this just sums the number of differing values between the two lists (ignoring the extra values in the longer list).

Related

Why is else required in if statements when defining a list of items?

When defining a list of items (i.e. not actual list comprehensions), why do you need an else statement to accompany any if statements? For example, using Python 3.7 say that we have some condition which could be true or false, and we want to include an element in a list if the condition is true, and not include it if it's false.
condition = False
lyst = ['a', 'b', 'c' if condition]
I would expect it to assign ['a', 'b'] to lyst, but it throws a syntax error. It works if we include an else statement:
lyst = ['a', 'b', 'c' if condition else 'd']
But now the result is that lyst = ['a', 'b', 'd']
In this question, user Plankton's answer further specifies that you can't use a "pass" in the else statement. My question is why the else statement is required? Is it so that the list that gets created is guaranteed to have the number of items expected? If so, this strikes me as not very pythonic.
This is particularly weird because you can have if statements without else statements in actual list comprehensions e.g.:
post = [x for x in range(10) if x > 5]
# equivalent to: post = [6, 7, 8, 9]
I realize a list comprehension is totally different functionality. Please remember that my question is why you can't use an if statement without an else in a list literal? Does this limitation avoid some issue I'm not aware of?
You are confusing the conditional expression a if b else c with the syntax for a list comprehension, which requires a for ... in ... clause that can have an optional if (but no else).
The expression
['a', 'b', 'c' if condition]
is a syntax error, because it's not a list comprehension (no for), and c if condition is not a valid expression (no else).
The expression
['a', 'b', 'c' if condition else 'd']
is a valid list literal, consisting of the values 'a', 'b', and 'c' if condition else 'd' (which evaluates to either 'c' or 'd' depending on the value of condition).
The expression
[x for x in range(10) if x > 5]
is a list comprehension, with if x > 5 being a filter applied to the value of x each time you take a value from range(10).
This has nothing to do with list-comprehensions. It's purely about lists, and how to create one using list-literals.
And a list literal is comprised of an opening bracket, and a list of expressions separated by commas.
So in your case, your 'a' and 'b' are expressions (string literals). However 'c' if condition' is not an expression. It's a half-backed if-STATEMENT (much different beast)!
But as ternary operators (x if c else y) are expressions, you can use them.
['a', 'b', 'c' if condition else 'd'] is interpreted as ['a', 'b', ('c' if condition else 'd')]. It's not a list comprehension, you're just creating a literal list.
As posted in the comments, your snippet is not list comprehension. You are creating a list with one of the elements as an expression.
condition = False
list = ['a', 'b', 'c' if condition else None]
With you example of list = ['a', 'b', 'c' if condition] (which will fail) you might as well do any of the following:
Add c conditionally
condition = False
list = ['a', 'b']
if condition:
list.append('c')
Use list comprehension to remove None values
condition = False
list = ['a', 'b', 'c' if condition else None]
final_list = [i for i in list if i is not None]
There are two seperate language features here: ternaries (conditional expressions) and filtering.
Ternary:
x = 1 if 5 == 5 else 2
#1
Filtering:
l = [1,2,3,4,5]
l = [i for i in l if i < 3]
#[1,2,3]
These are the only two syntactical features you are using, just that you are confusing yourself by using a ternary inside a list definition:
l = [1,2,3 if 5 == 5 else 4]
see how this is equivalent to:
x = 3 if 5 == 5 else 4
l = [1,2,x]
It's not a list comprehension. It's a list with 3 values. The problem is, your 3rd value is 'c' if condition. That's not a valid value in python. It has nothing to do with lists. You could not write x = 'c' if condition either. On the other hand, the ternary if-else which you are using on the second example is a valid value, and that's why it gets accepted.
I believe if you want your list to be ['a', 'b', 'c'] or `['a', 'b'] then you put the if statement outside as follows:
mylist = ['a', 'b', 'c' if condition else None]
This isn't a list comprehension (above): in list comprehensions there is some loop and a single if that acts as a filter. Your code is a conditional (ternary) expression which must be evaluated to produce a result.

How to force Python to correctly identify list element index in FOR loop

There is a simple list, for example,
my_list = ['a', 'b', 'b', 'c', 'c']
I want to run through my_list[1:] with for loop to get previous element value for each iteration:
for i in my_list[1:]:
print(my_list[my_list.index(i)-1])
I expected to see a b b c on output, but get a a b b instead.
I guess this is because index() method search only for first i value, ignoring the fact that there are two elements "b" as well as two "c"...
How to fix this code to get expected output?
The list.index() method will return the index of first occurrence of its argument. And since you have multiple duplicate items in your list it doesn't give you the expected result. You can use a simple slicing to get your expected output:
>>> my_list = ['a', 'b', 'b', 'c', 'c']
>>>
>>> my_list[:-1]
['a', 'b', 'b', 'c']
Or if you want to access these items through a loop you can use zip() function like following:
>>> for i, j in zip(my_list,my_list[1:]):
... print(i, j)
...
a b
b b
b c
c c
Matching elements with their predecessors or sucessors is a common use case for zip:
In [13]: for i,prior in zip(my_list[1:], my_list[0:]):
print (prior)
....:
a
b
b
c
You can always emulate the behaviour of C/Pascal/Perl/whatever 'for' instead of Python 'for' (which is actually more like foreach). Note that the range starts with 1 to avoid returning the last element on the first iteration.
for i in range(1, len(my_list)):
print(my_list[i], my_list[i-1])
Not very Pythonic, but this approach is sometimes more intuitive for people with background in other languages.
As you noticed, using index does not work here, as it always finds the first position of the given element. Also, it is pretty wasteful, as in the worst case you have to search the entire list each time.
You could use enumerate with start parameter to get the element along with its index:
start = 1
for i, x in enumerate(my_list[start:], start):
print(i, x, my_list[i-1]) # index, current, last
This will do the trick:
for i in range(len(my_list)+1):
try: print(my_list[i-1])
except: print 'it is 1st iteration'

How to remove None when iterating through a list in python

I have this two unequal lists and i'm using itertools to loop through them and i'm trying to use the filter function to remove the None generated in List1 so that at the end of the day a contains only two elements instead of three (counting the none) but i keep getting this error: Type error: NoneType object is not iterable
import itertools
List1 = [['a'],['b']]
List2 = ['A','b','C']
l = list(itertools.chain(*List1))
print(l)
for a, b in itertools.zip_longest((b for a in List1 for b in a),List2):
filter(None, a)
print(a,b)
Not entirely clear what you want. As I understand the question and the comments, you want to use izip_longest to combine the lists, but without any None elements in the result.
This will filter the None from the zipped 'slices' of the lists and print only the non-None values. But note that this way you can not be sure whether, e.g., the first element in the non_none list came from the first list or the second or third.
a = ["1", "2"]
b = ["a", "b", "c", "d"]
c = ["x", "y", "z"]
for zipped in izip_longest(a, b, c):
non_none = filter(None, zipped)
print non_none
Output:
('1', 'a', 'x')
('2', 'b', 'y')
('c', 'z')
('d',)
BTW, what your filter(None, a) does: It filters the None values from your a, i.e. from the strings "a" and "b" (which does not do much, as they contain no None values), until it fails for the last value, as None is not iterable. Also, it discards the result anyway, as you do not bind it to a variable. filter does not alter the original list, but returns a filtered copy!
Why not just use zip?
for a, b in zip((b for a in List1 for b in a),List2):
print(a,b)
However, if you really insist on using zip_longest, you don't need to use filter to remove None values. You just need an if.
for a, b in itertools.zip_longest((b for a in List1 for b in a),List2):
if a is None: continue
print(a,b)
import itertools as it
def grouper(inputs, n, fillvalue=None):
iters = [iter(inputs)] * n
interim = it.zip_longest(*iters, fillvalue=fillvalue)
return interim
nums = range(23)
results = (list(grouper(nums, 4)))
finalanswer = []
for zipped in results:
# non_none = tuple(filter(None, zipped))
# unfortunately the line above filters 0 which is incorrect so instead use:
non_none = tuple(filter(lambda x: x is not None, zipped))
finalanswer.append(non_none)
print(finalanswer)
The code above uses zip_longest to illustrate generating zipped iterations of 'n' lists, irrespective of whether a corresponding entry exists in a given list or not --- and then strips out 'None' values --- noting that FILTER considers None and 0 as equivalent, so you have to use the 'lambda' version.

Sort list in place using another list of index

given
a = [1,4,5,3,2,6,0]
b = ['b','e','f','d','c','g','a']
order b in place, the expected order of b is available in the corresponding positional element of a.
output will be
['a','b','c','d','e','f','g']
try for other similar input sets.
a = [4,0,1,3,2]
b = ['E','A','B','D','C']
I can get it done using a third list, even sorted() creates a third list, but the key is to sort b in place
print sorted(b,key=lambda bi : a[b.index(bi)])
core of the problem is how to prevent iterating over items in b that were already iterated.
Try this:
zip(*sorted(zip(a, b)))[1]
Should give:
('a', 'b', 'c', 'd', 'e', 'f', 'g')
Since during sorting the b itself appears to be empty (see my question about that), you can use that piece of code to do it in-place:
b.sort(key=lambda x, b=b[:]: a[b.index(x)])
This uses a copy of the b to search in during sorting. This is certainly not very good for performance, so don't blame me ;-)
The key is to realise that the items in b aren't much use to the key function. You are interested in their counterparts in a. To do this inplace, means you can't just use zip to pair the items up. Here I use the default argument trick to get an iterator over a into the lambda function.
>>> a = [1,4,5,3,2,6,0]
>>> b = ['b','e','f','d','c','g','a']
>>> b.sort(key=lambda x, it=iter(a): next(it))
>>> b
['a', 'b', 'c', 'd', 'e', 'f', 'g']
def sorter(a,b):
for i in range(len(a)):
while i != a[i]:
ai = a[i]
b[i], b[ai], a[i], a[ai] = b[ai], b[i], a[ai], a[i]
return b
Simple bubble sort:
for i in range( len(a) ):
for j in range(len(a)-1-i):
if (a[j] > a[j+1]):
#swap a[j] & a[j+1]
#swap b[j] & b[j+1]

nested for loops in python with lists

Folks - I have two lists
list1=['a','b']
list2=['y','z']
I would like to send the variables to a function like below:
associate_address(list1[0],list2[0])
associate_address(list1[1],list2[1])
my script:
for l in list1:
for i in list2:
conn.associate_address(i,l)
I receive the below output:
conn.associate_address(a,y)
conn.associate_address(a,z)
I would like it to look like this:
conn.associate_address(a,y)
conn.associate_address(b,z)
Use the zip function, like this:
list1=['a','b']
list2=['y','z']
for i, j in zip(list1, list2):
print(i, j)
Output:
('a', 'y')
('b', 'z')
Why do you suppose this is?
>>> for x in [1,2]:
... for y in ['a','b']:
... print x,y
...
1 a
1 b
2 a
2 b
Nested loops will be performed for each iteration in their parent loop. Think about truth tables:
p q
0 0
0 1
1 0
1 1
Or combinations:
Choose an element from a set of two elements.
2 C 1 = 2
Choose one element from each set, where each set contains two elements.
(2 C 1) * (2 C 1) = 4
Let's say you have a list of 10 elements. Iterating over it with a for loop will take 10 iterations. If you have another list of 5 elements, iterating over it with a for loop will take 5 iterations. Now, if you nest these two loops, you will have to perform 50 iterations to cover every possible combination of the elements of each list.
You have many options to solve this.
# use tuples to describe your pairs
lst = [('a','y'), ('b','z')]
for pair in lst:
conn.associate_address(pair[0], pair[1])
# use a dictionary to create a key-value relationship
dct = {'a':'y', 'b':'z'}
for key in dct:
conn.associate_address(key, dct[key])
# use zip to combine pairwise elements in your lists
lst1, lst2 = ['a', 'b'], ['y', 'z']
for p, q in zip(lst1, lst2):
conn.associate_address(p, q)
# use an index instead, and sub-index your lists
lst1, lst2 = ['a', 'b'], ['y', 'z']
for i in range(len(lst1)):
conn.associate_address(lst1[i], lst2[i])
I would recommend using a dict instead of 2 lists since you clearly want them associated.
Dicts are explained here
Once you have your dicts set up you will be able to say
>>>mylist['a']
y
>>>mylist['b']
z

Categories

Resources