컴퓨터를 활용한 실수의 연산은 본질적으로 오차 발생 가능성을 항상 안고 있다.
크게 두 가지 이유 때문인데,
실수를 정수부와 소수부로 나누고
각각에서 위의 두 가지 문제가 어떻게 발생할 수 있는지 확인해보자.
일단 2진법으로 모든 정수를 표현할 수 있기 때문에,
이론적으로 모든 정수는 컴퓨터로 정확하게 표현이 가능하고
정수 연산에서 첫 번째 문제는 걱정하지 않아도 된다.
하지만 컴퓨터도, 그 안의 자원도 유한하기 때문에
수를 표현할 때 사용할 수 있는 자릿수에 제한이 있어 어쩔 수 없이 표현할 수 있는 수의 크기에 한계가 존재하게 되고
일반적으로 int
와 long
자료형 각각으로 표현할 수 있는 자릿수를 넘어선 값을 다루게 되면 Integer Overflow가 발생하고 따라서 부정확한 수 처리가 일어난다.
Python3는 정수 타입을
int
로 통일했다. 따라서 형 변환 실수로 인한 Integer Overflow는 걱정하지 않아도 된다.그리고 수가 시스템이 표현 가능한
sys.maxsize
를 넘어가는 경우 그 최대 크기 수의 진법으로 각 자릿수를 배열에 담아 표현하는 방식이 마련되어 있기 때문에 메모리가 허용하는 한 이론적으로는 모든 크기의 정수를 표현할 수 있다.
반면 소수 표현의 경우 첫 번째 문제부터 발생한다.
N진법 소수 표현에 기본적으로 한계가 있기 때문에,
2진법을 사용하는 컴퓨터는 모든 소수를 정확하게 표현할 수 없다.
- 일단 무리수는 소수 표현이 불가능하고
- 유리수의 경우 N의 소인수 곱으로 분모를 만들 수 있는 경우만 N진법 소수의 형태로 (유한하게) 표현할 수 있다. 그렇지 않은 경우엔 영원히 반복되는 순환소수가 된다.
따라서 2진법을 사용하는 컴퓨터로는 형태의 소수에 한해 정확한 표현이 가능하다.
ex) ,
그리고 소수점 아래 역시
자원의 유한함으로 인해 표현 가능한 자릿수의 한계가 있기 때문에
표현하고자 하는 소수부의 길이가 float
, double
자료형으로 각각 표현할 수 있는 최대 자릿수를 넘어가는 경우에도 Round-off 에러가 발생해 부정확한 수 처리가 이뤄지게 된다.
int
의 케이스와 비슷하게 파이썬은double
타입이 따로 없고 빌트인float
이 일반적인double
타입의 64비트 정확도를 갖는다. 따라서 두 타입의 형 변환 실수로 인한 오차는 파이썬에선 걱정하지 않아도 된다.
순환 소수의 경우에도 무한한 자릿수가 있다면 정확한 표현이 되니,
기본적으론 컴퓨터의 유한한 자릿수로 인해 부정확한 수 처리로 귀결된다고 볼 수 있겠다.
결론적으로 소수 연산의 경우 두 가지 문제가 모두 발생해, 부정확한 연산이 일어날 가능성이 비교적 크다.
부동소수점/고정소수점 표현과 소수 연산의 한계
부동소수점(Floating Point)과 고정소수점(Fixed Point) 방식 모두 실수 표현에 사용할 수 있는 최대 자릿수는 정해져 있다.
오히려 부동소수점 표현이 고정소수점보다 자릿수를 유연하고 효율적으로 사용하기에 더 넓은 범위의 실수를 표현할 수 있어 높은 정확도를 보여준다.
따라서 컴퓨터의 2진 부동소수점 연산의 한계는 부동소수점 표현이 아니라 2진법 체계에 방점이 찍히는 것으로 보인다.
(많은 글들에서 부동소수점의 한계라고 표현하고 있는데 어떤 맥락에서 하는 표현인지 정확한 이유를 아직 모르겠다)
먼저 자릿수의 한계로 인한 문제를 간단히 짚고 넘어가면, 더 많은 자릿수를 할당하는 것밖에는 간단히 방법이 없다.
numpy
같은 라이브러리를 활용해 빌트인 타입보다 더 많은 자릿수를 할당하는 자료형을 사용해볼 수 있겠다.
decimal
이제 파이썬에서 2진법 연산의 문제를 해결하는 두 가지 방법을 살펴보자.
보통 사람에게 익숙한 진법 체계는 10진법이기 때문에
소수 연산을 구현하는 과정에서 컴퓨터가 사용하는 체계가 2진법이라는 사실을 간과해 발생하는 계산 오차들이 많다.
ex)
0.1 + 0.2
가 컴퓨터에선 정확히0.3
이 되지 않는다는 사실은 전혀 직관적이지 않다.
따라서 둘 중에 한 쪽을 나머지에 맞추면 진법 차이로 인한 소수 연산의 오차는 예방할 수 있는데,
파이썬의 decimal
모듈을 활용하면 컴퓨터가 10진법을 기준으로 연산하도록 할 수 있다. (정확히는 10진법 부동소수점 표현을 하도록)
그러면 개발자는 하던대로 직관적인 10진법 아래에서 작업을 계속하면 된다.
기본적인 용법은 다음과 같다.
int
타입은 그대로 넘겨 Decimal
객체로 만든다.>>> decimal.Decimal(10)
Decimal
객체는 float
타입 대신 문자열 등을 인자로 넘겨 만든다. float
타입을 넘기면 해당 수를 최대한 2진법으로 표현한 걸 10진법으로 다시 변환한 결과가 저장된다.>>> decimal.Decimal('0.1')
Decimal('0.1')
>>> decimal.Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
>>> decimal.Decimal('0.1') * 5
Decimal('0.5')
>>> decimal.Decimal(1) / 5
Decimal('0.2')
>>> decimal.Decimal('0.1') * 0.5
TypeError: unsupported operand type(s) for *: 'decimal.Decimal' and 'float'
>>> decimal.getcontext().prec
28
>>> decimal.getcontext().prec = 4
>>> decimal.Decimal('0.99') * decimal.Decimal('0.21')
Decimal('0.2079')
>>> decimal.getcontext().prec = 2
>>> decimal.Decimal('0.99') * decimal.Decimal('0.21')
Decimal('0.21')
Decimal
객체의 유효숫자는 입력되는 숫자에 의해서만 결정된다. 컨텍스트 정밀도는 연산이 일어날 때만 작용.fractions
하지만 여전히 10진법 소수 표현에서도 순환 소수 이슈는 남아있다.
분수의 형태로 실수를 표현하는 파이썬의 fractions
모듈을 사용하면
오차 없이 모든 유리수를 표현할 수 있다.
(분자, 분모)
형태로 인자를 넣어 Fraction
객체를 생성한다.>>> fractions.Fraction(1, 10)
Fraction(1, 10)
Fraction
객체>>> fractions.Fraction(1, 10) + 3
Fraction(31, 10)
float
객체>>> fractions.Fraction(1, 10) + 3.0
3.1
다음의 두 가지 원인으로 인해 컴퓨터를 활용한 실수 표현에는 한계가 있다.
따라서 컴퓨터로 실수 연산을 수행할 때 예상치 못한 오차가 발생할 수 있다.
그리고 파이썬에선 다음 두 가지 모듈을 활용해 실수 연산 과정에서의 소수 표현 오차 중 유리수 표현 오차를 예방할 수 있다. (소수 표현을 하는 이상 무리수는 완전히 표현할 수 없다.)
decimal
모듈을 통해 2진법과 10진법 사이 소수 연산의 간극을 줄이거나fractions
모듈을 통해 모든 유리수를 분수 형태로 오차 없이 표현하거나