Безопасно ли выводить изнутри блок «с» в Python (и почему)? - PullRequest
43 голосов
/ 26 марта 2009

Сочетание сопрограмм и получения ресурсов может иметь непредвиденные (или не интуитивные) последствия.

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

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()

Ответы [ 5 ]

22 голосов
/ 26 марта 2009

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

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

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

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

generator = coroutine()
generator.next()
lock.acquire() # whoops!
9 голосов
/ 26 марта 2009

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

with coroutine() as cr:
  doSomething(cr)

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

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

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

1 голос
/ 26 марта 2009

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

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

0 голосов
/ 19 января 2018

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

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

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

TLDR: контекст 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
0 голосов
/ 26 марта 2009

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

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

...