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

Python 개선 제안 한국어 번역

PEP 532 – 회로 차단 프로토콜과 이항 연산자

Author:
Alyssa Coghlan <ncoghlan at gmail.com>, Mark E. Haase <mehaase at gmail.com>
Status:
Deferred
Type:
Standards Track
Created:
30-Oct-2016
Python-Version:
3.8
Post-History:
05-Nov-2016

Table of Contents

번역·라이선스 안내

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

PEP 보류

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

초록

PEP 335, PEP 505, PEP 531 및 관련 논의에서 영감을 받아, 이 PEP는 다음 항목에 공통의 기반 의미론적 토대를 제공하는 새로운 회로 차단 프로토콜(메서드 이름 __then__과 __else__를 사용)의 정의를 제안합니다:

  • 조건 표현식: LHS if COND else RHS
  • 논리곱: LHS and RHS
  • 논리합: LHS or RHS
  • PEP 505에서 제안된 None 인식 연산자
  • PEP 535에서 제안된 풍부한 비교 연쇄 모델

또한 새 프로토콜을 활용하여, 조건 표현식의 정의를 수정해 if와 else를 각각 오른쪽 결합 및 왼쪽 결합 범용 단락 연산자로도 사용할 수 있도록 허용할 것을 제안합니다:

  • 오른쪽 결합 단락: LHS if RHS
  • 왼쪽 결합 단락: LHS else RHS

논리 반전(not EXPR)을 위의 변경 사항과 일관되게 만들기 위해, 새로운 논리 반전 프로토콜(메서드 이름 __not__을 사용)의 도입도 제안합니다.

서킷 브레이커를 생성하는 표현식을 두 번 평가하지 않고도 서킷 브레이커의 단락 평가를 강제할 수 있도록, 새로운 operator.short_circuit(obj) 헬퍼 함수가 operator(연산자) 모듈에 추가될 것입니다.

마지막으로, (제어 흐름을 결정하는 데 사용되는) 객체의 진릿값을 단락 평가된 서킷 브레이킹 표현식에서 객체가 반환하는 값과 분리하기 위해 새로운 표준 types.CircuitBreaker 타입을 제안하며, 특히 흔히 쓰이는 전환 관용구를 표현하기 위해 다음 팩토리 함수들을 operator(연산자) 모듈에 추가합니다:

  • bool(obj)에 따른 전환: operator.true(obj)
  • not bool(obj)에 따른 전환: operator.false(obj)
  • obj is value에 따른 전환: operator.is_sentinel(obj, value)
  • obj is not value에 따른 전환: operator.is_not_sentinel(obj, value)

다른 PEP와의 관계

이 PEP는 다른 제안들에서 오랫동안 이어져 온 작업을 기반으로 합니다. 주요 제안 중 일부를 아래에서 논의합니다.

PEP 531: 존재 확인 프로토콜

이 PEP는 PEP 531의 직접적인 후속 제안으로, 그곳에서 정의된 존재 확인 프로토콜과 새로운 ?then 및 ?else 구문 연산자를 새로운 회로 차단 프로토콜, 그리고 조건 표현식과 not 연산자에 대한 조정으로 대체합니다.

PEP 505: None 인식 연산자

이 PEP는 PEP 505의 None 인식 연산자 제안을 보완합니다. 이 PEP는 그 연산자들의 단락 동작을 조건 표현식의 특정 용법에 대한 고도로 최적화된 문법적 설탕으로 설명하는, 프로토콜 기반의 기저 의미 프레임워크를 제공합니다.

이 PEP에서 제안하는 변경 사항이 적용되면 다음과 같습니다:

  • LHS ?? RHS는 대략 is_not_sentinel(LHS, None) else RHS가 됩니다
  • EXPR?.attr은 대략 EXPR.attr if is_not_sentinel(EXPR, None)이 됩니다
  • EXPR?[key]는 대략 EXPR[key] if is_not_sentinel(EXPR, None)이 됩니다

세 경우 모두 전용 구문 형식은 서킷 브레이커 인스턴스를 실제로 생성하지 않고, 그 대신 기저의 제어 흐름을 직접 구현하도록 최적화될 것입니다. 뒤의 두 경우에는 구문 형식이 EXPR을 두 번 평가하는 것도 피하게 됩니다.

즉, None 인식 연산자는 여전히 고도로 특화되어 None에만 국한되지만, 다른 센티널 값은 이 PEP의 더 일반적인 프로토콜 기반 제안을 통해 계속 사용할 수 있습니다.

PEP 335: 오버로드 가능한 불리언 연산자

PEP 335는 단락 평가를 하는 and와 or 연산자를 직접 오버로드하는 기능을 제안했으며, 비교 연쇄의 의미를 오버로드하는 기능은 그 변경에 따른 결과 중 하나였습니다. 이 PEP의 이전 버전에서 제안했던, 비교 체이닝의 의미 정의를 바꾸는 방식으로 요소별 비교 사용 사례를 처리하자는 제안은 Guido가 PEP 335를 거부한 내용에서 직접 가져온 것입니다 [1].

