2013-03-12 2 views
18

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

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

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

ответ

10

Data model entry for 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. Теперь вы только см. Ошибку атрибута. Возможно, эмпирическое правило должно быть таким же, как с любой обработкой исключений - только перехват Исключения, если вы знаете, как обращаться с ними. Имея __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" 
+1

Это удивительный ответ! – lukecampbell

+0

@mgilson Спасибо за отличный ответ. Кажется, что функция генератора не является тем, что улавливает ошибку атрибута. Это то поведение, которое я хотел, но, похоже, это невозможно. Я хочу использовать один оператор with для упорядочения обработки исключений в последовательности генераторов. – David

+0

@David - контекстные менеджеры - это то, над чем я работал с приличным количеством в последнее время, поэтому они свежие на моем уме :). Это был хороший вопрос. Я хочу, чтобы все новые пользователи задавали такие приятные первые вопросы :). – mgilson

1
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 
+0

Документы утверждают, что '__exit__' не должен повторно поднимать исключение, переданное в ... – mgilson

+0

@mgilson, Odd ... Я ущипнул эту идею от contextlib.py –

+0

Честно говоря, я действительно не знаю почему ты не должен * ... Хотя я не знаю, зачем тебе это нужно. Если вы вернете значение «False-y», исключение продолжает распространяться в любом случае. – mgilson

Смежные вопросы