kdkarthik
kdkarthik

Reputation: 113

List comprehension where the condition depends on the list being generated

I am new to Python programming. I want to rewrite the following code as a list comprehension:

lx = [1, 2, 3, 4, 5, 1, 2]
ly = [2, 5, 4]

lz = []
for x in lx:
    if x in ly and x not in lz:
        lz.append(x)

This will create a new list with common elements of lx and ly; but the condition x not in lz depends on the list that is being built. How can this code be rewritten as a list comprehension?

Upvotes: 2

Views: 337

Answers (3)

pk786
pk786

Reputation: 2400

If you don't want to use set, this can be another approach using list comprehension.

lx = [1, 2, 3, 4, 5, 1, 2]
ly = [2, 5, 4]

lz=[]
[lz.append(x) for x in lx if (x in ly and x not in lz)]
print(lz)

Upvotes: 0

kaya3
kaya3

Reputation: 51037

The correct answer here is to use sets, because (1) sets naturally have distinct elements, and (2) sets are more efficient than lists for membership tests. So the simple solution is list(set(lx) & set(ly)).

However, sets do not preserve the order that elements are inserted in, so in case the order is important, here's a solution which preserves the order from lx. (If you want the order from ly, simply swap the roles of the two lists.)

def ordered_intersection(lx, ly):
    ly_set = set(ly)
    return [ly_set.remove(x) or x for x in lx if x in ly_set]

Example:

>>> ordered_intersection(lx, ly)
[2, 4, 5]
>>> ordered_intersection(ly, lx)
[2, 5, 4]

It works because ly_set.remove(x) always returns None, which is falsy, so ly_set.remove(x) or x always has the value of x.

The reason you cannot do this with a simpler list comprehension like lz = [... if x in lz] is because the whole list comprehension will be evaluated before the resulting list is assigned to the variable lz; so the x in lz test will give a NameError because there is no such variable yet.


That said, it is possible to rewrite your code to directly use a generator expression (which is somewhat like a list comprehension) instead of a for loop; but it is bad code and you shouldn't do this:

def ordered_intersection_bad_dont_do_this(lx, ly):
    lz = []
    lz.extend(x for x in lx if x in ly and x not in lz)
    return lz

This is not just bad because of repeatedly testing membership of lists; it is worse, because it depends on an unspecified behaviour of the extend method. In particular, it adds each element one by one rather than exhausting the iterator first and then adding them all at once. The docs don't say that this is guaranteed to happen, so this bad solution won't necessarily work in other versions of Python.

Upvotes: 0

Gavin H
Gavin H

Reputation: 10482

You cannot do it that way in a list comprehension as you cannot compare against the list lz that does not yet exist - assuming you are trying to avoid duplicates in the resulting list as in your example.

Instead, you can use the python set which will enforce only a single instance of each value:

lz = set(x for x in lx if x in ly)

And if what you are really after is a set intersection (elements in common):

lz = set(lx) & set(ly)

UPDATE: As pointed out by @Błotosmętek in the comments - using the set will not retain the order of the elements as the set is, by definition, unordered. If the order of the elements is significant a different strategy will be necessary.

Upvotes: 3

Related Questions