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

Python Enhancement Proposals

PEP 532 – Ein "Circuit Breaking"-Protokoll und binäre Operatoren

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>, Mark E. Haase <mehaase at gmail.com>
Status:
Verschoben
Typ:
Standards Track
Erstellt:
30-Okt-2016
Python-Version:
3.8
Post-History:
05-Nov-2016

Inhaltsverzeichnis

PEP Verschiebung

Weitere Überlegungen zu diesem PEP wurden auf frühestens Python 3.8 verschoben.

Zusammenfassung

Inspiriert von PEP 335, PEP 505, PEP 531 und den zugehörigen Diskussionen schlägt dieser PEP die Definition eines neuen "Circuit Breaking"-Protokolls (mit den Methodennamen __then__ und __else__) vor, das eine gemeinsame zugrundeliegende semantische Grundlage für

  • bedingte Ausdrücke: LHS if COND else RHS
  • logische Konjunktion: LHS and RHS
  • logische Disjunktion: LHS or RHS
  • die in PEP 505 vorgeschlagenen None-aware Operatoren
  • das in PEP 535 vorgeschlagene Modell für verkettete Rich-Vergleiche

Nutzen des neuen Protokolls ziehend, schlägt es ferner vor, die Definition bedingter Ausdrücke zu überarbeiten, um auch die Verwendung von if und else als rechtsassoziative bzw. linksassoziative allgemeine "Short-Circuiting"-Operatoren zu erlauben.

  • Rechtsassoziatives "Short-Circuiting": LHS if RHS
  • Linksassoziatives "Short-Circuiting": LHS else RHS

Um die logische Inversion (not EXPR) mit den oben genannten Änderungen konsistent zu machen, schlägt er auch die Einführung eines neuen logischen Inversionsprotokolls (unter Verwendung des Methodennamens __not__) vor.

Um das "Short-Circuiting" eines "Circuit Breakers" zu erzwingen, ohne den Ausdruck, der ihn erstellt, zweimal auswerten zu müssen, wird eine neue Hilfsfunktion operator.short_circuit(obj) zum operator-Modul hinzugefügt.

Schließlich wird ein neuer Standardtyp types.CircuitBreaker vorgeschlagen, um den Wahrheitswert eines Objekts (wie er zur Bestimmung des Kontrollflusses verwendet wird) von dem Wert zu entkoppeln, den es aus "short-circuited" "Circuit Breaking"-Ausdrücken zurückgibt, wobei die folgenden Factory-Funktionen zum operator-Modul hinzugefügt werden, um besonders gängige Umschalt-Idiome darzustellen.

  • Umschalten basierend auf bool(obj): operator.true(obj)
  • Umschalten basierend auf not bool(obj): operator.false(obj)
  • Umschalten basierend auf obj is value: operator.is_sentinel(obj, value)
  • Umschalten basierend auf obj is not value: operator.is_not_sentinel(obj, value)

Beziehung zu anderen PEPs

Dieser PEP baut auf einer erweiterten Historie von Arbeiten in anderen Vorschlägen auf. Einige der wichtigsten Vorschläge werden im Folgenden diskutiert.

PEP 531: Existenzprüfprotokoll

Dieser PEP ist ein direkter Nachfolger von PEP 531 und ersetzt das dort definierte Existenzprüfprotokoll sowie die neuen Syntaxoperatoren ?then und ?else durch das neue "Circuit Breaking"-Protokoll und Anpassungen an bedingten Ausdrücken und dem not Operator.

PEP 505: None-aware Operatoren

Dieser PEP ergänzt die Vorschläge für None-aware Operatoren in PEP 505, indem er einen zugrundeliegenden protokollgesteuerten semantischen Rahmen bietet, der ihr "Short-Circuiting"-Verhalten als hochoptimierte syntaktische Zucker für spezielle Verwendungen bedingter Ausdrücke erklärt.

Angesichts der von diesem PEP vorgeschlagenen Änderungen

  • LHS ?? RHS wäre grob is_not_sentinel(LHS, None) else RHS
  • EXPR?.attr wäre grob EXPR.attr if is_not_sentinel(EXPR, None)
  • EXPR?[key] wäre grob EXPR[key] if is_not_sentinel(EXPR, None)

In allen drei Fällen wäre die dedizierte syntaktische Form optimiert, um die Erstellung der "Circuit Breaker"-Instanz zu vermeiden und stattdessen den zugrundeliegenden Kontrollfluss direkt zu implementieren. In den letzteren beiden Fällen würde die syntaktische Form auch vermeiden, EXPR zweimal auszuwerten.

Das bedeutet, dass die None-aware Operatoren zwar hochspezialisiert und spezifisch für None bleiben würden, andere Sentinel-Werte aber weiterhin über den allgemeineren protokollgesteuerten Vorschlag in diesem PEP nutzbar wären.

PEP 335: Überladbare boolesche Operatoren

PEP 335 schlug die Möglichkeit vor, die "Short-Circuiting"-Operatoren and und or direkt zu überladen, wobei die Möglichkeit, die Semantik der Vergleichsverkettung zu überladen, eine Folge dieser Änderung wäre. Der Vorschlag in einer früheren Version dieses PEP, den elementweisen Vergleichsfall stattdessen durch Änderung der semantischen Definition der Vergleichsverkettung zu behandeln, stammt direkt aus Guidos Ablehnung von PEP 335 [1].

Jedoch deutete erstes Feedback zu diesem PEP an, dass die Anzahl der verschiedenen darin behandelten Vorschläge das Lesen erschwerte, so dass dieser Teil des Vorschlags als PEP 535 abgetrennt wurde.

PEP 535: Verkettete Rich-Vergleiche

Wie oben erwähnt, ist PEP 535 ein Vorschlag, der auf dem in diesem PEP definierten "Circuit Breaking"-Protokoll aufbaut, um die in PEP 207 eingeführte Unterstützung für Rich-Vergleiche zu erweitern, um auch Vergleichsverkettungsoperationen wie LEFT_BOUND < VALUE < RIGHT_BOUND zu unterstützen.

Spezifikation

Das "Circuit Breaking"-Protokoll (if-else)

Bedingte Ausdrücke (LHS if COND else RHS) werden derzeit als Ausdrucksebene äquivalent zu

if COND:
    _expr_result = LHS
else:
    _expr_result = RHS

Dieser PEP schlägt vor, diese Erweiterung zu ändern, um der geprüften Bedingung zu erlauben, ein neues "Circuit Breaking"-Protokoll zu implementieren, das es ihr erlaubt, das Ergebnis einer oder beider Zweige des Ausdrucks zu sehen und potenziell zu ändern.

_cb = COND
_type_cb = type(cb)
if _cb:
    _expr_result = LHS
    if hasattr(_type_cb, "__then__"):
        _expr_result = _type_cb.__then__(_cb, _expr_result)
else:
    _expr_result = RHS
    if hasattr(_type_cb, "__else__"):
        _expr_result = _type_cb.__else__(_cb, _expr_result)

Wie gezeigt, wären Implementierungen des Interpreters erforderlich, nur auf die Protokollmethode zuzugreifen, die für den Zweig des bedingten Ausdrucks benötigt wird, der tatsächlich ausgeführt wird. Im Einklang mit anderen Protokollmethoden würden die speziellen Methoden über den Typ des "Circuit Breakers" nachgeschlagen, nicht direkt auf der Instanz.

"Circuit Breaking"-Operatoren (binäres if und binäres else)

Der vorgeschlagene Name des Protokolls stammt nicht von den vorgeschlagenen Änderungen an der Semantik bedingter Ausdrücke. Vielmehr stammt er von der vorgeschlagenen Ergänzung von if und else als allgemeine "Short-Circuiting"-Operatoren, die von Protokollen gesteuert werden, um die bestehenden, auf True und False basierenden "Short-Circuiting"-Operatoren (or und and, bzw.) sowie den in PEP 505 vorgeschlagenen, auf None basierenden "Short-Circuiting"-Operator (??) zu ergänzen.

Zusammen würden diese beiden Operatoren als "Circuit Breaking"-Operatoren bekannt sein.

Um diese Verwendung zu unterstützen, würde die Definition bedingter Ausdrücke in der Sprachgrammatik aktualisiert, um sowohl die if-Klausel als auch die else-Klausel optional zu machen.

test: else_test ['if' or_test ['else' test]] | lambdef
else_test: or_test ['else' test]

Beachten Sie, dass wir die scheinbare Vereinfachung zu else_test ('if' else_test)* vermeiden müssten, um es für Compiler-Implementierungen einfacher zu machen, die Semantik normaler bedingter Ausdrücke korrekt zu erhalten.

Der Knoten test_nocond in der Grammatik (der bedingte Ausdrücke absichtlich ausschließt) würde unverändert bleiben, so dass die "Circuit Breaking"-Operatoren Klammern benötigen, wenn sie in der if-Klausel von Comprehensions und Generator Expressions verwendet werden, genau wie bedingte Ausdrücke selbst.

Diese Grammatikdefinition bedeutet, dass sich die Präzedenz/Assoziativität im ansonsten mehrdeutigen Fall von expr1 if cond else expr2 else expr3 als (expr1 if cond else expr2) else epxr3 auflöst. Es wird jedoch auch ein Richtlinie zu PEP 8 hinzugefügt werden, die besagt „tun Sie das nicht“, da ein solcher Konstrukt für Leser unabhängig davon, wie der Interpreter ihn ausführt, inhärent verwirrend sein wird.

Der rechtsassoziative "Circuit Breaking"-Operator (LHS if RHS) würde dann wie folgt erweitert:

_cb = RHS
_expr_result = LHS if _cb else _cb

Während der linksassoziative "Circuit Breaking"-Operator (LHS else RHS) wie folgt erweitert würde:

_cb = LHS
_expr_result = _cb if _cb else RHS

Der entscheidende Punkt, der in beiden Fällen zu beachten ist: Wenn der "Circuit Breaking"-Ausdruck "short-circuited", wird der Bedingungsausdruck als Ergebnis des Ausdrucks verwendet, *es sei denn*, die Bedingung ist ein "Circuit Breaker". Im letzteren Fall wird die entsprechende "Circuit Breaker"-Protokollmethode wie gewohnt aufgerufen, aber der "Circuit Breaker" selbst wird als Argument an die Methode übergeben.

Dies ermöglicht es "Circuit Breakern", "Short-Circuiting" zuverlässig zu erkennen, indem sie Fälle prüfen, in denen das als Kandidaten-Ausdrucksergebnis übergebene Argument self ist.

Überladen der logischen Inversion (not)

Jede "Circuit Breaker"-Definition wird eine logische Umkehrung haben, die immer noch ein "Circuit Breaker" ist, aber die Antwort umkehrt, wann der Ausdruck ausgewertet werden soll. Zum Beispiel sind die in diesem PEP vorgeschlagenen "Circuit Breaker" operator.true und operator.false gegenseitig logische Umkehrungen.

Eine neue Protokollmethode, __not__(self), wird eingeführt, um es "Circuit Breakern" und anderen Typen zu ermöglichen, not-Ausdrücke zu überschreiben, um ihre logische Umkehrung anstelle eines konvertierten booleschen Ergebnisses zurückzugeben.

Um die Semantik bestehender Sprachoptimierungen beizubehalten (wie z.B. die Eliminierung doppelter Negationen direkt in einem booleschen Kontext als redundant), müssen __not__-Implementierungen die folgende Invariante respektieren.

assert not bool(obj) == bool(not obj)

Symmetrische "Circuit Breaker" (die alle von __bool__, __not__, __then__ und __else__ implementieren) müssten jedoch nur die volle Semantik der booleschen Logik respektieren, wenn alle an dem Ausdruck beteiligten "Circuit Breaker" eine konsistente Definition von "Wahrheit" verwenden. Dies wird weiter in Beachtung der De Morganschen Gesetze behandelt.

Erzwingen des Short-Circuiting-Verhaltens

Die Auslösung des "Short-Circuiting"-Verhaltens eines "Circuit Breakers" kann erzwungen werden, indem er als alle drei Operanden in einem bedingten Ausdruck verwendet wird.

obj if obj else obj

Oder, gleichwertig, als beide Operanden in einem "Circuit Breaking"-Ausdruck.

obj if obj
obj else obj

Anstatt die Verwendung eines dieser Muster zu verlangen, schlägt dieser PEP vor, eine dedizierte Funktion zum operator-Modul hinzuzufügen, um einen "Circuit Breaker" explizit zu "short-circuiten" und andere Objekte unverändert durchzulassen.

def short_circuit(obj)
    """Replace circuit breakers with their short-circuited result

    Passes other input values through unmodified.
    """
    return obj if obj else obj

"Circuit Breaking"-Identitätsvergleiche (is und is not)

In Abwesenheit von Standard-"Circuit Breakern" wären die vorgeschlagenen Operatoren if und else weitgehend nur ungewöhnliche Schreibweisen der bestehenden logischen Operatoren and und or.

Dieser PEP schlägt jedoch ferner vor, einen neuen allgemeinen Typ types.CircuitBreaker bereitzustellen, der die entsprechende "Short-Circuiting"-Logik implementiert, sowie Factory-Funktionen im operator-Modul, die den Operatoren is und is not entsprechen.

Diese würden so definiert, dass die folgenden Ausdrücke VALUE anstelle von False ergeben, wenn die bedingte Prüfung fehlschlägt.

EXPR if is_sentinel(VALUE, SENTINEL)
EXPR if is_not_sentinel(VALUE, SENTINEL)

Und ähnlich würden diese VALUE anstelle von True ergeben, wenn die bedingte Prüfung erfolgreich ist.

is_sentinel(VALUE, SENTINEL) else EXPR
is_not_sentinel(VALUE, SENTINEL) else EXPR

Im Wesentlichen würden diese Vergleiche so definiert, dass die führenden Klauseln VALUE if und die nachfolgende Klausel else VALUE als impliziert in Ausdrücken der folgenden Formen weggelassen werden können.

# To handle "if" expressions, " else VALUE" is implied when omitted
EXPR if is_sentinel(VALUE, SENTINEL) else VALUE
EXPR if is_not_sentinel(VALUE, SENTINEL) else VALUE
# To handle "else" expressions, "VALUE if " is implied when omitted
VALUE if is_sentinel(VALUE, SENTINEL) else EXPR
VALUE if is_not_sentinel(VALUE, SENTINEL) else EXPR

Der vorgeschlagene Typ types.CircuitBreaker würde dieses Verhalten programmatisch wie folgt darstellen:

class CircuitBreaker:
    """Simple circuit breaker type"""
    def __init__(self, value, bool_value):
        self.value = value
        self.bool_value = bool(bool_value)
    def __bool__(self):
        return self.bool_value
    def __not__(self):
        return CircuitBreaker(self.value, not self.bool_value)
    def __then__(self, result):
        if result is self:
            return self.value
        return result
    def __else__(self, result):
        if result is self:
            return self.value
        return result

Das Hauptmerkmal dieser "Circuit Breaker" ist, dass sie *ephemer* sind: Wenn ihnen mitgeteilt wird, dass ein "Short-Circuiting" stattgefunden hat (indem sie eine Referenz auf sich selbst als Kandidaten-Ausdrucksergebnis erhalten), geben sie den ursprünglichen Wert zurück und nicht den "Circuit Breaking"-Wrapper.

Die Erkennung von "Short-Circuiting" ist so definiert, dass der Wrapper immer entfernt wird, wenn Sie dieselbe "Circuit Breaker"-Instanz explizit an beide Seiten eines "Circuit Breaking"-Operators übergeben oder einen als alle drei Operanden in einem bedingten Ausdruck verwenden.

breaker = types.CircuitBreaker(foo, foo is None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo
breaker = types.CircuitBreaker(foo, foo is not None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo

Die Factory-Funktionen im operator-Modul würden es dann einfach machen, "Circuit Breaker" zu erstellen, die Identitätsprüfungen mit den Operatoren is und is not entsprechen.

def is_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is sentinel'"""
    return types.CircuitBreaker(value, value is sentinel)

def is_not_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is not sentinel'"""
    return types.CircuitBreaker(value, value is not sentinel)

Wahrheitsprüfvergleiche

Aufgrund ihrer "Short-Circuiting"-Natur war die Laufzeitlogik, die den Operatoren and und or zugrunde liegt, noch nie über die Module operator oder types zugänglich gewesen.

Die Einführung von "Circuit Breaking"-Operatoren und "Circuit Breakern" ermöglicht es, diese Logik wie folgt im operator-Modul zu erfassen:

def true(value):
    """Returns a circuit breaker switching on 'bool(value)'"""
    return types.CircuitBreaker(value, bool(value))

def false(value):
    """Returns a circuit breaker switching on 'not bool(value)'"""
    return types.CircuitBreaker(value, not bool(value))
  • LHS or RHS wäre effektiv true(LHS) else RHS
  • LHS and RHS wäre effektiv false(LHS) else RHS

Es würde keine tatsächliche Änderung an diesen Operator-Definitionen stattfinden; das neue "Circuit Breaking"-Protokoll und die neuen Operatoren würden lediglich eine Möglichkeit bieten, die Kontrollflusslogik programmierbar zu machen, anstatt den Sinn der Prüfung zur Entwicklungszeit fest zu codieren.

Unter Beachtung der Regeln der booleschen Logik könnten diese Ausdrücke auch in ihrer invertierten Form erweitert werden, indem stattdessen der rechtsassoziative "Circuit Breaking"-Operator verwendet wird.

  • LHS or RHS wäre effektiv RHS if false(LHS)
  • LHS and RHS wäre effektiv RHS if true(LHS)

None-bewusste Operatoren

Wenn sowohl dieser PEP als auch die None-aware Operatoren von PEP 505 akzeptiert würden, würden die vorgeschlagenen Factory-Funktionen is_sentinel und is_not_sentinel verwendet, um die Vorstellung der "None-Prüfung" zu kapseln: zu sehen, ob ein Wert None ist, und entweder auf einen alternativen Wert zurückzufallen (eine Operation, die als "None-Coalescing" bekannt ist) oder ihn als Ergebnis des gesamten Ausdrucks weiterzugeben (eine Operation, die als "None-Severing" oder "None-Propagating" bekannt ist).

Mit diesen "Circuit Breakern" wäre LHS ?? RHS grob äquivalent zu beiden folgenden:

  • is_not_sentinel(LHS, None) else RHS
  • RHS if is_sentinel(LHS, None)

Aufgrund der Art und Weise, wie sie den Kontrollfluss in Attributzugriffs- und Subscripting-Operationen einfügen, können None-aware Attributzugriffe und None-aware Subscripting nicht direkt anhand der "Circuit Breaking"-Operatoren ausgedrückt werden, aber sie können dennoch anhand des zugrundeliegenden "Circuit Breaking"-Protokolls definiert werden.

In diesen Begriffen wäre EXPR?.ATTR[KEY].SUBATTR() semantisch äquivalent zu

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base.ATTR[KEY].SUBATTR() if _circuit_breaker

Ebenso wäre EXPR?[KEY].ATTR.SUBATTR() semantisch äquivalent zu

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base[KEY].ATTR.SUBATTR() if _circuit_breaker

Die tatsächlichen Implementierungen der None-aware Operatoren würden vermutlich optimiert, um die Erstellung der "Circuit Breaker"-Instanz zu überspringen, aber die obigen Erweiterungen würden immer noch eine genaue Beschreibung des beobachtbaren Verhaltens der Operatoren zur Laufzeit liefern.

Verkettete Rich-Vergleiche

Für eine detaillierte Diskussion dieses möglichen Anwendungsfalls siehe PEP 535.

Andere bedingte Konstrukte

Es werden keine Änderungen an `if`-Anweisungen, `while`-Anweisungen, Comprehensions oder Generator Expressions vorgeschlagen, da die dort enthaltenen booleschen Klauseln ausschließlich zu Kontrollflusszwecken verwendet werden und niemals ein Ergebnis als solches zurückgeben.

Es ist jedoch erwähnenswert, dass, obwohl solche Vorschläge außerhalb des Rahmens dieses PEP liegen, das hier definierte "Circuit Breaking"-Protokoll bereits ausreichen würde, um Konstrukte wie

def is_not_none(obj):
    return is_sentinel(obj, None)

while is_not_none(dynamic_query()) as result:
    ... # Code using result

und

if is_not_none(re.search(pattern, text)) as match:
    ... # Code using match

Dies könnte geschehen, indem das Ergebnis von operator.short_circuit(CONDITION) dem Namen in der as-Klausel zugewiesen wird, anstatt CONDITION direkt dem gegebenen Namen zuzuweisen.

Empfehlungen zum Styleguide

Die folgenden Ergänzungen zu PEP 8 werden in Bezug auf die durch diesen PEP eingeführten neuen Funktionen vorgeschlagen.

  • Vermeiden Sie die Kombination von bedingten Ausdrücken (if-else) und den eigenständigen "Circuit Breaking"-Operatoren (if und else) in einem einzigen Ausdruck – verwenden Sie je nach Situation den einen oder den anderen, aber nicht beides.
  • Vermeiden Sie die Verwendung von bedingten Ausdrücken (if-else) und den eigenständigen "Circuit Breaking"-Operatoren (if und else) als Teil von if-Bedingungen in if-Anweisungen und den Filterklauseln von Comprehensions und Generator Expressions.

Begründung

Hinzufügen neuer Operatoren

Ähnlich wie bei PEP 335 konzentrierten sich frühe Entwürfe dieses PEP darauf, die bestehenden Operatoren and und or weniger starr in ihrer Interpretation zu machen, anstatt neue Operatoren vorzuschlagen. Dies erwies sich jedoch aus einigen wichtigen Gründen als problematisch.

  • Die Operatoren and und or haben eine lange etablierte und stabile Bedeutung, so dass die Leser zwangsläufig überrascht wären, wenn ihre Bedeutung nun vom Typ des linken Operanden abhinge. Selbst neue Benutzer wären von dieser Änderung verwirrt, aufgrund von über 25 Jahren Lehrmaterial, das von den aktuellen, bekannten Semantiken für diese Operatoren ausgeht.
  • Implementierungen von Python-Interpretern, einschließlich CPython, haben die bestehenden Semantiken von and und or bei der Definition von Laufzeit- und Compile-Zeit-Optimierungen genutzt, die alle überprüft und möglicherweise verworfen werden müssten, wenn sich die Semantiken dieser Operationen ändern würden.
  • Es ist unklar, welche Namen für die neuen Methoden, die zur Definition des Protokolls benötigt werden, angemessen wären.

Das Vorschlagen von binären "Short-Circuiting"-Varianten des bestehenden ternären Operators if-else löst all diese Probleme.

  • die Laufzeitsemantik von and und or bleibt vollständig unverändert.
  • Während sich die Semantik des unären Operators not ändert, bedeutet die Invariante, die für __not__-Implementierungen erforderlich ist, dass bestehende Ausdrucksoptimierungen in booleschen Kontexten gültig bleiben.
  • __else__ ist das "Short-Circuiting"-Ergebnis für if-Ausdrücke aufgrund des Fehlens einer nachfolgenden else-Klausel.
  • __then__ ist das "Short-Circuiting"-Ergebnis für else-Ausdrücke aufgrund des Fehlens einer vorangehenden if-Klausel (diese Verbindung wäre sogar noch klarer, wenn der Methodenname __if__ wäre, aber das wäre mehrdeutig angesichts der anderen Verwendungen des Schlüsselworts if, die das "Circuit Breaking"-Protokoll nicht auslösen werden).

Benennung des Operators und Protokolls

Die Namen "Circuit Breaking Operator", "Circuit Breaking Protocol" und "Circuit Breaker" sind alle von der Phrase "short circuiting operator" inspiriert: dem allgemeinen Begriff des Sprachdesigns für Operatoren, die ihren rechten Operanden nur bedingt auswerten.

Die elektrische Analogie besagt, dass Circuit Breaker in Python Kurzschlüsse in Ausdrücken erkennen und behandeln, bevor diese Ausnahmen auslösen, ähnlich wie Circuit Breaker Kurzschlüsse in elektrischen Systemen erkennen und behandeln, bevor sie Geräte beschädigen oder Menschen verletzen.

Die Python-Analogie besagt, dass, so wie eine break-Anweisung es Ihnen erlaubt, eine Schleife zu beenden, bevor sie ihren natürlichen Abschluss erreicht, ein "Circuit Breaking"-Ausdruck es Ihnen erlaubt, die Auswertung des Ausdrucks zu beenden und sofort ein Ergebnis zu liefern.

Verwendung bestehender Schlüsselwörter

Die Verwendung bestehender Schlüsselwörter hat den Vorteil, dass die neuen Operatoren ohne eine __future__-Anweisung eingeführt werden können.

if und else sind semantisch für das vorgeschlagene neue Protokoll geeignet, und die einzige zusätzliche syntaktische Mehrdeutigkeit entsteht, wenn die neuen Operatoren mit der expliziten Syntax des bedingten Ausdrucks if-else kombiniert werden.

Der PEP behandelt diese Mehrdeutigkeit, indem er explizit festlegt, wie sie von Interpreter-Implementierern behandelt werden soll, schlägt aber vor, in PEP 8 darauf hinzuweisen, dass, obwohl Interpreter es verstehen werden, menschliche Leser es wahrscheinlich nicht tun werden, und es daher keine gute Idee sein wird, sowohl bedingte Ausdrücke als auch die "Circuit Breaking"-Operatoren in einem einzigen Ausdruck zu verwenden.

Benennung der Protokollmethoden

Die Benennung der Methode __else__ war unkompliziert, da die Wiederverwendung des Operator-Schlüsselwortnamens zu einem Spezialmethodennamen führt, der sowohl offensichtlich als auch eindeutig ist.

Die Benennung der Methode __then__ war weniger unkompliziert, da es eine weitere mögliche Option gab, nämlich die Verwendung des schlüsselwortbasierten Namens __if__.

Das Problem mit __if__ ist, dass es weiterhin viele Fälle geben würde, in denen das Schlüsselwort if mit einem Ausdruck direkt daneben vorkäme, aber die Spezialmethode __if__ nicht aufgerufen würde. Stattdessen würde die eingebaute Funktion bool() und ihre zugrundeliegenden Spezialmethoden (__bool__, __len__) aufgerufen, während __if__ keine Wirkung hätte.

Da das boolesche Protokoll bereits eine Rolle bei bedingten Ausdrücken und dem neuen "Circuit Breaking"-Protokoll spielt, wurde der weniger mehrdeutige Name __then__ basierend auf der Terminologie gewählt, die üblicherweise in der Informatik und Sprachgestaltung verwendet wird, um die erste Klausel einer if-Anweisung zu beschreiben.

Rechtsassoziative Benennung von binärem if

Das von bedingten Ausdrücken gesetzte Präzedenzfall bedeutet, dass ein binärer "Short-Circuiting"-if-Ausdruck notwendigerweise die Bedingung auf der rechten Seite haben muss, um Konsistenz zu gewährleisten.

Da der rechte Operand immer zuerst ausgewertet wird und der linke Operand gar nicht ausgewertet wird, wenn der rechte Operand in einem booleschen Kontext wahr ist, ist das natürliche Ergebnis ein rechtsassoziativer Operator.

Benennung der Standard-"Circuit Breaker"

Wenn nur der linksassoziative "Circuit Breaking"-Operator verwendet wird, lesen sich explizite "Circuit Breaker"-Namen für unäre Prüfungen gut, wenn sie mit der Präposition if_ beginnen.

operator.if_true(LHS) else RHS
operator.if_false(LHS) else RHS

Die Einbeziehung von if_ liest sich jedoch nicht so gut bei der logischen Inversion.

not operator.if_true(LHS) else RHS
not operator.if_false(LHS) else RHS

Oder bei der Verwendung des rechtsassoziativen "Circuit Breaking"-Operators.

LHS if operator.if_true(RHS)
LHS if operator.if_false(RHS)

Oder bei der Benennung einer binären Vergleichsoperation.

operator.if_is_sentinel(VALUE, SENTINEL) else EXPR
operator.if_is_not_sentinel(VALUE, SENTINEL) else EXPR

Im Gegensatz dazu liest sich das Weglassen der Präposition vom "Circuit Breaker"-Namen in allen Formen für unäre Prüfungen vernünftig.

operator.true(LHS) else RHS       # Preceding "LHS if " implied
operator.false(LHS) else RHS      # Preceding "LHS if " implied
not operator.true(LHS) else RHS   # Preceding "LHS if " implied
not operator.false(LHS) else RHS  # Preceding "LHS if " implied
LHS if operator.true(RHS)         # Trailing " else RHS" implied
LHS if operator.false(RHS)        # Trailing " else RHS" implied
LHS if not operator.true(RHS)     # Trailing " else RHS" implied
LHS if not operator.false(RHS)    # Trailing " else RHS" implied

Und liest sich auch gut für binäre Prüfungen.

operator.is_sentinel(VALUE, SENTINEL) else EXPR
operator.is_not_sentinel(VALUE, SENTINEL) else EXPR
EXPR if operator.is_sentinel(VALUE, SENTINEL)
EXPR if operator.is_not_sentinel(VALUE, SENTINEL)

Risiken und Bedenken

Dieser PEP wurde speziell entwickelt, um die Risiken und Bedenken anzugehen, die bei der Diskussion von PEPs 335, 505 und 531 aufgetreten sind.

  • er definiert neue Operatoren und passt die Definition von verketteten Vergleichen an (in einem separaten PEP), anstatt die bestehenden Operatoren and und or zu beeinflussen.
  • die vorgeschlagenen neuen Operatoren sind allgemeine binäre "Short-Circuiting"-Operatoren, die sogar verwendet werden können, um die bestehenden Semantiken von and und or auszudrücken, anstatt sich ausschließlich und unflexibel auf die Identitätsprüfung gegen None zu konzentrieren.
  • die Änderungen am unären Operator not und an den binären Vergleichsoperatoren is und is not sind so definiert, dass Kontrollflussoptimierungen, die auf der bestehenden Semantik basieren, gültig bleiben

Eine Folge dieses Ansatzes ist, dass dieses PEP *für sich allein* keine großen direkten Vorteile für Endbenutzer bringt, abgesehen davon, dass es möglich wird, einige übliche None if Präfixe und else None Suffixe aus bestimmten Formen bedingter Ausdrücke wegzulassen.

Stattdessen bietet es hauptsächlich eine gemeinsame Grundlage, die es ermöglichen würde, die Vorschläge für None-aware Operatoren in PEP 505 und den Vorschlag für Rich Comparison Chaining in PEP 535 auf der Grundlage eines gemeinsamen zugrunde liegenden semantischen Rahmens zu verfolgen, der auch mit bedingten Ausdrücken und den bestehenden Operatoren and und or geteilt würde.

Design-Diskussion

Durchlauf des Protokolls

Das folgende Diagramm veranschaulicht die Kernkonzepte des Circuit-Breaking-Protokolls (obwohl es die technischen Details des Nachschlagens der speziellen Methoden über den Typ und nicht über die Instanz vereinfacht)

diagram of circuit breaking protocol applied to ternary expression

Wir werden den folgenden Ausdruck durcharbeiten

>>> def is_not_none(obj):
...     return operator.is_not_sentinel(obj, None)
>>> x if is_not_none(data.get("key")) else y

is_not_none ist eine Hilfsfunktion, die die vorgeschlagene operator.is_not_sentinel types.CircuitBreaker Fabrik mit None als Sentinel-Wert aufruft. data ist ein Container (wie eine eingebaute dict Instanz), der None zurückgibt, wenn die Methode get() mit einem unbekannten Schlüssel aufgerufen wird.

Wir können das Beispiel umschreiben, um der Circuit-Breaker-Instanz einen Namen zu geben

>>> maybe_value = is_not_none(data.get("key"))
>>> x if maybe_value else y

Hier entspricht die maybe_value Circuit-Breaker-Instanz breaker im Diagramm.

Die ternäre Bedingung wird durch den Aufruf von bool(maybe_value) ausgewertet, was dem bestehenden Verhalten von Python entspricht. Die Verhaltensänderung besteht darin, dass das Circuit-Breaking-Protokoll anstatt direkt einen der Operanden x oder y zurückzugeben, den relevanten Operanden an den in der Bedingung verwendeten Circuit Breaker übergibt.

Wenn bool(maybe_value) zu True ausgewertet wird (d.h. der angeforderte Schlüssel existiert und sein Wert ist nicht None), dann ruft der Interpreter type(maybe_value).__then__(maybe_value, x) auf. Andernfalls ruft er type(maybe_value).__else__(maybe_value, y) auf.

Das Protokoll gilt auch für die neuen binären Operatoren if und else, aber in diesen Fällen benötigt der Interpreter eine Möglichkeit, den fehlenden dritten Operanden anzuzeigen. Dies geschieht durch die Wiederverwendung des Circuit Breakers selbst in dieser Rolle.

Betrachten Sie diese beiden Ausdrücke

>>> x if data.get("key") is None
>>> x if operator.is_sentinel(data.get("key"), None)

Die erste Form dieses Ausdrucks gibt x zurück, wenn data.get("key") is None, gibt aber andernfalls False zurück, was höchstwahrscheinlich nicht das ist, was wir wollen.

Im Gegensatz dazu gibt die zweite Form dieses Ausdrucks immer noch x zurück, wenn data.get("key") is None, gibt aber andernfalls data.get("key") zurück, was ein deutlich nützlicheres Verhalten ist.

Wir können dieses Verhalten verstehen, indem wir es als ternären Ausdruck mit einer explizit benannten Circuit-Breaker-Instanz umschreiben

>>> maybe_value = operator.is_sentinel(data.get("key"), None)
>>> x if maybe_value else maybe_value

Wenn bool(maybe_value) True ist (d.h. data.get("key") ist None), dann ruft der Interpreter type(maybe_value).__then__(maybe_value, x) auf. Die Implementierung von types.CircuitBreaker.__then__ sieht nichts, das auf eine Short-Circuiting hinweist, und gibt daher x zurück.

Im Gegensatz dazu, wenn bool(maybe_value) False ist (d.h. data.get("key") ist *nicht* None), ruft der Interpreter type(maybe_value).__else__(maybe_value, maybe_value) auf. Die Implementierung von types.CircuitBreaker.__else__ erkennt, dass die Instanzmethode sich selbst als Argument erhalten hat, und gibt den umschlossenen Wert (d.h. data.get("key")) anstelle des Circuit Breakers zurück.

Die gleiche Logik gilt für else, nur umgekehrt

>>> is_not_none(data.get("key")) else y

Dieser Ausdruck gibt data.get("key") zurück, wenn es nicht None ist, andernfalls wertet er y aus und gibt es zurück. Um die Mechanik zu verstehen, schreiben wir den Ausdruck wie folgt um

>>> maybe_value = is_not_none(data.get("key"))
>>> maybe_value if maybe_value else y

Wenn bool(maybe_value) True ist, dann wird der Ausdruck kurzgeschlossen und der Interpreter ruft type(maybe_value).__else__(maybe_value, maybe_value) auf. Die Implementierung von types.CircuitBreaker.__then__ erkennt, dass die Instanzmethode sich selbst als Argument erhalten hat, und gibt den umschlossenen Wert (d.h. data.get("key")) anstelle des Circuit Breakers zurück.

Wenn bool(maybe_value) True ist, ruft der Interpreter type(maybe_value).__else__(maybe_value, y) auf. Die Implementierung von types.CircuitBreaker.__else__ sieht nichts, das auf eine Short-Circuiting hinweist, und gibt daher y zurück.

Beachtung der De Morganschen Gesetze

Ähnlich wie bei and und or werden die binären Short-Circuiting-Operatoren mehrere Möglichkeiten erlauben, im Wesentlichen denselben Ausdruck zu schreiben. Diese scheinbare Redundanz ist leider eine implizite Folge der Definition des Protokolls als vollständige Boolesche Algebra, da Boolesche Algebren ein Paar Eigenschaften respektieren, die als „De Morgansche Gesetze“ bekannt sind: die Fähigkeit, die Ergebnisse von and und or Operationen durch sich gegenseitig und eine geeignete Kombination von not Operationen auszudrücken.

Für and und or in Python können diese Invarianten wie folgt beschrieben werden

assert bool(A and B) == bool(not (not A or not B))
assert bool(A or B) == bool(not (not A and not B))

Das heißt, wenn man einen der Operatoren nimmt, beide Operanden invertiert, zum anderen Operator wechselt und dann das Gesamtergebnis invertiert, erhält man dasselbe Ergebnis (im booleschen Sinne), wie vom ursprünglichen Operator erhalten. (Das mag redundant erscheinen, aber in vielen Situationen kann es tatsächlich helfen, doppelte Verneinungen zu eliminieren und tautologisch wahre oder falsche Teilausdrücke zu finden, wodurch die Gesamtgröße des Ausdrucks reduziert wird).

Für Circuit Breaker wird die Definition einer geeigneten Invariante dadurch erschwert, dass sie oft so konzipiert sein werden, dass sie sich selbst aus dem Ausdrucksergebnis eliminieren, wenn sie kurzgeschlossen werden, was ein inhärent asymmetrisches Verhalten ist. Dementsprechend muss diese inhärente Asymmetrie bei der Abbildung von De Morganschen Gesetzen auf das erwartete Verhalten von symmetrischen Circuit Breaker berücksichtigt werden.

Eine Möglichkeit, diese Komplikation zu beheben, besteht darin, den Operanden, der andernfalls kurzgeschlossen würde, mit operator.true zu umschließen. Dies stellt sicher, dass, wenn bool auf das Gesamtergebnis angewendet wird, die gleiche Wahrheitsdefinition verwendet wird, die zur Entscheidung verwendet wurde, welcher Zweig ausgewertet werden soll, anstatt bool direkt auf den Eingabewert des Circuit Breakers anzuwenden.

Insbesondere für die neuen Short-Circuiting-Operatoren würden die folgenden Eigenschaften für jeden gut funktionierenden symmetrischen Circuit Breaker, der sowohl __bool__ als auch __not__ implementiert, vernünftigerweise erwartet werden

assert bool(B if true(A)) == bool(not (true(not A) else not B))
assert bool(true(A) else B) == bool(not (not B if true(not A)))

Beachten Sie die Reihenfolge der Operationen auf der rechten Seite (Anwendung von true *nach* dem Invertieren des Eingabe-Circuit-Breakers) - dies stellt sicher, dass eine Aussage tatsächlich über type(A).__not__ gemacht wird und nicht nur über das Verhalten von type(true(A)).__not__.

Zumindest würden types.CircuitBreaker Instanzen dieser Logik folgen, wodurch bestehende Optimierungen für boolesche Ausdrücke (wie die Eliminierung doppelter Verneinungen) weiterhin angewendet werden können.

Beliebige Sentinel-Objekte

Im Gegensatz zu PEPs 505 und 531 verarbeitet der Vorschlag in diesem PEP problemlos benutzerdefinierte Sentinel-Objekte

_MISSING = object()

# Using the sentinel to check whether or not an argument was supplied
def my_func(arg=_MISSING):
    arg = make_default() if is_sentinel(arg, _MISSING) # "else arg" implied

Implizit definierte "Circuit Breaker" in "Circuit Breaking"-Ausdrücken

Ein nie veröffentlichter Entwurf dieses PEP untersuchte die Idee, die binären Operatoren is und is not speziell zu behandeln, so dass sie automatisch als Circuit Breaker behandelt würden, wenn sie im Kontext eines Circuit-Breaking-Ausdrucks verwendet werden. Leider stellte sich heraus, dass dieser Ansatz zwangsläufig zu einem von zwei höchst unerwünschten Ergebnissen führte

  1. der Rückgabetyp dieser Ausdrücke änderte sich universell von bool zu types.CircuitBreaker, was potenziell ein Problem mit der Abwärtskompatibilität verursachte (insbesondere bei der Arbeit mit Erweiterungsmodul-APIs, die explizit nach einem booleschen Built-in-Wert mit PyBool_Check suchen, anstatt den übergebenen Wert durch PyObject_IsTrue zu leiten oder das p (Prädikat) Format in einer der Argumentparsing-Funktionen zu verwenden)
  2. der Rückgabetyp dieser Ausdrücke wurde *kontextabhängig*, was bedeutet, dass andere routinemäßige Refactorings (wie das Auslagern einer Vergleichsoperation in eine lokale Variable) einen erheblichen Einfluss auf die Laufzeitsemantik eines Codeteils haben könnten

Keines dieser möglichen Ergebnisse scheint durch den Vorschlag in diesem PEP gerechtfertigt zu sein, daher wurde zum aktuellen Design zurückgekehrt, bei dem Circuit-Breaker-Instanzen explizit über API-Aufrufe erstellt werden müssen und niemals implizit erzeugt werden.

Implementierung

Wie bei PEP 505 wurde die tatsächliche Implementierung bis auf weiteres aufgeschoben, bis ein prinzipielles Interesse an der Idee besteht, diese Änderungen vorzunehmen.

…wird noch ausgearbeitet…

Danksagungen

Dank geht an Steven D’Aprano für seine detaillierte Kritik [2] am ersten Entwurf dieses PEP, die viele der Änderungen im zweiten Entwurf inspiriert hat, sowie an alle anderen Teilnehmer dieses Diskussionsstrangs [3].

Referenzen


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT