我最近比较了[]和list()的处理速度,惊讶地发现[]比list()快三倍多。我用{}和dict()进行了相同的测试,结果几乎相同:[]和{}都花了大约0.128秒/百万次循环,而list()和dict()分别花了大约0.428秒/百万次循环。

为什么会这样?[]和{}(和probably()和",太)立即传递回一些空股票文字的副本,而他们的显式命名对应(list(), dict(), tuple(), str())完全创建一个对象,无论他们是否实际上有元素?

我不知道这两种方法有什么不同,但我很想知道。 我在文档或SO上找不到答案,而且搜索空括号的问题比我想象的要多。

我通过调用timeit.timeit("[]")和timeit.timeit("list()"),以及timeit.timeit("{}")和timeit.timeit("dict()")来获得计时结果,分别比较列表和字典。我运行的是Python 2.7.9。

我最近发现了“为什么if True比if 1慢?”,它比较了if True和if 1的性能,似乎涉及到类似的字面与全局场景;也许这也值得考虑。


因为list是一个将字符串转换为列表对象的函数,而[]则用于立即创建列表。试试这个(对你来说可能更有意义):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

y = ["wham bam"]
>>> y
["wham bam"]

给你一个实际的列表,包含你放入的任何东西。

因为[]和{}是字面语法。Python可以创建字节码来创建列表或字典对象:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

List()和dict()是分开的对象。它们的名称需要解析,必须使用堆栈来推送参数,必须存储帧以便稍后检索,并且必须进行调用。这些都需要更多的时间。

对于空的情况,这意味着你至少有一个LOAD_NAME(它必须搜索全局命名空间和内置模块),后面跟着一个CALL_FUNCTION,它必须保存当前帧:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

你可以用timeit单独计算名称查找的时间:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

这里的时间差异可能是字典哈希冲突。从调用这些对象的时间中减去这些时间,并将结果与使用字面量的时间进行比较:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

因此,每1000万次调用需要额外的1.00 - 0.31 - 0.30 == 0.39秒。

你可以通过将全局名称别名为局部名称来避免全局查找成本(使用timeit设置,你绑定到一个名称的所有东西都是局部名称):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

但你永远无法克服CALL_FUNCTION的代价。

List()需要全局查找和函数调用,但[]编译为一条指令。看到的:

Python 2.7.3
>>> import dis
>>> dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        

这里的答案很好,一针见血,完全涵盖了这个问题。对于感兴趣的人,我将从字节码中进一步删除一步。我正在使用最新的CPython回购;旧版本在这方面表现类似,但可能会有轻微的更改。

下面是每个函数的执行情况,BUILD_LIST用于[],CALL_FUNCTION用于list()。


BUILD_LIST指令:

你应该看看恐怖的场景:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

非常复杂,我知道。就是这么简单:

使用PyList_New创建一个新列表(这主要是为一个新的列表对象分配内存),oparg表示堆栈上的参数数量。开门见山。 检查if (list==NULL)没有出错。 使用PyList_SET_ITEM(宏)添加位于堆栈上的任何参数(在本例中不执行)。

难怪它这么快!它是为创建新列表定制的,没有别的:-)

CALL_FUNCTION指令:

这是你在查看处理CALL_FUNCTION的代码时看到的第一件事:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

看起来很无害,对吧?不,不幸的是,call_function不是一个直接调用函数的家伙,它不能。相反,它从堆栈中获取对象,获取堆栈的所有参数,然后根据对象的类型进行切换;是a:

PyCFunction_Type吗?不,它是list, list不是PyCFunction类型 PyMethodType吗?不,见前文。 PyFunctionType吗?不,见前文。

我们正在调用列表类型,传递给call_function的参数是PyList_Type。CPython现在必须调用一个通用函数来处理任何名为_PyObject_FastCallKeywords的可调用对象,还有更多的函数调用。

这个函数再次检查某些函数类型(我不明白为什么),然后,如果需要,在为kwargs创建dict之后,继续调用_PyObject_FastCallDict。

_PyObject_FastCallDict终于把我们带到某个地方了!在执行更多的检查之后,它从我们传递进来的类型的类型中抓取tp_call插槽,也就是说,它抓取type.tp_call。然后,它继续从_PyStack_AsTuple传入的参数中创建一个元组,最后,终于可以调用了!

匹配type的Tp_call。__call__接管并最终创建list对象。它调用对应于PyType_GenericNew的列表__new__,并使用PyType_GenericAlloc为它分配内存:这实际上是它最终赶上PyList_New的部分。要以通用的方式处理对象,所有前面的方法都是必需的。

最后,type_call调用列表。__init__并使用任何可用参数初始化列表,然后按照我们来的方式返回。: -)

最后,还记得LOAD_NAME吗,它是这里的另一个变量。


很容易看出,在处理我们的输入时,Python通常必须跳出一圈才能真正找到合适的C函数来完成这项工作。它没有立即调用它的礼节,因为它是动态的,有人可能会屏蔽列表(男孩,许多人都这样做),必须采取另一条路径。

这就是list()丢失很多东西的地方:Python需要去探索它到底应该做什么。

另一方面,字面语法只意味着一件事;它不能被改变,总是以一种预先确定的方式表现。

脚注:所有函数名都可能在不同版本之间发生变化。这一点现在仍然成立,而且很可能在未来的任何版本中都成立,是动态查找减慢了速度。

为什么[]比list()快?

最大的原因是Python将list()视为用户定义的函数,这意味着您可以通过将其他东西别名为list来拦截它,并做一些不同的事情(比如使用您自己的子类列表或deque)。

它立即使用[]创建一个内置列表的新实例。

我的解释是为了给你们一个直观的理解。

解释

[]通常被称为字面语法。

在语法中,这被称为“列表显示”。从文档中可以看出:

A list display is a possibly empty series of expressions enclosed in square brackets: list_display ::= "[" [starred_list | comprehension] "]" A list display yields a new list object, the contents being specified by either a list of expressions or a comprehension. When a comma-separated list of expressions is supplied, its elements are evaluated from left to right and placed into the list object in that order. When a comprehension is supplied, the list is constructed from the elements resulting from the comprehension.

简而言之,这意味着创建了一个list类型的内置对象。

这是无法避免的——这意味着Python可以尽可能快地做到这一点。

另一方面,list()可以在使用内置列表构造函数创建内置列表时被拦截。

例如,假设我们希望我们的列表创建噪音:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

然后,我们可以在模块级别的全局作用域中拦截name列表,然后当我们创建一个列表时,我们实际上创建了我们的子类型列表:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

类似地,我们可以从全局命名空间中删除它

del list

并把它放在内置命名空间中:

import builtins
builtins.list = List

现在:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

注意,列表显示会无条件地创建一个列表:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

我们可能只是暂时这样做,所以让我们撤销我们的更改-首先从内置中删除新的List对象:

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

不,我们找不到原版了。

不用担心,我们仍然可以得到list -它是列表文字的类型:

>>> builtins.list = type([])
>>> list()
[]

所以…

为什么[]比list()快?

正如我们所看到的,我们可以覆盖list,但我们不能拦截文字类型的创建。当我们使用list时,我们必须进行查找,看看是否有任何东西在那里。

然后我们必须调用我们查到的任何可调用对象。从语法来看:

一个调用调用一个可调用对象(例如,一个函数) 空参数系列: 调用::= primary "(" [argument_list [","] | comprehension] ")"

我们可以看到,它对任何名称都是一样的,而不仅仅是列表:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

对于[],Python字节码级别没有函数调用:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

它只是直接构建列表,而不需要在字节码级别进行任何查找或调用。

结论

我们已经演示了使用作用域规则可以用用户代码拦截list,并且list()查找可调用对象,然后调用它。

而[]是一个列表显示,或一个文字,因此避免了名称查找和函数调用。