2016-10-30 07:38:30 -04:00
|
|
|
|
PEP: 532
|
2016-11-05 05:21:05 -04:00
|
|
|
|
Title: A circuit breaking operator and protocol
|
2016-10-30 07:38:30 -04:00
|
|
|
|
Version: $Revision$
|
|
|
|
|
Last-Modified: $Date$
|
|
|
|
|
Author: Nick Coghlan <ncoghlan@gmail.com>
|
|
|
|
|
Status: Draft
|
|
|
|
|
Type: Standards Track
|
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
|
Created: 30-Oct-2016
|
|
|
|
|
Python-Version: 3.7
|
2016-11-05 05:21:05 -04:00
|
|
|
|
Post-History: 5-Nov-2016
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
|
========
|
|
|
|
|
|
|
|
|
|
Inspired by PEP 335, PEP 505, PEP 531, and the related discussions, this PEP
|
2016-11-03 01:13:00 -04:00
|
|
|
|
proposes the addition of a new protocol-driven circuit breaking operator to
|
|
|
|
|
Python that allows the left operand to decide whether or not the expression
|
|
|
|
|
should short circuit and return a result immediately, or else continue
|
|
|
|
|
on with evaluation of the right operand::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
exists(foo) else bar
|
|
|
|
|
missing(foo) else foo.bar()
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
These two expressions can be read as:
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
* "the expression result is 'foo' if it exists, otherwise it is 'bar'"
|
|
|
|
|
* "the expression result is 'foo' if it is missing, otherwise it is 'foo.bar()'"
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Execution of these expressions relies on a new circuit breaking protocol that
|
|
|
|
|
implicitly avoids repeated evaluation of the left operand while letting
|
|
|
|
|
that operand fully control the result of the expression, regardless of whether
|
|
|
|
|
it skips evaluation of the right operand or not::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
_lhs = LHS
|
|
|
|
|
type(_lhs).__then__(_lhs) if _lhs else type(_lhs).__else__(_lhs, RHS)
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
To properly support logical negation of circuit breakers, a new ``__not__``
|
|
|
|
|
protocol method would also be introduced allowing objects to control
|
2016-11-02 11:34:50 -04:00
|
|
|
|
the result of ``not obj`` expressions.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
As shown in the basic example above, the PEP further proposes the addition of
|
|
|
|
|
builtin ``exists`` and ``missing`` circuit breakers that provide conditional
|
|
|
|
|
branching based on whether or not an object is ``None``, but return the
|
|
|
|
|
original object rather than the existence checking wrapper when the expression
|
|
|
|
|
evaluation short circuits.
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
In addition to being usable as simple boolean operators (e.g. as in
|
|
|
|
|
``assert all(exists, items)`` or ``if any(missing, items):``), these circuit
|
|
|
|
|
breakers will allow existence checking fallback operations (aka None-coalescing
|
|
|
|
|
operations) to be written as::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
value = exists(expr1) else exists(expr2) else expr3
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
and existence checking precondition operations (aka None-propagating
|
|
|
|
|
or None-severing operations) to be written as::
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
value = missing(obj) else obj.field.of.interest
|
|
|
|
|
value = missing(obj) else obj["field"]["of"]["interest"]
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
A change to the definition of chained comparisons is also proposed, where
|
|
|
|
|
the comparison chaining will be updated to use the circuit breaking operator
|
|
|
|
|
rather than the logical disjunction (``and``) operator if the left hand
|
|
|
|
|
comparison returns a circuit breaker as its result.
|
|
|
|
|
|
|
|
|
|
While there are some practical complexities arising from the current handling
|
|
|
|
|
of single-valued arrays in NumPy, this change should be sufficient to allow
|
|
|
|
|
elementwise chained comparison operations for matrices, where the result
|
|
|
|
|
is a matrix of boolean values, rather than tautologically returning ``True``
|
|
|
|
|
or raising ``ValueError``.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Relationship with other PEPs
|
|
|
|
|
============================
|
|
|
|
|
|
|
|
|
|
This PEP is a direct successor to PEP 531, replacing the existence checking
|
|
|
|
|
protocol and the new ``?then`` and ``?else`` syntactic operators defined there
|
2016-11-03 01:13:00 -04:00
|
|
|
|
with a single protocol driven ``else`` operator and adjustments to the ``not``
|
|
|
|
|
operator. The existence checking use cases are taken from that PEP.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
It is also a direct successor to PEP 335, which proposed the ability to
|
2016-11-03 01:13:00 -04:00
|
|
|
|
overload the ``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 this PEP to instead handle the element-wise
|
|
|
|
|
comparison use case by changing the semantic definition of comparison chaining
|
|
|
|
|
is drawn from Guido's rejection of PEP 335.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
This PEP competes with the dedicated null-coalescing operator in PEP 505,
|
|
|
|
|
proposing that improved support for null-coalescing operations be offered
|
|
|
|
|
through a more general protocol-driven short circuiting operator and related
|
|
|
|
|
builtins, rather than through a dedicated single-purpose operator.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
It doesn't compete with PEP 505's proposed shorthands for existence checking
|
|
|
|
|
attribute access and subscripting, but instead offers an alternative underlying
|
|
|
|
|
semantic framework for defining them:
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
* ``EXPR?.attr`` would be syntactic sugar for ``missing(EXPR) else EXPR.attr``
|
|
|
|
|
* ``EXPR?[key]`` would be syntactic sugar for ``missing(EXPR) else EXPR[key]``
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
In both cases, the dedicated syntactic form could be optimised to avoid
|
|
|
|
|
actually creating the circuit breaker instance.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Specification
|
|
|
|
|
=============
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
The circuit breaking operator (``else``)
|
|
|
|
|
----------------------------------------
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Circuit breaking expressions would be written using ``else`` as a new binary
|
|
|
|
|
operator, akin to the existing ``and`` and ``or`` logical operators::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
LHS else RHS
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Ignoring the hidden variable assignment, this is semantically equivalent to::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
_lhs = LHS
|
|
|
|
|
type(_lhs).__then__(_lhs) if _lhs else type(_lhs).__else__(_lhs, RHS)
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
The key difference relative to the existing ``or`` operator is that the value
|
|
|
|
|
determining which branch of the conditional expression gets executed *also*
|
|
|
|
|
gets a chance to postprocess the results of the expressions on each of the
|
|
|
|
|
branches.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
As part of the short-circuiting behaviour, interpreter implementations
|
|
|
|
|
are expected to access only the protocol method needed for the branch
|
|
|
|
|
that is actually executed, but it is still recommended that circuit
|
|
|
|
|
breaker authors that always return ``True`` or always return ``False`` from
|
|
|
|
|
``__bool__`` explicitly raise ``NotImplementedError`` with a suitable
|
|
|
|
|
message from branch methods that are never expected to be executed (see the
|
|
|
|
|
comparison chaining use case in the Rationale section below for an example
|
|
|
|
|
of that).
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-05 05:21:05 -04:00
|
|
|
|
It is proposed that the ``else`` operator use a new precedence level that binds
|
|
|
|
|
less tightly than the ``or`` operator by adjusting the relevant line in
|
|
|
|
|
Python's grammar from the current::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-05 05:21:05 -04:00
|
|
|
|
test: or_test ['if' or_test 'else' test] | lambdef
|
|
|
|
|
|
|
|
|
|
to instead be::
|
|
|
|
|
|
|
|
|
|
test: else_test ['if' or_test 'else' test] | lambdef
|
|
|
|
|
else_test: or_test ['else' test]
|
|
|
|
|
|
|
|
|
|
The definition of ``test_nocond`` would remain unchanged, so circuit
|
|
|
|
|
breaking expressions 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 epxr3`` resolves as
|
|
|
|
|
``(expr1 if cond else expr2) else epxr3``.
|
|
|
|
|
|
|
|
|
|
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.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Overloading logical inversion (``not``)
|
|
|
|
|
---------------------------------------
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Any circuit breaker definition will have a logical inverse that is still a
|
|
|
|
|
circuit breaker, but inverts the answer as to whether or not to short circuit
|
|
|
|
|
the expression evaluation. For example, the ``exists`` and ``missing`` circuit
|
|
|
|
|
breakers proposed in this PEP are each other's logical inverse.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
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.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
To preserve the semantics of existing language optimisations, ``__not__``
|
|
|
|
|
implementations will be obliged to respect the following invariant::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
assert not bool(obj) == bool(not obj)
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Chained comparisons
|
|
|
|
|
-------------------
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
A chained comparison like ``0 < x < 10`` written as::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
LEFT_BOUND left_op EXPR right_op RIGHT_BOUND
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
is currently roughly semantically equivalent to::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
_expr = EXPR
|
2016-11-03 01:13:00 -04:00
|
|
|
|
_lhs_result = LEFT_BOUND left_op _expr
|
|
|
|
|
_expr_result = _lhs_result and (_expr right_op RIGHT_BOUND)
|
|
|
|
|
|
|
|
|
|
This PEP proposes that this be changed to explicitly check if the left
|
|
|
|
|
comparison returns a circuit breaker, and if so, use ``else`` rather than
|
|
|
|
|
``and`` to implement the comparison chaining::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
_expr = EXPR
|
|
|
|
|
_lhs_result = LEFT_BOUND left_op _expr
|
|
|
|
|
if hasattr(type(_lhs_result), "__then__"):
|
|
|
|
|
_expr_result = _lhs_result else (_expr right_op RIGHT_BOUND)
|
|
|
|
|
else:
|
|
|
|
|
_expr_result = _lhs_result and (_expr right_op RIGHT_BOUND)
|
|
|
|
|
|
|
|
|
|
This allows types like NumPy arrays to control the behaviour of chained
|
|
|
|
|
comparisons by returning circuit breakers from comparison operations.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Existence checking comparisons
|
|
|
|
|
------------------------------
|
|
|
|
|
|
2016-11-02 11:34:50 -04:00
|
|
|
|
Two new builtins implementing the new protocol are proposed to encapsulate the
|
2016-10-30 07:38:30 -04:00
|
|
|
|
notion of "existence 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").
|
|
|
|
|
|
2016-11-02 11:34:50 -04:00
|
|
|
|
These builtins would be defined as follows::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
class CircuitBreaker:
|
|
|
|
|
"""Base circuit breaker type (available as types.CircuitBreaker)"""
|
|
|
|
|
def __init__(self, value, condition, inverse_type):
|
2016-10-30 07:38:30 -04:00
|
|
|
|
self.value = value
|
2016-11-03 01:13:00 -04:00
|
|
|
|
self._condition = condition
|
|
|
|
|
self._inverse_type = inverse_type
|
2016-10-30 07:38:30 -04:00
|
|
|
|
def __bool__(self):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
return self._condition
|
2016-11-02 11:34:50 -04:00
|
|
|
|
def __not__(self):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
return self._inverse_type(self.value)
|
|
|
|
|
def __then__(self):
|
|
|
|
|
return self.value
|
|
|
|
|
def __else__(self, other):
|
|
|
|
|
if other is self:
|
|
|
|
|
return self.value
|
|
|
|
|
return other
|
|
|
|
|
|
|
|
|
|
class exists(types.CircuitBreaker):
|
|
|
|
|
"""Circuit breaker for 'EXPR is not None' checks"""
|
|
|
|
|
def __init__(self, value):
|
|
|
|
|
super().__init__(value, value is not None, missing)
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
class missing(types.CircuitBreaker):
|
|
|
|
|
"""Circuit breaker for 'EXPR is None' checks"""
|
|
|
|
|
def __init__(self, value):
|
|
|
|
|
super().__init__(value, value is None, exists)
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
Aside from changing the definition of ``__bool__`` to be based on
|
|
|
|
|
``is not None`` rather than normal truth checking, the key characteristic of
|
2016-11-03 01:13:00 -04:00
|
|
|
|
``exists`` is that when it is used as a circuit breaker, it is *ephemeral*:
|
|
|
|
|
when it is told that short circuiting has taken place, it returns the original
|
|
|
|
|
value, rather than the existence checking wrapper.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-02 11:34:50 -04:00
|
|
|
|
``missing`` is defined as the logically inverted counterpart of ``exists``:
|
|
|
|
|
``not exists(obj)`` is semantically equivalent to ``missing(obj)``.
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
The ``__else__`` implementations for both builtin circuit breakers are defined
|
|
|
|
|
such that the wrapper will always be removed even if you explicitly pass the
|
|
|
|
|
circuit breaker to both sides of the ``else`` expression::
|
|
|
|
|
|
|
|
|
|
breaker = exists(foo)
|
|
|
|
|
assert (breaker else breaker) is foo
|
|
|
|
|
breaker = missing(foo)
|
|
|
|
|
assert (breaker else breaker) is foo
|
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
Other conditional constructs
|
|
|
|
|
----------------------------
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
No changes are proposed to if statements, while statements, conditional
|
|
|
|
|
expressions, comprehensions, or generator expressions, as the boolean clauses
|
|
|
|
|
they contain are already used for control flow purposes.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-02 11:34:50 -04:00
|
|
|
|
However, it's worth noting that while such proposals are outside the scope of
|
2016-11-03 01:13:00 -04:00
|
|
|
|
this PEP, the circuit breaking protocol defined here would be sufficient to
|
|
|
|
|
support constructs like::
|
2016-11-02 11:34:50 -04:00
|
|
|
|
|
|
|
|
|
while exists(dynamic_query()) as result:
|
|
|
|
|
... # Code using result
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
and:
|
|
|
|
|
|
|
|
|
|
if exists(re.search(pattern, text)) as match:
|
|
|
|
|
... # Code using match
|
|
|
|
|
|
|
|
|
|
Leaving the door open to such a future extension is the main reason for
|
|
|
|
|
recommending that circuit breaker implementations handle the ``self is other``
|
|
|
|
|
case in ``__else__`` implementations the same way as they handle the
|
|
|
|
|
short-circuiting behaviour in ``__then__``.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Style guide recommendations
|
|
|
|
|
---------------------------
|
|
|
|
|
|
|
|
|
|
The following additions to PEP 8 are proposed in relation to the new features
|
|
|
|
|
introduced by this PEP:
|
|
|
|
|
|
|
|
|
|
* In the absence of other considerations, prefer the use of the builtin
|
|
|
|
|
circuit breakers ``exists`` and ``missing`` over the corresponding
|
|
|
|
|
conditional expressions
|
|
|
|
|
|
|
|
|
|
* Do not combine conditional expressions (``if-else``) and circuit breaking
|
|
|
|
|
expressions (the ``else`` operator) in a single expression - use one or the
|
|
|
|
|
other depending on the situation, but not both.
|
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
Rationale
|
|
|
|
|
=========
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Adding a new operator
|
|
|
|
|
---------------------
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
reasons:
|
|
|
|
|
|
|
|
|
|
* defining a shared protocol for both ``and`` and ``or`` was confusing, as
|
|
|
|
|
``__then__`` was the short-circuiting outcome for ``or``, while ``__else__``
|
2016-11-05 05:49:37 -04:00
|
|
|
|
was the short-circuiting outcome for ``and``
|
2016-11-03 01:13:00 -04:00
|
|
|
|
* 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
|
|
|
|
|
|
|
|
|
|
Proposing a single new operator instead resolves all of those issues -
|
|
|
|
|
``__then__`` always indicates short circuiting, ``__else__`` only indicates
|
|
|
|
|
"short circuiting" if the circuit breaker itself is also passed in as the
|
|
|
|
|
right operand, and the 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.
|
|
|
|
|
|
|
|
|
|
As a result of that design simplification, the new protocol and operator would
|
2016-11-05 05:21:05 -04:00
|
|
|
|
even allow us to expose ``operator.true`` and ``operator.false``
|
2016-11-03 01:13:00 -04:00
|
|
|
|
as circuit breaker definitions if we chose to do so::
|
|
|
|
|
|
2016-11-05 05:21:05 -04:00
|
|
|
|
class true(types.CircuitBreaker):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
"""Circuit breaker for 'bool(EXPR)' checks"""
|
|
|
|
|
def __init__(self, value):
|
2016-11-05 05:49:37 -04:00
|
|
|
|
super().__init__(value, bool(value), false)
|
2016-11-03 01:13:00 -04:00
|
|
|
|
|
2016-11-05 05:21:05 -04:00
|
|
|
|
class false(types.CircuitBreaker):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
"""Circuit breaker for 'not bool(EXPR)' checks"""
|
|
|
|
|
def __init__(self, value):
|
2016-11-05 05:49:37 -04:00
|
|
|
|
super().__init__(value, not bool(value), true)
|
2016-11-05 05:21:05 -04:00
|
|
|
|
|
|
|
|
|
Given those circuit breakers:
|
|
|
|
|
|
|
|
|
|
* ``LHS or RHS`` would be roughly ``operator.true(LHS) else RHS``
|
2016-11-05 05:49:37 -04:00
|
|
|
|
* ``LHS and RHS`` would be roughly ``operator.false(LHS) else RHS``
|
2016-11-03 01:13:00 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Using an existing keyword
|
|
|
|
|
-------------------------
|
|
|
|
|
|
|
|
|
|
Using an existing keyword has the benefit of allowing the new expression to
|
|
|
|
|
be introduced without a ``__future__`` statement.
|
|
|
|
|
|
|
|
|
|
``else`` is semantically appropriate for the proposed new protocol, and the
|
|
|
|
|
only syntactic ambiguity introduced arises when the new operator is combined
|
|
|
|
|
with the explicit ``if-else`` conditional expression syntax.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Element-wise chained comparisons
|
|
|
|
|
--------------------------------
|
|
|
|
|
|
|
|
|
|
In ultimately rejecting PEP 335, Guido van Rossum noted [1_]:
|
|
|
|
|
|
|
|
|
|
The NumPy folks brought up a somewhat separate issue: for them,
|
|
|
|
|
the most common use case is chained comparisons (e.g. A < B < C).
|
|
|
|
|
|
2016-11-05 05:49:37 -04:00
|
|
|
|
To understand this observation, we first need to look at how comparisons work
|
2016-10-30 07:38:30 -04:00
|
|
|
|
with NumPy arrays::
|
|
|
|
|
|
|
|
|
|
>>> import numpy as np
|
|
|
|
|
>>> increasing = np.arange(5)
|
|
|
|
|
>>> increasing
|
|
|
|
|
array([0, 1, 2, 3, 4])
|
|
|
|
|
>>> decreasing = np.arange(4, -1, -1)
|
|
|
|
|
>>> decreasing
|
|
|
|
|
array([4, 3, 2, 1, 0])
|
|
|
|
|
>>> increasing < decreasing
|
|
|
|
|
array([ True, True, False, False, False], dtype=bool)
|
|
|
|
|
|
|
|
|
|
Here we see that NumPy array comparisons are element-wise by default, comparing
|
2016-11-05 05:49:37 -04:00
|
|
|
|
each element in the left hand array to the corresponding element in the right
|
|
|
|
|
hand array, and producing a matrix of boolean results.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
If either side of the comparison is a scalar value, then it is broadcast across
|
|
|
|
|
the array and compared to each individual element::
|
|
|
|
|
|
|
|
|
|
>>> 0 < increasing
|
|
|
|
|
array([False, True, True, True, True], dtype=bool)
|
|
|
|
|
>>> increasing < 4
|
|
|
|
|
array([ True, True, True, True, False], dtype=bool)
|
|
|
|
|
|
|
|
|
|
However, this broadcasting idiom breaks down if we attempt to use chained
|
|
|
|
|
comparisons::
|
|
|
|
|
|
|
|
|
|
>>> 0 < increasing < 4
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
|
|
|
|
|
|
|
|
|
|
The problem is that internally, Python implicitly expands this chained
|
|
|
|
|
comparison into the form::
|
|
|
|
|
|
|
|
|
|
>>> 0 < increasing and increasing < 4
|
|
|
|
|
Traceback (most recent call last):
|
|
|
|
|
File "<stdin>", line 1, in <module>
|
|
|
|
|
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
|
|
|
|
|
|
|
|
|
|
And NumPy only permits implicit coercion to a boolean value for single-element
|
|
|
|
|
arrays where ``a.any()`` and ``a.all()`` can be assured of having the same
|
|
|
|
|
result::
|
|
|
|
|
|
|
|
|
|
>>> np.array([False]) and np.array([False])
|
|
|
|
|
array([False], dtype=bool)
|
|
|
|
|
>>> np.array([False]) and np.array([True])
|
|
|
|
|
array([False], dtype=bool)
|
|
|
|
|
>>> np.array([True]) and np.array([False])
|
|
|
|
|
array([False], dtype=bool)
|
|
|
|
|
>>> np.array([True]) and np.array([True])
|
|
|
|
|
array([ True], dtype=bool)
|
|
|
|
|
|
|
|
|
|
The proposal in this PEP would allow this situation to be changed by updating
|
|
|
|
|
the definition of element-wise comparison operations in NumPy to return a
|
2016-11-03 01:13:00 -04:00
|
|
|
|
dedicated subclass that implements the new circuit breaking protocol and also
|
2016-10-30 07:38:30 -04:00
|
|
|
|
changes the result array's interpretation in a boolean context to always
|
2016-11-03 01:13:00 -04:00
|
|
|
|
return ``False`` and hence never trigger the short-circuiting behaviour::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
class ComparisonResultArray(np.ndarray):
|
|
|
|
|
def __bool__(self):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
return False
|
|
|
|
|
def _raise_NotImplementedError(self):
|
|
|
|
|
msg = ("Comparison array truth values are ambiguous outside "
|
|
|
|
|
"chained comparisons. Use a.any() or a.all()")
|
|
|
|
|
raise NotImplementedError(msg)
|
|
|
|
|
def __not__(self):
|
|
|
|
|
self._raise_NotImplementedError()
|
|
|
|
|
def __then__(self):
|
|
|
|
|
self._raise_NotImplementedError()
|
|
|
|
|
def __else__(self, other):
|
|
|
|
|
return np.logical_and(self, other.view(ComparisonResultArray))
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
With this change, the chained comparison example above would be able to return::
|
|
|
|
|
|
|
|
|
|
>>> 0 < increasing < 4
|
|
|
|
|
ComparisonResultArray([ False, True, True, True, False], dtype=bool)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Existence checking expressions
|
|
|
|
|
------------------------------
|
|
|
|
|
|
|
|
|
|
An increasingly common requirement in modern software development is the need
|
|
|
|
|
to work with "semi-structured data": data where the structure of the data is
|
|
|
|
|
known in advance, but pieces of it may be missing at runtime, and the software
|
|
|
|
|
manipulating that data is expected to degrade gracefully (e.g. by omitting
|
|
|
|
|
results that depend on the missing data) rather than failing outright.
|
|
|
|
|
|
|
|
|
|
Some particularly common cases where this issue arises are:
|
|
|
|
|
|
|
|
|
|
* handling optional application configuration settings and function parameters
|
|
|
|
|
* handling external service failures in distributed systems
|
|
|
|
|
* handling data sets that include some partial records
|
|
|
|
|
|
|
|
|
|
At the moment, writing such software in Python can be genuinely awkward, as
|
|
|
|
|
your code ends up littered with expressions like:
|
|
|
|
|
|
|
|
|
|
* ``value1 = expr1.field.of.interest if expr1 is not None else None``
|
|
|
|
|
* ``value2 = expr2["field"]["of"]["interest"] if expr2 is not None else None``
|
|
|
|
|
* ``value3 = expr3 if expr3 is not None else expr4 if expr4 is not None else expr5``
|
|
|
|
|
|
|
|
|
|
PEP 531 goes into more detail on some of the challenges of working with this
|
|
|
|
|
kind of data, particularly in data transformation pipelines where dealing with
|
|
|
|
|
potentially missing content is the norm rather than the exception.
|
|
|
|
|
|
|
|
|
|
The combined impact of the proposals in this PEP is to allow the above sample
|
|
|
|
|
expressions to instead be written as:
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
* ``value1 = missing(expr1) else expr1.field.of.interest``
|
|
|
|
|
* ``value2 = missing(expr2) else expr2.["field"]["of"]["interest"]``
|
|
|
|
|
* ``value3 = exists(expr3) else exists(expr4) else expr5``
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
In these forms, significantly more of the text presented to the reader is
|
|
|
|
|
immediately relevant to the question "What does this code do?", while the
|
|
|
|
|
boilerplate code to handle missing data by passing it through to the output
|
2016-11-03 01:13:00 -04:00
|
|
|
|
or falling back to an alternative input, has shrunk to two uses of the new
|
|
|
|
|
``missing`` builtin, and two uses of the new ``exists`` builtin.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
In the first two examples, the 31 character boilerplate suffix
|
|
|
|
|
``if exprN is not None else None`` (minimally 27 characters for a single letter
|
2016-11-03 01:13:00 -04:00
|
|
|
|
variable name) has been replaced by a 19 character ``missing(expr1) else``
|
|
|
|
|
prefix (minimally 15 characters with a single letter variable name), markedly
|
2016-10-30 07:38:30 -04:00
|
|
|
|
improving the signal-to-pattern-noise ratio of the lines (especially if it
|
|
|
|
|
encourages the use of more meaningful variable and field names rather than
|
2016-11-03 01:13:00 -04:00
|
|
|
|
making them shorter purely for the sake of expression brevity). The additional
|
|
|
|
|
syntactic sugar proposals in PEP 505 would further reduce this boilerplate to
|
|
|
|
|
a single ``?`` character that also eliminated the repetition of the expession
|
|
|
|
|
being checked for existence.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
In the last example, not only are two instances of the 21 character boilerplate,
|
|
|
|
|
`` if exprN is not None`` (minimally 17 characters) replaced with the
|
|
|
|
|
8 character function call ``exists()``, but that function call is placed
|
|
|
|
|
directly around the original expression, eliminating the need to duplicate it
|
|
|
|
|
in the conditional existence check.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Risks and concerns
|
|
|
|
|
==================
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
This PEP has been designed specifically to address the risks and concerns
|
|
|
|
|
raised when discussing PEPs 335, 505 and 531.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
* it defines a new operator and adjusts the definition of chained comparison
|
|
|
|
|
rather than impacting the existing ``and`` and ``or`` operators
|
|
|
|
|
* the changes to the ``not`` unary operator are defined in such a way that
|
|
|
|
|
control flow optimisations based on the existing semantics remain valid
|
|
|
|
|
* rather than the cryptic ``??``, it uses ``else`` as the operator keyword in
|
|
|
|
|
exactly the same sense as it is already used in conditional expressions
|
|
|
|
|
* it defines a general purpose short-circuiting binary operator that can even
|
|
|
|
|
be used to express the existing semantics of ``and`` and ``or`` rather than
|
|
|
|
|
focusing solely and inflexibly on existence checking
|
|
|
|
|
* it names the proposed builtins in such a way that they provide a strong
|
|
|
|
|
mnemonic hint as to when the expression containing them will short-circuit
|
|
|
|
|
and skip evaluating the right operand
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
2016-11-05 05:21:05 -04:00
|
|
|
|
Possible confusion with conditional expressions
|
|
|
|
|
-----------------------------------------------
|
|
|
|
|
|
|
|
|
|
The proposal in this PEP is essentially for an "implied ``if``" where if you
|
|
|
|
|
omit the ``if`` clause from a conditional expression, you invoke the circuit
|
|
|
|
|
breaking protocol instead. That is::
|
|
|
|
|
|
|
|
|
|
exists(foo) else calculate_default()
|
|
|
|
|
|
|
|
|
|
invokes the new protocol, but::
|
|
|
|
|
|
|
|
|
|
foo.field.of.interest if exists(foo) else calculate_default()
|
|
|
|
|
|
|
|
|
|
bypasses it entirely, *including* the non-short-circuiting ``__else__`` method.
|
|
|
|
|
|
|
|
|
|
This mostly wouldn't be a problem for the proposed ``types.CircuitBreaker``
|
|
|
|
|
implementation (and hence the ``exists`` and ``missing`` builtins), as the
|
|
|
|
|
only purpose the extended protocol serves in that case is to remove the
|
|
|
|
|
wrapper in the short-circuiting case - the ``__else__`` method passes the
|
|
|
|
|
right operand through unchanged.
|
|
|
|
|
|
|
|
|
|
However, this discrepancy could potentially be eliminated entirely by also
|
|
|
|
|
updating conditional expressions to use the circuit breaking protocol if
|
|
|
|
|
the condition defines those methods. In that case, ``__then__`` would need
|
|
|
|
|
to be updated to accept the left operand as a parameter, with short-circuiting
|
|
|
|
|
indicated by passing in the circuit breaker itself::
|
|
|
|
|
|
|
|
|
|
class CircuitBreaker:
|
|
|
|
|
"""Base circuit breaker type (available as types.CircuitBreaker)"""
|
|
|
|
|
def __init__(self, value, condition, inverse_type):
|
|
|
|
|
self.value = value
|
|
|
|
|
self._condition = condition
|
|
|
|
|
self._inverse_type = inverse_type
|
|
|
|
|
def __bool__(self):
|
|
|
|
|
return self._condition
|
|
|
|
|
def __not__(self):
|
|
|
|
|
return self._inverse_type(self.value)
|
|
|
|
|
def __then__(self, other):
|
|
|
|
|
if other is not self:
|
|
|
|
|
return other
|
|
|
|
|
return self.value # Short-circuit, remove the wrapper
|
|
|
|
|
def __else__(self, other):
|
|
|
|
|
if other is not self:
|
|
|
|
|
return other
|
|
|
|
|
return self.value # Short-circuit, remove the wrapper
|
|
|
|
|
|
|
|
|
|
With this symmetric protocol, the definition of conditional expressions
|
|
|
|
|
could be updated to also make the ``else`` clause optional::
|
|
|
|
|
|
|
|
|
|
test: else_test ['if' or_test ['else' test]] | lambdef
|
|
|
|
|
else_test: or_test ['else' test]
|
|
|
|
|
|
|
|
|
|
(We would avoid the apparent simplification to ``else_test ('if' else_test)*``
|
|
|
|
|
in order to make it easier to correctly preserve the semantics of normal
|
|
|
|
|
conditional expressions)
|
|
|
|
|
|
|
|
|
|
Given that expanded definition, the following statements would be
|
|
|
|
|
functionally equivalent::
|
|
|
|
|
|
|
|
|
|
foo = calculate_default() if missing(foo)
|
|
|
|
|
foo = calculate_default() if foo is None else foo
|
|
|
|
|
|
|
|
|
|
Just as the base proposal already makes the following equivalent::
|
|
|
|
|
|
|
|
|
|
foo = exists(foo) else calculate_default()
|
|
|
|
|
foo = foo if foo is not None else calculate_default()
|
|
|
|
|
|
|
|
|
|
The ``if`` based circuit breaker form has the virtue of reading significantly
|
|
|
|
|
better when used for conditional imperative commands like debug messages::
|
|
|
|
|
|
|
|
|
|
print(some_expensive_query()) if verbosity > 2
|
|
|
|
|
|
|
|
|
|
If we went down this path, then ``operator.true`` would need to be declared
|
|
|
|
|
as the nominal implicit circuit breaker when the condition didn't define the
|
|
|
|
|
circuit breaker protocol itself (so the above example would produce ``None``
|
|
|
|
|
if the debugging message was printed, and ``False`` otherwise)
|
|
|
|
|
|
|
|
|
|
The main objection to this expansion of the proposal is that it makes it a
|
|
|
|
|
more intrusive change that may potentially affect the behaviour of existing
|
|
|
|
|
code, while the main point in its favour is that allowing both ``if`` and
|
|
|
|
|
``else`` as circuit breaking operators and also supporting the circuit breaking
|
|
|
|
|
protocol for normal conditional expressions would be significantly more
|
|
|
|
|
self-consistent than special-casing a bare ``else`` as a stand-alone operator.
|
|
|
|
|
|
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
Design Discussion
|
|
|
|
|
=================
|
|
|
|
|
|
|
|
|
|
Arbitrary sentinel objects
|
|
|
|
|
--------------------------
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Unlike PEPs 505 and 531, this proposal readily handles custom sentinel objects::
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
class defined(types.CircuitBreaker):
|
|
|
|
|
MISSING = object()
|
2016-10-30 07:38:30 -04:00
|
|
|
|
def __init__(self, value):
|
2016-11-03 02:00:50 -04:00
|
|
|
|
super().__init__(self, value is not self.MISSING, undefined)
|
2016-11-03 01:13:00 -04:00
|
|
|
|
|
2016-11-03 02:00:50 -04:00
|
|
|
|
class undefined(types.CircuitBreaker):
|
2016-11-03 01:13:00 -04:00
|
|
|
|
def __init__(self, value):
|
|
|
|
|
super().__init__(self, value is defined.MISSING, defined)
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
# Using the sentinel to check whether or not an argument was supplied
|
2016-11-03 01:13:00 -04:00
|
|
|
|
def my_func(arg=defined.MISSING):
|
|
|
|
|
arg = defined(arg) else calculate_default()
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Implementation
|
|
|
|
|
==============
|
|
|
|
|
|
|
|
|
|
As with PEP 505, actual implementation has been deferred pending in-principle
|
2016-11-03 01:13:00 -04:00
|
|
|
|
interest in the idea of making these changes - aside from the possible
|
2016-11-05 05:21:05 -04:00
|
|
|
|
syntactic ambiguity concerns covered by the grammer proposals above, the
|
|
|
|
|
implementation isn't really the hard part of these proposals, the hard part
|
|
|
|
|
is deciding whether or not this is a change where the long term benefits for
|
|
|
|
|
new and existing Python users outweigh the short term costs involved in the
|
|
|
|
|
wider ecosystem (including developers of other implementations, language
|
|
|
|
|
curriculum developers, and authors of other Python related educational
|
|
|
|
|
material) adjusting to the change.
|
2016-10-30 07:38:30 -04:00
|
|
|
|
|
|
|
|
|
...TBD...
|
|
|
|
|
|
|
|
|
|
|
2016-11-03 01:13:00 -04:00
|
|
|
|
Acknowledgements
|
|
|
|
|
================
|
|
|
|
|
|
|
|
|
|
Thanks go to Mark E. Haase for feedback on and contributions to earlier drafts
|
|
|
|
|
of this proposal. However, his clear and exhaustive explanation of the original
|
|
|
|
|
protocol design that modified the semantics of ``if-else`` conditional
|
|
|
|
|
expressions to use an underlying ``__then__``/``__else__`` protocol helped
|
|
|
|
|
convince me it was too complicated to keep, so this iteration contains neither
|
|
|
|
|
that version of the protocol, nor Mark's explanation of it.
|
|
|
|
|
|
|
|
|
|
|
2016-10-30 07:38:30 -04:00
|
|
|
|
References
|
|
|
|
|
==========
|
|
|
|
|
|
|
|
|
|
.. [1] PEP 335 rejection notification
|
|
|
|
|
(http://mail.python.org/pipermail/python-dev/2012-March/117510.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:
|