Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 511 – API für Code-Transformer

Autor:
Victor Stinner <vstinner at python.org>
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
04-Jan-2016
Python-Version:
3.6

Inhaltsverzeichnis

Ablehnungsbescheid

Dieser PEP wurde von seinem Autor abgelehnt.

Dieser PEP wurde als Ermöglichung neuer, Python-ähnlicher Programmiersprachen gesehen, die zwar nah, aber inkompatibel mit regulärem Python sind. Es wurde entschieden, Syntaxen, die mit Python inkompatibel sind, nicht zu fördern.

Dieser PEP wurde auch als ein nützliches Werkzeug zum Experimentieren mit neuen Python-Features angesehen, aber es ist bereits möglich, diese ohne den PEP, nur mit `importlib`-Hooks, zu experimentieren. Wenn eine Funktion nützlich wird, sollte sie direkt Teil von Python sein, anstatt von einem externen Python-Modul abzuhängen.

Schließlich wurde dieser PEP vom FAT Python Optimization Project vorangetrieben, das 2016 aufgegeben wurde, da es nicht möglich war, signifikante Geschwindigkeitssteigerungen zu zeigen, aber auch wegen des Mangels an Zeit, die fortschrittlichsten und komplexesten Optimierungen zu implementieren.

Zusammenfassung

Schlägt eine API zur Registrierung von Bytecode- und AST-Transformern vor. Fügt außerdem die Befehlszeilenoption `-o OPTIM_TAG hinzu, um .pyc-Dateinamen zu ändern. `-o noopt deaktiviert den Peephole-Optimierer. Löst beim Import eine `ImportError`-Ausnahme aus, wenn die .pyc-Datei fehlt und die zur Transformation des Codes erforderlichen Code-Transformer fehlen. Code-Transformer werden nicht benötigt, um den transformierten Code vorab (aus .pyc-Dateien) zu laden.

Begründung

Python bietet keine standardmäßige Methode zur Code-Transformation. Projekte, die Code transformieren, verwenden verschiedene Hooks. Das MacroPy-Projekt verwendet einen Import-Hook: Es fügt seinen eigenen Modul-Finder zu sys.meta_path hinzu, um seinen AST-Transformer einzuhaken. Eine andere Option ist, die eingebaute compile()-Funktion per Monkey-Patching zu modifizieren. Es gibt noch mehr Optionen, um einen Code-Transformer einzuhaken.

Python 3.4 fügte der importlib.abc.SourceLoader eine compile_source()-Methode hinzu. Code-Transformation ist jedoch breiter als nur das Importieren von Modulen, siehe die unten beschriebenen Anwendungsfälle.

Das Schreiben eines Optimierers oder Präprozessors liegt außerhalb des Umfangs dieses PEPs.

Anwendungsfall 1: AST-Optimierer

Die Transformation eines Abstract Syntax Tree (AST) ist eine praktische Methode zur Implementierung eines Optimierers. Es ist einfacher, mit dem AST als mit dem Bytecode zu arbeiten, da der AST mehr Informationen enthält und auf einer höheren Ebene angesiedelt ist.

Da die Optimierung im Voraus erfolgen kann, können komplexe, aber langsame Optimierungen implementiert werden.

Beispiele für Optimierungen, die mit einem AST-Optimierer implementiert werden können

Unter Verwendung von Guards (siehe PEP 510) ist es möglich, eine viel größere Auswahl an Optimierungen zu implementieren. Beispiele:

Die folgenden Probleme können mit einem AST-Optimierer implementiert werden:

  • Issue #1346238: Eine Konstantenfaltungs-Optimierungsphase für den AST
  • Issue #2181: Lokale Variablen am Ende einer Funktion optimiert entfernen
  • Issue #2499: Unäre Operatoren `+` und `not` auf Konstanten falten
  • Issue #4264: Patch: Code optimieren, um `LIST_APPEND` zu verwenden, anstatt `list.append` aufzurufen
  • Issue #7682: Optimierung von `if`-Anweisungen mit konstanten Ausdrücken
  • Issue #10399: AST-Optimierung: Inlining von Funktionsaufrufen
  • Issue #11549: Aufbau eines AST-Optimierers, der einige Funktionalitäten aus dem Peephole-Optimierer verschiebt
  • Issue #17068: Peephole-Optimierung für konstante Strings
  • Issue #17430: Verpasste Peephole-Optimierung

Anwendungsfall 2: Präprozessor

Ein Präprozessor kann einfach mit einem AST-Transformer implementiert werden. Ein Präprozessor hat verschiedene und unterschiedliche Anwendungsfälle.

Einige Beispiele

MacroPy hat eine lange Liste von Beispielen und Anwendungsfällen.

Dieser PEP fügt keine neuen Code-Transformer hinzu. Die Verwendung eines Code-Transformers erfordert ein externes Modul und dessen manuelle Registrierung.

Siehe auch PyXfuscator: Python Obfuscator, Deobfuscator und benutzergesteuerter Dekompiler.

Anwendungsfall 3: Deaktivieren aller Optimierungen

Ned Batchelder bat darum, eine Option zum Deaktivieren des Peephole-Optimierers hinzuzufügen, da dieser die Implementierung von Code Coverage erschwert. Siehe die Diskussion in der Python-Ideas-Mailingliste: Alle Peephole-Optimierungen deaktivieren.

Dieser PEP fügt eine neue Befehlszeilenoption `-o noopt hinzu, um den Peephole-Optimierer zu deaktivieren. In Python ist das so einfach wie

sys.set_code_transformers([])

Dies wird Issue #2506 beheben: Mechanismus zum Deaktivieren von Optimierungen hinzufügen.

Anwendungsfall 4: Neue Bytecode-Optimierer in Python schreiben

Python 3.6 optimiert den Code mit einem Peephole-Optimierer. Per Definition hat ein Peephole-Optimierer eine eng begrenzte Sicht auf den Code und kann daher nur grundlegende Optimierungen implementieren. Der Optimierer schreibt den Bytecode neu. Es ist schwierig, ihn zu erweitern, da er in C geschrieben ist.

Mit diesem PEP wird es möglich, einen neuen Bytecode-Optimierer in reinem Python zu implementieren und neue Optimierungen zu experimentieren.

Einige Optimierungen sind auf dem AST einfacher zu implementieren, wie z. B. Konstantenfaltung, aber Optimierungen auf dem Bytecode sind immer noch nützlich. Zum Beispiel können, wenn der AST in Bytecode kompiliert wird, nutzlose Sprünge erzeugt werden, da der Compiler naiv ist und versucht, nichts zu optimieren.

Anwendungsfälle

Dieser Abschnitt gibt Beispiele für Anwendungsfälle, die erklären, wann und wie Code-Transformer verwendet werden.

Interaktiver Interpreter

Code-Transformer können mit dem interaktiven Interpreter verwendet werden, der in Python beliebt und üblicherweise zur Demonstration von Python verwendet wird.

Der Code wird zur Laufzeit transformiert, daher kann der Interpreter langsamer sein, wenn teure Code-Transformer verwendet werden.

Erstellen eines transformierten Pakets

Es wird möglich sein, ein Paket mit transformiertem Code zu erstellen.

Ein Transformer kann eine Konfiguration haben. Die Konfiguration wird nicht im Paket gespeichert.

Alle .pyc-Dateien des Pakets müssen mit denselben Code-Transformern und derselben Transformer-Konfiguration transformiert werden.

Es ist möglich, verschiedene .pyc-Dateien mit unterschiedlichen Optimizer-Tags zu erstellen. Beispiel: `fat für die Standardkonfiguration und `fat_inline für eine andere Konfiguration mit aktivierter Funktionsinlining.

Ein Paket kann .pyc-Dateien mit verschiedenen Optimizer-Tags enthalten.

Installieren eines Pakets, das transformierte .pyc-Dateien enthält

Es wird möglich sein, ein Paket zu installieren, das transformierte .pyc-Dateien enthält.

Alle .pyc-Dateien mit einem beliebigen Optimizer-Tag im Paket werden installiert, nicht nur die für den aktuellen Optimizer-Tag.

Erstellen von .pyc-Dateien bei der Installation eines Pakets

Wenn ein Paket keine .pyc-Dateien für den aktuellen Optimizer-Tag enthält (oder einige .pyc-Dateien fehlen), werden die .pyc-Dateien während der Installation erstellt.

Code-Transformer des Optimizer-Tags sind erforderlich. Andernfalls schlägt die Installation mit einem Fehler fehl.

Ausführen transformierten Codes

Es wird möglich sein, transformierten Code auszuführen.

Löst beim Import eine `ImportError`-Ausnahme aus, wenn die .pyc-Datei des aktuellen Optimizer-Tags fehlt und die zur Transformation des Codes erforderlichen Code-Transformer fehlen.

Der interessante Punkt hierbei ist, dass Code-Transformer nicht benötigt werden, um den transformierten Code auszuführen, wenn alle erforderlichen .pyc-Dateien bereits verfügbar sind.

Code-Transformer-API

Ein Code-Transformer ist eine Klasse mit den Methoden ast_transformer() und/oder code_transformer() (API siehe unten) und einem name-Attribut.

Definieren Sie aus Effizienzgründen keine code_transformer()- oder ast_transformer()-Methode, wenn sie nichts tut.

Das name-Attribut (str) muss ein kurzer String sein, der zur Identifizierung eines Optimierers verwendet wird. Es wird zum Erstellen eines .pyc-Dateinamens verwendet. Der Name darf keine Punkte (`.`), Bindestriche (`-`) oder Verzeichnistrennzeichen enthalten: Punkte werden zur Trennung von Feldern in einem .pyc-Dateinamen verwendet, und Bindestriche werden zur Verbindung von Code-Transformernamen verwendet, um den Optimizer-Tag zu erstellen.

Hinweis

Es wäre schön, den vollqualifizierten Namen eines Moduls im *Kontext* zu übergeben, wenn ein AST-Transformer zum Transformieren eines Moduls beim Import verwendet wird, aber es scheint, dass diese Information in PyParser_ASTFromStringObject() nicht verfügbar ist.

code_transformer()-Methode

Prototyp

def code_transformer(self, code, context):
    ...
    new_code = ...
    ...
    return new_code

Parameter

  • code: Code-Objekt
  • context: Ein Objekt mit einem `optimize`-Attribut (int), der Optimierungsstufe (0, 1 oder 2). Der Wert des `optimize`-Attributs stammt vom `optimize`-Parameter der compile()-Funktion, standardmäßig ist er gleich sys.flags.optimize.

Jede Implementierung von Python kann dem *Kontext* zusätzliche Attribute hinzufügen. Zum Beispiel wird unter CPython der *Kontext* auch das folgende Attribut haben:

  • interactive (bool): Wahr, wenn im interaktiven Modus

XXX weitere Flags hinzufügen?

XXX Flags-Integer durch einen Sub-Namespace oder spezifische Attribute ersetzen?

Die Methode muss ein Code-Objekt zurückgeben.

Der Code-Transformer wird nach der Kompilierung in Bytecode ausgeführt.

ast_transformer()-Methode

Prototyp

def ast_transformer(self, tree, context):
    ...
    return tree

Parameter

  • tree: Ein AST-Baum
  • context: Ein Objekt mit einem `filename`-Attribut (str)

Es muss einen AST-Baum zurückgeben. Es kann den AST-Baum vor Ort modifizieren oder einen neuen AST-Baum erstellen.

Der AST-Transformer wird nach der Erstellung des AST durch den Parser und vor der Kompilierung in Bytecode aufgerufen. Neue Attribute können zukünftig zum *Kontext* hinzugefügt werden.

Änderungen

Kurz gesagt, fügen Sie hinzu:

  • -o OPTIM_TAG Befehlszeilenoption
  • sys.implementation.optim_tag
  • sys.get_code_transformers()
  • sys.set_code_transformers(transformers)
  • ast.PyCF_TRANSFORMED_AST

API zum Abrufen/Setzen von Code-Transformern

Neue Funktionen zum Registrieren von Code-Transformern hinzufügen

  • sys.set_code_transformers(transformers): Legt die Liste der Code-Transformer fest und aktualisiert sys.implementation.optim_tag
  • sys.get_code_transformers(): Ruft die Liste der Code-Transformer ab.

Die Reihenfolge der Code-Transformer ist wichtig. Wenn Transformer A und dann Transformer B ausgeführt wird, kann dies zu einem anderen Ergebnis führen als wenn Transformer B und dann Transformer A ausgeführt wird.

Beispiel zum Voranstellen eines neuen Code-Transformers

transformers = sys.get_code_transformers()
transformers.insert(0, new_cool_transformer)
sys.set_code_transformers(transformers)

Alle AST-Transformer werden sequenziell ausgeführt (z. B. der zweite Transformer erhält die Eingabe des ersten Transformers), und dann werden alle Bytecode-Transformer sequenziell ausgeführt.

Optimizer-Tag

Änderungen

  • Fügen Sie sys.implementation.optim_tag (str) hinzu: Optimierungs-Tag. Der Standard-Optimierungs-Tag ist 'opt'.
  • Fügen Sie eine neue Befehlszeilenoption `-o OPTIM_TAG hinzu, um sys.implementation.optim_tag zu setzen.

Änderungen an importlib

  • importlib verwendet sys.implementation.optim_tag, um den .pyc-Dateinamen für das Importieren von Modulen zu erstellen, anstatt immer opt zu verwenden. Entfernen Sie auch den Sonderfall für die Optimierungsstufe 0 mit dem Standard-Optimizer-Tag 'opt', um den Code zu vereinfachen.
  • Beim Laden eines Moduls, wenn die .pyc-Datei fehlt, aber die .py-Datei verfügbar ist, wird die .py-Datei nur verwendet, wenn die Code-Optimierer denselben Optimizer-Tag wie der aktuelle Tag haben, andernfalls wird eine `ImportError`-Ausnahme ausgelöst.

Pseudocode einer `use_py()`-Funktion, um zu entscheiden, ob eine .py-Datei kompiliert werden kann, um ein Modul zu importieren

def transformers_tag():
    transformers = sys.get_code_transformers()
    if not transformers:
        return 'noopt'
    return '-'.join(transformer.name
                    for transformer in transformers)

def use_py():
    return (transformers_tag() == sys.implementation.optim_tag)

Die Reihenfolge von sys.get_code_transformers() ist wichtig. Zum Beispiel ergibt der fat-Transformer gefolgt vom pythran-Transformer den Optimizer-Tag fat-pythran.

