[Final] PEP 238 - Changing the Division Operator
원문 링크: PEP 238 - Changing the Division Operator
상태: Final 유형: Standards Track 작성일: 11-Mar-2001
PEP 238 – 나눗셈 연산자 변경 (Changing the Division Operator)
개요 (Abstract)
현재 Python의 나눗셈 연산자 (/
)는 숫자 인수에 대해 모호한 의미를 가지고 있습니다. 정수(int)나 long 정수가 인수로 주어질 경우 수학적 나눗셈 결과의 내림(floor) 값을 반환하지만, 부동 소수점(float)이나 복소수(complex)가 주어지면 나눗셈 결과의 합리적인 근사치를 반환합니다. 이로 인해 float 또는 complex 결과가 예상되는 표현식에서 예기치 않게 정수가 입력으로 들어올 경우 오류가 발생하기 쉽습니다.
이 PEP는 이러한 문제를 해결하기 위해 다음과 같이 다른 연산자들을 도입할 것을 제안합니다:
x / y
: 나눗셈의 수학적 결과에 대한 합리적인 근사치를 반환하는 “참 나눗셈 (true division)”.x // y
: 내림(floor) 값을 반환하는 “바닥 나눗셈 (floor division)”.
기존의 x / y
의 혼합된 의미를 “고전적 나눗셈 (classic division)”이라고 부릅니다.
심각한 하위 호환성 문제 때문에, Python 2.2부터 다음과 같은 전환 조치들이 제안되었습니다:
- Python 2.x 시리즈에서는 고전적 나눗셈이 기본으로 유지됩니다. Python 3.0에서는 참 나눗셈이 표준이 됩니다.
//
연산자는 명확하게 바닥 나눗셈을 요청하는 데 사용될 수 있습니다.from __future__ import division
문은 해당 모듈 전체에서/
연산자가 참 나눗셈을 의미하도록 변경합니다.- 명령줄 옵션을 통해
int
또는long
인수에 고전적 나눗셈이 적용될 경우 실행 시 경고를 발생시킬 수 있습니다. 다른 명령줄 옵션은 참 나눗셈을 기본으로 설정할 수 있습니다. - 표준 라이브러리는 고전적 나눗셈을 완전히 피하기 위해 적절한 경우 future division statement와
//
연산자를 사용하게 됩니다.
동기 (Motivation)
고전적 나눗셈 연산자는 임의의 숫자 입력을 받아 올바른 결과를 내야 하는 수치 표현식을 작성하기 어렵게 만듭니다. 다른 모든 연산자 (x * y**2 + z
등)의 경우, 어떤 숫자 입력 타입(int, long, float, complex)에 대해서도 계산 결과가 수학적 결과에 가깝게 나옵니다 (물론 수치 정확도의 한계 내에서). 하지만 나눗셈은 문제가 됩니다. 양쪽 인수의 표현식이 우연히 정수 타입일 경우, 참 나눗셈이 아닌 바닥 나눗셈을 수행하기 때문입니다.
이 문제는 동적 타입 언어에 고유합니다. C와 같은 정적 타입 언어에서는 입력 (일반적으로 함수 인수)이 double
또는 float
로 선언되고, 정수 인수가 전달될 때 호출 시 double
또는 float
로 변환됩니다. Python은 인수 타입 선언이 없으므로, 정수 인수가 쉽게 표현식에 포함될 수 있습니다.
이 문제는 int
가 다른 모든 상황에서 float
의 완벽한 대체재가 되기 때문에 특히 해롭습니다. 예를 들어, math.sqrt(2)
는 math.sqrt(2.0)
와 동일한 값을 반환하고, 3.14 * 100
과 3.14 * 100.0
도 동일한 값을 반환합니다. 따라서 수치 루틴의 작성자는 부동 소수점 숫자로만 코드를 테스트하고 올바르게 작동한다고 믿을 수 있지만, 사용자가 실수로 정수 입력 값을 전달하여 잘못된 결과를 얻을 수 있습니다.
고전적 나눗셈은 float
또는 int
인수 모두에서 잘 작동하는 다형성(polymorphic) 함수를 작성하기 어렵게 만듭니다. 다른 모든 연산자들은 이미 올바르게 작동합니다. 정수와 부동 소수점 숫자 모두에 적용되는 어떤 알고리즘도 한 경우에는 절단 나눗셈(truncating division)을, 다른 경우에는 참 나눗셈을 필요로 하지 않습니다.
올바른 해결책은 미묘합니다. 인수를 float()
으로 캐스팅하는 것은 복소수일 경우 잘못될 수 있습니다. 인수에 0.0
을 더하는 것은 인수가 음수 0일 경우 부호를 유지하지 못합니다. 이 두 가지 단점 없이 사용할 수 있는 유일한 해결책은 인수에 1.0
을 곱하는 것입니다. 이것은 float
및 complex
의 값과 부호를 변경하지 않고, int
및 long
을 해당 값을 가진 float
으로 변환합니다.
저자들은 이것이 Python의 진정한 설계 버그이며, 가급적 빨리 수정해야 한다고 판단했습니다. Python 사용이 계속 증가할 것이라고 가정할 때, 이 버그를 언어에 남겨두는 비용은 결국 오래된 코드를 수정하는 비용을 넘어설 것입니다. 수정해야 할 코드의 양에는 상한선이 있지만, 미래에 이 버그의 영향을 받을 수 있는 코드의 양은 무한합니다.
이러한 변경의 또 다른 이유는 궁극적으로 Python의 숫자 모델을 통합하려는 바람 때문입니다. 이는 PEP 228 (현재 미완성)의 주제입니다. 통합된 숫자 모델은 사용자가 다른 숫자 타입을 인지해야 할 필요성을 대부분 제거합니다. 이는 초보자에게도 좋지만, 숙련된 프로그래머에게도 다른 숫자 동작에 대한 우려를 덜어줍니다. (물론, 수치 안정성과 정확성에 대한 우려를 제거하지는 않습니다.)
통합된 숫자 모델에서, 다른 타입들(int, long, float, complex 및 잠재적으로 새로운 유리수(rational) 타입 등)은 주로 저장 최적화 역할을 하며, 부정확성(inexactness)이나 복소수(complexity)와 같은 직교적인 속성을 나타냅니다. 통합 모델에서는 정수 1이 부동 소수점 숫자 1.0과 구별할 수 없어야 하며 (부정확성을 제외하고), 둘 다 모든 숫자 문맥에서 동일하게 동작해야 합니다. 분명히, 통합된 숫자 모델에서 a==b
이고 c==d
라면, a/c
는 b/d
와 같아야 합니다 (부정확한 숫자의 반올림으로 인한 약간의 자유는 허용). 그리고 모두가 1.0/2.0
이 0.5
와 같다는 데 동의하므로, 1/2
도 0.5
와 같아야 합니다. 마찬가지로, 1//2
가 0과 같으므로, 1.0//2.0
도 0과 같아야 합니다.
변경안 (Variations)
미적으로 x // y
는 모두에게 만족스러운 것은 아니며, 따라서 여러 변형이 제안되었습니다.
x div y
: 새로운 키워드를 도입하게 됩니다.div
는 인기 있는 식별자이므로, 새로운 키워드가 future division statement 하에서만 인식되지 않는 한, 상당수의 기존 코드를 손상시킬 것입니다. 변환해야 할 코드의 대부분이 정수를 나누는 코드일 것으로 예상되므로, 이는 future division statement의 필요성을 크게 증가시킬 것입니다. future statement가 있더라도, 절대적으로 필요한 경우가 아니면 새로운 키워드를 추가하는 것에 대한 일반적인 정서는 이에 반대합니다.div(x, y)
: 이것은 오래된 코드의 변환을 훨씬 어렵게 만듭니다.x/y
를x//y
또는x div y
로 교체하는 것은 간단한 쿼리-대체(query replace)로 가능합니다. 대부분의 경우 프로그래머는 특정 모듈이 정수와만 작동하므로x/y
의 모든 발생을 대체할 수 있음을 쉽게 확인할 수 있습니다. (query replace
는 주석이나 문자열 리터럴에 나타나는 슬래시를 걸러내기 위해 여전히 필요합니다.)x/y
를div(x, y)
로 교체하려면/
의 왼쪽과 오른쪽에 있는 표현식의 범위를 분석하여div(
와)
부분을 어디에 배치할지 결정해야 하므로 훨씬 더 지능적인 도구가 필요할 것입니다.x \ y
: 백슬래시는 이미 줄 연속(line continuation)을 의미하는 토큰이며, 일반적으로 Unix 사용자에게는 이스케이프를 제안합니다. 또한 (Terry Reedy에 따르면)eval("x\y")
와 같은 것을 올바르게 구현하기 어렵게 만들 것입니다.
대안 (Alternatives)
변환해야 할 오래된 코드의 양을 줄이기 위해 여러 대안 제안이 제시되었습니다. 각 제안(또는 제안 범주)에 대한 간략한 논의는 다음과 같습니다.
/
가 고전적 의미를 유지하고,//
를 참 나눗셈에 도입: 이는 언어에 여전히 깨진 연산자를 남기고, 깨진 동작을 사용하도록 유도합니다. 또한 PEP 228과 같은 통합 숫자 모델로 가는 길을 막습니다.int
나눗셈이 정수 문맥에서는 정수처럼, float 문맥에서는 float처럼 동작하는 특별한 “합성(portmanteau)” 타입을 반환하게 함: 몇 번의 연산 후에int
와float
값이 크게 달라질 수 있고, 비교에서 어떤 값을 사용해야 할지 불분명하며, 물론 많은 문맥(예: 문자열 변환)에서는 명확한int
또는float
선호도가 없다는 문제가 있습니다.- future statement 대신 모듈에서 특정 나눗셈 의미를 사용하도록 지시어(directive) 사용: 이는 고전적 나눗셈을 언어의 영구적인 문제점으로 남겨두어, 미래 세대의 Python 프로그래머가 문제와 해결책을 인지하도록 요구하게 됩니다.
from __past__ import division
를 사용하여 모듈에서 고전적 나눗셈 의미를 사용: 이는 또한 고전적 나눗셈을 영구적인 문제점으로 남기거나 (결국past division statement
가ImportError
를 발생시킬 수 있습니다), 적어도 오랫동안 유지합니다.- 지시어(또는 다른 방법)를 사용하여 특정 코드 조각이 개발된 Python 버전을 지정: 이는 미래 Python 인터프리터가 여러 이전 Python 버전을 정확하게 에뮬레이션할 수 있어야 하며, 동일한 인터프리터 내에서 여러 버전을 에뮬레이션해야 합니다. 이는 너무 많은 작업입니다. 훨씬 간단한 해결책은 여러 인터프리터를 설치하는 것입니다. 이 제안에 대한 또 다른 반론은 버전 지시어가 거의 항상 과도하게 지정된다는 것입니다. 대부분의 Python X.Y용 코드는 Python X.(Y-1) 및 X.(Y+1)에서도 작동하므로, X.Y를 버전으로 지정하는 것은 필요 이상으로 제약적입니다. 동시에, 코드가 미래 또는 과거의 어떤 버전에서 깨질지 알 방법이 없습니다.
API 변경 (API Changes)
전환 단계 동안, 우리는 동일한 프로그램 내에서 세 가지 나눗셈 연산자를 지원해야 합니다: 고전적 나눗셈 (future division statement가 없는 모듈의 /
), 참 나눗셈 (future division statement가 있는 모듈의 /
), 바닥 나눗셈 (//
). 각 연산자는 일반적인 형태와 복합 할당 연산자 (/=
또는 //=
)의 두 가지 형태로 제공됩니다.
이러한 변형과 관련된 이름은 다음과 같습니다:
- 오버로드된 연산자 메서드:
__div__()
,__floordiv__()
,__truediv__()
__idiv__()
,__ifloordiv__()
,__itruediv__()
- 추상 API C 함수:
PyNumber_Divide()
,PyNumber_FloorDivide()
,PyNumber_TrueDivide()
PyNumber_InPlaceDivide()
,PyNumber_InPlaceFloorDivide()
,PyNumber_InPlaceTrueDivide()
- 바이트 코드 opcode:
BINARY_DIVIDE
,BINARY_FLOOR_DIVIDE
,BINARY_TRUE_DIVIDE
INPLACE_DIVIDE
,INPLACE_FLOOR_DIVIDE
,INPLACE_TRUE_DIVIDE
PyNumberMethod
슬롯:nb_divide
,nb_floor_divide
,nb_true_divide
,nb_inplace_divide
,nb_inplace_floor_divide
,nb_inplace_true_divide
추가된 PyNumberMethod
슬롯은 tp_flags
에 추가 플래그를 요구합니다. 이 플래그는 Py_TPFLAGS_HAVE_NEWDIVIDE
로 명명되며 Py_TPFLAGS_DEFAULT
에 포함될 것입니다.
참 나눗셈 및 바닥 나눗셈 API는 해당 슬롯을 찾아 호출하며, 해당 슬롯이 NULL
이면 예외를 발생시킵니다. 고전적 나눗셈 슬롯으로의 폴백(fallback)은 없습니다.
Python 3.0에서는 고전적 나눗셈 의미가 제거될 것입니다. 고전적 나눗셈 API는 참 나눗셈과 동의어가 될 것입니다.
명령줄 옵션 (Command Line Option)
-Q
명령줄 옵션은 old
, warn
, warnall
, new
네 가지 값을 가질 수 있는 문자열 인수를 받습니다. Python 2.2에서는 기본값이 old
이지만, 이후 2.x 버전에서는 warn
으로 변경될 것입니다.
old
: 고전적 나눗셈 연산자가 설명된 대로 작동합니다.warn
:int
또는long
인수에 고전적 나눗셈 연산자가 적용될 때 경고 (표준 경고 프레임워크를 사용하는DeprecationWarning
)를 발생시킵니다.warnall
:float
또는complex
인수에 고전적 나눗셈이 적용될 때도 경고를 발생시킵니다. 이는 아래에서 언급된fixdiv.py
변환 스크립트에서 사용됩니다.new
: 기본값을 전역적으로 변경하여/
연산자가 항상 참 나눗셈으로 해석되도록 합니다. 이new
옵션은 참 나눗셈이 필요하지만, 학생들이 모든 코드에 future division statement를 포함하도록 요구하는 것이 문제가 되는 특정 교육 환경에서만 사용하도록 의도되었습니다.
이 옵션은 Python 3.0에서는 지원되지 않습니다. Python 3.0은 항상 /
를 참 나눗셈으로 해석할 것입니다.
바닥 나눗셈의 의미 (Semantics of Floor Division)
바닥 나눗셈은 모든 Python 숫자 타입에서 구현될 것이며, 다음과 같은 의미를 가집니다:
a // b == floor(a / b)
단, 결과 타입은 연산 전에 a
와 b
가 강제 변환되는 공통 타입이 됩니다.
특히, a
와 b
가 같은 타입이면 a // b
도 해당 타입이 됩니다. 입력이 다른 타입이면, 다른 모든 산술 연산자에 사용되는 동일한 규칙을 사용하여 먼저 공통 타입으로 강제 변환됩니다.
특히, a
와 b
가 모두 int
또는 long
이면, 결과는 이 타입들에 대한 고전적 나눗셈과 동일한 타입과 값을 가집니다 (혼합 입력 타입의 경우 포함; int // long
및 long // int
는 모두 long
을 반환합니다).
부동 소수점 입력의 경우 결과는 float
입니다. 예를 들어:
3.5 // 2.0 == 1.0
복소수의 경우, 복소수에 대한 floor()
가 허용되지 않으므로 //
는 예외를 발생시킵니다.
사용자 정의 클래스 및 확장 타입의 경우, 모든 의미는 클래스 또는 타입의 구현에 달려 있습니다.
참 나눗셈의 의미 (Semantics of True Division)
int
와 long
에 대한 참 나눗셈은 인수를 float
로 변환한 다음 float
나눗셈을 적용합니다. 즉, 2/1
도 int
가 아닌 float
(2.0
)를 반환합니다. float
와 complex
의 경우 고전적 나눗셈과 동일합니다.
Python 2.2에서 참 나눗셈 구현은 float
타입이 무한한 범위를 가진 것처럼 작동하여, 수학적 결과의 크기가 float
로 표현하기에 너무 크지 않는 한 오버플로우가 발생하지 않습니다. 예를 들어, x = 1L << 40000
이후 float(x)
는 OverflowError
를 발생시킵니다 (이는 2.2의 새로운 사항입니다. 이전에는 결과가 플랫폼 종속적이었고, 대부분 float infinity
였습니다). 그러나 x/x
는 예외 없이 1.0
을 반환하는 반면, x/1
은 OverflowError
를 발생시킵니다.
int
및 long
인수의 경우, 참 나눗셈은 정보 손실을 초래할 수 있습니다. 이는 참 나눗셈의 본질적인 특성입니다 (유리수가 언어에 포함되지 않는 한). long
정수를 의식적으로 사용하는 알고리즘은 //
를 사용하는 것을 고려해야 합니다. long
정수의 참 나눗셈은 (대부분의 플랫폼에서) 53비트 이상의 정밀도를 유지하지 않기 때문입니다.
유리수 타입이 Python에 추가될 경우 (PEP 239 참조), int
및 long
에 대한 참 나눗셈은 아마도 유리수를 반환해야 할 것입니다. 이는 int
및 long
의 참 나눗셈에서 정보 손실 문제를 피할 수 있습니다. 그러나 그때까지는 일관성을 위해 float
가 참 나눗셈에 대한 유일한 선택입니다.
Future Division Statement (미래 나눗셈 문)
모듈에 from __future__ import division
이 있거나 -Qnew
가 사용되면, /
및 /=
연산자는 참 나눗셈 opcode로 변환됩니다. 그렇지 않으면 (Python 3.0이 나오기 전까지는) 고전적 나눗셈으로 변환됩니다. Python 3.0에서는 항상 참 나눗셈으로 변환됩니다.
future division statement는 //
및 //=
의 인식 또는 변환에 영향을 미치지 않습니다.
future statement에 대한 일반적인 규칙은 PEP 236을 참조하십시오.
FAQ (자주 묻는 질문)
- Python 3.0은 언제 출시되나요?
- 장기적인 계획은 없으므로 확실히 말할 수 없습니다. 전환 기간으로 최소 2년이 필요하다고 생각합니다. Python 3.0이 더 빨리 나온다면, Python 2.2 출시 후 최소 2년 동안 하위 호환성을 위해 2.x 라인을 유지할 것입니다. 실제로 Python 3.0 출시 후에도 몇 년 동안 Python 2.x 라인을 계속 사용할 수 있으므로 전환에 시간을 할애할 수 있습니다. 사이트에서는 Python 2.x와 Python 3.x를 동시에 설치할 수 있을 것으로 예상됩니다.
- 참 나눗셈을 왜
float division
이라고 부르지 않나요?- 유리수를 도입하고
1/2
이float
가 아닌 유리수를 반환할 가능성을 열어두고 싶기 때문입니다. PEP 239를 참조하십시오.
- 유리수를 도입하고
__truediv__
와__itruediv__
가 필요한 이유는 무엇인가요?- 사용자 정의 클래스를 2류 시민으로 만들고 싶지 않습니다. 특히 타입/클래스 통합이 진행 중인 상황에서는 더욱 그렇습니다.
//
또는 future division statement를 사용하지 않고 고전적 규칙과 새로운 규칙 모두에서 작동하는 코드를 어떻게 작성하나요?- 참 나눗셈에는
x * 1.0 / y
를, 정수 나눗셈에는divmod(x, y)
(PEP 228)를 사용하세요. 특히 후자는 함수 내부에 숨겨두는 것이 가장 좋습니다. 복소수를 예상하지 않는다면float(x) / y
를 사용하여 참 나눗셈을 할 수도 있습니다. 정수가 음수가 아니라는 것을 안다면int(x / y)
를 사용할 수 있습니다.int()
의 문서는int()
가 C 구현에 따라 반올림(round) 또는 절단(truncate)할 수 있다고 말하지만, 절단하지 않는 C 구현은 알지 못하며,int()
의 사양을 절단을 약속하도록 변경할 것입니다. 고전적 나눗셈(및 바닥 나눗셈)은 음의 무한대 방향으로 반올림하는 반면,int()
는 0 방향으로 반올림하여 음수에 대해 다른 결과를 제공한다는 점에 유의하십시오.
- 참 나눗셈에는
input()
,compile()
,execfile()
,eval()
,exec
에 대한 나눗셈 의미를 어떻게 지정하나요?- 호출하는 모듈에서 선택을 상속합니다. PEP 236은 이제 이를 PEP 264를 참조하는 해결된 문제로 나열합니다.
codeop
모듈에 의해 컴파일된 코드는 어떻게 되나요?- 적절하게 처리됩니다. PEP 264를 참조하십시오.
- 변환 도구나 지원 기능이 있나요?
- 물론입니다. 이들은 PEP의 범위를 벗어나지만, Python 2.2a3와 함께 출시될 두 가지 간단한 도구를 지적해야 합니다:
Tools/scripts/finddiv.py
는 나눗셈 연산자를 찾고 (grep /
보다 약간 더 스마트함),Tools/scripts/fixdiv.py
는 런타임 분석을 기반으로 패치를 생성할 수 있습니다.
- 물론입니다. 이들은 PEP의 범위를 벗어나지만, Python 2.2a3와 함께 출시될 두 가지 간단한 도구를 지적해야 합니다:
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments