Как использовать контекстный менеджер python внутри генератора
в python следует ли использовать операторы with-внутри генератора? Чтобы быть ясным, я не спрашиваю об использовании декоратора для создания контекстного менеджера из функции генератора. Я спрашиваю, есть ли неотъемлемая проблема с использованием оператора with в качестве контекстного менеджера внутри генератора, поскольку он поймает StopIteration
и GeneratorExit
исключения по крайней мере в некоторых случаях. Приведем два примера.
хороший пример проблемы поднят примером Бизли (стр. 106). Я изменил его на используйте оператор with, чтобы файлы были явно закрыты после выхода в методе opener. Я также добавил два способа создания исключения при итерации результатов.
import os
import fnmatch
def find_files(topdir, pattern):
for path, dirname, filelist in os.walk(topdir):
for name in filelist:
if fnmatch.fnmatch(name, pattern):
yield os.path.join(path,name)
def opener(filenames):
f = None
for name in filenames:
print "F before open: '%s'" % f
#f = open(name,'r')
with open(name,'r') as f:
print "Fname: %s, F#: %d" % (name, f.fileno())
yield f
print "F after yield: '%s'" % f
def cat(filelist):
for i,f in enumerate(filelist):
if i ==20:
# Cause and exception
f.write('foobar')
for line in f:
yield line
def grep(pattern,lines):
for line in lines:
if pattern in line:
yield line
pylogs = find_files("/var/log","*.log*")
files = opener(pylogs)
lines = cat(files)
pylines = grep("python", lines)
i = 0
for line in pylines:
i +=1
if i == 10:
raise RuntimeError("You're hosed!")
print 'Counted %d linesn' % i
в этом примере context manager успешно закрывает файлы в функции opener. Когда возникает исключение, я вижу след от исключения, но генератор молча останавливается. Если оператор with-ловит исключение, почему генератор не продолжать?
когда я определяю свои собственные менеджеры контекста для использования внутри генератора. Я получаю ошибки выполнения, говоря, что я проигнорировал GeneratorExit
. Например:
class CManager(object):
def __enter__(self):
print " __enter__"
return self
def __exit__(self, exctype, value, tb):
print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
return True
def foo(n):
for i in xrange(n):
with CManager() as cman:
cman.val = i
yield cman
# Case1
for item in foo(10):
print 'Pass - val: %d' % item.val
# Case2
for item in foo(10):
print 'Fail - val: %d' % item.val
item.not_an_attribute
эта небольшая демонстрация отлично работает в case1 без каких-либо исключений, но терпит неудачу в case2, где возникает ошибка атрибута. Здесь я вижу RuntimeException
поднят, потому что оператор with поймал и проигнорировал GeneratorExit
исключения.
может кто-нибудь помочь уточнить правила для этого сложного случая использования? Я подозреваю, что это то, что я делаю или не делаю в своем __exit__
метод. Я попытался добавить код для повторного вызова GeneratorExit
, но это не помогло.
2 ответов
С ввод модели данных для object.__exit__
если предоставлено исключение, и метод хочет подавить исключение (т. е. предотвратить его распространение), он должен вернуть значение true. В противном случае исключение будет обработано нормально при выходе из этого метода.
в своем __exit__
функция, вы возвращаетесь True
, который будет подавлять все исключения. Если вы измените его, чтобы вернуться False
, исключения будут по-прежнему подниматься как обычно (с той лишь разницей, что вы гарантируете, что ваш __exit__
функция вызывается, и вы можете убедиться, убирать за собой)
например, изменение кода:
def __exit__(self, exctype, value, tb):
print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
if exctype is GeneratorExit:
return False
return True
позволяет делать правильные вещи и не подавляют GeneratorExit
. Теперь ты!--24-->только см. ошибку атрибута. Может быть, правило должно быть таким же как и при любой обработке исключений -- только перехватывать исключения, если вы знаете, как с ними обращаться. Имея __exit__
возвращение True
находится на одном уровне (может чуть хуже!) чем голый кроме:
try:
something()
except: #Uh-Oh
pass
обратите внимание, что когда AttributeError
поднят (и не пойман), я считаю, что это приводит к тому, что счетчик ссылок на ваш объект генератора падает до 0, который затем запускает GeneratorExit
исключение в генераторе, чтобы он мог очистить себя. Используя мой __exit__
, поиграйте со следующими двумя случаями и надеюсь, вы поймете, что я имею в виду:
try:
for item in foo(10):
print 'Fail - val: %d' % item.val
item.not_an_attribute
except AttributeError:
pass
print "Here" #No reference to the generator left.
#Should see __exit__ before "Here"
и
g = foo(10)
try:
for item in g:
print 'Fail - val: %d' % item.val
item.not_an_attribute
except AttributeError:
pass
print "Here"
b = g #keep a reference to prevent the reference counter from cleaning this up.
#Now we see __exit__ *after* "Here"
class CManager(object):
def __enter__(self):
print " __enter__"
return self
def __exit__(self, exctype, value, tb):
print " __exit__; excptype: '%s'; value: '%s'" % (exctype, value)
if exctype is None:
return
# only re-raise if it's *not* the exception that was
# passed to throw(), because __exit__() must not raise
# an exception unless __exit__() itself failed. But throw()
# has to raise the exception to signal propagation, so this
# fixes the impedance mismatch between the throw() protocol
# and the __exit__() protocol.
#
if sys.exc_info()[1] is not (value or exctype()):
raise