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

Python Enhancement Proposals

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

Inhaltsverzeichnis

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 von f(obj) würde obj.name zurückgeben, wenn das Attribut existiert, andernfalls XYZ.
  • itemgetter: f = itemgetter(2, default=XYZ) gefolgt von f(obj) würde obj[2] zurückgeben, wenn dies gültig ist, andernfalls XYZ.
  • getitem: getitem(obj, k, XYZ) oder getitem(obj, k, default=XYZ) würde obj[k] zurückgeben, wenn dies gültig ist, andernfalls XYZ.

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.


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

Zuletzt geändert: 2025-05-26 08:01:11 GMT