1 Punkte von GN⁺ 1 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • PEP 661 schlägt das Python-Builtin-Callable sentinel() und die C-API PySentinel_New() vor, um einen separat unterscheidbaren Sentinel-Wert zu erzeugen, wenn None ein gültiger Wert ist
  • Das bisherige Idiom _sentinel = object() hat in Funktionssignaturen ein langes und unklar wirkendes repr, und es kann Probleme mit klaren Typsignaturen, Kopieren und Pickling geben
  • Der Aufruf sentinel('MISSING') erzeugt ein neues eindeutiges Objekt mit kurzem repr; wenn derselbe Sentinel gemeinsam genutzt werden soll, muss er explizit wiederverwendet werden, etwa durch Zuweisung an eine Variable wie MISSING = sentinel('MISSING')
  • Für Sentinel-Werte wird ein Vergleich per is empfohlen, sie werden als wahr ausgewertet, copy.copy() und copy.deepcopy() geben dasselbe Objekt zurück, und wenn sie aus einem Modul per Namen importierbar sind, bleibt ihre Identität auch nach dem Pickling erhalten
  • Das Typsystem erlaubt, den Sentinel selbst in Typsausdrücken wie int | MISSING zu verwenden; die aktuelle offizielle Dokumentation findet sich in der Python-3.15-Dokumentation zu [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)")

Hintergrund der Einführung

  • Ein Sentinel-Wert (sentinel value) als eindeutiger Platzhalterwert wird etwa als Standardwert verwendet, wenn ein Funktionsargument nicht übergeben wurde, als Rückgabewert zur Kennzeichnung einer erfolglosen Suche oder als Wert für fehlende Daten
  • In Python gibt es dafür meist den speziellen Wert None, aber in Kontexten, in denen None selbst ein gültiger Wert ist, wird ein separater Sentinel-Wert benötigt, der sich von None unterscheiden lässt
  • Im Mai 2021 wurde auf der Mailingliste python-dev diskutiert, wie sich der in traceback.print_exception verwendete Sentinel-Wert besser implementieren ließe
  • Die bestehende Implementierung verwendete das verbreitete Idiom _sentinel = object(), aber dessen repr war zu lang und wenig aussagekräftig, wodurch Funktionssignaturen schwer lesbar wurden
    >>> 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)  
    
  • Im Verlauf der Diskussion wurden weitere Probleme bestehender Sentinel-Implementierungen festgestellt
    • Einige Sentinel-Werte haben keinen eigenen Typ, wodurch sich für Funktionen, die einen Sentinel als Standardwert verwenden, nur schwer eine klare Typsignatur definieren lässt
    • Nach dem Kopieren entsteht teils eine separate Instanz, sodass Vergleiche mit is fehlschlagen und sich das Verhalten anders als erwartet zeigt
    • Einige verbreitete Idiome haben nach dem Pickling und anschließenden Unpickling ähnliche Probleme
  • Victor Stinner stellte eine Liste der in der Python-Standardbibliothek verwendeten Sentinel-Werte bereit; dabei zeigte sich, dass selbst innerhalb der Standardbibliothek verschiedene Implementierungsansätze verwendet werden und viele Implementierungen mindestens eines der genannten Probleme haben
  • Die Abstimmung auf discuss.python.org brachte bei 39 Stimmen kein klares Ergebnis
    • 40 % wählten „der aktuelle Zustand ist in Ordnung und Konsistenz ist nicht nötig“
    • Die Mehrheit entschied sich für eine oder mehrere standardisierte Lösungen
    • 37 % wählten die Option, „eine neue dedizierte Sentinel-Factory/Klasse/Metaklasse konsistent zu verwenden und in der Standardbibliothek öffentlich bereitzustellen“
  • Wegen dieses gemischten Ergebnisses wurde ein PEP verfasst, was zu der Schlussfolgerung führte, dass eine einfache und gute Implementierung in der Standardbibliothek sowohl innerhalb als auch außerhalb der Standardbibliothek nützlich ist
  • Es ist nicht zwingend erforderlich, alle bestehenden Sentinel-Werte der Standardbibliothek auf diese Weise umzustellen; das liegt im Ermessen der jeweiligen Maintainer
  • Das PEP-Dokument ist ein historisches Dokument; die aktuelle offizielle Dokumentation findet sich in der Python-3.15-Dokumentation zu [sentinel](<https://docs.python.org/3.15/library/functions.html#sentinel "(in Python v3.15>)")

Designkriterien

  • Ein Sentinel-Objekt muss bei einem Vergleich mit dem is-Operator immer mit sich selbst identisch sein und mit keinem anderen Objekt identisch sein
  • Die Erzeugung eines Sentinel-Objekts soll eine einfache und intuitive einzeilige Anweisung sein
  • Es soll leicht möglich sein, beliebig viele unterschiedliche Sentinel-Werte zu definieren
  • Sentinel-Objekte sollen ein kurzes und klares repr haben
  • Für Sentinel-Werte sollen klare Typsignaturen verwendbar sein
  • Sie sollen auch nach dem Kopieren korrekt funktionieren und beim Pickling und Unpickling ein vorhersehbares Verhalten zeigen
  • Sie sollen mit CPython 3.x und PyPy3 funktionieren und möglichst auch mit anderen Python-Implementierungen
  • Sowohl Implementierung als auch Nutzung sollen so einfach und intuitiv wie möglich sein und beim Erlernen von Python nicht noch ein weiteres besonderes Konzept als Belastung hinzufügen
  • Da die Standardbibliothek nicht von PyPI-Paketimplementierungen wie sentinels oder sentinel abhängen kann, wird eine Implementierung benötigt, die innerhalb der Standardbibliothek verwendet werden kann

sentinel()-Spezifikation

  • Ein neues eingebautes aufrufbares Objekt sentinel wird hinzugefügt
    >>> MISSING = sentinel('MISSING')  
    >>> MISSING  
    MISSING  
    
  • sentinel() akzeptiert genau ein positionsgebundenes Argument name, und name muss ein str sein
  • Wird ein Nicht-String-Wert übergeben, wird ein TypeError ausgelöst
  • name dient als Name und repr des Sentinel-Werts
  • Sentinel-Objekte haben zwei öffentliche Attribute
    • __name__: Name des Sentinel-Werts
    • __module__: Name des Moduls, in dem sentinel() aufgerufen wurde
  • sentinel kann nicht als Basisklasse verwendet werden
  • Jeder Aufruf von sentinel(name) gibt ein neues Sentinel-Objekt zurück
  • Wenn dasselbe Sentinel an mehreren Stellen verwendet werden soll, muss man es – wie beim bisherigen Idiom MISSING = object() – einer Variablen zuweisen und dasselbe Objekt anschließend explizit wiederverwenden
    MISSING = sentinel('MISSING')  
    
    def read_value(default=MISSING):  
        ...  
    
  • Um zu prüfen, ob ein bestimmter Wert ein Sentinel ist, wird – wie bei None – die Verwendung des is-Operators empfohlen
  • Auch ein ==-Vergleich verhält sich wie erwartet und gibt nur beim Vergleich mit sich selbst True zurück
  • Eine Identitätsprüfung wie if value is MISSING: ist in der Regel passender als ein Bool-Test wie if value: oder if not value:
  • Sentinel-Objekte sind truthy, ihre Bool-Auswertung ist also True
    • Das entspricht dem Standardverhalten beliebiger Klassen und dem Bool-Wert von Ellipsis
    • Anders als bei None, das falsy ist
  • Wenn ein Sentinel-Objekt mit copy.copy() oder copy.deepcopy() kopiert wird, wird dasselbe Objekt zurückgegeben
  • Sentinel-Werte, die aus dem definierenden Modul per Name importierbar sind, behalten gemäß dem Standard-Pickle-Mechanismus ihre Identität auch nach Pickling und Unpickling bei
    MISSING = sentinel('MISSING')  
    assert pickle.loads(pickle.dumps(MISSING)) is MISSING  
    
  • sentinel() speichert beim Erzeugen eines Sentinel-Werts das aufrufende Modul im Attribut __module__
  • Beim Pickling werden Modul und Name des Sentinel-Werts gespeichert, beim Unpickling wird das Modul importiert und der Sentinel-Wert anhand seines Namens geholt
  • Sentinel-Werte, die nicht per Modul und Name importierbar sind – etwa solche, die in einem lokalen Scope erzeugt und nicht einem passenden Namen auf Modulebene oder als Klassenattribut zugewiesen wurden – können nicht gepickelt werden
  • Das repr eines Sentinel-Objekts ist das an sentinel() übergebene name; ein impliziter Modulqualifizierer wird nicht angehängt
  • Wenn ein qualifiziertes repr benötigt wird, muss es explizit im Namen enthalten sein
    >>> MyClass_NotGiven = sentinel('MyClass.NotGiven')  
    >>> MyClass_NotGiven  
    MyClass.NotGiven  
    
  • Ordnungsvergleiche für Sentinel-Objekte sind nicht definiert
  • Sentinel-Werte unterstützen keine weakrefs

Typisierung

  • Damit die Verwendung von Sentinel-Werten in typisiertem Python-Code klar und einfach wird, wird dem Typsystem eine Sonderbehandlung für Sentinel-Objekte hinzugefügt
  • Sentinel-Objekte können in Typausdrücken") als Werte verwendet werden, die sich selbst repräsentieren
  • Das ist vergleichbar mit der bestehenden Behandlung von None im Typsystem
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING = MISSING) -> int:  
        ...  
    
  • Type-Checker sollen eine Sentinel-Erzeugung der Form NAME = sentinel('NAME') als Erzeugung eines neuen Sentinel-Objekts erkennen
  • Wenn der an sentinel() übergebene Name nicht mit dem Namen des Zuweisungsziels übereinstimmt, soll der Type-Checker einen Fehler melden
  • Mit dieser Syntax definierte Sentinel-Werte können in Typausdrücken") verwendet werden
  • Der entsprechende Sentinel-Typ stellt einen vollständig statischen Typ") dar, dessen einziges Element das Sentinel-Objekt selbst ist
  • Type-Checker sollen die Einengung von Union-Typen mit Sentinel-Werten über die Operatoren is und is not unterstützen
    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)  
    
  • Die Runtime-Implementierung muss die Methoden __or__ und __ror__ bereitstellen, um die Verwendung in Typausdrücken zu unterstützen; diese Methoden geben ein Objekt vom Typ typing.Union") zurück
  • Der Typing Council unterstützt die typbezogenen Teile dieses Vorschlags

C-API

  • Da Sentinel-Werte auch in C-Erweiterungen nützlich sein können, werden zwei neue C-API-Funktionen vorgeschlagen
  • PyObject *PySentinel_New(const char *name, const char *module_name) erzeugt ein neues Sentinel-Objekt
  • bool PySentinel_Check(PyObject *obj) prüft, ob ein Objekt ein Sentinel ist
  • C-Code kann zur Prüfung auf ein bestimmtes Sentinel den Operator == verwenden

Kompatibilität und Sicherheit

  • Das Hinzufügen eines neuen eingebauten Namens bedeutet, dass Code, der derzeit annimmt, dass der Bare Name sentinel einen NameError auslöst, nicht mehr dasselbe Ergebnis sieht
  • Das ist ein üblicher Kompatibilitätsaspekt beim Hinzufügen neuer eingebauter Namen
  • Bereits vorhandene lokale, globale oder importierte Namen sentinel sind davon nicht betroffen
  • Bereits existierender Code, der den Namen sentinel verwendet, muss möglicherweise angepasst werden, um das neue eingebaute Objekt zu verwenden, und könnte neue Warnungen von Lintern erhalten, die vor Kollisionen mit eingebauten Namen warnen
  • Die übliche Dokumentation neuer Built-ins über Docstrings, Bibliotheksdokumentation und den Abschnitt „What’s New“ wird als ausreichend angesehen
  • Es wird davon ausgegangen, dass dieser Vorschlag keine Sicherheitsauswirkungen hat

