Python Basics
Collections - Dictionaries
A mapping object maps hashable values to arbitrary objects. Mappings are mutable objects. There is currently only one standard mapping type, the dictionary.
A dictionary’s keys are almost arbitrary values. Values that are not hashable, that is, values containing lists, dictionaries or other mutable types (that are compared by value rather than by object identity) may not be used as keys. Numeric types used for keys obey the normal rules for numeric comparison: if two numbers compare equal (such as 1 and 1.0) then they can be used interchangeably to index the same dictionary entry. (Note however, that since computers store floating-point numbers as approximations it is usually unwise to use them as dictionary keys.).
Tristan
contact-datartichaut@pm.me
Python projects list
Libraries.
Beginner
What are dictionnaries and why use them.
In more technical termes, dictionnaries are defined as :
Dictionnaries are a little different from other collections.
Their use cases are easier to spot, they work with keys and every task where you would need to link a value to a key can be addressed with dictionnaries.
ToC
import copy
import itertools as it
from collections import defaultdict
Beginner
Declaring a dictionary.
Contrary to other collections, there is only one way to declare a dictionary.
ToC
d1 = dict()
It is not possible to declare an empty dictionary with d1 = {}
as this notation is reserverd
for sets ...
... But there are two ways to declare a dictionary with values.
# The most used version is :
d1 = {'A': 1, 'B': 2}
# But we can also use :
d1 = dict([('A', 1), ('B', 2)])
The second version works with any combination of iterables, as long as it is a "list" of "pairs" of values.
For each pair, the first value become a key, the second is the value attached to it.
A dictionary can also be initialized with only keys.
d1 = dict.fromkeys(['first', 'second'])
print(d1)
Intermediate
The zip
built-in function allows to create a dictionary from two lists.
It simply creates a list a tuples with each pairs of elements from the two lists.
keys_list = ['A', 'B', 'C']
values_list = [1, 2, 3]
zipped = zip(keys_list, values_list)
lst_zip = list(zipped)
print(lst_zip)
At this point we can easly convert this object to a dictionary.
d1 = dict(lst_zip)
print(d1)
This can be achieved in one line.
d1 = dict(list(zip(keys_list, values_list)))
print(d1)
The itertools.ziplongest
function allows to work with unevenly sized lists.
keys_list = ['A', 'B', 'C', 'D', 'E']
values_list = [1, 2, 3]
lst_zip = list(it.zip_longest(keys_list, values_list))
d1 = dict(lst_zip)
print(d1)
Finaly, it should be noted that keys and values can be of any types, even iterables allowing to create really complex structures with dictionaries.
d1 = {'A': [1, 2], 2: 3, 5.1: 'value'}
the only constraint is that keys must be unique.
Beginner
Accessing elements in a dictionary.
Dictionary are made to easly retrieve a value from a key.
There are several ways to do that, some with default values, some with other features.
ToC
We can access any value with its key with the []
notation.
d1 = {'A': 'word', 'B': [1, 2], 'C': 3}
print(d1['A'])
The dict.get
method can also be useful if we are not sure if the key exists.
print("Original dictionary :")
print(d1)
Using dict.get
on an existing item.
print(d1.get('A', 'default value'))
Using dict.get
on an item that does not existing in the dictionary.
print(d1.get('D', 'default value'))
When searching for 'D'
, which is not a key in the dictionary, the get method returns 'default value'
but the dictionary itself is not altered.
Another possibility is the setdefault function.
print(d1.setdefault('D', 'default value'))
Just like with get, the default value is returned if the searched key is not in the dictionary, but this time the key is added to the dictionary.
print(d1)
When searching for 'D'
, which is not a key in the dictionary, the setdefault methodreturns
'default value' and the value is adde to the dictionary itself with 'D'
as a key.
We can easly check if a key is in the dictionary.
print('A' in d1)
The dict.items
method returns a dict_items
object that act as a list of tuples.
Each tuple is a pair of key with the associated value.
print(d1.items())
The dict.keys
method returns a dict_keys
object that act as a list.
This list contains all keys in the first level of the dictionary.
A list of keys can also be obtained with list(d1)
.
print(d1.keys())
The dict.values
method returns a dict_values object that act as a list.
This list contains all values in the first level of the dictionary.
print(d1.values())
Beginner
Adding elements to a dictionary.
Only the dict.update
method (same with sets) is available to add elements to dictionaries.
ToC
d1 = {'A': 1, 'B': 2}
d1.update({'C': 3})
print(d1)
Just like before, we can use any iterable shaped like a "list of pairs".
d1.update([('D', 4), ('E', 5)])
print(d1)
Beginner
Modifying elements in a dictionary.
Since we can access values, we can modify it just as easily as with other mutable collections.
ToC
d1 = {'A': 1, 'B': 2}
print('Original dictionary.')
print(d1)
Now let's change the value linked to the 'A'
key.
d1['A'] = 'New value'
print('\nDictionary after modification.')
print(d1)
Intermediate
We cannot modify a key direclty but there are some workarounds.
d1 = {'A': 1, 'B': 2}
d1['new_A'] = d1.get('A')
del d1['A']
print(d1)
Basically we just create a new key with the value of the old key and we delete the old item.
Beginner
Deleting elements from a dictionary.
Just like accessing, we can delete a key-value pair if we know the key.
ToC
d1 = {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6}
The dict.pop
method remove the key-value pair passing the key as a parameter.
The value is returned.
print('Item return by pop :')
print(d1.pop('A'))
print('\nState of the dictionary :')
print(d1)
The dict.popitem
method remove the last key-value pair in the dictionary.
The key-value pair is returned.
print('Item return by popitem :')
print(d1.popitem())
print('\nState of the dictionary :')
print(d1)
The del
statement deletes a specified key and the associated value.
Nothing is returned.
del d1['E']
print(d1)
the dict.clear
method removes everything in a dictionary.
d1.clear()
print(d1)
Beginner
Operations with dictionaries.
Dictionaries do not support as many operators as sets but we can still perform a union as of version 3.9.
ToC
d1 = {'A': 10, 'B': 15}
d2 = {'C': 30}
d3 = d1 | d2
the |=
operator updates the dictionary on the left of the operator.
print(d3)
Beginner
Copying a dictionary.
Copies work just like they do for lists and other mutable collections.
ToC
d1 = {'A': 1, 'B': [1, 2]}
Dictionary have a built-in method that returns a shallow copy.
d2 = d1.copy()
d2['A'] = 2
print('The original dictionary after modifying the first level of the copied dictionary.')
print(d1)
d2['B'][0] = 3
print('The original dictionary after modifying a deeper level of the copied dictionary.')
print(d1)
Intermediate
It is possible to make a "real" copy.
d2 = copy.deepcopy(d1)
Advanced
We can illustrate that by accessing the memories addresses of these objects.
d1 = {'A': 1, 'B': [1, 2]}
d2 = d1.copy()
print(f"Address of the original dictionary : {hex(id(d1))}")
print(f"Address of the copied dictionary : {hex(id(d2))}")
print(f"\nAddress of the deeper level of the original dictionary : {hex(id(d1['B']))}")
print(f"Address of the deeper level of the copied dictionary : {hex(id(d2['B']))}")
Same with deepcopies.
d1 = {'A': 1, 'B': [1, 2]}
d2 = copy.deepcopy(d1)
print(f"Address of the original dictionary : {hex(id(d1))}")
print(f"Address of the copied dictionary : {hex(id(d2))}")
print(f"\nAddress of the deeper level of the original dictionary : {hex(id(d1['B']))}")
print(f"Address of the deeper level of the copied dictionary : {hex(id(d2['B']))}")
Now the addresses are all different.
Copying part of a dictionary works in the same fashion, some elements can be copied directly, some do not
d1 = {'A': 500, 'B': [250, 300]}
elt_1 = d1['A']
print(f"Address of the original element : {hex(id(d1['A']))}.")
print(f"Address of the copied element : {hex(id(elt_1))}.")
print("\nAssigning a new value to the copied element.\n")
elt_1 = 501
print(f"Address of the original element : {hex(id(d1['A']))}, and its value : {d1['A']}.")
print(f"Address of the copied element : {hex(id(elt_1))}, and its value : {elt_1}.")
At first we can see that the "copy" (the elt_1
variable) share the same address as
the element directly in the dictionary. We could think that it is not a copy.
But when we try to change this value, we can see that the address of the copied value changed and the actual value inside the dictionary remained the same.
This come from the fact that python base types
(int
, float
, str
, etc.) are immutable. We cannot change
directly the value of an integer, python have to create a new variable first.
Now we can do the same thing but with a mutable object such as a list.
elt_2 = d1['B']
print(f"Address of the original element : {hex(id(d1['B']))}")
print(f"Address of the copied element : {hex(id(elt_2))}")
print("\nAssigning a new value to the copied element.\n")
elt_2[0] = 251
# %%s
print(f"Address of the original element : {hex(id(d1['B']))}, and its value : {d1['B']}.")
print(f"Address of the copied element : {hex(id(elt_2))}, and its value : {elt_2}.")
We observe the same behaviour as when we try to copy a regular list. The list is not copied but a reference
to it is. If we need a copy of this list we must use the copy.copy
or the copy.deepcopy
functions from the copy
librairy.
Beginner
Iterating through a dictionary.
As seen before, the values, keys and items methods return lists of elements which allow to iterate through them.
ToC
d1 = {'A': 1, 'B': 2, 'C': 3}
Iterating through values.
for x in d1.values():
print(x)
Iterating through keys.
for x in d1.keys():
print(x)
Iterating through items (key-value pairs).
for x in d1.items():
print(x)
Intermediate
The iter function can also be used to get an iterator over the keys of the dictionary.
for x in iter(d1):
print(x)
Beginner
Logic tests with dictionaries.
Just like with lists, a dictionary can be tested and is Truthy if not empty.
ToC
d1 = {'A': 1, 'B': 2}
while d1:
item = d1.popitem()
print(f"key {item[0]} -> value {item[1]}")
Intermediate
Dictionary comprehension.
Dictionary comprehensions work in a similar fashion as with other iterables.
ToC
l1 = [10, 15, 'E', [1, 'K'], True]
d1 = {f"elt_{i}": x for i, x in enumerate(l1, start=1)}
print(d1)
l1 = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
dd1 = defaultdict(list)
for k, v in l1:
dd1[k].append(v)
print(dd1.items())
In this example, in the first iteration of the loop, we try to access dd1['yellow'] which would fail with a normal dictionary since this key does not exist.
The definition dd1 = defaultdict(list)
allows to initialize by default every unkown key with
an empty list.
So by default dd1['any_key']
will be an empty list and the dict.append
method can
be applied to it.
Defaultdicts are very useful in a lot of situation where we cannot know how many keys are going to be needed.
Intermediate
Tips and tricks.
This example is a classic interview skill test.
The goal is to add a prefix to every key in a dictionary and all sub-dictionaries.
ToC
def modif_dict(d1, str_prefix):
"""Add a prefix to any key in a dictionary."""
dict_tmp = {}
for str_key in d1.keys():
dict_tmp[str_prefix + str(str_key)] = d1.get(str_key)
if isinstance(d1[str_key], dict):
dict_tmp[str_prefix + str_key] = modif_dict(d1[str_key], 'clé_')
return dict_tmp
d1 = {'A': 1, 'B': 2, 'C':
{'C1': 3, 'C2': {'C21': 4, 'C22': 5, 'C23': {'C231': 6, 'C232': 7}}, 'C3': 8}}
d2 = modif_dict(d1, 'key_')