Das Verhalten des `importlib`-Moduls bleibt mit dem Standard-Optimizer-Tag ('opt') unverändert.

Peephole-Optimierer

Standardmäßig ist sys.implementation.optim_tag opt und sys.get_code_transformers() gibt eine Liste eines Code-Transformers zurück: den Peephole-Optimierer (optimiert den Bytecode).

Verwenden Sie `-o noopt, um den Peephole-Optimierer zu deaktivieren. In diesem Fall ist der Optimizer-Tag noopt und kein Code-Transformer ist registriert.

Die Verwendung der Option `-o opt hat keine Auswirkung.

AST-Erweiterungen

Erweiterungen zur Vereinfachung der Implementierung von AST-Transformern

  • Fügen Sie ein neues Compiler-Flag PyCF_TRANSFORMED_AST hinzu, um den transformierten AST zu erhalten. PyCF_ONLY_AST gibt den AST vor den Transformern zurück.

Beispiele

.pyc-Dateinamen

Beispiele für .pyc-Dateinamen des `os`-Moduls.

Mit dem Standard-Optimizer-Tag 'opt'

.pyc-Dateiname Optimierungsstufe
os.cpython-36.opt-0.pyc 0
os.cpython-36.opt-1.pyc 1
os.cpython-36.opt-2.pyc 2

Mit dem Optimizer-Tag `'fat'

.pyc-Dateiname Optimierungsstufe
os.cpython-36.fat-0.pyc 0
os.cpython-36.fat-1.pyc 1
os.cpython-36.fat-2.pyc 2

Bytecode-Transformer

Gruseliger Bytecode-Transformer, der alle Strings durch `"Ni! Ni! Ni!" ersetzt

import sys
import types

