PEP 432: updates prior to python-ideas posting
- propose specific Grammar changes - rename potential boolean circuit breakers so they read better when using them to explain and/or behaviour - operator.logical_or -> operator.true - operator.logical_and -> operator.false - discuss a problem Guido raised regarding inconsistency between the proposed operator and conditional expressions (I think it's fixable, but fixing it would have some pretty significant consequences for the overall language) - add today to Post-History
This commit is contained in:
parent
d30bf73e24
commit
9f8dddecc0
145
pep-0532.txt
145
pep-0532.txt
|
@ -1,5 +1,5 @@
|
||||||
PEP: 532
|
PEP: 532
|
||||||
Title: Defining "else" as a protocol based circuit breaking operator
|
Title: A circuit breaking operator and protocol
|
||||||
Version: $Revision$
|
Version: $Revision$
|
||||||
Last-Modified: $Date$
|
Last-Modified: $Date$
|
||||||
Author: Nick Coghlan <ncoghlan@gmail.com>
|
Author: Nick Coghlan <ncoghlan@gmail.com>
|
||||||
|
@ -8,6 +8,7 @@ Type: Standards Track
|
||||||
Content-Type: text/x-rst
|
Content-Type: text/x-rst
|
||||||
Created: 30-Oct-2016
|
Created: 30-Oct-2016
|
||||||
Python-Version: 3.7
|
Python-Version: 3.7
|
||||||
|
Post-History: 5-Nov-2016
|
||||||
|
|
||||||
Abstract
|
Abstract
|
||||||
========
|
========
|
||||||
|
@ -130,14 +131,29 @@ 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
|
comparison chaining use case in the Rationale section below for an example
|
||||||
of that).
|
of that).
|
||||||
|
|
||||||
It is proposed that the ``else`` operator use the same precedence level as
|
It is proposed that the ``else`` operator use a new precedence level that binds
|
||||||
the existing ``and`` and ``or`` operators.
|
less tightly than the ``or`` operator by adjusting the relevant line in
|
||||||
|
Python's grammar from the current::
|
||||||
|
|
||||||
The precedence/associativity in the otherwise ambiguous case of
|
test: or_test ['if' or_test 'else' test] | lambdef
|
||||||
``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
|
to instead be::
|
||||||
will be inherently confusing for readers, regardless of how the interpreter
|
|
||||||
executes it).
|
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.
|
||||||
|
|
||||||
|
|
||||||
Overloading logical inversion (``not``)
|
Overloading logical inversion (``not``)
|
||||||
|
@ -316,18 +332,23 @@ invariant required of ``__not__`` implementations means that existing
|
||||||
expression optimisations in boolean contexts will remain valid.
|
expression optimisations in boolean contexts will remain valid.
|
||||||
|
|
||||||
As a result of that design simplification, the new protocol and operator would
|
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``
|
even allow us to expose ``operator.true`` and ``operator.false``
|
||||||
as circuit breaker definitions if we chose to do so::
|
as circuit breaker definitions if we chose to do so::
|
||||||
|
|
||||||
class logical_or(types.CircuitBreaker):
|
class true(types.CircuitBreaker):
|
||||||
"""Circuit breaker for 'bool(EXPR)' checks"""
|
"""Circuit breaker for 'bool(EXPR)' checks"""
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
super().__init__(value, bool(value), logical_and)
|
super().__init__(value, bool(value), when_false)
|
||||||
|
|
||||||
class logical_and(types.CircuitBreaker):
|
class false(types.CircuitBreaker):
|
||||||
"""Circuit breaker for 'not bool(EXPR)' checks"""
|
"""Circuit breaker for 'not bool(EXPR)' checks"""
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
super().__init__(value, not bool(value), logical_or)
|
super().__init__(value, not bool(value), when_true)
|
||||||
|
|
||||||
|
Given those circuit breakers:
|
||||||
|
|
||||||
|
* ``LHS or RHS`` would be roughly ``operator.true(LHS) else RHS``
|
||||||
|
* ``LHS and RHS`` would be roughly ``opreator.false(LHS) else RHS``
|
||||||
|
|
||||||
|
|
||||||
Naming the operator and protocol
|
Naming the operator and protocol
|
||||||
|
@ -525,6 +546,91 @@ raised when discussing PEPs 335, 505 and 531.
|
||||||
and skip evaluating the right operand
|
and skip evaluating the right operand
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
Design Discussion
|
Design Discussion
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
@ -552,12 +658,13 @@ Implementation
|
||||||
|
|
||||||
As with PEP 505, actual implementation has been deferred pending in-principle
|
As with PEP 505, actual implementation has been deferred pending in-principle
|
||||||
interest in the idea of making these changes - aside from the possible
|
interest in the idea of making these changes - aside from the possible
|
||||||
syntactic ambiguity concerns noted above, the implementation isn't really the
|
syntactic ambiguity concerns covered by the grammer proposals above, the
|
||||||
hard part of these proposals, the hard part is deciding whether or not this is
|
implementation isn't really the hard part of these proposals, the hard part
|
||||||
a change where the long term benefits for new and existing Python users
|
is deciding whether or not this is a change where the long term benefits for
|
||||||
outweigh the short term costs involved in the wider ecosystem (including
|
new and existing Python users outweigh the short term costs involved in the
|
||||||
developers of other implementations, language curriculum developers, and
|
wider ecosystem (including developers of other implementations, language
|
||||||
authors of other Python related educational material) adjusting to the change.
|
curriculum developers, and authors of other Python related educational
|
||||||
|
material) adjusting to the change.
|
||||||
|
|
||||||
...TBD...
|
...TBD...
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue