포스트

파이썬에서의 반올림과 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

# 사사오입과 오사오입의 차이를 확인할 수 있다. 

다만, Decimalround()를 같이 사용하는 경우 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

https://en.wikipedia.org/wiki/Rounding#Rounding_half_up

https://ko.wikipedia.org/wiki/%EB%B0%98%EC%98%AC%EB%A6%BC

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그