Как вы можете передать итерируемый объект нескольким потребителям в постоянном пространстве?
TLDR
Напишите реализацию, которая проходит следующий тест в CONSTANT SPACE, рассматривая min
, max
и sum
как черные ящики.
def testit(implementation, N):
assert implementation(range(N), min, max, sum) == (0, N-1, N*(N-1)//2)
Обсуждение
Мы любим итераторы, потому что они позволяют нам лениво обрабатывать потоки данных, позволяя обрабатывать огромные объемы данных в ПОСТОЯННОМ ПРОСТРАНСТВЕ.
def source_summary(source, summary):
return summary(source)
N = 10 ** 8
print(source_summary(range(N), min))
print(source_summary(range(N), max))
print(source_summary(range(N), sum))
Каждая строка выполнялась несколько секунд, но использовала очень мало памяти. Однако для этого потребовалось 3 отдельных обхода источника. Таким образом, это не будет работать, если вашим источником является сетевое соединение, оборудование для сбора данных и т. д., если только вы не кэшируете все данные где-то, теряя требование ПОСТОЯННОГО ПРОСТРАНСТВА.
Вот версия, которая демонстрирует эту проблему
def source_summaries(source, *summaries):
from itertools import tee
return tuple(map(source_summary, tee(source, len(summaries)),
summaries))
testit(source_summaries, N)
print('OK')
Тест пройден, но tee
пришлось сохранить копию всех данных, поэтому использование пространства увеличилось с O(1)
до O(N)
.
Как вы можете получить результаты за один проход с постоянной памятью?
Конечно, можно пройти тест, указанный выше, с использованием O(1)
пространства, путем обмана: используя знание конкретных потребителей итераторов, которые использует тест. Но дело не в этом: source_summaries
должен работать с любыми расходными материалами для итераторов, такими как set
, collections.Counter
, ''.join
, включая все, что может быть написано в будущем. Реализация должна рассматривать их как черные ящики.
Для ясности: единственное знание, доступное о потребителях, состоит в том, что каждый потребляет одну итерацию и возвращает один результат. Использование любых других знаний о потребителе является обманом.
Идеи
[EDIT: я опубликовал реализацию этой идеи в качестве ответа]
Я могу представить решение (которое мне очень не нравится), которое использует
упреждающая потоковая передача
пользовательский итератор, связывающий потребителя с источником
Назовем пользовательский итератор link
.
- Для каждого потребителя запустите
result = consumer(<link instance for this thread>)
<link instance for this thread>.set_result(result)
на отдельной ветке.
- В основной теме что-то вроде
for item in source:
for l in links:
l.push(item)
for l in links:
l.stop()
for thread in threads:
thread.join()
return tuple(link.get_result, links)
link.__next__
блокирует до тех пор, пока экземплярlink
не получит.push(item)
in which case it returns the item.stop()
в этом случае повышаетсяStopIteration
Гонки данных выглядят как кошмар. Вам понадобится очередь для push-уведомлений, и, вероятно, дозорный объект должен быть помещен в очередь
link.stop()
... и множество других вещей, которые я упускаю из виду.
Я бы предпочел использовать кооперативную многопоточность, но consumer(link)
кажется неизбежно некооперативной.
Есть ли у вас менее грязные предложения?
reduce
? Таким образом, вместо вычисленияsum(some_list)
вы можете инициализироватьtmp = 0
, а затем на каждой итерации делатьtmp = sum(tmp, current_value)
. Вы можете сделать это для всех трех операций (min
,max
,sum
) одновременно и потребуется только один проход по элементам. Единственная проблема состоит в том, чтобы выбрать осмысленное начальное значениеtmp
для каждой из трех операций. - person Daniel Junglas   schedule 08.04.2020reduce
в эквивалентной бинарной функции требует специальных знаний потребителя. Таким образом, это подпадает под «обман», о котором я упоминал в вопросе. Я хочу предоставить (что-то вроде) это как библиотечную утилиту, которую пользователи могут вызывать с любыми потребителями, которых они хотят, включая те, которые не были изобретены сегодня, так что единственное, что я могу знать о потребитель заключается в том, что он потребляет итерацию для получения результата. Все, что помимо этого, является мошенничеством. - person jacg   schedule 08.04.2020tmp
в качестве аргумента. Если вы посмотрите на встроенную функциюsum()
, то это именно то, что делает эта функция. Вот как вы можете использовать эту функцию для суммирования чисел или объединения списков с одной и той же реализацией. Во всяком случае, это только мои два цента. - person Daniel Junglas   schedule 08.04.2020consumer(iterable)
: ничего другого. Не имеет значения, чтоsum
,max
или любой другой конкретный вариант, который вы имеете в виду, предлагает больше, библиотека может полагаться только на наименьший общий знаменатель. Это верно как в упражнениях, так и в реальном мире: это фундаментальное свойство того, что означает «интерфейс»! - person jacg   schedule 08.04.2020max
иmin
:max((1,2)) == max(1,2)
[примечание: во втором случае меньше скобок].max
в этом отношении необычно: дажеsum
(которое вы использовали в первом примере) нельзя использовать таким образом:sum((1,2)) == 3
ноsum(1,2)
— это ошибка. Так что ваш примерtmp = sum(tmp, current_value)
тоже ошибка. Подавляющее большинство попадают в эту категорию:list
,tuple
,set
,dict
,collections.Counter
,enumerate
,partial(map, fn)
и т.д. и т.п. - person jacg   schedule 09.04.2020sum
был написан неправильно. Он должен был читаться какsum([current_value], tmp)
илиsum([current_value, tmp])
, что совпадает сsum([current_value, tmp])
илиsum((current_value, tmp))
. И вы правы, есть много потенциальных потребителей, которые не соответствуют шаблону использования начального значения. - person Daniel Junglas   schedule 09.04.2020