Redefinindo o objeto gerador em Python


153

Eu tenho um objeto gerador retornado por vários rendimentos. A preparação para chamar esse gerador é uma operação bastante demorada. É por isso que quero reutilizar o gerador várias vezes.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

Claro, estou pensando em copiar o conteúdo para uma lista simples. Existe uma maneira de redefinir meu gerador?

Respostas:


119

Outra opção é usar a itertools.tee()função para criar uma segunda versão do seu gerador:

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

Isso pode ser benéfico do ponto de vista do uso de memória, se a iteração original não puder processar todos os itens.


33
Se você está se perguntando o que ele fará nesse caso, está essencialmente armazenando elementos em cache na lista. Portanto, você também pode usar y = list(y)o restante do seu código inalterado.
ilya n.

5
tee () criará uma lista internamente para armazenar os dados, então é o mesmo que fiz na minha resposta.
Nosklo 13/08/09

6
Olhada implmentation ( docs.python.org/library/itertools.html#itertools.tee ) - este usa estratégia de carga lenta, então itens à lista copiado somente sob demanda
Dewfy

11
@ Dewfy: que será mais lento, pois todos os itens terão que ser copiados de qualquer maneira.
nosklo

8
sim, list () é melhor nesse caso. tee só é útil se você não está consumindo toda a lista
gravitação

148

Geradores não podem ser rebobinados. Você tem as seguintes opções:

  1. Execute a função do gerador novamente, reiniciando a geração:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Armazene os resultados do gerador em uma estrutura de dados na memória ou no disco que você pode iterar novamente:

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

A desvantagem da opção 1 é que ela calcula os valores novamente. Se isso exige muita CPU, você acaba calculando duas vezes. Por outro lado, a desvantagem de 2 é o armazenamento. A lista inteira de valores será armazenada na memória. Se houver muitos valores, isso pode ser impraticável.

Então você tem a troca clássica de memória versus processamento . Não consigo imaginar uma maneira de rebobinar o gerador sem armazenar os valores ou calculá-los novamente.


Pode existir uma maneira de salvar a assinatura da chamada de função? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: certeza: def call_my_func (): FunctionWithYield retorno (param1, param2)
nosklo

@ Dewfy O que você quer dizer com "salvar assinatura de chamada de função"? Você poderia explicar? Você quer dizer salvar os parâmetros passados ​​para o gerador?
Андрей Беньковский

2
Outra desvantagem de (1) é também que FunctionWithYield () pode ser não apenas caro, mas impossível de recalcular, por exemplo, se estiver lendo a partir de stdin.
Max

2
Para ecoar o que o @Max disse, se a saída da função puder (ou irá) mudar entre as chamadas, (1) poderá gerar resultados inesperados e / ou indesejáveis.
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

Provavelmente, a solução mais simples é envolver a peça cara em um objeto e passá-la ao gerador:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

Dessa forma, você pode armazenar em cache os cálculos caros.

Se você puder manter todos os resultados na RAM ao mesmo tempo, use list()para materializar os resultados do gerador em uma lista simples e trabalhe com isso.


23

Quero oferecer uma solução diferente para um problema antigo

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

O benefício disso, quando comparado a algo semelhante, list(iterator)é que isso é O(1)complexidade do espaço e list(iterator)é O(n). A desvantagem é que, se você tiver apenas acesso ao iterador, mas não a função que produziu o iterador, não poderá usar esse método. Por exemplo, pode parecer razoável fazer o seguinte, mas não funcionará.

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@ Dewfy No primeiro trecho, o gerador está na linha "quadrados = ...". Expressões de gerador se comportam da mesma maneira que chamar uma função que usa yield, e eu usei apenas uma porque é menos detalhada do que escrever uma função com yield para um exemplo tão curto. No segundo trecho, usei FunctionWithYield como o generator_factory, portanto ele será chamado sempre que o iter for chamado, ou seja, sempre que eu escrever "para x em y".
217166 michaelsnowden

Boa solução. Na verdade, isso torna um objeto iterável sem estado em vez de um objeto iterador com estado, para que o próprio objeto seja reutilizável. Especialmente útil se você deseja passar um objeto iterável para uma função e essa função usará o objeto várias vezes.
Cosyn

5

Se a resposta de GrzegorzOledzki não for suficiente, você provavelmente poderia usar send()para atingir seu objetivo. Consulte PEP-0342 para obter mais detalhes sobre geradores aprimorados e expressões de rendimento.

UPDATE: Veja também itertools.tee(). Envolve parte dessa troca de memória versus processamento mencionada acima, mas pode economizar memória ao armazenar apenas os resultados do gerador em a list; depende de como você está usando o gerador.


5

Se o seu gerador é puro, no sentido de que sua saída depende apenas dos argumentos passados ​​e do número da etapa, e você deseja que o gerador resultante seja reiniciado, aqui está um trecho de classificação que pode ser útil:

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

saídas:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
0
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1

3

Da documentação oficial do tee :

Em geral, se um iterador usa a maioria ou todos os dados antes de outro iterador iniciar, é mais rápido usar list () em vez de tee ().

Portanto, é melhor usar list(iterable)no seu caso.


6
e os geradores infinitos?
Dewfy 08/09

1
Velocidade não é a única consideração; list()coloca toda a iteráveis na memória
Chris_Rands

@ Chris_Rands O mesmo acontecerá tee()se um iterador consumir todos os valores - é assim que teefunciona.
AChampion

2
@Dewfy: para geradores infinitas, a solução da utilização Aaron Digulla (função ExpensiveSetup retornar os dados preciosos.)
Jeff Learman

3

Usando uma função de wrapper para manipular StopIteration

Você pode escrever uma função de wrapper simples na função de geração do gerador que rastreia quando o gerador está esgotado. Isso será feito usando a StopIterationexceção que um gerador lança quando atinge o final da iteração.

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

Como você pode ver acima, quando nossa função wrapper captura uma StopIterationexceção, ela simplesmente reinicializa o objeto gerador (usando outra instância da chamada de função).

E então, supondo que você defina sua função de fornecimento de gerador em algum lugar como abaixo, você pode usar a sintaxe do decorador da função Python para envolvê-la implicitamente:

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

Você pode definir uma função que retorne seu gerador

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

Agora você pode fazer quantas vezes quiser:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
Obrigado pela resposta, mas o ponto principal da questão era evitar a criação , invocando função interna apenas esconde criação - criá-lo duas vezes
Dewfy

1

Não sei ao certo o que você quis dizer com preparação cara, mas acho que você realmente

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

Se for esse o caso, por que não reutilizar data?


1

Não há opção para redefinir iteradores. O iterador geralmente aparece quando itera através da next()função. A única maneira é fazer um backup antes de iterar no objeto iterador. Confira abaixo.

Criando objeto iterador com itens de 0 a 9

i=iter(range(10))

Iterando através da função next () que será exibida

print(next(i))

Convertendo o objeto iterador na lista

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

então o item 0 já foi exibido. Todos os itens também são exibidos quando convertemos o iterador em lista.

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

Portanto, você precisa converter o iterador em listas para backup antes de começar a iterar. A lista pode ser convertida em iterador comiter(<list-object>)


1

Agora você pode usar more_itertools.seekable(uma ferramenta de terceiros) que permite redefinir os iteradores.

Instalar via > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

Nota: o consumo de memória aumenta ao avançar o iterador, portanto, tenha cuidado com iteráveis ​​grandes.


1

Você pode fazer isso usando itertools.cycle (). Você pode criar um iterador com esse método e, em seguida, executar um loop for sobre o iterador, que fará um loop sobre seus valores.

Por exemplo:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

irá gerar 20 números, de 0 a 4 repetidamente.

Uma observação dos documentos:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1 porque ele funciona, mas vejo 2 questões há 1) grande consumo de memória desde estados de documentação "criar uma cópia de" 2) Loop infinito definitivamente não é o que eu quero
Dewfy

