为什么 [0] 是一个不同的函数而 0 不是?

Ove*_*gon 5 python cpython python-3.x

我检查了.__code__对象的两个我认为不同但发现相同的函数,用于各种表达式。如果代码对象相同,据我所知,它们编译为相同的字节码,因此是“相同”的函数。

下表是之前插入的东西,; pass这使得g有不同的__code__。由于f是一个“什么都不做”的函数,这表明“相同”下的所有内容都不要执行,包括长算术。此外,元组是“相同的”,但列表和字符串是“差异”的——所以我们可能会得出结论,涉及不可变文字的未分配表达式不会被评估。但是,由于引发异常,这可能是“异常” - 那么vs.呢? 不会引发异常并且可以分配。1/010**9910**910**99

我无法从分析中看出太多;“相同”和“差异”都有无法区分的执行时间。然而,当它们可以被区分时,它总是带有“diff”。

如果“相同”从不执行,那么 Python 如何确定执行或不执行什么?如果它们确实执行,它们的代码对象如何相同?


相同

  • 0, (0,), True, False,None
  • 10 ** 9
  • ()
  • -314159.265358 ** (1/12345) / 2.718281828 + 500 - 7j

差异

  • [0], {0: 0}
  • 10 ** 99
  • [], {},""

比较代码

def compare(fn1, fn2):
    for name in dir(fn1.__code__):
        if (name.startswith("co_") and
            name not in ("co_filename", "co_name", "co_firstlineno")):
            v1 = getattr(fn1.__code__, name)
            v2 = getattr(fn2.__code__, name)
            if v1 == v2:
                print(name.ljust(18), "same")
            else:
                print(name.ljust(18), "diff", v1, v2)

def f():
    pass

def g():
    10 ** 99; pass
Run Code Online (Sandbox Code Playgroud)

以下不同:(co_name总是),co_filename(IPython),co_firstlineno(来自文件)-但不影响“执行”的内容,如果有误请纠正我;来自docsco_code应该有所不同。


注意:接受的答案忽略了一个重要的直觉:如果存储值所需的代码比存储表达式所需的代码占用更多的内存来计算值,则可能会保留未分配的文字代码;情况就是这样10 ** 99(至少,这是评论中所断言的)。有关更多信息,请参阅答案下方的评论。

Mis*_*agi 3

“diff”组的所有文字要么不是常量([], {}),要么不利于优化(例如10 ** 99小于其值)。“同一”组的所有表达式的计算结果都是可以被丢弃的常量。检查字节码显示表达式已完全删除:

>>> # CPython 3.7.4
>>> def g(): 10/1; pass
>>> dis.dis(g)
1           0 LOAD_CONST               0 (None)
            2 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

值得注意的是,删除的表达式都不会改变可观察的行为。Python 实现是否消除不可观察的行为纯粹是一个实现细节。具有副作用的表达式(例如1/0)不会被删除。

>>> # CPython 3.7.4
>>> def g(): 10/0; pass
>>> dis.dis(g)
1           0 LOAD_CONST               1 (10)
            2 LOAD_CONST               2 (0)
            4 BINARY_TRUE_DIVIDE
            6 POP_TOP
            8 LOAD_CONST               0 (None)
           10 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

对于所示表达式,字节码在 CPython 3.7.4、CPython 3.8.2、PyPy 3.6.9 [PyPy 7.3.0] 上是相同的。

在 CPython 3.4.3、CPython 2.7.10、PyPy 2.7.13 [PyPy 7.1.1] 上,常量表达式10/1会被计算但不会被丢弃。

>>> # CPython 3.4.3
>>> def g(): 10/1; pass
>>> dis.dis(g)
1           0 LOAD_CONST               3 (10.0)                                                                                  
            3 POP_TOP                                                                                                            
            4 LOAD_CONST               0 (None)                                                                                  
            7 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

该表达式""在我可用的任何 Python 实现中都被丢弃。


由于这些优化是实现细节,因此没有正式的规范。如果需要更深入的理解,应该咨询实现本身。对于 CPython,窥视孔优化器源代码是一个很好的起点。

为了保持优化器简单,当 lineno 表具有间隙 >= 255 的复杂编码时,它会放弃。

优化仅限于单个基本块内发生的简单转换。所有转换都保持代码大小相同或更小。对于那些缩小尺寸的,间隙最初由 NOP 填充。随后,这些 NOP 被删除,并且跳转地址会在一次传递中重新定位。

  • @OverLordGoldDragon 由“10 ** 99”计算得出的“int”对象在 64 位机器上需要 68 字节的存储空间(其中一些对任何对象来说都是通用的,但特定于值的存储空间仍然远远超过30字节);存储计算函数的代码对象只需要 12 个字节。 (2认同)
  • @OverLordGoldDragon 我添加了 CPython 当前优化器的引用和链接。这解决了仅当常量表达式的值小于表达式本身时折叠常量表达式的标准。 (2认同)
  • @OverLordGoldDragon 折叠常量保证是正确的。折叠可变变量需要证明它们是正确折叠的常量。没有人努力做出有效的证明,即使他们这样做了,我们现在也会争论他们未能证明的一个边缘情况。恐怕“那又怎样”这个问题我无法客观回答。如果您认为这值得讨论而不是问答,请将其移至聊天。 (2认同)