상위 목록: 하위 목록: 작성 날짜: 읽는 데 9 분 소요

집(zip) 함수

집(zip) 함수는 여러 개의 반복 가능한 데이터(목록, 사전 등)에서 같은 위치의 원소끼리 Tuple로 묶어 반환하는 함수입니다.

주로, 형태가 다른 값들을 하나의 구조나 변수로 활용하기 위해 사용합니다.

집 함수는 결괏값 = zip(범위 1, 범위 2, ...)의 형태로 사용합니다.

문자열(str), 목록(list), 튜플(tuple) 등을 묶어 결과를 반환합니다.


이름은 옷의 지퍼(zipper)에서 따왔습니다. 지퍼의 양쪽 이가 하나씩 맞물리듯, 여러 데이터의 첫 번째끼리, 두 번째끼리, 세 번째끼리 짝을 지어 줍니다.

이름 목록과 점수 목록, 키 목록과 값 목록처럼 같은 순서로 대응되는 데이터가 따로 저장되어 있을 때 이를 한 쌍씩 함께 다루기 위해 사용합니다.

집 함수도 제 22강의 필터 함수, 제 23강의 맵 함수와 마찬가지로 지연 평가(lazy evaluation) 방식의 반복자를 반환합니다.

이번 강좌에서는 집 함수로 데이터를 묶는 방법과 반복문에서의 활용, *를 이용해 묶인 데이터를 다시 푸는 방법, 그리고 길이가 서로 다른 데이터를 다루는 방법을 알아봅니다.

  • Tip : 집합(set)은 순서가 없으므로 zip으로 묶으면 어떤 원소끼리 짝지어질지 보장할 수 없습니다. 사전(dict)을 전달하면 key가 묶입니다.



함수 생성

a = "YUN"
b = [1, 2, 3]
c = ("하나", "둘", "셋")

print(list(zip(a, b, c)))
print(set(zip(a, b, c)))
print(dict(zip(a, b)))
결과
[(‘Y’, 1, ‘하나’), (‘U’, 2, ‘둘’), (‘N’, 3, ‘셋’)]
{(‘U’, 2, ‘둘’), (‘Y’, 1, ‘하나’), (‘N’, 3, ‘셋’)}
{‘Y’: 1, ‘U’: 2, ‘N’: 3}

zip(자료형1, 자료형2, ....)을 사용하여 묶어 결과를 반환합니다.

첫 번째 결과를 보면, 각 데이터의 첫 번째 원소인 'Y', 1, '하나'가 첫 번째 Tuple이 되고, 두 번째 원소끼리 두 번째 Tuple이 되는 방식으로 묶였습니다.

문자열은 문자 하나씩 반복되므로 "YUN"은 'Y', 'U', 'N'으로 나뉘어 묶입니다. 전달한 데이터가 3개이므로 각 Tuple의 원소도 3개입니다.

집(zip) 함수는 list() 또는 set() 등의 구문을 추가하지 않으면 <zip object at 0x000001941889FB00> 등의 iterator 형태로 출력됩니다. (메모리 주소는 실행할 때마다 달라집니다.)

set()으로 변환한 결과는 집합이므로 출력 순서가 실행할 때마다 달라질 수 있습니다.

dict()의 경우 key와 value로 구성되어 있으므로 2개의 범위만 묶어 변환할 수 있습니다. 3개 이상을 묶으면 ValueError가 발생합니다.

  • Tip : 집 함수가 반환하는 zip 객체는 반복자(iterator)이므로, 한 번 끝까지 꺼낸 뒤 다시 list()로 변환하면 빈 리스트가 반환됩니다.


keys = ["name", "age", "city"]
values = ["YUN", 20, "Seoul"]

info = dict(zip(keys, values))
print(info)

print(dict(zip(info.values(), info.keys())))
결과
{‘name’: ‘YUN’, ‘age’: 20, ‘city’: ‘Seoul’}
{‘YUN’: ‘name’, 20: ‘age’, ‘Seoul’: ‘city’}

dict(zip(키 목록, 값 목록))은 따로 저장된 키와 값을 사전 하나로 합치는 가장 간단한 방법입니다.

dict()는 (키, 값) 형태의 쌍을 차례로 받아 사전을 만들므로, 두 개의 범위를 묶은 집 함수의 결과를 그대로 전달할 수 있습니다.

두 번째 줄처럼 값과 키의 순서를 바꿔 묶으면 키와 값이 뒤바뀐 사전을 만들 수 있습니다. 단, 값에 중복이 있다면 뒤에 나온 키만 남습니다.


try:
    dict(zip("AB", [1, 2], ["x", "y"]))
except ValueError as e:
    print(e)
결과
dictionary update sequence element #0 has length 3; 2 is required

