Change PEP 532 to propose a new operator
- if-else, and, or are now left alone - else is introduced as a new short-circuiting binary operator - "circuit breaking" is introduced as the operator and protocol name - exists() builtin checks for existence - missing() builtin checks for non-existence - "not exists" gives a missing instance and vice-versa - chained comparison is adjusted to use the new protocol when available
This commit is contained in:
parent
38371b4ece
commit
8f095cf8c0
629
pep-0532.txt
629
pep-0532.txt
|
@ -1,5 +1,5 @@
|
|||
PEP: 532
|
||||
Title: Defining a conditional result management protocol
|
||||
Title: Defining "else" as a protocol based circuit breaking operator
|
||||
Version: $Revision$
|
||||
Last-Modified: $Date$
|
||||
Author: Nick Coghlan <ncoghlan@gmail.com>
|
||||
|
@ -13,60 +13,60 @@ Abstract
|
|||
========
|
||||
|
||||
Inspired by PEP 335, PEP 505, PEP 531, and the related discussions, this PEP
|
||||
proposes the addition of a new conditional result management protocol to Python
|
||||
that allows objects to customise the behaviour of the following expressions:
|
||||
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::
|
||||
|
||||
* ``if-else`` conditional expressions
|
||||
* the ``and`` logical conjunction operator
|
||||
* the ``or`` logical disjunction operator
|
||||
* chained comparisons (which implicitly invoke ``and``)
|
||||
* the ``not`` logical negation operator
|
||||
exists(foo) else bar
|
||||
missing(foo) else foo.bar()
|
||||
|
||||
Each of these expressions is ultimately a variant on the underlying pattern::
|
||||
These two expressions can be read as:
|
||||
|
||||
THEN_RESULT if CONDITION else ELSE_RESULT
|
||||
* "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()'"
|
||||
|
||||
Currently, the ``CONDITION`` expression can control *which* branch is taken
|
||||
(based on whether it evaluates to ``True`` or ``False`` in a boolean context),
|
||||
but it can't influence the *result* of taking that branch.
|
||||
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::
|
||||
|
||||
This PEP proposes the addition of two new "conditional result management"
|
||||
protocol methods that allow conditional result managers to influence the
|
||||
results of each branch directly:
|
||||
_lhs = LHS
|
||||
type(_lhs).__then__(_lhs) if _lhs else type(_lhs).__else__(_lhs, RHS)
|
||||
|
||||
* ``__then__(self, result)``, to alter the result when the condition is ``True``
|
||||
* ``__else__(self, result)``, to alter the result when the condition is ``False``
|
||||
|
||||
While there are some practical complexities arising from the current handling
|
||||
of single-valued arrays in NumPy, this 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``.
|
||||
|
||||
To properly support logical negation of conditional result managers, a new
|
||||
``__not__`` protocol methro would also be introduced allowing objects to control
|
||||
To properly support logical negation of circuit breakers, a new ``__not__``
|
||||
protocol method would also be introduced allowing objects to control
|
||||
the result of ``not obj`` expressions.
|
||||
|
||||
The PEP further proposes the addition of new ``exists`` and ``missing`` builtins
|
||||
that allow conditional branching based on whether or not an object is ``None``,
|
||||
but return the original object rather than the existence checking wrapper as
|
||||
the result of any conditional expressions. In addition to being usable as
|
||||
a simple boolean operator (e.g. as in ``assert all(exists, items)``), this
|
||||
allows existence checking fallback operations (aka null-coalescing operations)
|
||||
to be written as::
|
||||
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.
|
||||
|
||||
value = exists(expr1) or exists(expr2) or expr3
|
||||
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::
|
||||
|
||||
and existence checking precondition operations (aka null-propagating
|
||||
or null-severing operations) can be written as either::
|
||||
value = exists(expr1) else exists(expr2) else expr3
|
||||
|
||||
value = exists(obj) and obj.field.of.interest
|
||||
value = exists(obj) and obj["field"]["of"]["interest"]
|
||||
and existence checking precondition operations (aka None-propagating
|
||||
or None-severing operations) to be written as::
|
||||
|
||||
or::
|
||||
value = missing(obj) else obj.field.of.interest
|
||||
value = missing(obj) else obj["field"]["of"]["interest"]
|
||||
|
||||
value = missing(obj) or obj.field.of.interest
|
||||
value = missing(obj) or obj["field"]["of"]["interest"]
|
||||
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``.
|
||||
|
||||
|
||||
Relationship with other PEPs
|
||||
|
@ -74,127 +74,116 @@ 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
|
||||
with the ability to customise the behaviour of the established ``not``,
|
||||
``and`` and ``or`` operators. The existence checking use case is taken from
|
||||
that PEP.
|
||||
with a single protocol driven ``else`` operator and adjustments to the ``not``
|
||||
operator. The existence checking use cases are taken from that PEP.
|
||||
|
||||
It is also a direct successor to PEP 335, which proposed the ability to
|
||||
overload the ``and`` and ``or`` operators directly, rather than indirectly
|
||||
via interpretation as variants of the more general ``if-else`` conditional
|
||||
expressions. The discussion of the element-wise comparison use case is
|
||||
drawn from Guido's rejection of that PEP.
|
||||
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.
|
||||
|
||||
This PEP competes with a number of aspects of PEP 505, proposing that improved
|
||||
support for null-coalescing operations be offered through a new protocol and
|
||||
new builtin, rather than through new syntax. It doesn't compete specifically
|
||||
with the proposed shorthands for existence checking attribute access and
|
||||
subscripting, but instead offers an alternative underlying semantic framework
|
||||
for defining them:
|
||||
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.
|
||||
|
||||
* ``LHS ?? RHS`` would mean ``exists(LHS) or RHS``
|
||||
* ``EXPR?.attr`` would mean ``missing(EXPR) or EXPR.attr``
|
||||
* ``EXPR?[key]`` would mean ``missing(EXPR) or EXPR[key]``
|
||||
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:
|
||||
|
||||
* ``EXPR?.attr`` would be syntactic sugar for ``missing(EXPR) else EXPR.attr``
|
||||
* ``EXPR?[key]`` would be syntactic sugar for ``missing(EXPR) else EXPR[key]``
|
||||
|
||||
In both cases, the dedicated syntactic form could be optimised to avoid
|
||||
actually creating the circuit breaker instance.
|
||||
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
Conditional expressions (``if-else``)
|
||||
-------------------------------------
|
||||
The circuit breaking operator (``else``)
|
||||
----------------------------------------
|
||||
|
||||
The conditional expression ``THEN_RESULT if CONDITION else ELSE_RESULT`` is
|
||||
currently approximately equivalent to the following code::
|
||||
Circuit breaking expressions would be written using ``else`` as a new binary
|
||||
operator, akin to the existing ``and`` and ``or`` logical operators::
|
||||
|
||||
if CONDITION:
|
||||
_expr_result = THEN_RESULT
|
||||
else:
|
||||
_expr_result = ELSE_RESULT
|
||||
LHS else RHS
|
||||
|
||||
The new protocol proposed in this PEP would change that to::
|
||||
Ignoring the hidden variable assignment, this is semantically equivalent to::
|
||||
|
||||
_condition = CONDITION
|
||||
_condition_type = type(CONDITION)
|
||||
if _condition:
|
||||
_then_result = THEN_RESULT
|
||||
if hasattr(_condition_type, "__then__"):
|
||||
_then_result = _condition_type.__then__(_condition, _then_result)
|
||||
_expr_result = _then_result
|
||||
else:
|
||||
_else_result = ELSE_RESULT
|
||||
if hasattr(_condition_type, "__else__"):
|
||||
_else_result = _condition_type.__else__(_condition, _else_result)
|
||||
_expr_result = _else_result
|
||||
_lhs = LHS
|
||||
type(_lhs).__then__(_lhs) if _lhs else type(_lhs).__else__(_lhs, RHS)
|
||||
|
||||
The key change 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.
|
||||
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.
|
||||
|
||||
Interpreter implementations may check eagerly for the new protocol methods
|
||||
on condition objects in order to retain an optimised fast path for the great
|
||||
many objects that support use in a boolean context, but don't implement the new
|
||||
protocol.
|
||||
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).
|
||||
|
||||
It is proposed that the ``else`` operator use the same precedence level as
|
||||
the existing ``and`` and ``or`` operators.
|
||||
|
||||
The precedence/associativity in the otherwise ambiguous case of
|
||||
``expr1 if cond else expr2 else epxr3`` is TBD based on ease of implementation
|
||||
(a guideline will be added to PEP 8 to say "don't do that", as this construct
|
||||
will be inherently confusing for readers, regardless of how the interpreter
|
||||
executes it).
|
||||
|
||||
|
||||
Logical conjunction (``and``)
|
||||
-----------------------------
|
||||
Overloading logical inversion (``not``)
|
||||
---------------------------------------
|
||||
|
||||
Logical conjunction is affected by this proposal as if::
|
||||
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.
|
||||
|
||||
LHS and RHS
|
||||
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.
|
||||
|
||||
was internally implemented by the interpreter as::
|
||||
To preserve the semantics of existing language optimisations, ``__not__``
|
||||
implementations will be obliged to respect the following invariant::
|
||||
|
||||
_lhs_result = LHS
|
||||
_expr_result = RHS if _lhs_result else _lhs_result
|
||||
|
||||
Conditional result managers can force non-shortcircuiting evaluation under
|
||||
logical conjunction by always returning ``True`` from ``__bool__`` and
|
||||
enforce this at runtime by raising ``NotImplementedError`` by raising
|
||||
``NotImplementedError`` in ``__else__``.
|
||||
|
||||
Alternatively, conditional result managers can detect short-circuited evaluation
|
||||
of logical conjunction in ``__else__`` implementations by looking for cases
|
||||
where ``self`` and ``result`` are the exact same object.
|
||||
|
||||
|
||||
Logical disjunction (``or``)
|
||||
-----------------------------
|
||||
|
||||
Logical disjunction is affected by this proposal as if::
|
||||
|
||||
LHS or RHS
|
||||
|
||||
was internally implemented by the interpreter as::
|
||||
|
||||
_lhs_result = LHS
|
||||
_expr_result = _lhs_result if _lhs_result else RHS
|
||||
|
||||
Conditional result managers can force non-shortcircuiting evaluation under
|
||||
logical disjunction by always returning ``False`` from ``__bool__`` and
|
||||
enforce this at runtime by raising ``NotImplementedError`` by raising
|
||||
``NotImplementedError`` in ``__then__``.
|
||||
|
||||
Alternatively, conditional result managers can detect short-circuited evaluation
|
||||
of logical disjunction in ``__then__`` implementations by looking for cases
|
||||
where ``self`` and ``result`` are the exact same object.
|
||||
assert not bool(obj) == bool(not obj)
|
||||
|
||||
|
||||
Chained comparisons
|
||||
-------------------
|
||||
|
||||
Chained comparisons are affected by this proposal as if::
|
||||
A chained comparison like ``0 < x < 10`` written as::
|
||||
|
||||
LEFT_BOUND left_op EXPR right_op RIGHT_BOUND
|
||||
|
||||
was internally implemented by the interpreter as::
|
||||
is currently roughly semantically equivalent to::
|
||||
|
||||
_expr = EXPR
|
||||
_lhs_result = LEFT_BOUND left_op EXPR
|
||||
_expr_result = _lhs_result if _lhs_result else (_expr right_op RIGHT_BOUND)
|
||||
_lhs_result = LEFT_BOUND left_op _expr
|
||||
_expr_result = _lhs_result and (_expr right_op RIGHT_BOUND)
|
||||
|
||||
As with any logical conjunction, conditional result managers returned by
|
||||
comparison operations can force non-shortcircuiting evaluating in these
|
||||
cases by always returning ``True`` from ``__bool__``.
|
||||
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::
|
||||
|
||||
_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.
|
||||
|
||||
|
||||
Existence checking comparisons
|
||||
|
@ -208,76 +197,167 @@ known as "None-severing" or "None-propagating").
|
|||
|
||||
These builtins would be defined as follows::
|
||||
|
||||
class exists:
|
||||
"""Conditional result manager for 'EXPR is not None' checks"""
|
||||
def __init__(self, value):
|
||||
class CircuitBreaker:
|
||||
"""Base circuit breaker type (available as types.CircuitBreaker)"""
|
||||
def __init__(self, value, condition, inverse_type):
|
||||
self.value = value
|
||||
def __not__(self):
|
||||
return missing(self.value)
|
||||
self._condition = condition
|
||||
self._inverse_type = inverse_type
|
||||
def __bool__(self):
|
||||
return self.value is not None
|
||||
def __then__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
def __else__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
return self._condition
|
||||
def __not__(self):
|
||||
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 missing:
|
||||
"""Conditional result manager for 'EXPR is None' checks"""
|
||||
class exists(types.CircuitBreaker):
|
||||
"""Circuit breaker for 'EXPR is not None' checks"""
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __not__(self):
|
||||
return exists(self.value)
|
||||
def __bool__(self):
|
||||
return self.value is None
|
||||
def __then__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
def __else__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
super().__init__(value, value is not None, missing)
|
||||
|
||||
class missing(types.CircuitBreaker):
|
||||
"""Circuit breaker for 'EXPR is None' checks"""
|
||||
def __init__(self, value):
|
||||
super().__init__(value, value is None, exists)
|
||||
|
||||
Aside from changing the definition of ``__bool__`` to be based on
|
||||
``is not None`` rather than normal truth checking, the key characteristic of
|
||||
``exists`` is that when it is used as a conditional result manager, it is
|
||||
*ephemeral*: when it detects that short circuiting has taken place, it returns
|
||||
the original value, rather than the existence checking wrapper.
|
||||
``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.
|
||||
|
||||
``missing`` is defined as the logically inverted counterpart of ``exists``:
|
||||
``not exists(obj)`` is semantically equivalent to ``missing(obj)``.
|
||||
|
||||
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
|
||||
|
||||
|
||||
Other conditional constructs
|
||||
----------------------------
|
||||
|
||||
No changes are proposed to if statements, while statements, comprehensions, or
|
||||
generator expressions, as the boolean clauses they contain are purely used for
|
||||
control flow purposes and don't have programmatically accessible "results".
|
||||
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.
|
||||
|
||||
However, it's worth noting that while such proposals are outside the scope of
|
||||
this PEP, the conditional result management protocol defined here would be
|
||||
sufficient to support constructs like::
|
||||
this PEP, the circuit breaking protocol defined here would be sufficient to
|
||||
support constructs like::
|
||||
|
||||
while exists(dynamic_query()) as result:
|
||||
... # Code using result
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Rationale
|
||||
=========
|
||||
|
||||
Avoiding new syntax
|
||||
-------------------
|
||||
Adding a new operator
|
||||
---------------------
|
||||
|
||||
Adding new syntax to Python to make particular software design problems easier
|
||||
to handle is considered a solution of last resort. As a successor to PEP 335,
|
||||
this PEP focuses on making the existing ``and`` and ``or`` operators less rigid
|
||||
in their interpretation, rather than on proposing 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
|
||||
reasons:
|
||||
|
||||
* defining a shared protocol for both ``and`` and ``or`` was confusing, as
|
||||
``__then__`` was the short-circuiting outcome for ``or``, while ``__else__``
|
||||
was the short-circuiting outcome for ``__and__``
|
||||
* 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
|
||||
even allow us to expose ``operator.logical_and`` and ``operator.logical_or``
|
||||
as circuit breaker definitions if we chose to do so::
|
||||
|
||||
class logical_or(types.CircuitBreaker):
|
||||
"""Circuit breaker for 'bool(EXPR)' checks"""
|
||||
def __init__(self, value):
|
||||
super().__init__(value, bool(value), logical_and)
|
||||
|
||||
class logical_and(types.CircuitBreaker):
|
||||
"""Circuit breaker for 'not bool(EXPR)' checks"""
|
||||
def __init__(self, value):
|
||||
super().__init__(value, not bool(value), logical_or)
|
||||
|
||||
|
||||
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 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.
|
||||
|
||||
|
||||
Element-wise chained comparisons
|
||||
|
@ -344,21 +424,23 @@ result::
|
|||
|
||||
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
|
||||
dedicated subclass that both implements the new protocol methods and also
|
||||
dedicated subclass that implements the new circuit breaking protocol and also
|
||||
changes the result array's interpretation in a boolean context to always
|
||||
return true and hence avoid Python's default short-circuiting behaviour::
|
||||
return ``False`` and hence never trigger the short-circuiting behaviour::
|
||||
|
||||
class ComparisonResultArray(np.ndarray):
|
||||
def __bool__(self):
|
||||
return True
|
||||
def __then__(self, result):
|
||||
if result is self:
|
||||
msg = ("Comparison array truth values are ambiguous outside "
|
||||
"chained comparisons. Use a.any() or a.all()")
|
||||
raise ValueError(msg)
|
||||
return np.logical_and(self, result.view(ComparisonResultArray))
|
||||
def __else__(self, result):
|
||||
raise NotImplementedError("Comparison result arrays are never False")
|
||||
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))
|
||||
|
||||
With this change, the chained comparison example above would be able to return::
|
||||
|
||||
|
@ -395,115 +477,52 @@ 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:
|
||||
|
||||
* ``value1 = exists(expr1) and expr1.field.of.interest``
|
||||
* ``value2 = exists(expr2) and expr2.["field"]["of"]["interest"]``
|
||||
* ``value3 = exists(expr3) or exists(expr4) or expr5``
|
||||
* ``value1 = missing(expr1) else expr1.field.of.interest``
|
||||
* ``value2 = missing(expr2) else expr2.["field"]["of"]["interest"]``
|
||||
* ``value3 = exists(expr3) else exists(expr4) else expr5``
|
||||
|
||||
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
|
||||
or falling back to an alternative input, has shrunk to four uses of the new
|
||||
``exists`` builtin, two uses of the ``and`` keyword, and two uses of the
|
||||
``or`` keyword.
|
||||
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.
|
||||
|
||||
In the first two examples, the 31 character boilerplate suffix
|
||||
``if exprN is not None else None`` (minimally 27 characters for a single letter
|
||||
variable name) has been replaced by a 20 character `exists(expr1) and``
|
||||
prefix (minimally 16 characters with a single letter variable name), somewhat
|
||||
variable name) has been replaced by a 19 character ``missing(expr1) else``
|
||||
prefix (minimally 15 characters with a single letter variable name), markedly
|
||||
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
|
||||
making them shorter purely for the sake of expression brevity).
|
||||
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.
|
||||
|
||||
In the last example, not only are two instances of the 26 character boilerplate,
|
||||
``if exprN is not None else`` (minimally 22 characters) replaced with the
|
||||
14 character function call ``exists() or``, with that function call being
|
||||
placed directly around the original expression, eliminating the need to
|
||||
duplicate it in the conditional existence check.
|
||||
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.
|
||||
|
||||
|
||||
Risks and concerns
|
||||
==================
|
||||
|
||||
Readability
|
||||
-----------
|
||||
This PEP has been designed specifically to address the risks and concerns
|
||||
raised when discussing PEPs 335, 505 and 531.
|
||||
|
||||
Python has a long history of disallowing customisation of the control flow
|
||||
operators, and overloading them isn't particularly common in other languages
|
||||
either. Even languages which do permit overloading may lose the property of
|
||||
short-circuiting evaluation when overloaded (e.g. that happens when overloading
|
||||
``&&`` and ``||`` in C++).
|
||||
|
||||
This history means that the idea of ``and`` and ``or`` suddenly gaining the
|
||||
ability to be interpreted differently based on the type of the left-hand
|
||||
operand is a potentially controversial one from a readability and
|
||||
maintainability perspective, to the point where it may be *less* controversial
|
||||
to define a single new ``??`` operator as proposed in PEP 505, or separate
|
||||
``?then`` and ``?else`` operators as suggested in PEP 531 than it would be to
|
||||
redefine the existing operators (as currently proposed in this PEP).
|
||||
|
||||
Such an approach would also address one of Guido's key concerns with PEP 335
|
||||
[1_] that would also apply to this PEP as currently written:
|
||||
|
||||
Amongst other reasons, I really dislike that the PEP adds to the bytecode
|
||||
for all uses of these operators even though almost no call sites will ever
|
||||
need the feature.
|
||||
|
||||
If the protocol in this PEP was combined with the core syntactic proposals in
|
||||
PEP 531, then the end result would look something like:
|
||||
|
||||
* ``value1 = exists(expr1) ?then expr1.field.of.interest``
|
||||
* ``value2 = exists(expr2) ?then expr2["field"]["of"]["interest"]``
|
||||
* ``value3 = exists(expr3) ?else exists(expr4) ?else expr5``
|
||||
|
||||
Rather than indicating use of the existence protocol as suggested in PEP 531,
|
||||
the ``?`` here would indicate use of the conditional result management protocol,
|
||||
and hence the fact the result may be something other than the LHS as written
|
||||
when the short-circuiting path is executed.
|
||||
|
||||
Alternatively, if only a single new operator was added as proposed in PEP
|
||||
505, but it used the semantics proposed for ``or`` in this PEP, then the end
|
||||
result would look something like:
|
||||
|
||||
* ``value1 = missing(expr1) ?? expr1.field.of.interest``
|
||||
* ``value2 = missing(expr2) ?? expr2["field"]["of"]["interest"]``
|
||||
* ``value3 = exists(expr3) ?? exists(expr4) ?? expr5``
|
||||
|
||||
If new operators were added rather than redefining the semantics of ``and``,
|
||||
``or`` and ``if-else``, then it would make sense to *require* that their left
|
||||
hand operand be a conditional result manager that defines both ``__then__``
|
||||
and ``__else__``, rather than accepting arbitrary objects as ``and`` and ``or``
|
||||
do.
|
||||
|
||||
With that approach, chained comparisons would be conditionally redefined in
|
||||
terms of the new protocol when the left comparison produces a conditional result
|
||||
manager, while continuing to be defined in terms of ``and`` for any other
|
||||
left comparison result.
|
||||
|
||||
|
||||
Compatibility
|
||||
-------------
|
||||
|
||||
At least CPython's peephole optimizer, and presumably other Python optimizers,
|
||||
include a lot of assumptions about the semantics of ``and`` and ``or``
|
||||
expressions. This means that any changes to those semantics are likely to
|
||||
require interpreter implementors to closely review a whole lot of code
|
||||
related not only to the way those operations are implemented, but also to the
|
||||
way they're optimized.
|
||||
|
||||
By contrast, new operators would be substantially lower risk, as existing
|
||||
optimizers couldn't be making any assumptions about how they work.
|
||||
|
||||
|
||||
Speed of execution
|
||||
------------------
|
||||
|
||||
Making relatively common operations like ``and`` and ``or`` check for additional
|
||||
protocol methods is likely to slow them down in the common case. The additional
|
||||
overhead should be small relative to the cost of boolean truth checking, but
|
||||
it won't be zero.
|
||||
|
||||
Defining new operators rather than reusing existing ones would address this
|
||||
concern as well.
|
||||
* 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
|
||||
|
||||
|
||||
Design Discussion
|
||||
|
@ -512,48 +531,48 @@ Design Discussion
|
|||
Arbitrary sentinel objects
|
||||
--------------------------
|
||||
|
||||
Unlike PEP 531, this proposal readily handles custom sentinel objects::
|
||||
Unlike PEPs 505 and 531, this proposal readily handles custom sentinel objects::
|
||||
|
||||
# Definition of a base configurable sentinel check that defaults to None
|
||||
class SentinelCheck:
|
||||
sentinel = None
|
||||
class defined(types.CircuitBreaker):
|
||||
MISSING = object()
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __bool__(self):
|
||||
return self.value is not self.sentinel
|
||||
def __then__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
def __else__(self, result):
|
||||
if result is self:
|
||||
return result.value
|
||||
return result
|
||||
super().__init__(self, value is not self.MISSING, not_defined)
|
||||
|
||||
# Local subclass using a custom sentinel object
|
||||
class if_defined(SentinelCheck):
|
||||
sentinel=object()
|
||||
class not_defined(types.CircuitBreaker):
|
||||
def __init__(self, value):
|
||||
super().__init__(self, value is defined.MISSING, defined)
|
||||
|
||||
# Using the sentinel to check whether or not an argument was supplied
|
||||
def my_func(arg=if_defined.sentinel):
|
||||
arg = if_defined(arg) or calculate_default()
|
||||
def my_func(arg=defined.MISSING):
|
||||
arg = defined(arg) else calculate_default()
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
As with PEP 505, actual implementation has been deferred pending in-principle
|
||||
interest in the idea of making these changes - aside from the compatibility
|
||||
concerns noted 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.
|
||||
interest in the idea of making these changes - aside from the possible
|
||||
syntactic ambiguity concerns noted 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.
|
||||
|
||||
...TBD...
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
|
|
Loading…
Reference in New Issue