Python의 yield 키워드 알아보기

이 글은 Stackoverflow "What does the yield keyword do in Python? (Python에서 yield 키워드는 무엇을 하나요?)"의 번역문입니다. 예재를 포함한 원문은 링크에서 확인해보실 수 있습니다.

Python의 yield 키워드 알아보기

주의: 이 글은 2017년 현재 Python yield 키워드에 대한 모든 것을 설명해주지는 않습니다. 하지만 Python의 기초 지식을 쌓는 데에는 도움이 됩니다.

yield가 무엇을 하는지 이해하기 전에 우리는 제너레이터(Generators)가 무엇인지부터 알 필요가 있습니다. 그리고 이 제너레이터를 알기 위해서는 iterables를 먼저 알아야 합니다.

이터러블(Iterables)

우리는 리스트를 만든 후 해당 리스트에 있는 객체를 순환하며 하나씩 꺼내서 사용할 수 있습니다. 이러한 과정을 "순환(Iteration)"이라 부릅니다:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

위 코드에서 mylist이터러블합니다. 우리가 list comprehension(이해하기 쉽게 "리스트 표현식"이라 부르겠습니다)를 사용할 때, 우리는 리스트를 새로 만들게 됩니다, 이렇게 만들어진 리스트 또한 물론 이터러블합니다:

>>> mylist = [x * x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

즉 "for... in..." 표현식으로 나타낼 수 있는 모든 것들은 다 이터러블하다고 말할 수 있습니다. listsstrings도 말이죠.

이런 이터러블한 것들은 우리가 원하는 만큼 접근해서 사용할 수 있기 때문에 매우 유용한 한편 이렇게 하기 위해 모든 값을 메모리에 담고 있어야 하기 때문에 큰 값을 다룰 때에는 별로 좋지 않습니다.

제너레이터(Generators)

제너레이터(generators)는 이터레이터(iterators)입니다. 하지만 제너레이터는 모든 값을 메모리에 담고 있지 않고 그때그때 값을 생성(generator)해서 반환하기 때문에 제너레이터를 사용할 때에는 한 번에 한 개의 값만 순환(iterate) 할 수 있습니다:

>>> mygenerator = (x * x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

위 코드에서 볼 수 있듯 [] 대신 ()를 사용한다는 것을 제외하고는 일반적인 이터레이터와 동일합니다. 하지만 우리는 제너레이터를 이용해 for i in mygenerator 코드를 두 번 실행할 수는 없습니다. 제너레이터는 한 번만 사용될 수 있기 때문입니다: 제너레이터는 0을 계산해서 반환한 후 0에 대해서는 아예 잊습니다, 그리고 1을 계산하고 한 번에 하나씩 처리해가며 순환을 종료합니다.

Yield

Yield는 함수가 제너레이터를 반환한다는 것을 제외하고 return과 비슷하게 사용되는 키워드입니다.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i * i
...
>>> mygenerator = createGenerator() # 제너레이터 생성
>>> print(mygenerator) # mygenerator는 객체입니다.
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

쓸모없는 예를 들어봤습니다, 하지만 만약 한 번만 순환할 거대한 리스트(set)를 반환하는 함수를 만들어야 한다면 이와 같은 방법은 매우 유용하게 사용됩니다.

yield를 완벽히 마스터하기 위해 우리는 **함수를 호출해도 함수 내에 있는 코드들이 실행되지 않는다.**라는 것을 이해해야 합니다. 함수는 실행될 때 그저 제너레이터 객체를 반환하고 이는 약간 생각을 복잡하게 합니다.

코드는 실제로 for 루프로 제너레이터를 돌 때 실행됩니다.

함수로부터 만들어진 제너레이터 객체가 for 루프를 통해 처음 실행될 때 Python은 함수 내에 있는 코드를 yield 키워드를 만나기 전까지 실행하고 첫 번째 루프의 값을 반환하게 됩니다. 다음 루프 때에는 yield 키워드 뒤에 있는 코드를 실행하고 다시 루프를 돌면서 반환할 값이 아예 없을 때까지 계속 같은 과정을 반복하게 됩니다.

제너레이터는 함수가 실행됐는데 더 이상 yield를 만나지 못했을 때 다 끝난 것으로 간주합니다. 루프가 끝났거나 if/else와 같은 조건문을 더 이상 만족하지 않는 경우에 말이죠.

제너레이터 다루기

>>> class Bank(): # 은행을 만들고 ATM도 만듭시다.
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # 아무 문제 없다면 ATM은 원하는 만큼 줄겁니다.
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # 경제 공황이 오고 있습니다, 돈이 더 없겠네요!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # 새로운 ATM을 만들어도 마찬가지입니다.
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # 경제 공황이 끝났음에도 ATM은 모두 비어있습니다.
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # 다시 일상으로 돌아가기 위해 새로운 ATM을 만듭니다.
>>> for cash in brand_new_atm:
...    print(cash)
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

제너레이터는 위 코드와 같이 리소스에 대한 제어가 필요할 때에 매우 유용하게 사용됩니다.

Itertools, 최고의 친구

itertools 모듈은 이터러블한 객체를 다루기 위한 매우 특별한 함수들을 포함하고 있습니다. 제너레이터 객체를 복사하고 싶었을 때가 있었나요? 두 개의 제너레이터를 이어보고 싶었을 때가 있었나요? 중첩 리스트에 있는 값을 한 줄로 합치고 싶었던 적이 있었나요? 리스트를 추가로 생성하지 않고 Map / Zip을 사용하고 싶었을 때가 있었나요?

그렇다면 import itertools가 모든 것을 해결해줄 것입니다.

예가 필요하겠죠? 4개의 경주말이 출발해서 도착할 수 있는 모든 경우의 수를 구해봅시다:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

iteration의 내부 매커니즘 이해하기

순환(Iteration)은 이터러블(iterables; __iter__() 메소드)과 이터레이터(iterators; __next__() 메소드)를 처리하는 방법입니다. 이터러블은 이터레이터 객체를 가져올 수 있는 모든 객체입니다. 이터레이터는 이터러블 객체를 순환할 수 있게 해주는 객체입니다.

이에 대한 더 자세한 설명은 Python의 for문 이해하기를 보시면 됩니다.

내용 추가

모든 이터레이터는 제너레이터와 비슷하게 한 번에 한 번만 순환될 수 있습니다. 이터레이터는 __iter()__ 메소드를 갖고 있기 때문에 실제로는 iter()를 통해 반환된 객체를 순환하는 것과 동일합니다:

>>> a = [1, 2, 3, 4, 5]
>>> iter(a)
<list_iterator object at 0x103dde668>
>>> b = iter(a)
>>> next(b)
1
>>> next(b)
2
>>> next(b)
3
>>> next(b)
4
>>> next(b)
5
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration