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

Python Enhancement Proposals

PEP 661 – Sentinel-Werte

Autor:
Tal Einat <tal at python.org>
Discussions-To:
Discourse thread
Status:
Verschoben
Typ:
Standards Track
Erstellt:
06-Juni-2021
Post-History:
20. Mai 2021, 06. Juni 2021

Inhaltsverzeichnis

TL;DR: Siehe die Spezifikation und die Referenzimplementierung.

Zusammenfassung

Eindeutige Platzhalterwerte, allgemein bekannt als „Sentinel-Werte“, sind in der Programmierung üblich. Sie haben viele Verwendungszwecke, wie z. B. für

  • Standardwerte für Funktionsargumente, wenn kein Wert angegeben wurde
    def foo(value=None):
        ...
    
  • Rückgabewerte von Funktionen, wenn etwas nicht gefunden oder verfügbar ist
    >>> "abc".find("d")
    -1
    
  • Fehlende Daten, wie NULL in relationalen Datenbanken oder „N/A“ („nicht verfügbar“) in Tabellenkalkulationen

Python hat den Sonderwert None, der in den meisten Fällen als solcher Sentinel-Wert verwendet werden soll. Manchmal wird jedoch ein alternativer Sentinel-Wert benötigt, normalerweise, wenn er sich von None unterscheiden muss, da None in diesem Kontext ein gültiger Wert ist. Solche Fälle sind häufig genug, dass im Laufe der Jahre mehrere Idiome zur Implementierung solcher Sentinels entstanden sind, aber nicht häufig genug, dass es keinen klaren Bedarf an Standardisierung gab. Die gängigen Implementierungen, einschließlich einiger in der Standardbibliothek, leiden jedoch unter mehreren erheblichen Nachteilen.

Diese PEP schlägt die Hinzufügung eines Dienstprogramms zur Definition von Sentinel-Werten vor, das in der Standardbibliothek verwendet und als Teil der Standardbibliothek öffentlich zugänglich gemacht werden soll.

Hinweis: Die Änderung aller bestehenden Sentinels in der Standardbibliothek, um sie auf diese Weise zu implementieren, wird nicht als notwendig erachtet, und ob dies geschieht, liegt im Ermessen der Maintainer.

Motivation

Im Mai 2021 wurde auf der python-dev Mailingliste [1] eine Frage aufgeworfen, wie ein Sentinel-Wert für traceback.print_exception besser implementiert werden kann. Die bestehende Implementierung verwendete das folgende gängige Idiom

_sentinel = object()

Dieses Objekt hat jedoch ein uninformatives und übermäßig ausführliches `repr`, wodurch die Signatur der Funktion zu lang und schwer zu lesen wird.

>>> help(traceback.print_exception)
Help on function print_exception in module traceback:

print_exception(exc, /, value=<object object at
0x000002825DF09650>, tb=<object object at 0x000002825DF09650>,
limit=None, file=None, chain=True)

Zusätzlich wurden in der Diskussion zwei weitere Nachteile vieler bestehender Sentinels angesprochen

  1. Einige haben keinen eindeutigen Typ, daher ist es unmöglich, klare Typ-Signaturen für Funktionen mit solchen Sentinels als Standardwerten zu definieren.
  2. Sie verhalten sich nach dem Kopieren oder Entpickeln unerwartet, da eine separate Instanz erstellt wird und Vergleiche mit is fehlschlagen.

In der darauf folgenden Diskussion lieferte Victor Stinner eine Liste der derzeit in der Python-Standardbibliothek verwendeten Sentinel-Werte [2]. Dies zeigte, dass der Bedarf an Sentinels recht häufig ist, dass es selbst innerhalb der Standardbibliothek verschiedene Implementierungsmethoden gibt und dass viele davon unter mindestens einem der drei oben genannten Nachteile leiden.

