Os iteradores podem ser redefinidos no Python?


Respostas:


83

Vejo muitas respostas sugerindo itertools.tee , mas isso é ignorar um aviso crucial nos documentos para isso:

Essa ferramenta pode exigir armazenamento auxiliar significativo (dependendo da quantidade de dados temporários que precisam ser armazenados). Em geral, se um iterador usa a maioria ou todos os dados antes de mais iteradoras começa, é mais rápido para usar list()em vez de tee().

Basicamente, ele teeé projetado para situações em que dois (ou mais) clones de um iterador, embora "saindo da sincronia" um com o outro, não o fazem muito - eles dizem na mesma "vizinhança" (um alguns itens atrás ou à frente um do outro). Não é adequado para o problema do OP de "refazer desde o início".

L = list(DictReader(...))por outro lado, é perfeitamente adequado, desde que a lista de ditados possa caber confortavelmente na memória. Um novo "iterador desde o início" (muito leve e de baixo custo) pode ser criado a qualquer momento iter(L)e usado em parte ou no todo, sem afetar os novos ou os existentes; outros padrões de acesso também estão facilmente disponíveis.

Como várias respostas comentaram corretamente, no caso específico de csvvocê também é possível .seek(0)o objeto de arquivo subjacente (um caso bastante especial). Não sei se isso está documentado e garantido, embora atualmente funcione; provavelmente valeria a pena considerar apenas os arquivos csv realmente enormes, nos quais listrecomendo que a abordagem geral tenha uma área de armazenamento de memória muito grande.


6
Usar o list()cache de várias páginas em um arquivo csvreader em um arquivo de 5 MB faz com que meu tempo de execução vá de ~ 12s para ~ 0,5s.
John Mee

33

Se você tiver um arquivo csv chamado 'blah.csv', será semelhante a

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

você sabe que pode abrir o arquivo para leitura e criar um DictReader com

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

Então, você poderá obter a próxima linha com reader.next(), que deve gerar

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

usá-lo novamente produzirá

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

No entanto, neste momento, se você usar blah.seek(0), na próxima vez que ligar, reader.next()você receberá

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

novamente.

Essa parece ser a funcionalidade que você está procurando. Tenho certeza de que existem alguns truques associados a essa abordagem, dos quais não estou ciente. @ Brian sugeriu simplesmente criar outro DictReader. Isso não funcionará se você for o primeiro leitor a meio da leitura do arquivo, pois seu novo leitor terá chaves e valores inesperados em qualquer lugar do arquivo.


Foi o que minha teoria me disse, bom ver que o que eu pensei que deveria acontecer, acontece.
Wayne Werner

@Wilduck: o comportamento que você está descrevendo com outra instância do DictReader não acontecerá se você criar um novo arquivo e passar isso para o segundo DictReader, certo?

Se você tiver dois manipuladores de arquivos, eles se comportarão independentemente, sim.
Wilduck

24

Não. O protocolo do iterador do Python é muito simples e fornece apenas um único método ( .next()ou __next__()) e nenhum método para redefinir um iterador em geral.

O padrão comum é criar um novo iterador usando o mesmo procedimento novamente.

Se você deseja "salvar" um iterador para poder voltar ao início, também pode bifurcar o iterador usando itertools.tee


1
Enquanto sua análise do método .next () provavelmente está correta, existe uma maneira bastante simples de obter o que a operação está pedindo.
Wilduck 16/07/10

2
@Wilduck: Eu vejo que a sua resposta. Acabei de responder à pergunta do iterador e não faço ideia do csvmódulo. Espero que ambas as respostas sejam úteis para o pôster original.
U0b34a0f6ae 16/07

Estritamente, o protocolo do iterador também exige __iter__. Ou seja, os iteradores também precisam ser iteráveis.
Steve Jessop

11

Sim , se você usar numpy.nditerpara criar seu iterador.

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

Pode nditerpercorrer a matriz como itertools.cycle?
LWZ 24/08

1
@LWZ: Eu não penso assim, mas você pode try:o next()e em uma StopIterationexceção fazer reset().
Pausado até novo aviso.

... seguido de umnext()
Pausado até novo aviso.

Era isso que eu estava procurando!
Sr-

1
Observe que o limite de "operandos" aqui é 32: stackoverflow.com/questions/51856685/…
Simon

11

Existe um erro no uso, .seek(0)conforme defendido por Alex Martelli e Wilduck acima, a saber, a próxima chamada para .next()fornecer um dicionário da linha do cabeçalho na forma de {key1:key1, key2:key2, ...}. A solução é seguir file.seek(0)com uma chamada reader.next()para se livrar da linha do cabeçalho.

Portanto, seu código seria algo como isto:

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

Talvez isso seja ortogonal à pergunta original, mas é possível agrupar o iterador em uma função que retorna o iterador.

def get_iter():
    return iterator

Para redefinir o iterador, basta chamar a função novamente. É claro que isso é trivial se a função quando a referida função não aceita argumentos.

Caso a função exija alguns argumentos, use functools.partial para criar um fechamento que possa ser passado em vez do iterador original.

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

Isso parece evitar o armazenamento em cache que tee (n cópias) ou lista (1 cópia) precisariam fazer


3

Para arquivos pequenos, considere usar more_itertools.seekable- uma ferramenta de terceiros que oferece a possibilidade de redefinir iteráveis.

Demo

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))

Resultado

{'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'}

Aqui a DictReaderé envolvido em um seekableobjeto (1) e avançado (2). O seek()método é usado para redefinir / rebobinar o iterador para a 0a posição (3).

Nota: o consumo de memória aumenta com a iteração; portanto, tenha cuidado ao aplicar esta ferramenta a arquivos grandes, conforme indicado nos documentos .


2

Embora não haja redefinição do iterador, o módulo "itertools" do python 2.6 (e posterior) possui alguns utilitários que podem ajudá-lo. Um deles é o "tee", que pode fazer várias cópias de um iterador e armazenar em cache os resultados do que está sendo executado adiante, para que esses resultados sejam usados ​​nas cópias. Vou cumprir seus propósitos:

>>> 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

Para DictReader:

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

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

Para 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()) retorna todos os valores restantes para um gerador e o redefine efetivamente se não estiver em loop.


1

Problema

Eu já tive o mesmo problema antes. Depois de analisar meu código, percebi que tentar redefinir o iterador dentro de loops aumenta um pouco a complexidade do tempo e também torna o código um pouco feio.

Solução

Abra o arquivo e salve as linhas em uma variável na memória.

# 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)

Agora você pode percorrer as linhas em qualquer lugar do seu escopo sem precisar lidar com um iterador.


1

Uma opção possível é usar itertools.cycle(), o que permitirá iterar indefinidamente, sem nenhum truque .seek(0).

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

1

Estou chegando ao mesmo problema - embora goste da tee()solução, não sei qual será o tamanho dos meus arquivos e os avisos de memória sobre o consumo de um primeiro antes do outro me impedem de adotar esse método.

Em vez disso, estou criando um par de iteradores usando iter()instruções e usando o primeiro para o meu detalhamento inicial, antes de mudar para o segundo para o final.

Portanto, no caso de um dict-reader, se o leitor for definido usando:

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

Eu posso criar um par de iteradores a partir desta "especificação" - usando:

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

Posso então executar meu código de 1ª passagem d1, seguro, sabendo que o segundo iterador d2foi definido a partir da mesma especificação raiz.

Eu não testei isso exaustivamente, mas parece funcionar com dados fictícios.



0

Retornar um iterador recém-criado na última iteração durante a chamada '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=' ')

Resultado:

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 
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.