PEP 207 – 풍부한 비교
- Author:
- Guido van Rossum <guido at python.org>, David Ascher <DavidA at ActiveState.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 25-Jul-2000
- Python-Version:
- 2.1
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 비교에 관한 몇 가지 새로운 기능을 제안합니다:
- 클래스와 C 확장 모두에서 <, >, <=, >=, ==, !=를 각각 따로 오버로드할 수 있도록 합니다.
- 오버로드된 이 연산자들이 불리언 결과 외에 다른 것도 반환할 수 있도록 합니다.
동기
주된 동기는 NumPy에서 비롯됩니다. NumPy 사용자들은 A<B가 요소별 비교 결과의 배열을 반환해야 한다는 데 동의합니다. 현재 A<B는 불리언 결과를 반환하거나 예외를 발생시키는 것만 가능하므로, 사용자들은 이를 less(A,B)로 써야 합니다.
또 다른 동기는 타입에 자연스러운 순서가 없지만 그래도 동등성은 비교해야 하는 경우가 많다는 점입니다. 현재 그러한 타입은 단지 동등성을 검사할 수 있도록 하기 위해서 반드시 비교를 구현해야 하며, 그에 따라 임의의 순서를 정의해야 합니다.
또한 일부 객체 타입에서는 동등성 검사를 순서 검사보다 훨씬 효율적으로 구현할 수 있습니다. 예를 들어 길이가 다른 리스트와 딕셔너리는 같지 않지만, 순서를 정하려면 항목 일부(경우에 따라서는 전부)를 살펴보아야 합니다.
이전 작업
풍부한 비교는 이전에도 제안된 적이 있습니다. 특히 David Ascher가 Numerical Python을 사용한 경험을 바탕으로 제안했습니다:
이 제안은 아래에 부록으로도 수록되어 있습니다. 이 PEP 내용의 대부분은 David의 제안에서 가져온 것입니다.
우려 사항
- 하위 호환성은 Python 수준(
__cmp__를 사용하는 클래스는 변경할 필요가 없음)과 C 수준(tp_comparea를 정의하는 확장은 변경할 필요가 없으며,PyObject_Compare()를 사용하는 코드는 비교 대상 객체가 새로운 풍부한 비교 방식을 사용하더라도 동작해야 함) 모두에서 보장되어야 합니다. - A<B가 요소별 비교 결과의 행렬을 반환할 때, 이 표현식을 불리언 문맥에서 사용하는 실수를 저지르기 쉽습니다. 특별한 예방 조치가 없으면 이 표현식은 항상 참이 됩니다. 이렇게 사용하면 대신 예외가 발생해야 합니다.
- 클래스가 x==y만 오버라이드하고 다른 것은 오버라이드하지 않는다면, x!=y를 not(x==y)로 계산해야 합니까, 아니면 실패해야 합니까? <와 >= 사이, 또는 >와 <= 사이의 비슷한 관계는 어떻게 해야 합니까?
- 마찬가지로, x<y를 y>x로부터 계산하도록 허용해야 합니까? 그리고 x<=y를 not(x>y)로부터 계산하도록 허용해야 합니까? 그리고 x==y를 y==x로부터, 또는 x!=y를 y!=x로부터 계산하도록 허용해야 합니까?
- 비교 연산자가 요소별 비교 결과를 반환할 때, A<B<C,
A<B and C<D,A<B or C<D같은 단축 연산자는 어떻게 처리해야 합니까? min()과max(), ‘in’과 ‘not in’ 연산자,list.sort(), 딕셔너리 키 비교, 그리고 내장 연산에서 비교를 사용하는 그 밖의 경우는 어떻게 처리해야 합니까?
제안하는 해결책
- 완전한 하위 호환성은 다음과 같이 달성할 수 있습니다. 객체가
tp_compare()는 정의하지만tp_richcompare()는 정의하지 않은 상태에서 풍부한 비교가 요청되면,tp_compare()의 결과를 자명한 방식으로 사용합니다. 예를 들어 “<”가 요청되면,tp_compare()가 예외를 발생시킬 경우 예외가 발생하고,tp_compare()가 음수이면 결과는 1이며, 0이거나 양수이면 결과는 0입니다. 나머지 경우도 마찬가지입니다.완전한 상위 호환성은 다음과 같이 달성할 수 있습니다.
tp_richcompare()를 구현하는 객체에 고전적 비교가 요청되면 최대 세 번의 비교가 사용됩니다. 먼저 ==를 시도하여 참을 반환하면 0을 반환하고, 다음으로 <를 시도하여 참을 반환하면 -1을 반환하며, 다음으로 >를 시도하여 참을 반환하면 +1을 반환합니다. 시도한 연산자 중 하나라도 불리언이 아닌 값을 반환하면(아래 참조), 불리언으로 변환할 때 발생한 예외를 그대로 전달합니다. 시도한 연산자 중 어느 것도 참을 반환하지 않으면, 다음으로 전통적인 비교 대체 방법을 시도합니다.(세 가지 비교를 어떤 순서로 시도해야 하는지 오랫동안 깊이 고민했습니다. 한때는 순환 자료 구조에 대한 비교 동작을 근거로, 이 순서대로 해야 한다는 설득력 있는 인자(논거)가 있었습니다. 하지만 그 코드가 다시 바뀌었기 때문에, 이제는 순서가 차이를 만드는지 확신할 수 없습니다.)
- 단일 불리언 대신 불리언의 컬렉션을 반환하는 타입은 예외를 발생시키도록
nb_nonzero()를 정의해야 합니다. 그러한 타입은 불리언이 아닌 것으로 간주합니다. - == 연산자와 != 연산자가 서로의 여(complement)라고 가정하지 않습니다(예를 들어 IEEE 754 부동소수점 수는 이를 만족하지 않습니다). 필요하다면 이를 구현하는 것은 타입의 몫입니다. <와 >=, 또는 >와 <=도 마찬가지입니다. 이러한 가정이 성립하지 않는 예는 많습니다(예: tabnanny).
- 반사성 규칙은 Python이 가정합니다. 따라서 인터프리터는 y>x를 x<y로, y>=x를 x<=y로 바꿀 수 있고, x==y와 x!=y의 인자를 서로 바꿀 수 있습니다. (참고: 현재 Python은 x==x가 항상 참이고 x!=x가 결코 참이 아니라고 가정하지만, 이렇게 가정해서는 안 됩니다.)
- 현재 제안에서는 A<B가 원소별 비교 결과의 배열을 반환하는 경우 이 결과를 불리언이 아닌 것으로 간주하며, 단축 연산자가 이를 불리언으로 해석하려 하면 예외가 발생합니다. David Ascher의 제안은 이 문제를 다루려고 시도하지만, 저는 이것이 코드 제너레이터에 추가되는 복잡성을 감수할 만한 가치가 있다고 생각하지 않습니다. A<B<C 대신 (A<B)&(B<C)로 작성할 수 있습니다.
min()과list.sort()연산은 < 연산자만 사용하고, max()는 > 연산자만 사용합니다. ‘in’과 ‘not in’ 연산자, 그리고 딕셔너리 조회는 == 연산자만 사용합니다.
구현 제안
이 제안은 David Ascher의 제안을 거의 그대로 따릅니다.
C API
- 새 함수:
PyObject *PyObject_RichCompare(PyObject *, PyObject *, int)
이 함수는 요청된 풍부한 비교를 수행하여 Python 객체를 반환하거나 예외를 발생시킵니다. 세 번째 인자는 Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE 중 하나여야 합니다.
int PyObject_RichCompareBool(PyObject *, PyObject *, int)
이 함수는 요청된 풍부한 비교를 수행하여 불리언을 반환합니다. 예외이면 -1, 거짓이면 0, 참이면 1을 반환합니다. 세 번째 인자는 Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE 중 하나여야 합니다.
PyObject_RichCompare()가 불리언이 아닌 객체를 반환하면PyObject_RichCompareBool()은 예외를 발생시킨다는 점에 유의하십시오. - 새 typedef:
typedef PyObject *(*richcmpfunc) (PyObject *, PyObject *, int);
- 타입 객체의 새 슬롯(예비 슬롯 tp_xxx7을 대체):
richcmpfunc tp_richcompare;
이것은
PyObject_RichCompare()와 같은 시그니처를 가지고 같은 비교를 수행하는 함수여야 합니다. 인자 중 적어도 하나는 지금 사용되는 tp_richcompare 슬롯을 가진 타입이지만, 다른 하나는 다른 타입일 수 있습니다. 함수가 해당 객체 조합을 비교할 수 없으면Py_NotImplemented에 대한 새 참조를 반환해야 합니다. PyObject_Compare()는 풍부한 비교가 정의되어 있으면 이를 시도하도록 변경됩니다(단, 고전적 비교가 정의되어 있지 않은 경우에만 그렇습니다).
인터프리터 변경 사항
- 특정 비교의 결과를 얻으려고
PyObject_Compare()를 호출하는 곳에서는 언제나(예:list.sort()에서, 그리고 물론 ceval.c의 비교 연산자에서) 대신PyObject_RichCompare()또는PyObject_RichCompareBool()을 호출하도록 코드가 변경됩니다. C 코드가 비교 결과를 알아야 하면 그 결과에 대해PyObject_IsTrue()를 호출합니다(이 호출은 예외를 일으킬 수 있습니다). - 현재 비교를 정의하는 대부분의 내장 타입은 대신 풍부한 비교를 정의하도록 수정됩니다. (이것은 선택 사항입니다. 저는 지금까지 리스트, 튜플, 복소수, 배열을 변환했으며, 다른 타입도 변환할지는 아직 확실하지 않습니다.)
클래스
- 클래스는 새로운 특수 메서드
__lt__,__le__,__eq__,__ne__,__gt__,__ge__를 정의하여 대응하는 연산자를 재정의할 수 있습니다. (즉, <, <=, ==, !=, >, >=입니다. 포트란의 유산은 정말 사랑하지 않을 수 없습니다.) 클래스가__cmp__도 정의하면, 이것은__lt__등을 먼저 시도해NotImplemented를 반환한 경우에만 사용됩니다.
Copyright
This document has been placed in the public domain.
Appendix
Here is most of David Ascher’s original proposal (version 0.2.1, dated Wed Jul 22 16:49:28 1998; I’ve left the Contents, History and Patches sections out). It addresses almost all concerns above.
Abstract
A new mechanism allowing comparisons of Python objects to return
values other than -1, 0, or 1 (or raise exceptions) is
proposed. This mechanism is entirely backwards compatible, and can
be controlled at the level of the C PyObject type or of the Python
class definition. There are three cooperating parts to the
proposed mechanism:
- the use of the last slot in the type object structure to store a pointer to a rich comparison function
- the addition of special methods for classes
- the addition of an optional argument to the builtin
cmp()function.
Motivation
The current comparison protocol for Python objects assumes that
any two Python objects can be compared (as of Python 1.5, object
comparisons can raise exceptions), and that the return value for
any comparison should be -1, 0 or 1. -1 indicates that the first
argument to the comparison function is less than the right one, +1
indicating the contrapositive, and 0 indicating that the two
objects are equal. While this mechanism allows the establishment
of an order relationship (e.g. for use by the sort() method of list
objects), it has proven to be limited in the context of Numeric
Python (NumPy).
Specifically, NumPy allows the creation of multidimensional arrays, which support most of the numeric operators. Thus:
x = array((1,2,3,4)) y = array((2,2,4,4))
are two NumPy arrays. While they can be added elementwise,:
z = x + y # z == array((3,4,7,8))
they cannot be compared in the current framework - the released version of NumPy compares the pointers, (thus yielding junk information) which was the only solution before the recent addition of the ability (in 1.5) to raise exceptions in comparison functions.
Even with the ability to raise exceptions, the current protocol
makes array comparisons useless. To deal with this fact, NumPy
includes several functions which perform the comparisons: less(),
less_equal(), greater(), greater_equal(), equal(),
not_equal(). These functions return arrays with the same shape as
their arguments (modulo broadcasting), filled with 0’s and 1’s
depending on whether the comparison is true or not for each
element pair. Thus, for example, using the arrays x and y defined
above:
less(x,y)
would be an array containing the numbers (1,0,0,0).
The current proposal is to modify the Python object interface to allow the NumPy package to make it so that x < y returns the same thing as less(x,y). The exact return value is up to the NumPy package – what this proposal really asks for is changing the Python core so that extension objects have the ability to return something other than -1, 0, 1, should their authors choose to do so.
Current State of Affairs
The current protocol is, at the C level, that each object type
defines a tp_compare slot, which is a pointer to a function which
takes two PyObject* references and returns -1, 0, or 1. This
function is called by the PyObject_Compare() function defined in
the C API. PyObject_Compare() is also called by the builtin
function cmp() which takes two arguments.
Proposed Mechanism
- Changes to the C structure for type objects
The last available slot in the
PyTypeObject, reserved up to now for future expansion, is used to optionally store a pointer to a new comparison function, of type richcmpfunc defined by:typedef PyObject *(*richcmpfunc) Py_PROTO((PyObject *, PyObject *, int));
This function takes three arguments. The first two are the objects to be compared, and the third is an integer corresponding to an opcode (one of LT, LE, EQ, NE, GT, GE). If this slot is left NULL, then rich comparison for that object type is not supported (except for class instances whose class provide the special methods described below).
The above opcodes need to be added to the published Python/C API (probably under the names Py_LT, Py_LE, etc.)
- Additions of special methods for classes
Classes wishing to support the rich comparison mechanisms must add one or more of the following new special methods:
def __lt__(self, other): ... def __le__(self, other): ... def __gt__(self, other): ... def __ge__(self, other): ... def __eq__(self, other): ... def __ne__(self, other): ...
Each of these is called when the class instance is the on the left-hand-side of the corresponding operators (<, <=, >, >=, ==, and != or <>). The argument other is set to the object on the right side of the operator. The return value of these methods is up to the class implementor (after all, that’s the entire point of the proposal).
If the object on the left side of the operator does not define an appropriate rich comparison operator (either at the C level or with one of the special methods, then the comparison is reversed, and the right hand operator is called with the opposite operator, and the two objects are swapped. This assumes that a < b and b > a are equivalent, as are a <= b and b >= a, and that == and != are commutative (e.g. a == b if and only if b == a).
For example, if obj1 is an object which supports the rich comparison protocol and x and y are objects which do not support the rich comparison protocol, then obj1 < x will call the
__lt__method of obj1 with x as the second argument. x < obj1 will call obj1’s__gt__method with x as a second argument, and x < y will just use the existing (non-rich) comparison mechanism.The above mechanism is such that classes can get away with not implementing either
__lt__and__le__or__gt__and__ge__. Further smarts could have been added to the comparison mechanism, but this limited set of allowed “swaps” was chosen because it doesn’t require the infrastructure to do any processing (negation) of return values. The choice of six special methods was made over a single (e.g.__richcmp__) method to allow the dispatching on the opcode to be performed at the level of the C implementation rather than the user-defined method. - Addition of an optional argument to the builtin
cmp()The builtin
cmp()is still used for simple comparisons. For rich comparisons, it is called with a third argument, one of “<”, “<=”, “>”, “>=”, “==”, “!=”, “<>” (the last two have the same meaning). When called with one of these strings as the third argument,cmp()can return any Python object. Otherwise, it can only return -1, 0 or 1 as before.
Chained Comparisons
Problem
It would be nice to allow objects for which the comparison returns something other than -1, 0, or 1 to be used in chained comparisons, such as:
x < y < z
Currently, this is interpreted by Python as:
temp1 = x < y
if temp1:
return y < z
else:
return temp1
Note that this requires testing the truth value of the result of comparisons, with potential “shortcutting” of the right-side comparison testings. In other words, the truth-value of the result of the result of the comparison determines the result of a chained operation. This is problematic in the case of arrays, since if x, y and z are three arrays, then the user expects:
x < y < z
to be an array of 0’s and 1’s where 1’s are in the locations corresponding to the elements of y which are between the corresponding elements in x and z. In other words, the right-hand side must be evaluated regardless of the result of x < y, which is incompatible with the mechanism currently in use by the parser.
Solution
Guido mentioned that one possible way out would be to change the code generated by chained comparisons to allow arrays to be chained-compared intelligently. What follows is a mixture of his idea and my suggestions. The code generated for x < y < z would be equivalent to:
temp1 = x < y
if temp1:
temp2 = y < z
return boolean_combine(temp1, temp2)
else:
return temp1
where boolean_combine is a new function which does something like the following:
def boolean_combine(a, b):
if hasattr(a, '__boolean_and__') or \
hasattr(b, '__boolean_and__'):
try:
return a.__boolean_and__(b)
except:
return b.__boolean_and__(a)
else: # standard behavior
if a:
return b
else:
return 0
where the __boolean_and__ special method is implemented for
C-level types by another value of the third argument to the
richcmp function. This method would perform a boolean comparison
of the arrays (currently implemented in the umath module as the
logical_and ufunc).
Thus, objects returned by rich comparisons should always test true, but should define another special method which creates boolean combinations of them and their argument.
This solution has the advantage of allowing chained comparisons to work for arrays, but the disadvantage that it requires comparison arrays to always return true (in an ideal world, I’d have them always raise an exception on truth testing, since the meaning of testing “if a>b:” is massively ambiguous.
The inlining already present which deals with integer comparisons would still apply, resulting in no performance cost for the most common cases.