Skip to content

4.01. 부록: 프로그래밍 시 발견한 문제점과 해결

흔한 찐따 edited this page Apr 6, 2022 · 1 revision

실수(부동 소수점) 연산 문제

이 문제는 내가 try-except-finally 문을 공부하고 있었을 때 우연찮게 발견한 문제였다.

문제

나는 다음과 같은 작업을 수행하는 프로그램 코드를 작성했었다.

  1. 사용자로부터 첫번째 수와 두번째 수를 입력값으로 받는다.
  2. 입력값으로 받은 첫번째 수와 두번째 수를 덧셈 연산을 한 후에 그 결과를 출력한다.
  3. 만약 연산된 결과가 원주율인 pi(π) 라면, 원주율입니다. 라는 메시지를 대신 출력하도록 한다.

그래서 맨 처음에는 아래와 같이 코드를 작성했다.

x = input('첫번째 수를 입력하세요: ')
y = input('두번째 수를 입력하세요: ')

try:
    x = float(x)
    y = float(y)

except Exception as e:
    print('잘못된 입력값입니다.')

finally:
    if x + y == 3.14:
        print('원주율입니다.')
    else:
        print(f'{x} + {y} = {x + y}')

그 다음, 위의 코드를 실행한 뒤에 아래처럼 3.10.04 를 입력값으로 주었다.

첫번째 수를 입력하세요: 3.1
두번째 수를 입력하세요: 0.04
원주율입니다.

위의 결과는 정상적으로 출력되었다.

그러나, 내가 우연찮게 값을 21.14 로 입력해봤는데, 이렇게 값을 입력하면 아래처럼 이상한 결과가 나온다.

첫번째 수를 입력하세요: 2
두번째 수를 입력하세요: 1.14
2.0 + 1.14 = 3.1399999999999997

원인

그래서 나는 이 문제점이 왜 발생하는지 찾아보았는데, 그 원인은 바로 파이썬에서는 실수를 표현하기 위해 근산값으로 반올림하는데, 그 과정에서 부동 소수점 반올림 오차가 발생해서 그렇다.

컴퓨터에서는 부동 소수점을 근산값으로 표현할 때 머신 앱실론(Machine Epsilon) 이라는 것을 사용한다다. 머신 앱실론이란, 1과 1 위의 부동 소수점 사이의 간격을 의미한다. 파이썬에서는 머신 앱실론에 의해 부동 소수점 연산 시 오차가 발생하게 된다. 따라서 2.0 + 1.14 와 같은 연산을 했을 때, 3.14 가 아닌, 그 근산값인 3.1399999999999997 가 나오게 되는 것이었다.

해결

결론부터 이야기하자면, 파이썬에서 부동 소수점 연산을 하는 경우, 비교 연산자를 사용하면 안 된다.

그 대신, 파이썬 표준 라이브러리인 math 를 사용해서 비교해야 한다. 파이썬 공식 문서의 math에 자세한 내용이 있다. math 라이브러리에서 제공되는 isclose 함수를 사용해서 비교하면 이 문제를 해결할 수 있다.

import math

x = input('첫번째 수를 입력하세요: ')
y = input('두번째 수를 입력하세요: ')

try:
    x = float(x)
    y = float(y)

except Exception as e:
    print('잘못된 입력값입니다.')

finally:
    if math.isclose(x + y, 3.14):
        print('원주율입니다.')
    else:
        print(f'{x} + {y} = {x + y}')

결과

첫번째 수를 입력하세요: 2
두번째 수를 입력하세요: 1.14
원주율입니다.

그러나, isclose 함수는 파이썬 3.5 버전 이상부터 사용이 가능하다. 때문에 십진 고정 소수점 및 부동 소수점 산술을 위한 파이썬 표준 라이브러리인 decimal 을 사용하는 것이 좋다. decimal 라이브러리의 Decimal 객체로 부동 소수점을 비교하는 것이 가능하다.

from decimal import Decimal

x = input('첫번째 수를 입력하세요: ')
y = input('두번째 수를 입력하세요: ')

try:
    x = Decimal(x)
    y = Decimal(y)
    z = x + y

except Exception as e:
    print('잘못된 입력값입니다.')

finally:
    pi = Decimal('3.14')
    if z == pi:
        print('원주율입니다.')
    else:
        print(f'{float(x)} + {float(y)} = {float(z)}')

결과

첫번째 수를 입력하세요: 2
두번째 수를 입력하세요: 1.14
원주율입니다.

참고

아래는 이 문제점을 해결하기 위해 내가 참고했던 문서들이다.

행렬 구조 문제

리스트는 모든 타입을 요소로 가질 수 있으므로, 리스트 안에 리스트를 갖는 것 역시 가능하다. 이 문제는 내가 리스트 안에 리스트를 요소로 갖는 리스트를 만들면서 발견한 문제점이다.

문제

위에서 언급하였듯, 리스트를 통해 다음과 같이 리스트 안에 리스트를 요소로 갖는 행렬 구조로 표현하는 것이 가능하다.

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
print(matrix)

위는 3 x 3 의 정방행렬을 리스트 구조로 표현한 것이다. 그리고 나는 이 행렬의 단위행렬을 만들기 위해 아래와 같이 코드를 작성했다.

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# 행과 열을 구한다.
row, col = len(matrix), len(matrix[0])
print(f'{row}x{col} 행렬: {matrix}')

# 리스트의 copy 메서드를 통해 행렬을 복사한다.
unit_matrix = matrix.copy()

for i in range(row):
    for j in range(col):
        # 행과 열이 서로 같으면 1로 변환하고, 아니면 0으로 변환한다.
        if i == j:
            unit_matrix[i][j] = 1
        else:
            unit_matrix[i][j] = 0
else:
    print(f'{row}x{col} 행렬: {matrix}')
    print(f'{row}x{col} 단위행렬: {unit_matrix}')

그런데 위의 코드를 실행하면 다음과 같이 나온다.

결과

3x3 행렬: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3x3 행렬: [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
3x3 단위행렬: [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

분명히 나는 리스트 행렬 matrix 의 값을 바꾼 적이 없다. 그런데도 리스트 행렬 matrix 안의 값이 바뀌어 있었다.

원인

이 문제의 원인은 바로 얕은 복사가 일어났기 때문에 발생하는 문제였다.

얕은 복사란, 객체 안의 객체를 완전히 복사하는 것이 아닌, 객체 안의 객체를 단순히 참조하는 것을 의미한다. 쉽게 말해서 값 자체를 복사한 것이 아니라, 주소값을 복사한 것이다. 즉, 리스트의 copy 메서드는 얕은 복사를 하는 메서드였던 것이다.

설명만으로는 잘 이해가 안 되어서 직접 아래의 예시를 작성해서 눈으로 확인해보니까 이해가 되었다.

a = [[1, 2], [3, 4]]
b = a.copy()

print('a is b:', a is b)

print('a의 주소값:', id(a))
print('b의 주소값:', id(b))

print('a[0] is b[0]:', a[0] is b[0])

print('a[0]의 주소값:', id(a[0]))
print('b[0]의 주소값:', id(b[0]))

결과

a is b: False
a의 주소값: 1320699826240
b의 주소값: 1320689489856
a[0] is b[0]: True
a[0]의 주소값: 1320689491200
b[0]의 주소값: 1320689491200

해결

결론부터 이야기하자면, 요소의 주소값이 아닌, 값 자체를 온전히 복사하는 깊은 복사를 통해 해결할 수 있다.

즉, 이 원인을 해결하기 위해서는 리스트 안의 요소들까지 전부 copy 메서드를 사용해서 복사하거나, append 메서드를 통해 직접 값을 넣어줘야 한다.

a = [[1, 2], [3, 4]]
b = []

for row, element in enumerate(a):
    b.append([])
    for e in element:
        b[row].append(e)
else:
    print('a:', a)
    print('b:', b)
    print('a[0] is b[0]:', a[0] is b[0])

결과

a: [[1, 2], [3, 4]]
b: [[1, 2], [3, 4]]
a[0] is b[0]: False

이보다 훨씬 더 간단한 방법이 존재한다.

파이썬에서는 깊은 복사를 지원하는 모듈인 copy 모듈이 있는데, copy 모듈 안에 정의된 deepcopy 함수를 사용하면 훨씬 더 간단하고 쉽게 깊은 복사를 할 수 있다.

from copy import deepcopy

a = [[1, 2], [[3, 4]]]
b = deepcopy(a)

print('a:', a)
print('b:', b)
print('a[0] is b[0]:', a[0] is b[0])

결과

a: [[1, 2], [[3, 4]]]
b: [[1, 2], [[3, 4]]]
a[0] is b[0]: False

이제 단위행렬을 만드는 코드를 다음과 같이 만들 수 있다.

from copy import deepcopy

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# 행과 열을 구한다.
row, col = len(matrix), len(matrix[0])
print(f'{row}x{col} 행렬: {matrix}')

# deepcopy 함수를 통해 깊은 복사를 한다.
unit_matrix = deepcopy(matrix)

for i in range(row):
    for j in range(col):
        # 행과 열이 서로 같으면 1로 변환하고, 아니면 0으로 변환한다.
        if i == j:
            unit_matrix[i][j] = 1
        else:
            unit_matrix[i][j] = 0
else:
    print(f'{row}x{col} 행렬: {matrix}')
    print(f'{row}x{col} 단위행렬: {unit_matrix}')

결과

3x3 행렬: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3x3 행렬: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3x3 단위행렬: [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

참고

아래는 이 문제점을 해결하기 위해 내가 참고했던 문서들이다.