23. Fonctions de première classe et techniques fonctionnelles
Dans les chapitres précédents, nous avons appris à définir et appeler des fonctions (functions), à travailler avec des paramètres et des arguments, et à comprendre la portée des variables. Maintenant, nous allons explorer une fonctionnalité puissante qui distingue Python : les fonctions sont des objets de première classe (first-class objects). Cela signifie que les fonctions peuvent être traitées comme n’importe quelle autre valeur — stockées dans des variables, passées en arguments à d’autres fonctions et renvoyées par des fonctions.
Cette capacité ouvre la voie à des techniques de programmation élégantes qui rendent le code plus flexible, réutilisable et expressif. Nous verrons comment tirer parti des fonctions de première classe à travers des exemples pratiques, comprendre les fermetures (closures) (des fonctions qui « se souviennent » de leur environnement), utiliser les expressions lambda (lambda expressions) pour des définitions de fonctions concises, et appliquer des fonctions intégrées comme map(), filter(), any() et all() pour travailler efficacement avec des collections.
23.1) Les fonctions comme objets de première classe
23.1.1) Ce que signifie « première classe »
En Python, les fonctions sont des objets de première classe (first-class objects), ce qui signifie qu’elles peuvent :
- Être affectées à des variables
- Être stockées dans des structures de données (listes, dictionnaires, etc.)
- Être passées en arguments à d’autres fonctions
- Être renvoyées comme valeurs par d’autres fonctions
Cela diffère de certains langages de programmation où les fonctions ont un statut particulier et ne peuvent pas être manipulées comme des valeurs ordinaires. En Python, une fonction est simplement un autre type d’objet, similaire aux entiers, aux chaînes de caractères ou aux listes.
Voyons cela en action :
# Définir une fonction simple
def greet(name):
return f"Hello, {name}!"
# Affecter la fonction à une variable
say_hello = greet
# Appeler la fonction via la nouvelle variable
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# Vérifier que les deux noms font référence à la même fonction
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: TrueRemarquez que lorsque nous écrivons say_hello = greet, nous n’appelons pas la fonction (pas de parenthèses). Nous créons un nouveau nom qui fait référence au même objet fonction. greet et say_hello pointent maintenant vers la même fonction, ce que nous pouvons vérifier avec l’opérateur is.
23.1.2) Stocker des fonctions dans des structures de données
Puisque les fonctions sont des objets, nous pouvons les stocker dans des listes, des dictionnaires ou toute autre collection :
# Calculatrice avec des opérations stockées dans un dictionnaire
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# Stocker les fonctions dans un dictionnaire
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# Utiliser le dictionnaire pour effectuer des calculs
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50Ce modèle est extrêmement utile pour construire des systèmes flexibles. Au lieu d’écrire de longues chaînes d’instructions if-elif pour choisir quelle fonction appeler, nous pouvons rechercher la fonction appropriée dans un dictionnaire et l’appeler directement.
23.2) Passer des fonctions en arguments
23.2.1) Le concept de base
L’une des utilisations les plus puissantes des fonctions de première classe consiste à les passer en arguments à d’autres fonctions. Cela nous permet d’écrire du code flexible et réutilisable qui peut fonctionner avec des comportements différents.
Voici un exemple simple :
# Fonction qui applique une autre fonction à une valeur
def apply_operation(value, operation):
"""Appliquer la fonction operation reçue en paramètre à la valeur."""
return operation(value)
# Différentes opérations
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# Utiliser la même fonction apply_operation avec différentes opérations
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5La fonction apply_operation ne sait pas et ne se soucie pas de l’opération spécifique qu’elle effectue. Elle appelle simplement la fonction qui lui est passée. Cette séparation des responsabilités rend le code plus modulaire et plus facile à étendre.
23.2.2) Traiter des collections avec des fonctions personnalisées
Un modèle courant consiste à traiter chaque élément d’une collection à l’aide d’une fonction passée en argument :
# Traiter chaque élément d'une liste en utilisant une fonction donnée
def process_list(items, processor):
"""Appliquer la fonction processor à chaque élément de la liste."""
results = []
for item in items:
results.append(processor(item))
return results
# Différentes fonctions de traitement
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# Traiter la même liste de différentes façons
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]Ce modèle est si utile que Python fournit des fonctions intégrées comme map() et filter() qui fonctionnent de cette manière (nous explorerons celles-ci dans la section 23.6).
23.2.3) Trier en fournissant une fonction clé (brève introduction)
La fonction sorted() de Python accepte un paramètre key — une fonction qui détermine comment comparer les éléments :
# Trier des étudiants selon différents critères
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# Fonction pour extraire la note
def get_grade(student):
return student["grade"]
# Fonction pour extraire le nom
def get_name(student):
return student["name"]
# Trier par note (croissant)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# Trier par nom (alphabétique)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95La fonction key est appelée une fois pour chaque élément, et sa valeur de retour est utilisée pour la comparaison. C’est beaucoup plus flexible que de devoir écrire une logique de tri personnalisée.
Ce modèle consistant à passer des fonctions pour personnaliser un comportement est extrêmement courant en Python. Nous explorerons des techniques de tri plus avancées au chapitre 38.
23.3) Renvoyer des fonctions depuis des fonctions
23.3.1) Des fonctions qui créent des fonctions
Tout comme nous pouvons passer des fonctions en arguments, nous pouvons aussi renvoyer des fonctions depuis d’autres fonctions. Cela nous permet de créer dynamiquement des fonctions spécialisées :
# Fonction qui crée et renvoie une nouvelle fonction
def create_multiplier(factor):
"""Créer une fonction qui multiplie par le facteur donné."""
def multiplier(x):
return x * factor
return multiplier
# Créer des fonctions multiplicatrices spécialisées
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# Utiliser les fonctions créées
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50Que se passe-t-il ici ? La fonction create_multiplier définit une fonction interne appelée multiplier et la renvoie. Chaque fois que nous appelons create_multiplier avec un facteur différent, nous récupérons une nouvelle fonction qui « se souvient » de ce facteur spécifique. C’est notre premier aperçu des fermetures (closures), que nous explorerons en profondeur dans la section suivante.
23.3.2) Créer des validateurs personnalisés
Renvoyer des fonctions est particulièrement utile pour créer des fonctions de validation ou de traitement personnalisées :
# Créer dynamiquement des validateurs de plage
def create_range_validator(min_value, max_value):
"""Créer une fonction qui valide si un nombre est dans l'intervalle."""
def validator(number):
return min_value <= number <= max_value
return validator
# Créer des validateurs spécifiques
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# Utiliser les validateurs
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) Comprendre les closures : des fonctions qui se souviennent
23.4.1) Qu’est-ce qu’une closure ?
Une fermeture (closure) est une fonction qui « se souvient » des variables de la portée (scope) où elle a été créée, même après que cette portée a fini de s’exécuter. Dans les exemples de la section 23.3, nous avons déjà utilisé des closures sans les nommer explicitement.
Examinons comment les closures fonctionnent :
def create_counter(start=0):
"""Créer une fonction compteur qui se souvient de son compte."""
count = start # Cette variable est « capturée » par la closure
def counter():
nonlocal count # Accéder à la variable capturée
count += 1
return count
return counter
# Créer deux compteurs indépendants
counter1 = create_counter(0)
counter2 = create_counter(100)
# Chaque compteur maintient son propre compte
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)La fonction counter interne forme une closure sur la variable count. Même si create_counter a fini de s’exécuter, la fonction counter renvoyée a toujours accès à count. Chaque appel à create_counter crée une nouvelle closure indépendante avec sa propre variable count.
23.4.2) Comment les closures capturent les variables
Lorsqu’une fonction est définie à l’intérieur d’une autre fonction, elle peut accéder aux variables de la portée de la fonction externe. Ces variables sont « capturées » et restent accessibles même après que la fonction externe a renvoyé :
Lorsque Python crée la fonction interne, il ne sauvegarde pas seulement le code de la fonction — il sauvegarde aussi des références vers toutes les variables de la fonction externe que la fonction interne utilise. Ce processus s’appelle la « capture » des variables.
def create_greeter(greeting):
"""Créer une fonction de salutation avec une salutation personnalisée."""
def greet(name):
return f"{greeting}, {name}!"
return greet
# Créer différents saluteurs
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# Chaque saluteur se souvient de sa salutation spécifique
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!Le paramètre greeting est capturé par la closure. Chaque fonction de salutation possède sa propre valeur greeting capturée qu’elle utilise à chaque appel.
23.4.3) Utilisation pratique : des fonctions de configuration
Les closures sont excellentes pour créer des fonctions avec un comportement préconfiguré :
# Créer des calculateurs de prix avec différents taux de taxe
def create_price_calculator(tax_rate):
"""Créer un calculateur qui applique un taux de taxe spécifique."""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# Créer des calculateurs pour différentes régions
us_calculator = create_price_calculator(0.07) # Taxe de 7%
uk_calculator = create_price_calculator(0.20) # TVA de 20%
japan_calculator = create_price_calculator(0.10) # Taxe à la consommation de 10%
# Calculer les prix dans différentes régions
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) Quand utiliser des closures
Les closures sont particulièrement utiles lorsque vous devez :
- Créer des fonctions avec un comportement préconfiguré
- Conserver un état entre des appels de fonction sans utiliser de classes
- Implémenter des fonctions de rappel(callback functions) qui doivent se souvenir du contexte
- Créer des fabriques de fonctions(function factories) qui produisent des fonctions spécialisées
23.5) Utiliser lambda pour de courtes fonctions anonymes
23.5.1) Que sont les expressions lambda ?
Une expression lambda (lambda expression) crée une petite fonction anonyme — une fonction sans nom. Les expressions lambda sont utiles lorsque vous avez besoin d’une fonction simple pendant une courte durée et que vous ne voulez pas la définir formellement avec def.
La syntaxe est :
lambda parameters: expressionLa lambda prend des paramètres (comme une fonction classique) et renvoie le résultat de l’évaluation de l’expression. Voici un exemple simple :
# Fonction classique
def add(x, y):
return x + y
# Expression lambda équivalente
add_lambda = lambda x, y: x + y
# Les deux fonctionnent de la même manière
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8Les expressions lambda sont limitées à une seule expression — elles ne peuvent pas contenir d’instructions(statement) comme if, for ou plusieurs lignes de code. Cette limitation les garde simples et ciblées.
23.5.2) Les expressions lambda comme arguments
Les expressions lambda brillent lorsque vous devez passer une fonction simple en argument et que vous ne voulez pas définir une fonction nommée séparée :
# Trier des étudiants par note en utilisant lambda
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Au lieu de définir une fonction séparée :
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# Nous pouvons utiliser une lambda directement :
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95C’est plus concis lorsque la fonction est simple et n’est utilisée qu’une seule fois. La lambda lambda student: student["grade"] est équivalente à une fonction qui prend un étudiant et renvoie sa note.
23.5.3) Lambda avec plusieurs paramètres
Les expressions lambda peuvent prendre plusieurs paramètres, tout comme les fonctions classiques :
# Opérations de calculatrice en utilisant lambda
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# Utiliser les expressions lambda
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: ErrorRemarquez que nous pouvons utiliser une expression conditionnelle (x / y if y != 0 else "Error") dans une lambda, mais nous ne pouvons pas utiliser une instruction if (qui nécessiterait plusieurs lignes).
23.5.4) Quand utiliser lambda vs des fonctions nommées
Utilisez les expressions lambda lorsque :
- La fonction est très simple (une expression)
- La fonction n’est utilisée qu’une seule fois ou dans un contexte très localisé
- Définir une fonction nommée ajouterait une verbosité inutile
Utilisez une fonction nommée lorsque :
- La fonction est complexe ou nécessite plusieurs instructions(statements)
- La fonction sera réutilisée à plusieurs endroits
- La fonction a besoin d’un nom descriptif pour plus de clarté
- La fonction a besoin d’une docstring
23.5.5) Limitations de lambda et alternatives
Les expressions lambda ont des limitations importantes :
# ❌ Ceci ne fonctionnera pas - lambda ne peut pas contenir d'instructions
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ Utiliser plutôt une expression conditionnelle
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ Pour plusieurs opérations, utiliser une fonction classique
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10Les expressions lambda sont des outils pour des situations spécifiques. Lorsqu’elles rendent le code plus clair et plus concis, utilisez-les. Lorsqu’elles rendent le code plus difficile à comprendre, utilisez plutôt une fonction nommée classique.
23.6) Utiliser map() et filter() avec des fonctions simples
23.6.1) La fonction map()
La fonction map() applique une function donnée à chaque élément d’un itérable (comme une liste, un tuple ou une chaîne) et renvoie un itérateur contenant les résultats. C’est une manière de transformer chaque élément d’une collection sans écrire une boucle explicite.
map(function, iterable, *iterables)Paramètres :
function(obligatoire) : Une fonction qui prend un ou plusieurs arguments, les traite, et renvoie une valeur. La fonction est appelée une fois pour chaque élément dans le(s)iterable(s).iterable(obligatoire) : Une séquence (liste, tuple, chaîne, etc.) dont les éléments seront passés à lafunction.*iterables(optionnel) : Des itérables supplémentaires pour unefunctionà plusieurs arguments.
Si plusieurs itérables sont fournis, la fonction doit accepter autant d’arguments.
map() s’arrêtera lorsque l’itérable le plus court sera épuisé
Renvoie :
Un objet map (itérateur) contenant les résultats renvoyés par la function pour chaque élément d’entrée.
Important : l’objet map est un itérateur, pas une séquence comme une list.
# Doubler chaque nombre dans une liste
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# Appliquer double à chaque nombre
doubled = map(double, numbers)
result = list(doubled) # Convertir l'objet map (itérateur) en liste
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) Utiliser map() avec lambda
Les expressions lambda fonctionnent parfaitement avec map() pour des transformations simples :
# Convertir des températures de Celsius en Fahrenheit
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) La fonction filter()
La fonction filter() applique une function donnée à chaque élément d’un iterable et renvoie un itérateur contenant uniquement les éléments pour lesquels la fonction renvoie True. C’est une façon de sélectionner des éléments d’une collection sans écrire une boucle explicite.
filter(function, iterable)Paramètres :
function: Une fonction qui prend un argument, l’évalue, et renvoieTrueouFalse. La fonction est appelée une fois pour chaque élément dans l’iterable.iterable: Une séquence (liste, tuple, chaîne, etc.) dont les éléments seront testés par lafunction.
Renvoie :
Un objet filter (itérateur) contenant uniquement les éléments pour lesquels la function a renvoyé True.
Important : l’objet filter est un itérateur, pas une séquence comme une liste.
Exemple :
# Conserver uniquement les nombres pairs
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# Appliquer is_even à chaque nombre, ne garder que ceux qui renvoient True
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # Convertir l'objet filter en liste
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) Utiliser filter() avec lambda
Les expressions lambda sont couramment utilisées avec filter() pour un filtrage concis :
# Filtrer les étudiants qui ont réussi (grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) Combiner map() et filter()
Vous pouvez enchaîner des opérations map() et filter() pour effectuer des transformations complexes :
# Obtenir les carrés des nombres pairs
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# D'abord filtrer les nombres pairs, puis calculer leur carré
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]Comparaison visuelle : map() vs filter()
Différences clés :
map(): Applique une fonction pour transformer chaque élément → la sortie a la même longueurfilter(): Teste chaque élément et ne conserve que ceux qui passent → la sortie a une longueur égale ou plus courte
Dans ce chapitre, nous avons exploré les puissantes fonctionnalités de programmation fonctionnelle de Python. Nous avons appris que les fonctions sont des objets de première classe qui peuvent être manipulés comme n’importe quelle autre valeur, permettant des modèles de code flexibles et réutilisables. Nous avons découvert comment des fonctions peuvent renvoyer d’autres fonctions, créant des closures qui se souviennent de leur environnement. Nous avons exploré les expressions lambda pour des définitions de fonctions concises, et nous avons utilisé map() et filter() pour traiter des collections de manière élégante.
Ces concepts constituent la base de techniques avancées de programmation Python. Au chapitre 38, nous nous appuierons sur ces connaissances pour maîtriser les décorateurs(decorators), l’une des fonctionnalités les plus élégantes de Python.