PEP 769 – Füge ein Schlüsselwortargument „default“ zu „attrgetter“, „itemgetter“ und „getitem“ hinzu
- Autor:
- Facundo Batista <facundo at taniquetil.com.ar>
- Discussions-To:
- Discourse thread
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 22-Dez-2024
- Python-Version:
- 3.14
- Post-History:
- 07-Jan-2025
- Resolution:
- 14-Mrz-2025
Zusammenfassung
Dieser Vorschlag zielt darauf ab, das operator-Modul zu verbessern, indem ein Schlüsselwortargument default zu den Funktionen attrgetter, itemgetter und getitem hinzugefügt wird. Diese Ergänzung würde es diesen Funktionen ermöglichen, einen angegebenen Standardwert zurückzugeben, wenn das Zielattribut oder -element fehlt, wodurch Ausnahmen verhindert und der Code zur Behandlung optionaler Attribute oder Elemente vereinfacht wird.
Motivation
Derzeit lösen attrgetter und itemgetter Ausnahmen aus, wenn das angegebene Attribut oder Element fehlt. Diese Einschränkung erfordert von Entwicklern zusätzliche Fehlerbehandlung zu implementieren, was zu komplexerem und weniger lesbarem Code führt.
Die Einführung eines default-Parameters würde Operationen mit optionalen Attributen oder Elementen optimieren, Boilerplate-Code reduzieren und die Klarheit des Codes verbessern.
Eine ähnliche Situation tritt bei getitem auf, mit der zusätzlichen Nuance, dass die Möglichkeit, einen Standardwert anzugeben, eine langjährige Asymmetrie mit der integrierten Funktion getattr() beheben würde.
Begründung
Die primäre Designentscheidung ist die Einführung eines einzigen default-Parameters, der für alle angegebenen Attribute oder Elemente gilt.
Dieser Ansatz bewahrt die Einfachheit und vermeidet die Komplexität der Zuweisung individueller Standardwerte zu mehreren Attributen oder Elementen. Obwohl einige Diskussionen die Zulassung mehrerer Standardwerte berücksichtigten, führten die erhöhte Komplexität und die potenzielle Verwirrung dazu, einen einzelnen Standardwert für alle Fälle zu bevorzugen (mehr dazu unten unter Abgelehnte Ideen).
Spezifikation
Vorgeschlagene Verhaltensweisen
- attrgetter:
f = attrgetter("name", default=XYZ)gefolgt vonf(obj)würdeobj.namezurückgeben, wenn das Attribut existiert, andernfallsXYZ. - itemgetter:
f = itemgetter(2, default=XYZ)gefolgt vonf(obj)würdeobj[2]zurückgeben, wenn dies gültig ist, andernfallsXYZ. - getitem:
getitem(obj, k, XYZ)odergetitem(obj, k, default=XYZ)würdeobj[k]zurückgeben, wenn dies gültig ist, andernfallsXYZ.
In den ersten beiden Fällen wirkt sich die Verbesserung auf einzelne und mehrfache Attribut-/Elementabrufe aus, wobei der Standardwert für jedes fehlende Attribut oder Element zurückgegeben wird.
Es wird keine Funktionalitätsänderung in einem Fall vorgenommen, wenn das zusätzliche Standard-(Schlüsselwort-)Argument nicht verwendet wird.
Beispiele für attrgetter
Das aktuelle Verhalten bleibt unverändert
>>> class C:
... class D:
... class X:
... pass
... class E:
... pass
...
>>> attrgetter("D")(C)
<class '__main__.C.D'>
>>> attrgetter("badname")(C)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D", "E")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname")(C)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'C' has no attribute 'badname'
>>> attrgetter("D.X")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname")(C)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'D' has no attribute 'badname'
Mit diesem PEP, unter Verwendung des vorgeschlagenen default-Schlüsselworts
>>> attrgetter("D", default="noclass")(C)
<class '__main__.C.D'>
>>> attrgetter("badname", default="noclass")(C)
'noclass'
>>> attrgetter("D", "E", default="noclass")(C)
(<class '__main__.C.D'>, <class '__main__.C.E'>)
>>> attrgetter("D", "badname", default="noclass")(C)
(<class '__main__.C.D'>, 'noclass')
>>> attrgetter("D.X", default="noclass")(C)
<class '__main__.C.D.X'>
>>> attrgetter("D.badname", default="noclass")(C)
'noclass'
Beispiele für itemgetter
Das aktuelle Verhalten bleibt unverändert
>>> obj = ["foo", "bar", "baz"]
>>> itemgetter(1)(obj)
'bar'
>>> itemgetter(5)(obj)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> itemgetter(1, 0)(obj)
('bar', 'foo')
>>> itemgetter(1, 5)(obj)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Mit diesem PEP, unter Verwendung des vorgeschlagenen default-Schlüsselworts
>>> itemgetter(1, default="XYZ")(obj)
'bar'
>>> itemgetter(5, default="XYZ")(obj)
'XYZ'
>>> itemgetter(1, 0, default="XYZ")(obj)
('bar', 'foo')
>>> itemgetter(1, 5, default="XYZ")(obj)
('bar', 'XYZ')
Beispiele für getitem
Das aktuelle Verhalten bleibt unverändert
>>> obj = ["foo", "bar", "baz"]
>>> getitem(obj, 1)
'bar'
>>> getitem(obj, 5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Mit diesem PEP, unter Verwendung des vorgeschlagenen zusätzlichen Standardwerts, positionsgebunden oder mit einem Schlüsselwort
>>> getitem(obj, 1, "XYZ")
'bar'
>>> getitem(obj, 5, "XYZ")
'XYZ'
>>> getitem(obj, 1, default="XYZ")
'bar'
>>> getitem(obj, 5, default="XYZ")
'XYZ'
Über mögliche Implementierungen
Die Implementierung von attrgetter ist recht direkt: Sie beinhaltet die Verwendung von getattr und das Abfangen eines möglichen AttributeError. Daher wäre attrgetter("name", default=XYZ)(obj) so etwas wie
try:
value = getattr(obj, "name")
except AttributeError:
value = XYZ
Beachten Sie, dass wir uns nicht auf die Verwendung von getattr mit einem Standardwert verlassen können, da es unmöglich wäre zu unterscheiden, was bei jedem Schritt zurückgegeben wurde, wenn eine Attributkette angegeben ist (z. B. attrgetter("foo.bar.baz", default=XYZ)).
Die Implementierung für itemgetter und getitem ist nicht so einfach. Der einfachste Weg ist ebenfalls einfach zu definieren und zu verstehen: Versuchen Sie __getitem__ und fangen Sie eine mögliche Ausnahme ab (siehe unten). Auf diese Weise wäre itemgetter(123, default=XYZ)(obj) oder getitem(obj, 123, default=XYZ) gleichwertig mit
try:
value = obj[123]
except (IndexError, KeyError):
value = XYZ
Aus Leistungsgründen kann die Implementierung jedoch eher wie folgt aussehen, was das exakt gleiche Verhalten aufweist
if type(obj) == dict:
value = obj.get(123, XYZ)
else:
try:
value = obj[123]
except (IndexError, KeyError):
value = XYZ
Beachten Sie, dass die Überprüfung sich auf den exakten Typ bezieht und nicht isinstance verwendet; dies dient der Sicherstellung des exakten Verhaltens, was unmöglich wäre, wenn das Objekt ein benutzerdefiniertes ist, das von dict erbt, aber get überschreibt (ähnlicher Grund, warum nicht geprüft wird, ob das Objekt eine get-Methode hat).
Auf diese Weise ist die Leistung besser, aber es handelt sich nur um ein Implementierungsdetail, sodass wir die ursprüngliche Erklärung, wie es sich verhält, beibehalten können.
Bezüglich der abzufangenden Ausnahme: Auch wenn __getitem__ IndexError, KeyError oder TypeError auslösen kann (siehe dessen Referenz), können nur die ersten beiden auftreten, wenn der Container den angegebenen Schlüssel oder Index nicht enthält, und letzteres signalisiert wahrscheinlich einen Fehler im Code, daher fangen wir ihn nicht ab, um das Standardverhalten auszulösen.
Grenzfälle
Die Bereitstellung einer default-Option würde nur funktionieren, wenn der Zugriff auf das Element/Attribut im Normalfall fehlschlägt. Mit anderen Worten, das zugegriffene Objekt sollte keine eigenen Standardwerte handhaben.
Zum Beispiel wäre Folgendes redundant/verwirrend, da defaultdict beim Zugriff auf das Element nie einen Fehler auslöst
>>> from collections import defaultdict
>>> from operator import itemgetter
>>> dd = defaultdict(int)
>>> itemgetter("foo", default=-1)(dd)
0
Dasselbe gilt für jedes benutzerdefinierte Objekt, das __getitem__ oder __getattr__ überschreibt und seine eigenen Fallbacks implementiert.
Abgelehnte Ideen
Mehrere Standardwerte
Die Idee, mehrere Standardwerte für mehrere Attribute oder Elemente zuzulassen, wurde in Betracht gezogen.
Es wurden zwei Alternativen diskutiert: die Verwendung eines Iterables, das die gleiche Anzahl von Elementen wie Parameter für attrgetter/itemgetter haben muss, oder die Verwendung eines Dictionaries mit Schlüsseln, die mit den an attrgetter/itemgetter übergebenen Namen übereinstimmen.
Das wirklich komplexe Problem hier (das die Funktion schwer zu erklären macht und verwirrende Grenzfälle aufweist), ist, was passieren würde, wenn ein Iterable oder Dictionary der *tatsächliche* gewünschte Standard für alle Elemente ist. Zum Beispiel
>>> itemgetter("a", default=(1, 2))({})
(1, 2)
>>> itemgetter("a", "b", default=(1, 2))({})
((1, 2), (1, 2))
Wenn wir „mehrere Standardwerte“ mit default zulassen, würde der erste Fall im obigen Beispiel eine Ausnahme auslösen, da es mehr Elemente als Namen im Standard gibt, und der zweite Fall würde (1, 2)) zurückgeben. Deshalb haben wir die Möglichkeit erwogen, einen anderen Namen für mehrere Standards zu verwenden (z. B. defaults, was ausdrucksstark, aber möglicherweise fehleranfällig ist, da es default zu ähnlich ist).
Ein weiterer Vorschlag, der mehrere Standardwerte ermöglichen würde, ist die Zulassung von Kombinationen von attrgetter und itemgetter, z. B.
>>> ig_a = itemgetter("a", default=1)
>>> ig_b = itemgetter("b", default=2)
>>> ig_combined = itemgetter(ig_a, ig_b)
>>> ig_combined({"a": 999})
(999, 2)
>>> ig_combined({})
(1, 2)
Das Kombinieren von itemgetter oder attrgetter ist jedoch ein völlig neues Verhalten und sehr komplex zu definieren. Obwohl nicht unmöglich, liegt es außerhalb des Rahmens dieses PEP.
Letztendlich wurde die Idee, mehrere Standardwerte zu haben, als zu komplex und potenziell verwirrend angesehen, und ein einzelner default-Parameter wurde aus Gründen der Einfachheit und Vorhersagbarkeit bevorzugt.
Konsistenz bei Tupelrückgaben
Ein weiterer abgelehnter Vorschlag war das Hinzufügen einer Flagge, um immer ein Tupel zurückzugeben, unabhängig davon, wie viele Schlüssel/Namen/Indizes angegeben wurden. Zum Beispiel
>>> letters = ["a", "b", "c"]
>>> itemgetter(1, return_tuple=True)(letters)
('b',)
>>> itemgetter(1, 2, return_tuple=True)(letters)
('b', 'c')
Dies wäre für die Konsistenz mehrerer Standardwerte von geringem Nutzen, erfordert weitere Diskussionen und liegt außerhalb des Rahmens dieses PEP.
Offene Fragen
Zu diesem Zeitpunkt gibt es keine offenen Probleme.
Wie man das lehrt
Da das grundlegende Verhalten nicht geändert wird, kann dieser neue default-Parameter beim erstmaligen Unterrichten von attrgetter und itemgetter vermieden werden. Er kann nur dann eingeführt werden, wenn die Funktionalität benötigt wird.
Abwärtskompatibilität
Die vorgeschlagenen Änderungen sind abwärtskompatibel. Der default-Parameter ist optional; bestehender Code ohne diesen Parameter funktioniert wie bisher. Nur Code, der den neuen default-Parameter explizit verwendet, zeigt das neue Verhalten, wodurch keine Unterbrechung bestehender Implementierungen entsteht.
Sicherheitsimplikationen
Die Einführung eines default-Parameters führt an sich keine Sicherheitslücken ein.
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-0769.rst
Zuletzt geändert: 2025-05-26 08:01:11 GMT