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

Python Enhancement Proposals

PEP 637 – Unterstützung für Indizierung mit Schlüsselwortargumenten

Autor:
Stefano Borini
Sponsor:
Steven D’Aprano
Discussions-To:
Python-Ideas Liste
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
24-Aug-2020
Python-Version:
3.10
Post-History:
23-Sep-2020
Resolution:
Python-Dev thread

Inhaltsverzeichnis

Hinweis

Diese PEP wurde abgelehnt. Im Allgemeinen wurden die Kosten für die Einführung neuer Syntax durch den wahrgenommenen Nutzen nicht aufgewogen. Einzelheiten finden Sie im Link im Feld „Resolution Header“.

Zusammenfassung

Derzeit sind Schlüsselwortargumente bei Funktionsaufrufen erlaubt, aber nicht beim Elementzugriff. Diese PEP schlägt vor, Python um die Zulassung von Schlüsselwortargumenten beim Elementzugriff zu erweitern.

Das folgende Beispiel zeigt Schlüsselwortargumente für normale Funktionsaufrufe

>>> val = f(1, 2, a=3, b=4)

Der Vorschlag würde die Syntax erweitern, um eine ähnliche Konstruktion wie bei Indizierungsoperationen zuzulassen

>>> val = x[1, 2, a=3, b=4]  # getitem
>>> x[1, 2, a=3, b=4] = val  # setitem
>>> del x[1, 2, a=3, b=4]    # delitem

und würde auch entsprechende Semantik bereitstellen. Ein- und doppelte Stern-Entpackung von Argumenten ist ebenfalls vorgesehen

>>> val = x[*(1, 2), **{a=3, b=4}]  # Equivalent to above.

Diese PEP ist ein Nachfolger von PEP 472, die 2019 mangels Interesse abgelehnt wurde. Seitdem gibt es ein erneutes Interesse an der Funktion.

Übersicht

Hintergrund

PEP 472 wurde 2014 eröffnet. Die PEP detaillierte verschiedene Anwendungsfälle und wurde durch Extraktion von Implementierungsstrategien aus einer breiten Diskussion auf der python-ideas-Mailingliste erstellt, obwohl kein klarer Konsens darüber erzielt wurde, welche Strategie verwendet werden sollte. Viele Sonderfälle wurden genauer untersucht und fühlten sich umständlich, rückwärtsinkompatibel oder beides an.

Die PEP wurde schließlich 2019 [1] abgelehnt, hauptsächlich aufgrund mangelnden Interesses an der Funktion trotz ihres 5-jährigen Bestehens.

Mit der Einführung von Typ-Hints in PEP 484 wurde die eckige Klammersyntax jedoch durchgängig verwendet, um Typannotationen zu bereichern, z. B. um eine Liste von Ganzzahlen als Sequence[int] anzugeben. Darüber hinaus gab es ein erweitertes Wachstum von Paketen für Datenanalyse wie pandas und xarray, die Namen zur Beschreibung von Spalten in einer Tabelle (pandas) oder Achsen in einem nd-Array (xarray) verwenden. Diese Pakete ermöglichen es Benutzern, auf bestimmte Daten über Namen zuzugreifen, können aber derzeit diese Funktionalität nicht über die Indexnotation ([]) nutzen.

Infolgedessen wurde gelegentlich in vielen verschiedenen Threads auf python-ideas ein erneutes Interesse an einer flexibleren Syntax geäußert, die benannte Informationen zulässt, zuletzt von Caleb Donovick [2] im Jahr 2019 und Andras Tantos [3] im Jahr 2020. Diese Anfragen lösten eine starke Aktivität auf der python-ideas-Mailingliste aus, wo die verschiedenen Optionen erneut diskutiert wurden und nun ein allgemeiner Konsens über eine Implementierungsstrategie erzielt wurde.

Anwendungsfälle

Die folgenden praktischen Anwendungsfälle zeigen verschiedene Fälle, in denen eine Schlüsselwortspezifikation die Notation verbessern und zusätzlichen Wert bieten würde

  1. Um dem Index eine kommunikativere Bedeutung zu verleihen und z. B. versehentliche Vertauschung von Indizes zu verhindern
    >>> grid_position[x=3, y=5, z=8]
    >>> rain_amount[time=0:12, location=location]
    >>> matrix[row=20, col=40]
    
  2. Um die Tippnotation mit Schlüsselwörtern anzureichern, insbesondere bei der Verwendung von Generika
    def function(value: MyType[T=int]):
    
  3. In einigen Bereichen wie der Computerphysik und -chemie ist die Verwendung einer Notation wie Basis[Z=5] eine domänenspezifische Sprachnotation zur Darstellung eines Genauigkeitsgrades
    >>> low_accuracy_energy = computeEnergy(molecule, BasisSet[Z=3])
    
  4. Pandas verwendet derzeit eine Notation wie
    >>> df[df['x'] == 1]
    

    was durch df[x=1] ersetzt werden könnte.

  5. xarray hat benannte Dimensionen. Derzeit werden diese mit den Funktionen .isel behandelt
    >>> data.isel(row=10)  # Returns the tenth row
    

    was auch durch data[row=10] ersetzt werden könnte. Ein komplexeres Beispiel

    >>> # old syntax
    >>> da.isel(space=0, time=slice(None, 2))[...] = spam
    >>> # new syntax
    >>> da[space=0, time=:2] = spam
    

    Ein weiteres Beispiel

    >>> # old syntax
    >>> ds["empty"].loc[dict(lon=5, lat=6)] = 10
    >>> # new syntax
    >>> ds["empty"][lon=5, lat=6] = 10
    
    >>> # old syntax
    >>> ds["empty"].loc[dict(lon=slice(1, 5), lat=slice(3, None))] = 10
    >>> # new syntax
    >>> ds["empty"][lon=1:5, lat=6:] = 10
    
  6. Funktionen/Methoden, deren Argument eine andere Funktion ist (plus deren Argumente), benötigen eine Möglichkeit, zu bestimmen, welche Argumente für die Ziel-Funktion bestimmt sind und welche zur Konfiguration ihrer Ausführung verwendet werden. Dies ist einfach (wenn auch nicht erweiterbar) für Positionsargumente, aber wir benötigen eine Möglichkeit, diese für Schlüsselwörter zu unterscheiden. [4]

    Eine indizierte Notation würde eine Python-konforme Möglichkeit bieten, Schlüsselwortargumente an diese Funktionen zu übergeben, ohne den Code des Aufrufers zu überladen.

    >>> # Let's start this example with basic syntax without keywords.
    >>> # the positional values are arguments to `func` while
    >>> # `name=` is processed by `trio.run`.
    >>> trio.run(func, value1, value2, name="func")
    >>> # `trio.run` ends up calling `func(value1, value2)`.
    
    >>> # If we want/need to pass value2 by keyword (keyword-only argument,
    >>> # additional arguments that won't break backwards compatibility ...),
    >>> # currently we need to resort to functools.partial:
    >>> trio.run(functools.partial(func, param2=value2), value1, name="func")
    >>> trio.run(functools.partial(func, value1, param2=value2), name="func")
    
    >>> # One possible workaround is to convert `trio.run` to an object
    >>> # with a `__call__` method, and use an "option" helper,
    >>> trio.run.option(name="func")(func, value1, param2=value2)
    >>> # However, foo(bar)(baz) is uncommon and thus disruptive to the reader.
    >>> # Also, you need to remember the name of the `option` method.
    
    >>> # This PEP allows us to replace `option` with `__getitem__`.
    >>> # The call is now shorter, more mnemonic, and looks+works like typing
    >>> trio.run[name="func"](func, value1, param2=value2)
    
  7. Die Verfügbarkeit von Sternargumenten würde PEP 646 Variadic Generics zugute kommen, insbesondere in den Formen a[*x] und a[*x, *y, p, q, *z]. Die PEP beschreibt genau diese Notation in ihrem Abschnitt „Unpacking: Star Operator“.

