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

Python Enhancement Proposals

PEP 505 – None-aware Operatoren

Autor:
Mark E. Haase <mehaase at gmail.com>, Steve Dower <steve.dower at python.org>
Status:
Verschoben
Typ:
Standards Track
Erstellt:
18-Sep-2015
Python-Version:
3.8

Inhaltsverzeichnis

Zusammenfassung

Viele moderne Programmiersprachen verfügen über sogenannte „null-Coalescing“- oder „null-aware“-Operatoren, darunter C# [1], Dart [2], Perl, Swift und PHP (ab Version 7). Es gibt auch Entwurfsanträge der Stufe 3 für deren Aufnahme in ECMAScript (auch bekannt als JavaScript) [3] [4]. Diese Operatoren bieten syntaktischen Zucker für gängige Muster mit Nullreferenzen.

  • Der „null-Coalescing“-Operator ist ein binärer Operator, der seinen linken Operanden zurückgibt, wenn er nicht null ist. Andernfalls gibt er seinen rechten Operanden zurück.
  • Der „null-aware Member-Zugriffs“-Operator greift nur auf ein Instanzmitglied zu, wenn diese Instanz nicht null ist. Andernfalls gibt er null zurück. (Dies wird auch als „Safe Navigation“-Operator bezeichnet.)
  • Der „null-aware Index-Zugriffs“-Operator greift nur auf ein Element einer Sammlung zu, wenn diese Sammlung nicht null ist. Andernfalls gibt er null zurück. (Dies ist eine weitere Art von „Safe Navigation“-Operator.)

Dieses PEP schlägt drei None-aware Operatoren für Python vor, basierend auf den Definitionen und Implementierungen dieser in anderen Sprachen.

  • Der binäre „None-Coalescing“-Operator ?? gibt die linke Seite zurück, wenn sie zu einem Wert ausgewertet wird, der nicht None ist, oder er wertet die rechte Seite aus und gibt sie zurück. Ein.??= Coalescing augmentierter Zuweisungsoperator ist enthalten.
  • Der „None-aware Attributzugriffs“-Operator ?. („maybe dot“) wertet den vollständigen Ausdruck aus, wenn die linke Seite zu einem Wert ausgewertet wird, der nicht None ist.
  • Der „None-aware Indexierungs“-Operator ?[] („maybe subscript“) wertet den vollständigen Ausdruck aus, wenn die linke Seite zu einem Wert ausgewertet wird, der nicht None ist.

Siehe den Abschnitt Grammatikänderungen für Details und Beispiele der erforderlichen Grammatikänderungen.

Siehe den Abschnitt Beispiele für realistischere Beispiele von Code, der mit den neuen Operatoren aktualisiert werden könnte.

Syntax und Semantik

Besonderheit von None

Das Objekt None bezeichnet das Fehlen eines Wertes. Für die Zwecke dieser Operatoren bedeutet das Fehlen eines Wertes, dass auch der Rest des Ausdrucks keinen Wert hat und nicht ausgewertet werden sollte.

Ein abgelehnter Vorschlag war, jeden Wert, der in einem booleschen Kontext als „falsch“ ausgewertet wird, als fehlend zu behandeln. Der Zweck dieser Operatoren ist es jedoch, den Zustand des „fehlenden Wertes“ und nicht den Zustand des „falschen Wertes“ weiterzugeben.

Einige argumentieren, dass dies None besonders macht. Wir argumentieren, dass None bereits besonders ist und dass die Verwendung als Test und Ergebnis dieser Operatoren die bestehenden Semantik in keiner Weise verändert.

Siehe den Abschnitt Abgelehnte Ideen für Diskussionen über alternative Ansätze.

Grammatikänderungen

Die folgenden Regeln der Python-Grammatik werden aktualisiert und lauten wie folgt:

augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
            '<<=' | '>>=' | '**=' | '//=' | '??=')

power: coalesce ['**' factor]
coalesce: atom_expr ['??' factor]
atom_expr: ['await'] atom trailer*
trailer: ('(' [arglist] ')' |
          '[' subscriptlist ']' |
          '?[' subscriptlist ']' |
          '.' NAME |
          '?.' NAME)

Die Coalesce-Regel

Die Regel coalesce bietet den binären Operator ??. Im Gegensatz zu den meisten binären Operatoren wird die rechte Seite erst ausgewertet, wenn die linke Seite als None bestimmt wurde.

Der Operator ?? hat eine engere Bindung als andere binäre Operatoren, da die meisten bestehenden Implementierungen dieser Operatoren keine None-Werte weitergeben (sie würden typischerweise TypeError auslösen). Ausdrücke, von denen bekannt ist, dass sie potenziell zu None führen können, können ohne zusätzliche Klammern durch einen Standardwert ersetzt werden.

Einige Beispiele dafür, wie implizite Klammern bei der Auswertung der Operatorrangfolge im Beisein des ??-Operators gesetzt werden

a, b = None, None
def c(): return None
def ex(): raise Exception()

(a ?? 2 ** b ?? 3) == a ?? (2 ** (b ?? 3))
(a * b ?? c // d) == a * (b ?? c) // d
(a ?? True and b ?? False) == (a ?? True) and (b ?? False)
(c() ?? c() ?? True) == True
(True ?? ex()) == True
(c ?? ex)() == c()

Insbesondere bei Fällen wie a ?? 2 ** b ?? 3 würde eine andere Klammerung der Teilausdrücke zu TypeError führen, da int.__pow__ nicht mit None aufgerufen werden kann (und die Tatsache, dass der Operator ?? überhaupt verwendet wird, impliziert, dass a oder b None sein können). Wie üblich sollten jedoch, auch wenn Klammern nicht erforderlich sind, diese hinzugefügt werden, wenn sie zur Lesbarkeit beitragen.

Eine augmentierte Zuweisung für den Operator ?? wird ebenfalls hinzugefügt. Die augmentierte Coalescing-Zuweisung bindet den Namen nur neu, wenn sein aktueller Wert None ist. Wenn der Zielname bereits einen Wert hat, wird die rechte Seite nicht ausgewertet. Zum Beispiel:

a = None
b = ''
c = 0

a ??= 'value'
b ??= undefined_name
c ??= shutil.rmtree('/')    # don't try this at home, kids

assert a == 'value'
assert b == ''
assert c == 0 and any(os.scandir('/'))

Die Maybe-Dot- und Maybe-Subscript-Operatoren

Die Maybe-Dot- und Maybe-Subscript-Operatoren werden als Trailer für Atome hinzugefügt, so dass sie an allen Stellen verwendet werden können wie die regulären Operatoren, einschließlich als Teil eines Zuweisungsziels (mehr Details unten). Da die bestehenden Auswertungsregeln nicht direkt in die Grammatik eingebettet sind, spezifizieren wir die erforderlichen Änderungen unten.

Angenommen, das atom wird immer erfolgreich ausgewertet. Jeder trailer wird dann von links nach rechts ausgewertet, wobei sein eigener Parameter (entweder seine Argumente, Indizes oder der Attributname) verwendet wird, um den Wert für den nächsten trailer zu erzeugen. Schließlich wird, falls vorhanden, await angewendet.

Zum Beispiel wird await a.b(c).d[e] derzeit geparst als ['await', 'a', '.b', '(c)', '.d', '[e]'] und ausgewertet

_v = a
_v = _v.b
_v = _v(c)
_v = _v.d
_v = _v[e]
await _v

Wenn ein None-aware Operator vorhanden ist, kann die links-nach-rechts-Auswertung unterbrochen werden. Zum Beispiel wird await a?.b(c).d?[e] ausgewertet

_v = a
if _v is not None:
    _v = _v.b
    _v = _v(c)
    _v = _v.d
    if _v is not None:
        _v = _v[e]
await _v

Hinweis

await wird in diesem Kontext fast sicher fehlschlagen, da dies auch der Fall wäre, wenn der Code await None versucht. Wir schlagen nicht vor, hier ein None-aware await Schlüsselwort hinzuzufügen, und erwähnen es nur in diesem Beispiel zur Vollständigkeit der Spezifikation, da die Grammatikregel atom_expr das Schlüsselwort enthält. Wenn es in einer eigenen Regel stünde, hätten wir es nie erwähnt.

Klammerausdrücke werden durch die Regel atom (nicht oben gezeigt) behandelt, die das Short-Circuiting-Verhalten der obigen Transformation implizit beendet. Zum Beispiel wird (a?.b ?? c).d?.e ausgewertet als

# a?.b
_v = a
if _v is not None:
    _v = _v.b

# ... ?? c
if _v is None:
    _v = c

# (...).d?.e
_v = _v.d
if _v is not None:
    _v = _v.e

Bei Verwendung als Zuweisungsziel dürfen die None-aware Operationen nur in einem „Load“-Kontext verwendet werden. Das heißt, a?.b = 1 und a?[b] = 1 lösen SyntaxError aus. Frühere Verwendung im Ausdruck (a?.b.c = 1) ist erlaubt, aber wahrscheinlich nicht nützlich, es sei denn, sie wird mit einer Coalescing-Operation kombiniert.

(a?.b ?? d).c = 1

Ausdrücke lesen

Bei den Maybe-Dot- und Maybe-Subscript-Operatoren ist beabsichtigt, dass Ausdrücke, die diese Operatoren enthalten, wie für die regulären Versionen dieser Operatoren gelesen und interpretiert werden. In „normalen“ Fällen sind die Endergebnisse eines Ausdrucks wie a?.b?[c] und a.b[c] identisch, und genauso wie wir „a.b“ derzeit nicht als „lese Attribut b von a *wenn es ein Attribut a hat oder sonst wirft AttributeError*“ lesen, gibt es keinen Grund, „a?.b“ als „lese Attribut b von a *wenn a nicht None ist*“ zu lesen (es sei denn in einem Kontext, in dem der Zuhörer über das spezifische Verhalten informiert werden muss).

Bei Coalescing-Ausdrücken, die den Operator ?? verwenden, sollten die Ausdrücke entweder als „oder… wenn None“ oder „coalesced with“ gelesen werden. Zum Beispiel würde der Ausdruck a.get_value() ?? 100 als „rufe a dot get_value oder 100 wenn None auf“, oder „rufe a dot get_value coalesced with 100 auf“ gelesen werden.

Hinweis

Das Lesen von Code in gesprochenem Text ist immer verlustbehaftet, daher unternehmen wir keinen Versuch, eine eindeutige Art der Aussprache dieser Operatoren zu definieren. Diese Vorschläge sollen Kontext zu den Auswirkungen der Einführung der neuen Syntax hinzufügen.

Beispiele

Dieser Abschnitt präsentiert einige Beispiele für gängige None-Muster und zeigt, wie die Verwendung von None-aware Operatoren aussehen könnte.

Standardbibliothek

Mit dem Skript find-pep505.py [5] fand eine Analyse der Python 3.7 Standardbibliothek bis zu 678 Code-Snippets, die durch die Verwendung eines der None-aware Operatoren ersetzt werden könnten.

$ find /usr/lib/python3.7 -name '*.py' | xargs python3.7 find-pep505.py
<snip>
Total None-coalescing `if` blocks: 449
Total [possible] None-coalescing `or`: 120
Total None-coalescing ternaries: 27
Total Safe navigation `and`: 13
Total Safe navigation `if` blocks: 61
Total Safe navigation ternaries: 8

Einige davon werden unten als Beispiele vor und nach der Konvertierung unter Verwendung der neuen Operatoren gezeigt.

Aus bisect.py

def insort_right(a, x, lo=0, hi=None):
    # ...
    if hi is None:
        hi = len(a)
    # ...

Nach der Aktualisierung auf die Verwendung der augmentierten Zuweisung ??=

def insort_right(a, x, lo=0, hi=None):
    # ...
    hi ??= len(a)
    # ...

Aus calendar.py

encoding = options.encoding
if encoding is None:
    encoding = sys.getdefaultencoding()
optdict = dict(encoding=encoding, css=options.css)

Nach der Aktualisierung auf die Verwendung des ??-Operators

optdict = dict(encoding=options.encoding ?? sys.getdefaultencoding(),
               css=options.css)

Aus email/generator.py (und beachten Sie wichtig, dass es in dieser Situation keine Möglichkeit gibt, or durch ?? zu ersetzen)

mangle_from_ = True if policy is None else policy.mangle_from_

Nach der Aktualisierung

mangle_from_ = policy?.mangle_from_ ?? True

Aus asyncio/subprocess.py

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    if reader is not None:
        reader.feed_data(data)

Nach der Aktualisierung auf die Verwendung des ?.-Operators

def pipe_data_received(self, fd, data):
    if fd == 1:
        reader = self.stdout
    elif fd == 2:
        reader = self.stderr
    else:
        reader = None
    reader?.feed_data(data)

Aus asyncio/tasks.py

try:
    await waiter
finally:
    if timeout_handle is not None:
        timeout_handle.cancel()

Nach der Aktualisierung auf die Verwendung des ?.-Operators

try:
    await waiter
finally:
    timeout_handle?.cancel()

Aus ctypes/_aix.py

if libpaths is None:
    libpaths = []
else:
    libpaths = libpaths.split(":")

Nach der Aktualisierung

libpaths = libpaths?.split(":") ?? []

Aus os.py

if entry.is_dir():
    dirs.append(name)
    if entries is not None:
        entries.append(entry)
else:
    nondirs.append(name)

Nach der Aktualisierung auf die Verwendung des ?.-Operators

if entry.is_dir():
    dirs.append(name)
    entries?.append(entry)
else:
    nondirs.append(name)

Aus importlib/abc.py

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    found = self.find_spec(fullname, path)
    return found.loader if found is not None else None

Nach teilweiser Aktualisierung

def find_module(self, fullname, path):
    if not hasattr(self, 'find_spec'):
        return None
    return self.find_spec(fullname, path)?.loader

Nach umfangreicher Aktualisierung (wohl übertrieben, das ist aber Sache der Styleguides)

def find_module(self, fullname, path):
    return getattr(self, 'find_spec', None)?.__call__(fullname, path)?.loader

Aus dis.py

def _get_const_info(const_index, const_list):
    argval = const_index
    if const_list is not None:
        argval = const_list[const_index]
    return argval, repr(argval)

Nach der Aktualisierung zur Verwendung der Operatoren ?[] und ??

def _get_const_info(const_index, const_list):
    argval = const_list?[const_index] ?? const_index
    return argval, repr(argval)

jsonify

Dieses Beispiel stammt aus einem Python-Webcrawler, der das Flask-Framework als Frontend verwendet. Diese Funktion ruft Informationen über eine Website aus einer SQL-Datenbank ab und formatiert sie als JSON, um sie an einen HTTP-Client zu senden.

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen.isoformat() if site.first_seen is not None else None,
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen.isoformat() if site.last_seen is not None else None,
            url=site.url.rstrip('/')
        )

Sowohl first_seen als auch last_seen dürfen in der Datenbank null sein, und sie dürfen auch in der JSON-Antwort null sein. JSON hat keine native Möglichkeit, ein datetime darzustellen, daher besagt der Vertrag des Servers, dass jedes nicht null Datum als ISO-8601-String dargestellt wird.

Ohne die genauen Semantik der Attribute first_seen und last_seen zu kennen, ist es unmöglich zu wissen, ob das Attribut sicher oder performant mehrmals abgerufen werden kann.

Eine Möglichkeit, diesen Code zu korrigieren, besteht darin, jeden bedingten Ausdruck durch eine explizite Wertzuweisung und einen vollständigen if/else Block zu ersetzen.

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        first_seen_dt = site.first_seen
        if first_seen_dt is None:
            first_seen = None
        else:
            first_seen = first_seen_dt.isoformat()

        last_seen_dt = site.last_seen
        if last_seen_dt is None:
            last_seen = None
        else:
            last_seen = last_seen_dt.isoformat()

        return jsonify(
            first_seen=first_seen,
            id=site.id,
            is_active=site.is_active,
            last_seen=last_seen,
            url=site.url.rstrip('/')
        )

Dies fügt zehn Zeilen Code und vier neue Code-Pfade zur Funktion hinzu und erhöht die scheinbare Komplexität drastisch. Die Neufassung mit dem None-aware Attributoperator führt zu kürzerem Code mit klarerer Absicht.

