부동소수점(floating-point)은 실수를 부호(sign)·지수(exponent)·가수(mantissa, significand)로 나눠 정해진 비트 수에 담는 표현 방식이다. 대부분의 언어가 따르는 IEEE 754(IEEE Standard for Floating-Point Arithmetic) 배정밀도(double precision, 64비트)는 유효숫자가 십진수로 15~17자리 정도라, 그보다 정밀한 값이나 2진수로 끝나지 않는 소수는 근삿값으로 저장된다.
0.1 + 0.2 # 0.30000000000000004
0.1 + 0.2 == 0.3 # False구조 (64비트)
| 부호 | 지수 | 가수 |
|---|---|---|
| 1비트 | 11비트 | 52비트 |
- 값 = (−1)^부호 × 1.가수 × 2^(지수 − 1023)
- 지수에 1023을 더해 저장하는 바이어스(exponent bias) 덕분에 지수를 부호 없는 수로 비교할 수 있다
- 0.1은 2진수로 무한히 반복되는 수라(십진수의 1/3처럼) 52비트에서 잘린다. 언어의 문제가 아니라 표현 방식의 성질이다
비교와 계산
- 동등 비교 대신 허용 오차로 비교한다:
math.isclose(a, b, rel_tol=1e-9) - 큰 수와 작은 수를 더하면 작은 수가 사라질 수 있다. 많은 값을 더할 때는
math.fsum이 오차를 줄인다 - 돈처럼 십진수가 정확해야 하는 값은
decimal.Decimal(문자열로 만든다:Decimal("0.1"))이나, 가장 작은 단위의 정수(원·센트)로 다룬다 - 분수 그대로 계산해야 하면
fractions.Fraction
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3") # True정수는 다르다
파이썬의 int는 메모리가 허용하는 한 크기 제한이 없다(임의 정밀도, arbitrary precision). 자바스크립트는 모든 숫자가 64비트 부동소수점이라 2^53을 넘는 정수가 정확하지 않아 BigInt가 따로 있다. 큰 ID를 JSON으로 주고받을 때 문자열로 보내는 이유다. 진법 변환은 bin()·oct()·hex()로, 긴 숫자는 1_000_000처럼 밑줄로 끊어 쓴다.
비교 코드에 매직 넘버처럼 박힌 허용 오차는 이름 붙인 상수로 뺀다(매직 넘버와 설명 상수). 숫자를 다루는 알고리즘의 비용은 빅오 표기법를 본다.
출처: Python 튜토리얼: Floating-Point Arithmetic · Representation Error