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

Python 개선 제안 한국어 번역

PEP 535 – 풍부한 비교 연쇄

Author:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Deferred
Type:
Standards Track
Requires:
532
Created:
12-Nov-2016
Python-Version:
3.8

Table of Contents

번역·라이선스 안내

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

PEP 보류

이 PEP에 대한 추가 검토는 빨라도 Python 3.8까지 연기되었습니다.

초록

이 PEP는 PEP 335에서 영감을 받았고 PEP 532에 설명된 회로 차단 프로토콜을 바탕으로, 연쇄 비교의 정의를 변경할 것을 제안합니다. 이에 따라 왼쪽 비교가 그 결과로 회로 차단기를 반환하면, 비교 연쇄는 논리합 연산자 (and) 대신 좌결합 회로 차단 연산자(else)를 사용하도록 갱신됩니다.

현재 NumPy가 단일 값 배열을 처리하는 방식 때문에 실제로는 몇 가지 복잡한 문제가 있지만, 이 변경만으로도 행렬에 대한 요소별 연쇄 비교 연산이 충분히 가능해질 것입니다. 이때 결과는 ValueError를 발생시키거나 (비어 있지 않은 행렬임을 나타내는) True를 항진적으로 반환하는 대신, 불리언 값의 행렬이 됩니다.

다른 PEP와의 관계

이 PEP는 PEP 532의 이전 버전들에서 분리되었습니다. 회로 차단 프로토콜을 도입하는 데 필수적인 부분이 아니라 그 프로토콜의 후속 사용 사례이기 때문입니다.

비교 연쇄의 의미론적 정의를 변경하여 요소별 비교 사용 사례를 처리하자는 이 PEP의 구체적인 제안은 Guido가 PEP 335를 거부하면서 한 말에서 직접 가져온 것입니다.

명세

다음과 같이 작성된 0 < x < 10 같은 연쇄 비교는:

LEFT_BOUND LEFT_OP EXPR RIGHT_OP RIGHT_BOUND

현재 의미상 대략 다음과 동등합니다:

_expr = EXPR
_lhs_result = LEFT_BOUND LEFT_OP _expr
_expr_result = _lhs_result and (_expr RIGHT_OP RIGHT_BOUND)

이 PEP는 PEP 532에서 도입된 회로 차단 개념을 사용하여, 비교 연쇄가 왼쪽 비교에서 회로 차단기가 반환되는지 명시적으로 검사하고, 그렇다면 and 대신 else를 사용해 비교 연쇄를 구현하도록 변경할 것을 제안합니다:

_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)

이렇게 하면 NumPy 배열 같은 타입이 비교 연산에서 적절히 정의된 회로 차단기를 반환하여 연쇄 비교의 동작을 제어할 수 있습니다.

이 로직을 임의 개수의 연쇄 비교 연산으로 확장하는 방식은 and에 대한 기존 확장 방식과 같습니다.

근거

Guido van Rossum은 PEP 335를 최종적으로 거부하면서 다음과 같이 언급했습니다 [1]:

NumPy 개발자들은 다소 별개의 문제를 제기했습니다. 그들에게 가장 흔한 사용 사례는 연쇄 비교(예: A < B < C)입니다.

이 지적을 이해하려면 먼저 NumPy 배열에서 비교가 어떻게 동작하는지 살펴보아야 합니다:

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

여기서 NumPy 배열 비교는 기본적으로 요소별로 이루어진다는 것을 알 수 있습니다. 즉, 왼쪽 배열의 각 요소를 오른쪽 배열의 대응하는 요소와 비교하여 불리언 결과의 행렬을 만들어 냅니다.

비교의 어느 한쪽이 스칼라 값이면, 그 값은 배열 전체에 브로드캐스트되어 각 개별 요소와 비교됩니다:

>>> 0 < increasing
array([False,  True,  True,  True,  True], dtype=bool)
>>> increasing < 4
array([ True,  True,  True,  True, False], dtype=bool)

그러나 연쇄 비교를 사용하려고 하면 이 브로드캐스팅 관용구가 깨집니다:

>>> 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()

문제는 Python이 내부적으로 이 연쇄 비교를 암묵적으로 다음과 같은 형태로 확장한다는 점입니다:

>>> 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()

그리고 NumPy는 a.any()와 a.all()이 같은 결과를 가진다고 보장할 수 있는 단일 요소 배열에 대해서만 불리언 값으로의 암묵적 강제 변환을 허용합니다:

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

이 PEP의 제안을 따르면 NumPy의 요소별 비교 연산 정의를 갱신하여 이 상황을 바꿀 수 있습니다. 즉, 새로운 회로 차단 프로토콜을 구현하는 전용 서브클래스를 반환하도록 하고, 불리언 문맥에서 결과 배열을 해석할 때 항상 False를 반환하도록 바꾸어 단락 평가 동작이 절대 일어나지 않게 합니다:

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

이 변경을 적용하면 위의 연쇄 비교 예제는 다음을 반환할 수 있게 됩니다:

>>> 0 < increasing < 4
ComparisonResultArray([ False,  True,  True,  True, False], dtype=bool)

구현

실제 구현은 PEP 532에서 제안된 변경 사항을 적용하자는 아이디어에 원칙적인 관심이 모일 때까지 연기되었습니다.

…미정…

참고 문헌