8 votes

Python Diviser le rendement d'un générateur en deux parties

J'ai accès à un générateur qui produit deux valeurs :

def get_document_values():
    docs = query_database()  # returns a cursor to database documents
    for doc in docs:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield doc['x'], doc['y']

J'ai une autre fonction, process_x Je ne peux pas modifier ce système qui peut recevoir un générateur en entrée et qui traite toutes les données de la base de données. x pour tous les documents (si un tuple est produit, il ne traite que le premier élément du tuple et ignore les autres éléments) :

X = process_x(get_document_values())  # This processes x but ignores y

Cependant, j'ai besoin de stocker tous les y du générateur. Ma seule solution est d'exécuter get_document_values deux fois :

Y = [y for x,y in get_document_values()]  #Throw away x
X = process_x(get_document_values())      #Throw away y

Cela fonctionne techniquement, mais lorsqu'il y a de nombreux documents à traiter, il est possible qu'un nouveau document soit inséré dans la base de données et que les longueurs de X et Y sera différente. Il doit y avoir une correspondance univoque entre X et Y et j'aimerais n'avoir qu'à appeler get_document_values une fois au lieu de deux.

J'ai envisagé quelque chose comme :

Y = []

def process_y(doc_generator):
    global Y
    for x,y in doc_generator:
        Y.append(y)
        yield x

X = process_x(process_y(get_document_values()))

Mais.. :

  1. Cela ne semble pas pythonique
  2. Y doit être déclarée comme variable globale

J'espère qu'il existe un moyen plus propre et plus pythonique de faire cela.

Mise à jour

En réalité, get_document_values renverra des valeurs de x qui sont trop volumineux pour être stockés collectivement dans la mémoire et process_x réduit en fait cette exigence de mémoire. Il n'est donc pas possible de mettre en cache l'ensemble des x . La mise en cache de tous les y est très bien.

2voto

Daniel Sanchez Points 2626

Vous mettez en cache toutes les valeurs dans une liste déjà lors de l'appel :

all_values = [(x,y) for x,y in get_document_values()] #or list(get_document_values())

Vous pouvez obtenir un itérateur vers y avec :

Y = map(itemgetter(1), all_values)

Et pour x utilisation simple :

X = process_x(map(itemgetter(0), all_values))

L'autre option consiste à séparer le générateur, par exemple :

def get_document_values(getter):
    docs = query_database()  # returns a cursor to database documents
    for doc in docs:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield getter(doc)

from operator import itemgetter
X = process_x(get_document_values(itemgetter("x")))
Y = list(get_document_values(itemgetter("y")))

Si vous trouvez un moyen d'effectuer la requête une seule fois et de dupliquer le curseur, vous pouvez également l'abstraire :

def get_document_values(cursor, getter):
    for doc in cursor:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield getter(doc)

1voto

Reut Sharabani Points 1353

Il n'est pas nécessaire de sauvegarder les données :

def process_entry(x, y):
    process_x((x,))
    return y

ys = itertools.starmap(process_entry, your_generator)

N'oubliez pas que seulement lorsque vous obtenez un y sa valeur correspondante x est traitée.

Si vous avez utilisé les deux, renvoyez-les sous la forme d'un tuple :

def process_entry(x, y):
    return next(process_x((x,))), y

0voto

voldmar Points 36

Vous pouvez utiliser itertools.tee pour faire deux itérateurs à partir d'un seul, puis utiliser un itérateur pour process_x et le second pour l'autre objectif

0voto

r.ook Points 6810

Ce n'est probablement pas pythonique, mais vous pouvez tricher un peu s'il est permis de modifier légèrement le générateur principal et d'utiliser son attribut de fonction :

from random import randrange
def get_vals():
        # mock creation of a x/y dict list
        docs =[{k: k+str(randrange(50)) for k in ('x','y')} for _ in range(10)]
        # create a function list attribute
        get_vals.y = []
        for doc in docs:
            # store the y value into the attribute
            get_vals.y.append(doc['y'])
            yield doc['x'], doc['y']  
            # if doc['y'] is purely for storage, you  might opt to not yield it at all.

Testez-le :

# mock the consuming of generator for process_x            
for i in get_vals():
    print(i)    
# ('x13', 'y9'), ('x15', 'y40'), ('x41', 'y49')...

# access the ys stored in get_val function attribute after consumption
print(get_vals.y)
# ['y9', 'y40', 'y49', ...]

# instantiate the generator a second time a la process_x...
for i in get_vals():
    print(i)
# ('x18', 'y0'), ('x6', 'y35'), ('x24', 'y45')...

# access the cached y again
print(get_vals.y)
# ['y0', 'y35', 'y45', ...] 
  1. Cela permet de mettre en cache les valeurs y pendant que le générateur émet son x à chaque appel.
  2. Et il élimine votre global mot-clé
  3. Et vous pouvez être sûr que la cartographie x/y est exacte.

Certains pourraient considérer qu'il s'agit d'un hack, mais j'aime à penser qu'il s'agit d'une fonctionnalité, puisque tout en Python est un objet, ce qui permet de s'en sortir...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X