Функция пустого генератора Python

104

В python можно легко определить функцию итератора, поместив ключевое слово yield в тело функции, например:

def gen():
    for i in range(100):
        yield i

Как я могу определить функцию генератора, которая не дает значения (генерирует 0 значений), следующий код не работает, поскольку python не может знать, что он должен быть генератором, а не нормальной функцией:

def empty():
    pass

Я мог бы сделать что-то вроде

def empty():
    if False:
        yield None

Но это было бы очень некрасиво. Есть ли хороший способ реализовать пустую функцию итератора?

Константин Вайц
источник

Ответы:

139

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

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Я не уверен, что это намного лучше, чем то, что у вас есть - он просто заменяет оператор без операции на ifоператор без операции yield. Но это более идиоматично. Обратите внимание, что просто использование yieldне работает.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Почему бы просто не использовать iter(())?

Этот вопрос конкретно касается пустой функции генератора . По этой причине я считаю, что это вопрос о внутренней согласованности синтаксиса Python, а не о том, как лучше всего создать пустой итератор в целом.

Если на самом деле вопрос заключается в том, как лучше всего создать пустой итератор, то вы можете согласиться с Zectbumo в использовании iter(())вместо него. Однако важно отметить, что iter(())функция не возвращается! Он напрямую возвращает пустой итерируемый объект. Предположим, вы работаете с API, который ожидает вызываемого объекта, который возвращает итерацию при каждом вызове, как и обычная функция генератора. Вам нужно будет сделать что-то вроде этого:

def empty():
    return iter(())

(Следует отметить, что Unutbu дал первую правильную версию этого ответа.)

Теперь вы можете найти вышесказанное более ясным, но я могу представить себе ситуации, в которых это было бы менее ясно. Рассмотрим этот пример длинного списка (надуманных) определений функций генератора:

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

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

def empty():
    return
    yield

или, в Python 3.3 и выше (как предлагает DSM ), это:

def empty():
    yield from ()

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

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

См. Также этот отличный ответ от user3840170, который disпоказывает еще одну причину, почему этот подход предпочтительнее: он выдает наименьшее количество инструкций при компиляции.

отправитель
источник
Это действительно лучше, чем, if False: yieldно все же немного сбивает с толку людей, которые не знают эту закономерность
Константин Вайц
1
Фу, что-то после возвращения? Я ожидал чего-то подобного itertools.empty().
Grault
1
@Jesdisciple, ну, returnвнутри генераторов означает нечто иное. Это больше похоже break.
senderle
Мне нравится это решение, потому что оно (относительно) краткое и не выполняет дополнительной работы, как сравнение с False.
Пи Мариллион
Ответ Unutbu не является «истинной функцией генератора», как вы упомянули, поскольку он возвращает итератор.
Zectbumo
71
iter(())

Вам не нужен генератор. Ну же, ребята!

Зектбумо
источник
3
Мне определенно больше всего нравится этот ответ. Это быстро, легко писать, быстро выполнять и более привлекательно для меня, чем из- iter([])за того простого факта, что он ()является константой, в то время как он []может создавать экземпляр нового объекта списка в памяти при каждом его вызове.
Mumbleskates
2
Возвращаясь к этой теме, я чувствую себя обязанным указать, что если вам нужна полноценная замена функции генератора, вы должны написать что-нибудь вроде empty = lambda: iter(())или def empty(): return iter(()).
senderle
Если у вас должен быть генератор, вы можете использовать (_ for _ in ()), как предлагали другие
Zectbumo
1
@Zectbumo, это все еще не функция генератора . Это просто генератор. Функция генератора возвращает новый генератор каждый раз, когда вызывается.
senderle
1
Это возвращает tuple_iteratorвместо generator. Если у вас есть случай, когда ваш генератор ничего не должен возвращать, не используйте этот ответ.
Борис
54

Python 3.3 (потому что я в yield fromвосторге и потому что @senderle украл мою первую мысль):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Но я должен признать, что мне сложно придумать вариант использования для этого, который iter([])или (x)range(0)не работал бы одинаково хорошо.

DSM
источник
Мне очень нравится этот синтаксис. доход от потрясающий!
Константин Вайц
2
Я думаю , что это гораздо более удобным для чтения для новичка , чем любой return; yieldили if False: yield None.
abarnert
1
Это самое элегантное решение
Максим Ганенко
«Но я должен признать, что мне трудно придумать вариант использования для этого, который iter([])или (x)range(0)не работал бы одинаково хорошо». -> Не уверен, что (x)range(0)есть, но вариантом использования может быть метод, который должен быть переопределен с помощью полномасштабного генератора в некоторых из наследующих классов. Для обеспечения согласованности вы бы хотели, чтобы даже базовый генератор, от которого наследуются другие, возвращал генератор, как и те, которые его переопределяют.
Vedran Šego 02
18

Другой вариант:

(_ for _ in ())
Бен Рейнвар
источник
В отличие от других вариантов, Pycharm считает, что это согласуется со стандартными подсказками типа, используемыми для генераторов, напримерGenerator[str, Any, None]
Михал Яблоньски
3

Как сказал @senderle, используйте это:

def empty():
    return
    yield

Я пишу этот ответ в основном для того, чтобы поделиться еще одним его оправданием.

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

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Как мы видим, у empty_returnнего точно такой же байт-код, что и у обычной пустой функции; остальные выполняют ряд других операций, которые в любом случае не меняют поведения. Единственная разница между empty_returnи noopзаключается в том, что у первого установлен флаг генератора:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Конечно, сила этого аргумента очень зависит от конкретной реализации Python в использовании; достаточно умный альтернативный интерпретатор может заметить, что другие операции не дают ничего полезного, и оптимизировать их. Однако даже если такие оптимизации присутствуют, они требуют, чтобы интерпретатор потратил время на их выполнение и защитился от сбоев в предположениях оптимизации, например iter, отскок идентификатора в глобальной области видимости на что-то еще (даже если это, скорее всего, укажет на ошибку, если он на самом деле произошло). В случае empty_returnс оптимизацией просто нечего, поэтому даже относительно наивный CPython не будет тратить время на какие-либо ложные операции.

user3840170
источник
О, классно. Не могли бы вы также добавить результаты yield from ()? (См. Ответ DSM .)
отправитель
3

Это должна быть функция генератора? Если нет, как насчет

def f():
    return iter(())
Unutbu
источник
2

«Стандартный» способ сделать пустой итератор - iter ([]). Я предложил сделать [] аргументом по умолчанию для iter (); это было отклонено с хорошими аргументами, см. http://bugs.python.org/issue25215 - Jurjen

Юрьен Бос
источник
0

Я хочу привести пример на основе классов, поскольку мы еще ничего не предлагали. Это вызываемый итератор, который не генерирует никаких элементов. Я считаю, что это простой и наглядный способ решения проблемы.

class EmptyGenerator:
    def __iter__(self):
        return self
    def __next__(self):
        raise StopIteration

>>> list(EmptyGenerator())
[]
Зектбумо
источник
Можете ли вы добавить объяснение, почему / как это работает для решения проблемы OP?
Шерил Хохман,
Пожалуйста, не публикуйте только код в качестве ответа, но также объясните, что делает ваш код и как он решает проблему вопроса. Ответы с объяснением обычно более полезны и качественнее, и с большей вероятностью получат положительные отзывы.
Шерил Хохман,