PythonProgrammationDéveloppeur Python

À travers quel mécanisme interne **Python** implémente-t-il la portée lexicale pour les fonctions imbriquées, et comment l’instruction **nonlocal** manipule-t-elle les **objets cellule** pour permettre la modification des variables définies dans les portées englobantes ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse à la question

Python implémente la portée lexicale via un mécanisme impliquant des objets cellule qui agissent en tant qu'intermédiaires entre les fonctions imbriquées et leurs portées englobantes. Lorsqu'une fonction imbriquée fait référence à une variable d'une portée extérieure, le compilateur la marque comme une variable libre (stockée dans co_freevars) et la fonction englobante stocke la valeur de cette variable à l'intérieur d'un objet cellule plutôt que dans un emplacement de variable locale standard. Le mot-clé nonlocal indique à l'interpréteur de résoudre la recherche de nom vers cet objet cellule existant plutôt que de créer une nouvelle liaison locale, permettant ainsi à la portée interne de lire et d'écrire à la même adresse mémoire que la portée extérieure.

Situation de la vie réelle

Nous devions implémenter un journal d’audit léger pour une pipeline de traitement de données qui maintiendrait un compte courant des enregistrements assainis à travers plusieurs invocations de rappels sans polluer l’espace de noms global ou créer toute une hiérarchie de classes. Le défi était de s'assurer que l'état du compteur persiste entre les appels à la fonction de journalisation interne tout en restant encapsulé au sein de la fonction d’usine qui le créait.

Une solution envisagée était d'utiliser un dictionnaire global pour stocker des compteurs indexés par ID de journal. Cette approche offrait simplicité et permettait une inspection externe de l'état, mais introduisait une pollution de l'espace de noms global et nécessitait des mécanismes de verrouillage complexes pour garantir la sécurité des threads dans l'ensemble de l'application. De plus, cela brisait l'encapsulation en exposant les détails d'implémentation à d'autres modules.

Une autre approche impliquait la création d'une classe dédiée avec un attribut d'instance pour contenir le compteur. Cela offrait une encapsulation appropriée et des sémantiques orientées objet familières, mais ajoutait un code inutile pour ce qui était essentiellement une utilité à fonction unique, et le coût de création d'instances était jugé excessif pour une opération de journalisation à haute fréquence qui serait instanciée des milliers de fois.

La solution choisie a utilisé une closure avec la déclaration nonlocal pour lier le compteur à un objet cellule dans la portée englobante. Cette approche maintenait une encapsulation fonctionnelle propre sans le surcoût des classes, garantissait que l'état restait privé à la closure et tirait parti du mécanisme optimisé de désréférencement des cellules de Python, qui, bien que légèrement plus lent que les variables locales, était négligeable par rapport aux opérations d'E/S. Le résultat fut une réduction de 40 % de la surcharge mémoire par rapport à l'approche basée sur les classes et l'élimination des conflits d'état globaux.

Ce que les candidats oublient souvent

Pourquoi l'assignation à une variable d'une portée extérieure crée-t-elle une nouvelle variable locale au lieu de modifier l'ancienne sans le mot-clé nonlocal ?

Dans Python, l'assignation est une instruction qui lie un nom à une valeur dans la portée locale actuelle par défaut. Lorsque le compilateur rencontre une assignation à l'intérieur d'une fonction imbriquée, il détermine que la variable est locale à cette fonction à moins qu'elle ne soit déclarée autrement. Sans nonlocal, la fonction interne crée une nouvelle entrée dans son propre dictionnaire f_locals, masquant complètement la variable extérieure. La déclaration nonlocal oblige le compilateur à traiter la variable comme une référence à l'objet cellule créé dans la portée englobante, permettant un accès en lecture et en écriture à l'emplacement mémoire partagé.

Quelle est la différence fondamentale entre nonlocal et global concernant la résolution de portée ?

Bien que les deux mots-clés modifient la portée dans laquelle une assignation opère, global restreint la résolution de noms à l'espace de noms global au niveau du module, contournant toute portée de fonction englobante. En revanche, nonlocal ignore spécifiquement la portée locale actuelle et recherche à travers les définitions de fonctions englobantes (mais pas les globales du module) pour trouver le plus proche objet cellule associé au nom. Cela signifie que nonlocal ne peut pas être utilisé pour modifier des variables au niveau du module, et global ne peut pas voir des variables à l'intérieur de fonctions imbriquées, à moins qu'elles ne soient également explicitement déclarées globales dans ces fonctions extérieures.

Comment plusieurs fonctions imbriquées partagent-elles le même état via des objets cellule, et quand ces cellules sont-elles réellement allouées ?

Lorsqu'une fonction extérieure définit plusieurs fonctions internes qui font référence à la même variable de la portée extérieure, le compilateur Python crée un seul objet cellule pour cette variable dans l'environnement de la fonction extérieure. Toutes les fonctions internes reçoivent une référence à ce même objet cellule dans leur tuple __closure__. Ces cellules sont allouées au moment de l'exécution lorsque la fonction extérieure s'exécute (et non lorsque le code est compilé), et elles persistent tant qu'une fonction interne (ou une référence à elles) existe. Cet objet cellule partagé est ce qui permet aux différentes fonctions internes d'observer les modifications de l'autre sur la variable englobée, créant un mécanisme d'état partagé similaire à celui des variables d'instance, mais sans classes.