Безопасно ли выходить из блока" 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
контроля версий.
на самом деле есть только два правила, которые здесь актуальны:
-
когда делает
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