903 lines
36 KiB
Plaintext
903 lines
36 KiB
Plaintext
PEP: 532
|
||
Title: A circuit breaking protocol and binary operators
|
||
Version: $Revision$
|
||
Last-Modified: $Date$
|
||
Author: Nick Coghlan <ncoghlan@gmail.com>,
|
||
Mark E. Haase <mehaase@gmail.com>
|
||
Status: Draft
|
||
Type: Standards Track
|
||
Content-Type: text/x-rst
|
||
Created: 30-Oct-2016
|
||
Python-Version: 3.7
|
||
Post-History: 5-Nov-2016
|
||
|
||
Abstract
|
||
========
|
||
|
||
Inspired by PEP 335, PEP 505, PEP 531, and the related discussions, this PEP
|
||
proposes the definition of a new circuit breaking protocol (using the
|
||
method names ``__then__`` and ``__else__``) that provides a common underlying
|
||
semantic foundation for:
|
||
|
||
* conditional expressions: ``LHS if COND else RHS``
|
||
* logical conjunction: ``LHS and RHS``
|
||
* logical disjunction: ``LHS or RHS``
|
||
* the None-aware operators proposed in PEP 505
|
||
* the rich comparison chaining model proposed in PEP 535
|
||
|
||
Taking advantage of the new protocol, it further proposes that the definition
|
||
of conditional expressions be revised to also permit the use of ``if`` and
|
||
``else`` respectively as right-associative and left-associative general
|
||
purpose short-circuiting operators:
|
||
|
||
* Right-associative short-circuiting: ``LHS if RHS``
|
||
* Left-associative short-circuiting: ``LHS else RHS``
|
||
|
||
In order to make logical inversion (``not EXPR``) consistent with the above
|
||
changes, it also proposes the introduction of a new logical inversion protocol
|
||
(using the method name ``__not__``).
|
||
|
||
To force short-circuiting of a circuit breaker without having to evaluate
|
||
the expression creating it twice, a new ``operator.short_circuit(obj)``
|
||
helper function will be added to the operator module.
|
||
|
||
Finally, a new standard ``types.CircuitBreaker`` type is proposed to decouple
|
||
an object's truth value (as used to determine control flow) from the value
|
||
it returns from short-circuited circuit breaking expressions, with the
|
||
following factory functions added to the operator module to represent
|
||
particularly common switching idioms:
|
||
|
||
* switching on ``bool(obj)``: ``operator.true(obj)``
|
||
* switching on ``not bool(obj)``: ``operator.false(obj)``
|
||
* switching on ``obj is value``: ``operator.is_sentinel(obj, value)``
|
||
* switching on ``obj is not value``: ``operator.is_not_sentinel(obj, value)``
|
||
|
||
|
||
Relationship with other PEPs
|
||
============================
|
||
|
||
This PEP builds on an extended history of work in other proposals. Some of
|
||
the key proposals are discussed below.
|
||
|
||
|
||
PEP 531: Existence checking protocol
|
||
------------------------------------
|
||
|
||
This PEP is a direct successor to PEP 531, replacing the existence checking
|
||
protocol and the new ``?then`` and ``?else`` syntactic operators defined there
|
||
with the new circuit breaking protocol and adjustments to conditional
|
||
expressions and the ``not`` operator.
|
||
|
||
|
||
PEP 505: None-aware operators
|
||
-----------------------------
|
||
|
||
This PEP complements the None-aware operator proposals in PEP 505, by offering
|
||
an underlying protocol-driven semantic framework that explains their
|
||
short-circuiting behaviour as highly optimised syntactic sugar for particular
|
||
uses of conditional expressions.
|
||
|
||
Given the changes proposed by this PEP:
|
||
|
||
* ``LHS ?? RHS`` would roughly be ``is_not_sentinel(LHS, None) else RHS``
|
||
* ``EXPR?.attr`` would roughly be ``EXPR.attr if is_not_sentinel(EXPR, None)``
|
||
* ``EXPR?[key]`` would roughly be ``EXPR[key] if is_not_sentinel(EXPR, None)``
|
||
|
||
In all three cases, the dedicated syntactic form would be optimised to avoid
|
||
actually creating the circuit breaker instance and instead implement the
|
||
underlying control flow directly. In the latter two cases, the syntactic form
|
||
would also avoid evaluating ``EXPR`` twice.
|
||
|
||
This means that while the None-aware operators would remain highly specialised
|
||
and specific to None, other sentinel values would still be usable through the
|
||
more general protocol-driven proposal in this PEP.
|
||
|
||
|
||
PEP 335: Overloadable Boolean operators
|
||
---------------------------------------
|
||
|
||
PEP 335 proposed the ability to overload the short-circuiting ``and`` and
|
||
``or`` operators directly, with the ability to overload the semantics of
|
||
comparison chaining being one of the consequences of that change. The
|
||
proposal in an earlier version of this PEP to instead handle the element-wise
|
||
comparison use case by changing the semantic definition of comparison chaining
|
||
is drawn directly from Guido's rejection of PEP 335 [1_].
|
||
|
||
However, initial feedback on this PEP indicated that the number of different
|
||
proposals that it covered made it difficult to read, so that part of the
|
||
proposal has been separated out as PEP 535.
|
||
|
||
|
||
PEP 535: Rich comparison chaining
|
||
---------------------------------
|
||
|
||
As noted above, PEP 535 is a proposal to build on the circuit breaking protocol
|
||
defined in this PEP in order to expand the rich comparison support introduced
|
||
in PEP 207 to also handle comparison chaining operations like
|
||
``LEFT_BOUND < VALUE < RIGHT_BOUND``.
|
||
|
||
|
||
Specification
|
||
=============
|
||
|
||
The circuit breaking protocol (``if-else``)
|
||
-------------------------------------------
|
||
|
||
Conditional expressions (``LHS if COND else RHS``) are currently interpreted
|
||
as an expression level equivalent to::
|
||
|
||
if COND:
|
||
_expr_result = LHS
|
||
else:
|
||
_expr_result = RHS
|
||
|
||
This PEP proposes changing that expansion to allow the checked condition to
|
||
implement a new "circuit breaking" protocol that allows it to see, and
|
||
potentially alter, the result of either or both branches of the expression::
|
||
|
||
_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)
|
||
|
||
As shown, interpreter implementations would be required to access only the
|
||
protocol method needed for the branch of the conditional expression that is
|
||
actually executed. Consistent with other protocol methods, the special methods
|
||
would be looked up via the circuit breaker's type, rather than directly on the
|
||
instance.
|
||
|
||
|
||
Circuit breaking operators (binary ``if`` and binary ``else``)
|
||
--------------------------------------------------------------
|
||
|
||
The proposed name of the protocol doesn't come from the proposed changes to
|
||
the semantics of conditional expressions. Rather, it comes from the proposed
|
||
addition of ``if`` and ``else`` as general purpose protocol driven
|
||
short-circuiting operators to complement the existing ``True`` and ``False``
|
||
based short-circuiting operators (``or`` and ``and``, respectively) as well
|
||
as the ``None`` based short-circuiting operator proposed in PEP 505 (``??``).
|
||
|
||
Together, these two operators would be known as the circuit breaking operators.
|
||
|
||
In order to support this usage, the definition of conditional expressions in
|
||
the language grammar would be updated to make both the ``if`` clause and
|
||
the ``else`` clause optional::
|
||
|
||
test: else_test ['if' or_test ['else' test]] | lambdef
|
||
else_test: or_test ['else' test]
|
||
|
||
Note that we would need to avoid the apparent simplification to
|
||
``else_test ('if' else_test)*`` in order to make it easier for compiler
|
||
implementations to correctly preserve the semantics of normal conditional
|
||
expressions.
|
||
|
||
The definition of the ``test_nocond`` node in the grammar (which deliberately
|
||
excludes conditional expressions) would remain unchanged, so the circuit
|
||
breaking operators would require parentheses when used in the ``if``
|
||
clause of comprehensions and generator expressions just as conditional
|
||
expressions themselves do.
|
||
|
||
This grammar definition means precedence/associativity in the otherwise
|
||
ambiguous case of ``expr1 if cond else expr2 else expr3`` resolves as
|
||
``(expr1 if cond else expr2) else epxr3``. However, a guideline will also be
|
||
added to PEP 8 to say "don't do that", as such a construct will be inherently
|
||
confusing for readers, regardless of how the interpreter executes it.
|
||
|
||
The right-associative circuit breaking operator (``LHS if RHS``) would then
|
||
be expanded as follows::
|
||
|
||
_cb = RHS
|
||
_expr_result = LHS if _cb else _cb
|
||
|
||
While the left-associative circuit breaking operator (``LHS else RHS``) would
|
||
be expanded as::
|
||
|
||
_cb = LHS
|
||
_expr_result = _cb if _cb else RHS
|
||
|
||
The key point to note in both cases is that when the circuit breaking
|
||
expression short-circuits, the condition expression is used as the result of
|
||
the expression *unless* the condition is a circuit breaker. In the latter
|
||
case, the appropriate circuit breaker protocol method is called as usual, but
|
||
the circuit breaker itself is supplied as the method argument.
|
||
|
||
This allows circuit breakers to reliably detect short-circuiting by checking
|
||
for cases when the argument passed in as the candidate expression result is
|
||
``self``.
|
||
|
||
|
||
Overloading logical inversion (``not``)
|
||
---------------------------------------
|
||
|
||
Any circuit breaker definition will have a logical inverse that is still a
|
||
circuit breaker, but inverts the answer as to when to short circuit the
|
||
expression evaluation. For example, the ``operator.true`` and
|
||
``operator.false`` circuit breakers proposed in this PEP are each other's
|
||
logical inverse.
|
||
|
||
A new protocol method, ``__not__(self)``, will be introduced to permit circuit
|
||
breakers and other types to override ``not`` expressions to return their
|
||
logical inverse rather than a coerced boolean result.
|
||
|
||
To preserve the semantics of existing language optimisations (such as
|
||
eliminating double negations directly in a boolean context as redundant),
|
||
``__not__`` implementations will be required to respect the following
|
||
invariant::
|
||
|
||
assert not bool(obj) == bool(not obj)
|
||
|
||
However, symmetric circuit breakers (those that implement all of ``__bool__``,
|
||
``__not__``, ``__then__`` and ``__else__``) would only be expected to respect
|
||
the full semantics of boolean logic when all circuit breakers involved in the
|
||
expression are using a consistent definition of "truth". This is covered
|
||
further in `Respecting De Morgan's Laws`_.
|
||
|
||
|
||
Forcing short-circuiting behaviour
|
||
----------------------------------
|
||
|
||
Invocation of a circuit breaker's short-circuiting behaviour can be forced by
|
||
using it as all three operands in a conditional expression::
|
||
|
||
obj if obj else obj
|
||
|
||
Or, equivalently, as both operands in a circuit breaking expression::
|
||
|
||
obj if obj
|
||
obj else obj
|
||
|
||
Rather than requiring the using of any of these patterns, this PEP proposes
|
||
to add a dedicated function to the ``operator`` to explicitly short-circuit
|
||
a circuit breaker, while passing other objects through unmodified::
|
||
|
||
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 identity comparisons (``is`` and ``is not``)
|
||
-------------------------------------------------------------
|
||
|
||
In the absence of any standard circuit breakers, the proposed ``if`` and
|
||
``else`` operators would largely just be unusual spellings of the existing
|
||
``and`` and ``or`` logical operators.
|
||
|
||
However, this PEP further proposes to provide a new general purpose
|
||
``types.CircuitBreaker`` type that implements the appropriate short
|
||
circuiting logic, as well as factory functions in the operator module
|
||
that correspond to the ``is`` and ``is not`` operators.
|
||
|
||
These would be defined in such a way that the following expressions produce
|
||
``VALUE`` rather than ``False`` when the conditional check fails::
|
||
|
||
EXPR if is_sentinel(VALUE, SENTINEL)
|
||
EXPR if is_not_sentinel(VALUE, SENTINEL)
|
||
|
||
And similarly, these would produce ``VALUE`` rather than ``True`` when the
|
||
conditional check succeeds::
|
||
|
||
is_sentinel(VALUE, SENTINEL) else EXPR
|
||
is_not_sentinel(VALUE, SENTINEL) else EXPR
|
||
|
||
In effect, these comparisons would be defined such that the leading
|
||
``VALUE if`` and trailing ``else VALUE`` clauses can be omitted as implied in
|
||
expressions of the following forms::
|
||
|
||
# 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
|
||
|
||
The proposed ``types.CircuitBreaker`` type would represent this behaviour
|
||
programmatically as follows::
|
||
|
||
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
|
||
|
||
The key characteristic of these circuit breakers is that they are *ephemeral*:
|
||
when they are told that short circuiting has taken place (by receiving a
|
||
reference to themselves as the candidate expression result), they return the
|
||
original value, rather than the circuit breaking wrapper.
|
||
|
||
The short-circuiting detection is defined such that the wrapper will always
|
||
be removed if you explicitly pass the same circuit breaker instance to both
|
||
sides of a circuit breaking operator or use one as all three operands in a
|
||
conditional expression::
|
||
|
||
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
|
||
|
||
The factory functions in the ``operator`` module would then make it
|
||
straightforward to create circuit breakers that correspond to identity
|
||
checks using the ``is`` and ``is not`` operators:
|
||
|
||
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)
|
||
|
||
|
||
Truth checking comparisons
|
||
--------------------------
|
||
|
||
Due to their short-circuiting nature, the runtime logic underlying the ``and``
|
||
and ``or`` operators has never previously been accessible through the
|
||
``operator`` or ``types`` modules.
|
||
|
||
The introduction of circuit breaking operators and circuit breakers allows
|
||
that logic to be captured in the operator module as follows::
|
||
|
||
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`` would be effectively ``true(LHS) else RHS``
|
||
* ``LHS and RHS`` would be effectively ``false(LHS) else RHS``
|
||
|
||
No actual change would take place in these operator definitions, the new
|
||
circuit breaking protocol and operators would just provide a way to make the
|
||
control flow logic programmable, rather than hardcoding the sense of the check
|
||
at development time.
|
||
|
||
Respecting the rules of boolean logic, these expressions could also be
|
||
expanded in their inverted form by using the right-associative circuit
|
||
breaking operator instead:
|
||
|
||
* ``LHS or RHS`` would be effectively ``RHS if false(LHS)``
|
||
* ``LHS and RHS`` would be effectively ``RHS if true(LHS)``
|
||
|
||
|
||
None-aware operators
|
||
--------------------
|
||
|
||
If both this PEP and PEP 505's None-aware operators were accepted, then the
|
||
proposed ``is_sentinel`` and ``is_not_sentinel`` circuit breaker factories
|
||
would be used to encapsulate the notion of "None checking": seeing if a value
|
||
is ``None`` and either falling back to an alternative value (an operation known
|
||
as "None-coalescing") or passing it through as the result of the overall
|
||
expression (an operation known as "None-severing" or "None-propagating").
|
||
|
||
Given these circuit breakers, ``LHS ?? RHS`` would be roughly equivalent to
|
||
both of the following:
|
||
|
||
* ``is_not_sentinel(LHS, None) else RHS``
|
||
* ``RHS if is_sentinel(LHS, None)``
|
||
|
||
Due to the way they inject control flow into attribute lookup and subscripting
|
||
operations, None-aware attribute access and None-aware subscripting can't be
|
||
expressed directly in terms of the circuit breaking operators, but they can
|
||
still be defined in terms of the underlying circuit breaking protocol.
|
||
|
||
In those terms, ``EXPR?.ATTR[KEY].SUBATTR()`` would be semantically
|
||
equivalent to::
|
||
|
||
_lookup_base = EXPR
|
||
_circuit_breaker = is_not_sentinel(_lookup_base, None)
|
||
_expr_result = _lookup_base.ATTR[KEY].SUBATTR() if _circuit_breaker
|
||
|
||
Similarly, ``EXPR?[KEY].ATTR.SUBATTR()`` would be semantically equivalent
|
||
to::
|
||
|
||
_lookup_base = EXPR
|
||
_circuit_breaker = is_not_sentinel(_lookup_base, None)
|
||
_expr_result = _lookup_base[KEY].ATTR.SUBATTR() if _circuit_breaker
|
||
|
||
The actual implementations of the None-aware operators would presumably be
|
||
optimised to skip actually creating the circuit breaker instance, but the
|
||
above expansions would still provide an accurate description of the observable
|
||
behaviour of the operators at runtime.
|
||
|
||
|
||
Rich chained comparisons
|
||
------------------------
|
||
|
||
Refer to PEP 535 for a detailed discussion of this possible use case.
|
||
|
||
|
||
Other conditional constructs
|
||
----------------------------
|
||
|
||
No changes are proposed to if statements, while statements, comprehensions,
|
||
or generator expressions, as the boolean clauses they contain are used
|
||
entirely for control flow purposes and never return a result as such.
|
||
|
||
However, it's worth noting that while such proposals are outside the scope of
|
||
this PEP, the circuit breaking protocol defined here would already be
|
||
sufficient to support constructs like::
|
||
|
||
def is_not_none(obj):
|
||
return is_sentinel(obj, None)
|
||
|
||
while is_not_none(dynamic_query()) as result:
|
||
... # Code using result
|
||
|
||
and::
|
||
|
||
if is_not_none(re.search(pattern, text)) as match:
|
||
... # Code using match
|
||
|
||
This could be done by assigning the result of
|
||
``operator.short_circuit(CONDITION)`` to the name given in the ``as`` clause,
|
||
rather than assigning ``CONDITION`` to the given name directly.
|
||
|
||
|
||
Style guide recommendations
|
||
---------------------------
|
||
|
||
The following additions to PEP 8 are proposed in relation to the new features
|
||
introduced by this PEP:
|
||
|
||
* Avoid combining conditional expressions (``if-else``) and the standalone
|
||
circuit breaking operators (``if`` and ``else``) in a single expression -
|
||
use one or the other depending on the situation, but not both.
|
||
|
||
* Avoid using conditional expressions (``if-else``) and the standalone
|
||
circuit breaking operators (``if`` and ``else``) as part of ``if``
|
||
conditions in ``if`` statements and the filter clauses of comprehensions
|
||
and generator expressions.
|
||
|
||
|
||
Rationale
|
||
=========
|
||
|
||
Adding new operators
|
||
--------------------
|
||
|
||
Similar to PEP 335, early drafts of this PEP focused on making the existing
|
||
``and`` and ``or`` operators less rigid in their interpretation, rather than
|
||
proposing new operators. However, this proved to be problematic for a few key
|
||
reasons:
|
||
|
||
* the ``and`` and ``or`` operators have a long established and stable meaning,
|
||
so readers would inevitably be surprised if their meaning now became
|
||
dependent on the type of the left operand. Even new users would be confused
|
||
by this change due to 25+ years of teaching material that assumes the
|
||
current well-known semantics for these operators
|
||
* Python interpreter implementations, including CPython, have taken advantage
|
||
of the existing semantics of ``and`` and ``or`` when defining runtime and
|
||
compile time optimisations, which would all need to be reviewed and
|
||
potentially discarded if the semantics of those operations changed
|
||
* it isn't clear what names would be appropriate for the new methods needed
|
||
to define the protocol
|
||
|
||
Proposing short-circuiting binary variants of the existing ``if-else`` ternary
|
||
operator instead resolves all of those issues:
|
||
|
||
* the runtime semantics of ``and`` and ``or`` remain entirely unchanged
|
||
* while the semantics of the unary ``not`` operator do change, the invariant
|
||
required of ``__not__`` implementations means that existing expression
|
||
optimisations in boolean contexts will remain valid.
|
||
* ``__else__`` is the short-circuiting outcome for ``if`` expressions due to
|
||
the absence of a trailing ``else`` clause
|
||
* ``__then__`` is the short-circuiting outcome for ``else`` expressions due to
|
||
the absence of a leading ``if`` clause (this connection would be even clearer
|
||
if the method name was ``__if__``, but that would be ambiguous given the
|
||
other uses of the ``if`` keyword that won't invoke the circuit breaking
|
||
protocol)
|
||
|
||
|
||
Naming the operator and protocol
|
||
--------------------------------
|
||
|
||
The names "circuit breaking operator", "circuit breaking protocol" and
|
||
"circuit breaker" are all inspired by the phrase "short circuiting operator":
|
||
the general language design term for operators that only conditionally
|
||
evaluate their right operand.
|
||
|
||
The electrical analogy is that circuit breakers in Python detect and handle
|
||
short circuits in expressions before they trigger any exceptions similar to the
|
||
way that circuit breakers detect and handle short circuits in electrical
|
||
systems before they damage any equipment or harm any humans.
|
||
|
||
The Python level analogy is that just as a ``break`` statement lets you
|
||
terminate a loop before it reaches its natural conclusion, a circuit breaking
|
||
expression lets you terminate evaluation of the expression and produce a result
|
||
immediately.
|
||
|
||
|
||
Using existing keywords
|
||
-----------------------
|
||
|
||
Using existing keywords has the benefit of allowing the new operators to
|
||
be introduced without a ``__future__`` statement.
|
||
|
||
``if`` and ``else`` are semantically appropriate for the proposed new protocol,
|
||
and the only additional syntactic ambiguity introduced arises when the new
|
||
operators are combined with the explicit ``if-else`` conditional expression
|
||
syntax.
|
||
|
||
The PEP handles that ambiguity by explicitly specifying how it should be
|
||
handled by interpreter implementers, but proposing to point out in PEP 8
|
||
that even though interpreters will understand it, human readers probably
|
||
won't, and hence it won't be a good idea to use both conditional expressions
|
||
and the circuit breaking operators in a single expression.
|
||
|
||
|
||
Naming the protocol methods
|
||
---------------------------
|
||
|
||
Naming the ``__else__`` method was straightforward, as reusing the operator
|
||
keyword name results in a special method name that is both obvious and
|
||
unambiguous.
|
||
|
||
Naming the ``__then__`` method was less straightforward, as there was another
|
||
possible option in using the keyword-based name ``__if__``.
|
||
|
||
The problem with ``__if__`` is that there would continue to be many cases
|
||
where the ``if`` keyword appeared, with an expression to its immediate right,
|
||
but the ``__if__`` special method would not be invoked. Instead, the
|
||
``bool()`` builtin and its underlying special methods (``__bool__``,
|
||
``__len__``) would be invoked, while ``__if__`` had no effect.
|
||
|
||
With the boolean protocol already playing a part in conditional expressions and
|
||
the new circuit breaking protocol, the less ambiguous name ``__then__`` was
|
||
chosen based on the terminology commonly used in computer science and
|
||
programming language design to describe the first clause of an ``if``
|
||
statement.
|
||
|
||
|
||
Making binary ``if`` right-associative
|
||
--------------------------------------
|
||
|
||
The precedent set by conditional expressions means that a binary
|
||
short-circuiting ``if`` expression must necessarily have the condition on the
|
||
right as a matter of consistency.
|
||
|
||
With the right operand always being evaluated first, and the left operand not
|
||
being evaluated at all if the right operand is true in a boolean context,
|
||
the natural outcome is a right-associative operator.
|
||
|
||
|
||
Naming the standard circuit breakers
|
||
------------------------------------
|
||
|
||
When used solely with the left-associative circuit breaking operator,
|
||
explicit circuit breaker names for unary checks read well if they start with
|
||
the preposition ``if_``::
|
||
|
||
operator.if_true(LHS) else RHS
|
||
operator.if_false(LHS) else RHS
|
||
|
||
However, incorporating the ``if_`` doesn't read as well when performing
|
||
logical inversion::
|
||
|
||
not operator.if_true(LHS) else RHS
|
||
not operator.if_false(LHS) else RHS
|
||
|
||
Or when using the right-associative circuit breaking operator::
|
||
|
||
LHS if operator.if_true(RHS)
|
||
LHS if operator.if_false(RHS)
|
||
|
||
Or when naming a binary comparison operation::
|
||
|
||
operator.if_is_sentinel(VALUE, SENTINEL) else EXPR
|
||
operator.if_is_not_sentinel(VALUE, SENTINEL) else EXPR
|
||
|
||
By contrast, omitting the preposition from the circuit breaker name gives a
|
||
result that reads reasonably well in all forms for unary checks::
|
||
|
||
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
|
||
|
||
And also reads well for binary checks::
|
||
|
||
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)
|
||
|
||
|
||
Risks and concerns
|
||
==================
|
||
|
||
This PEP has been designed specifically to address the risks and concerns
|
||
raised when discussing PEPs 335, 505 and 531.
|
||
|
||
* it defines new operators and adjusts the definition of chained comparison
|
||
(in a separate PEP) rather than impacting the existing ``and`` and ``or``
|
||
operators
|
||
* the proposed new operators are general purpose short-circuiting binary
|
||
operators that can even be used to express the existing semantics of ``and``
|
||
and ``or`` rather than focusing solely and inflexibly on identity checking
|
||
against ``None``
|
||
* the changes to the ``not`` unary operator and the ``is`` and ``is not``
|
||
binary comparison operators are defined in such a way that control flow
|
||
optimisations based on the existing semantics remain valid
|
||
|
||
One consequence of this approach is that this PEP *on its own* doesn't produce
|
||
much in the way of direct benefits to end users aside from making it possible
|
||
to omit some common ``None if `` prefixes and `` else None`` suffixes from
|
||
particular forms of conditional expression.
|
||
|
||
Instead, what it mainly provides is a common foundation that would allow the
|
||
None-aware operator proposals in PEP 505 and the rich comparison chaining
|
||
proposal in PEP 535 to be pursued atop a common underlying semantic framework
|
||
that would also be shared with conditional expressions and the existing ``and``
|
||
and ``or`` operators.
|
||
|
||
|
||
Design Discussion
|
||
=================
|
||
|
||
Protocol walk-through
|
||
---------------------
|
||
|
||
The following diagram illustrates the core concepts behind the circuit
|
||
breaking protocol (although it glosses over the technical detail of looking
|
||
up the special methods via the type rather than the instance):
|
||
|
||
.. image:: pep-0532/circuit-breaking-protocol.svg
|
||
:alt: diagram of circuit breaking protocol applied to ternary expression
|
||
|
||
We will work through the following expression::
|
||
|
||
>>> 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`` is a helper function that invokes the proposed
|
||
``operator.is_not_sentinel`` ``types.CircuitBreaker`` factory with ``None`` as
|
||
the sentinel value. ``data`` is a container (such as a builtin ``dict``
|
||
instance) that returns ``None`` when the ``get()`` method is called with an
|
||
unknown key.
|
||
|
||
We can rewrite the example to give a name to the circuit breaker instance::
|
||
|
||
>>> maybe_value = is_not_none(data.get("key"))
|
||
>>> x if maybe_value else y
|
||
|
||
Here the ``maybe_value`` circuit breaker instance corresponds to ``breaker``
|
||
in the diagram.
|
||
|
||
The ternary condition is evaluated by calling ``bool(maybe_value)``, which is
|
||
the same as Python's existing behavior. The change in behavior is that instead
|
||
of directly returning one of the operands ``x`` or ``y``, the circuit breaking
|
||
protocol passes the relevant operand to the circuit breaker used in the
|
||
condition.
|
||
|
||
If ``bool(maybe_value)`` evaluates to ``True`` (i.e. the requested
|
||
key exists and its value is not ``None``) then the interpreter calls
|
||
``type(maybe_value).__then__(maybe_value, x)``. Otherwise, it calls
|
||
``type(maybe_value).__else__(maybe_value, y)``.
|
||
|
||
The protocol also applies to the new ``if`` and ``else`` binary operators,
|
||
but in these cases, the interpreter needs a way to indicate the missing third
|
||
operand. It does this by re-using the circuit breaker itself in that role.
|
||
|
||
Consider these two expressions::
|
||
|
||
>>> x if data.get("key") is None
|
||
>>> x if operator.is_sentinel(data.get("key"), None)
|
||
|
||
The first form of this expression returns ``x`` if ``data.get("key") is None``,
|
||
but otherwise returns ``False``, which almost certainly isn't what we want.
|
||
|
||
By contrast, the second form of this expression still returns ``x`` if
|
||
``data.get("key") is None``, but otherwise returns ``data.get("key")``, which
|
||
is significantly more useful behaviour.
|
||
|
||
We can understand this behavior by rewriting it as a ternary expression with
|
||
an explicitly named circuit breaker instance::
|
||
|
||
>>> maybe_value = operator.is_sentinel(data.get("key"), None)
|
||
>>> x if maybe_value else maybe_value
|
||
|
||
If ``bool(maybe_value)`` is ``True`` (i.e. ``data.get("key")`` is ``None``),
|
||
then the interpreter calls ``type(maybe_value).__then__(maybe_value, x)``. The
|
||
implementation of ``types.CircuitBreaker.__then__`` doesn't see anything that
|
||
indicates short-circuiting has taken place, and hence returns ``x``.
|
||
|
||
By contrast, if ``bool(maybe_value)`` is ``False`` (i.e. ``data.get("key")``
|
||
is *not* ``None``), the interpreter calls
|
||
``type(maybe_value).__else__(maybe_value, maybe_value)``. The implementation of
|
||
``types.CircuitBreaker.__else__`` detects that the instance method has received
|
||
itself as its argument and returns the wrapped value (i.e. ``data.get("key")``)
|
||
rather than the circuit breaker.
|
||
|
||
The same logic applies to ``else``, only reversed::
|
||
|
||
>>> is_not_none(data.get("key")) else y
|
||
|
||
This expression returns ``data.get("key")`` if it is not ``None``, otherwise it
|
||
evaluates and returns ``y``. To understand the mechancics, we rewrite the
|
||
expression as follows::
|
||
|
||
>>> maybe_value = is_not_none(data.get("key"))
|
||
>>> maybe_value if maybe_value else y
|
||
|
||
If `bool(maybe_value)`` is ``True``, then the expression short-circuits and
|
||
the interpreter calls ``type(maybe_value).__else__(maybe_value, maybe_value)``.
|
||
The implementation of ``types.CircuitBreaker.__then__`` detects that the
|
||
instance method has received itself as its argument and returns the wrapped
|
||
value (i.e. ``data.get("key")``) rather than the circuit breaker.
|
||
|
||
If `bool(maybe_value)`` is ``True``, the interpreter calls
|
||
``type(maybe_value).__else__(maybe_value, y)``. The implementation of
|
||
``types.CircuitBreaker.__else__`` doesn't see anything that indicates
|
||
short-circuiting has taken place, and hence returns ``y``.
|
||
|
||
|
||
Respecting De Morgan's Laws
|
||
---------------------------
|
||
|
||
Similar to ``and`` and ``or``, the binary short-circuiting operators will
|
||
permit multiple ways of writing essentially the same expression. This
|
||
seeming redundancy is unfortunately an implied consequence of defining the
|
||
protocol as a full boolean algebra, as boolean algebras respect a pair of
|
||
properties known as "De Morgan's Laws": the ability to express the results
|
||
of ``and`` and ``or`` operations in terms of each other and a suitable
|
||
combination of ``not`` operations.
|
||
|
||
For ``and`` and ``or`` in Python, these invariants can be described as follows::
|
||
|
||
assert bool(A and B) == bool(not (not A or not B))
|
||
assert bool(A or B) == bool(not (not A and not B))
|
||
|
||
That is, if you take one of the operators, invert both operands, switch to the
|
||
other operator, and then invert the overall result, you'll get the same
|
||
answer (in a boolean sense) as you did from the original operator. (This may
|
||
seem redundant, but in many situations it actually lets you eliminate double
|
||
negatives and find tautologically true or false subexpressions, thus reducing
|
||
the overall expression size).
|
||
|
||
For circuit breakers, defining a suitable invariant is complicated by the
|
||
fact that they're often going to be designed to eliminate themselves from the
|
||
expression result when they're short-circuited, which is an inherently
|
||
asymmetric behaviour. Accordingly, that inherent asymmetry needs to be
|
||
accounted for when mapping De Morgan's Laws to the expected behaviour of
|
||
symmetric circuit breakers.
|
||
|
||
One way this complication can be addressed is to wrap the operand that would
|
||
otherwise short-circuit in ``operator.true``, ensuring that when ``bool`` is
|
||
applied to the overall result, it uses the same definition of truth that was
|
||
used to decide which branch to evaluate, rather than applying ``bool`` directly
|
||
to the circuit breaker's input value.
|
||
|
||
Specifically, for the new short-circuiting operators, the following properties
|
||
would be reasonably expected to hold for any well-behaved symmetric circuit
|
||
breaker that implements both ``__bool__`` and ``__not__``::
|
||
|
||
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)))
|
||
|
||
Note the order of operations on the right hand side (applying ``true``
|
||
*after* inverting the input circuit breaker) - this ensures that an
|
||
assertion is actually being made about ``type(A).__not__``, rather than
|
||
merely being about the behaviour of ``type(true(A)).__not__``.
|
||
|
||
At the very least, ``types.CircuitBreaker`` instances would respect this
|
||
logic, allowing existing boolean expression optimisations (like double
|
||
negative elimination) to continue to be applied.
|
||
|
||
|
||
Arbitrary sentinel objects
|
||
--------------------------
|
||
|
||
Unlike PEPs 505 and 531, the proposal in this PEP readily handles custom
|
||
sentinel objects::
|
||
|
||
_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
|
||
|
||
|
||
Implicitly defined circuit breakers in circuit breaking expressions
|
||
-------------------------------------------------------------------
|
||
|
||
A never-posted draft of this PEP explored the idea of special casing the
|
||
``is`` and ``is not`` binary operators such that they were automatically
|
||
treated as circuit breakers when used in the context of a circuit breaking
|
||
expression. Unfortunately, it turned out that this approach necessarily
|
||
resulted in one of two highly undesirable outcomes:
|
||
|
||
A. the return type of these expressions changed universally from ``bool`` to
|
||
``types.CircuitBreaker``, potentially creating a backwards compatibility
|
||
problem (especially when working with extension module APIs that
|
||
specifically look for a builtin boolean value with ``PyBool_Check`` rather
|
||
than passing the supplied value through ``PyObject_IsTrue`` or using
|
||
the ``p`` (predicate) format in one of the argument parsing functions)
|
||
B. the return type of these expressions became *context dependent*, meaning
|
||
that other routine refactorings (like pulling a comparison operation out
|
||
into a local variable) could have a significant impact on the runtime
|
||
semantics of a piece of code
|
||
|
||
Neither of those possible outcomes seems warranted by the proposal in this PEP,
|
||
so it reverted to the current design where circuit breaker instances must be
|
||
created explicitly via API calls, and are never produced implicitly.
|
||
|
||
|
||
Implementation
|
||
==============
|
||
|
||
As with PEP 505, actual implementation has been deferred pending in-principle
|
||
interest in the idea of making these changes.
|
||
|
||
...TBD...
|
||
|
||
|
||
Acknowledgements
|
||
================
|
||
|
||
Thanks go to Steven D'Aprano for his detailed critique [2_] of the initial
|
||
draft of this PEP that inspired many of the changes in the second draft, as
|
||
well as to all of the other participants in that discussion thread [3_]
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
.. [1] PEP 335 rejection notification
|
||
(https://mail.python.org/pipermail/python-dev/2012-March/117510.html)
|
||
|
||
.. [2] Steven D'Aprano's critique of the initial draft
|
||
(https://mail.python.org/pipermail/python-ideas/2016-November/043615.html)
|
||
|
||
.. [3] python-ideas thread discussing initial draft
|
||
(https://mail.python.org/pipermail/python-ideas/2016-November/043563.html)
|
||
|
||
Copyright
|
||
=========
|
||
|
||
This document has been placed in the public domain under the terms of the
|
||
CC0 1.0 license: https://creativecommons.org/publicdomain/zero/1.0/
|
||
|
||
|
||
..
|
||
Local Variables:
|
||
mode: indented-text
|
||
indent-tabs-mode: nil
|
||
sentence-end-double-space: t
|
||
fill-column: 70
|
||
coding: utf-8
|
||
End:
|