Referenzimplementierung und Backport

  • Die Referenzimplementierung wird als CPython-Pull-Request [10] bereitgestellt
  • Eine frühere Referenzimplementierung befindet sich in einem separaten GitHub-Repository [7]
  • Eine Skizze des beabsichtigten Verhaltens sieht wie folgt aus
    class sentinel:  
        """Unique sentinel values."""  
    
        __slots__ = ("__name__", "_module_name")  
    
        def __init_subclass__(cls):  
            raise TypeError("type 'sentinel' is not an acceptable base type")  
    
        def __init__(self, name, /):  
            if not isinstance(name, str):  
                raise TypeError("sentinel name must be a string")  
            self.__name__ = name  
            self._module_name = sys._getframemodulename(1)  
    
        @property  
        def __module__(self):  
            return self._module_name  
    
        def __repr__(self):  
            return self.__name__  
    
        def __reduce__(self):  
            return self.__name__  
    
        def __copy__(self):  
            return self  
    
        def __deepcopy__(self, memo):  
            return self  
    
        def __or__(self, other):  
            return typing.Union[self, other]  
    
        def __ror__(self, other):  
            return typing.Union[other, self]  
    
    • Das Modul typing-extensions enthält zwar einen Backport, dieser entspricht derzeit jedoch nicht exakt dem Verhalten der aktuellen PEP-Iteration.

