고성능 파이썬(6)

CHAPTER 5. 이터레이터와 제네레이터

 

def range(start, stop, step=1):
  numbers = []
  while start < stop:
    numbers.append(start)
    start ++ step
  return numbers
  
def xrange(start, stop, step=1):
  while start < stop:
    yield start
    start += step


for i in range(1, 10000):
  pass

for i in range(1, 10000):
  pass

 

코드가 yield를 실행하는 순간 이 함수는 그 값을 방출하고, 다른 값 요청이 들어오면 이전 상태를 유지한 채로 실행을 재개하여 새로운 값을 방출한다. 함수가 끝나면 StopIteration 예외를 발생시켜 생산할 값이 더는 남아 있지 않음을 알린다.

 

# 다음 코드는
for i in object:
  do_work(i)

# 아래의 코드와 동일하다.
object_iterator = iter(object)
while True:
  try:
    i = object_iterator.next()
    do_work(i)
  except StopIteration:
    break

대부분의 객체에서 이터레이터를 생성하려면 그저 파이썬의 내장 함수인 iter를 사용하면 된다. 이 함수는 리스트, 튜플, 사전, 셋에서 사용할 수 있으며 객체의 값이나 키에 대한 이터레이터를 반환한다. 이터레이터를 생성하고 나면 그 이터레이터에 next()를 호출하는 것만으로 StopIteration 예외가 발생할 때까지 새로운 값을 얻어올 수 있다.

 

list comprehension은 [<값> for <항목> in <배열> if <조건>] 문법으로 생성할 수 있다.

list comprehension for dictionary: {str(i):i for i in [1,2,3,4,5]}

list comprehension if else: <참일때의 값> if <조건> else <거짓일때의 값>

제너레이터 (<값> for <항목> in <배열> if <조건>)

제너레이터는 length 속성이 없어서, divisible_by_three = sum((1 for n in list_of_numbers if n % 3 == 0)) 다음과 같이 길이를 구할 수 있다.

 

5.1. 무한급수와 이터레이터

지나온 상태 중 일부만 저장해두고 현재 값만 출력하면 되는 상황이라면 제너레이터는 무한급수(infinite series)를 위한 이상적인 해법이다.

def fibonacci():
  i, j = 0, 1
  while True:
    yield j
    i, j = j, i + j

피보나치 수열의 제너레이터 버젼

 

제너레이터는 코드의 흐름을 감추므로 너무 남발하는 것은 좋지 않다. 즉, 제너레이터는 실제론 코드를 정돈하고 매끈한 루프를 작성하기 위한 수단이다.

 

5.2 제너레이터의 지연 실행

 

앞에서 잠깐 언급했듯이, 메모리 사용 측면에서 제너레이터가 유리한 경우는 현재 값만 필요한 경우다. 제너레이터를 사용한 피보나치 수열 계산에서도 항시 현재 값만 사용할 뿐 수열 앞쪽에 나온 다른 값을 참조할 수는 없다(이런 알고리즘을 '단일 패스(single pass)' 또는 '온라인(online)' 이라고 한다). 이런 특징 때문에 가끔 제너레이터를 사용하기 어려운 경우가 있는데, 그럴 때 도움이 되는 모듈이나 함수가 많이 있다.

 

그 중 표준 라이브러리인 itertools가 가장 대표적이다. 대표적인 유용한 함수는 다음과 같다.

 

islice: 제너레이터에 대한 슬라이싱 기능을 제공한다.

chain: 여러 제너레이터를 연결할 수 있다.

takewhile: 제너레이터 종료 조건을 추가할 수 있다.

cycle: 유한한 제너레이터를 계속 반복하게 하여 무한 제너레이터로 동작하도록 한다.

 

 

대용량 데이터 분석에 제너레이터를 사용하는 예제를 작성해보자. 초 단위로 기록된 20년 치의 데이터를 분석한다고 하면 처리해야 할 데이터의 수는 631,152,000개다! 파일에 저장되어 있고 초 단위로 한 줄씩 기록되어 있다. 이 전부를 모두 메모리에 올릴 수는 없는 상황이다. 데이터에서 간단한 특이점을 찾아야 한다고 가정하면 리스트 할당 없이 제너레이터만으로 해결할 수 있다!

 

# 필요할 때만 데이터 읽기
# 데이터 파일은 "타임스탬프, 값"으로 구성되어 있음

from random import normalvariate, rand
from itertools import count

def read_data(filename):
  with open(filename) as fd:
    for line in fd:
      data = line.strip().split(',')
      yield list(map(int, data))

def read_fake_data(filename):
  for i in count():
    sigma = rand()*10
    yield (i, normalvariate(0, sigma))

 

*groupby: 입력이 'A A A A B B A A'이고 groupby로 글자별로 묶으면 (A, [A, A, A, A]), (B, [B, B]), (A, [A, A]) 이렇게 세 그룹이 생긴다.

# 데이터 그룹 만들기
from datetime import date
from itertools import groupby

def day_grouper(iterable):
  key = lambda (timestamp, value) : date.fromtimestamp(timestamp)
  return groupby(iterable, key)

 

# 제너레이터 기반의 특이점 찾기
import math

def check_anomaly(day, day_data)):
  # 해당 날짜의 평균, 표준편차, 최댓값을 구한다.
  # 해당 날짜의 데이터를 한 번만 읽어서 계산할 수 있도록
  # 단일 패스 평균/표준편차 알고리즘을 사용한다.
  n = 0
  mean = 0
  M2 = 0
  max_value = None
  for timestamp, value in day_data:
    n += 1
    delta = value - mean
    mean = mean + delta/n
    M2 += delta*(value - mean)
    if max_value:
      max_value = max(max_value, value)
    else:
      max_value = value
  variance = M2/(n-1)
  standard_deviation = math.sqrt(variance)
  
  # 다음은 실제로 해당 날짜의 데이터가 특이점이라면 해당 날짜를 반환하고
  # 그렇지 않다면 False 반환한다.
  
  if max_value > mean + 3*standard_deviation:
    return day
  return False
# 모두 함께 연결하기
data = read_data(data_filename)
data_day = day_grouper(data)
anomalous_dates = filter(None, map(check_anomaly, data_day)) # filter는 조건을 충족하지 못하는 항목을 제거한다.
# 기본값으로 False인 항목을 제거한다.
first_anomalous_date, first_anomalous_data = anomalous_dates.next()
print("The first anomalous date is: ", first_anomalous_date)

 

'책읽기' 카테고리의 다른 글

고성능 파이썬(7) - 6장. 행렬과 벡터 연산  (0) 2019.04.18
고성능 파이썬(5)  (0) 2019.04.18
고성능 파이썬(4)  (0) 2019.04.16
고성능 파이썬(3)  (0) 2019.04.16
고성능 파이썬(2)  (0) 2019.04.16

댓글

Designed by JB FACTORY