세 개의 범위를 묶으면 각 원소가 ('A', 1, 'x')처럼 3개의 값을 가지므로, 키와 값 2개를 기대하는 dict()에서 ValueError가 발생합니다. 오류 메시지의 #0은 첫 번째 원소에서 문제가 발생했다는 의미입니다.



반복문 적용

L1 = ["A", "B", "C", "D"]
L2 = ["가", "나", "다", "라"]


for i, j in zip(L1, L2):
    print(i, j)
결과
A 가
B 나
C 다
D 라

집(zip) 함수는 서로 다른 자료형을 하나로 묶을 수 있기 때문에, 두 개 이상의 데이터를 같은 위치끼리 동시에 반복할 수 있습니다.

이 특성으로 인해, L1[i], L2[i]처럼 색인(index)을 사용해 각각 접근하지 않고 하나의 반복문으로 간소화 할 수 있습니다.

반복할 때마다 ('A', '가') 같은 Tuple이 하나씩 나오며, for i, j in에서 Tuple의 두 값이 각각 i, j에 나뉘어 할당됩니다.

  • Tip : 색인도 함께 필요하다면 for i, (a, b) in enumerate(zip(L1, L2)):처럼 enumerate()와 같이 사용합니다.


names = ["A", "B", "C"]
scores = [90, 80, 70]

for i in range(len(names)):
    print(names[i], scores[i])

for name, score in zip(names, scores):
    print(name, score)
결과
A 90
B 80
C 70
A 90
B 80
C 70

두 반복문은 같은 결과를 출력하지만, 집 함수를 사용한 반복문이 더 안전하고 읽기 쉽습니다.

range(len(names)) 방식은 names의 길이만 기준으로 하므로, scores가 더 짧다면 IndexError가 발생합니다. 또한, 색인 i를 거쳐야 하므로 반복 변수의 의미가 드러나지 않습니다.

집 함수를 사용하면 name, score처럼 의미 있는 이름으로 바로 값을 받을 수 있으며, 색인을 지원하지 않는 반복자(파일, 맵 객체 등)도 함께 반복할 수 있습니다.


a = [1, 2, 3]
b = [4, 5, 6]

print(sum(x * y for x, y in zip(a, b)))
print(list(map(lambda x, y: x * y, a, b)))
결과
32
[4, 10, 18]

집 함수는 간소화 구문과 함께 자주 사용합니다. 첫 번째 줄은 같은 위치의 값끼리 곱한 뒤 모두 더하는 계산(1×4 + 2×5 + 3×6)입니다.

두 번째 줄처럼 맵 함수에 여러 범위를 전달해도 같은 위치의 값을 함께 처리할 수 있습니다. 집 함수는 값을 묶기만 하고, 맵 함수는 묶은 값을 함수에 전달해 변환까지 한다는 차이가 있습니다.



반복자(iterator)와 지연 평가

z = zip("AB", [1, 2])

print(z)
print(next(z))
print(list(z))
print(list(z))
결과
<zip object at 0x000001908357FE00>
(‘A’, 1)
[(‘B’, 2)]
[]

집 객체는 묶은 결과를 미리 만들어 두지 않고, next()로 요청할 때마다 각 범위에서 원소를 하나씩 꺼내 Tuple로 묶어 반환합니다.

next(z)가 첫 번째 쌍을 꺼냈으므로 다음 list(z)에는 남은 쌍만 들어가고, 마지막 list(z)는 빈 리스트가 됩니다.


z = zip([1, 2], ["a", "b"])

for x in z:
    print(x)

for x in z:
    print("다시 :", x)

print("종료")
결과
(1, ‘a’)
(2, ‘b’)
종료

같은 집 객체로 두 번 반복하면 두 번째 반복문은 오류 없이 한 번도 실행되지 않습니다. 오류가 발생하지 않기 때문에 찾기 어려운 실수입니다.

같은 묶음을 여러 번 반복해야 한다면 pairs = list(zip(...))처럼 리스트로 저장하거나, 반복할 때마다 zip()을 새로 호출합니다.


import sys

a = range(1_000_000)
b = range(1_000_000)

lazy = zip(a, b)
eager = list(zip(a, b))

print(sys.getsizeof(lazy))
print(sys.getsizeof(eager))
결과
64
8448728

백만 쌍을 리스트로 만든 eager는 약 8.4MB이지만, 집 객체 lazy는 64 바이트입니다. (64비트 Windows의 Python 3.14.6 기준)

리스트의 크기에는 리스트가 Tuple을 가리키는 공간만 포함되며, 백만 개의 Tuple 객체 자체는 별도의 메모리를 차지합니다. 반면 집 객체는 각 범위의 반복자만 저장하고 있다가 요청이 오면 그때 Tuple을 하나 만듭니다.


from itertools import count

names = ["A", "B", "C"]

print(list(zip(count(1), names)))
결과
[(1, ‘A’), (2, ‘B’), (3, ‘C’)]

itertools.count(1)은 끝없이 1, 2, 3, ...을 만드는 반복자이지만, 집 함수는 가장 짧은 범위가 끝나면 멈추므로 names의 길이만큼만 꺼냅니다.

지연 평가 덕분에 끝이 없는 반복자와도 안전하게 묶을 수 있으며, 이 예제는 enumerate(names, start=1)과 같은 결과입니다.



언패킹(Unpacking) / 패킹(Packing)

numbers = [[1, 2, 3], [4, 5, 6]]

print(*numbers)
print(list(zip(*numbers)))
print(list(zip([1, 2, 3], [4, 5, 6])))
결과
[1, 2, 3] [4, 5, 6]
[(1, 4), (2, 5), (3, 6)]
[(1, 4), (2, 5), (3, 6)]

반복 가능한 객체에 별표(Asterisk, *)를 함께 사용한다면, 언패킹되어 묶여있던 객체들이 나눠지게 됩니다.

즉, 2차원 리스트는 1차원 리스트 만큼 나눠서 반환하게 됩니다.

이 때, 집 함수를 사용하여 다시 패킹한다면 각 원소마다 묶기 때문에 여러 반복문을 구성하지 않아도 다시 패킹 할 수 있습니다.

zip(*n차원 객체)는 zip(n-1차원 객체1, n-1차원 객체2, ...)로 볼 수 있습니다.

그러므로, 반복 가능한 객체의 원소들을 대상으로 새로운 데이터를 구현할 수 있습니다.

결과를 한 줄씩 보면, print(*numbers)는 print([1, 2, 3], [4, 5, 6])과 같으므로 두 리스트가 공백으로 구분되어 출력됩니다. zip(*numbers)도 마찬가지로 zip([1, 2, 3], [4, 5, 6])과 같아지므로 세 번째 줄과 결과가 같습니다.

*는 제 20강에서 다룬 것처럼, 함수를 호출할 때 사용하면 원소를 풀어서 각각의 인수로 전달합니다.


pairs = [("A", 90), ("B", 80), ("C", 70)]

names, scores = zip(*pairs)

print(names)
print(scores)
결과
(‘A’, ‘B’, ‘C’)
(90, 80, 70)

zip(*쌍 목록)은 묶여 있는 쌍을 다시 따로 분리하는 역할을 합니다.

zip(*pairs)는 zip(("A", 90), ("B", 80), ("C", 70))과 같으므로, 첫 번째 원소끼리 ('A', 'B', 'C'), 두 번째 원소끼리 (90, 80, 70)이 묶입니다. 이 두 Tuple을 names, scores에 나눠 할당합니다.

즉, 집 함수로 묶은 결과에 다시 zip(*)을 적용하면 원래의 데이터로 되돌릴 수 있습니다. 단, 결과는 List가 아닌 Tuple이 됩니다.


matrix = [[1, 2, 3], [4, 5, 6]]

T = [list(row) for row in zip(*matrix)]
print(T)
print([list(row) for row in zip(*T)])
결과
[[1, 4], [2, 5], [3, 6]]
[[1, 2, 3], [4, 5, 6]]

같은 원리로 2차원 리스트의 행과 열을 바꾸는 전치(transpose)를 구현할 수 있습니다.

2행 3열인 matrix의 각 행이 집 함수의 인수가 되므로, 같은 열의 값끼리 묶여 3행 2열이 됩니다. 집 함수는 Tuple을 반환하므로 간소화 구문으로 각 행을 List로 변환했습니다.

전치한 결과를 다시 전치하면 원래의 행렬로 돌아옵니다.


data = [1, 2, 3, 4, 5, 6]
it = iter(data)

print(list(zip(it, it)))
결과
[(1, 2), (3, 4), (5, 6)]

iter() 함수는 리스트에서 반복자를 만들어 반환합니다.

집 함수에 같은 반복자를 두 번 전달하면, 첫 번째 범위에서 꺼낼 때와 두 번째 범위에서 꺼낼 때 모두 같은 반복자가 다음 값을 내놓습니다. 그러므로 1과 2, 3과 4, 5와 6이 차례로 묶여 두 개씩 끊어 묶는 결과가 됩니다.

zip(data, data)처럼 리스트를 두 번 전달하면 각각 독립적으로 반복되므로 (1, 1), (2, 2), ...가 되는 것과 비교하면, 반복자는 한 번 꺼낸 값을 다시 꺼낼 수 없다는 특성이 그대로 드러납니다.



이웃한 원소 묶기

from itertools import pairwise

temps = [20, 23, 21, 25]

print([b - a for a, b in zip(temps, temps[1:])])
print(list(pairwise(temps)))
결과
[3, -2, 4]
[(20, 23), (23, 21), (21, 25)]

zip(리스트, 리스트[1:])처럼 원본과 한 칸 밀린 원본을 묶으면 (첫 번째, 두 번째), (두 번째, 세 번째)처럼 이웃한 원소끼리 묶을 수 있습니다.

temps[1:]이 한 칸 짧으므로 마지막 원소에서 자연스럽게 종료되며, 예제는 하루 간의 기온 변화를 계산합니다.

itertools 모듈의 pairwise() 함수를 사용하면 같은 결과를 슬라이스로 복사본을 만들지 않고 얻을 수 있습니다. (Python 3.10 이상)



길이가 다른 경우

from itertools import zip_longest

names = ["A", "B", "C"]
scores = [90, 80]

print(list(zip(names, scores)))
print(list(zip_longest(names, scores, fillvalue=0)))
결과
[(‘A’, 90), (‘B’, 80)]
[(‘A’, 90), (‘B’, 80), (‘C’, 0)]

집 함수는 길이가 서로 다른 데이터를 묶을 경우, 가장 짧은 데이터의 길이에 맞춰 종료되며 남은 원소는 오류 없이 버려집니다.

가장 긴 데이터에 맞춰 묶으려면 itertools 모듈의 zip_longest() 함수를 사용하며, 부족한 값은 fillvalue로 채웁니다.

fillvalue를 생략하면 부족한 자리는 None으로 채워집니다.


print(list(zip([1, 2, 3])))
print(list(zip()))
결과
[(1,), (2,), (3,)]
[]

범위를 하나만 전달하면 각 원소가 원소 1개짜리 Tuple로 묶이며, 범위를 하나도 전달하지 않으면 빈 결과가 반환됩니다.

zip(*목록)을 사용할 때 목록이 비어 있으면 zip()이 되므로, 오류 없이 빈 결과가 나온다는 점을 알아 둡니다.



길이 검사(strict)

names = ["A", "B", "C"]
scores = [90, 80]

print(list(zip(names, names, strict=True)))

try:
    print(list(zip(names, scores, strict=True)))
except ValueError as e:
    print("ValueError :", e)
결과
[(‘A’, ‘A’), (‘B’, ‘B’), (‘C’, ‘C’)]
ValueError : zip() argument 2 is shorter than argument 1

zip(범위 1, 범위 2, ..., strict=True)를 사용하면 데이터의 길이가 서로 다를 때 ValueError가 발생합니다. (Python 3.10 이상)

기본값인 strict=False에서는 길이가 달라도 짧은 쪽에 맞춰 조용히 종료되므로, 데이터가 누락되어도 알아차리기 어렵습니다.

길이가 반드시 같아야 하는 데이터를 묶을 때는 strict=True를 사용하여 실수를 빠르게 발견할 수 있습니다.

오류 메시지의 argument 2 is shorter than argument 1은 두 번째 범위(scores)가 첫 번째 범위(names)보다 먼저 끝났다는 의미입니다.

  • Tip : 길이 검사는 반복하는 도중 짧은 쪽이 끝나는 시점에 이루어지므로, 오류가 발생하기 전까지 묶인 값은 이미 처리된 상태입니다.

  • Tip : Python 3.14 이상에서는 제 23강의 맵 함수에도 같은 strict 인수가 추가되었습니다.



정리

이름 설명 반환값/특징
zip(범위1, 범위2, …) 같은 위치의 원소끼리 묶음 zip 반복자, 가장 짧은 범위에서 종료
zip(…, strict=True) 길이가 다르면 오류 Python 3.10 이상, ValueError
itertools.zip_longest(…, fillvalue=값) 가장 긴 범위에 맞춰 묶음 부족한 값은 fillvalue (기본 None)
zip(*묶음) 묶인 데이터를 분리하거나 행렬을 전치 Tuple 단위로 반환
dict(zip(키, 값)) 키 목록과 값 목록으로 사전 생성 범위는 2개만 가능
itertools.pairwise(범위) 이웃한 원소끼리 묶음 Python 3.10 이상


  • 같은 순서로 대응되는 데이터를 함께 반복할 때는 색인 대신 zip을 사용합니다.
  • 집 객체도 한 번만 사용할 수 있는 반복자이므로, 여러 번 사용하려면 list()로 저장합니다.
  • 길이가 달라도 오류 없이 짧은 쪽에 맞춰 끝나므로, 길이가 같아야 하는 데이터에는 strict=True를 사용합니다.

댓글 남기기