0

I've just started learning python few weeks ago. I'm trying to convert this for loop to a list comprehension. The goal is to retrieve all values with USA as a key. Here's my code:

countries = {'Beijing':{'China':51, 'USA':36, 'Russia':22, 'Great Britain':19}, 'London':{'USA':46, 'China':38, 'Great Britain':29, 'Russia':22}, 'Rio':{'USA':35, 'Great Britain':22, 'China':20, 'Germany':13}}
gold_medals = []
for i in countries:
    for j in i:
        if countries[i]['USA'] not in gold_medals:
                gold_medals.append(countries[i]['USA'])

I tried converting it to list comprehension:

gold_medals2 = [countries[i]['USA'] for i in countries for j in i]

I was able to retrieve all the values associated to 'USA' but there are repetitions. I assume it's because I have no if statement to filter the result.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • 1
    @Chris: That one doesn't work here sadly; they're using their `if` for deduping, and an `if` in a listcomp won't be able to do that. – ShadowRanger Dec 10 '20 at 02:30
  • @ShadowRanger, good catch. I voted too quickly based on the title of the question and didn't look closely enough at the (at the time) unformatted code. Retracted and deleted. – ChrisGPT was on strike Dec 10 '20 at 02:31
  • @ShadowRanger Well darn... I wrote and was just about to post an answer doing it with a list comp accessing itself while being built :-(. Not nice to close the question after yourself. – superb rain Dec 10 '20 at 02:56
  • @superbrain: Well, I found an even better duplicate that I've now linked as the primary; you can always post it there. I'm horrified/intrigued at what evil you're contemplating to access a listcomp in the process of being built though. :-) – ShadowRanger Dec 10 '20 at 03:03
  • @ShadowRanger Found a question that's directly about referencing the list, [answered there](https://stackoverflow.com/a/65228120/13008439). – superb rain Dec 10 '20 at 03:32

1 Answers1

1

You can't do what you're trying to do with an if in a listcomp; listcomps don't bind to the name they're assigned to until they're finished, so there is no gold_medals2 to perform membership testing against.

You can use other tricks for deduping (while preserving order) though, e.g.:

gold_medals2 = list(dict.fromkeys(countries[i]['USA'] for i in countries for j in i))

On modern Python (3.6+) with insertion-ordered dicts, for hashable values, dict.fromkeys quickly produces the unique items from the input in the order each was first observed. list strips off the (garbage) values (all None) and hash table structure so you end up with the unique items with no kruft. If you're on an older Python, you can use collections.OrderedDict instead of dict.

Bonus, it's much faster than what you were doing. Your original solution is a O(n²) solution; using dict.fromkeys keeps it at O(n) (so if you multiply the input size by 10x, you'll only do ~10x more work, not 100x more work).

user2357112
  • 260,549
  • 28
  • 431
  • 505
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • I somehow managed to do it with list comprehension but probably much worse in terms of time and hard to read: ```goldmedals = [goldmedals:=[] if i is None else [goldmedals.append(countries[j]['USA']) for j in i if countries[j]['USA'] not in goldmedals] for i in (None, countries)][0]```. (Not really an expert on assignment expression). – Henry Tjhia Dec 10 '20 at 04:15
  • @HenryTjhia: Why? WHY WOULD YOU DO THIS?!?!? I'll note, the `[0]` indexing at the end and assignment of `goldmedals = ` isn't necessary, since the walrus did the assignment to `goldmedals` already (for reasons I somewhat disagree with, the walrus assignment applies outside the scope of the comprehension). I think `[goldmedals:=[] if i is None else [goldmedals.append(countries[j]['USA']) for j in i if countries[j]['USA'] not in goldmedals] for i in (None, countries)]` would have the same effect. Obviously a ludicrous solution though. :-) – ShadowRanger Dec 10 '20 at 04:19
  • Without the indexing at the end, the expression will give output such as ```[[36, 46, 35], [None, None, None]]```, while ```goldmedals``` will print out as ```[None, None, None]```. Tested on Python 3.9. – Henry Tjhia Dec 10 '20 at 04:23
  • @HenryTjhia: Ah, I missed the precedence issue. You'd need to parenthesize the walrus to avoid reassigning it on the second outer loop: `[(goldmedals:=[]) if i is None else [goldmedals.append(countries[j]['USA']) for j in i if countries[j]['USA'] not in goldmedals] for i in (None, countries)]`. The result of the expression is ignored (`goldmedals` will have the correct value in it already). I'd feel bad about it, but this was already violating "no side-effects in functional constructs" in half a dozen ways, so whatever. – ShadowRanger Dec 10 '20 at 04:31