如何使一个已经打开的文件可读(例如 sys.stdout)?

How does one make an already opened file readable (e.g. sys.stdout)?

我试图在字符串中获取 sys.stdout 的内容。我尝试了显而易见的:

def get_stdout():
    import sys

    print('a')
    print('b')
    print('c')

    repr(sys.stdout)

    contents = ""
    #with open('some_file.txt','r') as f:
    #with open(sys.stdout) as f:
    for line in sys.stdout.readlines():
        contents += line
    print(contents)

但这给出了错误:

Exception has occurred: UnsupportedOperation
not readable

那么我该如何更改已打开文件的权限?

我试过了:

    sys.stdout.mode = 'r'

但这仍然给出相同的错误...

其他可行的方法是以独立于硬件的方式让我获得 stdout 的 name/path。


另一件有用的事情是让我在 运行 我的主脚本在一个字符串中之后将 sys.stdout 的内容。


如果您遇到像我这样的错误,这些可能是相关的:why __builtins__ is both module and dict Python: What's the difference between __builtin__ and __builtins__?

错误:

line 37, in my_print
    __builtins__["print"](*args, file=f)  # saves to file
TypeError: 'module' object is not subscriptable

我读过的问题没有帮助:

您可以使用以下代码:

import sys
from builtins import print as builtin_print
myfile = "output.txt"
def print(*args):
    builtin_print(*args, file=sys.__stdout__)    # prints to terminal
    with open(myfile, "a+") as f:
        builtin_print(*args, file=f)    # saves in a file

这应该重新定义 print 函数,以便它打印到 stdout 和您的文件。然后您可以从文件中读取。

我想分享我正在使用的代码,灵感来自已接受的答案:

def my_print(*args, filepath="~/my_stdout.txt"):
    """Modified print statement that prints to terminal/scree AND to a given file (or default).

    Note: import it as follows:

    from utils.utils import my_print as print

    to overwrite builtin print function

    Keyword Arguments:
        filepath {str} -- where to save contents of printing (default: {'~/my_stdout.txt'})
    """
    import sys
    from builtins import print as builtin_print
    filepath = Path(filepath).expanduser()
    # do normal print
    builtin_print(*args, file=sys.__stdout__)  # prints to terminal
    # open my stdout file in update mode
    with open(filepath, "a+") as f:
        # save the content we are trying to print
        builtin_print(*args, file=f)  # saves to file

请注意 a+ 如果文件不存在则能够创建该文件。

请注意,如果您想删除自定义的旧内容my_stdout.txt,您需要删除该文件并检查它是否存在:

    # remove my stdout if it exists
    os.remove(Path('~/my_stdout.txt').expanduser()) if os.path.isfile(Path('~/my_stdout.txt').expanduser()) else None

我想应该就这些了。


编辑:

我遇到了一个错误:

line 37, in my_print
    __builtins__["print"](*args, file=f)  # saves to file
TypeError: 'module' object is not subscriptable

我查看了更多细节:

  • why __builtins__ is both module and dict
  • Python: What's the difference between __builtin__ and __builtins__?

并了解到 __builtins__ 似乎不可靠(由于 python 实施细节)。

似乎访问内置函数的最可靠方法是使用导入,所以我将其返回给原始回答者给我的代码。

您可以暂时 stdout 重定向到您选择的对象。下面显示的示例将打印数据存储在 StringIO 实例中。一旦上下文管理器块结束,正常打印恢复并允许显示一些调试信息:

#! /usr/bin/env python3
import contextlib
import io


def main():
    file = io.StringIO()
    with contextlib.redirect_stdout(file):
        print('a')
        print('b')
        print('c')
    print(f'{file!r}\n{file.getvalue()!r}\n{file.getvalue()!s}')


if __name__ == '__main__':
    main()

附录:

如果您希望像平常一样使用 stdout 并且仍然捕获打印到它的内容,您可能需要使用以下示例。 Apply class 可以包装多个实例并在所有实例中重复调用方法。因此,对 redirect_stdout 的调用稍作修改:

#! /usr/bin/env python3
import contextlib
import io
import sys


def main():
    file = io.StringIO()
    with contextlib.redirect_stdout(Apply(sys.stdout, file)):
        print('a')
        print('b')
        print('c')
    print(f'{file!r}\n{file.getvalue()!r}\n{file.getvalue()!s}')


