python是否重复使用计算结果?

Does python reuse repeated calculation results?

如果我有一个表达式希望在 Python 中求值,例如下面代码片段中 r 的表达式,Python 解释器是否会智能并重用子结果 x+y+z,还是只计算两次?我也很想知道这个问题的答案对于编译语言是否相同,例如C.

x = 1
y = 2
z = 3
r = (x+y+z+1) + (x+y+z+2)

有人提出这个问题类似于this question。我相信这是相似的。但是,我相信链接的问题不是 'minimal example'。同样在链接的问题中,操作顺序没有歧义,例如,在类似于此问题的示例中,没有定义操作顺序(数学上),具体取决于各个函数调用的顺序(它们是模棱两可),可能会完成更差或更好的优化工作。考虑 (abba)(abba)(baa*b),有嵌套的重复子串,根据预处理的顺序和数量,可以执行许多不同的优化。

不,python 默认情况下不这样做。如果您需要 python 为您保留某个计算的结果,您需要隐式告诉 python 这样做,一种方法是定义一个函数并使用 functools.lru_cache docs:

from functools import lru_cache

@lru_cache(maxsize=32)
def add3(x,y,z):
  return x + y + z

x=1
y=2
z=3
r = (add3(x,y,z)+1) + (add3(x,y,z)+2)

编辑: 此答案仅适用于 Python 语言的默认 CPython 解释器。它可能不适用于采用 JIT compilation techniques or uses a restricted Python sublanguage that allows static type inference. See 的其他 Python 实现以获取更多详细信息。

不,不会。您可以这样做以查看编译后的代码:

from dis import dis
dis("r=(x+y+z+1) + (x+y+z+2)")

输出:

          0 LOAD_NAME                0 (x)
          2 LOAD_NAME                1 (y)
          4 BINARY_ADD
          6 LOAD_NAME                2 (z)
          8 BINARY_ADD
         10 LOAD_CONST               0 (1)
         12 BINARY_ADD
         14 LOAD_NAME                0 (x)
         16 LOAD_NAME                1 (y)
         18 BINARY_ADD
         20 LOAD_NAME                2 (z)
         22 BINARY_ADD
         24 LOAD_CONST               1 (2)
         26 BINARY_ADD
         28 BINARY_ADD
         30 STORE_NAME               3 (r)
         32 LOAD_CONST               2 (None)
         34 RETURN_VALUE

这部分是因为 Python 是动态类型的。所以编译时不容易知道变量的类型。并且编译器无法知道可以由用户 类 重载的 + 运算符是否有任何副作用。考虑以下简单示例:

class A:
    def __init__(self, v):
        self.value = v

    def __add__(self, b):
        print(b)
        return self.value + b

x = A(3)
y = 4
r = (x + y + 1) + (x + y + 2)

对于简单的表达式,您可以将中间结果保存到一个新变量中:

z = x + y + 1
r = z + (z + 1)

对于函数调用,functools.lru_cache 是另一种选择,正如其他答案所指出的那样。

您可以使用 dis.dis 检查。输出是:

  2           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (x)

  3           4 LOAD_CONST               1 (2)
              6 STORE_NAME               1 (y)

  4           8 LOAD_CONST               2 (3)
             10 STORE_NAME               2 (z)

  5          12 LOAD_NAME                0 (x)
             14 LOAD_NAME                1 (y)
             16 BINARY_ADD
             18 LOAD_NAME                2 (z)
             20 BINARY_ADD
             22 LOAD_CONST               0 (1)
             24 BINARY_ADD
             26 LOAD_NAME                0 (x)
             28 LOAD_NAME                1 (y)
             30 BINARY_ADD
             32 LOAD_NAME                2 (z)
             34 BINARY_ADD
             36 LOAD_CONST               1 (2)
             38 BINARY_ADD
             40 BINARY_ADD
             42 STORE_NAME               3 (r)
             44 LOAD_CONST               3 (None)
             46 RETURN_VALUE

因此它不会缓存括号中表达式的结果。虽然对于那个特定的情况是可能的,但一般来说它不是,因为自定义 类 可以定义 __add__ (或任何其他二进制操作)来修改自己。例如:

class Foo:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        self.value += 1
        return self.value + other

x = Foo(1)
y = 2
z = 3
print(x + y + z + 1)  # prints 8
print(x + y + z + 1)  # prints 9

如果你有一个昂贵的函数,你想缓存它的结果,你可以通过 functools.lru_cache 来实现。

另一方面,编译器会执行constant folding,从下面的例子可以看出:

>>> import dis
>>> dis.dis("x = 'abc' * 5")
  1           0 LOAD_CONST               0 ('abcabcabcabcabc')
              2 STORE_NAME               0 (x)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE
>>> dis.dis("x = 1 + 2 + 3 + 4")
  1           0 LOAD_CONST               0 (10)
              2 STORE_NAME               0 (x)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE

If I have an expression that I wish to evaluate in Python, such as the expression for r in the code snippet below, will the Python interpreter be smart and reuse the subresult x+y+z, or just evaluate it twice?

您指的是哪个 Python 口译员?目前有四种广泛使用的生产就绪、稳定的 Python 实现。 None 其中实际上有一个 Python 解释器,它们中的每一个都可以编译 Python.

在至少某些情况下,他们中的一些人可能会或可能不会对至少某些程序执行此优化。

Python 语言规范既不要求也不禁止这种优化,因此任何符合规范的 Python 实现都可以执行此优化,但不是必需的。

我非常确定,与所有其他说明 Python 无法做到这一点的答案相反,PyPy 能够执行此优化。此外,根据您使用的底层平台,使用 Jython 或 IronPython 执行的代码也将受益于此优化,例如我 100% 确定 Oracle HotSpot 的 C2 编译器 会执行此优化。

I'd also be interested to know if the answer to this question would be the same for a compiled language […].

没有 "compiled language" 这样的东西。编译和解释是编译器或解释器(duh!)的特征,而不是语言。每种语言都可以由编译器实现,每种语言都可以由解释器实现。恰当的例子:有 C 的解释器,相反,每个当前存在的生产就绪、稳定、广泛使用的 Python、ECMAScript、Ruby 和 PHP 的实现至少有一个编译器,许多编译器甚至不止一个(例如 PyPy、V8、SpiderMonkey、Squirrelfish Extreme、Chakra)。

语言是写在一张纸上的一组抽象的数学规则和限制。一种语言既不编译也不解释,一种语言只是。这些概念存在于不同的抽象层上;如果英语是一种打字语言,术语 "compiled language" 将是打字错误。

I'd also be interested to know if the answer to this question would be the same for […] e.g. C.

有许多广泛使用的生产就绪、稳定的 C 实现。在至少某些情况下,他们中的一些人可能会或可能不会对至少某些程序执行此优化。

C 语言规范既不要求也不禁止这种优化,因此任何符合规范的 C 实现都可以执行这种优化,但不是必需的。