# 简化版
def print(*args, sep=' ', end='\n', file=sys.stdout, flush=False):
text = sep.join(str(arg) for arg in args) + end
file.write(text)
if flush:
file.flush()
arg 都执行一次 str(arg)str(arg) 的 C 级实现(PyObject_Str)大致逻辑:type(arg) 自定义了 __str__,直接调用 arg.__str__()__repr__ 但没定义 __str__,不会自动回落到 __repr__;而是使用父类的 __str__(这正是你刚才碰到的情况)__repr__ 但没定义 __str__,没有继承父类,那么调用__repr__<__main__.MyException object at 0x...>不是只有 print() 才会触发 __str__,但 print() 是最常见的触发场景
format(obj) / f-string → 默认走 __format__,但如果不指定格式,也会回落到 str(obj),于是间接调用 __str__,
f"{obj}" 不指定格式 |
隐式 str(obj) |
|
"{0}".format(obj) |
同上 |
日志模块 logging.info("%s", obj) |
内部 str(obj) |
内置函数 str(obj) → 直接调用 obj.__str__()print(obj) → 内部调用 str(obj) → 调用 obj.__str__()
str()、f-string、"{}".format(obj)、日志里的 %s 等)都会走到同一条 C 级函数:PyObject_Str。PyObject_Str 的伪代码逻辑就是type(obj)->tp_str 非空 → 直接调用 obj.__str__()object.__str__,生成 <__main__.Foo object at 0x...>)str() 行为,再调用 __str__。str(obj) 字节码只有一条: LOAD_NAME 0 (obj) CALL_FUNCTION 1 (str) CPython 内部 builtin_str → PyObject_Str(obj) → obj.__str__() 这是 C 语言 的写法,不是 Python。 type(obj) 在 CPython 的 C API 里是一个指向“类型对象”的指针,->tp_str 就是去取这个类型对象里的一个字段,名字叫 tp_str,它保存着指向该类型的 __str__ 实现函数的指针。 逐字解释: type(obj) 在 C 层等价于 obj.__class__(拿到“类型对象”)。 type(obj)->tp_str 用 C 的“箭头运算符” -> 访问结构体成员,相当于 obj.__class__.__dict__.get("__str__") 的 C 版本,只不过这里取出来的是 C 函数指针。 如果这个指针非空(即类里真正定义了 __str__),就直接调用它; 如果是空指针(没有自定义 __str__),就回落到 object.__str__。 f-string f'prefix{obj}' 编译期把 f-string 拆成 CONST + FORMAT_VALUE: LOAD_CONST 'prefix' LOAD_NAME 0 (obj) FORMAT_VALUE 0 # 0 表示默认格式 BUILD_STRING 2 FORMAT_VALUE 的 C 实现: if (fmt_spec == NULL) res = PyObject_Str(value); // 直接调用 __str__ 日志 logging.info("%s", obj) logging 内部通过 Formatter → record.getMessage() → msg % args → 最终 PyUnicode_Format → PyObject_Str(obj)。
str(obj),就不会触发 __str____repr__ 只在需要“官方字符串表示”时被调用(交互式回显、repr(obj)、调试器显示等)PyObject_Str 是 C 语言级别的运行时函数(在 Objects/object.c 里),永远也不会出现在 Python 字节码里。
表格
| 你在 Python 层写的 | 字节码看到的 | 解释器运行时最终调用的 C 函数 |
|---|---|---|
str(obj) |
CALL_FUNCTION 1 |
builtin_str() → PyObject_Str() |
f'{obj}' |
FORMAT_VALUE |
FORMAT_VALUE 字节码 → PyObject_Str() |
PyObject_Str 属于后者,不出现在字节码层面compile() / import 时)生成的 中间指令序列,保存在 .pyc 文件里。x = 1 + 2 操作数 = 这条指令工作时需要用到的“数据”。 在 CPython 字节码里,操作数就是紧跟在 opcode 后面的 1~4 个字节,解释器按规则把它取出来,当作: 常量池索引 局部变量索引 跳转偏移量 其他立即数 举例 Python 源码 Python 复制 x = 1 + 2 编译后字节码(dis 输出) 复制 0 LOAD_CONST 0 (1) # opcode=LOAD_CONST, 操作数=0 2 LOAD_CONST 1 (2) # opcode=LOAD_CONST, 操作数=1 4 BINARY_ADD # opcode=BINARY_ADD, 无操作数 6 STORE_NAME 0 (x) # opcode=STORE_NAME, 操作数=0 对 LOAD_CONST 来说,操作数是 常量表索引(co_consts[0] 得到 1)。 对 STORE_NAME 来说,操作数是 名字表索引(co_names[0] 得到字符串 'x')。 BINARY_ADD 不需要额外数据,因此没有操作数字节
switch(opcode) 循环,每个分支里再 调用对应的 C 函数。switch 里决定,而不是字节码里存的地址。LOAD_CONST、BINARY_ADD …)在 CPython 源码中都对应一个整数值(如 LOAD_CONST = 100, BINARY_ADD = 23)。switch(opcode) 里跳转到对应的 C 代码分支,再调用相应的 C 函数完成具体操作。