Селектор итератора в Python

Существует ли стандартный питоновский способ выбора значения из списка предоставленных итераторов без продвижения тех, которые не были выбраны?

Что-то в духе этого для двух итераторов (не судите об этом слишком сложно: он был быстро брошен вместе, чтобы проиллюстрировать идею):

def iselect(i1, i2, f):
    e1_read = False
    e2_read = False

    while True:
        try:
            if not e1_read:
                e1 = next(i1)
                e1_read = True

            if not e2_read:
                e2 = next(i2)
                e2_read = True

            if f(e1, e2):
                yield e1
                e1_read = False
            else:
                yield e2
                e2_read = False
        except StopIteration:
            return

Обратите внимание, что если вы используете что-то вроде этого:

[e1 if f(e1, e2) else e2 for (e1, e2) in zip(i1, i2)]

то не выбранный итератор продвигается каждый раз, что не то, что я хочу.

Ответ 1

Пакет more-itertools имеет обходную оболочку для итераторов. Казалось бы, это должно обеспечить очень чистое решение, если я правильно понял ваш вопрос. Вам нужно заглянуть в текущие значения набора итераторов и изменить только выбранный итератор, вызывая next() на нем.

from more_itertools import peekable

# the implementation of iselect can be very clean if
# the iterators are peekable
def iselect(peekable_iters, selector):
    """
    Parameters
    ----------
    peekable_iters: list of peekables
       This is the list of iterators which have been wrapped using
       more-itertools peekable interface.
    selector: function
       A function that takes a list of values as input, and returns
       the index of the selected value.
    """
    while True:
        peeked_vals = [it.peek(None) for it in peekable_iters]
        selected_idx = selector(peeked_vals)  # raises StopIteration
        yield next(peekable_iters[selected_idx])

Проверьте этот код:

# sample input iterators for testing
# assume python 3.x so range function returns iterable
iters = [range(i,5) for i in range(4)]

# the following could be encapsulated...
peekables = [peekable(it) for it in iters]

# sample selection function, returns index of minimum
# value among those being compared, or StopIteration if
# one of the lists contains None
def selector_func(vals_list):
    if None in vals_list:
        raise StopIteration
    else:
        return vals_list.index(min(vals_list))

for val in iselect(peekables, selector_func):
    print(val)    

Вывод:

0
1
1
2
2
2
3
3
3
3
4

Ответ 2

Вы можете использовать itertools.chain, чтобы добавить последний item обратно в iterator:

import itertools as IT
iterator = IT.chain([item], iterator)

И со многими итераторами:

items = map(next, iterators)
idx = f(*items)
iterators = [IT.chain([item], iterator) if i != idx else iterator
             for i, (item, iterator) in enumerate(zip(items, iterators))]

Например,

import itertools as IT

def iselect(f, *iterators):
    iterators = map(iter, iterators)
    while True:
        try:
            items = map(next, iterators)
        except StopIteration:
            return
        idx = f(*items)
        iterators = [IT.chain([item], iterator) if i != idx else iterator
                     for i, (item, iterator) in enumerate(zip(items, iterators))]
        yield items[idx]

def foo(*args):
    return sorted(range(len(args)), key=args.__getitem__)[0]

i1 = range(4)
i2 = range(4)
i3 = range(4)
for item in iselect(foo, i1, i2, i3):
    print(item)

дает

0
0
0
1
1
1
2
2
2
3

Ответ 3

Вместо "функции выбора" я бы использовал "функцию сортировки", которая сообщает, какой элемент должен идти первым.

Программа начинается с создания списка из 2-х кортежей: (итератор, текущее значение). Поскольку один итератор может быть пустым, это нужно сделать с помощью try..catch (т.е. Он не может быть в компактной форме).

Во-вторых, мы итерации, пока существует хотя бы один итератор. Функция сортировки поместила элемент, который должен выйти в первую очередь. Этот элемент "уступил". После этого итератор вызывается для получения следующего элемента. Если элементов больше нет, итератор удаляется из списка.

Это дает следующий код

def iselect( list_of_iterators, sort_function ):
  work_list = []
  for i in list_of_iterators:
    try:
      new_item = ( i, next(i) )  # iterator and its first element
      work_list.append( new_item )
    except StopIteration:
      pass                      # this iterator is empty, skip it
  #
  while len(work_list) > 0:
    # this selects which element should go first
    work_list.sort( lambda e1,e2: sort_function(e1[1],e2[1]) )
    yield work_list[0][1]
    # update the first element of the list
    try:
      i, e = work_list[0]
      e = next(i)
      work_list[0] = ( i, e )
    except StopIteration:
      work_list = work_list[1:]

чтобы протестировать эту программу (включая итератор, который ничего не дает), я использовал

def iter_vowels():
  for v in 'aeiouy':
    yield v

def iter_consonnants():
  for c in 'bcdfghjklmnpqrstvwxz':
    yield c

def no_letters():
  if 1==2:     # not elegant, but.. 
    yield "?"  # .."yield" must appear to make this a generator

def test():
  i1 = iter_vowels()
  i2 = iter_consonnants()
  i3 = no_letters()
  sf = lambda x,y: cmp(x,y)
  for r in iselect( (i1,i2,i3), sf ):
    print (r)

test()

Ответ 4

Вы можете отправить его обратно в генератор:

def iselect(i1, i2, f):
    while True:
        try:
            e1, e2 = next(i1), next(i2)
            if f(e1, e2):
                yield e1
                i2.send(e2)
            else:
                yield e2
                i1.send(e1)
        except StopIteration:
            return