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에서 사전 PEP를 제안한 후 커뮤니티는 필요한 산술 동작을 갖춘 숫자 데이터 타입을 만든 다음 그 위에 Money를 구축하는 데 동의했습니다. 소수점 이하 자릿수, 반올림 등에 관한 모든 고려 사항은 Money를 통해 처리됩니다. 이 PEP의 목적은 추가 작업 없이 Money로 사용할 수 있는 데이터 타입을 제공하는 것이 아닙니다.

표준을 구현할 때의 가장 큰 장점 중 하나는 누군가가 모든 까다로운 경우를 이미 대신 고민해 두었다는 것입니다. 그리고 표준을 찾던 중 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…이 됩니다. 보시다시피 문제는 일부 십진수를 이진법으로 정확하게 표현할 수 없어서 작은 반올림 오차가 발생한다는 것입니다.

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

왜 부동 소수점인가?

십진수 방식을 선택한다면, 왜 floating point입니까?

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

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

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

이와 대조적으로 무한 정밀도를 가진 long 정수의 예가 있습니다. 즉, 원하는 만큼 큰 수를 사용할 수 있으며 어떤 정보도 잃지 않습니다.

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

그런데 왜 무한 정밀도의 부동 소수점 수를 사용할 수 없습니까? 부정확한 나눗셈 때문에 그렇게 간단하지 않습니다. 예: 1/3 = 0.3333333333333… 무한히 계속됩니다. 이 경우 3을 무한히 많이 저장해야 하므로 메모리를 너무 많이 차지합니다, ;).

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

이 내용을 접하고 나면 아마 이렇게 생각할 것입니다. “이봐요! 1과 3을 분자와 분모로 저장하면 안 됩니까?” 그러면 다음 주제로 이어집니다.

유리수는 어떻습니까?

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

Alex Martelli의 말을 인용하면 다음과 같습니다.

두 유리수를 합한 결과가 O(M+N)의 메모리 공간을 차지하는 유리수가 된다는 사실의 성능상 영향은, 두 유리수가 각각 O(M)과 O(N)의 공간을 차지한다는 점을 고려하면 정말 감당하기 어렵습니다. 순수 Python과 확장 기능(예: gmpy) 모두에 훌륭한 Rational 구현이 있지만, 제 생각에는 언제나 “틈새 시장”에 머물 것입니다. 아마도 PEP로 제안할 가치는 있지만, Decimal 없이는 구현할 가치가 없습니다. Decimal이야말로 화폐 총액을 표현하는 올바른 방법이며, 실제 세계에서 정말 중요한 사용 사례이기 때문입니다.

어쨌든 이 데이터 형식에 관심이 있다면 PEP 239: Python에 Rational 형식 추가를 살펴보는 것이 좋습니다.

그럼 무엇을 얻게 됩니까?

그 결과는 정밀도가 제한된 부동 소수점 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]에서 정의한 십진 산술 모델을 기반으로 합니다.

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

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

숫자

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

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

  • 부호: 0(양수) 또는 1(음수)입니다.
  • 계수: 음이 아닌 정수입니다.
  • 지수: 부호가 있는 정수이며, 계수 승수의 10의 거듭제곱입니다.

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

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

특수 값의 명칭은 다음과 같습니다.

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

컨텍스트

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

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

이러한 정의는 Decimal 숫자의 내부 저장 방식에는 영향을 주지 않고, 산술 연산이 수행되는 방식에만 영향을 줍니다.

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

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

기본 컨텍스트

사양에서는 사용자가 쉽게 선택할 수 있는 두 가지 기본 컨텍스트를 정의합니다.

기본 기본 컨텍스트:

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

확장 기본 컨텍스트:

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

예외 조건

아래 표에는 산술 연산 중 발생할 수 있는 예외 조건, 해당 신호 및 정의된 결과가 나열되어 있습니다. 자세한 내용은 사양 [2]을 참조하십시오.

조건 신호 결과
클램프됨 clamped 사양 [2]을 참조하십시오.
0으로 나누기 division-by-zero [sign,inf]
부정확함 inexact 변경되지 않음
잘못된 연산 잘못된 연산 [0,qNaN] (또는 원인이 신호 NaN인 경우 [s,qNaN] 또는 [s,qNaN,d])
오버플로 오버플로 반올림 모드에 따라 다름
반올림됨 반올림됨 변경되지 않음
서브노멀 서브노멀 변경되지 않음
언더플로 언더플로 사양 [2]을 참조하십시오.

참고: 표준에서 “저장 공간 부족”을 언급할 때, 숫자의 내부 정보를 유지하기에 충분한 저장 공간이 없는 것에 관한 구현별 동작인 한 이 구현은 MemoryError를 발생시킵니다.

오버플로와 언더플로에 관해서는 인위적인 한계에 대해 python-dev에서 오랫동안 논의해 왔습니다. 일반적인 의견은 그렇게 해야 할 중요한 이유가 있는 경우에만 인위적인 한계를 유지하자는 것입니다. Tim Peters는 다음 세 가지를 제시합니다:

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

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

규모가 큰 IBM 표준도 완전한 수치 기능을 제공하기 위한 작은 출발점에 불과합니다. 지수 크기에 한계가 없으면 예를 들어 decimal sin() 및 cos() 구현이 크게 복잡해집니다(그러면 인자 축소를 수행하기 위해 사실상 알아야 하는 pi의 자릿수에 선험적인 한계가 없어집니다).

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 10진 정수 리터럴과 Python 부동 소수점 리터럴을 포함하는 문자열이 지원됩니다. 이 변환에서는 문자열이 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)이 더 나은 선택이라고 주장했습니다. 그리고 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를 통한 합의된 해결책은 float로 Decimal을 호출할 수 없다는 것입니다. 대신 메서드를 사용해야 합니다: Decimal.from_float(). 구문은 다음과 같습니다.:

Decimal.from_float(floatNumber, [decimal_places])

여기서 floatNumber는 생성의 원본인 부동소수점 수이고, 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()을 제외하기로 결정했습니다. 이러한 사고 과정에는 몇 가지 아이디어가 기여했습니다.

  • 십진수와 이진 부동소수점의 상호작용으로 인해 사용자는 표현과 반올림 오차의 까다로운 문제를 다루어야 합니다. 그러한 문제를 피하는 것이 애초에 이 모듈을 만드는 주된 이유입니다.
  • 모듈의 첫 번째 릴리스는 안전하고 최소한이며 필수적인 기능에 집중해야 합니다.
  • 이론적으로는 훌륭하지만, 실제 세계에서 부동소수점 수와 십진수의 상호작용을 사용하는 사례는 부족합니다. 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, string, 3-튜플 또는 Decimal일 수 있고, value2는 float만 가능하며, decimal_places는 선택적인 음이 아닌 int입니다.

컨텍스트에서 생성하기

이 항목은 python-dev에서 병렬적으로 두 출처에서 제기되었습니다. Ka-Ping Yee는 인스턴스를 생성할 때 컨텍스트를 인자로 전달할 것을 제안합니다(그는 전달한 컨텍스트가 생성 시점에만 사용되기를 원합니다. “지속되지는 않을 것입니다.”). Tony Meyer는 “honour_context” 매개변수가 True 값으로 전달되면 from_string이 컨텍스트를 따르도록 요청합니다. (문서에서 컨텍스트를 따라야 한다고 명시하고 있으며, 인자 값에 관한 명세를 메서드가 따르도록 하고 싶지 않기 때문에 저는 이를 좋아하지 않습니다.)

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() 생성자가 기본적으로 컨텍스트를 무시하는 것은 명세의 확장입니다. 명세를 충족하려면 컨텍스트를 따르는 from-string 연산을 제공해야 합니다. 어떤 연산에서도 “로컬 컨텍스트”라는 개념을 사용하지 않는 것을 권장합니다. 이는 모델을 복잡하게 만들며 필요하지도 않습니다.

따라서 특정 컨텍스트를 사용하여 Decimal을 생성하는 컨텍스트 메서드를 사용하기로 결정했습니다(생성할 때만 해당 컨텍스트를 사용하고, 이후 연산에서는 스레드의 컨텍스트를 사용합니다). 그러나 그 메서드의 이름은 무엇으로 해야 합니까?

Tim Peters는 다양한 원천에서 생성하기 위한 세 가지 메서드(from_string, from_int, from_float)를 제안합니다. 저는 데이터 형식을 신경 쓰지 않고 하나의 메서드인 create_decimal()을 사용하자고 제안했습니다. Michael Chermside: “그 이름이 제 머릿속에 딱 들어맞습니다. 컨텍스트를 사용한다는 사실은 그것이 Context 메서드라는 점에서 분명합니다.”

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

따라서 간단히 말해, 특정 컨텍스트를 사용하여 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는 OK이지만 Decimal(35) + 1.1은 오류를 발생시키므로, 이는 다소 까다로울 수 있습니다).

이는 너무 까다로운 것으로 드러났습니다. 너무 까다로웠기 때문에 c.l.p는 이 경우 TypeError를 발생시키기로 동의했습니다. Decimal과 float을 혼합할 수 없게 된 것입니다.

Decimal에서

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

컨텍스트 사용

마지막 PEP 이전 문서에서 저는 “컨텍스트는 항상 존재해야 하며, 이는 컨텍스트의 변경 사항이 현재 및 미래의 모든 Decimal 인스턴스에 영향을 미친다는 의미입니다”라고 말했습니다. 제가 틀렸습니다. 이에 대해 John Roth는 다음과 같이 말했습니다.

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

comp.lang.python에서 Aahz는 이 아이디어가 “스레드마다 하나의 컨텍스트”를 두는 것이라고 설명했습니다. 따라서 한 스레드의 모든 인스턴스는 하나의 컨텍스트에 속하며, 스레드 A에서 컨텍스트를 변경해도 스레드 B의 컨텍스트는 물론 스레드 B 인스턴스의 동작도 변경되지 않습니다.

또한, 그리고 다시 한번 저를 바로잡으며, 그는 다음과 같이 말했습니다:

컨텍스트는 연산에만 적용되고 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
    • hash
    • bool (0은 거짓이고 그 외에는 참입니다)

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

중요한 점은 Decimal을 부동 소수점 수나 문자열과 비교할 수 없으므로, 이들이 같은 해시값을 제공하는지는 걱정할 필요가 없다는 것입니다. 요컨대:

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 객체를 재구성하는 데 필요한 모든 정보가 포함되어 있기” 때문에 가능합니다.

또한 이는 Spec에도 부합합니다. 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(): 조정된 지수를 반환합니다. 이 개념은 Spec에 정의되어 있습니다. 조정된 지수는 어떤 수를 소수점 앞에 한 자리만 있는 과학적 표기법으로 표현한 것처럼 했을 때 그 수의 지수 값입니다.:
    >>> d = Decimal('12.56')
    >>> d.adjusted()
    1
    
  • from_float(): 부동 소수점 데이터 형식에서 인스턴스를 생성하는 클래스 메서드입니다.:
    >>> d = Decimal.from_float(12.35)
    >>> d
    Decimal('12.3500000')
    
  • as_tuple(): Decimal의 내부 구조인 세 요소 튜플을 표시합니다. 이 메서드는 Spec에서 요구하지 않지만 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
    

컨텍스트 메서드

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

이러한 메서드를 사용하려면 연산자가 이항 연산자인지 단항 연산자인지에 따라 구문이 변경된다는 점에 유의하십시오.:

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

따라서 다음은 Spec의 연산 및 변환과, 컨텍스트를 통해 이를 수행하는 방법입니다(d는 Decimal 인스턴스이고 nImplicit 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

참고 문헌