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

Python Enhancement Proposals

PEP 713 – Aufrufbare Module

Autor:
Amethyst Reese <amethyst at n7.gg>
Sponsor:
Łukasz Langa <lukasz at python.org>
Discussions-To:
Discourse thread
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
20-Apr-2023
Python-Version:
3.12
Post-History:
23-Apr-2023
Resolution:
Discourse-Nachricht

Inhaltsverzeichnis

Ablehnungsbescheid

Der Steering Council sah keinen zwingenden Grund für diese PEP, auch wenn sie aus Konsistenzsicht klar umsetzbar wäre. Sollte diese Idee in Zukunft wieder aufkommen, ist dies eine nützliche frühere Diskussion, auf die man sich beziehen kann.

Zusammenfassung

Module sind derzeit nicht direkt aufrufbar. Klassen können eine Methode __call__ definieren, die Instanzobjekte aufrufbar macht, aber das Definieren einer Funktion mit demselben Namen im globalen Modulbereich hat keine Auswirkung, und diese Funktion kann nur importiert oder direkt als module.__call__ referenziert werden. PEP 562 fügte Unterstützung für __getattr__() und __dir__() für Module hinzu, aber das Definieren von __getattr__, um einen Wert für __call__ zurückzugeben, macht ein Modul immer noch nicht aufrufbar.

Diese PEP schlägt die Unterstützung für die direkte Aufrufbarkeit von Modulen vor, indem ein Objekt __call__ im globalen Namensraum des Moduls definiert wird, entweder als Standardfunktion oder als beliebiges aufrufbares Objekt.

Motivation

Viele Module haben nur eine einzige primäre Schnittstelle zu ihrer Funktionalität. In vielen Fällen ist diese Schnittstelle ein einzelnes aufrufbares Objekt, und die Möglichkeit, das Modul direkt als aufrufbares Objekt zu importieren und zu verwenden, bietet eine "pythonischere" Schnittstelle für Benutzer.

# user.py

import fancy

@fancy
def func(...):
    ...

Derzeit erfordert die Bereitstellung dieser Art von Schnittstelle eine Laufzeitmodifikation des Modulobjekts, um es aufrufbar zu machen.

Dies geschieht üblicherweise durch Ersetzen des Modulobjekts in sys.modules durch eine aufrufbare Alternative (wie z. B. eine Funktion oder eine Klasseninstanz).

# fancy.py

def fancy(...):
    ...

sys.modules[__name__] = fancy

Dies hat zur Folge, dass das ursprüngliche Modul effektiv unerreichbar wird, ohne weitere Hooks vom Autor, selbst mit from module import member. Außerdem resultiert dies in einem "Modul"-Objekt, dem all die speziellen Modulattribute fehlen, einschließlich __doc__, __package__, __path__, etc.

Alternativ kann ein Modulautor die Eigenschaft __class__ des Moduls durch einen benutzerdefinierten Typ überschreiben, der eine aufrufbare Schnittstelle bereitstellt.

# fancy.py

def fancy(...):
    ...

class FancyModule(types.ModuleType):
    def __call__(self, ...):
        return fancy(...)

sys.modules[__name__].__class__ = FancyModule

Der Nachteil beider Ansätze ist, dass dies nicht nur zusätzlichen Boilerplate-Code erzeugt, sondern auch zu Fehlern bei Typ-Checkern führt, da diese zur Laufzeit nicht erkennen, dass das Modul aufrufbar ist.

$ mypy user.py
user.py:3: error: Module not callable  [operator]
Found 1 error in 1 file (checked 1 source file)

Spezifikation

Wenn ein Modulobjekt aufgerufen wird und ein Objekt __call__ gefunden wird (entweder als Ergebnis einer __getattr__- oder __dict__-Suche), dann wird dieses Objekt mit den angegebenen Argumenten aufgerufen.

Wenn kein Objekt __call__ gefunden wird, wird ein TypeError ausgelöst, was dem bestehenden Verhalten entspricht.

Alle diese Beispiele würden als gültige, aufrufbare Module betrachtet werden.

# hello.py

def __call__(...):
    pass
# hello.py

class Hello:
    pass

__call__ = Hello
# hello.py

def hello():
    pass

def __getattr__(name):
    if name == "__call__":
        return hello

Die ersten beiden Stile sollten generell bevorzugt werden, da sie eine einfachere statische Analyse durch Werkzeuge wie Typ-Checker ermöglichen, obwohl die dritte Form zulässig wäre, um die Implementierung konsistenter zu gestalten.

Die Absicht ist es, es zu ermöglichen, beliebige aufrufbare Objekte der Eigenschaft __call__ des Moduls zuzuweisen oder sie von der Methode __getattr__ des Moduls zurückgeben zu lassen, was Modulautoren ermöglicht, den am besten geeigneten Mechanismus zur Aufrufbarkeit ihres Moduls für Benutzer zu wählen.

Abwärtskompatibilität und Auswirkungen auf die Leistung

Es wird nicht erwartet, dass diese PEP zu Rückwärtskompatibilitätsproblemen führt. Alle Module, die bereits ein Objekt __call__ enthalten, funktionieren weiterhin wie bisher, mit der zusätzlichen Fähigkeit, direkt aufgerufen zu werden. Es wird als unwahrscheinlich erachtet, dass Module mit einem bestehenden Objekt __call__ von dem bestehenden Verhalten abhängen, einen TypeError beim Aufruf auszulösen.

Die Leistungsauswirkungen dieser PEP sind minimal, da sie eine neue Schnittstelle definiert. Das Aufrufen eines Moduls würde eine Suche nach dem Namen __call__ auf einem Modulobjekt auslösen. Bestehende Workarounds zur Erstellung aufrufbarer Module hängen bereits für generische Objekte von diesem Verhalten ab, was zu ähnlicher Leistung für diese aufrufbaren Module führt.

Typ-Checker müssen wahrscheinlich entsprechend aktualisiert werden, um Module mit einem Objekt __call__ als aufrufbar zu behandeln. Dies sollte in Typ-Checkern möglich sein, wenn Code überprüft wird, der sich an ältere Python-Versionen richtet, die keine aufrufbaren Module unterstützen, mit der Erwartung, dass diese Module auch einen der zuvor erwähnten Workarounds enthalten, um das Modul aufrufbar zu machen.

Wie man das lehrt

Die Dokumentation für aufrufbare Typen wird Module in die Liste aufnehmen, mit einem Link zu __call__(). Die Dokumentation Emulieren aufrufbarer Objekte wird einen Abschnitt über aufrufbare Module enthalten, mit Beispielcode, ähnlich dem Abschnitt für Anpassen des Zugriffs auf Modulattribute.

Referenzimplementierung

Die vorgeschlagene Implementierung für aufrufbare Module ist in CPython PR #103742 verfügbar.

Abgelehnte Ideen

Angesichts der Einführung von __getattr__ und __dir__ und des Vorschlags zur Ermöglichung der Verwendung von __call__ wurde überlegt, ob es sich lohnt, die Verwendung *aller* Spezielle Methodennamen für Module zu erlauben, wie z. B. __or__ und __iter__. Obwohl dies nicht völlig unerwünscht wäre, erhöht es das Potenzial für Rückwärtskompatibilitätsprobleme, und diese anderen speziellen Methoden bieten im Vergleich zu __call__ wahrscheinlich weniger Nutzen für Bibliotheksautoren.


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT