Python

itertools: iterator를 위한 도구 모음

둔진 2023. 2. 19. 17:48

iterator? iterable?

itertoolsiterator를 잘 다루기 위한 기능들을 모아둔 모듈입니다. iterator에 대한 설명은 Iterator, Generator 라는 글을 참조하시면 좋습니다. 왜 iterator가 필요한지, 어떻게 쓰는지, generator와 관계는 무엇인지가 설명되어 있습니다.

iterator를 언제 쓰는지 간단히 요약해 보면 다음과 같습니다.

  • element 수가 너무 많아서 한 번에 메모리에 올릴 수 없다.
  • 또는 element 수가 몇 개인지 미리 알 수 없다.
  • element 하나하나를 만드는데 시간이 오래 걸리거나 리소스를 많이 쓴다. 그래서 필요할 때 만들어 쓰고 싶다.
  • 특정 index의 element를 꺼내올 수 없거나 매우 어렵다. 하지만 다음 element는 쉽게 구할 수 있다.

이런 것들이 lazy evaluation순차 접근이라는 iterator의 특징과 잘 맞아떨어집니다.

builtin 함수들

Python은 자주 사용하는 iterator 관련 함수들을 builtin으로 제공하고 있습니다. 본격적으로 itertools에 대해서 알아보기 전에 iterator를 다루는 유용한 builtin 함수들을 알아보려고 합니다.

range()

규칙성을 가지고 연속되는 숫자를 만들기 위해 사용합니다.

>>> list(range(10)) # 마지막 숫자 지정. 마지막 숫자 전에 멈춤.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(10, 20)) # 처음 숫자(포함)와 마지막 숫자(제외) 지정.
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> list(range(10, 20, 2)) # 처음 숫자(포함), 마지막 숫자(제외), 증가분 지정.
[10, 12, 14, 16, 18]

엄밀히 range()의 결과는 iterator가 아닙니다.

>>> r = range(10)
>>> next(r)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    next(r)
TypeError: 'range' object is not an iterator
>>> type(r)
<class 'range'>
>>> it = iter(r)
>>> next(it)
0
>>> type(it)
<class 'range_iterator'>

위에서 보시는 것처럼 range()의 결과는 range라는 클래스입니다. range 클래스는 iterator가 아니기 때문에 next()를 사용할 수 없습니다. 흔한 오해 중 하나가 range()iterator를 반환한다인데 엄밀히는 틀린 이야기입니다.

range 클래스를 iterator로 변환해 주기 위해서는 iter()를 사용해야 합니다. range 클래스는 __iter__()를 제공하기 때문에 iter()를 사용해 iterator를 얻을 수 있습니다.

iteratoriterable, __iter__() 대해서는 Iterator, Generator를 참고하시기를 추천드립니다.

enumerator()

for문을 쓰다 보면 각 element에 순서대로 숫자를 붙이고 싶을 때가 있습니다. 이때 유용한 함수가 enumerate()입니다.
(여담인데 C계열 언어를 하다가 Python으로 옮겼을 때 혼동스러운 부분 중 하나가 이 부분입니다.)

names = ['Tom', 'Jane', 'Mike', 'Sally']

print("Counting from 0")
for i, name in enumerate(names): # 특별히 이야기가 없으면 0부터 시작
    print(i, name)

print("Counting from 2")
for i, name in enumerate(names, 2): # 두번째 인자로 시작 숫자를 지정할 수 있습니다
    print(i, name)
Counting from 0
0 Tom
1 Jane
2 Mike
3 Sally
Counting from 2
2 Tom
3 Jane
4 Mike
5 Sally

enumerate()enumerate 클래스를 반환합니다. 그럼 enumerate 클래스는 iterator일까요?

>>> names = ['Tom', 'Jane', 'Mike', 'Sally']
>>> e = enumerate(names)
>>> type(e)
<class 'enumerate'>
>>> next(e)
(0, 'Tom')
>>> 

답은 그렇다입니다.

zip()

zip()은 옷의 지퍼와 비슷한 역할을 합니다. 여러 개의 iterable에서 elemet를 하나씩 뽑아서 새로운 tuple들을 만듭니다. 말로는 복잡하니 코드를 보시죠.

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]

print(list(zip(names, ages)))

for name, age in zip(names, ages):
    print(f'{name} is {age} years old.')
