Python 'yield from' или вернуть генератор?

Я написал этот простой кусок кода:

def mymap(func, *seq):
  return (func(*args) for args in zip(*seq))

Должен ли я использовать оператор return, как указано выше, чтобы вернуть генератор, или использовать инструкцию yield from, например:

def mymap(func, *seq):
  yield from (func(*args) for args in zip(*seq))

и помимо технической разницы между "доходностью" и "доходностью от", какой подход лучше в общем случае?

Ответ 1

Действительно, это зависит от ситуации. yield в основном подходит для случаев, когда вы просто хотите перебирать возвращаемые значения, а затем манипулировать ими. return в основном подходит для тех случаев, когда вы хотите сохранить все значения, которые генерировала ваша функция в памяти, а не просто перебирать их один раз. Обратите внимание, что один раз вы можете выполнять только итерацию по генератору (что возвращает доход), есть некоторые алгоритмы, для которых это определенно не подходит.

Ответ 2

Разница в том, что ваш первый mymap - это обычная функция, в этом случае a factory, который возвращает генератор. Все внутри тела выполняется выполнение, как только вы вызываете функцию.

def gen_factory(func, seq):
    """Generator factory returning a generator."""
    # do stuff ... immediately when factory gets called
    print("build generator & return")
    return (func(*args) for args in seq)

Второй mymap также является factory, но он также является генератором само собой, уступая из встроенного встроенного подгенератора внутри. Поскольку это сам генератор, выполнение тела делает не запускается до первого вызова следующего (генератора).

def gen_generator(func, seq):
    """Generator yielding from sub-generator inside."""
    # do stuff ... first time when 'next' gets called
    print("build generator & yield")
    yield from (func(*args) for args in seq)

Я думаю, что следующий пример станет более понятным. Мы определяем пакеты данных, которые должны обрабатываться с помощью функций, объединенных в задания, которые мы передаем генераторам.

def add(a, b):
    return a + b

def sqrt(a):
    return a ** 0.5

data1 = [*zip(range(1, 5))]  # [(1,), (2,), (3,), (4,)]
data2 = [(2, 1), (3, 1), (4, 1), (5, 1)]

job1 = (sqrt, data1)
job2 = (add, data2)

Теперь мы запускаем следующий код внутри интерактивной оболочки, такой как IPython, чтобы см. различное поведение. gen_factory сразу печатает , а gen_generator делает это только после вызова next().

gen_fac = gen_factory(*job1)
# build generator & return <-- printed immediately
next(gen_fac)  # start
# Out: 1.0
[*gen_fac]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

gen_gen = gen_generator(*job1)
next(gen_gen)  # start
# build generator & yield <-- printed with first next()
# Out: 1.0
[*gen_gen]  # deplete rest of generator
# Out: [1.4142135623730951, 1.7320508075688772, 2.0]

Чтобы дать вам более разумный пример использования для конструкции например gen_generator, мы немного расширим его и сделаем сопрограмму из этого, присвоив доходность переменным, чтобы мы могли вводить задания в рабочий генератор с send().

Кроме того, мы создаем вспомогательную функцию, которая будет выполнять все задачи внутри работы и спросить, как для нового по завершении.

def gen_coroutine():
    """Generator coroutine yielding from sub-generator inside."""
    # do stuff... first time when 'next' gets called
    print("receive job, build generator & yield, loop")
    while True:
        try:
            func, seq = yield "send me work ... or I quit with next next()"
        except TypeError:
            return "no job left"
        else:
            yield from (func(*args) for args in seq)


def do_job(gen, job):
    """Run all tasks in job."""
    print(gen.send(job))
    while True:
        result = next(gen)
        print(result)
        if result == "send me work ... or I quit with next next()":
            break

Теперь мы запускаем gen_coroutine с помощью нашей вспомогательной функции do_job и двух заданий.

gen_co = gen_coroutine()
next(gen_co)  # start
# receive job, build generator & yield, loop  <-- printed with first next()
# Out:'send me work ... or I quit with next next()'
do_job(gen_co, job1)  # prints out all results from job
# 1
# 1.4142135623730951
# 1.7320508075688772
# 2.0
# send me work... or I quit with next next()
do_job(gen_co, job2)  # send another job into generator
# 3
# 4
# 5
# 6
# send me work... or I quit with next next()
next(gen_co)
# Traceback ...
# StopIteration: no job left

Чтобы вернуться к вашему вопросу, какая версия - лучший подход в целом. IMO что-то вроде gen_factory имеет смысл, если вам нужно то же самое для нескольких генераторов, которые вы собираетесь создавать, или в случаях, когда ваш процесс построения генераторов достаточно сложный, чтобы оправдать использование factory вместо того, чтобы создавать отдельные генераторы в место с пониманием генератора.

Примечание:

Описание выше для функции gen_generator (второй mymap) "это сам генератор". Это немного расплывчато и технически не действительно правильно, но облегчает рассуждение о различиях функций в этой сложной настройке, где gen_factory также возвращает генератор, а именно: один из которых встроен в генераторное понимание внутри.

Фактически любая функция (а не только те из этого вопроса с встроенными генераторами внутри!) с yield внутри, после вызова, просто возвращает объект-генератор, который создается из тела функции.

type(gen_coroutine) # function
gen_co = gen_coroutine(); type(gen_co) # generator

Итак, все действие, которое мы наблюдали выше для gen_generator и gen_coroutine происходит внутри этих объектов генератора, функции с yield внутри выплевывались раньше.

Ответ 3

Генераторы используют yield, функции используют return.

Генераторы обычно используются в циклах for для многократного повторения значений, автоматически предоставляемых генератором, но могут также использоваться в другом контексте, e. г. в функции list() для создания списка - снова из значений, автоматически предоставляемых генератором.

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