Fonctions#
Définir une fonction#
Les fonctions sont le premier mécanisme d”abstraction offert par Python. Elles permettent de donner un nom à un bloc de code réutilisable, de l’isoler du reste du programme, et de définir clairement son interface : ce qu’elle reçoit en entrée et ce qu’elle produit en sortie. Une bonne fonction fait une seule chose, la fait bien, et son nom dit exactement ce qu’elle fait.
En Python, on définit une fonction avec le mot-clé def, suivi du nom, de la liste des paramètres entre parenthèses, et d’un deux-points. Le corps de la fonction est indenté. Le mot-clé return permet de retourner une valeur.
def aire_rectangle(largeur, hauteur):
"""Calcule l'aire d'un rectangle.
Args:
largeur: La largeur du rectangle (nombre positif).
hauteur: La hauteur du rectangle (nombre positif).
Returns:
L'aire du rectangle (largeur × hauteur).
"""
return largeur * hauteur
print(aire_rectangle(5, 3))
print(aire_rectangle(10, 0.5))
15
5.0
Fonctions sans valeur de retour#
Une fonction qui ne contient pas d’instruction return, ou dont le return est sans expression, retourne implicitement None. Ce comportement est parfaitement valide pour les fonctions dites « procédurales », qui agissent par effets de bord (affichage, modification d’un objet, écriture dans un fichier).
def saluer(nom):
"""Affiche un message de bienvenue."""
print(f"Bonjour, {nom} !")
# Pas de return : retourne None implicitement
résultat = saluer("Alice")
print(résultat) # None
Bonjour, Alice !
None
Docstrings#
La docstring est une chaîne de caractères placée immédiatement après la ligne def. Elle documente la fonction et est accessible via help() et fonction.__doc__. La convention PEP 257 recommande d’utiliser des guillemets triples """. Un style populaire est le format Google (illustré ci-dessus) ou le format NumPy (préféré pour les projets scientifiques).
help(aire_rectangle)
Help on function aire_rectangle in module __main__:
aire_rectangle(largeur, hauteur)
Calcule l'aire d'un rectangle.
Args:
largeur: La largeur du rectangle (nombre positif).
hauteur: La hauteur du rectangle (nombre positif).
Returns:
L'aire du rectangle (largeur × hauteur).
Paramètres et arguments#
Python offre un système de paramètres très riche qui couvre la grande majorité des cas d’usage.
Paramètres positionnels et par mot-clé#
def créer_profil(nom, age, ville="Paris"):
return f"{nom}, {age} ans, habite à {ville}"
# Appel positionnel
print(créer_profil("Alice", 30))
# Appel par mot-clé (keyword argument)
print(créer_profil(age=25, nom="Bob"))
# Mélange des deux (positionnels d'abord)
print(créer_profil("Charlie", 35, ville="Lyon"))
Alice, 30 ans, habite à Paris
Bob, 25 ans, habite à Paris
Charlie, 35 ans, habite à Lyon
Valeurs par défaut : attention aux défauts mutables#
Remarque 10
Le piège le plus classique en Python concerne les valeurs par défaut mutables. Les valeurs par défaut ne sont évaluées qu”une seule fois, au moment de la définition de la fonction, pas à chaque appel. Utiliser une liste, un dictionnaire ou tout autre objet mutable comme valeur par défaut est une erreur classique qui conduit à un état partagé et des comportements inattendus.
# MAUVAIS : liste mutable comme valeur par défaut
def ajouter_élément_mauvais(élément, liste=[]):
liste.append(élément)
return liste
print(ajouter_élément_mauvais(1)) # [1]
print(ajouter_élément_mauvais(2)) # [1, 2] — surprise !
print(ajouter_élément_mauvais(3)) # [1, 2, 3] — la liste persiste !
[1]
[1, 2]
[1, 2, 3]
# BON : utiliser None comme sentinelle
def ajouter_élément(élément, liste=None):
if liste is None:
liste = []
liste.append(élément)
return liste
print(ajouter_élément(1)) # [1]
print(ajouter_élément(2)) # [2]
print(ajouter_élément(3)) # [3]
[1]
[2]
[3]
*args et **kwargs#
# *args : nombre variable d'arguments positionnels -> tuple
def somme(*args):
"""Somme d'un nombre arbitraire de valeurs."""
return sum(args)
print(somme(1, 2, 3))
print(somme(10, 20, 30, 40, 50))
# **kwargs : nombre variable d'arguments par mot-clé -> dict
def décrire(**kwargs):
"""Affiche des paires clé=valeur."""
for clé, valeur in kwargs.items():
print(f" {clé}: {valeur}")
décrire(nom="Alice", age=30, langage="Python")
6
150
nom: Alice
age: 30
langage: Python
# Combinaison des deux
def journal(niveau, *messages, séparateur="\n", **métadonnées):
print(f"[{niveau.upper()}]")
print(séparateur.join(messages))
for k, v in métadonnées.items():
print(f" {k}={v}")
journal("info", "Démarrage", "Connexion établie",
séparateur=" | ", service="api", version="1.0")
[INFO]
Démarrage | Connexion établie
service=api
version=1.0
Paramètres positionnels-seulement et mot-clé-seulement#
Python 3.8+ permet de préciser exactement comment les paramètres peuvent être passés :
# Le / sépare les paramètres positionnels-seulement (à gauche)
# Le * sépare les paramètres mot-clé-seulement (à droite)
def f(pos1, pos2, /, normal, *, kwonly1, kwonly2):
print(f"pos1={pos1}, pos2={pos2}, normal={normal}, "
f"kwonly1={kwonly1}, kwonly2={kwonly2}")
f(1, 2, 3, kwonly1=4, kwonly2=5) # OK
f(1, 2, normal=3, kwonly1=4, kwonly2=5) # OK
# f(pos1=1, ...) # Erreur : pos1 est positionnel-seulement
pos1=1, pos2=2, normal=3, kwonly1=4, kwonly2=5
pos1=1, pos2=2, normal=3, kwonly1=4, kwonly2=5
Portée et espaces de noms#
La portée (scope) d’une variable désigne la région du code dans laquelle elle est accessible. Python résout les noms de variables selon la règle LEGB : Local → Enclosing → Global → Built-in.
Définition 6 (Règle LEGB)
Lorsque Python rencontre un nom de variable, il le cherche successivement dans quatre portées, de la plus proche à la plus lointaine :
Local : l’espace de noms de la fonction courante (les variables définies dans la fonction).
Enclosing : les espaces de noms des fonctions englobantes (pour les fonctions imbriquées).
Global : l’espace de noms du module courant (les variables définies au niveau du module).
Built-in : les noms prédéfinis par Python (
print,len,range,type…).
La première portée où le nom est trouvé gagne. Si le nom n’est trouvé dans aucune portée, Python lève une NameError.
x = "global" # Variable globale
def extérieure():
x = "enclosing" # Variable dans la portée englobante
def intérieure():
x = "local" # Variable locale
print(f"intérieure voit : {x}")
intérieure()
print(f"extérieure voit : {x}")
extérieure()
print(f"global voit : {x}")
intérieure voit : local
extérieure voit : enclosing
global voit : global
global et nonlocal#
compteur = 0
def incrémenter():
global compteur # Déclare que l'on modifie la variable globale
compteur += 1
incrémenter()
incrémenter()
print(compteur) # 2
2
def fabrique_compteur():
valeur = 0
def incrémenter():
nonlocal valeur # Modifie la variable de la portée englobante
valeur += 1
return valeur
return incrémenter
compteur = fabrique_compteur()
print(compteur()) # 1
print(compteur()) # 2
print(compteur()) # 3
1
2
3
Remarque 11
L’utilisation de global est généralement un signe de conception discutable : elle introduit un état partagé qui rend le code difficile à tester et à comprendre. Dans la plupart des cas, il est préférable de passer la valeur en paramètre et de la retourner. En revanche, nonlocal est souvent légitime dans les fermetures (closures) et les décorateurs, où l’on veut maintenir un état entre les appels d’une fonction retournée par une autre fonction.
Fonctions comme objets#
En Python, les fonctions sont des objets de première classe (first-class objects) : elles peuvent être stockées dans des variables, passées en argument à d’autres fonctions, retournées par des fonctions, et insérées dans des structures de données. Cette propriété est au cœur du style de programmation fonctionnel en Python.
# Assigner une fonction à une variable
def carré(x):
return x ** 2
ma_fonction = carré
print(ma_fonction(5)) # 25
print(type(ma_fonction)) # <class 'function'>
# Stocker des fonctions dans une liste
def cube(x):
return x ** 3
def racine(x):
return x ** 0.5
transformations = [carré, cube, racine]
for fn in transformations:
print(f"{fn.__name__}(9) = {fn(9)}")
25
<class 'function'>
carré(9) = 81
cube(9) = 729
racine(9) = 3.0
Passage en argument et fonctions d’ordre supérieur#
# Passer une fonction en argument
def appliquer(fonction, valeurs):
return [fonction(v) for v in valeurs]
données = [1, 4, 9, 16, 25]
print(appliquer(carré, données))
print(appliquer(racine, données))
# map et filter : fonctions d'ordre supérieur intégrées
print(list(map(carré, données)))
print(list(filter(lambda x: x > 5, données)))
[1, 16, 81, 256, 625]
[1.0, 2.0, 3.0, 4.0, 5.0]
[1, 16, 81, 256, 625]
[9, 16, 25]
lambda#
Les expressions lambda définissent des fonctions anonymes en une seule ligne. Elles sont utiles pour de petites fonctions passées en argument à d’autres fonctions.
# Forme : lambda paramètres: expression
doubler = lambda x: x * 2
print(doubler(7))
# Usage courant : clé de tri
élèves = [("Alice", 17), ("Bob", 19), ("Charlie", 15)]
triés_par_note = sorted(élèves, key=lambda e: e[1], reverse=True)
print(triés_par_note)
# Avec sorted et une clé composée
mots = ["banane", "pomme", "cerise", "abricot", "kiwi"]
triés = sorted(mots, key=lambda m: (len(m), m))
print(triés)
14
[('Bob', 19), ('Alice', 17), ('Charlie', 15)]
['kiwi', 'pomme', 'banane', 'cerise', 'abricot']
Fermetures (closures)#
def multiplicateur(facteur):
"""Retourne une fonction qui multiplie par facteur."""
def multiplier(x):
return x * facteur # facteur est capturé de la portée englobante
return multiplier
doubler = multiplicateur(2)
tripler = multiplicateur(3)
print(doubler(10)) # 20
print(tripler(10)) # 30
# La fermeture "se souvient" de son environnement
print(doubler.__closure__[0].cell_contents) # 2
20
30
2
Fonctions récursives#
La récursion est une technique où une fonction s’appelle elle-même pour résoudre un problème en le décomposant en sous-problèmes de même nature mais de taille réduite. Elle repose sur deux éléments essentiels : un cas de base (qui arrête la récursion) et un cas récursif (qui réduit le problème).
def factorielle(n):
"""Calcule n! de façon récursive."""
if n <= 1: # Cas de base
return 1
return n * factorielle(n - 1) # Cas récursif
print(factorielle(0))
print(factorielle(5))
print(factorielle(10))
1
120
3628800
# Fibonacci récursif (naïf mais illustratif)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print([fibonacci(i) for i in range(10)])
# Version mémoïsée (bien plus efficace)
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci_rapide(n):
if n <= 1:
return n
return fibonacci_rapide(n - 1) + fibonacci_rapide(n - 2)
print(fibonacci_rapide(50))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
12586269025
Limite de récursion#
import sys
# La profondeur de récursion maximale par défaut
print(sys.getrecursionlimit())
# On peut l'augmenter, mais avec prudence
# sys.setrecursionlimit(5000)
# Dépasser la limite lève RecursionError
def infini(n):
return infini(n + 1)
try:
infini(0)
except RecursionError:
print("RecursionError : limite de récursion atteinte !")
3000
RecursionError : limite de récursion atteinte !
Remarque 12
Python n’optimise pas la récursion terminale (tail call optimization), contrairement à des langages comme Haskell ou Scheme. Une fonction récursive très profonde lèvera donc toujours une RecursionError, même si l’appel récursif est en dernière position. Pour les algorithmes nécessitant une grande profondeur, il est préférable de les réécrire de façon itérative, ou d’utiliser functools.lru_cache pour la mémoïsation quand le problème a une structure de sous-problèmes récurrents (programmation dynamique).
Annotations de types#
Python 3.5+ introduit les annotations de types (type hints) : une syntaxe optionnelle pour déclarer le type attendu des paramètres et la valeur de retour d’une fonction. Ces annotations ne sont pas vérifiées à l’exécution (Python reste dynamiquement typé) mais servent de documentation et sont exploitées par les outils d’analyse statique comme mypy, pyright et l’extension Pylance de VS Code.
# Annotations de paramètres et de retour
def saluer(nom: str, fois: int = 1) -> str:
return (f"Bonjour, {nom} ! " * fois).strip()
print(saluer("Alice"))
print(saluer("Bob", 3))
# Avec des types plus complexes
from typing import Optional, list as List
def trouver_premier(valeurs: list[int], cible: int) -> int | None:
"""Retourne l'indice de la première occurrence de cible, ou None."""
for i, v in enumerate(valeurs):
if v == cible:
return i
return None
print(trouver_premier([1, 5, 3, 5, 2], 5)) # 1
print(trouver_premier([1, 5, 3, 5, 2], 9)) # None
Bonjour, Alice !
Bonjour, Bob ! Bonjour, Bob ! Bonjour, Bob !
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
Cell In[21], line 9
6 print(saluer("Bob", 3))
8 # Avec des types plus complexes
----> 9 from typing import Optional, list as List
11 def trouver_premier(valeurs: list[int], cible: int) -> int | None:
12 """Retourne l'indice de la première occurrence de cible, ou None."""
ImportError: cannot import name 'list' from 'typing' (/usr/lib/python3.13/typing.py)
# Types de retour avancés
from typing import Callable
def composer(f: Callable[[float], float],
g: Callable[[float], float]) -> Callable[[float], float]:
"""Retourne la composition h = f ∘ g."""
def h(x: float) -> float:
return f(g(x))
return h
import math
racine_de_absolu = composer(math.sqrt, abs)
print(racine_de_absolu(-16)) # 4.0
Définition 7 (Annotations de types)
Les annotations de types en Python sont des métadonnées attachées aux paramètres et à la valeur de retour d’une fonction. Elles sont stockées dans l’attribut __annotations__ de la fonction et n’ont aucun effet sur l’exécution. Leur rôle est triple : documentation (elles explicitent le contrat de la fonction), vérification statique (des outils comme mypy peuvent détecter des incohérences de types sans exécuter le code), et complétion intelligente (l’IDE peut proposer des suggestions précises en se basant sur les types déclarés).
Visualisation de la règle LEGB#
Résumé#
Dans ce chapitre, nous avons exploré les fonctions Python sous toutes leurs facettes :
Une fonction se définit avec
def, peut retourner une valeur avecreturn, et retourneNoneimplicitement si aucunreturnn’est présent. La docstring documente le contrat de la fonction.Les paramètres peuvent être positionnels, par mot-clé, avec des valeurs par défaut. Les valeurs par défaut mutables sont un piège classique : utiliser
Nonecomme sentinelle.*argscapture des arguments positionnels supplémentaires dans un tuple ;**kwargscapture des arguments par mot-clé dans un dictionnaire./et*permettent de contraindre le mode de passage des arguments.La règle LEGB (Local → Enclosing → Global → Built-in) gouverne la résolution des noms.
globalpermet de modifier une variable globale ;nonlocalpermet de modifier une variable d’une portée englobante.Les fonctions sont des objets de première classe : elles peuvent être passées en argument, retournées par d’autres fonctions et stockées dans des structures de données. Les lambdas permettent de définir de petites fonctions anonymes. Les fermetures capturent l’environnement dans lequel elles sont définies.
La récursion décompose un problème en sous-problèmes de même nature ; elle nécessite impérativement un cas de base. Python limite la profondeur de récursion (1000 par défaut) et n’optimise pas la récursion terminale.
Les annotations de types documentent le contrat d’une fonction et permettent la vérification statique, sans affecter l’exécution.
Dans le chapitre suivant, nous explorerons les structures de données natives de Python : listes, tuples, dictionnaires, ensembles et les collections avancées du module collections.