# 简化版
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 函数完成具体操作。