Python 代码对象、函数和默认参数

Python code object, function, and default parameters

调查

我有这个程序

def gen_baz():
    return 2

def gen_bar(a, b, c=1):
    return a, b, c

# ---------- #

# Save the code objects
c1 = gen_bar.__code__
c2 = gen_baz.__code__

dis.dis(gen_bar) # [Disassambly 1]
print(inspect.getsource(gen_bar)) # [Inspect 1]

bar = gen_bar(1, 2) # bar: (1, 2, 1)
baz = gen_baz() # baz: 2
print(f"before: bar={bar}, baz={baz}")

# --------- #

gen_bar.__code__, gen_baz.__code__ = c2, c1 # swap code objects between 2 functions

dis.dis(gen_baz) # [Disassambly 2]
print(inspect.getsource(gen_baz))  # [Inspect 1]

bar = gen_bar() # bar: 2
baz = gen_baz(1, 2) # baz: [Error 1]
print(f"after: bar={bar}, baz={baz}")

这里是反汇编和检查的输出

[Disassembly 1 & 2]
 17           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 LOAD_FAST                2 (c)
              6 BUILD_TUPLE              3
              8 RETURN_VALUE

[Inspect 1 & 2]
def gen_bar(a, b, c=1):
    return a, b, c

[Error 1]
Traceback (most recent call last):
  File "C:\Users\something\test.py", line 32, in <module>
    baz = gen_baz(1, 2)
TypeError: gen_baz() missing 1 required positional argument: 'c'

问题

  1. 为什么我在baz和bar之间交换代码对象后,程序崩溃了?即使它们具有相同的字节码 and inspect result
  2. python中的默认参数存放在哪里?它是否与函数对象一起出现而不是与代码对象一起出现?如果是这样,那么 Inspect 模块如何在第二次检查时给我 c=1

非常感谢!

勾选 docs:

  • __code__: 包含已编译函数字节码的代码对象
  • __defaults__:位置参数或关键字参数的任何默认值的元组
  • __kwdefaults__:keyword-only 参数的任何默认值映射

默认值未存储在 code 对象中。它们直接存储在 function 对象中。

>>> gen_bar.__defaults__
(1,)

getsource返回的文本,正如名称所暗示的那样,来自源代码本身,而不是源代码生成的对象。 (在对象级别,签名仅由函数尝试从堆栈、全局命名空间等加载的值暗示)

您可以使用 inspect.signature 来查看 gen_baz 现在接受三个参数,但是 包括未传输到的默认值gen_baz 以及 code 对象。

>>> inspect.signature(gen_baz)
<Signature (a, b, c)>

默认参数值的使用隐藏在CALL_FUNCTION操作码的计算中。鉴于

def foo(a=1):
    return a

可以看到它的字节码都没有

>>> dis.dis(foo)
  2           0 LOAD_FAST                0 (a)
              2 RETURN_VALUE

无论有无显式参数都不会调用

>>> dis.dis('foo(9)')
  1           0 LOAD_NAME                0 (foo)
              2 LOAD_CONST               0 (9)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis('foo()')
  1           0 LOAD_NAME                0 (foo)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

利用函数的 __defaults__ 属性。在函数中,只是 假定 某些值可以从局部变量 a 推入堆栈,而不管该变量是如何设置的。在这两个调用中,在使用 CALL_FUNCTION.

之前,一个值被简单地加载到堆栈上或不加载到堆栈上