【问题标题】:Proper way to wrap and unwrap a Python function?包装和解包 Python 函数的正确方法?
【发布时间】:2021-04-24 20:02:51
【问题描述】:

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

这可行,但我对此有两个担忧:

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() 类的实例化会发生什么?这看起来很尴尬,也许我应该阻止 - 但是如何?

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

我可以在 __exit__ 方法中执行此操作,但这会将其使用限制为 with 块 - 我认为。有没有更好的办法?

Pythonic 的方法是什么?

(我还应该提到,我预计会有 嵌套 包装器,我认为这使得正确地做到这一点更加重要......)

【问题讨论】:

  • 只有一个全局“print”,因此应该只有一个“old_print”。例如。您可以将其放置为类变量而不是实例变量。它可以具有默认值“无”,因此您可以测试是否已经存在活动替换。
  • 我想说根本不要重新标记/重置 print() 函数。如果您想编辑它或自定义它并专门调用它,请不要理会它,只需编写您自己的。称它为my_print() 或其他任何名称。然后,您可以根据需要使用其中任何一个。也更清楚了。
  • @acrobat 这将破坏目的,即能够更改 print() 函数,以便可以调用预先存在的函数(打印) - 而无需更改其源代码。
  • @MichaelButscher 好的,我读到它建议我应该将其限制为单个替换并且不允许类的多个实例。您如何/何时建议调用解包器(析构函数,无论您想调用它)?使用 enterexit 或其他方式?
  • 我认为,如果您打算这样做,您对上下文管理器 (with/__exit__) 的想法可能是最简洁的方法。

标签: python wrapper


【解决方案1】:

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

虽然有一种方法可以做到这一点,但我确实有一点警告。 "pythonic code"的规则之一是

显式优于隐式。

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

这很糟糕,因为大概您只是想更改打印在屏幕上的输出,而不是可能使用或不使用 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 既不会破坏 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
【解决方案2】:

我想我已经解决了这个问题 - 至少让我自己满意。在这里,我调用了 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)")

不管T() 被调用多少次,它都会默默地强制类的单个(功能性)实例,正如 @MichaelButscher 建议的那样(谢谢;这是迄今为止最有用的评论) .

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

输出如预期:

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)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-02
    • 2019-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-25
    相关资源
    最近更新 更多