Menu

PHP 제너레이터: yield, yield from, 메모리 절약

제너레이터는 yield로 값을 하나씩 돌려주는 함수라서, foreach가 배열을 만들지 않고도 백만 개의 항목을 반복할 수 있습니다. yield, 키, 메모리 절약, 무한 시퀀스, yield from, send(), getReturn()을 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

제너레이터는 yield가 들어 있는 함수입니다. 호출해도 본문이 실행되지 않고, foreach로 반복할 수 있는 Generator 객체를 반환합니다. 각 yield $value는 반복문에 값 하나를 넘기고 반복문이 다음 값을 요청할 때까지 함수를 멈추므로, 값을 먼저 배열에 모으는 대신 한 번에 하나씩 만듭니다.

출력은 제너레이터와 반복문을 번갈아 오갑니다. 각 값은 반복문이 요청할 때만 만들어집니다.

제너레이터는 메모리를 아낀다

range(1, 1000000)은 반복이 시작되기 전에 정수 백만 개의 배열을 만듭니다. 같은 숫자를 yield하는 제너레이터는 한 번에 하나 이상을 담지 않습니다. 실행해서 각 버전에 필요한 메모리를 비교해 보세요.

두 반복문 모두 같은 합계 500000500000을 출력하지만, 배열 버전은 extra memory 16.0 MB를, 제너레이터는 extra memory 0.0 MB를 보고합니다. 데이터가 커질수록 차이도 커집니다. 같은 아이디어 덕분에 제너레이터는 큰 파일, 데이터베이스 결과, API 페이지에 유용합니다. 항목 하나를 처리하고, 버리고, 다음 것을 가져옵니다.

키와 값 yield하기

yield $key => $value는 foreach ($gen as $key => $value)가 받는 키를 정합니다. 키가 없으면 제너레이터는 리스트처럼 값에 0부터 번호를 매깁니다. 흔한 용도는 줄 번호와 함께 파일을 한 줄씩 읽는 것입니다.

한 번에 한 줄만 메모리에 있으므로 같은 함수로 2GB 로그 파일도 처리합니다. 여기서 쓴 파일 함수는 fopen()과 fgets()에서 다룹니다.

무한 시퀀스와 지연 파이프라인

제너레이터는 소비하는 쪽이 요청하는 만큼만 실행되므로 영원히 반복할 수 있습니다. break로 멈추거나, 처음 몇 개 값만 가져오는 다른 제너레이터로 감싸세요. 작은 제너레이터를 이어 붙이면 모든 단계가 지연 실행되는 파이프라인이 됩니다.

fibonacci()는 스스로 끝나지 않지만 프로그램은 끝납니다. take()가 요청한 개수 이후로는 묻지 않으므로 그 너머는 아무것도 계산되지 않습니다.

yield from: 다른 제너레이터에 위임하기

yield from은 배열, 이터레이터, 다른 제너레이터의 모든 값을 그대로 넘깁니다. 제너레이터를 작은 제너레이터로 나누거나 중첩 구조를 재귀적으로 순회하는 방법입니다.

yield from은 안쪽의 키를 유지하므로 두 배열 모두 키 0과 1을 yield합니다. 그러면 iterator_to_array()가 a와 b를 덮어씁니다. 값만 중요할 때는 두 번째 인수로 false를 넘기세요.

send()와 getReturn()

제너레이터는 값을 받을 수도 있습니다. $x = yield;는 멈춰서 기다리고, $gen->send($value)는 $value를 그 yield의 결과로 삼아 제너레이터를 재개합니다. 그리고 제너레이터는 최종 값을 return할 수 있으며, 끝난 뒤 getReturn()으로 읽습니다.

대부분의 코드는 send()가 필요 없습니다. 일상적인 용도는 foreach로 지연 읽기를 하는 것입니다. send()는 코루틴 라이브러리의 기반입니다.

제너레이터는 한 번만 반복할 수 있다

제너레이터는 앞으로만 실행되며 되감을 수 없습니다. foreach가 한 번 소비하고 나면 같은 객체를 다시 반복할 때 예외가 납니다.

해결 방법은 반복할 때마다 제너레이터 함수를 다시 호출하거나(foreach (colors() as $c)), 값이 정말 두 번 필요할 때 iterator_to_array(colors(), false)로 한 번 모으는 것입니다. 이런 일이 자주 생긴다면 그 데이터에는 평범한 배열이 맞는 선택이었습니다.

자주 묻는 질문

PHP에서 yield는 무엇을 하나요?

yield $value는 함수를 반복하는 코드에 값 하나를 넘기고 함수를 그 자리에서 멈춥니다. 다음 반복이 yield 뒤에서 함수를 재개합니다. yield가 들어 있는 함수는 호출해도 실행되지 않고 Generator 객체를 반환합니다.

배열 대신 제너레이터를 쓰는 이유는 무엇인가요?

배열은 모든 요소를 한꺼번에 메모리에 담고, 제너레이터는 요소를 한 번에 하나씩 만듭니다. range(1, 1000000)을 반복하면 백만 개의 정수를 모두 할당하지만, 그것을 yield하는 제너레이터는 메모리를 일정하게 유지합니다. 제너레이터는 무한 시퀀스도 가능하고, 반복문이 break하면 작업을 일찍 멈춥니다.

PHP의 yield from은 무엇인가요?

yield from $source는 다른 제너레이터, 배열, Traversable의 모든 값을 현재 제너레이터가 직접 yield한 것처럼 넘깁니다. 안쪽 제너레이터의 반환값으로 평가되기도 합니다.

제너레이터를 두 번 반복할 수 있나요?

아니요. 제너레이터는 앞으로만 실행됩니다. foreach가 끝난 뒤 다시 반복하면 Exception: Cannot traverse an already closed generator를 던집니다. 새 제너레이터를 얻으려면 제너레이터 함수를 다시 호출하거나, 값이 두 번 필요하다면 iterator_to_array()로 저장하세요.

제너레이터를 배열로 바꾸려면 어떻게 하나요?

iterator_to_array($gen)은 제너레이터의 키를 유지하면서 모든 값을 모읍니다. 예를 들어 yield from 뒤에 키가 반복되면 나중 값이 앞의 값을 덮어쓰므로, 평범한 리스트를 얻으려면 두 번째 인수로 false를 넘기세요: iterator_to_array($gen, false).

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기