Python 강좌 : 제 24강 - 집 함수
집(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를 사용합니다.
댓글 남기기