Eine ausführliche Analyse von Ertrag und Generatoren in Python

巴扎黑
Freigeben: 2017-08-16 13:13:43
Original
1665 Leute haben es durchsucht

Das Schlüsselwort „Generator“ und „Yield“ ist möglicherweise eines der mächtigsten und am schwierigsten zu verstehenden Konzepte in Python (vielleicht keines), aber es verhindert nicht, dass „Yield“ das mächtigste Schlüsselwort in Python wird, was für Anfänger in der Tat sehr schwierig ist Lesen Sie einen Artikel eines ausländischen Experten zum Thema Ertrag, damit Sie den Ertrag schnell verstehen. Der Artikel ist etwas lang, also haben Sie bitte Geduld und lesen Sie ihn bis zum Ende. Es gibt einige Beispiele, damit Sie sich nicht langweilen.

Generator

Ein Generator ist eine Funktion, die aus einem oder mehreren Ertragsausdrücken besteht. Jeder Generator ist ein Iterator (aber ein Iterator ist nicht unbedingt ein Generator).

Wenn eine Funktion das Schlüsselwort yield enthält, wird die Funktion zu einem Generator.

Der Generator gibt nicht alle Ergebnisse auf einmal zurück, sondern gibt die entsprechenden Ergebnisse jedes Mal zurück, wenn er auf das Schlüsselwort yield trifft, und behält den aktuellen Ausführungsstatus der Funktion bei und wartet auf den nächsten Aufruf.

Da der Generator auch ein Iterator ist, sollte er die nächste Methode unterstützen, um den nächsten Wert zu erhalten. (Sie können auch das Attribut .__next__() verwenden, das in Python2 .next() ist)

Koroutinen und Unterroutinen

Wir rufen bei der Ausführung eine gewöhnliche Python-Funktion auf Die Ausführung beginnt im Allgemeinen mit der ersten Codezeile der Funktion und endet mit einer Return-Anweisung, einer Ausnahme oder dem Ende der Funktion (was als implizite Rückgabe von None angesehen werden kann). Sobald die Funktion die Kontrolle an den Aufrufer zurückgibt, ist alles vorbei. Alle in der Funktion durchgeführten Arbeiten und in lokalen Variablen gespeicherten Daten gehen verloren. Wenn diese Funktion erneut aufgerufen wird, wird alles von Grund auf neu erstellt.

Dies ist ein ziemlich normales Verfahren für Funktionen, die in der Computerprogrammierung behandelt werden. Eine solche Funktion kann nur einen einzelnen Wert zurückgeben, aber manchmal ist es hilfreich, eine Funktion zu erstellen, die eine Sequenz erzeugt. Dazu muss eine solche Funktion in der Lage sein, „ihre eigene Arbeit zu speichern“.

Ich sagte, dass die Fähigkeit, „eine Sequenz zu erzeugen“, daran liegt, dass unsere Funktion nicht im üblichen Sinne zurückkehrt. Die implizite Bedeutung von return besteht darin, dass die Funktion die Kontrolle über den ausgeführten Code an die Stelle zurückgibt, an der die Funktion aufgerufen wurde. Die implizite Bedeutung von „Ertrag“ besteht darin, dass die Übertragung der Kontrolle vorübergehend und freiwillig ist und unsere Funktion in Zukunft die Kontrolle zurückerlangen wird.

In Python werden „Funktionen“ mit dieser Fähigkeit als Generatoren bezeichnet und sind sehr nützlich. Generatoren (und damit auch die Yield-Anweisung) wurden ursprünglich eingeführt, um Programmierern das Schreiben von Code zu erleichtern, der Wertesequenzen erzeugt. Um so etwas wie einen Zufallszahlengenerator zu implementieren, implementierten Sie bisher eine Klasse oder ein Modul, das Daten generierte und gleichzeitig den Status zwischen den einzelnen Aufrufen verfolgte. Mit der Einführung von Generatoren wird dies sehr einfach.

Um das vom Generator gelöste Problem besser zu verstehen, schauen wir uns ein Beispiel an. Denken Sie beim Durcharbeiten dieses Beispiels immer an das Problem, das wir lösen müssen: die Generierung einer Wertefolge.

Hinweis: Außerhalb von Python sind die einfachsten Generatoren sogenannte Coroutinen. In diesem Artikel werde ich diesen Begriff verwenden. Denken Sie daran, dass die hier erwähnten Coroutinen im Python-Konzept Generatoren sind. Der formale Begriff für Python ist „Generator“. Er dient lediglich der Diskussion und hat keine formale Definition auf Sprachebene.

Beispiel: Interessante Primzahlen

Angenommen, Ihr Chef bittet Sie, eine Funktion zu schreiben und gibt ein iterierbares Ergebnis zurück, das die Primzahl 1 enthält.

Denken Sie daran, dass ein Iterable nur die Fähigkeit eines Objekts ist, jedes Mal ein bestimmtes Mitglied zurückzugeben.

Sie müssen denken: „Das ist sehr einfach“ und dann schnell den folgenden Code schreiben:

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
Nach dem Login kopieren

Die obige Implementierung von is_prime erfüllt die Anforderungen vollständig, also sagen wir dem Chef, dass dies der Fall ist ist erledigt. Sie berichtete, dass unsere Veranstaltung gut funktioniert habe und genau ihren Vorstellungen entsprochen habe.

Umgang mit unendlichen Sequenzen

Oh, wirklich? Ein paar Tage später kam die Chefin vorbei und teilte uns mit, dass sie auf einige kleine Probleme gestoßen sei: Sie wollte unsere Funktion get_primes für eine große Liste mit Zahlen verwenden. Tatsächlich ist diese Liste so umfangreich, dass allein ihre Erstellung den gesamten Systemspeicher verbrauchen würde. Zu diesem Zweck hofft sie, beim Aufruf der Funktion get_primes einen Startparameter zu übergeben und alle Primzahlen zurückzugeben, die größer als dieser Parameter sind (vielleicht möchte sie Project Euler-Problem 10 lösen).

Werfen wir einen Blick auf diese neue Anforderung. Es ist offensichtlich, dass eine einfache Änderung von get_primes nicht möglich ist. Natürlich ist es unmöglich, eine Liste zurückzugeben, die alle Primzahlen vom Anfang bis ins Unendliche enthält (obwohl es viele nützliche Anwendungen zur Manipulation unendlicher Folgen gibt). Es scheint, dass die Möglichkeit, gewöhnliche Funktionen zur Lösung dieses Problems zu verwenden, relativ gering ist.

Bevor wir aufgeben, wollen wir das Haupthindernis identifizieren, das uns daran hindert, Funktionen zu schreiben, die den neuen Anforderungen des Chefs gerecht werden. Durch Nachdenken kamen wir zu dem Schluss, dass die Funktion nur eine Chance hat, das Ergebnis zurückzugeben, also alle Ergebnisse auf einmal zurückgeben muss. Es scheint sinnlos, eine solche Schlussfolgerung zu ziehen; „Funktionieren Funktionen nicht so?“ Sie werden jedoch keinen Erfolg haben, wenn Sie nicht lernen, und Sie werden es nicht wissen, wenn Sie nicht fragen: „Was ist, wenn sie nicht so sind?“

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

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

def get_primes(start):
    for element in magical_infinite_range(start):
        if is_prime(element):
            return element
Nach dem Login kopieren

假设这样去调用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
Nach dem Login kopieren
Nach dem Login kopieren

显然,在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
Nach dem Login kopieren

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

>>> 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
Nach dem Login kopieren

魔法?

那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用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
Nach dem Login kopieren

如果生成器函数调用了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
Nach dem Login kopieren

因此,这个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
Nach dem Login kopieren
Nach dem Login kopieren

我们来看一下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 # <<<<<<<<<<
Nach dem Login kopieren

最关键的是,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()来忽略一些值

Das obige ist der detaillierte Inhalt vonEine ausführliche Analyse von Ertrag und Generatoren in Python. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage