파이썬 부동 소수점
컴퓨터에서 실수를 다루다보면 원하는 결과를 얻지 못할 때가 가끔 있다.
>> 0.1 * 3
0.30000000000000004
>> 0.1 * 3 == 0.3
False
>> sum([0.1] * 10) == 1
False
이런 현상이 부동 소수점 때문이라는 것은 알고 있었지만 그 이상은 알고 있지 않았기 때문에 이번 기회에 자세히 공부해보았다. 그리고 부동 소수점 문제없이 실수를 나타낼 수 있는 Frac 클래스도 직접 구현해보았다.
실수를 2진수로
컴퓨터는 2진수를 사용하기 때문에 모든 수를 0과 1로 표현한다. 정수를 2진수로 나타내기 위해 2로 계속 나누듯이 실수를 2진수로 나타내기 위해 2를 계속 곱한다. 아래 예시를 보면 쉽게 이해할 수 있다.
0.75는 2진수로 정확하게 0.75를 나타낼 수 있지만 0.2 같은 경우 0.00110011…로 무한히 반복되기 때문에 정확하게 표현하지 못하고 근사치를 저장하게 된다. 이렇게 근사치를 저장하는 방법에 고정 소수점과 부동 소수점이 있다.
고정 소수점
고정 소수점은 말 그대로 소수점이 고정된 형태다. 실수를 32bit로 나타내기 위해 부호부에 1비트, 정수부에 n비트, 소수부에 m비트를 할당한다.
정수부에 많은 비트를 할당하면 큰 숫자를 표현할 수 있지만 정밀한 숫자를 표현할 수 없고, 소수부에 많은 비트를 할당하면 정밀한 숫자를 표현할 수 있지만 큰 숫자를 표현할 수 없다. 그렇기 때문에 소수점이 고정되지 않은 부동 소수점을 사용한다.
부동 소수점
부동 소수점은 말 그대로 소수점이 둥둥 떠다니는 형태다. 여러 표현 방식 중에서 가장 대표적인 방식은 IEEE에서 표준으로 제안한 방식으로 실수를 32bit로 나타내기 위해 부호부에 1비트, 지수부에 8비트, 가수부에 23비트를 할당한다.
부동 소수점 방식을 쉽게 이해하기 위해 5.2를 2진수로 나타내보자!
- 부호부는 양수면 0, 음수면 1이 된다.
- 5와 0.2를 각각 2진수로 나타낸다.
5 = 101
0.2 = 0.001100110011...
5.2 = 101.001100110011...
2. 소수점을 왼쪽으로 이동시켜, 왼쪽에는 1만 남게 만든다.
101.001100110011... = 1.01001100110011... * 2^2
3. 지수는 2이므로, 지수에 bias(IEEE 754 표현 방식에서는 127)를 더하여 이진법으로 변환한뒤 지수부가 된다.
2 + 127 = 129 = 10000001(2)
4. 가수부는 소수점의 오른쪽 부분으로 23bit를 채운다.
0.2가 0.00110011…로 무한히 반복되기 때문에 이렇게 부동 소수점으로 표현해도 정확히 0.2를 나타낼 수 없다. 이렇게 실수를 정확히 표현하지 못하기 때문에 0.1 * 3 = 0.30000000000000004과 같은 현상이 나타난다. 이런 부동 소수점 방식의 한계는 부동 소수점을 지원하는 모든 언어에서 나타난다.
해결책
파이썬에서 부동 소수점 문제를 해결하는 방법은 여러가지가 있다.
- decimal.Decimal
from decimal import Decimal>> Decimal('0.1') * 3
Decimal('0.3')
>> Decimal('0.1') * 3 == Decimal('0.3')
True
- fractions.Fraction
from fractions import Fraction>> Fraction(1, 10) * 3
Fraction(3, 10)
>> Fraction(1, 10) * 3 == Fraction(3, 10)
True
cpython/fractions.py를 참고하여 Frac 클래스를 직접 구현해보았다.
Frac 클래스를 사용하여 0.1 * 3을 계산해도 0.3을 정확하게 표현할 수 있다.
num = Frac(1, 10) * 3
print(num) # 3/10
이번 기회에 컴퓨터가 실수를 어떻게 저장하는지 알게 되었고, 컴퓨터랑 조금 친해진 기분이었다:) 또한 Frac 클래스를 직접 구현해보면서 무심코 import해서 사용하던 라이브러리가 어떻게 만들어졌는지 이해할 수 있었다.