최근 값을 계산하는 로직을 개발하면서 기본적이지만 까먹고 있었던 부분에서 애를 먹었습니다. 

파이썬에서는 실수 (유리수 + 무리수)를 부동 소수점으로 표현하기 때문에 소수점이 있는 계산에서 우리가 생각하고 있는 것과 약간 다르게 동작합니다.

예시

아래는 0.1 + 0.2를 연산하는 로직입니다. 해당 연산의 결과는 0.3으로 보통 생각하고 있지만 파이썬의 결과는 다르게 나옵니다.

print(0.1+0.2)


## 0.30000000000000004


컴퓨터에서는 숫자를 비트로 표현하는데 실수의 경우 유리수 + 무리수 이기 때문에 정확한 표현이 어렵습니다. 그래서 제한적인 비트를 사용하여 근삿값을 표현하기 때문에 0.3의 근삿값인 0.30000000000000004이 나오게 되고 0.1 + 0.2 == 0.3 을 하면 같지 않다가 나오게 됩니다.

해결방법

math.isclose()

만약 소수점 단위까지 정확하게 맞도록 비교를 해야 한다면 파이썬 3.5 이상부터 제공하는 함수를 사용하면 됩니다.

import math




math.isclose(0.1 + 0.2, 0.3)


## True

Decimal()

만약 우리가 흔히 알고 있는 연산으로 처리를 하려면 Decimal을 사용하면 됩니다.

Decimal()을 사용해서 연산을 하면 파이썬 내부 특성 때문에 신경을 써야 되는 지점이 생깁니다.

from decimal import Decimal




print(Decimal(0.1) + Decimal(0.2))
print(Decimal('0.1') + Decimal('0.2'))




## Decimal('0.3000000000000000166533453694')
## Decimal('0.3')


위와 같이 실수로 표현하면 우리가 알고 있는 것과는 약간 다르게 나오는 반면, 실수의 타입을 문자화 하면 우리가 알고 있는 것과 동일하게 표현이 됩니다.


Decimal()을 사용할 때, int, float과 같은 실수 타입으로 표현할 때엔 아래처럼 소수점 어디까지 나타내고 round() 처리할 지, 정해야 합니다.

from decimal import Decimal, getcontext




# 소수점 둘 째자리까지 표현
getcontext().prec = 2


print(Decimal(0.1) + Decimal(0.2))
print(Decimal('0.1') + Decimal('0.2'))
print(Decimal(0.16) + Decimal(0.21))
print(Decimal('0.16') + Decimal('0.21'))




## Decimal('0.30')
## Decimal('0.3')
## Decimal('0.37')
## Decimal('0.37')

주의할 점!!!

위와 같이 둘 째자리 까지 명시하고 만약 소수점 아래 자리가 3개인 케이스일 때는 아래와 같이 표현이 됩니다.

from decimal import Decimal, getcontext





# 소수점 둘 째자리까지 표현
getcontext().prec = 2




print(Decimal(0.164) + Decimal(0.211)) # 0.375
print(Decimal(0.154) + Decimal(0.211)) # 0.365
print(Decimal('0.164') + Decimal('0.211')) # 0.375
print(Decimal('0.154') + Decimal('0.211')) # 0.365




## Decimal('0.38')
## Decimal('0.36')
## Decimal('0.38')
## Decimal('0.36')


위와 같이, 반올림 부분에서 차이가 나는 부분은 사사오입의 효과로 인해 발생한 것이고, 파이썬에서는 우리가 알고 있는 통계학적 반올림이 아닌 수학적 반올림을 사용하기 때문입니다. 


이러한 문제가 발생하기 때문에 Decimal의 소수점 지정을 어느정도 여유있게 나눈 다음, 우리가 이해하고 있는 반올림 기능을 하는 함수를 만들어 사용해야 합니다.