Селектор итератора в 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