어느덧 파이썬을 독학한 지 벌써 한 달이 넘었다.
그렇다... 벌써 한 달이 훨씬 넘었다.
나 같은 일자무식 고졸 찐따가 프로그래밍을 독학할지 그 누가 알았겠는가.
그래서 그런지 공부를 하면서 굉장히 많은 벽에 부딪혔던 적이 한두 번이 아니었다.
이 포스팅은 내가 파이썬으로 프로그래밍을 하면서 겪었던 문제점들과 그에 대한 해결법들을 정리해놓은 문서이다.
더 자세한 내용은 그동안 내가 독학하면서 정리해놓은 나의 깃허브(GitHub) 저장소를 참고하면 된다.
GitHub - iam-jjintta/python-tutorial: 흔한 찐따의 파이썬 튜토리얼 (Python Tutorial)
흔한 찐따의 파이썬 튜토리얼 (Python Tutorial). Contribute to iam-jjintta/python-tutorial development by creating an account on GitHub.
github.com
실수(부동 소수점) 연산 문제
이 문제는 내가 try-except-finally
문을 공부하고 있었을 때 우연찮게 발견한 문제였다.
문제
나는 다음과 같은 작업을 수행하는 프로그램 코드를 작성했었다.
- 사용자로부터 첫 번째 수와 두 번째 수를 입력값으로 받는다.
- 입력값으로 받은 첫 번째 수와 두 번째 수를 덧셈 연산을 한 후에 그 결과를 출력한다.
- 만약 연산된 결과가 원주율인
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.1
과 0.04
를 입력값으로 주었다.
첫번째 수를 입력하세요: 3.1
두번째 수를 입력하세요: 0.04
원주율입니다.
위의 결과는 정상적으로 출력되었다.
그러나, 내가 우연찮게 값을 2
와 1.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
원주율입니다.
참고
아래는 이 문제점을 해결하기 위해 내가 참고했던 문서들이다.
- 기술 문서: Floating Point Math
- 논문: 모든 컴퓨터 과학자가 알아야할 부동 소수점의 모든 것
- 파이썬 공식 문서: 부동 소수점 산술: 문제점 및 한계
- 파이썬 표준 라이브러리 공식 문서: math — 수학 함수
- 파이썬 표준 라이브러리 공식 문서: decimal — 십진 고정 소수점 및 부동 소수점 산술
행렬 구조 문제
리스트는 모든 타입을 요소로 가질 수 있으므로, 리스트 안에 리스트를 갖는 것 역시 가능하다.
이 문제는 내가 리스트 안에 리스트를 요소로 갖는 리스트를 만들면서 발견한 문제점이다.
문제
위에서 언급하였듯, 리스트를 통해 다음과 같이 리스트 안에 리스트를 요소로 갖는 행렬 구조로 표현하는 것이 가능하다.
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]]
참고
아래는 이 문제점을 해결하기 위해 내가 참고했던 문서들이다.
- 파이썬 공식 문서: 시퀀스 형 — list, tuple, range 의 가변 시퀀스 형
- 파이썬 표준 라이브러리 공식 문서: copy — 얕은 복사와 깊은 복사 연산
'찐따의 프로그래밍 독학 > 찐따의 파이썬 독학' 카테고리의 다른 글
찐따의 파이썬 독학 - 디시인사이드 디시콘 다운로더 (Dcinside DC-CON Downloader) (9) | 2022.04.12 |
---|---|
찐따의 파이썬 독학 - 간단한 파이썬 GUI 이메일 프로그램 만들기 (0) | 2022.04.01 |
찐따의 파이썬 독학 - 문제점과 해결 (2) (0) | 2022.03.30 |
찐따의 파이썬 독학 - 나만의 파이썬 GUI 런처 만들기 (0) | 2022.03.29 |
찐따의 파이썬 독학 - 첫 GUI 프로그램 만들기 (메모장) (0) | 2022.03.28 |