PEP 810 – Explizite Lazy Imports
- Autor:
- Pablo Galindo <pablogsal at python.org>, Germán Méndez Bravo <german.mb at gmail.com>, Thomas Wouters <thomas at python.org>, Dino Viehland <dinoviehland at gmail.com>, Brittany Reynoso <brittanyrey at gmail.com>, Noah Kim <noahbkim at gmail.com>, Tim Stumbaugh <me at tjstum.com>
- Discussions-To:
- Discourse thread
- Status:
- Entwurf
- Typ:
- Standards Track
- Erstellt:
- 02. Okt. 2025
- Python-Version:
- 3.15
- Post-History:
- 03. Okt. 2025
Inhaltsverzeichnis
- Zusammenfassung
- Motivation
- Begründung
- Spezifikation
- Referenzimplementierung
- Abwärtskompatibilität
- Sicherheitsimplikationen
- Wie man das lehrt
- FAQ
- Wie unterscheidet sich das von dem abgelehnten PEP 690?
- Was ändert sich zur Zeit der Reifikation? Was bleibt gleich?
- Was passiert, wenn Lazy Imports auf Fehler stoßen?
- Wie beeinflussen Lazy Imports Module mit Import-Zeit-Nebeneffekten?
- Kann ich Lazy Imports mit
from ... import ...Anweisungen verwenden? - Lädt
lazy from module import Classdas gesamte Modul oder nur die Klasse? - Was ist mit Typ-Annotationen und
TYPE_CHECKINGImports? - Was ist der Performance-Overhead von Lazy Imports?
- Kann ich Lazy und Eager Imports desselben Moduls mischen?
- Wie migriere ich bestehenden Code zur Verwendung von Lazy Imports?
- Was ist mit Sternchen-Imports (
from module import *)? - Wie interagieren Lazy Imports mit Import-Hooks und benutzerdefinierten Ladern?
- Was passiert in Multi-Thread-Umgebungen?
- Kann ich die Reifikation eines Lazy Imports erzwingen, ohne ihn zu verwenden?
- Warum nicht
importlib.util.LazyLoaderverwenden? - Wird das Tools wie
isortoderblackbrechen? - Woher weiß ich, ob eine Bibliothek mit Lazy Imports kompatibel ist?
- Was passiert, wenn ich den globalen Lazy Imports Modus aktiviere und eine Bibliothek nicht korrekt funktioniert?
- Kann ich Lazy Imports innerhalb von Funktionen verwenden?
- Was ist mit Vorwärtskompatibilität mit älteren Python-Versionen?
- Wie interagieren explizite Lazy Imports mit PEP 649 und PEP 749?
- Wie interagieren Lazy Imports mit
dir(),getattr()und Modul-Introspektion? - Funktionieren Lazy Imports bei zirkulären Importen?
- Werden Lazy Imports die Performance meiner Hot Paths beeinträchtigen?
- Was ist mit
sys.modules? Wann erscheint dort ein Lazy Import? - Funktioniert
lazy from __future__ import feature? - Warum wurde
lazyals Schlüsselwort gewählt?
- Zurückgestellte Ideen
- Alternative Implementierungs-Ideen
- Abgelehnte Ideen
- Das neue Verhalten zum Standard machen
- Lazy Imports in
withBlöcken verbieten - Eager Imports in
withBlöcken unter dem globalen Flag erzwingen - Modifikation des dict-Objekts
- Lazy Imports Module finden, ohne sie zu laden
- Das
lazySchlüsselwort in der Mitte von from-Imports platzieren - Das
lazySchlüsselwort am Ende von Import-Anweisungen platzieren - Ein explizites
eagerSchlüsselwort hinzufügen - Dem Filter erlauben, Lazy Imports zu erzwingen, auch wenn global deaktiviert
- Unterstrich-präfixierte Namen für erweiterte Funktionen
- Eine Decorator-Syntax für Lazy Imports verwenden
- Einen Context Manager anstelle eines neuen Soft-Keywords verwenden
- Einen Proxy-Dict von
globals()zurückgeben - Automatische Reifikation bei Zugriff auf
__dict__oderglobals()
- Danksagungen
- Fußnoten
- Urheberrecht
Zusammenfassung
Dieses PEP führt Syntax für Lazy Imports als explizites Sprachmerkmal ein
lazy import json
lazy from json import dumps
Lazy Imports verzögern das Laden und Ausführen eines Moduls bis zur ersten Verwendung des importierten Namens, im Gegensatz zu „normalen“ Imports, die ein Modul an der Stelle der Import-Anweisung eager laden und ausführen.
Indem Entwicklern erlaubt wird, einzelne Imports mit expliziter Syntax als lazy zu markieren, können Python-Programme Startzeiten, Speicherverbrauch und unnötige Arbeit reduzieren. Dies ist besonders vorteilhaft für Kommandozeilen-Tools, Testsuiten und Anwendungen mit großen Abhängigkeitsgraphen.
Dieser Vorschlag wahrt die volle Abwärtskompatibilität: Normale Import-Anweisungen bleiben unverändert, und Lazy Imports werden nur dort aktiviert, wo sie explizit angefordert werden.
Motivation
Die dominante Konvention in Python-Code ist es, alle Imports auf Modulebene zu platzieren, typischerweise am Anfang der Datei. Dies vermeidet Wiederholungen, macht Import-Abhängigkeiten klar und minimiert Laufzeit-Overhead, indem eine Import-Anweisung pro Modul nur einmal ausgewertet wird.
Ein großer Nachteil dieses Ansatzes ist, dass der Import des ersten Moduls für eine Ausführung von Python (das „Hauptmodul“) oft eine sofortige Kaskade von Imports auslöst und optimistisch viele Abhängigkeiten lädt, die möglicherweise nie verwendet werden. Der Effekt ist besonders kostspielig für Kommandozeilen-Tools mit mehreren Unterbefehlen, wo selbst das Ausführen des Befehls mit --help Dutzende unnötiger Module laden und mehrere Sekunden dauern kann. Dieses grundlegende Beispiel zeigt, was nur geladen werden muss, um dem Benutzer hilfreiches Feedback zur Ausführung des Programms zu geben. Ineffizient trägt der Benutzer diesen Overhead erneut, wenn er den gewünschten Befehl herausfindet und das Programm „richtig“ aufruft.
Eine etwas übliche Methode, Imports zu verzögern, ist das Verschieben der Imports in Funktionen (Inline-Imports), aber diese Praxis erfordert mehr Aufwand bei Implementierung und Wartung und kann durch einen einzigen unbeabsichtigten Import auf Modulebene unterlaufen werden. Zusätzlich verschleiert sie den vollständigen Satz von Abhängigkeiten für ein Modul. Die Analyse der Python-Standardbibliothek zeigt, dass ungefähr 17% aller Imports außerhalb von Tests (fast 3500 Imports insgesamt über 730 Dateien) bereits innerhalb von Funktionen oder Methoden platziert sind, um ihre Ausführung zu verzögern. Dies zeigt, dass Entwickler bereits manuell Lazy Imports in performance-sensiblem Code implementieren, aber dies erfordert das Verteilen von Imports im gesamten Code und erschwert das Verständnis des vollständigen Abhängigkeitsgraphen auf einen Blick.
Die Standardbibliothek bietet die Klasse LazyLoader, um einige dieser Ineffizienzprobleme zu lösen. Sie erlaubt Imports auf Modulebene, die *meistens* wie Inline-Imports funktionieren. Viele wissenschaftliche Python-Bibliotheken haben ein ähnliches Muster übernommen, formalisiert in SPEC 1. Es gibt auch das Drittanbieter-Paket lazy_loader, eine weitere Implementierung von Lazy Imports. Imports, die ausschließlich zur statischen Typüberprüfung verwendet werden, sind eine weitere Quelle potenziell unnötiger Imports, und es gibt ähnlich disparate Ansätze zur Minimierung des Overheads. Die verschiedenen hier verwendeten Ansätze zur Verzögerung oder Entfernung von Eager Imports decken nicht alle potenziellen Anwendungsfälle für einen allgemeinen Lazy Import Mechanismus ab. Es gibt keinen klaren Standard, und es gibt mehrere Nachteile, einschließlich Laufzeit-Overhead an unerwarteten Stellen oder schlimmer noch Laufzeit-Introspektion.
Dieser Vorschlag führt Syntax für Lazy Imports mit einem Design ein, das lokal, explizit, kontrolliert und granular ist. Jede dieser Eigenschaften ist entscheidend, um das Feature in der Praxis vorhersagbar und sicher nutzbar zu machen.
Das Verhalten ist lokal: Faulheit gilt nur für den spezifischen Import, der mit dem Schlüsselwort lazy markiert ist, und es kaskadiert nicht rekursiv in andere Imports. Dies stellt sicher, dass Entwickler die Auswirkung von Faulheit betrachten können, indem sie nur die Codezeile vor sich ansehen, ohne sich darum zu kümmern, ob importierte Module sich selbst anders verhalten werden. Ein lazy import ist eine isolierte Entscheidung, jedes Mal wenn er verwendet wird, keine globale Änderung der Semantik.
Die Semantik ist explizit. Wenn ein Name faul importiert wird, wird die Bindung sofort im importierenden Modul erstellt, aber das Zielmodul wird erst geladen, wenn der Name zum ersten Mal aufgerufen wird. Danach ist die Bindung von einer, die durch einen normalen Import erstellt wurde, nicht zu unterscheiden. Diese Klarheit reduziert Überraschungen und macht das Feature für Entwickler zugänglich, die möglicherweise nicht tief mit Pythons Import-Mechanismen vertraut sind.
Lazy Imports sind kontrolliert, in dem Sinne, dass die Lazy-Ladung nur durch den importierenden Code selbst ausgelöst wird. Im Allgemeinen wird eine Bibliothek nur dann Lazy Imports erfahren, wenn ihre eigenen Autoren sie als solche markieren. Dies vermeidet die Übertragung der Verantwortung auf nachgelagerte Benutzer und verhindert versehentliche Überraschungen im Bibliotheksverhalten. Da Bibliotheksautoren typischerweise ihre eigenen Import-Subgraphen verwalten, behalten sie die vorhersehbare Kontrolle darüber, wann und wie Faulheit angewendet wird.
Der Mechanismus ist ebenfalls granular. Er wird durch explizite Syntax für einzelne Imports eingeführt, anstatt durch ein globales Flag oder eine implizite Einstellung. Dies ermöglicht es Entwicklern, ihn inkrementell zu übernehmen, beginnend mit den performance-sensibelsten Bereichen eines Codebestands. Da dieses Feature der Community vorgestellt wird, möchten wir die Einarbeitung optional, progressiv und an die Bedürfnisse jedes Projekts anpassbar gestalten.
Lazy Imports bieten mehrere konkrete Vorteile
- Kommandozeilen-Tools werden oft direkt von einem Benutzer aufgerufen, daher ist Latenz – insbesondere Startlatenz – sehr spürbar. Diese Programme sind auch typischerweise kurzlebige Prozesse (im Gegensatz zu z. B. einem Webserver). Mit Lazy Imports wird nur der tatsächlich erreichte Code-Pfad ein Modul importieren. Dies kann die Startzeit in der Praxis um 50-70% reduzieren, was eine signifikante Verbesserung der allgemeinen Benutzererfahrung darstellt und Pythons Wettbewerbsfähigkeit in Domänen verbessert, in denen schnelles Starten am wichtigsten ist.
- Typ-Annotationen erfordern häufig Imports, die zur Laufzeit nie verwendet werden. Die übliche Umgehung besteht darin, sie in
if TYPE_CHECKING:Blöcke zu verpacken [1]. Mit Lazy Imports verursachen reine Annotations-Imports keine Laufzeit-Strafe, wodurch solche Schutzmaßnahmen überflüssig werden und annotierte Codebasen sauberer werden. - Große Anwendungen importieren oft Tausende von Modulen, und jedes Modul erstellt Funktions- und Typobjekte, was Speicherkosten verursacht. In langlebigen Prozessen erhöht dies den Baseline-Speicherverbrauch spürbar. Lazy Imports verzögern diese Kosten bis ein Modul benötigt wird, und halten ungenutzte Teilsysteme ungeladen. Speichereinsparungen von 30-40% wurden in realen Arbeitslasten beobachtet.
Begründung
Das Design dieses Vorschlags konzentriert sich auf Klarheit, Vorhersagbarkeit und einfache Annahme. Jede Entscheidung wurde getroffen, um sicherzustellen, dass Lazy Imports greifbare Vorteile bieten, ohne unnötige Komplexität in die Sprache oder ihre Laufzeit einzuführen.
Es ist auch erwähnenswert, dass dieses PEP zwar einen spezifischen Ansatz darlegt, wir aber alternative Implementierungsstrategien für einige der Kernaspekte und Semantiken des Vorschlags auflisten. Wenn die Community eine starke Präferenz für einen anderen technischen Weg äußert, der dieselben Kernsemantiken beibehält, oder wenn es grundlegende Meinungsverschiedenheiten über die spezifische Option gibt, haben wir die bereits abgeschlossene Ideenfindung als Referenz beigefügt.
Die Wahl, ein neues Schlüsselwort lazy einzuführen, spiegelt die Notwendigkeit expliziter Syntax wider. Lazy Imports haben andere Semantiken als normale Imports: Fehler und Nebeneffekte treten beim ersten Gebrauch auf, nicht bei der Import-Anweisung. Dieser semantische Unterschied macht es entscheidend, dass Faulheit am Importort selbst sichtbar ist, nicht in globalen Konfigurationen oder entfernten Modul-Deklarationen versteckt. Das Schlüsselwort lazy ermöglicht lokale Argumentation über das Importverhalten und vermeidet die Notwendigkeit, woanders im Code nachzuschlagen, um zu verstehen, ob ein Import verzögert wird. Der Rest der Import-Semantik bleibt unverändert: die gleichen Import-Mechanismen, Modul-Findung und Ladeverfahren werden verwendet.
Eine weitere wichtige Entscheidung ist die Darstellung von Lazy Imports durch Proxy-Objekte im Namensraum des Moduls, anstatt durch Modifikation der Dictionary-Suche. Frühere Ansätze experimentierten mit der Einbettung von Faulheit in Dictionaries, aber dies verschwamm Abstraktionen und riskierte die Beeinträchtigung von unerwarteten Teilen der Laufzeit. Das Dictionary ist eine fundamentale Datenstruktur in Python – buchstäblich jedes Objekt ist auf Dictionaries aufgebaut – und das Hinzufügen von Hooks zu Dictionaries würde kritische Optimierungen verhindern und die gesamte Laufzeit verkomplizieren. Der Proxy-Ansatz ist einfacher: Er verhält sich wie ein Platzhalter bis zur ersten Verwendung, an dem Punkt wird der Import aufgelöst und der Name neu gebunden. Danach ist die Bindung von einem normalen Import nicht zu unterscheiden. Dies macht den Mechanismus einfach zu erklären und hält den Rest des Interpreters unverändert.
Die Kompatibilität für Bibliotheksautoren war ebenfalls ein Hauptanliegen. Viele Maintainer benötigen einen Migrationspfad, der es ihnen ermöglicht, sowohl neue als auch alte Versionen von Python gleichzeitig zu unterstützen. Aus diesem Grund enthält der Vorschlag das globale Attribut __lazy_modules__ als Übergangsmechanismus. Ein Modul kann deklarieren, welche Imports als lazy behandelt werden sollen (indem die Modulnamen als Strings aufgelistet werden), und unter Python 3.15 oder später werden diese Imports automatisch lazy, als wären sie mit dem Schlüsselwort lazy importiert worden. In früheren Versionen wird die Deklaration ignoriert, wodurch Imports eager bleiben. Dies gibt Autoren eine praktische Brücke, bis sie das Schlüsselwort als kanonische Syntax verwenden können.
Schließlich ist das Feature darauf ausgelegt, inkrementell übernommen zu werden. Nichts ändert sich, es sei denn, ein Entwickler wählt es explizit aus, und die Übernahme kann mit nur wenigen Imports in performance-sensiblen Bereichen beginnen. Dies spiegelt die Erfahrung der graduellen Typisierung in Python wider: Ein Mechanismus, der schrittweise eingeführt werden kann, ohne Projekte zu zwingen, sich global vom ersten Tag an festzulegen. Bemerkenswerterweise kann die Übernahme auch von „außen nach innen“ erfolgen, was CLI-Autoren ermöglicht, Lazy Imports einzuführen und benutzerorientierte Tools zu beschleunigen, ohne Änderungen an jeder Bibliothek vornehmen zu müssen, die das Tool möglicherweise verwendet.
Andere Designentscheidungen
- Der Umfang der Faulheit ist bewusst lokal und nicht-rekursiv. Ein Lazy Import betrifft nur die spezifische Anweisung, in der er erscheint; er kaskadiert nicht in andere Module oder Untermodule. Diese Wahl ist entscheidend für die Vorhersagbarkeit. Wenn Entwickler Code lesen, können sie das Importverhalten Zeile für Zeile argumentieren, ohne sich um verborgene Faulheit tiefer im Abhängigkeitsgraphen sorgen zu müssen. Das Ergebnis ist ein mächtiges, aber dennoch leicht verständliches Feature im Kontext.
- Darüber hinaus ist es nützlich, einen Mechanismus zur Aktivierung oder Deaktivierung von Lazy Imports für allen im Interpreter laufenden Code bereitzustellen (in diesem PEP als „globales Lazy Imports Flag“ bezeichnet). Während das primäre Design auf der expliziten
lazy importSyntax basiert, gibt es Szenarien – wie große Anwendungen, Testumgebungen oder Frameworks –, in denen die konsistente Aktivierung von Faulheit über viele Module hinweg den größten Nutzen bringt. Ein globaler Schalter erleichtert das Experimentieren oder Erzwingen eines konsistenten Verhaltens, während er gleichzeitig mit der Filter-API kombiniert wird, um Ausschlüsse oder tool-spezifische Konfigurationen zu respektieren. Dies stellt sicher, dass die globale Annahme praktisch ist, ohne die Flexibilität oder Kontrolle zu verringern.
Spezifikation
Grammatik
Ein neues Soft-Keyword lazy wird hinzugefügt. Ein Soft-Keyword ist ein kontextabhängiges Schlüsselwort, das nur in bestimmten grammatikalischen Kontexten eine besondere Bedeutung hat; anderswo kann es als regulärer Bezeichner (z. B. als Variablenname) verwendet werden. Das Schlüsselwort lazy hat nur dann eine besondere Bedeutung, wenn es vor Import-Anweisungen steht.
import_name:
| 'lazy'? 'import' dotted_as_names
import_from:
| 'lazy'? 'from' ('.' | '...')* dotted_name 'import' import_from_targets
| 'lazy'? 'from' ('.' | '...')+ 'import' import_from_targets
Syntax-Beschränkungen
Das Soft-Keyword ist nur auf der globalen (Modul-)Ebene erlaubt, nicht innerhalb von Funktionen, Klassenkörpern, try Blöcken oder import *. Import-Anweisungen, die das Soft-Keyword verwenden, sind *potenziell lazy*. Imports, die nicht lazy sein können, werden vom globalen Lazy Imports Flag nicht beeinflusst und sind stattdessen immer eager. Zusätzlich können from __future__ import Anweisungen nicht lazy sein.
Beispiele für Syntaxfehler
# SyntaxError: lazy import not allowed inside functions
def foo():
lazy import json
# SyntaxError: lazy import not allowed inside classes
class Bar:
lazy import json
# SyntaxError: lazy import not allowed inside try/except blocks
try:
lazy import json
except ImportError:
pass
# SyntaxError: lazy from ... import * is not allowed
lazy from json import *
# SyntaxError: lazy from __future__ import is not allowed
lazy from __future__ import annotations
Semantik
Wenn das Schlüsselwort lazy verwendet wird, wird der Import *potenziell lazy* (siehe Lazy Imports Filter für erweiterte Überschreibungsmechanismen). Das Modul wird nicht sofort bei der Import-Anweisung geladen; stattdessen wird ein lazy Proxy-Objekt erstellt und an den Namen gebunden. Das eigentliche Modul wird beim ersten Zugriff auf diesen Namen geladen.
Bei Verwendung von lazy from ... import wird jeder importierte Name an ein lazy Proxy-Objekt gebunden. Der erste Zugriff auf einen dieser Namen löst das Laden des gesamten Moduls aus und reifiziert nur diesen spezifischen Namen zu seinem tatsächlichen Wert. Andere Namen bleiben als lazy Proxys, bis auf sie zugegriffen wird. Die adaptive Spezialisierung des Interpreters wird die Lazy-Checks nach einigen Zugriffen optimieren.
Beispiel mit lazy import
import sys
lazy import json
print('json' in sys.modules) # False - module not loaded yet
# First use triggers loading
result = json.dumps({"hello": "world"})
print('json' in sys.modules) # True - now loaded
Beispiel mit lazy from ... import
import sys
lazy from json import dumps, loads
print('json' in sys.modules) # False - module not loaded yet
# First use of 'dumps' triggers loading json and reifies ONLY 'dumps'
result = dumps({"hello": "world"})
print('json' in sys.modules) # True - module now loaded
# Accessing 'loads' now reifies it (json already loaded, no re-import)
data = loads(result)
Ein Modul kann ein Attribut __lazy_modules__ enthalten, eine Sequenz von vollständig qualifizierten Modulnamen (Strings), die *potenziell lazy* gemacht werden sollen (als ob das Schlüsselwort lazy verwendet worden wäre). Dieses Attribut wird bei jeder import Anweisung überprüft, um festzustellen, ob der Import *potenziell lazy* gemacht werden soll. Wenn ein Modul auf diese Weise lazy gemacht wird, sind From-Imports, die dieses Modul verwenden, ebenfalls lazy, aber nicht unbedingt Imports von Untermodulen.
Die normale (nicht-lazy) Import-Anweisung prüft das globale Lazy Imports Flag. Wenn es auf „all“ gesetzt ist, sind alle Imports *potenziell lazy* (mit Ausnahme von Imports, die nicht lazy sein können, wie oben erwähnt).
Beispiel
__lazy_modules__ = ["json"]
import json
print('json' in sys.modules) # False
result = json.dumps({"hello": "world"})
print('json' in sys.modules) # True
Wenn das globale Lazy Imports Flag auf „none“ gesetzt ist, wird kein *potenziell lazy* Import jemals lazy importiert, und das Verhalten ist äquivalent zu einer regulären Import-Anweisung: Der Import ist *eager* (als ob das lazy Schlüsselwort nicht verwendet worden wäre).
Schließlich kann die Anwendung eine benutzerdefinierte Filterfunktion für alle *potenziell lazy* Imports verwenden, um zu bestimmen, ob sie lazy oder nicht lazy sein sollen (dies ist ein fortgeschrittenes Feature, siehe Lazy Imports Filter). Wenn eine Filterfunktion gesetzt ist, wird sie mit dem Namen des importierenden Moduls, dem Namen des importierten Moduls und (falls zutreffend) der From-Liste aufgerufen. Ein Import bleibt nur dann lazy, wenn die Filterfunktion True zurückgibt. Wenn kein Lazy Import Filter gesetzt ist, sind alle *potenziell lazy* Imports lazy.
Lazy Objekte
Lazy Module sowie Namen, die lazy aus Modulen importiert wurden, werden durch Instanzen von types.LazyImportType repräsentiert, die zur echten Objekt (reifiziert) aufgelöst werden, bevor sie verwendet werden können. Diese Reifikation erfolgt normalerweise automatisch (siehe unten), kann aber auch durch Aufruf der resolve Methode des lazy Objekts erfolgen.
Lazy Import Mechanismus
Wenn ein Import lazy ist, wird __lazy_import__ anstelle von __import__ aufgerufen. __lazy_import__ hat die gleiche Funktionssignatur wie __import__. Es fügt den Modulnamen zu sys.lazy_modules hinzu, einer Menge von vollständig qualifizierten Modulnamen, die zu einem bestimmten Zeitpunkt lazy importiert wurden (hauptsächlich für Diagnostik und Introspektion), und gibt ein types.LazyImportType Objekt für das Modul zurück.
Die Implementierung von from ... import (die IMPORT_FROM Bytecode-Implementierung) prüft, ob das Modul, von dem bezogen wird, ein lazy Modul-Objekt ist, und gibt in diesem Fall für jeden Namen ein types.LazyImportType zurück, anstatt es sofort abzurufen.
Das Endergebnis dieses Prozesses ist, dass Lazy Imports (unabhängig davon, wie sie aktiviert werden) dazu führen, dass lazy Objekte globalen Variablen zugewiesen werden.
Lazy Modul-Objekte erscheinen nicht in sys.modules, sie sind nur in der Menge sys.lazy_modules aufgeführt. Unter normalem Betrieb enden lazy Objekte nur in globalen Variablen, und die gängigen Wege, auf diese Variablen zuzugreifen (regulärer Variablenzugriff, Modulattribute), lösen Lazy Imports auf (reifizieren) und ersetzen sie, wenn sie aufgerufen werden.
Es ist immer noch möglich, lazy Objekte auf andere Weise verfügbar zu machen, z. B. über Debugger. Dies wird nicht als Problem betrachtet.
Reifikation
Wenn ein lazy Objekt verwendet wird, muss es reifiziert werden. Das bedeutet, den Import zu diesem Zeitpunkt im Programm aufzulösen und das lazy Objekt durch das konkrete zu ersetzen. Reifikation importiert das Modul zu diesem Zeitpunkt im Programm. Insbesondere ruft die Reifikation immer noch __import__ auf, um den Import aufzulösen, was den Zustand des Importsystems (z. B. sys.path, sys.meta_path, sys.path_hooks und __import__) zum Reifikationszeitpunkt verwendet, nicht den Zustand, als die lazy import Anweisung ausgewertet wurde.
Wenn das Modul reifiziert wird, wird es aus sys.lazy_modules entfernt (auch wenn noch andere unreifizierte lazy Referenzen darauf existieren). Wenn ein Paket reifiziert wird und Untermodule im Paket zuvor ebenfalls lazy importiert wurden, werden diese Untermodule nicht automatisch reifiziert, aber sie werden zu den globalen Variablen des reifizierten Pakets hinzugefügt (es sei denn, das Paket hat bereits etwas anderes unter dem Namen des Untermoduls zugewiesen).
Wenn die Reifikation fehlschlägt (z. B. wegen eines ImportError), wird das lazy Objekt nicht reifiziert oder ersetzt. Nachfolgende Verwendungen des lazy Objekts versuchen erneut die Reifikation. Ausnahmen, die während der Reifikation auftreten, werden normal ausgelöst, aber die Ausnahme wird mit Verkettung erweitert, um sowohl zu zeigen, wo der lazy Import definiert wurde, als auch wo er aufgerufen wurde (obwohl er von dem Code propagiert wird, der die Reifikation ausgelöst hat). Dies liefert klare Debugging-Informationen
# app.py - has a typo in the import
lazy from json import dumsp # Typo: should be 'dumps'
print("App started successfully")
print("Processing data...")
# Error occurs here on first use
result = dumsp({"key": "value"})
Der Traceback zeigt beide Orte
App started successfully
Processing data...
Traceback (most recent call last):
File "app.py", line 2, in <module>
lazy from json import dumsp
ImportError: lazy import of 'json.dumsp' raised an exception during resolution
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 8, in <module>
result = dumsp({"key": "value"})
^^^^^
ImportError: cannot import name 'dumsp' from 'json'. Did you mean: 'dump'?
Diese Ausnahmekettung zeigt klar
- wo der Lazy Import definiert wurde,
- dass das Modul nicht eager importiert wurde, und
- wo der tatsächliche Zugriff stattfand, der den Fehler ausgelöst hat.
Reifikation tritt nicht automatisch auf, wenn ein zuvor lazy importiertes Modul anschließend eager importiert wird. Reifikation löst nicht sofort alle lazy Objekte auf (z. B. lazy from Anweisungen), die auf das Modul verwiesen haben. Es löst nur das aufgerufene lazy Objekt auf.
Der Zugriff auf ein lazy Objekt (aus einer globalen Variable oder einem Modulattribut) reifiziert das Objekt.
Das Aufrufen von globals() oder der Zugriff auf das __dict__ eines Moduls löst jedoch keine Reifikation aus – sie geben das Dictionary des Moduls zurück, und der Zugriff auf lazy Objekte über dieses Dictionary gibt immer noch lazy Proxy-Objekte zurück, die bei Verwendung manuell reifiziert werden müssen. Ein lazy Objekt kann explizit durch Aufruf der Methode resolve aufgelöst werden. Das Aufrufen von dir() im globalen Geltungsbereich reifiziert die Globals nicht, noch reifiziert das Aufrufen von dir(mod) (durch spezielle Behandlung in mod.__dir__.) Andere, indirektere Wege des Zugriffs auf beliebige Globals (z. B. Inspektion von frame.f_globals) reifizieren ebenfalls nicht alle Objekte.
Beispiel mit globals() und __dict__
# my_module.py
import sys
lazy import json
# Calling globals() does NOT trigger reification
g = globals()
print('json' in sys.modules) # False - still lazy
print(type(g['json'])) # <class 'LazyImport'>
# Accessing __dict__ also does NOT trigger reification
d = __dict__
print(type(d['json'])) # <class 'LazyImport'>
# Explicitly reify using the resolve() method
resolved = g['json'].resolve()
print(type(resolved)) # <class 'module'>
print('json' in sys.modules) # True - now loaded
Referenzimplementierung
Eine Referenzimplementierung ist verfügbar unter: https://github.com/LazyImportsCabal/cpython/tree/lazy
Eine Demo ist zu Evaluierungszwecken verfügbar (nicht unbedingt synchron mit dem neuesten PEP): https://lazy-import-demo.pages.dev/
Bytecode und adaptive Spezialisierung
Lazy Imports werden durch Modifikationen an vier Bytecode-Instruktionen implementiert: IMPORT_NAME, IMPORT_FROM, LOAD_GLOBAL und LOAD_NAME.
Die lazy Syntax setzt ein Flag im Oparg der IMPORT_NAME Instruktion (oparg & 0x01). Der Interpreter prüft dieses Flag und ruft _PyEval_LazyImportName() anstelle von _PyEval_ImportName() auf, wodurch ein lazy Import-Objekt erstellt wird, anstatt den Import sofort auszuführen. Die IMPORT_FROM Instruktion prüft, ob ihre Quelle ein lazy Import ist (PyLazyImport_CheckExact()) und erstellt ein lazy Objekt für das Attribut, anstatt es sofort abzurufen.
Wenn ein lazy Objekt abgerufen wird, muss es reifiziert werden. Die LOAD_GLOBAL Instruktion (in Funktionsbereichen verwendet) und die LOAD_NAME Instruktion (auf Modul- und Klassenebene verwendet) prüfen beide, ob das geladene Objekt ein lazy Import ist. Wenn ja, rufen sie _PyImport_LoadLazyImportTstate() auf, um den eigentlichen Import durchzuführen und das Modul in sys.modules zu speichern.
Diese Prüfung verursacht bei jedem Zugriff einen sehr geringen Kostenfaktor. Allerdings kann der adaptive Interpreter von Python LOAD_GLOBAL spezialisieren, nachdem er beobachtet hat, dass ein lazy Import reifiziert wurde. Nach mehreren Ausführungen wird LOAD_GLOBAL zu LOAD_GLOBAL_MODULE, das direkt auf das Modul-Dictionary zugreift, ohne auf lazy Imports zu prüfen.
Beispiele für den generierten Bytecode
lazy import json # IMPORT_NAME with flag set
Generiert
IMPORT_NAME 1 (json + lazy)
lazy from json import dumps # IMPORT_NAME + IMPORT_FROM
Generiert
IMPORT_NAME 1 (json + lazy)
IMPORT_FROM 1 (dumps)
lazy import json
x = json # Module-level access
Generiert
LOAD_NAME 0 (json)
lazy import json
def use_json():
return json.dumps({}) # Function scope
Vor jeglichen Aufrufen
LOAD_GLOBAL 0 (json)
LOAD_ATTR 2 (dumps)
Nach mehreren Aufrufen spezialisiert sich LOAD_GLOBAL auf LOAD_GLOBAL_MODULE
LOAD_GLOBAL_MODULE 0 (json)
LOAD_ATTR_MODULE 2 (dumps)
Lazy Imports Filter
Hinweis: Dies ist ein fortgeschrittenes Feature. Bibliotheksentwickler sollten diese Funktionen NICHT aufrufen. Diese sind für spezialisierte/fortgeschrittene Benutzer gedacht, die eine feingranulare Kontrolle über das Verhalten von Lazy Imports benötigen, wenn sie die globalen Flags verwenden.
Dieses PEP fügt dem Modul sys die folgenden neuen Funktionen zur Verwaltung des Lazy-Imports-Filters hinzu.
sys.set_lazy_imports_filter(func)- Setzt die Filterfunktion. Wennfunc=Noneist, wird der Importfilter entfernt. Der Parameterfuncmuss die Signatur haben:func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> boolsys.get_lazy_imports_filter()- Gibt die aktuell installierte Filterfunktion zurück oderNone, wenn kein Filter gesetzt ist.sys.set_lazy_imports(mode, /)- Programmatische API zur Steuerung von Lazy-Imports zur Laufzeit. Der Parametermodekann sein:"normal"(beachtet nur das Schlüsselwortlazy),"all"(erzwingt, dass alle Imports potenziell lazy sind) oder"none"(erzwingt, dass alle Imports eager sind).
Die Filterfunktion wird für jeden potenziell lazyen Import aufgerufen und muss True zurückgeben, wenn der Import lazy sein soll. Dies ermöglicht eine fein abgestimmte Kontrolle darüber, welche Imports lazy sein sollen, was nützlich ist, um Module mit bekannten Abhängigkeiten von Nebeneffekten oder Registrierungsmustern auszuschließen. Die Filterfunktion wird zum Zeitpunkt der Ausführung der Lazy-Import- oder Lazy-From-Import-Anweisung aufgerufen, nicht zum Zeitpunkt der Reifikation. Die Filterfunktion kann gleichzeitig aufgerufen werden.
Der Filtermechanismus dient als Grundlage, die Tools, Debugger, Linter und andere Ökosystem-Dienstprogramme nutzen können, um bessere Lazy-Import-Erlebnisse zu bieten. So könnten beispielsweise statische Analysetools Module mit Nebeneffekten erkennen und automatisch geeignete Filter konfigurieren. Zukünftig (außerhalb des Rahmens dieses PEP) kann diese Grundlage bessere Möglichkeiten bieten, deklarativ festzulegen, welche Module sicher für Lazy-Imports sind, z. B. Paketmetadaten, Typ-Stubs mit Lazy-Safety-Annotationen oder Konfigurationsdateien. Die aktuelle Filter-API ist so konzipiert, dass sie flexibel genug ist, um solche zukünftigen Erweiterungen zu unterstützen, ohne Änderungen an der Kernsprachenspezifikation vornehmen zu müssen.
Beispiel
import sys
def exclude_side_effect_modules(importer, name, fromlist):
"""
Filter function to exclude modules with import-time side effects.
Args:
importer: Name of the module doing the import
name: Name of the module being imported
fromlist: Tuple of names being imported (for 'from' imports), or None
Returns:
True to allow lazy import, False to force eager import
"""
# Modules known to have important import-time side effects
side_effect_modules = {'legacy_plugin_system', 'metrics_collector'}
if name in side_effect_modules:
return False # Force eager import
return True # Allow lazy import
# Install the filter
sys.set_lazy_imports_filter(exclude_side_effect_modules)
# These imports are checked by the filter
lazy import data_processor # Filter returns True -> stays lazy
lazy import legacy_plugin_system # Filter returns False -> imported eagerly
print('data_processor' in sys.modules) # False - still lazy
print('legacy_plugin_system' in sys.modules) # True - loaded eagerly
# First use of data_processor triggers loading
result = data_processor.transform(data)
print('data_processor' in sys.modules) # True - now loaded
Globale Lazy Imports Kontrolle
Hinweis: Dies ist ein fortgeschrittenes Feature. Bibliotheksentwickler sollten den globalen Aktivierungsmechanismus NICHT verwenden. Dies ist für Anwendungsentwickler und Framework-Autoren gedacht, die die Kontrolle über Lazy-Imports in ihrer gesamten Anwendung benötigen.
Das globale Flag für Lazy-Imports kann gesteuert werden über
- Die Befehlszeilenoption
-X lazy_imports=<mode> - Die Umgebungsvariable
PYTHON_LAZY_IMPORTS=<mode> - Die Funktion
sys.set_lazy_imports(mode)(hauptsächlich für Tests)
Wobei <mode> sein kann
"normal"(oder nicht gesetzt): Nur explizit markierte Lazy-Imports sind lazy"all": Alle Imports auf Modulebene (außer intry-Blöcken undimport *) werden potenziell lazy"none": Keine Imports sind lazy, selbst die, die explizit mit dem Schlüsselwortlazymarkiert sind
Wenn das globale Flag auf "all" gesetzt ist, sind alle Imports auf globaler Ebene aller Module potenziell lazy, außer denen innerhalb eines try-Blocks oder eines beliebigen Wildcard-Imports (from ... import *).
Wenn das globale Lazy-Imports-Flag auf "none" gesetzt ist, wird kein potenziell lazy Import jemals lazy importiert, der Importfilter wird nie aufgerufen und das Verhalten ist äquivalent zu einer normalen import-Anweisung: Der Import ist eager (als ob das Lazy-Schlüsselwort nicht verwendet worden wäre).
Python-Code kann die Funktion sys.set_lazy_imports() aufrufen, um den Zustand des globalen Lazy-Imports-Flags zu überschreiben, das von der Umgebung oder der CLI geerbt wurde. Dies ist besonders nützlich, wenn eine Anwendung sicherstellen muss, dass alle Imports eager ausgewertet werden, über sys.set_lazy_imports("none").
Abwärtskompatibilität
Lazy-Imports sind Opt-in. Bestehende Programme laufen unverändert weiter, es sei denn, ein Projekt aktiviert explizit Lazy-Imports (über lazy-Syntax, __lazy_modules__ oder einen Interpreter-weiten Schalter).
Unveränderte Semantik
- Reguläre
import- undfrom ... import ...-Anweisungen bleiben eager, es sei denn, sie werden explizit potenziell lazy durch die bereitgestellten lokalen oder globalen Mechanismen gemacht. - Dynamische Import-APIs bleiben eager und unverändert:
__import__()undimportlib.import_module(). - Import-Hooks und Loader laufen weiterhin unter dem Standard-Importprotokoll, wenn ein Lazy-Objekt reifiziert wird.
Beobachtbare Verhaltensänderungen (nur Opt-in)
Diese Änderungen beschränken sich auf Bindungen, die explizit als lazy markiert wurden.
- Fehlerzeitpunkt. Ausnahmen, die während eines eager Imports aufgetreten wären (z. B.
ImportErroroderAttributeErrorfür ein fehlendes Mitglied), treten nun bei der Verwendung des lazyen Namens auf.# With eager import - error at import statement import broken_module # ImportError raised here # With lazy import - error deferred lazy import broken_module print("Import succeeded") broken_module.foo() # ImportError raised here on use
- Zeitpunkt der Nebeneffekte. Import-Zeit-Nebeneffekte in lazy importierten Modulen treten beim ersten Gebrauch der Bindung auf, nicht zum Zeitpunkt des Modulimports.
- Importreihenfolge. Da Module beim ersten Gebrauch importiert werden, kann die Reihenfolge, in der Module importiert werden, davon abweichen, wie sie im Code erscheinen.
- Vorhandensein in ``sys.modules``. Ein lazy importiertes Modul erscheint erst bei der ersten Verwendung in
sys.modules. Nach der Reifikation muss es insys.moduleserscheinen. Wenn anderer Code dasselbe Modul vor der ersten Verwendung eager importiert, löst die lazy Bindung beim ersten Gebrauch dieses bestehende (lazy) Modulobjekt auf. - Sichtbarkeit des Proxys. Vor der ersten Verwendung verweist der gebundene Name auf einen Lazy-Proxy. Indirekte Introspektion, die den Wert berührt, kann eine Proxy-Lazy-Objektrepräsentation beobachten. Nach der ersten Verwendung (vorausgesetzt, das Modul wurde erfolgreich importiert) wird der Name an das echte Objekt gebunden und ist von einem eager Import nicht zu unterscheiden.
Thread-Sicherheit und Reifikation
Die Reifikation folgt der bestehenden Import-Lock-Disziplin. Genau ein Thread führt den Import durch und bindet atomar das globale des importierenden Moduls an das aufgelöste Objekt. Gleichzeitige Leser beobachten danach das echte Objekt.
Lazy-Imports sind thread-sicher und erfordern keine besonderen Überlegungen für Free-Threading. Ein Modul, das normalerweise im Hauptthread importiert würde, kann in einem anderen Thread importiert werden, wenn dieser Thread den ersten Zugriff auf den Lazy-Import auslöst. Das ist kein Problem: Der Import-Lock gewährleistet die Thread-Sicherheit, unabhängig davon, welcher Thread den Import durchführt.
Subinterpreters werden unterstützt. Jeder Subinterpreter pflegt seine eigenen sys.lazy_modules und seinen eigenen Importstatus, so dass Lazy-Imports in einem Subinterpreter keine anderen beeinflussen.
Performance
Lazy-Imports haben keinen messbaren Performance-Overhead. Die Implementierung ist darauf ausgelegt, sowohl für Code, der Lazy-Imports verwendet, als auch für Code, der dies nicht tut, performance-neutral zu sein.
Laufzeit-Performance
Nach der Reifikation (vorausgesetzt, der Import war erfolgreich) haben Lazy-Imports Null-Overhead. Der adaptive Interpreter spezialisiert den Bytecode (typischerweise nach 2-3 Zugriffen) und eliminiert jegliche Prüfungen. Zum Beispiel wird LOAD_GLOBAL zu LOAD_GLOBAL_MODULE, das direkt auf das Modul zugreift, identisch zu normalen Imports.
Die pyperformance-Suite bestätigt, dass die Implementierung performance-neutral ist.
Performance der Filterfunktion
Die Filterfunktion (gesetzt über sys.set_lazy_imports_filter()) wird für jeden potenziell lazyen Import aufgerufen, um zu bestimmen, ob er tatsächlich lazy sein soll. Wenn kein Filter gesetzt ist, ist dies einfach ein NULL-Check (Prüfung, ob eine Filterfunktion registriert wurde), was ein sehr vorhersagbarer Zweig ist, der im Wesentlichen keinen Overhead hinzufügt. Wenn ein Filter installiert ist, wird er für jeden potenziell lazyen Import aufgerufen, aber dies hat immer noch fast keinen messbaren Performance-Kosten. Um dies zu messen, haben wir den Import aller 278 importierbaren Top-Level-Module aus der Python-Standardbibliothek gemessen (was transitiv 392 Module einschließlich aller Submodule und Abhängigkeiten lädt) und dann die Reifikation jedes geladenen Moduls erzwungen, um sicherzustellen, dass alles vollständig materialisiert wurde.
Beachten Sie, dass diese Messungen den Basis-Overhead des Filtermechanismus selbst feststellen. Natürlich wird jede benutzerdefinierte Filterfunktion, die über eine triviale Prüfung hinaus zusätzliche Arbeit leistet, einen Overhead proportional zur Komplexität dieser Arbeit hinzufügen. Wir gehen jedoch davon aus, dass dieser Overhead in der Praxis von den Leistungsvorteilen durch die Vermeidung unnötiger Imports in den Schatten gestellt wird. Die untenstehenden Benchmarks messen die minimalen Kosten des Filter-Dispatch-Mechanismus, wenn die Filterfunktion im Wesentlichen nichts tut.
Wir verglichen vier verschiedene Konfigurationen
| Konfiguration | Mittelwert ± Standardabweichung (ms) | Overhead im Vergleich zur Basislinie |
|---|---|---|
| Eager Imports (Basislinie) | 161,2 ± 4,3 | 0% |
| Lazy + Filter erzwingt Eager | 161,7 ± 4,2 | +0,3% ± 3,7% |
| Lazy + Filter erlaubt Lazy + Reifikation | 162,0 ± 4,0 | +0,5% ± 3,7% |
| Lazy + kein Filter + Reifikation | 161,4 ± 4,3 | +0,1% ± 3,8% |
Die vier Konfigurationen
- Eager Imports (Basislinie): Normale Python-Imports ohne Lazy-Mechanismus. Standard-Python-Verhalten.
- Lazy + Filter erzwingt Eager: Die Filterfunktion gibt für alle Imports
Falsezurück, erzwingt die eager Ausführung, und dann werden alle Imports am Ende des Skripts reifiziert. Misst den reinen Overhead des Filteraufrufs, da jeder Import durch den Filter geht, aber eager ausgeführt wird. - Lazy + Filter erlaubt Lazy + Reifikation: Die Filterfunktion gibt für alle Imports
Truezurück und erlaubt die lazy Ausführung. Alle Imports werden am Ende des Skripts reifiziert. Misst den Filter-Overhead, wenn Imports tatsächlich lazy sind. - Lazy + kein Filter + Reifikation: Kein Filter installiert, Imports sind lazy und werden am Ende des Skripts reifiziert. Basislinie für das Lazy-Verhalten ohne Filter.
Die Benchmarks verwendeten hyperfine und testeten 278 Module der Standardbibliothek. Jeder Lauf fand in einem frischen Python-Prozess statt. Alle Konfigurationen erzwingen den Import genau desselben Satzes von Modulen (alle vom Eager-Baseline geladenen Module), um einen fairen Vergleich zu gewährleisten.
Die Benchmark-Umgebung verwendete CPU-Isolation mit 32 logischen CPUs (0-15 bei 3200 MHz, 16-31 bei 2400 MHz), den Performance-Scaling-Governor, deaktiviertes Turbo Boost und vollständige ASLR-Randomisierung. Die Overhead-Fehlerbalken werden mittels Standard-Fehlerfortpflanzung für die Formel (Wert - Basislinie) / Basislinie berechnet, wobei Unsicherheiten sowohl im gemessenen Wert als auch in der Basislinie berücksichtigt werden.
Verbesserungen der Startzeit
Der primäre Leistungsvorteil von Lazy-Imports ist die reduzierte Startzeit durch das Laden nur der Module, die tatsächlich zur Laufzeit verwendet werden, anstatt optimistisch ganze Abhängigkeitsbäume beim Start zu laden.
Reale Implementierungen im großen Maßstab haben gezeigt, dass die Vorteile immens sein können, obwohl dies natürlich von der spezifischen Codebasis und den Nutzungsmustern abhängt. Organisationen mit großen, vernetzten Codebasen berichten von erheblichen Reduzierungen der Server-Reload-Zeiten, der ML-Trainingsinitialisierung, des Starts von Kommandozeilentools und des Ladens von Jupyter-Notebooks. Es wurden auch Speicherverbesserungen beobachtet, da ungenutzte Module ungeladen bleiben.
Detaillierte Fallstudien und Leistungsdaten aus Produktionsumgebungen finden Sie unter
- Python Lazy Imports With Cinder (Meta Instagram Server)
- Lazy ist das neue schnell: Wie Lazy Imports und Cinder maschinelles Lernen bei Meta beschleunigen (Meta ML Workloads)
- Inside HRT’s Python Fork (Hudson River Trading)
- Create an On-Demand Initializer for PySide (Qt for Python/PySide) - Christian Tisomers Implementierung von Lazy-Initialisierung für PySide6 basierend auf Ideen aus PEP 690, die eine Verbesserung der Startzeit von 10-20% für PySide-Anwendungen zeigt. Dies verdeutlicht den besonderen Wert von Lazy-Imports für Frameworks mit umfangreicher Initialisierung zur Importzeit.
Die Vorteile skalieren mit der Komplexität der Codebasis: Je größer und vernetzter die Codebasis, desto dramatischer die Verbesserungen. Die PySide-Implementierung hebt besonders hervor, wie Frameworks mit hohem Initialisierungs-Overhead erheblich von opt-in Lazy Loading profitieren können.
Typisierung und Werkzeuge
Typenprüfer und statische Analysatoren können lazy-Imports für die Namensauflösung wie gewöhnliche Imports behandeln. Zur Laufzeit können nur Anmerkungs-Imports als lazy markiert werden, um Start-Overhead zu vermeiden. IDEs und Debugger sollten darauf vorbereitet sein, Lazy-Proxys vor der ersten Verwendung und danach die echten Objekte anzuzeigen.
Sicherheitsimplikationen
Tools, die Pakete installieren und gleichzeitig aus derselben Umgebung importieren, sollten sicherstellen, dass alle Module vor dem Installationsschritt eager importiert oder reifiziert werden, um zu vermeiden, dass neu installierte Distributionen diese überschatten.
Solche Tools können sys.set_lazy_imports() mit "none" verwenden, um die eager Auswertung zu erzwingen, oder eine sys.set_lazy_imports_filter()-Funktion für feingranulare Kontrolle bereitstellen.
Wie man das lehrt
Das neue Schlüsselwort lazy wird als Teil des Sprachstandards dokumentiert.
Da dieses Feature Opt-in ist, sollten neue Python-Benutzer die Sprache weiterhin wie gewohnt verwenden können. Für erfahrene Entwickler erwarten wir, dass sie Lazy-Imports aufgrund der vielfältigen Vorteile (geringere Latenz, geringerer Speicherverbrauch usw.) fallweise nutzen. Entwickler, die an der Leistung ihrer Python-Binärdatei interessiert sind, werden wahrscheinlich Profiling nutzen, um den Import-Zeit-Overhead in ihrer Codebasis zu verstehen und die notwendigen Imports als lazy zu markieren. Zusätzlich können Entwickler Imports, die nur für Typanmerkungen verwendet werden, als lazy markieren.
Zusätzliche Dokumentation wird zur Python-Dokumentation hinzugefügt, einschließlich Anleitungen, einem speziellen How-To-Guide und Aktualisierungen der Dokumentation zum Import-System, die Folgendes abdecken: Identifizierung langsam ladender Module mit Profiling-Tools (wie -X importtime), Migrationsstrategien für bestehende Codebasen, Best Practices zur Vermeidung häufiger Fallstricke mit Import-Zeit-Nebeneffekten und Muster für die effektive Nutzung von Lazy-Imports mit Typanmerkungen und zirkulären Imports.
Nachfolgend finden Sie Anleitungen, wie Sie Lazy-Imports am besten nutzen und Inkompatibilitäten vermeiden können.
- Bei der Einführung von Lazy-Imports sollten sich Benutzer bewusst sein, dass das Auslassen eines Imports bis zu seiner Verwendung dazu führt, dass Nebeneffekte nicht ausgeführt werden. Daher sollten Benutzer vorsichtig sein bei Modulen, die auf Import-Zeit-Nebeneffekte angewiesen sind. Möglicherweise die häufigste Abhängigkeit von Import-Nebeneffekten ist das Registrierungsmuster, bei dem die Population einer externen Registrierung implizit während des Imports von Modulen geschieht, oft über Decorators, aber manchmal auch über Metaklassen oder
__init_subclass__implementiert. Stattdessen sollten Registrierungen von Objekten über explizite Erkundungsprozesse (z. B. eine bekannte aufzurufende Funktion) erstellt werden.# Problematic: Plugin registers itself on import # my_plugin.py from plugin_registry import register_plugin @register_plugin("MyPlugin") class MyPlugin: pass # In main code: lazy import my_plugin # Plugin NOT registered yet - module not loaded! # Better: Explicit discovery # plugin_registry.py def discover_plugins(): from my_plugin import MyPlugin register_plugin(MyPlugin) # In main code: plugin_registry.discover_plugins() # Explicit loading
- Importieren Sie immer benötigte Submodule explizit. Es reicht nicht aus, sich darauf zu verlassen, dass ein anderer Import sicherstellt, dass ein Modul seine Submodule als Attribute hat. Ganz einfach, es sei denn, es gibt eine explizite
from . import barinfoo/__init__.py, verwenden Sie immerimport foo.bar; foo.bar.Baz, nichtimport foo; foo.bar.Baz. Letzteres funktioniert (unzuverlässig) nur, weil das Attributfoo.barals Nebeneffekt des Imports vonfoo.baran anderer Stelle hinzugefügt wird. - Benutzer, die Imports in Funktionen verschieben, um die Startzeit zu verbessern, sollten stattdessen erwägen, sie dort zu belassen, wo sie sind, aber das Schlüsselwort
lazyhinzuzufügen. Dies ermöglicht es ihnen, Abhängigkeiten klar zu halten und den Overhead der wiederholten Auflösung des Imports zu vermeiden, wird das Programm aber dennoch beschleunigen.# Before: Inline import (repeated overhead) def process_data(data): import json # Re-resolved on every call return json.dumps(data) # After: Lazy import at module level lazy import json def process_data(data): return json.dumps(data) # Loaded once on first call
- Vermeiden Sie die Verwendung von Wildcard-Imports (Sternchen), da diese immer eager sind.
FAQ
Wie unterscheidet sich das von dem abgelehnten PEP 690?
PEP 810 verfolgt einen expliziten Opt-in-Ansatz anstelle des impliziten globalen Ansatzes von PEP 690. Die wichtigsten Unterschiede sind:
- Explizite Syntax:
lazy import foomarkiert klar, welche Imports lazy sind. - Lokaler Geltungsbereich: Lazy-ness wirkt sich nur auf die spezifische Importanweisung aus, nicht kaskadierend auf Abhängigkeiten.
- Einfachere Implementierung: Verwendet Proxy-Objekte anstelle der Modifikation des Kern-Dictionary-Verhaltens.
Was ändert sich zur Zeit der Reifikation? Was bleibt gleich?
Was ändert sich (der Zeitpunkt)
- Wann das Modul importiert wird - verschoben auf die erste Verwendung anstelle der Importanweisung
- Wann Importfehler auftreten - bei der ersten Verwendung anstelle zur Importzeit
- Wann Modul-Level-Nebeneffekte ausgeführt werden - bei der ersten Verwendung anstelle zur Importzeit
Was gleich bleibt (alles andere)
- Die verwendete Import-Maschinerie - dasselbe
__import__, dieselben Hooks, dieselben Loader - Das erstellte Modulobjekt - identisch mit einem eager importierten Modul
- Abgefragter Import-Status -
sys.path,sys.meta_pathusw. zum Reifikationszeitpunkt (nicht zum Import-Anweisungszeitpunkt) - Modulattribute und Verhalten - nach der Reifikation vollständig ununterscheidbar
- Thread-Sicherheit - dieselbe Import-Lock-Disziplin wie bei normalen Imports
Anders ausgedrückt: Lazy-Imports ändern nur wann etwas passiert, nicht was passiert. Nach der Reifikation ist ein lazy importiertes Modul von einem eager importierten nicht zu unterscheiden.
Was passiert, wenn Lazy Imports auf Fehler stoßen?
Importfehler (ImportError, ModuleNotFoundError, Syntaxfehler) werden bis zur ersten Verwendung des lazyen Namens aufgeschoben. Dies ähnelt dem Verschieben eines Imports in eine Funktion. Der Fehler tritt mit einem klaren Traceback auf, der auf den ersten Zugriff auf das Lazy-Objekt verweist.
Die Implementierung bietet verbesserte Fehlerberichterstattung durch Ausnahmeketten. Wenn ein Lazy-Import während der Reifikation fehlschlägt, wird die ursprüngliche Ausnahme beibehalten und verkettet, wodurch sowohl angezeigt wird, wo der Import definiert wurde, als auch wo er zum ersten Mal verwendet wurde.
Traceback (most recent call last):
File "test.py", line 1, in <module>
lazy import broken_module
ImportError: lazy import of 'broken_module' raised an exception during resolution
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 3, in <module>
broken_module.foo()
^^^^^^^^^^^^^
File "broken_module.py", line 2, in <module>
1/0
ZeroDivisionError: division by zero
Ausnahmen während der Reifikation verhindern den Ersatz des Lazy-Objekts, und nachfolgende Verwendungen des Lazy-Objekts werden die gesamte Reifikation erneut versuchen.
Wie beeinflussen Lazy Imports Module mit Import-Zeit-Nebeneffekten?
Nebeneffekte werden bis zur ersten Verwendung aufgeschoben. Dies ist generell wünschenswert für die Leistung, kann aber Codeänderungen für Module erfordern, die auf Import-Zeit-Registrierungsmuster angewiesen sind. Wir empfehlen:
- Verwenden Sie explizite Initialisierungsfunktionen anstelle von Import-Zeit-Nebeneffekten.
- Rufen Sie Initialisierungsfunktionen explizit auf, wenn nötig.
- Vermeiden Sie die Abhängigkeit von der Importreihenfolge für Nebeneffekte.
Kann ich Lazy Imports mit from ... import ... Anweisungen verwenden?
Ja, solange Sie nicht from ... import * verwenden. Sowohl lazy import foo als auch lazy from foo import bar werden unterstützt. Der Name bar wird an ein Lazy-Objekt gebunden, das bei der ersten Verwendung zu foo.bar aufgelöst wird.
Lädt lazy from module import Class das gesamte Modul oder nur die Klasse?
Es lädt das gesamte Modul, nicht nur die Klasse. Das liegt daran, dass das Importsystem von Python immer die vollständige Moduldatei ausführt – es gibt keinen Mechanismus, um nur einen Teil einer .py-Datei auszuführen. Wenn Sie zum ersten Mal auf Class zugreifen, Python
- Lädt und führt die gesamte
module.py-Datei aus - Extrahiert das Attribut
Classaus dem resultierenden Modulobjekt - Bindet
Classan den Namen in Ihrem Namensraum
Dies ist identisch mit dem Verhalten eines eager from module import Class. Der einzige Unterschied zu Lazy-Imports ist, dass die Schritte 1-3 bei der ersten Verwendung und nicht bei der Importanweisung stattfinden.
# heavy_module.py
print("Loading heavy_module") # This ALWAYS runs when module loads
class MyClass:
pass
class UnusedClass:
pass # Also gets defined, even though we don't import it
# app.py
lazy from heavy_module import MyClass
print("Import statement done") # heavy_module not loaded yet
obj = MyClass() # NOW "Loading heavy_module" prints
# (and UnusedClass gets defined too)
Wichtiger Punkt: Lazy-Imports verschieben wann ein Modul geladen wird, nicht was geladen wird. Sie können nicht selektiv nur Teile eines Moduls laden – das Python-Importsystem unterstützt keine partielle Modulausführung.
Was ist mit Typ-Annotationen und TYPE_CHECKING Imports?
Lazy-Imports eliminieren die häufige Notwendigkeit von TYPE_CHECKING-Guards. Sie können schreiben
lazy from collections.abc import Sequence, Mapping # No runtime cost
def process(items: Sequence[str]) -> Mapping[str, int]:
...
Anstatt
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence, Mapping
def process(items: Sequence[str]) -> Mapping[str, int]:
...
Was ist der Performance-Overhead von Lazy Imports?
Der Overhead ist minimal
- Null Overhead nach der ersten Verwendung (vorausgesetzt, der Import schlägt nicht fehl) dank des adaptiven Interpreters, der den langsamen Pfad wegoptimiert.
- Geringe einmalige Kosten für die Erstellung des Proxy-Objekts.
- Die Reifikation (erste Verwendung) hat die gleichen Kosten wie ein regulärer Import.
- Keine fortlaufende Leistungsstrafe.
Benchmarking mit der pyperformance-Suite zeigt, dass die Implementierung performance-neutral ist, wenn Lazy-Imports nicht verwendet werden.
Kann ich Lazy und Eager Imports desselben Moduls mischen?
Ja. Wenn das Modul foo im selben Programm sowohl lazy als auch eager importiert wird, hat der eager Import Vorrang und beide Bindungen lösen auf dasselbe Modulobjekt auf.
Wie migriere ich bestehenden Code zur Verwendung von Lazy Imports?
Migration ist inkrementell
- Identifizieren Sie langsam ladende Module mit Profiling-Tools.
- Fügen Sie das Schlüsselwort
lazyzu Imports hinzu, die nicht sofort benötigt werden. - Testen Sie, ob Änderungen am Timing von Nebeneffekten die Funktionalität nicht beeinträchtigen.
- Verwenden Sie
__lazy_modules__für Kompatibilität mit älteren Python-Versionen.
Was ist mit Sternchen-Imports (from module import *)?
Wildcard-Imports (Sternchen) können nicht lazy sein - sie bleiben eager. Dies liegt daran, dass die Menge der zu importierenden Namen nicht bestimmt werden kann, ohne das Modul zu laden. Die Verwendung des Schlüsselworts lazy mit Sternchen-Imports führt zu einem Syntaxfehler. Wenn Lazy-Imports global aktiviert sind, sind Sternchen-Imports immer noch eager.
Wie interagieren Lazy Imports mit Import-Hooks und benutzerdefinierten Ladern?
Import-Hooks und Loader funktionieren normal. Wenn ein Lazy-Objekt verwendet wird, wird das Standard-Importprotokoll ausgeführt, einschließlich aller benutzerdefinierten Hooks oder Loader, die zum Zeitpunkt der Reifikation vorhanden waren.
Was passiert in Multi-Thread-Umgebungen?
Die Reifikation von Lazy-Imports ist thread-sicher. Nur ein Thread führt den tatsächlichen Import durch und die Bindung wird atomar aktualisiert. Andere Threads sehen entweder den Lazy-Proxy oder das endgültig aufgelöste Objekt.
Kann ich die Reifikation eines Lazy Imports erzwingen, ohne ihn zu verwenden?
Ja, einzelne Lazy-Objekte können durch Aufruf ihrer resolve()-Methode aufgelöst werden.
Warum nicht importlib.util.LazyLoader verwenden?
LazyLoader hat erhebliche Einschränkungen
- Erfordert aufwändigen Einrichtungs-Code für jeden Lazy-Import.
- Funktioniert nicht gut mit
from ... import-Anweisungen. - Weniger klar und standardmäßig als dedizierte Syntax.
Wird das Tools wie isort oder black brechen?
Linter, Formatter und andere Tools benötigen Updates, um das Schlüsselwort lazy zu erkennen, aber die Änderungen sollten minimal sein, da die Importstruktur gleich bleibt. Das Schlüsselwort erscheint am Anfang, was die Analyse erleichtert.
Woher weiß ich, ob eine Bibliothek mit Lazy Imports kompatibel ist?
Die meisten Bibliotheken sollten mit Lazy-Imports gut funktionieren. Bibliotheken, die möglicherweise Probleme haben:
- Diejenigen mit wesentlichen Import-Zeit-Nebeneffekten (Registrierung, Monkey-Patching).
- Diejenigen, die eine spezifische Importreihenfolge erwarten.
- Diejenigen, die während des Imports globale Zustände modifizieren.
Im Zweifelsfall testen Sie Lazy-Imports mit Ihren spezifischen Anwendungsfällen.
Was passiert, wenn ich den globalen Lazy Imports Modus aktiviere und eine Bibliothek nicht korrekt funktioniert?
Hinweis: Dies ist ein fortgeschrittenes Feature. Sie können den Lazy-Imports-Filter verwenden, um bestimmte Module auszuschließen, von denen bekannt ist, dass sie problematische Nebeneffekte haben.
import sys
def my_filter(importer, name, fromlist):
# Don't lazily import modules known to have side effects
if name in {'problematic_module', 'another_module'}:
return False # Import eagerly
return True # Allow lazy import
sys.set_lazy_imports_filter(my_filter)
Die Filterfunktion erhält den Namen des importierenden Moduls, das zu importierende Modul und die fromlist (bei Verwendung von from ... import). Wenn False zurückgegeben wird, wird ein eager Import erzwungen.
Alternativ kann der globale Modus über -X lazy_imports=none auf "none" gesetzt werden, um alle Lazy-Imports zum Debugging zu deaktivieren.
Kann ich Lazy Imports innerhalb von Funktionen verwenden?
Nein, das Schlüsselwort lazy ist nur auf Modulebene erlaubt. Für Lazy Loading auf Funktionsebene verwenden Sie traditionelle Inline-Imports oder verschieben Sie den Import auf Modulebene mit lazy.
Was ist mit Vorwärtskompatibilität mit älteren Python-Versionen?
Verwenden Sie das globale __lazy_modules__ für die Kompatibilität.
# Works on Python 3.15+ as lazy, eager on older versions
__lazy_modules__ = ['expensive_module', 'expensive_module_2']
import expensive_module
from expensive_module_2 import MyClass
Das Attribut __lazy_modules__ ist eine Liste von Modulnamens-Strings. Wenn eine Importanweisung ausgeführt wird, prüft Python, ob der importierte Modulname in __lazy_modules__ enthalten ist. Wenn ja, wird der Import so behandelt, als hätte er das Schlüsselwort lazy (und wird potenziell lazy). Auf Python-Versionen vor 3.15, die Lazy-Imports nicht unterstützen, wird das Attribut __lazy_modules__ einfach ignoriert und Imports erfolgen wie gewohnt eager.
Dies bietet einen Migrationspfad, bis Sie sich auf das Schlüsselwort lazy verlassen können. Für maximale Vorhersagbarkeit wird empfohlen, __lazy_modules__ einmal zu definieren, bevor irgendwelche Imports erfolgen. Da es jedoch bei jedem Import geprüft wird, kann es zwischen import-Anweisungen geändert werden.
Wie interagieren explizite Lazy Imports mit PEP 649 und PEP 749?
Python 3.14 implementierte die verzögerte Auswertung von Annotationen, wie in PEP 649 und PEP 749 spezifiziert. Wenn eine Annotation nicht als String vorliegt, ist sie ein Ausdruck, der zu einem späteren Zeitpunkt ausgewertet wird. Sie wird erst aufgelöst, wenn auf die Annotation zugegriffen wird. Im folgenden Beispiel wird das Modul fake_typing nur geladen, wenn der Benutzer das Wörterbuch __annotations__ inspiziert. Das Modul fake_typing würde auch geladen werden, wenn der Benutzer annotationlib.get_annotations() oder getattr verwendet, um auf die Annotationen zuzugreifen.
lazy from fake_typing import MyFakeType
def foo(x: MyFakeType):
pass
print(foo.__annotations__) # Triggers loading the fake_typing module
Wie interagieren Lazy Imports mit dir(), getattr() und Modul-Introspektion?
Der Zugriff auf lazy Imports über normalen Attributzugriff oder getattr() löst die Reifikation des zugegriffenen Attributs aus. Der Aufruf von dir() auf einem Modul wird in mod.__dir__ speziell behandelt, um die Reifikation zu vermeiden.
lazy import json
# Before any access
# json not in sys.modules
# Any of these trigger reification:
dumps_func = json.dumps
dumps_func = getattr(json, 'dumps')
# Now json is in sys.modules
Funktionieren Lazy Imports bei zirkulären Importen?
Lazy Imports lösen zirkuläre Importprobleme nicht automatisch. Wenn zwei Module eine zirkuläre Abhängigkeit haben, kann das verzögern der Imports helfen, **nur wenn** die zirkuläre Referenz nicht während der Modulinitialisierung aufgerufen wird. Wenn jedoch ein Modul das andere zur Importzeit aufruft, erhalten Sie immer noch einen Fehler.
Beispiel, das funktioniert (verzögerter Zugriff in Funktionen)
# user_model.py
lazy import post_model
class User:
def get_posts(self):
# OK - post_model accessed inside function, not during import
return post_model.Post.get_by_user(self.name)
# post_model.py
lazy import user_model
class Post:
@staticmethod
def get_by_user(username):
return f"Posts by {username}"
Dies funktioniert, da keines der Module zur Modul-Ebene das andere aufruft – der Zugriff erfolgt später, wenn get_posts() aufgerufen wird.
Beispiel, das fehlschlägt (Zugriff während des Imports)
# module_a.py
lazy import module_b
result = module_b.get_value() # Error! Accessing during import
def func():
return "A"
# module_b.py
lazy import module_a
result = module_a.func() # Circular dependency error here
def get_value():
return "B"
Dies schlägt fehl, da module_a versucht, module_b zur Importzeit aufzurufen, welches dann versucht, module_a aufzurufen, bevor es vollständig initialisiert ist.
Die beste Praxis ist immer noch, zirkuläre Imports in Ihrem Code-Design zu vermeiden.
Werden Lazy Imports die Performance meiner Hot Paths beeinträchtigen?
Nach der ersten Verwendung (vorausgesetzt, der Import ist erfolgreich) haben Lazy Imports dank des adaptiven Interpreters **null Overhead**. Der Interpreter spezialisiert den Bytecode (z. B. wird LOAD_GLOBAL zu LOAD_GLOBAL_MODULE), was die Lazy-Prüfung bei nachfolgenden Zugriffen eliminiert. Das bedeutet, sobald ein Lazy Import reifiziert ist, ist der Zugriff darauf genauso schnell wie ein normaler Import.
lazy import json
def use_json():
return json.dumps({"test": 1})
# First call triggers reification
use_json()
# After 2-3 calls, bytecode is specialized
use_json()
use_json()
Sie können die Spezialisierung mit dis.dis(use_json, adaptive=True) beobachten.
=== Before specialization ===
LOAD_GLOBAL 0 (json)
LOAD_ATTR 2 (dumps)
=== After 3 calls (specialized) ===
LOAD_GLOBAL_MODULE 0 (json)
LOAD_ATTR_MODULE 2 (dumps)
Die spezialisierten Instruktionen LOAD_GLOBAL_MODULE und LOAD_ATTR_MODULE sind optimierte Fast-Pfade ohne Overhead für die Prüfung von Lazy Imports.
Was ist mit sys.modules? Wann erscheint dort ein Lazy Import?
Ein lazy importiertes Modul erscheint erst dann in sys.modules, wenn es reifiziert (erstmals verwendet) wird. Sobald es reifiziert ist, erscheint es in sys.modules, genau wie jeder eager Import.
import sys
lazy import json
print('json' in sys.modules) # False
result = json.dumps({"key": "value"}) # First use
print('json' in sys.modules) # True
Funktioniert lazy from __future__ import feature?
Nein, zukünftige Imports können nicht lazy sein, da sie Parser-/Compiler-Direktiven sind. Es ist technisch möglich, dass das Laufzeitverhalten lazy ist, aber es gibt keinen wirklichen Nutzen dafür.
Warum wurde lazy als Schlüsselwort gewählt?
Nicht "warum" ... auswendig lernen! :)
Zurückgestellte Ideen
Die folgenden Ideen wurden berücksichtigt, aber bewusst aufgeschoben, um sich zunächst auf die Bereitstellung eines stabilen, nutzbaren Kernfeatures zu konzentrieren. Diese könnten für zukünftige Verbesserungen in Betracht gezogen werden, sobald wir reale Erfahrungen mit Lazy Imports gesammelt haben.
Alternative Syntax und ergonomische Verbesserungen
Es wurden mehrere alternative Syntaxformen vorgeschlagen, um die Ergonomie zu verbessern.
- Nur-Typ-Imports: Eine spezialisierte Syntax für Imports, die ausschließlich in Typ-Annotationen verwendet werden (ähnlich dem
type-Schlüsselwort in anderen Kontexten), könnte hinzugefügt werden, wie z. B.type from collections.abc import Sequence. Dies würde die Absicht klarer machen, alslazyfür Nur-Typ-Imports zu verwenden, und würde Lesern signalisieren, dass der Import zur Laufzeit nie verwendet wird. Dalazy-Imports jedoch bereits das Laufzeitkostenproblem für Typ-Annotationen lösen, bevorzugen wir es, mit dem einfacheren, allgemeineren Mechanismus zu beginnen und zu bewerten, ob eine spezialisierte Syntax nach Sammlung von Nutzungsdaten einen ausreichenden Mehrwert bietet. - Blockbasierte Syntax: Gruppierung mehrerer lazy Imports in einem Block, wie zum Beispiel
as lazy: import foo from bar import baz
Dies könnte Wiederholungen reduzieren, wenn viele Imports als lazy markiert werden. Es würde jedoch die Einführung einer völlig neuen Statement-Form (
as lazy:Blöcke) erfordern, die nicht in die bestehenden Grammatikmuster von Python passt. Es ist unklar, wie dies mit anderen Sprachfunktionen interagieren würde oder welche Präzedenzfälle es für ähnliche Block-Level-Modifikatoren gäbe. Dieser Ansatz macht es auch weniger klar, wenn Code gescannt wird, ob ein bestimmter Import lazy ist, da man den umgebenden Kontext statt der Importzeile selbst betrachten muss.
Während diese Alternativen in bestimmten Kontexten unterschiedliche Ergonomie bieten könnten, teilen sie ähnliche Nachteile: Sie würden die Einführung neuer Statement-Formen oder die Überladung bestehender Syntax auf nicht offensichtliche Weise erfordern, und sie eröffnen die Tür für viele andere potenzielle Verwendungen ähnlicher Syntaxmuster, die die Sprache erheblich erweitern würden. Wir bevorzugen es, mit der expliziten lazy import-Syntax zu beginnen und echtes Feedback zu sammeln, bevor wir zusätzliche Syntaxvariationen in Betracht ziehen. Jegliche zukünftigen ergonomischen Verbesserungen sollten auf tatsächlichen Nutzungsmustern und nicht auf spekulativen Vorteilen basieren.
Automatische Lazy Imports für if TYPE_CHECKING Blöcke
Eine zukünftige Verbesserung könnte alle Imports innerhalb von if TYPE_CHECKING: Blöcken automatisch als lazy behandeln.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from foo import Bar # Could be automatically lazy
Dies würde jedoch erhebliche Änderungen erfordern, um zur Kompilierungszeit zu funktionieren, da TYPE_CHECKING derzeit nur eine Laufzeitvariable ist. Der Compiler müsste spezielle Kenntnisse dieses Musters haben, ähnlich wie from __future__ import-Anweisungen behandelt werden. Darüber hinaus wäre es erforderlich, TYPE_CHECKING zu einem Built-in zu machen, damit dies zuverlässig funktioniert. Da lazy-Imports bereits das Laufzeitkostenproblem für Nur-Typ-Imports lösen, ziehen wir es vor, mit der expliziten Syntax zu beginnen und zu bewerten, ob diese Optimierung einen ausreichenden Mehrwert bietet.
Modul-Level Lazy Import Modus
Eine Deklaration auf Modul-Ebene, um alle Imports in diesem Modul standardmäßig lazy zu machen.
from __future__ import lazy_imports
import foo # Automatically lazy
Dies wurde diskutiert, aber aufgeschoben, da es mehrere Fragen aufwirft. Die Verwendung von from __future__ import impliziert, dass dies das Standardverhalten in einer zukünftigen Python-Version werden würde, was unklar und derzeit nicht geplant ist. Es wirft auch Fragen auf, wie ein solcher Modus mit dem globalen Flag interagieren würde und wie der Übergangspfad aussehen würde. Die aktuelle explizite Syntax und __lazy_modules__ bieten ausreichende Kontrolle für die anfängliche Einführung.
Paket-Metadaten für Lazy-Safe Deklarationen
Zukünftige Verbesserungen könnten es Paketen ermöglichen, in ihren Metadaten zu deklarieren, ob sie für lazy Imports sicher sind (z. B. keine Import-Zeit-Nebeneffekte). Dies könnte vom Filtermechanismus oder von statischen Analysewerkzeugen verwendet werden. Die aktuelle Filter-API ist so konzipiert, dass sie solche zukünftigen Ergänzungen unterstützt, ohne Änderungen an der Kernsprachenspezifikation zu erfordern.
Alternative Implementierungs-Ideen
Hier sind einige alternative Designentscheidungen, die während der Entwicklung dieser PEP berücksichtigt wurden. Während der aktuelle Vorschlag das darstellt, was wir für die beste Balance aus Einfachheit, Leistung und Wartbarkeit halten, bieten diese Alternativen unterschiedliche Kompromisse, die für Implementierer oder zukünftige Verfeinerungen wertvoll sein können.
Nutzung einer Unterklasse von dict
Anstatt das interne Dict-Objekt zu aktualisieren, um direkt die benötigten Felder für Lazy Imports hinzuzufügen, könnten wir eine Unterklasse des Dict-Objekts erstellen, die speziell zur Aktivierung von Lazy Imports verwendet wird. Dies wäre jedoch immer noch eine leaky Abstraction – Methoden wie dict.__getitem__ könnten direkt aufgerufen werden, und es würde die Leistung von Globals-Lookups im Interpreter beeinträchtigen.
Alternative Schlüsselwortnamen
Für diese PEP haben wir entschieden, lazy als explizites Schlüsselwort vorzuschlagen, da es sich für diejenigen, die sich bereits auf die Optimierung von Import-Overhead konzentrieren, am vertrautesten anfühlte. Wir haben auch eine Vielzahl anderer Optionen für die Unterstützung expliziter lazy Imports in Betracht gezogen. Die überzeugendsten Alternativen waren defer und delay.
Abgelehnte Ideen
Das neue Verhalten zum Standard machen
Das Ändern von import, um standardmäßig lazy zu sein, liegt außerhalb des Rahmens dieser PEP. Aus der Diskussion zu PEP 690 geht hervor, dass dies eine ziemlich umstrittene Idee ist, obwohl dies vielleicht noch einmal überdacht werden kann, sobald wir eine breite Nutzung von Lazy Imports haben.
Lazy Imports in with Blöcken verbieten
Eine frühere Version dieser PEP schlug vor, lazy import-Anweisungen innerhalb von with-Blöcken zu verbieten, ähnlich der Einschränkung für try-Blöcke. Die Sorge war, dass bestimmte Kontextmanager (wie contextlib.suppress(ImportError)) Importfehler auf verwirrende Weise unterdrücken könnten, wenn sie mit lazy Imports kombiniert werden.
Diese Einschränkung wurde jedoch abgelehnt, da with-Anweisungen weitaus breitere Semantiken haben als try/except-Blöcke. Während try/except ausdrücklich zum Abfangen von Ausnahmen dient, werden with-Blöcke üblicherweise für Ressourcenverwaltung, temporäre Zustandsänderungen oder Scoping verwendet – Kontexte, in denen lazy Imports einwandfrei funktionieren. Die lazy import-Syntax ist explizit genug, dass Entwickler, die sie innerhalb eines with-Blocks schreiben, eine bewusste Entscheidung treffen, was mit Pythons Philosophie der „zustimmenden Erwachsenen“ übereinstimmt. Für wirklich problematische Fälle wie with suppress(ImportError): lazy import foo sind statische Analysewerkzeuge und Linter besser geeignet, diese Muster zu erkennen, als harte Sprachbeschränkungen.
Eager Imports in with Blöcken unter dem globalen Flag erzwingen
Eine weitere abgelehnte Idee war, Imports innerhalb von with-Blöcken weiterhin eager zu halten, selbst wenn das globale Lazy Imports-Flag auf "all" gesetzt ist. Die Begründung war, konservativ zu sein: Da with-Anweisungen das Verhalten von Imports beeinflussen können (z. B. durch Änderung von sys.path oder Unterdrückung von Ausnahmen), könnte das Erzwingen von Imports als eager das Entstehen subtiler Fehler verhindern. Dies würde jedoch zu inkonsistentem Verhalten führen, bei dem lazy import explizit in with-Blöcken erlaubt ist, normale Imports jedoch weiterhin eager sind, wenn das globale Flag aktiviert ist. Diese Inkonsistenz zwischen expliziter und impliziter Lazy-Natur ist verwirrend und schwer zu erklären.
Die einfachere, konsistentere Regel ist, dass das globale Flag Imports überall beeinflusst, wo die explizite lazy import-Syntax erlaubt ist. Dies vermeidet drei verschiedene Regelwerke (explizite Syntax, Verhalten des globalen Flags und Filtermechanismus) und bietet stattdessen zwei: Explizite Syntaxregeln entsprechen dem, was das globale Flag beeinflusst, und der Filtermechanismus bietet Ausweichmöglichkeiten für Randfälle. Für Benutzer, die eine feingranulare Kontrolle benötigen, bietet der Filtermechanismus (sys.set_lazy_imports_filter()) bereits eine Möglichkeit, bestimmte Imports oder Muster auszuschließen. Außerdem gibt es keine umgekehrte Operation: Wenn das globale Flag Imports in with-Blöcken eager erzwingt, aber ein Benutzer sie lazy haben möchte, gibt es keine Möglichkeit, dies zu überschreiben, was zu einer Asymmetrie führt.
Zusammenfassend lässt sich sagen: Imports in with-Blöcken verhalten sich konsistent, egal ob sie explizit mit lazy import markiert oder implizit über das globale Flag aktiviert werden, was zu einer einfachen Regel führt, die leicht zu erklären und nachzuvollziehen ist.
Modifikation des dict-Objekts
Die ursprüngliche PEP für lazy Imports (PEP 690) stützte sich stark auf die Modifikation des internen Dict-Objekts zur Unterstützung von lazy Imports. Wir erkennen an, dass diese Datenstruktur hoch optimiert, weitreichend im Code verwendet und sehr leistungssensibel ist. Aufgrund der Bedeutung dieser Datenstruktur und des Wunsches, die Implementierung von lazy Imports von Benutzern, die kein Interesse an der Funktion haben, zu kapseln, haben wir uns entschieden, in einen alternativen Ansatz zu investieren.
Das Dictionary ist die grundlegende Datenstruktur in Python. Die Attribute jedes Objekts werden in einem Dict gespeichert, und Dicts werden im gesamten Laufzeitsystem für Namensräume, Schlüsselwortargumente und mehr verwendet. Das Hinzufügen jeder Art von Hook oder speziellem Verhalten zu Dicts zur Unterstützung von lazy Imports würde
- Kritische Interpreter-Optimierungen verhindern, einschließlich zukünftiger JIT-Kompilierung.
- Komplexität zu einer Datenstruktur hinzufügen, die einfach und schnell bleiben muss.
- Jeden Teil von Python beeinflussen, nicht nur das Importverhalten.
- Trennung von Belangen verletzen – die Hashtabelle sollte nichts über das Importsystem wissen.
Frühere Entscheidungen, die gegen dieses Prinzip der sauberen Kernabstrakionen verstießen, haben im CPython-Ökosystem erhebliche Probleme verursacht, die Optimierung erschwert und subtile Fehler eingeführt.
Lazy Imports Module finden, ohne sie zu laden
Die Python import-Maschinerie trennt das Finden eines Moduls von dessen Laden, und die Lazy Import-Implementierung könnte technisch nur den Lade-Teil verzögern. Dieser Ansatz wurde jedoch aus mehreren kritischen Gründen abgelehnt.
Ein erheblicher Teil des Leistungsgewinns stammt aus dem Überspringen der Suchphase. Das Problem ist besonders akut auf NFS-basierten Dateisystemen und verteilten Speichern, wo jeder stat()-Aufruf Netzwerklatenz verursacht. In solchen Umgebungen können stat()-Aufrufe je nach Netzwerkbedingungen zehn bis Hunderte von Millisekunden dauern. Bei Dutzenden von Imports, die jeweils mehrere Dateisystemprüfungen durchführen und sys.path durchlaufen, kann die Zeit, die für das Finden von Modulen aufgewendet wird, bevor Python-Code ausgeführt wird, erheblich werden. In einigen Messungen macht das Suchen von Spezifikationen den größten Teil der gesamten Importzeit aus. Das Überspringen nur der Ladephase würde den Großteil des Leistungsproblems ungelöst lassen.
Entscheidender ist, dass die Trennung von Finden und Laden das Schlimmste aus beiden Welten für die Fehlerbehandlung darstellt. Einige Ausnahmen von der Import-Maschinerie (z. B. ImportError von einem fehlenden Modul, Fehler bei der Pfadauflösung, ModuleNotFoundError) würden bei der lazy import-Anweisung ausgelöst, während andere (z. B. SyntaxError, ImportError aus zirkulären Imports, Attributfehler aus from module import name) später beim ersten Gebrauch ausgelöst würden. Diese Aufteilung ist sowohl verwirrend als auch unvorhersehbar: Entwickler müssten die interne Import-Maschinerie verstehen, um zu wissen, welche Fehler wann auftreten. Das aktuelle Design ist einfacher: Bei vollständigen Lazy Imports treten alle Import-bezogenen Fehler beim ersten Gebrauch auf, was das Verhalten konsistent und vorhersehbar macht.
Zusätzlich gibt es technische Einschränkungen: Das Finden eines Moduls garantiert nicht, dass der Import erfolgreich ist, noch dass er nicht ImportError auslöst. Das Finden von Modulen in Paketen erfordert, dass diese Pakete geladen werden, sodass es nur beim Lazy Loading einer Ebene einer Paket-Hierarchie helfen würde. Da das "Finden" von Attributen in Modulen deren Laden *erfordert*, würde dies einen schwer zu erklärenden Unterschied zwischen from package import module und from module import function schaffen.
Das lazy Schlüsselwort in der Mitte von from-Imports platzieren
Obwohl wir from foo lazy import bar als eine wirklich intuitive Platzierung für die neue explizite Syntax empfanden, haben wir schnell gelernt, dass die Platzierung des lazy-Schlüsselworts hier bereits syntaktisch in Python zulässig ist. Dies liegt daran, dass from . lazy import bar eine legale Syntax ist (da Leerzeichen keine Rolle spielen).
Das lazy Schlüsselwort am Ende von Import-Anweisungen platzieren
Wir diskutierten das Anhängen von lazy am Ende von Importanweisungen wie import foo lazy oder from foo import bar, baz lazy, entschieden uns aber letztendlich, dass dieser Ansatz weniger Klarheit bietet. Wenn beispielsweise mehrere Module in einer einzigen Anweisung importiert werden, ist unklar, ob die lazy-Bindung für alle importierten Objekte oder nur für eine Untermenge der Elemente gilt.
Ein explizites eager Schlüsselwort hinzufügen
Da wir das Standardverhalten nicht ändern und die Verwendung globaler Flags nicht fördern wollen, ist es noch zu früh, überflüssige Syntax für den üblichen, Standardfall nachzudenken. Dies würde zu viel Verwirrung darüber stiften, was der Standard ist, wann das eager-Schlüsselwort notwendig wäre oder ob es lazy Imports *in* dem explizit eager importierten Modul beeinflusst.
Dem Filter erlauben, Lazy Imports zu erzwingen, auch wenn global deaktiviert
Da lazy Imports einige Formen von zirkulären Imports zulassen, die sonst fehlschlagen würden, wurde als absichtliche und erwünschte Sache (insbesondere für typbezogene Imports) vorgeschlagen, eine Möglichkeit hinzuzufügen, die globale Deaktivierung zu überschreiben und bestimmte Imports zu erzwingen, lazy zu sein, z. B. indem der lazy Imports-Filter aufgerufen wird, auch wenn lazy Imports global deaktiviert sind.
Dieser Ansatz könnte eine komplexe Hierarchie verschiedener "Override"-Systeme einführen, was die Analyse und Nachvollziehbarkeit des Codes erheblich erschwert. Darüber hinaus könnte dies zusätzliche Komplexität erfordern, um feingranularere Systeme zur Aktivierung oder Deaktivierung bestimmter Imports einzuführen, wenn sich die Verwendung von lazy Imports weiterentwickelt. Die globale Deaktivierung wird voraussichtlich nicht häufig genutzt, sondern eher als Debugging- und selektives Testwerkzeug für diejenigen dienen, die ihre Abhängigkeit von lazy Imports streng kontrollieren möchten. Wir denken, es ist für Paketverantwortliche angemessen, wenn sie Pakete zur Einführung von lazy Imports aktualisieren, zu entscheiden, dass sie das Ausführen mit global deaktivierten lazy Imports *nicht* unterstützen.
Es kann sein, dass dies bedeutet, dass im Laufe der Zeit, wenn immer mehr Pakete sowohl Typisierung als auch lazy Imports übernehmen, die globale Deaktivierung weitgehend ungenutzt und unbrauchbar wird. Ähnliche Dinge sind in der Vergangenheit mit anderen globalen Flags passiert, und angesichts der geringen Kosten des Flags scheint dies akzeptabel zu sein. Es ist auch einfacher, später spezifischere Reaktivierungsmechanismen hinzuzufügen, wenn wir ein klareres Bild von der realen Nutzung und den Mustern haben, als einen überstürzt hinzugefügten Mechanismus zu entfernen, der nicht ganz richtig ist.
Unterstrich-präfixierte Namen für erweiterte Funktionen
Die globalen Aktivierungs- und Filterfunktionen (sys.set_lazy_imports, sys.set_lazy_imports_filter, sys.get_lazy_imports_filter) könnten als "privat" oder "fortgeschritten" markiert werden, indem Unterstrichpräfixe verwendet werden (z. B. sys._set_lazy_imports_filter). Dies wurde abgelehnt, da die Kennzeichnung als fortgeschrittene Funktionen durch Dokumentation ausreichend ist. Diese Funktionen haben legitime Anwendungsfälle für fortgeschrittene Benutzer, insbesondere Betreiber großer Deployments. Die Bereitstellung eines offiziellen Mechanismus verhindert eine Abweichung vom CPython-Upstream. Der globale Modus wird absichtlich als fortgeschrittene Funktion für Betreiber dokumentiert, die riesige Flotten betreiben, nicht für alltägliche Benutzer oder Bibliotheken. Python hat Präzedenzfälle für fortgeschrittene Funktionen, die öffentliche APIs ohne Unterstrichpräfixe bleiben – zum Beispiel sind gc.disable(), gc.get_objects() und gc.set_threshold() fortgeschrittene Funktionen, die bei falscher Verwendung Probleme verursachen können, aber sie sind nicht mit Unterstrichen versehen.
Eine Decorator-Syntax für Lazy Imports verwenden
Eine dekoratorbasierte Syntax könnte Imports als lazy markieren.
@lazy
import json
@lazy
from foo import bar
Dieser Ansatz wurde abgelehnt, da er zu viele offene Fragen und Komplikationen einführt. Dekoratoren in Python sind dafür konzipiert, aufrufbare Objekte (Funktionen, Klassen, Methoden) zu wrappen und zu transformieren, nicht Statements. Das Zulassen von Dekoratoren für Importanweisungen würde die Tür für viele andere potenzielle Statement-Dekoratoren öffnen (@cached, @traced, @deprecated usw.) und die Grammatik der Sprache erheblich erweitern, in einer Weise, die wir nicht erkunden wollen. Darüber hinaus wirft dies die Frage auf, woher solche Dekoratoren kommen würden: Sie müssten entweder importiert oder eingebaut sein, was ein Bootstrapping-Problem für importbezogene Dekoratoren schafft. Dies ist weitaus spekulativer und generischer als die fokussierte lazy import-Syntax.
Einen Context Manager anstelle eines neuen Soft-Keywords verwenden
Eine rückwärtskompatible Syntax, zum Beispiel in Form eines Kontextmanagers, wurde vorgeschlagen.
with lazy_imports(...):
import json
Dies würde die Notwendigkeit von __lazy_modules__ ersetzen und Bibliotheken ermöglichen, ältere Python-Versionen mit einer der vorhandenen lazy Imports-Implementierungen zu verwenden. Das Hinzufügen von magischen with-Anweisungen mit dieser Art von Effekt wäre jedoch eine signifikante Änderung für Python und with-Anweisungen im Allgemeinen, und es wäre nicht einfach, sie mit der Implementierung für lazy Imports in diesem Vorschlag zu kombinieren. Das Hinzufügen von Standardbibliotheksunterstützung für vorhandene lazy Importer *ohne* Änderungen an der Implementierung entspricht dem Status quo und löst nicht die Performance- und Usability-Probleme dieser bestehenden Lösungen.
Einen Proxy-Dict von globals() zurückgeben
Eine Alternative zur Reifikation bei globals() oder zur Offenlegung von lazy Objekten wäre die Rückgabe eines Proxy-Dictionaries, das lazy Objekte automatisch reifiziert, wenn sie über den Proxy angesprochen werden. Dies würde scheinbar das Beste aus beiden Welten bieten: globals() gibt sofort und ohne Reifikationskosten zurück, aber der Zugriff auf Elemente über das Ergebnis würde lazy Imports automatisch auflösen.
Dieser Ansatz ist jedoch grundlegend inkompatibel damit, wie globals() in der Praxis verwendet wird. Viele Standardbibliotheksfunktionen und Built-ins erwarten, dass globals() ein echtes dict-Objekt zurückgibt, nicht einen Proxy.
exec(code, globals())benötigt ein echtes Dict.eval(expr, globals())benötigt ein echtes Dict.- Funktionen, die
type(globals()) is dictprüfen, würden fehlschlagen. - Dictionary-Methoden wie
.update()müssten speziell behandelt werden. - Die Leistung würde durch die Indirektion bei jedem Zugriff leiden.
Der Proxy müsste so transparent sein, dass er in fast allen Fällen von einem echten Dict nicht zu unterscheiden wäre, was äußerst schwierig korrekt zu erreichen ist. Jede Abweichung vom echten Dict-Verhalten wäre eine Quelle subtiler Fehler.
Automatische Reifikation bei Zugriff auf __dict__ oder globals()
Drei Optionen wurden erwogen, wie globals() und mod.__dict__ mit lazy Imports umgehen sollen.
- Der Aufruf von
globals()odermod.__dict__durchläuft und reifiziert alle lazy Objekte, bevor er zurückkehrt. - Der Aufruf von
globals()odermod.__dict__gibt das Dictionary mit lazy Objekten zurück (gewählt). - Der Aufruf von
globals()gibt das Dictionary mit lazy Objekten zurück, abermod.__dict__reifiziert alles.
Wir haben Option 2 gewählt: Sowohl globals() als auch __dict__ geben das rohe Namensraum-Dictionary zurück, ohne Reifikation auszulösen. Dies bietet ein sauberes, vorhersagbares Modell, bei dem Low-Level-Introspektions-APIs keine Nebeneffekte auslösen.
Das identische Verhalten von globals() und __dict__ schafft Symmetrie und ein einfaches mental-Modell: beide zeigen die rohe Namensraumansicht. Low-Level-Introspektions-APIs sollten nicht automatisch Imports auslösen, was überraschend und potenziell kostspielig wäre. Erfahrungen bei der Implementierung von lazy Imports in der Standardbibliothek (wie das traceback-Modul) zeigten, dass automatische Reifikation beim Zugriff auf __dict__ umständlich war und Introspektionscode dazu zwang, Module zu laden, die er nur untersuchte.
Option 1 (immer reifizieren) wurde abgelehnt, da sie den Zugriff auf globals() und __dict__ überraschend teuer machen und die Introspektion des lazy Zustands eines Moduls verhindern würde. Option 3 wurde zunächst in Betracht gezogen, um externen Code vor dem Sehen von lazy Objekten zu "schützen", aber die reale Nutzung zeigte, dass dies mehr Probleme als Lösungen mit sich brachte, insbesondere für Standardbibliotheks-Code, der Module introspektieren muss, ohne Nebeneffekte auszulösen.
Danksagungen
Wir möchten Paul Ganssle, Yury Selivanov, Łukasz Langa, Lysandros Nikolaou, Pradyun Gedam, Mark Shannon, Hana Joo und dem Python Google Team, den Python-Teams von Meta, dem Python @ HRT-Team, dem Bloomberg Python-Team, der Scientific Python-Community, allen, die an der anfänglichen Diskussion von PEP 690 teilgenommen haben, und vielen anderen, die wertvolles Feedback und Einblicke gegeben haben, die zur Gestaltung dieser PEP beigetragen haben, danken.
Fußnoten
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0810.rst
Zuletzt geändert: 2025-10-15 16:08:56 GMT