파이썬에서의 반올림과 decimal 모듈에 대한 고찰
배경
https://www.acmicpc.net/problem/18110 문제를 풀던 중, 파이썬의 round()
함수의 반올림 방식이 우리에게 익숙한 사사오입이 아닌 오사오입인 것을 뒤늦게 알게 되었다.
반올림의 방법에는 여러 방법이 있으나, 위의 두 방식에 추가적으로 오사육입까지 간단하게 알아보면 아래와 같다.
- 사사오입 (Rounding half up) : 4 이하이면 버리고, 5 이상이면 올림한다.
- 오사오입 (Rounding half to even) : 5 미만이면 버리고, 5 초과이면 올림한다. 5인 경우에는 5의 앞자리가 홀수인 경우 올리고 짝수인 경우 버린다. 즉, 앞자리를 짝수로 만들어 준다.
- 오사육입 (Rounding half down) : 사사오입과는 반대로 5이하이면 버리고, 5 초과이면 올림한다.
컴퓨터에서의 실수 표현
컴퓨터에서 정수는 완벽하게 표현이 가능하지만, 정수가 아닌 유리수를 포함하는 실수는 정확하게 표현할 수 없으며, 근사값으로 표현하게 되는데 이때 대부분의 컴퓨터가 사용하는 방식이 이진수로 나타내는 부동 소수점 이다.
대부분의 십진 소수는 컴퓨터에서 이진 소수로 표현되는 과정에서 오차가 발생할 수 밖에 없다.
분수 1/3을 컴퓨터에서 나타낸다고 생각해 보자. 1/3은 0.3333333…과 같으며 이것을 컴퓨터에서 많은 비트를 사용하여 실제 값과 거의 같도록 만들 수는 있지만, 유한한 자릿수로 정확하게 같게 만들지는 못한다.
파이썬 환경에서의 실수 표현
파이썬에서 흔히 실수를 나타내기 위해 자주 사용하는 float
도 이진 부동 소수점으로 표현되기 때문에 정확한 소수 값을 표현하지 못하며, print()
로 값을 출력했을 때 사용자가 보는 값은 사실 파이썬이 사용자가 사용하기 쉽도록 반올림하여 출력한 값이다.
파이썬에서는 이에 대응하여 정확한 십진 표현과 연산을 제공하는 decimal
이라는 내장 모듈을 제공한다.
유리수를 기반으로 정확한 표현과 연산을 제공하는 fractions
모듈도 있지만 여기서는 decimal
만을 다룰 것이다.
decimal 모듈
일반적으로 아래와 같이 import하여 사용할 수 있다. getcontext()
함수는 활성 스레드의 현재 컨텍스트를 반환한다. 아래 코드를 실행하면 디폴트로 적용되어 있는 context 정보를 확인할 수 있다.
1
2
3
4
5
from decimal import *
print(getcontext())
# Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
아래와 같이 사용하면 현재 컨텍스트에 액세스하고 설정을 변경할 수 있다.
1
2
# 산술 연산의 정밀도를 기본값인 28에서 7로 수정
getcontext().prec = 7
setcontext()
함수는 인자로 주어진 컨텍스트를 활성화시킨다.
1
2
3
# 새로운 컨텍스트를 생성자로 생성하고 활성화
testContext = Context(prec=7, rounding=ROUND_HALF_UP)
setcontext(testContext)
Decimal()
생성자를 사용하여 새로운 십진 부동 소수점 객체를 만들 수 있으며 생성되는 객체는 getcontext()로 반환되는 컨텍스트를 따른다. 정수, 문자열, 실수(float) 또는 튜플을 생성자의 인자로 사용할 수 있다.
decimal을 다룬 여러 포스트에서 Decimal() 생성자의 인자를 문자열로 한정하였으나, 버전 3.2부터
float
형 인자가 허용되었음을 공식 문서에서 확인하였다.
컨텍스트에 여러 신호를 예외로 처리할 지 여부를 트랩 활성화기(trap enabler)
로 설정할 수 있다.
1
2
3
4
5
print(type(getcontext().traps))
# <class 'abc.SignalDict'>
# traps는 딕셔너리 형태의 추상 베이스 클래스이다.
Context
클래스 내부의 traps
속성은 아래와 같은 형태이다.
1
traps: dict[_TrapType, bool]
예를 들어, 기본값이 False인 FloatOperation
신호를 True로 트랩하게 되면, Decimal()
생성자에 float
를 인자로 제공하거나 Decimal
객체와 float
를 비교하는 경우 예외가 발생한다.
1
2
3
4
5
6
7
getcontext().traps[FloatOperation] = True
print(Decimal(1.5) < 0.3)
# Traceback (most recent call last):
# File "D:\pycharmProjects\test\main.py", line 4, in <module>
# print(Decimal(1.5) < 0.3)
# decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
decimal 모듈을 사용한 사사오입 방식 반올림
기존 방법으로 사용한 round()
함수와 decimal
모듈을 사용한 round()
함수로 각각 실수 2.5 를 반올림해 보았다.
1
2
3
4
5
6
7
8
9
10
11
from decimal import *
print(round(2.5, 0))
getcontext().rounding = ROUND_HALF_UP # 반올림 방식 지정
print(round(Decimal(2.5), 0))
# 2.0
# 3
# 사사오입과 오사오입의 차이를 확인할 수 있다.
다만, Decimal
과 round()
를 같이 사용하는 경우 round()
의 두번째 인수로 ndigits를 명시해 주어야 한다.
그 이유는 round()
함수는 두번째 인자가 주어지지 않는 경우 반환형이 int
이지만 주어지는 경우 반환형이 첫번째 인자와 같은 type
을 반환하게 되기 때문이다.
즉, 두번째 인자값을 생략하면 반환값이 Decimal
객체가 아님으로 인해 컨텍스트를 따르지 않게 되어 기존의 round()
와 동일하게 오사오입 방식으로 반올림되는 것으로 보인다.
1
2
3
4
5
6
7
print(round(Decimal(2.5)) == Decimal(3.0))
print(round(Decimal(2.5), 0) == Decimal(3.0))
# False
# True
# 두 경우의 차이를 확인할 수 있다.
결론
컴퓨터 상에서 실수를 사용할 때의 오차를 언어 차원에서 처리하는 방식이 다양한 것을 알게 되었다.
다른 언어에서도 마찬가지겠지만, 여러 함수 또는 메소드를 같이 사용하는 경우 반환형을 제대로 인식하는 것이 중요하다.
아래의 참고문헌 중 파이썬 공식문서 두 페이지는 읽으면 좋을만한 내용이 많다.
참고문헌
https://docs.python.org/ko/3/library/decimal.html
https://docs.python.org/ko/3/tutorial/floatingpoint.html