Python - 如何检测我的对象何时写入stdout?

11 投票
2 回答
1407 浏览
提问于 2025-04-18 10:24

我有一个比较特别的请求,我想先解释一下我想要做什么,然后再说说为什么。

我想做什么

我想要检测我的对象什么时候被写入到标准输出(stdout),这样我就可以在那个时刻执行一些额外的操作。比如,当我输入:

sys.stdout.write(instance_of_my_class)

时,它应该执行一些额外的操作。我让我的类继承了 str,并重写了很多方法,比如 __call____unicode____str____repr__indexdecodeencodeformat__format____getattribute____getitem____len__,这样每次调用这些方法时都会打印一条信息,表明它们被调用了。但是,似乎 sys.stdout.write 并不会调用这些方法来打印对象。

需要注意的是,我特别提到的是 sys.stdout.write,而不是像 print 这样的函数——我发现 print 会调用它所给对象的 __str__ 方法。

我为什么这么做

这个问题是从关于 Windows中的彩色Python提示符 的回答延续下来的。

我发现每次 Python 需要显示交互式提示符时,它会调用 sys.ps1sys.ps2__str__ 方法,然后将结果保存以在命令行上显示。这意味着在 sys.ps2.__str__ 中的任何额外操作都是在 sys.ps1.__str__ 的操作之后立即发生的,但我希望这些操作等到显示 sys.ps2 时再执行。

所以,我在 sys.ps2.__str__ 中没有返回一个普通的 str,而是返回了我自己定义的 str 的子类,我希望它能在调用 sys.stdout.write 时捕捉到这个事件。

2 个回答

4

这个问题很有趣!我首先猜测,sys.stdout.write 不会调用 __str__ 方法,因为你的对象已经是一个 str(或者至少是它的一个子类,这在大多数情况下已经足够了)……所以不需要进行类型转换。

进一步调查显示,sys.stdout.write 确实从来不想调用 __str__ 方法……

子类方法

通过一些简单的检查,你可以发现 sys.stdout.write 调用你的 str 子类的哪些方法(答案是,不多):

class superstring(str):
    def __getattribute__(self, name):
        print "*** lookup attribute %s of %s" % (name, repr(self))
        return str.__getattribute__(self, name)

foo = superstring("UberL33tPrompt> ")
sys.stdout.write(foo)

在一个 Unicode 环境中(比如 Python 2.7,iPython notebook),这段代码会打印:

*** lookup attribute __class__ of 'UberL33tPrompt> '
*** lookup attribute decode of 'UberL33tPrompt> '
UberL33tPrompt> 

这看起来有点笨拙,但你可以重写子类的 decode 方法来实现想要的效果。

不过,在非 Unicode 环境中是没有属性查找的。

包装器方法

与其使用 str 的子类,也许你需要的是某种“包装器”来包裹 str。这里有一个看起来不太优雅的探索性黑客代码,它创建了一个类,大部分属性都委托给 str,但它并不严格是 str 的子类:

class definitely_not_a_string(object):
    def __init__(self, s):
        self.s = s
    def __str__(self):
        print "*** Someone wants to see my underlying string object!"
        return self.s
    def decode(self, encoding, whatever):
        print "*** Someone wants to decode me!"
        return self.s.decode(encoding, whatever)
    def __getattribute__(self, name):
        print "*** lookup attribute %s of %s" % (name, repr(self))
        if name in ('s', '__init__', '__str__', 'decode', '__class__'):
            return object.__getattribute__(self, name)
        else:
            return str.__getattribute__(self, name)

foo = definitely_not_a_string("UberL33tPrompt> ")
sys.stdout.write(foo)

在 Unicode 环境中,这基本上会得到相同的结果:

*** lookup attribute __class__ of <__main__.definitely_not_a_string object at 0x00000000072D79B0>
*** lookup attribute decode of <__main__.definitely_not_a_string object at 0x00000000072D79B0>
*** Someone wants to decode me!
*** lookup attribute s of <__main__.definitely_not_a_string object at 0x00000000072D79B0>
UberL33tPrompt> 

然而,当我在非 Unicode 环境中运行时,definitely_not_a_string 会出现错误信息:

TypeError: expected a character buffer object

……这表明 .write 方法直接访问了 C 级别的 缓冲接口,当它不需要进行任何 Unicode 解码时。

我的结论

看起来在 Unicode 环境中重写 decode 方法是一个可能的权宜之计,因为 sys.stdout.write 在需要将 str 解码为 Unicode 时会调用这个方法。

然而,在非 Unicode 环境中,.write 似乎根本不进行任何属性查找,而是直接访问 C 级别的字符缓冲协议,所以没有办法从 Python 代码中拦截它的访问。实际上,help(sys.stdout.write) 验证了它是一个内置函数(也就是说是用 C 写的,而不是 Python)。

2

为什么不直接修改stdout.write呢?

stdoutRegistry = set()

class A(object):
    def __init__(self):
        self.stdoutRegistry.add(self)

    def stdoutNotify(self):
        pass

original_stdoutWrite = sys.stdout.write
def stdoutWrite(*a, **kw):
    if a in stdoutRegistry:
        a.stdoutNotify()
    original_stdoutWrite(*a, **kw)
sys.stdout.write = stdoutWrite

撰写回答