Supongamos que tengo la siguiente función:
def print_twice(x): for i in x: print(i) for i in x: print(i)Cuando corro:
print_twice([1,2,3])o:
print_twice((1,2,3))Obtengo el resultado esperado: los números 1,2,3 se imprimen dos veces.
Pero cuando corro:
print_twice(zip([1,2,3],[4,5,6])) los pares (1,4),(2,5),(3,6) se imprimen una sola vez. Probablemente, esto se deba a que el zip devuelve un generador que finaliza después de una pasada.
¿Cómo puedo modificar la función print_twice para que maneje correctamente todas las entradas?
Podría insertar una línea al principio de la función: x = list(x) . Pero esto podría ser ineficiente en caso de que x ya sea una lista, una tupla, un rango o cualquier otro iterador que pueda iterarse más de una vez. ¿Hay una solución más eficiente?
Podría insertar una línea al principio de la función:
x = list(x). Pero esto podría ser ineficiente en caso de que x ya sea una lista, una tupla, un rango o cualquier otro iterador que pueda iterarse más de una vez. ¿Hay una solución más eficiente?
Copiar iterables de un solo uso en una list es perfectamente adecuado y razonablemente eficiente incluso para iterables de usos múltiples.
El tipo de list (y hasta cierto punto tuple ) es una de las estructuras de datos más optimizadas en Python. Las operaciones comunes, como copiar una list o una tuple a una list , se optimizan internamente; 1 incluso para iterables que no tienen un caso especial, copiarlos en una list es significativamente más rápido que cualquier trabajo realista realizado por dos (o más) bucles.
def print_twice(x): x = list(x) for i in x: print(i) for i in x: print(i) La copia indiscriminada también puede ser ventajosa en el contexto de la concurrencia, cuando el iterable se puede modificar mientras se ejecuta la función. Los casos comunes son subprocesos y colecciones de weakref .
En caso de que uno quiera evitar copias innecesarias, verificar si el iterable es una Collection es un resguardo razonable.
from collections.abc import Collection x = list(x) if not isinstance(x, Collection) else xAlternativamente, uno puede verificar si el iterable es de hecho un iterado o , ya que esto implica estado y, por lo tanto, un solo uso.
from collections.abc import Iterator x = list(x) if isinstance(x, Iterator) else x x = list(x) if iter(x) is x else x En particular, los componentes zip , filter , map , ... y generadores son todos iteradores.
1 Copiar una list de 128 elementos es aproximadamente tan rápido como comprobar si se trata de una Collection .
Una prueba simple para ver si x se consumirá cuando lo repita es iter(x) is x . Esto es confiable, ya que se especifica como parte del protocolo iterador ( docs ):
Se requiere que los iteradores tengan un
__iter__()que devuelva el propio objeto del iterador
Por el contrario, si iter(x) devuelve x , entonces x debe ser un iterador, ya que fue devuelto por la función iter .
Algunos cheques:
def is_iterator(x): return iter(x) is x for obj in [ # not iterators [1, 2, 3], (1, 2, 3), {1: 2, 3: 4}, range(3), # iterators (x for x in range(3)), iter([1, 2, 3]), zip([1, 2], [3, 4]), filter(lambda x: x % 2 == 0, [1, 2, 3]), map(lambda x: 2 * x, [1, 2, 3]), ]: name = type(obj).__name__ if is_iterator(obj): print(name, 'is an iterator') else: print(name, 'is not an iterator')Resultados:
list is not an iterator tuple is not an iterator dict is not an iterator range is not an iterator generator is an iterator list_iterator is an iterator zip is an iterator filter is an iterator map is an iterator Entonces, para asegurarse de que x se pueda iterar varias veces, sin hacer una copia innecesaria si ya se puede, puede escribir algo como:
if iter(x) is x: x = list(x)zip devolverá un iterador. Una vez desempaquetado, no se puede volver a desempaquetar, se agota.
Tal vez si desea asegurarse de que solo los objetos zip se conviertan a la list como dijo que funcionaría pero no sería eficiente, puede verificarlo escriba:
if isinstance(x, zip): x = list(x)Modifique su función print_twice
def print_twice(x): val = x for i in range(2): for i in val: print(i)