Est-il sûr de produire à partir d'un « avec » bloc en Python (et pourquoi)?
Question
La combinaison de coroutines et l'acquisition des ressources semble que cela pourrait avoir des non intentionnelles (ou peu intuitives) conséquences.
La question fondamentale est de savoir si quelque chose comme cela fonctionne:
def coroutine():
with open(path, 'r') as fh:
for line in fh:
yield line
Ce qu'il fait. (Vous pouvez le tester!)
La préoccupation profonde est que with
est censé être une alternative à quelque chose finally
, où vous assurer qu'une ressource est libérée à la fin du bloc. Coroutines peut suspendre et reprendre l'exécution de dans les le bloc with
, donc comment le conflit résolu?
Par exemple, si vous ouvrez un fichier en lecture / écriture à la fois à l'intérieur et à l'extérieur d'un coroutine alors que le coroutine n'a pas encore retourné:
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?
Mise à jour
J'allais pour la poignée de fichier verrouillé en écriture contention dans l'exemple précédent, mais puisque la plupart des systèmes d'exploitation allouent par processus handles de fichiers il n'y aura pas de conflit là. (Kudos à @Miles pour remarquer l'exemple ne pas faire trop de sens.) Voici mon exemple révisé, ce qui montre une véritable condition de blocage:
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()
La solution
Je ne comprends pas vraiment ce conflit vous poser des questions sur, ni le problème avec l'exemple. Il est bien d'avoir deux coexistant, poignées indépendantes dans le même fichier
Une chose que je ne savais pas que j'ai appris en réponse à votre question, il qu'il ya une nouvelle méthode close () sur les générateurs:
close()
soulève une nouvelle exceptionGeneratorExit
à l'intérieur du générateur de mettre fin à l'itération. A la réception de cette exception, le code du générateur doit soit augmenterGeneratorExit
ouStopIteration
.
close()
est appelée lorsqu'un générateur est ramasse-miettes, donc cela signifie le code du générateur obtient une dernière chance de courir avant que le générateur est détruit. Cette dernière chance signifie que les déclarations detry...finally
dans les générateurs peuvent désormais être garantis au travail; la clausefinally
va maintenant toujours une chance de courir. Cela semble vraiment nécessaire est comme un peu mineur de trivia de langue, mais en utilisant des générateurs ettry...finally
afin de mettre en œuvre la déclaration dewith
décrit par PEP 343.http://docs.python.org/ whatsnew / 2.5.html # pep-342-nouvelle-générateur caractéristiques
qui gère la situation dans laquelle une déclaration de with
est utilisé dans un générateur, mais il cède au milieu, mais ne retourne-la méthode de __exit__
de gestionnaire de contexte sera appelé lorsque le générateur est collecté des déchets.
Modifier :
En ce qui concerne la question de la poignée de fichiers: J'oublie parfois qu'il existe des plates-formes qui ne sont pas comme POSIX. :)
En ce qui concerne les verrous vont, je pense que Rafał Dowgird frappe la tête sur l'ongle quand il dit: « Il vous suffit d'être conscient que le générateur est comme tout autre objet qui contient des ressources. » Je ne pense pas que la déclaration de with
est vraiment pertinent en l'espèce, puisque cette fonction souffre des mêmes problèmes de blocage:
def coroutine():
lock.acquire()
yield 'spam'
yield 'eggs'
lock.release()
generator = coroutine()
generator.next()
lock.acquire() # whoops!
Autres conseils
Je ne pense pas qu'il y ait un vrai conflit. Il vous suffit d'être conscient que le générateur est comme tout autre objet qui contient des ressources, il est de la responsabilité du créateur pour vous assurer qu'il est correctement mis au point (et pour éviter les conflits / impasse avec les ressources détenues par l'objet). Le seul problème (mineur) que je vois ici est que les générateurs ne mettent pas en œuvre le protocole de gestion de contexte (au moins de Python 2.5), de sorte que vous ne pouvez pas simplement:
with coroutine() as cr:
doSomething(cr)
mais plutôt avoir à:
cr = coroutine()
try:
doSomething(cr)
finally:
cr.close()
Le garbage collector ne le close()
de toute façon, mais il est une mauvaise pratique de compter sur cela pour libérer des ressources.
Parce que yield
peut exécuter du code arbitraire, je serais très prudent de tenir un verrou sur une déclaration de rendement. Vous pouvez obtenir un effet similaire dans beaucoup d'autres façons, cependant, y compris appeler une méthode ou des fonctions qui pourraient être ont été redéfinis autrement modifiées.
Les générateurs, cependant, sont toujours (presque toujours) « fermée », que ce soit avec un appel close()
explicite, ou tout simplement en étant le ramasse-miettes. La fermeture d'un générateur jette une exception GeneratorExit
à l'intérieur du générateur et va donc enfin clauses, avec le nettoyage des états, etc. Vous pouvez attraper l'exception, mais vous devez lancer ou quitter la fonction (à savoir lancer une exception StopIteration
), plutôt que de céder. Il est pratique probablement pauvre de compter sur le garbage collector pour fermer le générateur dans des cas comme vous avez écrit, parce que cela pourrait se produire plus tard que vous voudrez peut-être, et si quelqu'un appelle, alors votre nettoyage sys._exit () pourrait ne pas se produire du tout .
Ce serait ce que je pensais des choses à travailler. Oui, le bloc ne libérera pas ses ressources jusqu'à ce qu'il complète, donc dans ce sens que la ressource a échappé à son imbrication lexical. Cependant, mais cela ne diffère pas de faire un appel de fonction qui a essayé d'utiliser la même ressource dans un avec bloc - rien vous aide dans le cas où le bloc a pas encore terminée, pour quelle que soit raison. Ce n'est pas vraiment quoi que ce soit spécifique aux producteurs.
Une chose qui pourrait être utile se soucier si le comportement si le générateur est jamais a repris. Je me serais attendu le bloc with
d'agir comme un bloc de finally
et appeler la partie __exit__
à la résiliation, mais cela ne semble pas être le cas.
Pour un TLDR, regardez cette façon:
with Context():
yield 1
pass # explicitly do nothing *after* yield
# exit context after explicitly doing nothing
Le Context
se termine après pass
est fait (à savoir rien), pass
exécute après yield
est fait (à savoir l'exécution reprend). Ainsi, les extrémités de with
après contrôle reprend à yield
.
TLDR. Un contexte with
reste maintenu lorsque yield
rend le contrôle
Il y a en fait seulement deux règles qui sont pertinentes en l'espèce:
-
Quand est-ce
with
libérer ses ressources?Il le fait une fois et directement après son bloc est fait. Le premier signifie qu'il ne libère pas pendant un
yield
, car cela pourrait se produire plusieurs fois. Ce dernier signifie qu'il ne libère aprèsyield
est terminée. -
Quand est-
yield
complète?Pensez à
yield
comme un appel inverse: le contrôle est passé à un appelant, pas à une appelée un. De même, lorsque le contrôleyield
complète est repassée à elle, tout comme lorsqu'un appel retourne le contrôle.
Notez que les deux with
et yield
fonctionnent comme prévu ici! Le point d'un with lock
est de protéger une ressource et il reste protégé pendant une yield
. Vous pouvez toujours libérer explicitement cette protection:
def safe_generator():
while True:
with lock():
# keep lock for critical operation
result = protected_operation()
# release lock before releasing control
yield result