Zip and more_itertools.unzip: Handling the zero length case

For a function that transposes a list using zip, there’s the case were the list of entries is empty, then zip returns an empty iterator instead of an iterator of empty containers. I understand that with no entries the zip function cannot deduce any dimensions, so it is reasonable to return nothing. What would be the best way of handling that? Is there a general way to solve this?

T = TypeVar("T")
U = TypeVar("U")
def transpose2(entries: Iterable[Tuple[T, U]]) -> Tuple[Iterable[T], Iterable[U]]:
    # Broken when len(list(entries)) == 0
    first, second = zip(*entries)
    return first, second

T = TypeVar("T")
U = TypeVar("U")
def transpose2(entries: Iterable[Tuple[T, U]]) -> Tuple[Iterable[T], Iterable[U]]:
    # Ugly fix
    first_second = tuple(zip(*entries))
    first, second = (list(first_second[0]), list(first_second[1])) if len(first_second) > 1 else ([], [])
    return first, second

This is toy function, here’s the function where I stumbled upon this

def scatter_annotated(
        entries: Iterable[Tuple[str, Tuple[Number, Number]]],
        ax: matplotlib.axes.Axes,
        scatter_kwarg: Dict[str, Any],
        annotate_kwarg: Dict[str, Any]):
    """Adds a scatter plot to the axis and annotates each point"""
    def annotate(entry: Tuple[str, Tuple[Number, Number]]):
        ax.annotate(*entry, **annotate_kwarg)
    # more_itertools.side_effect
    entries = side_effect(annotate, entries)
    xy = unzip(((x, y) for _, (x, y) in entries))
    # In the case of no entries, xy becomes an empty tuple instead
    # of a tuple of two empty lists
    x, y = (list(xy[0]), list(xy[1])) if len(xy) > 1 else ([], [])
    ax.scatter(x, y, scatter_kwarg)

One possible solution I’m thinking is to have an unzip function that takes a dimention as an extra argument and returns an iterable of empty iterables with that size on the zero size case

This seems to solve my issue:

T = TypeVar("T")
def unzipd(iterable: Iterable[Sequence[T]], dimention: int
) -> Tuple[Iterator[T], ...]:
    """
    Transposes iterable, similar to more_itertools.unzip
    except it retuns the correct number of iterables even
    when the iterable is empty
    """
    if not dimention > 0:
        raise ValueError(f"Dimention must be larger than 0, got {dimention}")
    unzipped = unzip(iterable)
    if len(unzipped) == dimention:
        return unzipped
    if len(unzipped) == 0:
        return tuple(map(iter, [()]*dimention))
    raise ValueError(f"Dimention provided does not match dimentions"
                     f"of unzipped iterable, provided {dimention}, "
                     f"got {len(unzipped)}")

Do you mind making an issue at https://github.com/more-itertools/more-itertools/issues?

1 Like