이 글은 2019년 6월에 제가 어떤 네이버 카페에 적었던 글을 옮겨 적은 것입니다.
파이썬으로 코딩을 하다보면 리스트를 자주 사용합니다.
xs = [1, 2, 3]
리스트의 길이가 크지 않다면 상관 없지만 리스트 길이가 너무 클 때는 문제가 될 수 있습니다. 메모리를 많이 차지할 수도 있고 계산하는데 시간이 오래걸릴 수도 있습니다.
그렇다면 리스트의 내용을 처음부터 끝까지 다 다루는 것이 아니라 진짜 필요할때 처음부터 하나씩만 꺼내서 처리한다면 어떨까요?
이게 가능하다면 리스트의 길이가 크다고 해서 걱정하지 않아도 됩니다.
게다가 길이가 큰 리스트 뿐만 아니라 길이가 무한대인 리스트도 다룰 수 있게 됩니다!
파이썬에는 yield
라는 키워드가 있습니다.
return
과 기능이 비슷합니다. 둘 다 함수 안에서 쓰이고 값을 돌려줍니다.
return
은 리턴하는 순간 함수가 종료되고 함수 안에서 사용했던 지역 변수들은 버립니다.
yield
는 사용하는 순간 함수가 종료되고 값을 돌려주는 것은 같지만 함수 안에서 사용했던 지역 변수들을 버리지 않고 기억해둡니다.
아래 코드는 yield
를 이용해서 무한 리스트를 만드는 코드입니다.
def infinite(n):
while True:
yield n
n += 1
함수 infinite
는 인자 하나를 받습니다.
while True
를 이용해서 무한 루프를 돕니다. 무한 루프이긴 하지만 다음 줄에 있는 yield
를 만나는 순간 인자로 받았던 n
을 리턴하고 함수가 끝납니다.
그러나 여기에서는 return
이 아니라 yield
를 사용했기 때문에 함수가 정말로 완전히 종료된 것은 아니고 종료되기 전의 상태는 저장되어 있습니다.
아래 코드와 같이 next
함수를 이용하면 무한 리스트의 원소를 앞에서부터 하나씩만 꺼낼 수 있습니다.
def infinite(n):
while True:
yield n
n += 1
xs = infinite(2) # [2, 3, 4, ...]
next(xs) # 2
next(xs) # 3
첫번째로 무한 리스트에 next
함수를 사용하면 첫번째 숫자인 2
가 나옵니다.
두번째로 next
함수를 사용하면 infinite
함수가 바로 전에 yield
를 사용하기 전 상태를 기억하고 있다가 이어서 실행합니다.
여기에서는 n
에 1
을 더해서 2
가 3
이 되고
무한 루프 때문에 코드는 다시 yield
를 만나게 되고 이제서야 3
을 리턴합니다. 이때 리턴하긴 하지만 지금까지 상태를 버리진 않고 계속 기억합니다.
이런 식으로 yield
키워드를 이용하면 무한 리스트를 만들 수 있습니다.
파이썬에서는 이러한 형태의 리스트를 제네레이터 Generator 라고 부릅니다.
프로그래밍 언어에서는 어려운 말로 게으른 평가 Lazy Evaluation 라고 하기도 합니다.
하스켈 같은 함수형 언어에서는 이 게으른 평가가 기본으로 설정되어 있습니다.
여기서부터는 위 글을 쓴지 1년 정도 지난 시점에서 쓰는 것입니다.
당시에는 제가 yield
라는 키워드를 겉으로 동작하는 모습만 보고 이해한 것을 적었습니다.
그런데 지금 다시 생각해보면 파이썬이 yield
를 내부에서 어떻게 구현했는지는 잘 모르겠습니다.
통상 함수의 호출과 반환은 스택 메모리로 구현되는데 그렇다면 제네레이터는 스택 영역이 아니라 힙 영역에 생성되는 것인지 궁금하긴 하네요.