Es ist wichtig zu beachten, dass die Interpretation der Notation der Implementierung überlassen bleibt. Diese PEP definiert und diktiert nur das Verhalten von Python in Bezug auf übergebene Schlüsselwortargumente, nicht, wie diese Argumente von der implementierenden Klasse interpretiert und verwendet werden sollten.

Aktueller Status der Indizierungsoperation

Bevor die neue Syntax und Semantik für die Indizierungsnotation detailliert beschrieben wird, ist es relevant zu analysieren, wie die Indizierungsnotation heute funktioniert, in welchen Kontexten und wie sie sich von einem Funktionsaufruf unterscheidet.

Subscripting obj[x] ist effektiv eine alternative und spezialisierte Form der Funktionsaufrufsyntax mit einer Reihe von Unterschieden und Einschränkungen im Vergleich zu obj(x). Die aktuelle Python-Syntax konzentriert sich ausschließlich auf die Position zur Angabe des Indexes und enthält auch syntaktischen Zucker, um nicht-punktuelle Auswahlen (Slices) zu referenzieren. Einige gängige Beispiele

>>> a[3]       # returns the fourth element of 'a'
>>> a[1:10:2]  # slice notation (extract a non-trivial data subset)
>>> a[3, 2]    # multiple indexes (for multidimensional arrays)

Dies wird in einen __(get|set|del)item__ Dunder-Aufruf übersetzt, an den ein einzelner Parameter mit dem Index (für __getitem__ und __delitem__) oder zwei Parameter mit Index und Wert (für __setitem__) übergeben wird.

Das Verhalten des Indizierungsaufrufs unterscheidet sich in verschiedenen Aspekten grundlegend von einem Funktionsaufruf

Der erste Unterschied liegt in der Bedeutung für den Leser. Ein Funktionsaufruf bedeutet „beliebiger Funktionsaufruf mit potenziellen Seiteneffekten“. Eine Indizierungsoperation bedeutet „Nachschlagen“, typischerweise um auf eine Teilmenge oder einen spezifischen Teilaspekt einer Entität zu zeigen (wie im Fall der Tippnotation). Dieser grundlegende Unterschied bedeutet, dass, obwohl wir Missbrauch nicht verhindern können, Implementierer sich bewusst sein sollten, dass die Einführung von Schlüsselwortargumenten zur Änderung des Nachschlageverhaltens gegen diese intrinsische Bedeutung verstoßen kann.

Der zweite Unterschied der Indizierungsnotation im Vergleich zu einer Funktion besteht darin, dass die Indizierung sowohl für Lese- als auch für Schreibvorgänge verwendet werden kann. In Python kann eine Funktion nicht auf der linken Seite einer Zuweisung stehen. Mit anderen Worten, beides ist gültig

>>> x = a[1, 2]
>>> a[1, 2] = 5

aber nur das erste dieser beiden ist gültig

>>> x = f(1, 2)
>>> f(1, 2) = 5  # invalid

Diese Asymmetrie ist wichtig und lässt einen verstehen, dass es ein natürliches Ungleichgewicht zwischen den beiden Formen gibt. Es ist daher nicht gegeben, dass die beiden transparent und symmetrisch funktionieren sollten.

Der dritte Unterschied besteht darin, dass Funktionen Namen für ihre Argumente zugewiesen haben, es sei denn, die übergebenen Parameter werden mit *args erfasst, in welchem Fall sie als Einträge im args-Tupel landen. Mit anderen Worten, Funktionen haben bereits anonyme Argumentsemantik, genau wie die Indizierungsoperation. Allerdings erhält __(get|set|del)item__ nicht immer ein Tupel als index-Argument (um im Verhalten mit *args einheitlich zu sein). Tatsächlich wird bei einer trivialen Klasse

class X:
    def __getitem__(self, index):
        print(index)

Die Indexoperation leitet den Inhalt der eckigen Klammern praktisch „wie er ist“ an das index-Argument weiter

>>> x=X()
>>> x[0]
0
>>> x[0, 1]
(0, 1)
>>> x[(0, 1)]
(0, 1)
>>>
>>> x[()]
()
>>> x[{1, 2, 3}]
{1, 2, 3}
>>> x["hello"]
hello
>>> x["hello", "hi"]
('hello', 'hi')

Der vierte Unterschied besteht darin, dass die Indizierungsoperation dank Unterstützung durch den Parser weiß, wie sie Doppelpunkt-Notation in Slices umwandelt. Dies ist gültig

a[1:3]

dieses hier nicht

f(1:3)

Der fünfte Unterschied besteht darin, dass es keine Null-Argument-Form gibt. Dies ist gültig

f()

dieses hier nicht

a[]

Spezifikation

Bevor die Spezifikation beschrieben wird, ist es wichtig, den Unterschied in der Nomenklatur zwischen *Positionsindex*, *finalem Index* und *Schlüsselwortargument* hervorzuheben, da es wichtig ist, die fundamentalen Asymmetrien zu verstehen. __(get|set|del)item__ ist im Grunde eine Indizierungsoperation, und die Art und Weise, wie das Element abgerufen, gesetzt oder gelöscht wird, geschieht über einen Index, den *finalen Index*.

Der aktuelle Status quo ist, den *finalen Index* direkt aus dem, was zwischen eckigen Klammern übergeben wird, dem *Positionsindex*, zu erstellen. Mit anderen Worten, was in den eckigen Klammern übergeben wird, wird trivial verwendet, um das zu generieren, was der Code in __getitem__ dann für die Indizierungsoperation verwendet. Wie wir bereits für das dict gesehen haben, hat d[1] einen Positionsindex von 1 und auch einen finalen Index von 1 (weil es das Element ist, das dann zum Dictionary hinzugefügt wird), und d[1, 2] hat einen Positionsindex von (1, 2) und einen finalen Index ebenfalls von (1, 2) (weil es erneut das Element ist, das zum Dictionary hinzugefügt wird). Der Positionsindex d[1,2:3] wird jedoch vom Dictionary nicht akzeptiert, da es keine Möglichkeit gibt, den Positionsindex in einen finalen Index umzuwandeln, da das Slice-Objekt nicht hashbar ist. Der Positionsindex ist das, was derzeit als index-Parameter in __getitem__ bekannt ist. Nichtsdestotrotz hindert nichts daran, eine dictionary-ähnliche Klasse zu erstellen, die den finalen Index erzeugt, indem sie z. B. den Positionsindex in eine Zeichenkette umwandelt.

Diese PEP erweitert den aktuellen Status Quo und gewährt mehr Flexibilität, den finalen Index über eine erweiterte Syntax zu erstellen, die den Positionsindex und Schlüsselwortargumente kombiniert, falls übergeben.

Das Obige bringt einen wichtigen Punkt hervor. Schlüsselwortargumente können im Kontext der Indexoperation verwendet werden, um Indexierungsentscheidungen zur Erlangung des finalen Index zu treffen, und müssen daher Werte akzeptieren, die für Funktionen unkonventionell sind. Siehe zum Beispiel Anwendungsfall 1, bei dem ein Slice akzeptiert wird.

Die erfolgreiche Implementierung dieser PEP führt zu folgendem Verhalten

  1. Ein leerer Subscript ist weiterhin illegal, unabhängig vom Kontext (siehe Abgelehnte Ideen)
    obj[]  # SyntaxError
    
  2. Ein einzelner Indexwert bleibt ein einzelner Indexwert, wenn er übergeben wird
    obj[index]
    # calls type(obj).__getitem__(obj, index)
    
    obj[index] = value
    # calls type(obj).__setitem__(obj, index, value)
    
    del obj[index]
    # calls type(obj).__delitem__(obj, index)
    

    Dies gilt auch dann, wenn der Index von Schlüsselwörtern gefolgt wird; siehe Punkt 5 unten.

  3. Komma-getrennte Argumente werden weiterhin als Tupel geparst und als einzelnes Positionsargument übergeben
    obj[spam, eggs]
    # calls type(obj).__getitem__(obj, (spam, eggs))
    
    obj[spam, eggs] = value
    # calls type(obj).__setitem__(obj, (spam, eggs), value)
    
    del obj[spam, eggs]
    # calls type(obj).__delitem__(obj, (spam, eggs))
    

    Die oben genannten Punkte bedeuten, dass Klassen, die keine Schlüsselwortargumente in Subscripts unterstützen möchten, nichts tun müssen und das Feature daher vollständig abwärtskompatibel ist.

  4. Schlüsselwortargumente, falls vorhanden, müssen auf Positionsargumente folgen
    obj[1, 2, spam=None, 3]  # SyntaxError
    

    Dies ist wie bei Funktionsaufrufen, wo die Vermischung von Positions- und Schlüsselwortargumenten zu einem SyntaxError führt.

  5. Schlüsselwort-Subscripts, falls vorhanden, werden wie bei Funktionsaufrufen behandelt. Beispiele
    # Single index with keywords:
    
    obj[index, spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, index, spam=1, eggs=2)
    
    obj[index, spam=1, eggs=2] = value
    # calls type(obj).__setitem__(obj, index, value, spam=1, eggs=2)
    
    del obj[index, spam=1, eggs=2]
    # calls type(obj).__delitem__(obj, index, spam=1, eggs=2)
    
    # Comma-separated indices with keywords:
    
    obj[foo, bar, spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, (foo, bar), spam=1, eggs=2)
    
    obj[foo, bar, spam=1, eggs=2] = value
    # calls type(obj).__setitem__(obj, (foo, bar), value, spam=1, eggs=2)
    
    del obj[foo, bar, spam=1, eggs=2]
    # calls type(obj).__detitem__(obj, (foo, bar), spam=1, eggs=2)
    

    Beachten Sie, dass

    • ein einzelner Positionsindex nicht in ein Tupel umgewandelt wird, nur weil ein Schlüsselwort hinzugefügt wird.
    • für __setitem__ wird die gleiche Reihenfolge für Index und Wert beibehalten. Die Schlüsselwortargumente kommen am Ende, wie es bei einer Funktionsdefinition üblich ist.
  6. Die gleichen Regeln gelten für Schlüsselwort-Subscripts wie für Schlüsselwörter bei Funktionsaufrufen
    • der Interpreter ordnet jedes Schlüsselwort-Subscript einem benannten Parameter in der entsprechenden Methode zu;
    • wenn ein benannter Parameter zweimal verwendet wird, ist dies ein Fehler;
    • wenn nach der Verwendung aller Schlüsselwörter noch benannte Parameter übrig sind (ohne Wert), erhalten sie ihren Standardwert (falls vorhanden);
    • wenn ein solcher Parameter keinen Standardwert hat, ist dies ein Fehler;
    • wenn nach dem Auffüllen aller benannten Parameter noch Schlüsselwort-Subscripts übrig sind und die Methode einen **kwargs-Parameter hat, werden diese als Dict an den **kwargs-Parameter gebunden;
    • aber wenn kein **kwargs-Parameter definiert ist, ist dies ein Fehler.
  7. Sequenz-Unpacking ist innerhalb von Subscripts erlaubt
    obj[*items]
    

    Dies ermöglicht Notationen wie [:, *args, :], die als [(slice(None), *args, slice(None))] behandelt werden könnten. Mehrfach-Stern-Unpacking ist erlaubt

    obj[1, *(2, 3), *(4, 5), 6, foo=5]
    # Equivalent to obj[(1, 2, 3, 4, 5, 6), foo=3)
    

    Die folgende Notation-Äquivalenz muss eingehalten werden

    obj[*()]
    # Equivalent to obj[()]
    
    obj[*(), foo=3]
    # Equivalent to obj[(), foo=3]
    
    obj[*(x,)]
    # Equivalent to obj[(x,)]
    
    obj[*(x,),]
    # Equivalent to obj[(x,)]
    

    Beachten Sie insbesondere Fall 3: Sequenz-Unpacking eines einzelnen Elements verhält sich nicht so, als ob nur ein einzelnes Argument übergeben wurde. Ein ähnlicher Fall ist das folgende Beispiel

    obj[1, *(), foo=5]
    # Equivalent to obj[(1,), foo=5]
    # calls type(obj).__getitem__(obj, (1,), foo=5)
    

    Wie wir jedoch bereits gesehen haben, wird aus Kompatibilitätsgründen ein einzelner Index unverändert übergeben

    obj[1, foo=5]
    # calls type(obj).__getitem__(obj, 1, foo=5)
    

    Mit anderen Worten, ein einzelner Positionsindex wird „wie er ist“ übergeben, nur wenn kein Sequenz-Unpacking vorhanden ist. Wenn Sequenz-Unpacking vorhanden ist, wird der Index zu einem Tupel, unabhängig von der resultierenden Anzahl von Elementen im Index nach dem Unpacking.

  8. Dict-Unpacking ist erlaubt
    items = {'spam': 1, 'eggs': 2}
    obj[index, **items]
    # equivalent to obj[index, spam=1, eggs=2]
    

    Die folgende Notation-Äquivalenz sollte eingehalten werden

    obj[**{}]
    # Equivalent to obj[()]
    
    obj[3, **{}]
    # Equivalent to obj[3]
    
  9. Nur-Schlüsselwort-Subscripts sind erlaubt. Der Positionsindex ist das leere Tupel
    obj[spam=1, eggs=2]
    # calls type(obj).__getitem__(obj, (), spam=1, eggs=2)
    
    obj[spam=1, eggs=2] = 5
    # calls type(obj).__setitem__(obj, (), 5, spam=1, eggs=2)
    
    del obj[spam=1, eggs=2]
    # calls type(obj).__delitem__(obj, (), spam=1, eggs=2)
    

    Die Wahl des leeren Tupels als Sentinel wurde diskutiert. Details sind im Abschnitt „Abgelehnte Ideen“ enthalten.

  10. Schlüsselwortargumente müssen Slice-Syntax erlauben
    obj[3:4, spam=1:4, eggs=2]
    # calls type(obj).__getitem__(obj, slice(3, 4, None), spam=slice(1, 4, None), eggs=2)
    

    Dies kann die Möglichkeit eröffnen, die gleiche Syntax für allgemeine Funktionsaufrufe zu akzeptieren, aber dies ist nicht Teil dieser Empfehlung.

  11. Schlüsselwortargumente erlauben Standardwerte
    # Given type(obj).__getitem__(obj, index, spam=True, eggs=2)
    obj[3]               # Valid. index = 3, spam = True, eggs = 2
    obj[3, spam=False]   # Valid. index = 3, spam = False, eggs = 2
    obj[spam=False]      # Valid. index = (), spam = False, eggs = 2
    obj[]                # Invalid.
    
  12. Die oben angegebenen Semantiken müssen auf __class__getitem__ erweitert werden: Seit PEP 560 werden Typ-Hints so verteilt, dass für x[y], wenn keine __getitem__-Methode gefunden wird und x ein Typ(Klassen)-Objekt ist und x eine Klassenmethode __class_getitem__ hat, diese Methode aufgerufen wird. Die gleichen Änderungen sollten auch auf diese Methode angewendet werden, sodass eine Schreibweise wie list[T=int] akzeptiert werden kann.

Indizierungsverhalten in Standardklassen (dict, list, etc.)

Keine der in dieser PEP vorgeschlagenen Änderungen wird das Verhalten der aktuellen Kernklassen, die Indizierung verwenden, ändern. Das Hinzufügen von Schlüsselwörtern zur Indexoperation für benutzerdefinierte Klassen ist nicht dasselbe wie die Modifizierung z. B. des Standard-dict-Typs zur Handhabung von Schlüsselwortargumenten. Tatsächlich werden dict (sowie list und andere stdlib-Klassen mit Indizierungssemantik) unverändert bleiben und weiterhin keine Schlüsselwortargumente akzeptieren. Mit anderen Worten, wenn d ein dict ist, löst die Anweisung d[1, a=2] einen TypeError aus, da ihre Implementierung die Verwendung von Schlüsselwortargumenten nicht unterstützt. Das Gleiche gilt für alle anderen Klassen (list, dict, etc.).

Sonderfälle und Tücken

Mit der Einführung der neuen Notation müssen einige Sonderfälle analysiert werden.

  1. Technisch gesehen, wenn eine Klasse ihre Getter wie folgt definiert
    def __getitem__(self, index):
    

    dann könnte der Aufrufer dies über Schlüsselwortsyntax aufrufen, wie diese beiden Fälle

    obj[3, index=4]
    obj[index=1]
    

    Das resultierende Verhalten wäre automatisch ein Fehler, da es so wäre, als würde versucht, die Methode mit zwei Werten für das index-Argument aufzurufen, und ein TypeError würde ausgelöst. Im ersten Fall wäre der index 3, im zweiten Fall wäre es das leere Tupel ().

    Beachten Sie, dass dieses Verhalten für alle derzeit vorhandenen Klassen gilt, die auf Indizierung angewiesen sind, was bedeutet, dass das neue Verhalten in dieser Hinsicht keine Rückwärtskompatibilitätsprobleme verursachen kann.

    Klassen, die dieses Verhalten explizit betonen möchten, können ihre Parameter als nur-positional definieren

    def __getitem__(self, index, /):
    
  2. ein ähnlicher Fall tritt bei der Setter-Notation auf
    # Given type(obj).__setitem__(obj, index, value):
    obj[1, value=3] = 5
    

    Dies stellt kein Problem dar, da der Wert automatisch übergeben wird und der Python-Interpreter TypeError: got multiple values for keyword argument 'value' auslöst

  3. Wenn die Subscript-Dunders so deklariert sind, dass sie Positions-oder-Schlüsselwortparameter verwenden, kann es zu überraschenden Fällen kommen, wenn Argumente an die Methode übergeben werden. Bei der Signatur
    def __getitem__(self, index, direction='north')
    

    wenn der Aufrufer dies verwendet

    obj[0, 'south']
    

    wird er wahrscheinlich vom Methodenaufruf überrascht sein

    # expected type(obj).__getitem__(obj, 0, direction='south')
    # but actually get:
    type(obj).__getitem__(obj, (0, 'south'), direction='north')
    

    Lösung: Beste Praxis legt nahe, dass Schlüsselwort-Subscripts, wenn möglich, als nur-Schlüsselwort gekennzeichnet werden sollten

    def __getitem__(self, index, *, direction='north')
    

    Der Interpreter muss diese Regel nicht erzwingen, da es Szenarien geben kann, in denen dies das gewünschte Verhalten ist. Linter können jedoch darauf hinweisen, wenn Subscript-Methoden das Schlüsselwort-Nur-Flag nicht verwenden.

  4. Wie wir gesehen haben, wird ein einzelner Wert, gefolgt von einem Schlüsselwortargument, nicht in ein Tupel umgewandelt, d. h.: d[1, a=3] wird als __getitem__(d, 1, a=3) behandelt, NICHT als __getitem__(d, (1,), a=3). Es wäre extrem verwirrend, wenn das Hinzufügen von Schlüsselwortargumenten den Typ des übergebenen Index ändern würde. Mit anderen Worten, das Hinzufügen eines Schlüsselworts zu einem einwertigen Subscript ändert es nicht in ein Tupel. Für Fälle, in denen ein tatsächliches Tupel übergeben werden muss, muss eine korrekte Syntax verwendet werden
    obj[(1,), a=3]
    # calls type(obj).__getitem__(obj, (1,), a=3)
    

    In diesem Fall übergibt der Aufruf ein einzelnes Element (das unverändert übergeben wird, wie in der obigen Regel), nur dass das einzelne Element zufällig ein Tupel ist.

    Beachten Sie, dass dieses Verhalten lediglich die Tatsache aufzeigt, dass die Notation obj[1,] eine Abkürzung für obj[(1,)] ist (und auch obj[1] eine Abkürzung für obj[(1)] ist, mit dem erwarteten Verhalten). Wenn Schlüsselwörter vorhanden sind, ist die Regel, dass diese äußerste Klammerpaar weggelassen werden kann, nicht mehr gültig

    obj[1]
    # calls type(obj).__getitem__(obj, 1)
    
    obj[1, a=3]
    # calls type(obj).__getitem__(obj, 1, a=3)
    
    obj[1,]
    # calls type(obj).__getitem__(obj, (1,))
    
    obj[(1,), a=3]
    # calls type(obj).__getitem__(obj, (1,), a=3)
    

    Dies ist besonders relevant, wenn zwei Einträge übergeben werden

    obj[1, 2]
    # calls type(obj).__getitem__(obj, (1, 2))
    
    obj[(1, 2)]
    # same as above
    
    obj[1, 2, a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    
    obj[(1, 2), a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    

    Und insbesondere, wenn das Tupel als Variable extrahiert wird

    t = (1, 2)
    obj[t]
    # calls type(obj).__getitem__(obj, (1, 2))
    
    obj[t, a=3]
    # calls type(obj).__getitem__(obj, (1, 2), a=3)
    

    Warum? Weil im Fall obj[1, 2, a=3] zwei Elemente übergeben werden (die dann als Tupel gepackt und als Index übergeben werden). Im Fall obj[(1, 2), a=3] wird ein einzelnes Element übergeben (das unverändert übergeben wird), das zufällig ein Tupel ist. Das Endergebnis ist, dass sie gleich sind.

C-Schnittstelle

Die Auflösung der Indizierungsoperation erfolgt über einen Aufruf der folgenden Funktionen

  • PyObject_GetItem(PyObject *o, PyObject *key) für die Get-Operation
  • PyObject_SetItem(PyObject *o, PyObject *key, PyObject *value) für die Set-Operation
  • PyObject_DelItem(PyObject *o, PyObject *key) für die Del-Operation

Diese Funktionen werden im gesamten Python-Interpreter ausgiebig verwendet und sind auch Teil der öffentlichen C-API, wie sie von Include/abstract.h exportiert werden. Es ist klar, dass die Signatur dieser Funktion nicht geändert werden kann und unterschiedliche C-Level-Funktionen implementiert werden müssen, um den erweiterten Aufruf zu unterstützen. Wir schlagen vor

  • PyObject_GetItemWithKeywords(PyObject *o, PyObject *key, PyObject *kwargs)
  • PyObject_SetItemWithKeywords(PyObject *o, PyObject *key, PyObject *value, PyObject *kwargs)
  • PyObject_GetItemWithKeywords(PyObject *o, PyObject *key, PyObject *kwargs)

Neue Opcodes werden für den erweiterten Aufruf benötigt. Derzeit verwendet die Implementierung BINARY_SUBSCR, STORE_SUBSCR und DELETE_SUBSCR, um die alten Funktionen aufzurufen. Wir schlagen BINARY_SUBSCR_KW, STORE_SUBSCR_KW und DELETE_SUBSCR_KW für die neuen Operationen vor. Der Compiler muss diese neuen Opcodes generieren. Die alten C-Implementierungen rufen die erweiterten Methoden auf und übergeben NULL als kwargs.

Schließlich müssen die folgenden neuen Slots zur Struktur PyMappingMethods hinzugefügt werden

  • mp_subscript_kw
  • mp_ass_subscript_kw

Diese Slots haben die entsprechende Signatur, um das Wörterbuch-Objekt zu handhaben, das die Schlüsselwörter enthält.

Empfehlungen „Wie man lehrt“

Eine Anfrage, die während der Feedbackgespräche aufkam, war, eine mögliche Erzählung für das Lehren der Funktion zu detaillieren, z. B. für Studenten, Datenwissenschaftler und ähnliche Zielgruppen. Dieser Abschnitt adressiert diesen Bedarf.

Wir werden die Indizierung nur aus der Perspektive der Nutzung beschreiben, nicht der Implementierung, da dies der Aspekt ist, dem die oben genannten Zielgruppen wahrscheinlich begegnen werden. Nur ein Teil der Benutzer muss eigene Dunder-Funktionen implementieren und kann als fortgeschrittene Nutzung betrachtet werden. Eine angemessene Erklärung könnte lauten:

Die Indizierungsoperation wird im Allgemeinen verwendet, um mit Hilfe eines Index auf eine Teilmenge eines größeren Datensatzes zu verweisen. In den gängigen Fällen besteht der Index aus einer oder mehreren Zahlen, Zeichenketten, Slices usw.

Einige Typen können Indizierung nicht nur mit dem Index, sondern auch mit benannten Werten zulassen. Diese benannten Werte werden zwischen eckigen Klammern angegeben, wobei die gleiche Syntax wie für Funktionsaufruf-Schlüsselwortargumente verwendet wird. Die Bedeutung der Namen und ihre Verwendung finden sich in der Dokumentation des Typs, da sie von Typ zu Typ variieren.

Der Lehrer wird nun einige praktische Beispiele aus der realen Welt zeigen und die Semantik der Funktion in der gezeigten Bibliothek erklären. Zum Zeitpunkt der Erstellung existieren diese Beispiele offensichtlich nicht, aber die wahrscheinlichsten Bibliotheken, die die Funktion implementieren werden, sind pandas und numpy, möglicherweise als Methode, um Spalten nach Namen zu referenzieren.

Referenzimplementierung

Eine Referenzimplementierung wird derzeit hier entwickelt [6].

Workarounds

Jede PEP, die die Python-Sprache ändert, sollte „klar erklären, warum die bestehende Sprachspezifikation nicht ausreicht, um das Problem zu lösen, das die PEP löst“.

Einige grobe Entsprechungen zur vorgeschlagenen Erweiterung, die wir Workarounds nennen, sind bereits möglich. Die Workarounds bieten eine Alternative zur Aktivierung der neuen Syntax und überlassen die Semantik der Definition an anderer Stelle.

Diese Workarounds folgen. In ihnen sind die Helfer H und P nicht als universell gedacht. Zum Beispiel könnte ein Modul oder Paket die Verwendung seiner eigenen Helfer erfordern.

  1. Benutzerdefinierte Klassen können getitem und delitem Methoden erhalten, die Werte aus einem Container abrufen bzw. löschen
    >>> val = x.getitem(1, 2, a=3, b=4)
    >>> x.delitem(1, 2, a=3, b=4)
    

    Das Gleiche kann nicht für setitem getan werden. Es ist keine gültige Syntax

    >>> x.setitem(1, 2, a=3, b=4) = val
    SyntaxError: can't assign to function call
    
  2. Eine Hilfsklasse, hier H genannt, kann verwendet werden, um die Rollen von Container und Parameter zu vertauschen. Mit anderen Worten, wir verwenden
    H(1, 2, a=3, b=4)[x]
    

    als Ersatz für

    x[1, 2, a=3, b=4]
    

    Diese Methode funktioniert für getitem, delitem und auch für setitem. Dies liegt daran, dass

    >>> H(1, 2, a=3, b=4)[x] = val
    

    gültige Syntax ist, der die entsprechende Semantik gegeben werden kann.

  3. Eine Hilfsfunktion, hier P genannt, kann verwendet werden, um die Argumente in einem einzelnen Objekt zu speichern. Zum Beispiel
    >>> x[P(1, 2, a=3, b=4)] = val
    

    gültige Syntax ist und ihr die entsprechende Semantik gegeben werden kann.

  4. Die lo:hi:step-Syntax für Slices ist manchmal sehr nützlich. Diese Syntax ist in den Workarounds nicht direkt verfügbar. Allerdings
    s[lo:hi:step]
    

    bietet einen Workaround, der überall verfügbar ist, wo

    class S:
        def __getitem__(self, key): return key
    
    s = S()
    

    das Hilfsobjekt s definiert.

Abgelehnte Ideen

Frühere PEP 472-Lösungen

PEP 472 präsentiert eine gute Anzahl von Ideen, die nun alle als abgelehnt gelten. Eine persönliche E-Mail von D’Aprano an den Autor besagte ausdrücklich

Ich habe nun die PEP 472 vollständig gelesen und fürchte, ich kann keine der derzeit in der PEP enthaltenen Strategien unterstützen.

Wir stimmen zu, dass diese Optionen aus den einen oder anderen Gründen den aktuell vorgestellten unterlegen sind.

Um dieses Dokument kompakt zu halten, werden wir hier nicht die Einwände gegen alle in PEP 472 vorgestellten Optionen darstellen. Es genügt zu sagen, dass sie diskutiert wurden und jede vorgeschlagene Alternative einen oder wenige Dealbreaker hatte.

Hinzufügen neuer Dunders

Es wurde vorgeschlagen, neue Dunders __(get|set|del)item_ex__ einzuführen, die über die __(get|set|del)item__-Triade aufgerufen werden, falls sie vorhanden sind.

Die Begründung für diese Wahl ist, die Intuition für die Unterstützung von Schlüsselwortargumenten für eckige Klammern offensichtlicher und im Einklang mit dem Funktionsverhalten zu machen. Angesichts

def __getitem_ex__(self, x, y): ...

All diese funktionieren einfach und produzieren mühelos das gleiche Ergebnis

obj[1, 2]
obj[1, y=2]
obj[y=2, x=1]

Mit anderen Worten, diese Lösung würde das Verhalten von __getitem__ an die traditionelle Funktionssignatur anpassen. Da wir jedoch __getitem__ nicht ändern und die Abwärtskompatibilität brechen können, hätten wir eine erweiterte Version, die bevorzugt verwendet wird.

Die Probleme mit diesem Ansatz wurden wie folgt festgestellt:

  • Es verlangsamt die Indizierung. Bei jedem Zugriff auf einen Index wird dieses neue Dunder-Attribut in der Klasse untersucht. Wenn es nicht vorhanden ist, wird die Standardfunktion zur Schlüsselübersetzung ausgeführt. Es wurden verschiedene Ideen zur Bewältigung dieses Problems vorgeschlagen, vom reinen Umwickeln der Methode zum Zeitpunkt der Klasseninstanziierung bis hin zum Hinzufügen eines Bitflags, das die Verfügbarkeit dieser Methoden signalisiert. Unabhängig von der Lösung wäre das neue Dunder nur dann wirksam, wenn es bei der Klassenerstellung hinzugefügt wird, nicht aber, wenn es später hinzugefügt wird. Dies wäre ungewöhnlich und würde das Monkeypatching der Methoden aus welchen Gründen auch immer verbieten (und unerwartet funktionieren).
  • Es erhöht die Komplexität des Mechanismus.
  • Erfordert eine lange und schmerzhafte Übergangszeit, in der Bibliotheken irgendwie beide Aufrufkonventionen unterstützen müssen, da höchstwahrscheinlich die erweiterten Methoden bei Übereinstimmung der richtigen Bedingungen in den Argumenten auf die traditionellen delegieren, oder einige Klassen die traditionellen Dunder und andere die erweiterten Dunder unterstützen werden. Dies wird zwar den aufrufenden Code nicht beeinträchtigen, aber die Entwicklung.
  • Es könnte potenziell zu gemischten Situationen führen, in denen die erweiterte Version für den Getter definiert ist, aber nicht für den Setter.
  • In der Signatur von __setitem_ex__ müsste `value` zum ersten Element gemacht werden, da der Index eine beliebige Länge hat, abhängig von den angegebenen Indizes. Dies würde ungeschickt aussehen, da die visuelle Notation nicht mit der Signatur übereinstimmt.
    obj[1, 2] = 3
    # calls type(obj).__setitem_ex__(obj, 3, 1, 2)
    
  • Die Lösung beruht auf der Annahme, dass alle Schlüsselwortindizes notwendigerweise Positionsindizes zugeordnet sind oder einen Namen haben müssen. Diese Annahme könnte falsch sein: xarray, das wichtigste Python-Paket für numpy-Arrays mit benannten Dimensionen, unterstützt die Indizierung durch zusätzliche Dimensionen (sogenannte „non-dimension coordinates“), die nicht direkt den Dimensionen des zugrundeliegenden numpy-Arrays entsprechen, und diese haben keine Position, zu der sie passen würden. Mit anderen Worten, anonyme Indizes sind ein plausibler Anwendungsfall, den diese Lösung eliminieren würde, obwohl man argumentieren könnte, dass die Verwendung von *args dieses Problem lösen würde.

Hinzufügen einer Adapterfunktion

Ähnlich wie oben, in dem Sinne, dass eine Vorfunktion aufgerufen würde, um die „neue Stil“-Indizierung in eine „alte Stil“-Indizierung umzuwandeln, die dann übergeben wird. Hat Probleme, die den oben genannten ähneln.

Erstellen eines neuen „kwslice“-Objekts

Dieser Vorschlag wurde bereits in „New arguments contents“ P4 in PEP 472 untersucht.

obj[a, b:c, x=1]
# calls type(obj).__getitem__(obj, a, slice(b, c), key(x=1))

Diese Lösung erfordert, dass jeder, der Schlüsselwortargumente benötigt, das Tupel und/oder das Schlüsselobjekt manuell parst, um sie zu extrahieren. Dies ist mühsam und führt dazu, dass die Get/Set/Del-Funktion immer beliebige Schlüsselwortargumente akzeptiert, ob sie sinnvoll sind oder nicht. Wir möchten, dass der Entwickler angeben kann, welche Argumente sinnvoll sind und welche nicht.

Verwendung eines einzelnen Bits zur Verhaltensänderung

Ein spezielles Klassen-Dunder-Flag

__keyfn__ = True

würde die Signatur von __get|set|delitem__ in eine „funktionsähnliche“ Dispatch ändern, was bedeutet, dass dies

>>> d[1, 2, z=3]

zu einem Aufruf von führen würde

>>> type(obj).__getitem__(obj, 1, 2, z=3)
# instead of type(obj).__getitem__(obj, (1, 2), z=3)

Diese Option wurde abgelehnt, da es seltsam erscheint, dass die Signatur einer Methode von einem bestimmten Wert eines anderen Dunders abhängt. Dies wäre sowohl für statische Typenprüfer als auch für Menschen verwirrend: Ein statischer Typenprüfer müsste einen Sonderfall dafür hart kodieren, da es wirklich nichts anderes in Python gibt, bei dem die Signatur eines Dunders vom Wert eines anderen Dunders abhängt. Ein Mensch, der einen __getitem__ Dunder implementieren muss, müsste in der Klasse (oder in einer ihrer Unterklassen) nach einem __keyfn__ suchen, bevor der Dunder geschrieben werden kann. Darüber hinaus würde das Hinzufügen von Basisklassen, bei denen das Flag __keyfn__ gesetzt ist, die Signatur der aktuellen Methoden brechen. Dies wäre noch problematischer, wenn das Flag zur Laufzeit geändert wird oder wenn das Flag durch Aufruf einer Funktion generiert wird, die zufällig True oder etwas anderes zurückgibt.

Zulassen leerer Indexnotation obj[]

Der aktuelle Vorschlag verhindert, dass obj[] eine gültige Notation ist. Ein Kommentator bemerkte jedoch

Wir haben Tuple[int, int] als Tupel von zwei Ganzzahlen. Und wir haben Tuple[int] als Tupel von einer Ganzzahl. Und gelegentlich müssen wir ein Tupel von *keinen* Werten schreiben, da dies der Typ von () ist. Aber wir sind derzeit gezwungen, dies als Tuple[()] zu schreiben. Wenn wir Tuple[] erlauben würden, würde dieser seltsame Grenzfall entfallen.

Also wäre ich wahrscheinlich damit einverstanden, obj[] syntaktisch zuzulassen, solange der Diktatyp ihn ablehnen kann.

Dieser Vorschlag hat bereits festgelegt, dass im Fall, dass kein positionsbezogener Index angegeben wird, der übergebene Wert das leere Tupel sein muss. Das Zulassen der Notation eines leeren Index würde dazu führen, dass der Diktatyp ihn automatisch akzeptiert, um den Wert mit dem leeren Tupel als Schlüssel einzufügen oder darauf zu verweisen. Darüber hinaus kann eine Typnotationsweise wie Tuple[] leicht als Tuple ohne die Indizierungsnotation geschrieben werden.

Nachfolgende Diskussionen mit Brandt Bucher während der Implementierung haben jedoch ergeben, dass der Fall obj[] sich gut in eine natürliche Entwicklung für variadische Generika einfügen würde, was dem obigen Kommentar mehr Gewicht verleiht. Am Ende haben wir uns nach einer Diskussion zwischen D’Aprano, Bucher und dem Autor darauf geeinigt, die Notation obj[] vorerst als Syntaxfehler zu belassen und die Notation möglicherweise mit einer zusätzlichen PEP zu erweitern, um die Äquivalenz obj[] als obj[()] zu behandeln.

Sentinel-Wert für keinen gegebenen Positionsindex

Das Thema, welcher Wert als Index im Fall von übergeben werden soll

obj[k=3]

wurde erheblich debattiert.

Eine scheinbar rationale Wahl wäre, gar keinen Wert zu übergeben, indem man die Keyword-Only-Argument-Funktion nutzt, aber das funktioniert leider nicht gut mit dem __setitem__ Dunder, da immer ein positionelles Element für den Wert übergeben wird, und wir können den Index-Parameter nicht „überspringen“, es sei denn, wir führen ein sehr seltsames Verhalten ein, bei dem das erste Argument sich auf den Index bezieht, wenn es angegeben ist, und auf den Wert, wenn der Index nicht angegeben ist. Dies ist extrem täuschend und fehleranfällig.

Die obige Überlegung macht es unmöglich, einen Keyword-Only-Dunder zu haben, und wirft die Frage auf, welche Entität für die Indexposition übergeben werden soll, wenn kein Index übergeben wird.

obj[k=3] = 5
# would call type(obj).__setitem__(obj, ???, 5, k=3)

Ein vorgeschlagener Hack wäre, den Benutzer angeben zu lassen, welche Entität verwendet werden soll, wenn kein Index angegeben wird, indem ein Standardwert für den index angegeben wird. Dies zwingt jedoch notwendigerweise, auch einen (nie zu verwendenden, da ein Wert aufgrund des Designs immer übergeben wird) Standardwert für den value anzugeben, da wir keine Nicht-Standardargumente nach Standardargumenten haben können.

def __setitem__(self, index=SENTINEL, value=NEVERUSED, *, k)

was hässlich, redundant und verwirrend erscheint. Wir müssen daher akzeptieren, dass eine Form von Sentinel-Index von der Python-Implementierung übergeben werden muss, wenn die Notation obj[k=3] verwendet wird. Dies bedeutet auch, dass Standardargumente für diese Parameter einfach nie verwendet werden (aber das ist bereits bei der aktuellen Implementierung der Fall, also keine Änderung dort).

Zusätzlich möchten einige Klassen **kwargs anstelle eines Keyword-Only-Arguments verwenden, was bedeutet, dass eine Definition wie

def __setitem__(self, index, value, **kwargs):

und ein Benutzer, der ein Schlüsselwort value übergeben möchte

x[value=1] = 0

erwartet einen Aufruf wie

type(obj).__setitem__(obj, SENTINEL, 0, **{"value": 1})

stattdessen versehentlich vom benannten value erfasst wird, was zu einem duplicate value error führt. Der Benutzer sollte sich nicht um die tatsächlichen lokalen Namen dieser beiden Argumente sorgen müssen, wenn sie für alle praktischen Zwecke positionsbezogen sind. Leider stellt die Verwendung von positionsbezogenen Werten sicher, dass dies nicht geschieht, löst aber immer noch nicht die Notwendigkeit, sowohl index als auch value zu übergeben, auch wenn der Index nicht bereitgestellt wird. Der Punkt ist, dass der Benutzer nicht daran gehindert werden sollte, Schlüsselwortargumente zu verwenden, um auf eine Spalte index, value (oder self) zu verweisen, nur weil der Klassenimplementierer diese Namen in der Parameterliste verwendet.

Darüber hinaus verlangen wir, dass die drei Dunder auf die gleiche Weise funktionieren: Es wäre äußerst unbequem, wenn nur __setitem__ diesen Sentinel erhalten würde und __get|delitem__ nicht, da sie mit einer Signatur davonkommen können, die keine Indexspezifikation erlaubt, und somit einen benutzerdefinierten Standardindex ermöglicht.

Welche Wahl des Sentinels auch immer getroffen wird, sie wird dazu führen, dass die folgenden Fälle degenerieren und somit im Dunder nicht mehr zu unterscheiden sind.

obj[k=3]
obj[SENTINEL, k=3]

Die Frage verschiebt sich nun darauf, welche Entität den Sentinel darstellen soll: Die Optionen waren

  1. Leeres Tupel
  2. Keine
  3. NichtImplementiert
  4. Ein neues Sentinel-Objekt (z. B. `NoIndex`)

Für Option 1 wird der Aufruf lauten

type(obj).__getitem__(obj, (), k=3)

wodurch obj[k=3] und obj[(), k=3] degenerieren und nicht mehr unterscheidbar sind.

Diese Option klingt ansprechend, weil

  1. Die NumPy-Community wurde befragt [5], und der allgemeine Konsens der Antworten war, dass das leere Tupel als passend empfunden wurde.
  2. Sie zeigt eine Parallele zum Verhalten von *args in einer Funktion, wenn keine Positionsargumente gegeben sind.
    >>> def foo(*args, **kwargs):
    ...     print(args, kwargs)
    ...
    >>> foo(k=3)
    () {'k': 3}
    

    Obwohl wir die folgende Asymmetrie im Verhalten im Vergleich zu Funktionen akzeptieren, wenn ein einzelner Wert übergeben wird, ist dieses Schiff bereits abgefahren.

    >>> foo(5, k=3)
    (5,) {'k': 3}   # for indexing, a plain 5, not a 1-tuple is passed
    

Für Option 2, die Verwendung von None, wurde beanstandet, dass NumPy sie verwendet, um das Einfügen einer neuen Achse/Dimension anzuzeigen (es gibt auch einen np.newaxis Alias).

arr = np.array(5)
arr.ndim == 0
arr[None].ndim == arr[None,].ndim == 1

Das ist zwar kein unüberwindbares Problem, aber es wird sich sicherlich auf NumPy auswirken.

Die einzigen Probleme bei beiden obigen Optionen sind, dass sowohl das leere Tupel als auch `None` potenzielle legitime Indizes sind und es von Wert sein könnte, die beiden degenerierten Fälle unterscheiden zu können.

Eine alternative Strategie (Option 3) wäre daher, eine existierende Entität zu verwenden, die unwahrscheinlich als gültiger Index verwendet wird. Eine Option könnte die aktuelle eingebaute Konstante NotImplemented sein, die derzeit von Operatormethoden zurückgegeben wird, um zu melden, dass sie eine bestimmte Operation nicht implementieren und eine andere Strategie versucht werden sollte (z. B. das andere Objekt abfragen). Leider rufen ihr Name und ihre traditionelle Verwendung eine nicht verfügbare Funktion auf, anstatt die Tatsache, dass etwas nicht vom Benutzer übergeben wurde.

Dies lässt uns mit Option 4: eine neue eingebaute Konstante. Diese Konstante muss unhashable sein (damit sie nie ein gültiger Schlüssel ist) und einen klaren Namen haben, der ihren Kontext deutlich macht: NoIndex. Dies würde alle obigen Probleme lösen, aber die Frage ist: Lohnt es sich?

Aus einer schnellen Nachfrage scheint es, dass die meisten Leute auf python-ideas es nicht für entscheidend halten und das leere Tupel eine akzeptable Option ist. Daher wird die resultierende Serie sein

obj[k=3]
# type(obj).__getitem__(obj, (), k=3). Empty tuple

obj[1, k=3]
# type(obj).__getitem__(obj, 1, k=3). Integer

obj[1, 2, k=3]
# type(obj).__getitem__(obj, (1, 2), k=3). Tuple

und die folgenden beiden Notationen werden degeneriert sein.

obj[(), k=3]
# type(obj).__getitem__(obj, (), k=3)

obj[k=3]
# type(obj).__getitem__(obj, (), k=3)

Gängige Einwände

  1. Verwenden Sie einfach einen Methodenaufruf.

    Einer der Anwendungsfälle ist die Typisierung, bei der die Indizierung ausschließlich verwendet wird und Funktionsaufrufe außer Frage stehen. Darüber hinaus verarbeiten Funktionsaufrufe keine Slice-Notation, die in einigen Fällen für Arrays üblich ist.

    Ein Problem ist, dass die Erstellung von Typ-Hinweisen in Python 3.9 auf integrierte Typen erweitert wurde, sodass Sie `Dict`, `List` usw. nicht mehr importieren müssen.

    Ohne `kwargs` innerhalb von [] könnten Sie dies nicht tun.

    Vector = dict[i=float, j=float]
    

    Aber aus offensichtlichen Gründen ist die Aufrufsyntax mit integrierten Typen zur Erstellung benutzerdefinierter Typ-Hinweise keine Option.

    dict(i=float, j=float)
    # would create a dictionary, not a type
    

    Schließlich erlauben Funktionsaufrufe keine `setitem`-ähnliche Notation, wie in der Übersicht gezeigt: Operationen wie f(1, x=3) = 5 sind nicht erlaubt und stattdessen für Indexierungsoperationen zulässig.

Referenzen


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

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