PEP 532 revision and restructure (#156)

- Language level inconsistencies eliminated by proposing both
  `if` and `else` as circuit-breaking operators
- PEP 532 no longer competes with PEP 505 at all
- Rich comparison chaining split out as PEP 535
- Mark E. Haase is now listed as a co-author (as I was better
  able to integrate some of his earlier suggestions)
- acknowledged key feedback on the initial draft
This commit is contained in:
Nick Coghlan 2016-12-12 00:11:36 +10:00 committed by GitHub
parent 77122d16c9
commit 80f963d066
3 changed files with 867 additions and 475 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

192
pep-0535.txt Normal file
View File

@ -0,0 +1,192 @@
PEP: 535
Title: Rich comparison chaining
Version: $Revision$
Last-Modified: $Date$
Author: Nick Coghlan <ncoghlan@gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Requires: 532
Created: 12-Nov-2016
Python-Version: 3.7
Abstract
========
Inspired by PEP 335, and building on the circuit breaking protocol described
in PEP 532, this PEP proposes a change to the definition of chained comparisons,
where the comparison chaining will be updated to use the left-associative
circuit breaking operator (``else``) rather than the logical disjunction
operator (``and``) 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 raising ``ValueError``
or tautologically returning ``True`` (indicating a non-empty matrix).
Relationship with other PEPs
============================
This PEP has been extracted from earlier iterations of PEP 532, as a
follow-on use case for the circuit breaking protocol, rather than an essential
part of its introduction.
The specific proposal in this PEP to 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.
Specification
=============
A chained comparison like ``0 < x < 10`` written as::
LEFT_BOUND LEFT_OP EXPR RIGHT_OP RIGHT_BOUND
is currently roughly semantically equivalent to::
_expr = EXPR
_lhs_result = LEFT_BOUND LEFT_OP _expr
_expr_result = _lhs_result and (_expr RIGHT_OP RIGHT_BOUND)
Using the circuit breaking concepts introduced in PEP 532, this PEP proposes
that comparison chaining 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), "__else__"):
_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 suitably defined circuit breakers from comparison
operations.
The expansion of this logic to an arbitrary number of chained comparison
operations would be the same as the existing expansion for ``and``.
Rationale
=========
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).
To understand this observation, we first need to look at how comparisons work
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
each element in the left hand array to the corresponding element in the right
hand array, and producing a matrix of boolean results.
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
dedicated subclass that implements the new circuit breaking protocol and also
changes the result array's interpretation in a boolean context to always
return ``False`` and hence never trigger the short-circuiting behaviour::
class ComparisonResultArray(np.ndarray):
def __bool__(self):
# Element-wise comparison chaining never short-circuits
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, result):
self._raise_NotImplementedError()
def __else__(self, result):
return np.logical_and(self, other.view(ComparisonResultArray))
With this change, the chained comparison example above would be able to return::
>>> 0 < increasing < 4
ComparisonResultArray([ False, True, True, True, False], dtype=bool)
Implementation
==============
Actual implementation has been deferred pending in-principle interest in the
idea of making the changes proposed in PEP 532.
...TBD...
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: