Recherche…


Introduction

Les générateurs sont des itérateurs paresseux créés par des fonctions de générateur (à l'aide de yield ) ou des expressions de générateur (using (an_expression for x in an_iterator) ).

Syntaxe

  • rendement <expr>
  • rendement de <expr>
  • <var> = rendement <expr>
  • suivant ( <iter> )

Itération

Un objet générateur prend en charge le protocole itérateur . En d'autres __next__() , il fournit une méthode next() ( __next__() dans Python 3.x), qui est utilisée pour __iter__ son exécution, et sa méthode __iter__ . Cela signifie qu'un générateur peut être utilisé dans n'importe quelle construction de langage prenant en charge les objets itérables génériques.

# naive partial implementation of the Python 2.x xrange()
def xrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

# looping
for i in xrange(10):
    print(i)  # prints the values 0, 1, ..., 9

# unpacking
a, b, c = xrange(3)  # 0, 1, 2

# building a list
l = list(xrange(10))  # [0, 1, ..., 9]

La fonction next ()

La fonction intégrée next() est un wrapper pratique qui peut être utilisé pour recevoir une valeur de n'importe quel itérateur (y compris un générateur d'itérateurs) et pour fournir une valeur par défaut si l'itérateur est épuisé.

def nums():
    yield 1
    yield 2
    yield 3
generator = nums()

next(generator, None)  # 1
next(generator, None)  # 2
next(generator, None)  # 3
next(generator, None)  # None
next(generator, None)  # None
# ...

La syntaxe est la next(iterator[, default]) . Si l'itérateur se termine et qu'une valeur par défaut a été transmise, il est renvoyé. Si aucune valeur par défaut n'a été fournie, StopIteration est StopIteration .

Envoi d'objets à un générateur

En plus de recevoir des valeurs d'un générateur, il est possible d' envoyer un objet à un générateur à l'aide de la méthode send() .

def accumulator():
    total = 0
    value = None
    while True:
        # receive sent value
        value = yield total
        if value is None: break
        # aggregate values
        total += value

generator = accumulator()

# advance until the first "yield"
next(generator)      # 0

# from this point on, the generator aggregates values
generator.send(1)    # 1
generator.send(10)   # 11
generator.send(100)  # 111
# ...

# Calling next(generator) is equivalent to calling generator.send(None)
next(generator)      # StopIteration

Ce qui se passe ici est le suivant:

  • Lorsque vous appelez next(generator) , le programme avance à la première déclaration de yield et retourne la valeur de total à ce point, qui est 0. L'exécution du générateur est suspendue à ce stade.
  • Lorsque vous appelez ensuite generator.send(x) , l'interpréteur prend l'argument x et en fait la valeur de retour de la dernière instruction de yield , qui est affectée à la value . Le générateur se déroule alors comme d'habitude, jusqu'à ce qu'il produise la valeur suivante.
  • Lorsque vous appelez enfin next(generator) , le programme traite cela comme si vous envoyiez None au générateur. Il n'y a rien de particulier à propos de None , cependant, cet exemple utilise None comme valeur spéciale pour demander au générateur de s'arrêter.

Expressions du générateur

Il est possible de créer des itérateurs de générateur en utilisant une syntaxe de type compréhension.

generator = (i * 2 for i in range(3))

next(generator)  # 0
next(generator)  # 2
next(generator)  # 4
next(generator)  # raises StopIteration

Si une fonction n'a pas nécessairement besoin d'une liste, vous pouvez enregistrer des caractères (et améliorer la lisibilité) en plaçant une expression de générateur dans un appel de fonction. La parenthèse de l'appel de fonction fait implicitement de votre expression une expression de générateur.

sum(i ** 2 for i in range(4))  # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14

De plus, vous économiserez de la mémoire car au lieu de charger la liste complète que vous parcourez ( [0, 1, 2, 3] dans l'exemple ci-dessus), le générateur permet à Python d'utiliser les valeurs nécessaires.

introduction

Les expressions de générateur sont similaires à celles de liste, dictionnaire et ensemble, mais sont entourées de parenthèses. Les parenthèses ne doivent pas nécessairement être présentes lorsqu'elles sont utilisées comme seul argument pour un appel de fonction.

expression = (x**2 for x in range(10))

Cet exemple génère les 10 premiers carrés parfaits, dont 0 (dans lequel x = 0).

Les fonctions du générateur sont similaires aux fonctions normales, sauf qu’elles comportent un ou plusieurs énoncés de yield . Ces fonctions ne peuvent return aucune valeur (toutefois, les return vides sont autorisés si vous souhaitez arrêter le générateur au début).

def function():
    for x in range(10):
        yield x**2

Cette fonction de générateur est équivalente à l'expression précédente du générateur, elle produit la même chose.

Note : toutes les expressions du générateur ont leurs propres fonctions équivalentes , mais pas l'inverse.


Une expression de générateur peut être utilisée sans parenthèses si les deux parenthèses sont répétées autrement:

sum(i for i in range(10) if i % 2 == 0)   #Output: 20
any(x = 0 for x in foo)                   #Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1)    #Output: <class 'generator'>

Au lieu de:

sum((i for i in range(10) if i % 2 == 0))
any((x = 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))

Mais non:

fooFunction(i for i in range(10) if i % 2 == 0,foo,bar)
return x = 0 for x in foo
barFunction(baz, a > b for a in foo if a % 2 == 1)

L'appel d'une fonction de générateur produit un objet générateur , qui peut ensuite être itéré. Contrairement aux autres types d'itérateurs, les objets générateurs ne peuvent être parcourus qu'une seule fois.

g1 = function()
print(g1)  # Out: <generator object function at 0x1012e1888>

Notez que le corps d'un générateur n'est pas exécuté immédiatement: lorsque vous appelez function() dans l'exemple ci-dessus, il retourne immédiatement un objet générateur, sans même exécuter la première instruction d'impression. Cela permet aux générateurs de consommer moins de mémoire que les fonctions qui renvoient une liste et permet de créer des générateurs produisant des séquences infiniment longues.

Pour cette raison, les générateurs sont souvent utilisés dans la science des données et dans d'autres contextes impliquant de grandes quantités de données. Un autre avantage est que l'autre code peut immédiatement utiliser les valeurs fournies par un générateur, sans attendre que la séquence complète soit produite.

Cependant, si vous devez utiliser les valeurs produites par un générateur plusieurs fois et si leur génération coûte plus cher que le stockage, il peut être préférable de stocker les valeurs fournies sous forme de list plutôt que de générer à nouveau la séquence. Voir 'Réinitialiser un générateur' ci-dessous pour plus de détails.

Un objet générateur est généralement utilisé dans une boucle ou dans toute fonction nécessitant une itération:

for x in g1:
    print("Received", x)

# Output:
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81

arr1 = list(g1)
# arr1 = [], because the loop above already consumed all the values.
g2 = function()
arr2 = list(g2)  # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Les objets générateurs étant des itérateurs, on peut les parcourir manuellement en utilisant la fonction next() . Cela renverra les valeurs obtenues une par une à chaque invocation suivante.

Sous le capot, chaque fois que vous appelez next() sur un générateur, Python exécute des instructions dans le corps de la fonction du générateur jusqu'à ce qu'il atteigne la déclaration de yield suivante. À ce stade, il retourne l'argument de la commande de yield et se souvient du point où cela s'est produit. L'appel next() reprendra l'exécution à partir de ce point et continuera jusqu'à la déclaration de yield suivante.

Si Python atteint la fin de la fonction du générateur sans plus de yield s, une exception StopIteration est StopIteration (ceci est normal, tous les itérateurs se comportent de la même manière).

g3 = function()
a = next(g3)  # a becomes 0
b = next(g3)  # b becomes 1
c = next(g3)  # c becomes 2
...
j = next(g3)  # Raises StopIteration, j remains undefined

Notez que dans Python 2, les objets du générateur avaient des méthodes .next() qui pouvaient être utilisées pour parcourir manuellement les valeurs générées. Dans Python 3, cette méthode a été remplacée par la norme .__next__() pour tous les itérateurs.

Réinitialisation d'un générateur

Rappelez-vous que vous ne pouvez parcourir que les objets générés par un générateur une fois . Si vous avez déjà parcouru les objets dans un script, toute nouvelle tentative à cet effet aboutira à None .

Si vous devez utiliser plusieurs fois les objets générés par un générateur, vous pouvez soit définir à nouveau la fonction du générateur et l'utiliser une seconde fois, ou vous pouvez également stocker la sortie de la fonction du générateur dans une liste lors de la première utilisation. La redéfinition de la fonction du générateur sera une bonne option si vous traitez de gros volumes de données et que le stockage d'une liste de tous les éléments de données prendrait beaucoup d'espace disque. À l'inverse, s'il est coûteux de générer les éléments initialement, vous pouvez préférer les stocker dans une liste pour pouvoir les réutiliser.

Utiliser un générateur pour trouver les numéros de Fibonacci

Un cas d'utilisation pratique d'un générateur consiste à parcourir les valeurs d'une série infinie. Voici un exemple de recherche des dix premiers termes de la séquence de Fibonacci .

def fib(a=0, b=1):
    """Generator that yields Fibonacci numbers. `a` and `b` are the seed values"""
    while True:
        yield a
        a, b = b, a + b

f = fib()
print(', '.join(str(next(f)) for _ in range(10)))

0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Séquences infinies

Les générateurs peuvent être utilisés pour représenter des séquences infinies:

def integers_starting_from(n):
    while True:
        yield n
        n += 1

natural_numbers = integers_starting_from(1)

Une séquence infinie de nombres comme ci-dessus peut également être générée à l'aide de itertools.count . Le code ci-dessus pourrait être écrit comme ci-dessous

natural_numbers = itertools.count(1)

Vous pouvez utiliser les compréhensions de générateur sur des générateurs infinis pour produire de nouveaux générateurs:

multiples_of_two = (x * 2 for x in natural_numbers)
multiples_of_three = (x for x in natural_numbers if x % 3 == 0)

Sachez qu’un générateur infini n’a pas de fin, donc le transmettre à une fonction qui essaiera de consommer le générateur entièrement aura des conséquences désastreuses :

list(multiples_of_two)  # will never terminate, or raise an OS-specific error

Au lieu de cela, utilisez list / set compréhensions avec range (ou xrange pour python <3.0):

first_five_multiples_of_three = [next(multiples_of_three) for _ in range(5)] 
# [3, 6, 9, 12, 15]

ou utilisez itertools.islice() pour découper l'itérateur en un sous-ensemble:

from itertools import islice
multiples_of_four = (x * 4 for x in integers_starting_from(1))
first_five_multiples_of_four = list(islice(multiples_of_four, 5))
# [4, 8, 12, 16, 20]

Notez que le générateur d'origine est également mis à jour, comme tous les autres générateurs provenant de la même racine:

next(natural_numbers)    # yields 16
next(multiples_of_two)   # yields 34
next(multiples_of_four)  # yields 24

Une séquence infinie peut également être itérée avec un for -loop . Assurez-vous d'inclure une instruction de break conditionnelle pour que la boucle se termine éventuellement:

for idx, number in enumerate(multiplies_of_two):
    print(number)
    if idx == 9:
        break  # stop after taking the first 10 multiplies of two

Exemple classique - Numéros de Fibonacci

import itertools

def fibonacci():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

first_ten_fibs = list(itertools.islice(fibonacci(), 10))
# [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

def nth_fib(n):
    return next(itertools.islice(fibonacci(), n - 1, n))

ninety_nineth_fib = nth_fib(99)  # 354224848179261915075

Céder toutes les valeurs d'une autre itération

Python 3.x 3.3

Utilisez le yield from si vous souhaitez générer toutes les valeurs d'une autre itération:

def foob(x):
    yield from range(x * 2)
    yield from range(2)

list(foob(5))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1]

