파이썬에서 생성기 이해


218

나는 현재 파이썬 요리 책을 읽고 있으며 현재 발전기를보고 있습니다. 머리를 돌리기가 어렵다.

Java 배경에서 왔을 때 Java에 해당하는 것이 있습니까? 이 책은 '프로듀서 / 소비자'에 대해 이야기하고 있었지만 스레딩에 대한 생각을 들었습니다.

발전기 란 무엇이며 왜 사용 하시겠습니까? 어떤 책도 인용하지 않고 분명히 (책에서 직접적이고 알맞은 대답을 찾을 수 없다면). 관대 한 느낌이 든다면 아마도 예가있을 것입니다!

답변:


402

참고 :이 게시물은 Python 3.x 구문을 가정합니다.

발전기는 단순히 당신이 호출 할 수있는 객체를 반환하는 함수입니다 next그것이 제기 할 때까지 모든 호출 것이, 어떤 값을 반환 등 StopIteration모든 값이 생성 된 것을 신호, 예외를. 이러한 객체를 반복 자라고합니다 .

일반 함수 return는 Java와 마찬가지로을 사용하여 단일 값을 반환합니다 . 그러나 파이썬에는이라는 대안이 yield있습니다. yield함수의 어느 곳에서나 사용 하면 생성기가됩니다. 이 코드를 준수하십시오.

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

당신이 볼 수 있듯이, myGen(n)산출하는 기능입니다 nn + 1. 모든 next값 을 산출 할 때까지 모든 호출 은 단일 값을 산출합니다. for루프 next는 백그라운드에서 호출 되므로 다음과 같습니다.

>>> for n in myGen(6):
...     print(n)
... 
6
7

마찬가지로 특정 일반적인 유형의 생성기를 간결하게 설명하는 수단을 제공하는 생성기 표현식 이 있습니다 .

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

생성기 표현식은 목록 이해 와 매우 비슷 합니다 .

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

생성기 객체는 한 번 생성 되지만 해당 코드는 한 번에 실행 되지 않습니다 . next실제로 코드를 실행 (일부)하는 호출 만 가능합니다 . yield명령문에 도달 하면 생성기에서 코드 실행이 중지 되고 값이 리턴됩니다. 다음에 호출 next하면 생성기가 마지막 이후에 남아있는 상태에서 실행이 계속됩니다 yield. 이는 정규 함수와의 근본적인 차이점입니다. 즉, "상단"에서 실행을 시작하고 값을 반환하면 상태를 버립니다.

이 주제에 관해 할 말이 더 있습니다. 예를 들어 send생성기로 다시 데이터를 보내는 것이 가능합니다 ( 참조 ). 그러나 그것은 발전기의 기본 개념을 이해할 때까지 살펴 보지 말 것을 제안하는 것입니다.

이제 물어볼 수 있습니다 : 왜 발전기를 사용합니까? 몇 가지 좋은 이유가 있습니다.

  • 특정 개념은 생성기를 사용하여 훨씬 간결하게 설명 할 수 있습니다.
  • 값 목록을 반환하는 함수를 만드는 대신 값을 즉시 생성하는 생성기를 작성할 수 있습니다. 이것은리스트를 구성 할 필요가 없다는 것을 의미하며, 결과 코드는 메모리 효율성이 높다는 것을 의미합니다. 이러한 방식으로 메모리에 맞추기에는 너무 큰 데이터 스트림을 설명 할 수도 있습니다.
  • 생성기는 무한 스트림 을 자연스럽게 표현할 수있는 방법을 제공 합니다. 예를 들어 피보나치 수를 고려하십시오 .

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    이 코드는 itertools.islice무한 스트림에서 유한 요소 수를 가져 오는 데 사용 됩니다. itertools고급 생성기를 작성하기위한 필수 도구이므로 모듈 의 기능을 잘 살펴 보는 것이 좋습니다 .


   Python <= 2.6 정보 : 위의 예 에서 지정된 객체 next의 메서드를 호출하는 함수입니다 __next__. 파이썬 <2.6 용도, 즉 하나는 약간 다른 기술 = o.next()대신이 next(o). Python 2.7에는 next()호출이 .next있으므로 2.7 에서 다음을 사용할 필요가 없습니다.

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
send발전기 에 데이터를 보내는 것이 가능하다고 언급했습니다 . 그렇게하면 '코 루틴'이 생깁니다. 언급 된 소비자 / 프로듀서와 같은 패턴을 코 루틴으로 구현하는 것은 매우 간단합니다. 왜냐하면 그것들은 필요가 없기 때문에 Lock교착 상태에 빠질 수 없기 때문입니다. 스레드를 강타하지 않고 코 루틴을 설명하는 것은 어렵 기 때문에 코 루틴이 스레딩의 매우 우아한 대안이라고 말할 것입니다.
Jochen Ritzel

