Различия между выражениями выражения генератора
Есть, насколько я знаю, три способа создания генератора через понимание 1.
Классический:
def f1():
g = (i for i in range(10))
Вариант yield
:
def f2():
g = [(yield i) for i in range(10)]
Вариант yield from
(который вызывает SyntaxError
, кроме внутренней функции):
def f3():
g = [(yield from range(10))]
Три варианта приводят к разному байт-коду, что не удивительно.
Казалось бы логичным, что первый из них является лучшим, поскольку он представляет собой выделенный, простой синтаксис для создания генератора через понимание.
Тем не менее, это не тот, который производит кратчайший байт-код.
Разбор в Python 3.6
Понимание классического генератора
>>> dis.dis(f1)
4 0 LOAD_CONST 1 (<code object <genexpr> at...>)
2 LOAD_CONST 2 ('f1.<locals>.<genexpr>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
5 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield
вариант
>>> dis.dis(f2)
8 0 LOAD_CONST 1 (<code object <listcomp> at...>)
2 LOAD_CONST 2 ('f2.<locals>.<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
9 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield from
вариант
>>> dis.dis(f3)
12 0 LOAD_GLOBAL 0 (range)
2 LOAD_CONST 1 (10)
4 CALL_FUNCTION 1
6 GET_YIELD_FROM_ITER
8 LOAD_CONST 0 (None)
10 YIELD_FROM
12 BUILD_LIST 1
14 STORE_FAST 0 (g)
13 16 LOAD_FAST 0 (g)
18 RETURN_VALUE
Кроме того, сравнение timeit
показывает, что вариант yield from
является самым быстрым (все еще выполняется с Python 3.6):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
более или менее в 2,7 раза быстрее, чем f1
и f2
.
В качестве Leon, упомянутого в комментарии, эффективность генератора лучше всего измеряется скоростью, с которой он может быть повторен.
Поэтому я изменил три функции, чтобы они перебирали генераторы и вызывали фиктивную функцию.
def f():
pass
def fn():
g = ...
for _ in g:
f()
Результаты еще более очевидны:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
f3
теперь в 8,4 раза быстрее, чем f1
, и в 9,3 раза быстрее, чем f2
.
Примечание. Результаты более или менее одинаковы, если итерабельность не range(10)
, а статическая итерация, например [0, 1, 2, 3, 4, 5]
.
Следовательно, разница в скорости не имеет ничего общего с range
как-то оптимизирована.
Итак, каковы различия между тремя способами?
В частности, в чем разница между вариантом yield from
и двумя другими?
Является ли это обычное поведение естественной конструкцией (elt for elt in it)
медленнее, чем сложная [(yield from it)]
?
Должен ли я отныне заменить первый на всех во всех моих сценариях или есть ли какие-либо недостатки в использовании конструкции yield from
?
Изменить
Все это связано, поэтому мне не хочется открывать новый вопрос, но это становится еще более странным.
Я попытался сравнить range(10)
и [(yield from range(10))]
.
def f1():
for i in range(10):
print(i)
def f2():
for i in [(yield from range(10))]:
print(i)
>>> timeit(f1, number=100000)
26.715589237537195
>>> timeit(f2, number=100000)
0.019948781941049987
Итак. Теперь, итерация по [(yield from range(10))]
в 186 раз быстрее, чем итерация по голой range(10)
?
Как вы объясните, почему итерация по [(yield from range(10))]
выполняется намного быстрее, чем повторение с помощью range(10)
?
1: для скептического ответа следующие три выражения вызывают объект generator
; попробуйте и назовите type
на них.
Ответы
Ответ 1
g = [(yield i) for i in range(10)]
Эта конструкция накапливает данные, которые/могут быть переданы обратно в генератор через его метод send()
и возвращает его через исключение StopIteration
, когда итерация исчерпана 1:
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> g.send('abc')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: ['abc', 123, 4.5]
>>> # ^^^^^^^^^^^^^^^^^
Ничего подобного не происходит с понятием простого генератора:
>>> g = (i for i in range(3))
>>> next(g)
0
>>> g.send('abc')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
Что касается версии yield from
- в Python 3.5 (которую я использую), она не работает вне функций, поэтому иллюстрация немного отличается:
>>> def f(): return [(yield from range(3))]
...
>>> g = f()
>>> next(g)
0
>>> g.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in f
AttributeError: 'range_iterator' object has no attribute 'send'
OK, send()
не работает для генератора yield
ing from
range()
, но пусть по крайней мере посмотрит, что в конце итерации:
>>> g = f()
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None]
>>> # ^^^^^^
1 Обратите внимание, что даже если вы не используете метод send()
, предполагается send(None)
, поэтому построенный таким образом генератор всегда использует больше памяти, чем простое понимание генератора (так как это должен накапливать результаты выражения yield
до конца итерации):
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None]
UPDATE
Относительно различий в производительности между тремя вариантами. yield from
превосходит два других, потому что он устраняет уровень косвенности (что, насколько мне известно, является одной из двух основных причин, по которым был введен yield from
). Однако в этом конкретном примере сам по себе yield from
лишний - g = [(yield from range(10))]
фактически почти идентичен g = range(10)
.
Ответ 2
Возможно, это не так, как вы думаете.
def f2():
for i in [(yield from range(10))]:
print(i)
Назовите его:
>>> def f2():
... for i in [(yield from range(10))]:
... print(i)
...
>>> f2() #Doesn't print.
<generator object f2 at 0x02C0DF00>
>>> set(f2()) #Prints `None`, because `(yield from range(10))` evaluates to `None`.
None
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Поскольку yield from
не находится в понимании, он привязан к функции f2
вместо неявной функции, превращая f2
в функцию генератора.
Я вспомнил, как кто-то заметил, что на самом деле он не повторяется, но я не могу вспомнить, где я это видел. Я сам тестировал код, когда открыл его. Я не нашел исходный поиск через почтовый список и Отслеживание ошибок. Если кто-то найдет источник, скажите мне или добавьте его в сообщение, поэтому его можно зачислить.