Ответ 1
Вы поднимаете критику, которая была поднята до 1. Очистка в этом случае недетерминирована, но это произойдет с CPython, когда генератор получает собранный мусор. Ваш пробег может отличаться для других реализаций python...
Вот пример:
from __future__ import print_function
import contextlib
@contextlib.contextmanager
def manager():
"""Easiest way to get a custom context manager..."""
try:
print('Entered')
yield
finally:
print('Closed')
def gen():
"""Just a generator with a context manager inside.
When the context is entered, we'll see "Entered" on the console
and when exited, we'll see "Closed" on the console.
"""
man = manager()
with man:
for i in range(10):
yield i
# Test what happens when we consume a generator.
list(gen())
def fn():
g = gen()
next(g)
# g.close()
# Test what happens when the generator gets garbage collected inside
# a function
print('Start of Function')
fn()
print('End of Function')
# Test what happens when a generator gets garbage collected outside
# a function. IIRC, this isn't _guaranteed_ to happen in all cases.
g = gen()
next(g)
# g.close()
print('EOF')
Запустив этот script в CPython, я получаю:
$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
EOF
Closed
В основном, мы видим, что для истощенных генераторов менеджер контекста очищает, когда вы ожидаете. Для генераторов, которые не исчерпаны, функция очистки работает, когда генератор собирается сборщиком мусора. Это происходит, когда генератор выходит из области действия (или, IIRC в следующий gc.collect
цикл не позднее).
Однако, выполняя некоторые быстрые эксперименты (например, запуская приведенный выше код в pypy
), я не очищаю всех моих менеджеров контекста:
$ pypy --version
Python 2.7.10 (f3ad1e1e1d62, Aug 28 2015, 09:36:42)
[PyPy 2.6.1 with GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)]
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
End of Function
Entered
EOF
Итак, утверждение, что диспетчер контекста __exit__
будет вызван для всех реализаций python, неверен. Вероятно, промахи здесь связаны с стратегией сбора мусора pypy (которая не является подсчетом ссылок), и к моменту времени pypy
решает собрать генераторы, процесс уже закрывается, и поэтому он не беспокоится об этом... В большинстве приложений реального мира генераторы, вероятно, получат прибыль и завершатся достаточно быстро, чтобы на самом деле это не имело значения...
Предоставление строгих гарантий
Если вы хотите, чтобы ваш менеджер контекста был правильно доработан, вы должны позаботиться о close генераторе, когда закончите с это 2. Разоружение строк g.close()
выше дает мне детерминированную очистку, потому что GeneratorExit
возникает в выражении yield
(который находится внутри диспетчера контекста), а затем он улавливается/подавляется генератором...
$ pypy ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
$ python3 ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
$ python ~/sandbox/cm.py
Entered
Closed
Start of Function
Entered
Closed
End of Function
Entered
Closed
EOF
FWIW, это означает, что вы можете очистить свои генераторы с помощью contextlib.closing
:
from contextlib import closing
with closing(gen_function()) as items:
for item in items:
pass # Do something useful!
1 Совсем недавно некоторые обсуждения вращались вокруг PEP 533, целью которого является очистка итератора больше детерминированным.
2 Вполне нормально закрыть уже закрытый и/или потребляемый генератор, чтобы вы могли его вызвать, не беспокоясь о состоянии генератора.