Table of Contents
- 2.1 Container data types in Python
- 2.2 Lists
- 2.2.1 List bases
- 2.2.2 Slicing and copies
- 2.2.3 Advanced methods on lists
- 2.2.4 List use cases
- 2.3 Dictionaries
- 2.3.1 Dictionary bases
- 2.3.2 Advanced dictionary methods
- 2.3.3 Dictionary use cases
- 2.4 Tuples
- 2.4.1 Tuple bases
- 2.4.2 Use cases for tuples
- 2.5 Sets
- 2.5.1 Set bases
- 2.5.2 Set use cases
- 2.6 Module 2 Summary
- 3.1 defaultdict
- 3.1.1 Understand defaultdict
- 3.1.2 Use case of defaultdict
- 3.2 OrderedDict
- 3.2.1 Understand OrderedDict
- 3.2.2 OrderedDict use case
- 3.3 Counter
- 3.3.1 Understand Counter
- 3.3.2 Counter use cases
- 3.4 Module 3 Summary
- 4.1 namedtuple()
- 4.1.1 Understand namedtuple()
- 4.1.2 Use case of namedtuple()
- 4.2 ChainMap
- 4.2.1 Understand ChainMap
- 4.2.2 ChainMap use cases
- 4.3 deque
- 4.3.1 Understand deque
- 4.3.2 Deque use case
- 4.4 Module 4 Summary
- 5.1 UserString
- 5.2 UserList
- 5.3 UserDict
- 5.4 Module 5 Summary
1. Course presentation
Python offers four built-in container data types — list, dict, tuple and set — that streamline the process of storing, accessing and managing data collections in a flexible and intuitive way. However, in some more complex data manipulation and storage scenarios, these basic types are not enough. This is where the collections module comes in.
This course covers the following topics:
- Using built-in container data types (
list,dict,tuple,set) - Managing specialized dictionary subclasses (
defaultdict,OrderedDict,Counter) - Defining custom data structures with
namedtuple - Managing scopes with
ChainMap - Implementing queues and stacks with
deque - Customizing built-in types with
UserString,UserListandUserDict
At the end of this course, you will know how to use all the specialized container data types of the collections module.
2. Using built in containers
2.1 Container data types in Python
Container data types are data structures that can contain other objects. They allow you to organize, store and manipulate data in a structured way.
Python offers four built-in container data types:
| Type | Orderly | Mutable | Unique Elements | Access |
|---|---|---|---|---|
list | Yes | Yes | No | By index |
dict | Yes (Python 3.7+) | Yes | Unique keys | By key |
tuple | Yes | No | No | By index |
set | No | Yes | Yes | Iteration / membership test |
- List: ordered collection of objects of mixed types. Mutable, dynamic, accessible by index.
- Dictionary: collection of key-value pairs where each key is unique. Allows quick retrieval, addition and modification of values.
- Tuple: ordered and immutable collection. Unable to add or remove items after creation.
- Set: collection of unique and unordered elements. Ideal for membership tests and mathematical operations (intersection, union, etc.).
2.2 Lists
2.2.1 Basics of lists
Lists are one of the most used container data types in Python. They are :
- Mutables: elements can be modified, added or deleted.
- Dynamic: they can increase or decrease in size.
- Ordered: the order of insertion of the elements is preserved.
- Accessible by index: any element can be directly accessed via its index.
Creating a list
empty_list = []
print(empty_list)
books = ["1984", "Don Quixote", "The Great Gatsby"]
print(books)
Element access
The index of the first element is 0. Negative indexes allow you to access elements from the end.
movies = ["Inception", "The Matrix", "Interstellar"]
print(movies[0]) # Premier film
print(movies[1]) # Deuxième film
print(movies[-1]) # Dernier film
List length
languages = ["Python", "Rust", "C++"]
print(len(languages))
Add items
# Ajouter à la fin avec append()
shopping_list = ["milk", "apples", "bread"]
shopping_list.append("butter")
print(shopping_list)
# Insérer à une position précise avec insert()
primes = [2, 3, 5, 7]
primes.insert(2, 11)
print(primes)
Delete items
planets = ["Mercury", "Venus", "Earth", "Mars"]
# pop() supprime et retourne l'élément à l'index donné (dernier par défaut)
popped_planet_1 = planets.pop() # Dernier élément
popped_planet_2 = planets.pop(1) # pop() retourne l'élément supprimé
del planets[0] # Ne retourne pas l'élément supprimé
print(f"Removed Planet: {popped_planet_1}")
print(f"Removed Planet: {popped_planet_2}")
print(planets)
# remove() supprime la première occurrence d'une valeur
elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"]
elements.remove("Lithium")
print(elements)
# clear() vide entièrement la liste
elements = ["Hydrogen", "Helium", "Lithium", "Beryllium"]
elements.clear()
print(elements)
Expand a list
colors = ["red", "green", "blue"]
more_colors = ["orange", "purple"]
colors.extend(more_colors)
print(colors)
Modify an element
continents = ["Asia", "Africa", "Europe"]
continents[2] = "Antarctica"
print(continents)
Count occurrences and find an index
notes = ["C", "D", "E", "F", "G", "A", "B", "C", "D", "C"]
print(notes.count("C"))
animals = ["lion", "tiger", "bear", "wolf"]
print(animals.index("bear"))
Sort a list
temperatures = [23, 18, 30, 15, 22]
print("Original order:", temperatures)
temperatures.sort()
print("Sorted in ascending order:", temperatures)
temperatures.sort(reverse=True)
print("Sorted in descending order:", temperatures)
# sorted() retourne une nouvelle liste sans modifier l'originale
sorted_temperatures_asc = sorted(temperatures)
print("Sorted in ascending order:", sorted_temperatures_asc)
sorted_temperatures_desc = sorted(temperatures, reverse=True)
print("Sorted in descending order:", sorted_temperatures_desc)
print("Original order:", temperatures)
Invert a list
colors = ["red", "green", "blue", "yellow"]
colors.reverse()
print("Reversed list:", colors)
Test membership
instruments = ["guitar", "piano", "violin"]
print("piano" in instruments)
print("drums" in instruments)
if "piano" in instruments:
print("I can play piano")
Iterate over a list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
Concatenation and repetition
list1 = [1, 2, 3]
list2 = [4, 5, 6]
concatenated_list = list1 + list2
repeated_list = list1 * 3
print("Concatenated:", concatenated_list)
print("Repeated:", repeated_list)
2.2.2 Slicing and copies
Slicing
Slicing allows you to extract a sublist. The syntax is list[start:stop:step].
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Avec les trois paramètres : start, stop et step
# numbers[start_index:stop_index:step]
even_numbers = numbers[1:8:2]
print("Even numbers:", even_numbers)
# Sans index de départ (commence au début)
first_three = numbers[:3]
print("First three numbers:", first_three)
# Sans index de fin (va jusqu'à la fin)
from_four_onwards = numbers[3:]
print("Numbers from 4 onwards:", from_four_onwards)
# Sans step (inclut chaque élément)
first_to_fourth = numbers[0:4]
print("First to fourth numbers:", first_to_fourth)
# Avec deux paramètres (start et stop)
middle_numbers = numbers[3:6]
print("Middle numbers (4, 5, 6):", middle_numbers)
# Avec des index négatifs
last_three = numbers[-3:]
print("Last three numbers:", last_three)
List copies — pitfalls to avoid
A common pitfall is to create a reference instead of a copy.
# Mauvaise façon : création d'une référence, pas d'une copie
planets_before_2006 = ["Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]
# Cela crée une référence, pas une copie !
planets_after_2006 = planets_before_2006
planets_after_2006.pop() # Modifie aussi planets_before_2006 !
print("Planets before 2006:", planets_before_2006)
print("Planets after 2006:", planets_after_2006)
# Bonne façon : utiliser copy() ou le slicing [:]
planets_before_2006 = ["Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"]
# Copie correcte avec .copy()
planets_after_2006_copy = planets_before_2006.copy()
# Copie correcte avec le slicing
planets_after_2006_slice = planets_before_2006[:]
planets_after_2006_copy.remove("Pluto")
planets_after_2006_slice.remove("Pluto")
print("Planets before 2006:", planets_before_2006) # Pluto inclus
print("Planets after 2006 (using .copy()):", planets_after_2006_copy) # Pluto retiré
print("Planets after 2006 (using slicing):", planets_after_2006_slice) # Pluto retiré
2.2.3 Advanced methods on lists
List comprehensions
List comprehensions provide a concise syntax for creating lists.
squares = [x ** 2 for x in range(1, 11)]
print(squares)
squares_of_even = [x ** 2 for x in range(10) if x % 2 == 0]
print(squares_of_even)
Nested lists
Lists can contain other lists, allowing data structures to be represented in 2D or more.
2.2.4 Use cases for lists
Prototyping and inventory management
Lists are excellent for prototyping because of their simplicity.
# Inventory Management Example
inventory = [
{"id": 1, "name": "T-shirt", "quantity": 25, "price": 15.99},
{"id": 2, "name": "Jeans", "quantity": 30, "price": 39.99},
{"id": 3, "name": "Socks", "quantity": 50, "price": 4.99}
]
def add_product(inventory, product):
inventory.append(product)
def remove_product(inventory, product_id):
inventory[:] = [product for product in inventory if product["id"] != product_id]
add_product(inventory, {"id": 4, "name": "Jacket", "quantity": 15, "price": 59.99})
remove_product(inventory, 2)
print("Current Inventory:")
for product in inventory:
print(product)
Note: The
[:]ininventory[:] = ...is important: it modifies the global list in place rather than creating a new local variable.
Managing a music playlist
playlist = []
def add_song(playlist, song):
playlist.append(song)
def remove_song(playlist, song_title):
playlist[:] = [song for song in playlist if song["title"] != song_title]
def move_song(playlist, song_title, new_position):
for i, song in enumerate(playlist):
if song["title"] == song_title:
playlist.insert(new_position, playlist.pop(i))
break
add_song(playlist, {"title": "The Entertainer", "artist": "Scott Joplin"})
add_song(playlist, {"title": "St. Louis Blues", "artist": "W.C. Handy"})
add_song(playlist, {"title": "Clair de Lune", "artist": "Claude Debussy"})
move_song(playlist, "Clair de Lune", 0)
remove_song(playlist, "St. Louis Blues")
print("Current Playlist:")
for song in playlist:
print(f"{song['title']} by {song['artist']}")
Sensor data collection
sensor_data = []
def receive_sensor_data():
import random
return random.uniform(20, 30)
for _ in range(1000):
new_data = receive_sensor_data()
sensor_data.append(new_data)
recent_data = sensor_data[-100:] # Les 100 dernières valeurs
if len(sensor_data) > 1000:
sensor_data = sensor_data[-1000:]
average_recent = sum(recent_data) / len(recent_data)
print(f"Average of recent data: {average_recent}")
Real-time event logging
event_log = []
def receive_event(event):
event_log.append(event)
for i in range(100):
event = f"Event {i}"
receive_event(event)
recent_events = event_log[-10:]
print("Recent Events:")
for event in recent_events:
print(event)
Time complexity of list operations
import timeit
large_list = list(range(100000))
def access_element():
_ = large_list[50000]
def append_element():
large_list.append("new_element")
def remove_element():
large_list.remove("new_element")
def insert_element():
large_list.insert(50000, "inserted_element")
access_time = timeit.timeit(access_element, number=1000)
append_time = timeit.timeit(append_element, number=1000)
remove_time = timeit.timeit(remove_element, number=1000)
insert_time = timeit.timeit(insert_element, number=1000)
print(f"Access time: {access_time} seconds")
print(f"Append time: {append_time} seconds")
print(f"Remove time: {remove_time} seconds")
print(f"Insert time: {insert_time} seconds")
| Operation | Complexity |
|---|---|
| Access by index | O(1) |
| Append (end) | O(1) amortized |
| Insert (middle) | O(n) |
| Remove | O(n) |
| Search (in) | O(n) |
2.3 Dictionaries
2.3.1 Dictionary basics
Dictionaries are collections of key-value pairs where each key is unique. They are :
- Mutable and dynamic: you can add, modify or delete pairs.
- Ordered since Python 3.7: insertion order is preserved.
- Optimized for search: key access is O(1).
Creating a dictionary
# Dictionnaire vide
my_dict = {}
print(my_dict)
# Dictionnaire avec des valeurs initiales
person_info = {"name": "Some name", "age": 30, "city": "Some city"}
print(person_info)
Element access
fruit = {"apple": 2, "banana": 3}
print("Price of apple:", fruit["apple"])
print("Price of banana:", fruit["banana"])
# get() retourne None (ou la valeur par défaut) si la clé n'existe pas
print("Price of apple:", fruit.get("apple", "Price not found"))
print("Price of mango:", fruit.get("mango", "Price not found"))
Adding and updating elements
my_dict = {}
# Ajout d'une nouvelle paire clé-valeur
my_dict["language"] = "Python"
# Mise à jour d'une valeur existante
my_dict["language"] = "JavaScript"
# Ajout de plusieurs éléments
my_dict.update({"version": "ES6", "typing": "dynamic"})
# Opérateur de mise à jour |=
# my_dict |= other_dict
# Fusion de deux dictionnaires avec l'opérateur de fusion
merged_dict = my_dict | {"new_key": "new_value", "version": "ES7"}
print("my_dict:", my_dict)
print("merged_dict:", merged_dict)
Deleting items
my_dict = {"language": "Python", "release_year": "2019",
"version": "3.8", "platform": "Windows"}
del my_dict["platform"] # Suppression directe
version = my_dict.pop("version") # Suppression et retour de la valeur
release_year = my_dict.popitem() # Suppression et retour du dernier élément
my_dict.clear() # Vidage complet
print("Removed version:", version)
print("Release year element:", release_year)
print("Current dictionary:", my_dict)
Iteration over a dictionary
person_info = {"name": "Some name", "age": 28, "city": "Some city"}
# Itération sur les clés et valeurs
for key, value in person_info.items():
print(f"{key}: {value}")
# Itération sur les clés uniquement
for key in person_info.keys():
print(f"Key: {key}")
# Itération sur les valeurs uniquement
for value in person_info.values():
print(f"Value: {value}")
Checking for the existence of a key
my_dict = {"language": "Python", "version": "3.9"}
if "version" in my_dict:
print("Version found:", my_dict["version"])
else:
print("Version key does not exist.")
2.3.2 Advanced methods on dictionaries
Dictionary comprehensions
# Création d'un dictionnaire avec une comprehension
squares = {x: x*x for x in range(6)}
print("Squares:", squares)
# Transformation d'un dictionnaire (inversion clés/valeurs)
original_dict = {"a": 1, "b": 2, "c": 3, "d": 4}
inverted_dict = {value: key for key, value in original_dict.items()}
print("Inverted dict:", inverted_dict)
Copying dictionaries
As with lists, a simple assignment creates a reference, not a copy.
import copy
# Dictionnaire original
original_dict = {"name": "Some name", "hobbies": ["reading", "traveling"]}
# Copie superficielle (shallow copy)
shallow_copied_dict = original_dict.copy()
shallow_copied_dict["age"] = 30
# Copie profonde (deep copy)
deep_copied_dict = copy.deepcopy(original_dict)
# Modification des copies
shallow_copied_dict["hobbies"].append("hiking")
deep_copied_dict["hobbies"].append("swimming")
print("Original Dict:", original_dict)
print("Shallow Copied Dict:", shallow_copied_dict)
print("Deep Copied Dict:", deep_copied_dict)
Warning:
copy()only copies references to nested mutable objects (like lists).copy.deepcopy()creates independent copies of all nested objects.
The setdefault() method
my_dict = {"a": 1, "b": 2}
# "a" existe, retourne la valeur de "a"
value_a = my_dict.setdefault("a", 99)
print("Value of a:", value_a)
print("Dictionary:", my_dict)
# "c" n'existe pas, ajoute "c" avec la valeur par défaut 99
value_c = my_dict.setdefault("c", 99)
print("Value of c:", value_c)
print("Dictionary:", my_dict)
# Groupement par première lettre avec setdefault
fruit = ["apple", "pear", "banana", "apricot", "blueberry", "orange"]
groups = {}
for item in fruit:
key = item[0]
groups.setdefault(key, []).append(item)
print(groups)
Sorting a dictionary
from operator import itemgetter
scores = {"KeyC": 42, "KeyA": 25, "KeyB": 162}
print("scores.items():", scores.items())
# Tri par clé
sorted_by_key = dict(sorted(scores.items()))
print("Sorted by key:", sorted_by_key)
# Tri par valeur
sorted_by_value = dict(sorted(scores.items(), key=itemgetter(1)))
print("Sorted by value:", sorted_by_value)
2.3.3 Dictionaries use cases
Configuration parameters
config = {
"debug_mode": True,
"api_endpoint": "https://api.example.com",
"retry_attempts": 3,
"themes": ["light", "dark"]
}
debug_mode = config["debug_mode"]
print(f"Debug Mode: {debug_mode}")
config["retry_attempts"] = 5
print(f"Retry Attempts: {config['retry_attempts']}")
Dispatch tables (Function Dispatch Tables)
import os
import platform
def list_files(directory='.'):
"""Lists files in the given directory."""
files = [f for f in os.listdir(directory)
if os.path.isfile(os.path.join(directory, f))]
for file in files:
print(file)
def count_files(directory='.'):
"""Counts the number of files in the given directory."""
files = [f for f in os.listdir(directory)
if os.path.isfile(os.path.join(directory, f))]
print(f"Number of files: {len(files)}")
def sys_info():
"""Displays basic system information."""
print(f"System: {platform.system()}")
print(f"Version: {platform.version()}")
# Table de dispatch
commands = {
"list_files": list_files,
"count_files": count_files,
"sys_info": sys_info,
}
user_command = input("Enter command (list_files, count_files, sys_info): ").strip()
directory = '.'
if user_command in commands:
if user_command in ["list_files", "count_files"]:
commands[user_command](directory)
else:
commands[user_command]()
else:
print("Unknown command.")
Memoization
# Mémoïsation de la fonction de Fibonacci
def fibonacci(n, memo={}):
if n in memo:
return memo[n]
if n <= 2:
return 1
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
return memo[n]
print(fibonacci(10))
Plugin Registry
plugins = {}
def register_plugin(name, func):
plugins[name] = func
def call_plugin(name):
if name in plugins:
plugins[name]()
def plugin_greet():
print("Hello from the plugin!")
register_plugin("greeting_plugin", plugin_greet)
call_plugin("greeting_plugin")
Counting and grouping (frequency tables)
log_data = [
{"ip": "192.168.1.1", "url": "/index.html", "status": "200"},
{"ip": "192.168.1.2", "url": "/about.html", "status": "200"},
{"ip": "192.168.1.1", "url": "/contact.html", "status": "200"},
{"ip": "192.168.1.3", "url": "/index.html", "status": "200"},
{"ip": "192.168.1.2", "url": "/products.html", "status": "200"},
{"ip": "192.168.1.1", "url": "/products.html", "status": "404"},
]
request_counts = {}
for log_entry in log_data:
ip_address = log_entry["ip"]
request_counts[ip_address] = request_counts.get(ip_address, 0) + 1
for ip, count in request_counts.items():
print(f"IP Address {ip} made {count} requests")
The **kwargs parameter
def html_tag(tag, content, **kwargs):
"""
Returns a string representation of an HTML tag.
"""
print("kwargs:", kwargs)
attributes = ' '.join([f'{key}="{value}"' for key, value in kwargs.items()])
return f'<{tag} {attributes}>{content}</{tag}>'
print(html_tag('a', 'Click Here', href="https://example.com", style="color: red;"))
2.4 Tuples
2.4.1 Tuple bases
Like lists, tuples are used to store objects in an ordered sequence. But unlike lists, tuples are immutable: you cannot modify, add or delete elements after their creation. Tuples are also more memory efficient.
Creating tuples
# Tuple vide
my_tuple = ()
print(my_tuple)
# Tuple avec éléments
my_tuple = ("String", 2, 3.5)
print(my_tuple)
# Tuple à un seul élément (la virgule est obligatoire !)
my_tuple = ("String",)
print(my_tuple)
# Sans parenthèses (syntaxe valide mais déconseillée dans ce contexte)
my_tuple = "String", 2, 3.5
print(my_tuple)
Element access
my_tuple = ('p','e','r','m','i','t')
print(my_tuple[0]) # p
print(my_tuple[5]) # t
print(my_tuple[-1]) # t
print(my_tuple[-6]) # p
Immutability
my_tuple = ('p','e','r','m','i','t')
try:
my_tuple[3] = 'a'
except TypeError as e:
print("Error: Cannot modify a tuple. Tuples are immutable.")
Tuple methods
my_tuple = (1, 2, 3, 1, 2, 1, 2, 3, 1)
print(my_tuple.count(1)) # Nombre d'occurrences de 1
print(my_tuple.index(3)) # Index de la première occurrence de 3
Packing and unpacking
# Packing
my_tuple = 3.14, "Python", "Tuples"
# Unpacking
pi, language, concept = my_tuple
print(pi)
print(language)
print(concept)
2.4.2 Use cases for tuples
Immutable collection of related elements
Geographic coordinates are a good example: longitude and latitude should always go together and should never be changed.
def calculate_distance(coord1, coord2):
return ((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) ** 0.5
# Coordonnées en tant que tuples
point_a = (40.7128, -74.0060) # New York
point_b = (34.0522, -118.2437) # Los Angeles
distance = calculate_distance(point_a, point_b)
print(f"Distance: {distance} units")
Return multiple values from a function
Python does not support returning multiple values natively — it actually returns a single object, a tuple. Packing and unpacking allow you to simulate this behavior.
def min_max(numbers):
return min(numbers), max(numbers) # Retourne un tuple
min_result, max_result = min_max([1, 20, 33, 401, 5])
print(f"Min is {min_result} and max is {max_result}")
The *args parameter
def sum_numbers(*args):
print(args)
return sum(args)
print(sum_numbers(1, 2, 3))
print(sum_numbers(1, 2, 3, 4, 5))
Storing configuration constants
ENV_VARIABLES = ('DB_HOST', 'DB_USER', 'DB_PASS', 'API_KEY')
import os
db_host = os.getenv(ENV_VARIABLES[0])
Working with database records
def get_employee_record(employee_id):
# Simulation de la récupération d'un enregistrement
return (123, "John Doe", "Software Engineer", 75000)
employee_record = get_employee_record(123)
print(f"Employee Record: ID={employee_record[0]}, Name={employee_record[1]}, "
f"Role={employee_record[2]}, Salary={employee_record[3]}")
2.5 Sets
2.5.1 Set basics
Sets are data structures designed to store and manipulate collections of unique elements. They :
- Automatically remove duplicates.
- Are dynamic: they can increase or decrease.
- Are unordered: no access by index, no slicing.
- Cannot contain mutable objects (no
dictorlistinside).
Creating sets
# Utiliser le constructeur set() pour créer un set vide
my_set = set()
print(my_set)
# Avec des accolades (les doublons sont automatiquement supprimés)
my_set = {1, 2, 3, "Python", 1, 2, 3}
print(my_set)
# Depuis un itérable
my_set = set([1, 2, 3])
print(my_set)
Warning:
{}alone creates an empty dictionary, not an empty set.
Element access
my_set = {1, 2, 3, 4}
for element in my_set:
print(element)
print(1 in my_set)
print(5 in my_set)
Modification of a set
my_set = {1, 2, 3, 4}
my_set.add("element")
print(my_set)
my_set.remove("element")
my_set.discard(5) # Ne lève pas d'erreur si l'élément n'existe pas
print(my_set)
Set operations
set1 = {1, 2, 3}
set2 = {3, 4, 5}
print("Set 1:", set1)
print("Set 2:", set2)
print("Union:", set1 | set2) # Éléments des deux sets sans doublons
print("Intersection:", set1 & set2) # Éléments présents dans les deux sets
print("Difference:", set1 - set2) # Éléments de set1 absents de set2
print("Symmetric Difference:", set1 ^ set2) # Éléments des deux sets sans l'intersection
# Les méthodes équivalentes (acceptent n'importe quel itérable)
# print(set1.union(set2))
# print(set1.intersection(set2))
# print(set1.difference(set2))
# print(set1.symmetric_difference(set2))
Augmented assignment operators
# Modifient le premier set en place
# set1 |= set2
# set1 &= set2
# set1 -= set2
# set1 ^= set2
Subsets and supersets
setA = {1, 2, 3}
setB = {1, 2, 3, 4, 5}
setC = {10, 11, 12}
print("setA is a subset of setB:", setA.issubset(setB)) # True
# Équivalent : setA <= setB
# Sous-ensemble propre : setA < setB
print("setB is a superset of setA:", setB.issuperset(setA)) # True
# Équivalent : setB >= setA
# Deux sets sont disjoints quand ils n'ont aucun élément en commun
print("setA has no intersection with setC:", setA.isdisjoint(setC))
print("setA has no intersection with setB:", setA.isdisjoint(setB))
Frozen sets
Frozen sets are immutable sets.
my_set = frozenset([1, 2, 3])
try:
my_set.add(4)
except AttributeError as error:
print(f"Error: {error}")
2.5.2 Use cases for sets
Removing duplicates
raw_hashtags = ['#python', '#dev', '#python', '#coding',
'#tech', '#dev', '#coding', '#tech', '#pluralsight']
unique_hashtags = set(raw_hashtags)
print(unique_hashtags)
Effective membership test
Sets provide O(1) complexity for membership testing, making them much faster than lists for large collections.
import timeit
user_ids = list(range(1, 1000001))
test_id = 999999
user_ids_set = set(user_ids)
def check_list():
return test_id in user_ids
def check_set():
return test_id in user_ids_set
list_time = timeit.timeit(check_list, number=100)
set_time = timeit.timeit(check_set, number=100)
print(f"List membership test time: {list_time:.5f} seconds")
print(f"Set membership test time: {set_time:.5f} seconds")
Set operations for data analysis
interests_group1 = {"reading", "traveling", "cooking", "gardening"}
interests_group2 = {"traveling", "music", "video games", "cooking"}
# Intérêts communs
common_interests = interests_group1.intersection(interests_group2)
print(f"Common interests: {common_interests}")
# Intérêts uniques au groupe 1
unique_group1 = interests_group1.difference(interests_group2)
print(f"Unique to Group 1: {unique_group1}")
# Intérêts uniques au groupe 2
unique_group2 = interests_group2.difference(interests_group1)
print(f"Unique to Group 2: {unique_group2}")
2.6 Module 2 Summary
- Lists: dynamic tables that can store data of mixed types. Mutable — they allow their elements to be added, deleted, and modified. Found in most applications, their use cases range from prototyping to raw data preparation.
- Dictionaries: collections of key-value pairs allowing fast retrieval by key. Useful for managing configuration parameters, implementing frequency tables, dispatch tables, etc.
- Tuples: immutable. Provide a reliable container for fixed data sequences, often used to store records or return multiple values from a function.
- Sets: Unordered collections of unique elements, known for their rapid membership testing, automatic elimination of duplicates, and mathematical set operations.
3. Improve efficiency with advanced dictionaries
The collections module provides several specialized subclasses of the built-in dict type. These classes meet specific needs that ordinary dictionaries do not cover optimally.
3.1 defaultdict
3.1.1 Understanding defaultdict
defaultdict is a subclass of the built-in dict. It automatically provides a default value whenever we try to access a key that does not exist.
from collections import defaultdict
print(issubclass(defaultdict, dict)) # True
# Utiliser list() comme factory function
dd = defaultdict(list)
The factory function is called without arguments to provide the default value. It must be callable.
Adding elements to a defaultdict
from collections import defaultdict
dd = defaultdict(list)
dd["key1"].append(1)
dd["key2"].append(2)
print(dd["key3"]) # [] — clé créée automatiquement avec une liste vide
print(dd)
defaultdict vs setdefault method
from collections import defaultdict
std_dict = {}
std_dict.setdefault("key", "Default")
print(std_dict["key"]) # Default
dd = defaultdict(lambda: "Default")
print(dd["key"]) # Default
defaultdict is slightly faster for handling missing keys. Additionally, the factory function of defaultdict is only called when the key does not exist, while setdefault always evaluates its second argument.
from timeit import timeit
setup_defaultdict_append = """
from collections import defaultdict
dd = defaultdict(list)
"""
stmt_defaultdict_append = "[dd[f'key_{i // 2}'].append(1) for i in range(2000000)]"
time_defaultdict_append = timeit(stmt=stmt_defaultdict_append,
setup=setup_defaultdict_append, number=10)
setup_setdefault_append = "std_dict = {}"
stmt_setdefault_append = "[std_dict.setdefault(f'key_{i // 2}', []).append(1) for i in range(2000000)]"
time_setdefault_append = timeit(stmt=stmt_setdefault_append,
setup=setup_setdefault_append, number=10)
print("defaultdict:", time_defaultdict_append)
print("setdefault:", time_setdefault_append)
Behavior of the factory function
from collections import defaultdict
def long_factory_function(trigger):
print(f"Factory function ran by {trigger}")
return []
dd = defaultdict(lambda: long_factory_function("defaultdict"), {"existing_key": []})
std_dict = {"existing_key": []}
dd["existing_key"].append(1)
# La factory n'est PAS appelée car "existing_key" existe déjà
std_dict.setdefault("existing_key", long_factory_function("setdefault")).append(1)
# long_factory_function EST appelée même si "existing_key" existe déjà
3.1.2 Use cases of defaultdict
Grouping of elements
from collections import defaultdict
items = [
("Apple", "Fruit"),
("Banana", "Fruit"),
("Hammer", "Tool"),
("Screwdriver", "Tool"),
("Laptop", "Electronics"),
("Smartphone", "Electronics")
]
dd = defaultdict(list)
for item, item_type in items:
dd[item_type].append(item)
# Avec un dict ordinaire : std_dict.setdefault(item_type, []).append(item)
print(dd)
Unique element grouping (no duplicates)
from collections import defaultdict
items = [
("Apple", "Fruit"),
("Banana", "Fruit"),
("Banana", "Fruit"), # Doublon
("Banana", "Fruit"), # Doublon
("Hammer", "Tool"),
("Hammer", "Tool"), # Doublon
("Screwdriver", "Tool"),
("Laptop", "Electronics"),
("Smartphone", "Electronics")
]
dd = defaultdict(set) # set comme factory — ignore les doublons automatiquement
for item, item_type in items:
dd[item_type].add(item)
print(dd)
Counting items by category
from collections import defaultdict
items = [
("Apple", "Fruit"),
("Banana", "Fruit"),
("Hammer", "Tool"),
("Screwdriver", "Tool"),
("Laptop", "Electronics"),
("Smartphone", "Electronics")
]
dd = defaultdict(int) # int() retourne 0 par défaut
for _, item_type in items:
dd[item_type] += 1
print(dd)
Calculation of the total amount by category
from collections import defaultdict
items = [
("Apple", 5, "Fruit"),
("Banana", 3, "Fruit"),
("Hammer", 10, "Tool"),
("Screwdriver", 10, "Tool"),
("Laptop", 5000, "Electronics"),
("Smartphone", 4000, "Electronics")
]
dd = defaultdict(int)
for _, price, item_type in items:
dd[item_type] += price
for item_type, price in dd.items():
print(f'Total sum for {item_type} items is: {price}')
3.2 OrderedDict
3.2.1 Understanding OrderedDict
OrderedDict is a subclass of dict that maintains the insertion order of keys. Since Python 3.7, ordinary dictionaries also preserve insertion order, but OrderedDict provides additional functionality.
Creating an OrderedDict
from collections import OrderedDict
print(issubclass(OrderedDict, dict)) # True
# Dictionnaire ordonné vide
od = OrderedDict()
print(od)
# Depuis un itérable de tuples
od = OrderedDict([("key1", "value1"), ("key2", "value2")])
print(od)
# Depuis un dictionnaire ordinaire
od = OrderedDict({"key1": "value1", "key2": "value2"})
print(od)
# Avec des keyword arguments
od = OrderedDict(key1="value1", key2="value2")
print(od)
Comparison of two OrderedDict
The main difference with ordinary dictionaries: two OrderedDict are equal only if their keys are in the same order.
from collections import OrderedDict
od1 = OrderedDict({"a": 1, "b": 2})
od2 = OrderedDict({"b": 2, "a": 1})
print(od1 == od2) # False — ordre différent !
# Deux dict ordinaires seraient égaux :
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2) # True
Moving elements
from collections import OrderedDict
od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
od.move_to_end("b") # Déplace "b" à la fin
print(od)
od.move_to_end("b", last=False) # Déplace "b" au début
print(od)
Delete from both ends
from collections import OrderedDict
od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
print(od)
last_item = od.popitem() # Supprime le dernier élément
print(f"Popped last item: {last_item}")
print(od)
first_item = od.popitem(last=False) # Supprime le premier élément
print(f"Popped first item: {first_item}")
print(od)
3.2.2 OrderedDict use cases
Managing a sequence of ordered operations
from collections import OrderedDict
operations = OrderedDict([
("clean_data", "Clean the loaded data"),
("load_data", "Load data from source"),
("analyze_data", "Analyze the cleaned data"),
("save_results", "Save analysis results")
])
# Prioriser une opération spécifique
operations.move_to_end("load_data", last=False)
# Exécuter et supprimer la première opération
first_operation_key, _ = operations.popitem(last=False)
print(f"Running '{first_operation_key}' operation.")
Checking order in tests
from collections import OrderedDict
expected_sequence = OrderedDict([
("baseline", "Original version without changes"),
("change1", "Increased font size for better readability"),
("change2", "Changed call-to-action button color"),
("final", "Added customer testimonials")
])
actual_sequence_test = OrderedDict([
("baseline", "Original version without changes"),
("change2", "Changed call-to-action button color"),
("change1", "Increased font size for better readability"),
("final", "Added customer testimonials")
])
def is_sequence_correct(expected, actual):
return expected == actual
print("Test 1 sequence correct:", is_sequence_correct(expected_sequence, actual_sequence_test))
# False — l'ordre de change1 et change2 est inversé
Backward compatibility
Before Python 3.7, ordinary dictionaries did not guarantee insertion order. If you are maintaining legacy codebases, OrderedDict guarantees the expected behavior.
3.3 Counter
3.3.1 Understanding Counter
Counter is a subclass of dict specially designed for counting hashable objects. The elements are stored as dictionary keys, and their frequency (count) is the value.
from collections import Counter
print(issubclass(Counter, dict)) # True
# Initialisation depuis une liste
letters = ["a", "b", "c", "a", "c", "a", "b", "c"]
letter_counter = Counter(letters)
print("Counter from a list:\t", letter_counter)
# Initialisation depuis une chaîne
string_letter_counter = Counter("banana")
print("Counter from a string:\t", string_letter_counter)
# Initialisation avec des comptes explicites
dict_letter_counter = Counter({"a": 4, "b": 2, "c": -1})
print("Initialized Counter:\t", dict_letter_counter)
Element access
fruits = ["apple", "banana", "cherry", "apple", "cherry"]
fruit_counter = Counter(fruits)
print(fruit_counter["apple"]) # 2
print(fruit_counter["pear"]) # 0 (pas d'erreur KeyError)
Updating accounts
letters = ["a", "b", "c", "a", "c", "a", "b", "c"]
letter_counter = Counter(letters)
print("Original counter:\t\t\t\t", letter_counter)
# Mise à jour avec une chaîne ou une liste
letter_counter.update("aa")
letter_counter.update(["c", "c"])
print("Updated counter with strings and lists:\t\t", letter_counter)
# Mise à jour avec un dictionnaire ou des keyword arguments
letter_counter.update({"b": 3})
letter_counter.update(a=2, b=2, c=2)
print("Updated counter with dictionaries and kw args:\t", letter_counter)
# Soustraction
letter_counter.subtract(a=7, b=6, c=8)
print("Subtracted counter:\t\t\t\t", letter_counter)
# Réinitialisation
letter_counter.clear()
print("Reset Counter:\t\t\t\t\t", letter_counter)
Counter Operators
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
# Addition (ne garde que les comptes positifs)
print("Addition:\t\t", c1 + c2)
# Soustraction (ne garde que les comptes positifs)
print("Subtraction:\t\t", c1 - c2)
# Intersection (minimum des comptes)
print("Intersection (min):\t", c1 & c2)
# Union (maximum des comptes)
print("Union (max):\t\t", c1 | c2)
# Opérations unaires (zéro toujours exclu)
print("Positive counts:\t", +c1) # Comptes > 0
print("Negative counts:\t", -Counter(a=1, b=-2)) # Comptes < 0
print("c1 == c2", c1 == c2)
Counter Methods
c = Counter(a=2, b=3, c=4)
# Reconstruction de la séquence originale (sans l'ordre original)
print("c.elements():\t\t", list(c.elements()))
# Liste des éléments les plus communs
print("c.most_common():\t", c.most_common())
print("c.most_common(2):\t", c.most_common(2))
# Total de tous les comptes
print("c.total():\t\t", c.total())
3.3.2 Counter use cases
Word frequency counting
from collections import Counter
text = "a quick brown fox jumps over the lazy dog"
words = text.split()
word_counts = Counter(words)
print(word_counts)
Find the most common elements
visits = ["home", "about", "contact", "home", "about", "home",
"profile", "home", "about", "contact"]
visit_counts = Counter(visits)
most_visited = visit_counts.most_common(2)
least_visited = visit_counts.most_common()[:-3:-1]
print("Most visited pages:", most_visited)
print("Least visited pages:", least_visited)
Inventory management
from collections import Counter
inventory_a = Counter(apples=3, oranges=2)
inventory_b = Counter(apples=1, bananas=2, oranges=1)
# Combiner les inventaires
total_inventory = inventory_a + inventory_b
print("Total inventory:\t\t", total_inventory)
# Articles vendus
sold_items = Counter(apples=2, bananas=1)
remaining_inventory = total_inventory - sold_items
print("Inventory after the sale:\t", remaining_inventory)
Analysis of voting results
votes = ["CandidateA", "CandidateB", "CandidateB", "CandidateA",
"CandidateB", "CandidateB", "CandidateC"]
vote_counts = Counter(votes)
print(vote_counts)
Implementation of multisets (first factorization)
A multiset (or bag) is a modification of the concept of a set that allows multiple instances of the same element. Counter is essentially a multiset implementation.
from collections import Counter
import math
def prime_factorization(n):
factors = Counter()
divisor = 2
while divisor**2 <= n:
while n % divisor == 0:
factors[divisor] += 1
n //= divisor
divisor += 1
if n > 1:
factors[n] += 1
return factors
n = 2376
prime_factors = prime_factorization(n)
factors_str = f"{n} = " + " x ".join([f"{factor}^{power}"
for factor, power in prime_factors.items()])
print(prime_factors)
print("Prime factorization:", factors_str)
# Recalculer le nombre à partir des facteurs premiers
number = math.prod(prime_factors.elements())
print("Number:", number)
3.4 Module 3 Summary
defaultdict: subclass ofdictthat returns a default value for missing keys. The default value is specified by thefactory functionprovided at creation. Eliminates the need to check for the existence of keys to initialize lists or counters.OrderedDict: subclass ofdictwhich remembers the insertion order. Useful for order-aware comparisons and maintaining old codebases. Offersmove_to_end()and apopitem()capable of removing from both ends.Counter: subclass ofdictdesigned to count hashable objects. Particularly useful for frequency analysis and algorithms requiring counting.
4. Using Specialized Collections Classes
4.1 namedtuple()
4.1.1 Understanding namedtuple()
namedtuple() is a factory function of the collections module which returns a new tuple subclass with named fields. It combines the immutability of tuples with the readability of attribute access.
Distinction between class and alias
class Point:
pass
Point2 = Point
print(Point.__name__) # Point
print(Point2.__name__) # Point aussi — Point2 est juste un alias
Creating a namedtuple
from collections import namedtuple
# La factory function retourne une nouvelle classe nommée "Pixel"
Pixel = namedtuple("Pixel", "red green blue")
# Syntaxes équivalentes :
# Pixel = namedtuple("Pixel", "red, green, blue")
# Pixel = namedtuple("Pixel", ["red", "green", "blue"])
# Pixel = namedtuple("Pixel", (field for field in ["red", "green", "blue"]))
# Instanciation
pixel = Pixel(red=255, green=50, blue=0)
print(pixel)
# Obtenir les noms des champs
print(Pixel._fields)
Element access
from collections import namedtuple
Pixel = namedtuple("Pixel", "red green blue")
pixel = Pixel(red=255, green=50, blue=0)
print("Accessing values by indices:")
print(pixel[0])
print(pixel[1])
print(pixel[2])
print("Accessing values by field names with the dot syntax:")
print(pixel.red)
print(pixel.green)
print(pixel.blue)
Optional arguments of namedtuple()
from collections import namedtuple
# L'argument 'rename' : renomme automatiquement les champs invalides
user_fields = ["username", "_password", "username", "as"]
User = namedtuple("User", user_fields, rename=True)
print("The 'rename' argument")
print("User._fields:", User._fields)
# L'argument 'defaults' : valeurs par défaut pour les derniers champs
Dog = namedtuple("Dog", ["name", "age", "location"], defaults=[0, "Home"])
dog = Dog("Balto")
print("\nThe 'defaults' argument")
print("dog:", dog)
print("dog._field_defaults:", dog._field_defaults)
# L'argument 'module' : définit __module__ de la classe
Item = namedtuple("Item", ["name"], module="my_module")
print("\nThe 'module' argument")
print("Item.__module__:", Item.__module__)
Construction from iterables with _make()
from collections import namedtuple
Pixel = namedtuple("Pixel", "red green blue")
image_pixel_data = [
[255, 43, 22],
[230, 44, 23],
[230, 44, 23]
]
sprite = [Pixel._make(pixel) for pixel in image_pixel_data]
print(sprite)
Conversion to and from dictionary
from collections import namedtuple
Pixel = namedtuple("Pixel", "red green blue")
# Depuis un dictionnaire avec le déballage (**)
pixel = Pixel(**{"red": 255, "green": 50, "blue": 0})
print(pixel)
# Vers un dictionnaire
print(pixel._asdict())
Updating fields with _replace()
As namedtuples are immutable, _replace() creates a new instance with the modified fields.
from collections import namedtuple
Dog = namedtuple("Dog", ["name", "age", "location"])
dog = Dog("Hachiko", 11, "Shibuya Station")
dog = dog._replace(name="Scooby-Don't")
print(dog)
4.1.2 Use cases of namedtuple()
Immutable container with named fields
Namedtuples improve code readability. Compared to (mutable) data classes and dictionaries, namedtuples offer better performance and a reduced memory footprint.
from collections import namedtuple
City = namedtuple("City", ["name", "latitude", "longitude"])
cities = [
City("New York", 40.7128, -74.0060),
City("Los Angeles", 34.0522, -118.2437),
City("Chicago", 41.8781, -87.6298),
]
def find_city_by_name(city_name):
for city in cities:
if city.name == city_name:
return city
return None
found_city = find_city_by_name("Chicago")
if found_city:
print(f"The coordinates of {found_city.name} are "
f"({found_city.latitude}, {found_city.longitude})")
Using typing.NamedTuple for type hints and custom methods
from typing import NamedTuple
class City(NamedTuple):
"""
Represents a city with a name and its geographic coordinates.
Attributes:
name (str): The name of the city.
latitude (float): The latitude of the city.
longitude (float): The longitude of the city.
"""
name: str
latitude: float
longitude: float
def __str__(self):
return (f"The city of {self.name} can be found at "
f"({self.latitude}, {self.longitude}) coordinates.")
city = City("New York", 40.7128, -74.0060)
print(city)
Reduce the number of parameters of a function
from collections import namedtuple
CustomerInfo = namedtuple("CustomerInfo",
["id", "first_name", "last_name", "email",
"address", "city", "state", "zip_code"])
def process_customer_info(customer_info):
print(f"Processing {customer_info.first_name} {customer_info.last_name} "
f"living in {customer_info.city}, {customer_info.state}.")
customer = CustomerInfo(1, "X", "Y", "x.y@example.com",
"123 Elm St", "Anytown", "Anystate", "12345")
process_customer_info(customer)
Return a namedtuple from a function
from collections import namedtuple
FinancialStats = namedtuple("FinancialStats",
["average_expense", "total_expense", "highest_expense"])
def calculate_financial_stats(expenses):
total_expense = sum(expenses)
average_expense = total_expense / len(expenses)
highest_expense = max(expenses)
return FinancialStats(average_expense, total_expense, highest_expense)
expenses = [250, 320, 150, 400, 500]
stats = calculate_financial_stats(expenses)
print(f"Average Expense: ${stats.average_expense:.2f}")
print(f"Total Expense: ${stats.total_expense}")
print(f"Highest Expense: ${stats.highest_expense}")
4.2 ChainMap
4.2.1 Understanding ChainMap
ChainMap is a class that allows you to group several mappings (dictionaries) into a single logical view. It is particularly useful for managing multiple scopes and creates an updated view that acts as an access interface to multiple linked dictionaries.
ChainMap does not merge dictionaries — it stores references to those dictionaries in an internal list.
Creating a ChainMap
from collections import ChainMap
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
chain_map = ChainMap(dict1, dict2)
print(chain_map)
# Modifier les dictionnaires originaux affecte le ChainMap
dict1["a"] = 20
print(chain_map)
Access to values
from collections import ChainMap
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4, "d": 5}
chain_map = ChainMap(dict1, dict2)
print(chain_map)
print("chain_map['a']:", chain_map["a"]) # Depuis dict1
print("chain_map['b']:", chain_map["b"]) # Depuis dict1 (priorité sur dict2)
print("chain_map['c']:", chain_map["c"]) # Depuis dict2 (absent de dict1)
Keys from first dictionary take precedence. If a key exists in several dictionaries, the value of the first is returned.
Mutations
from collections import ChainMap
dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)
# Les mises à jour affectent le premier mapping
cm["a"] = 100
cm["b"] = 200
# Ajouter une nouvelle clé va dans le premier mapping
cm["c"] = 400
# Seules les suppressions depuis le premier mapping sont possibles
cm.pop("z")
# clear() vide également seulement le premier mapping
print(cm)
Addition and deletion of the first mapping
from collections import ChainMap
dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)
# new_child() retourne un nouveau ChainMap avec le nouveau mapping en tête
dict3 = {"e": 6, "f": 7}
cm2 = cm.new_child(dict3)
print(cm2)
# parents retourne un nouveau ChainMap sans le premier mapping
cm3 = cm2.parents
print(cm3)
The maps attribute
from collections import ChainMap
dict1 = {"a": 1, "b": 2, "z": 10}
dict2 = {"b": 3, "c": 4, "d": 5}
cm = ChainMap(dict1, dict2)
print("cm.maps", cm.maps)
# Inverser l'ordre des mappings
cm.maps.reverse()
print("cm.maps", cm.maps)
# Supprimer le dernier dictionnaire
cm.maps.pop()
print("cm.maps", cm.maps)
Iteration
from collections import ChainMap
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4, "d": 5}
chain_map = ChainMap(dict1, dict2)
# L'itération se fait du dernier mapping vers le premier
for key, value in chain_map.items():
print(key, value)
# Même ordre pour .keys() et .values()
4.2.2 ChainMap use cases
Application configuration with priorities
from collections import ChainMap
import os
# Configuration par défaut
default_config = {"theme": "Default", "language": "English", "show_ads": True}
# Les variables d'environnement ont la priorité sur les paramètres par défaut
env_config = os.environ
app_config = ChainMap(env_config, default_config)
print("Theme:", app_config["theme"])
# La configuration utilisateur a la priorité maximale
user_config = {"theme": "Dark Mode", "show_ads": False}
app_config = app_config.new_child(user_config)
print("\nAfter adding the user config")
print("Theme:", app_config["theme"])
print("Language:", app_config["language"])
print("Show Ads:", app_config["show_ads"])
CLI argument handling
import argparse
from collections import ChainMap
def main():
parser = argparse.ArgumentParser(description="CLI tool example with ChainMap")
parser.add_argument("--output", type=str, help="Output file name")
parser.add_argument("--verbose", action="store_true", help="Enable verbose mode")
parser.add_argument("--mode", type=str, help="Set the mode of operation")
# Simulation d'arguments en ligne de commande
args = parser.parse_args(["--output", "output_from_cli.log", "--verbose"])
default_settings = {
"output": "default.log",
"verbose": False,
"mode": "normal"
}
# Conversion de l'espace de noms args en dictionnaire (sans les None)
cli_arguments = {k: v for k, v in vars(args).items() if v is not None}
settings = ChainMap(cli_arguments, default_settings)
print("Output file:", settings["output"])
print("Verbose mode:", ("enabled" if settings["verbose"] else "disabled"))
print("Mode:", settings["mode"])
if __name__ == "__main__":
main()
Scope resolution in interpreters
from collections import ChainMap
global_scope = {"x": 2, "y": 3}
local_scope = {"y": 5}
# La portée locale a la priorité sur la portée globale
current_env = ChainMap(local_scope, global_scope)
print("x:", current_env["x"]) # Depuis global_scope
print("y:", current_env["y"]) # La portée locale surpasse le global
# Ajout d'une variable dans la portée globale
current_env.parents["z"] = 100
print("z:", current_env["z"])
print(current_env)
4.3 deque
4.3.1 Understanding deque
deque (pronounced “deck”) is short for double-ended queue. This is a generalization of stacks and queues. This collection is implemented as a doubly linked list, where each element contains a reference to the next and previous element. This allows elements to be added and removed from both ends in O(1).
Creating a deque
from collections import deque
# Deque vide
dq = deque()
print(dq)
# Depuis un tuple
dq = deque((1, 2, 3))
print(dq)
# Depuis une liste
dq = deque([1, 2, 3])
print(dq)
# Depuis un dictionnaire view
dict1 = {"a": 1, "b": 2, "c": 3}
dq = deque(dict1.items())
print(dq)
Add and delete from both ends
from collections import deque
dq = deque([1])
dq.append(2) # Ajout à droite
dq.appendleft(0) # Ajout à gauche
dq.append(3) # Ajout à droite
print(dq)
dq.pop() # Suppression à droite
popped_el = dq.popleft() # Suppression à gauche
print("Popped element from the left:", popped_el)
print(dq)
Performance comparison: deque.appendleft() vs list.insert(0, ...)
import timeit
# Ajout à gauche dans une liste — O(n)
list_time = timeit.timeit(
'for i in range(10000): lst.insert(0, i)',
setup='lst = []',
number=10
)
# Ajout à gauche dans un deque — O(1)
deque_time = timeit.timeit(
'for i in range(10000): deq.appendleft(i)',
setup='from collections import deque; deq = deque()',
number=10
)
print(f"List left-append time: {list_time}")
print(f"Deque left-append time: {deque_time}")
Accessing, deleting and inserting elements
from collections import deque
numbers = deque([1, 2, 3, 4, 5, 6])
# Accès par index — O(n)
print("numbers[5]:", numbers[5])
# Suppression avec del
del numbers[5]
# Insertion à une position spécifique
numbers.insert(1, 10)
# Suppression d'un élément par valeur
numbers.remove(5)
print(numbers)
bounded deque
from collections import deque
numbers = deque([0, 1, 2, 3], maxlen=5)
print("Maxlen:", numbers.maxlen)
print(numbers)
numbers.appendleft(-1)
print("After numbers.appendleft(-1):\t", numbers) # [-1, 0, 1, 2, 3]
numbers.append(4) # Discarde -1
print("After numbers.append(4):\t", numbers) # [0, 1, 2, 3, 4]
numbers.append(5) # Discarde 0
print("After numbers.append(5):\t", numbers) # [1, 2, 3, 4, 5]
numbers.appendleft(0) # Discarde 5
print("After numbers.appendleft(0):\t", numbers) # [0, 1, 2, 3, 4]
Special deque methods
from collections import deque
letters = deque(["a", "b", "c"])
print(letters)
# Rotation d'un pas vers la droite
letters.rotate()
print("letters.rotate():\t", letters) # deque(['c', 'a', 'b'])
# Rotation de deux pas vers la droite
letters.rotate(2)
print("letters.rotate(2):\t", letters) # deque(['a', 'b', 'c'])
# Rotation d'un pas vers la gauche
letters.rotate(-1)
print("letters.rotate(-1):\t", letters) # deque(['b', 'c', 'a'])
# Ajout de plusieurs éléments à droite
letters.extend(["d", "e"])
# Ajout de plusieurs éléments à gauche
letters.extendleft(["x", "y"])
print(letters)
4.3.2 Use cases of deque
Maintaining a list of recent items
from collections import deque
recent_items = deque(maxlen=3)
for i in range(5):
recent_items.append(i)
print(f"Item {i} added, recent items: {list(recent_items)}")
Implementing a queue (FIFO)
from collections import deque
import time
def process_task(task):
print("Processing task:", task)
time.sleep(0.5)
task_queue = deque()
# Simulation d'ajout de tâches
for i in range(1, 5):
task_queue.append(f"task_{i}")
# Traitement des tâches (FIFO — First In, First Out)
while task_queue:
current_task = task_queue.popleft()
process_task(current_task)
Implementing a stack (LIFO)
from collections import deque
class BrowserHistory:
def __init__(self):
self.pages = deque(maxlen=5)
def visit_page(self, page_url):
"""Visit a new page and add it to the history."""
self.pages.append(page_url)
print("Visiting:", page_url)
def go_back(self):
"""Go back to the previous page."""
if self.pages:
current_page = self.pages.pop()
print("Going back from:", current_page)
if self.pages:
print("Current page is now:", self.pages[-1])
else:
print("No more pages in history.")
else:
print("No pages in history.")
browser_history = BrowserHistory()
browser_history.visit_page("home.html")
browser_history.visit_page("about.html")
browser_history.visit_page("contact.html")
browser_history.go_back() # Retour de contact.html
browser_history.go_back() # Retour de about.html
browser_history.go_back() # Plus de pages
Schedule rotation
from collections import deque
schedule = deque(["Alice", "Bob", "Charlie"])
for week in range(1, 5):
print(f"Week {week} schedule: {list(schedule)}")
schedule.rotate(1)
Processing from both ends
from collections import deque
class RestaurantWaitlist:
def __init__(self):
self.waitlist = deque()
def arrive(self, name, vip=False):
if vip:
self.waitlist.appendleft(name)
print(f"VIP customer {name} added to the front of the waitlist.")
else:
self.waitlist.append(name)
print(f"Customer {name} added to the end of the waitlist.")
def seat_customer(self):
if self.waitlist:
name = self.waitlist.popleft()
print(f"Customer {name} is now being seated.")
else:
print("The waitlist is currently empty.")
waitlist = RestaurantWaitlist()
waitlist.arrive("A")
waitlist.arrive("B")
waitlist.arrive("C", vip=True) # Client VIP — passe en premier
waitlist.arrive("D")
waitlist.seat_customer() # C en premier (VIP)
waitlist.seat_customer() # Puis A (FIFO)
Implementation of a sliding window
from collections import deque
def moving_average(temperatures, n=5):
it = iter(temperatures)
window = deque(maxlen=n)
for temperature in it:
window.append(temperature)
if len(window) == n:
yield sum(window) / n
temperatures = [22, 21, 23, 26, 24, 25, 27, 28, 22, 19, 20, 18]
print("5-hour Moving Average:", list(moving_average(temperatures)))
4.4 Summary of Module 4
namedtuple(): factory function returning a tuple subclass with named fields. Combines the immutability of tuples with the readability of attribute access. Offers better performance and a reduced memory footprint compared to data classes and dictionaries.ChainMap: groups several dictionaries into a single view. Particularly useful for applications requiring multiple dictionaries representing scopes or contexts, such as in templating languages or variable resolution.deque: thread-safe and memory-efficient double-sided queue, allowing addition and deletion from both ends in O(1). Ideal for implementing queues (FIFO) and stacks (LIFO).
5. Customizing built in data types
The collections module provides three classes — UserString, UserList and UserDict — which are wrappers around the built-in types str, list and dict. Their main purpose was to allow subclassing of built-in Python types before Python 2.2. Since this version, it is possible to directly subclass built-in types.
These classes are all implemented in pure Python, making them slower than native types implemented in C.
5.1 UserString
UserString is a wrapper around the built-in str type. The content is stored in a data attribute which is a real instance of str.
Extend str functionality
from collections import UserString
class PalindromeString(UserString):
def is_palindrome(self):
cleaned = ''.join(filter(str.isalnum, self.data.lower()))
return cleaned == cleaned[::-1]
class StrPalindromeString(str):
def is_palindrome(self):
cleaned = ''.join(filter(str.isalnum, self.lower()))
return cleaned == cleaned[::-1]
str1 = PalindromeString("A man, a plan, a canal, Panama")
str2 = StrPalindromeString("A man, a plan, a canal, Panama")
print(str1.is_palindrome()) # True
print(str2.is_palindrome()) # True
Change str functionality
from collections import UserString
# Rendre les opérateurs de comparaison insensibles à la casse
class CIString(UserString):
def __eq__(self, other):
return self.data.lower() == other.lower()
def __lt__(self, other):
return self.data.lower() < other.lower()
def __gt__(self, other):
return self.data.lower() > other.lower()
str1 = CIString("ABCD")
str2 = "abCD"
str3 = "Abcd"
print(str1 == str2) # True
print(str2 == str3) # True (avec CIString, sinon False)
This modification also works with the str class directly.
When UserString is preferable to str
UserString is particularly useful when we need to modify the behavior during initialization. The reason: str is an immutable class implemented in C, and its creation is handled by __new__() rather than __init__().
from collections import UserString
from urllib.parse import quote_plus
class URLEncodedString(UserString):
def __init__(self, string):
encoded = quote_plus(string)
super().__init__(encoded)
book_title = "Moby-Dick; or, The Whale"
encoded_title = URLEncodedString(book_title)
search_url = f"https://example.com/search?q={encoded_title}"
print(search_url)
The same implementation with str directly would produce an error, because str.__init__() is not the correct place to modify the value.
5.2 UserList
UserList is a wrapper around the built-in list type. Like UserString, its content is stored in a data attribute.
Subclassing of list vs UserList
# Implémentation avec list
class UniqueList(list):
def __init__(self, iterable=[]):
super().__init__()
self.extend(iterable)
def append(self, item):
if item not in self:
super().append(item)
def extend(self, iterable):
for item in iterable:
self.append(item)
unique_list = UniqueList([1, 2, 2, 3, 4, 4, 4])
print(unique_list)
unique_list.append(5)
unique_list.append(5) # Doublon ignoré
print(unique_list)
unique_list.extend([6, 6, 7])
print(unique_list)
# Implémentation avec UserList
from collections import UserList
class UniqueList(UserList):
def __init__(self, iterable=[]):
super().__init__()
self.extend(iterable)
def append(self, item):
if item not in self.data: # Utiliser self.data
super().append(item)
def extend(self, iterable):
for item in iterable:
self.append(item)
unique_list = UniqueList([1, 2, 2, 3, 4, 4, 4])
print(unique_list)
unique_list.append(5)
unique_list.append(5)
print(unique_list)
unique_list.extend([6, 6, 7])
print(unique_list)
The only difference is the use of self.data instead of self. In most cases, UserList offers no real advantage over directly subclassing list, other than backwards compatibility.
5.3 UserDict
UserDict is the only one of the three wrappers that offers any real practical utility over direct subclassing of the built-in type.
The reason: the dict class is implemented in C, and its __init__() and update() methods do not use __setitem__() internally. As a result, overriding __setitem__() in a subclass of dict does not capture all updates. UserDict, on the other hand, is implemented in pure Python and uses __setitem__() for all update operations.
Comparison: subclassing of UserDict vs dict
# Avec UserDict — comportement attendu
from collections import UserDict
class StringDict(UserDict):
def __setitem__(self, key, value):
if not isinstance(value, str):
raise TypeError(f"Value must be of type string")
super().__setitem__(key, value)
d = StringDict({"a": "1"})
d["b"] = "2"
d.update({"c": "3"})
print(d)
# Avec dict — comportement inattendu
class StringDict(dict):
def __setitem__(self, key, value):
if not isinstance(value, str):
raise TypeError(f"Value must be of type string")
super().__setitem__(key, value)
d = StringDict({"a": 1}) # Pas d'erreur ! __init__ n'appelle pas __setitem__
d["b"] = "2" # Pas d'erreur
d.update({"c": 3}) # Pas d'erreur ! update() n'appelle pas __setitem__
print(d)
With dict, to obtain the same behavior, it would also be necessary to override __init__() and update().
Conclusion on UserDict: If you need to create a simple dictionary class that overloads the way updates are performed, and performance is not a priority, UserDict is the best choice.
5.4 Summary of Module 5
The UserString, UserList and UserDict classes of the collections module allow you to override the behavior of built-in Python types. These are all wrappers around native classes, implemented in pure Python (therefore slower).
| Class | Main advantage | Recommendation |
|---|---|---|
UserString | Allows you to modify the behavior during initialization (__init__) | Useful when str.__new__ is a problem |
UserList | No real advantage vs subclassing of list | Use for backwards compatibility only |
UserDict | __setitem__ is used for all update operations | Recommended for simply overriding dictionary updates |
6. General conclusion
Python’s collections module extends the capabilities of built-in data types to meet more specific and complex needs. Here is a summary of the classes covered:
| Class | Description | Main use case |
|---|---|---|
list | Ordered and mutable collection | Prototyping, dynamic sequences |
dict | Ordered and mutable key-value pairs | Configuration, dispatch, memorization |
tuple | Ordered and immutable sequence | Fixed data, multiple return, records |
set | Unordered collection of unique elements | Deduplication, fast membership tests |
defaultdict | dict with automatic default | Grouping, counting, accumulation |
OrderedDict | dict with order comparison | Ordered sequences, backwards compatibility |
Counter | frequency counting dict | NLP, data analysis, inventories |
namedtuple() | Tuple with named fields | Readable immutable records, replacing simple tuples |
ChainMap | Combined view of several mappings | Scope management, configuration with priorities |
deque | Double-sided thread-safe tail | FIFO queues, LIFO stacks, sliding windows |
UserString | Wrap around str | Customizing str with __init__ |
UserList | Wrap around list | Backward Compatibility |
UserDict | Wrap around dict | Own overload of __setitem__ |
Mastering these data structures will allow you to write Python code that is more elegant, more efficient and more idiomatic, always choosing the structure best suited to the problem at hand.
Search Terms
python · collections · foundations · data · analysis · engineering · analytics · cases · dictionaries · lists · chainmap · counter · defaultdict · deque · namedtuple · ordereddict · methods · sets · tuples · types