Maison > développement back-end > Tutoriel Python > Une analyse approfondie du rendement et des générateurs en Python

Une analyse approfondie du rendement et des générateurs en Python

巴扎黑
Libérer: 2017-08-16 13:13:43
original
1679 Les gens l'ont consulté

Le mot-clé générateur et rendement est peut-être l'un des concepts les plus puissants et les plus difficiles à comprendre en Python (peut-être aucun), mais cela n'empêche pas le rendement de devenir le mot-clé le plus puissant en Python, pour les débutants. Il est en effet très difficile de comprendre. Lisons un article écrit par un expert étranger sur le rendement pour vous aider à comprendre rapidement le rendement. L’article est un peu long, alors soyez patient et lisez-le jusqu’à la fin. Il y a quelques exemples en cours de route, pour ne pas vous ennuyer.

Générateur

Un générateur est une fonction composée d'une ou plusieurs expressions de rendement. Chaque générateur est un itérateur (mais un itérateur n'est pas nécessairement un générateur).

Si une fonction contient le mot clé rendement, la fonction deviendra un générateur.

Le générateur ne renvoie pas tous les résultats en même temps, mais renvoie les résultats correspondants à chaque fois qu'il rencontre le mot-clé rendement, et conserve l'état d'exécution actuel de la fonction, en attendant le prochain appel.

Puisque le générateur est également un itérateur, il doit prendre en charge la méthode suivante pour obtenir la valeur suivante. (Vous pouvez également utiliser l'attribut .__next__(), qui est .next() en python2)

Coroutines et sous-programmes

Nous appelons une fonction Python ordinaire lors de l'exécution , l'exécution commence généralement à partir de la première ligne de code de la fonction et se termine par une instruction return, une exception ou la fin de la fonction (qui peut être considérée comme un retour implicite de None). Une fois que la fonction rend le contrôle à l'appelant, c'est fini. Tout le travail effectué dans la fonction et les données enregistrées dans les variables locales seront perdus. Lorsque cette fonction sera à nouveau appelée, tout sera créé à partir de zéro.

Il s'agit d'une procédure assez standard pour les fonctions abordées en programmation informatique. Une telle fonction ne peut renvoyer qu'une seule valeur, mais il est parfois utile de créer une fonction qui produit une séquence. Pour ce faire, une telle fonction doit pouvoir « sauvegarder son propre travail ».

J'ai dit que le fait de pouvoir "produire une séquence" est dû au fait que notre fonction ne renvoie pas dans le sens habituel. La signification implicite de return est que la fonction rend le contrôle du code exécuté à l'endroit où la fonction a été appelée. Le sens implicite de « rendement » est que le transfert de contrôle est temporaire et volontaire, et que notre fonction reprendra le contrôle à l'avenir.

En Python, les "fonctions" dotées de cette capacité sont appelées générateurs, et elles sont très utiles. Les générateurs (et donc l'instruction rendement) ont été introduits à l'origine pour permettre aux programmeurs d'écrire plus facilement du code produisant des séquences de valeurs. Auparavant, pour implémenter quelque chose comme un générateur de nombres aléatoires, vous implémentiez une classe ou un module qui générait des données tout en gardant une trace de l'état entre chaque appel. Avec l’introduction des générateurs, cela devient très simple.

Pour mieux comprendre le problème résolu par le générateur, regardons un exemple. Pendant que nous travaillons sur cet exemple, gardez toujours à l’esprit le problème que nous devons résoudre : générer une séquence de valeurs.

Remarque : en dehors de Python, les générateurs les plus simples sont ce qu'on appelle des coroutines. Dans cet article, j'utiliserai ce terme. N'oubliez pas que dans le concept de Python, les coroutines mentionnées ici sont des générateurs. Le terme formel pour Python est générateur ; coroutine est juste pour la discussion et n'a pas de définition formelle au niveau du langage.

Exemple : nombres premiers intéressants

Supposons que votre patron vous demande d'écrire une fonction. Le paramètre d'entrée est une liste d'entiers et renvoie un résultat itérable contenant le nombre premier 1.

Rappelez-vous qu'un Itérable est simplement la capacité d'un objet à renvoyer un membre spécifique à chaque fois.

Vous devez penser "c'est très simple", puis écrire rapidement le code suivant :

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
Copier après la connexion

L'implémentation ci-dessus de is_prime répond pleinement aux besoins, nous disons donc au patron qu'elle est fait. Elle nous a répondu que notre fonction fonctionnait bien et correspondait exactement à ce qu'elle souhaitait.

Gérer des séquences infinies

Oh, vraiment ? Quelques jours plus tard, la patronne est venue nous dire qu'elle avait rencontré quelques petits soucis : elle envisageait d'utiliser notre fonction get_primes pour une grande liste contenant des nombres. En fait, cette liste est si longue que sa simple création utiliserait toute la mémoire du système. À cette fin, elle espère apporter un paramètre de démarrage lors de l'appel de la fonction get_primes et renvoyer tous les nombres premiers supérieurs à ce paramètre (peut-être souhaite-t-elle résoudre le problème 10 du projet Euler).

Jetons un coup d'œil à cette nouvelle exigence. Il est évident que modifier simplement get_primes n'est pas possible. Naturellement, il est impossible de renvoyer une liste contenant tous les nombres premiers du début à l’infini (bien qu’il existe de nombreuses applications utiles pour manipuler des séquences infinies). Il semble que la possibilité d’utiliser des fonctions ordinaires pour résoudre ce problème soit relativement mince.

Avant d’abandonner, identifions le principal obstacle, ce qui nous empêche d’écrire des fonctions qui répondent aux nouveaux besoins du patron. Après réflexion, nous sommes arrivés à la conclusion que la fonction n'a qu'une seule chance de renvoyer le résultat, elle doit donc renvoyer tous les résultats en même temps. Il semble inutile d’arriver à une telle conclusion : « N’est-ce pas ainsi que fonctionnent les fonctions ? » Cependant, si vous n’apprenez pas, vous ne le saurez pas, et si vous ne demandez pas, vous ne le saurez pas « Et s’ils n’étaient pas comme ça ? »

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

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

def get_primes(start):
    for element in magical_infinite_range(start):
        if is_prime(element):
            return element
Copier après la connexion

假设这样去调用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
Copier après la connexion
Copier après la connexion

显然,在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
Copier après la connexion

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

>>> 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
Copier après la connexion

魔法?

那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用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
Copier après la connexion

如果生成器函数调用了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
Copier après la connexion

因此,这个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
Copier après la connexion
Copier après la connexion

我们来看一下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 # <<<<<<<<<<
Copier après la connexion

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal