Code mit exec erstellen

Beginner

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

Einführung

In diesem Lab lernen Sie die exec()-Funktion in Python kennen. Diese Funktion ermöglicht es Ihnen, Python-Code, der als Zeichenkette dargestellt wird, dynamisch auszuführen. Es ist ein leistungsstarkes Feature, das es Ihnen ermöglicht, Code zur Laufzeit zu generieren und auszuführen, wodurch Ihre Programme flexibler und anpassungsfähiger werden.

Die Ziele dieses Labs sind, die grundlegende Verwendung der exec()-Funktion zu lernen, sie zur dynamischen Erstellung von Klassenmethoden zu nutzen und zu untersuchen, wie die Python-Standardbibliothek exec() im Hintergrund einsetzt.

Grundlagen der exec()-Funktion verstehen

In Python ist die exec()-Funktion ein leistungsstarkes Werkzeug, das es Ihnen ermöglicht, Code auszuführen, der zur Laufzeit dynamisch erstellt wird. Dies bedeutet, dass Sie Code "on the fly" basierend auf bestimmten Eingaben oder Konfigurationen generieren können, was in vielen Programmier-Szenarien äußerst nützlich ist.

Beginnen wir damit, die grundlegende Verwendung der exec()-Funktion zu untersuchen. Dazu öffnen wir eine Python-Shell. Öffnen Sie Ihr Terminal und geben Sie python3 ein. Dieser Befehl startet den interaktiven Python-Interpreter, in dem Sie direkt Python-Code ausführen können.

python3

Nun werden wir ein Stück Python-Code als Zeichenkette definieren und dann die exec()-Funktion verwenden, um ihn auszuführen. So funktioniert es:

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

In diesem Beispiel:

  1. Zunächst haben wir eine Zeichenkette namens code definiert. Diese Zeichenkette enthält eine Python-for-Schleife. Die Schleife ist so konzipiert, dass sie n Mal durchläuft und die Nummer jeder Iteration ausgibt.
  2. Dann haben wir eine Variable n definiert und ihr den Wert 10 zugewiesen. Diese Variable wird als obere Grenze für die range()-Funktion in unserer Schleife verwendet.
  3. Danach haben wir die exec()-Funktion mit der code-Zeichenkette als Argument aufgerufen. Die exec()-Funktion nimmt die Zeichenkette und führt sie als Python-Code aus.
  4. Schließlich hat die Schleife ausgeführt und die Zahlen von 0 bis 9 ausgegeben.

Die wirkliche Stärke der exec()-Funktion wird deutlicher, wenn wir sie verwenden, um komplexere Code-Strukturen wie Funktionen oder Methoden zu erstellen. Versuchen wir ein fortgeschritteneres Beispiel, in dem wir dynamisch eine __init__()-Methode für eine Klasse erstellen.

>>> 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

In diesem komplexeren Beispiel:

  1. Zunächst haben wir eine Stock-Klasse mit einem _fields-Attribut definiert. Dieses Attribut ist ein Tupel, das die Namen der Attribute der Klasse enthält.
  2. Dann haben wir eine Zeichenkette erstellt, die Python-Code für eine __init__-Methode darstellt. Diese Methode wird verwendet, um die Attribute des Objekts zu initialisieren.
  3. Als Nächstes haben wir die exec()-Funktion verwendet, um die Code-Zeichenkette auszuführen. Wir haben auch ein leeres Dictionary locs an exec() übergeben. Die resultierende Funktion aus der Ausführung wird in diesem Dictionary gespeichert.
  4. Danach haben wir die im Dictionary gespeicherte Funktion als __init__-Methode unserer Stock-Klasse zugewiesen.
  5. Schließlich haben wir eine Instanz der Stock-Klasse erstellt und überprüft, ob die __init__-Methode korrekt funktioniert, indem wir auf die Attribute des Objekts zugegriffen haben.

Dieses Beispiel zeigt, wie die exec()-Funktion verwendet werden kann, um Methoden dynamisch basierend auf Daten zu erstellen, die zur Laufzeit verfügbar sind.

Dynamisches Erstellen einer __init__()-Methode

Jetzt werden wir das, was wir über die exec()-Funktion gelernt haben, auf ein reales Programmier-Szenario anwenden. In Python ermöglicht die exec()-Funktion die Ausführung von Python-Code, der in einer Zeichenkette gespeichert ist. In diesem Schritt werden wir die Structure-Klasse modifizieren, um eine __init__()-Methode dynamisch zu erstellen. Die __init__()-Methode ist eine spezielle Methode in Python-Klassen, die aufgerufen wird, wenn ein Objekt der Klasse instanziiert wird. Die Erstellung dieser Methode basiert auf der Klassenvariablen _fields, die eine Liste von Feldnamen für die Klasse enthält.

Zunächst werfen wir einen Blick auf die vorhandene Datei structure.py. Diese Datei enthält die aktuelle Implementierung der Structure-Klasse und eine Stock-Klasse, die von ihr erbt. Um den Inhalt der Datei anzuzeigen, öffnen Sie sie im WebIDE mit dem folgenden Befehl:

cat /home/labex/project/structure.py

In der Ausgabe sehen Sie, dass die aktuelle Implementierung einen manuellen Ansatz zur Initialisierung von Objekten verwendet. Dies bedeutet, dass der Code zur Initialisierung der Objektattribute explizit geschrieben wird, anstatt dynamisch generiert zu werden.

Jetzt werden wir die Structure-Klasse modifizieren. Wir werden eine Klassenmethode create_init() hinzufügen, die die __init__()-Methode dynamisch generiert. Um diese Änderungen vorzunehmen, öffnen Sie die Datei structure.py im WebIDE-Editor und befolgen Sie diese Schritte:

  1. Entfernen Sie die vorhandenen Methoden _init() und set_fields() aus der Structure-Klasse. Diese Methoden gehören zum manuellen Initialisierungsansatz, und wir brauchen sie nicht mehr, da wir einen dynamischen Ansatz verwenden werden.

  2. Fügen Sie die Klassenmethode create_init() zur Structure-Klasse hinzu. Hier ist der Code für die Methode:

@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__'])

In dieser Methode erstellen wir zunächst eine Zeichenkette argstr, die alle Feldnamen durch Kommas getrennt enthält. Diese Zeichenkette wird als Argumentliste für die __init__()-Methode verwendet. Dann erstellen wir den Code für die __init__()-Methode als Zeichenkette. Wir gehen durch die Feldnamen in einer Schleife und fügen Zeilen zum Code hinzu, die jedem Argument das entsprechende Objektattribut zuweisen. Danach verwenden wir die exec()-Funktion, um den Code auszuführen und die generierte Funktion im locs-Dictionary zu speichern. Schließlich verwenden wir die setattr()-Funktion, um die generierte Funktion als __init__()-Methode der Klasse festzulegen.

  1. Modifizieren Sie die Stock-Klasse, um diesen neuen Ansatz zu verwenden:
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

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

Hier definieren wir die _fields für die Stock-Klasse und rufen dann die Methode create_init() auf, um die __init__()-Methode für die Stock-Klasse zu generieren.

Ihre vollständige structure.py-Datei sollte jetzt in etwa so aussehen:

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()

Jetzt testen wir unsere Implementierung, um sicherzustellen, dass sie korrekt funktioniert. Wir werden die Unittests ausführen, um zu überprüfen, ob alle Tests erfolgreich sind. Verwenden Sie die folgenden Befehle:

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

Wenn Ihre Implementierung korrekt ist, sollten Sie sehen, dass alle Tests erfolgreich sind. Dies bedeutet, dass die dynamisch generierte __init__()-Methode wie erwartet funktioniert.

Sie können die Klasse auch manuell in der Python-Shell testen. So können Sie es tun:

>>> 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

In der Python-Shell importieren wir zunächst die Stock-Klasse aus der Datei structure.py. Dann erstellen wir eine Instanz der Stock-Klasse und geben sie aus. Wir können auch das Attribut shares des Objekts ändern. Wenn wir jedoch versuchen, ein Attribut festzulegen, das nicht in der _fields-Liste vorhanden ist, sollten wir einen AttributeError erhalten.

Herzlichen Glückwunsch! Sie haben erfolgreich die exec()-Funktion verwendet, um eine __init__()-Methode basierend auf Klassenattributen dynamisch zu erstellen. Dieser Ansatz macht Ihren Code flexibler und einfacher zu warten, insbesondere wenn es um Klassen mit einer variablen Anzahl von Attributen geht.

Untersuchung der Verwendung von exec() in Python's Standardbibliothek

In Python ist die Standardbibliothek eine leistungsstarke Sammlung von vordefiniertem Code, die verschiedene nützliche Funktionen und Module bietet. Eine solche Funktion ist exec(), die zur dynamischen Generierung und Ausführung von Python-Code verwendet werden kann. Dynamische Codegenerierung bedeutet, dass der Code während der Programmausführung "on the fly" erstellt wird, anstatt hartcodiert zu sein.

Die namedtuple-Funktion aus dem collections-Modul ist ein bekanntes Beispiel in der Standardbibliothek, das exec() verwendet. Ein namedtuple ist eine spezielle Art von Tupel, das es Ihnen ermöglicht, auf seine Elemente sowohl über Attributnamen als auch über Indizes zuzugreifen. Es ist ein praktisches Werkzeug zum Erstellen einfacher Datenträgerklassen, ohne dass Sie eine vollständige Klassendefinition schreiben müssen.

Lassen Sie uns untersuchen, wie namedtuple funktioniert und wie es exec() im Hintergrund verwendet. Zunächst öffnen Sie Ihre Python-Shell. Sie können dies tun, indem Sie den folgenden Befehl in Ihrem Terminal ausführen. Dieser Befehl startet einen Python-Interpreter, in dem Sie direkt Python-Code ausführen können:

python3

Jetzt sehen wir uns an, wie die namedtuple-Funktion verwendet wird. Der folgende Code zeigt, wie man ein namedtuple erstellt und auf seine Elemente zugreift:

>>> 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

Im obigen Code importieren wir zunächst die namedtuple-Funktion aus dem collections-Modul. Dann erstellen wir einen neuen namedtuple-Typ namens Stock mit den Feldern name, shares und price. Wir erstellen eine Instanz s des Stock-namedtuple und greifen auf seine Elemente sowohl über Attributnamen (s.name, s.shares) als auch über Indizes (s[1]) zu.

Jetzt werfen wir einen Blick auf die Implementierung von namedtuple. Wir können das inspect-Modul verwenden, um seinen Quellcode anzuzeigen. Das inspect-Modul bietet mehrere nützliche Funktionen, um Informationen über lebende Objekte wie Module, Klassen, Methoden usw. zu erhalten.

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

Wenn Sie diesen Code ausführen, wird eine große Menge an Code ausgegeben. Wenn Sie genau hinsehen, werden Sie feststellen, dass namedtuple die exec()-Funktion verwendet, um eine Klasse dynamisch zu erstellen. Es konstruiert eine Zeichenkette, die Python-Code für eine Klassendefinition enthält. Dann verwendet es exec(), um diese Zeichenkette als Python-Code auszuführen.

Dieser Ansatz ist sehr leistungsstark, da er es namedtuple ermöglicht, Klassen mit benutzerdefinierten Feldnamen zur Laufzeit zu erstellen. Die Feldnamen werden durch die Argumente bestimmt, die Sie an die namedtuple-Funktion übergeben. Dies ist ein reales Beispiel dafür, wie exec() zur dynamischen Codegenerierung verwendet werden kann.

Hier sind einige wichtige Punkte zur Implementierung von namedtuple zu beachten:

  1. Es verwendet String-Formatierung, um eine Klassendefinition zu konstruieren. String-Formatierung ist eine Möglichkeit, Werte in eine String-Vorlage einzufügen. Im Fall von namedtuple wird dies verwendet, um eine Klassendefinition mit den richtigen Feldnamen zu erstellen.
  2. Es behandelt die Validierung von Feldnamen. Dies bedeutet, dass es überprüft, ob die von Ihnen angegebenen Feldnamen gültige Python-Bezeichner sind. Wenn nicht, wird eine entsprechende Fehlermeldung ausgelöst.
  3. Es bietet zusätzliche Funktionen wie Docstrings und Methoden. Docstrings sind Strings, die den Zweck und die Verwendung einer Klasse oder Funktion dokumentieren. namedtuple fügt nützliche Docstrings und Methoden zu den von ihm erstellten Klassen hinzu.
  4. Es führt den generierten Code mit exec() aus. Dies ist der Kernschritt, der die Zeichenkette mit der Klassendefinition in eine echte Python-Klasse verwandelt.

Dieses Muster ähnelt dem, das wir in unserer create_init()-Methode implementiert haben, aber auf einem ausgefeilteren Niveau. Die namedtuple-Implementierung muss komplexere Szenarien und Randfälle behandeln, um eine robuste und benutzerfreundliche Schnittstelle bereitzustellen.

Zusammenfassung

In diesem Lab haben Sie gelernt, wie Sie die exec()-Funktion in Python verwenden können, um Code dynamisch zur Laufzeit zu erstellen und auszuführen. Die Schlüsselpunkte umfassen die grundlegende Verwendung von exec() zum Ausführen von zeichenkettenbasierten Codefragmenten, die fortgeschrittene Verwendung zur dynamischen Erstellung von Klassenmethoden basierend auf Attributen und ihre reale Anwendung in Python's Standardbibliothek mit namedtuple.

Die Fähigkeit, Code dynamisch zu generieren, ist ein leistungsstarkes Merkmal, das flexiblere und anpassungsfähigere Programme ermöglicht. Obwohl es aufgrund von Sicherheits- und Lesbarkeitsproblemen mit Vorsicht eingesetzt werden sollte, ist es ein wertvolles Werkzeug für Python-Programmierer in bestimmten Szenarien, wie z.B. beim Erstellen von APIs, Implementieren von Dekoratoren oder Bauen von domänenspezifischen Sprachen. Sie können diese Techniken anwenden, wenn Sie Code erstellen, der sich an Laufzeitbedingungen anpasst, oder Frameworks bauen, die Code basierend auf Konfigurationen generieren.