是否打印管道到引擎盖下的不同流?

Does print pipe to different streams under the hood?

我在 运行 以下脚本中遇到了(对我来说)有点奇怪的行为。

如您所见,似乎 write 被调用了多次,我想知道这是为什么,因为我已经明确覆盖了 file=sys.stdout 行为。

究竟如何在引擎盖下打印管道流,它是否通过管道传输到所有通道?它是否有一些默认行为,the docs 除了以下内容外不是很具体:

The file argument must be an object with a write(string) method; if it is not present or None, sys.stdout will be used.

测试脚本

import sys

def debug(*args, **kwargs):
    pass

def _debugwrite(obj):
    print("You're looking at Attila, the psychopathic killer, the caterpillar")
    out = sys.stderr
    out.write(obj)

debug.write = _debugwrite

print("Don't you ever disrespect the caterpillar", file=debug)

输出:

You're looking at Attila, the psychopathic killer, the caterpillar
You're looking at Attila, the psychopathic killer, the caterpillar
Don't you ever disrespect the caterpillar

如我所料:

You're looking at Attila, the psychopathic killer, the caterpillar
Don't you ever disrespect the caterpillar

我试过的:

我尝试使用 inspect 模块来获取调用者,也许会看到实际调用 write 的人是谁,但我得到了 module,我不知道为什么 :( 这很明显吗?


更多问题

有没有什么方法可以调试 Python 之外的函数并进入底层 C 调用?因为主要的 Python 分布是 CPython,如果我的理解是正确的,Python 只是底层 C 代码的 apiPython 中的调用最终会在幕后转换为 C 调用。例如,我发现 print is defined as follows in C,但我很难理解那里发生了什么(因为,呃,我不知道 C),但也许通过使用调试器,我可以打印出一些东西,看看是什么,如果不是全部,至少可以弄清楚流程。我非常想了解一般情况下发生的事情,而不是想当然。

提前感谢您的时间!

当答案非常简单时,您正在寻找非常复杂的东西。

我什至不知道 "pipe to all channels" 是什么意思,但是 print 什么也不知道。它所做的只是在您传递给它的 file 对象上调用 write

但是,它为每个参数调用一次 write,为每个 sep 调用一次,为 end.

调用一次

所以,这一行:

print("Don't you ever disrespect the caterpillar", file=debug)

…大致相当于:

debug.write(str("Don't you ever disrespect the caterpillar"))
debug.write("\n")

… 这当然意味着您会收到两次额外的 print 消息。


顺便说一句,为了将来调试或理解这样的事情:如果您将额外的 print 更改为包括 repr(obj),那么发生的事情将是显而易见的:

def _debugwrite(obj):
    print("stderring " + repr(obj))
    out = sys.stderr
    out.write(obj)

然后输出为:

stderring "Don't you ever disrespect the caterpillar"
stderring '\n'
Don't you ever disrespect the caterpillar

不那么神秘了吧?


当然 stdoutstderr 是独立的流,有自己的缓冲区。 (默认情况下,当与 TTY 通话时,stdout 是行缓冲的,而 stderr 是无缓冲的。)所以顺序不是您天真地期望的,但它是有道理的。如果你只是添加 flushes,输出变成:

stderring "Don't you ever disrespect the caterpillar"
Don't you ever disrespect the caterpillarstderring '\n'

(末尾空行)


您的奖励问题:

I tried to use inspect module to get the caller, maybe see who does the actual call to write but I get module, idk why :( is this obvious?

我假设你做了类似 inspect.stack()[1].function 的事情?如果是这样,您正在检查的代码是模块中的顶级代码,因此 inspect 将其显示为名为 <module>.

的假函数

Is there any way to debug a function beyond Python and go into the underlying C call?

当然可以。只是 运行 CPython 本身在 lldb、gdb、Microsoft 的调试器或您通常用于调试二进制程序的任何其他工具下。您可以在 ceval 循环或特定 C API 函数中或任何您想要的地方放置断点。您可能想要制作 CPython 的调试版本(执行 ./configure --help 以查看选项)以使其变得更好。

Because well the main Python distribution, is CPython, and if my understanding is correct, Python is just an api for the underlying C code.

嗯,不相当。它是一个编译器和一个字节码解释器。该字节码解释器 很大程度上 使用与 extending/embedding 接口相同的 C API,但重叠不是 100%; CAPI以下的结构也有处理

A call in Python gets translated to a C call under the hood eventually. So for instance I found out that the print is defined as follows in C, but it's tough for me to understand what's going on there (because, erm, I don't know C) but maybe by going with a debugger I could print stuff out, see what is what and figure out maybe at least the flow if not everything. I'd very much like to understand what's going on under the hood in general instead of taking stuff for granted.

是的,你可以这样做,但你需要同时了解 C 和 CPython API(例如,如何找到等同于 [=36= 的 C 槽]) 找出断点的位置并开始跟踪。

对于这样的情况,在 Python 中编写包装器并在 Python 中调试它们要容易得多。例如:

import builtins
def print(*args, **kwargs):
    return builtins.print(*args, **kwargs)

或者,如果您担心 print 在其他模块中被调用,而不仅仅是在您的模块中,您甚至可以在 builtins:

中隐藏它
builtins._print = builtins.print
def print(*args, **kwargs):
    return builtins._print(*args, **kwargs)
builtins.print = print

现在您可以使用 pdb 在 Python 级别中断对 print 的每次调用,而无需担心 C.

当然,您甚至可以在 PyPy 或 Jython 或其他任何工具中调试此代码,以查看它是否与 "builtin" 级别以上的 CPython 有任何不同。

你得到你看到的结果是因为 builtin_print() 调用 PyFile_Write*() 两次 ,一次是为了 print the argument, and again to print the EOL。它们是乱序的,因为默认情况下 stderr 是无缓冲的,而 stdout 是行缓冲的。