Die Diskussion führte zu keinem klaren Konsens darüber, ob eine Standard-Implementierungsmethode benötigt oder erwünscht ist, ob die genannten Nachteile signifikant sind und welche Art von Implementierung gut wäre. Der Autor dieser PEP erstellte ein Issue auf bugs.python.org (jetzt ein GitHub Issue [3]), das Verbesserungsoptionen vorschlug, aber nur einen problematischen Aspekt einiger Fälle betraf und keine Unterstützung fand.

Eine Umfrage [4] wurde auf discuss.python.org erstellt, um ein klareres Bild von den Meinungen der Community zu bekommen. Nach fast zwei Wochen, erheblicher weiterer Diskussion und 39 Stimmen waren die Ergebnisse der Umfrage nicht schlüssig. 40 % stimmten für „Der Status quo ist in Ordnung / Es gibt keinen Bedarf an Konsistenz in dieser Frage“, aber die meisten Wähler stimmten für eine oder mehrere standardisierte Lösungen. Insbesondere wählten 37 % der Wähler „Konsequente Verwendung eines neuen, dedizierten Sentinel-Fabrik / Klasse / Meta-Klasse, die ebenfalls öffentlich in der Standardbibliothek verfügbar gemacht wird“.

Bei solch gemischten Meinungen wurde diese PEP erstellt, um eine Entscheidung zu diesem Thema zu erleichtern.

Während der Arbeit an dieser PEP, beim Iterieren über verschiedene Optionen und Implementierungen und bei fortlaufenden Diskussionen ist der Autor zu der Meinung gelangt, dass eine einfache, gute Implementierung, die in der Standardbibliothek verfügbar ist, lohnenswert wäre, sowohl für die Verwendung in der Standardbibliothek selbst als auch anderswo.

Begründung

Die Kriterien, die die gewählte Implementierung leiteten, waren

  1. Die Sentinel-Objekte sollten sich wie von einem Sentinel-Objekt erwartet verhalten: Bei Vergleichen mit dem is-Operator sollten sie immer als identisch mit sich selbst, aber niemals mit einem anderen Objekt betrachtet werden.
  2. Die Erstellung eines Sentinel-Objekts sollte eine einfache, unkomplizierte Einzeiler sein.
  3. Es sollte einfach sein, beliebig viele verschiedene Sentinel-Werte zu definieren.
  4. Die Sentinel-Objekte sollten ein klares und kurzes `repr` haben.
  5. Es sollte möglich sein, klare Typ-Signaturen für Sentinels zu verwenden.
  6. Die Sentinel-Objekte sollten sich nach dem Kopieren und/oder Entpickeln korrekt verhalten.
  7. Solche Sentinels sollten unter Verwendung von CPython 3.x und PyPy3 funktionieren, und idealerweise auch mit anderen Implementierungen von Python.
  8. So einfach und unkompliziert wie möglich, sowohl in der Implementierung als auch insbesondere in der Anwendung. Vermeiden Sie, dass dies eine weitere besondere Sache wird, die man beim Erlernen von Python lernen muss. Es sollte leicht zu finden und bei Bedarf zu verwenden sein und offensichtlich genug beim Lesen von Code, dass man normalerweise nicht das Bedürfnis hat, die Dokumentation nachzuschlagen.

Bei so vielen Verwendungen in der Python-Standardbibliothek [2] wäre eine Implementierung in der Standardbibliothek nützlich, da die Standardbibliothek keine Implementierungen von Sentinel-Objekten verwenden kann, die anderswo verfügbar sind (wie die PyPI-Pakete sentinels [5] oder sentinel [6]).

Nach der Recherche bestehender Idiome und Implementierungen und der Durchsicht vieler verschiedener möglicher Implementierungen wurde eine Implementierung geschrieben, die alle diese Kriterien erfüllt (siehe Referenzimplementierung).

Spezifikation

Eine neue Klasse Sentinel wird zu einem neuen Modul sentinellib hinzugefügt.

>>> from sentinellib import Sentinel
>>> MISSING = Sentinel('MISSING')
>>> MISSING
MISSING

