PEP 335 – 오버로드 가능한 불리언 연산자
- Author:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 29-Aug-2004
- Python-Version:
- 3.3
- Post-History:
- 05-Sep-2004, 30-Sep-2011, 25-Oct-2011
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
거부 통지
이 PEP는 거부되었습니다. https://mail.python.org/pipermail/python-dev/2012-March/117510.html 을 참조하십시오.
초록
이 PEP는 객체가 불리언 연산자 ‘and’, ‘or’ 및 ‘not’에 대한 자체 의미를 정의할 수 있도록 하는 확장을 제안하고, 이를 효율적으로 구현하기 위한 전략을 제시합니다. 이 구현의 프로토타입을 다운로드할 수 있습니다.
배경
Python은 현재 ‘and’, ‘or’ 및 ‘not’ 불리언 연산자에 대응하는 ‘__xxx__’ 특수 메서드를 제공하지 않습니다. ‘and’ 및 ‘or’의 경우 가장 가능성 높은 이유는 이러한 연산자가 단락 평가 의미를 가지기 때문입니다. 즉, 첫 번째 피연산자만으로 결과를 결정할 수 있으면 두 번째 피연산자를 평가하지 않습니다. 따라서 이러한 연산자에 특수 메서드를 제공하는 일반적인 기법은 작동하지 않습니다.
그러나 ‘not’의 경우에는 이러한 어려움이 없으며, 이 연산자에 대한 특수 메서드를 제공하는 것은 간단합니다. 따라서 이 제안의 나머지 부분에서는 주로 ‘and’ 및 ‘or’를 오버로드하는 방법을 제공하는 데 집중합니다.
동기
Python 연산자에 대한 사용자 정의 의미를 제공하는 것이 자연스러운 애플리케이션이 많으며, 이러한 애플리케이션 중 일부에서는 사용자 정의할 수 있는 연산자에서 불리언 연산자가 제외되는 것이 불편할 수 있습니다. 예는 다음과 같습니다.
- NumPy에서는 거의 모든 연산자가 배열에 대해 정의되어 대응하는 요소 사이에서 적절한 연산을 수행하고 결과 배열을 반환합니다. 일관성을 고려하면 두 배열 사이의 불리언 연산이 불리언 배열을 반환할 것으로 예상되지만, 현재는 이것이 불가능합니다.
이러한 종류의 확장에는 선례가 있습니다. 비교 연산자는 원래 불리언 결과를 반환하는 것으로 제한되었지만, NumPy 배열의 비교가 불리언 배열을 반환할 수 있도록 풍부한 비교가 추가되었습니다.
- 기호 대수 시스템에서는 Python 표현식이 해당 표현식의 구조에 대응하는 객체 트리를 구성하는 환경에서 평가됩니다.
- 관계형 데이터베이스 인터페이스에서는 Python 표현식을 사용하여 SQL 쿼리를 구성합니다.
흔히 제안되는 우회 방법은 ‘and’, ‘or’ 및 ‘not’ 대신 비트 연산자 ‘&’, ‘|’ 및 ‘~’를 사용하는 것이지만, 여기에는 몇 가지 단점이 있습니다.
- 이러한 연산자의 우선순위는 다른 연산자와의 관계에서 다르며, 이미 다른 용도로 사용되고 있을 수도 있습니다(예제 1에서와 같습니다).
- 사용자가 표현하려는 내용에 가장 분명한 구문이 아닌 것을 사용하도록 강제하는 것은 미학적으로 바람직하지 않습니다. 불리언 연산이 SQL 쿼리의 기본 요소라는 점을 고려하면, 이는 특히 예제 3의 경우에 심각합니다.
- 비트 연산자는 암시적인 ‘and’ 연산을 포함하는 ‘a < b < c’와 같은 연쇄 비교 문제에 대한 해결책을 제공하지 않습니다. 현재 이러한 표현식은 비교 결과를 일반적인 불리언 의미로 처리할 수 없는 NumPy 배열과 같은 데이터 형식에서는 전혀 사용할 수 없습니다. 이러한 표현식은 (a < b) & (b < c)와 같은 형태로 확장해야 하며, 그 결과 명확성이 상당히 떨어집니다.
근거
불리언 연산자의 사용자 정의를 허용하는 문제를 성공적으로 해결하기 위한 요구 사항은 다음과 같습니다.
- 기본적인 경우(사용자 정의가 없는 경우)에는 기존의 단락 평가 의미를 보존해야 합니다.
- 기본적인 경우에 속도가 눈에 띄게 저하되어서는 안 됩니다.
- 이상적으로는 사용자 정의 메커니즘이 객체의 재량에 따라 단락 평가 의미 또는 비단락 평가 의미를 제공할 수 있도록 해야 합니다.
이전에 제안된 한 가지 분명한 전략은 특수 메서드에 첫 번째 인자와 두 번째 인자를 평가하는 함수를 전달하는 것입니다. 이는 요구 사항 1과 3을 충족하지만, 모든 불리언 연산에서 함수 객체를 생성하고 경우에 따라 Python 함수 호출을 수행하는 오버헤드가 발생하므로 요구 사항 2는 충족하지 못합니다. 따라서 여기에서는 이를 더 이상 고려하지 않습니다.
다음 절에서는 세 가지 요구 사항을 모두 해결하는 전략을 제안합니다. 이 전략의 prototype implementation을 다운로드할 수 있습니다.
사양
특수 메서드
Python 수준에서 객체는 다음 특수 메서드를 정의할 수 있습니다.
| 단항 | 이항, 1단계 | 이항, 2단계 |
|---|---|---|
|
|
|
__not__ 메서드가 정의되어 있으면 ‘not’ 연산자를 구현합니다. 정의되어 있지 않거나 NotImplemented를 반환하면 기존 의미가 사용됩니다.
단락 평가를 허용하기 위해 ‘and’ 및 ‘or’ 연산자의 처리는 두 단계로 나뉩니다. 1단계는 첫 번째 피연산자를 평가한 후 두 번째 피연산자를 평가하기 전에 수행됩니다. 첫 번째 피연산자가 관련 1단계 메서드를 정의하면 첫 번째 피연산자를 인자로 하여 해당 메서드를 호출합니다. 해당 메서드가 두 번째 피연산자를 필요로 하지 않고 결과를 결정할 수 있으면 결과를 반환하고 추가 처리를 건너뜁니다.
1단계 메서드가 두 번째 피연산자가 필요하다고 판단하면 특수 값 NeedOtherOperand를 반환합니다. 그러면 두 번째 피연산자가 평가되고 관련 2단계 메서드가 호출됩니다. 2단계에서는 __and2__/__rand2__ 및 __or2__/__ror2__ 메서드 쌍이 다른 이항 연산자에서와 동일하게 작동합니다.
어느 단계에서든 관련 특수 메서드를 찾지 못하거나 해당 메서드가 NotImplemented를 반환하면 처리는 기존 의미로 대체됩니다.
특별한 경우로, 첫 번째 피연산자가 2단계 메서드를 정의했지만 이에 대응하는 1단계 메서드는 정의하지 않은 경우에는 두 번째 피연산자를 항상 평가하고 2단계 메서드를 호출합니다. 이를 통해 단락 평가 의미를 원하지 않는 객체는 2단계 메서드만 구현하고 1단계를 무시할 수 있습니다.
바이트코드
이 패치는 LOGICAL_AND_1, LOGICAL_AND_2, LOGICAL_OR_1 및 LOGICAL_OR_2라는 네 가지 새로운 바이트코드를 추가합니다. 이러한 바이트코드의 사용 예로, ‘and’ 표현식에 대해 생성되는 바이트코드는 다음과 같습니다.:
.
.
.
evaluate first operand
LOGICAL_AND_1 L
evaluate second operand
LOGICAL_AND_2
L: .
.
.
LOGICAL_AND_1 바이트코드는 1단계 처리를 수행합니다. 두 번째 피연산자가 필요하다고 판단하면 첫 번째 피연산자를 스택에 남겨 두고 다음 코드로 계속 진행합니다. 그렇지 않으면 첫 번째 피연산자를 꺼내고 결과를 푸시한 다음 L로 분기합니다.
LOGICAL_AND_2 바이트코드는 2단계 처리를 수행하여 두 피연산자를 모두 팝하고 결과를 푸시합니다.
타입 슬롯
C 수준에서 새로운 특수 메서드는 타입 객체 내의 다섯 가지 새로운 슬롯으로 나타납니다. 패치에서는 이들이 tp_as_number 하위 구조체에 추가되는데, 이는 단항 및 이항 연산자를 다루는 기존 코드 일부를 활용할 수 있게 해주기 때문입니다. 이들의 존재는 Py_TPFLAGS_HAVE_BOOLEAN_OVERLOAD라는 새로운 타입 플래그로 표시됩니다.
새로운 타입 슬롯은 다음과 같습니다:
unaryfunc nb_logical_not;
unaryfunc nb_logical_and_1;
unaryfunc nb_logical_or_1;
binaryfunc nb_logical_and_2;
binaryfunc nb_logical_or_2;
Python/C API 함수
새로운 연산에 대응하는 다섯 가지 새로운 Python/C API 함수도 있습니다:
PyObject *PyObject_LogicalNot(PyObject *);
PyObject *PyObject_LogicalAnd1(PyObject *);
PyObject *PyObject_LogicalOr1(PyObject *);
PyObject *PyObject_LogicalAnd2(PyObject *, PyObject *);
PyObject *PyObject_LogicalOr2(PyObject *, PyObject *);
대안 및 최적화
이 절에서는 이 제안에 대한 몇 가지 가능한 변형과, 불리언 표현식에 대해 생성되는 바이트코드 시퀀스를 최적화할 수 있는 방법을 논의합니다.
축소된 특수 메서드 집합
완전성을 위해, 이 제안의 완전한 버전에는 타입이 자체적으로 맞춤화된 단락 평가(short-circuiting) 동작을 정의할 수 있는 메커니즘이 포함되어 있습니다. 하지만 여기서 제시한 주요 사용 사례를 다루는 데는 완전한 메커니즘이 필요하지 않으며, 2단계 메서드만을 포함하는 단순화된 버전을 정의하는 것도 가능합니다. 그러면 3개의 관련 타입 슬롯과 3개의 API 함수를 가진 5개의 새로운 특수 메서드(__and2__, __rand2__, __or2__, __ror2__, __not__)만 있게 됩니다.
원한다면 이 단순화된 버전을 나중에 완전한 버전으로 확장할 수 있습니다.
추가 바이트코드
여기서 정의한 바와 같이, 불리언 표현식의 결과에 따라 분기하는 코드에 대한 바이트코드 시퀀스는 현재보다 다소 길어질 것입니다. 예를 들어, Python 2.7에서는
if a and b:
statement1
else:
statement2
다음을 생성합니다
LOAD_GLOBAL a
POP_JUMP_IF_FALSE false_branch
LOAD_GLOBAL b
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
지금까지 설명한 이 제안에 따르면, 이는 다음과 같은 형태가 될 것입니다
LOAD_GLOBAL a
LOGICAL_AND_1 test
LOAD_GLOBAL b
LOGICAL_AND_2
test:
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
여기에는 단락 평가(short-circuiting)가 일어나는 경우에는 바이트코드 하나를 추가로 실행하고, 단락 평가가 일어나지 않는 경우에는 바이트코드 두 개를 추가로 실행하는 것이 포함됩니다.
하지만 논리 연산을 결과에 대한 검사 및 분기와 결합한 추가 바이트코드를 도입함으로써, 원래와 같은 수의 바이트코드로 줄일 수 있습니다:
LOAD_GLOBAL a
AND1_JUMP true_branch, false_branch
LOAD_GLOBAL b
AND2_JUMP_IF_FALSE false_branch
true_branch:
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
여기서 AND1_JUMP는 위와 같이 1단계 처리를 수행한 다음, 결과를 검사합니다. 결과가 있으면 스택에서 팝되어 참 값 여부가 검사되고, 두 위치 중 하나로 분기가 이루어집니다.
그렇지 않으면 첫 번째 피연산자가 스택에 남겨진 채 다음 바이트코드로 실행이 계속됩니다. AND2_JUMP_IF_FALSE 바이트코드는 2단계 처리를 수행하여 결과를 팝하고, 그 값이 거짓으로 검사되면 분기합니다
‘or’ 연산자의 경우, 이에 대응하는 OR1_JUMP 및 OR2_JUMP_IF_TRUE 바이트코드가 있게 됩니다.
1단계 메서드가 없는 단순화된 버전을 사용하는 경우, 조기 종료는 ‘and’의 경우 첫 번째 피연산자가 거짓일 때, ‘or’의 경우 참일 때만 발생할 수 있습니다. 따라서 두 개의 목표 지점을 가진 AND1_JUMP 및 OR1_JUMP 바이트코드는, 목표 지점이 하나뿐인 일반적인 분기 명령인 AND1_JUMP_IF_FALSE 및 OR1_JUMP_IF_TRUE로 대체될 수 있습니다.
‘not’의 최적화
최근 버전의 Python은 부정된 불리언 표현식에 대한 분기를, 분기의 방향을 반전시켜 UNARY_NOT 옵코드를 절약하는 방식으로 구현하는 간단한 최적화를 적용합니다.
엄격하게 보면, ‘not’ 연산자가 재정의되어 일반적인 경우와 상당히 다른 결과를 낼 수 있으므로, 이 최적화는 더 이상 수행되어서는 안 됩니다. 그러나 일반적인 사용 사례에서는, 사용자 정의된 불리언 연산이 포함된 표현식이 분기에 사용될 것으로 예상되지 않습니다 – 그 결과가 다른 방식으로 사용될 가능성이 훨씬 더 높습니다.
따라서 컴파일러가 불리언 문맥에 직접 나타나는 표현식을 단순화하기 위해 불리언 대수의 법칙을 사용하는 것을 허용하도록 명시하더라도 거의 해가 되지 않을 것입니다. 이것이 불편하다면, 그 결과를 먼저 임시 이름에 할당하면 됩니다.
이렇게 하면 기존의 ‘not’ 최적화가 유지될 수 있으며, 드 모르간의 법칙을 사용해 표현식 더 깊숙이까지 확장하는 것과 같은 향후 확장도 가능해질 것입니다.
사용 예제
예제 1: NumPy 배열
#-----------------------------------------------------------------
#
# This example creates a subclass of numpy array to which
# 'and', 'or' and 'not' can be applied, producing an array
# of booleans.
#
#-----------------------------------------------------------------
from numpy import array, ndarray
class BArray(ndarray):
def __str__(self):
return "barray(%s)" % ndarray.__str__(self)
def __and2__(self, other):
return (self & other)
def __or2__(self, other):
return (self & other)
def __not__(self):
return (self == 0)
def barray(*args, **kwds):
return array(*args, **kwds).view(type = BArray)
a0 = barray([0, 1, 2, 4])
a1 = barray([1, 2, 3, 4])
a2 = barray([5, 6, 3, 4])
a3 = barray([5, 1, 2, 4])
print "a0:", a0
print "a1:", a1
print "a2:", a2
print "a3:", a3
print "not a0:", not a0
print "a0 == a1 and a2 == a3:", a0 == a1 and a2 == a3
print "a0 == a1 or a2 == a3:", a0 == a1 or a2 == a3
예제 1 출력
a0: barray([0 1 2 4])
a1: barray([1 2 3 4])
a2: barray([5 6 3 4])
a3: barray([5 1 2 4])
not a0: barray([ True False False False])
a0 == a1 and a2 == a3: barray([False False False True])
a0 == a1 or a2 == a3: barray([False False False True])
예제 2: 데이터베이스 쿼리
#-----------------------------------------------------------------
#
# This example demonstrates the creation of a DSL for database
# queries allowing 'and' and 'or' operators to be used to
# formulate the query.
#
#-----------------------------------------------------------------
class SQLNode(object):
def __and2__(self, other):
return SQLBinop("and", self, other)
def __rand2__(self, other):
return SQLBinop("and", other, self)
def __eq__(self, other):
return SQLBinop("=", self, other)
class Table(SQLNode):
def __init__(self, name):
self.__tablename__ = name
def __getattr__(self, name):
return SQLAttr(self, name)
def __sql__(self):
return self.__tablename__
class SQLBinop(SQLNode):
def __init__(self, op, opnd1, opnd2):
self.op = op.upper()
self.opnd1 = opnd1
self.opnd2 = opnd2
def __sql__(self):
return "(%s %s %s)" % (sql(self.opnd1), self.op, sql(self.opnd2))
class SQLAttr(SQLNode):
def __init__(self, table, name):
self.table = table
self.name = name
def __sql__(self):
return "%s.%s" % (sql(self.table), self.name)
class SQLSelect(SQLNode):
def __init__(self, targets):
self.targets = targets
self.where_clause = None
def where(self, expr):
self.where_clause = expr
return self
def __sql__(self):
result = "SELECT %s" % ", ".join([sql(target) for target in self.targets])
if self.where_clause:
result = "%s WHERE %s" % (result, sql(self.where_clause))
return result
def sql(expr):
if isinstance(expr, SQLNode):
return expr.__sql__()
elif isinstance(expr, str):
return "'%s'" % expr.replace("'", "''")
else:
return str(expr)
def select(*targets):
return SQLSelect(targets)
#-----------------------------------------------------------------
dishes = Table("dishes")
customers = Table("customers")
orders = Table("orders")
query = select(customers.name, dishes.price, orders.amount).where(
customers.cust_id == orders.cust_id and orders.dish_id == dishes.dish_id
and dishes.name == "Spam, Eggs, Sausages and Spam")
print repr(query)
print sql(query)
예제 2 출력
<__main__.SQLSelect object at 0x1cc830>
SELECT customers.name, dishes.price, orders.amount WHERE
(((customers.cust_id = orders.cust_id) AND (orders.dish_id =
dishes.dish_id)) AND (dishes.name = 'Spam, Eggs, Sausages and Spam'))
Copyright
This document has been placed in the public domain.