【问题标题】:Python compiler and constants defined inside a functionPython 编译器和函数内部定义的常量
【发布时间】:2018-04-07 00:39:40
【问题描述】:

Python 编译器是否识别函数中定义的常量,以便无论随后在代码中调用该函数多少次,它只计算一次它们的值?

例如,

def f():
    x = [ 1, 2, 3, 4 ]
    # stuff

for i in range( 100 ):
    f()

x 是否会被重新计算 100 次 f() 被调用?

在使用它们的函数之外定义常量并不总是可能的,我很好奇 Python 是否在这些情况下支持你。

【问题讨论】:

  • Python 中没有什么是不变的……它是一种超动态语言。以后甚至可以修补函数、类和模块。
  • 如何将x 定义为python 中的常量?编译器怎么知道x 不会改变(例如在你的#stuff 部分)?

标签: python performance compiler-optimization


【解决方案1】:

(请注意,这适用于 CPython,在其他实现中可能会有所不同)

Python 代码被解析并编译成字节码。您可以查看与dis 模块一起使用的说明。

>>> def f(x):
...     x = [1, 2, 3, 4]
>>> dis.dis(f)
  2           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (2)
              4 LOAD_CONST               3 (3)
              6 LOAD_CONST               4 (4)
              8 BUILD_LIST               4
             10 STORE_FAST               0 (x)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

>>> print(dis.Bytecode(f).info())
Name:              f
Filename:          <stdin>
Argument count:    1
Kw-only arguments: 0
Number of locals:  1
Stack size:        4
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
   1: 1
   2: 2
   3: 3
   4: 4
Variable names:
   0: x

如您所见,整数字面量是常量,但每次都必须构建列表。

这是一个相对较快的操作(可能比查找全局更快,但时间仍然可以忽略不计)

如果您有一个使用元组的函数g,则将其作为常量加载:

>>> def g(x):
...     x = (1, 2, 3, 4)
>>> dis.dis(g)
  2           0 LOAD_CONST               5 ((1, 2, 3, 4))
              2 STORE_FAST               0 (x)
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE
>>> print(dis.Bytecode(g).info())
Name:              g
Filename:          <stdin>
Argument count:    1
Kw-only arguments: 0
Number of locals:  1
Stack size:        4
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
   1: 1
   2: 2
   3: 3
   4: 4
   5: (1, 2, 3, 4)
Variable names:
   0: x

但这似乎是过早优化的情况。

可以找到为函数存储的常量function.__code__.co_consts

>>> g.__code__.co_consts
(None, 1, 2, 3, 4, (1, 2, 3, 4))

每次都必须构建一个新列表的原因是,如果列表被更改,它不会影响每次加载的列表。

如果不是常量列表,元组优化就会消失。

>>> def h(x):
...     x = (1, 2, 3, x)
>>> dis.dis(h)
  2           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (2)
              4 LOAD_CONST               3 (3)
              6 LOAD_FAST                0 (x)
              8 BUILD_TUPLE              4
             10 STORE_FAST               0 (x)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE
>>> print(dis.Bytecode(h).info())
Name:              h
Filename:          <stdin>
Argument count:    1
Kw-only arguments: 0
Number of locals:  1
Stack size:        4
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
   1: 1
   2: 2
   3: 3
Variable names:
   0: x

【讨论】:

  • 注意:同样作为一个实现细节,lists 的常量以排除突变的方式使用(直接迭代或包含检查而不分配给命名变量,例如 for x in [1,2,3]: 或 @ 987654331@) 是编译时常量;窥视孔优化器用tuple 常量替换它们(因为,当不可能改变它们并且它们用于一些已知模式时,listtuple 行为相同,但tuple 稍微更有效存储和恒定,使其安全)。
【解决方案2】:

简答:对于列表,它没有

如果我们用dis检查编译后的中间代码,我们看到:

>>> dis.dis(f)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 LOAD_CONST               4 (4)
             12 BUILD_LIST               4
             15 STORE_FAST               0 (x)
             18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

如您所见,程序首先将常量1 加载到4 并将它们压入堆栈,然后用这些常量构造一个列表,这意味着它每次都构造一个列表强>.

如果列表没有变异,我建议在函数外定义常量

some_constant = [1, 2, 3, 4]
def f():
    # use some_constant
    # ...
    pass

【讨论】:

  • 如果它没有发生变异,并且您不依赖于一些 list 特定行为(例如与另一个 list 的相等比较),则在函数中使用常量文字的 tuple更快并避免范围问题(避免其他一些函数/模块意外地弄乱它),因为它可以完全存储在函数的常量表中。每次使用都变成一个简单的LOAD_CONST(直接使用文字)或LOAD_FAST(如果在命名的本地),归结为C数组查找,而不是LOAD_GLOBAL,它(至少)是一个dict查找。
  • @ShadowRanger:我知道,但元组不是列表,反之亦然。如果您稍后将此列表传递给检查isinstance(x, list) 的函数,那么对于列表,它将失败。所以我们不能假设它会有完全相同的行为。
  • 是的,但是 1. 像这样进行严格类型检查的函数通常是错误的形式和 2. 它不是错误形式的情况通常是它正在改变 list 的情况,所以全局不会安全的。您需要在输入函数时复制全局 (x = some_constant[:]) 以避免这种风险。有趣的替代方案:Python 3.5 的解包概括让您拥有蛋糕并吃掉它。你可以做x = [*(1, 2, 3, 4)],它通过加载常量tuple来构造list,然后执行一个单字节码BUILD_LIST_UNPACK
  • 四元素tuple->list 的通用解包技术对list 字面量没有真正的改进,但对于更大的序列,解包速度更快。它们都比对全局 list 进行切片以获得任何合理大小 list 的浅层副本要快(切片方法对于四个元素需要多 33% 的时间;切片在这里非常昂贵)。
猜你喜欢
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 2014-04-25
  • 1970-01-01
  • 2013-09-20
  • 1970-01-01
  • 2013-02-14
相关资源
最近更新 更多