Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[클린코드 파이썬] 8장: 파이썬에서 빠지기 쉬운 함정들 본문

Python/클린 코드, 이제는 파이썬이다

[클린코드 파이썬] 8장: 파이썬에서 빠지기 쉬운 함정들

끈기JK 2024. 9. 2. 20:41

루프문 진행 중에는 리스트에서 아이템을 추가/삭제하지 말자

이 코드는 작동하지 않을 것이다. 무한 반복에 빠져버려 CTRL-C를 눌러야만 멈출 수 있다.

clothes = ['skirt', 'red sock']
for clothing in clothes:  # 리스트를 반복
    if 'sock' in clothing:  # 'sock' 문자열 찾기
    	clothes.append(clothing)  # 일치하는 'sock' 짝을 추가
        print('Added a sock:', clothing)  # 사용자에게 알림

# Added a sock: red sock
# Added a sock: red sock
# Added a sock: red sock
# ...생략...
# Added a sock: red sock
# Traceback (most recent call last):
#   file "<stdin>", line 3, in <module>
#
# KeyboardInterrupt

 

여기서 문제는 'red sock'을 clothes 리스트에 추가할 때, 반복해야 하는 새로운 세 번째 아이템을 포함하는 형태인 ['skirt', 'red sock', 'red sock']로 해당 리스트가 바뀐다는 것이다.

 

여기서 얻은 교훈은 리스트를 반복하는 동안 이 리스트에 새 아이템을 추가하면 안 된다는 점이다. 이렇게 하는 대신 이 예제에서는 다음 'newClothes'와 같이 수정된 새 리스트를 구분해서 사용한다.

clothes = ['skirt', 'red sock', 'blue sock']
newClothes = []
for clothing in clothes:
    if 'sock' in clothing:
    	print('Appending:', clothing)
    	newClothes.append(clothing)  # 기존 clothes가 아니라 newClothes리스트를 변경
        
# Appending: red sock
# Appending: blue sock

 

 

참조, 메모리 사용, SYS.GETSIZEOF() 메소드


sys.getsizeof() 함수를 사용하면 객체가 메모리에서 차지하는 바이트 수를 반환한다.
import sys
sys.getsizeof('cat')
# 52
sys.getsizeof('a much longer string than just "cat"')
# 85

 

리스트는 엄밀히 말하면 문자열을 포함하는 대신 문자열을 참조할 뿐이며, 참조(reference)는 참조된 데이터의 크기에 관계없이 크기가 동일하다.
sys.getsizeof(['cat'])
# 72
sys.getsizeof(['a much longer string than just "cat"'])
# 72

 

 

 

copy.copy()나 copy.deepcopy() 없이 가변 값을 복사하지 말자

다음 코드를 대화형 셸에 입력해서, spam 변수를 바꾸면 cheese 변수도 바뀐다는 점에 유의하자.

spam = ['cat', 'dog', 'eel']
cheese = spam
spam
# ['cat', 'dog', 'eel']
cheese
# ['cat', 'dog', 'eel']
spam[2] = 'MOOSE'
spam
# ['cat', 'dog', 'MOOSE']
cheese
# ['cat', 'dog', 'MOOSE']
id(cheese), id(spam)
# 2356896337288, 2356896337288

 

이런 함정에서 벗어나는 한 가지 방법은 copy.copy() 메소드로 리스트 객체의 복사본(단순한 참조가 아닌)을 만드는 것이다.

import copy
bacon = [2, 4, 8, 16]
ham = copy.copy(bacon)
id(bacon), id(ham)
# (2356896337352, 2356896337480)
bacon[0] = 'CHANGED'
bacon
# ['CHANGED', 4, 8, 16]
ham
# [2, 4, 8, 16]

 

그러나 변수가 객체를 포함하는 상자가 아닌 레이블이나 이름표와 같듯이, 리스트에도 실제 객체가 아닌 객체를 참조하는 레이블이나 이름표가 포함된다. 리스트에 다른 리스트가 있는 경우, copy.copy()는 이 내부 리스트에 대한 참조만 복사한다.

해결 방법은 copy.deepcopy()를 사용하면 리스트 객체 내의 모든 리스트 객체(그리고 재귀적으로 이런 내부 객체 내의 모든 리스트 객체)를 복사할 수 있다.

import copy
bacon = [[1, 2,], [3, 4]]
ham = copy.deepcopy(bacon)
id(bacon[0]), id(ham[0])
# (2356896337352, 2356896466184)
bacon[0][0] = 'CHANGED'
bacon
# [['CHANGED', 2], [3, 4]]
ham
# [[1, 2], [3, 4]]

 

 

 

문자열을 문자열 연결로 생성하지 말자

파이썬에서 문자열은 불변 객체다. 이는 문자열 값이 변경될 수 없음을 의미하며, 문자열을 수정하는 것처럼 보이는 코드도 실제로는 새로운 문자열 객체를 생성한다.

 

finalString = ''
for i in range(100000):
    finalString += 'spam '
    
finalString
# spam spam spam spam spam spam spam spam spam ...생략...

 

CPU는 현재의 finalString을 'spam'과 결합해 이러한 중간 문자열 값을 만들어 메모리에 넣은 후 다음 반복 시 거의 즉시 그 값을 버려야 한다. 마지막 문자열만 중요하기 때문에, 이것은 엄청난 메모리 낭비다.

 

문자열을 만드는 파이썬다운 방법은 더 작은 문자열을 리스트에 추가한 다음, 리스트를 하나의 문자열로 함께 결합하는 것이다.

finalString = []
for i in ragen(100000):
    finalString.append('spam ')

finalString = ''.join(finalString)
finalString
# spam spam spam spam spam spam spam spam spam ...생략...

 

내 컴퓨터에서 상기 두 가지 접근법을 구현한 코드의 런타임을 측정해보니, 리스트에 추가하는 접근법은 문자열을 연결하는 접근법보다 10배 이상 빨랐다.

 

 

 

sort()가 알파벳 순으로 정렬하리라 기대하지는 말자

sort()가 소문자 a보다 대문자 Z를 우선시하는 이상한 정렬 행동을 보인다.

letters = ['z', 'A', 'a', 'Z']
letters.sort()
letters
# ['A', 'Z', 'a', 'z']

 

sort() 방식은 알파벳 정렬이 아닌 아스키 정렬(서수에 따라 정렬한다는 뜻의 일반적인 용어)을 사용한다.

 

문자를 ord() 함수에 넘겨서 코드 포인트, 즉 서수를 구할 수 있다. 서수를 chr() 함수에 전달하여 역을 구할수도 있으며, 이 함수는 문자열을 반환한다.

ord('a')
# 97
chr(97)
# 'a'

 

알파벳 정렬을 하려면 str.lower 메소드를 key 파라미터에 전달한다. 이렇게 하면 리스트는 값이 lower() 문자열 메소드에서 호출된 것처럼 정렬된다.

letters = ['z', 'A', 'a', 'Z']
letters.sort(key=str.lower)
letters
# ['A', 'a', 'z', 'Z')

 

 

 

부동소수가 완벽히 정확할 거라고 가정하지 말자

0.3
# 0.3

 

부동소수의 IEEE 754 표현이 소수점 이하 숫자와 항상 정확히 일치하지는 않을 것이다.

0.1 + 0.1 + 0.1
# 0.30000000000000004
0.3 == (0.1 + 0.1 + 0.1)
# False

 

이 요상하고도 다소 부정확한 덧셈 결과는 컴퓨터가 부동소수를 표현하고 다루는 방식에 의해 야기되는 반올림 에러( rounding error의 결과다.

 

부동소수점 데이터 유형을 사용하는 한, 이러한 반올림 에러를 해결할 방법은 없다. 하지만 걱정하지 않아도 된다. 은행이나 원자로, 또는 은행의 원자로에 대한 소프트웨어를 작성하지 않는 한, 반올림 에러는 일반적으로 작성할 프로그램에서 중요한 문제가 될 가능성이 거의 없다. 물론, 더 작은 단위의 정수를 사용하면 해결할 수도 있다. 예를 들어 1.33달러 대신 133센트, 또는 0.2초 대신 200밀리초처럼 말이다.

 

하지만 예를 들어 과학이나 재무 계산을 위해 정확한 정밀도가 필요하다면, 파이썬의 내장된 decimal 모듈을 사용하라.

import decimal
d = decimal.Decimal(0.1)
d
# Decimal('0.1000000000000000055511151231257827021181583404541015625')
d = decimal.Decimal('0.1')
d
# Decimal('0.1')
d + d + d
# Decimal('0.3')

 

그러나 Decimal 객체의 정밀도는 무한하지 않다. 예측 가능하고 안정적인 수준의 정밀도를 지원할 뿐이다.

import decimal
d = decimal.Decimal(1) / 3
d
# Decimal('0.3333333333333333333333333333')
d * 3
# Decimal('0.9999999999999999999999999999')
(d * 3) == 1  # d is not exactly 1/3
# False