파이썬 생성기는 기본적으로 튜링 머신이 작동하는 방식입니까?
Fiery Phoenix

48

생성기는 실제로 완료되기 전에 (데이터)를 반환하는 함수이지만 해당 시점에서 일시 중지되며 해당 시점에서 함수를 다시 시작할 수 있습니다.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

등등. 생성기 (또는 하나)의 이점은 한 번에 하나의 데이터를 처리하기 때문에 대량의 데이터를 처리 할 수 ​​있다는 것입니다. 목록을 사용하면 과도한 메모리 요구 사항이 문제가 될 수 있습니다. 목록과 마찬가지로 생성기는 반복 가능하므로 동일한 방식으로 사용할 수 있습니다.

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

예를 들어 생성기는 무한대를 처리하는 다른 방법을 제공합니다.

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

제너레이터는 무한 루프를 캡슐화하지만 요청할 때마다 각 답변을 얻으므로 문제가되지 않습니다.


30

우선, 생성기 라는 용어 는 원래 파이썬에서 다소 잘못 정의되어 많은 혼란을 초래했습니다. 아마도 반복자이터 러블을 의미 할 것입니다 ( 여기 참조 ). 그런 다음 파이썬에는 생성기 함수 (생성기 객체를 반환), 생성기 객체 (반복자) 및 생성기 표현식 (생성기 객체로 평가)이 있습니다.

제너레이터 용어집 에 따르면 이제 공식 용어는 제너레이터 가 "제너레이터 기능"의 줄임말 인 것으로 보입니다 . 과거에는 문서가 용어를 일관성없이 정의했지만 다행스럽게도이 용어는 수정되었습니다.

더 구체적으로 지정하지 않고 "발전기"라는 용어를 사용하지 않는 것이 좋습니다.


2
흠 적어도 파이썬 2.6의 몇 줄을 테스트 한 결과가 옳다고 생각합니다. 제너레이터 표현식은 제너레이터가 아닌 반복자 (일명 '제너레이터 객체')를 반환합니다.
Craig McQueen

22

제너레이터는 이터레이터를 만들기위한 속기라고 생각할 수 있습니다. 그것들은 Java Iterator처럼 동작합니다. 예:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

이것이 도움이 되길 바랍니다.

최신 정보:

다른 많은 답변이 보여주는 것처럼 생성기를 만드는 다른 방법이 있습니다. 위의 예제와 같이 괄호 구문을 사용하거나 yield를 사용할 수 있습니다. 또 다른 흥미로운 특징은 제너레이터가 "무한"할 수 있다는 것입니다.

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Java에는 Stream놀라운 번거 로움없이 다음 요소를 얻을 수 없다는 점을 제외하고는 생성기와 훨씬 유사한 s가 있습니다.
기금 모니카의 소송

12

이에 상응하는 Java는 없습니다.

다음은 약간의 고려 된 예입니다.

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

생성기에는 0에서 n까지 실행되는 루프가 있으며 루프 변수가 3의 배수이면 변수가 생성됩니다.

for루프가 반복 될 때마다 발전기가 실행됩니다. 생성기가 처음으로 실행되면 처음부터 시작하고 그렇지 않으면 이전에 생성 한 시간부터 계속됩니다.


2
마지막 단락은 매우 중요합니다. 생성기 함수의 상태는 sth를 생성 할 때마다 '냉동'이며 다음에 호출 될 때 정확히 동일한 상태로 계속됩니다.
Johannes Charra

Java에서 "제너레이터 표현식"과 동등한 구문은 없지만 생성자는 일단 일단 생성되면 본질적으로 단지 반복자입니다 (Java 반복기와 동일한 기본 특성).
09:14에 오버

@overthink : 음, 생성기는 Java 반복자가 가질 수없는 다른 부작용을 가질 수 있습니다. 내가 넣어한다면 print "hello"애프터 x=x+1내 예제에서 루프의 몸은 여전히 33 번 실행 될 때, "안녕하세요", 100 번 인쇄 할 것이다.
Wernsey

@ iWerner : Java에서도 동일한 효과가 발생할 수 있습니다. 동등한 Java 반복자에서 next ()의 구현은 여전히 ​​mygen (100) 예제를 사용하여 0에서 99까지 검색해야하므로 원할 때마다 System.out.println () 할 수 있습니다. 그래도 next ()에서 33 번만 반환합니다. Java가 부족한 것은 읽고 쓰는 것이 훨씬 쉬운 매우 편리한 yield 구문입니다.
overthink

나는이 한 줄짜리 def를 읽고 기억하는 것을 좋아했습니다. 제너레이터가 처음으로 실행되면 처음부터 시작하고 그렇지 않으면 이전에 생성 된 시간부터 계속됩니다.
이크 라.

8

나는 프로그래밍 언어와 컴퓨팅에 대한 적절한 배경을 가진 사람들에게 스택 프레임 측면에서 생성기를 설명하고 싶습니다.

많은 언어에서 스택은 현재 스택 "프레임"입니다. 스택 프레임에는 해당 함수에 전달 된 인수를 포함하여 함수에 로컬 인 변수에 할당 된 공간이 포함됩니다.

함수를 호출하면 현재 실행 지점 ( "프로그램 카운터"또는 이와 동등한 기능)이 스택으로 푸시되고 새 스택 프레임이 생성됩니다. 그런 다음 실행은 호출되는 함수의 시작 부분으로 전송됩니다.

일반 함수를 사용하면 어느 시점에서 함수가 값을 반환하고 스택이 "팝핑"됩니다. 함수의 스택 프레임이 삭제되고 이전 위치에서 실행이 재개됩니다.

함수가 제너레이터 인 경우 yield 문을 사용하여 스택 프레임을 버리지 않고 반환 할 수 있습니다 . 함수 내의 지역 변수 값과 프로그램 카운터는 유지됩니다. 따라서 yield 문에서 실행을 계속하면서 생성기를 나중에 다시 시작할 수 있으며 더 많은 코드를 실행하고 다른 값을 반환 할 수 있습니다.

Python 2.5 이전에는 모든 생성자가 수행했습니다. 파이썬 2.5 다시 값을 전달하는 기능이 추가 발생기로도있다. 이렇게하면 전달 된 값은 생성기에서 제어 (및 값)를 일시적으로 리턴 한 yield 문에서 발생하는 표현식으로 사용할 수 있습니다.

생성기의 주요 장점은 스택 프레임을 폐기 할 때마다 일반 상태와 달리 함수의 "상태"가 유지된다는 것입니다. 두 번째 이점은 일부 기능 호출 오버 헤드 (스택 프레임 생성 및 삭제)를 피할 수 있다는 것입니다.


6

Stephan202의 답변에 추가 할 수있는 유일한 것은 David Beazley의 PyCon '08 프레젠테이션 "시스템 프로그래머를위한 생성자 트릭"을 보도록 권장하는 것입니다. 어딘가에. 이것은 "Python looks fun"에서 "이것이 내가 찾던 것"으로 나를 데려 간 것입니다. 그것은에서의 http://www.dabeaz.com/generators/ .


6

함수 foo와 생성자 foo (n)을 명확하게 구분하는 데 도움이됩니다.

def foo(n):
    yield n
    yield n+1

foo는 함수입니다. foo (6)는 생성기 객체입니다.

생성기 객체를 사용하는 일반적인 방법은 루프입니다.

for n in foo(6):
    print(n)

루프 인쇄

# 6
# 7

생성기를 재개 가능한 기능으로 생각하십시오.

yieldreturn생성 된 값이 생성자에 의해 "반환"된다는 의미에서 작동 합니다. 그러나 반환 값과 달리 다음에 생성기가 값을 요청하면 생성기의 함수 foo는 마지막 yield 문 이후에 중단 된 위치에서 다시 시작하고 다른 yield 문에 도달 할 때까지 계속 실행됩니다.

장면 뒤에서 bar=foo(6)생성자 를 호출 할 때 next속성 을 갖도록 정의됩니다 .

foo에서 얻은 값을 검색하기 위해 직접 호출 할 수 있습니다.

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

foo가 끝나고 (더 이상 산출 된 값이없는 경우) 호출 next(bar)하면 StopInteration 오류가 발생합니다.


5

이 포스트는 피보나치 수파이썬 생성기 의 유용성을 설명하는 도구로 사용할 것 입니다.

이 포스트는 C ++과 Python 코드를 다룰 것입니다.

피보나치 수는 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 등의 순서로 정의됩니다.

또는 일반적으로 :

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

이것은 C ++ 함수로 매우 쉽게 옮길 수 있습니다 :

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

그러나 처음 6 개의 피보나치 수를 인쇄하려면 위 함수를 사용하여 많은 값을 다시 계산해야합니다.

예를 들어 Fib(3) = Fib(2) + Fib(1),하지만 Fib(2)또한 다시 계산합니다 Fib(1). 계산하려는 값이 높을수록 더 나빠질 수 있습니다.

따라서의 상태를 추적하여 위의 내용을 다시 쓰려고 할 수 있습니다 main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

