Безопасно ли выходить из блока" with " в Python (и почему)?

комбинация корутинов и приобретения ресурсов, похоже, может иметь некоторые непреднамеренные (или непреднамеренные) последствия.

основной вопрос заключается в том, работает ли что-то подобное:

def coroutine():
    with open(path, 'r') as fh:
        for line in fh:
            yield line

что он и делает. (Вы можете проверить это!)

более глубокое беспокойство это with должно быть что-то альтернативное finally, где вы гарантируете, что ресурс будет выпущен в конце блока. Coroutines может приостановить и возобновить казнь из внутри the with блок, так что как разрешить конфликт?

например, если вы открываете файл с read / write как внутри, так и снаружи coroutine, пока coroutine еще не вернулся:

def coroutine():
    with open('test.txt', 'rw+') as fh:
        for line in fh:
            yield line

a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
    for line in fh:
        print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?

обновление

Я собирался для записи заблокированного файла обрабатывать конфликт в предыдущем примере, но так как большинство ОС выделяют файловые ручки для каждого процесса, там не будет никакого конфликта. (Спасибо @миль указывать на пример не имело смысла.) Вот мой пересмотренный пример, который показывает реальное состояние тупика:

import threading

lock = threading.Lock()

def coroutine():
    with lock:
        yield 'spam'
        yield 'eggs'

generator = coroutine()
assert generator.next()
with lock: # Deadlock!
    print 'Outside the coroutine got the lock'
assert generator.next()

5 ответов


Я действительно не понимаю, о каком конфликте вы спрашиваете, ни проблема с примером: хорошо иметь два сосуществующих независимых дескриптора в одном файле.

одна вещь, я не знал, что я узнал в ответ на ваш вопрос, что есть новый метод close() на генераторы:

close() возникает новый вопрос:GeneratorExit исключение внутри генератора для завершения итерации. При получении этого исключения код генератора должен либо поднять GeneratorExit или StopIteration.

close() вызывается, когда генератор собирается мусором, поэтому это означает, что код генератора получает последний шанс запустить перед уничтожением генератора. Этот последний шанс означает, что try...finally операторы в генераторах теперь могут гарантированно работать;finally предложение теперь всегда будет иметь возможность работать. Это похоже на незначительную языковую мелочь, но с использованием генераторов и try...finally фактически необходимо для того чтобы снабдить with заявление, описанное PEP 343.

http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features

так, что обрабатывает ситуацию, когда with оператор используется в генераторе, но он дает в середине, но никогда не возвращается -__exit__ метод будет вызван, когда генератор будет собран мусора.


редактировать:

в отношении проблема с обработкой файлов: я иногда забываю, что существуют платформы, которые не похожи на POSIX. :)

что касается замков, я думаю, что Рафал Даугирд ударяет головой по гвоздю, когда он говорит: "Вы просто должны знать, что генератор так же, как и любой другой объект, который содержит ресурсы.- Я не думаю, что ... --9--> утверждение действительно актуально здесь, так как эта функция страдает от тех же проблем с тупиком:

def coroutine():
    lock.acquire()
    yield 'spam'
    yield 'eggs'
    lock.release()

generator = coroutine()
generator.next()
lock.acquire() # whoops!

Я не думаю, что есть реальный конфликт. Вы просто должны знать, что генератор так же, как и любой другой объект, который содержит ресурсы, поэтому ответственность создателя заключается в том, чтобы убедиться, что он правильно завершен (и избежать конфликтов/тупика с ресурсами, удерживаемыми объектом). Единственная (незначительная) проблема, которую я вижу здесь, заключается в том, что генераторы не реализуют протокол управления контекстом (по крайней мере, с Python 2.5), поэтому вы не можете просто:

with coroutine() as cr:
  doSomething(cr)

но вместо этого кому:

cr = coroutine()
try:
  doSomething(cr)
finally:
  cr.close()

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


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

генераторы, однако, всегда (почти всегда) "закрыты", либо с явным close() вызов, или просто путем сбора мусора. Закрытие генератора выбрасывает GeneratorExit исключение внутри генератора и следовательно, запускает предложения finally, с очисткой оператора и т. д. Вы можете поймать исключение, но вы должны бросить или выйти из функции (т. е. бросить StopIteration исключение), а не доходность. Вероятно, это плохая практика полагаться на сборщик мусора, чтобы закрыть генератор в случаях, как вы написали, потому что это может произойти позже, чем вы хотите, и если кто-то вызывает sys._exit (), тогда ваша очистка может вообще не произойти.


вот как я ожидал, что все будет работать. Да, блок не выпустит свои ресурсы, пока он не завершится, поэтому в этом смысле ресурс избежал лексического вложения. Однако это ничем не отличается от вызова функции, которая пыталась использовать тот же ресурс в блоке with - ничто не помогает вам в случае, когда блок имеет не все же прекращено, ибо все причина. Ничего особенного в генераторах.

один вещь, о которой стоит беспокоиться, - это поведение, если генератор никогда возобновлено. Я ожидал with блок, чтобы действовать как finally заблокировать и вызвать __exit__ часть по окончании, но это, похоже, не так.


для TLDR посмотрите на это так:

with Context():
    yield 1
    pass  # explicitly do nothing *after* yield
# exit context after explicitly doing nothing

на Context заканчивается pass - это сделано (т. е. ничего), pass выполняется после yield делается (т. е. выполнение возобновляется). Итак,with заканчивается после управление возобновлено на yield.

TLDR: A with контекст остается, когда yield контроля версий.


на самом деле есть только два правила, которые здесь актуальны:

  1. когда делает with выпустить свой ресурс?

    это так после и непосредственно после его блок делается. Первое означает, что он не освобождает во время a yield, как это может произойти несколько раз. Позже означает, что он выпускает после yield завершено.

  2. когда yield полной?

    думать yield как обратный вызов: управление передается вызывающему абоненту, а не вплоть до вызываемого. Аналогично,yield завершается, когда элемент управления передается обратно, так же, как при вызове возвращает элемент управления.

обратите внимание, что оба with и yield работают по назначению здесь! Пункт with lock для защиты ресурса, и он остается защищенным во время yield. Вы всегда можете явно освободить эту защиту:

def safe_generator():
  while True:
    with lock():
      # keep lock for critical operation
      result = protected_operation()
    # release lock before releasing control
    yield result