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

Python Enhancement Proposals

PEP 718 – Subscriptable functions

Autor:
James Hilton-Balfe <gobot1234yt at gmail.com>
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
23-Jun-2023
Python-Version:
3.15
Post-History:
24-Jun-2023

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt vor, Funktionsobjekte für Typisierungszwecke subscriptable (indizierbar) zu machen. Dies gibt Entwicklern die explizite Kontrolle über die Typen, die vom Typ-Checker erzeugt werden, wenn bidirektionale Inferenz (die es ermöglicht, die Typen von Parametern anonymer Funktionen zu inferieren) und andere Methoden als Spezialisierung unzureichend sind. Es bringt auch Funktionen in Bezug auf ihre Fähigkeit, subscriptable zu sein, auf eine Stufe mit regulären Klassen.

Motivation

Unbekannte Typen

Derzeit ist es in bestimmten Situationen nicht möglich, die Typparameter für generische Funktionen zu inferieren

def make_list[T](*args: T) -> list[T]: ...
reveal_type(make_list())  # type checker cannot infer a meaningful type for T

Wenn Instanzen von FunctionType subscriptable wären, könnte dieser Konstruktor typisiert werden

reveal_type(make_list[int]())  # type is list[int]

Derzeit müssen Sie eine Zuweisung verwenden, um einen präzisen Typ anzugeben

x: list[int] = make_list()
reveal_type(x)  # type is list[int]

aber dieser Code ist unnötig ausführlich und nimmt mehrere Zeilen für einen einfachen Funktionsaufruf in Anspruch.

Ähnlich kann T in diesem Beispiel derzeit nicht sinnvoll inferiert werden, sodass x ohne eine zusätzliche Zuweisung untyped ist

def factory[T](func: Callable[[T], Any]) -> Foo[T]: ...

reveal_type(factory(lambda x: "Hello World" * x))

Wenn Funktionsobjekte jedoch subscriptable wären, könnte ein spezifischerer Typ angegeben werden

reveal_type(factory[int](lambda x: "Hello World" * x))  # type is Foo[int]

Nicht entscheidbare Inferenz

Es gibt sogar Fälle, in denen Unterklassenbeziehungen die Typinferenz unmöglich machen. Wenn Sie jedoch die Funktion spezialisieren können, können Typ-Checker einen sinnvollen Typ inferieren.

def foo[T](x: Sequence[T] | T) -> list[T]: ...

reveal_type(foo[bytes](b"hello"))

Derzeit synthetisieren Typ-Checker hier keinen konsistenten Typ.

Nicht lösbare Typparameter

Derzeit ist es mit nicht spezialisierten Literalen nicht möglich, einen Typ für Situationen wie diese zu bestimmen

def foo[T](x: list[T]) -> T: ...
reveal_type(foo([]))  # type checker cannot infer T (yet again)
reveal_type(foo[int]([]))  # type is int

Es ist auch nützlich, in Fällen angeben zu können, in denen ein bestimmter Typ im Voraus an eine Funktion übergeben werden muss

words = ["hello", "world"]
foo[int](words)  # Invalid: list[str] is incompatible with list[int]

Die Zulassung von Subscription macht Funktionen und Methoden konsistent mit generischen Klassen, wo sie es nicht bereits waren. Während alle vorgeschlagenen Änderungen mit aufrufbaren generischen Klassen implementiert werden können, wäre syntaktischer Zucker sehr willkommen.

Aus diesem Grund ist die Spezialisierung der Funktion und deren Verwendung als neue Fabrik in Ordnung

make_int_list = make_list[int]
reveal_type(make_int_list())  # type is list[int]

Monomorphisierung und Reifikation

Dieser Vorschlag öffnet auch die Tür für Monomorphisierung und reifizierte Typen.

Dies würde eine Funktionalität ermöglichen, die anekdotisch bereits viele Male angefordert wurde.

Bitte beachten Sie, dass diese Funktion nicht von der PEP vorgeschlagen wird, aber in Zukunft implementiert werden könnte.

Die Syntax für eine solche Funktion könnte wie folgt aussehen

def foo[T]():
   return T.__value__

assert foo[int]() is int

Begründung

Funktionsobjekte in dieser PEP bezieht sich auf FunctionType, MethodType, BuiltinFunctionType, BuiltinMethodType und MethodWrapperType.

Für MethodType sollten Sie schreiben können

class Foo:
    def make_list[T](self, *args: T) -> list[T]: ...

Foo().make_list[int]()

und es ähnlich wie ein FunctionType funktionieren lassen.

Für BuiltinFunctionType, damit generische Built-in-Funktionen (z. B. max und min) wie in Python definierte funktionieren. Built-in-Funktionen sollten sich so weit wie möglich wie in Python implementierte Funktionen verhalten.

BuiltinMethodType ist vom gleichen Typ wie BuiltinFunctionType.

MethodWrapperType (z. B. der Typ von object().__str__) ist nützlich für generische magische Methoden.

Spezifikation

Funktionsobjekte sollten __getitem__ implementieren, um Subscription zur Laufzeit zu ermöglichen, und eine Instanz von types.GenericAlias zurückgeben, wobei __origin__ als aufrufbar und __args__ als übergebene Typen gesetzt sind.

Typ-Checker sollten die Subscription von Funktionen unterstützen und verstehen, dass die an die Funktions-Subscription übergebenen Parameter denselben Regeln wie eine generische Callable-Klasse folgen sollten.

Setzen von __orig_class__

Derzeit ist __orig_class__ ein Attribut, das in GenericAlias.__call__ auf die Instanz von GenericAlias gesetzt wird, die die aufgerufene Klasse erstellt hat, z. B.

class Foo[T]: ...

assert Foo[int]().__orig_class__ == Foo[int]

Derzeit wird __orig_class__ bedingungslos gesetzt. Um jedoch eine potenzielle Löschung bei erstellten Instanzen zu vermeiden, sollte dieses Attribut nicht gesetzt werden, wenn __origin__ eine Instanz eines beliebigen Funktionsobjekts ist.

Der folgende Code-Schnipsel würde ohne diese Änderung zur Laufzeit fehlschlagen, da __orig_class__ bar[str] und nicht Foo[int] wäre.

def bar[U]():
    return Foo[int]()

assert bar[str]().__orig_class__  == Foo[int]

Interaktionen mit @typing.overload

Überladene Funktionen sollten weitgehend wie bisher funktionieren, da sie keine Auswirkungen auf den Laufzeittyp haben. Die einzige Änderung besteht darin, dass mehr Situationen entscheidbar sein werden und das Verhalten/die Überladung vom Entwickler spezifiziert werden kann, anstatt es der Reihenfolge von Überladungen/Vereinigungen zu überlassen.

Abwärtskompatibilität

Derzeit sind diese Klassen nicht unterklassbar, daher gibt es keine Kompatibilitätsprobleme in Bezug auf Klassen, die bereits __getitem__ implementieren.

Referenzimplementierung

Die vorgeschlagenen Änderungen zur Laufzeit finden Sie hier: https://github.com/Gobot1234/cpython/tree/function-subscript

Danksagungen

Vielen Dank an Alex Waygood und Jelle Zijlstra für ihr Feedback zu dieser PEP und an Guido für einige motivierende Beispiele.


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

Zuletzt geändert: 2025-05-06 20:54:28 GMT