Подтвердить что ты не робот

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

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

Основной вопрос: работает ли что-то подобное:

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

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

Более глубокое беспокойство состоит в том, что with должно быть чем-то альтернативой finally, где вы гарантируете, что ресурс будет выпущен в конце блока. Coroutines может приостановить и возобновить выполнение из блока 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?

Update

В предыдущем примере я собирался конкурировать с файлами с блокировкой записи, но поскольку большинство операционных систем выделяют дескрипторы файлов для каждого процесса, там не будет никаких утверждений. (Kudos @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()
4b9b3361

Ответ 1

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

Одна вещь, которую я не знал, что я узнал в ответ на ваш вопрос, что существует новый метод 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__ вызывается, когда генератор собирает мусор.


Edit

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

Что касается замков, я думаю, что Rafał Dowgird бьет головой по гвоздю, когда говорит: "Вы просто должны знать, что генератор точно так же, как и любой другой объект, который содержит ресурсы". Я не думаю, что инструкция with действительно актуальна здесь, так как эта функция страдает от одних и тех же ошибок:

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

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

Ответ 2

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

with coroutine() as cr:
  doSomething(cr)

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

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

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

Ответ 3

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

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

Ответ 4

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

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