Python tech/고급 파이썬 공부

자주 실수하는 파이썬의 함정들

콜레오네 2023. 3. 25. 17:56

파이썬의 특징이 있는만큼 장점도 있지만, 그로 인해 생기는 문제도 많다.

필자도 이번 포스팅에 서술하는 문제들을 경험한 적이 많으며

많은 디버깅 경험을 통해 그러지 않아야겠다고 다짐한 반면

이 책과 포스팅을 읽은 여러분들은 그러지말도록 하자

.

해당 포스팅은 [클린코드, 이제는 파이썬이다] 저서의 일부입니다.


for loop 도중 리스트 item 추가/삭제 금지, 수정은 OK

리스트는 가변 객체이다.
for loop 동작 도중 이를 추가 또는 삭제하게 된다면, 손쉽게 오류를 일으킬 수 있다.

추가의 오류

>>> foo = [1,2,3,4]
>>> for i in foo:
...     if i < 10:
...         foo.append(i)

위 코드는 리스트에 10보다 작은 값을 하나 더 추가하려는 의도로 보인다.
하지만 저 코드는 결국 무한루프에 빠지게 된다.

for loop에서 리스트의 삭제도 잘못된 결과를 불러온다.
삭제의 오류

>>> foo
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> for i, e in enumerate(foo):
...     if 20 % e == 0:
...         del foo[i]
...
>>> foo
[2, 3, 5, 6, 7, 8, 9]

위 코드는 1부터 10까지 숫자중에 20의 약수를 모두 제거하려는 것으로 보인다.
올바른 결과는 [3, 6, 7, 8, 9] 이지만 결과값은 완전 틀렸다.
왜냐면 리스트 iterator는 다음 인덱스를 참조하려 하는데, 리스트 길이가 줄어들었으니
4를 지우고 5를 참조해야하지만, 5가 아닌 6을 참조하게 되어서이다.

하지만 수정은 상관없다

>>> foo
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> for i, e in enumerate(foo):
...     if 20 % e == 0:
...         foo[i] = -1
...
>>> foo
[-1, -1, 3, -1, -1, 6, 7, 8, 9, -1]

올바르게 수행되었다.

이를 해결하기 위해서는 새로운 리스트를 생성해서, 원하는 결과값을 담은 다음 값을 복사하던가 하자

리스트 값 복사를 원한다면, 무조건 copy() or deepcopy()

이 또한 리스트가 가변 객체이기 때문에 생긴 오류다.
예시를 살펴보자

>>> spam = [1,2,3]
>>> bacon = spam
>>> bacon
[1, 2, 3]
>>> bacon.append(4)
>>> bacon
[1, 2, 3, 4]
>>> spam
[1, 2, 3, 4]

# id가 같다
>>> id(spam)
2079340113472
>>> id(bacon)
2079340113472

spam과 bacon을 = 연산자로 할당한다면, 값이 아닌 참조를 복사한다.
이로 인해 spam 또는 bacon 둘 중 하나만 값을 수정하여도, 같은 id의 값이 수정되어서 같은 값으로 보인다.

이를 해결하기 위해선 copy() 메서드를 사용하자

>>> import copy
>>> spam = [1,2,3]
>>> bacon = copy.copy(spam)
>>> bacon
[1, 2, 3]
>>> bacon.append(5)
>>> bacon
[1, 2, 3, 5]
>>> spam
[1, 2, 3]

하지만 copy() 메서드도 문제가 있는 경우가 있다.
바로 다중 리스트인 경우

>>> spam = [[1,2],[3,4]]
>>> spam
[[1, 2], [3, 4]]
>>> bacon = copy.copy(spam)
>>> bacon
[[1, 2], [3, 4]]
>>> bacon.append(5)
>>> bacon[1][1] = 10
>>> bacon
[[1, 2], [3, 10], 5]
>>> spam
[[1, 2], [3, 10]]

어라? 분명 copy() 메서드를 사용했음에도 5가 추가되지 않았지만, 동일하게 10으로 변경되었다.
이는 가장 바깥 리스트 객체는 복사되었지만, 내부 리스트는 참조되고있기 때문이다.
내부 리스트까지 복사하려면, copy() 대신 deepcopy()를 사용하라고 하십니다.

문자열 연결은 + 대신 join을 습관화하자

문자열을 이어붙이는 가장 간단한 방법은 + 연산자일 것이다

>>> hello = "hello"
>>> hello += " world"
>>> hello += "!!"
>>> hello
'hello world!!'

그리고 문자열은 리스트와 다르게 불변 객체이다.
즉, 문자열 객체가 변경되지 않고, 완전히 새로운 객체로 대체된다.
새로운 메모리를 할당하고, 기존 메모리는 버려진다는 것을 의미한다.
이는 메모리 낭비일 뿐만 아니라, 프로그램의 속도 저하를 유발한다.
그렇다면
이제 + 연산자 대신, join() 메서드를 활용해보자
연결해야할 문자열을 모두 리스트에 먼저 담은 다음, 한 번에 이어붙이자는 것이다.
이렇게

>>> hello_s = ['hello']
>>> hello_s.append(' world')
>>> hello_s.append('!!')
>>> hello_s
['hello', ' world', '!!']
>>> hello = ''.join(hello_s)
>>> hello
'hello world!!'

연결자를 이용할 수도 있고, 프로그램의 성능 또한 높일 수 있다.
꼭 기억하자, 문자열 연결에는 join()

sort()가 무조건 알파벳 정렬은 아니다

sort()에서 문자열을 정렬하는 경우, ASCII 표준을 사용한다.
이 아스키 표준에서는 대문자가 소문자보다 앞선다.
따라서 무조건 알파벳 순서가 아닌, 대문자로 알파벳 정렬을 먼저 수행한 다음 소문자에 대한 정렬을 수행한다.

>>> foo
['a', 'B', 'C', 'd', 'e', 'F']
>>> foo.sort()
>>> foo
['B', 'C', 'F', 'a', 'd', 'e']

채이닝 연산에서 != 사용은 주의하자

채이닝은 단순히
(A == B) and (B == C) 에 대한 긴 조건을
A == B == C 같이 간단하게 해결해주는 파이썬의 특징이다.

(A != B) and (B != C) 이것을 A != B != C 로 줄인다면?

>>> a = 10
>>> b = 11
>>> c = 10
>>> a== b== c
False
>>> a != b != c
True

분명 a와 b와 c는 다른 값이지만 같다는 결과를 도출한다.
그러니, != 채이닝 연산은 항상 조심하자

단일 아이템을 가지는 튜플에는 항상 마침 쉼표를 찍자

튜플 내 객체가 만약 하나라면?
무조건 마지막 쉼표를 찍어, 이녀석이 튜플이라는 점을 명시해야 한다.

>>> spam = (2)
>>> spam
2
>>> foo = (2,)
>>> foo
(2,)

spam과 foo 모두 단일 객체를 가지는 튜플을 의도했겠지만
spam은 int형으로 변환되었다.
따라서, 단일 객체를 가지더라도 튜플에는 항상 쉼표를 습관화하자

반응형