ホームページ  >  記事  >  バックエンド開発  >  Python の利回りとジェネレーターの詳細な分析

Python の利回りとジェネレーターの詳細な分析

巴扎黑
巴扎黑オリジナル
2017-08-16 13:13:431611ブラウズ

ジェネレーターと yield キーワードは、Python で最も強力で理解しにくい概念の 1 つである可能性があります (おそらく何もありません)。しかし、だからといって、yield が Python で最も強力なキーワードになることを妨げるものではありません。実際、初心者にとっては理解するのが非常に困難です。利回りをすぐに理解するために、海外の専門家が書いた利回りに関する記事を見てみましょう。 この記事は少し長いので、最後まで読んでください。途中にいくつかの例があるので、飽きることはありません。

ジェネレーター

ジェネレーターは、1 つ以上の yield 式で構成される関数です (ただし、イテレーターは必ずしもジェネレーターである必要はありません)。

関数に yield キーワードが含まれている場合、その関数はジェネレーターになります。

ジェネレーターは一度にすべての結果を返すのではなく、yield キーワードに遭遇するたびに対応する結果を返し、関数の現在の実行ステータスを保持して次の呼び出しを待ちます。

ジェネレータはイテレータでもあるため、次の値を取得するための next メソッドをサポートする必要があります。 (Python2 の .next() である .__next__() 属性も使用できます)

コルーチンとサブルーチン

通常の Python 関数を呼び出すときは、通常、関数のコードの最初の行から開始します。実行は return ステートメント、例外、または関数の終了 (None の暗黙的な戻りとみなすことができます) で終了します。関数が呼び出し元に制御を返したら、すべて終了です。関数内で行われたすべての作業とローカル変数に保存されたデータは失われます。この関数を再度呼び出すと、すべてが最初から作成されます。

これは、コンピュータープログラミングで議論される関数の非常に標準的な手順です。このような関数は単一の値しか返せませんが、シーケンスを生成する関数を作成すると便利な場合があります。これを行うには、そのような関数が「独自の作業を保存」できる必要があります。

「シーケンスを生成」できるのは、関数が通常の意味で返らないからだと言いました。 return の暗黙の意味は、関数が実行されたコードの制御を関数が呼び出された場所に返すことです。 「譲歩」の暗黙の意味は、制御の譲渡は一時的かつ自発的なものであり、私たちの機能が将来制御を取り戻すということです。

Pythonでは、この機能を備えた「関数」をジェネレーターと呼び、とても便利です。ジェネレーター (したがって、yield ステートメント) はもともと、プログラマーが値のシーケンスを生成するコードを簡単に作成できるようにするために導入されました。 以前は、乱数ジェネレーターのようなものを実装するには、各呼び出し間の状態を追跡しながらデータを生成するクラスまたはモジュールを実装していました。ジェネレーターの導入により、これが非常に簡単になります。

ジェネレーターによって解決される問題をより深く理解するために、例を見てみましょう。この例を進めるときは、解決する必要がある問題、つまり一連の値を生成することを常に念頭に置いてください。

注: Python 以外では、最も単純なジェネレーターはコルーチンと呼ばれるものです。この記事ではこの用語を使用します。 Python の概念では、ここで言及されているコルーチンはジェネレーターであることを思い出してください。 Python の正式な用語はジェネレーターです。コルーチンは議論のためのものであり、言語レベルでの正式な定義はありません。

例: 興味深い素数

上司が関数を書くように頼んだとします。入力パラメーターは int のリストで、素数 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 関数を使用する予定だったのです。実際、このリストは非常に大きいため、作成するだけでシステムのメモリをすべて使い切ってしまいます。この目的を達成するために、彼女は start パラメータを指定して get_primes 関数を呼び出し、このパラメータより大きいすべての素数を返したいと考えています (おそらくプロジェクト オイラーの問題 10 を解決したいと考えています)。

この新しい要件を見てみましょう。get_primes を単に変更することが不可能であることは明らかです。 当然のことながら、先頭から無限までのすべての素数を含むリストを返すことは不可能です (ただし、無限シーケンスを操作するための便利なアプリケーションは数多くあります)。この問題に通常の関数を使用して対処できる可能性は比較的低いと思われます。

諦める前に、上司の新しいニーズを満たす関数を書くことを妨げている根本的な障害を特定しましょう。考えた結果、この関数は結果を返す機会が 1 回しかないため、すべての結果を一度に返さなければならないという結論に達しました。 「それが関数の仕組みではないでしょうか?」というような結論を導くのは無意味に思えます。でも、学ばないと成功しないし、「こうじゃなかったらどうする?」って聞かないと分からないんです

想象一下,如果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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。