¿Es seguro para producir desde dentro de un “con” bloque en Python (y por qué)?

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

  •  22-08-2019
  •  | 
  •  

Pregunta

La combinación de co-rutinas y adquisición de recursos parece que podría tener algunas consecuencias no deseadas (o poco intuitivos).

La pregunta básica es si o no algo como esto funciona:

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

Lo que lo hace. (Puede probarlo!)

La preocupación más profunda es que with se supone que es una alternativa a algo finally, donde se asegura que un recurso es liberado al final del bloque. Corrutinas pueden suspender y reanudar la ejecución de en bloque with, por lo que cómo se resuelve el conflicto?

Por ejemplo, si abre un archivo con lectura / escritura, tanto dentro como fuera de un co-rutina, mientras que la co-rutina aún no ha regresado:

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?

Actualizar

Yo iba a la afirmación identificador de archivo de escritura-bloqueado en el ejemplo anterior, pero como la mayoría de sistemas operativos asignan identificadores de archivo por proceso no habrá discordia allí. (Felicitaciones a @Miles por señalar el ejemplo no demasiado sentido.) Aquí está mi ejemplo revisado, que muestra una verdadera condición de interbloqueo:

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()
¿Fue útil?

Solución

No entiendo muy bien qué es el conflicto que estés preguntando por, ni el problema con el ejemplo:. Está bien tener coexistencia de dos asas independientes en el mismo archivo

Una cosa que no sabía que aprendí en respuesta a su pregunta es que hay un nuevo método close () en los generadores:

  

close() plantea una nueva excepción GeneratorExit el interior del generador de interrumpir la iteración. Al recibir esta excepción, el código del generador debe o bien aumentar o GeneratorExit StopIteration.

     

close() se llama cuando un generador es recolección de basura, por lo que esto significa el código del generador se pone una última oportunidad de correr antes de que se destruyó el generador. Esta última posibilidad significa que las declaraciones try...finally en generadores pueden ahora ser garantizados para trabajar; la cláusula finally ahora siempre tener la oportunidad de correr. Este parece ser un poco menor del lenguaje trivia, pero utilizando generadores y try...finally es realmente necesario con el fin de poner en práctica la declaración with descrito por PEP 343.

     

http://docs.python.org/ whatsnew / 2.5.html # pep-342-nueva-generador de funciones

Así que maneja la situación en la que se utiliza una declaración with en un generador, pero produce en el medio pero nunca se regresa, el método __exit__ del gestor de contexto se llamará cuando el generador está recolección de basura.


Editar

En cuanto a la cuestión de identificador de archivo: a veces me olvido de que existen plataformas que no son POSIX similares. :)

En cuanto a las cerraduras van, creo que Rafał Dowgird golpea la cabeza en el clavo cuando dice "Sólo hay que tener en cuenta que el generador es igual que cualquier otro objeto que contiene los recursos." No creo que la declaración with que es realmente relevante aquí, ya que esta función adolece de los mismos problemas de estancamiento:

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

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

Otros consejos

No creo que hay un conflicto real. Sólo hay que tener en cuenta que el generador es igual que cualquier otro objeto que contiene los recursos, por lo que es responsabilidad del creador para asegurarse de que esté finalizado correctamente (y para evitar conflictos / punto muerto con los recursos mantenidos por el objeto). El único problema (menor) que veo aquí es que los generadores no implementan el protocolo de gestión de contexto (por lo menos a partir de Python 2.5), por lo que puede no sólo:

with coroutine() as cr:
  doSomething(cr)

pero en cambio tiene que:

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

El recolector de basura hace el close() de todos modos, pero es una mala práctica que confiar en que para liberar recursos.

Debido yield puede ejecutar código arbitrario, me gustaría ser muy cuidadosos de que mantiene un bloqueo sobre una declaración de rendimiento. Usted puede obtener un efecto similar en muchas otras maneras, sin embargo, incluyendo llamar a un método o funciones que podrían haber sido anulado o modificado de otro modo.

Generadores, sin embargo, siempre son (casi siempre) "cerrado", ya sea con una llamada explícita close(), o simplemente por estar recolección de basura. Cierre de un generador produce una excepción GeneratorExit el interior del generador y por lo tanto se ejecuta finalmente cláusulas, con la limpieza declaración, etc. Se puede detectar la excepción, pero hay que tirar o salir de la función (es decir, una excepción de StopIteration), en lugar de rendimiento. Es probablemente una mala práctica que confiar en el recolector de basura para cerrar el generador en casos como el que has escrito, porque eso podría ocurrir más tarde de lo que desee, y si alguien llama sys._exit (), a continuación, su limpieza no podría suceder en absoluto .

Esa sería la forma en que esperaba que las cosas funcionen. Sí, el bloque no va a liberar sus recursos hasta que se complete, así que en ese sentido el recurso ha escapado es anidamiento léxico. Sin embargo, pero esto no es diferente a hacer una llamada a la función que trató de usar el mismo recurso dentro de una con bloqueo - nada le ayuda en el caso de que el bloque tiene no todavía terminado, por lo razón. En realidad no es nada específico a los generadores.

Una cosa que podría valer la pena preocuparse, aunque es el comportamiento si el generador es no reanudó. Yo habría esperado que el bloque de with para actuar como un bloque finally y llamar a la parte __exit__ a la terminación, pero eso no parece ser el caso.

Para una TLDR, lo veo de esta manera:

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

El Context termina después se hace pass (es decir, nada), pass ejecuta después yield se realiza (es decir, la ejecución se reanuda). Por lo tanto, los extremos with después de control se reanuda a yield.

TLDR:. Un contexto with permanece retenida cuando el control comunicados yield


En realidad, hay sólo dos reglas que son relevantes aquí:

  1. Cuando hace with liberar sus recursos?

    Lo hace una vez y directamente después su bloque está hecho. El primero significa que no libera durante a yield, ya que ello podría ocurrir varias veces. La tarde es decir la liberación después yield ha completado.

  2. Cuando hace yield completar?

    Piense en yield como una llamada a la inversa: el control se pasa a una persona que llama, no hacia abajo a un llamado uno. Del mismo modo, yield completa cuando el control se pasa de nuevo a él, al igual que cuando una llamada devuelve el control.

Tenga en cuenta que tanto with y yield están funcionando según lo previsto aquí! El punto de un with lock es proteger un recurso y que permanece protegido durante una yield. Siempre se puede liberar de forma explícita esta protección:

def safe_generator():
  while True:
    with lock():
      # keep lock for critical operation
      result = protected_operation()
    # release lock before releasing control
    yield result
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top