web-dev-qa-db-fra.com

Comment empêcher BrokenPipeError lors d'un flush en Python?

Question: Existe-t-il un moyen d'utiliser flush=True pour la fonction print() sans obtenir le BrokenPipeError ?

J'ai un script pipe.py:

for i in range(4000):
    print(i)

J'appelle ça comme ça depuis une ligne de commande Unix:

python3 pipe.py | head -n3000

Et ça revient:

0
1
2

Ainsi fait ce script:

import sys
for i in range(4000):
    print(i)
    sys.stdout.flush()

Cependant, lorsque j'exécute ce script et le transfère sur head -n3000:

for i in range(4000):
    print(i, flush=True)

Puis j'obtiens cette erreur:

    print(i, flush=True)
BrokenPipeError: [Errno 32] Broken pipe
Exception BrokenPipeError: BrokenPipeError(32, 'Broken pipe') in <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'> ignored

J'ai aussi essayé la solution ci-dessous, mais j'obtiens toujours la BrokenPipeError:

import sys
for i in range(4000):
    try:
        print(i, flush=True)
    except BrokenPipeError:
        sys.exit()
20
tommy.carstensen

La BrokenPipeError est normale en tant que dit fantôme parce que le processus de lecture (tête) se termine et ferme son extrémité du tuyau tandis que le processus d'écriture (python) tente toujours d'écrire.

Est-ce que est une condition anormale et que les scripts python reçoivent un BrokenPipeError - plus exactement, l'interpréteur Python reçoit un signal système SIGPIPE qu'il attrape et soulève le BrokenPipeError pour permettre au script de traiter l'erreur.

Et vous pouvez effectivement traiter l'erreur, car dans votre dernier exemple, vous ne voyez qu'un message disant que l'exception a été ignorée - ok, ce n'est pas vrai, mais semble liée à cette question ouverte dans Python: les développeurs Python pensent important avertir l'utilisateur de la condition anormale.

Ce qui se passe réellement, c’est que, autant que je sache, l’interprète python le signale toujours sur stderr, même si vous attrapez l’exception. Mais il vous suffit de fermer stderr avant de quitter pour vous débarrasser du message.

J'ai légèrement changé votre script en:

  • attraper l'erreur comme vous l'avez fait dans votre dernier exemple
  • attrapez soit IOError (que je reçois dans Python34 sous Windows64), soit BrokenPipeError (dans Python 33 sous FreeBSD 9.0) - et affiche un message à cet effet
  • affiche un message personnalisé Terminé sur stderr (stdout est fermé en raison d'un tuyau cassé)
  • close stderr avant de quitter pour se débarrasser du message

Voici le script que j'ai utilisé: 

import sys

try:
    for i in range(4000):
            print(i, flush=True)
except (BrokenPipeError, IOError):
    print ('BrokenPipeError caught', file = sys.stderr)

print ('Done', file=sys.stderr)
sys.stderr.close()

et voici le résultat de python3.3 pipe.py | head -10:

0
1
2
3
4
5
6
7
8
9
BrokenPipeError caught
Done

Si vous ne voulez pas que les messages superflus, utilisez simplement:

import sys

try:
    for i in range(4000):
            print(i, flush=True)
except (BrokenPipeError, IOError):
    pass

sys.stderr.close()
22
Serge Ballesta

Selon la documentation Python, ceci est levé quand:

essayer d'écrire sur un tuyau alors que l'autre extrémité est fermée

Cela est dû au fait que l'utilitaire principal lit stdout, puis le ferme promptement .

Comme vous pouvez le constater, il est possible de contourner le problème en ajoutant simplement une sys.stdout.flush() après chaque print(). Notez que cela ne fonctionne parfois pas dans Python 3.

Vous pouvez également le diriger vers awk comme ceci pour obtenir le même résultat que head -3:

python3 0to3.py | awk 'NR >= 4 {exit} 1'

J'espère que cela vous a aidé, bonne chance!

4
phantom

Comme vous pouvez le voir dans le résultat que vous avez publié, la dernière exception est déclenchée dans la phase de destruction: c'est pourquoi vous avez ignored à la fin. 

Exception BrokenPipeError: BrokenPipeError(32, 'Broken pipe') in <_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'> ignored

Voici un exemple simple pour comprendre ce qui se passe dans ce contexte:

>> class A():
...     def __del__(self):
...         raise Exception("It will be ignored!!!")
... 
>>> a = A()
>>> del a
Exception Exception: Exception('It will be ignored!!!',) in <bound method A.__del__ of <__builtin__.A instance at 0x7ff1d5c06d88>> ignored
>>> a = A()
>>> import sys
>>> sys.stderr.close()
>>> del a

Chaque exception déclenchée lors de la destruction de l'objet provoquera une sortie d'erreur standard expliquant que l'exception s'est produite et qu'elle a été ignorée (car python vous informera que quelque chose ne peut pas être correctement géré en phase de destruction). Quoi qu'il en soit, ce type d'exceptions ne peut pas être mis en cache, vous pouvez donc simplement supprimer les appels qui peuvent le générer ou fermer stderr.

Revenez à la question. Cette exception n’est pas un réel problème (comme elle est ignorée) mais si vous ne voulez pas l’imprimer, vous devez remplacer la fonction qui peut être appelée lorsque l’objet sera détruit ou fermer stderr comme @SergeBallesta l’a bien suggéré si vous pouvez shutdown write et flush et qu'aucune exception ne sera déclenchée dans le contexte de destruction

C'est un exemple de la façon dont vous pouvez le faire:

import sys
def _void_f(*args,**kwargs):
    pass

for i in range(4000):
    try:
        print(i,flush=True)
    except (BrokenPipeError, IOError):
        sys.stdout.write = _void_f
        sys.stdout.flush = _void_f
        sys.exit()
1
Michele d'Amico

Ignorer SIGPPIE temporairement

Je ne suis pas sûr que ce soit une mauvaise idée, mais ça marche:

#!/usr/bin/env python3

import signal
import sys

sigpipe_old = signal.getsignal(signal.SIGPIPE)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
for i in range(4000):
    print(i, flush=True)
signal.signal(signal.SIGPIPE, sigpipe_old)