Here are some ways you can write a lookup in Python. Not all of these patterns are obviously a lookup, but all of them can be translated to one another, so it's worth to know these patterns. That way you can make a conscious decision to choose a specific form. Thanks to Ori for inspiring this post. Our talk about state machines was enlightening!
Classic.
# main.py
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
print(name_to_age['Alice']) # Raise KeyError if Alice not found
print(name_to_age.get('Alice', 0)) # Print 0 if Alice not found
Every dictionary corresponds to an if-else statement and vice-versa. Rewriting between the two can sometimes come in handy. The if-else is a lot more explicit, which makes it easier to debug, but it's also a lot more verbose. Additionally, a dictionary lookup cannot be anything but a lookup, whereas the if-else is more general and can do pretty much anything it wants, including deleting your system32 or /bin/.
# main.py
def name_to_age(name: str) -> int:
if name == 'Alice':
return 37
elif name == 'Bob':
return 21
elif name == 'Charlie':
return 59
else:
# return 0 if you want to reproduce name_to_age.get behaviour
raise NameError(f'{name=} not found!')
print(name_to_age('Alice'))
Sometimes data is in separate lists (or series, or whatever). This is a simple lookup pattern, just make sure you give the index a good name.
# main.py
names = ['Alice', 'Bob', 'Charlie']
ages = [37, 21, 59]
i_name = names.index('Alice')
print(ages[i_name])
If you find yourself doing this a lot with the same data, consider making a helper so you don't need to deal with indexes.
name_to_age = dict(zip(names, ages))
print(name_to_age('Alice'))
This scenario is also relevant when you have a list of objects. Like Person with attributes name and age.
# main.py
names_and_ages = [('Alice', 37), ('Bob', 21), ('Charlie', 59)]
alice_matches = [age for name, age in names_and_ages if name == 'Alice']
if len(alice_matches) != 1:
raise ValueError(f'Unexpected number of matches for Alice, got {alice_matches}')
print(alice_matches[0])
Here you can also quickly make a helper.
name_to_age = {name: age for name, age in names_and_ages} # verbose but explicit
# name_to_age = dict(names_and_ages) # short
print(name_to_age['Alice'])
Here, the age is encoded by the index of the name in the list. It's a little weird, but this is how you might encode a game of life grid, for instance.
# main.py
name_by_age = [None for _ in range(21)] + ['Bob'] + [None for _ in range(37-21-1)] + ['Alice'] + [None for _ in range(59-21-2)] + ['Charlie']
print(name_by_age.index('Alice'))
Flipping data will also work here. Maybe filter out the Nones.
name_to_age = {name: i for i, name in enumerate(name_by_age) if name is not None}
print(name_to_age['Alice'])
Imports always do a lookup from name to code. Not seen often, but not too far out there in my opinion.
# alice.py
age = 37
# bob.py
age = 21
# charlie.py
age = 59
# main.py
from alice import age
print(age)
Alternatively you might have something like this.
# ages.py
alice_age = 37
bob_age = 21
charlie_age = 59
# main.py
from ages import alice_age
print(alice_age)
This is often useful in Advent of Code if you have some kind of 2d grid. The most intuitive data structure is a list of lists or a Numpy array.
grid = [[1 if x % 2 == 0 and y % 7 == 0 else 0 for x in range(20)] for y in range(20)]
print(grid[7][8])
But this might not be the best solution. Doing things in this array means looking up a coordinate pair, so we can also use a dictionary.
grid = {(x, y): 1 if x % 2 == 0 and y % 7 == 0 else 0 for x in range(20) for y in range(20)}
print(grid[7, 8])
Sometimes you may write something like this.
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
if 'Dilbert' in name_to_age:
dilbert_age = name_to_age['Dilbert']
else:
dilbert_age = 88
Already mentioned up above, you can use get to provide a default argument instead.
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
print(name_to_age.get('Dilbert', 88))
defaultdict and invertingSay you need to know who is 37.
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
age_to_name = {v: k for k, v in in name_to_age.items()}
print(age_to_name[37])
This will only work if your values are unique. If Dilbert is also 37, you are not going to find Alice in age_to_name. The solution is to keep a list of keys that map to this value, like so.
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59, 'Dilbert': 37}
age_to_name = {v: [] for v in name_to_age.values()}
for name, age in name_to_age.items():
age_to_name[age].append(name)
print(age_to_name[37])
Use defaultdict for this, so you don't need to loop over your collection twice. A defaultdict puts a value provided by the function you give it into the dictionary if it hasn't seen the key before.
from collections import defaultdict
name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59, 'Dilbert': 37}
age_to_name = defaultdict(list)
for name, age in name_to_age.items():
age_to_name[age].append(name)
print(age_to_name[37])
home