class Apply:

    def __init__(self, *args):
        self.__objects = args

    def __getattr__(self, name):
        attr = _Attribute(getattr(obj, name) for obj in self.__objects)
        setattr(self, name, attr)
        return attr


class _Attribute:

    def __init__(self, iterable):
        self.__attributes = tuple(filter(callable, iterable))

    def __call__(self, *args, **kwargs):
        return [attr(*args, **kwargs) for attr in self.__attributes]


if __name__ == '__main__':
    main()

我之前对这个问题的回答并没有我想象的那么好()。我认为这个问题的真正答案是简单地使用记录器。直到最近我才知道记录器是什么,但它们要好得多。

最好创建一个记录器对象,将您的字符串发送到日志文件和标准输出。它甚至允许您根据阈值级别更精细地路由消息。这是代码:

def logger_SO_print_and_write_to_my_stdout():
    """My sample logger code to print to screen and write to file (the same thing).

    Note: trying to replace this old answer of mine using a logger: 
    - https://github.com/CoreyMSchafer/code_snippets/tree/master/Logging-Advanced

    Credit: 
    - https://www.youtube.com/watch?v=jxmzY9soFXg&t=468s
    - https://github.com/CoreyMSchafer/code_snippets/tree/master/Logging-Advanced
    - 

    Other resources:
    - https://docs.python-guide.org/writing/logging/
    - https://docs.python.org/3/howto/logging.html#logging-basic-tutorial
    """
    from pathlib import Path
    import logging
    import os
    import sys
    from datetime import datetime

    ## create directory (& its parents) if it does not exist otherwise do nothing :)
    # get current time
    current_time = datetime.now().strftime('%b%d_%H-%M-%S') 
    logs_dirpath = Path(f'~/logs/python_playground_logs_{current_time}/').expanduser()
    logs_dirpath.mkdir(parents=True, exist_ok=True)
    my_stdout_filename = logs_dirpath / Path('my_stdout.log')
    # remove my_stdout if it exists (note you can also just create a new log dir/file each time or append to the end of the log file your using)
    #os.remove(my_stdout_filename) if os.path.isfile(my_stdout_filename) else None

    ## create top logger
    logger = logging.getLogger(__name__) # loggers are created in hierarchy using dot notation, thus __name__ ensures no name collisions.
    logger.setLevel(logging.DEBUG) # note: use logging.DEBUG, CAREFUL with logging.UNSET: 

    ## log to my_stdout.log file
    file_handler = logging.FileHandler(filename=my_stdout_filename)
    #file_handler.setLevel(logging.INFO) # not setting it means it inherits the logger. It will log everything from DEBUG upwards in severity to this handler.
    log_format = "{asctime}:{levelname}:{lineno}:{name}:{message}" # see for logrecord attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
    formatter = logging.Formatter(fmt=log_format, style='{') # set the logging format at for this handler
    file_handler.setFormatter(fmt=formatter)

    ## log to stdout/screen
    stdout_stream_handler = logging.StreamHandler(stream=sys.stdout) # default stderr, though not sure the advatages of logging to one or the other
    #stdout_stream_handler.setLevel(logging.INFO) # Note: having different set levels means that we can route using a threshold what gets logged to this handler
    log_format = "{name}:{levelname}:-> {message}" # see for logrecord attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
    formatter = logging.Formatter(fmt=log_format, style='{') # set the logging format at for this handler
    stdout_stream_handler.setFormatter(fmt=formatter)

    logger.addHandler(hdlr=file_handler) # add this file handler to top logger
    logger.addHandler(hdlr=stdout_stream_handler) # add this file handler to top logger

    logger.log(logging.NOTSET, 'notset')
    logger.debug('debug')
    logger.info('info')
    logger.warning('warning')
    logger.error('error')
    logger.critical('critical')

日志内容:

2020-04-16 11:28:24,987:DEBUG:154:__main__:debug
2020-04-16 11:28:24,988:INFO:155:__main__:info
2020-04-16 11:28:24,988:WARNING:156:__main__:warning
2020-04-16 11:28:24,988:ERROR:157:__main__:error
2020-04-16 11:28:24,988:CRITICAL:158:__main__:critical

终端标准输出:

__main__:DEBUG:-> debug
__main__:INFO:-> info
__main__:WARNING:-> warning
__main__:ERROR:-> error
__main__:CRITICAL:-> critical

我觉得这是一个特别重要的 question/answer 参考,以防你遇到问题 UNSETAbout NOTSET in python logging 感谢上帝在那里的回答和问题。