zip_longest sin valor de relleno

Estoy buscando un punto intermedio entre las funciones zip de Python y zip_longest (del módulo itertools ), que agota todos los iteradores dados, pero no llena nada. Entonces, por ejemplo, debería transponer tuplas como tales:

 (11, 12, 13 ), (11, 21, 31, 41), (21, 22, 23, 24), --> (12, 22, 32, 42), (31, 32 ), (13, 23, 43), (41, 42, 43, 44), ( 24, 44) 

(Espacios agregados para una mejor alineación gráfica.)

fillvalue componer una solución cruda limpiando los fillvalue después de zip_longest .

 def zip_discard(*iterables, sentinel = object()): return map( partial(filter, partial(is_not, sentinel)), zip_longest(*iterables, fillvalue=sentinel)) 

¿Hay alguna manera de hacer esto sin presentar a los centinelas para empezar? ¿Se puede mejorar esto usando el yield ? ¿Qué enfoque parece más eficiente?

Tanto zip como zip_longest fueron diseñados para generar siempre tuplas de igual longitud, puedes definir tu propio generador que no se preocupa por el len con algo como esto:

 def _one_pass(iters): for it in iters: try: yield next(it) except StopIteration: pass #of some of them are already exhausted then ignore it. def zip_varlen(*iterables): iters = [iter(it) for it in iterables] while True: #broken when an empty tuple is given by _one_pass val = tuple(_one_pass(iters)) if val: yield val else: break 

Si los datos comprimidos juntos son bastante grandes, omitir los iteradores agotados cada vez puede ser costoso, puede ser más eficiente eliminar los iteradores terminados de los _one_pass en la función _one_pass esta manera:

 def _one_pass(iters): i = 0 while i 

ambas versiones eliminarían la necesidad de crear resultados intermedios o utilizar valores de relleno temporales.

Tu acercamiento es bueno. Creo que usar un centinela es elegante. Podría ser considerado un poco más python para usar una expresión generadora anidada:

 def zip_discard_gen(*iterables, sentinel=object()): return ((entry for entry in iterable if entry is not sentinel) for iterable in zip_longest(*iterables, fillvalue=sentinel)) 

Esto necesita menos importaciones porque no hay necesidad de partial() o ne() .

También es un poco más rápido:

 data = [(11, 12, 13 ), (21, 22, 23, 24), (31, 32 ), (41, 42, 43, 44)] %timeit [list(x) for x in zip_discard(*data)] 10000 loops, best of 3: 17.5 µs per loop %timeit [list(x) for x in zip_discard_gen(*data)] 100000 loops, best of 3: 14.2 µs per loop 

EDITAR

Una versión de la lista de comprensión es un poco más rápida:

 def zip_discard_compr(*iterables, sentinel=object()): return [[entry for entry in iterable if entry is not sentinel] for iterable in zip_longest(*iterables, fillvalue=sentinel)] 

Sincronización:

 %timeit zip_discard_compr(*data) 100000 loops, best of 3: 6.73 µs per loop 

Una versión de Python 2:

 from itertools import izip_longest SENTINEL = object() def zip_discard_compr(*iterables): sentinel = SENTINEL return [[entry for entry in iterable if entry is not sentinel] for iterable in izip_longest(*iterables, fillvalue=sentinel)] 

Tiempos

Esta versión devuelve la misma estructura de datos como zip_varlen por Tadhg McDonald-Jensen:

 def zip_discard_gen(*iterables, sentinel=object()): return (tuple([entry for entry in iterable if entry is not sentinel]) for iterable in zip_longest(*iterables, fillvalue=sentinel)) 

Es aproximadamente el doble de rápido:

 %timeit list(zip_discard_gen(*data)) 100000 loops, best of 3: 9.37 µs per loop %timeit list(zip_varlen(*data)) 10000 loops, best of 3: 18 µs per loop