Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python zip 함수로 여러 이터레이터를 병렬로 처리하는 방법


들어가며

파이썬의 리스트 컴프리헨션(list comprehension)을 활용하면 원본 리스트에 표현식을 적용해 새로운 리스트를 아주 간결하게 만들어낼 수 있습니다. 예를 들어, 리스트의 모든 요소에 5를 곱한 값을 담은 새 리스트를 만든다고 가정해 보겠습니다. 먼저 일반적인 for 루프로 구현하면 다음과 같습니다.

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
multiply_by_5 = []
for x in a:
    multiply_by_5.append(x * 5)
print(f"Output \n *** {multiply_by_5}")

실행 결과

*** [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

같은 작업을 리스트 컴프리헨션으로 작성하면 표현식과 순회할 입력 시퀀스만 지정해 한 줄로 처리할 수 있습니다.

# 리스트 컴프리헨션
multiply_by_5 = [x * 5 for x in a]
print(f"Output \n *** {multiply_by_5}")

실행 결과

*** [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

두 개의 리스트를 요소별로 더하기

이번에는 두 개의 리스트를 더해 새로운 리스트를 만들어 보겠습니다.

# 1. 숫자 리스트 생성
list1 = [100, 200, 300, 400]
list2 = [500, 600, 700, 800]

# 2. 두 리스트를 더해 새로운 리스트 생성
list3 = []

# for 루프 사용
for i in range(len(list1)):
    added_value = list1[i] + list2[i]
    list3.append(added_value)
print(f"Output \n*** {list3}")

실행 결과

*** [600, 800, 1000, 1200]

여기서 중요한 점은 결과 리스트(list3)의 각 항목이 인덱스를 기준으로 원본 리스트들의 항목과 서로 대응된다는 사실입니다.

zip으로 병렬 순회하기

이제 내장 함수 zip을 사용해 같은 작업을 처리하는 방법을 살펴보겠습니다. 정수 리스트 두 개, 즉 100, 200, 300, 400을 담은 리스트와 500, 600, 700, 800을 담은 리스트가 있다고 가정합니다. 물론 이 값들은 변수에 할당할 수 있으며, 꼭 리스트일 필요는 없습니다. 튜플 같은 다른 시퀀스도 충분히 가능합니다.

zip은 두 시퀀스의 요소들을 하나씩 짝지어 묶어줍니다. 즉, list1의 100과 list2의 500이 하나의 쌍으로 묶이는 식입니다. 그리고 각 튜플을 순회하면서 언패킹(unpacking)해 변수 a와 b에 담아 처리할 수 있습니다.

list4 = [(a + b) for a, b in zip(list1, list2)]
print(f"Output \n*** {list4}")

실행 결과

*** [600, 800, 1000, 1200]

위 코드는 매우 깔끔해 보이지만, 실무 코드에 적용하기 전에 반드시 알아야 할 심각한 함정이 하나 있습니다.

길이가 다른 이터레이터에서의 zip 동작

zip 내장 함수는 입력 이터레이터들의 길이가 서로 다를 때 의외로 동작합니다. 직접 확인해 보겠습니다.

# list1에 새로운 숫자 추가
list1.append(1000)
print(f"Output \n*** Length of List1 is {len(list1)}, Length of List2 is {len(list2)}")

# zip으로 두 리스트 더하기
list5 = [(a + b) for a, b in zip(list1, list2)]
print(f"*** {list5}")

실행 결과

*** Length of List1 is 5, Length of List2 is 4
*** [600, 800, 1000, 1200]

출력 결과를 보면 list1에 추가한 1000이 결과에 빠져 있는 것을 알 수 있습니다. 분명히 list1에는 값이 들어 있지만 zip의 출력에는 나타나지 않습니다.

이것이 바로 zip의 동작 방식입니다. zip은 둘 중 하나라도 이터레이터가 소진되면 튜플 생성을 멈춥니다. 따라서 list1에 처리할 항목이 더 남아 있더라도 list2가 먼저 소진되는 순간 루프가 종료됩니다.

놀랍게도 이 과정에서 어떤 예외도 발생하지 않고 조용히 넘어갑니다. 따라서 프로덕션 환경에서 zip을 사용할 때는 데이터가 조용히 누락되지 않도록 특히 주의해야 합니다.

itertools의 zip_longest로 해결하기

이 문제에 대한 해답은 파이썬 표준 라이브러리인 itertools의 zip_longest 함수입니다. zip_longest는 한쪽 이터레이터가 소진되더라도 끝까지 계속 진행합니다. 소진된 쪽의 자리는 기본적으로 None으로 채워지며, 필요하다면 fillvalue 인자로 다른 값을 지정할 수도 있습니다.

from itertools import zip_longest

list6 = []
for a, b in zip_longest(list1, list2):
    if b is None:
        print("<< do your logic here >>")
    elif a is None:
        print("<< do your logic here >>")
    else:
        list6.append(a + b)
print(f"Output \n*** {list6}")

실행 결과

<< do your logic here >>
*** [600, 800, 1000, 1200]

이처럼 zip_longest를 사용하면 짧은 쪽 이터레이터가 소진된 구간을 감지해 원하는 로직을 수행할 수 있으므로, 데이터 누락 없이 안전하게 병렬 순회를 처리할 수 있습니다.

마치며

  • zip 함수는 여러 이터레이터를 병렬로 순회할 때 매우 유용합니다.

  • 길이가 다른 이터레이터를 전달하면 zip은 가장 짧은 이터레이터를 기준으로 동작하며, 나머지 항목은 아무 경고 없이 버려집니다.

  • 길이가 다른 이터레이터를 다뤄야 한다면 itertools.zip_longest를 사용하는 것이 안전합니다.