code: def unzip(iterable):
      
      head, iterable = spy(iter(iterable))
      if not head:
          # empty iterable, e.g. zip([], [], [])
          return ()
      # spy returns a one-length iterable as head
      head = head[0]
      iterables = tee(iterable, len(head))
  
      def itemgetter(i):
          def getter(obj):
              try:
                  return obj[i]
              except IndexError:
                  # basically if we have an iterable like
                  # iter([(1, 2, 3), (4, 5), (6,)])
                  # the second unzipped iterable would fail at the third tuple
                  # since it would try to access tup[1]
                  # same with the third unzipped iterable and the second tuple
                  # to support these "improperly zipped" iterables,
                  # we create a custom itemgetter
                  # which just stops the unzipped iterables
                  # at first length mismatch
                  raise StopIteration
          return getter
  
      return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables))
docs: The inverse of :func:`zip`, this function disaggregates the elements
      of the zipped *iterable*.
  
      The ``i``-th iterable contains the ``i``-th element from each element
      of the zipped iterable. The first element is used to to determine the
      length of the remaining elements.
  
          >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
          >>> letters, numbers = unzip(iterable)
          >>> list(letters)
          ['a', 'b', 'c', 'd']
          >>> list(numbers)
          [1, 2, 3, 4]
  
      This is similar to using ``zip(*iterable)``, but it avoids reading
      *iterable* into memory. Note, however, that this function uses
      :func:`itertools.tee` and thus may require significant storage.