Насколько я понимаю yield from
, это похоже на yield
извлечение каждого элемента из итерируемого объекта. Тем не менее, я наблюдаю другое поведение в следующем примере.
у меня есть Class1
class Class1:
def __init__(self, gen):
self.gen = gen
def __iter__(self):
for el in self.gen:
yield el
и Class2, которые отличаются только заменой yield
в цикле for на yield from
class Class2:
def __init__(self, gen):
self.gen = gen
def __iter__(self):
yield from self.gen
Код ниже считывает первый элемент из экземпляра данного класса, а затем считывает остальные в цикле for:
a = Class1((i for i in range(3)))
print(next(iter(a)))
for el in iter(a):
print(el)
Это дает разные результаты для Class1
и Class2
. Для Class1
вывод
0
1
2
а для Class2
вывод
0
Какой механизм стоит за yield from
, который вызывает другое поведение?
Как ни странно, с Class2
, если вы извлечете iter(a)
в переменную (b = iter(a); print(next(b))
), это будет работать так же, как Class1
, то есть напечатать все числа. Это сбивает с толку и очень интересно.
Да, и если вы это сделаете del b
, он напечатает только первый @YevhenKuzmovych
@YevhenKuzmovych Уже слишком много фиктивных проблем, лучше спросите в вместо этого обсудите «Помощь по Python».
Когда вы используете next(iter(instance_of_Class2))
, iter()
вызывает .close()
внутренний генератор, когда он (итератор, а не генератор!) выходит за пределы области видимости (и удаляется), а с Class1
iter()
только закрывает свой экземпляр
>>> g = (i for i in range(3))
>>> b = Class2(g)
>>> i = iter(b) # hold iterator open
>>> next(i)
0
>>> next(i)
1
>>> del(i) # closes g
>>> next(iter(b))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Это поведение описано в PEP 342 в двух частях.
- Добавьте поддержку, чтобы гарантировать, что close() вызывается, когда итератор генератора очищается от мусора.
То, что происходит, немного яснее (хотя, возможно, удивительно), когда происходит делегирование нескольких генераторов; только делегируемый генератор закрывается при удалении его оболочки iter
>>> g1 = (a for a in range(10))
>>> g2 = (a for a in range(10, 20))
>>> def test3():
... yield from g1
... yield from g2
...
>>> next(test3())
0
>>> next(test3())
10
>>> next(test3())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Class2
Какие варианты есть, чтобы заставить Class2
вести себя так, как вы ожидаете?
Примечательно, что другие стратегии, хотя они и не обладают визуально приятным сахаром yield from
или некоторыми из его потенциальных преимуществ, дают вам возможность взаимодействовать со значениями, что кажется основным преимуществом.
>>> class Class3:
... def __init__(self, gen):
... self.iterator = iter(gen)
...
... def __iter__(self):
... return self.iterator
...
>>> c = Class3((i for i in range(3)))
>>> next(iter(c))
0
>>> next(iter(c))
1
iter()
несоответствия - см. комментарии ниже (например, почему e
не закрыт?)itertools.chain.from_iterable
>>> class Class5(collections.abc.Generator):
... def __init__(self, gen):
... self.gen = gen
... def send(self, value):
... return next(self.gen)
... def throw(self, value):
... raise StopIteration
... def close(self): # optional, but more complete
... self.gen.close()
...
>>> e = Class5((i for i in range(10)))
>>> next(e) # NOTE iter is not necessary!
0
>>> next(e)
1
>>> next(iter(e)) # but still works
2
>>> next(iter(e)) # doesn't close e?? (should it?)
3
>>> e.close()
>>> next(e)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/_collections_abc.py", line 330, in __next__
return self.send(None)
File "<stdin>", line 5, in send
StopIteration
Лучшая подсказка заключается в том, что если вы попытаетесь снова напрямую, next(iter(instance))
повышает StopIteration
, указывая на то, что генератор постоянно закрыт (либо из-за истощения, либо из-за .close()
), и почему повторение его с помощью цикла for
больше не дает значений.
>>> a = Class1((i for i in range(3)))
>>> next(iter(a))
0
>>> next(iter(a))
1
>>> b = Class2((i for i in range(3)))
>>> next(iter(b))
0
>>> next(iter(b))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Однако, если мы назовем итератор, он будет работать, как и ожидалось.
>>> b = Class2((i for i in range(3)))
>>> i = iter(b)
>>> next(i)
0
>>> next(i)
1
>>> j = iter(b)
>>> next(j)
2
>>> next(i)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Для меня это говорит о том, что когда у итератора нет имени, он вызывает .close()
, когда выходит за пределы области видимости.
>>> def gen_test(iterable):
... yield from iterable
...
>>> g = gen_test((i for i in range(3)))
>>> next(iter(g))
0
>>> g.close()
>>> next(iter(g))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Разобрав результат, мы обнаруживаем, что внутренности немного отличаются
>>> a = Class1((i for i in range(3)))
>>> dis.dis(a.__iter__)
6 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (gen)
4 GET_ITER
>> 6 FOR_ITER 10 (to 18)
8 STORE_FAST 1 (el)
7 10 LOAD_FAST 1 (el)
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE 6
>> 18 LOAD_CONST 0 (None)
20 RETURN_VALUE
>>> b = Class2((i for i in range(3)))
>>> dis.dis(b.__iter__)
6 0 LOAD_FAST 0 (self)
2 LOAD_ATTR 0 (gen)
4 GET_YIELD_FROM_ITER
6 LOAD_CONST 0 (None)
8
10 POP_TOP
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
Примечательно, что версия yield from
имеет GET_YIELD_FROM_ITER
Если
TOS
является итератором генератора или объектом сопрограммы, он остается как есть. В противном случае реализуетTOS = iter(TOS)
.
(незаметно, ключевое слово YIELD_FROM
кажется удаленным в 3.11)
Итак, если данный итерируемый объект (для класса) является итератором генератора, он будет передан напрямую, давая результат, который мы (можем) ожидать
Передача итератора, который не является генератором (iter()
каждый раз создает новый итератор в обоих случаях)
>>> a = Class1([i for i in range(3)])
>>> next(iter(a))
0
>>> next(iter(a))
0
>>> b = Class2([i for i in range(3)])
>>> next(iter(b))
0
>>> next(iter(b))
0
Явное закрытие внутреннего генератора Class1
>>> g = (i for i in range(3))
>>> a = Class1(g)
>>> next(iter(a))
0
>>> next(iter(a))
1
>>> a.gen.close()
>>> next(iter(a))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
генератор закрывается только iter
при удалении, если экземпляр выталкивается
>>> g = (i for i in range(10))
>>> b = Class2(g)
>>> i = iter(b)
>>> next(i)
0
>>> j = iter(b)
>>> del(j) # next() not called on j
>>> next(i)
1
>>> j = iter(b)
>>> next(j)
2
>>> del(j) # generator closed
>>> next(i) # now fails, despite range(10) above
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Вы также можете посмотреть на PEP, там вы можете увидеть close
звонок.
Это объяснимо, но все еще довольно непрозрачно, почему! .. но из этого PEP 342 выглядит так, как будто он приносит как новый метод close , так и пробирается 5. Добавьте поддержку, чтобы гарантировать, что close() вызывается, когда итератор генератора очищается от мусора.
@ti7 Почему итератор генератора собирает мусор, если на него все еще ссылается self.gen?
дело не в том, что нет остаточной ссылки (есть, вот как она вызывает StopIteration
, а не AttributeError
или NameError
и т. д.), скорее это происходит, когда iter()
' собирается и явно вызывает .close()
на генераторе, который он обертывает
Это всего лишь мое субъективное мнение, но это похоже на ошибку, хотя и соответствует спецификации. Если спецификация предписывает это, то я бы сказал, что спецификация предписывает неправильное поведение. Тем не менее, я не уверен на 100%, что спецификация предписывает такое поведение, потому что оно зависит от того, когда объект подвергается сборке мусора, и, насколько мне известно, спецификация не говорит, когда именно это должно произойти. В спецификации должна быть предусмотрена отсрочка сборки мусора до тех пор, пока генератор не будет исчерпан обычным образом.
мне трудно выбрать сторону - честно говоря, я в основном нахожусь в лагере "не делай этого", даже если это окажется ошибкой десятилетней давности... поведение здесь должно быть почти наверняка более последовательным , но, возможно, iter(generator)
следует raise RuntimeError
и/или yield from
(или просто yield
) повышать SyntaxError
в некоторых зарезервированных методах дандера, форсируя return
и предпочитая next()
(или await
) внутренне! PEP 525 для асинхронных генераторов также намекает на полезность imo, даже не реализуя делегирование и предлагая async for
peps.python.org/pep-0525/#asynchronous-yield-from
обновлен
Я не вижу в этом ничего сложного, и результирующее поведение можно рассматривать как на самом деле неудивительное.
Когда итератор выходит за пределы области действия, Python выдает исключение «GeneratorExit» в (самом внутреннем) генераторе.
В «классической» for
форме исключение происходит в написанном пользователем __iter__
методе, не перехватывается и подавляется при всплытии механизмами генератора.
В форме yield from
такое же исключение генерируется во внутреннем self.gen
, тем самым «убивая» его, и всплывает до написанного пользователем __iter__
.
Написание другого промежуточного генератора может сделать это легко видимым:
def inner_gen(gen):
try:
for item in gen:
yield item
except GeneratorExit:
print("Generator exit thrown in inner generator")
class Class1:
def __init__(self, gen):
self.gen = inner_gen(gen)
def __iter__(self):
try:
for el in self.gen:
yield el
except GeneratorExit:
print("Generator exit thrown in outer generator for 'classic' form")
class Class2(Class1):
def __iter__(self):
try:
yield from self.gen
except GeneratorExit as exit:
print("Generator exit thrown in outer generator for 'yield from' form" )
first = lambda g:next(iter(g))
И сейчас:
In [324]: c1 = Class1((i for i in range(2)))
In [325]: first(c1)
Generator exit thrown in outer generator for 'classic' form
Out[325]: 0
In [326]: first(c1)
Generator exit thrown in outer generator for 'classic' form
Out[326]: 1
In [327]: c2 = Class2((i for i in range(2)))
In [328]: first(c2)
Generator exit thrown in inner generator
Generator exit thrown in outter generator for 'yield from' form
Out[328]: 0
In [329]: first(c2)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
Cell In[329], line 1
(...)
StopIteration:
обновлять
У меня был предыдущий текст ответа, в котором предполагалось, как будет происходить вызов close
, пропуская промежуточный генератор - хотя это не так просто в отношении close
: Python всегда будет вызывать __del__
, а не close
, который вызывается только пользователем или в определенных случаях. обстоятельства, которые трудно было уловить. Но он всегда будет генерировать исключение GeneratorExit
в теле функции-генератора (но не в классе с explict __next__
и throw
— давайте пропустим это для другого вопроса :-D)
Отличное простое объяснение, спасибо за это!
несмотря на то, что мой ответ здесь был только текстовым, я провел несколько тестов в интерактивной среде. Я скопирую и вставлю фрагменты сюда. (возможно, что я явно не тестировал этот обход, но сейчас сделаю это).
Правильно протестировать этот материал еще сложнее :-) и, похоже, он не «укладывается в голове сразу» — но окончательный вывод может быть еще проще: Python бросает GeneratorExit
в самый внутренний генератор, когда он выходит за пределы области видимости и пусть размножается. В форме «для» это написанный пользователем метод __iter__
,
@joanis: получилось еще проще, но мне пришлось полностью переписать.
Я добавил код, который использовал для проверки вызовов __del__
и __close__
в каждом случае, здесь: gist.github.com/jsbueno/e4378521ead8f9dbb40565fb5cacd0b9
Не совсем ответ на ваш вопрос, но stackoverflow.com/a/26109157/3216427 предоставляет больше способов, которыми yield from отличается от цикла над yield.