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는 PEP 335, PEP 505, PEP 531 및 관련 논의에서 영감을 받아, __then____else__라는 메서드 이름을 사용하는 새로운 회로 차단 프로토콜의 정의를 제안하며, 이는 다음에 대한 공통적인 기저 의미 기반을 제공합니다:

  • 조건식: LHS if COND else RHS
  • 논리곱: LHS and RHS
  • 논리합: LHS or RHS
  • None 인식 연산자(PEP 505에서 제안됨)
  • 리치 비교 연결 모델(PEP 535에서 제안됨)

새로운 프로토콜을 활용하여, 조건식의 정의를 개정하여 ifelse를 각각 오른쪽 결합 및 왼쪽 결합의 범용 단락 연산자로도 사용할 수 있도록 허용할 것을 추가로 제안합니다:

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

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

회로 차단기(circuit breaker)를 생성하는 표현식을 두 번 평가하지 않고도 그 단락을 강제하기 위해, operator 모듈에 새로운 operator.short_circuit(obj) 헬퍼 함수가 추가될 것입니다.

마지막으로, 객체의 진리값(제어 흐름을 결정하는 데 사용되는 값)을 단락된 회로 차단 표현식이 반환하는 값과 분리하기 위해 새로운 표준 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 인식 연산자 제안을 보완하며, 조건식의 특정 용법에 대한 고도로 최적화된 구문 설탕으로서 그 단락 평가(short-circuiting) 동작을 설명하는 프로토콜 기반의 기저 의미 체계 프레임워크를 제공합니다.

이 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에 특정한 채로 남아있는 반면, 다른 sentinel 값들은 이 PEP의 더 일반적인 프로토콜 기반 제안을 통해 여전히 사용 가능함을 의미합니다.

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

PEP 335는 단락 평가(short-circuiting)를 하는 andor 연산자를 직접 오버로드하는 기능을 제안했으며, 비교 연쇄(comparison chaining)의 의미를 오버로드하는 기능은 그 변경의 결과 중 하나였습니다. 비교 연쇄의 의미론적 정의를 변경하여 요소별 비교(element-wise comparison) 사용 사례를 대신 처리하려던 이 PEP의 이전 버전의 제안은 Guido의 PEP 335 [1] 거부에서 직접 비롯된 것입니다.

하지만 이 PEP에 대한 초기 피드백은 이 PEP가 다루는 서로 다른 제안의 수가 많아 읽기 어렵다는 것을 보여주었으므로, 해당 부분의 제안은 PEP 535로 분리되었습니다.

PEP 535: 리치 비교 연쇄

위에서 언급했듯이, PEP 535PEP 207에서 도입된 리치 비교 지원을 확장하여 LEFT_BOUND < VALUE < RIGHT_BOUND와 같은 비교 연쇄 연산도 처리할 수 있도록, 이 PEP에서 정의된 회로 차단 프로토콜을 기반으로 구축하려는 제안입니다.

명세

회로 차단 프로토콜 (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)

이 프로토콜의 제안된 이름은 조건식의 의미에 대한 제안된 변경에서 비롯된 것이 아닙니다. 오히려, 이는 PEP 505에서 제안된 None 기반 단락 평가 연산자(??)뿐만 아니라 기존의 TrueFalse 기반 단락 평가 연산자(각각 orand)를 보완하는 범용 프로토콜 기반 단락 평가 연산자로서 ifelse를 추가하자는 제안에서 비롯됩니다.

이 두 연산자는 함께 회로 차단 연산자(circuit breaking operator)라고 불리게 됩니다.

이러한 용법을 지원하기 위해, 언어 문법에서 조건식의 정의는 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.trueoperator.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

회로 차단 아이덴티티 비교 (isis not)

표준 회로 차단기가 없는 경우, 제안된 ifelse 연산자는 대체로 기존 andor 논리 연산자를 특이하게 표기한 것에 지나지 않을 것입니다.

하지만 이 PEP는 나아가 적절한 단락 로직을 구현하는 새로운 범용 types.CircuitBreaker 타입과, isis 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 모듈의 팩토리 함수들은 isis 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)

진리값 검사 비교

단락회로 특성으로 인해, andor 연산자의 기반이 되는 런타임 로직은 이전에는 operatortypes 모듈을 통해 접근할 수 없었습니다.

회로 차단 연산자와 회로 차단기의 도입으로 이러한 로직을 다음과 같이 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_sentinelis_not_sentinel 회로 차단기 팩토리는 “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-인식 연산자의 실제 구현은 회로 차단기 인스턴스를 실제로 생성하는 과정을 생략하도록 최적화될 것으로 예상되지만, 위의 전개는 런타임에서 이 연산자들의 관찰 가능한 동작을 정확하게 설명해 줄 것입니다.

리치 연쇄 비교(rich chained comparisons)

이 가능한 사용 사례에 대한 자세한 논의는 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

이는 CONDITIONas 절에 주어진 이름에 직접 할당하는 대신, operator.short_circuit(CONDITION)의 결과를 그 이름에 할당함으로써 이루어질 수 있습니다.

스타일 가이드 권장 사항

이 PEP가 도입하는 새로운 기능과 관련하여 PEP 8에 다음 추가 사항이 제안됩니다:

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

근거

새 연산자 추가

이 PEP의 초기 초안은 PEP 335와 마찬가지로 새로운 연산자를 제안하기보다는 기존 andor 연산자의 해석을 덜 엄격하게 만드는 데 초점을 맞추었습니다. 하지만 이는 몇 가지 핵심적인 이유로 문제가 있는 것으로 판명되었습니다:

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

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

  • andor의 런타임 의미는 완전히 변경되지 않은 채로 유지됩니다
  • 단항 not 연산자의 의미는 변경되지만, __not__ 구현에 요구되는 불변 조건 덕분에 불리언 맥락에서의 기존 표현식 최적화는 여전히 유효합니다.
  • __else__는 후행 else 절이 없기 때문에 if 표현식에 대한 단락 평가 결과입니다
  • __then__는 선행 if 절이 없기 때문에 else 표현식에 대한 단락 평가 결과입니다(메서드 이름이 __if__였다면 이 연관성이 더욱 명확했겠지만, 그렇게 하면 회로 차단 프로토콜을 호출하지 않는 if 키워드의 다른 용법들과 모호해질 것입니다)

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

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

전기적 비유는, 전기 시스템의 회로 차단기가 장비를 손상시키거나 사람에게 해를 끼치기 전에 단락을 감지하고 처리하는 방식과 유사하게, 파이썬의 회로 차단기가 예외를 발생시키기 전에 표현식 내의 단락을 감지하고 처리한다는 것입니다.

Python 수준의 비유는, break 문이 루프가 자연스러운 종료에 도달하기 전에 종료할 수 있게 해주는 것처럼, 회로 차단 표현식도 표현식의 평가를 종료하고 즉시 결과를 산출할 수 있게 해준다는 것입니다.

기존 키워드 사용하기

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

ifelse는 제안된 새 프로토콜에 의미적으로 적합하며, 새 연산자가 명시적인 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을 논의하면서 제기된 위험과 우려 사항을 해결하기 위해 특별히 설계되었습니다.

  • 기존의 andor 연산자에 영향을 주는 대신, 새로운 연산자를 정의하고 (별도의 PEP에서) 연쇄 비교의 정의를 조정합니다
  • 제안된 새 연산자는 None에 대한 동일성 검사에만 배타적으로 집중하는 대신, 기존의 andor의 의미론을 표현하는 데도 사용할 수 있는 범용 단락 평가 이항 연산자입니다
  • 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)를 호출하여 평가되며, 이는 파이썬의 기존 동작과 동일합니다. 동작의 변화는 피연산자 x 또는 y 중 하나를 직접 반환하는 대신, 서킷 브레이킹 프로토콜이 조건에 사용된 서킷 브레이커에 해당 피연산자를 전달한다는 점입니다.

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

이 프로토콜은 새로운 ifelse 이항 연산자에도 적용되지만, 이 경우 인터프리터는 누락된 세 번째 피연산자를 나타낼 방법이 필요합니다. 인터프리터는 이를 위해 서킷 브레이커 자체를 그 역할로 재사용합니다.

다음 두 표현식을 살펴보겠습니다.:

>>> 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__의 구현은 단락 평가(short-circuiting)가 발생했음을 나타내는 어떤 것도 보지 못하므로 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를 반환합니다.

드모르간의 법칙 준수

andor와 마찬가지로, 이진 단락 평가 연산자는 본질적으로 같은 표현식을 작성하는 여러 방법을 허용합니다. 이러한 겉보기 중복성은 안타깝게도 프로토콜을 완전한 불 대수로 정의한 데 따른 함축적 결과인데, 불 대수는 “드모르간의 법칙”이라 불리는 한 쌍의 속성을 따르기 때문입니다: 이는 andor 연산의 결과를 서로와 적절한 not 연산의 조합으로 표현할 수 있는 능력입니다.

Python에서 andor에 대해, 이 불변식은 다음과 같이 기술될 수 있습니다:

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을 적용할 때 회로 차단기(circuit breaker)의 입력 값에 직접 bool을 적용하는 대신, 어느 분기를 평가할지 결정하는 데 사용된 것과 동일한 진리값 정의를 사용하게 됩니다.

구체적으로, 새로운 단락 평가 연산자의 경우, __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를 적용하는 것)에 주목하십시오. 이는 단언(assertion)이 단지 type(true(A)).__not__의 동작에 관한 것이 아니라, 실제로 type(A).__not__에 관해 이루어지도록 보장합니다.

적어도 types.CircuitBreaker 인스턴스는 이 논리를 준수하여, (이중 부정 제거와 같은) 기존 불리언 표현식 최적화가 계속 적용될 수 있도록 합니다.

임의의 sentinel 객체

PEP 505와 531과 달리, 이 PEP의 제안은 사용자 정의 sentinel 객체를 쉽게 처리합니다.:

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

  1. 이 표현식들의 반환 타입이 bool에서 보편적으로 types.CircuitBreaker로 바뀌어, (특히 PyObject_IsTrue를 통해 제공된 값을 전달하거나 인자 파싱 함수 중 하나에서 p (predicate) 형식을 사용하는 대신, PyBool_Check로 내장 불리언 값을 특별히 찾는 확장 모듈 API로 작업할 때) 하위 호환성 문제를 일으킬 가능성이 있었을 것입니다.
  2. 이 표현식들의 반환 타입이 맥락에 의존적이 되어, 다음을 의미했을 것입니다: (비교 연산을 로컬 변수로 빼내는 것과 같은) 다른 일상적인 리팩터링이 코드 조각의 런타임 의미에 상당한 영향을 미칠 수 있었다는 것입니다.

이 PEP의 제안으로는 그 두 가지 가능한 결과 중 어느 것도 정당화되지 않는 것으로 보여, 회로 차단기 인스턴스가 반드시 API 호출을 통해 명시적으로 생성되어야 하며 결코 암묵적으로 생성되지 않는 현재의 설계로 되돌아갔습니다.

구현

관련 PEP 505와 마찬가지로, 이러한 변경을 한다는 발상에 원칙적인 관심이 확인될 때까지 실제 구현은 보류되었습니다.

…TBD…

감사의 말

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

참고 문헌