如何在不显着减速的情况下使用 Python 3.4 的枚举?
How to use Python 3.4's enums without significant slowdown?
我正在编写一个井字游戏并使用枚举来表示三个结果 -- lose
、draw
和 win
。我认为这比使用字符串 ("lose", "win", "draw")
来指示这些值更好。但是使用枚举给我带来了显着的性能损失。
这是一个最小的例子,我只是引用 Result.lose
或文字字符串 lose
.
import enum
import timeit
class Result(enum.Enum):
lose = -1
draw = 0
win = 1
>>> timeit.timeit('Result.lose', 'from __main__ import Result')
1.705788521998329
>>> timeit.timeit('"lose"', 'from __main__ import Result')
0.024598151998361573
这比简单地引用全局变量要慢得多。
k = 12
>>> timeit.timeit('k', 'from __main__ import k')
0.02403248500195332
我的问题是:
- 我知道在 Python 中全局查找比本地查找慢得多。但为什么枚举查找更糟糕?
- 如何在不牺牲性能的情况下有效地使用枚举?结果证明,枚举查找完全支配了我的井字游戏程序的运行时间。我们可以在每个函数中保存枚举的本地副本,或者将所有内容包装在 class 中,但这两者看起来都很尴尬。
您正在为计时循环计时。字符串文字本身 完全被忽略 :
>>> import dis
>>> def f(): "lose"
...
>>> dis.dis(f)
1 0 LOAD_CONST 1 (None)
3 RETURN_VALUE
这是一个什么都不做的函数。所以计时循环需要 0.024598151998361573
秒到 运行 100 万次。
在这种情况下,字符串实际上变成了 f
函数的文档字符串:
>>> f.__doc__
'lose'
但 CPython 通常会在代码中省略字符串文字(如果未分配)或表达式的其他部分:
>>> def f():
... 1 + 1
... "win"
...
>>> dis.dis(f)
2 0 LOAD_CONST 2 (2)
3 POP_TOP
3 4 LOAD_CONST 0 (None)
7 RETURN_VALUE
此处 1 + 1
折叠成常量 (2
),字符串文字再次消失。
因此,您不能将其与查找 enum
对象的属性进行比较。是的,查找属性需要周期。但是查找另一个变量也是如此。如果你真的担心性能,你总是可以 cache 属性查找:
>>> import timeit
>>> import enum
>>> class Result(enum.Enum):
... lose = -1
... draw = 0
... win = 1
...
>>> timeit.timeit('outcome = Result.lose', 'from __main__ import Result')
1.2259576459764503
>>> timeit.timeit('outcome = lose', 'from __main__ import Result; lose = Result.lose')
0.024848614004440606
在 timeit
测试中,所有变量都是本地变量,因此 Result
和 lose
都是本地查找。
enum
属性查找确实比 'regular' 属性查找花费更多时间:
>>> class Foo: bar = 'baz'
...
>>> timeit.timeit('outcome = Foo.bar', 'from __main__ import Foo')
0.04182224802207202
那是因为 enum
元 class 包含一个 specialised __getattr__
hook,每次查找属性时都会调用它; enum
class 的属性在专门的字典中查找,而不是 class __dict__
。执行该挂钩方法和额外的属性查找(以访问地图)都需要额外的时间:
>>> timeit.timeit('outcome = Result._member_map_["lose"]', 'from __main__ import Result')
0.25198313599685207
>>> timeit.timeit('outcome = map["lose"]', 'from __main__ import Result; map = Result._member_map_')
0.14024519600206986
在 Tic-Tac-Toe 游戏中,您通常不会担心归结为微不足道的时间差异。当 人类玩家 比您的计算机慢几个数量级时就不会了。人类玩家不会注意到 1.2 微秒或 0.024 微秒之间的差异。
我正在编写一个井字游戏并使用枚举来表示三个结果 -- lose
、draw
和 win
。我认为这比使用字符串 ("lose", "win", "draw")
来指示这些值更好。但是使用枚举给我带来了显着的性能损失。
这是一个最小的例子,我只是引用 Result.lose
或文字字符串 lose
.
import enum
import timeit
class Result(enum.Enum):
lose = -1
draw = 0
win = 1
>>> timeit.timeit('Result.lose', 'from __main__ import Result')
1.705788521998329
>>> timeit.timeit('"lose"', 'from __main__ import Result')
0.024598151998361573
这比简单地引用全局变量要慢得多。
k = 12
>>> timeit.timeit('k', 'from __main__ import k')
0.02403248500195332
我的问题是:
- 我知道在 Python 中全局查找比本地查找慢得多。但为什么枚举查找更糟糕?
- 如何在不牺牲性能的情况下有效地使用枚举?结果证明,枚举查找完全支配了我的井字游戏程序的运行时间。我们可以在每个函数中保存枚举的本地副本,或者将所有内容包装在 class 中,但这两者看起来都很尴尬。
您正在为计时循环计时。字符串文字本身 完全被忽略 :
>>> import dis
>>> def f(): "lose"
...
>>> dis.dis(f)
1 0 LOAD_CONST 1 (None)
3 RETURN_VALUE
这是一个什么都不做的函数。所以计时循环需要 0.024598151998361573
秒到 运行 100 万次。
在这种情况下,字符串实际上变成了 f
函数的文档字符串:
>>> f.__doc__
'lose'
但 CPython 通常会在代码中省略字符串文字(如果未分配)或表达式的其他部分:
>>> def f():
... 1 + 1
... "win"
...
>>> dis.dis(f)
2 0 LOAD_CONST 2 (2)
3 POP_TOP
3 4 LOAD_CONST 0 (None)
7 RETURN_VALUE
此处 1 + 1
折叠成常量 (2
),字符串文字再次消失。
因此,您不能将其与查找 enum
对象的属性进行比较。是的,查找属性需要周期。但是查找另一个变量也是如此。如果你真的担心性能,你总是可以 cache 属性查找:
>>> import timeit
>>> import enum
>>> class Result(enum.Enum):
... lose = -1
... draw = 0
... win = 1
...
>>> timeit.timeit('outcome = Result.lose', 'from __main__ import Result')
1.2259576459764503
>>> timeit.timeit('outcome = lose', 'from __main__ import Result; lose = Result.lose')
0.024848614004440606
在 timeit
测试中,所有变量都是本地变量,因此 Result
和 lose
都是本地查找。
enum
属性查找确实比 'regular' 属性查找花费更多时间:
>>> class Foo: bar = 'baz'
...
>>> timeit.timeit('outcome = Foo.bar', 'from __main__ import Foo')
0.04182224802207202
那是因为 enum
元 class 包含一个 specialised __getattr__
hook,每次查找属性时都会调用它; enum
class 的属性在专门的字典中查找,而不是 class __dict__
。执行该挂钩方法和额外的属性查找(以访问地图)都需要额外的时间:
>>> timeit.timeit('outcome = Result._member_map_["lose"]', 'from __main__ import Result')
0.25198313599685207
>>> timeit.timeit('outcome = map["lose"]', 'from __main__ import Result; map = Result._member_map_')
0.14024519600206986
在 Tic-Tac-Toe 游戏中,您通常不会担心归结为微不足道的时间差异。当 人类玩家 比您的计算机慢几个数量级时就不会了。人类玩家不会注意到 1.2 微秒或 0.024 微秒之间的差异。