Pour maîtriser les fonctions lambda en Python, il faut commencer par examiner le concept fondamental de fonction lui-même, son rôle en informatique, et l'implémentation unique qu'en propose ce langage.
1.1 La nature des fonctions : du concept abstrait à la matérialisation
Dans le contexte informatique, une fonction est un bloc de code réutilisable qui encapsule une logique de calcul spécifique. Elle zéro ou plusieurs entrées (paramètres), exécute une série d'opérations et peut produire une sortie (valeur de retour). En mathématiques, une fonction représente une relation de mapping d'un ensemble d'entrée (domaine) vers un ensemble de sortie (codomaine).
Les fonctions en programmation jouent plusieurs rôles clés :
- Abstraction et Encapsulation : Cacher les détails d'implémentation complexes derrière une interface simple.
- Modularité : Décomposer un grand programme en unités plus petites et plus gérables.
- Réutilisabilité : Éviter la répétition de code. Une fois définie, une fonction peut être appelée à plusieurs endroits.
- Contrôle du Flux : Diriger l'exécution du programme.
En Python, qu'elles soient nommées (via def) ou anonymes (via lambda), les fonctions suivent ces principes fondamentaux.
1.2 Le statut privilégié des fonctions en Python : Citoyens de Premier Ordre
Python traite les fonctions comme des citoyens de premier ordre (First-Class Citizens). Cela signifie qu'elles bénéficient des mêmes droits que les autres types de données comme les variables, les nombres ou les chaînes de caractères. Un type de données est dit "premier ordre" s'il peut être :
- Assigné à une variable : Une fonction peut être liée à un nom de variable.
- Passé en argument à une autre fonction.
- Retourné comme valeur par une autre fonction.
- Stocké dans une structure de données : (liste, dictionnaire, tuple...)
Cette propriété est le fondement qui permet l'existence et l'utilisation répandue des fonctions lambda ainsi que des fonctions d'ordre supérieur (Higher-Order Functions).
def calculate_sum(a, b):
return a + b
def calculate_difference(a, b):
return a - b
# 1. Assignation à une variable
sum_operation = calculate_sum
print(sum_operation(5, 3)) # Affiche 8
# 2. Passage en argument
def execute_operation(func, x, y):
return func(x, y)
print(execute_operation(calculate_sum, 10, 5)) # Affiche 15
print(execute_operation(calculate_difference, 10, 5)) # Affiche 5
# 3. Retour comme valeur
def get_calculator(operation_type):
if operation_type == 'add':
return calculate_sum
elif operation_type == 'subtract':
return calculate_difference
return None
my_calculator = get_calculator('add')
print(my_calculator(20, 10)) # Affiche 30
# 4. Stockage dans une structure
operations_list = [calculate_sum, calculate_difference]
for op_func in operations_list:
print(op_func(7, 2)) # Affiche 9 puis 5
Cette capacité permet aux fonctions lambda de s'intégrer de manière fluide dans l'écosystème Python, notamment dans les fonctions d'ordre supérieur et les rappels (callbacks).
1.3 L'essence de la fonction lambda : une syntaxe sucrée ?
Le point fondamental à comprendre est qu'une fonction lambda n'est, dans son implémentation sous-jacente, pas différente d'une fonction définie avec def. Elle peut être considérée comme une syntaxe sucrée (Syntactic Sugar) offrant une manière plus concise de définir des fonctions simples, généralement utilisées une seule fois.
Cela peut être vérifié en inspectant les propriétés runtime des objets fonctions.
def named_multiplier(x):
return x * 3
anonymous_multiplier = lambda x: x * 3
print(f"Type de named_multiplier: {type(named_multiplier)}") # <class 'function'>
print(f"Type de anonymous_multiplier: {type(anonymous_multiplier)}") # <class 'function'>
print(f"Nom de named_multiplier: {named_multiplier.__name__}") # 'named_multiplier'
print(f"Nom de anonymous_multiplier: {anonymous_multiplier.__name__}") # '<lambda>'
# Les deux produisent un bytecode similaire pour une opération simple.
Les deux objets sont de type function. Leur différence majeure réside dans l'attribut __name__ ('<lambda>' vs un nom explicite). Le bytecode généré pour des opérations simples est quasi identique. En conclusion, lambda est une syntaxe allégée pour la définition de fonctions.
1.4 Représentation interne de la fonction : l'objet function
En Python, tout est objet, y compris les fonctions. La définition d'une fonction (via def ou lambda) crée un objet de type function contenant des métadonnées et le code exécutable.
Les attributs principaux d'un objet fonction incluent __name__, __doc__, __module__, __defaults__ (valeurs par défaut des paramètres), __code__ (l'objet code compilé), __globals__ (espace de noms global au moment de la définition) et __closure__ (pour les fonctions fermées/closures).
def process_items(items, threshold=10, *args, **kwargs):
"""Filtre les items supérieurs au seuil et retourne un tuple."""
result = tuple(item for item in items if item > threshold)
print("Arguments supplémentaires:", args, kwargs)
return result
filter_func = lambda seq, min_val: [v for v in seq if v > min_val]
# Inspection des attributs
print("Nom:", process_items.__name__)
print("Docstring:", process_items.__doc__)
print("Défauts:", process_items.__defaults__)
print("Code object co_argcount:", process_items.__code__.co_argcount)
print("Variables locales (co_varnames):", process_items.__code__.co_varnames)
print("\nNom du lambda:", filter_func.__name__) # '<lambda>'
print("Docstring du lambda:", filter_func.__doc__) # None
L'attribut __code__ est crucial : il pointe vers l'objet code qui stocke réellement le bytecode compilé et les métadonnées d'exécution de la fonction. Pour lambda, certaines métadonnées sont simplifiées (comme __name__ et __doc__), mais le mécanisme central reste le même.
1.5 Déclaration vs Expression de fonction
En programmation, on définit souvent les fonctions de deux manières :
- Déclaration (Function Declaration) : Utilise un mot-clé (comme
def) pour définir une fonction nommée. Elle est généralement disponible dans toute la portée. - Expression (Function Expression) : Assigne une fonction (souvent anonyme) à une variable. Elle est créée et devient disponible au moment de l'exécution de l'instruction d'affectation.
En Python, l'instruction def crée et lie la fonction au moment de son exécution. lambda est l'exemple type d'une expression de fonction. Sa nature anonyme force son utilisation comme partie d'une expression (affectation, passage en argument...).
# C'est une "déclaration" (plus précisément, une instruction)
def square_declared(n):
return n ** 2
# C'est une "expression"
square_expressed = lambda n: n ** 2
Cette expressivité fait de lambda un outil pratique pour créer rapidement des fonctions simples, en ligne, sans polluer l'espace de noms avec un nom de fonction dédié.
Ce chapitre a établi les fondations en explorant l'essence des fonctions, leur statut premier ordre en Python, et la nature de lambda en tant que syntaxe sucrée. Nous avons également examiné la représentation interne des fonctions via les objets function et code. Ces connaissances sont essentielles pour comprendre la syntaxe, les limitations et le comportement en mémoire des fonctions lambda, que nous explorerons ensuite.
2.1 Syntaxe du mot-clé lambda
La syntaxe est concrète : lambda arguments: expression. Elle se compose de quatre parties : le mot-clé lambda, une liste d'arguments (similaire à celle de def), un deux-points et une expression unique dont le résultat est la valeur de retour de la fonction.
Elle supporte diverses signatures de paramètres :
# Sans argument
get_constant = lambda: 42
# Un argument
double = lambda x: x * 2
# Plusieurs arguments
combine = lambda a, b, c: a + b - c
# Avec valeurs par défaut
power = lambda base, exponent=2: base ** exponent
# Avec *args (arguments positionnels variables)
collect = lambda *args: args
# Avec **kwargs (arguments nommés variables)
report = lambda **kwargs: kwargs
# Mélange (ordre standard : positionnels, défauts, *args, nommés, **kwargs)
complex_lambda = lambda x, y=10, *others, **details: {'sum': x + y + sum(others), 'info': details}
2.2 Limitations fondamentales des fonctions lambda
La conception de lambda impose des contraintes intentionnelles qui en définissent le cadre d'utilisation.
Limitation 1 : Un corps constitué d'une seule expression, et non d'instructions.
C'est la restriction la plus cruciale. Le corps doit être une expression (quelque chose qui produit une valeur), et non une instruction (une action comme une boucle, une affectation, un bloc if complet).
Implications :
- On ne peut pas utiliser de blocs
if/elsecomplets (mais on peut utiliser l'opérateur ternaire :Vrai if condition else Faux). - On ne peut pas utiliser de boucles
forouwhile. - On ne peut pas faire d'affectation (
x = valeur) à l'intérieur du corps. - Le
returnimplicite est obligatoire ; on ne peut pas écrirereturnexplicitement.
Cette contrainte garantit la concision et la nature "expression" de lambda, adaptée à une logique de transformation unique.
# Erreur : Utilisation d'une instruction if
# bad_lambda = lambda x: if x > 0: "positif" else: "négatif" # SyntaxError
# Correct : Opérateur ternaire
check_sign = lambda x: "positif" if x > 0 else ("zéro" if x == 0 else "négatif")
# Erreur : Affectation
# bad_lambda = lambda x: y = x + 1 # SyntaxError
# Correct : Retour direct d'une expression
compute = lambda x: x * 2 + 5
# Erreur (pratique) : Boucle
# bad_lambda = lambda lst: for item in lst: print(item) # SyntaxError
Limitation 2 : Généralement dépourvu de docstring.
Une fonction lambda ne peut pas contenir de chaîne de documentation (docstring) multi-ligne comme une fonction def. Son attribut __doc__ est généralement None.
def documented_func(x):
"""Double la valeur d'entrée."""
return x * 2
undocumented_lambda = lambda x: x * 2
print(documented_func.__doc__) # Affiche la docstring
print(undocumented_lambda.__doc__) # Affiche None
Limitation 3 (Recommandation d'usage) : Privilégier pour une logique simple, courte et unique.
lambda excelle en tant que :
- Argument pour les fonctions d'ordre supérieur (
map,filter,sorted,reduce). - Gestionnaire d'événement ou rappel (callback) simple.
- Fonction temporaire créée à la volée pour un usage immédiat.
Pour une logique complexe, des branches conditionnelles multiples, ou une fonction réutilisable nommée, la définition avec def est plus claire et maintenable.
# Usage judicieux de lambda : comme clé de tri
users = [{'name': 'Alice', 'age': 30}, {'name': 'Bob', 'age': 25}]
sorted_users = sorted(users, key=lambda u: u['age'])
# Usage qui nuit à la lisibilité : logique trop complexe dans un lambda
# (Exemple d'anti-pattern)
complex_lambda = lambda data: [item['id'] * 2 if item['status'] == 'active' else item['id'] for item in data if item.get('verified')]
# Mieux vaut une fonction nommée pour ce niveau de complexité.
Le choix entre lambda et def doit se baser sur la complexité, la lisibilité et le besoin de réutilisation. Pour une logique simple et intégrée, lambda offre une solution élégante.
3.1 Le modèle mémoire d'un objet fonction lambda
Comme toute donnée en Python, une fonction lambda est un objet en mémoire. Sa création implique la construction d'un objet function qui référence un objet code (contenant le bytecode), l'espace de noms global (__globals__), les valeurs par défaut (__defaults__) et, pour les fonctions fermées, les cellules de fermeture (__closure__).
import sys
def regular_op(a, b):
return a + b
lambda_op = lambda a, b: a + b
# La taille des objets fonction eux-mêmes est similaire.
print("Taille de regular_op:", sys.getsizeof(regular_op))
print("Taille de lambda_op:", sys.getsizeof(lambda_op))
# Les véritables différences sont dans les objets code et les métadonnées.
print("\nObjet code de regular_op:", regular_op.__code__)
print("Objet code de lambda_op:", lambda_op.__code__)
Du point de vue du modèle mémoire, lambda est un moyen plus rapide de créer un objet function standard.
3.2 Portée des variables (Scope) et règle LEGB dans les lambda
Les fonctions lambda obéissent à la règle LEGB pour la résolution des noms de variables : Local (portée locale), Enclosing (portée de la fonction englobante), Global (portée du module), Built-in (portée des fonctions natives).
module_var = "globale"
def outer_function(outer_param):
enclosing_var = "de la fonction englobante"
# Ce lambda capture des variables de plusieurs portées.
inner_lambda = lambda local_arg: f"{local_arg} | {enclosing_var} | {module_var} | {outer_param}"
return inner_lambda
closure_func = outer_function("paramètre extérieur")
print(closure_func("argument local"))
# Affiche : argument local | de la fonction englobante | globale | paramètre extérieur
Piège classique : le moment de liaison des variables.
Une variable libre dans une lambda (comme i ci-dessous) est liée au moment de l'appel de la lambda, pas au moment de sa création. Cela peut surprendre dans une boucle.
# Problème
multipliers = []
for i in range(3):
multipliers.append(lambda x: x * i) # Capture 'i', pas sa valeur à cet instant
# Quand les lambdas sont appelées, 'i' vaut 2 (sa dernière valeur dans la boucle).
print(multipliers[0](10)) # Affiche 20 (2 * 10), pas 0 (0 * 10)
print(multipliers[1](10)) # Affiche 20
print(multipliers[2](10)) # Affiche 20
# Solution : utiliser un paramètre par défaut pour "geler" la valeur de 'i'
fixed_multipliers = []
for i in range(3):
fixed_multipliers.append(lambda x, captured_i=i: x * captured_i)
print(fixed_multipliers[0](10)) # Affiche 0
print(fixed_multipliers[1](10)) # Affiche 10
print(fixed_multipliers[2](10)) # Affiche 20
La valeur du paramètre par défaut captured_i=i est évaluée et fixée au moment de la création de la lambda dans chaque itération.
3.3 Mécanisme des fonctions fermées (Closures) et l'attribut __closure__
Une fermeture (closure) survient quand une fonction (comme une lambda) "se souvient" des variables de son environnement de définition, même après que cet environnement a fini de s'exécuter. Ces variables capturées sont stockées dans l'attribut __closure__ de l'objet fonction, sous forme d'objets "cell" individuels.
def make_adder(n):
# 'n' est une variable locale de make_adder, mais la lambda y fait référence.
return lambda x: x + n
add_10 = make_adder(10)
add_20 = make_adder(20)
print(add_10(5)) # 15
print(add_20(5)) # 25
# Inspection de la fermeture
print("add_10.__closure__:", add_10.__closure__)
print("Valeur capturée dans la cellule:", add_10.__closure__[0].cell_contents) # 10
print("add_20.__closure__:", add_20.__closure__)
print("Valeur capturée dans la cellule:", add_20.__closure__[0].cell_contents) # 20
Chaque appel à make_adder crée une fermeture distincte avec sa propre cellule contenant la valeur spécifique de n.
3.4 Performance à l'exécution
Pour des fonctions simples, il n'y a généralement pas de différence de performance significative entre lambda et def. L'interpréteur Python génère un bytecode similaire. Les micro-benchmarks montrent des temps quasi identiques pour l'appel de fonctions très simples.
Les considérations principales sont la lisibilité et la maintenabilité du code. L'utilisation abusive de lambda pour une logique complexe crée une dette technique plus coûteuse en termes de temps de développement et de maintenance que le gain potentiel en cycles CPU minuscule.
Ce chapitre a approfondi les aspects techniques sous-jacents : le modèle mémoire, les interactions avec les portées et les fermetures. Le prochain chapitre se concentrera sur les applications pratiques et avancées des fonctions lambda.
4.1 Intégration avec les fonctions d'orrdre supérieur
C'est le cas d'usage le plus courant pour lambda.
4.1.1 map() : Appliquer une fonction à chaque élément
temperatures_c = [0, 15, 20, 25, 30]
temperatures_f = list(map(lambda c: (c * 9/5) + 32, temperatures_c))
print(temperatures_f) # [32.0, 59.0, 68.0, 77.0, 86.0]
4.1.2 filter() : Sélectionner des éléments selon un critère
numbers = [12, 5, 33, 8, 19, 27, 2]
multiples_of_3 = list(filter(lambda x: x % 3 == 0, numbers))
print(multiples_of_3) # [12, 33, 27]
4.1.3 sorted() : Trier avec une clé personnalisée
employees = [
{'name': 'Martin', 'salary': 50000},
{'name': 'Sophie', 'salary': 65000},
{'name': 'Lucas', 'salary': 45000}
]
by_salary_desc = sorted(employees, key=lambda emp: emp['salary'], reverse=True)
print(by_salary_desc)
# [{'name': 'Sophie', ...}, {'name': 'Martin', ...}, {'name': 'Lucas', ...}]
4.1.4 functools.reduce() : Réduire une séquence à une seule valeur
from functools import reduce
def multiply(x, y):
return x * y
product = reduce(multiply, [1, 2, 3, 4, 5])
print(product) # 120
# Version lambda
product_lambda = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5])
4.2 Comme gestionnaires d'événements ou rappels (callbacks)
Dans les interfaces graphiques (GUI) ou la programmation événementielle, lambda est parfait pour définir un comportement réactif léger.
import tkinter as tk
root = tk.Tk()
button = tk.Button(root, text="Cliquez ici")
# Liaison d'un lambda comme commande du bouton
button.config(command=lambda: print("Bouton cliqué !"))
button.pack()
# root.mainloop() # Décommenter pour exécuter l'interface
4.3 Dans les compréhensions de liste et dictionnaire
Bien que lambda ne soit pas une instruction, elle peut être définie et appelée à l'intérieur d'une compréhension, ou stockée comme valeur fonctionnelle.
# Transformation à l'aide d'un lambda défini préalablement
increment = lambda x: x + 10
transformed = [increment(n) for n in range(5)]
# Création d'un dictionnaire de fonctions
operations = {
'double': lambda x: x * 2,
'triple': lambda x: x * 3,
'square': lambda x: x ** 2
}
print(operations['square'](4)) # 16
4.4 Complémentarité avec functools.partial
partial permet d'appliquer partiellement des arguments à une fonction, créant une nouvelle fonction avec moins de paramètres. lambda peut être utilisé pour un wrapping supplémentaire ou pour créer des fonctions partielles d'une manière plus directe.
from functools import partial
def log(level, message):
print(f"[{level}] {message}")
log_info = partial(log, "INFO")
log_warning = partial(log, "WARNING")
log_info("Application démarrée.") # [INFO] Application démarrée.
# Lambda pour créer une fonction usine (factory)
def make_multiplier(factor):
return lambda value: value * factor
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
4.5 Construction de pipelines de traitement de données légers
data = [
{"value": 10, "type": "A"},
{"value": 5, "type": "B"},
{"value": 20, "type": "A"},
{"value": 15, "type": "C"}
]
# Étape 1 : Filtrer le type 'A'
filter_type_a = lambda d: d['type'] == 'A'
# Étape 2 : Extraire la valeur
extract_value = lambda d: d['value']
# Étape 3 : Doubler
double_value = lambda v: v * 2
# Pipeline en une ligne
result = list(map(double_value, map(extract_value, filter(filter_type_a, data))))
print(result) # [20, 40]
Ce chapitre a illustré comment exploiter les fonctions lambda dans des scénarios pratiques et avancés. Leur concision et leur intégration native en font un outil puissant dans la boîte à outils du développeur Python, à utiliser avec discernement pour améliorer la clarté et l'expressivité du code.