Die Überprüfung, ob ein Wert ein solcher Sentinel ist, *sollte* mit dem is-Operator erfolgen, wie es für None empfohlen wird. Gleichheitsprüfungen mit == funktionieren ebenfalls wie erwartet und geben nur dann True zurück, wenn das Objekt mit sich selbst verglichen wird. Identitätsprüfungen wie if value is MISSING: sollten normalerweise anstelle von booleschen Prüfungen wie if value: oder if not value: verwendet werden.

Sentinel-Instanzen sind „wahrheitsgetreu“ (truthy), d. h. die boolesche Auswertung ergibt True. Dies entspricht dem Standard für beliebige Klassen sowie dem booleschen Wert von Ellipsis. Dies steht im Gegensatz zu None, das „falsch“ (falsy) ist.

Die Namen von Sentinels sind innerhalb jedes Moduls eindeutig. Wenn Sentinel() in einem Modul aufgerufen wird, in dem bereits ein Sentinel mit diesem Namen definiert wurde, wird der bestehende Sentinel mit diesem Namen zurückgegeben. Sentinels mit demselben Namen, die in verschiedenen Modulen definiert wurden, sind voneinander verschieden.

Das Erstellen einer Kopie eines Sentinel-Objekts, z. B. durch Verwendung von copy.copy() oder durch Pickling und Unpickling, gibt dasselbe Objekt zurück.

Sentinel() akzeptiert auch ein einzelnes optionales Argument, module_name. Dies sollte normalerweise nicht angegeben werden müssen, da Sentinel() normalerweise das Modul erkennen kann, in dem es aufgerufen wurde. module_name sollte nur in ungewöhnlichen Fällen angegeben werden, wenn diese automatische Erkennung nicht wie beabsichtigt funktioniert, z. B. vielleicht bei der Verwendung von Jython oder IronPython. Dies folgt dem Design von Enum und namedtuple. Weitere Details finden Sie in PEP 435.

Die Klasse Sentinel kann nicht als Basisklasse verwendet werden, um die größere Komplexität der Unterstützung von Vererbung zu vermeiden.

Ordnungsvergleiche sind für Sentinel-Objekte undefiniert.

Typisierung

Um die Verwendung von Sentinels in typisiertem Python-Code klar und einfach zu gestalten, schlagen wir vor, das Typsystem mit einem Sonderfall für Sentinel-Objekte zu ergänzen.

Sentinel-Objekte können in Typ-Ausdrücken verwendet werden und stellen sich selbst dar. Dies ähnelt der Handhabung von None im vorhandenen Typsystem. Zum Beispiel

from sentinels import Sentinel

MISSING = Sentinel('MISSING')

def foo(value: int | MISSING = MISSING) -> int:
    ...

Formal sollten Typ-Checker Sentinel-Kreationen der Form NAME = Sentinel('NAME') als Erstellung eines neuen Sentinel-Objekts erkennen. Wenn der an den Sentinel-Konstruktor übergebene Name nicht mit dem Namen übereinstimmt, dem das Objekt zugewiesen wird, sollten Typ-Checker einen Fehler ausgeben.

Mit dieser Syntax definierte Sentinels können in Typ-Ausdrücken verwendet werden. Sie stellen einen vollständig statischen Typ dar, der ein einzelnes Mitglied hat, nämlich das Sentinel-Objekt selbst.

Typ-Checker sollten das Verengen von Union-Typen, die Sentinels beinhalten, mit den Operatoren is und is not unterstützen.

from sentinels import Sentinel
from typing import assert_type

MISSING = Sentinel('MISSING')

def foo(value: int | MISSING) -> None:
    if value is MISSING:
        assert_type(value, MISSING)
    else:
        assert_type(value, int)

Um die Verwendung in Typ-Ausdrücken zu unterstützen, sollte die Laufzeitimplementierung der Klasse Sentinel die Methoden __or__ und __ror__ haben, die typing.Union-Objekte zurückgeben.

Abwärtskompatibilität

Dieser Vorschlag sollte keine Auswirkungen auf die Abwärtskompatibilität haben.

Wie man das lehrt

Die üblichen Dokumentationsarten für neue Standardbibliotheksmodule und -funktionen, nämlich Docstrings, Moduldokumentationen und ein Abschnitt in „What’s New“, sollten ausreichen.