class BytecodeTransformer:
    name = "knights_who_say_ni"

    def code_transformer(self, code, context):
        consts = ['Ni! Ni! Ni!' if isinstance(const, str) else const
                  for const in code.co_consts]
        return types.CodeType(code.co_argcount,
                              code.co_kwonlyargcount,
                              code.co_nlocals,
                              code.co_stacksize,
                              code.co_flags,
                              code.co_code,
                              tuple(consts),
                              code.co_names,
                              code.co_varnames,
                              code.co_filename,
                              code.co_name,
                              code.co_firstlineno,
                              code.co_lnotab,
                              code.co_freevars,
                              code.co_cellvars)

# replace existing code transformers with the new bytecode transformer
sys.set_code_transformers([BytecodeTransformer()])

# execute code which will be transformed by code_transformer()
exec("print('Hello World!')")

Ausgabe

Ni! Ni! Ni!

AST-Transformer

Ähnlich wie beim Bytecode-Transformer ersetzt auch der AST-Transformer alle Strings durch `"Ni! Ni! Ni!"

import ast
import sys

class KnightsWhoSayNi(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'Ni! Ni! Ni!'
        return node

class ASTTransformer:
    name = "knights_who_say_ni"

    def __init__(self):
        self.transformer = KnightsWhoSayNi()

    def ast_transformer(self, tree, context):
        self.transformer.visit(tree)
        return tree

# replace existing code transformers with the new AST transformer
sys.set_code_transformers([ASTTransformer()])

# execute code which will be transformed by ast_transformer()
exec("print('Hello World!')")

Ausgabe

Ni! Ni! Ni!

Andere Python-Implementierungen

Der PEP 511 sollte von allen Python-Implementierungen umgesetzt werden, aber der Bytecode und der AST sind nicht standardisiert.

Übrigens gibt es selbst zwischen minoren Versionen von CPython Änderungen an der AST-API. Es gibt Unterschiede, aber nur geringfügige. Es ist ziemlich einfach, einen AST-Transformer zu schreiben, der beispielsweise unter Python 2.7 und Python 3.5 funktioniert.

Diskussion

Vorhandene Lösungen

AST-Optimierer

Das Issue #17515 „Add sys.setasthook() to allow to use a custom AST“ optimizer war ein erster Versuch einer API für Code-Transformer, aber speziell für AST.

Im Jahr 2015 schrieb Victor Stinner das Projekt fatoptimizer, einen AST-Optimierer, der Funktionen mithilfe von Guards spezialisiert.

Im Jahr 2014 erstellte Kevin Conway den PyCC-Optimierer.

Im Jahr 2012 schrieb Victor Stinner das Projekt astoptimizer, einen AST-Optimierer, der verschiedene Optimierungen implementiert. Die interessantesten Optimierungen verletzen die Python-Semantik, da kein Guard verwendet wird, um die Optimierung zu deaktivieren, wenn sich etwas ändert.

Im Jahr 2011 schlug Eugene Toder vor, einige Peephole-Optimierungen in einem neuen AST-Optimierer neu zu schreiben: Issue #11549, Build-out an AST optimizer, moving some functionality out of the peephole optimizer. Der Patch fügt ast.Lit hinzu (es wurde vorgeschlagen, es in ast.Literal umzubenennen).

Python-Präprozessoren

  • MacroPy: MacroPy ist eine Implementierung von Syntaktischen Makros in der Python-Programmiersprache. MacroPy bietet einen Mechanismus für benutzerdefinierte Funktionen (Makros), um Transformationen am abstrakten Syntaxbaum (AST) eines Python-Programms zur Importzeit durchzuführen.
  • pypreprocessor: C-Style Präprozessor-Direktiven in Python, wie `#define` und `#ifdef`

Bytecode-Transformer

  • codetransformer: Bytecode-Transformer für CPython, inspiriert von `NodeTransformer` des `ast`-Moduls.
  • byteplay: Byteplay ermöglicht es Ihnen, Python-Codeobjekte in äquivalente Objekte umzuwandeln, mit denen leicht gearbeitet werden kann, und diese Objekte wieder in lebende Python-Codeobjekte zurückzuwandeln. Es ist nützlich, um verrückte Transformationen auf Python-Funktionen anzuwenden, und auch nützlich, um die Feinheiten des Python-Bytecodes zu lernen. Siehe Byteplay-Dokumentation.

Siehe auch


Quelle: https://github.com/python/peps/blob/main/peps/pep-0511.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT