2012-08-23 4 views
13

Da un previous question ho imparato qualcosa di interessante. Se Python itertools.product viene alimentato con una serie di iteratori, questi iteratori verranno convertiti in tuple prima del. Inizia il prodotto cartesiano. Relatedquestions guardare il codice sorgente di itertools.product per concludere che, mentre nessun risultato intermedio è memorizzato in memoria, le versioni di tuple degli iteratori originali vengono create prima dell'inizio dell'iterazione del prodotto.Prodotto cartesiano di grandi iteratori (itertools)

Domanda: C'è un modo per creare un iteratore su un prodotto cartesiano quando gli ingressi (convertiti nella tupla) sono troppo grandi per essere conservati in memoria? banale esempio:

import itertools 
A = itertools.permutations(xrange(100)) 
itertools.product(A) 

più pratica caso d'uso prenderebbe in una serie di (*iterables[, repeat]) come l'implementazione originale della funzione - quanto sopra è solo un esempio. Non sembra che tu possa usare l'attuale implementazione di itertools.product, quindi accolgo con favore in sottomissione in puro python (anche se non puoi battere il backend C di itertools!).

+0

Come si propone che i prodotti vengano quindi prodotti? Dovresti usare '.tee()' che memorizza nella cache gli iteratori per fare il loro lavoro. –

+3

In alternativa, avresti bisogno di iteratori re-startable, ad es. puoi raggiungere il tuo obiettivo solo se in qualche modo puoi creare ogni iteratore a-nuovo, per produrre gli stessi risultati identici durante la precedente esecuzione completa. –

+0

@MartijnPieters Non sono sicuro (da qui la domanda!). Il cuore della domanda è, dare un'implementazione di prodotto esterno senza alcun deposito intermedio. Possibile in 'itertools', non ne sono sicuro - in Python puro, penso che sia possibile in quanto possiamo" riavviare "un iteratore aggiungendolo ogni volta che è necessario. – Hooked

risposta

4

Ecco un'implementazione che chiama callable e itera iterables, che si presume riavviabile:

def product(*iterables, **kwargs): 
    if len(iterables) == 0: 
     yield() 
    else: 
     iterables = iterables * kwargs.get('repeat', 1) 
     it = iterables[0] 
     for item in it() if callable(it) else iter(it): 
      for items in product(*iterables[1:]): 
       yield (item,) + items 

Testing:

import itertools 
g = product(lambda: itertools.permutations(xrange(100)), 
      lambda: itertools.permutations(xrange(100))) 
print next(g) 
print sum(1 for _ in g) 
+0

Non capisco perché l'input per 'product' debba essere una funzione lambda, sembra funzionare anche se si usa il mio' A' dell'esempio. – Hooked

+0

@Hooked che funzionerà per iniziare, ma una volta raggiunta la fine (nei loop interni) non sarà in grado di riavviare le permutazioni. Prova per una piccola gamma in 'A' da vedere. – ecatmur

+0

@Hooked: se vogliamo iteratori ricreabili, è necessario passare l'autore iteratore alla funzione prodotto. Gli iteratori devono essere creati solo nella funzione stessa. –

2

Senza "ricreazione iteratore", può essere possibile per il primo dei fattori. Ma ciò farebbe risparmiare solo 1/n spazio (dove n è il numero di fattori) e aggiungere confusione.

Quindi la risposta è ricreazione iteratore. Un cliente della funzione dovrebbe assicurarsi che la creazione degli iteratori sia pura (senza effetti collaterali). Come

def iterProduct(ic): 
    if not ic: 
     yield [] 
     return 

    for i in ic[0](): 
     for js in iterProduct(ic[1:]): 
      yield [i] + js 


# Test 
x3 = lambda: xrange(3) 
for i in iterProduct([x3,x3,x3]): 
    print i 
+0

La chiamata a 'len (ic)' fallisce con il mio input 'A' come' oggetto di tipo 'itertools.permutations' non ha len() '. Se non sbaglio, non puoi accedere ad un iteratore indicizzando 'ic [0]'. come '__getitem__' non è implementato in generale. – Hooked

+0

@Hooked: la chiamata a len() è ora fuori. 'ic' non dovrebbe essere un iteratore, è solo un numero fisso di fattori forniti come una lista (potrebbe esserci una soluzione con varargs, o uno più funzionale (' x: xs = ... ') uno, ma il mio pitone è piuttosto vecchio –

+0

@DSM: 'if ic:' è implicito nel secondo blocco Provalo –

1

Questo non può essere fatto con i generatori standard di Python, perché alcuni dei iterabili devono essere ripetuti più volte. Devi usare una sorta di tipo di dati in grado di "reiterare". Ho creato una semplice classe "reiterabile" e un algoritmo di prodotto non ricorsivo. product dovrebbe avere più controllo degli errori, ma questo è almeno un primo approccio. Il semplice classe reiterabile ...

class PermutationsReiterable(object): 
    def __init__(self, value): 
     self.value = value 
    def __iter__(self): 
     return itertools.permutations(xrange(self.value)) 

E product iteslf ...

def product(*reiterables, **kwargs): 
    if not reiterables: 
     yield() 
     return 
    reiterables *= kwargs.get('repeat', 1) 

    iterables = [iter(ri) for ri in reiterables] 
    try: 
     states = [next(it) for it in iterables] 
    except StopIteration: 
     # outer product of zero-length iterable is empty 
     return 
    yield tuple(states) 

    current_index = max_index = len(iterables) - 1 
    while True: 
     try: 
      next_item = next(iterables[current_index]) 
     except StopIteration: 
      if current_index > 0: 
       new_iter = iter(reiterables[current_index]) 
       next_item = next(new_iter) 
       states[current_index] = next_item 
       iterables[current_index] = new_iter 
       current_index -= 1 
      else: 
       # last iterable has run out; terminate generator 
       return 
     else: 
      states[current_index] = next_item 
      current_index = max_index 
      yield tuple(states) 

Provato:

>>> pi2 = PermutationsReiterable(2) 
>>> list(pi2); list(pi2) 
[(0, 1), (1, 0)] 
[(0, 1), (1, 0)] 
>>> list(product(pi2, repeat=2)) 
[((0, 1), (0, 1)), ((0, 1), (1, 0)), ((1, 0), (0, 1)), ((1, 0), (1, 0))] 
>>> giant_product = product(PermutationsReiterable(100), repeat=5) 
>>> len(list(itertools.islice(giant_product, 0, 5))) 
5 
>>> big_product = product(PermutationsReiterable(10), repeat=2) 
>>> list(itertools.islice(big_product, 0, 5)) 
[((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), (0, 1, 2, 3, 4, 5, 6, 7, 9, 8)), 
((0, 1, 2, 3, 4, 5, 6, 7, 8, 9), (0, 1, 2, 3, 4, 5, 6, 8, 7, 9)), 
((0, 1, 2, 3, 4, 5, 6, 7, 8, 9), (0, 1, 2, 3, 4, 5, 6, 8, 9, 7)), 
((0, 1, 2, 3, 4, 5, 6, 7, 8, 9), (0, 1, 2, 3, 4, 5, 6, 9, 7, 8))] 
+0

Grazie per l'ottima risposta. Quando dici "error-checking", che cosa staresti cercando esattamente? Controllerebbe per vedere se l'iteratore è riavviabile? Come hai potuto controllarlo? – Hooked

+0

Sembra eccessivamente complicato e confuso, quando una soluzione semplice era già stata fornita molto tempo prima. Questo fa qualcosa in un modo migliore? –

+0

Intendo solo che 'prodotto' dovrebbe veramente generare un errore quando viene passato un argomento di parole chiave non valido; questo no. – senderle

0

Mi dispiace di questa discussione, ma dopo passare ore di debug di un programma che tenta di iterare su generatori di prodotti cartesiani generati in modo ricorsivo. Posso dirti che nessuna delle soluzioni di cui sopra funziona se non funziona con numeri costanti come in tutti gli esempi sopra.

Correzione:

from itertools import tee 

def product(*iterables, **kwargs): 
    if len(iterables) == 0: 
     yield() 
    else: 
     iterables = iterables * kwargs.get('repeat', 1) 
     it = iterables[0] 
     for item in it() if callable(it) else iter(it): 
      iterables_tee = list(map(tee, iterables[1:])) 
      iterables[1:] = [it1 for it1, it2 in iterables_tee] 
      iterable_copy = [it2 for it1, it2 in iterables_tee] 
      for items in product(*iterable_copy): 
       yield (item,) + items 

Se i generatori contengono generatori, è necessario passare una copia per la chiamata ricorsiva.