Como lidar bem com `with open (…)` e `sys.stdout`?

91

Freqüentemente, preciso enviar dados para o arquivo ou, se o arquivo não for especificado, para o stdout. Eu uso o seguinte snippet:

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

Gostaria de reescrevê-lo e lidar com os dois alvos de maneira uniforme.

No caso ideal, seria:

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

mas isso não funcionará bem porque sys.stdout é fechado ao sair do withbloco e eu não quero isso. Eu nem quero

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

porque eu precisaria me lembrar de restaurar o stdout original.

Relacionado:

Editar

Eu sei que posso encapsular target, definir funções separadas ou usar o gerenciador de contexto . Procuro uma solução simples, elegante e idiomática que não exija mais do que 5 linhas

Jakub M.
fonte
Pena que você não adicionou a edição antes;) De qualquer forma ... alternativamente, você pode simplesmente não se preocupar em
limpar

Respostas:

92

Apenas pensando fora da caixa aqui, que tal um open()método personalizado ?

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()

Use-o assim:

# 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
fonte
27

Fique com seu código atual. É simples e você pode dizer exatamente o que está fazendo apenas olhando para ele.

Outra maneira seria com um embutido if:

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

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

Mas isso não é muito mais curto do que o que você tem e parece provavelmente pior.

Você também pode tornar sys.stdoutimpossível fechar, mas isso não parece muito pitônico:

sys.stdout.close = lambda: None

with (open(target, 'w') if target else sys.stdout) as handle:
    handle.write(content)
Liquidificador
fonte
1
Você pode manter a unclosability pelo tempo que precisar criando um gerenciador de contexto para ele também: with unclosable(sys.stdout): ...configurando sys.stdout.close = lambda: Nonedentro deste gerenciador de contexto e reconfigurando-o para o valor antigo depois. Mas isso parece um pouco exagerado ...
glglgl
3
Estou dividido entre votar a favor de "deixe isso, você pode dizer exatamente o que está fazendo" e votar contra a sugestão horrenda não revelável!
GreenAsJade
8

Por que LBYL quando você pode EAFP?

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

Por que reescrevê-lo para usar o bloco with/ asuniformemente quando você precisa fazê-lo funcionar de uma maneira complicada? Você adicionará mais linhas e reduzirá o desempenho.

2rs2ts
fonte
3
As exceções não devem ser usadas para controlar o fluxo "normal" da rotina. Atuação? borbulhar um erro será mais rápido do que if / else?
Jakub M.
2
Depende da probabilidade de você usar um ou outro.
2rs2ts
31
@JakubM. As exceções podem, devem ser e são usadas assim no Python.
Gareth Latty
13
Considerando que o forloop do Python termina capturando um erro StopIteration lançado pelo iterador que está percorrendo o loop, eu diria que o uso de exceções para controle de fluxo é totalmente Pythônico.
Kirk Strauser
1
Assumindo que targeté Nonequando sys.stdout se destina, você precisa pegar TypeErrorem vez de IOError.
Torek
5

Outra solução possível: não tente evitar o método de saída do gerenciador de contexto, apenas duplique o stdout.

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

Uma melhoria da resposta 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

Isso permite E / S binária e passa eventuais argumentos estranhos para opense filenamerealmente for um nome de arquivo.

Evpok
fonte
1

Eu também escolheria uma função de wrapper simples, que pode ser muito simples se você puder ignorar o modo (e, consequentemente, stdin x stdout), por exemplo:

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
fonte
Esta solução não fecha explicitamente o arquivo no encerramento normal ou por erro da cláusula with, portanto, não é muito um gerenciador de contexto. Uma classe que implemente entrar e sair seria uma escolha melhor.
tdelaney
1
Recebo ValueError: I/O operation on closed filese tento gravar no arquivo fora do with open_or_stdout(..)bloco. o que estou perdendo? sys.stdout não deve ser fechado.
Tommi Komulainen
1

Ok, se estamos entrando em guerras de uma linha, aqui está:

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

Gosto do exemplo original de Jacob, contanto que o contexto seja escrito apenas em um lugar. Seria um problema se você acabasse reabrindo o arquivo para muitas gravações. Acho que só tomaria a decisão uma vez no início do script e deixaria o sistema fechar o arquivo ao sair:

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

Você pode incluir seu próprio manipulador de saída se achar que é mais organizado

import atexit

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

atexit(cleanup_output)
tdelaney
fonte
Não acho que seu one-liner fecha o objeto de arquivo. Estou errado?
2rs2ts
1
@ 2rs2ts - Sim ... condicionalmente. O refcount do objeto de arquivo vai para zero porque não há variáveis ​​apontando para ele, então ele está disponível para ter seu método __del__ chamado imediatamente (em cpython) ou posteriormente quando a coleta de lixo acontecer. Há avisos no documento para não confiar que isso sempre funcionará, mas eu uso isso o tempo todo em scripts mais curtos. Algo grande que funciona por muito tempo e abre muitos arquivos ... bem, acho que usaria 'com' ou 'tentar / finalmente'.
tdelaney
TIL. Eu não sabia que os objetos de arquivo __del__fariam isso.
2rs2ts
@ 2rs2ts: O CPython usa um coletor de lixo de contagem de referência (com um GC "real" invocado conforme necessário) para que possa fechar o arquivo assim que você eliminar todas as referências ao identificador de fluxo. Jython e aparentemente IronPython têm apenas o GC "real", então eles não fecham o arquivo até um eventual GC.
Torek em
0

Se você realmente deve insistir em algo mais "elegante", ou seja, uma linha:

>>> 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.txtaparece e contém o texto foo.

2rs2ts
fonte
Deve ser movido para CodeGolf StackExchange: D
kaiser
0

Que tal abrir um novo fd para sys.stdout? Desta forma, você não terá problemas para fechá-lo:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)
user2602746
fonte
1
Infelizmente, a execução deste script python precisa de um sudo na minha instalação. / dev / stdout é propriedade do root.
Manur
Em muitas situações, reabrir um fd para stdout não é o esperado. Por exemplo, este código truncará o stdout, fazendo com que coisas do shell como ./script.py >> file sobrescrever o arquivo em vez de anexar a ele.
salicideblock
Isso não funcionará em janelas que não tenham / dev / stdout.
Bryan Oakley
0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

Melhora leve em alguns casos.

Eugene K
fonte
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)

Apenas duas linhas extras se você estiver usando Python 3.3 ou superior: uma linha para o extra importe uma linha para o stack.enter_context.

romanows
fonte