【问题标题】:How to capture stdout output from a Python function call?如何从 Python 函数调用中捕获标准输出输出?
【发布时间】:2013-05-10 09:14:33
【问题描述】:

我正在使用一个对对象执行某些操作的 Python 库

do_something(my_object)

并改变它。这样做时,它会将一些统计信息打印到标准输出,我想掌握这些信息。正确的解决方案是更改do_something() 以返回相关信息,

out = do_something(my_object)

但是do_something() 的开发人员还需要一段时间才能解决这个问题。作为一种解决方法,我考虑解析 do_something() 写入标准输出的任何内容。

如何捕获代码中两点之间的标准输出输出,例如,

start_capturing()
do_something(my_object)
out = end_capturing()

?

【问题讨论】:

标签: python stdout capture


【解决方案1】:

试试这个上下文管理器:

from io import StringIO 
import sys

class Capturing(list):
    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self
    def __exit__(self, *args):
        self.extend(self._stringio.getvalue().splitlines())
        del self._stringio    # free up some memory
        sys.stdout = self._stdout

用法:

with Capturing() as output:
    do_something(my_object)

output 现在是一个包含函数调用打印的行的列表。

高级用法:

可能不明显的是,这可以多次执行并且结果连接起来:

with Capturing() as output:
    print('hello world')

print('displays on screen')

with Capturing(output) as output:  # note the constructor argument
    print('hello world2')

print('done')
print('output:', output)

输出:

displays on screen                     
done                                   
output: ['hello world', 'hello world2']

更新:他们在 Python 3.4 中将 redirect_stdout() 添加到 contextlib(以及 redirect_stderr())。所以你可以使用io.StringIO 来获得类似的结果(尽管Capturing 是一个列表以及上下文管理器可以说更方便)。

【讨论】:

  • 谢谢!并感谢您添加高级部分...我最初使用切片分配将捕获的文本粘贴到列表中,然后我把自己放在头上并使用 .extend() 代替,所以它可以连接使用,就像你注意到的那样。 :-)
  • P.S.如果要重复使用,我建议在__exit__()方法中的self.extend()调用之后添加self._stringio.truncate(0),以释放_stringio成员持有的部分内存。
  • 很好的答案,谢谢。对于 Python 3,使用 from io import StringIO 而不是上下文管理器中的第一行。
  • 这是线程安全的吗?如果其他线程/调用在 do_something 运行时使用 print() 会发生什么?
  • 此答案不适用于 C 共享库的输出,请参阅 this answer
【解决方案2】:

在 python >= 3.4 中,contextlib 包含一个 redirect_stdout 装饰器。它可以用来回答您的问题,如下所示:

import io
from contextlib import redirect_stdout

f = io.StringIO()
with redirect_stdout(f):
    do_something(my_object)
out = f.getvalue()

来自the docs

用于临时将 sys.stdout 重定向到另一个文件的上下文管理器 或类似文件的对象。

此工具为现有的函数或类增加了灵活性 输出硬连线到标准输出。

例如,help() 的输出通常被发送到 sys.stdout。你 可以通过将输出重定向到一个字符串来捕获该输出 io.StringIO 对象:

  f = io.StringIO() 
  with redirect_stdout(f):
      help(pow) 
  s = f.getvalue()

要将 help() 的输出发送到磁盘上的文件,请将输出重定向到 常规文件:

 with open('help.txt', 'w') as f:
     with redirect_stdout(f):
         help(pow)

将 help() 的输出发送到 sys.stderr:

with redirect_stdout(sys.stderr):
    help(pow)

请注意,对 sys.stdout 的全局副作用意味着此上下文 manager 不适合在库代码和大多数线程中使用 应用程序。它对子流程的输出也没有影响。 但是,对于许多实用程序脚本来说,它仍然是一种有用的方法。

这个上下文管理器是可重入的。

【讨论】:

  • 尝试f = io.StringIO() with redirect_stdout(f): logger = getLogger('test_logger') logger.debug('Test debug message') out = f.getvalue() self.assertEqual(out, 'DEBUG:test_logger:Test debug message') 时。它给了我一个错误:AssertionError: '' != 'Test debug message'
  • 这意味着我做错了什么或者它无法捕获标准输出日志。
  • @EzizDurdyyev, logger.debug 默认情况下不写入标准输出。如果您将日志调用替换为 print(),您应该会看到该消息。
  • 是的,我知道,但我确实让它像这样写入标准输出:stream_handler = logging.StreamHandler(sys.stdout)。并将该处理程序添加到我的记录器中。所以它应该写到标准输出,redirect_stdout 应该抓住它,对吧?
  • 我怀疑问题出在您配置记录器的方式上。我将验证它是否在没有 redirect_stdout 的情况下打印到标准输出。如果是这样,则可能在上下文管理器退出之前缓冲区不会被刷新。
【解决方案3】:

这是一个使用文件管道的异步解决方案。

import threading
import sys
import os

class Capturing():
    def __init__(self):
        self._stdout = None
        self._stderr = None
        self._r = None
        self._w = None
        self._thread = None
        self._on_readline_cb = None

    def _handler(self):
        while not self._w.closed:
            try:
                while True:
                    line = self._r.readline()
                    if len(line) == 0: break
                    if self._on_readline_cb: self._on_readline_cb(line)
            except:
                break

    def print(self, s, end=""):
        print(s, file=self._stdout, end=end)

    def on_readline(self, callback):
        self._on_readline_cb = callback

    def start(self):
        self._stdout = sys.stdout
        self._stderr = sys.stderr
        r, w = os.pipe()
        r, w = os.fdopen(r, 'r'), os.fdopen(w, 'w', 1)
        self._r = r
        self._w = w
        sys.stdout = self._w
        sys.stderr = self._w
        self._thread = threading.Thread(target=self._handler)
        self._thread.start()

    def stop(self):
        self._w.close()
        if self._thread: self._thread.join()
        self._r.close()
        sys.stdout = self._stdout
        sys.stderr = self._stderr

示例用法:

from Capturing import *
import time

capturing = Capturing()

def on_read(line):
    # do something with the line
    capturing.print("got line: "+line)

capturing.on_readline(on_read)
capturing.start()
print("hello 1")
time.sleep(1)
print("hello 2")
time.sleep(1)
print("hello 3")
capturing.stop()

【讨论】:

    【解决方案4】:

    基于kindallForeverWintr 的回答。

    我为Python<3.4 创建redirect_stdout 函数:

    import io
    from contextlib import contextmanager
    
    @contextmanager
    def redirect_stdout(f):
        try:
            _stdout = sys.stdout
            sys.stdout = f
            yield
        finally:
            sys.stdout = _stdout
    
    
    f = io.StringIO()
    with redirect_stdout(f):
        do_something()
    out = f.getvalue()
    

    【讨论】:

      【解决方案5】:

      还借鉴了@kindall 和@ForeveWintr 的答案,这里有一个可以完成此任务的类。与之前的答案的主要区别在于,这会将其捕获为作为字符串,而不是作为StringIO 对象,这样使用起来更方便!

      import io
      from collections import UserString
      from contextlib import redirect_stdout
      
      class capture(UserString, str, redirect_stdout):
          '''
          Captures stdout (e.g., from ``print()``) as a variable.
      
          Based on ``contextlib.redirect_stdout``, but saves the user the trouble of
          defining and reading from an IO stream. Useful for testing the output of functions
          that are supposed to print certain output.
          '''
      
          def __init__(self, seq='', *args, **kwargs):
              self._io = io.StringIO()
              UserString.__init__(self, seq=seq, *args, **kwargs)
              redirect_stdout.__init__(self, self._io)
              return
      
          def __enter__(self, *args, **kwargs):
              redirect_stdout.__enter__(self, *args, **kwargs)
              return self
      
          def __exit__(self, *args, **kwargs):
              self.data += self._io.getvalue()
              redirect_stdout.__exit__(self, *args, **kwargs)
              return
      
          def start(self):
              self.__enter__()
              return self
      
          def stop(self):
              self.__exit__(None, None, None)
              return
      

      例子:

      # Using with...as
      with capture() as txt1:
          print('Assign these lines')
          print('to a variable')
      
      # Using start()...stop()
      txt2 = capture().start()
      print('This works')
      print('the same way')
      txt2.stop()
      
      print('Saved in txt1:')
      print(txt1)
      print('Saved in txt2:')
      print(txt2)
      

      这在Sciris 中实现为sc.capture()

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-07-09
        • 1970-01-01
        • 1970-01-01
        • 2010-10-27
        相关资源
        最近更新 更多