그러나 이것은 매우 추악하며의 논리가 복잡합니다 main. main함수의 상태에 대해 걱정할 필요가없는 것이 좋습니다 .

vector값을 반환하고 값을 사용하여 iterator해당 값 집합을 반복 할 수 있지만 많은 반환 값을 얻으려면 한 번에 많은 메모리가 필요합니다.

예전의 접근 방식으로 돌아가서 숫자 인쇄 외에 다른 작업을 수행하려면 어떻게됩니까? 전체 코드 블록을 복사하여 붙여 main넣고 출력 문을 원하는 다른 것으로 변경해야합니다. 코드를 복사하여 붙여 넣으면 촬영해야합니다. 당신은 총에 맞고 싶지 않습니까?

이러한 문제를 해결하고 문제를 해결하기 위해 콜백 함수를 사용하여이 코드 블록을 다시 작성할 수 있습니다. 새로운 피보나치 번호가 발견 될 때마다 콜백 함수를 호출합니다.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

이것은 분명히 개선 된 것이며, 논리 main는 복잡하지 않으며 피보나치 번호로 원하는 것을 수행하고 단순히 새로운 콜백을 정의 할 수 있습니다.

그러나 이것은 여전히 ​​완벽하지 않습니다. 처음 두 피보나치 수를 얻은 다음 무언가를 한 다음 더 많이 얻은 다음 다른 것을 원한다면 어떨까요?

글쎄, 우리는 예전처럼 갈 main수 있었고, GetFibNumbers가 임의의 지점에서 시작할 수 있도록 상태를 다시 추가 할 수 있습니다. 그러나 이것은 우리 코드를 더욱 부 풀릴 것이며 이미 피보나치 숫자 인쇄와 같은 간단한 작업에는 너무 커 보입니다.

몇 개의 스레드를 통해 생산자와 소비자 모델을 구현할 수 있습니다. 그러나 이것은 코드를 더욱 복잡하게 만듭니다.

대신 발전기에 대해 이야기합시다.

파이썬에는 생성기라고하는 이러한 문제를 해결하는 매우 유용한 언어 기능이 있습니다.

제너레이터를 사용하면 기능을 실행하고 임의의 지점에서 정지 한 다음 중단 한 지점에서 다시 계속할 수 있습니다. 매번 값을 반환합니다.

생성기를 사용하는 다음 코드를 고려하십시오.

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

결과는 다음과 같습니다.

0 1 2 3 5

yield문장은 파이썬 생성기와 함께 사용됩니다. 함수의 상태를 저장하고 yeilded 값을 반환합니다. 다음에 생성기에서 next () 함수를 호출하면 수율이 중단 된 곳에서 계속됩니다.

이것은 콜백 함수 코드보다 훨씬 깨끗합니다. 우리는 더 깨끗한 코드, 더 작은 코드를 가지고 있으며 훨씬 더 많은 기능적 코드를 언급하지 않습니다 (Python은 임의로 큰 정수를 허용합니다).

출처


3

반복자와 생성기의 첫 등장은 약 20 년 전에 Icon 프로그래밍 언어로되어 있다고 생각합니다.

당신은 즐길 수 있습니다 아이콘 개요 는 (아이콘 당신은 아마 모르는 언어이며, 그리스 울드는 다른 언어에서 오는 사람들에게 자신의 언어의 장점을 설명 이후) 구문에 집중하지 않고 그들 주위에 당신의 머리를 정리 할 수 있습니다.

몇 단락을 읽은 후 생성기 및 반복기의 유틸리티가 더 분명해질 수 있습니다.


2

목록 이해력에 대한 경험은 파이썬 전체에서 광범위한 유틸리티를 보여주었습니다. 그러나 많은 유스 케이스에는 메모리에 작성된 전체 목록이 필요하지 않습니다. 대신, 한 번에 하나씩 요소를 반복하면됩니다.

예를 들어, 다음 합계 코드는 메모리에 전체 제곱 목록을 작성하고 해당 값을 반복하며 참조가 더 이상 필요하지 않으면 목록을 삭제합니다.

sum([x*x for x in range(10)])

대신 생성기 표현식을 사용하여 메모리를 보존합니다.

sum(x*x for x in range(10))

컨테이너 객체의 생성자에도 비슷한 이점이 있습니다.

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

생성기 표현식은 반복 가능한 입력을 단일 값으로 줄이는 sum (), min () 및 max ()와 같은 함수에 특히 유용합니다.

max(len(line)  for line in file  if line.strip())


1

생성기에 대한 세 가지 주요 개념을 설명하는이 코드를 작성했습니다.

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.