Règles de portée et astuces

PythonPythonBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire, vous allez apprendre les règles de portée (scope) de Python et explorer des techniques avancées pour travailler avec la portée. Comprendre la portée en Python est essentiel pour écrire un code propre et maintenable, et cela permet d'éviter des comportements inattendus.

Les objectifs de ce laboratoire incluent la compréhension détaillée des règles de portée de Python, l'apprentissage de techniques pratiques de portée pour l'initialisation de classes, la mise en œuvre d'un système d'initialisation d'objets flexible et l'application de techniques d'inspection de trames (frame inspection) pour simplifier le code. Vous allez travailler avec les fichiers structure.py et stock.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") subgraph Lab Skills python/function_definition -.-> lab-132510{{"Règles de portée et astuces"}} python/arguments_return -.-> lab-132510{{"Règles de portée et astuces"}} python/scope -.-> lab-132510{{"Règles de portée et astuces"}} python/classes_objects -.-> lab-132510{{"Règles de portée et astuces"}} python/constructor -.-> lab-132510{{"Règles de portée et astuces"}} end

Comprendre le problème de l'initialisation de classe

Dans le monde de la programmation, les classes sont un concept fondamental qui vous permet de créer des types de données personnalisés. Dans les exercices précédents, vous avez peut-être créé une classe Structure. Cette classe est un outil utile pour définir facilement des structures de données. Une structure de données est une façon d'organiser et de stocker des données afin qu'elles puissent être accessibles et utilisées efficacement. La classe Structure, en tant que classe de base, s'occupe d'initialiser les attributs en fonction d'une liste prédéfinie de noms de champs. Les attributs sont des variables appartenant à un objet, et les noms de champs sont les noms que nous donnons à ces attributs.

Regardons de plus près l'implémentation actuelle de la classe Structure. Pour ce faire, nous devons ouvrir le fichier structure.py dans l'éditeur de code. Ce fichier contient le code de la classe Structure. Voici les commandes pour accéder au répertoire du projet et ouvrir le fichier :

cd ~/project
code structure.py

La classe Structure fournit un cadre de base pour définir des structures de données simples. Lorsque nous créons une sous-classe, comme la classe Stock, nous pouvons définir les champs spécifiques que nous voulons pour cette sous-classe. Une sous-classe hérite des propriétés et des méthodes de sa classe de base, dans ce cas, la classe Structure. Par exemple, dans la classe Stock, nous définissons les champs name, shares et price :

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

Maintenant, ouvrons le fichier stock.py pour voir comment la classe Stock est implémentée dans le contexte du code global. Ce fichier contient probablement le code qui utilise la classe Stock et interagit avec elle. Utilisez la commande suivante pour ouvrir le fichier :

code stock.py

Bien que cette approche d'utilisation de la classe Structure et de ses sous-classes fonctionne, elle présente plusieurs limitations. Pour identifier ces problèmes, nous allons exécuter l'interpréteur Python et explorer le comportement de la classe Stock. La commande suivante importe la classe Stock et affiche son information d'aide :

python3 -c "from stock import Stock; help(Stock)"

Lorsque vous exécutez cette commande, vous remarquerez que la signature affichée dans la sortie d'aide n'est pas très utile. Au lieu d'afficher les noms de paramètres réels comme name, shares et price, elle n'affiche que *args. Ce manque de noms de paramètres clairs rend difficile pour les utilisateurs de comprendre comment créer correctement une instance de la classe Stock.

Essayons également de créer une instance de Stock en utilisant des arguments nommés (keyword arguments). Les arguments nommés vous permettent de spécifier les valeurs des paramètres par leur nom, ce qui peut rendre le code plus lisible. Exécutez la commande suivante :

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Vous devriez obtenir un message d'erreur comme celui-ci :

TypeError: __init__() got an unexpected keyword argument 'name'

Cette erreur se produit parce que notre méthode __init__ actuelle, qui est responsable de l'initialisation des objets de la classe Stock, ne gère pas les arguments nommés. Elle n'accepte que les arguments positionnels, ce qui signifie que vous devez fournir les valeurs dans un ordre spécifique sans utiliser les noms de paramètres. C'est une limitation que nous voulons corriger dans ce laboratoire.

Dans ce laboratoire, nous allons explorer différentes approches pour rendre notre classe Structure plus flexible et conviviale. En faisant cela, nous pouvons améliorer l'utilisabilité de la classe Stock et des autres sous-classes de Structure.

Utilisation de locals() pour accéder aux arguments d'une fonction

En Python, comprendre les portées (scopes) des variables est crucial. La portée d'une variable détermine où dans le code elle peut être accédée. Python propose une fonction intégrée appelée locals() qui est très pratique pour les débutants afin de comprendre la portée des variables. La fonction locals() renvoie un dictionnaire contenant toutes les variables locales dans la portée actuelle. Cela peut être extrêmement utile lorsque vous souhaitez inspecter les arguments d'une fonction, car cela vous donne une vue claire des variables disponibles dans une partie spécifique de votre code.

Créons une simple expérience dans l'interpréteur Python pour voir comment cela fonctionne. Tout d'abord, nous devons accéder au répertoire du projet et démarrer l'interpréteur Python. Vous pouvez le faire en exécutant les commandes suivantes dans votre terminal :

cd ~/project
python3

Une fois que vous êtes dans le shell interactif Python, nous allons définir une classe Stock. Une classe en Python est comme un modèle pour créer des objets. Dans cette classe, nous allons utiliser la méthode spéciale __init__. La méthode __init__ est un constructeur en Python, ce qui signifie qu'elle est appelée automatiquement lorsqu'un objet de la classe est créé. À l'intérieur de cette méthode __init__, nous allons utiliser la fonction locals() pour afficher toutes les variables locales.

class Stock:
    def __init__(self, name, shares, price):
        print(locals())

Maintenant, créons une instance de cette classe Stock. Une instance est un objet réel créé à partir du modèle de la classe. Nous allons passer quelques valeurs pour les paramètres name, shares et price.

s = Stock('GOOG', 100, 490.1)

Lorsque vous exécutez ce code, vous devriez voir une sortie similaire à :

{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}

Cette sortie montre que locals() nous donne un dictionnaire contenant toutes les variables locales dans la méthode __init__. La référence self est une variable spéciale dans les classes Python qui fait référence à l'instance de la classe elle - même. Les autres variables sont les valeurs des paramètres que nous avons passées lors de la création de l'objet Stock.

Nous pouvons utiliser cette fonctionnalité de locals() pour initialiser automatiquement les attributs d'un objet. Les attributs sont des variables associées à un objet. Définissons une fonction auxiliaire et modifions notre classe Stock.

def _init(locs):
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

class Stock:
    def __init__(self, name, shares, price):
        _init(locals())

La fonction _init prend le dictionnaire des variables locales obtenu à partir de locals(). Elle supprime d'abord la référence self du dictionnaire en utilisant la méthode pop. Ensuite, elle itère sur les paires clé - valeur restantes dans le dictionnaire et utilise la fonction setattr pour définir chaque variable comme un attribut sur l'objet.

Maintenant, testons cette implémentation avec des arguments positionnels et des arguments nommés (keyword arguments). Les arguments positionnels sont passés dans l'ordre dans lequel ils sont définis dans la signature de la fonction, tandis que les arguments nommés sont passés avec les noms de paramètres spécifiés.

## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)

## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)

Les deux approches devraient fonctionner maintenant ! La fonction _init nous permet de gérer à la fois les arguments positionnels et les arguments nommés de manière transparente. Elle préserve également les noms de paramètres dans la signature de la fonction, ce qui rend la sortie de la fonction help() plus utile. La fonction help() en Python fournit des informations sur les fonctions, les classes et les modules, et avoir les noms de paramètres intacts rend ces informations plus significatives.

Une fois que vous avez terminé vos expériences, vous pouvez quitter l'interpréteur Python en exécutant la commande suivante :

exit()

Exploration de l'inspection des cadres de pile (stack frames)

L'approche _init(locals()) que nous avons utilisée est fonctionnelle, mais elle présente un inconvénient. Chaque fois que nous définissons une méthode __init__, nous devons explicitement appeler locals(). Cela peut devenir un peu fastidieux, surtout lorsqu'on travaille avec plusieurs classes. Heureusement, nous pouvons rendre notre code plus propre et plus efficace en utilisant l'inspection des cadres de pile. Cette technique nous permet d'accéder automatiquement aux variables locales de l'appelant sans avoir à appeler locals() explicitement.

Commençons à explorer cette technique dans l'interpréteur Python. Tout d'abord, ouvrez votre terminal et accédez au répertoire du projet. Ensuite, lancez l'interpréteur Python. Vous pouvez le faire en exécutant les commandes suivantes :

cd ~/project
python3

Maintenant que nous sommes dans l'interpréteur Python, nous devons importer le module sys. Le module sys permet d'accéder à certaines variables utilisées ou maintenues par l'interpréteur Python. Nous l'utiliserons pour accéder aux informations sur le cadre de pile.

import sys

Ensuite, nous allons définir une version améliorée de notre fonction _init(). Cette nouvelle version accédera directement au cadre de l'appelant, éliminant ainsi le besoin de passer locals() explicitement.

def _init():
    ## Get the caller's frame (1 level up in the call stack)
    frame = sys._getframe(1)

    ## Get the local variables from that frame
    locs = frame.f_locals

    ## Extract self and set other variables as attributes
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

Dans ce code, sys._getframe(1) récupère l'objet cadre de la fonction appelante. L'argument 1 signifie que nous regardons un niveau plus haut dans la pile d'appels. Une fois que nous avons l'objet cadre, nous pouvons accéder à ses variables locales en utilisant frame.f_locals. Cela nous donne un dictionnaire de toutes les variables locales dans la portée de l'appelant. Nous extrayons ensuite la variable self et définissons les variables restantes comme des attributs de l'objet self.

Maintenant, testons cette nouvelle fonction _init() avec une nouvelle version de notre classe Stock.

class Stock:
    def __init__(self, name, shares, price):
        _init()  ## No need to pass locals() anymore!

## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)

## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)

Comme vous pouvez le voir, la méthode __init__ n'a plus besoin de passer locals() explicitement. Cela rend notre code plus propre et plus facile à lire du point de vue de l'appelant.

Fonctionnement de l'inspection des cadres de pile

Lorsque vous appelez sys._getframe(1), Python renvoie l'objet cadre représentant le cadre d'exécution de l'appelant. L'argument 1 signifie "un niveau au - dessus du cadre actuel" (la fonction appelante).

Un objet cadre contient des informations importantes sur le contexte d'exécution. Cela inclut la fonction actuellement exécutée, les variables locales dans cette fonction et le numéro de ligne actuellement exécutée.

En accédant à frame.f_locals, nous obtenons un dictionnaire de toutes les variables locales dans la portée de l'appelant. C'est similaire à ce que locals() renverrait si appelé directement depuis cette portée.

Cette technique est assez puissante, mais elle doit être utilisée avec prudence. Elle est généralement considérée comme une fonctionnalité avancée de Python et peut sembler un peu "magique" car elle dépasse les limites normales de portée de Python.

Une fois que vous avez terminé vos expériences avec l'inspection des cadres de pile, vous pouvez quitter l'interpréteur Python en exécutant la commande suivante :

exit()

Implémentation de l'initialisation avancée dans la classe Structure

Nous venons de découvrir deux techniques puissantes pour accéder aux arguments d'une fonction. Maintenant, nous allons utiliser ces techniques pour mettre à jour notre classe Structure. Commençons par comprendre pourquoi nous le faisons. Ces techniques rendront notre classe plus flexible et plus facile à utiliser, notamment lorsqu'il s'agit de gérer différents types d'arguments.

Ouvrez le fichier structure.py dans votre éditeur de code. Vous pouvez le faire en exécutant les commandes suivantes dans le terminal. La commande cd permet de changer de répertoire pour accéder au dossier du projet, tandis que la commande code ouvre le fichier structure.py dans l'éditeur de code.

cd ~/project
code structure.py

Remplacez le contenu du fichier par le code suivant. Ce code définit une classe Structure avec plusieurs méthodes. Analysons chaque partie pour comprendre son fonctionnement.

import sys

class Structure:
    _fields = ()

    @staticmethod
    def _init():
        ## Get the caller's frame (the __init__ method that called this)
        frame = sys._getframe(1)

        ## Get the local variables from that frame
        locs = frame.f_locals

        ## Extract self and set other variables as attributes
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({values})'

    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')

Voici ce que nous avons fait dans le code :

  1. Nous avons supprimé l'ancienne méthode __init__(). Étant donné que les sous - classes définiront leurs propres méthodes __init__, nous n'en avons plus besoin.
  2. Nous avons ajouté une nouvelle méthode statique _init(). Cette méthode utilise l'inspection des cadres de pile pour capturer automatiquement tous les paramètres et les définir comme attributs. L'inspection des cadres de pile nous permet d'accéder aux variables locales de la méthode appelante.
  3. Nous avons conservé la méthode __repr__(). Cette méthode fournit une représentation sous forme de chaîne de caractères de l'objet, ce qui est utile pour le débogage et l'affichage.
  4. Nous avons ajouté une méthode __setattr__(). Cette méthode assure la validation des attributs, garantissant que seuls les attributs valides peuvent être définis sur l'objet.

Maintenant, mettons à jour la classe Stock. Ouvrez le fichier stock.py en utilisant la commande suivante :

code stock.py

Remplacez son contenu par le code suivant :

from structure import Structure

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    def __init__(self, name, shares, price):
        self._init()  ## This magically captures and sets all parameters!

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Le changement clé ici est que notre méthode __init__ appelle maintenant self._init() au lieu de définir manuellement chaque attribut. La méthode _init() utilise l'inspection des cadres de pile pour capturer automatiquement tous les paramètres et les définir comme attributs. Cela rend le code plus concis et plus facile à maintenir.

Testons notre implémentation en exécutant les tests unitaires. Les tests unitaires nous aideront à nous assurer que notre code fonctionne comme prévu. Exécutez les commandes suivantes dans le terminal :

cd ~/project
python3 teststock.py

Vous devriez constater que tous les tests passent, y compris le test pour les arguments nommés (keyword arguments) qui échouait auparavant. Cela signifie que notre implémentation fonctionne correctement.

Vérifions également la documentation d'aide pour notre classe Stock. La documentation d'aide fournit des informations sur la classe et ses méthodes. Exécutez la commande suivante dans le terminal :

python3 -c "from stock import Stock; help(Stock)"

Maintenant, vous devriez voir une signature appropriée pour la méthode __init__, affichant tous les noms de paramètres. Cela facilite la compréhension pour les autres développeurs sur la façon d'utiliser la classe.

Enfin, testons de manière interactive que les arguments nommés fonctionnent comme prévu. Exécutez la commande suivante dans le terminal :

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Vous devriez voir l'objet Stock correctement créé avec les attributs spécifiés. Cela confirme que notre système d'initialisation de classe prend en charge les arguments nommés.

Avec cette implémentation, nous avons mis en place un système d'initialisation de classe beaucoup plus flexible et convivial qui :

  1. Préserve les signatures de fonction appropriées dans la documentation, facilitant la compréhension pour les développeurs sur la façon d'utiliser la classe.
  2. Prend en charge à la fois les arguments positionnels et les arguments nommés, offrant plus de flexibilité lors de la création d'objets.
  3. Nécessite un minimum de code boilerplate dans les sous - classes, réduisant ainsi la quantité de code à écrire.
✨ Vérifier la solution et pratiquer

Résumé

Dans ce laboratoire, vous avez appris les règles de portée (scoping) en Python et certaines techniques puissantes pour gérer la portée. Tout d'abord, vous avez exploré comment utiliser la fonction locals() pour accéder à toutes les variables locales d'une fonction. Ensuite, vous avez appris à inspecter les cadres de pile (stack frames) à l'aide de sys._getframe() pour accéder aux variables locales de l'appelant.

Vous avez également appliqué ces techniques pour créer un système d'initialisation de classe flexible. Ce système capture automatiquement les paramètres de fonction et les définit comme attributs d'objet, conserve les signatures de fonction appropriées dans la documentation et prend en charge à la fois les arguments positionnels et les arguments nommés (keyword arguments). Ces techniques mettent en évidence la flexibilité et les capacités d'introspection de Python. Bien que l'inspection des cadres de pile soit une technique avancée qui doit être utilisée avec prudence, elle peut efficacement réduire le code boilerplate lorsqu'elle est utilisée de manière appropriée. Comprendre les règles de portée et ces techniques avancées vous fournit plus d'outils pour écrire un code Python plus propre et plus facilement maintenable.