Cela fonctionne également avec des générateurs.

def fibto(n):
    a, b = 1, 1
    while True:
        if a >= n: break
        yield a
        a, b = b, a + b

def usefib():
    yield from fibto(10)
    yield from fibto(20)

list(usefib())  # [1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]

Coroutines

Les générateurs peuvent être utilisés pour implémenter des coroutines:

# create and advance generator to the first yield
def coroutine(func):
    def start(*args,**kwargs):
        cr = func(*args,**kwargs)
        next(cr)
        return cr
    return start

# example coroutine
@coroutine
def adder(sum = 0):
    while True:
        x = yield sum
        sum += x

# example use
s = adder()
s.send(1) # 1
s.send(2) # 3

Les coroutines sont couramment utilisées pour implémenter des machines à états, car elles sont principalement utiles pour créer des procédures à méthode unique nécessitant un état pour fonctionner correctement. Ils opèrent sur un état existant et renvoient la valeur obtenue à l'issue de l'opération.

Rendement avec récursivité: liste récursive de tous les fichiers d'un répertoire

Tout d'abord, importez les bibliothèques qui fonctionnent avec les fichiers:

from os import listdir
from os.path import isfile, join, exists

Une fonction d'assistance pour lire uniquement les fichiers d'un répertoire:

def get_files(path):
    for file in listdir(path):
        full_path = join(path, file)
        if isfile(full_path):
            if exists(full_path):
                yield full_path

Une autre fonction d'assistance pour obtenir uniquement les sous-répertoires:

def get_directories(path):
    for directory in listdir(path):
        full_path = join(path, directory)
        if not isfile(full_path):
            if exists(full_path):
                yield full_path

Maintenant, utilisez ces fonctions pour récupérer tous les fichiers dans un répertoire et tous ses sous-répertoires (en utilisant des générateurs):

def get_files_recursive(directory):
    for file in get_files(directory):
        yield file
    for subdirectory in get_directories(directory):
        for file in get_files_recursive(subdirectory): # here the recursive call
            yield file

Cette fonction peut être simplifiée en utilisant le yield from :

def get_files_recursive(directory):
    yield from get_files(directory)
    for subdirectory in get_directories(directory):
        yield from get_files_recursive(subdirectory)

Itérer sur les générateurs en parallèle

Pour itérer plusieurs générateurs en parallèle, utilisez le zip intégré:

for x, y in zip(a,b):
    print(x,y)

Résulte en:

1 x
2 y
3 z

En python 2, utilisez plutôt itertools.izip . Ici, nous pouvons également voir que toutes les fonctions zip donnent des tuples.

Notez que zip cessera d’itérer dès que l’une des itérables sera à court d’éléments. Si vous souhaitez effectuer une itération aussi longue que la plus longue itération, utilisez itertools.zip_longest() .

Code de construction de refactoring

Supposons que vous ayez un code complexe qui crée et renvoie une liste en commençant par une liste vide et en y ajoutant de manière répétée:

def create():
    result = []
    # logic here...
    result.append(value) # possibly in several places
    # more logic...
    return result # possibly in several places

values = create()

Lorsqu'il n'est pas pratique de remplacer la logique interne par une compréhension de liste, vous pouvez transformer l'intégralité de la fonction en générateur, puis collecter les résultats:

def create_gen():
    # logic...
    yield value
    # more logic
    return # not needed if at the end of the function, of course

values = list(create_gen())

Si la logique est récursive, utilisez le yield from pour inclure toutes les valeurs de l'appel récursif dans un résultat "aplati":

def preorder_traversal(node):
    yield node.value
    for child in node.children:
        yield from preorder_traversal(child)

Recherche

La fonction next est utile même sans itération. Passer une expression de générateur à next est un moyen rapide de rechercher la première occurrence d'un élément correspondant à un prédicat. Code procédural comme

def find_and_transform(sequence, predicate, func):
    for element in sequence:
        if predicate(element):
            return func(element)
    raise ValueError

item = find_and_transform(my_sequence, my_predicate, my_func)

peut être remplacé par:

item = next(my_func(x) for x in my_sequence if my_predicate(x))
# StopIteration will be raised if there are no matches; this exception can
# be caught and transformed, if desired.

À cette fin, il peut être souhaitable de créer un alias, tel que first = next , ou une fonction wrapper pour convertir l'exception:

def first(generator):
    try:
        return next(generator)
    except StopIteration:
        raise ValueError


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow