Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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:


Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 비교와 관련된 몇 가지 새로운 기능을 제안합니다:

  • <, >, <=, >=, ==, != 를 클래스와 C 확장 모두에서 개별적으로 오버로딩할 수 있도록 허용합니다.
  • 이러한 오버로딩된 연산자들이 불리언 결과 이외의 다른 무언가를 반환할 수 있도록 허용합니다.

동기

주된 동기는 NumPy에서 비롯되었는데, NumPy 사용자들은 A<B가 원소별 비교 결과의 배열을 반환해야 한다는 데 동의합니다. 현재는 A<B가 불리언 결과만 반환하거나 예외를 발생시킬 수밖에 없기 때문에, 이를 less(A,B)로 표현해야 합니다.

추가적인 동기로는, 많은 경우 타입들이 자연스러운 순서를 가지지 않지만 그럼에도 동등성 비교는 필요하다는 점이 있습니다. 현재는 그러한 타입도 동등성을 검사하기 위해서 반드시 비교를 구현해야 하며, 그에 따라 임의의 순서를 정의할 수밖에 없습니다.

또한 일부 객체 타입의 경우 동등성 검사가 순서 검사보다 훨씬 효율적으로 구현될 수 있습니다. 예를 들어 길이가 다른 리스트와 딕셔너리는 동등하지 않지만, 순서를 판단하려면 일부(경우에 따라 전체) 항목을 검사해야 합니다.

이전 작업

리치 비교는 이전에도 제안된 적이 있는데, 특히 David Ascher가 Numerical Python을 경험한 뒤 다음과 같이 제안했습니다:

이 내용은 아래 부록에도 포함되어 있습니다. 이 PEP에 담긴 내용의 대부분은 David의 제안에서 비롯된 것입니다.

우려 사항

  1. Python 레벨(__cmp__을 사용하는 클래스는 변경할 필요가 없음)과 C 레벨(tp_comparea를 정의하는 확장은 변경할 필요가 없으며, PyObject_Compare()를 사용하는 코드는 비교 대상 객체가 새로운 리치 비교 방식을 사용하더라도 동작해야 함) 양쪽 모두에서의 하위 호환성.
  2. A<B가 원소별 비교의 행렬을 반환할 때, 흔히 저지르기 쉬운 실수는 이 표현식을 불리언 컨텍스트에서 사용하는 것입니다. 특별한 주의 없이는 항상 참이 될 것입니다. 이런 사용은 대신 예외를 발생시켜야 합니다.
  3. 어떤 클래스가 x==y만 재정의하고 다른 것은 재정의하지 않는 경우, x!=y는 not(x==y)로 계산되어야 합니까, 아니면 실패해야 합니까? <와 >= 사이, 또는 >와 <= 사이의 유사한 관계는 어떻습니까?
  4. 마찬가지로, x<y를 y>x로부터 계산하도록 허용해야 합니까? 그리고 x<=y를 not(x>y)로부터? 그리고 x==y를 y==x로부터, 또는 x!=y를 y!=x로부터?
  5. 비교 연산자가 원소별 비교를 반환할 때, A<B<C, A<B and C<D, A<B or C<D와 같은 단축 연산자는 어떻게 처리해야 합니까?
  6. min()max(), ‘in’과 ‘not in’ 연산자, list.sort(), 딕셔너리 키 비교, 그리고 내장 연산에 의한 비교의 다른 사용은 어떻게 처리해야 합니까?

제안된 해결책

  1. 완전한 하위 호환성은 다음과 같이 달성할 수 있습니다. 객체가 tp_richcompare()는 정의하지 않고 tp_compare()만 정의한 상태에서 리치 비교가 요청되면, tp_compare()의 결과가 자명한 방식으로 사용됩니다. 예를 들어 “<”가 요청된 경우, tp_compare()가 예외를 발생시키면 예외가 발생하고, tp_compare()가 음수이면 결과는 1이며, 0이거나 양수이면 결과는 0입니다. 기타 등등.

    완전한 순방향 호환성은 다음과 같이 달성할 수 있습니다. tp_richcompare()를 구현하는 객체에 대해 클래식 비교가 요청되면, 최대 세 가지 비교가 사용됩니다: 먼저 ==을 시도하여 참을 반환하면 0을 반환하고, 다음으로 <를 시도하여 참을 반환하면 -1을 반환하며, 다음으로 >를 시도하여 참을 반환하면 +1을 반환합니다. 시도된 연산자 중 하나가 불리언이 아닌 값을 반환하면(아래 참조), 불리언으로의 변환에서 발생한 예외가 그대로 전달됩니다. 시도된 연산자 중 어느 것도 참을 반환하지 않으면, 다음으로 클래식 비교 폴백이 시도됩니다.

    (저는 세 가지 비교를 시도해야 하는 순서에 대해 오랫동안 깊이 고민했습니다. 한때는 순환 데이터 구조에 대한 비교 동작을 근거로 이 순서로 하는 것에 대한 설득력 있는 논거가 있었습니다. 하지만 그 코드가 다시 변경되었기 때문에, 이제는 그것이 차이를 만드는지 확신할 수 없습니다.)

  2. 단일 불리언 대신 불리언의 컬렉션을 반환하는 타입은 예외를 발생시키도록 nb_nonzero()를 정의해야 합니다. 이러한 타입은 비불리언으로 간주됩니다.
  3. ==와 != 연산자는 서로의 여집합이라고 가정되지 않습니다(예: IEEE 754 부동소수점 숫자는 이를 만족하지 않습니다). 원한다면 이를 구현하는 것은 타입에 달려 있습니다. <와 >=, 또는 >와 <=도 마찬가지이며, 이러한 가정이 성립하지 않는 예가 많습니다(예: tabnanny).
  4. 반사성 규칙은 Python에서 가정됩니다. 따라서 인터프리터는 y>x를 x<y로, y>=x를 x<=y로 바꿀 수 있으며, x==y와 x!=y의 인자를 바꿀 수도 있습니다. (참고: Python은 현재 x==x가 항상 참이고 x!=x가 결코 참이 아니라고 가정하지만, 이는 가정되어서는 안 됩니다.)
  5. 현재 제안에서 A<B가 원소별 비교 결과의 배열을 반환하면, 이 결과는 불리언이 아닌 것으로 간주되며, 단축 연산자가 이를 불리언으로 해석하려 하면 예외가 발생합니다. David Ascher의 제안은 이 문제를 다루려 하지만, 저는 이것이 코드 제너레이터에 추가되는 복잡성을 감수할 만한 가치가 있다고 생각하지 않습니다. A<B<C 대신 (A<B)&(B<C)와 같이 작성할 수 있습니다.
  6. min()list.sort() 연산은 < 연산자만 사용하며, max()는 > 연산자만 사용합니다. ‘in’과 ‘not in’ 연산자, 그리고 딕셔너리 조회는 == 연산자만 사용합니다.

구현 제안

이는 David Ascher의 제안을 충실히 따릅니다.

C API

  • 새 함수:
    PyObject *PyObject_RichCompare(PyObject *, PyObject *, int)
    

    이 함수는 요청된 리치 비교를 수행하여 파이썬 객체를 반환하거나 예외를 발생시킵니다. 세 번째 인자는 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()는 리치 비교(rich comparison)가 정의되어 있으면 이를 시도하도록 변경됩니다(단, 전통적인 비교가 정의되어 있지 않은 경우에만).

인터프리터 변경 사항

  • 특정 비교의 결과를 얻으려는 의도로 PyObject_Compare()가 호출될 때마다(예를 들어 list.sort()에서, 그리고 물론 ceval.c의 비교 연산자에서도), 해당 코드는 대신 PyObject_RichCompare() 또는 PyObject_RichCompareBool()을 호출하도록 변경됩니다. C 코드가 비교 결과를 알아야 하는 경우, 결과에 대해 PyObject_IsTrue()가 호출됩니다(예외가 발생할 수 있습니다).
  • 현재 비교를 정의하고 있는 대부분의 내장 타입은 대신 리치 비교를 정의하도록 수정될 것입니다. (이것은 선택 사항입니다. 저는 지금까지 리스트, 튜플, 복소수, 배열을 변환했으며, 다른 것들도 변환할지는 아직 확실하지 않습니다.)

클래스

  • 클래스는 해당 연산자를 오버라이드하기 위해 새로운 특수 메서드 __lt__, __le__, __eq__, __ne__, __gt__, __ge__를 정의할 수 있습니다. (즉, <, <=, ==, !=, >, >=입니다. Fortran의 유산은 정말 사랑스럽지 않을 수 없습니다.) 클래스가 __cmp__도 함께 정의하는 경우, 이는 __lt__ 등이 시도되어 NotImplemented를 반환한 경우에만 사용됩니다.

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

  1. 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.)

  2. 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.

  3. 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.