decimal — 10진 고정소수점 및 부동소수점 산술

decimal — 10진 고정소수점 및 부동소수점 산술

소스 코드: Lib/decimal.py

decimal 모듈은 빠르고 올바르게 반올림되는 10진 부동소수점 산술을 지원해요. float 타입보다 몇 가지 장점이 있어요.

  • Decimal은 "사람을 염두에 두고 설계된 부동소수점 모델"을 기반으로 해요. 사람이 학교에서 배우는 산술과 같은 방식으로 동작하는 산술을 컴퓨터가 제공해야 한다는 원칙을 따르죠.
  • Decimal 숫자는 정확히 표현돼요. 반면 1.1, 2.2 같은 수는 이진 부동소수점에서는 정확히 표현되지 않아요. 일반 사용자는 1.1 + 2.2가 이진 부동소수점처럼 3.3000000000000003으로 표시되는 걸 기대하지 않죠.
  • 정확성은 산술에도 이어져요. 10진 부동소수점에서 0.1 + 0.1 + 0.1 - 0.3은 정확히 0이 돼요. 이진 부동소수점에서는 5.5511151231257827e-017이 나와요. 0에 가깝긴 하지만 신뢰할 수 있는 동등성 테스트를 막고 오차가 누적될 수 있죠. 그래서 decimal은 엄격한 동등성 불변식을 요구하는 회계 애플리케이션에서 선호돼요.
  • decimal 모듈은 유효 자릿수 개념을 도입해서 1.30 + 1.202.50이 돼요. 끝의 0을 유지해 유효성을 나타내죠. 이것이 통화 애플리케이션의 관례적인 표시 방식이에요. 곱셈에서는 "교과서" 방식처럼 피승수의 모든 자릿수를 사용해서, 1.3 * 1.21.56, 1.30 * 1.201.5600이 돼요.
  • 하드웨어 기반 이진 부동소수점과 달리 decimal 모듈은 사용자가 바꿀 수 있는 정밀도(기본 28자리)를 가지며, 문제에 필요한 만큼 크게 설정할 수 있어요.
>>> from decimal import *
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')

이진·10진 부동소수점 둘 다 공개된 표준에 따라 구현돼요. 내장 float 타입은 기능의 일부만 드러내는 반면, decimal 모듈은 표준의 필요한 모든 부분을 노출해요. 필요하면 프로그래머가 반올림과 시그널 처리에 대한 전체 제어권을 가져요. 비정확한 연산을 예외로 차단해 정확한 산술을 강제하는 옵션도 있어요.

출처: Python 표준 라이브러리

본문

모듈 설계는 세 가지 개념을 중심으로 해요. 10진 숫자(Decimal number), 산술을 위한 컨텍스트(context), 그리고 시그널(signal)이에요.

  • 10진 숫자는 불변(immutable)이에요. 부호(sign), 계수 자릿수(coefficient digits), 지수(exponent)를 가져요. 유효성을 보존하기 위해 계수 자릿수는 끝의 0을 버리지 않아요. 또한 Infinity, -Infinity, NaN 같은 특수 값도 포함하며, 표준은 -0+0을 구분해요.
  • 산술을 위한 컨텍스트는 정밀도, 반올림 규칙, 지수 한계, 연산 결과를 나타내는 플래그, 그리고 시그널을 예외로 취급할지 결정하는 트랩 활성화(trap enabler)를 지정하는 환경이에요. 반올림 옵션에는 ROUND_CEILING, ROUND_DOWN, ROUND_FLOOR, ROUND_HALF_DOWN, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_UP, ROUND_05UP이 있어요.
  • 시그널은 계산 과정에서 발생하는 비정상 조건들의 집합이에요. 애플리케이션의 필요에 따라 무시하거나, 정보 제공용으로 보거나, 예외로 취급할 수 있어요. decimal 모듈의 시그널은 Clamped, InvalidOperation, DivisionByZero, Inexact, Rounded, Subnormal, Overflow, Underflow, FloatOperation이에요.

각 시그널에는 플래그와 트랩 활성화가 있어요. 시그널을 만나면 플래그가 1로 설정되고, 트랩 활성화가 1이면 예외가 발생해요. 플래그는 끈적(sticky)이므로 계산을 모니터링하기 전에 사용자가 재설정해야 해요.

참고로 IBM의 General Decimal Arithmetic Specification도 함께 보세요.

빠른 시작 튜토리얼

보통 decimal을 쓰려면 모듈을 임포트하고, getcontext()로 현재 컨텍스트를 본 다음, 필요하면 정밀도·반올림·활성 트랩에 새 값을 설정하는 걸로 시작해요.

>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[], traps=[Overflow, DivisionByZero,
        InvalidOperation])

>>> getcontext().prec = 7       # Set a new precision

Decimal 인스턴스는 정수, 문자열, 부동소수점, 튜플에서 만들 수 있어요. 정수나 float에서 만드는 것은 그 값의 정확한 변환이에요. Decimal 숫자에는 "숫자가 아님"을 뜻하는 NaN, 양·음의 Infinity, -0 같은 특수 값이 포함돼요.

>>> getcontext().prec = 28
>>> Decimal(10)
Decimal('10')
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> Decimal((0, (3, 1, 4), -2))
Decimal('3.14')
>>> Decimal(str(2.0 ** 0.5))
Decimal('1.4142135623730951')
>>> Decimal(2) ** Decimal('0.5')
Decimal('1.414213562373095048801688724')
>>> Decimal('NaN')
Decimal('NaN')
>>> Decimal('-Infinity')
Decimal('-Infinity')

FloatOperation 시그널이 트랩되면 생성자나 순서 비교에서 decimal과 float를 실수로 섞으면 예외가 발생해요.

>>> c = getcontext()
>>> c.traps[FloatOperation] = True
>>> Decimal(3.14)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') < 3.7
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') == 3.5
True

버전 3.3에 추가됨.

Decimal의 유효성은 입력 자릿수에 의해서만 결정돼요. 컨텍스트 정밀도와 반올림은 산술 연산 중에만 작용해요.

>>> getcontext().prec = 6
>>> Decimal('3.0')
Decimal('3.0')
>>> Decimal('3.1415926535')
Decimal('3.1415926535')
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85987')
>>> getcontext().rounding = ROUND_UP
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85988')

C 버전의 내부 한계를 초과하면 decimal을 만들 때 InvalidOperation이 발생해요.

>>> Decimal("1e9999999999999999999")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]

버전 3.3에서 변경.

Decimal은 나머지 Python과 잘 어울려요. 작은 예시를 볼게요.

>>> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split()))
>>> max(data)
Decimal('9.25')
>>> min(data)
Decimal('0.03')
>>> sorted(data)
[Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'),
 Decimal('2.35'), Decimal('3.45'), Decimal('9.25')]
>>> sum(data)
Decimal('19.29')
>>> a,b,c = data[:3]
>>> str(a)
'1.34'
>>> float(a)
1.34
>>> round(a, 1)
Decimal('1.3')
>>> int(a)
1
>>> a * 5
Decimal('6.70')
>>> a * b
Decimal('2.5058')
>>> c % a
Decimal('0.77')

Decimal은 format() 내장 함수나 f-문자열로 내장 float 타입과 같은 형식 문법을 사용해 고정소수점·지수 표기로 포맷할 수 있어요.

>>> format(Decimal('2.675'), "f")
'2.675'
>>> format(Decimal('2.675'), ".2f")
'2.68'
>>> f"{Decimal('2.675'):.2f}"
'2.68'
>>> format(Decimal('2.675'), ".2e")
'2.68e+0'
>>> with localcontext() as ctx:
...     ctx.rounding = ROUND_DOWN
...     print(format(Decimal('2.675'), ".2f"))
...
2.67

Decimal에는 수학 함수도 쓸 수 있어요.

>>> getcontext().prec = 28
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801688724')
>>> Decimal(1).exp()
Decimal('2.718281828459045235360287471')
>>> Decimal('10').ln()
Decimal('2.302585092994045684017991455')
>>> Decimal('10').log10()
Decimal('1')

quantize() 메서드는 숫자를 고정 지수로 반올림해요. 결과를 고정된 자릿수로 반올림하는 통화 애플리케이션에 유용해요.

>>> Decimal('7.325').quantize(Decimal('.01'), rounding=ROUND_DOWN)
Decimal('7.32')
>>> Decimal('7.325').quantize(Decimal('1.'), rounding=ROUND_UP)
Decimal('8')

위에서 보듯 getcontext() 함수는 현재 컨텍스트에 접근해 설정을 바꾸게 해 줘요. 이 접근 방식은 대부분의 애플리케이션 요구를 충족해요. 더 고급 작업에서는 Context() 생성자로 대체 컨텍스트를 만들고, setcontext() 함수로 활성화할 수 있어요.

표준에 따라 decimal 모듈은 BasicContextExtendedContext라는 두 가지 바로 쓸 수 있는 표준 컨텍스트를 제공해요. 전자는 트랩이 많이 활성화되어 있어 디버깅에 특히 유용해요.

>>> myothercontext = Context(prec=60, rounding=ROUND_HALF_DOWN)
>>> setcontext(myothercontext)
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857142857142857142857142857')

>>> ExtendedContext
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[], traps=[])
>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(7)
Decimal('0.142857143')
>>> Decimal(42) / Decimal(0)
Decimal('Infinity')

>>> setcontext(BasicContext)
>>> Decimal(42) / Decimal(0)
Traceback (most recent call last):
  File "<pyshell#143>", line 1, in -toplevel-
    Decimal(42) / Decimal(0)
DivisionByZero: x / 0

컨텍스트는 계산 중 만난 비정상 조건을 모니터링하는 시그널 플래그도 가져요. 플래그는 명시적으로 지울 때까지 유지되므로, 모니터링하는 계산 묶음 전마다 clear_flags() 메서드로 플래그를 지우는 게 좋아요.

>>> setcontext(ExtendedContext)
>>> getcontext().clear_flags()
>>> Decimal(355) / Decimal(113)
Decimal('3.14159292')
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[])

플래그 항목은 파이(pi)의 유리수 근사가 반올림됐고(컨텍스트 정밀도를 넘는 자릿수가 버려짐) 결과가 정확하지 않다(버려진 일부 자릿수가 0이 아님)는 걸 보여줘요. 개별 트랩은 컨텍스트의 traps 속성 딕셔너리로 설정해요.

>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(0)
Decimal('Infinity')
>>> getcontext().traps[DivisionByZero] = 1
>>> Decimal(1) / Decimal(0)
Traceback (most recent call last):
  File "<pyshell#112>", line 1, in -toplevel-
    Decimal(1) / Decimal(0)
DivisionByZero: x / 0

대부분의 프로그램은 프로그램 시작 때 한 번만 현재 컨텍스트를 조정해요. 그리고 많은 애플리케이션에서 데이터는 루프 안에서 단일 캐스트로 Decimal로 변환돼요. 컨텍스트가 설정되고 decimal이 만들어지면, 프로그램의 나머지 부분은 다른 Python 숫자 타입과 다르지 않게 데이터를 다뤄요.

Decimal 객체

class decimal.Decimal(value='0', context=None)

value로부터 새 Decimal 객체를 만들어요. value는 정수, 문자열, 튜플, float, 다른 Decimal 객체가 될 수 있어요. 값이 없으면 Decimal('0')을 반환해요. value가 문자열이면 앞뒤 공백과 밑줄을 제거한 후 다음 10진 숫자 문자열 문법을 따라야 해요.

sign           ::=  '+' | '-'
digit          ::=  '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
indicator      ::=  'e' | 'E'
digits         ::=  digit [digit]...
decimal-part   ::=  digits '.' [digits] | ['.'] digits
exponent-part  ::=  indicator [sign] digits
infinity       ::=  'Infinity' | 'Inf'
nan            ::=  'NaN' [digits] | 'sNaN' [digits]
numeric-value  ::=  decimal-part [exponent-part] | infinity
numeric-string ::=  [sign] numeric-value | [sign] nan

위에서 digit가 나오는 곳에 다른 유니코드 10진 자릿수도 허용돼요. 여기엔 다양한 문자 체계(예: 아랍-인도, 데바나가리 자릿수)와 전각 자릿수 '\uff10'부터 '\uff19'가 포함돼요. 대소문자는 중요하지 않아서 inf, Inf, INFINITY, iNfINity 모두 양의 무한대의 허용 표기예요.

value가 튜플이면 세 구성 요소 — 부호(양수 0, 음수 1), 자릿수 튜플, 정수 지수 — 를 가져야 해요. 예를 들어 Decimal((0, (1, 4, 1, 4), -3))Decimal('1.414')를 반환해요.

valuefloat이면 이진 부동소수점 값이 손실 없이 정확한 10진 등가로 변환돼요. 이 변환은 종종 53자리 이상의 정밀도를 요구할 수 있어요. 예를 들어 Decimal(float('1.1'))Decimal('1.100000000000000088817841970012523233890533447265625')로 변환돼요.

컨텍스트 정밀도는 저장되는 자릿수에 영향을 주지 않아요. 그것은 오로지 value의 자릿수에 의해서만 결정돼요. 예를 들어 컨텍스트 정밀도가 3이라도 Decimal('3.00000')은 다섯 개의 0을 모두 기록해요.

context 인자의 목적은 value가 잘못된 문자열일 때 무엇을 할지 결정하는 거예요. 컨텍스트가 InvalidOperation을 트랩하면 예외가 발생하고, 그렇지 않으면 생성자는 값이 NaN인 새 Decimal을 반환해요.

일단 만들어진 Decimal 객체는 불변(immutable)이에요.

버전 3.2에서 변경: 생성자 인자에 float 인스턴스가 허용됨. 버전 3.3에서 변경: FloatOperation 트랩이 설정되면 float 인자가 예외를 발생시킴(기본은 꺼짐). 버전 3.6에서 변경: 코드의 정수·부동소수점 리터럴처럼 그룹화용 밑줄이 허용됨.

Decimal 부동소수점 객체는 float, int 같은 다른 내장 숫자 타입과 많은 속성을 공유해요. 모든 일반적인 수학 연산과 특수 메서드가 적용돼요. 또한 decimal 객체는 복사, 피클, 출력, 딕셔너리 키로 쓰기, 집합 요소로 쓰기, 비교, 정렬, 다른 타입(float, int)으로 강제 변환이 가능해요.

Decimal 객체와 정수·부동소수점 간 산술에는 몇 가지 작은 차이가 있어요. % 연산자를 Decimal 객체에 적용하면 결과의 부호는 제수(divisor)가 아니라 피제수(dividend)의 부호예요.

>>> (-7) % 4
1
>>> Decimal(-7) % Decimal(4)
Decimal('-3')

정수 나눗셈 연산자 //도 유사하게 동작해서, 바닥(floor)이 아니라 참 몫의 정수 부분(0 쪽으로 절사)을 반환해 x == (x // y) * y + x % y라는 관례적인 항등식을 유지해요.

>>> -7 // 4
-2
>>> Decimal(-7) // Decimal(4)
Decimal('-1')

%// 연산자는 명세에 설명된 나머지·정수 나눗셈 연산을 각각 구현해요.

Decimal 객체는 일반적으로 산술 연산에서 floatfractions.Fraction 인스턴스와 결합할 수 없어요. 예를 들어 Decimal에 float를 더하려 하면 TypeError가 발생해요. 하지만 Python의 비교 연산자로 Decimal 인스턴스 x를 다른 숫자 y와 비교할 수는 있어요. 이렇게 하면 다른 타입의 숫자 사이의 동등성 비교에서 혼란스러운 결과를 피할 수 있어요.

버전 3.2에서 변경: Decimal 인스턴스와 다른 숫자 타입 사이의 혼합 타입 비교가 완전히 지원됨.

표준 숫자 속성 외에 decimal 부동소수점 객체는 여러 특화된 메서드도 가져요.

  • adjusted() — 계수의 맨 오른쪽 자릿수를 맨 왼쪽 자릿수만 남을 때까지 빼낸 후 조정된 지수를 반환해요. Decimal('321e+5').adjusted()는 7을 반환해요. 소수점을 기준으로 가장 유효한 자릿수의 위치를 결정하는 데 사용해요.
  • as_integer_ratio() — 주어진 Decimal 인스턴스를 기약 분수(양의 분모)로 나타내는 정수 쌍 (n, d)을 반환해요. 변환은 정확하고, 무한대에서는 OverflowError, NaN에서는 ValueError를 발생시켜요. (3.6)
  • as_tuple()DecimalTuple(sign, digits, exponent) 형식의 명명된 튜플 표현을 반환해요.
  • canonical() — 인자의 정규 인코딩을 반환해요. 현재 Decimal 인스턴스의 인코딩은 항상 정규라서 그대로 반환돼요.
  • compare(other, context=None) — 두 Decimal 인스턴스의 값을 비교해요. 피연산자 중 하나가 NaN이면 결과도 NaN이에요. a or b is a NaN → Decimal('NaN'), a < b → Decimal('-1'), a == b → Decimal('0'), a > b → Decimal('1').
  • compare_signal(other, context=None)compare()와 동일하지만 모든 NaN이 신호를 보내는 점만 달라요.
  • compare_total(other, context=None) — 수치 대신 추상 표현으로 두 피연산자를 비교해요. Decimal 인스턴스에 전체 순서(total ordering)를 부여해요. 값은 같지만 표현이 다른 두 인스턴스는 이 순서에서 다르게 비교돼요. quiet·signaling NaN도 전체 순서에 포함돼요. 컨텍스트의 영향을 받지 않고 조용해요(플래그 변경·반올림 없음).
  • compare_total_mag(other, context=None)compare_total()과 같지만 두 피연산자의 부호를 무시해요. x.compare_total_mag(y)x.copy_abs().compare_total(y.copy_abs())와 같아요.
  • conjugate() — 자기 자신을 반환해요. Decimal 명세를 따르기 위한 메서드예요.
  • copy_abs() — 인자의 절댓값을 반환해요. 컨텍스트 영향 없음·조용.
  • copy_negate() — 인자의 부정을 반환해요. 컨텍스트 영향 없음·조용.
  • copy_sign(other, context=None) — 첫 피연산자의 복사본에 두 번째 피연산자의 부호를 적용해요. Decimal('2.3').copy_sign(Decimal('-1.5'))Decimal('-2.3'). 컨텍스트 영향 없음·조용.
  • exp(context=None) — 주어진 숫자에서 자연 지수 함수 e**x의 값을 반환해요. 결과는 ROUND_HALF_EVEN 반올림 모드로 올바르게 반올림돼요.
  • classmethod from_float(f, /)float 또는 int 인스턴스만 받는 대체 생성자예요. Decimal.from_float(0.1)Decimal('0.1')과 같지 않아요(0.1은 이진 부동소수점에서 정확히 표현되지 않으므로 값이 0x1.999999999999ap-4로 저장돼요). (3.1)
  • classmethod from_number(number, /)float, int 또는 Decimal 인스턴스만 받는 대체 생성자예요(문자열·튜플은 안 됨). (3.14)
  • fma(other, third, context=None) — 융합 곱셈-덧셈(fused multiply-add). 중간 곱 self*other를 반올림하지 않고 self*other+third를 반환해요.
  • is_canonical() — 인자가 정규이면 True, 아니면 False. 현재 항상 정규라 항상 True를 반환해요.
  • is_finite() — 유한 숫자면 True, 무한대나 NaN이면 False.
  • is_infinite() — 양·음 무한대면 True, 아니면 False.
  • is_nan() — (quiet 또는 signaling) NaN이면 True, 아니면 False.
  • is_normal(context=None) — 정규 유한 숫자면 True. 0, 비정규(subnormal), 무한대, NaN이면 False.
  • is_qnan() — quiet NaN이면 True, 아니면 False.
  • is_signed() — 음의 부호를 가지면 True, 아니면 False. 0과 NaN은 부호를 가질 수 있어요.
  • is_snan() — signaling NaN이면 True, 아니면 False.
  • is_subnormal(context=None) — 비정규면 True, 아니면 False.
  • is_zero() — (양·음) 0이면 True, 아니면 False.
  • ln(context=None) — 피연산자의 자연(밑 e) 로그를 반환해요. ROUND_HALF_EVEN 모드로 올바르게 반올림돼요.
  • log10(context=None) — 밑 10 로그를 반환해요. ROUND_HALF_EVEN 모드로 올바르게 반올림돼요.
  • logb(context=None) — 0이 아닌 숫자의 조정된 지수를 Decimal 인스턴스로 반환해요. 0이면 Decimal('-Infinity')DivisionByZero 플래그, 무한대면 Decimal('Infinity').
  • logical_and(other, context=None) — 두 논리 피연산자의 자릿 단위 and 연산 결과.
  • logical_invert(context=None) — 피연산자의 자릿 단위 반전.
  • logical_or(other, context=None) — 자릿 단위 or 연산.
  • logical_xor(other, context=None) — 자릿 단위 배타적 or(xor) 연산.
  • max(other, context=None) / max_mag(...)max(self, other)와 같지만 반환 전 컨텍스트 반올림 규칙이 적용되고 NaN이 신호되거나 무시돼요. max_mag는 절댓값으로 비교해요.
  • min(other, context=None) / min_mag(...)max와 대칭적으로 최솟값을 반환해요.
  • next_minus(context=None) — 주어진(또는 현재 스레드의) 컨텍스트에서 나타낼 수 있는, 주어진 피연산자보다 작은 가장 큰 수를 반환해요.
  • next_plus(context=None) — 주어진 피연산자보다 큰 가장 작은 수를 반환해요.
  • next_toward(other, context=None) — 두 피연산자가 같지 않으면 첫 번째 피연산자에서 두 번째 방향으로 가장 가까운 수를 반환해요. 수치로 같으면 두 번째 피연산자의 부호를 첫 번째에 적용한 복사본을 반환해요.
  • normalize(context=None) — 현재 또는 지정된 컨텍스트 안에서 동치류의 정규 값을 만드는 데 사용해요. 단항 플러스 연산과 같은 의미지만, 최종 결과가 유한하면 가장 단순한 형태로 줄여 끝의 0을 제거하고 부호를 보존해요. 예를 들어 Decimal('32.100')Decimal('0.321000e+2') 모두 Decimal('32.1')로 정규화돼요. 최신 명세에서는 reduce로도 알려져요.
  • number_class(context=None) — 피연산자의 클래스를 설명하는 문자열을 반환해요. 다음 10가지 문자열 중 하나예요: "-Infinity", "-Normal", "-Subnormal", "-Zero", "+Zero", "+Subnormal", "+Normal", "+Infinity", "NaN", "sNaN".
  • quantize(exp, rounding=None, context=None) — 반올림 후 첫 피연산자와 값이 같고 두 번째 피연산자의 지수를 가지는 값을 반환해요. Decimal('1.41421356').quantize(Decimal('1.000'))Decimal('1.414'). 다른 연산과 달리 quantize 후 계수 길이가 정밀도를 초과하면 InvalidOperation이 신호돼요. 다른 연산과 달리 결과가 비정규·부정확해도 Underflow를 신호하지 않아요. 두 번째 피연산자의 지수가 첫 번째보다 크면 반올림이 필요할 수 있어요.
  • radix()Decimal(10)을 반환해요. 명세 호환용이에요.
  • remainder_near(other, context=None)selfother로 나눈 나머지를 반환해요. self % other와 달리 나머지의 부호가 절댓값을 최소화하도록 선택돼요. 정확히 말하면 self - n * other인데, nself / other의 정확한 값에 가장 가까운 정수예요.
  • rotate(other, context=None) — 두 번째 피연산자가 지정한 양만큼 첫 피연산자의 자릿수를 회전한 결과를 반환해요. 두 번째 피연산자는 -precision~precision 범위의 정수여야 해요. 양수면 왼쪽, 음수면 오른쪽 회전이에요.
  • same_quantum(other, context=None)selfother가 같은 지수를 가지는지 또는 둘 다 NaN인지 테스트해요. 컨텍스트 영향 없음·조용.
  • scaleb(other, context=None) — 지수를 두 번째 인자만큼 조정한 첫 피연산자를 반환해요. 즉 10**other를 곱한 것과 같아요. 두 번째 인자는 정수여야 해요.
  • shift(other, context=None) — 두 번째 피연산자가 지정한 양만큼 첫 피연산자의 자릿수를 이동한 결과를 반환해요. 계수로 이동된 자릿수는 0이에요.
  • sqrt(context=None) — 인자의 제곱근을 전체 정밀도로 반환해요.
  • to_eng_string(context=None) — 지수가 필요하면 공학 표기로 문자열로 변환해요. 공학 표기는 지수가 3의 배수예요. Decimal('123E+1')Decimal('1.23E+3')으로 변환해요.
  • to_integral(rounding=None, context=None)to_integral_value()와 동일해요. 이전 버전 호환용 예전 이름이에요.
  • to_integral_exact(rounding=None, context=None) — 반올림이 발생하면 적절히 Inexact 또는 Rounded를 신호하며 가장 가까운 정수로 반올림해요.
  • to_integral_value(rounding=None, context=None)Inexact·Rounded를 신호하지 않고 가장 가까운 정수로 반올림해요.

round()를 사용한 반올림 — Decimal 숫자는 round() 함수로 반올림할 수 있어요.

  • round(number)ndigits가 없거나 None이면 nearest int를 반환하고, 짝수로 동점을 반올림하며(round ties to even), Decimal 컨텍스트의 반올림 모드를 무시해요. 무한대면 OverflowError, (quiet·signaling) NaN이면 ValueError를 발생시켜요.
  • round(number, ndigits)ndigitsint이면 컨텍스트의 반올림 모드를 존중하고 numberDecimal('1E-ndigits')의 가장 가까운 배수로 반올림한 Decimal을 반환해요. self.quantize(Decimal('1E-ndigits'))와 같아요. quiet NaN이면 Decimal('NaN')을 반환하고, 무한대·signaling NaN·quantize 후 계수 길이가 현재 컨텍스트 정밀도를 초과하면 InvalidOperation을 발생시켜요. 즉 비경계 경우에 ndigits가 양수면 ndigits 소수 자리로, 0이면 가장 가까운 정수로, 음수면 10**abs(ndigits)의 가장 가까운 배수로 반올림해요.
>>> from decimal import Decimal, getcontext, ROUND_DOWN
>>> getcontext().rounding = ROUND_DOWN
>>> round(Decimal('3.75'))     # context rounding ignored
4
>>> round(Decimal('3.5'))      # round-ties-to-even
4
>>> round(Decimal('3.75'), 0)  # uses the context rounding
Decimal('3')
>>> round(Decimal('3.75'), 1)
Decimal('3.7')
>>> round(Decimal('3.75'), -1)
Decimal('0E+1')

논리 피연산자 (Logical operands)logical_and(), logical_invert(), logical_or(), logical_xor() 메서드는 인자가 논리 피연산자이길 기대해요. 논리 피연산자는 지수와 부호가 모두 0이고 자릿수가 모두 0 또는 1인 Decimal 인스턴스예요.

컨텍스트 객체

컨텍스트는 산술 연산을 위한 환경이에요. 정밀도를 관리하고, 반올림 규칙을 설정하고, 어떤 시그널을 예외로 취급할지 결정하고, 지수 범위를 제한해요.

각 스레드는 getcontext()setcontext() 함수로 접근·변경되는 자신만의 현재 컨텍스트를 가져요.

  • decimal.getcontext() — 활성 스레드의 현재 컨텍스트를 반환해요.
  • decimal.setcontext(c, /) — 활성 스레드의 현재 컨텍스트를 c로 설정해요.
  • decimal.localcontext(ctx=None, **kwargs) — with-문 진입 시 활성 스레드의 현재 컨텍스트를 ctx의 복사본으로 설정하고, 종료 시 이전 컨텍스트를 복원하는 컨텍스트 관리자를 반환해요. 컨텍스트가 지정되지 않으면 현재 컨텍스트의 복사본을 사용해요. kwargs 인자는 새 컨텍스트의 속성을 설정하는 데 사용돼요. (3.11에서 키워드 인자 지원)
from decimal import localcontext

with localcontext() as ctx:
    ctx.prec = 42   # Perform a high precision calculation
    s = calculate_something()
s = +s  # Round the final result back to the default precision

키워드 인자를 쓰면 이렇게 돼요.

from decimal import localcontext

with localcontext(prec=42) as ctx:
    s = calculate_something()
s = +s
  • decimal.IEEEContext(bits) — IEEE 상호 교환 형식 중 하나에 맞게 초기화된 컨텍스트 객체를 반환해요. 인자는 32의 배수이고 IEEE_CONTEXT_MAX_BITS보다 작아야 해요. (3.14)

새 컨텍스트는 아래 설명된 Context 생성자로도 만들 수 있어요. 게다가 모듈은 세 가지 미리 만들어진 컨텍스트를 제공해요.

  • decimal.BasicContext — General Decimal Arithmetic Specification이 정의한 표준 컨텍스트예요. 정밀도는 9, 반올림은 ROUND_HALF_UP. 모든 플래그가 지워지고 Inexact·Rounded·Subnormal을 제외한 모든 트랩이 활성화(예외로 취급)돼요. 트랩이 많이 활성화되어 디버깅에 유용해요.
  • decimal.ExtendedContext — 정밀도 9, 반올림 ROUND_HALF_EVEN. 모든 플래그가 지워지고 트랩이 없어요(계산 중 예외가 발생하지 않음). 트랩이 비활성이라, 예외를 내는 대신 NaN·Infinity 결과를 선호하는 애플리케이션에 유용해요.
  • decimal.DefaultContextContext 생성자가 새 컨텍스트의 프로토타입으로 사용하는 컨텍스트예요. 필드(예: precision)를 바꾸면 앞으로 생성되는 새 컨텍스트의 기본값이 바뀌어요. 기본값은 Context.prec=28, Context.rounding=ROUND_HALF_EVEN, Overflow·InvalidOperation·DivisionByZero에 대한 활성 트랩이에요. 멀티스레드 환경에서 스레드 시작 전에 필드를 바꾸면 전역 기본값을 설정하는 효과가 있어요.

class decimal.Context(prec=None, rounding=None, Emin=None, Emax=None, capitals=None, clamp=None, flags=None, traps=None) — 새 컨텍스트를 만들어요. 필드가 지정되지 않거나 None이면 DefaultContext에서 기본값을 복사해요. flags 필드가 지정되지 않거나 None이면 모든 플래그가 지워져요.

  • prec[1, MAX_PREC] 범위의 정수. 컨텍스트의 산술 연산 정밀도를 설정해요.
  • rounding — "반올림 모드" 섹션에 나열된 상수 중 하나.
  • traps / flags — 설정할 시그널 리스트. 일반적으로 새 컨텍스트는 트랩만 설정하고 플래그는 비워 두는 게 좋아요.
  • Emin / Emax — 지수에 허용되는 외부 한계를 지정하는 정수. Emin[MIN_EMIN, 0], Emax[0, MAX_EMAX] 범위여야 해요.
  • capitals — 0 또는 1(기본). 1이면 지수를 대문자 E로, 아니면 소문자 e로 출력해요: Decimal('6.02e+23').
  • clamp — 0(기본) 또는 1. 1이면 이 컨텍스트에서 나타낼 수 있는 Decimal의 지수 eEmin - prec + 1 <= e <= Emax - prec + 1 범위로 엄격히 제한돼요. clamp가 0이면 조정된 지수만 Emax 이하라는 약한 조건이 성립해요. clamp=1은 크고 정상적인 숫자가 가능하면 지수를 줄이고 계수에 그에 해당하는 0을 더해 지수 제약에 맞추게 해요 (값은 보존하지만 유효한 끝의 0 정보는 잃어요). 예: Context(prec=6, Emax=999, clamp=1).create_decimal('1.23e999')Decimal('1.23000E+999'). clamp=1은 IEEE 754에 지정된 고정폭 십진 교환 형식과의 호환을 가능하게 해요.

Context 클래스는 여러 범용 메서드와 주어진 컨텍스트에서 직접 산술하는 많은 메서드를 정의해요. 게다가 위에 설명한 Decimal 메서드 각각(adjusted()as_tuple() 제외)에 해당하는 Context 메서드가 있어요. 예를 들어 Context 인스턴스 CDecimal 인스턴스 x에 대해 C.exp(x)x.exp(context=C)와 같아요. 각 Context 메서드는 Decimal 인스턴스가 받아들여지는 자리에 Python 정수(int 인스턴스)도 받아들여요.

컨텍스트 메서드 요약:

  • clear_flags() — 모든 플래그를 0으로 재설정.
  • clear_traps() — 모든 트랩을 0으로 재설정. (3.3)
  • copy() — 컨텍스트의 사본 반환.
  • copy_decimal(num, /)Decimal 인스턴스 num의 사본 반환.
  • create_decimal(num='0', /)num에서 새 Decimal 인스턴스를 만들되 self를 컨텍스트로 사용. Decimal 생성자와 달리 컨텍스트 정밀도·반올림 메서드·플래그·트랩이 변환에 적용돼요. 상수는 종종 애플리케이션에 필요한 것보다 높은 정밀도로 주어지므로 유용해요. IBM 명세의 to-number 연산을 구현해요. 인자가 문자열이면 앞뒤 공백이나 밑줄은 허용되지 않아요.
  • create_decimal_from_float(f, /)float f에서 새 Decimal 인스턴스를 만들되 self를 컨텍스트로 반올림. Decimal.from_float() 클래스 메서드와 달리 컨텍스트 정밀도·반올림·플래그·트랩이 변환에 적용돼요. (3.1)
  • Etiny()Emin - prec + 1과 같은 값을 반환. 서브노멀 결과의 최소 지수값이에요. 언더플로 시 지수가 Etiny로 설정돼요.
  • Etop()Emax - prec + 1과 같은 값을 반환.
  • abs(x, /)x의 절댓값 반환.
  • add(x, y, /)xy의 합 반환.
  • canonical(x, /) — 같은 Decimal 객체 x 반환.
  • compare(x, y, /)xy를 수치로 비교.
  • compare_signal(x, y, /) — 두 피연산자의 값을 수치로 비교.
  • compare_total(x, y, /) / compare_total_mag(x, y, /) — 추상 표현으로 비교(마그는 부호 무시).
  • copy_abs(x, /) / copy_negate(x, /) — 부호를 0으로/반전시킨 사본 반환.
  • copy_sign(x, y, /)y의 부호를 x로 복사.
  • divide(x, y, /)xy로 나눈 값 반환.
  • divide_int(x, y, /)xy로 나눈 값을 정수로 절사해 반환.
  • divmod(x, y, /) — 두 숫자를 나누고 결과의 정수 부분을 반환.
  • exp(x, /)e ** x 반환.
  • fma(x, y, z, /)x 곱하기 y 더하기 z 반환.
  • is_canonical(x, /) / is_finite(x, /) / is_infinite(x, /) / is_nan(x, /) / is_normal(x, /) / is_qnan(x, /) / is_signed(x, /) / is_snan(x, /) / is_subnormal(x, /) / is_zero(x, /) — 각각 판정하는 불리언 메서드.
  • ln(x, /)x의 자연 로그 반환.
  • log10(x, /)x의 밑 10 로그 반환.
  • logb(x, /) — 피연산자 MSD 크기의 지수 반환.
  • logical_and(x, y, /) / logical_invert(x, /) / logical_or(x, y, /) / logical_xor(x, y, /) — 자릿 단위 논리 연산.
  • max(x, y, /) / max_mag(x, y, /) / min(x, y, /) / min_mag(x, y, /) — 수치 비교로 최대/최소 반환(마그는 부호 무시).
  • minus(x, /) — Python의 단항 접두 마이너스 연산자에 해당.
  • multiply(x, y, /)xy의 곱 반환.
  • next_minus(x, /) / next_plus(x, /)x보다 작은 가장 큰 / 큰 가장 작은 나타낼 수 있는 수 반환.
  • next_toward(x, y, /)y 방향으로 x에 가장 가까운 수 반환.
  • normalize(x, /)x를 가장 단순한 형태로 축약.
  • number_class(x, /)x의 클래스 표시 반환.
  • plus(x, /) — Python의 단항 접두 플러스 연산자에 해당. 컨텍스트 정밀도와 반올림을 적용하므로 항등 연산이 아니에요.
  • power(x, y, modulo=None)modulo가 주어지면 그것으로 나눈 나머지로 xy 거듭제곱 반환. 두 인자면 x**y를 계산해요. x가 음수면 y는 정수여야 해요. 결과는 y가 정수이고 유한하며 'precision' 자릿수로 정확히 표현되는 경우가 아니면 부정확해요. Decimal(0) ** Decimal(0)InvalidOperation을 만드는데, 트랩 안 되면 Decimal('NaN')이 돼요. (3.3에서 C 모듈이 exp·ln으로 계산). 세 인자면 (x**y) % modulo를 계산해요. x·y·modulo 모두 정수, y는 음이 아니어야 하며, x·y 중 하나는 0이 아니어야 하고, modulo는 0이 아니며 'precision' 자릿수 이하여야 해요. 결과는 항상 정확해요.
  • quantize(x, y, /) — 반올림된 x와 같고 y의 지수를 가지는 값 반환.
  • radix() — 그냥 10을 반환해요(Decimal이니까요, :) ).
  • remainder(x, y, /) — 정수 나눗셈의 나머지 반환. 결과의 부호는 0이 아니면 원래 피제수의 부호와 같아요.
  • remainder_near(x, y, /)x - y * n 반환, nx / y의 정확한 값에 가장 가까운 정수(결과가 0이면 부호는 x의 부호).
  • rotate(x, y, /)xy번 회전한 사본 반환.
  • same_quantum(x, y, /) — 두 피연산자가 같은 지수를 가지면 True 반환.
  • scaleb(x, y, /) — 첫 피연산자의 지수에 두 번째 값을 더한 결과 반환.
  • shift(x, y, /)xy번 이동한 사본 반환.
  • sqrt(x, /) — 음이 아닌 숫자의 제곱근을 컨텍스트 정밀도로 계산.
  • subtract(x, y, /)xy의 차 반환.
  • to_eng_string(x, /) — 지수가 필요하면 공학 표기로 문자열 변환.
  • to_integral_exact(x, /) — 정수로 반올림.
  • to_sci_string(x, /) — 숫자를 과학 표기 문자열로 변환.

상수

이 섹션의 상수는 C 모듈에만 관련돼요. 순수 Python 버전에도 호환용으로 포함돼요.

상수 32-bit 64-bit
decimal.MAX_PREC 425000000 999999999999999999
decimal.MAX_EMAX 425000000 999999999999999999
decimal.MIN_EMIN -425000000 -999999999999999999
decimal.MIN_ETINY -849999999 -1999999999999999997
decimal.IEEE_CONTEXT_MAX_BITS 256 512
  • decimal.HAVE_THREADS — 값은 True. Python이 이제 항상 스레드를 가지므로 폐기됨. (3.9부터 폐기)
  • decimal.HAVE_CONTEXTVAR — 기본값은 True. Python이 --without-decimal-contextvar 옵션으로 구성되면 C 버전이 코루틴-로컬 대신 스레드-로컬 컨텍스트를 사용하고 값이 False예요. (3.8.3)

반올림 모드

  • decimal.ROUND_CEILING — 무한대 쪽으로 반올림.
  • decimal.ROUND_DOWN — 0 쪽으로 반올림.
  • decimal.ROUND_FLOOR-Infinity 쪽으로 반올림.
  • decimal.ROUND_HALF_DOWN — 가장 가까운 값으로, 동점은 0 쪽.
  • decimal.ROUND_HALF_EVEN — 가장 가까운 값으로, 동점은 가장 가까운 짝수.
  • decimal.ROUND_HALF_UP — 가장 가까운 값으로, 동점은 0에서 멀어지게.
  • decimal.ROUND_UP — 0에서 멀어지게 반올림.
  • decimal.ROUND_05UP — 0 쪽으로 반올림 후 마지막 자릿수가 0 또는 5였으면 0에서 멀어지게, 아니면 0 쪽으로 반올림.

시그널

시그널은 계산 중 발생하는 조건을 나타내요. 각각 하나의 컨텍스트 플래그와 하나의 컨텍스트 트랩 활성화에 대응돼요. 플래그는 조건을 만날 때마다 설정되고, 컨텍스트의 트랩 활성화가 설정되어 있으면 그 조건이 Python 예외를 발생시켜요.

  • class decimal.Clamped — 표현 제약에 맞추기 위해 지수를 변경함. 대개 지수가 컨텍스트의 Emin·Emax 한계를 벗어날 때 발생해요.
  • class decimal.DecimalException — 다른 시그널의 기본 클래스이자 ArithmeticError의 서브클래스.
  • class decimal.DivisionByZero — 0이 아닌 숫자의 0 나눗셈 신호. 나눗셈·모듈로 나눗셈·음의 거듭제곱으로 숫자를 올릴 때 발생할 수 있어요. 트랩 안 되면 계산 입력의 부호로 결정된 Infinity/-Infinity를 반환해요.
  • class decimal.Inexact — 반올림이 발생했고 결과가 정확하지 않음을 나타냄. 반올림 중 0이 아닌 자릿수가 버려졌을 때 신호돼요.
  • class decimal.InvalidOperation — 의미 없는 연산이 수행됨. 트랩 안 되면 NaN을 반환해요. 가능한 원인에는 Infinity - Infinity, 0 * Infinity, Infinity / Infinity, x % 0, Infinity % x, sqrt(-x) and x > 0, 0 ** 0, x ** (non-integer), x ** Infinity가 있어요.
  • class decimal.Overflow — 수치 오버플로. 반올림 후 지수가 Context.Emax보다 큼을 나타냄. 트랩 안 되면 반올림 모드에 따라 가장 큰 유한 수로 안쪽으로 당기거나 Infinity로 바깥쪽으로 반올림해요. 어느 쪽이든 InexactRounded도 신호돼요.
  • class decimal.Rounded — 정보 손실이 없을 수도 있지만 반올림이 발생함. 반올림이 자릿수를 버릴 때마다 신호돼요(그 자릿수가 0이어도 — 예: 5.00을 5.0으로 반올림).
  • class decimal.Subnormal — 반올림 전 지수가 Emin보다 낮음. 연산 결과가 서브노멀(지수가 너무 작음)일 때 발생해요.
  • class decimal.Underflow — 결과가 0으로 반올림된 수치 언더플로. 서브노멀 결과가 반올림으로 0으로 밀릴 때 발생하고 Inexact·Subnormal도 신호돼요.
  • class decimal.FloatOperation — float과 decimal을 섞는 것에 더 엄격한 의미론 활성화. 기본(트랩 안 됨)에서는 Decimal 생성자·create_decimal()·모든 비교 연산자에서 섞는 게 허용되고, 어느 혼합 연산도 FloatOperation을 플래그로 조용히 기록해요. from_float()·create_decimal_from_float()로의 명시적 변환은 플래그를 설정하지 않아요. 트랩되면 동등성 비교와 명시적 변환만 조용하고, 그 외 모든 혼합 연산이 FloatOperation을 발생시켜요.

시그널의 계층 구조 요약:

exceptions.ArithmeticError(exceptions.Exception)
    DecimalException
        Clamped
        DivisionByZero(DecimalException, exceptions.ZeroDivisionError)
        Inexact
            Overflow(Inexact, Rounded)
            Underflow(Inexact, Rounded, Subnormal)
        InvalidOperation
        Rounded
        Subnormal
        FloatOperation(DecimalException, exceptions.TypeError)

부동소수점 참고 사항

정밀도 증가로 반올림 오차 완화 — 10진 부동소수점을 쓰면 10진 표현 오차가 없어지지만(0.1을 정확히 나타낼 수 있게 됨), 0이 아닌 자릿수가 고정 정밀도를 초과하면 일부 연산은 여전히 반올림 오차를 겪을 수 있어요. 반올림 오차의 영향은 거의 상쇄되는 양의 덧셈·뺄셈으로 증폭되어 유효 자릿수 손실을 일으킬 수 있어요. Knuth는 불충분한 정밀도의 반올림 부동소수점 산술이 덧셈의 결합·분배 법칙을 깨뜨리는 두 가지 예를 제시해요.

# Examples from Seminumerical Algorithms, Section 4.2.2.
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 8

>>> u, v, w = Decimal(11111113), Decimal(-11111111), Decimal('7.51111111')
>>> (u + v) + w
Decimal('9.5111111')
>>> u + (v + w)
Decimal('10')

>>> u, v, w = Decimal(20000), Decimal(-6), Decimal('6.0000003')
>>> (u*v) + (u*w)
Decimal('0.01')
>>> u * (v+w)
Decimal('0.0060000')

decimal 모듈은 유효 자릿수 손실을 피할 만큼 정밀도를 늘려 항등식을 복원할 수 있게 해 줘요.

>>> getcontext().prec = 20
>>> u, v, w = Decimal(11111113), Decimal(-11111111), Decimal('7.51111111')
>>> (u + v) + w
Decimal('9.51111111')
>>> u + (v + w)
Decimal('9.51111111')
>>>
>>> u, v, w = Decimal(20000), Decimal(-6), Decimal('6.0000003')
>>> (u*v) + (u*w)
Decimal('0.0060000')
>>> u * (v+w)
Decimal('0.0060000')

특수 값decimal 모듈의 숫자 체계는 NaN, sNaN, -Infinity, Infinity, 그리고 +0, -0 두 개의 0을 포함한 특수 값을 제공해요. 무한대는 Decimal('Infinity')로 직접 만들거나, DivisionByZero 시그널이 트랩 안 되면 0 나눗셈에서, Overflow 시그널이 트랩 안 되면 반올림에서 생길 수 있어요. 무한대는 부호가 있고(affine), 매우 크고 불확정적인 숫자로 취급되며 산술에 사용할 수 있어요.

0/0처럼 일부 연산은 NaN("숫자가 아님")을 반환하거나, InvalidOperation 시그널이 트랩되면 예외를 발생시켜요. 이 변형 NaN은 조용해서(quiet), 만들어지면 다른 계산에도 흘러 항상 또 다른 NaN이 돼요. 이 동작은 가끔 입력이 없는 일련의 계산에 유용해요 — 특정 결과를 무효로 표시하면서 계산을 계속 진행하게 해 주니까요. sNaN은 각 연산 후 조용히 남는 대신 신호를 보내는 변형이에요. NaN이 관여하면 Python 비교 연산자의 동작이 다소 놀라울 수 있어요. NaN 피연산자가 있는 동등성 테스트는 항상 False(Decimal('NaN')==Decimal('NaN')도)이고, 부등 테스트는 항상 True예요. <, <=, >, >=로 NaN과 비교하려 하면 InvalidOperation 시그널이 발생하고, 트랩 안 되면 False를 반환해요. 엄격한 표준 준수를 위해 compare()·compare_signal() 메서드를 사용하세요.

부호 있는 0은 언더플로되는 계산에서 생길 수 있어요. 값이 0이라 양·음 0 모두 동등하게 취급되고 그 부호는 정보용이에요. 다양한 정밀도의 0 표현이 있는데, 예를 들어 1 / Decimal('Infinity')Decimal('0E-1000026')이라는 0과 같은 값을 반환해요.

스레드로 작업하기

getcontext() 함수는 각 스레드에 대해 다른 Context 객체에 접근해요. 스레드 컨텍스트가 분리되어 있어 스레드가 getcontext().prec=10 같은 변경을 다른 스레드를 방해하지 않고 할 수 있어요. 마찬가지로 setcontext() 함수는 자동으로 대상이 현재 스레드에 할당돼요. getcontext() 전에 setcontext()가 호출되지 않았다면 getcontext()가 현재 스레드용 새 컨텍스트를 자동으로 만드는데, 새 컨텍스트 객체는 decimal.DefaultContext 객체에서 기본값을 가져와요.

sys.flags.thread_inherit_context 플래그가 새 스레드의 컨텍스트에 영향을 줘요. 플래그가 거짓이면 새 스레드는 빈 컨텍스트로 시작해 getcontext()가 호출될 때 새 컨텍스트 객체를 만들어요. 참이면 threading.Thread.start() 호출자의 컨텍스트 사본으로 시작해요. 기본값을 제어해 각 스레드가 애플리케이션 전체에서 같은 값을 사용하게 하려면 DefaultContext 객체를 직접 수정해요. 경쟁 조건을 피하려면 스레드 시작 전에 해야 해요.

# Set applicationwide defaults for all threads about to be launched
DefaultContext.prec = 12
DefaultContext.rounding = ROUND_DOWN
DefaultContext.traps = ExtendedContext.traps.copy()
DefaultContext.traps[InvalidOperation] = 1
setcontext(DefaultContext)

# Afterwards, the threads can be started
t1.start()
t2.start()
t3.start()
 . . .

레시피

유틸리티 함수 역할을 하고 Decimal 클래스로 작업하는 방법을 보여 주는 몇 가지 레시피예요.

def moneyfmt(value, places=2, curr='', sep=',', dp='.',
             pos='', neg='-', trailneg=''):
    """Convert Decimal to a money formatted string.

    places:  required number of places after the decimal point
    curr:    optional currency symbol before the sign (may be blank)
    sep:     optional grouping separator (comma, period, space, or blank)
    dp:      decimal point indicator (comma or period)
             only specify as blank when places is zero
    pos:     optional sign for positive numbers: '+', space or blank
    neg:     optional sign for negative numbers: '-', '(', space or blank
    trailneg:optional trailing minus indicator:  '-', ')', space or blank

    >>> d = Decimal('-1234567.8901')
    >>> moneyfmt(d, curr='$')
    '-$1,234,567.89'
    >>> moneyfmt(d, places=0, sep='.', dp='', neg='', trailneg='-')
    '1.234.568-'
    >>> moneyfmt(d, curr='$', neg='(', trailneg=')')
    '($1,234,567.89)'
    >>> moneyfmt(Decimal(123456789), sep=' ')
    '123 456 789.00'
    >>> moneyfmt(Decimal('-0.02'), neg='<', trailneg='>')
    '<0.02>'

    """
    q = Decimal(10) ** -places      # 2 places --> '0.01'
    sign, digits, exp = value.quantize(q).as_tuple()
    result = []
    digits = list(map(str, digits))
    build, next = result.append, digits.pop
    if sign:
        build(trailneg)
    for i in range(places):
        build(next() if digits else '0')
    if places:
        build(dp)
    if not digits:
        build('0')
    i = 0
    while digits:
        build(next())
        i += 1
        if i == 3 and digits:
            i = 0
            build(sep)
    build(curr)
    build(neg if sign else pos)
    return ''.join(reversed(result))

def pi():
    """Compute Pi to the current precision.

    >>> print(pi())
    3.141592653589793238462643383

    """
    getcontext().prec += 2  # extra digits for intermediate steps
    three = Decimal(3)      # substitute "three=3.0" for regular floats
    lasts, t, s, n, na, d, da = 0, three, 3, 1, 0, 0, 24
    while s != lasts:
        lasts = s
        n, na = n+na, na+8
        d, da = d+da, da+32
        t = (t * n) / d
        s += t
    getcontext().prec -= 2
    return +s               # unary plus applies the new precision

def exp(x):
    """Return e raised to the power of x.  Result type matches input type.

    >>> print(exp(Decimal(1)))
    2.718281828459045235360287471
    >>> print(exp(Decimal(2)))
    7.389056098930650227230427461
    >>> print(exp(2.0))
    7.38905609893
    >>> print(exp(2+0j))
    (7.38905609893+0j)

    """
    getcontext().prec += 2
    i, lasts, s, fact, num = 0, 0, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 1
        fact *= i
        num *= x
        s += num / fact
    getcontext().prec -= 2
    return +s

def cos(x):
    """Return the cosine of x as measured in radians.

    The Taylor series approximation works best for a small value of x.
    For larger values, first compute x = x % (2 * pi).

    >>> print(cos(Decimal('0.5')))
    0.8775825618903727161162815826
    >>> print(cos(0.5))
    0.87758256189
    >>> print(cos(0.5+0j))
    (0.87758256189+0j)

    """
    getcontext().prec += 2
    i, lasts, s, fact, num, sign = 0, 0, 1, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 2
        fact *= i * (i-1)
        num *= x * x
        sign *= -1
        s += num / fact * sign
    getcontext().prec -= 2
    return +s

def sin(x):
    """Return the sine of x as measured in radians.

    The Taylor series approximation works best for a small value of x.
    For larger values, first compute x = x % (2 * pi).

    >>> print(sin(Decimal('0.5')))
    0.4794255386042030002732879352
    >>> print(sin(0.5))
    0.479425538604
    >>> print(sin(0.5+0j))
    (0.479425538604+0j)

    """
    getcontext().prec += 2
    i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
    while s != lasts:
        lasts = s
        i += 2
        fact *= i * (i-1)
        num *= x * x
        sign *= -1
        s += num / fact * sign
    getcontext().prec -= 2
    return +s

Decimal FAQ

Q: decimal.Decimal('1234.5')라고 타자치는 건 번거로워요. 대화형 인터프리터에서 타자를 줄이는 방법이 있나요?

A: 어떤 사용자는 생성자를 한 글자로 줄여요.

>>> D = decimal.Decimal
>>> D('1.23') + D('3.45')
Decimal('4.68')

Q: 소수점 두 자리를 쓰는 고정소수점 애플리케이션에서, 어떤 입력은 자릿수가 많아 반올림이 필요하고 다른 입력은 초과 자릿수가 없어야 해 검증이 필요해요. 어떤 메서드를 써야 하나요?

A: quantize() 메서드는 고정된 소수 자릿수로 반올림해요. Inexact 트랩이 설정되면 검증에도 유용해요.

>>> TWOPLACES = Decimal(10) ** -2       # same as Decimal('0.01')

>>> # Round to two places
>>> Decimal('3.214').quantize(TWOPLACES)
Decimal('3.21')

>>> # Validate that a number does not exceed two places
>>> Decimal('3.21').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Decimal('3.21')

>>> Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Traceback (most recent call last):
   ...
Inexact: None

Q: 유효한 두 자리 입력을 얻은 뒤에는 그 불변식을 애플리케이션 전체에서 어떻게 유지하나요?

A: 덧셈·뺄셈·정수 곱셈 같은 일부 연산은 자동으로 고정소수점을 유지해요. 나눗셈·비정수 곱셈 같은 다른 연산은 소수 자릿수를 바꾸므로 quantize() 단계가 필요해요.

>>> a = Decimal('102.72')           # Initial fixed-point values
>>> b = Decimal('3.17')
>>> a + b                           # Addition preserves fixed-point
Decimal('105.89')
>>> a - b
Decimal('99.55')
>>> a * 42                          # So does integer multiplication
Decimal('4314.24')
>>> (a * b).quantize(TWOPLACES)     # Must quantize non-integer multiplication
Decimal('325.62')
>>> (b / a).quantize(TWOPLACES)     # And quantize division
Decimal('0.03')

고정소수점 애플리케이션을 개발할 때는 quantize() 단계를 처리하는 함수를 정의하는 게 편리해요.

>>> def mul(x, y, fp=TWOPLACES):
...     return (x * y).quantize(fp)
...
>>> def div(x, y, fp=TWOPLACES):
...     return (x / y).quantize(fp)

>>> mul(a, b)                       # Automatically preserve fixed-point
Decimal('325.62')
>>> div(b, a)
Decimal('0.03')

Q: 같은 값을 표현하는 방법이 많아요. 200, 200.000, 2E2, .02E+4는 모두 다양한 정밀도에서 같은 값을 가져요. 그것들을 단일한 인식 가능한 정규 값으로 변환하는 방법이 있나요?

A: normalize() 메서드가 모든 동등 값을 단일 대표로 매핑해요.

>>> values = map(Decimal, '200 200.000 2E2 .02E+4'.split())
>>> [v.normalize() for v in values]
[Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2')]

Q: 계산에서 반올림은 언제 발생하나요?

A: 계산 후에 발생해요. decimal 명세의 철학은 숫자가 정확한 것으로 간주되고 현재 컨텍스트와 독립적으로 만들어진다는 거예요. 계산은 이런 정확한 입력으로 처리된 후 반올림(또는 다른 컨텍스트 연산)이 계산 결과에 적용돼요.

>>> getcontext().prec = 5
>>> pi = Decimal('3.1415926535')   # More than 5 digits
>>> pi                             # All digits are retained
Decimal('3.1415926535')
>>> pi + 0                         # Rounded after an addition
Decimal('3.1416')
>>> pi - Decimal('0.00005')        # Subtract unrounded numbers, then round
Decimal('3.1415')
>>> pi + 0 - Decimal('0.00005').   # Intermediate values are rounded
Decimal('3.1416')

Q: 어떤 decimal 값은 항상 지수 표기로 출력돼요. 지수 없는 표현을 얻는 방법이 있나요?

A: 어떤 값에서는 지수 표기가 계수의 유효 자릿수를 표현하는 유일한 방법이에요. 예를 들어 5.0E+3을 5000으로 표현하면 값은 유지되지만 원래의 두 자리 유효성을 보여 줄 수 없어요. 애플리케이션이 유효성 추적에 신경 쓰지 않는다면 지수와 끝의 0을 제거해 유효성을 잃지만 값을 유지하는 건 쉽죠.

>>> def remove_exponent(d):
...     return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()

>>> remove_exponent(Decimal('5E+3'))
Decimal('5000')

Q: 일반 float를 Decimal로 변환하는 방법이 있나요?

A: 네, 어떤 이진 부동소수점 숫자도 정확히 Decimal로 표현될 수 있어요. 다만 정확한 변환이 직관이 시사하는 것보다 더 많은 정밀도를 요구할 수 있어요.

>>> Decimal(math.pi)
Decimal('3.141592653589793115997963468544185161590576171875')

Q: 복잡한 계산 안에서, 불충분한 정밀도나 반올림 이상으로 잘못된 결과를 얻지 않았는지 어떻게 확인하나요?

A: decimal 모듈은 결과를 테스트하기 쉽게 해 줘요. 좋은 방법은 더 높은 정밀도와 다양한 반올림 모드로 계산을 다시 실행하는 거예요. 결과가 크게 다르면 정밀도 부족, 반올림 모드 문제, 나쁜 조건의 입력, 수치적으로 불안정한 알고리즘을 나타내요.

Q: 컨텍스트 정밀도는 연산 결과에 적용되지만 입력에는 적용되지 않는 걸 알았어요. 다른 정밀도의 값을 섞을 때 주의할 점이 있나요?

A: 네. 원칙은 모든 값이 정확한 것으로 간주되고 그 값에 대한 산술도 정확하다는 거예요. 오직 결과만 반올림돼요. 입력의 장점은 "입력한 그대로 얻는다"는 거예요. 단점은 입력이 반올림되지 않았다는 걸 잊으면 결과가 이상해 보일 수 있다는 거예요.

>>> getcontext().prec = 3
>>> Decimal('3.104') + Decimal('2.104')
Decimal('5.21')
>>> Decimal('3.104') + Decimal('0.000') + Decimal('2.104')
Decimal('5.20')

해결책은 정밀도를 높이거나 단항 플러스 연산으로 입력의 반올림을 강제하는 거예요.

>>> getcontext().prec = 3
>>> +Decimal('1.23456789')      # unary plus triggers rounding
Decimal('1.23')

또는 생성 시 Context.create_decimal() 메서드로 입력을 반올림할 수 있어요.

>>> Context(prec=5, rounding=ROUND_DOWN).create_decimal('1.2345678')
Decimal('1.2345')

Q: CPython 구현은 큰 수에 빠른가요?

A: 네. CPython과 PyPy3 구현에서 decimal 모듈의 C/CFFI 버전은 임의 정밀도 올바른 반올림 10진 부동소수점 산술을 위한 고속 libmpdec 라이브러리를 통합해요. libmpdec는 중간 크기 숫자에는 Karatsuba 곱셈을, 매우 큰 숫자에는 Number Theoretic Transform을 사용해요.

정확한 임의 정밀도 산술에는 컨텍스트를 조정해야 해요. EminEmax는 항상 최대값으로 설정하고, clamp는 항상 0(기본)이어야 해요. prec 설정에는 신중함이 필요해요. 큰 수 산술을 시험하는 가장 쉬운 접근은 prec에도 최대값을 쓰는 거예요.

>>> setcontext(Context(prec=MAX_PREC, Emax=MAX_EMAX, Emin=MIN_EMIN))
>>> x = Decimal(2) ** 256
>>> x / 128
Decimal('904625697166532776746648320380374280103671755200316906558262375061821325312')

부정확한 결과에는 64비트 플랫폼에서 MAX_PREC가 너무 커서 사용 가능한 메모리가 부족해요.

>>> Decimal(1) / 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
MemoryError

과할당(overallocation)이 있는 시스템(예: Linux)에서는 prec를 사용 가능한 RAM 양에 맞추는 더 정교한 접근이 있어요. 일반적으로(특히 과할당 없는 시스템에서) 더 타이트한 경계를 추정하고 모든 계산이 정확할 것으로 예상되면 Inexact 트랩을 설정하는 걸 권장해요.

[1] 버전 3.3에 추가됨. [2] 버전 3.9에서 변경: 이 접근이 이제 정수 거듭제곱이 아닌 경우를 제외한 모든 정확한 결과에 대해 작동함.