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

StackOverflow https://stackoverflow.com/questions/685046

  •  22-08-2019
  •  | 
  •  

Вопрос

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

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

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 освобождает управление.


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

  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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top