包装和解包 Python 函数的正确方法?

Proper way to wrap and unwrap a Python function?

我正在为 Python print 函数编写包装器,但我的问题更笼统 - 包装了一个函数,打开它的正确方法是什么?

这可行,但我有两个顾虑:

class Iprint():

    def __init__(self, tab=4, level=0):
        ''' Indented printer class.
                tab controls number of spaces per indentation level (equiv. to tabstops)
                level is the indentation level (0=none)'''

        global print

        self.tab = tab
        self.level = level

        self.old_print = print
        print = self.print

    def print(self, *args, end="\n", **kwargs):

        indent = self.tab * self.level

        self.old_print(" "*indent, end="", **kwargs)
        self.old_print(*args, end=end, **kwargs)

indent = Iprint()
indent.level = 3

print("this should be indented")
print = indent.old_print
print("this shouldn't be indented")

我的两个担忧:

  1. 如果 Iprint() class 有第二次实例化会怎样?这看起来很尴尬,也许是我应该避免的事情 - 但是如何呢?

  2. 倒数第二行print = indent.old_print“展开”打印函数,将其返回到它的原始函数。这看起来也很尴尬-如果忘记了怎么办?

我可以用 __exit__ 方法来完成,但我认为这会将它的使用限制在 with 块中。有没有更好的方法?

执行此操作的 Pythonic 方法是什么?

(我还应该提到,我预计会有 嵌套 包装器,我认为这使得正确地执行此操作更加重要...)

我想我已经解决了这个问题 - 至少让我自己满意。在这里,我调用了 class T(用于测试):

class T():

    old_print = None

    def __init__(self, tab=4, level=0):
        ''' Indented printer class.
                tab controls number of spaces per indentation level (equiv. to tabstops)
                level is the indentation level (0=none)'''

        T.tab = tab
        T.level = level

        self.__enter__()


    def print(self, *args, end="\n", **kwargs):

        indent = T.tab * T.level

        T.old_print(" "*indent, end="", **kwargs)
        T.old_print(*args, end=end, **kwargs)


    def close(self):

        if T.old_print is not None:

            global print
            print = T.old_print
            T.old_print = None

    def __enter__(self):
        if T.old_print is None:

            global print
            T.old_print = print
            print = self.print

    def __exit__(self, exception_type, exception_value, exception_traceback):
        self.close()


print("this should NOT be indented")

i = T(level=1)

print("level 1")

i2 = T(level=2)

print("level 2")

i.close()

print("this should not be indented")

i3 = T(level=3)

print("level 3")

i2.close()

print("not indented")

with i:
    print("i")

print("after i")

with T(level=3):
    print("T(level=3)")

print("after T(level=3)")

它会默默地强制 class 的单个(功能)实例,而不管 T() 被调用了多少次,正如 @MichaelButscher 所建议的(谢谢;这是迄今为止最有帮助的评论)。

它与 WITH 块一起工作干净,如果不使用 WITH 块,您可以手动调用关闭方法。

输出如预期的那样:

this should NOT be indented
    level 1
        level 2
this should not be indented
            level 3
not indented
            i
after i
            T(level=3)
after T(level=3)

你在这里真正想做的似乎是找到一种方法以“pythonic”方式覆盖内置 print 函数。

虽然有办法做到这一点,但我还是要小心一点。 "pythonic code"的规则之一是

Explicit is better than implicit.

覆盖print本质上是一种隐式解决方案,允许自定义打印功能来解决您的需求会更“pythonic”。

但是,假设我们正在讨论一个用例,其中可用的最佳选项是覆盖 print。例如,假设您想缩进 help() 函数的输出。

可以直接覆盖print,但是您运行有可能导致您看不到的意外更改。

例如:

def function_that_prints():
    log_file = open("log_file.txt", "a")
    print("This should be indented")
    print("internally logging something", file = log_file)
    log_file.close()
    

indent = Iprint()
indent.level = 3
function_that_prints() # now this internal log_file.txt has been corrupted
print = indent.old_print

这很糟糕,因为您可能只是想更改屏幕上打印的输出,而不是可能使用或不使用打印的内部位置。 相反,您应该只覆盖 stdout,而不是 print.

Python 现在包含一个名为 contextlib.redirect_stdout() 的实用程序,记录在 here.

实现可能如下所示:

import io
import sys
import contextlib

class StreamIndenter(io.TextIOBase):
    # io.TextIOBase provides some base functions, such as writelines()

    def __init__(self, tab = 4, level = 1, newline = "\n", stream = sys.stdout):
        """Printer that adds an indent at the start of each line"""
        self.tab = tab
        self.level = level
        self.stream = stream
        self.newline = newline
        self.linestart = True

    def write(self, buf, *args, **kwargs):
        if self.closed:
            raise ValueError("write to closed file")

        if not buf:
            # Quietly ignore printing nothing
            # prevents an issue with print(end='')
            return 

        indent = " " * (self.tab * self.level)

        if self.linestart:
            # The previous line has ended. Indent this one
            self.stream.write(indent)

        # Memorize if this ends with a newline
        if buf.endswith(self.newline):
            self.linestart = True

            # Don't replace the last newline, as the indent would double
            buf = buf[:-len(self.newline)]
            self.stream.write(buf.replace(self.newline, self.newline + indent))
            self.stream.write(self.newline)
        else:
            # Does not end on a newline
            self.linestart = False
            self.stream.write(buf.replace(self.newline, self.newline + indent))

    # Pass some calls to internal stream
    @property
    def writable(self):
        return self.stream.writable

    @property
    def encoding(self):
        return self.stream.encoding

    @property
    def name(self):
        return self.stream.name


with contextlib.redirect_stdout(StreamIndenter()) as indent:
    indent.level = 2
    print("this should be indented")
print("this shouldn't be indented")

以这种方式覆盖打印既不会破坏 print 的其他用途,又允许正确处理更复杂的用法。

例如:

with contextlib.redirect_stdout(StreamIndenter()) as indent:
    indent.level = 2
    print("this should be indented")

    indent.level = 3
    print("more indented")

    indent.level = 2
    for c in "hello world\n": print(c, end='')
    print()
    print("\n", end='')
    print(end = '')
    
print("this shouldn't be indented")

格式正确为:

        this should be indented
            more indented
        hello world
        
        
this shouldn't be indented