2013-01-17 12 views
5

Ho una sequenza di generatori: (gen_0, gen_1, ... gen_n)Attraversando una sequenza di generatori

Questi generatori creeranno i valori pigramente ma sono limitate e avranno lunghezze potenzialmente diversi.

Devo essere in grado di costruire un altro generatore che produce il primo elemento di ciascun generatore in ordine, seguito dal secondo e così via, saltando i valori dai generatori che sono stati esauriti.

Credo che questo problema è analogo a prendere tupla

((1, 4, 7, 10, 13, 16), (2, 5, 8, 11, 14), (3, 6, 9, 12, 15, 17, 18)) 

e attraversa in modo che dovesse produrre i numeri da 1 a 18 in ordine.

Sto lavorando alla risoluzione di questo semplice esempio utilizzando (genA, genB, genC) con valori di resa genA da (1, 4, 7, 10, 13, 16), generazione di genB (2, 5, 8, 11 , 14) e resa genC (3, 6, 9, 12, 15, 17, 18).

Per risolvere il problema più semplice con la tupla di tuple, la risposta è abbastanza semplice se gli elementi della tupla hanno la stessa lunghezza. Se la variabile 'a' di cui la tupla, è possibile utilizzare

[i for t in zip(*a) for i in t] 

Purtroppo le voci non sono necessariamente la stessa lunghezza e il trucco zip non sembra funzionare per i generatori comunque.

Finora il mio codice è orribilmente brutto e non riesco a trovare nulla che si avvicini ad una soluzione pulita. Aiuto?

+0

'itertools.izip_longest'; puoi passare una sentinella a tamponare i generatori che si esauriscono. Se lo desideri, puoi filtrare quel sentinella dai risultati. – katrielalex

risposta

1

Si potrebbe considerare itertools.izip_longest, ma nel caso in cui nessuno è un valore valido, che la soluzione non riuscirà. Ecco un esempio "un altro generatore", che fa esattamente quello che hai chiesto, ed è abbastanza pulito:

def my_gen(generators): 
    while True: 
     rez =() 
     for gen in generators: 
      try: 
       rez = rez + (gen.next(),) 
      except StopIteration: 
       pass 
     if rez: 
      yield rez 
     else: 
      break 

print [x for x in my_gen((iter(xrange(2)), iter(xrange(3)), iter(xrange(1))))] 

[(0, 0, 0), (1, 1), (2,)] #output 
+0

Puoi usare 'iter (intervallo (x))' invece di 'simple_gen (x)'. –

+0

O meglio ancora: 'iter (xrange (x))' su Python 2.x. – phant0m

+0

Grazie, sistemerò. – kaspersky

8

Penso che è necessario itertools.izip_longest

>>> list([e for e in t if e is not None] for t in itertools.izip_longest(*some_gen, 
                   fillvalue=None)) 
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17], [18]] 
>>> 
+2

Suggerisco di usare "se e non è nessuno". – kaspersky

+2

Inoltre, cosa succede se None era un valore valido? – kaspersky

+0

È facile gestire il caso di "Nessuno" e mantenere questo approccio.Aggiungi semplicemente una riga 'sentinel = object()' e poi usa 'e non è sentinel' e' fillvalue = sentinel'. – DSM

2

Un'altra opzione itertools se li è crollato in un unico elenco che si desidera; questo (come @ gg.kaspersky ha già indicato in un'altra discussione) non gestisce i valori generati None.

g = (generator1, generator2, generator3) 

res = [e for e in itertools.chain(*itertools.izip_longest(*g)) if e is not None] 
print res 

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] 
4

Se si guarda la documentazione per itertools.izip_longest, vedrai che dà un'implementazione puro Python. E 'facile da modificare questa implementazione in modo da produrre i risultati desiderati invece (cioè, come izip_longest, ma senza alcuna fillvalue):

class ZipExhausted(Exception): 
    pass 

def izip_longest_nofill(*args): 
    """ 
    Return a generator whose .next() method returns a tuple where the 
    i-th element comes from the i-th iterable argument that has not 
    yet been exhausted. The .next() method continues until all 
    iterables in the argument sequence have been exhausted and then it 
    raises StopIteration. 

    >>> list(izip_longest_nofill(*[xrange(i,2*i) for i in 2,3,5])) 
    [(2, 3, 5), (3, 4, 6), (5, 7), (8,), (9,)] 
    """ 
    iterators = map(iter, args) 
    def zip_next(): 
     i = 0 
     while i < len(iterators): 
      try: 
       yield next(iterators[i]) 
       i += 1 
      except StopIteration: 
       del iterators[i] 
     if i == 0: 
      raise ZipExhausted 
    try: 
     while iterators: 
      yield tuple(zip_next()) 
    except ZipExhausted: 
     pass 

Questo evita la necessità di ri-filtro all'uscita del izip_longest scartare i valori di riempimento. In alternativa, se si desidera un'uscita "appiattita":

def iter_round_robin(*args): 
    """ 
    Return a generator whose .next() method cycles round the iterable 
    arguments in turn (ignoring ones that have been exhausted). The 
    .next() method continues until all iterables in the argument 
    sequence have been exhausted and then it raises StopIteration. 

    >>> list(iter_round_robin(*[xrange(i) for i in 2,3,5])) 
    [0, 0, 0, 1, 1, 1, 2, 2, 3, 4] 
    """ 
    iterators = map(iter, args) 
    while iterators: 
     i = 0 
     while i < len(iterators): 
      try: 
       yield next(iterators[i]) 
       i += 1 
      except StopIteration: 
       del iterators[i]