2012-08-23 27 views
13

De un previous question aprendí algo interesante. Si Python's itertools.product se alimenta con una serie de iteradores, estos iteradores se convertirán en tuplas antes de comienza el producto cartesiano. Relatedquestions observe el código fuente de itertools.product para concluir que, aunque no se almacenan en la memoria resultados intermedios, las versiones tuple de los iteradores originales se crean antes de que comience la iteración del producto.Producto cartesiano de iteradores grandes (itertools)

Pregunta: ¿Hay alguna manera de crear un iterador en un producto cartesiano cuando las entradas (convertidas en tupla) son demasiado grandes para mantenerlas en la memoria? ejemplo trivial:

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

Un caso más práctico el uso tomaría en una serie de (*iterables[, repeat]) como la implementación original de la función - lo anterior es sólo un ejemplo. No parece que puedas usar la implementación actual de itertools.product, así que doy la bienvenida en la presentación en python puro (aunque no puedes superar el backend C de itertools).

+0

¿Cómo propone que se produzcan los productos? Tendría que usar '.tee()' que también almacena en caché iteradores para hacer su trabajo. –

+3

Alternativamente, necesitaría iteradores reiniciables, p. Ej. solo puedes lograr tu objetivo si de alguna manera puedes crear cada iterador a-new, para producir exactamente los mismos resultados que durante la ejecución completa previa. –

+0

@MartijnPieters No estoy seguro (¡de ahí la pregunta!). El corazón de la pregunta es, dar una implementación externa del producto sin ningún almacenamiento intermedio. Posible en 'itertools', no estoy seguro, en Python puro, creo que puede ser posible, ya que podemos" reiniciar "un iterador con una nueva versión cada vez que sea necesario. – Hooked

Respuesta

4

He aquí una aplicación que llama a callables e itera iterables, que se supone reiniciable:

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 

Pruebas:

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

No entiendo completamente por qué la entrada a 'product' tiene que ser una función lambda, parece funcionar incluso si usa mi' A' del ejemplo. – Hooked

+0

@Hooked que funcionará para empezar, pero una vez que llegue al final (en los bucles internos) no sabrá reiniciar las permutaciones. Intente obtener un pequeño rango en 'A' para ver. – ecatmur

+0

@Hooked: si queremos iteradores recreables, debe pasar el creador del iterador a la función del producto. Los iteradores solo se deben crear en la función en sí. –

2

Sin "recreación de iterador", puede ser posible para el primero de los factores. Pero eso ahorraría solo 1/n de espacio (donde n es la cantidad de factores) y agregaría confusión.

Así que la respuesta es iterator recreation. Un cliente de la función debería asegurarse de que la creación de los iteradores sea pura (sin efectos secundarios). Al igual que

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 llamada a 'len (ic)' falla con mi entrada 'A' como' objeto de tipo 'itertools.permutations' no tiene len() '. Si no me equivoco, tampoco puede acceder a un iterador indexando 'ic [0]'. ya que '__getitem__' no se implementa en general. – Hooked

+0

@Hooked: La llamada a len() está ahora ausente. 'ic' no debe ser un iterador, es solo un número fijo de factores provistos como una lista (puede haber una solución con varargs, o una más funcional (' x: xs = ... ') uno, pero mi pitón es bastante viejo. –

+0

@DSM: 'if ic:' está implícito en el segundo bloque. Pruébalo. –

1

Esto no se puede hacer con los generadores estándar de Python, porque algunos de los iterables se deben reciclar varias veces. Tienes que usar algún tipo de tipo de datos capaz de "reiterar". Creé una clase simple "reiterable" y un algoritmo de producto no recursivo. product debería tener más comprobación de errores, pero este es al menos un primer enfoque. El simple clase reiterable ...

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

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

Probado:

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

Gracias por la gran respuesta. Cuando dice "comprobación de errores", ¿qué busca exactamente? ¿Verificaría si el iterador es reiniciable? ¿Cómo puedes verificar eso? – Hooked

+0

Parece demasiado complicado y confuso, cuando una solución simple ya se había proporcionado mucho antes. ¿Hace esto algo de una mejor manera? –

+0

Solo quiero decir que 'product' realmente debería arrojar un error cuando se pasa un argumento de palabra clave inválido; esto no. – senderle

0

Siento este tema, pero después de pasar horas de depuración de una programa que intenta iterar sobre productos cartesianos generados recursivamente de generadores. Puedo decirle que ninguna de las soluciones anteriores funciona si no funciona con números constantes como en todos los ejemplos anteriores.

Corrección:

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 

Si sus generadores contienen generadores, lo necesario para pasar una copia a la llamada recursiva.

Cuestiones relacionadas