There are two issues with your code. The first issue is what you're explicitly asking about: The list comprehension version is going to assign a whole bunch of None
values to self.elements
. The None
values are simply the return values from your calls to list.remove
. It modifies the list in place, and doesn't have anything useful to return (so it returns None
).
The comprehension [element for element in elements if elements.count(element) == 1 or elements.remove(element)]
will work the same as your other code (since None
is falsey and or
short-circuits), but it still runs into the second issue. (It's also a bit of an ugly hack: The new list created by the comprehension will have the same contents as elements
since remove
modifies elements
in place, and it's quite confusing. Writing hard to understand code is generally not a good idea.)
The second issue is that modifying a list while you are iterating over it can cause issues. List iterators work by index. The first item yielded by the iterator is from index 0, the second is from index 1, and so on. If you modify the list by removing an item early in the list, you'll shift the indexes of all the later items.
So, say you remove the first item (from index 0) just after your iterator has shown it to you. The list will shift all the later values up, but the iterator won't know about this. It will still yield the item at index 1 next, even though that used to be the item at index 2 (before the list was modified). The item originally at index 1 (and at index 0 after the previous item was removed) will be skipped by the iteration.
Here's a simple example of this issue, where values 2, 5, and 8 will be not be printed:
L = list(range(10)) # [0,1,2,3,4,5,6,7,8,9]
for x in L:
print(x)
if x % 3 == 1: # true for 1,4, and 7
L.remove(x)
In the example, the logic for removing values is pretty simple and we never skip a value we would normally want to remove (so L
has the expected value of [0,2,3,5,6,8,9]
at the end), other code may not work as nicely.
A way to avoid this issue is to iterate over a copy of the list, while modifying the original. In this situation we'll also need to count
in the original, rather than the copy:
for element in elements[:]: # copy list with a slice here!
if elements.count(element) > 1:
elements.remove(element) # modify the original list
This is rather inefficient though, since removing an item from a list (at a position other than the end) needs to take the time to shift all the later values up one position. Counting is also slow, as you need to iterate the whole list for each item. It's much more efficient to keep track of the unique items you've seen so far, and skip duplicated items when you see them later:
seen = set()
results = []
for element in elements:
if element not in seen:
seen.add(element)
results.append(element)
You can even build a somewhat awkward list comprehension (with side effects) of this code:
seen = set()
results = [element for element in elements
if not (element in seen or seen.add(element))]
A better approach is usually to bundle the deduplicating logic into a generator function (like the unique_everseen
recipe in the itertools
documentation), and then call it with list(dedupe(elements))
.