0

Ok, você diz que deseja ligar para um gerador várias vezes, mas a inicialização é cara ... E algo assim?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

Como alternativa, você pode criar sua própria classe que segue o protocolo do iterador e define algum tipo de função 'reset'.

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


Você acabou de delegar o problema ao wrapper. Suponha que a inicialização cara crie um gerador. A minha pergunta era sobre como redefinir dentro do seu.__call__
Dewfy

Adicionado um segundo exemplo em resposta ao seu comentário. Este é essencialmente um gerador personalizado com um método de redefinição.
tvt173

0

Minha resposta resolve um problema ligeiramente diferente: se o gerador é caro para inicializar e cada objeto gerado é caro para gerar. Mas precisamos consumir o gerador várias vezes em várias funções. Para chamar o gerador e cada objeto gerado exatamente uma vez, podemos usar threads e executar cada um dos métodos de consumo em threads diferentes. Podemos não alcançar um verdadeiro paralelismo devido ao GIL, mas alcançaremos nosso objetivo.

Essa abordagem fez um bom trabalho no seguinte caso: o modelo de aprendizado profundo processa muitas imagens. O resultado são muitas máscaras para muitos objetos na imagem. Cada máscara consome memória. Temos cerca de 10 métodos que produzem estatísticas e métricas diferentes, mas eles capturam todas as imagens de uma só vez. Todas as imagens não cabem na memória. Os métodos podem ser reescritos facilmente para aceitar o iterador.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

Uso:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

Você acabou de reinventar itertools.isliceou para assíncrono aiostream.stream.take, e este post permite fazê-lo de maneira assíncrona / aguardada stackoverflow.com/a/42379188/149818
Dewfy 08/07

-3

Isso pode ser feito por objeto de código. Aqui está o exemplo.

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4


4
bem, na verdade, era necessário redefinir o gerador para evitar a execução duas vezes do código de inicialização. Sua abordagem (1) executa a inicialização duas vezes de qualquer maneira, (2) envolve execisso ligeiramente não recomendado para casos tão simples.
Dewfy 27/08
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.