class SiteView(FlaskView):
    @route('/site/<id_>', methods=['GET'])
    def get_site(self, id_):
        site = db.query('site_table').find(id_)

        return jsonify(
            first_seen=site.first_seen?.isoformat(),
            id=site.id,
            is_active=site.is_active,
            last_seen=site.last_seen?.isoformat(),
            url=site.url.rstrip('/')
        )

Grab

Das nächste Beispiel stammt aus einer Python-Scraping-Bibliothek namens Grab

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        if ctype is None:
            return 'application/octet-stream'
        else:
            return ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        if filename is None:
            self.filename = self.get_random_filename()
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        if filename is None:
            self.filename = os.path.split(path)[1]
        else:
            self.filename = filename
        if content_type is None:
            self.content_type = self.find_content_type(self.filename)
        else:
            self.content_type = content_type

Dieses Beispiel enthält mehrere gute Beispiele für die Notwendigkeit, Standardwerte bereitzustellen. Die Neufassung zur Verwendung bedingter Ausdrücke reduziert die Gesamtzahl der Codezeilen, verbessert aber nicht unbedingt die Lesbarkeit.

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return 'application/octet-stream' if ctype is None else ctype

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = (self.get_random_filename() if filename
            is None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = (os.path.split(path)[1] if filename is
            None else filename)
        self.content_type = (self.find_content_type(self.filename)
            if content_type is None else content_type)

Der erste ternäre Ausdruck ist ordentlich, kehrt aber die intuitive Reihenfolge der Operanden um: Er sollte ctype zurückgeben, wenn es einen Wert hat, und den String-Literal als Fallback verwenden. Die anderen ternären Ausdrücke sind unintuitiv und so lang, dass sie umgebrochen werden müssen. Die allgemeine Lesbarkeit verschlechtert sich, statt sich zu verbessern.

Neufassung mit dem None-Coalescing-Operator

class BaseUploadObject(object):
    def find_content_type(self, filename):
        ctype, encoding = mimetypes.guess_type(filename)
        return ctype ?? 'application/octet-stream'

class UploadContent(BaseUploadObject):
    def __init__(self, content, filename=None, content_type=None):
        self.content = content
        self.filename = filename ?? self.get_random_filename()
        self.content_type = content_type ?? self.find_content_type(self.filename)

class UploadFile(BaseUploadObject):
    def __init__(self, path, filename=None, content_type=None):
        self.path = path
        self.filename = filename ?? os.path.split(path)[1]
        self.content_type = content_type ?? self.find_content_type(self.filename)

Diese Syntax hat eine intuitive Reihenfolge der Operanden. In find_content_type beispielsweise erscheint der bevorzugte Wert ctype vor dem Fallback-Wert. Die Kürze der Syntax bedeutet auch weniger Codezeilen und weniger Code zum visuellen Parsen, und das Lesen von links nach rechts und von oben nach unten folgt dem Ausführungsfluss genauer.

Abgelehnte Ideen

Die ersten drei Ideen in diesem Abschnitt sind oft vorgeschlagene Alternativen zur Behandlung von None als besonders. Weitere Hintergründe, warum diese abgelehnt werden, finden Sie in PEP 531 und PEP 532 und den zugehörigen Diskussionen.

No-Value-Protokoll

Die Operatoren könnten für benutzerdefinierte Typen verallgemeinert werden, indem ein Protokoll definiert wird, um anzuzeigen, wann ein Wert „keinen Wert“ repräsentiert. Ein solches Protokoll könnte eine Dunder-Methode __has_value__(self) sein, die True zurückgibt, wenn der Wert als wertvoll behandelt werden soll, und False, wenn der Wert als wertlos behandelt werden soll.

Mit dieser Verallgemeinerung würde object eine Dunder-Methode implementieren, die diesem entspricht

def __has_value__(self):
    return True

NoneType würde eine Dunder-Methode implementieren, die diesem entspricht

def __has_value__(self):
    return False

In der Spezifikation würden alle Verwendungen von x is None durch not x.__has_value__() ersetzt werden.

Diese Verallgemeinerung würde es ermöglichen, domänenspezifische „No-Value“-Objekte genauso zu coalescen wie None. Beispielsweise hat das Paket pyasn1 einen Typ namens Null, der ein ASN.1 null repräsentiert.

>>> from pyasn1.type import univ
>>> univ.Null() ?? univ.Integer(123)
Integer(123)

Ähnlich könnten Werte wie math.nan und NotImplemented als „kein Wert“ behandelt werden.

Der „No-Value“-Charakter dieser Werte ist jedoch domänenspezifisch, was bedeutet, dass sie von der Sprache als Wert behandelt *werden sollten*. Zum Beispiel ist math.nan.imag wohl definiert (es ist 0.0), und daher wäre das Short-Circuiting von math.nan?.imag, um math.nan zurückzugeben, falsch.

Da None bereits von der Sprache als der Wert definiert ist, der „keinen Wert“ repräsentiert, und die aktuelle Spezifikation einen zukünftigen Wechsel zu einem Protokoll nicht ausschließen würde (obwohl Änderungen an eingebauten Objekten nicht kompatibel wären), wird diese Idee vorerst abgelehnt.

Boolesche Operatoren

Dieser Vorschlag ist im Grunde derselbe wie die Hinzufügung eines No-Value-Protokolls, daher gilt die obige Diskussion ebenfalls.

Ähnliches Verhalten wie beim Operator ?? kann mit einem or-Ausdruck erzielt werden. Allerdings prüft or, ob sein linker Operand falschy ist und nicht speziell None. Dieser Ansatz ist attraktiv, da er weniger Änderungen am Sprachsystem erfordert, löst aber letztendlich nicht das zugrunde liegende Problem korrekt.

Unter der Annahme, dass die Prüfung auf Wahrhaftigkeit statt auf None erfolgt, besteht keine Notwendigkeit mehr für den Operator ??. Die Anwendung dieser Prüfung auf die Operatoren ?. und ?[] verhindert jedoch, dass einwandfreie Operationen ausgeführt werden können.

Betrachten Sie das folgende Beispiel, bei dem get_log_list() entweder eine Liste mit aktuellen Log-Nachrichten (möglicherweise leer) oder None zurückgibt, wenn Logging nicht aktiviert ist.

lst = get_log_list()
lst?.append('A log message')

Wenn ?. auf Wahrhaftigkeit prüft und nicht speziell auf None, und das Protokoll noch nicht mit Einträgen initialisiert wurde, wird kein Element angehängt. Dies widerspricht der offensichtlichen Absicht des Codes, nämlich ein Element anzuhängen. Die Methode append ist auf einer leeren Liste verfügbar, ebenso wie alle anderen Listenmethoden, und es gibt keinen Grund anzunehmen, dass diese Mitglieder nicht verwendet werden sollten, nur weil die Liste derzeit leer ist.

Darüber hinaus gibt es kein sinnvolles Ergebnis, das anstelle des Ausdrucks verwendet werden kann. Eine normale lst.append gibt None zurück, aber unter dieser Idee kann lst?.append entweder [] oder None ergeben, abhängig vom Wert von lst. Wie bei den Beispielen im vorherigen Abschnitt ergibt dies keinen Sinn.

Da die Prüfung auf Wahrhaftigkeit anstelle von None dazu führt, dass scheinbar gültige Ausdrücke nicht mehr wie beabsichtigt ausgeführt werden, wird diese Idee abgelehnt.

Ausnahme-sichere Operatoren

Man könnte argumentieren, dass der Grund für das Short-Circuiting eines Ausdrucks, wenn None angetroffen wird, darin besteht, die AttributeError oder TypeError zu vermeiden, die unter normalen Umständen ausgelöst würden. Als Alternative zur Prüfung auf None könnten die Operatoren ?. und ?[] stattdessen AttributeError und TypeError behandeln, die von der Operation ausgelöst werden, und den Rest des Ausdrucks überspringen.

Dies erzeugt eine Transformation für a?.b.c?.d.e ähnlich wie diese

_v = a
try:
    _v = _v.b
except AttributeError:
    pass
else:
    _v = _v.c
    try:
        _v = _v.d
    except AttributeError:
        pass
    else:
        _v = _v.e

Eine offene Frage ist, welcher Wert als Ausdruck zurückgegeben werden soll, wenn eine Ausnahme behandelt wird. Das obige Beispiel lässt das Teilergebnis einfach stehen, aber dies ist nicht hilfreich, um es durch einen Standardwert zu ersetzen. Eine Alternative wäre, das Ergebnis auf None zu erzwingen, was die Frage aufwirft, warum None besonders genug ist, um das Ergebnis zu sein, aber nicht besonders genug, um der Test zu sein.

Zweitens maskiert dieser Ansatz Fehler in Code, der implizit als Teil des Ausdrucks ausgeführt wird. Für ?. würden alle AttributeError innerhalb einer Eigenschaft oder einer __getattr__-Implementierung versteckt werden, und ähnlich für ?[] und __getitem__-Implementierungen.

Ebenso könnten einfache Tippfehler wie {}?.ietms() unbemerkt bleiben.

Bestehende Konventionen für die Behandlung dieser Arten von Fehlern in Form des integrierten getattr und des von dict etablierten Musters .get(key, default) zeigen, dass es bereits möglich ist, dieses Verhalten explizit zu nutzen.

Da dieser Ansatz Fehler im Code verstecken würde, wird er abgelehnt.

None-aware Funktionsaufruf

Die None-aware Syntax gilt für Attribut- und Indexzugriffe, daher scheint es natürlich zu fragen, ob sie auch für die Syntax von Funktionsaufrufen gelten sollte. Sie könnte als foo?() geschrieben werden, wobei foo nur aufgerufen wird, wenn es nicht None ist.

Dies wurde zurückgestellt, da die vorgeschlagenen Operatoren dazu bestimmt sind, die Traversierung von teilweise gefüllten hierarchischen Datenstrukturen zu erleichtern, *nicht* für die Traversierung beliebiger Klassenhierarchien. Dies spiegelt sich in der Tatsache wider, dass keine der anderen Mainstream-Sprachen, die diese Syntax bereits anbieten, es für wert erachtet hat, eine ähnliche Syntax für optionale Funktionsaufrufe zu unterstützen.

Ein Workaround, ähnlich dem von C# verwendeten, wäre, maybe_none?.__call__(arguments) zu schreiben. Wenn der Aufrufbare None ist, wird der Ausdruck nicht ausgewertet. (Das C#-Äquivalent verwendet ?.Invoke() auf seinem Callable-Typ.)

? Unärer Postfix-Operator

Um das None-aware Verhalten zu verallgemeinern und die Anzahl der eingeführten neuen Operatoren zu begrenzen, wurde ein unärer, postfix-Operator mit der Schreibweise ? vorgeschlagen. Die Idee ist, dass ? ein spezielles Objekt zurückgeben könnte, das Dunder-Methoden überschreiben würde, die self zurückgeben. Zum Beispiel würde foo? zu foo ausgewertet, wenn es nicht None ist, andernfalls würde es zu einer Instanz von NoneQuestion ausgewertet.

class NoneQuestion():
    def __call__(self, *args, **kwargs):
        return self

    def __getattr__(self, name):
        return self

    def __getitem__(self, key):
        return self

Mit diesem neuen Operator und diesem neuen Typ würde ein Ausdruck wie foo?.bar[baz] zu NoneQuestion ausgewertet, wenn foo None ist. Dies ist eine raffinierte Verallgemeinerung, aber sie ist in der Praxis schwierig zu verwenden, da die meisten bestehenden Codes nicht wissen, was NoneQuestion ist.

Zurück zu einem der motivierenden Beispiele oben, betrachten Sie Folgendes:

>>> import json
>>> created = None
>>> json.dumps({'created': created?.isoformat()})

Der JSON-Serialisierer weiß nicht, wie NoneQuestion serialisiert wird, und jede andere API auch nicht. Dieser Vorschlag erfordert tatsächlich *viel spezialisierte Logik* in der Standardbibliothek und jeder Drittanbieterbibliothek.

Gleichzeitig kann der Operator ? auch zu allgemein sein, da er mit jedem anderen Operator kombiniert werden kann. Was sollen die folgenden Ausdrücke bedeuten?

>>> x? + 1
>>> x? -= 1
>>> x? == 1
>>> ~x?

Dieser Grad der Verallgemeinerung ist nicht nützlich. Die hier tatsächlich vorgeschlagenen Operatoren sind absichtlich auf wenige Operatoren beschränkt, von denen erwartet wird, dass sie das Schreiben gängiger Code-Muster erleichtern.

Eingebautes maybe

Haskell hat ein Konzept namens Maybe, das die Idee eines optionalen Werts kapselt, ohne sich auf ein spezielles Schlüsselwort (z. B. null) oder eine spezielle Instanz (z. B. None) zu verlassen. In Haskell besteht der Zweck von Maybe darin, eine separate Handhabung von "etwas" und nichts zu vermeiden.

Ein Python-Paket namens pymaybe bietet eine grobe Annäherung. Die Dokumentation zeigt folgendes Beispiel

>>> maybe('VALUE').lower()
'value'

>>> maybe(None).invalid().method().or_else('unknown')
'unknown'

Die Funktion maybe() gibt entweder eine Something-Instanz oder eine Nothing-Instanz zurück. Ähnlich wie der im vorherigen Abschnitt beschriebene unäre Postfix-Operator überschreibt Nothing Dunder-Methoden, um Verkettungen bei fehlenden Werten zu ermöglichen.

Beachten Sie, dass or_else() letztendlich benötigt wird, um den zugrunde liegenden Wert aus den Wrappern von pymaybe abzurufen. Darüber hinaus wertet pymaybe keine Auswertungen kurz. Obwohl pymaybe einige Stärken hat und an sich nützlich sein mag, zeigt es auch, warum eine reine Python-Implementierung der Koaleszenz nicht annähernd so leistungsfähig ist wie die in der Sprache integrierte Unterstützung.

Die Idee, einen integrierten maybe-Typ hinzuzufügen, um dieses Szenario zu ermöglichen, wird abgelehnt.

Verwenden Sie einfach einen bedingten Ausdruck

Eine weitere gängige Methode zur Initialisierung von Standardwerten ist die Verwendung des ternären Operators. Hier ist ein Auszug aus dem beliebten Requests-Paket

data = [] if data is None else data
files = [] if files is None else files
headers = {} if headers is None else headers
params = {} if params is None else params
hooks = {} if hooks is None else hooks

Diese spezielle Formulierung hat den unerwünschten Effekt, die Operanden in einer unintuitiven Reihenfolge zu platzieren: Das Gehirn denkt: „verwende data, wenn möglich, und verwende [] als Fallback“, aber der Code platziert den Fallback *vor* dem bevorzugten Wert.

Der Autor dieses Pakets hätte es auch so schreiben können

data = data if data is not None else []
files = files if files is not None else []
headers = headers if headers is not None else {}
params = params if params is not None else {}
hooks = hooks if hooks is not None else {}

Diese Reihenfolge der Operanden ist intuitiver, erfordert aber 4 zusätzliche Zeichen (für „not “). Sie hebt auch die Wiederholung von Bezeichnern hervor: data if data, files if files usw.

Wenn es mit dem None-Koaleszenzoperator geschrieben wird, liest sich das Beispiel

data = data ?? []
files = files ?? []
headers = headers ?? {}
params = params ?? {}
hooks = hooks ?? {}

Referenzen


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

Zuletzt geändert: 2025-02-01 08:55:40 GMT