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

Python 개선 제안 한국어 번역

PEP 327 – Decimal 데이터 타입

Author:
Facundo Batista <facundo at taniquetil.com.ar>
Status:
Final
Type:
Standards Track
Created:
17-Oct-2003
Python-Version:
2.4
Post-History:
30-Nov-2003, 02-Jan-2004, 29-Jan-2004

Table of Contents

번역·라이선스 안내

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

초록

십진수가 필요하지만 이진 부동소수점으로는 너무 부정확한 모든 용도에 쓸 Decimal 데이터 타입을 두자는 것이 이 제안의 취지입니다.

Decimal 데이터 타입은 Python 표준 함수와 연산을 지원할 것이며, 십진 산술 ANSI 표준 X3.274-1996 [1]을 준수해야 합니다.

Decimal은 (고정소수점이 아닌) 부동소수점이며, 제한된 정밀도를 가집니다(정밀도는 결과의 유효 자릿수의 상한입니다). 하지만 정밀도는 사용자가 설정할 수 있으며, 유효한 후행 0이라는 개념도 지원하므로 고정소수점 방식으로도 사용할 수 있습니다.

이 작업은 Eric Price, Aahz, Tim Peters가 작성한 코드와 테스트 함수를 기반으로 합니다. Python 2.4a1 직전에 decimal.py Reference Implementation이 표준 라이브러리로 옮겨졌습니다. 문서 및 테스트 스위트와 함께 이것은 Raymond Hettinger의 작업이었습니다. 이 PEP의 설명 중 상당 부분은 Cowlishaw의 작업 [2], comp.lang.python, python-dev에서 가져왔습니다.

동기

여기서는 Decimal 데이터 타입이 왜 필요하다고 생각하는지, 그리고 다른 숫자 데이터 타입으로는 왜 충분하지 않은지 그 이유를 설명하겠습니다.

저는 Money 데이터 타입을 원했고, comp.lang.python에 pre-PEP를 제안한 뒤 커뮤니티는 필요한 산술 동작을 갖춘 숫자 데이터 타입을 먼저 만들고 그 위에 Money를 구축하기로 합의했습니다. 소수점 이하 자릿수, 반올림 등에 관한 모든 고려 사항은 Money를 통해 처리됩니다. 추가적인 노력 없이 Money로 사용할 수 있는 데이터 타입을 만드는 것은 이 PEP의 목적이 아닙니다.

표준을 구현할 때의 가장 큰 장점 중 하나는 누군가가 까다로운 경우들을 이미 모두 대신 고민해 두었다는 점입니다. 그리고 GvR은 저에게 한 표준을 알려 주었습니다: Mike Cowlishaw의 General Decimal Arithmetic(일반 십진 산술) 명세 [2]. 이 문서는 범용 십진 산술을 정의합니다. 이 명세를 올바르게 구현하면 몇 가지 사소한 제약을 제외하고 ANSI/IEEE 표준 854-1987에 정의된 십진 산술을 따르게 되며, 반올림하지 않는 십진 산술과 정수 산술도 진부분집합으로 제공하게 됩니다.

이진 부동소수점의 문제

십진 수학에서는 고정된 개수의 십진 자릿수로 표현할 수 없는 수가 많습니다. 예를 들어 1/3 = 0.3333333333…….입니다.

2진법(표준 부동소수점이 계산되는 방식)에서는 1/2 = 0.1, 1/4 = 0.01, 1/8 = 0.001 등입니다. 십진수 0.2는 2/10, 즉 1/5과 같으며, 이는 이진 소수 0.001100110011001…이 됩니다. 보시다시피 문제는 일부 십진수를 이진수로 정확하게 표현할 수 없어 작은 반올림 오차가 생긴다는 것입니다.

따라서 십진수를 정확하게 표현하는 십진 데이터 타입이 필요합니다. 이진 데이터 타입이 아니라 십진 데이터 타입이 필요합니다.

왜 부동소수점입니까?

그래서 십진수를 택하지만, 왜 부동소수점입니까?

부동소수점 수는 고정된 개수의 자릿수(정밀도)로 수를 표현하며, 수가 너무 커지거나 너무 작아지면 지수를 사용합니다. 예를 들어 정밀도가 5인 경우는 다음과 같습니다.:

  1234 ==>   1234e0
 12345 ==>  12345e0
123456 ==>  12346e1

(마지막 줄에서는 다섯 자리에 맞추기 위해 수가 반올림되었다는 점에 유의하십시오).

이와 대조적으로, 무한한 정밀도를 가진 long 정수를 예로 들 수 있습니다. 이는 수를 원하는 만큼 크게 만들 수 있으며 어떤 정보도 잃지 않는다는 뜻입니다.

고정소수점 수에서는 소수점의 위치가 고정되어 있습니다. 고정소수점 데이터 타입에 대해서는 SourceForge에 있는 Tim Peter의 FixedPoint [4]를 확인하십시오. 저는 부동소수점을 택하겠습니다. 표준의 산술 동작을 구현하기가 더 쉽고, 그런 다음 Decimal 위에 고정소수점 데이터 타입을 구현할 수 있기 때문입니다.

그렇다면 왜 무한 정밀도를 가진 부동 소수점 수는 가질 수 없는 것입니까? 나누어떨어지지 않는 나눗셈 때문에 그리 쉽지 않습니다. 예: 1/3 = 0.3333333333333… 무한히 계속됩니다. 이 경우 3을 무한히 저장해야 하는데, 이는 메모리를 너무 많이 차지합니다 ;).

John Roth는 바로 이런 문제를 피하고자 나눗셈 연산자를 없애고 사용자가 명시적인 메서드를 사용하도록 강제하자고 제안했습니다. 모두가 숫자 데이터 타입에서 / 연산자가 지원되기를 원하기 때문에, 이 제안은 comp.lang.python에서 부정적인 반응을 불러일으켰습니다.

이렇게 설명하고 나면 아마 여러분은 “이봐요! 1과 3을 그냥 분자와 분모로 저장하면 되지 않습니까?”라고 생각하실 텐데, 이것이 다음 주제로 이어집니다.

유리수는 왜 안 됩니까?

유리수는 분자와 분모라는 두 개의 정수를 사용해 저장합니다. 이는 산술 연산을 직접 실행할 수 없다는 것을 의미합니다(예를 들어 두 유리수를 더하려면 먼저 공통 분모를 계산해야 합니다).

Alex Martelli의 말을 인용합니다:

두 유리수(각각 O(M)과 O(N)의 공간을 차지함)를 더하면 O(M+N)의 메모리 공간을 차지하는 유리수가 나온다는 사실이 성능에 미치는 영향은 너무나 골치 아픕니다. 순수 Python으로 작성된 것과 확장 모듈로 작성된 것(예: gmpy) 모두 훌륭한 Rational 구현이 있지만, 제 생각에 그것들은 언제나 “틈새시장”에 머물 것입니다. 아마 PEP로 만들 가치는 있겠지만, Decimal 없이 할 가치는 없습니다 – Decimal이야말로 금액을 표현하는 올바른 방법이며, 이는 현실 세계에서 정말로 중요한 사용 사례입니다.

어쨌든 이 데이터 타입에 관심이 있다면 PEP 239: Adding a Rational Type to Python을 살펴보시는 것이 좋겠습니다.

그렇다면 우리에게 남은 것은 무엇입니까?

그 결과가 제한된 정밀도를 가진 부동 소수점 방식의 Decimal 데이터 타입입니다.

이것이 유용하겠습니까? Alex Martelli보다 더 잘 말할 수는 없을 것 같습니다:

Python은 (기본 상태로는) 사용자가 지정하는 어떤 정밀도로든 이진 부동 소수점 수를 다룰 수 있게 해 주지 않습니다: 하드웨어가 제공하는 정밀도로 제한됩니다. Decimal은 고정 소수점 수로 쓰이든 부동 소수점 수로 쓰이든 그런 제약이 없어야 합니다: 수를 생성할 때 지정하는 어떤 제한된 정밀도든(메모리가 허용하는 한) 똑같이 잘 작동해야 합니다. 프로그래밍 단순성을 위한 비용 대부분은 애플리케이션 프로그램에서 감추고 적절한 십진 산술 타입에 둘 수 있습니다. http://speleotrove.com/decimal/ 에 따르면, 하나의 데이터 타입으로 정수, 고정 소수점, 부동 소수점 십진 산술을 모두 처리할 수 있으며 – 애플리케이션 프로그래머를 미치게 만들지 않는 금액 산술에도 쓸 수 있습니다.

이러한 데이터 타입에는 여러 가지 용도가 있습니다. 앞서 말했듯이, 저는 이것을 Money의 기반으로 사용할 것입니다. 이 경우 제한된 정밀도는 문제가 되지 않습니다. Tim Peters의 말을 인용합니다:

20자리 정밀도면 태초부터 지금까지 전 세계의 경제 총생산을 1페니 단위까지 계산하고도 남을 것입니다.

일반 십진 산술 명세

여기에서는 명세 [2]의 일부인 정보와 설명(수의 구조, 컨텍스트 등)을 다룹니다. 이 절에 포함된 모든 요구 사항은 표준에 있는 것이고 이 PEP는 단지 그 표준을 구현하기 위한 것이므로 (오타나 다른 실수를 제외하고는) 논의 대상이 아닙니다.

저작권 제한 때문에 명세의 설명을 여기에 그대로 옮길 수 없으므로, 제 나름의 말로 설명하겠습니다. 자세한 내용이 필요하거나 의문이 있다면 원본 명세 문서 [2]를 읽어 보시기를 강력히 권합니다.

산술 모델

이 명세는 관련 표준인 IEEE 854 [3], ANSI X3-274 [1], 그리고 IEEE 754 [6]의 개정 제안안 [5]에서 정의한 십진 산술 모델을 기반으로 합니다.

이 모델은 세 가지 구성 요소로 이루어집니다:

  • 수(Numbers): 연산이 입력이나 출력으로 사용하는 값입니다.
  • 연산(Operations): 덧셈, 곱셈 등입니다.
  • 컨텍스트(Context): 사용자가 선택할 수 있으며 연산 결과를 좌우하는 매개변수와 규칙의 집합입니다(예: 사용할 정밀도).

수

수는 유한한 값이거나 특수 값일 수 있습니다. 전자는 정확하게 표현할 수 있습니다. 후자는 무한대와 정의되지 않은 값(0/0 등)입니다.

유한한 수는 세 가지 매개변수로 정의됩니다:

  • 부호(Sign): 0(양수) 또는 1(음수)입니다.
  • 계수(Coefficient): 음이 아닌 정수입니다.
  • 지수(Exponent): 부호 있는 정수로, 계수에 곱하는 10의 거듭제곱의 지수입니다.

유한한 수의 수치 값은 다음과 같이 주어집니다:

(-1)**sign * coefficient * 10**exponent

특수 값의 이름은 다음과 같습니다:

  • 무한대(Infinity): 무한히 큰 값입니다. 양수일 수도 있고 음수일 수도 있습니다.
  • 조용한 NaN(“qNaN”): 정의되지 않은 결과(Not a Number)를 나타냅니다. 잘못된 연산(Invalid operation) 조건을 일으키지 않습니다. NaN의 부호는 아무 의미가 없습니다.
  • 시그널링 NaN(“sNaN”): 역시 Not a Number이지만, 어떤 연산에든 사용되면 잘못된 연산(Invalid operation) 조건을 일으킵니다.

컨텍스트

컨텍스트는 사용자가 선택할 수 있으며 연산 결과를 좌우하는 매개변수와 규칙의 집합입니다(예: 사용할 정밀도).

컨텍스트라는 이름은 컨텍스트가 Decimal 수를 둘러싸고 있고, 컨텍스트의 일부가 연산의 입력과 출력으로 작용하기 때문에 붙은 것입니다. 하나의 컨텍스트로 작업할지 여러 컨텍스트로 작업할지는 애플리케이션이 정할 일이지만, Decimal 수마다 컨텍스트를 하나씩 두려는 것은 결코 아닙니다. 예를 들어 전형적인 사용법은 프로그램 시작 시 컨텍스트의 정밀도를 20자리로 설정하고, 이후로는 컨텍스트를 다시는 명시적으로 사용하지 않는 것입니다.

이 정의들은 Decimal 수의 내부 저장 방식에는 영향을 주지 않으며, 산술 연산이 수행되는 방식에만 영향을 줍니다.

컨텍스트는 주로 다음 매개변수로 정의됩니다(모든 컨텍스트 속성은 Context Attributes를 참조하십시오):

  • 정밀도(precision): 산술 연산의 결과로 나올 수 있는 유효 숫자의 최대 개수입니다(0보다 큰 정수). 이 값에는 최댓값이 없습니다.
  • 반올림(rounding): 반올림이 필요할 때 사용할 알고리즘의 이름으로, “round-down”, “round-half-up”, “round-half-even”, “round-ceiling”, “round-floor”, “round-half-down”, “round-up” 중 하나입니다. 아래의 Rounding Algorithms를 참조하십시오.
  • 플래그와 트랩 활성화기(trap-enabler): Exceptional Conditions는 개별적으로 제어할 수 있는 시그널로 묶이며, 각 시그널은 플래그(시그널이 발생하면 설정되는 불리언)와 트랩 활성화기(동작을 제어하는 불리언)로 구성됩니다. 시그널은 “clamped”, “division-by-zero”, “inexact”, “invalid-operation”, “overflow”, “rounded”, “subnormal”, “underflow”입니다.

기본 컨텍스트

명세는 두 가지 기본 컨텍스트를 정의하며, 사용자가 이를 쉽게 선택할 수 있어야 합니다.

기본형(Basic) 기본 컨텍스트:

  • 플래그: 모두 0으로 설정됩니다
  • 트랩 활성화기: inexact, rounded, subnormal은 0으로 설정되고, 나머지는 모두 1로 설정됩니다
  • 정밀도: 9로 설정됩니다
  • 반올림: round-half-up으로 설정됩니다

확장형(Extended) 기본 컨텍스트:

  • 플래그: 모두 0으로 설정됩니다
  • 트랩 활성화기: 모두 0으로 설정됩니다
  • 정밀도: 9로 설정됩니다
  • 반올림: round-half-even으로 설정됩니다

예외 조건

아래 표는 산술 연산 중에 발생할 수 있는 예외 조건과 그에 대응하는 시그널, 그리고 정의된 결과를 나열합니다. 자세한 내용은 명세 [2]를 참조하십시오.

조건 시그널 결과
클램프됨(Clamped) clamped 명세 [2] 참조
0으로 나누기 division-by-zero [sign,inf]
부정확(Inexact) inexact 변경 없음
잘못된 연산 invalid-operation [0,qNaN] (원인이 시그널링 NaN인 경우에는 [s,qNaN] 또는 [s,qNaN,d])
오버플로 overflow 반올림 모드에 따라 다름
반올림됨 rounded 변경 없음
비정규 subnormal 변경 없음
언더플로 underflow 명세 [2]를 참조하십시오

참고: 표준에서 말하는 “Insufficient storage(저장 공간 부족)”가 수의 내부 표현을 담을 저장 공간이 부족하다는 구현별 동작을 뜻하는 경우, 이 구현은 MemoryError를 발생시킵니다.

오버플로와 언더플로에 관해서는 python-dev에서 인위적인 한계를 두고 오랜 논의가 있었습니다. 전반적인 합의는 중요한 이유가 있는 경우에만 인위적인 한계를 유지하자는 것입니다. Tim Peters는 다음 세 가지 이유를 제시합니다:

…지수의 한계를 없애면 사실상 오버플로(및 언더플로)가 절대 일어날 수 없게 됩니다. 하지만 실제 부동소수점 사용에서 오버플로는 탄광의 카나리아처럼 프로그램이 이상하게 동작하기 시작할 때 위험 신호를 일찍 알려 주는, 분명히 귀중한 안전망입니다.

854의 거의 모든 구현은 유한하지 않은 수(무한대와 NaN)를 인코딩하는 데 “금지된” 지수 값을 사용합니다(IBM의 표준도 그렇게 하도록 제안합니다). 지수에 한계가 있으면 추가 저장 공간 비용이 사실상 전혀 없이 이를 수행할 수 있습니다. 지수에 한계가 없다면 그 대신 추가 비트를 사용해야 합니다. 이 비용은 시간과 공간 면에서 더 효율적인 구현을 시도하기 전까지는 드러나지 않습니다.

IBM 표준은 방대하기는 하지만, 완전한 수치 기능을 제공하기 위한 작은 출발점에 불과합니다. 지수 크기에 한계가 없으면 예를 들어 십진수 sin()과 cos()의 구현이 엄청나게 복잡해집니다(인자 축소를 수행하려면 원주율을 사실상 몇 자리까지 알아야 하는지에 대한 선험적 한계가 없어지기 때문입니다).

Edward Loper는 한계를 넘어서야 하는 경우의 예로 확률을 제시합니다.

그렇기는 하지만, Robert Brewer와 Andrew Lentvorski는 사용자가 한계를 쉽게 수정할 수 있기를 원합니다. 사실 이는 충분히 가능합니다:

>>> d1 = Decimal("1e999999999")     # at the exponent limit
>>> d1
Decimal("1E+999999999")
>>> d1 * 10                         # exceed the limit, got infinity
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in ?
    d1 * 10
  ...
  ...
Overflow: above Emax
>>> getcontext().Emax = 1000000000  # increase the limit
>>> d1 * 10                         # does not exceed any more
Decimal("1.0E+1000000000")
>>> d1 * 100                        # exceed again
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in ?
    d1 * 100
  ...
  ...
Overflow: above Emax

반올림 알고리즘

round-down: 버려지는 자릿수는 무시하며, 결과는 변경되지 않습니다(0 방향으로 반올림, 절사):

1.123 --> 1.12
1.128 --> 1.12
1.125 --> 1.12
1.135 --> 1.13

round-half-up: 버려지는 자릿수가 절반(0.5) 이상을 나타내면 결과를 1 증가시켜야 합니다. 그렇지 않으면 버려지는 자릿수는 무시합니다:

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14

round-half-even: 버려지는 자릿수가 절반(0.5)보다 큰 값을 나타내면 결과 계수를 1 증가시킵니다. 절반보다 작은 값을 나타내면 결과를 조정하지 않습니다. 그 밖의 경우에는 결과의 가장 오른쪽 자릿수가 짝수이면 결과를 변경하지 않고, 홀수이면 1 증가시킵니다(짝수 자릿수를 만들기 위해):

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.14

round-ceiling: 버려지는 자릿수가 모두 0이거나 부호가 음수이면 결과는 변경되지 않습니다. 그렇지 않으면 결과를 1 증가시킵니다(양의 무한대 방향으로 반올림):

 1.123 -->  1.13
 1.128 -->  1.13
-1.123 --> -1.12
-1.128 --> -1.12

round-floor: 버려지는 자릿수가 모두 0이거나 부호가 양수이면 결과는 변경되지 않습니다. 그렇지 않으면 결과의 절댓값을 1 증가시킵니다(음의 무한대 방향으로 반올림):

 1.123 -->  1.12
 1.128 -->  1.12
-1.123 --> -1.13
-1.128 --> -1.13

round-half-down: 버려지는 자릿수가 절반(0.5)보다 큰 값을 나타내면 결과를 1 증가시킵니다. 그렇지 않으면 버려지는 자릿수는 무시합니다:

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.13

round-up: 버려지는 자릿수가 모두 0이면 결과는 변경되지 않으며, 그렇지 않으면 결과를 1 증가시킵니다(0에서 멀어지는 방향으로 반올림):

1.123 --> 1.13
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14

근거

요구 사항을 두 부분으로 나누어야 합니다. 첫 번째는 ANSI 표준을 준수하는 것입니다. 이를 위한 모든 요구 사항은 Mike Cowlishaw의 작업 [2]에 명시되어 있습니다. 그는 또한 매우 방대한 테스트 케이스 모음을 제공했습니다.

두 번째 부분의 요구 사항(표준 Python 함수 지원, 사용성 등)은 여기서부터 자세히 설명하며, 내려진 모든 결정과 그 이유, 그리고 아직 논의 중인 모든 주제를 포함할 것입니다.

명시적 생성

컨텍스트는 연산 결과에만 영향을 미치므로, 명시적 생성은 컨텍스트의 영향을 받지 않습니다(반올림도 없고, 정밀도에 의한 제한도 없습니다). 이에 대한 유일한 예외는 Creating from Context의 경우입니다.

int 또는 long에서 생성

손실이 없으며 다른 정보를 지정할 필요도 없습니다:

Decimal(35)
Decimal(-124)

문자열에서 생성

Python 십진 정수 리터럴과 Python float 리터럴을 담은 문자열을 지원할 것입니다. 이 변환에서는 문자열이 Decimal로 직접 변환되므로(float를 거치는 중간 변환이 없으므로) 정보 손실이 없습니다:

Decimal("-12")
Decimal("23.2e-7")

또한 이 방법으로 모든 특수 값(Infinity와 Not a Number)을 생성할 수 있습니다:

Decimal("Inf")
Decimal("NaN")

float에서 생성

이 항목에 대한 초기 논의는 생성자에 부동 소수점 수를 전달할 때 어떤 일이 일어나야 하는가였습니다:

  1. Decimal(1.1) == Decimal('1.1')
  2. Decimal(1.1) == Decimal('110000000000000008881784197001252...e-51')
  3. 예외가 발생합니다

여러 사람이 Decimal(1.1)이라고 쓸 때 기대하는 것이 바로 (1)이므로 여기서는 (1)이 더 나은 선택이라고 주장했습니다. 그리고 John Roth의 말을 인용하면, 이는 구현하기 쉽습니다:

실제 숫자가 어디에서 끝나고 불분명한 부분이 어디에서 시작되는지 찾는 것은 전혀 어렵지 않습니다. 눈으로 보고 할 수도 있으며, 이를 위한 알고리즘도 꽤 잘 알려져 있습니다.

하지만 제 숫자가 정말로 Decimal('110000000000000008881784197001252...e-51')이기를 원한다면, 왜 Decimal(1.1)이라고 쓸 수 없습니까? 왜 Decimal이 그 값을 “반올림”하리라고 기대해야 합니까? 1.1은 바로 이진 부동 소수점이므로 결과를 예측할 수 있다는 점을 기억하십시오. 초보자에게는 직관적이지 않지만, 원래 그런 것입니다.

어쨌든 Paul Moore는 다음과 같은 이유로 (1)이 동작할 수 없음을 보였습니다:

(1) says  D(1.1) == D('1.1')
but       1.1 == 1.1000000000000001
so        D(1.1) == D(1.1000000000000001)
together: D(1.1000000000000001) == D('1.1')

이는 틀렸습니다. Decimal('1.1')이라고 쓰면 D(1.1000000000000001)이 아니라 정확한 값이기 때문입니다. 그는 또한 float로의 명시적 변환을 두자고 제안했습니다. bokr는 생성자에 정밀도를 넣어야 한다고 말했고 mwilson도 이에 동의했습니다:

d = Decimal (1.1, 1)  # take float value to 1 decimal place
d = Decimal (1.1)  # gets `places` from pre-set context

하지만 Alex Martelli는 다음과 같이 말합니다:

지정한 정밀도로 생성하는 것은 괜찮을 것입니다. 따라서 저는 “어떤 기본 정밀도로 float에서 생성하는 것”은 순진한 사용자를 속일 위험이 상당하다고 생각합니다.

그래서 c.l.p를 통해 받아들여진 해결책은 Decimal을 float로 호출할 수 없게 하는 것입니다. 그 대신 Decimal.from_float()라는 메서드를 사용해야 합니다. 문법은 다음과 같습니다:

Decimal.from_float(floatNumber, [decimal_places])

여기서 floatNumber는 생성의 원본이 되는 float 숫자이고, decimal_places는 반올림(round-half-up)을 적용할 경우 그 적용 대상이 되는 소수점 이하 자릿수입니다. 이런 방식으로, 예를 들어 다음과 같이 할 수 있습니다:

Decimal.from_float(1.1, 2): The same as doing Decimal('1.1').
Decimal.from_float(1.1, 16): The same as doing Decimal('1.1000000000000001').
Decimal.from_float(1.1): The same as doing Decimal('1100000000000000088817841970012523233890533447265625e-51').

이후의 논의를 바탕으로, Py2.4용 API에서 from_float()를 빼기로 결정했습니다. 이 결정 과정에는 여러 생각이 영향을 주었습니다:

  • 십진 부동 소수점과 이진 부동 소수점 사이의 상호 작용은 사용자가 표현과 반올림 오차라는 까다로운 문제를 다루도록 강요합니다. 애초에 이 모듈을 두는 주된 이유가 그러한 문제를 피하는 것입니다.
  • 모듈의 첫 릴리스는 안전하고, 최소한이며, 필수적인 것에 집중해야 합니다.
  • 이론적으로는 좋지만, float와 decimal 사이의 상호 작용에 대한 실제 사용 사례는 부족합니다. Java는 레거시 자료 구조가 입력과 출력을 이진 부동소수점으로 저장하도록 요구하더라도 계산은 십진수로 수행하는 것이 가장 좋은 특이한 경우를 처리하기 위해 float/decimal 변환을 포함했습니다.
  • 필요하다면 사용자는 문자열 표현을 중간 타입으로 사용할 수 있습니다. 이 접근 방식의 장점은 정밀도와 표현에 대한 가정을 명시적으로 드러낸다는 것입니다(내부에서 무슨 일이 일어나는지 궁금해할 필요가 없습니다).
  • BigDecimal(double val)에 대한 Java 문서에는 이 생성자에 대한 그들의 경험이 반영되어 있습니다:
    The results of this constructor can be somewhat
    unpredictable and its use is generally not recommended.
    

튜플에서

Aahz는 튜플에서 생성하자고 제안했습니다: eval()의 왕복 변환을 구현하기가 더 쉽고, “Decimal을 나타내는 숫자 값을 가진 사람은 그것을 문자열로 변환할 필요가 없다”는 것입니다.

구조는 부호, 숫자, 지수라는 세 요소로 이루어진 튜플이 될 것입니다. 부호는 1 또는 0이고, 숫자는 십진 숫자들의 튜플이며, 지수는 부호 있는 int 또는 long입니다:

Decimal((1, (3, 2, 2, 5), -2))     # for -32.25

물론 이런 방식으로 모든 특수 값을 생성할 수 있습니다:

Decimal( (0, (0,), 'F') )          # for Infinity
Decimal( (0, (0,), 'n') )          # for Not a Number

Decimal에서

특별한 것은 없고, 그저 복사일 뿐입니다.

모든 경우에 대한 문법

Decimal(value1)
Decimal.from_float(value2, [decimal_places])

여기서 value1은 int, long, 문자열, 3-튜플 또는 Decimal일 수 있고, value2는 float만 가능하며, decimal_places는 선택적인 음이 아닌 int입니다.

컨텍스트로부터 생성하기

이 항목은 python-dev에서 두 곳으로부터 동시에 제기되었습니다. Ka-Ping Yee는 인스턴스를 생성할 때 컨텍스트를 인자로 전달하자고 제안합니다(그는 자신이 전달하는 컨텍스트가 생성 시점에만 사용되기를 원합니다: “지속되지 않을 것입니다”). Tony Meyer는 from_string이 True 값을 가진 “honour_context” 매개변수를 받으면 컨텍스트를 따르도록 하자고 요청합니다. (저는 이것이 마음에 들지 않습니다. 문서에는 컨텍스트를 따라야 한다고 명시되어 있으며, 저는 메서드가 인자의 값에 따라 명세를 준수하는 것을 원하지 않기 때문입니다.)

Tim Peters는 컨텍스트를 사용하는 생성 방식이 필요한 이유를 제시합니다:

일반적인 수치 계산에서 리터럴은 높은 정밀도로 주어질 수 있지만, 그 정밀도는 공짜가 아니며 대개는 필요하지 않습니다

Casey Duncan은 bool 인자가 아닌 별도의 메서드를 사용하기를 원합니다:

저는 불리언 인자가 일반적으로 안티 패턴이라고 생각하며, 클래스 메서드가 있는 상황에서는 더욱 그렇습니다. Decimal.rounded_to_context(“3.14159265”)와 같은 대체 생성자를 사용하는 것은 어떻습니까?

그 문법을 결정하는 과정에서 Tim이 더 나은 아이디어를 내놓았습니다. 그는 Decimal에 다른 컨텍스트로 생성하는 메서드를 두는 대신, Context에 Decimal 인스턴스를 생성하는 메서드를 두자고 제안합니다. 기본적으로, 다음 대신:

D.using_context(number, context)

다음과 같이 됩니다:

context.create_decimal(number)

Tim의 말입니다:

사양의 모든 연산은 두 가지 문자열 변환 연산을 제외하고 컨텍스트를 사용하지만, 사양의 어떤 연산도 선택적인 지역 컨텍스트를 지원하지 않습니다. Decimal() 생성자가 기본적으로 컨텍스트를 무시하는 것은 사양을 확장한 것입니다. 사양을 충족하려면 컨텍스트를 따르는 문자열로부터의 변환 연산을 제공해야 합니다. 저는 어떤 연산에서든 “지역 컨텍스트”라는 개념을 두는 것을 권하지 않습니다 – 이는 모델을 복잡하게 만들며 필요하지도 않습니다.

그래서 우리는 컨텍스트 메서드를 사용하여 Decimal을 생성하기로 결정했으며, 이 Decimal은 (생성될 때에만) 바로 그 컨텍스트를 사용합니다(이후 연산에서는 스레드의 컨텍스트를 사용합니다). 그렇다면 메서드의 이름은 무엇으로 해야 하겠습니까?

Tim Peters는 다양한 소스로부터 생성하는 세 가지 메서드(from_string, from_int, from_float)를 제안합니다. 저는 데이터 타입에 상관없이 하나의 메서드인 create_decimal()을 사용하자고 제안했습니다. Michael Chermside: “이 이름은 제 머릿속에 딱 들어맞습니다. 이것이 컨텍스트를 사용한다는 사실은 Context의 메서드라는 점에서 분명합니다”.

커뮤니티는 이에 동의했습니다. 초보자는 Context의 생성 메서드를 사용하지 않을 것이므로 저는 이것이 괜찮다고 생각합니다(float로부터 생성하기 위해 Decimal에 별도의 메서드를 두는 것은 단지 초보자가 이진 부동 소수점 문제를 겪지 않도록 하기 위한 것입니다).

요약하자면, 특정 컨텍스트(생성 시점에만 사용되고 이후에는 사용되지 않음)를 사용하여 Decimal 인스턴스를 생성하려면 해당 컨텍스트의 메서드를 사용해야 합니다:

# n is any datatype accepted in Decimal(n) plus float
mycontext.create_decimal(n)

예시:

>>> # create a standard decimal instance
>>> Decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using the thread context
>>> thread_context = getcontext()
>>> thread_context.prec
28
>>> thread_context.create_decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using other context
>>> other_context = thread_context.copy()
>>> other_context.prec = 4
>>> other_context.create_decimal("11.2233445566778899")
Decimal("11.22")

암시적 생성

암시적 생성은 연산의 결과이므로, 각 항목에서 자세히 설명한 대로 컨텍스트의 영향을 받습니다.

John Roth는 “다른 타입은 decimal() 생성자가 처리하는 것과 같은 방식으로 처리되어야 합니다”라고 제안했습니다. 그러나 Alex Martelli는 다음과 같이 생각합니다.

이렇게 Python의 전통을 완전히 깨뜨리는 것은 끔찍한 실수가 될 것입니다. 23+”43”은 23+int(“45”)와 같은 방식으로 처리되지 않으며, 이는 매우 좋은 일이기도 합니다. 사용자가 생성(변환)을 원한다고 명시적으로 나타내는 것과, 우연히 두 객체를 더했는데 그중 하나가 실수로 문자열일 수 있는 것은 완전히 다른 일입니다.

그래서 여기서 각 데이터 타입별 동작을 다시 정의합니다.

int 또는 long에서

int 또는 long은 현재 컨텍스트에서 Decimal(str(x))로 명시적으로 생성한 Decimal처럼 취급됩니다(즉, 문자열 변환 시의 반올림 규칙이 적용되고 해당 플래그가 설정됩니다). 이를 통해 Decimal('1234567') + 13579 같은 표현식이 Decimal('1234567') + Decimal('13579')라는 멘탈 모델과 일치함이 보장됩니다. 모든 정수는 표현 오차 없이 문자열로 나타낼 수 있으므로 이 모델이 성립합니다.

문자열에서

여기서는 예외를 발생시켜야 한다는 데 모두가 동의합니다.

float에서

Aahz는 float과의 상호 작용에 강하게 반대하며, 명시적 변환을 제안합니다.

문제는 Decimal이 float보다 더 높은 정밀도, 정확도, 범위를 가질 수 있다는 점입니다.

유효한 Python 표현식인 35 + 1.1의 예는 Decimal(35) + 1.1도 유효해야 함을 시사하는 것처럼 보입니다. 그러나 자세히 살펴보면, 이 예는 정수에서 부동 소수점으로의 변환이 가능하다는 것만 보여 줍니다. 따라서 십진 부동 소수점에 대한 올바른 대응은 35 + Decimal(1.1)입니다. int에서 float으로의 강제 변환과 int에서 Decimal로의 강제 변환은 모두 표현 오차 없이 수행할 수 있습니다.

이진 부동 소수점과 십진 부동 소수점 사이의 강제 변환을 어떻게 할 것인가 하는 문제는 더 복잡합니다. 저는 float과의 상호 작용을 허용하되, 정확한 변환을 수행하고 현재 컨텍스트의 정밀도를 초과하면 ValueError를 발생시키자고 제안했습니다(이는 너무 까다로울 수 있습니다. 예를 들어 정밀도가 9일 때 Decimal(35) + 1.2는 문제가 없지만 Decimal(35) + 1.1은 오류를 발생시키기 때문입니다).

결국 이는 너무 까다로운 것으로 드러났습니다. 너무 까다로워서 c.l.p에서는 이 경우 TypeError를 발생시키기로 합의했습니다. 즉, Decimal과 float을 섞어 쓸 수 없습니다.

Decimal에서

여기서는 아무런 문제가 없습니다.

컨텍스트의 사용

지난 pre-PEP에서 저는 “컨텍스트는 어디에나 존재해야 하며, 이는 컨텍스트를 변경하면 현재와 미래의 모든 Decimal 인스턴스에 영향을 준다는 뜻입니다”라고 말했습니다. 제가 틀렸습니다. 이에 대해 John Roth는 다음과 같이 말했습니다.

컨텍스트는 특정 용도에 맞게 선택할 수 있어야 합니다. 즉, 하나의 애플리케이션에서 여러 개의 서로 다른 컨텍스트를 동시에 사용할 수 있어야 합니다.

comp.lang.python에서 Aahz는 “스레드별 컨텍스트”를 두자는 것이 이 아이디어라고 설명했습니다. 따라서 한 스레드의 모든 인스턴스는 하나의 컨텍스트에 속하며, 스레드 B에는 아무런 변경도 가하지 않고 스레드 A의 컨텍스트(그리고 그 스레드에 속한 인스턴스의 동작)를 변경할 수 있습니다.

또한, 이번에도 제 말을 바로잡으면서 그는 이렇게 말했습니다:

컨텍스트는 연산에만 적용되며 Decimal 인스턴스에는 적용되지 않습니다. 기존 인스턴스에 대해 연산을 수행하지 않는다면, 컨텍스트를 변경해도 해당 인스턴스는 영향을 받지 않습니다.

현재 컨텍스트의 규칙과 다른 규칙으로 연산을 수행해야 하는 특수한 경우에 대해 논의하면서, Tim Peters는 컨텍스트가 연산을 메서드로 갖게 될 것이라고 말했습니다. 이렇게 하면 사용자는 “필요한 비공개 컨텍스트 객체를 얼마든지 만들 수 있고, 산술 연산을 그 비공개 컨텍스트 객체에 대한 명시적인 메서드 호출로 표현할 수 있으므로, 기본 스레드 컨텍스트 객체를 참조하지도 수정하지도 않게” 됩니다.

Python 사용성

  • Decimal은 다음과 같은 경우에 기본 산술 연산자(+, -, *, /, //, **, %, divmod)와 비교 연산자(==, !=, <, >, <=, >=, cmp)를 지원해야 합니다(OtherType이 어떤 타입일 수 있는지, 그리고 각 경우에 어떤 일이 일어나는지는 Implicit construction을 확인하십시오):
    • Decimal op Decimal
    • Decimal op otherType
    • otherType op Decimal
    • Decimal op= Decimal
    • Decimal op= otherType
  • Decimal은 단항 연산자(-, +, abs)를 지원해야 합니다.
  • repr()은 왕복 변환이 가능해야 하며, 이는 다음을 의미합니다:
    m = Decimal(...)
    m == eval(repr(m))
    
  • Decimal은 불변이어야 합니다.
  • Decimal은 다음 내장 메서드를 지원해야 합니다:
    • min, max
    • float, int, long
    • str, repr
    • 해시
    • bool (0은 거짓, 그 외에는 참)

python-dev에서 hash()의 동작에 대해 몇 차례 논의가 있었습니다. 커뮤니티는 값이 같다면 그 값들의 해시도 같아야 한다는 데 동의합니다. 따라서 Decimal(25) == 25가 True이므로, hash(Decimal(25))의 해시값은 hash(25)와 같아야 합니다.

세부적으로 보면 Decimal은 float나 문자열과 비교할 수 없으므로, 이들이 같은 해시를 내는지는 신경 쓸 필요가 없습니다. 요약하면:

hash(n) == hash(Decimal(n))   # Only if n is int, long, or Decimal

str()과 repr()의 동작에 관해서는, Ka-Ping Yee가 repr()이 str()과 같은 동작을 하도록 제안하고, Tim Peters는 str()이 Spec의 to-scientific-string(과학 표기 문자열 변환) 연산처럼 동작하도록 제안합니다.

이는 가능한데, (Aahz에 따르면) “문자열 형식에는 이미 Decimal 객체를 재구성하는 데 필요한 모든 정보가 담겨 있기” 때문입니다.

그리고 이는 명세에도 부합합니다. Tim Peters의 말입니다:

“to_sci_string”이라는 이름을 가진 메서드가 있어야 한다는 요구 사항은 없으며, 유일한 요구 사항은 to-sci-string(과학 표기 문자열 변환)의 기능을 표현할 어떤 방법이 제공되어야 한다는 것입니다. to-sci-string(과학 표기 문자열 변환)의 의미는 표준에 정확히 명세되어 있으며, str(Decimal)과 repr(Decimal) 모두에 좋은 선택입니다.

문서

이 절에서는 Decimal과 Context의 모든 공개 메서드와 속성을 설명합니다.

Decimal 속성

Decimal에는 공개 속성이 없습니다. 내부 정보는 슬롯에 저장되며 최종 사용자가 접근해서는 안 됩니다.

Decimal 메서드

다음은 명세에 정의된 변환 및 산술 연산과 실제 구현에서 해당 기능을 수행하는 방법입니다.

  • to-scientific-string(과학 표기 문자열 변환): 내장 함수 str()을 사용하십시오:
    >>> d = Decimal('123456789012.345')
    >>> str(d)
    '1.23456789E+11'
    
  • to-engineering-string(공학 표기 문자열 변환): to_eng_string() 메서드를 사용하십시오:
    >>> d = Decimal('123456789012.345')
    >>> d.to_eng_string()
    '123.456789E+9'
    
  • to-number: Context 메서드 create_decimal()을 사용하십시오. 표준 생성자나 from_float() 생성자는 (이 변환에 대해 명세에 규정된 것과 달리) 컨텍스트를 사용하지 않으므로 사용할 수 없습니다.
  • abs: 내장 함수 abs()를 사용하십시오:
    >>> d = Decimal('-15.67')
    >>> abs(d)
    Decimal('15.67')
    
  • add: 연산자 +를 사용하십시오:
    >>> d = Decimal('15.6')
    >>> d + 8
    Decimal('23.6')
    
  • subtract: 연산자 -를 사용하십시오:
    >>> d = Decimal('15.6')
    >>> d - 8
    Decimal('7.6')
    
  • compare: 메서드 compare()를 사용하십시오. 이 메서드는 (내장 함수 cmp()가 아니라) 특수 값을 다룰 때에만 사용해야 합니다:
    >>> d = Decimal('-15.67')
    >>> nan = Decimal('NaN')
    >>> d.compare(23)
    '-1'
    >>> d.compare(nan)
    'NaN'
    >>> cmp(d, 23)
    -1
    >>> cmp(d, nan)
    1
    
  • divide: 연산자 /를 사용하십시오:
    >>> d = Decimal('-15.67')
    >>> d / 2
    Decimal('-7.835')
    
  • divide-integer(정수 나눗셈): // 연산자를 사용하십시오:
    >>> d = Decimal('-15.67')
    >>> d // 2
    Decimal('-7')
    
  • max: 메서드 max()를 사용하십시오. 이 메서드는 (내장 함수 max()가 아니라) 특수 값을 다룰 때에만 사용하십시오:
    >>> d = Decimal('15')
    >>> nan = Decimal('NaN')
    >>> d.max(8)
    Decimal('15')
    >>> d.max(nan)
    Decimal('NaN')
    
  • min: 메서드 min()을 사용하십시오. 이 메서드는 (내장 함수 min()이 아니라) 특수 값을 다룰 때에만 사용하십시오:
    >>> d = Decimal('15')
    >>> nan = Decimal('NaN')
    >>> d.min(8)
    Decimal('8')
    >>> d.min(nan)
    Decimal('NaN')
    
  • minus: 단항 연산자 -를 사용하십시오:
    >>> d = Decimal('-15.67')
    >>> -d
    Decimal('15.67')
    
  • plus: 단항 연산자 +를 사용하십시오:
    >>> d = Decimal('-15.67')
    >>> +d
    Decimal('-15.67')
    
  • multiply: 연산자 *를 사용하십시오:
    >>> d = Decimal('5.7')
    >>> d * 3
    Decimal('17.1')
    
  • normalize: 메서드 normalize()를 사용하십시오:
    >>> d = Decimal('123.45000')
    >>> d.normalize()
    Decimal('123.45')
    >>> d = Decimal('120.00')
    >>> d.normalize()
    Decimal('1.2E+2')
    
  • quantize: 메서드 quantize()를 사용하십시오:
    >>> d = Decimal('2.17')
    >>> d.quantize(Decimal('0.001'))
    Decimal('2.170')
    >>> d.quantize(Decimal('0.1'))
    Decimal('2.2')
    
  • remainder: 연산자 %를 사용하십시오:
    >>> d = Decimal('10')
    >>> d % 3
    Decimal('1')
    >>> d % 6
    Decimal('4')
    
  • remainder-near: 메서드 remainder_near()를 사용하십시오:
    >>> d = Decimal('10')
    >>> d.remainder_near(3)
    Decimal('1')
    >>> d.remainder_near(6)
    Decimal('-2')
    
  • round-to-integral-value: 메서드 to_integral()을 사용하십시오:
    >>> d = Decimal('-123.456')
    >>> d.to_integral()
    Decimal('-123')
    
  • same-quantum: 메서드 same_quantum()을 사용하십시오:
    >>> d = Decimal('123.456')
    >>> d.same_quantum(Decimal('0.001'))
    True
    >>> d.same_quantum(Decimal('0.01'))
    False
    
  • square-root: 메서드 sqrt()를 사용하십시오:
    >>> d = Decimal('123.456')
    >>> d.sqrt()
    Decimal('11.1110756')
    
  • power: 연산자 **를 사용하십시오:
    >>> d = Decimal('12.56')
    >>> d ** 2
    Decimal('157.7536')
    

다음은 그 밖의 메서드와 각 메서드가 존재하는 이유입니다:

  • adjusted(): 조정된 지수를 반환합니다. 이 개념은 명세에 정의되어 있습니다. 조정된 지수란 어떤 수를 소수점 앞에 숫자가 하나만 오는 과학적 표기법으로 나타냈을 때의 지수 값입니다:
    >>> d = Decimal('12.56')
    >>> d.adjusted()
    1
    
  • from_float(): float 데이터 타입으로부터 인스턴스를 생성하는 클래스 메서드:
    >>> d = Decimal.from_float(12.35)
    >>> d
    Decimal('12.3500000')
    
  • as_tuple(): Decimal의 내부 구조인 세 요소 튜플을 보여 줍니다. 이 메서드는 명세에서 요구하는 것은 아니지만, Tim Peters가 제안했고 커뮤니티가 이를 두기로 합의했습니다(개발과 디버깅에 유용합니다):
    >>> d = Decimal('123.4')
    >>> d.as_tuple()
    (0, (1, 2, 3, 4), -1)
    >>> d = Decimal('-2.34e5')
    >>> d.as_tuple()
    (1, (2, 3, 4), 3)
    

컨텍스트 속성

다음은 컨텍스트를 수정하기 위해 변경할 수 있는 속성입니다.

  • prec (int): 정밀도:
    >>> c.prec
    9
    
  • rounding (str): 반올림 유형(반올림 방법):
    >>> c.rounding
    'half_even'
    
  • trap_enablers (dict): trap_enablers[exception] = 1이면, 해당 예외 상황이 일어났을 때 예외가 발생합니다:
    >>> c.trap_enablers[Underflow]
    0
    >>> c.trap_enablers[Clamped]
    0
    
  • flags (dict): 예외가 발생하면 (trap_enabler의 설정 여부와 관계없이) 해당 플래그인 flags[exception]이 증가합니다. Decimal 인스턴스의 사용자가 재설정해야 합니다:
    >>> c.flags[Underflow]
    0
    >>> c.flags[Clamped]
    0
    
  • Emin (int): 최소 지수:
    >>> c.Emin
    -999999999
    
  • Emax (int): 최대 지수:
    >>> c.Emax
    999999999
    
  • capitals (int): 문자열에서 ‘E’(True/1)를 사용할지 ‘e’(False/0)를 사용할지 정하는 불리언 플래그(예: ‘1.32e+2’ 또는 ‘1.32E+2’):
    >>> c.capitals
    1
    

컨텍스트 메서드

다음 메서드는 명세의 Decimal 기능을 따릅니다. 특정 컨텍스트를 통해 호출되는 연산은 스레드 컨텍스트가 아니라 해당 컨텍스트를 사용한다는 점에 유의하십시오.

이 메서드를 사용할 때는 연산자가 이항인지 단항인지에 따라 문법이 달라진다는 점에 유의하십시오. 예를 들면 다음과 같습니다:

>>> mycontext.abs(Decimal('-2'))
'2'
>>> mycontext.multiply(Decimal('2.3'), 5)
'11.5'

따라서 다음은 명세의 연산 및 변환과, 이를 컨텍스트를 통해 수행하는 방법입니다(여기서 d는 Decimal 인스턴스이고 n은 Implicit construction에서 사용할 수 있는 수입니다):

  • to-scientific-string(과학 표기 문자열 변환): to_sci_string(d)
  • to-engineering-string(공학 표기 문자열 변환): to_eng_string(d)
  • to-number: create_decimal(number), number에 대해서는 Explicit construction을 참고하십시오.
  • abs: abs(d)
  • add: add(d, n)
  • subtract: subtract(d, n)
  • compare: compare(d, n)
  • divide: divide(d, n)
  • divide-integer(정수 나눗셈): divide_int(d, n)
  • max: max(d, n)
  • min: min(d, n)
  • minus: minus(d)
  • plus: plus(d)
  • multiply: multiply(d, n)
  • normalize: normalize(d)
  • quantize: quantize(d, d)
  • remainder: remainder(d)
  • remainder-near: remainder_near(d)
  • round-to-integral-value: to_integral(d)
  • same-quantum: same_quantum(d, d)
  • square-root: sqrt(d)
  • power: power(d, n)

divmod(d, n) 메서드는 Context를 통해 십진수 기능을 지원합니다.

다음은 Context에서 유용한 정보를 반환하는 메서드입니다:

  • Etiny(): 정밀도를 고려한 최소 지수입니다.
    >>> c.Emin
    -999999999
    >>> c.Etiny()
    -1000000007
    
  • Etop(): 정밀도를 고려한 최대 지수입니다.
    >>> c.Emax
    999999999
    >>> c.Etop()
    999999991
    
  • copy(): 컨텍스트의 복사본을 반환합니다.

참조 구현

Python 2.4-alpha 시점에 이 코드는 표준 라이브러리에 체크인되었습니다. 최신 버전은 다음에서 구할 수 있습니다:

http://svn.python.org/view/python/trunk/Lib/decimal.py

테스트 케이스는 다음에 있습니다:

http://svn.python.org/view/python/trunk/Lib/test/test_decimal.py

참고 문헌