理解为什么来自不同代码的这些操作码是相同的

Understanding why theses opcodes from different codes are the same

我想深入理解为什么下面这两个生成的操作码是相同的(除了值loaded/stored)。

特别是这个 'BINARY_MULTIPLY' 如何同时用于 str 和 int ? C (CPython) 是否在后台进行类型检查并应用正确的函数,无论值是字符串还是整数?

我们可以说这个机制与鸭子类型有关吗?

>>> def tata():
...     a = 1
...     b = 1
...     c = a * b
... 
>>> dis.dis(tata)
  2           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (a)

  3           6 LOAD_CONST               1 (1)
              9 STORE_FAST               1 (b)

  4          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 BINARY_MULTIPLY     
             19 STORE_FAST               2 (c)
             22 LOAD_CONST               0 (None)
             25 RETURN_VALUE      

>>> def toto():
...     a = "1"
...     b = "1"
...     c = a * b
... 
>>> dis.dis(toto)
  2           0 LOAD_CONST               1 ('1')
              3 STORE_FAST               0 (a)

  3           6 LOAD_CONST               1 ('1')
              9 STORE_FAST               1 (b)

  4          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 BINARY_MULTIPLY     
             19 STORE_FAST               2 (c)
             22 LOAD_CONST               0 (None)
             25 RETURN_VALUE      

你的第一个问题的答案是肯定的。 Python(CPython 实现)在内部检查操作数的类型并应用正确的函数,无论值是字符串还是整数。这种行为的原因虽然与实现相关,但通常是因为它更优化(C 显然比 Python 快)并且在某种意义上更整洁地推迟确定操作后的类型检查。原因之一可能是因为 1) 操作数的数量大多多于操作。 2) 类型检查(至少在 CPython 实现中)可以在内部进程中轻松正确地完成。

第二个问题的答案是否定的,因为我们不会根据 code/equation/etc 的其他 属性 来确定这些对象的类型。我们只是降低了优先级。

另请注意,关于字节码在等式中的顺序的另一个重要点是执行字节码的顺序与最终解析树相关,由各自的解析器创建。考虑以下示例:

In [4]: dis.dis("a, b, c, d = 4, 5, 7, 0; a + b * c - d")
  1           0 LOAD_CONST               5 ((4, 5, 7, 0))
              3 UNPACK_SEQUENCE          4
              6 STORE_NAME               0 (a)
              9 STORE_NAME               1 (b)
             12 STORE_NAME               2 (c)
             15 STORE_NAME               3 (d)
             18 LOAD_NAME                0 (a)
             21 LOAD_NAME                1 (b)
             24 LOAD_NAME                2 (c)
             27 BINARY_MULTIPLY
             28 BINARY_ADD
             29 LOAD_NAME                3 (d)
             32 BINARY_SUBTRACT
             33 POP_TOP
             34 LOAD_CONST               4 (None)
             37 RETURN_VALUE

Python 字节码是非常高级的,鉴于该语言极其动态的语义,它不能做太多不同的事情。当您在源代码中指定 * 时,无论操作数的类型如何,都会发出 BINARY_MULTIPLY 。具体做什么是在运行时确定的。

事后看来这是很明显的:在 Python 一般情况下 类型仅在运行时已知,并且考虑到它允许的灵活性(通过例如 monkeypatching)你只能在执行的那一刻决定要做什么。不出所料,这就是 CPython 这么慢的原因之一。

在特定情况下,例如您的示例中显示的这些情况,编译器可以执行类型推断并在编译时执行计算,或者至少发出一些(假想的)更具体的操作码。不幸的是,这会使解释器复杂化并且在一般情况下不会有太大帮助,因为通常您的计算涉及来自外部的参数,例如:

def square(x):
    return x*x

x 这里可以是任何类型,所以编译时智能没有用。

def times5(x):
    return x * 5

即使已知这里的 5,times5 也会根据 x 的类型做完全不同的事情("a" -> "aaaaa"2 -> 104.5 -> 22.5;一些自定义 class 类型 -> 它取决于运算符重载,仅在运行时才知道)。

您可以采用 asm.js 方式并找到提供类型提示的倾斜方式,但 Python (PyPy) 的高性能实现仅使用跟踪 JIT 方法自行推导常用的参数类型(在 运行 代码之后一段时间)并生成针对观察到的情况定制的优化机器代码。

这确实与延迟类型检查或 method-validity/existence 检查到调用之前的鸭子类型有关。 python BINARY_MULTIPLY 的作用与 python 表达式 lambda x, y: x * y 的作用完全相同。只要支持 __mul__ 协议,它就不会与任何类型明确相关。

如果你想知道这在 C 中是如何工作的,python 将操作码委托给 PyNumber_Multiply,如果可能的话,它从 __mul__ 槽中获取方法(或者它落下如果对象是 sequence),则返回重复,其中此方法是特定于类型的。换句话说,int、float、str、list 的 __mul__s 可能不同。