How to handle both `with open(...)` and `sys.stdout` nicely?

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

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

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

In ideal case it would be:

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

但是这不会工作得很好,因为当离开 with块时,sys.stdout 是关闭的,我不希望这样。我也不想

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

因为我需要记住恢复原始的标准输出。

相关阅读:

剪辑

我知道我可以包装 target,定义单独的函数或者使用 上下文管理器。我寻找一个简单,优雅,惯用的解决方案配件,不会需要超过5行

53791 次浏览

为什么 LBYL,当你可以 EAFP?

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

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

跳出框架思考一下,用一个定制的 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)

坚持使用你当前的代码。它很简单,你只需扫一眼就可以告诉 没错它在做什么。

另一种方法是使用内联 if:

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


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

But that isn't much shorter than what you have and it looks arguably worse.

你也可以让 sys.stdout变得不可关闭,但是这看起来不太像 Python:

sys.stdout.close = lambda: None


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

I'd also go for a simple wrapper function, which can be pretty simple if you can ignore the mode (and consequently stdin vs. stdout), for example:

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

如果你真的必须坚持一些更“优雅”的东西,比如一句俏皮话:

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

好吧,如果我们要进入一句话的战争,这里是:

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

只要上下文只写在一个地方,我喜欢雅各布的原始例子。如果您最终重新打开该文件进行多次写操作,那将是一个问题。我想我只需要在脚本的顶部做一次决定,然后让系统在退出时关闭文件:

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)

另一个可能的解决方案是: 不要试图避免上下文管理器退出方法,只需复制 stdout。

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

为 sys.stdout 打开一个新的 fd 怎么样? 用这种方法关闭它不会有任何问题:

if not target:
target = "/dev/stdout"
with open(target, 'w') as f:
f.write(content)
if (out != sys.stdout):
with open(out, 'wb') as f:
f.write(data)
else:
out.write(data)

在某些情况下略有改善。

沃尔夫的回答的改进

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

如果 filename确实是一个文件名,这允许二进制 IO 并将最终无关参数传递给 open

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

如果 sys.stdoutwith体后关闭是可行的,你也可以使用这样的模式:

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

或者更一般地说:

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

下面的解决办法并不美好,而是来自很久很久以前; 只是以前..。

handler = open(path, mode = 'a') if path else sys.stdout
try:
print('stuff', file = handler)
... # other stuff or more writes/prints, etc.
except Exception as e:
if not (path is None): handler.close()
raise e
handler.close()

解决这个问题的一种方法是使用多态性。 Pathlib.path有一个 open方法,它的功能正如你所期望的那样:

from pathlib import Path


output = Path("/path/to/file.csv")


with output.open(mode="w", encoding="utf-8") as f:
print("hello world", file=f)

我们可以复制这个界面进行打印

import sys


class Stdout:
def __init__(self, *args):
pass


def open(self, mode=None, encoding=None):
return self


def __enter__(self):
return sys.stdout


def __exit__(self, exc_type, exc_value, traceback):
pass

现在我们简单地用 Stdout代替 Path

output = Stdout("/path/to/file.csv")


with output.open(mode="w", encoding="utf-8") as f:
print("hello world", file=f)

这不一定比重载 open好,但是如果您使用的是 Path对象,这是一个方便的解决方案。

在 python 3中,你可以使用 IO 对象来包装 stdout文件描述符,避免关闭上下文,使用 closefd=False:

h = open(target, 'w') if target else open(sys.stdout.fileno(), 'w', closefd=False)


with h as h:
h.write(content)

正如在 Conditional with statement in Python中所指出的,Python 3.7允许使用 Nullcontext:

from contextlib import nullcontext


with open(target, "w") if target else nullcontext(sys.stdout) as f:
f.write(content)