Créer du code avec exec

Beginner

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

Introduction

Dans ce laboratoire, vous allez apprendre à utiliser la fonction exec() en Python. Cette fonction vous permet d'exécuter dynamiquement du code Python représenté sous forme de chaîne de caractères. C'est une fonctionnalité puissante qui vous permet de générer et d'exécuter du code à l'exécution, rendant vos programmes plus flexibles et adaptables.

Les objectifs de ce laboratoire sont d'apprendre l'utilisation de base de la fonction exec(), de l'utiliser pour créer dynamiquement des méthodes de classe et d'examiner comment la bibliothèque standard de Python utilise exec() en coulisse.

Comprendre les bases de exec()

En Python, la fonction exec() est un outil puissant qui vous permet d'exécuter du code créé dynamiquement à l'exécution. Cela signifie que vous pouvez générer du code à la volée en fonction de certaines entrées ou configurations, ce qui est extrêmement utile dans de nombreux scénarios de programmation.

Commençons par explorer l'utilisation de base de la fonction exec(). Pour ce faire, nous allons ouvrir un shell Python. Ouvrez votre terminal et tapez python3. Cette commande lancera l'interpréteur Python interactif, où vous pouvez exécuter directement du code Python.

python3

Maintenant, nous allons définir un morceau de code Python sous forme de chaîne de caractères, puis utiliser la fonction exec() pour l'exécuter. Voici comment cela fonctionne :

>>> code = '''
for i in range(n):
    print(i, end=' ')
'''
>>> n = 10
>>> exec(code)
0 1 2 3 4 5 6 7 8 9

Dans cet exemple :

  1. Tout d'abord, nous avons défini une chaîne de caractères nommée code. Cette chaîne contient une boucle for Python. La boucle est conçue pour itérer n fois et afficher chaque numéro d'itération.
  2. Ensuite, nous avons défini une variable n et lui avons assigné la valeur 10. Cette variable est utilisée comme borne supérieure pour la fonction range() dans notre boucle.
  3. Après cela, nous avons appelé la fonction exec() avec la chaîne code comme argument. La fonction exec() prend la chaîne et l'exécute comme code Python.
  4. Enfin, la boucle s'est exécutée et a affiché les nombres de 0 à 9.

Le véritable potentiel de la fonction exec() devient plus évident lorsque nous l'utilisons pour créer des structures de code plus complexes, telles que des fonctions ou des méthodes. Essayons un exemple plus avancé où nous allons créer dynamiquement une méthode __init__() pour une classe.

>>> class Stock:
...     _fields = ('name', 'shares', 'price')
...
>>> argstr = ','.join(Stock._fields)
>>> code = f'def __init__(self, {argstr}):\n'
>>> for name in Stock._fields:
...     code += f'    self.{name} = {name}\n'
...
>>> print(code)
def __init__(self, name,shares,price):
    self.name = name
    self.shares = shares
    self.price = price

>>> locs = { }
>>> exec(code, locs)
>>> Stock.__init__ = locs['__init__']

>>> ## Now try the class
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s.price
490.1

Dans cet exemple plus complexe :

  1. Nous avons d'abord défini une classe Stock avec un attribut _fields. Cet attribut est un tuple qui contient les noms des attributs de la classe.
  2. Ensuite, nous avons créé une chaîne qui représente le code Python pour une méthode __init__. Cette méthode est utilisée pour initialiser les attributs de l'objet.
  3. Ensuite, nous avons utilisé la fonction exec() pour exécuter la chaîne de code. Nous avons également passé un dictionnaire vide locs à exec(). La fonction résultante de l'exécution est stockée dans ce dictionnaire.
  4. Après cela, nous avons assigné la fonction stockée dans le dictionnaire comme méthode __init__ de notre classe Stock.
  5. Enfin, nous avons créé une instance de la classe Stock et vérifié que la méthode __init__ fonctionne correctement en accédant aux attributs de l'objet.

Cet exemple montre comment la fonction exec() peut être utilisée pour créer dynamiquement des méthodes en fonction de données disponibles à l'exécution.

Création d'une méthode init() dynamique

Maintenant, nous allons appliquer ce que nous avons appris sur la fonction exec() à un scénario de programmation du monde réel. En Python, la fonction exec() vous permet d'exécuter du code Python stocké dans une chaîne de caractères. Dans cette étape, nous allons modifier la classe Structure pour créer dynamiquement une méthode __init__(). La méthode __init__() est une méthode spéciale dans les classes Python, qui est appelée lorsque un objet de la classe est instancié. Nous allons baser la création de cette méthode sur la variable de classe _fields, qui contient une liste de noms de champs pour la classe.

Tout d'abord, regardons le fichier structure.py existant. Ce fichier contient l'implémentation actuelle de la classe Structure et d'une classe Stock qui en hérite. Pour afficher le contenu du fichier, ouvrez - le dans l'éditeur WebIDE en utilisant la commande suivante :

