0.1+0.2가 0.3이 아닌 이유를 IEEE 754로 설명하고 수치 오류를 피하는 방법을 배운다.
🎯 이 강의에서 배우는 것
파이썬 REPL에서 0.1 + 0.2를 입력해보세요. 0.3이 아닌 0.30000000000000004가 출력됩니다. 이것은 파이썬의 버그가 아닙니다. 모든 현대 컴퓨터가 공유하는 IEEE 754 부동소수점 표준의 특성입니다. 이 강의에서는 이 현상의 근본 원인을 이해하고, 금융·과학 계산에서 정확한 결과를 얻는 방법을 배웁니다.
🔢 왜 0.1 + 0.2 ≠ 0.3 인가
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False!
# 더 많은 소수점 자리를 확인
print(f"{0.1:.55f}")
# 0.1000000000000000055511151231257827021181583404541015625
# 실제로 저장된 값은 0.1이 아님!
문제의 근원은 이진 소수에 있습니다. 10진수 0.1을 2진수 소수로 변환하면 무한 반복소수가 됩니다:
0.1 (10진수) = 0.00011001100110011... (2진수, 무한 반복)
컴퓨터는 유한한 비트로 이 무한 소수를 저장해야 하므로 적당한 위치에서 끊어버립니다. 이 끊김이 표현 오차(representation error)의 원인입니다. 10진수 1/3 = 0.333...이 종이에 완전히 쓸 수 없는 것과 같은 원리입니다.
📐 IEEE 754 배정밀도(64비트) 표준
파이썬의 float는 IEEE 754 배정밀도(double precision) 형식을 사용합니다. 64비트를 다음과 같이 나눕니다:
| 필드 | 비트 수 | 역할 |
|---|---|---|
| 부호(sign) | 1비트 | 0 = 양수, 1 = 음수 |
| 지수(exponent) | 11비트 | 2의 거듭제곱 (±1022) |
| 가수(mantissa) | 52비트 | 유효숫자 (약 15~17자리) |
import sys
# 부동소수점 시스템 정보
info = sys.float_info
print(f"최대값: {info.max}") # 1.7976931348623157e+308
print(f"최솟값(정상): {info.min}") # 2.2250738585072014e-308
print(f"머신 엡실론: {info.epsilon}") # 2.220446049250313e-16
# 머신 엡실론: 1.0에 더했을 때 1.0과 달라지는 가장 작은 수
# 특수 부동소수점 값
pos_inf = float('inf')
neg_inf = float('-inf')
nan = float('nan')
print(pos_inf) # inf
print(neg_inf + 1) # -inf
print(pos_inf + pos_inf) # inf
print(pos_inf - pos_inf) # nan (정의 불가)
print(nan == nan) # False! (NaN은 자기 자신과도 같지 않음)
print(math.isnan(nan)) # True — NaN 확인하는 올바른 방법
import math
print(math.isinf(pos_inf)) # True
⚠️ 부동소수점 비교의 황금 규칙
부동소수점 값을 ==로 비교하는 것은 거의 항상 잘못된 방법입니다:
import math
# 절대로 하면 안 되는 방법
print(0.1 + 0.2 == 0.3) # False — 의도와 다른 결과
# 올바른 방법 1: math.isclose()
print(math.isclose(0.1 + 0.2, 0.3)) # True
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)) # True
# rel_tol: 상대 허용 오차 (기본 1e-9)
# abs_tol: 절대 허용 오차 (기본 0.0)
# 두 조건 중 하나라도 만족하면 같다고 판단
# |a-b| <= max(rel_tol * max(|a|, |b|), abs_tol)
# 0에 가까운 값 비교 시 abs_tol 사용
a = 1e-10
b = 2e-10
print(math.isclose(a, b)) # False (상대 오차 100%)
print(math.isclose(a, b, abs_tol=1e-9)) # True (절대 오차 1e-10)
# 올바른 방법 2: 반올림 후 비교 (간단한 경우)
print(round(0.1 + 0.2, 10) == round(0.3, 10)) # True
💰 금융 계산을 위한 decimal 모듈
금융 계산에서 0.1원의 오차도 허용되지 않습니다. 이런 경우 decimal 모듈을 사용합니다:
from decimal import Decimal, getcontext
# Decimal로 정확한 10진수 계산
print(Decimal("0.1") + Decimal("0.2")) # 0.3 (정확!)
print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3")) # True
# 주의: float에서 Decimal로 변환하면 이미 손실된 값을 가져옴
print(Decimal(0.1)) # 0.1000000000000000055511151231257827021181583404541015625
print(Decimal("0.1")) # 0.1 ← 문자열로 변환해야 정확
# 정밀도(유효 숫자 수) 설정
getcontext().prec = 50 # 50자리 정밀도
result = Decimal("1") / Decimal("3")
print(result) # 0.33333333333333333333333333333333333333333333333333
# 반올림 방법 설정
from decimal import ROUND_HALF_UP, ROUND_HALF_EVEN
price = Decimal("2.455")
print(price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) # 2.46
print(price.quantize(Decimal("0.01"), rounding=ROUND_HALF_EVEN)) # 2.46
언제 decimal을 사용해야 하는가:
- 금융 계산 (세금, 환율, 이자율)
- 정확한 소수점 반올림이 필요한 경우
- 법적으로 정확한 계산이 요구되는 경우
🔢 fractions 모듈: 유리수의 정확한 표현
from fractions import Fraction
# 정확한 유리수 표현
f = Fraction(1, 3)
print(f) # 1/3
print(f + Fraction(1, 6)) # 1/2 (정확!)
# 문자열로 생성 (정확)
print(Fraction("0.1")) # 1/10 — 정확!
# float에서 생성 (이미 손실된 값)
print(Fraction(0.1)) # 3602879701896397/36028797018963968 — 부정확!
# Fraction과 int의 혼합 연산
print(Fraction(1, 7) * 7) # 1 (정확히 1)
print((1/7) * 7) # 0.9999999999999999 (부동소수점 오차)
🎯 round()의 은행가 반올림
파이썬의 round()는 일반적인 반올림과 다르게 동작합니다:
# 파이썬의 round()는 "은행가 반올림(Banker's rounding)" 사용
# 0.5의 경우 가장 가까운 짝수로 반올림
print(round(0.5)) # 0 (짝수 방향)
print(round(1.5)) # 2 (짝수 방향)
print(round(2.5)) # 2 (짝수 방향)
print(round(3.5)) # 4 (짝수 방향)
print(round(4.5)) # 4 (짝수 방향)
# 이유: 통계적 편향 최소화
# 항상 올림 또는 항상 내림하면 많은 수의 반올림에서 편향 발생
# 짝수로 반올림하면 기대값이 0에 가까워짐
# 특정 소수점 자리로 반올림
print(round(3.14159, 2)) # 3.14
print(round(1234, -2)) # 1200 (음수 자리수 = 10의 거듭제곱)
# 다른 반올림 방법
import math
print(math.floor(2.9)) # 2 (바닥 방향)
print(math.ceil(2.1)) # 3 (천장 방향)
print(math.trunc(2.9)) # 2 (0 방향, 소수점 버림)
print(math.trunc(-2.9)) # -2 (0 방향, -3이 아님!)
⚠️ 자주 하는 실수
- float 값을 == 로 비교: 반드시
math.isclose()를 사용하세요. - 금융 계산에 float 사용: 반드시
Decimal을 사용하세요. 1원도 틀리면 안 됩니다. Decimal(0.1)와Decimal("0.1")혼동: float로 먼저 변환하면 이미 오차가 포함됩니다. 문자열"0.1"로 생성해야 정확합니다.- round()가 항상 .5를 올림한다고 가정: 파이썬은 은행가 반올림(가장 가까운 짝수)을 사용합니다.
📝 정리 및 다음 강의 예고
- 부동소수점 오차는 이진 소수의 표현 한계로 발생하며, 모든 현대 컴퓨터가 공유합니다.
- float 비교는
==대신math.isclose()를 사용하세요. - 금융 계산에는
decimal.Decimal, 정확한 유리수에는fractions.Fraction을 사용하세요. - 파이썬의
round()는 은행가 반올림(가장 가까운 짝수)을 사용합니다.
다음 강의: 문자열(str)을 완전히 해부합니다. 유니코드가 무엇인지, UTF-8이 어떻게 작동하는지, 문자열이 왜 불변인지, 그리고 인코딩 오류를 어떻게 해결하는지 배웁니다.
관련 주제
- IEEE 754 부동소수점
- 0.1+0.2 오차 원인
- math.isclose 비교
- decimal 모듈
- fractions 모듈
- 은행가 반올림(round)
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 파이썬 기초 40강 — 처음 배우는 프로그래밍
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
불러오는 중...
