Безопасно ли выводить данные из блока “with” в Python (и почему)?
Вопрос
Комбинация сопрограмм и приобретения ресурсов, похоже, может иметь некоторые непреднамеренные (или неинтуитивные) последствия.
Основной вопрос заключается в том, работает ли что-то подобное или нет:
def coroutine():
with open(path, 'r') as fh:
for line in fh:
yield line
Что он и делает.(Вы можете проверить это!)
Более глубокая проблема заключается в том, что with
предполагается, что это что-то альтернативное finally
, где вы гарантируете, что ресурс будет освобожден в конце блока.Сопрограммы могут приостанавливать и возобновлять выполнение с внутри в with
блок, так что как разрешается конфликт?
Например, если вы открываете файл с функцией чтения / записи как внутри, так и вне сопрограммы, в то время как сопрограмма еще не вернула:
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?
Обновить
В предыдущем примере я собирался использовать конфликт дескрипторов файлов с блокировкой записи, но поскольку большинство операционных систем выделяют дескрипторы файлов для каждого процесса, конфликта не будет.(Спасибо @Miles за указание на то, что пример не имел особого смысла.) Вот мой пересмотренный пример, который показывает реальное состояние взаимоблокировки:
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()
Решение
Я действительно не понимаю, ни о каком конфликте вы спрашиваете, ни о проблеме с примером:нормально иметь два сосуществующих независимых дескриптора одного и того же файла.
Одна вещь, которую я не знал, которую я узнал в ответ на ваш вопрос, это то, что в генераторах существует новый метод close():
close()
поднимает новыйGeneratorExit
исключение внутри генератора для завершения итерации.При получении этого исключения код генератора должен либо вызватьGeneratorExit
илиStopIteration
.
close()
вызывается, когда генератор собирает мусор, так что это означает, что код генератора получает последний шанс для запуска перед уничтожением генератора.Этот последний шанс означает, чтоtry...finally
теперь можно гарантировать, что инструкции в генераторах будут работать;вfinally
теперь у clause всегда будет шанс убежать.Это кажется незначительной языковой мелочью, но использование генераторов иtry...finally
на самом деле необходимо для того, чтобы реализоватьwith
заявление, описанное в PEP 343.http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features
Таким образом, это справляется с ситуацией, когда with
оператор используется в генераторе, но он выдает результат в середине, но никогда не возвращает — контекстный менеджер __exit__
метод будет вызван, когда генератор будет собран из мусора.
Редактировать:
Что касается проблемы с дескриптором файла:Иногда я забываю, что существуют платформы, которые не похожи на POSIX.:)
Что касается блокировок, я думаю, Рафал Довгирд попадает в самую точку, когда говорит: "Вы просто должны знать, что генератор такой же, как и любой другой объект, содержащий ресурсы". Я не думаю, что with
заявление действительно настолько актуально здесь, поскольку эта функция страдает от тех же проблем взаимоблокировки:
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
освобождает управление.
На самом деле здесь уместны только два правила:
Когда происходит
with
высвободить свой ресурс?Он делает это однажды и непосредственно после его блок завершен.Первое означает, что он не освобождает во время a
yield
, поскольку это могло произойти несколько раз.Более позднее означает, что оно действительно освобождает послеyield
завершилось.Когда происходит
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