51 votes

Est-il sûr de céder des ressources à l'intérieur d'un bloc "with" en Python (et pourquoi) ?

La combinaison des coroutines et de l'acquisition de ressources semble pouvoir avoir des conséquences inattendues (ou peu intuitives).

La question fondamentale est de savoir si quelque chose comme cela fonctionne ou non :

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

Ce qu'il fait. (Vous pouvez le tester !)

L'inquiétude la plus profonde est que with est censé être une alternative à finally où vous vous assurez qu'une ressource est libérée à la fin du bloc. Les coroutines peuvent suspendre et reprendre l'exécution de sur le site with bloc, donc comment le conflit est-il résolu ?

Par exemple, si vous ouvrez un fichier en lecture/écriture à l'intérieur et à l'extérieur d'une coroutine alors que la coroutine n'est pas encore revenue :

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

Dans l'exemple précédent, je voulais parler de la contention des poignées de fichiers verrouillées en écriture, mais comme la plupart des systèmes d'exploitation allouent les poignées de fichiers par processus, il n'y aura pas de contention. (Bravo à @Miles qui m'a fait remarquer que l'exemple n'avait pas beaucoup de sens). Voici mon exemple révisé, qui montre une vraie 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()

0 votes

@Miles a fait remarquer que l'exemple est quelque peu malformé. J'ai opté pour un gestionnaire de fichiers verrouillé en écriture, mais comme le système d'exploitation alloue probablement des gestionnaires de fichiers par processus, cela devrait fonctionner correctement.

0 votes

TL;DR yield y return sont sûres (dans la mesure où elles finiront par libérer des ressources). Cependant, return pourrait ne pas se comporter gentiment. Pensez à with os.scandir() as entries: return entries . Cela ne fonctionne tout simplement pas ! Utilisez with os.scandir() as entries: yield from entries ou simplement return os.scandir() à la place. La deuxième solution vous obligera à appeler .close() sur le ScandirIterator instance si elle n'est pas épuisée. Ce n'est qu'un exemple, mais il illustre ce qui peut se passer lorsque l'on retourne des ressources temporaires d'une instance de with déclaration.

24voto

Miles Points 12977

Je ne comprends pas vraiment le conflit que vous voulez soulever, ni le problème de l'exemple : il est tout à fait possible d'avoir deux handles coexistants et indépendants pour un même fichier.

Une chose que je ne savais pas et que j'ai apprise en réponse à votre question, c'est qu'il existe une nouvelle méthode close() sur les générateurs :

close() soulève un nouveau GeneratorExit exception dans le générateur pour mettre fin à l'itération. A la réception de cette exception, le code du générateur doit soit lever GeneratorExit o StopIteration .

close() est appelé lorsqu'un générateur est collecté, ce qui signifie que le code du générateur a une dernière chance de s'exécuter avant que le générateur ne soit détruit. Cette dernière chance signifie que try...finally dans les générateurs peuvent maintenant être garantis de fonctionner. finally clause aura désormais toujours une chance de se présenter. Cela peut sembler être un détail mineur du langage, mais l'utilisation des générateurs et des clauses try...finally est en fait nécessaire afin de mettre en œuvre la with déclaration décrite par le PEP 343.

http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features

Donc cela gère la situation où un with est utilisée dans un générateur, mais elle cède au milieu sans jamais revenir - l'instruction du gestionnaire de contexte __exit__ sera appelée lorsque le générateur sera collecté.


Modifier :

En ce qui concerne la question de la gestion des fichiers : J'oublie parfois qu'il existe des plateformes qui ne sont pas de type POSIX :)

En ce qui concerne les verrous, je pense que Rafał Dowgird fait mouche quand il dit : "Il faut juste être conscient que le générateur est comme tout autre objet qui détient des ressources." Je ne pense pas que le with est vraiment pertinente ici, 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!

0 votes

Mise à jour selon votre confusion avec l'exemple. En fait, cela a aidé à formuler une réponse : il n'y a pas de problème à avoir plusieurs gestionnaires de contexte de gestion de fichiers en lecture/écriture (puisque la plupart des systèmes d'exploitation les allouent par processus), mais ce n'est pas le cas pour la plupart des autres ressources (c'est-à-dire les verrous non récursifs).

0 votes

+1 : Quel conflit ? Lorsque le rendement s'arrête, l'instruction with ferme la ressource. Quel conflit ?

0 votes

Pour clarifier, "quand le rendement s'arrête" => quand le générateur sort du bloc avec. Cela peut être après de nombreux rendements.

9voto

Rafał Dowgird Points 16600

Je ne pense pas qu'il y ait un réel conflit. Il faut juste être conscient que le générateur est comme tout autre objet qui détient des ressources, donc c'est la responsabilité du créateur de s'assurer qu'il est correctement finalisé (et d'éviter les conflits/déblocages avec les ressources détenues par l'objet). Le seul problème (mineur) que je vois ici est que les générateurs n'implémentent pas le protocole de gestion de contexte (au moins à partir de Python 2.5), donc vous ne pouvez pas juste :

with coroutine() as cr:
  doSomething(cr)

mais au lieu de cela, ils doivent le faire :

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

Le ramasseur de déchets fait le close() de toute façon, mais c'est une mauvaise pratique de compter sur cela pour libérer des ressources.

2 votes

with contextlib.closing(coroutine()) as cr:

1voto

MisterMiyagi Points 2734

Pour un TLDR, regardez de cette façon :

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

Le site Context se termine après pass est fait (c'est-à-dire rien), pass s'exécute après yield est terminée (c'est-à-dire que l'exécution reprend). Ainsi, le with termine après Le contrôle est repris à yield .

TLDR : A with le contexte reste maintenu lorsque yield contrôle des rejets.


Il n'y a en fait que deux règles qui sont pertinentes ici :

  1. Quand est-ce que with libérer sa ressource ?

    Il le fait une fois et directement après son bloc est terminé. Dans le premier cas, il ne libère pas pendant a yield car cela pourrait se produire plusieurs fois. Le dernier signifie qu'il libère après yield est terminée.

  2. Quand est-ce que yield complet ?

    Pensez à yield comme un appel inversé : le contrôle est transmis à l'appelant et non à l'appelé. De même, yield se termine lorsque le contrôle lui est renvoyé, tout comme lorsqu'un appel renvoie le contrôle.

Notez que les deux with y yield fonctionnent comme prévu ici ! L'intérêt d'un with lock est de protéger une ressource et qu'elle reste protégée pendant une yield . Vous pouvez toujours renoncer 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

1voto

Doug Points 4923

Parce que yield peut exécuter du code arbitraire, je me méfierais beaucoup d'un verrou sur une instruction yield. Vous pouvez obtenir un effet similaire de bien d'autres façons, y compris en appelant une méthode ou des fonctions qui pourraient avoir été surchargées ou modifiées.

Les générateurs, cependant, sont toujours (presque toujours) "fermés", soit avec une clause explicite close() ou simplement en étant collecté par les ordures. La fermeture d'un générateur lance un GeneratorExit dans le générateur et, par conséquent, exécute les clauses finally, avec nettoyage des instructions, etc. Vous pouvez rattraper l'exception, mais vous devez lancer ou quitter la fonction (c'est-à-dire lancer une commande StopIteration exception), plutôt que le rendement. C'est probablement une mauvaise pratique de compter sur le ramasseur d'ordures pour fermer le générateur dans des cas comme celui que vous avez écrit, car cela pourrait arriver plus tard que vous ne le souhaitez, et si quelqu'un appelle sys._exit(), alors votre nettoyage pourrait ne pas avoir lieu du tout.

0voto

Brian Points 48423

C'est ainsi que je m'attendais à ce que les choses fonctionnent. Oui, le bloc ne libère pas ses ressources tant qu'il n'est pas terminé, donc dans ce sens la ressource a échappé à son imbrication lexicale. Cependant, cela n'est pas différent d'un appel de fonction qui tenterait d'utiliser la même ressource dans un bloc with - rien ne vous aide dans le cas où le bloc a été libéré. pas encore terminé, pour quoi que ce soit raison. Ce n'est pas vraiment quelque chose de spécifique aux générateurs.

Une chose dont il faut se préoccuper est le comportement du générateur s'il est en panne. jamais a repris. Je me serais attendu à ce que le with pour agir comme un finally et appeler le __exit__ partie sur la résiliation, mais cela ne semble pas être le cas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X