Как использовать диспетчер контекста python внутри генератора

В python должны использоваться внутрисегменты внутри генератора? Чтобы быть ясным, я не прошу об использовании декоратора для создания диспетчера контекста из функции генератора. Я спрашиваю, есть ли неотъемлемая проблема, используя оператор-оператор в качестве менеджера контекста внутри генератора, поскольку он будет ловить StopIteration и GeneratorExit исключения, по крайней мере, в некоторых случаях. Ниже приводятся два примера.

Хороший пример проблемы поднят примером Безли (стр. 106). Я изменил его, чтобы использовать оператор with, чтобы файлы были явно закрыты после выхода в методе openener. Я также добавил два способа исключения исключения при повторении результатов.

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 lines\n' % i

В этом примере менеджер контекста успешно закрывает файлы в функции открывания. Когда возникает исключение, я вижу след обратно из исключения, но генератор останавливается молча. Если оператор with-catch обнаруживает исключение, почему генератор не продолжается?

Когда я определяю свои собственные контекстные менеджеры для использования внутри генератора. Я получаю ошибки времени выполнения, говоря, что я проигнорировал 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 без каких-либо исключений, но не работает в случае2, где возникает ошибка атрибута. Здесь я вижу a RuntimeException, потому что оператор with поймал и проигнорировал исключение GeneratorExit.

Может кто-нибудь помочь прояснить правила для этого сложного варианта использования? Я подозреваю, что это то, что я делаю, или не делаю в моем методе __exit__. Я попытался добавить код для повторного рейза GeneratorExit, но это не помогло.

Ответы

Ответ 1

из Запись модели данных для object.__exit__

Если предоставляется исключение, и метод хочет подавить исключение (т.е. предотвратить его распространение), он должен вернуть истинное значение. В противном случае исключение будет обработано обычно после выхода из этого метода.

В вашей функции __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. Теперь вы видите только ошибку атрибута. Возможно, эмпирическое правило должно быть таким же, как с любой обработкой Exception - только перехватывать исключения, если вы знаете, как их обрабатывать. Наличие __exit__ return 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"

Ответ 2

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 *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