python itérateur iterable - À quoi sert le mot clé «rendement»?




15 Answers

Raccourci vers le yield Grokking

Lorsque vous voyez une fonction avec yield déclarations de yield , appliquez cette astuce simple pour comprendre ce qui va se passer:

  1. Insérer un result = [] ligne result = [] au début de la fonction.
  2. Remplacez chaque yield expr par result.append(expr) .
  3. Insérer un return result ligne au bas de la fonction.
  4. Yay - plus de déclarations de yield ! Lire et comprendre le code.
  5. Comparez la fonction à la définition d'origine.

Cette astuce peut vous donner une idée de la logique de la fonction, mais ce qui se produit réellement avec le yield est très différent de ce qui se passe dans l’approche par liste. Dans de nombreux cas, l’approche de rendement sera beaucoup plus efficace en termes de mémoire et plus rapide. Dans d'autres cas, cette astuce vous bloquera dans une boucle infinie, même si la fonction d'origine fonctionne parfaitement. Continuez à lire pour en savoir plus...

Ne confondez pas vos Iterables, itérateurs et générateurs

Tout d'abord, le protocole itérateur - lorsque vous écrivez

for x in mylist:
    ...loop body...

Python effectue les deux étapes suivantes:

  1. Obtient un itérateur pour mylist :

    Appel iter(mylist) -> Ceci retourne un objet avec une méthode next() (ou __next__() dans Python 3).

    [C'est l'étape que la plupart des gens oublient de vous dire]

  2. Utilise l'itérateur pour faire une boucle sur les éléments:

    Continuez à appeler la méthode next() sur l'itérateur renvoyé à l'étape 1. La valeur renvoyée par next() est affectée à x et le corps de la boucle est exécuté. Si une exception StopIteration est StopIteration à l'intérieur de next() , cela signifie qu'il n'y a plus de valeurs dans l'itérateur et que la boucle est sortie.

La vérité est que Python exécute les deux étapes ci-dessus chaque fois qu'il souhaite effectuer une boucle sur le contenu d'un objet - ce pourrait donc être une boucle for, mais il pourrait également s'agir d'un code similaire à otherlist.extend(mylist) (où autre otherlist est une liste Python) .

Ici, mylist est un itératif car il implémente le protocole itérateur. Dans une classe définie par l'utilisateur, vous pouvez implémenter la __iter__() pour rendre les instances de votre classe itérables. Cette méthode devrait retourner un itérateur . Un itérateur est un objet avec une méthode next() . Il est possible d'implémenter __iter__() et next() sur la même classe et que __iter__() retourne. Cela fonctionnera pour des cas simples, mais pas si vous voulez que deux itérateurs bouclent le même objet en même temps.

C'est donc le protocole itérateur, de nombreux objets implémentent ce protocole:

  1. Listes intégrées, dictionnaires, n-uplets, ensembles, fichiers.
  2. Classes définies par l'utilisateur qui implémentent __iter__() .
  3. Générateurs.

Notez qu'une boucle for ne sait pas quel type d'objet elle traite - elle ne fait que suivre le protocole itérateur, et est heureuse d'obtenir élément après élément lors de l'appel de next() . Les listes intégrées renvoient leurs éléments un par un, les dictionnaires renvoient les clés un par un, les fichiers renvoient les lignes un par un, etc.

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Au lieu des instructions de yield , si vous aviez trois instructions de return dans f123() seul le premier serait exécuté et la fonction se fermerait. Mais f123() n'est pas une fonction ordinaire. Lorsque f123() est appelé, il ne renvoie aucune des valeurs des déclarations de rendement! Il retourne un objet générateur. En outre, la fonction ne quitte pas vraiment - elle passe dans un état suspendu. Lorsque la boucle for tente de boucler sur l'objet générateur, la fonction reprend son état suspendu à la ligne suivante après le yield elle était précédemment renvoyée, exécute la ligne de code suivante, dans ce cas une instruction de yield , et le renvoie sous la forme suivante: l'élément suivant. Cela se produit jusqu'à la sortie de la fonction, moment auquel le générateur lève StopIteration et la boucle se StopIteration .

Ainsi, l'objet générateur ressemble à un adaptateur: à une extrémité, il présente le protocole d'itérateur, en exposant les __iter__() et next() pour que la boucle for reste heureuse. Toutefois, à l’autre extrémité, il exécute la fonction juste assez pour en extraire la valeur suivante et la remet en mode suspendu.

Pourquoi utiliser des générateurs?

Généralement, vous pouvez écrire du code qui n'utilise pas de générateurs mais implémente la même logique. Une option consiste à utiliser la "astuce" de la liste temporaire que j'ai mentionnée auparavant. Cela ne fonctionnera pas dans tous les cas, par exemple si vous avez des boucles infinies, ou cela peut rendre l'utilisation de la mémoire inefficace si vous avez une très longue liste. L'autre approche consiste à implémenter une nouvelle classe itérative, SomethingIter qui conserve l'état dans les membres de l'instance et effectue l'étape logique suivante dans sa méthode next() (ou __next__() dans Python 3). Selon la logique, le code de la méthode next() peut avoir l’air très complexe et être sujet à des bogues. Ici, les générateurs offrent une solution propre et facile.

iteration yield return

Quelle est l'utilisation du mot-clé de yield en Python? Qu'est ce que ça fait?

Par exemple, j'essaie de comprendre ce code 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

Et voici l'appelant:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Que se passe-t-il lorsque la méthode _get_child_candidates est appelée? Une liste est-elle renvoyée? Un seul élément? Est-il appelé à nouveau? Quand les appels suivants vont-ils s'arrêter?

1. Le code provient de Jochen Schulz (jrschulz), qui a créé une excellente bibliothèque Python pour les espaces métriques. Ceci est le lien vers la source complète: Module mspace .




Le mot-clé de yield est réduit à deux faits simples:

  1. Si le compilateur détecte le mot-clé de yield n'importe où dans une fonction, cette fonction ne sera plus renvoyée via l'instruction return . Au lieu de cela , il retourne immédiatement un objet "liste en attente" paresseux appelé générateur.
  2. Un générateur est itérable. Qu'est-ce qu'un itérable ? Cela ressemble à une list un set une range ou à une dict-view, avec un protocole intégré pour visiter chaque élément dans un certain ordre .

En un mot: un générateur est une liste paresseuse, en attente incrémentielle , et les instructions de yield vous permettent d’utiliser la notation de fonction pour programmer les valeurs de liste que le générateur doit extraire de façon incrémentielle.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Exemple

Définissons une fonction makeRange qui ressemble à la range de Python. L'appel de makeRange(n) :

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Pour forcer le générateur à renvoyer immédiatement ses valeurs en attente, vous pouvez le transmettre à list() (comme vous pouvez le faire avec une variable quelconque):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Comparaison exemple à "renvoyer juste une liste"

L’exemple ci-dessus peut être considéré comme une simple création d’une liste à ajouter et à renvoyer:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Il y a cependant une différence majeure. voir la dernière section.

Comment utiliser des générateurs

Un itérable est la dernière partie d'une liste de compréhension, et tous les générateurs sont itérables, ils sont donc souvent utilisés comme ceci:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Pour avoir une meilleure idée des générateurs, vous pouvez jouer avec le module itertools (veillez à utiliser chain.from_iterable plutôt que chain lorsque cela est justifié). Par exemple, vous pouvez même utiliser des générateurs pour implémenter des listes paresseuses d'une longueur infinie comme itertools.count() . Vous pouvez implémenter votre propre def enumerate(iterable): zip(count(), iterable) , ou alternativement avec le mot-clé yield dans une boucle while.

Remarque: les générateurs peuvent en réalité être utilisés pour beaucoup d'autres choses, telles que la mise en œuvre de coroutines, de programmes non déterministes ou autres choses élégantes. Cependant, le point de vue "listes paresseuses" que je présente ici est l'utilisation la plus courante que vous trouverez.

Dans les coulisses

Voici comment fonctionne le "protocole d'itération Python". C’est-à-dire ce qui se passe quand vous faites la list(makeRange(5)) . C’est ce que j’ai décrit précédemment comme une "liste incrémentale paresseuse".

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

La fonction intégrée next() appelle simplement la fonction objects .next() , qui fait partie du "protocole d'itération" et se trouve sur tous les itérateurs. Vous pouvez utiliser manuellement la fonction next() (et d'autres parties du protocole d'itération) pour implémenter des choses fantaisistes, généralement au détriment de la lisibilité, alors essayez d'éviter de le faire ...

Menus détails

Normalement, la plupart des gens ne se soucient pas des distinctions suivantes et veulent probablement arrêter de lire ici.

En Python, un itérable est un objet qui "comprend le concept d'une boucle for" comme une liste [1,2,3] , et un itérateur est une instance spécifique de la boucle for demandée telle que [1,2,3].__iter__() . Un générateur est identique à n'importe quel itérateur, à l'exception de la façon dont il a été écrit (avec la syntaxe de la fonction).

Lorsque vous demandez un itérateur dans une liste, un nouvel itérateur est créé. Cependant, lorsque vous demandez un itérateur à un itérateur (ce que vous feriez rarement), il vous en donne simplement une copie.

Ainsi, dans le cas improbable où vous ne feriez pas quelque chose comme ça ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... alors rappelez-vous qu'un générateur est un itérateur ; c’est-à-dire qu’il est à usage unique. Si vous souhaitez le réutiliser, vous devez à nouveau appeler myRange(...) . Si vous devez utiliser le résultat deux fois, convertissez-le en liste et stockez-le dans une variable x = list(myRange(5)) . Ceux qui ont absolument besoin de cloner un générateur (par exemple, qui font une métaprogrammation terrifiante) peuvent utiliser itertools.tee si itertools.tee est absolument nécessaire, car la proposition de normes Python PEP les itérateurs en copie a été différée.




yieldest juste comme return- il retourne tout ce que vous lui dites (en tant que générateur). La différence est que la prochaine fois que vous appelez le générateur, l'exécution commence à partir du dernier appel à l' yieldinstruction. Contrairement au retour, le cadre de pile n'est pas nettoyé lorsqu’un rendement se produit. Toutefois, le contrôle est transféré à l’appelant. Son état est rétabli lors de la prochaine exécution de la fonction.

Dans le cas de votre code, la fonction get_child_candidatesagit comme un itérateur. Ainsi, lorsque vous étendez votre liste, elle ajoute un élément à la fois à la nouvelle liste.

list.extendappelle un itérateur jusqu'à ce qu'il soit épuisé. Dans le cas de l'exemple de code que vous avez posté, il serait beaucoup plus clair de simplement renvoyer un tuple et de l'ajouter à la liste.




Pour ceux qui préfèrent un exemple de travail minimal, méditez sur cette session interactive Python :

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed



TL; DR

Au lieu de cela:

def squares_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

faire ceci:

def squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Chaque fois que vous vous trouvez en train de construire une liste à partir de zéro, yieldchaque morceau à la place.

Ce fut mon premier moment "aha" avec rendement.

yield est une façon sucrée de dire

construire une série de choses

Même comportement:

>>> for square in squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

Comportement différent:

Le rendement est en un seul passage : vous ne pouvez parcourir qu'une seule fois. Quand une fonction a un rendement, on l’appelle une fonction génératrice . Et un iterator est ce qu'il retourne. C'est révélateur. Nous perdons la commodité d'un conteneur mais gagnons en puissance une série arbitrairement longue.

Le rendement est paresseux , cela retarde les calculs. Une fonction contenant un rendement ne s’exécute pas du tout lorsque vous l’appelez. L'objet itérateur qu'il renvoie utilise la magic pour conserver le contexte interne de la fonction. Chaque fois que vous appelez next()l'itérateur (cela se produit dans une boucle for), l'exécution avance petit à petit au prochain rendement. ( returnsoulève StopIterationet termine la série.)

Le rendement est polyvalent . Il peut faire des boucles infinies:

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Si vous avez besoin de plusieurs passes et que la série n’est pas trop longue, appelez-la simplement list():

>>> list(squares_the_yield_way(4))
[0, 1, 4, 9]

Choix génial du mot yieldcar les deux significations s’appliquent:

rendement - produire ou fournir (comme dans l'agriculture)

... fournir les prochaines données de la série.

céder - céder ou abandonner (comme dans le pouvoir politique)

... abandonner l'exécution du processeur jusqu'à ce que l'itérateur avance.




À mon avis, il n’existe pas encore un type de réponse parmi les nombreuses excellentes qui décrivent comment utiliser des générateurs. Voici la réponse de la théorie du langage de programmation:

L' yieldinstruction en Python renvoie un générateur. Un générateur en Python est une fonction qui renvoie des continuations (et plus précisément un type de coroutine, mais les continuations représentent le mécanisme plus général permettant de comprendre ce qui se passe).

Les suites dans la théorie des langages de programmation sont un type de calcul beaucoup plus fondamental, mais elles ne sont pas souvent utilisées car elles sont extrêmement difficiles à raisonner et très difficiles à mettre en œuvre. Mais l’idée de ce qu’est une continuation est simple: c’est l’état d’un calcul qui n’est pas encore terminé. Dans cet état, les valeurs actuelles des variables, les opérations à exécuter, etc. sont enregistrées. Ensuite, à un moment ultérieur du programme, la suite peut être appelée, de telle sorte que les variables du programme sont réinitialisées à cet état et que les opérations sauvegardées sont effectuées.

Les continuations, sous cette forme plus générale, peuvent être mises en œuvre de deux manières. De la même call/ccmanière, la pile du programme est littéralement sauvegardée puis, lorsque la continuation est invoquée, la pile est restaurée.

Dans la suite passant style (CPS), les continuations ne sont que des fonctions normales (uniquement dans les langages où les fonctions sont de première classe) que le programmeur gère explicitement et transfère aux sous-routines. Dans ce style, l'état du programme est représenté par des fermetures (et les variables qui y sont codées) plutôt que par des variables qui résident quelque part sur la pile. Les fonctions qui gèrent le flux de contrôle acceptent la continuation comme argument (dans certaines variantes de CPS, les fonctions peuvent accepter plusieurs continuations) et manipulent le flux de contrôle en les appelant simplement en les appelant et en y retournant par la suite. Voici un exemple très simple de style de passage de continuation:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

Dans cet exemple (très simpliste), le programmeur enregistre l'opération consistant à écrire le fichier dans une continuation (qui peut être potentiellement une opération très complexe avec beaucoup de détails à écrire), puis passe cette continuation (c'est-à-dire en tant que premier). fermeture de classe) à un autre opérateur qui effectue un traitement supplémentaire, puis l’appelle si nécessaire. (J'utilise beaucoup ce modèle de conception dans la programmation d'interface graphique, soit parce qu'il m'enregistre des lignes de code, soit, plus important encore, pour gérer le flux de contrôle après le déclenchement d'événements d'interface graphique.)

Le reste de cet article va, sans perdre de généralité, conceptualiser les suites en tant que CPS, car il est extrêmement facile à comprendre et à lire.


Parlons maintenant des générateurs en Python. Les générateurs sont un sous-type spécifique de continuation. Alors que les continuations sont généralement capables de sauvegarder l'état d'un calcul (c'est-à-dire la pile d'appels du programme), les générateurs ne peuvent sauvegarder que l'état d'itération sur un itérateur . Bien que cette définition soit légèrement trompeuse pour certains cas d'utilisation de générateurs. Par exemple:

def f():
  while True:
    yield 4

Ceci est clairement un itératif raisonnable dont le comportement est bien défini - chaque fois que le générateur itère dessus, il retourne 4 (et le fait pour toujours). Mais ce n'est probablement pas le type prototypique d'iterable qui vient à l'esprit lorsqu'on pense aux itérateurs (c'est-à-dire for x in collection: do_something(x)). Cet exemple illustre la puissance des générateurs: s'il s'agit d'un itérateur, un générateur peut enregistrer l'état de son itération.

Pour réitérer: les continuations peuvent sauvegarder l'état de la pile d'un programme et les générateurs peuvent sauvegarder l'état de l'itération. Cela signifie que les continuations sont beaucoup plus puissantes que les générateurs, mais également que les générateurs sont beaucoup plus faciles. C’est plus facile à mettre en œuvre par le concepteur de langage et plus facile à utiliser pour le programmeur (si vous avez du temps à graver, essayez de lire et de comprendre cette page sur les continuations et call / cc ).

Mais vous pouvez facilement implémenter (et conceptualiser) des générateurs comme un cas simple et spécifique de style de passage de continuation:

Chaque fois qu'il yieldest appelé, il indique à la fonction de renvoyer une suite. Lorsque la fonction est appelée à nouveau, elle commence où qu'elle se soit arrêtée. Ainsi, en pseudo-pseudo-code (c'est-à-dire, pas de pseudo-code, mais pas de code), la nextméthode du générateur est essentiellement la suivante:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

où le yieldmot clé est en fait un sucre syntaxique pour la fonction de générateur réelle, à peu près comme:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Rappelez-vous qu'il ne s'agit que d'un pseudocode et que la mise en œuvre réelle des générateurs en Python est plus complexe. Mais comme exercice pour comprendre ce qui se passe, essayez d’utiliser le style de passage de continuation pour implémenter des objets générateurs sans utiliser le yieldmot - clé.




Bien que beaucoup de réponses montrent pourquoi vous utiliseriez un yieldpour créer un générateur, il y a plus d'utilisations pour yield. Il est assez facile de créer une coroutine, qui permet de passer des informations entre deux blocs de code. Je ne répéterai aucun des beaux exemples qui ont déjà été donnés sur l'utilisation yieldd'un générateur.

Pour vous aider à comprendre ce que yieldfait le code suivant, vous pouvez utiliser votre doigt pour suivre le cycle à travers tout code comportant un yield. Chaque fois que votre doigt frappe le bouton yield, vous devez attendre qu'un nextou plusieurs sendsoient saisis. Quand a nextest appelé, vous tracez le code jusqu'à ce que vous appuyiez sur yield… le code à droite de celui-ci yieldsoit évalué et renvoyé à l'appelant… puis vous attendez. Quand nextest rappelé, vous effectuez une autre boucle dans le code. Cependant, vous remarquerez que dans une coroutine, yieldpeut également être utilisé avec un send… qui enverra une valeur de l'appelant dans la fonction de cession. Si un sendest donné, alorsyieldreçoit la valeur envoyée et la crache à gauche… puis la trace dans le code progresse jusqu'à ce que vous appuyiez de yieldnouveau sur (en renvoyant la valeur à la fin, comme si elle nextétait appelée).

Par exemple:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()



J'allais poster "lire la page 19 de" Python: Référence essentielle "de Beazley pour une description rapide des générateurs", mais beaucoup d'autres ont déjà publié de bonnes descriptions.

En outre, notez que yieldpeut être utilisé dans les routines comme le double de leur utilisation dans les fonctions de générateur. Bien que ce ne soit pas la même utilisation que votre extrait de code, vous (yield)pouvez l'utiliser comme expression dans une fonction. Lorsqu'un appelant envoie une valeur à la méthode à l'aide de la send()méthode, la coroutine sera exécutée jusqu'à la prochaine (yield)instruction.

Les générateurs et les routines constituent un moyen pratique de configurer des applications de type flux de données. J'ai pensé qu'il serait utile de connaître l'autre utilisation de l' yieldénoncé dans des fonctions.




Du point de vue de la programmation, les itérateurs sont implémentés en tant que thunks .

Pour implémenter des itérateurs, des générateurs et des pools de threads pour une exécution simultanée, etc. sous la forme de thunks (également appelées fonctions anonymes), on utilise les messages envoyés à un objet de fermeture, qui possède un répartiteur, qui répond aux "messages".

http://en.wikipedia.org/wiki/Message_passing

" next " est un message envoyé à une fermeture, créé par l' appel " iter ".

Il y a beaucoup de façons d'implémenter ce calcul. J'ai utilisé mutation, mais il est facile de le faire sans mutation, en renvoyant la valeur actuelle et le prochain rendement.

Voici une démonstration qui utilise la structure de R6RS, mais la sémantique est absolument identique à celle de Python. C'est le même modèle de calcul, et seul un changement de syntaxe est nécessaire pour le réécrire en Python.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->



Voici un exemple simple:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Sortie:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Je ne suis pas un développeur Python, mais il me semble que yieldla position de déroulement du programme est maintenue et que la boucle suivante part de la position "rendement". On dirait qu’il attend à cette position et, juste avant, il renvoie une valeur à l’extérieur et continue de fonctionner.

Cela semble être une capacité intéressante et intéressante: D




Comme chaque réponse le suggère, yieldest utilisé pour créer un générateur de séquence. Il est utilisé pour générer une séquence dynamiquement. Par exemple, lorsque vous lisez un fichier ligne par ligne sur un réseau, vous pouvez utiliser la yieldfonction comme suit:

def getNextLines():
   while con.isOpen():
       yield con.read()

Vous pouvez l'utiliser dans votre code comme suit:

for line in getNextLines():
    doSomeThing(line)

Transfert de contrôle d'exécution obtenu

Le contrôle d'exécution sera transféré de getNextLines () vers la forboucle lorsque le rendement est exécuté. Ainsi, chaque fois que getNextLines () est appelée, l'exécution commence à partir du point où elle avait été mise en pause la dernière fois.

Donc, en bref, une fonction avec le code suivant

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

imprimera

"first time"
"second time"
"third time"
"Now some useful value 12"



En résumé, l' yieldinstruction transforme votre fonction en une fabrique qui produit un objet spécial appelé un generatorqui enveloppe le corps de votre fonction d'origine. Lorsqu'il generatorest itéré, il exécute votre fonction jusqu'à ce qu'elle atteigne la suivante yieldpuis suspend l'exécution et évalue la valeur transmise à yield. Il répète ce processus à chaque itération jusqu'à ce que le chemin d'exécution quitte la fonction. Par exemple,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

simplement des sorties

one
two
three

La puissance provient de l’utilisation du générateur avec une boucle qui calcule une séquence, le générateur exécute la boucle en s’arrêtant à chaque fois pour «donner» le résultat suivant du calcul, ce qui permet de calculer une liste à la volée, l’avantage étant la mémoire. enregistré pour des calculs particulièrement volumineux

Supposons que vous vouliez créer votre propre rangefonction qui produit une plage de nombres itérable, vous pouvez le faire comme ça,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

et l'utiliser comme ça;

for i in myRangeNaive(10):
    print i

Mais c'est inefficace parce que

  • Vous créez un tableau que vous n'utilisez qu'une fois (cela gaspille de la mémoire)
  • Ce code parcourt en fait deux fois ce tableau! :(

Heureusement, Guido et son équipe ont été assez généreux pour développer des générateurs afin que nous puissions le faire.

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Maintenant, à chaque itération, une fonction du générateur appelée next()exécute la fonction jusqu'à ce qu'elle atteigne une instruction 'yield' dans laquelle elle s'arrête et 'renvoie' la valeur ou atteint la fin de la fonction. Dans ce cas, lors du premier appel, next()s’exécute jusqu’à la déclaration de rendement et renvoie 'n', lors du prochain appel, il exécutera la déclaration d’incrémentation, retournera au 'while', l’évaluera et, si la valeur est vraie, s’arrêtera et à nouveau, cela continuera jusqu'à ce que la condition while retourne false et que le générateur passe à la fin de la fonction.




(Ma réponse ci-dessous ne parle que du point de vue de l'utilisation du générateur Python, et non de l' implémentation sous - jacente du mécanisme du générateur , qui implique quelques astuces de manipulation de pile et de tas.)

Quand yieldest utilisé à la place d'une returnfonction python, cette fonction est transformée en quelque chose de spécial appelé generator function. Cette fonction retournera un objet de generatortype. Le yieldmot clé est un drapeau pour informer le compilateur python de traiter cette fonction de manière spécifique. Les fonctions normales se termineront dès qu'une valeur sera renvoyée. Mais avec l’aide du compilateur, la fonction de générateur peut être considérée comme pouvant être reprise. En d'autres termes, le contexte d'exécution sera restauré et l'exécution se poursuivra à partir de la dernière exécution. Jusqu'à ce que vous appeliez explicitement return, ce qui déclencherait une StopIterationexception (qui fait également partie du protocole itérateur) ou atteindrait la fin de la fonction. J'ai trouvé beaucoup de références generatormais celui- onede la functional programming perspectiveest la plus digeste.

(Maintenant, je veux parler de la raison derrière generator, et de celle iteratorbasée sur ma propre compréhension. J'espère que cela peut vous aider à comprendre la motivation essentielle de l'itérateur et du générateur. Ce concept apparaît également dans d'autres langages tels que C #.)

Si je comprends bien, lorsque nous voulons traiter un ensemble de données, nous les stockons d’abord quelque part, puis nous les traitons une à une. Mais cette approche intuitive est problématique. Si le volume de données est énorme, il est coûteux de les stocker au préalable dans leur ensemble. Donc, au lieu de stocker le datalui - même directement, pourquoi ne pas stocker metadataindirectement, c.-à-dthe logic how the data is computed .

Il existe 2 approches pour envelopper de telles métadonnées.

  1. L'approche OO, nous enveloppons les métadonnées as a class. C'est ce qu'on appelle iteratorqui implémente le protocole itérateur (c'est-à-dire les méthodes __next__(), et __iter__()). C'est également le modèle de conception d'itérateur communément observé .
  2. L'approche fonctionnelle, nous enveloppons les métadonnées as a function. C'est le soi-disant generator function. Mais sous le capot, l' itérateur generator objecttoujours retourné IS-Acar il implémente également le protocole d'itérateur.

Dans les deux cas, un itérateur est créé, c’est-à-dire un objet pouvant vous fournir les données souhaitées. L'approche OO peut être un peu complexe. Quoi qu'il en soit, lequel choisir est à vous.




Le yieldmot clé collecte simplement les résultats renvoyés. Pensez yieldcommereturn +=




Encore un autre TL; DR

Iterator on list : next()renvoie le prochain élément de la liste

Générateur Iterator : next()calculera le prochain élément à la volée (exécuter du code)

Vous pouvez voir le rendement / générateur comme un moyen d'exécuter manuellement le flux de contrôle de l'extérieur (comme avec la boucle continue une étape), en appelant next, quelle que soit la complexité du flux.

Remarque : le générateur n'est PAS une fonction normale. Il se souvient de l'état précédent comme des variables locales (pile). Voir d'autres réponses ou articles pour une explication détaillée. Le générateur ne peut être itéré qu'une seule fois . Vous pourriez vous en passer yield, mais ce ne serait pas aussi agréable. On peut donc parler de sucre «très agréable».




Related