【问题标题】:How to handle both `with open(...)` and `sys.stdout` nicely?如何很好地处理 `with open(...)` 和 `sys.stdout`?
【发布时间】:2013-07-10 06:51:26
【问题描述】:

通常我需要将数据输出到文件,或者,如果未指定文件,则输出到标准输出。我使用以下sn-p:

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')
...

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

相关:

编辑

我知道我可以包装target,定义单独的函数或使用context manager。我寻找一个简单、优雅、惯用的解决方案,不需要超过 5 行

【问题讨论】:

  • 太糟糕了,你之前没有添加编辑;)无论如何......或者你可以根本不费心清理你打开的文件:P
  • 你的第一个代码 sn-p 在我看来不错:表达意图并做你想做的事。

标签: python


【解决方案1】:

只是在这里跳出框框思考,自定义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)

【讨论】:

    【解决方案2】:

    坚持使用您当前的代码。这很简单,您只需看一眼就可以准确地告诉它在做什么。

    另一种方法是使用内联 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)
    

    【讨论】:

    • 您可以通过为它创建一个上下文管理器来保持不可关闭性,只要您需要它:with unclosable(sys.stdout): ... 通过在此上下文管理器中设置sys.stdout.close = lambda: None 并随后将其重置为旧值。但这似乎有点牵强...
    • 我在投票赞成“离开它,你可以确切地知道它在做什么”和投票反对可怕的不可关闭的建议之间左右为难!
    • @GreenAsJade 我不认为他是在建议sys.stdout 无法关闭,只是指出这是可以做到的。与其不提它们并希望它们不会被其他人偶然发现,不如展示它们并解释它们为什么不好。
    【解决方案3】:

    当你可以 EAFP 时为什么要 LBYL?

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

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

    【讨论】:

    • 异常不应用于控制例程的“正常”流程。表现?冒泡错误会比 if/else 更快吗?
    • 取决于您使用其中一种的可能性。
    • @JakubM。异常可以、应该并且在 Python 中像这样使用。
    • 考虑到 Python 的 for 循环通过捕获它所循环的迭代器抛出的 StopIteration 错误而退出,我想说使用异常进行流控制完全是 Pythonic。
    • 假设targetNone 当sys.stdout 是预期的,你需要捕捉TypeError 而不是IOError
    【解决方案4】:

    Wolph's answer的改进

    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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

        我也会选择一个简单的包装函数,如果你可以忽略模式(以及因此 stdin 与 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 的类会是更好的选择。
        • 如果我尝试写入with open_or_stdout(..) 块之外的文件,我会得到ValueError: I/O operation on closed file。我错过了什么? sys.stdout 并不意味着要关闭。
        【解决方案7】:

        好的,如果我们要进入单线战争,这里是:

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

        【讨论】:

        • 我不认为你的单行关闭文件对象。我错了吗?
        • @2rs2ts - 它确实......有条件。文件对象的引用计数为零,因为没有指向它的变量,因此可以立即(在 cpython 中)或稍后在垃圾收集发生时调用其 __del__ 方法。文档中有警告不要相信这将始终有效,但我一直在较短的脚本中使用它。运行很长时间并打开大量文件的大东西......好吧,我想我会使用“with”或“try/finally”。
        • TIL。我不知道文件对象的__del__ 会这样做。
        • @2rs2ts:CPython 使用引用计数垃圾收集器(根据需要在下面调用“真实”GC),因此它可以在您删除对流句柄的所有引用后立即关闭文件。 Jython 和 IronPython 显然只有“真正的”GC,所以他们在最终的 GC 之前不会关闭文件。
        【解决方案8】:
        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

        【讨论】:

          【解决方案9】:

          如果sys.stdoutwith body 之后关闭没问题,你也可以使用这样的模式:

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

          【讨论】:

            【解决方案10】:

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

            >>> 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
            【解决方案11】:

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

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

            【讨论】:

            • 可悲的是,运行这个 python 脚本需要一个 sudo 在我的安装。 /dev/stdout 归 root 所有。
            • 在许多情况下,将 fd 重新打开到 stdout 并不是我们所期望的。例如,此代码将截断 stdout,从而使 shell 像 ./script.py >> file 覆盖 文件而不是附加到文件。
            • 这不适用于没有 /dev/stdout 的窗口。
            【解决方案12】:
            if (out != sys.stdout):
                with open(out, 'wb') as f:
                    f.write(data)
            else:
                out.write(data)
            

            在某些情况下略有改善。

            【讨论】:

              【解决方案13】:

              下面的解决方案不是美女,而是来自很久很久以前;就在之前...

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-12-24
                • 2023-03-12
                • 2010-12-12
                • 1970-01-01
                • 2012-06-01
                相关资源
                最近更新 更多