如何很好地处理带有open(…)和sys.stdout的两者?


91

通常,我需要将数据输出到文件,或者,如果未指定文件,则将数据输出到stdout。我使用以下代码段:

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

我想重写它并统一处理两个目标。

在理想情况下,它将是:

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

但这不能很好地工作,因为在离开with块时sys.stdout是关闭的,我不希望这样。我都不想要

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

因为我需要记住恢复原始的stdout。

有关:

编辑

我知道我可以包装target,定义单独的功能或使用上下文管理器。我正在寻找一种简单,优雅,惯用的解决方案配件,它不需要多于5条线


太糟糕了,您没有更早添加编辑;)无论如何...或者,您根本无法打扰清理打开的文件:P
Wolph 2013年

Answers:


92

只是想在这里跳出框框,自定义open()方法怎么样?

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

像这样使用它:

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

27

坚持使用您当前的代码。这很简单,你可以告诉究竟是什么它是由它一眼正是这样做。

另一种方法是使用内联if

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

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

但这并不比您所拥有的要短得多,而且看起来可以说更糟。

您也可以将其sys.stdout设为无法关闭,但这似乎不太像Python风格:

sys.stdout.close = lambda: None

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

1
你可以保持unclosability,只要你通过使上下文管理它,以及需要:with unclosable(sys.stdout): ...通过设置sys.stdout.close = lambda: None这个上下文管理器里面,之后重置为旧值。但这似乎有点牵强。。。
glglgl

3
我在投票赞成“离开它,您可以确切地说出它在做什么”与投票反对可怕的,无法公开的建议之间感到痛苦!
GreenAsJade

8

当您可以使用EAFP时为什么选择LBYL?

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

为什么要重写它以使用 withas当必须使它以复杂的方式工作时,统一 / 块?您将添加更多行并降低性能。


3
异常应用于控制例程的“正常”流程。性能?如果/否则,冒泡错误会更快吗?
Jakub M.

2
取决于您将使用另一个的可能性。
2013年

31
@JakubM。异常可以,应该并且应该在Python中像这样使用。
Gareth Latty

13
考虑到Python的for循环是通过捕获循环遍历的迭代器抛出的StopIteration错误而退出的,我想说使用异常进行流控制完全是Pythonic的。
Kirk Strauser

1
假设targetNone当sys.stdout的意,你需要捕捉TypeError,而不是IOError
torek

5

另一个可能的解决方案:不要尝试避免使用上下文管理器退出方法,而要重复使用stdout。

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

5

沃尔夫答案的改进

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

这允许二进制IO并将最终无关的参数传递给openif filename确实是文件名。


1

我还将使用一个简单的包装函数,如果您可以忽略该模式(因此可以忽略stdin vs. stdout),则它可以非常简单,例如:

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

此解决方案不会在with子句的正常或错误终止时显式关闭文件,因此它不是上下文管理器的主要组成部分。实现enterexit的类将是一个更好的选择。
tdelaney

1
ValueError: I/O operation on closed file如果尝试在with open_or_stdout(..)块外写入文件,我会明白。我想念什么?sys.stdout并非要关闭。
Tommi Komulainen

1

好吧,如果我们要进行单线战争,那就是:

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

只要上下文仅在一个地方编写,我就喜欢Jacob的原始示例。如果最终要多次打开该文件,那将是一个问题。我想我只在脚本顶部做出一次决定,然后让系统在退出时关闭文件:

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

如果您认为它更整洁,则可以包括自己的退出处理程序

import atexit

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

atexit(cleanup_output)

我不认为您的单行关闭文件对象。我错了吗?
2013年

1
@ 2rs2ts-有条件... 文件对象的refcount变为零,因为没有变量指向它,因此可以在垃圾回收发生时立即(在cpython中)或稍后调用__del__方法。文档中有警告,不要相信这将一直有效,但是我一直在较短的脚本中使用它。大型文件可以长时间运行并打开许多文件……我想我会使用“ with”或“ try / finally”。
tdelaney

瓷砖。我不知道文件对象__del__会做到这一点。
2rs2ts 2013年

@ 2rs2ts:CPython使用引用计数垃圾收集器(根据需要在其下面调用“真实” GC),因此一旦您将所有引用丢弃到流句柄,它就可以关闭文件。Jython和IronPython显然只有“真正的” GC,因此直到最终GC才关闭文件。
torek

0

如果您真的必须坚持一些更“优雅”的东西,即单线:

>>> 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.txt出现并包含文本foo


应该将其移至CodeGolf StackExchange:D
kaiser

0

如何为sys.stdout打开新的fd?这样一来,关闭它就不会有任何问题:

if not target:
    target = "/dev/stdout"
with open(target, 'w') as f:
    f.write(content)

1
可悲的是,运行此python脚本时需要在我的安装上安装sudo。/ dev / stdout由root拥有。
Manur 2015年

在许多情况下,重新打开fd到stdout并不是期望的。例如,此代码将截断stdout,从而使shell诸如./script.py >> file 覆盖文件而不是附加到文件之类的事情。
salicideblock

在没有/ dev / stdout的Windows上将无法使用。
Bryan Oakley

0
if (out != sys.stdout):
    with open(out, 'wb') as f:
        f.write(data)
else:
    out.write(data)

在某些情况下略有改善。


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)

如果您使用的是Python 3.3或更高版本,则只需增加两行:一行用于额外的import行,另一行用于stack.enter_context

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.