Les itérateurs peuvent-ils être réinitialisés en Python?


130

Puis-je réinitialiser un itérateur / générateur en Python? J'utilise DictReader et je souhaite le réinitialiser au début du fichier.



Sur une note latérale, j'ai trouvé que la list()fonction itérera à travers son argument (un itérable). Ainsi, en appelant list()deux fois le même itérable (par exemple le résultat de zip()), vous obtiendrez une liste vide au deuxième appel!
theaws.blog

Réponses:


84

Je vois de nombreuses réponses suggérant itertools.tee , mais cela ignore un avertissement crucial dans la documentation à ce sujet:

Cet itertool peut nécessiter un stockage auxiliaire important (en fonction de la quantité de données temporaires à stocker). En général, si un itérateur utilise la plupart ou toutes les données avant qu'un autre itérateur ne démarre, il est plus rapide à utiliser list()au lieu de tee().

Fondamentalement, teeest conçu pour les situations où deux (ou plus) clones d'un itérateur, tout en "se désynchronisant" l'un avec l'autre, ne le font pas de beaucoup - plutôt, disent-ils dans le même "voisinage" (un quelques éléments derrière ou devant les uns des autres). Ne convient pas au problème de l'OP de "refaire depuis le début".

L = list(DictReader(...))d'autre part est parfaitement adapté, tant que la liste des dictionnaires peut tenir confortablement dans la mémoire. Un nouvel «itérateur dès le départ» (très léger et peu encombrant) peut être créé à tout moment avec iter(L), et utilisé en partie ou en totalité sans affecter les nouveaux ou existants; d'autres modèles d'accès sont également facilement disponibles.

Comme plusieurs réponses l'ont fait remarquer à juste titre, dans le cas spécifique de csvvous pouvez également .seek(0)l'objet de fichier sous-jacent (un cas assez particulier). Je ne suis pas sûr que ce soit documenté et garanti, bien que cela fonctionne actuellement; cela ne vaudrait probablement la peine de considérer que pour les fichiers csv vraiment énormes, dans lesquels listje recommande car l'approche générale aurait une empreinte mémoire trop importante.


6
Utiliser list()pour mettre en cache le multipassage sur un csvreader sur un fichier de 5 Mo voit mon exécution passer de ~ 12secs à ~ 0.5s.
John Mee

33

Si vous avez un fichier csv nommé 'blah.csv' qui ressemble à

a,b,c,d
1,2,3,4
2,3,4,5
3,4,5,6

vous savez que vous pouvez ouvrir le fichier pour le lire et créer un DictReader avec

blah = open('blah.csv', 'r')
reader= csv.DictReader(blah)

Ensuite, vous pourrez obtenir la ligne suivante avec reader.next(), qui devrait afficher

{'a':1,'b':2,'c':3,'d':4}

l'utiliser à nouveau produira

{'a':2,'b':3,'c':4,'d':5}

Cependant, à ce stade, si vous utilisez blah.seek(0), la prochaine fois que vous appelez, reader.next()vous obtiendrez

{'a':1,'b':2,'c':3,'d':4}

encore.

Cela semble être la fonctionnalité que vous recherchez. Je suis sûr qu'il y a quelques astuces associées à cette approche dont je ne suis pas au courant cependant. @Brian a suggéré de créer simplement un autre DictReader. Cela ne fonctionnera pas si votre premier lecteur est à mi-chemin de la lecture du fichier, car votre nouveau lecteur aura des clés et des valeurs inattendues où que vous soyez dans le fichier.


C'est ce que ma théorie m'a dit, agréable de voir que ce que je pensais devrait arriver, le fait.
Wayne Werner

@Wilduck: le comportement que vous décrivez avec une autre instance de DictReader ne se produira pas si vous créez un nouveau descripteur de fichier et que vous le transmettez au deuxième DictReader, n'est-ce pas?

Si vous avez deux gestionnaires de fichiers, ils se comporteront indépendamment, oui.
Wilduck

24

Le protocole d'itérateur de Python est très simple et ne fournit qu'une seule méthode ( .next()ou __next__()), et aucune méthode pour réinitialiser un itérateur en général.

Le modèle courant consiste à créer à nouveau un nouvel itérateur en utilisant la même procédure.

Si vous souhaitez "enregistrer" un itérateur afin de pouvoir revenir à son début, vous pouvez également bifurquer l'itérateur en utilisant itertools.tee


1
Bien que votre analyse de la méthode .next () soit probablement correcte, il existe un moyen assez simple d'obtenir ce que l'op demande.
Wilduck

2
@Wilduck: Je vois que votre réponse. Je viens de répondre à la question de l'itérateur, et je n'ai aucune idée du csvmodule. Espérons que les deux réponses seront utiles à l'affiche originale.
u0b34a0f6ae

Strictement, le protocole itérateur nécessite également __iter__. Autrement dit, les itérateurs doivent également être itérables.
Steve Jessop

11

Oui , si vous utilisez numpy.nditerpour construire votre itérateur.

>>> lst = [1,2,3,4,5]
>>> itr = numpy.nditer([lst])
>>> itr.next()
1
>>> itr.next()
2
>>> itr.finished
False
>>> itr.reset()
>>> itr.next()
1

Pouvez-vous nditerparcourir le tableau comme itertools.cycle?
LWZ

1
@LWZ: Je ne pense pas, mais vous pouvez try:le next()et sur une StopIterationexception faire reset().
Suspendu jusqu'à nouvel ordre.

... suivi d'unnext()
pause jusqu'à nouvel ordre.

C'est ce que je cherchais !
sriram

1
Notez que la limite des "opérandes" ici est de 32: stackoverflow.com/questions/51856685/…
Simon

11

Il y a un bogue dans l'utilisation .seek(0)comme préconisé par Alex Martelli et Wilduck ci-dessus, à savoir que le prochain appel à .next()vous donnera un dictionnaire de votre ligne d'en-tête sous la forme de {key1:key1, key2:key2, ...}. Le travail autour consiste à suivre file.seek(0)avec un appel à reader.next()pour se débarrasser de la ligne d'en-tête.

Donc, votre code ressemblerait à ceci:

f_in = open('myfile.csv','r')
reader = csv.DictReader(f_in)

for record in reader:
    if some_condition:
        # reset reader to first row of data on 2nd line of file
        f_in.seek(0)
        reader.next()
        continue
    do_something(record)

5

C'est peut-être orthogonal à la question d'origine, mais on pourrait envelopper l'itérateur dans une fonction qui retourne l'itérateur.

def get_iter():
    return iterator

Pour réinitialiser l'itérateur, il suffit de rappeler la fonction. Ceci est bien sûr trivial si la fonction lorsque ladite fonction ne prend aucun argument.

Dans le cas où la fonction nécessite des arguments, utilisez functools.partial pour créer une fermeture qui peut être passée à la place de l'itérateur d'origine.

def get_iter(arg1, arg2):
   return iterator
from functools import partial
iter_clos = partial(get_iter, a1, a2)

Cela semble éviter la mise en cache que le tee (n copies) ou la liste (1 copie) aurait besoin de faire


3

Pour les petits fichiers, vous pouvez envisager d'utiliser more_itertools.seekable- un outil tiers qui propose des itérables de réinitialisation.

Démo

import csv

import more_itertools as mit


filename = "data/iris.csv"
with open(filename, "r") as f:
    reader = csv.DictReader(f)
    iterable = mit.seekable(reader)                    # 1
    print(next(iterable))                              # 2
    print(next(iterable))
    print(next(iterable))

    print("\nReset iterable\n--------------")
    iterable.seek(0)                                   # 3
    print(next(iterable))
    print(next(iterable))
    print(next(iterable))

Production

{'Sepal width': '3.5', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '5.1', 'Species': 'Iris-setosa'}
{'Sepal width': '3', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '4.9', 'Species': 'Iris-setosa'}
{'Sepal width': '3.2', 'Petal width': '0.2', 'Petal length': '1.3', 'Sepal length': '4.7', 'Species': 'Iris-setosa'}

Reset iterable
--------------
{'Sepal width': '3.5', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '5.1', 'Species': 'Iris-setosa'}
{'Sepal width': '3', 'Petal width': '0.2', 'Petal length': '1.4', 'Sepal length': '4.9', 'Species': 'Iris-setosa'}
{'Sepal width': '3.2', 'Petal width': '0.2', 'Petal length': '1.3', 'Sepal length': '4.7', 'Species': 'Iris-setosa'}

Ici, a DictReaderest enveloppé dans un seekableobjet (1) et avancé (2). La seek()méthode est utilisée pour réinitialiser / rembobiner l'itérateur à la position 0 (3).

Remarque: la consommation de mémoire augmente avec l'itération, alors méfiez-vous en appliquant cet outil à des fichiers volumineux, comme indiqué dans la documentation .


2

Bien qu'il n'y ait pas de réinitialisation d'itérateur, le module "itertools" de python 2.6 (et plus tard) a quelques utilitaires qui peuvent y aider. L'un d'eux est le «tee» qui peut faire plusieurs copies d'un itérateur, et mettre en cache les résultats de celui qui avance, de sorte que ces résultats soient utilisés sur les copies. Je vais répondre à vos objectifs:

>>> def printiter(n):
...   for i in xrange(n):
...     print "iterating value %d" % i
...     yield i

>>> from itertools import tee
>>> a, b = tee(printiter(5), 2)
>>> list(a)
iterating value 0
iterating value 1
iterating value 2
iterating value 3
iterating value 4
[0, 1, 2, 3, 4]
>>> list(b)
[0, 1, 2, 3, 4]

1

Pour DictReader:

f = open(filename, "rb")
d = csv.DictReader(f, delimiter=",")

f.seek(0)
d.__init__(f, delimiter=",")

Pour DictWriter:

f = open(filename, "rb+")
d = csv.DictWriter(f, fieldnames=fields, delimiter=",")

f.seek(0)
f.truncate(0)
d.__init__(f, fieldnames=fields, delimiter=",")
d.writeheader()
f.flush()

1

list(generator()) renvoie toutes les valeurs restantes pour un générateur et le réinitialise effectivement s'il n'est pas en boucle.


1

Problème

J'ai déjà eu le même problème. Après avoir analysé mon code, j'ai réalisé que tenter de réinitialiser l'itérateur à l'intérieur des boucles augmentait légèrement la complexité temporelle et rendait également le code un peu moche.

Solution

Ouvrez le fichier et enregistrez les lignes dans une variable en mémoire.

# initialize list of rows
rows = []

# open the file and temporarily name it as 'my_file'
with open('myfile.csv', 'rb') as my_file:

    # set up the reader using the opened file
    myfilereader = csv.DictReader(my_file)

    # loop through each row of the reader
    for row in myfilereader:
        # add the row to the list of rows
        rows.append(row)

Vous pouvez désormais parcourir les lignes n'importe où dans votre portée sans avoir à utiliser d'itérateur.


1

Une option possible est d'utiliser itertools.cycle(), ce qui vous permettra d'itérer indéfiniment sans aucune astuce comme .seek(0).

iterDic = itertools.cycle(csv.DictReader(open('file.csv')))

1

J'arrive à ce même problème - bien que j'aime la tee()solution, je ne sais pas quelle sera la taille de mes fichiers et les avertissements de mémoire concernant la consommation de l'un avant l'autre me découragent d'adopter cette méthode.

Au lieu de cela, je crée une paire d'itérateurs en utilisant des iter()instructions, et en utilisant le premier pour mon passage initial, avant de passer au second pour l'exécution finale.

Donc, dans le cas d'un lecteur de dict, si le lecteur est défini en utilisant:

d = csv.DictReader(f, delimiter=",")

Je peux créer une paire d'itérateurs à partir de cette "spécification" - en utilisant:

d1, d2 = iter(d), iter(d)

Je peux ensuite exécuter mon code de 1ère passe d1, en sachant que le deuxième itérateur d2a été défini à partir de la même spécification racine.

Je n'ai pas testé cela de manière exhaustive, mais cela semble fonctionner avec des données factices.



0

Renvoie un itérateur nouvellement créé à la dernière itération lors de l'appel 'iter ()'

class ResetIter: 
  def __init__(self, num):
    self.num = num
    self.i = -1

  def __iter__(self):
    if self.i == self.num-1: # here, return the new object
      return self.__class__(self.num) 
    return self

  def __next__(self):
    if self.i == self.num-1:
      raise StopIteration

    if self.i <= self.num-1:
      self.i += 1
      return self.i


reset_iter = ResetRange(10)
for i in reset_iter:
  print(i, end=' ')
print()

for i in reset_iter:
  print(i, end=' ')
print()

for i in reset_iter:
  print(i, end=' ')

Production:

0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9 
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.