563 votes

Une lecture non bloquante sur un subprocess.PIPE en Python

Je utilise le module subprocess pour démarrer un sous-processus et me connecter à son flux de sortie (sortie standard). Je veux être capable d'effectuer des lectures non bloquantes sur sa sortie standard. Y a-t-il un moyen de rendre .readline non bloquant ou de vérifier s'il y a des données sur le flux avant d'invoquer .readline? Je voudrais que cela soit portable ou au moins fonctionne sous Windows et Linux.

Voici comment je le fais actuellement (cela bloque sur le .readline si aucune donnée n'est disponible) :

p = subprocess.Popen('monprogramme.exe', stdout = subprocess.PIPE)
output_str = p.stdout.readline()

16 votes

(Vient de Google?) tous les tuyaux se bloqueront lorsque le tampon de l'un des tuyaux sera rempli et non lu. par exemple, le blocage stdout lorsque stderr est rempli. Ne transmettez jamais un tuyau que vous n'avez pas l'intention de lire.

1 votes

@NasserAl-Wohaibi est-ce que cela signifie qu'il est préférable de toujours créer des fichiers alors ?

1 votes

Quelque chose qui me rend curieux de comprendre est pourquoi cela bloque en premier lieu...Je demande parce que j'ai vu le commentaire: Pour éviter les interblocages: faites attention à: ajouter \n à la sortie, vider la sortie, utiliser readline() plutôt que read()

449voto

J.F. Sebastian Points 102961

fcntl, select, asyncproc ne seront pas utiles dans ce cas.

Une façon fiable de lire un flux sans bloquer indépendamment du système d'exploitation est d'utiliser Queue.get_nowait():

import sys
from subprocess import PIPE, Popen
from threading  import Thread

try:
    from queue import Queue, Empty
except ImportError:
    from Queue import Queue, Empty  # python 2.x

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line)
    out.close()

p = Popen(['myprogram.exe'], stdout=PIPE, bufsize=1, close_fds=ON_POSIX)
q = Queue()
t = Thread(target=enqueue_output, args=(p.stdout, q))
t.daemon = True # thread dies with the program
t.start()

# ... do other things here

# read line without blocking
try:  line = q.get_nowait() # or q.get(timeout=.1)
except Empty:
    print('no output yet')
else: # got line
    # ... do something with line

9 votes

Oui cela fonctionne pour moi, j'ai enlevé beaucoup cependant. Il inclut de bonnes pratiques mais pas toujours nécessaires. La compatibilité de Python 3.x 2.X et close_fds peuvent être omises, cela fonctionnera toujours. Mais soyez juste conscient de ce que chaque chose fait et ne copiez pas aveuglément, même si cela fonctionne juste ! (En fait, la solution la plus simple est d'utiliser un thread et de faire un readline comme l'a fait Seb, les files d'attente sont juste un moyen facile d'obtenir les données, il y en a d'autres, les threads sont la réponse !)

3 votes

À l'intérieur du fil, l'appel à out.readline bloque le fil et le fil principal, et je dois attendre que readline retourne avant que tout le reste ne continue. Y a-t-il un moyen simple de contourner cela ? (Je lis plusieurs lignes de mon processus, qui est également un autre fichier .py qui fait des opérations sur la base de données et d'autres choses)

3 votes

@Justin : 'out.readline' ne bloque pas le thread principal, il est exécuté dans un autre thread.

83voto

Jesse Points 462

J'ai souvent eu un problème similaire; les programmes Python que j'écris ont fréquemment besoin de pouvoir exécuter une fonctionnalité principale tout en acceptant simultanément l'entrée de l'utilisateur depuis la ligne de commande (stdin). Mettre simplement la fonctionnalité de gestion de l'entrée utilisateur dans un autre thread ne résout pas le problème car readline() bloque et n'a pas de délai d'attente. Si la fonctionnalité principale est terminée et qu'il n'y a plus besoin d'attendre une autre entrée de l'utilisateur, je veux généralement que mon programme se termine, mais il ne le peut pas car readline() bloque toujours dans l'autre thread en attendant une ligne. Une solution que j'ai trouvée à ce problème est de rendre stdin un fichier non bloquant en utilisant le module fcntl :

import fcntl
import os
import sys

# rendre stdin un fichier non bloquant
fd = sys.stdin.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

# thread de gestion de l'entrée utilisateur
while mainThreadIsRunning:
      try: input = sys.stdin.readline()
      except: continue
      handleInput(input)

À mon avis, c'est un peu plus propre que d'utiliser les modules select ou signal pour résoudre ce problème mais cela ne fonctionne que sur UNIX...

1 votes

Selon les documents, fcntl() peut recevoir soit un descripteur de fichier, soit un objet qui a une méthode .fileno().

12 votes

La réponse de Jesse n'est pas correcte. Selon Guido, readline ne fonctionne pas correctement en mode non-bloquant, et cela ne fonctionnera pas avant Python 3000. bugs.python.org/issue1175#msg56041 Si vous voulez utiliser fcntl pour définir le fichier en mode non-bloquant, vous devez utiliser la fonction os.read() de plus bas niveau et séparer les lignes vous-même. Mélanger fcntl avec des appels de haut niveau qui effectuent la mise en mémoire tampon de lignes est demander des ennuis.

2 votes

L'utilisation de readline semble incorrecte en Python 2. Voir la réponse de anonnn stackoverflow.com/questions/375427/…

46voto

J.F. Sebastian Points 102961

Python 3.4 introduit une nouvelle API provisoire pour l'IO asynchrone -- module asyncio.

L'approche est similaire à la réponse basée sur twisted de @Bryan Ward -- définir un protocole et ses méthodes sont appelées dès que les données sont prêtes :

#!/usr/bin/env python3
import asyncio
import os

class SubprocessProtocol(asyncio.SubprocessProtocol):
    def pipe_data_received(self, fd, data):
        if fd == 1: # données stdout reçues (bytes)
            print(data)

    def connection_lost(self, exc):
        loop.stop() # arrêter loop.run_forever()

if os.name == 'nt':
    loop = asyncio.ProactorEventLoop() # pour les pipes des sous-processus sur Windows
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(loop.subprocess_exec(SubprocessProtocol, 
        "myprogram.exe", "arg1", "arg2"))
    loop.run_forever()
finally:
    loop.close()

Voir "Sous-processus" dans la documentation.

Il existe une interface de haut niveau asyncio.create_subprocess_exec() qui renvoie des objets Process qui permettent de lire une ligne de manière asynchrone en utilisant la coroutine StreamReader.readline() (avec la syntaxe async/await Python 3.5+):

#!/usr/bin/env python3.5
import asyncio
import locale
import sys
from asyncio.subprocess import PIPE
from contextlib import closing

async def readline_and_kill(*args):
    # démarrer le processus enfant
    process = await asyncio.create_subprocess_exec(*args, stdout=PIPE)

    # lire la ligne (séquence d'octets se terminant par b'\n') de manière asynchrone
    async for line in process.stdout:
        print("ligne reçue :", line.decode(locale.getpreferredencoding(False)))
        break
    process.kill()
    return await process.wait() # attendre que le processus enfant se termine

if sys.platform == "win32":
    loop = asyncio.ProactorEventLoop()
    asyncio.set_event_loop(loop)
else:
    loop = asyncio.get_event_loop()

with closing(loop):
    sys.exit(loop.run_until_complete(readline_and_kill(
        "myprogram.exe", "arg1", "arg2")))

readline_and_kill() réalise les tâches suivantes :

  • démarrer le sous-processus, rediriger son stdout vers un pipe
  • lire une ligne du stdout du sous-processus de manière asynchrone
  • tuer le sous-processus
  • attendre sa sortie

Chaque étape pourrait être limitée par des secondes de timeout si nécessaire.

0 votes

Quand j'essaie quelque chose comme cela en utilisant les corroutines Python 3.4, je n'obtiens de résultats qu'une fois que le script entier a été exécuté. J'aimerais voir une ligne de sortie imprimée dès que le sous-processus imprime une ligne. Voici ce que j'ai : pastebin.com/qPssFGep.

1 votes

@flutefreak7: Les problèmes de mise en mémoire tampon ne sont pas liés à la question actuelle. Suivez le lien pour des solutions possibles.

0 votes

Merci! J'ai résolu le problème pour mon script en utilisant simplement print(text, flush=True) afin que le texte imprimé soit immédiatement disponible pour le watcher appelant readline. Lorsque je l'ai testé avec l'exécutable basé sur Fortran que je veux réellement envelopper/surveiller, il n'a pas mis en mémoire tampon sa sortie, donc il se comporte comme prévu.

19voto

Noah Points 3398

Essayez le module asyncproc. Par exemple :

import os
from asyncproc import Process
myProc = Process("myprogram.app")

while True:
    # vérifier si le processus est terminé
    poll = myProc.wait(os.WNOHANG)
    if poll != None:
        break
    # imprimer toute nouvelle sortie
    out = myProc.read()
    if out != "":
        print out

Le module s'occupe de tout le threading, comme suggéré par S.Lott.

1 votes

Absolument brillant. Beaucoup plus facile que le module de sous-processus brut. Fonctionne parfaitement pour moi sur Ubuntu.

14 votes

Asyncproc ne fonctionne pas sur Windows, et Windows ne prend pas en charge os.WNOHANG :-(

29 votes

Asyncproc est sous licence GPL, ce qui limite encore plus son utilisation :-(

17voto

Bryan Ward Points 1784

Vous pouvez faire cela très facilement dans Twisted. En fonction de votre code existant, cela peut ne pas être si simple à utiliser, mais si vous construisez une application Twisted, des choses comme celles-ci deviennent presque triviales. Vous créez une classe ProcessProtocol, et remplacez la méthode outReceived(). Twisted (en fonction du réacteur utilisé) n'est généralement qu'une grande boucle select() avec des rappels installés pour gérer les données provenant de différents descripteurs de fichiers (souvent des sockets réseau). Ainsi, la méthode outReceived() consiste simplement à installer un rappel pour gérer les données provenant de STDOUT. Un exemple simple démontrant ce comportement est le suivant :

from twisted.internet import protocol, reactor

class MyProcessProtocol(protocol.ProcessProtocol):

    def outReceived(self, data):
        print data

proc = MyProcessProtocol()
reactor.spawnProcess(proc, './myprogram', ['./myprogram', 'arg1', 'arg2', 'arg3'])
reactor.run()

La documentation Twisted contient des informations intéressantes à ce sujet.

Si vous construisez toute votre application autour de Twisted, cela rend la communication asynchrone avec d'autres processus, locaux ou distants, vraiment élégante comme ceci. En revanche, si votre programme n'est pas construit sur Twisted, cela ne sera pas vraiment utile. Espérons que cela puisse être utile à d'autres lecteurs, même si ce n'est pas applicable à votre application particulière.

0 votes

Pas bon. select ne devrait pas fonctionner sur les fenêtres avec des descripteurs de fichiers, selon docs

2 votes

@naxa Je ne pense pas que la select() à laquelle il fait référence soit la même que celle à laquelle vous faites référence. Je suppose cela car Twisted fonctionne sur windows...

0 votes

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