이 글은 "파이썬 코딩의 기술"(by 브렛 슬라킨)을 읽고 정리를 위해 작성했습니다.
일단 yield
를 사용하기 위해서는 제너레이터에 대해 알아야 한다.
제너레이터는 함수가 점진적으로 반환하는 값으로 이뤄지는 스트림을 만들어 준다.
[예시1] 리스트 사용
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == '':
result.append(index + 1)
return result
[예시2] 제너레이터 사용
def index_words_iter(text):
result = []
if text:
yield 0
for index, letter in enumerate(text):
if letter == '':
yield index + 1
예시1의 메서드 호출의 경우 덩어리가 너무 크다(result.append)는 단점이 있다.
- 이 때문에 리스트에 추가될 값(index + 1)의 중요성이 희석 된다.
이를 개선하는 방법은 예시2에서와 같이 제너레이터를 사용하는 것이다.
- 제너레이터는 yield식을 사용하는 함수를 통해 만들 수 있다.
- 제너레이터를 생성할 경우 return을 하지 않고 yield만 한다.
예시2에서 index_words_iter
를 호출하면 제너레이터 함수가 실제로 실행되지는 않으며 이터레이터를 반환한다.
- 이터레이터가 next 내장 함수를 호출할 때마다 이터레이터는 제너레이터 함수를 다음 yield 식까지 진행 시킨다.
- 아래의 예시를 보면 알 수 있다.
it = index_words_iter(address)
print(next(it))
print(next(it))
>>>
0 # 첫 번째 yield 0
8 # for문 안의 두 번째 yield 8
예시2의 경우 반환하는 리스트와 상호작용 하는 코드가 사라져 훨씬 가독성이 높아졌다.
- 단, 결과는 yield 식에 의해 전달 된다.
예시2에서 제너레이터가 반환하는 이터레이터를 리스트 내장 함수에 넘기면 필요할 때 제너레이터를 쉽게 리스트로 변환할 수 있다.
result = list(index_words_inter(address))
print(result[:10])
index_words
의 두 번째 문제점은 반환하기 전에 리스트에 모든 결과를 다 저장해야 한다는 것이다.index_file
의 작업 메모리는 입력 중 가장 긴 줄의 길이로 제한된다. def index_file(handle):
offset = 0
for line in handle:
if line:
yield offset
for letter in line:
offset += 1
if letter == ' ':
yield offset
def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
percent = 100 * value / total
result.append(percent)
return result
위 메서드는 도시별 방문자 수(단위: 100만 명 / 년) 데이터 집합이 주어질 때 각 도시가 전체 여행자 수 중에서 차지하는 비율을 계산하기 위한 것이다.
아래에서 위 코드의 규모 확장성을 높이기 위한 방법을 알아본다.
인자가 파일로 전달될 수도 있기 때문에 모든 도시에 대한 여행자 정보가 들어 있는 파일에서 데이터를 읽을 수 있는 함수를 만들어야 한다. 또한 파일을 읽는 함수를 재사용해야 할 수도 있으므로 제너레이터를 정의한다.
def read_visits(data_path):
with open(data_path) as f:
for line in f:
yield int(line)
그 다음 아래와 같이 위에서 정의했던 normalize
함수에 read_visits
의 반환 값을 전달하면 아무 결과도 나오지 않는다.
it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)
>>>
[]
이유는 다음과 같다.
print(list(it))
를 한 번 실행헤 StopIteration 예외가 발생한 뒤에 또 다시 print할 경우 빈 리스트가 출력 된다.it = read_visits('my_numbers.txt')
print(list(it))
print(list(it))
>>>
[15, 35, 80]
[]
list()
에 넣어 명시적으로 소진시키는 방법이 있다. def normalize_copy(numbers):
numbers_copy = list(numbers)
total = sum(numbers_copy)
result = []
for value in numbers_copy:
percent = 100 * value / total
result.append(percent)
return result
def normalize_func(get_iter):
total = sum(get_iter())
result = []
for value in get_iter():
percent = 100 * value / total
result.append(percent)
return result
path = 'my_numbers.txt'
percentages = normalize_func(lambda: read_visits(path))
print(percentages)
assert sum(percentages) == 100.0
방법2보다 더 나은 방법은 이터레이터 프로토콜을 구현한 새로운 컨테이너 클래스를 제공하는 것이다.
for x in foo
와 같은 구문을 사용하면 내부적으로는 iter(foo)를 호출한다.iter
내장함수는 foo.__iter__
라는 특별 메서드를 호출하며 해당 메서드는 반드시 이터레이터 객체를 반환해야 한다.next
내장 함수를 호출한다.__iter__
메서드를 제너레이터로 구현하기만 하면 된다.normalize
함수에 넘기면 함수가 잘 작동한다.class ReadVisits:
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
# 이터레이터를 구현한다.
with open(self.data_apth) as f:
for line in f:
yield int(line)
visits = ReadVisits()
percentages = normalize(visits)
print(percentages)
assert sum(percentages) == 100.0
>>>
[11.XXXXX, 26.XXXXX, 61.XXXXX]
normalize
함수가 visits
를 호출할 경우 ReadVisits.__iter__
를 호출해 새로운 이터레이터 객체를 할당하게 된다.__iter__
를 호출해 두 번째 이터레이터 객체를 만든다.sum(numbers)
를 위해 한 번, for 루프
를 위해 한 번 호출되어 이터레이터는 총 두 번 호출됨).이 접근 방식의 유일한 단점은 입력 데이터를 여러번 읽는다는 것이다.
주의할 점
따라서, 방어 로직을 작성해 반복적으로 이터레이션 할 수 없는 인자인 경우에는 TypeError
를 발생시켜 인자를 거부할 수 있다.
[타입 검사 방법1]: iter()
이용
def normalize_defensive(numbers):
if iter(numbers) is numbers: # iter()로 감쌌으나 그대로 이터레이터(객체)인 경우
raise TypeError('컨테이너를 제공해야 합니다!')
total = sum(numbers)
result = []
for value in get_iter():
percent = 100 * value / total
result.append(percent)
return result
[타입 검사 방법2]: isinstance()
이용
from colletions.abc import Iterator
def normalize_defensive(numbers):
if isinstance(numbers, Iterator):
raise TypeError('컨테이너를 제공해야 합니다!')
total = sum(numbers)
result = []
for value in get_iter():
percent = 100 * value / total
result.append(percent)
return result
이러한 방식으로 컨테이너를 사용하는 것은 앞에서 사용한 normalize_copy
함수처럼 전체 입력 이터레이터를 복사하고 싶지 않을 때 유용하다.
하지만, 입력 데이터를 여러 번 이터레이션 해야 한다는 단점이 있다.
컨테이너란?
컨테이너는 무언가를 담는 '그릇' 정도로 생각하면 된다. 파이썬에서는 리스트, 딕셔너리, 세트, 튜플, 문자열 이 이에 해당한다.
이터레이터란?
파이썬에서 이터레이터는 여러개의 요소를 가지는 컨테이너(리스트, 튜플, 셋, 사전, 문자열)에서 각 요소를 하나씩 꺼내 어떤 처리를 수행하는 간편한 방법을 제공하는 객체이다. 즉, 반복문을 활용해 데이터를 순회하면서 처리하는 것을 말한다.
for element in [1,2,3]:
print(element)
iter()
메소드를 호출해서 이터레이터 객체를 구한다.__next__()
를 호출한다.StopIteration
예외를 발생시킨다.next()
내장함수를 사용할 수 있다.참고
이터레이터
"파이썬 코딩의 기술"(by 브렛 슬라킨)