Comment gérer à la fois `with open (…)` et `sys.stdout` bien?

92

Souvent, j'ai besoin de sortir des données dans un fichier ou, si le fichier n'est pas spécifié, dans stdout. J'utilise l'extrait suivant:

if target:
    with open(target, 'w') as h:
        h.write(content)
else:
    sys.stdout.write(content)

Je voudrais le réécrire et gérer les deux cibles de manière uniforme.

Dans le cas idéal, ce serait:

with open(target, 'w') as h:
    h.write(content)

mais cela ne fonctionnera pas bien car sys.stdout est fermé en quittant le withbloc et je ne le veux pas. Je ne veux pas non plus

stdout = open(target, 'w')
...

parce que j'aurais besoin de me rappeler de restaurer la sortie standard d'origine.

En relation:

Éditer

Je sais que je peux envelopper target, définir une fonction distincte ou utiliser le gestionnaire de contexte . Je recherche une solution de montage simple, élégante et idiomatique qui ne nécessiterait pas plus de 5 lignes

Jakub M.
la source
Dommage que vous n'ayez pas ajouté la modification plus tôt;) Quoi qu'il en soit ... sinon vous ne pouvez tout simplement pas prendre la peine de nettoyer votre fichier ouvert: P
Wolph

Réponses:

92

En sortant des sentiers battus ici, que diriez-vous d'une open()méthode personnalisée ?

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename=None):
    if filename and filename != '-':
        fh = open(filename, 'w')
    else:
        fh = sys.stdout

    try:
        yield fh
    finally:
        if fh is not sys.stdout:
            fh.close()

Utilisez-le comme ceci:

# For Python 2 you need this line
from __future__ import print_function

# writes to some_file
with smart_open('some_file') as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open() as fh:
    print('some output', file=fh)

# writes to stdout
with smart_open('-') as fh:
    print('some output', file=fh)
Wolph
la source
28

Restez fidèle à votre code actuel. C'est simple et vous pouvez dire exactement ce qu'il fait simplement en le regardant.

Une autre façon serait avec un inline if:

handle = open(target, 'w') if target else sys.stdout
handle.write(content)

if handle is not sys.stdout:
    handle.close()

Mais ce n'est pas beaucoup plus court que ce que vous avez et cela semble sans doute pire.

Vous pouvez également rendre sys.stdoutinaccessible, mais cela ne semble pas trop pythonique:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
Mixeur
la source
2
Vous pouvez conserver la non-fermeture aussi longtemps que vous en avez besoin en créant également un gestionnaire de contexte: with unclosable(sys.stdout): ...en le définissant sys.stdout.close = lambda: Nonedans ce gestionnaire de contexte et en le réinitialisant à l'ancienne valeur par la suite. Mais cela semble un peu trop tiré par les cheveux ...
glglgl
3
Je suis déchiré entre voter pour "laissez-le, vous pouvez dire exactement ce qu'il fait" et voter contre cette horrible suggestion à ne pas divulguer!
GreenAsJade
@GreenAsJade Je ne pense pas qu'il suggérait de rendre sys.stdoutinvisible, juste en notant que cela pouvait être fait. Il vaut mieux montrer de mauvaises idées et expliquer pourquoi elles sont mauvaises que de ne pas les mentionner et d'espérer qu'elles ne seront pas tombées par hasard par d'autres.
cjs
8

Pourquoi LBYL quand vous pouvez EAFP?

try:
    with open(target, 'w') as h:
        h.write(content)
except TypeError:
    sys.stdout.write(content)

Pourquoi le réécrire pour utiliser le bloc with/ asuniformément lorsque vous devez le faire fonctionner de manière alambiquée? Vous allez ajouter plus de lignes et réduire les performances.

2rs2ts
la source
3
Les exceptions ne doivent pas être utilisées pour contrôler le déroulement "normal" de la routine. Performance? est-ce qu'une erreur bouillonnante sera plus rapide que si / sinon?
Jakub M.
2
Cela dépend de la probabilité que vous utilisiez l'un ou l'autre.
2rs2ts
31
@JakubM. Les exceptions peuvent, devraient être et sont utilisées comme ceci en Python.
Gareth Latty
13
Considérant que la forboucle de Python se termine en interceptant une erreur StopIteration lancée par l'itérateur sur lequel elle boucle, je dirais que l'utilisation d'exceptions pour le contrôle de flux est tout à fait pythonique.
Kirk Strauser du
1
En supposant que targetc'est à ce Nonemoment-là que sys.stdout est prévu, vous devez attraper TypeErrorplutôt que IOError.
torek
5

Autre solution possible: n'essayez pas d'éviter la méthode de sortie du gestionnaire de contexte, dupliquez simplement stdout.

with (os.fdopen(os.dup(sys.stdout.fileno()), 'w')
      if target == '-'
      else open(target, 'w')) as f:
      f.write("Foo")
Olivier Aubert
la source
5

Une amélioration de la réponse de Wolph

import sys
import contextlib

@contextlib.contextmanager
def smart_open(filename: str, mode: str = 'r', *args, **kwargs):
    '''Open files and i/o streams transparently.'''
    if filename == '-':
        if 'r' in mode:
            stream = sys.stdin
        else:
            stream = sys.stdout
        if 'b' in mode:
            fh = stream.buffer  # type: IO
        else:
            fh = stream
        close = False
    else:
        fh = open(filename, mode, *args, **kwargs)
        close = True

    try:
        yield fh
    finally:
        if close:
            try:
                fh.close()
            except AttributeError:
                pass

Cela permet les E / S binaires et de passer d'éventuels arguments superflus à openif filenameest en effet un nom de fichier.

Evpok
la source
1

J'opterais également pour une fonction wrapper simple, qui peut être assez simple si vous pouvez ignorer le mode (et par conséquent stdin vs stdout), par exemple:

from contextlib import contextmanager
import sys

@contextmanager
def open_or_stdout(filename):
    if filename != '-':
        with open(filename, 'w') as f:
            yield f
    else:
        yield sys.stdout
Tommi Komulainen
la source
Cette solution ne ferme pas explicitement le fichier à la fin normale ou par erreur de la clause with, donc ce n'est pas vraiment un gestionnaire de contexte. Une classe qui implémente l' entrée et la sortie serait un meilleur choix.
tdelaney
1
J'obtiens ValueError: I/O operation on closed filesi j'essaye d'écrire dans le fichier en dehors du with open_or_stdout(..)bloc. Qu'est-ce que je rate? sys.stdout n'est pas censé être fermé.
Tommi Komulainen
1

D'accord, si nous nous engageons dans des guerres à une ligne, voici:

(target and open(target, 'w') or sys.stdout).write(content)

J'aime l'exemple original de Jacob tant que le contexte n'est écrit qu'à un seul endroit. Ce serait un problème si vous finissez par rouvrir le fichier pour de nombreuses écritures. Je pense que je prendrais simplement la décision une fois en haut du script et laisserais le système fermer le fichier à la sortie:

output = target and open(target, 'w') or sys.stdout
...
output.write('thing one\n')
...
output.write('thing two\n')

Vous pouvez inclure votre propre gestionnaire de sortie si vous pensez que c'est plus ordonné

import atexit

def cleanup_output():
    global output
    if output is not sys.stdout:
        output.close()

atexit(cleanup_output)
tdelaney
la source
Je ne pense pas que votre one-liner ferme l'objet fichier. Ai-je tort?
2rs2ts
1
@ 2rs2ts - Il le fait ... sous condition. Le refcount de l'objet fichier passe à zéro car il n'y a pas de variables pointant vers lui, il est donc disponible pour que sa méthode __del__ soit appelée immédiatement (en cpython) ou plus tard lorsque le garbage collection se produit. Il y a des avertissements dans la documentation pour ne pas croire que cela fonctionnera toujours, mais je l'utilise tout le temps dans des scripts plus courts. Quelque chose de gros qui fonctionne longtemps et ouvre beaucoup de fichiers ... eh bien, je suppose que j'utiliserais «avec» ou «essayer / enfin».
tdelaney du
TIL. Je ne savais pas que les objets fichier __del__feraient cela.
2rs2ts le
@ 2rs2ts: CPython utilise un garbage collector comptant les références (avec un "vrai" GC en dessous appelé si nécessaire) afin de pouvoir fermer le fichier dès que vous déposez toutes les références au descripteur de flux. Jython et apparemment IronPython n'ont que le "vrai" GC donc ils ne ferment pas le fichier avant un éventuel GC.
torek
0

Si vous devez vraiment insister sur quelque chose de plus "élégant", c'est-à-dire un one-liner:

>>> import sys
>>> target = "foo.txt"
>>> content = "foo"
>>> (lambda target, content: (lambda target, content: filter(lambda h: not h.write(content), (target,))[0].close())(open(target, 'w'), content) if target else sys.stdout.write(content))(target, content)

foo.txtapparaît et contient le texte foo.

2rs2ts
la source
Cela devrait être déplacé vers CodeGolf StackExchange: D
kaiser
0

Que diriez-vous d'ouvrir un nouveau fd pour sys.stdout? De cette façon, vous n'aurez aucun problème à le fermer:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
la source
1
Malheureusement, l'exécution de ce script python nécessite un sudo sur mon installation. / dev / stdout appartient à root.
Manur
Dans de nombreuses situations, la réouverture d'un fd vers stdout n'est pas ce à quoi on s'attend. Par exemple, ce code tronquera stdout, faisant ainsi des choses du shell comme ./script.py >> file écraser le fichier au lieu de lui ajouter.
salicideblock
Cela ne fonctionnera pas sur les fenêtres qui n'ont pas de / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Légère amélioration dans certains cas.

Eugène K
la source
0
import contextlib
import sys

with contextlib.ExitStack() as stack:
    h = stack.enter_context(open(target, 'w')) if target else sys.stdout
    h.write(content)

Juste deux lignes supplémentaires si vous utilisez Python 3.3 ou supérieur: une ligne pour le supplément importet une ligne pour le stack.enter_context.

Romanows
la source
0

Si c'est bien qui sys.stdoutest fermé après le withcorps, vous pouvez également utiliser des modèles comme celui-ci:

# Use stdout when target is "-"
with open(target, "w") if target != "-" else sys.stdout as f:
    f.write("hello world")

# Use stdout when target is falsy (None, empty string, ...)
with open(target, "w") if target else sys.stdout as f:
    f.write("hello world")

ou encore plus généralement:

with target if isinstance(target, io.IOBase) else open(target, "w") as f:
    f.write("hello world")
Stefaan
la source