본문 바로가기

python/파이썬 효율적으로 코딩하기

순환 중 객체 변형

순환 중인 객체를 변형시키면 안된다. 때로는 잘못된 결과가 발생하지 않고 순환을 벗어날 수도 있지만, 그래도 안좋은 습관이다.

 

여기서 주목해야 할 점은 일부 파이썬 객체가 불변(immutable)이라는 것이다. 예를 들어 str, bytes, tuple, frozenset 객체를 순환하는 경우 기본 컬렉션이 변형되는 문제가 발생하지 않는다. 그럼에도 불구하고 많은 파이썬 객체들은 가변(mutable)임과 동시에 반복 가능하다. 대표적으로 list, dict, set, bytearray 등이 있다. 순환 중인 객체를 변형시키려고 하면 여러 가지 면에서 문제를 일으킬 수 있다.

 

my_set = {'h','e', 'l', 'l', 'o'}
my_set = set(my_set)
for i in my_set:
    if i > 'k':
        my_set.discard(i)
        
>>> Traceback (most recent call last):
>>> RuntimeError: Set changed size during iteration

위처럼 반복 가능한 객체를 변형시키려고 한다면 RuntimeError를 만나게 된다. 그러나 순서가 있는 컬렉션(ordered collection)을 사용하는 경우에는 RuntimeError가 발생하지 않는 경우가 있다. 이러한 경우 뭔가 잘못되어 가고 있지만, 오류는 훨씬 미묘하고 눈치채기 어렵다.

 

my_list = ['h','e', 'l', 'l', 'o']
for i, k in enumerate(my_list):
    if k > 'm':
        del my_list[i]

print(my_list)
>>> ['h', 'e', 'l', 'l']

언뜻보면 위 코드는 올바르게 동작하는 것처럼 보이며, 에러 역시 발생하지 않는다. 더군다난 실제로 일부 문자가 제거된 list가 반환된다. 그러나 좀 더 자세히 살펴보면 제거되어야 하는 문자가 변형된 객체 내에 여전히 남아 있다는 것을 알 수 있다.

이 문제는 요소가 삭제되면서 색인의 위치가 실제 기본 연속 순서열과 더 이상 일치하지 않기 때문에 발생한다. 새 요소가 삽일될 때도 같은 문제가 발생할 수 있다.

 

따라서 위 문제에 대한 올바른 접근 방법은 새로운 객체를 만들고 그 안에 선택적으로 추가하는 것이다. 파이썬의 list 같은 순서가 있는 컬렉션에 대한 추가 작업은 처리 비용은 낮다. 하지만 중간에 삽입하는 작업은 O(n²)에 도달할 수 있다.

 

my_list = ['h','e', 'l', 'l', 'o']
new_list = []
for i in my_list:
    if i < 'm':
        new_list.append(i)

print(new_list)
>>> ['h', 'e', 'l', 'l']

위 방법뿐만 아니라 my_list[:]라는 간편한 문법(얕은 복사)를 이용해 동일한 list를 만들 수도 있다.