Abgelehnte Alternativen

  • Verwendung von NotGiven = object()

    • Dieser Ansatz hat alle Nachteile, die in den Entwurfskriterien des PEP behandelt werden
    • Das repr ist lang und nicht eindeutig, Typsignaturen lassen sich nur schwer klar ausdrücken, und es kann Probleme beim Kopieren oder Pickling geben
  • Hinzufügen eines einzelnen neuen Sentinel-Werts wie MISSING oder Sentinel

    • Wenn ein einzelner Wert an mehreren Stellen für verschiedene Zwecke verwendet wird, ist es in manchen Anwendungsfällen schwer, immer sicher zu sein, dass dieser Wert selbst kein gültiger Wert ist
    • Dedizierte, unterschiedliche Sentinel-Werte lassen sich selbstbewusster verwenden, ohne potenzielle Edge Cases berücksichtigen zu müssen
    • Sentinel-Werte sollten aussagekräftige Namen und ein passendes repr für ihren Nutzungskontext bereitstellen können
    • Diese Option war sehr unbeliebt und wurde in der Abstimmung nur von 12 % gewählt
  • Verwendung des bestehenden Sentinel-Werts Ellipsis

    • Ellipsis ist ursprünglich nicht für diesen Zweck gedacht
    • Es wird zwar zunehmend verwendet, um leere Klassen- oder Funktionsblöcke anstelle von pass zu definieren, lässt sich aber nicht in allen Fällen so zuverlässig einsetzen wie dedizierte, unterschiedliche Sentinel-Werte
  • Verwendung eines Ein-Wert-Enum

    • Das vorgeschlagene Idiom lautet wie folgt
    class NotGivenType(Enum):  
      NotGiven = &#039;NotGiven&#039;  
      NotGiven = NotGivenType.NotGiven  
    
  • Es enthält zu viele Wiederholungen, und das repr ist mit etwas wie &lt;NotGivenType.NotGiven: &#039;NotGiven&#039;&gt; zu lang
  • Man könnte ein kürzeres repr definieren, aber dadurch würden Code und Wiederholungen noch zunehmen
  • Unter den 9 Optionen der Abstimmung war dies die einzige, die keine Stimme erhielt, und damit die unpopulärste
  • Sentinel-Klassendekorator

    • Das vorgeschlagene Idiom lautet wie folgt
      @sentinel  
      class NotGivenType: pass  
      NotGiven = NotGivenType()  
      
    • Die Implementierung des Dekorators selbst könnte einfach und klar sein, aber das Idiom ist zu wortreich, repetitiv und schwer zu merken
  • Verwendung von Klassenobjekten

    • Klassen sind ihrem Wesen nach Singletons, daher ist die Idee denkbar, sie als Sentinel-Werte zu verwenden
    • Die einfachste Form sieht so aus
      class NotGiven: pass  
      
      • Um ein eindeutiges repr zu erhalten, ist eine Metaklasse oder ein Klassendekorator erforderlich
      class NotGiven(metaclass=SentinelMeta): pass  
      
      @Sentinel  
      class NotGiven: pass  
      
    • Klassen auf diese Weise zu verwenden ist ungewöhnlich und kann verwirrend sein
    • Ohne Kommentare ist die Absicht des Codes schwer zu verstehen, und es entstehen unerwartete, unerwünschte Verhaltensweisen, etwa dass das Sentinel aufrufbar wird
  • Nur ein empfohlenes Standard-Idiom definieren, ohne Implementierung

    • Die meisten gängigen bestehenden Idiome haben erhebliche Nachteile
    • Bisher wurde kein klares und knappes Idiom gefunden, das diese Nachteile vermeidet
    • In der zugehörigen Abstimmung waren Optionen, die nur ein Idiom empfahlen, unbeliebt, und selbst die Option mit den meisten Stimmen kam nur auf 25 %
  • Verwendung eines neuen Standardbibliotheksmoduls

    • Der erste Entwurf schlug vor, die Klasse Sentinel in ein neues Modul sentinels oder sentinellib aufzunehmen
    • Für ein einzelnes öffentliches aufrufbares Objekt ein neues Modul hinzuzufügen ist unnötig
    • Mit einem Modul wäre die Nutzung weniger bequem als beim bestehenden object()-Idiom
    • Auch der Steering Council empfahl ausdrücklich, daraus eine eingebaute Funktionalität zu machen, damit sie sich mindestens so einfach wie object() verwenden lässt
    • Der Name sentinels kollidiert zudem mit einem bereits aktiv genutzten PyPI-Paket; als Built-in lässt sich dieses Namensproblem vermeiden
  • Verwendung eines modulweiten Namensregisters für Sentinel-Namen

    • Der erste Entwurf schlug vor, Sentinel-Namen innerhalb eines Moduls eindeutig zu machen
    • In diesem Entwurf würde ein wiederholter Aufruf von sentinel("MISSING") im selben Modul dasselbe Objekt zurückgeben, über ein prozessweites globales Register mit Modulname und Sentinel-Name als Schlüssel
    • Dieses Verhalten wurde als zu implizit angesehen und abgelehnt
    • Wenn ein gemeinsam genutztes Sentinel erforderlich ist, kann man wie beim bestehenden MISSING = object() explizit eines definieren und es per Namen wiederverwenden
    • In lokalem Scope möchte man bei jedem Aufruf oder jeder Wiederholung möglicherweise ein neues Sentinel, daher sollte ein wiederholter Aufruf von sentinel(name) wie ein wiederholter Aufruf von object() unterschiedliche Objekte erzeugen
    • Durch das Entfernen des Registers werden Implementierung und Denkmodell einfacher, und es bleibt nur noch die Regel: sentinel(name) erzeugt ein neues eindeutiges Objekt mit repr gleich name
  • Automatische Erkennung oder Übergabe des Modulnamens

    • Der erste Entwurf schlug ein optionales Argument module_name vor, um den registerbasierten Entwurf zu unterstützen
    • Mit dem Wegfall des Registers wird das öffentliche Argument module_name für den Kernvorschlag nicht mehr benötigt
    • Die Implementierung zeichnet das aufrufende Modul intern auf, damit Pickle importierbare Sentinel-Werte ähnlich wie TypeVar über Modul und Name serialisieren kann
    • Der interne Modulname hat keinen Einfluss auf das repr des Sentinels
    • Wenn ein repr mit Modul- oder Klassenname gewünscht ist, kann dies explizit in das einzelne Argument name aufgenommen werden, etwa mit sentinel("mymodule.MISSING")
  • Benutzerdefiniertes repr erlauben

    • Ein Vorteil wäre gewesen, dass sich bestehende Sentinel-Werte ohne Änderung ihres repr auf diese Weise migrieren ließen
    • Dies wurde jedoch ausgeschlossen, weil der zusätzliche Aufwand die Komplexität nicht rechtfertigt
  • Benutzerdefinierte Bool-Auswertung erlauben

    • In der Diskussion wurde geprüft, Sentinel-Werte explizit als wahr, falsch oder nicht in bool konvertierbar gestalten zu können
    • Einige Drittanbieter-Sentinels bieten falsches Verhalten als Teil ihrer öffentlichen API an
    • Mehrere Beteiligte hielten es für besser, in booleschen Kontexten eine Ausnahme auszulösen, um Identitätsprüfungen stärker zu erzwingen
    • Das PEP hält den ursprünglichen Vorschlag bewusst einfach, indem es das Standardverhalten normaler Objekte als wahr beibehält und Identitätsprüfungen empfiehlt
    • Benutzerdefiniertes boolesches Verhalten könnte später geprüft werden, wenn der zusätzliche API- und Typisierungsaufwand als lohnend angesehen wird
  • Verwendung von typing.Literal in Typannotationen

    • Dies wurde in der Diskussion von mehreren Personen vorgeschlagen, und das PEP übernahm diesen Ansatz anfangs auch
    • Es kann jedoch verwirrend sein, weil Literal["MISSING"] nicht eine Vorwärtsreferenz auf den Sentinel-Wert MISSING, sondern auf den String-Wert "MISSING" verweist
    • Auch die Verwendung eines bare name wurde in der Diskussion häufig vorgeschlagen
    • Der Ansatz mit bare name folgt dem von None geschaffenen Präzedenzfall und einem bekannten Muster, benötigt keinen Import und ist deutlich kürzer

Zusätzliche Nutzungshinweise

  • Wenn ein Sentinel im Klassen-Scope definiert wird, Namenskonflikte vermieden werden sollen oder ein qualifiziertes repr klarer ist, sollte der gewünschte qualifizierte Name explizit übergeben werden
    &gt;&gt;&gt; class MyClass:  
    ...    NotGiven = sentinel(&#039;MyClass.NotGiven&#039;)  
    &gt;&gt;&gt; MyClass.NotGiven  
    MyClass.NotGiven  
    
  • Es ist erlaubt, Sentinel-Werte innerhalb von Funktionen oder Methoden zu erzeugen
  • Da jeder Aufruf von sentinel() ein anderes Objekt erzeugt, verhalten sich Sentinel-Werte aus lokalem Scope wie Werte, die dort durch Aufrufe von object() erzeugt wurden
  • Der boolesche Wert von NotImplemented ist True, aber seine Verwendung ist seit Python 3.9 veraltet und löst eine Deprecation Warning aus
  • Diese Veraltung liegt an Problemen, die speziell NotImplemented betreffen und in bpo-35712 [8] beschrieben sind
  • Wenn mehrere zusammengehörige Sentinel-Werte definiert werden müssen oder eine Reihenfolge zwischen ihnen festgelegt werden soll, sollte Enum oder ein ähnlicher Ansatz verwendet werden
  • Zur Typisierung solcher Sentinel-Werte wurden in der Mailingliste typing-sig [9] mehrere Optionen diskutiert

1 Kommentare

 
GN⁺ 1 시간 전
Lobste.rs-Meinungen
  • Der gewählte Name wirkt seltsam, weil seine Bedeutung zu eng erscheint
    Dem Namen nach hätte etwas in Richtung eindeutiges Symbol wie ein flexibleres Grundelement gewirkt. In der Praxis würde es sich ohnehin fast wie ein Symbol verhalten, also könnte man es so verwenden, aber es „Sentinels“ zu nennen, fühlt sich merkwürdig an. Vielleicht liegt das auch daran, dass ich an Lisp gewöhnt bin

    • Das Ziel scheint zu sein, dass SENTINEL_A ein anderer Typ als SENTINEL_B ist, damit man fragen kann, ob ein Wert is_a SENTINEL_A ist
      Rubys Symbole verhalten sich nicht so: :beef.is_a? :droog.class #=> true
    • Die Lisp-artige Denkweise passt schon. Zwar wird vorausgesetzt, dass eine breite Verwendung wünschenswert und ein zu lösendes Problem ist, aber Python hat für die meisten Anwendungsfälle von Lisp-Symbolen bereits Literal und Literal-Strings
      Dass diese benannte Sentinels sind, liegt daran, dass sentinel values in Python ein verbreitetes Konzept und Muster sind und Sentinels einige Probleme gezielt lösen sollen, die bei der Nutzung dieses Musters entstehen. Genau so wird es in den Abschnitten „Motivation“ und „Rationale“ erklärt
      Außerdem haben Sentinels keine Wertsemantik, daher sind auch zwei Sentinels mit demselben Namen unterschiedliche Werte und einander nicht gleich. Sie verhalten sich also nicht wie Symbole und sollten auch nicht so verwendet werden
  • Beim Problem mit Standardwerten für benannte Argumente könnte Typst mit none plus einem auto-Wert fast alle gewünschten benannten Argument-Interfaces ausdrücken
    Mit none allein lässt sich die Bedeutung der meisten Standardwerte benannter Argumente nicht gut ausdrücken. none eignet sich als Standardrückgabewert, aber als Funktionsargument transportiert es oft nicht die richtige Bedeutung als Substantiv. Bei matrix(axes=None) ist unklar, ob das bedeutet, die Achsen zu entfernen, oder sie wie üblich beizubehalten. Auch ist nicht klar, ob es einen Unterschied macht, none zu übergeben oder gar nichts zu übergeben. Wenn man zur Unterscheidung der Präsenz eines Parameters zu Multiple Dispatch greift, verliert man die zentrale Stelle, an der das Verhalten dieses Parameters dokumentiert werden sollte
    auto ist ein hervorragender Standardwert, weil es direkt bedeutet: „Verarbeite es passend anhand der vorhandenen Informationen.“ Eine Signatur auto | none kann wie ein expliziteres Boolean verwendet werden, und T | auto | none verrät schon ziemlich viel darüber, wie eine Funktion den Wert nutzen wird. Wenn T zum Beispiel color ist, wird auto wahrscheinlich einen Standardwert wie Weiß/Schwarz wählen oder vom Elternkontext erben, T setzt die Farbe explizit, und none kann je nach Kontext bedeuten, die Farbe gar nicht zu setzen oder sie als transparent zu behandeln

  • Interessant, und ich frage mich, wie sich dadurch die Semantik einiger Pakete ändern wird. Statt zum Beispiel Item | None zurückzugeben, könnte man Folgendes schreiben

    NOT_FOUND = sentinel("NOT_FOUND")  
    def get_item(iid: str) -> Item | NOT_FOUND: ...  
    

    Natürlich könnte man auch mit mehreren Sentinels zusätzliche Bedeutung transportieren. Das war vorher schon möglich, aber es gab in der Dokumentation keinen „offiziell empfohlenen“ Weg dafür. Das könnte Paketautoren in eine andere Richtung lenken

    MISSING_ID = sentinel("MISSING_ID")  
    MISSING_VALUE = sentinel("MISSING_VALUE")
    
    def get_item(iid: str) -> Item | MISSING_ID | MISSING_VALUE: ...  
    

    Etwas konstruiertes Beispiel, aber hier könnte man unterscheiden zwischen dem Fall, dass eine bestehende ID keinen verknüpften Wert hat, und dem Fall, dass es diese ID gar nicht gibt und deshalb ein Fehler auftritt. Die „Pythonische“ Variante wäre vermutlich eher, eine Exception zu verwenden, aber das wirkt mehr nach einem funktionalen Ansatz, als man Python sonst typischerweise schreibt

    • Es wirkt wie eine sauberere Art, den Singleton zu verwenden, für den man früher eine Dummy-Klasse erstellt und pro Modul instanziiert hat
      class _MissingId: ...
      
      MISSING_ID = _MissingId()
      
      # elsewhere  
      from ... import MISSING_ID  
      
      Das erinnert an Symbols
    • Im PEP steht, dass man stattdessen Enum oder etwas Ähnliches verwenden sollte, wenn man mehrere zusammengehörige Sentinel-Werte definieren oder sogar eine Sortierreihenfolge zwischen ihnen festlegen möchte
  • Wahrscheinlich wäre es besser gewesen, einfach die Symbol-API aus JavaScript zu übernehmen. Sie ist allgemein nützlich und würde auch das hier zu lösende Problem mit abdecken