我最近比较了 []
和 list()
的处理速度,并惊讶地发现 []
的运行速度比 list()
快三倍多 . 我使用 {}
和 dict()
进行了相同的测试,结果实际上是相同的: []
和 {}
都花费了大约0.128秒/百万个周期,而 list()
和 dict()
大约花费了0.428秒/百万个周期 .
为什么是这样? []
和 {}
(也可能是 ()
和 ''
)立即传回一些空库存文字的副本,而他们明确命名的对应物( list()
, dict()
, tuple()
, str()
)完全用于创建对象,无论它们是否实际有元素?
我不知道这两种方法有何不同,但我很想知道 . 我在文档中或在SO上找不到答案,搜索空括号比我预期的问题更多 .
我通过调用 timeit.timeit("[]")
和 timeit.timeit("list()")
,以及 timeit.timeit("{}")
和 timeit.timeit("dict()")
来分别比较列表和词典,从而得到了我的计时结果 . 我正在运行Python 2.7.9 .
我最近发现“Why is if True slower than if 1?”将 if True
与 if 1
的性能进行了比较,似乎触及了类似的字面对全局情景;也许值得考虑一下 .
5 回答
最大的原因是Python将
list()
视为用户定义的函数,这意味着你可以通过别名别名来拦截它到list
并做一些不同的事情(比如使用你自己的子列表或者deque) .它立即使用
[]
创建内置列表的新实例 .我的解释试图给你直觉 .
解释
[]
通常称为文字语法 .在语法中,这被称为"list display" . From the docs:
简而言之,这意味着创建了
list
类型的内置对象 .没有规避这个 - 这意味着Python可以尽可能快地完成它 .
另一方面,
list()
可以使用内置列表构造函数从创建内置list
中截取 .例如,假设我们想要大声创建列表:
然后我们可以在模块级全局范围拦截名称
list
,然后当我们创建list
时,我们实际创建了我们的子类型列表:同样,我们可以从全局命名空间中删除它
并将其放在内置命名空间中:
现在:
请注意,列表显示无条件地创建列表:
我们可能只是临时执行此操作,因此我们撤消更改 - 首先从内置函数中删除新的
List
对象:哦,不,我们失去了对原作的追踪 .
不用担心,我们仍然可以获得
list
- 它是列表文字的类型:所以...
正如我们所见 - 我们可以覆盖
list
- 但是我们无法拦截文字类型的创建 . 当我们使用list
时,我们必须进行查找以查看是否有任何内容 .然后,我们必须调用我们查找的任何可调用对象 . 从语法:
我们可以看到它对任何名称都做同样的事情,而不仅仅是列表:
对于
[]
,Python字节码级别没有函数调用:它只是直接构建列表而没有在字节码级别进行任何查找或调用 .
结论
我们已经证明
list
可以使用作用域规则拦截用户代码,并且list()
查找可调用的然后调用它 .而
[]
是列表显示或文字,因此避免了名称查找和函数调用 .因为
[]
和{}
是字面语法 . Python可以创建字节码只是为了创建列表或字典对象:list()
和dict()
是单独的对象 . 需要解析它们的名称,必须涉及堆栈以推送参数,必须存储帧以便稍后检索,并且必须进行调用 . 这都需要更多时间 .对于空案例,这意味着你至少有一个LOAD_NAME(必须搜索全局命名空间以及builtin module),然后是CALL_FUNCTION,这必须是保留当前帧:
您可以使用
timeit
单独计算名称查找时间:时间差异可能存在字典哈希冲突 . 从调用这些对象的时间减去这些时间,并将结果与使用文字的时间进行比较:
因此,必须调用该对象每1000万次调用需要额外的_404411秒 .
您可以通过将全局名称别名为locals来避免全局查找成本(使用
timeit
设置,绑定到名称的所有内容都是本地的):但你永远无法克服成本 .
这里的答案很棒,到了这一点并完全涵盖了这个问题 . 对于那些感兴趣的人,我会从字节码中再下一步 . 我正在使用最新的CPython回购;旧版本在这方面表现相似,但可能会有细微的变化 .
以下是每个执行的细分,
BUILD_LIST
表示[]
,CALL_FUNCTION
表示list()
.BUILD_LIST指令:
你应该只看恐怖:
我知道,非常复杂 . 这是多么简单:
使用PyList_New创建一个新列表(这主要为新列表对象分配内存),
oparg
表示堆栈中的参数数量 . 开门见山 .检查
if (list==NULL)
没有出错 .使用PyList_SET_ITEM(宏)添加位于堆栈上的任何参数(在我们的示例中,这不会被执行) .
难怪它快!它是为创建新列表而定制的,没有别的:-)
CALL_FUNCTION指令:
这是您在查看代码处理时看到的第一件事
CALL_FUNCTION
:看起来很无害,对吧?嗯,不,不幸的是,call_function不是一个直接调用该函数的直接人,它不能 . 相反,它从堆栈中抓取对象,抓取堆栈的所有参数,然后根据对象的类型进行切换;它是一个:
PyCFunction_Type?不,它是
list
,list
不是PyCFunction
类型PyMethodType?不,见前一个 .
PyFunctionType?不,见前一个 .
我们调用
list
类型,传入call_function
的参数是PyList_Type . CPython现在必须调用一个泛型函数来处理名为_PyObject_FastCallKeywords的任何可调用对象,以及更多的函数调用 .这个函数再次对某些函数类型进行了一些检查(我无法理解为什么),然后,如果需要,在为kwargs创建一个dict之后,继续调用_PyObject_FastCallDict .
_PyObject_FastCallDict
终于让我们到了某个地方!在执行了更多检查之后,grabs the tp_call slot from the type我们已经传入了grabs the tp_call slot from the type,也就是说,它 grab 了type.tp_call
. 然后它继续创建一个由_PyStack_AsTuple
传递的参数中的元组,最后是a call can finally be made!tp_call
,匹配type.call接管并最终创建列表对象 . 它调用列表__new__
,它对应于PyType_GenericNew,并为PyType_GenericAlloc分配内存:这实际上是它最终赶上PyList_New
的部分 . 以前所有都是以通用方式处理对象所必需的 .最后,
type_call
调用list.__init__
并使用任何可用的参数初始化列表,然后我们继续返回我们的方式 . :-)最后,请记住
LOAD_NAME
,这是另一个在这里贡献的人 .很容易看出,在处理我们的输入时,Python通常必须跳过箍以实际找到适当的
C
函数来完成这项工作 . 它没有动态,有人可能会掩盖list
(男孩会做很多人这样做),必须采取另一条道路 .这就是
list()
失去的地方:探索Python需要做的是找出应该做些什么 .另一方面,文字语法恰恰意味着一件事;它不能改变,总是以预先确定的方式行事 .
脚注:所有功能名称可能会从一个版本更改为另一个版本 . 这个观点仍然存在,并且很可能会出现在任何未来的版本中,它是动态的查找,减慢了速度 .
因为
list
是function将一个字符串转换为一个列表对象,而[]
用于创建一个关闭该列表的列表 . 试试这个(可能对你更有意义):而
为您提供包含您放入其中的任何内容的实际列表 .
list()
需要全局查找和函数调用,但[]
需要编译为单个指令 . 看到: