왜 0.1 + 0.2는 0.3이 아닐까?
파이썬을 포함한 대부분의 프로그래밍 언어에서 부동 소수점(float) 숫자는 메모리에 2진법(binary) 분수 형태로 저장됩니다. 그 결과, 부동 소수점 연산은 때때로 직관에 어긋나는 결과를 내놓습니다. 가장 대표적인 예가 바로 다음과 같습니다.
>>> 0.1 + 0.2 0.30000000000000004
이것은 버그가 아니라 2진 부동 소수점 표현 방식의 본질적인 특성이며, 어떤 프로그래밍 언어에서든 마찬가지로 나타납니다. 파이썬은 이러한 문제를 해결하기 위해 빠르고 정확하게 반올림된 부동 소수점 연산을 수행할 수 있는 decimal 모듈을 표준 라이브러리로 제공합니다.
decimal 모듈이란?
decimal 모듈은 부동 소수점 숫자를 사람이 기대하는 대로 정확하게 표현하도록 설계되었습니다. 산술 연산의 결과 역시 직관적인 기대치와 일치하며, 표현 및 연산의 정밀도(자릿수)는 최대 28자리까지 설정할 수 있습니다.
Decimal 객체 생성하기
decimal 모듈은 Decimal 클래스를 정의합니다. 생성자에 정수, 숫자를 담은 문자열 또는 튜플을 전달하여 Decimal 객체를 만들 수 있습니다.
>>> from decimal import Decimal
>>> d1 = Decimal(10)
>>> d1
Decimal('10')
>>> d2 = Decimal('10')
>>> d2
Decimal('10')튜플을 인자로 사용할 때는 세 가지 요소, 즉 부호(양수는 0, 음수는 1), 각 자릿수의 튜플, 지수(exponent)를 담아야 합니다.
>>> d3 = Decimal((1, (1, 2, 3, 4), 2))
>>> d3
Decimal('-1.234E+5')정밀도 설정하기 – getcontext()
특정 정밀도로 부동 소수점을 다루는 더 편리한 방법은 getcontext() 함수로 현재 스레드의 컨텍스트(context) 환경을 가져온 뒤, Decimal 객체의 정밀도(precision)를 지정하는 것입니다.
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 5
>>> d3 = Decimal(10)
>>> d4 = Decimal(3)
>>> d3/d4
Decimal('3.3333')여기서 컨텍스트란 산술 연산이 수행되는 환경으로, 정밀도를 결정하고 반올림 규칙을 정의하며 지수의 범위를 제한하는 역할을 합니다.
- decimal.getcontext() – 현재 활성 스레드의 컨텍스트를 반환합니다.
- decimal.setcontext(c) – 현재 활성 스레드의 컨텍스트를 c로 설정합니다.
반올림 모드 상수
decimal 모듈에는 다음과 같은 반올림 모드 상수가 정의되어 있습니다.
| ROUND_CEILING | 양의 무한대 방향으로 반올림 |
| ROUND_DOWN | 0 방향으로 반올림(버림) |
| ROUND_FLOOR | 음의 무한대 방향으로 반올림 |
| ROUND_HALF_DOWN | 가장 가까운 값으로 반올림하되, 동률일 경우 0 방향으로 반올림 |
| ROUND_HALF_EVEN | 가장 가까운 값으로 반올림하되, 동률일 경우 가장 가까운 짝수로 반올림 |
| ROUND_HALF_UP | 가장 가까운 값으로 반올림하되, 동률일 경우 0에서 먼 방향으로 반올림 |
| ROUND_UP | 0에서 먼 방향으로 반올림(올림) |
| ROUND_05UP | 0 방향으로 반올림했을 때 마지막 자릿수가 0 또는 5가 되면 0에서 먼 방향으로, 그렇지 않으면 0 방향으로 반올림 |
다음 코드는 컨텍스트 객체의 정밀도와 반올림 옵션을 함께 활용하는 예입니다.
>>> from decimal import *
>>> getcontext().prec = 5
>>> getcontext().rounding = ROUND_UP
>>> d1 = Decimal(100)
>>> d2 = Decimal(6)
>>> d1/d2
Decimal('16.667')Decimal 객체의 산술 연산
Decimal 객체에는 일반 float처럼 평소 사용하는 모든 산술 연산을 그대로 적용할 수 있습니다.
>>> a = Decimal('2.4')
>>> b = Decimal('1.2')
>>> a + b
Decimal('3.6')
>>> a - b
Decimal('1.2')
>>> b - a
Decimal('-1.2')
>>> a * b
Decimal('2.88')
>>> a / b
Decimal('2')Decimal 객체와 정수(int)를 함께 연산하는 것은 가능하지만, 일반 float 객체와의 연산은 허용되지 않습니다.
>>> a = Decimal('2.4')
>>> c = 2.1
>>> a + c
Traceback (most recent call last):
File "<pyshell#37>", line 1, in <module>
a+c
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'이 TypeError는 덧셈뿐 아니라 다른 모든 산술 연산에서도 동일하게 발생합니다.
나머지(%) 연산자의 차이점
Decimal 객체에 나머지(%) 연산자를 사용할 때는 일반 숫자 타입과 약간 다르게 동작합니다. 일반 숫자에서는 결과의 부호가 나누는 수(제수)를 따르지만, Decimal에서는 나눠지는 수(피제수)의 부호를 따릅니다.
>>> -7%3
2
>>> 7%-3
-2
>>> Decimal(-7) % Decimal(3)
Decimal('-1')
>>> Decimal(7) % Decimal(-3)
Decimal('1')Decimal.from_float() – float를 Decimal로 변환
Decimal.from_float() 함수는 일반 float를 정확한 2진 표현 그대로 Decimal 객체로 변환합니다. 따라서 from_float(0.1)과 Decimal('0.1')은 서로 다른 값입니다.
>>> d1 = Decimal('0.1')
>>> d2 = Decimal.from_float(0.1)
>>> d1,d2
(Decimal('0.1'), Decimal('0.1000000000000000055511151231257827021181583404541015625'))마무리
이 글에서는 파이썬 표준 라이브러리의 decimal 모듈이 제공하는 핵심 기능을 살펴보았습니다. 금융 계산처럼 정확한 십진수 연산이 필요한 경우 float 대신 Decimal을 사용하면 2진 부동 소수점의 오차 없이 신뢰할 수 있는 결과를 얻을 수 있습니다.