Функція порожнього генератора Python


99

У python можна легко визначити функцію ітератора, помістивши ключове слово yield в тіло функції, наприклад:

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

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

def empty():
    pass

Я міг би зробити щось подібне

def empty():
    if False:
        yield None

Але це було б дуже потворно. Чи є якийсь приємний спосіб реалізувати порожню функцію ітератора?

Відповіді:


133

Ви можете використовувати 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основані функції є більш читабельними та ремонтопридатними.


Це справді краще, ніж, 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замість a generator. Якщо у вас є випадок, коли генератору не потрібно нічого повертати, не використовуйте цю відповідь.
Борис

53

Python 3.3 (тому що я на yield fromударі і тому, що @senderle вкрав мою першу думку):

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

Але мушу визнати, мені важко придумати варіант використання для цього, який iter([])або (x)range(0)не працював би однаково добре.


Мені дуже подобається цей синтаксис. вихід від приголомшливий!
Костянтин Вайц

2
Я думаю, що це набагато зручніше для початківців, ніж return; yieldабо if False: yield None.
abarnert

1
Це найелегантніше рішення
Максим Ганенко

"Але я повинен визнати, що мені важко придумати варіант використання для цього, який iter([])або (x)range(0)не працював би однаково добре". -> Не впевнений, що (x)range(0)є, але випадок використання може бути методом, який має бути замінено повномасштабним генератором у деяких класах успадкування. Для узгодженості ви хотіли б, щоб навіть базовий, від якого успадковуються інші, повернув генератор так само, як і той, що його замінює.
Ведран Шего,

19

Інший варіант:

(_ for _ in ())

На відміну від інших варіантів, Pycharm вважає, що це відповідає підказкам стандартного типу, що використовуються для генераторів, наприкладGenerator[str, Any, None]
Michał Jabłoński

3

Це має бути функція генератора? Якщо ні, то як

def f():
    return iter(())

2

"Стандартним" способом зробити порожній ітератор видається iter ([]). Я запропонував зробити [] аргументом за замовчуванням для iter (); це було відхилено з вагомими аргументами, див. http://bugs.python.org/issue25215 - Jurjen


2

Як сказав @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 не буде витрачати час на будь-які помилкові операції.


0
generator = (item for item in [])

0

Я хочу навести приклад на основі класу, оскільки ми ще не пропонували жодного. Це викликається ітератор, який не генерує жодних елементів. Я вважаю, що це прямий та описовий спосіб вирішення проблеми.

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

>>> list(EmptyGenerator())
[]

Чи можете ви додати пояснення, чому / як це працює для вирішення проблеми ОП?
SherylHohman

Будь ласка, не публікуйте лише код як відповідь, але також надайте пояснення, що робить ваш код і як він вирішує проблему питання. Відповіді з поясненнями, як правило, є більш корисними та якісними, і, швидше за все, залучають прихильників.
SherylHohman
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.