Sicherheitsimplikationen

Dieser Vorschlag sollte keine Sicherheitsauswirkungen haben.

Referenzimplementierung

Die Referenzimplementierung befindet sich in einem dedizierten GitHub-Repository [7]. Eine vereinfachte Version folgt

_registry = {}

class Sentinel:
    """Unique sentinel values."""

    def __new__(cls, name, module_name=None):
        name = str(name)

        if module_name is None:
            module_name = sys._getframemodulename(1)
            if module_name is None:
                module_name = __name__

        registry_key = f'{module_name}-{name}'

        sentinel = _registry.get(registry_key, None)
        if sentinel is not None:
            return sentinel

        sentinel = super().__new__(cls)
        sentinel._name = name
        sentinel._module_name = module_name

        return _registry.setdefault(registry_key, sentinel)

    def __repr__(self):
        return self._name

    def __reduce__(self):
        return (
            self.__class__,
            (
                self._name,
                self._module_name,
            ),
        )

Abgelehnte Ideen

Verwende NotGiven = object()

Dies leidet unter allen Nachteilen, die im Abschnitt Begründung erwähnt werden.

Füge einen einzelnen neuen Sentinel-Wert hinzu, wie z. B. MISSING oder Sentinel

Da ein solcher Wert für verschiedene Dinge an verschiedenen Orten verwendet werden könnte, könnte man nie sicher sein, dass er in einigen Anwendungsfällen niemals ein gültiger Wert wäre. Andererseits kann ein dedizierter und eindeutiger Sentinel-Wert mit Zuversicht verwendet werden, ohne potenzielle Randfälle berücksichtigen zu müssen.

Darüber hinaus ist es nützlich, einem Sentinel-Wert einen aussagekräftigen Namen und `repr` geben zu können, der spezifisch für den Kontext ist, in dem er verwendet wird.

Schließlich war dies eine sehr unbeliebte Option in der Umfrage [4], bei der nur 12 % der Stimmen dafür abgegeben wurden.

Verwende den vorhandenen Ellipsis Sentinel-Wert

Dies ist nicht der ursprüngliche Verwendungszweck von Ellipsis, obwohl es zunehmend üblich geworden ist, es zur Definition von leeren Klassen- oder Funktionsblöcken anstelle von pass zu verwenden.

Ähnlich wie bei einem potenziellen neuen einzelnen Sentinel-Wert kann Ellipsis nicht in allen Fällen mit so viel Zuversicht verwendet werden wie ein dedizierter, eindeutiger Wert.

Verwende ein Enum mit einem einzelnen Wert

Das vorgeschlagene Idiom ist

class NotGivenType(Enum):
    NotGiven = 'NotGiven'
NotGiven = NotGivenType.NotGiven

Neben der übermäßigen Wiederholung ist das `repr` übermäßig lang: <NotGivenType.NotGiven: 'NotGiven'>. Ein kürzeres `repr` kann auf Kosten von etwas mehr Code und noch mehr Wiederholung definiert werden.

Schließlich war diese Option die unbeliebteste der neun Optionen in der Umfrage [4] und die einzige Option, die keine Stimmen erhielt.

Ein Klassen-Decorator für Sentinels

Das vorgeschlagene Idiom ist

@sentinel
class NotGivenType: pass
NotGiven = NotGivenType()

Während dies eine sehr einfache und klare Implementierung des Dekorators ermöglicht, ist das Idiom zu wortreich, repetitiv und schwer zu merken.

Verwendung von Klassenobjekten

Da Klassen inhärent Singletons sind, ist die Verwendung einer Klasse als Sentinel-Wert sinnvoll und ermöglicht eine einfache Implementierung.

Die einfachste Version davon ist

class NotGiven: pass

Um ein klares `repr` zu erhalten, müsste man eine Meta-Klasse verwenden

class NotGiven(metaclass=SentinelMeta): pass

... oder einen Klassen-Decorator

@Sentinel
class NotGiven: pass

Die Verwendung von Klassen auf diese Weise ist ungewöhnlich und kann verwirrend sein. Die Absicht des Codes wäre ohne Kommentare schwer zu verstehen. Außerdem würden solche Sentinels einige unerwartete und unerwünschte Verhaltensweisen aufweisen, wie z. B. aufrufbar zu sein.

Anpassung von `repr` ermöglichen

Dies war erwünscht, um die Verwendung für bestehende Sentinel-Werte zu ermöglichen, ohne deren `repr` zu ändern. Dies wurde jedoch schließlich verworfen, da es nicht als lohnenswert für die hinzugefügte Komplexität erachtet wurde.

Verwendung von typing.Literal in Typ-Annotationen

Dies wurde von mehreren Personen in Diskussionen vorgeschlagen und ist das, womit diese PEP zunächst fortfuhr. Es wurde jedoch darauf hingewiesen, dass dies zu potenzieller Verwirrung führen würde, da z. B. Literal["MISSING"] sich auf den Zeichenkettenwert "MISSING" beziehen würde und nicht auf eine Forward-Referenz zu einem Sentinel-Wert MISSING. Die Verwendung des reinen Namens wurde ebenfalls oft in Diskussionen vorgeschlagen. Dies folgt dem Präzedenzfall und dem bekannten Muster von None und hat die Vorteile, dass kein Import erforderlich ist und es viel kürzer ist.

Zusätzliche Anmerkungen

  • Diese PEP und die ursprüngliche Implementierung sind in einem dedizierten GitHub-Repository [7] entworfen.
  • Für Sentinels, die im Geltungsbereich einer Klasse definiert sind, sollte zur Vermeidung potenzieller Namenskonflikte der vollqualifizierte Name der Variablen im Modul verwendet werden. Der vollständige Name wird als `repr` verwendet. Zum Beispiel
    >>> class MyClass:
    ...    NotGiven = sentinel('MyClass.NotGiven')
    >>> MyClass.NotGiven
    MyClass.NotGiven
    
  • Man sollte vorsichtig sein, wenn man Sentinels in einer Funktion oder Methode erstellt, da Sentinels mit demselben Namen, die von Code im selben Modul erstellt wurden, identisch sind. Wenn eindeutige Sentinel-Objekte benötigt werden, sollte man sicherstellen, dass eindeutige Namen verwendet werden.
  • Es gibt keinen einzelnen wünschenswerten Wert für die „Wahrhaftigkeit“ von Sentinels, d. h. ihren booleschen Wert. Es ist manchmal nützlich, dass der boolesche Wert True ist und manchmal False. Von den integrierten Sentinels in Python wird None zu False ausgewertet, während Ellipsis (auch bekannt als ...) zu True ausgewertet wird. Der Wunsch, dass dies nach Bedarf eingestellt werden kann, kam ebenfalls in Diskussionen auf.
  • Der boolesche Wert von NotImplemented ist True, aber die Verwendung dieses ist seit Python 3.9 veraltet (dies führt zu einer Verwarnung wegen Veralterung). Diese Veralterung beruht auf spezifischen Problemen mit NotImplemented, wie in bpo-35712 [8] beschrieben.
  • Um mehrere, zusammenhängende Sentinel-Werte zu definieren, möglicherweise mit einer definierten Reihenfolge zwischen ihnen, sollte stattdessen Enum oder etwas Ähnliches verwendet werden.
  • Es gab eine Diskussion auf der typing-sig Mailingliste [9] über die Typisierung für diese Sentinels, bei der verschiedene Optionen diskutiert wurden.

Offene Fragen

  • Ist das Hinzufügen eines neuen Standardbibliotheksmoduls der richtige Weg? Ich konnte kein vorhandenes Modul finden, das wie ein logischer Ort dafür erscheint. Das Hinzufügen neuer Standardbibliotheksmodule sollte jedoch mit Bedacht geschehen, daher wäre die Wahl eines bestehenden Moduls möglicherweise vorzuziehen, auch wenn es keine perfekte Passform ist?

Fußnoten


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

Zuletzt geändert: 2025-04-10 18:12:41 GMT