[('Tom', 23), ('Jane', 34), ('Mike', 52), ('Sally', 18)]
Tom is 23 years old.
Jane is 34 years old.
Mike is 52 years old.
Sally is 18 years old.
  1. names의 첫 번째 인자인 'Tom'을 꺼내고, ages의 첫번째 인자인 23을 꺼냅니다. 둘을 합쳐서 새로운 tuple('Tom', 23)을 만듭니다.
  2. names의 두 번째 인자인 'Jane'을 꺼내고, ages의 두번째 인자인 34를 꺼냅니다. 둘을 합쳐서 새로운 tuple('Jane', 34)를 만듭니다.
  3. 이 과정을 계속합니다.
  4. namesages 중 하나의 element를 끝까지 꺼내면 마칩니다.

마지막 항목이 중요합니다. zip()은 주어진 인자 중에 짧은 iterable에 맞추어서 동작을 끝냅니다.

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52]

for name, age in zip(names, ages):
    print(f'{name} is {age} years old.')
Tom is 23 years old.
Jane is 34 years old.
Mike is 52 years old.

names에는 아직 Sally가 남아있지만 ages52를 쓰고 끝났기 때문에 zip()도 동작을 끝냅니다.

zip()의 인자는 두 개 이상이어도 됩니다.

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]
hometowns = ['San Jose', 'Seattle', 'New York', 'LA']

for name, age, hometown in zip(names, ages, hometowns):
    print(f'{name} is {age} years old. Hometown is {hometown}.')
Tom is 23 years old. Hometown is San Jose.
Jane is 34 years old. Hometown is Seattle.
Mike is 52 years old. Hometown is New York.
Sally is 18 years old. Hometown is LA.

zip()zip 클래스를 반환합니다. 그리고 zip 클래스도 iterator입니다.

map()

map()은 첫 번째 인자로 함수를 받고, 두 번째 인자로 iterable을 받습니다. map(f, i)의 예를 들어보겠습니다.

  1. i에서 첫번째 element를 꺼내서 x에 저장합니다. f(x)를 호출하고 결과를 저장합니다.
  2. i에서 두번째 element를 꺼내서 x에 저장합니다. f(x)를 호출하고 결과를 저장합니다.
  3. i의 모든 element를 사용하면 마칩니다.

역시 복잡하니까 코드를 보겠습니다.

def generate_introduction(name):
    return f'Hi, my name is {name}.'

names = ['Tom', 'Jane', 'Mike', 'Sally']
introductions = map(generate_introduction, names)
for introduction in introductions:
    print(introduction)
Hi, my name is Tom.
Hi, my name is Jane.
Hi, my name is Mike.
Hi, my name is Sally.

반복 작업을 대신해 주는 고마운 친구 같은 존재입니다.

인자로 사용할 iterable을 여러 개 줄 수도 있습니다.

def generate_introduction(name, age, hometown):
    return f'{name} is {age} years old. Hometown is {hometown}.'

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]
hometowns = ['San Jose', 'Seattle', 'New York', 'LA']

introductions = map(generate_introduction, names, ages, hometowns)

for introduction in introductions:
    print(introduction)
Tom is 23 years old. Hometown is San Jose.
Jane is 34 years old. Hometown is Seattle.
Mike is 52 years old. Hometown is New York.
Sally is 18 years old. Hometown is LA.

map()list comprehension으로도 비슷한 기능을 구현할 수 있습니다. 참고: List Comprehension

def generate_introduction(name):
    return f'Hi, my name is {name}.'

names = ['Tom', 'Jane', 'Mike', 'Sally']
introductions = [generate_introduction(name) for name in names]

for introduction in introductions:
    print(introduction)

이 코드는 list compreshion을 썼을 뿐 map()을 쓴 코드와 같은 일을 합니다. 차이는 무엇일까요? map()iteratormap 클래스를 반환하고, list comprehension은 list 클래스를 반환합니다. map 클래스는 iterator이기 때문에 더 메모리 효율적입니다.

filter()

filter()map()과 같이 첫 번째 인자로 함수를 받고, 두번째 인자로 iterable을 받습니다. filter(f, i)를 예로 들어보겠습니다.

  • i의 첫번째 인자를 꺼내 x에 저장합니다. f(x)를 호출합니다. 결과가 참이면 x를 저장하고, 거짓이면 버립니다.
  • i의 두 번째 인자를 꺼내 x에 저장합니다. f(x)를 호출합니다. 결과가 참이면 x를 저장하고, 거짓이면 버립니다.
  • i의 모든 element를 쓸 때까지 반복합니다.
def is_longer_than_3(name):
    return len(name) > 3

names = ['Tom', 'Jane', 'Mike', 'Sally']
long_names = filter(is_longer_than_3, names)

for name in long_names:
    print(name)
Jane
Mike
Sally

마찬가지로 filter()도 list comprehension을 이용해 비슷하게 구현할 수 있습니다.

def is_longer_than_3(name):
    return len(name) > 3

names = ['Tom', 'Jane', 'Mike', 'Sally']
long_names = [name for name in names if is_longer_than_3(name)]

for name in long_names:
    print(name)

이 코드는 앞의 코드와 같은 일을 합니다.

그렇다면 차이는 무엇일까요? map()과 유사하게 filter()iteratorfilter 클래스를 반환합니다. 그렇기 때문에 list를 반환하는 list comprehension보다 메모리 효율적입니다.

연쇄 호출!

앞에 보여준 함수들은 인자로 iterable을 받습니다. iteratoriterable이고요. 즉, 앞에 보여준 함수의 결과를 iterable을 인자로 받는 다른 함수의 인자로 사용가능하다는 의미입니다.

아래와 같이 filter(), map(), enumerate()을 이어서 쓸 수 있습니다.

def is_longer_than_3(name):
    return len(name) > 3

def generate_introduction(name):
    return f'Hi, my name is {name}.'

names = ['Tom', 'Jane', 'Mike', 'Sally']
introductions = enumerate(map(generate_introduction, filter(is_longer_than_3, names)), 1)

for i, introduction in introductions:
    print(f'{i}: {introduction}')
1: Hi, my name is Jane.
2: Hi, my name is Mike.
3: Hi, my name is Sally.

그 외 builtin 함수들

이 외에도 all(), any(), max(), min(), sorted(), sum()과 같은 다른 유용한 함수들도 있습니다. 이런 함수들에 대해서는 https://docs.python.org/ko/3/library/functions.html 를 참고하시면 됩니다.

itertools

이제 본격적으로 itertools 모듈에 대해서 이야기해보려고 합니다. 이제 시작입니다.

앞에 소개해드린 builtin 함수들로도 유용한 기능들을 만들 수 있지만 itertools 모듈을 잘 사용한다면 보다 효율적으로 iterator를 다룰 수 있습니다.

chain(*iterables)

chain()은 여러 개의 iterable을 하나의 iterable으로 합치기 위해서 사용합니다.
(인자의 *에 대해서는 [https://jins-sw.tistory.com/30]을 참고하세요.)

import itertools

people_from_sanjose = ['Tom', 'Jane', 'Sam']
people_from_la = ['Mario', 'Mike']
people_from_newyork = ['Jessica', 'James', 'Kim']

for name in itertools.chain(people_from_sanjose, people_from_la, people_from_newyork):
    print(name)
Tom
Jane
Sam
Mario
Mike
Jessica
James
Kim

people_from_sanjose에서 하나씩 element를 꺼내다가 다 쓰고 나면, people_from_la로 넘어가면, 그다음으로 people_from_newyork으로 넘어갑니다.

>>> import itertools
>>> 
>>> people_from_sanjose = ['Tom', 'Jane', 'Sam']
>>> people_from_la = ['Mario', 'Mike']
>>> people_from_newyork = ['Jessica', 'James', 'Kim']
>>> 
>>> it = itertools.chain(people_from_sanjose, people_from_la, people_from_newyork)
>>> type(it)
<class 'itertools.chain'>

chain()chain이라는 클래스를 반환합니다. chain 클래스는 iterator입니다.

combinations(iterable, r)

combinations()iterable에서 가능한 모든 조합을 만들어냅니다. 여기에서 조합은 수학에서 나오는 개념입니다.

예를 들어 'Tom', 'Jane', 'Smith', 'Mike'가 있다고 해보겠습니다. 이 중에서 무작위로 2명을 뽑는다고 할 때 가능한 조합은 무엇이 있을까요?

  • (Tom, Jane)
  • (Tom, Smith)
  • (Tom, Mike)
  • (Jane, Smith)
  • (Jane, Mike)
  • (Smith, Mike)

이렇게 6가지 조합이 가능합니다. (Jane, Tom)은 (Tom, Jane)과 같은 조합이기 때문에 포함되지 않습니다.

코드로 만들어 보겠습니다.

import itertools

people = ['Tom', 'Jane', 'Smith', 'Mike']

for combination in itertools.combinations(people, 2):
    print(combination)
('Tom', 'Jane')
('Tom', 'Smith')
('Tom', 'Mike')
('Jane', 'Smith')
('Jane', 'Mike')
('Smith', 'Mike')

이런 식으로 가능한 모든 조합을 찾는 것은 프로그래밍에서 흔히 하는 일 중 하나입니다. 조합을 찾는 문제는 iterator에 아주 잘 어울립니다.

  • 목록이 커질수록 가능한 조합의 수가 급격하게 커집니다. 4명 중에서 2명을 뽑는 방법은 6가지이지만, 100명 중에 3명을 뽑는 조합은 161,700 가지입니다. 100명 중에 5명을 뽑는 조합은 75,287,520입니다.
  • 대부분의 경우 모든 조합이 한 번에 필요하지 않습니다. 차근차근 가능합 조합을 만들고 이를 하나씩 처리하면 됩니다.

오늘은 100명이 참여하는 행사가 있는 날입니다. 한 명씩 번호표를 나누어 주었고, 행사 마지막에 번호표를 뽑아 상품을 주기로 했습니다. 저는 7번, 친구는 28번을 받았습니다. 총 3명을 뽑는다고 했을 때 저는 상품을 받지만 친구는 상품을 받지 못하는 경우는 얼마나 될까요? 확률만 필요하다면 수식으로 해결할 수도 있지만, 어떤 경우가 있는지 알아보기 위해 코드를 만들어 보겠습니다.

import itertools

my_number = 7
friends_number = 28

numbers = range(1, 101)

total_draws = 0
target_draws = 0

for draw in itertools.combinations(numbers, 3):
    total_draws += 1
    if my_number in draw and friends_number not in draw:
        target_draws += 1
        print(draw)

print(f'{target_draws} / {total_draws} = {target_draws / total_draws * 100:.2f}%')
(7, 97, 100)
(7, 98, 99)
(7, 98, 100)
(7, 99, 100)
4753 / 161700 = 2.94%

2.94%이군요.

그렇다면 둘 다 받을 확률은 얼마일까요?

import itertools

my_number = 7
friends_number = 28

numbers = range(1, 101)

total_draws = 0
target_draws = 0

for draw in itertools.combinations(numbers, 3):
    total_draws += 1
    if my_number in draw and friends_number in draw:
        target_draws += 1
        print(draw)

print(f'{target_draws} / {total_draws} = {target_draws / total_draws * 100:.2f}%')
...
(7, 28, 97)
(7, 28, 98)
(7, 28, 99)
(7, 28, 100)
98 / 161700 = 0.06%

0.06%입니다. 저라도 받는 것이 낫겠군요.

combinations_with_replacement(iterable, r)

조합의 다른 변형도 있습니다. 앞의 경품 뽑기 예를 보겠습니다. 앞의 예에서는 한번 뽑힌 사람은 또 뽑히더라도 상품을 주지 않는다는 가정이 깔려있습니다. 하지만 주최자가 마음을 바꿔서 앞에서 뽑혔더라도 몇 번이든 중복으로 경품을 받을 수 있도록 마음을 바꾸었습니다. 즉, 뽑은 번호표를 다시 뽑기통에 넣는 경우입니다. 이때 저는 경품을 받지만 친구는 못 받을 확률을 알아보겠습니다.

combinations() 대신 combinations_with_replacement()를 쓰면 중복을 허락한 조합을 만들 수 있습니다.

import itertools

my_number = 7
friends_number = 28

numbers = range(1, 101)

total_draws = 0
target_draws = 0

for draw in itertools.combinations_with_replacement(numbers, 3):
    total_draws += 1
    if my_number in draw and friends_number not in draw:
        target_draws += 1
        print(draw)

print(f'{target_draws} / {total_draws} = {target_draws / total_draws * 100:.2f}%')
...
(7, 98, 100)
(7, 99, 99)
(7, 99, 100)
(7, 100, 100)
4950 / 171700 = 2.88%

한 번만 받을 수 있을 때는 2.94%였는데 중복 당첨이 가능하자 2.88%로 확률이 떨어졌습니다.

여러 번 받을 수 있어서 확률이 오를 것 같았는데 이상합니다. 내가 받을 경우가 늘었지만(4753->4950) 남들이 받을 경우가 더 늘어났기 때문입니다.

permutations(iterable, r=None)

permutations()combinations()와 비슷하지만 다른 점은 조합이 아닌 순열을 만든다는 것입니다. 조합은 순서를 따지지 않지만 순열은 순서를 따집니다. 예를 들어 ('Tom', 'Jane')과 ('Jane', 'Tom')은 조합 관점에서는 같지만 순열 관점에서는 다릅니다.

조합은 조 편성과 같습니다. 제비 뽑기로 조를 뽑을 때 ('Tom', 'Jane')이나 ('Jane', 'Tom')이나 같은 조 구성이죠.

순열은 달리기와 같습니다. ('Tom', 'Jane')인 경우에는 Tom이 1등이고, Jane은 2등입니다. 하지만 ('Jane', 'Tom')은 경우는 Jane이 1등이고, Tom이 2등입니다. 즉, 두 가지가 다른 경우가 됩니다.

코드를 볼까요?

import itertools

people = ['Tom', 'Jane', 'Smith', 'Mike']

for permutation in itertools.permutations(people, 2):
    print(permutation)
('Tom', 'Jane')
('Tom', 'Smith')
('Tom', 'Mike')
('Jane', 'Tom')
('Jane', 'Smith')
('Jane', 'Mike')
('Smith', 'Tom')
('Smith', 'Jane')
('Smith', 'Mike')
('Mike', 'Tom')
('Mike', 'Jane')
('Mike', 'Smith')

('Tom', 'Jane')과 ('Jane', 'Tom')이 다른 경우로 나오는 것을 볼 수 있습니다.

count(start=0, step=1)

count()range()와 아주 유사합니다. 차이라면 count()는 종료 조건을 받지 않기 때문에 무한히 숫자를 생성합니다. start에서 시작해서 step 씩 늘리며 무한히 숫자를 만듭니다.

끝나지 않고 무한히 계속되는 숫자를 어디에 쓸까요?

한 가지 예는 zip()과 함께 쓰는 것입니다. 앞의 zip() 예시 코드를 다시 보겠습니다.

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]

for name, age in zip(names, ages):
    print(f'{name} is {age} years old.')
Tom is 23 years old.
Jane is 34 years old.
Mike is 52 years old.
Sally is 18 years old.

출력 결과에 Index를 붙이고 싶다면 어떻게 해야 할까요? 앞에서 본 enumerate()을 사용해 볼 수 있습니다.

import itertools

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]

for i, (name, age) in enumerate(zip(names, ages)):
    print(f'{i}: {name} is {age} years old.')
0: Tom is 23 years old.
1: Jane is 34 years old.
2: Mike is 52 years old.
3: Sally is 18 years old.

되기는 하지만 깔끌하지는 않습니다. i, (name, age) 부분이 중첩되는 느낌도 들고요.

count()를 써볼까요?

from itertools import count

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52, 18]

for i, name, age in zip(count(), names, ages):
    print(f'{i}: {name} is {age} years old.')
0: Tom is 23 years old.
1: Jane is 34 years old.
2: Mike is 52 years old.
3: Sally is 18 years old.

보기에 따라 다를 수 있지만 저는 이 쪽이 더 깔끔하고 직관적으로 보입니다. zip()은 길이가 짧은 iterable에 맞추어 동작하기 때문에 count()가 무한히 숫자를 생성하더라도 문제가 없습니다.

비슷하게 map()을 위한 Index로도 사용할 수 있습니다.

from itertools import count

def generate_introduction(i, name):
    return f'{i}: Hi, my name is {name}.'

names = ['Tom', 'Jane', 'Mike', 'Sally']
introductions = map(generate_introduction, count(), names)
for introduction in introductions:
    print(introduction)
0: Hi, my name is Tom.
1: Hi, my name is Jane.
2: Hi, my name is Mike.
3: Hi, my name is Sally.

cycle(iterable)

cycle()iterable의 element를 하나씩 반환하다가 끝에 돌아가면 다시 앞으로 돌아가 반복합니다. count()처럼 무한히 값을 만듭니다.

from itertools import cycle

names = ['Tom', 'Jane', 'Mike', 'Sally']
for name in cycle(names):
    print(name)
Tom
Jane
Mike
Sally
Tom
Jane
Mike
Sally
Tom
...

cycle()count()처럼 단독으로 쓰이기보다는 map()이나 zip()과 같은 함수들과 조합으로 쓰이는 경우가 많습니다.

islice(iterable, stop) 또는 islice(iterable, start, stop[, step])

islice()list의 slice 연산자인 [start:stop:step]의 iterator 버전입니다.

element를 100,000,000,000개 가진 iteratorit가 있다고 생각해 보겠습니다. 저는 이 중에 0번째에 99번째 element까지만 필요합니다. 어떻게 하면 될까요?

it[:100]

안 됩니다. ititerator이기 때문에 []연산자를 사용할 수 없습니다. 한 가지 방법은 itlist로 변환하고 []을 사용하는 것입니다.

l = list(it)
small = l[:100]

작동은 하겠지만 심각한 문제가 있습니다. list를 만들기 위해 it의 100,000,000,000개 element를 모두 메모리에 올려야 합니다. 이렇게 하면 애초에 iterator를 사용하는 의미가 없습니다.

그나마 데이터가 유한하다면 조금이나마 시도해 볼 만하지만 무한한 데이터라면 이 방법은 시도 자체가 불가능합니다. 아래 코드는 작동하지 않습니다.

list(count()) # !!! 시도하지마세요

대신 아래와 같이 해야 합니다.

small_it = itertools.islice(it, 100)

small_it는 다시 iterator입니다.

it = iter(...)
l = [...]

islice(it, 10) # => l[:10]
islice(it, 3, 10) # => l[3:10]
islice(it, 3, 10, 2) # => l[3:10:2]

인자가 하나일 때는 마지막 인덱스(stop), 인자가 세 개일 때는 start, stop, step을 의미합니다.

pairwise(iterable)

pairwise()iterable에서 하나씩 겹치며 쌍을 만듭니다. 역시 말보다는 코드죠.

from itertools import pairwise

people = ['Tom', 'Jane', 'Smith', 'Mike']

for pair in pairwise(people):
    print(pair)
('Tom', 'Jane')
('Jane', 'Smith')
('Smith', 'Mike')

repeat(object[, times])

repeat이 하는 일은 아주 간단합니다. object를 무한히 반복합니다. times를 인자로 주면 times번 만큼만 반복하고 멈춥니다.

from itertools import repeat

for i in repeat('Tom'):
    print(i)
Tom
Tom
Tom
...

zip_longest(*iterables, fillvalue=None)

zip()은 인자 중에 가장 짧은 iterable에 맞춰서 동작이 끝난다는 것을 기억하시나요? 만약에 가장 긴 인자에 맞춰서 동작을 하고, element를 다 쓴 짧은 iterable에는 기본값(fillvalue)을 채우고 싶으면 어떻게 할까요? zip_longest()를 쓰면 됩니다.

from itertools import zip_longest

names = ['Tom', 'Jane', 'Mike', 'Sally']
ages = [23, 34, 52]

for name, age in zip_longest(names, ages):
    if age:
        print(f'{name} is {age} years old.')
    else:
        print(f"I don't know {name}'s age.")
Tom is 23 years old.
Jane is 34 years old.
Mike is 52 years old.
I don't know Sally's age.

zip()과 달리 Sally까지 처리가 됐습니다.

그 외 함수들

이 외에도 다양한 함수들이 itertools에 들어있습니다. 전체 함수는 https://docs.python.org/3/library/itertools.html 에서 확인하실 수 있습니다.

마무리

이번 글에서는 iterator를 잘 사용할 수 있는 함수들을 알아보았습니다. itertools 모듈의 목적은 복잡한 기능을 하는 함수를 제공하는 것이 아니라, 레고 조작처럼 iterator를 위한 빌딩 블록을 제공하는 것이라고 합니다. 그 때문에 "겨우?", "굳이?" 하는 생각이 드는 함수들도 있을 것입니다. 하지만 모든 도구는 적재적소에 잘 사용하는 것이 중요하다고 생각합니다. 특히 대용량 데이터를 다룰 때는 iterator를 잘 다루는 것이 필수이기 때문에 itertools 모듈 또한 눈여겨보시기를 추천드립니다.