본문 바로가기

실수 자료형

1. 실수란?

수학에서 실수(實數, real number)는 실제로 존재하는 수입니다. 파이썬에서 실수는 float로 표현됩니다. float는 소수점이 있는 숫자를 나타냅니다. 예를 들어, 3.14, 0.001, -0.1 등이 있습니다. 앞에 0은 생략할 수 있습니다. 0.1과 .1은 같은 값을 나타냅니다.

파이썬의 float는 IEEE 754 배정밀도(64비트) 부동소수점으로 구현되어 있습니다. 이로 인해 정밀도에 한계가 있어 0.1 + 0.2가 정확히 0.3이 되지 않는 현상이 발생합니다.

1.1 실수 타입의 변수 생성

x = 10.0
y = 10
print(type(x), type(y)) # 출력 : <class 'float'> <class 'int'>

이 예제에서 x, y는 모두 10을 저장하고 있습니다. 하지만 x는 실수형, y는 정수형입니다. 10.0은 뒤에 소숫점으로 인하여 실수로 인식되기 때문입니다.

1.2 실수의 사칙연산

실수도 기본적인 산술연산을 지원합니다. 이 챕터에서는 간단한 사칙연산만 해보도록 하겠습니다.

1.2.1 덧셈

실수와 실수의 덧셈은 결과가 실수로 나옵니다.

x = 9.9
y = 20.1
result = x + y
print(result)
print(type(result))

실수와 정수의 덧셈도 결과는 실수로 나옵니다. 다른 언어는 정수와 실수의 덧셈이 에러가 나는 경우도 있습니다. 이러한 결과가 일반적이진 않다는 것을 기억해주세요.

x = 10.0
y = 20
result = x + y
print(result)
print(type(result))

1.2.2 뺄셈

x = 15.5
y = 5.5
result = x - y
print(result)
print(type(result))

1.2.3 곱셈

x = 10.0
y = 2
result = x * y
print(result)
print(type(result))

1.2.4 나눗셈

나눗셈은 두 종류가 있습니다. 하나는 나눗셈의 결과를 소수점까지 표시(/)하는 것이고, 다른 하나는 몫만 남기는(//) 것입니다.

x = 10
y = 5
result = x / y
print(result)
print(type(result))

x = 10.0
y = 5.0
result = x // y
print(result)
print(type(result))

위 코드의 실행 결과는 정수가 나올 것 같지만 실수가 나옵니다.

x = 10.0
y = 5
result = x / y
print(result)
print(type(result))

실수와 정수의 나눗셈도 결과는 실수로 나옵니다.

2. float의 특수값

파이썬의 float 클래스에는 몇몇 특별한 값을 가진 상수들이 있습니다. 여기서는 자주 사용되는 양의 무한대와 음의 무한대를 알아보도록 하겠습니다.

  • inf: 양의 무한대를 나타내는 상수입니다.
x = float("inf")
  • -inf: 음의 무한대를 나타내는 상수입니다.
x = float("-inf")

이러한 무한대는 아래와 같이 비교 구문에서 활용될 수 있습니다.

float("inf") > 10

3. 실수의 부동소수점 문제

실수의 연산은 정수와 다르게 정확하지 않을 수 있습니다. 이를 고려하여 코드를 작성해야 합니다.

예를 들어 아래와 같은 연산은 컴퓨터가 정확히 연산할 수 없습니다. 대부분의 프로그래밍 언어에서 이를 정확히 처리하지 못합니다. 결과를 예상하고 실행해보세요.

10.1 + 10.2

위의 출력값은 20.3이 아닙니다. 20.299999999999997입니다. 이는 아래와 같은 연산도 마찬가지입니다.

0.1 + 0.2

파이썬의 float는 IEEE 754 표준을 따르는 64비트 배정밀도(double precision)로 구현되어 있습니다. 이 표준은 10진수 숫자를 표현하는데 완벽하지 않습니다. 대부분의 프로그래밍 언어는 이 표준을 따릅니다. 컴퓨터 연산은 2진수로 이뤄지기 때문에 10진수를 2진수로 변환하는 과정에서 오차가 발생하는 것이죠.

이를 이해하기 위해서는 우선 10진수를 2진수로 바꾸는 방법을 알아야 합니다. 프로그래밍 언어는 기본적으로 사람이 사용하기 편리하도록 10진수를 기반으로 수를 표현하지만 컴파일 과정에서 2진수로 전환되기 때문입니다.

소수부에 2를 곱하고, 그때 넘어온 정수부(0 또는 1)를 차례대로 받아 적습니다. 소수부가 0이 될 때까지 이 과정을 반복합니다. 0.5로 해보겠습니다.

.5 * 2 = 1.0 -> 정수부 1, 소수부 0이므로 종료

받아 적은 숫자를 순서대로 소수점 뒤에 붙이면 됩니다. 이것이 소수를 이진수로 바꾸는 방법입니다.

0.5의 이진수 표현은 0.1

이번에는 0.1을 이진수로 바꿔보겠습니다.

.1 * 2 = 0.2,
.2 * 2 = 0.4,
.4 * 2 = 0.8,
.8 * 2 = 1.6,
.6 * 2 = 1.2,
.2 * 2 = 0.4, # 여기서부터 같은 결과가 반복되기 시작합니다.
.4 * 2 = 0.8,
.8 * 2 = 1.6,
.6 * 2 = 1.2,
.2 * 2 = 0.4,
.4 * 2 = 0.8,
.8 * 2 = 1.6,
.6 * 2 = 1.2,
.
.
.
받아 적은 정수부를 이어 붙이면 결과는 0.000110011001100110011... 이 됩니다.

0.5와 다르게 0.1은 무한소수가 됩니다. 컴퓨터는 메모리의 한계로 무한한 값을 저장할 수 없기 때문에 적당한 소수점 위치에서 반올림하여 계산을 종료합니다.

이번에는 0.2를 이진수로 바꿔보겠습니다.

.2 * 2 = 0.4,
.4 * 2 = 0.8,
.8 * 2 = 1.6,
.6 * 2 = 1.2,
.2 * 2 = 0.4,
.4 * 2 = 0.8,
.8 * 2 = 1.6,
.
.
.
받아 적은 정수부를 이어 붙이면 결과는 0.00110011001100110011... 이 됩니다.

마찬가지로 0.2 역시 무한소수가 됩니다. 즉 컴퓨터에는 애초에 정확한 0.1도, 정확한 0.2도 들어있지 않습니다. 각각에 가장 가까운 근삿값이 들어있을 뿐이고, 이 두 근삿값을 더했으니 결과에도 오차가 남는 것입니다.

조금 더 들여다보겠습니다. 파이썬이 표현할 수 있는 수 중 0.3에 가장 가까운 것은 아래 값입니다.

0.299999999999999988897769753748434595763683319091796875

그런데 0.1의 근삿값과 0.2의 근삿값을 실제로 더하면 이보다 살짝 큰 아래 값이 나옵니다.

0.3000000000000000444089209850062616169452667236328125

두 값은 서로 다른 수이기 때문에 파이썬은 이를 0.3으로 줄여 보여줄 수 없고, 구분이 가능한 최소한의 자릿수인 0.30000000000000004로 출력합니다. 0.1 + 0.2 == 0.3이 False인 이유가 여기에 있습니다. 직접 확인해보세요.

from decimal import Decimal

print(Decimal(0.3))        # 0.3의 실제 근삿값
print(Decimal(0.1 + 0.2))  # 0.1 + 0.2의 실제 결괏값
print(0.1 + 0.2 == 0.3)    # False

해결하기

이를 해결하는 방법은 여러가지 방법이 있습니다. python에서는 decimal을 사용하여 좀 더 정확한 연산을 할 수 있습니다. 아래 서비스는 여러가지 언어들에서 발생하는 문제를 해결하는 방법을 담고 있습니다. 다른 언어를 하실 때에도 아래 서비스를 이용하시면 쉽게 해결책을 검색해볼 수 있습니다.

Floating Point Math
import decimal

float(decimal.Decimal('.1') + decimal.Decimal('.2')) # 출력: 0.3

4. 지수의 표현

10에 승수는 e나 E로 표현할 수 있습니다. 이는 모두 float형으로 표현됩니다.

print(2e3) # 지수표현, float형
print(2E3)

print(2 * (10 ** 3)) # int 형

아래처럼 float형이 표현할 수 있는 범위를 넘겨버리면 그 값 자체가 무한대(inf)가 되어버립니다. 따라서 float('inf')가 항상 다른 수보다 크다고 단정할 수 없습니다.

IEEE 754 배정밀도로 표현할 수 있는 가장 큰 유한 값은 대략 1.8 * (10 ** 308)입니다. 10e307은 이보다 작아 정상적인 실수로 남지만, 10e308은 이 한계를 넘어서기 때문에 리터럴을 적는 순간 inf가 됩니다. 반면 정수형인 int는 메모리가 허용하는 한 한계 없이 표현합니다.

print(10e307)                    # 1e+308, 표현 가능한 값
print(10e308)                    # inf, 범위를 넘겨 무한대가 됨

print(float('inf') > 10e307)     # True
print(float('inf') > 10e308)     # False, 양쪽 모두 inf라 크지 않음
print(float('inf') == 10e308)    # True