>  기사  >  백엔드 개발  >  Python의 수율 및 생성기에 대한 심층 분석

Python의 수율 및 생성기에 대한 심층 분석

巴扎黑
巴扎黑원래의
2017-08-16 13:13:431660검색

Generator 및 Yield 키워드는 Python에서 가장 강력하고 이해하기 어려운 개념 중 하나일 수 있지만(아마 없음) Yield가 Python에서 가장 강력한 키워드가 되는 것을 방해하지는 않으며, 실제로 초보자에게는 이해하기 매우 어렵습니다. Yield에 대한 빠른 이해를 돕기 위해 Yield에 관해 해외 전문가가 쓴 글을 함께 살펴보겠습니다. 글이 좀 길므로 인내심을 갖고 끝까지 읽어주시기 바랍니다.

Generator

생성기는 하나 이상의 항복 표현식으로 구성된 함수입니다. 각 생성기는 반복자입니다(그러나 반복자가 반드시 생성자는 아닙니다).

함수에 항복 키워드가 포함되어 있으면 해당 함수는 생성기가 됩니다.

생성기는 모든 결과를 한 번에 반환하지 않지만, Yield 키워드를 만날 때마다 해당 결과를 반환하고, 함수의 현재 실행 상태를 유지하면서 다음 호출을 기다립니다.

생성기도 반복자이므로 다음 값을 얻으려면 다음 메서드를 지원해야 합니다. (Python2의 .next()인 .__next__() 속성을 사용할 수도 있습니다.)

코루틴과 서브루틴

일반 Python 함수를 호출할 때 일반적으로 함수 코드의 첫 번째 줄부터 시작합니다. 실행은 return 문, 예외 또는 함수 종료로 끝납니다(None의 암시적 반환으로 간주될 수 있음). 함수가 호출자에게 제어권을 반환하면 모든 것이 끝납니다. 함수에서 수행된 모든 작업과 로컬 변수에 저장된 데이터가 손실됩니다. 이 함수를 다시 호출하면 모든 것이 처음부터 생성됩니다.

이것은 컴퓨터 프로그래밍에서 논의되는 기능에 대한 매우 표준적인 절차입니다. 이러한 함수는 단일 값만 반환할 수 있지만 때로는 시퀀스를 생성하는 함수를 만드는 것이 도움이 될 수 있습니다. 이를 위해서는 해당 함수가 "자체 작업을 저장"할 수 있어야 합니다.

"시퀀스 생성"이 가능하다는 것은 우리 함수가 일반적인 의미에서 반환되지 않기 때문이라고 말했습니다. return의 암묵적인 의미는 함수가 실행된 코드의 제어를 함수가 호출된 위치로 반환한다는 것입니다. "수율"의 암묵적인 의미는 통제권 이전이 일시적이고 자발적이며 우리 기능이 미래에 통제권을 되찾을 것이라는 것입니다.

파이썬에서는 이런 능력을 지닌 "함수"를 제너레이터라고 부르는데, 매우 유용합니다. 생성기(및 Yield 문)는 원래 프로그래머가 값 시퀀스를 생성하는 코드를 더 쉽게 작성할 수 있도록 도입되었습니다. 이전에는 난수 생성기와 같은 것을 구현하려면 각 호출 사이의 상태를 추적하면서 데이터를 생성하는 클래스나 모듈을 구현해야 했습니다. 발전기를 도입하면 이는 매우 쉬워집니다.

생성기가 해결한 문제를 더 잘 이해하기 위해 예를 살펴보겠습니다. 이 예제를 진행하면서 해결해야 할 문제, 즉 일련의 값을 생성하는 문제를 항상 염두에 두십시오.

참고: Python 외부에서 가장 간단한 생성기는 코루틴이라고 합니다. 이번 글에서는 이 용어를 사용하겠습니다. Python의 개념에서 여기에 언급된 코루틴은 생성기라는 점을 기억하세요. Python의 공식 용어는 생성기입니다. 코루틴은 단지 논의를 위한 것이며 언어 수준에서 공식적인 정의가 없습니다.

예: 흥미로운 소수

상사가 함수 작성을 요청하고 입력 매개변수가 정수 목록이고 소수 1을 포함하는 반복 가능한 결과를 반환한다고 가정해 보겠습니다.

Iterable은 매번 특정 멤버를 반환하는 개체의 기능일 뿐이라는 점을 기억하세요.

"이건 매우 간단하다"고 생각하고 빠르게 다음 코드를 작성해야 합니다.

def get_primes(input_list):
    result_list = list()
    for element in input_list:
        if is_prime(element):
            result_list.append()
    return result_list
# 或者更好一些的...
def get_primes(input_list):
    return (element for element in input_list if is_prime(element))
# 下面是 is_prime 的一种实现...
def is_prime(number):
    if number > 1:
        if number == 2:
            return True
        if number % 2 == 0:
            return False
        for current in range(3, int(math.sqrt(number) + 1), 2):
            if number % current == 0: 
                return False
        return True
    return False

위의 is_prime 구현은 요구 사항을 완전히 충족하므로 상사에게 완료되었다고 알립니다. 그녀는 우리의 기능이 잘 작동했고 정확히 그녀가 원하는 기능이었다고 보고했습니다.

무한 시퀀스 처리

아, 정말요? 며칠 후 상사가 와서 몇 가지 작은 문제에 직면했다고 말했습니다. 그녀는 숫자가 포함된 큰 목록에 get_primes 함수를 사용할 계획이었습니다. 실제로 이 목록은 너무 커서 목록을 만드는 것만으로도 시스템 메모리를 모두 소모하게 됩니다. 이를 위해 그녀는 시작 매개변수를 사용하여 get_primes 함수를 호출하고 이 매개변수보다 큰 모든 소수를 반환하기를 원합니다(아마도 그녀는 프로젝트 오일러 문제 10을 해결하고 싶을 것입니다).

이 새로운 요구사항을 살펴보겠습니다. 단순히 get_primes를 수정하는 것은 불가능하다는 것이 분명합니다. 당연히 처음부터 무한대까지의 모든 소수를 포함하는 목록을 반환하는 것은 불가능합니다(무한 수열을 조작하는 데 유용한 응용 프로그램이 많이 있지만). 이 문제를 해결하기 위해 일반적인 기능을 사용할 가능성은 상대적으로 희박해 보입니다.

포기하기 전에 상사의 새로운 요구 사항을 충족하는 함수를 작성하는 데 방해가 되는 핵심 장애물을 파악해 보겠습니다. 고민 끝에 우리는 함수가 결과를 반환할 수 있는 기회는 단 한 번뿐이므로 모든 결과를 한 번에 반환해야 한다는 결론에 도달했습니다. "함수가 작동하는 방식이 아닌가?"라는 결론을 내리는 것은 무의미해 보입니다. 하지만 배우지 않으면 성공할 수 없고, “이렇지 않으면 어떡하지?”라고 묻지 않으면 알 수 없습니다.

想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要创建列表。没有列表,就没有内存的问题。由于老板告诉我们的是,她只需要遍历结果,她不会知道我们实现上的区别。

不幸的是,这样做看上去似乎不太可能。即使是我们有神奇的函数,可以让我们从n遍历到无限大,我们也会在返回第一个值之后卡住:

def get_primes(start):
    for element in magical_infinite_range(start):
        if is_prime(element):
            return element

假设这样去调用get_primes:

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

显然,在get_primes中,一上来就会碰到输入等于3的,并且在函数的第4行返回。与直接返回不同,我们需要的是在退出时可以为下一次请求准备一个值。

不过函数做不到这一点。当函数返回时,意味着全部完成。我们保证函数可以再次被调用,但是我们没法保证说,“呃,这次从上次退出时的第4行开始执行,而不是常规的从第一行开始”。函数只有一个单一的入口:函数的第1行代码。

走进生成器

这类问题极其常见以至于Python专门加入了一个结构来解决它:生成器。一个生成器会“生成”值。创建一个生成器几乎和生成器函数的原理一样简单。

一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。除了以上内容,创建一个生成器没有什么多余步骤了。

生成器函数返回生成器的迭代器。这可能是你最后一次见到“生成器的迭代器”这个术语了, 因为它们通常就被称作“生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是__next__()【注意: 在python2中是: next() 方法】。如同迭代器一样,我们可以使用next()函数来获取下一个值。

为了从生成器获取下一个值,我们使用next()函数,就像对付迭代器一样。

(next()会操心如何调用生成器的__next__()方法)。既然生成器是一个迭代器,它可以被用在for循环中。

每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。**

yield就是专门给生成器用的return(加上点小魔法)。

下面是一个简单的生成器函数:

>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

这里有两个简单的方法来使用它:

>>> for value in simple_generator_function():
>>>     print(value)
1
2
3
>>> our_generator = simple_generator_function()
>>> next(our_generator)
1
>>> next(our_generator)
2
>>> next(our_generator)
3

魔法?

那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。

我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

如果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。 这会通知next()的调用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while循环在我们的get_primes()函数出现的原因。如果没有这个while,当我们第二次调用next()的时候,生成器函数会执行到函数末尾,触发StopIteration异常。一旦生成器的值用完了,再调用next()就会出现错误,所以你只能将每个生成器的使用一次。下面的代码是错误的:

>>> our_generator = simple_generator_function()
>>> for value in our_generator:
>>>     print(value)
>>> # 我们的生成器没有下一个值了...
>>> print(next(our_generator))
Traceback (most recent call last):
  File "<ipython-input-13-7e48a609051a>", line 1, in <module>
    next(our_generator)
StopIteration
>>> # 然而,我们总可以再创建一个生成器
>>> # 只需再次调用生成器函数即可
>>> new_generator = simple_generator_function()
>>> print(next(new_generator)) # 工作正常
1

因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。

执行流程

让我们回到调用get_primes的地方:solve_number_10。

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素是如何创建的有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。

进入第三行的while循环

停在if条件判断(3是素数)

通过yield将3和执行控制权返回给solve_number_10

接下来,回到insolve_number_10:

for循环得到返回值3

for循环将其赋给next_prime

total加上next_prime

for循环从get_primes请求下一个值

这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1 # <<<<<<<<<<

最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方,同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。

总结

关键点:

generator是用来产生一系列值的

yield则像是generator函数的返回结果

yield唯一所做的另一件事就是保存一个generator函数的状态

generator就是一个特殊类型的迭代器(iterator)

和迭代器相似,我们可以通过使用next()来从generator中获取下一个值

通过隐式地调用next()来忽略一些值

위 내용은 Python의 수율 및 생성기에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.