Различия между генераторами выражений понимания
Насколько я знаю, есть три способа создания генератора посредством понимания 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
,
Как упоминал Леон в комментарии, эффективность генератора лучше всего измеряется скоростью, с которой он может повторяться. Поэтому я изменил три функции, чтобы они перебирали генераторы и вызывали фиктивную функцию.
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
на них.
3 ответа
Это то, что вы должны делать:
g = (i for i in range(10))
Это выражение генератора. Это эквивалентно
def temp(outer):
for i in outer:
yield i
g = temp(range(10))
но если вы просто хотели итерируемый с элементами range(10)
Вы могли бы сделать
g = range(10)
Вам не нужно оборачивать все это в функцию.
Если вы здесь, чтобы узнать, какой код писать, вы можете перестать читать. Остальная часть этого поста - это длинное и техническое объяснение того, почему другие фрагменты кода не работают, и их не следует использовать, в том числе объяснение того, почему ваш тайминг тоже нарушен.
Это:
g = [(yield i) for i in range(10)]
это сломанная конструкция, которая должна была быть снята много лет назад. Спустя 8 лет после того, как проблема была первоначально сообщена, процесс ее устранения наконец начинается. Не делай этого.
Хотя это все еще на языке, на Python 3 это эквивалентно
def temp(outer):
l = []
for i in outer:
l.append((yield i))
return l
g = temp(range(10))
Понимания списков должны возвращать списки, но из-за yield
этот не делает. Он действует как выражение генератора и выдает то же самое, что и ваш первый фрагмент, но создает ненужный список и присоединяет его к StopIteration
поднят в конце.
>>> g = [(yield i) for i in range(10)]
>>> [next(g) for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None, None, None, None, None, None, None, None]
Это сбивает с толку и трата памяти. Не делай этого. (Если вы хотите знать, где все эти None
Прочитайте PEP 342.)
На Python 2 g = [(yield i) for i in range(10)]
делает что-то совершенно другое. Python 2 не дает пониманиям списков свою собственную область - в частности, списочные, а не диктатные или заданные - поэтому yield
выполняется любой функцией, содержащей эту строку. На Python 2 это:
def f():
g = [(yield i) for i in range(10)]
эквивалентно
def f():
temp = []
for i in range(10):
temp.append((yield i))
g = temp
изготовление f
сопрограмма на основе генератора в предсинхронном смысле. Опять же, если вашей целью было получить генератор, вы потратили кучу времени на создание бессмысленного списка.
Это:
g = [(yield from range(10))]
глупо, но на этот раз никто не обвиняет Python.
Здесь нет ни понимания, ни гена-выражения. Скобки не являются списком понимания; вся работа выполняется yield from
, а затем вы строите список из 1 элемента, содержащий (бесполезное) возвращаемое значение yield from
, Ваш f3
:
def f3():
g = [(yield from range(10))]
когда лишено ненужного построения списка, упрощается
def f3():
yield from range(10)
или, игнорируя все сопутствующие вещи поддержки yield from
делает,
def f3():
for i in range(10):
yield i
Ваши сроки также сломаны.
В первый раз, f1
а также f2
создавать объекты-генераторы, которые можно использовать внутри этих функций, хотя f2
Генератор странный. f3
не делает этого; f3
является функцией генератора. f3
Тело не бежит в твои времена, и если это так, его g
будет вести себя совсем не так, как другие функции g
s. Время, которое на самом деле было бы сопоставимо с f1
а также f2
было бы
def f4():
g = f3()
Во второй раз, f2
на самом деле не работает, по той же причине f3
был сломан в предыдущем времени. Во второй раз, f2
не перебирает генератор. Вместо этого yield from
витки f2
в саму функцию генератора.
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'
в порядке, send()
не работает для генератора yield
ИНГ 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]
ОБНОВИТЬ
По поводу различий в производительности между тремя вариантами. yield from
бьет два других, потому что это устраняет уровень косвенности (что, насколько я понимаю, является одной из двух основных причин, почему yield from
был представлен). Тем не менее, в этом конкретном примере yield from
само по себе лишнее - g = [(yield from range(10))]
на самом деле почти идентичен g = range(10)
,
Это может не делать то, что вы думаете, что делает.
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
в функцию генератора.
Я вспомнил, как кто-то заметил, что на самом деле это не повторяется, но я не могу вспомнить, где я это видел. Я сам тестировал код, когда обнаружил это заново. Я не нашел источник, ищущий через почту списка рассылки, ни ветку трекера ошибок. Если кто-то найдет источник, пожалуйста, сообщите мне или добавьте его в сам пост, чтобы его можно было зачислить.