cat /home/labex/project/structure.py

Dans la sortie, vous verrez que l'implémentation actuelle utilise une approche manuelle pour gérer l'initialisation des objets. Cela signifie que le code pour initialiser les attributs de l'objet est écrit explicitement, plutôt que d'être généré dynamiquement.

Maintenant, nous allons modifier la classe Structure. Nous allons ajouter une méthode de classe create_init() qui générera dynamiquement la méthode __init__(). Pour apporter ces modifications, ouvrez le fichier structure.py dans l'éditeur WebIDE et suivez ces étapes :

  1. Supprimez les méthodes _init() et set_fields() existantes de la classe Structure. Ces méthodes font partie de l'approche d'initialisation manuelle, et nous n'en aurons plus besoin car nous allons utiliser une approche dynamique.

  2. Ajoutez la méthode de classe create_init() à la classe Structure. Voici le code de la méthode :

@classmethod
def create_init(cls):
    """Dynamically create an __init__ method based on _fields."""
    ## Create argument string from field names
    argstr = ','.join(cls._fields)

    ## Create the function code as a string
    code = f'def __init__(self, {argstr}):\n'
    for name in cls._fields:
        code += f'    self.{name} = {name}\n'

    ## Execute the code and get the generated function
    locs = {}
    exec(code, locs)

    ## Set the function as the __init__ method of the class
    setattr(cls, '__init__', locs['__init__'])

Dans cette méthode, nous créons d'abord une chaîne argstr qui contient tous les noms de champs séparés par des virgules. Cette chaîne sera utilisée comme liste d'arguments pour la méthode __init__(). Ensuite, nous créons le code pour la méthode __init__() sous forme de chaîne. Nous parcourons les noms de champs et ajoutons des lignes au code qui attribuent chaque argument à l'attribut d'objet correspondant. Après cela, nous utilisons la fonction exec() pour exécuter le code et stockons la fonction générée dans le dictionnaire locs. Enfin, nous utilisons la fonction setattr() pour définir la fonction générée comme méthode __init__() de la classe.

  1. Modifiez la classe Stock pour utiliser cette nouvelle approche :
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

## Create the __init__ method for Stock
Stock.create_init()

Ici, nous définissons les _fields pour la classe Stock, puis nous appelons la méthode create_init() pour générer la méthode __init__() pour la classe Stock.

Votre fichier structure.py complet devrait maintenant ressembler à ceci :

class Structure:
    ## Restrict attribute assignment
    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f"No attribute {name}")

    ## String representation for debugging
    def __repr__(self):
        args = ', '.join(repr(getattr(self, name)) for name in self._fields)
        return f"{type(self).__name__}({args})"

    @classmethod
    def create_init(cls):
        """Dynamically create an __init__ method based on _fields."""
        ## Create argument string from field names
        argstr = ','.join(cls._fields)

        ## Create the function code as a string
        code = f'def __init__(self, {argstr}):\n'
        for name in cls._fields:
            code += f'    self.{name} = {name}\n'

        ## Execute the code and get the generated function
        locs = {}
        exec(code, locs)

        ## Set the function as the __init__ method of the class
        setattr(cls, '__init__', locs['__init__'])

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

## Create the __init__ method for Stock
Stock.create_init()

Maintenant, testons notre implémentation pour nous assurer qu'elle fonctionne correctement. Nous allons exécuter le fichier de tests unitaires pour vérifier si tous les tests passent. Utilisez les commandes suivantes :

cd /home/labex/project
python3 -m unittest test_structure.py

Si votre implémentation est correcte, vous devriez voir que tous les tests passent. Cela signifie que la méthode __init__() générée dynamiquement fonctionne comme prévu.

Vous pouvez également tester la classe manuellement dans le shell Python. Voici comment vous pouvez le faire :

>>> from structure import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s
Stock('GOOG', 100, 490.1)
>>> s.shares = 50
>>> s.share = 50  ## This should raise an AttributeError
Traceback (most recent call last):
  ...
AttributeError: No attribute share

Dans le shell Python, nous importons d'abord la classe Stock à partir du fichier structure.py. Ensuite, nous créons une instance de la classe Stock et l'affichons. Nous pouvons également modifier l'attribut shares de l'objet. Cependant, lorsque nous essayons de définir un attribut qui n'existe pas dans la liste _fields, nous devrions obtenir une AttributeError.

Félicitations ! Vous avez utilisé avec succès la fonction exec() pour créer dynamiquement une méthode __init__() en fonction des attributs de classe. Cette approche peut rendre votre code plus flexible et plus facile à maintenir, en particulier lorsqu'il s'agit de classes ayant un nombre variable d'attributs.

Examiner l'utilisation de exec() dans la bibliothèque standard de Python

En Python, la bibliothèque standard est une collection puissante de code pré - écrit qui offre diverses fonctions et modules utiles. Une de ces fonctions est exec(), qui peut être utilisée pour générer et exécuter dynamiquement du code Python. Générer du code dynamiquement signifie créer du code à la volée pendant l'exécution du programme, plutôt que de l'avoir codé en dur.

La fonction namedtuple du module collections est un exemple bien connu dans la bibliothèque standard qui utilise exec(). Un namedtuple est un type spécial de tuple qui vous permet d'accéder à ses éléments à la fois par noms d'attributs et par indices. C'est un outil pratique pour créer de simples classes de stockage de données sans avoir à écrire une définition de classe complète.

Explorons comment namedtuple fonctionne et comment il utilise exec() en coulisse. Tout d'abord, ouvrez votre shell Python. Vous pouvez le faire en exécutant la commande suivante dans votre terminal. Cette commande lance un interpréteur Python où vous pouvez exécuter directement du code Python :

python3

Maintenant, voyons comment utiliser la fonction namedtuple. Le code suivant montre comment créer un namedtuple et accéder à ses éléments :

>>> from collections import namedtuple
>>> Stock = namedtuple('Stock', ['name', 'shares', 'price'])
>>> s = Stock('GOOG', 100, 490.1)
>>> s.name
'GOOG'
>>> s.shares
100
>>> s[1]  ## namedtuples also support indexing
100

Dans le code ci - dessus, nous importons d'abord la fonction namedtuple du module collections. Ensuite, nous créons un nouveau type namedtuple appelé Stock avec les champs name, shares et price. Nous créons une instance s du namedtuple Stock et accédons à ses éléments à la fois par noms d'attributs (s.name, s.shares) et par indice (s[1]).

Maintenant, regardons comment namedtuple est implémenté. Nous pouvons utiliser le module inspect pour afficher son code source. Le module inspect fournit plusieurs fonctions utiles pour obtenir des informations sur des objets en temps réel tels que des modules, des classes, des méthodes, etc.

>>> import inspect
>>> from collections import namedtuple
>>> print(inspect.getsource(namedtuple))

Lorsque vous exécutez ce code, vous verrez une grande quantité de code affichée. Si vous regardez attentivement, vous trouverez que namedtuple utilise la fonction exec() pour créer dynamiquement une classe. Ce qu'il fait, c'est construire une chaîne de caractères qui contient le code Python pour une définition de classe. Ensuite, il utilise exec() pour exécuter cette chaîne comme code Python.

Cette approche est très puissante car elle permet à namedtuple de créer des classes avec des noms de champs personnalisés à l'exécution. Les noms de champs sont déterminés par les arguments que vous passez à la fonction namedtuple. C'est un exemple concret de l'utilisation de exec() pour générer du code dynamiquement.

Voici quelques points clés à noter concernant l'implémentation de namedtuple :

  1. Il utilise le formatage de chaînes pour construire une définition de classe. Le formatage de chaînes est un moyen d'insérer des valeurs dans un modèle de chaîne. Dans le cas de namedtuple, il l'utilise pour créer une définition de classe avec les bons noms de champs.
  2. Il gère la validation des noms de champs. Cela signifie qu'il vérifie si les noms de champs que vous fournissez sont des identifiants Python valides. Sinon, il lèvera une erreur appropriée.
  3. Il offre des fonctionnalités supplémentaires telles que des docstrings (chaînes de documentation) et des méthodes. Les docstrings sont des chaînes qui documentent le but et l'utilisation d'une classe ou d'une fonction. namedtuple ajoute des docstrings et des méthodes utiles aux classes qu'il crée.
  4. Il exécute le code généré à l'aide de exec(). C'est l'étape essentielle qui transforme la chaîne contenant la définition de classe en une véritable classe Python.

Ce modèle est similaire à ce que nous avons implémenté dans notre méthode create_init(), mais à un niveau plus sophistiqué. L'implémentation de namedtuple doit gérer des scénarios et des cas limites plus complexes pour offrir une interface robuste et conviviale.

Résumé

Dans ce laboratoire, vous avez appris à utiliser la fonction exec() de Python pour créer et exécuter dynamiquement du code à l'exécution. Les points clés incluent l'utilisation de base de exec() pour exécuter des fragments de code sous forme de chaînes de caractères, l'utilisation avancée pour créer dynamiquement des méthodes de classe en fonction d'attributs, et son application dans le monde réel dans la bibliothèque standard de Python avec namedtuple.

La capacité à générer du code dynamiquement est une fonctionnalité puissante qui permet de créer des programmes plus flexibles et adaptables. Bien qu'elle doive être utilisée avec prudence en raison de problèmes de sécurité et de lisibilité, c'est un outil précieux pour les programmeurs Python dans des scénarios spécifiques tels que la création d'API (Application Programming Interface), la mise en œuvre de décorateurs ou la construction de langages spécifiques au domaine. Vous pouvez appliquer ces techniques lors de la création de code qui s'adapte aux conditions d'exécution ou de la construction de frameworks qui génèrent du code en fonction de la configuration.