ProgrammationDéveloppeur Backend

Expliquez comment fonctionnent les closures en Python, en quoi elles diffèrent des fonctions ordinaires et quelle est leur application pratique ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Historique de la question

Le terme "closure" a été emprunté à la programmation fonctionnelle et est présent dans Python depuis le début. Les closures permettent à une fonction de mémoriser l'environnement dans lequel elle a été créée, même si elle est appelée en dehors de cet environnement. Ce concept offre de la flexibilité et permet de mettre en œuvre de nombreux modèles, y compris les usines de fonctions et le calcul paresseux.

Problème

En Python, les fonctions sont des objets de premier ordre. Parfois, il est nécessaire qu'une fonction imbriquée utilise des variables de la portée de la fonction englobante, même après sa fin. La portée lexicale ordinaire ne garantit pas cela lors du retour de la fonction. Si une telle fonction accède aux variables de son contexte de création, une closure se forme.

Solution

Une closure se forme si la fonction interne se réfère à des variables définies dans l'extérieur, et que cette extérieure a retourné la fonction interne vers l'extérieur. Cela est souvent utilisé pour créer des usines de fonctions, encapsuler des états sans classes et construire des fonctions avec des paramètres "sur place".

Exemple de code:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

Caractéristiques clés :

  • Une closure conserve les valeurs des variables d'environnement, même si la fonction extérieure a déjà terminé.
  • L'état dans la fonction imbriquée est essentiellement privé, ne pouvant pas être modifié directement depuis l'extérieur.
  • Pour modifier une variable non-immutable dans la fonction extérieure au sein de la closure, le mot-clé nonlocal est utilisé.

Questions piégeantes.

Une closure peut-elle conserver un état mutable entre les appels, si la variable est modifiée dans la fonction imbriquée ?

Oui, si dans la fonction imbriquée, on utilise le mot-clé nonlocal. Sans nonlocal, l'assignation crée une nouvelle variable locale, sans modifier l'extérieur.

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

Peut-on implémenter des variables privées en Python en utilisant des closures à la place des classes ?

Oui, une closure offre une implémentation simple de "variables privées", inaccessibles de l'extérieur, à moins que des getters/setters ne soient fournis dans la fonction imbriquée.

Les closures sont-elles appliquées uniquement aux fonctions ? Peut-on organiser une closure avec des lambdas en Python ?

Oui, une closure peut aussi se former avec des expressions lambda, car elles sont analogues à def en ce qui concerne la liaison des variables lexiques.

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

Erreurs typiques et anti-modèles

  • S'attendre à ce qu'une closure modifie automatiquement une variable externe lorsqu'elle est modifiée à l'intérieur sans nonlocal.
  • Capturer des objets mutables dans une closure et les muter, sans se rendre compte des désagréments lors du débogage.
  • Utiliser une boucle pour créer des fonctions dans une closure sans lier correctement les variables (piège "toutes les fonctions voient la dernière valeur de la variable").

Exemple de la vie réelle

Cas négatif

Une usine de fonctions formant des gestionnaires dans une boucle utilise une variable de boucle à l'intérieur d'une closure:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

Avantages :

  • Simple, peu de code.

Inconvénients :

  • Tous les gestionnaires se réfèrent à la même variable i, sa dernière valeur étant 2 — un comportement inattendu pour la plupart.

Cas positif

Un argument par défaut a été utilisé pour "fixer" la valeur :

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

Avantages :

  • Liaison de la valeur nécessaire.
  • Comportement prévisible.

Inconvénients :

  • Il faut se souvenir de ce détail et corriger la closure manuellement, ce qui complique la maintenabilité du code.