그러나 이 PEP에 대한 초기 피드백에서 이 PEP가 다루는 제안의 수가 너무 많아 읽기 어렵다는 의견이 나왔으므로, 제안의 해당 부분은 PEP 535로 분리되었습니다.

PEP 535: 풍부한 비교 연쇄

앞서 언급했듯이 PEP 535는 이 PEP에서 정의하는 서킷 브레이킹 프로토콜을 기반으로 PEP 207에서 도입된 풍부한 비교 지원을 확장하여, LEFT_BOUND < VALUE < RIGHT_BOUND 같은 비교 연쇄 연산도 처리하도록 하자는 제안입니다.

명세

서킷 브레이킹 프로토콜(if-else)

조건 표현식(LHS if COND else RHS)은 현재 표현식 수준에서 다음과 동등한 것으로 해석됩니다:

if COND:
    _expr_result = LHS
else:
    _expr_result = RHS

이 PEP는 그 확장 방식을 변경하여, 검사되는 조건이 새로운 “회로 차단” 프로토콜을 구현할 수 있도록 하자고 제안합니다. 이 프로토콜을 통해 조건은 표현식의 한쪽 또는 양쪽 브랜치의 결과를 확인하고, 경우에 따라 변경할 수 있습니다.:

_cb = COND
_type_cb = type(cb)
if _cb:
    _expr_result = LHS
    if hasattr(_type_cb, "__then__"):
        _expr_result = _type_cb.__then__(_cb, _expr_result)
else:
    _expr_result = RHS
    if hasattr(_type_cb, "__else__"):
        _expr_result = _type_cb.__else__(_cb, _expr_result)

위에서 보듯이, 인터프리터 구현은 조건 표현식에서 실제로 실행되는 브랜치에 필요한 프로토콜 메서드에만 접근해야 합니다. 다른 프로토콜 메서드와 마찬가지로, 특수 메서드는 인스턴스에서 직접 조회하는 대신 서킷 브레이커의 타입을 통해 조회됩니다.

서킷 브레이킹 연산자(이항 if와 이항 else)

제안된 프로토콜의 이름은 조건 표현식의 의미에 대해 제안된 변경에서 비롯된 것이 아닙니다. 오히려 이 이름은 if와 else를 범용 프로토콜 기반 단락 평가 연산자로 추가하자는 제안에서 비롯된 것으로, 이 연산자들은 기존의 True 및 False 기반 단락 평가 연산자(각각 or 및 and)와 PEP 505에서 제안된 None 기반 단락 평가 연산자(??)를 보완합니다.

이 두 연산자를 합쳐 서킷 브레이킹 연산자라고 부르게 됩니다.

이러한 사용을 지원하기 위해 언어 문법의 조건 표현식 정의를 갱신하여 if 절과 else 절을 모두 선택 사항으로 만들 것입니다:

test: else_test ['if' or_test ['else' test]] | lambdef
else_test: or_test ['else' test]

컴파일러 구현이 일반 조건 표현식의 의미를 올바르게 보존하기 쉽도록, 얼핏 더 단순해 보이는 else_test ('if' else_test)* 형태는 피해야 한다는 점에 유의하십시오.

문법의 test_nocond 노드 정의(의도적으로 조건 표현식을 제외함)는 변경되지 않으므로, 서킷 브레이킹 연산자를 컴프리헨션과 제너레이터 표현식의 if 절에서 사용할 때는 조건 표현식 자체와 마찬가지로 괄호가 필요합니다.

이 문법 정의에 따라, 달리 정해 두지 않으면 모호했을 expr1 if cond else expr2 else expr3의 우선순위/결합성은 (expr1 if cond else expr2) else epxr3로 결정됩니다. 그러나 인터프리터가 어떻게 실행하든 그러한 구성은 본질적으로 독자에게 혼란을 주므로, PEP 8에 “그렇게 하지 마십시오”라는 지침도 추가될 것입니다.

그러면 우결합 서킷 브레이킹 연산자(LHS if RHS)는 다음과 같이 확장됩니다:

_cb = RHS
_expr_result = LHS if _cb else _cb

반면 좌결합 서킷 브레이킹 연산자(LHS else RHS)는 다음과 같이 확장됩니다:

_cb = LHS
_expr_result = _cb if _cb else RHS

두 경우 모두 주목해야 할 핵심은, 서킷 브레이킹 표현식이 단락될 때 조건이 서킷 브레이커인 경우가 아니라면 조건 표현식이 표현식의 결과로 사용된다는 점입니다. 후자의 경우에는 평소와 같이 적절한 서킷 브레이커 프로토콜 메서드가 호출되지만, 메서드 인자로는 서킷 브레이커 자신이 전달됩니다.

이를 통해 서킷 브레이커는 후보 표현식 결과로 전달된 인자가 self인 경우를 검사하여 단락을 확실하게 감지할 수 있습니다.

논리 반전(not) 오버로딩

모든 서킷 브레이커 정의에는 논리적 역이 존재합니다. 이 역도 여전히 서킷 브레이커이지만, 표현식 평가를 언제 단락할지에 대한 판단은 반대가 됩니다. 예를 들어, 이 PEP에서 제안하는 operator.true와 operator.false 서킷 브레이커는 서로의 논리적 역입니다.

새로운 프로토콜 메서드 __not__(self)가 도입됩니다. 이 메서드를 통해 회로 차단기와 그 밖의 타입은 not 표현식을 재정의하여, 강제 변환된 불리언 결과 대신 자신의 논리적 역을 반환할 수 있습니다.

기존 언어 최적화의 의미(예: 불리언 문맥에서 이중 부정을 중복으로 보고 바로 제거하는 것)를 보존하기 위해, __not__ 구현은 다음 불변 조건을 지켜야 합니다:

assert not bool(obj) == bool(not obj)

그러나 대칭 회로 차단기(__bool__, __not__, __then__, __else__를 모두 구현하는 것)는 표현식에 관여하는 모든 회로 차단기가 일관된 “참”의 정의를 사용할 때에만 불리언 논리의 완전한 의미를 지킬 것으로 기대됩니다. 이에 대해서는 Respecting De Morgan’s Laws에서 더 자세히 다룹니다.

단락 동작 강제하기

회로 차단기를 조건 표현식의 세 피연산자 모두로 사용하면 그 회로 차단기의 단락 동작 호출을 강제할 수 있습니다:

obj if obj else obj

또는 동등하게, 회로 차단 표현식의 두 피연산자 모두로 사용할 수도 있습니다:

obj if obj
obj else obj

이 PEP는 이러한 패턴 중 어느 것을 사용하도록 요구하는 대신, operator에 전용 함수를 추가할 것을 제안합니다. 이 함수는 회로 차단기를 명시적으로 단락시키고, 다른 객체는 수정 없이 그대로 통과시킵니다:

def short_circuit(obj)
    """Replace circuit breakers with their short-circuited result

    Passes other input values through unmodified.
    """
    return obj if obj else obj

회로 차단 동일성 비교(is와 is not)

표준 회로 차단기가 전혀 없다면, 제안된 if와 else 연산자는 대체로 기존 and와 or 논리 연산자를 색다르게 표기한 것에 불과할 것입니다.

그러나 이 PEP는 더 나아가 적절한 단락 논리를 구현하는 새로운 범용 types.CircuitBreaker 타입을 제공할 것을 제안합니다. 아울러 is와 is not 연산자에 대응하는 팩토리 함수도 operator 모듈에 제공할 것을 제안합니다.

이들은 조건 검사가 실패할 때 다음 표현식이 False 대신 VALUE를 생성하도록 정의됩니다:

EXPR if is_sentinel(VALUE, SENTINEL)
EXPR if is_not_sentinel(VALUE, SENTINEL)

마찬가지로, 조건 검사가 성공할 때 다음 표현식은 True 대신 VALUE를 생성합니다:

is_sentinel(VALUE, SENTINEL) else EXPR
is_not_sentinel(VALUE, SENTINEL) else EXPR

사실상 이러한 비교는 다음 형태의 표현식에서 앞쪽의 VALUE if 절과 뒤쪽의 else VALUE 절을 암시된 것으로 보고 생략할 수 있도록 정의됩니다:

# To handle "if" expressions, " else VALUE" is implied when omitted
EXPR if is_sentinel(VALUE, SENTINEL) else VALUE
EXPR if is_not_sentinel(VALUE, SENTINEL) else VALUE
# To handle "else" expressions, "VALUE if " is implied when omitted
VALUE if is_sentinel(VALUE, SENTINEL) else EXPR
VALUE if is_not_sentinel(VALUE, SENTINEL) else EXPR

제안된 types.CircuitBreaker 타입은 이 동작을 프로그래밍 방식으로 다음과 같이 표현합니다:

class CircuitBreaker:
    """Simple circuit breaker type"""
    def __init__(self, value, bool_value):
        self.value = value
        self.bool_value = bool(bool_value)
    def __bool__(self):
        return self.bool_value
    def __not__(self):
        return CircuitBreaker(self.value, not self.bool_value)
    def __then__(self, result):
        if result is self:
            return self.value
        return result
    def __else__(self, result):
        if result is self:
            return self.value
        return result

이러한 회로 차단기의 핵심 특징은 일시적이라는 점입니다. 단락이 일어났다는 사실을 전달받으면(후보 표현식 결과로 자기 자신에 대한 참조를 받으면), 이들은 회로 차단 래퍼가 아니라 원래 값을 반환합니다.

단락 감지는 다음과 같이 정의됩니다. 동일한 회로 차단기 인스턴스를 회로 차단 연산자의 양쪽에 명시적으로 전달하거나, 하나의 인스턴스를 조건 표현식의 세 피연산자 모두로 사용하면 래퍼가 항상 제거됩니다:

breaker = types.CircuitBreaker(foo, foo is None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo
breaker = types.CircuitBreaker(foo, foo is not None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo

그러면 operator 모듈의 팩토리 함수를 사용하여 is와 is not 연산자를 이용한 동일성 검사에 대응하는 회로 차단기를 간단히 만들 수 있습니다:

def is_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is sentinel'"""
    return types.CircuitBreaker(value, value is sentinel)

def is_not_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is not sentinel'"""
    return types.CircuitBreaker(value, value is not sentinel)

참 검사 비교

and와 or 연산자의 바탕이 되는 런타임 논리는 단락이라는 특성 때문에 지금까지 operator나 types 모듈을 통해 접근할 수 없었습니다.

회로 차단 연산자와 회로 차단기를 도입하면 그 논리를 다음과 같이 operator 모듈에 담아낼 수 있습니다:

def true(value):
    """Returns a circuit breaker switching on 'bool(value)'"""
    return types.CircuitBreaker(value, bool(value))

def false(value):
    """Returns a circuit breaker switching on 'not bool(value)'"""
    return types.CircuitBreaker(value, not bool(value))
  • LHS or RHS는 사실상 true(LHS) else RHS와 같습니다
  • LHS and RHS는 사실상 false(LHS) else RHS와 같습니다

이 연산자들의 정의에는 실제로 아무런 변경도 일어나지 않습니다. 새로운 회로 차단 프로토콜과 연산자는 검사의 방향을 개발 시점에 하드코딩하는 대신, 제어 흐름 논리를 프로그래밍할 수 있게 하는 방법을 제공할 뿐입니다.

불리언 논리의 규칙을 따르면, 대신 오른쪽 결합 회로 차단 연산자를 사용하여 이 표현식들을 반전된 형태로 전개할 수도 있습니다:

  • LHS or RHS는 사실상 RHS if false(LHS)와 같습니다
  • LHS and RHS는 사실상 RHS if true(LHS)와 같습니다

None 인식 연산자

이 PEP와 PEP 505의 None 인식 연산자가 모두 채택된다면, 제안된 is_sentinel과 is_not_sentinel 회로 차단기 팩토리는 “None 검사”라는 개념을 캡슐화하는 데 사용됩니다. None 검사란 값이 None인지 확인한 뒤, 대체 값으로 넘어가거나(“None 병합”이라고 하는 연산) 그 값을 전체 표현식의 결과로 그대로 통과시키는(“None 절단” 또는 “None 전파”라고 하는 연산) 것을 말합니다.

이러한 회로 차단기가 있으면, LHS ?? RHS는 대략 다음 두 표현식 모두와 동등합니다:

  • is_not_sentinel(LHS, None) else RHS
  • RHS if is_sentinel(LHS, None)

None 인식 속성 접근과 None 인식 서브스크립션은 속성 조회 및 서브스크립션 연산에 제어 흐름을 주입하는 방식 때문에 회로 차단 연산자로 직접 표현할 수는 없지만, 그 기반이 되는 회로 차단 프로토콜로는 여전히 정의할 수 있습니다.

이러한 관점에서, EXPR?.ATTR[KEY].SUBATTR()는 의미상 다음과 동등합니다:

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base.ATTR[KEY].SUBATTR() if _circuit_breaker

마찬가지로, EXPR?[KEY].ATTR.SUBATTR()는 의미상 다음과 동등합니다:

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base[KEY].ATTR.SUBATTR() if _circuit_breaker

None 인식 연산자의 실제 구현은 아마도 회로 차단기 인스턴스를 실제로 생성하는 과정을 건너뛰도록 최적화되겠지만, 위의 전개는 런타임에 관찰할 수 있는 연산자 동작을 여전히 정확하게 기술합니다.

풍부한 연쇄 비교

이 가능한 사용 사례에 대한 자세한 논의는 PEP 535를 참조하십시오.

기타 조건부 구문

if 문, while 문, 컴프리헨션, 제너레이터 표현식에 대해서는 아무런 변경도 제안하지 않습니다. 이들에 포함된 불리언 절은 전적으로 제어 흐름 목적으로만 사용되며 그 자체로 결과를 반환하는 일이 없기 때문입니다.

다만, 이러한 제안은 이 PEP의 범위를 벗어나지만, 여기서 정의한 회로 차단 프로토콜만으로도 다음과 같은 구문을 지원하기에 이미 충분하다는 점은 주목할 만합니다:

def is_not_none(obj):
    return is_sentinel(obj, None)

while is_not_none(dynamic_query()) as result:
    ... # Code using result

그리고:

if is_not_none(re.search(pattern, text)) as match:
    ... # Code using match

이는 CONDITION을 주어진 이름에 직접 대입하는 대신, operator.short_circuit(CONDITION)의 결과를 as 절에 주어진 이름에 대입함으로써 구현할 수 있습니다.

스타일 가이드 권고 사항

이 PEP에서 도입하는 새 기능과 관련하여 PEP 8에 다음 내용을 추가할 것을 제안합니다:

  • 조건 표현식(if-else)과 독립형 회로 차단 연산자(if 및 else)를 하나의 표현식 안에서 함께 사용하지 마십시오. 상황에 따라 둘 중 하나만 사용하고, 둘을 함께 사용하지는 마십시오.
  • 조건 표현식(if-else)과 독립형 회로 차단 연산자(if 및 else)를 if 문의 if 조건이나 컴프리헨션 및 제너레이터 표현식의 필터 절의 일부로 사용하지 마십시오.

근거

새 연산자 추가

PEP 335와 마찬가지로, 이 PEP의 초기 초안은 새 연산자를 제안하기보다는 기존 and와 or 연산자의 해석을 덜 경직되게 만드는 데 초점을 맞추었습니다. 그러나 이 방식은 몇 가지 핵심적인 이유로 문제가 있는 것으로 드러났습니다:

  • and와 or 연산자는 오랫동안 확립되어 온 안정적인 의미를 지니고 있으므로, 이제 그 의미가 왼쪽 피연산자의 타입에 따라 달라진다면 읽는 사람은 필연적으로 놀라게 될 것입니다. 이 연산자들에 대해 현재 잘 알려진 의미 체계를 전제로 하는 25년 이상 축적된 교육 자료 때문에, 새로운 사용자조차도 이 변경으로 혼란을 겪을 것입니다
  • CPython을 포함한 Python 인터프리터 구현은 런타임 및 컴파일 타임 최적화를 정의할 때 and와 or의 기존 의미를 활용해 왔으며, 이러한 연산의 의미가 바뀌면 이 최적화들을 모두 검토하고 경우에 따라 폐기해야 합니다.
  • 프로토콜을 정의하는 데 필요한 새 메서드에 어떤 이름이 적절할지 명확하지 않습니다

대신 기존 if-else 삼항 연산자의 단락 평가 이항 변형을 제안하면 이 모든 문제가 해결됩니다:

  • and와 or의 런타임 의미 체계는 전혀 바뀌지 않습니다
  • 단항 not 연산자의 의미 체계는 바뀌지만, __not__ 구현에 요구되는 불변 조건 덕분에 불리언 문맥에서의 기존 표현식 최적화는 여전히 유효합니다.
  • __else__는 뒤따르는 else 절이 없으므로 if 표현식의 단락 평가 결과가 됩니다
  • __then__은 앞에 if 절이 없기 때문에 else 표현식의 단락 결과가 됩니다(메서드 이름이 __if__였다면 이 연결 관계가 더욱 분명했겠지만, 회로 차단 프로토콜을 호출하지 않는 if 키워드의 다른 용법들을 고려하면 모호해집니다).

연산자와 프로토콜의 이름 짓기

“회로 차단 연산자”, “회로 차단 프로토콜”, “회로 차단기”라는 이름은 모두 “단락 평가 연산자(short circuiting operator)”라는 표현에서 영감을 받았습니다. 이는 오른쪽 피연산자를 조건부로만 평가하는 연산자를 가리키는 일반적인 언어 설계 용어입니다.

전기적 비유로 설명하면, 전기 시스템의 회로 차단기가 장비를 손상시키거나 사람에게 해를 입히기 전에 단락을 감지하고 처리하는 것과 마찬가지로, Python의 회로 차단기는 표현식에서 예외가 발생하기 전에 단락을 감지하고 처리합니다.

Python 수준의 비유로 설명하면, break 문이 루프가 자연스럽게 끝나기 전에 루프를 종료할 수 있게 해 주는 것처럼, 회로 차단 표현식은 표현식의 평가를 종료하고 즉시 결과를 내놓을 수 있게 해 줍니다.

기존 키워드 사용

기존 키워드를 사용하면 __future__ 문 없이도 새 연산자를 도입할 수 있다는 이점이 있습니다.

if와 else는 제안된 새 프로토콜에 의미상 적합하며, 새로 도입되는 유일한 문법적 모호성은 새 연산자를 명시적인 if-else 조건 표현식 문법과 결합할 때 발생합니다.

이 PEP는 인터프리터 구현자가 이 모호성을 어떻게 처리해야 하는지 명시적으로 규정하여 이 문제를 다룹니다. 다만 인터프리터는 이를 이해하더라도 사람인 독자는 아마 이해하지 못할 것입니다. 따라서 조건 표현식과 회로 차단 연산자를 하나의 표현식 안에서 함께 사용하는 것은 좋지 않다는 점을 PEP 8에 명시할 것을 제안합니다.

프로토콜 메서드 이름 짓기

__else__ 메서드의 이름은 쉽게 정할 수 있었습니다. 연산자 키워드 이름을 재사용하면 명확하고 모호하지 않은 특수 메서드 이름이 되기 때문입니다.

__then__ 메서드의 이름은 그만큼 쉽게 정할 수 없었습니다. 키워드에 기반한 이름인 __if__를 쓰는 선택지도 있었기 때문입니다.

__if__의 문제는 if 키워드가 나타나고 그 바로 오른쪽에 표현식이 오는데도 __if__ 특수 메서드는 호출되지 않는 경우가 여전히 많이 남는다는 점입니다. 그런 경우에는 bool() 내장 함수와 그 기반이 되는 특수 메서드(__bool__, __len__)가 대신 호출되며, __if__는 아무 효과도 없습니다.

불리언 프로토콜이 이미 조건 표현식과 새 회로 차단 프로토콜에서 한 가지 역할을 맡고 있습니다. 그래서 컴퓨터 과학과 프로그래밍 언어 설계에서 if 문의 첫 번째 절을 가리킬 때 흔히 쓰는 용어를 따라, 덜 모호한 이름인 __then__을 선택했습니다.

이항 if를 오른쪽 결합으로 만들기

조건 표현식이 세운 선례에 따라, 이항 단락 평가 if 표현식은 일관성을 위해 반드시 조건을 오른쪽에 두어야 합니다.

오른쪽 피연산자가 항상 먼저 평가되고, 오른쪽 피연산자가 불리언 문맥에서 참이면 왼쪽 피연산자는 아예 평가되지 않습니다. 따라서 이 연산자는 자연스럽게 오른쪽 결합 연산자가 됩니다.

표준 회로 차단기 이름 짓기

왼쪽 결합 회로 차단 연산자와만 함께 사용할 때는, 단항 검사를 위한 명시적인 회로 차단기 이름이 전치사 if_로 시작하면 자연스럽게 읽힙니다:

operator.if_true(LHS) else RHS
operator.if_false(LHS) else RHS

하지만 논리 반전을 할 때는 if_를 넣으면 그만큼 자연스럽게 읽히지 않습니다:

not operator.if_true(LHS) else RHS
not operator.if_false(LHS) else RHS

오른쪽 결합 회로 차단 연산자를 사용할 때도 마찬가지입니다:

LHS if operator.if_true(RHS)
LHS if operator.if_false(RHS)

이항 비교 연산의 이름을 지을 때도 마찬가지입니다:

operator.if_is_sentinel(VALUE, SENTINEL) else EXPR
operator.if_is_not_sentinel(VALUE, SENTINEL) else EXPR

반면 회로 차단기 이름에서 전치사를 빼면 단항 검사에서는 모든 형태가 꽤 자연스럽게 읽힙니다:

operator.true(LHS) else RHS       # Preceding "LHS if " implied
operator.false(LHS) else RHS      # Preceding "LHS if " implied
not operator.true(LHS) else RHS   # Preceding "LHS if " implied
not operator.false(LHS) else RHS  # Preceding "LHS if " implied
LHS if operator.true(RHS)         # Trailing " else RHS" implied
LHS if operator.false(RHS)        # Trailing " else RHS" implied
LHS if not operator.true(RHS)     # Trailing " else RHS" implied
LHS if not operator.false(RHS)    # Trailing " else RHS" implied

이항 검사에서도 자연스럽게 읽힙니다:

operator.is_sentinel(VALUE, SENTINEL) else EXPR
operator.is_not_sentinel(VALUE, SENTINEL) else EXPR
EXPR if operator.is_sentinel(VALUE, SENTINEL)
EXPR if operator.is_not_sentinel(VALUE, SENTINEL)

위험과 우려 사항

이 PEP는 PEP 335, 505, 531을 논의할 때 제기된 위험과 우려 사항을 해결하도록 특별히 설계되었습니다.

  • 기존 and와 or 연산자를 건드리지 않고, 새 연산자를 정의하며 (별도의 PEP에서) 연쇄 비교의 정의를 조정합니다
  • 제안된 새 연산자는 None과의 동일성 검사에만 경직되게 초점을 맞추지 않습니다. 이 연산자는 기존 and와 or의 의미까지 표현할 수 있는 범용 단락 평가 이항 연산자입니다
  • not 단항 연산자와 is, is not 이항 비교 연산자에 대한 변경은 기존 의미에 기반한 제어 흐름 최적화가 계속 유효하도록 정의됩니다

이 접근 방식을 택한 결과, 이 PEP는 그 자체만으로는 최종 사용자에게 주는 직접적인 이점이 별로 없습니다. 특정 형태의 조건 표현식에서 흔히 쓰는 None if 접두사와 else None 접미사를 생략할 수 있게 해 주는 정도입니다.

그 대신 이 PEP가 주로 제공하는 것은 공통 기반입니다. 이를 통해 PEP 505의 None 인식 연산자 제안과 PEP 535의 풍부한 비교 체이닝 제안을, 조건 표현식 및 기존 and와 or 연산자와도 공유되는 공통의 기저 의미 프레임워크 위에서 추진할 수 있습니다.

설계 논의

프로토콜 따라가 보기

다음 다이어그램은 회로 차단 프로토콜의 핵심 개념을 보여 줍니다(다만 특수 메서드를 인스턴스가 아닌 타입을 통해 조회한다는 기술적 세부 사항은 생략합니다):

삼항 표현식에 적용된 회로 차단 프로토콜의 다이어그램

다음 표현식을 단계별로 살펴보겠습니다:

>>> def is_not_none(obj):
...     return operator.is_not_sentinel(obj, None)
>>> x if is_not_none(data.get("key")) else y

is_not_none은 제안된 operator.is_not_sentinel types.CircuitBreaker 팩토리를 None을 센티널 값으로 하여 호출하는 헬퍼 함수입니다. data는 알 수 없는 키로 get() 메서드를 호출하면 None을 반환하는 컨테이너(예: 내장 dict 인스턴스)입니다.

예제를 다시 작성하여 서킷 브레이커 인스턴스에 이름을 붙일 수 있습니다:

>>> maybe_value = is_not_none(data.get("key"))
>>> x if maybe_value else y

여기서 maybe_value 서킷 브레이커 인스턴스는 다이어그램의 breaker에 해당합니다.

삼항 조건은 bool(maybe_value)를 호출하여 평가되며, 이는 Python의 기존 동작과 같습니다. 달라지는 점은 서킷 브레이킹 프로토콜이 피연산자 x나 y 중 하나를 직접 반환하는 대신, 해당 피연산자를 조건에 사용된 서킷 브레이커에 전달한다는 것입니다.

bool(maybe_value)가 True로 평가되면(즉, 요청한 키가 존재하고 그 값이 None이 아니면) 인터프리터는 type(maybe_value).__then__(maybe_value, x)를 호출합니다. 그렇지 않으면 type(maybe_value).__else__(maybe_value, y)를 호출합니다.

이 프로토콜은 새로운 if와 else 이항 연산자에도 적용되지만, 이 경우 인터프리터에는 빠진 세 번째 피연산자를 나타낼 방법이 필요합니다. 인터프리터는 서킷 브레이커 자신을 그 역할에 재사용하여 이를 해결합니다.

다음 두 표현식을 살펴보십시오:

>>> x if data.get("key") is None
>>> x if operator.is_sentinel(data.get("key"), None)

이 표현식의 첫 번째 형태는 data.get("key") is None이면 x를 반환하지만, 그렇지 않으면 False를 반환하는데, 이는 거의 확실히 우리가 원하는 결과가 아닙니다.

반면, 이 표현식의 두 번째 형태는 data.get("key") is None이면 마찬가지로 x를 반환하지만, 그렇지 않으면 data.get("key")를 반환하며, 이는 훨씬 더 유용한 동작입니다.

이 동작은 명시적으로 이름을 붙인 서킷 브레이커 인스턴스를 사용하는 삼항 표현식으로 다시 작성해 보면 이해할 수 있습니다:

>>> maybe_value = operator.is_sentinel(data.get("key"), None)
>>> x if maybe_value else maybe_value

bool(maybe_value)가 True이면(즉, data.get("key")가 None이면) 인터프리터는 type(maybe_value).__then__(maybe_value, x)를 호출합니다. types.CircuitBreaker.__then__의 구현은 단락 평가가 일어났음을 나타내는 것을 전혀 발견하지 못하므로 x를 반환합니다.

반면, bool(maybe_value)가 False이면(즉, data.get("key")가 None이 아니면) 인터프리터는 type(maybe_value).__else__(maybe_value, maybe_value)를 호출합니다. types.CircuitBreaker.__else__의 구현은 인스턴스 메서드가 자기 자신을 인자로 받았음을 감지하고, 서킷 브레이커 대신 감싸고 있는 값(즉, data.get("key"))을 반환합니다.

else에도 같은 논리가 방향만 반대로 적용됩니다:

>>> is_not_none(data.get("key")) else y

이 표현식은 data.get("key")가 None이 아니면 그 값을 반환하고, 그렇지 않으면 y를 평가하여 반환합니다. 동작 원리를 이해하기 위해 표현식을 다음과 같이 다시 작성합니다:

>>> maybe_value = is_not_none(data.get("key"))
>>> maybe_value if maybe_value else y

bool(maybe_value)가 True이면 표현식이 단락 평가되고, 인터프리터는 type(maybe_value).__else__(maybe_value, maybe_value)를 호출합니다. types.CircuitBreaker.__then__의 구현은 인스턴스 메서드가 자기 자신을 인자로 받았음을 감지하고, 서킷 브레이커 대신 감싸고 있는 값(즉, data.get("key"))을 반환합니다.

bool(maybe_value)가 True이면 인터프리터는 type(maybe_value).__else__(maybe_value, y)를 호출합니다. types.CircuitBreaker.__else__의 구현은 단락 평가가 일어났음을 나타내는 것을 전혀 발견하지 못하므로 y를 반환합니다.

드모르간의 법칙 준수

and와 or와 마찬가지로, 이항 단락 평가 연산자를 사용하면 본질적으로 같은 표현식을 여러 방식으로 작성할 수 있습니다. 이처럼 겉보기에 중복되어 보이는 것은 안타깝게도 프로토콜을 완전한 불리언 대수로 정의한 데 따른 필연적인 결과입니다. 불리언 대수는 “드 모르간의 법칙”이라고 알려진 한 쌍의 성질, 즉 and와 or 연산의 결과를 서로와 적절한 not 연산의 조합으로 표현할 수 있다는 성질을 따르기 때문입니다.

Python의 and와 or에 대해서는 이러한 불변 조건을 다음과 같이 기술할 수 있습니다.:

assert bool(A and B) == bool(not (not A or not B))
assert bool(A or B) == bool(not (not A and not B))

즉, 한 연산자를 택해 두 피연산자를 모두 반전시키고, 다른 연산자로 바꾼 다음, 전체 결과를 반전시키면 원래 연산자에서 얻은 것과 (불리언 의미에서) 같은 답을 얻게 됩니다. (이는 중복처럼 보일 수 있지만, 실제로 많은 상황에서 이중 부정을 제거하고 항진적으로 참이거나 거짓인 부분 표현식을 찾아내어 전체 표현식의 크기를 줄일 수 있게 해 줍니다.)

회로 차단기의 경우, 단락될 때 표현식 결과에서 자기 자신을 제거하도록 설계되는 경우가 많다는 점 때문에 적절한 불변 조건을 정의하기가 복잡해지는데, 이는 본질적으로 비대칭적인 동작입니다. 따라서 드모르간의 법칙을 대칭적인 회로 차단기의 예상 동작에 매핑할 때는 이러한 본질적인 비대칭성을 고려해야 합니다.

이 복잡성을 해결하는 한 가지 방법은 단락될 피연산자를 operator.true로 감싸는 것입니다. 이렇게 하면 전체 결과에 bool이 적용될 때, 회로 차단기의 입력 값에 bool을 직접 적용하는 대신 어느 브랜치를 평가할지 결정하는 데 사용된 것과 같은 참의 정의를 사용하게 됩니다.

구체적으로, 새 단락 평가(short-circuiting) 연산자의 경우 __bool__과 __not__을 모두 구현하면서 올바르게 동작하는 대칭 회로 차단기라면 다음 속성이 성립할 것으로 합리적으로 기대할 수 있습니다.:

assert bool(B if true(A)) == bool(not (true(not A) else not B))
assert bool(true(A) else B) == bool(not (not B if true(not A)))

우변의 연산 순서(입력 회로 차단기를 반전한 후에 true를 적용함)에 주목하십시오. 이 순서 덕분에 단언은 단지 type(true(A)).__not__의 동작에 관한 것이 아니라 실제로 type(A).__not__에 관한 것이 됩니다.

최소한 types.CircuitBreaker 인스턴스는 이 논리를 따를 것이므로, 기존의 불리언 표현식 최적화(이중 부정 제거 등)를 계속 적용할 수 있습니다.

임의의 센티널 객체

PEP 505 및 531과 달리, 이 PEP의 제안은 사용자 정의 센티널 객체를 손쉽게 처리합니다.:

_MISSING = object()

# Using the sentinel to check whether or not an argument was supplied
def my_func(arg=_MISSING):
    arg = make_default() if is_sentinel(arg, _MISSING) # "else arg" implied

회로 차단 표현식에서 암시적으로 정의되는 회로 차단기

공개되지 않은 이 PEP의 한 초안에서는 is와 is not 이항 연산자를 특별히 취급하여, 회로 차단 표현식의 맥락에서 사용될 때 자동으로 회로 차단기로 취급되도록 하는 아이디어를 검토했습니다. 안타깝게도 이 접근 방식은 필연적으로 다음과 같이 매우 바람직하지 않은 두 가지 결과 중 하나로 이어진다는 것이 밝혀졌습니다.

  1. 이러한 표현식의 반환 타입이 bool에서 types.CircuitBreaker로 일괄 변경되어 하위 호환성 문제가 생길 수 있습니다(특히 전달된 값을 PyObject_IsTrue에 통과시키거나 인자 파싱 함수의 p (predicate) 형식을 사용하는 대신, PyBool_Check로 내장 불리언 값인지를 명시적으로 확인하는 확장 모듈 API를 다룰 때 그렇습니다)
  2. 이러한 표현식의 반환 타입이 문맥 의존적이 되어, 다른 일상적인 리팩터링(비교 연산을 지역 변수로 추출하는 것 등)이 코드의 런타임 의미에 상당한 영향을 미칠 수 있습니다

이 PEP의 제안으로는 두 결과 중 어느 쪽도 정당화되기 어려워 보였으므로, 회로 차단기 인스턴스는 반드시 API 호출을 통해 명시적으로 생성해야 하며 암시적으로는 절대 생성되지 않는 현재 설계로 되돌아갔습니다.

구현

PEP 505와 마찬가지로, 이러한 변경을 하자는 아이디어에 원칙적인 관심이 모일 때까지 실제 구현은 보류되었습니다.

…미정…

감사의 글

이 PEP의 초기 초안에 대해 상세한 비평 [2]을 해 주어 두 번째 초안의 많은 변경에 영감을 준 Steven D’Aprano와, 해당 토론 스레드 [3]에 참여한 다른 모든 분께 감사드립니다.

참고 문헌