在Python中存储常量的位置



我目前正在进行一个项目,在该项目中,我有大量的全局常量要在代码中进行硬编码。该项目是用Python编写的。众所周知,Python在访问全局变量时的性能显著下降。

我可以将那些只在一个方法中使用的常量移动到我使用它们的方法的本地范围,但这会降低可读性。然后我仍然会有一些在多个方法中使用的全局变量,我真的不能把它们隐藏在一个函数的范围内。

解决方案是什么?我看到一个人在做一个游戏(Ludum Dare 31),在3:30中,你可以看到他只有一个大文件constants.py,里面有很多全局变量(没有global关键字)。这是一个好的实践吗?

众所周知,Python在访问全局变量时的性能显著下降。

不是"显著"-局部查找确实有点便宜,但定义局部变量也有成本,所以除非你在一个非常紧密的循环中查找全局变量,否则你注意到差异的可能性非常小,然后你总是可以在本地别名全局变量,即:

FOO = 42
def foo():
for i in range(100000000):
x = i * FOO

FOO = 42
def foo():
localfoo = FOO
for i in range(100000000):
x = i * localfoo

换句话说,你真的不应该担心这里的性能问题,直到性能成为一个真正的问题,并且探查器将这种全局查找确定为一个主要瓶颈(这真的非常不可能),即便如此,我也非常怀疑你最终是否会得到任何显著的提升——如果全局查找的成本对你的应用程序来说已经太高了,那么Python就不是合适的工具,是时候用C.重写这一部分了

我可以将那些只在一个方法中使用的常量移动到我使用它们的方法的本地范围,但这会降低可读性。

而且,如上所述,不一定会提高性能:

>>> import dis
>>> import timeit
>>> 
>>> FOO = 42
>>> def foo1():
...     return FOO
... 
>>> def foo3():
...     foo = 42
...     return foo
... 
>>> dis.dis(foo1)
2           0 LOAD_GLOBAL              0 (FOO)
3 RETURN_VALUE        
>>>
>>> dis.dis(foo3
... )
2           0 LOAD_CONST               1 (42)
3 STORE_FAST               0 (foo)
3           6 LOAD_FAST                0 (foo)
9 RETURN_VALUE  
>>> timeit.timeit("func()", "from __main__ import foo1 as func")
0.06334185600280762
>>> timeit.timeit("func()", "from __main__ import foo3 as func")
0.06805109977722168

然后我仍然会有一些全局变量在多个方法中使用,我真的不能把它们隐藏在一个函数的范围内。解决方案是什么?

到底出了什么问题?

我看到一个人在做一个游戏(…)你可以看到他只有一个大文件constants.py,里面有很多全局变量(没有全局关键字)。

在模块顶层定义的所有名称(通过赋值、导入、函数定义或类定义)都是模块的"全局"名称(这是Python中唯一一种"全局",没有"应用程序范围的全局")。global关键字只能在函数中使用,并且只有当您真正想在函数中分配给该全局时才使用——我们都知道我们不应该这样做,是吗?

这是一个好的实践吗?

取决于这些"常量"的使用方式和位置。如果有多个模块使用的常量,并且这些模块之间没有其他依赖关系,那么这确实是有道理的,但大多数时间常数要么只由一个模块使用,要么使用它们的其他模块也需要来自同一模块的其他名称(函数、类等)。

长话短说:常量没有什么特别的,它们只是引用对象的名称(你可能没有意识到,但你所有的函数和类也是"常量"),因此,您只想应用与其他任何模块相同的指导原则:您的模块应该具有强大的内聚性(模块中的所有内容都是强相关的)和低耦合性(您的模块依赖于尽可能少的其他模块)。从这个角度来看,在一个文件中定义几十个不相关的常量,而10多个其他不相关的模块依赖于这个文件,这是完全错误的——它破坏了内聚性,并引入了强耦合。

请注意,您可能还有其他一些理由以这种方式"集中"常量(至少是其中的一些):使配置更容易-但这只适用于您想要使其可配置的常量(您会使值"pi"可配置吗?),这是一个完全不同的问题。

如果您只关心代码在全局命名空间查找中的性能,那么您可能可以执行

globals()['your_constant_name'] # inside your function/method

它将直接查找全局名称空间中的内容。请注意,如果由于某种原因常量不存在,则将引发"KeyError"而不是"AttributeError"。

此外,根据Python文档

这始终是当前模块的字典(在函数或方法内部,这是定义它的模块,而不是调用它的模块)

所以要小心使用。

更新:

这是一个有点极端的情况(在任何现实场景中都不太可能发生),但如果堆栈帧很大,尽管有@brunodeshuilliers提到的字典结构等,字典查找确实会提高一点性能。测试代码:

import itertools,timeit
globals().update({''.join(n):i for i,n in enumerate(itertools.permutations('ABCDEFGHI'))})

def with_dict():
def func():
try:
func()
except RecursionError:
globals()['ABCDEFGHI']

def without_dict():
def func():
try:
func()
except RecursionError:
ABCDEFGHI
print(timeit.timeit(with_dict)) # output: 0.33404375400277786
print(timeit.timeit(without_dict)) # output: 0.3390919269877486

尽管根据python-wiki,字典查找的平均时间复杂度为O(1)

最新更新