在这个问题之后的讨论让我感到疑惑,所以我决定运行一些测试,比较一下在Python中创建集合(我使用的是Python3.7)的set((x,y,z))和{x,y,z}的创建时间。
我比较了使用time和timeit的两种方法。两者均与以下结果一致:
1 2 3 4
| test1 ="""
my_set1 = set((1, 2, 3))
"""
print(timeit(test1)) |
结果:0.3024073549999993
1 2 3 4
| test2 ="""
my_set2 = {1,2,3}
"""
print(timeit(test2)) |
结果:0.10771795900000003
所以第二种方法比第一种方法快3倍。这对我来说是一个相当惊人的不同。在这种情况下,在set()方法的基础上,如何优化set-literal的性能?哪种情况比较合适?
*注:我只显示了timeit测试的结果,因为它们是在许多样本上的平均值,因此可能更可靠,但是使用time测试的结果在这两种情况下显示了相似的差异。
编辑:我知道这个类似的问题,虽然它回答了我最初问题的某些方面,但它并没有涵盖所有问题。在这个问题中,集合没有被解决,而且由于空集合在python中没有文本语法,我很好奇(如果有的话)使用文本创建的集合与使用set()方法有什么不同。另外,我想知道在后台如何处理set((x,y,z)中的tuple参数,以及它对运行时的可能影响。Coldspeed的伟大回答有助于解决问题。
- 相关stackoverflow.com/questions/36674083/…
- 是的,空的文本集不存在。非空的问题是这样的,你会发现,另一个问题的答案在很大程度上适用于你的问题。希望没有人会问关于tuple literals和tuple(...)的问题。
- @Andrasdeak这两个问题肯定有关联,但我不太确定它们是重复的。当set()比文本结构/理解语法更合适时,这个问题就没有解决了,后者似乎是这个xy问题的基础x。我不会自己关闭它,但如果关闭它,我不会失去任何睡眠。
- 这本质上是与[] vs list()相同的问题。使文字语法更快的因素是完全相同的。
- 现代Python的乐趣时光:它有一个"空的set字",独眼猴子操作员:{*()}。它使用一个空的tuple(在cpython上是一个单例,因此没有实际发生tuple构造)的解包归纳来施加必要的上下文,以便python看到正在构造一个set,而不是dict。
- @护林员太棒了,我不知道。谢谢你的信息
(这是对从初始问题中编辑出来的代码的响应)在第二种情况下,您忘记了调用函数。进行适当的修改,结果如预期:
1 2 3 4 5 6 7
| test1 ="""
def foo1():
my_set1 = set((1, 2, 3))
foo1()
"""
timeit(test1)
# 0.48808742000255734 |
1 2 3 4 5 6 7
| test2 ="""
def foo2():
my_set2 = {1,2,3}
foo2()
"""
timeit(test2)
# 0.3064506609807722 |
现在,计时不同的原因是因为set()是一个需要查找符号表的函数调用,而{...}集构造是语法的产物,速度快得多。
观察反汇编的字节码时,差异是明显的。
1 2 3 4 5 6 7
| import dis
dis.dis("set((1, 2, 3))")
1 0 LOAD_NAME 0 (set)
2 LOAD_CONST 3 ((1, 2, 3))
4 CALL_FUNCTION 1
6 RETURN_VALUE |
1 2 3 4 5 6
| dis.dis("{1, 2, 3}")
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 LOAD_CONST 2 (3)
6 BUILD_SET 3
8 RETURN_VALUE |
在第一种情况下,函数调用是由tuple (1, 2, 3)上的指令CALL_FUNCTION进行的(它也有它自己的开销,虽然很小,但它通过LOAD_CONST作为常量加载),而在第二种指令中,它只是一个BUILD_SET调用,这更有效。
回复:关于元组构造所用时间的问题,我们认为这实际上可以忽略不计:
1 2 3 4 5
| timeit("""(1, 2, 3)""")
# 0.01858693000394851
timeit("""{1, 2, 3}""")
# 0.11971827200613916 |
元组是不可变的,因此编译器通过将其作为常量加载来优化此操作,这称为常量折叠(可以从上面的LOAD_CONST指令中清楚地看到),因此所用的时间可以忽略不计。这在集合中看不到,它们是可变的(感谢@user2357112指出这一点)。
对于更大的序列,我们看到类似的行为。{..}语法在使用集合理解构造集合时比set()语法更快,后者必须从生成器构建集合。
1 2 3 4 5
| timeit("""set(i for i in range(10000))""", number=1000)
# 0.9775058150407858
timeit("""{i for i in range(10000)}""", number=1000)
# 0.5508635920123197 |
作为参考,您还可以在较新版本上使用ITerable解包:
1 2
| timeit("""{*range(10000)}""", number=1000)
# 0.7462548640323803 |
有趣的是,当直接调用range时,set()更快:
1 2
| timeit("""set(range(10000))""", number=1000)
# 0.3746800610097125 |
这恰好比集合结构更快。对于其他序列(如lists),您将看到类似的行为。
我的建议是在构造集合字面值时使用{...}集合理解,并作为将生成器理解传递给set()的替代方法;相反,使用set()将现有序列/iterable转换为集合。
- 他还创建了一个元组,然后将其传递给set函数,这个元组的创建时间是否计算在内?
- @可能是丹妮尔米乔,但我不能确定。在这种情况下,可能不像我相信的那样,python实习生(caches)元组,所以在第一对时间之后,这可能不会导致时间上的很大差异。但理论上,是的,这是有贡献的。
- 哎哟!我真傻,这完全可以解释。编辑这个问题,第一部分仍然相当鼓舞人心,我希望能更深入地了解正在发生的事情。还想知道@danielmesejo问了些什么。
- @DanielMesejo可能是错误的,但从字节码来看,它似乎没有创建元组,而是在最初解析Python代码时作为常量构建的。它只是一个元组。开销在后面,从那个元组构建集合。
- @Spectras您是对的,这实际上证明了一点,即元组是在初始调用之后缓存的。
- 关于你的tuple编辑,它大约快6倍。我不知道这在上下文中是否可以忽略不计,但这很有趣。谢谢你的信息!
- 实际上,set()并没有针对range进行专门的优化(至少从代码的快速浏览中找不到任何优化)。可能只是直接使用迭代器比需要额外抽象层的理解要快。另外,{*range(10000)}和set(range(10000))一样快,尽管我个人更喜欢后者。
- @infmagic2047我应该已经澄清,设置是为现成的iterables优化的(阅读:不是生成器)。我的测试表明解包比调用set()慢2倍左右,虽然我可以,但与讨论无关的解包没有添加它。
- @coldspeed你在测试什么样的python版本?我在3.6和3.7都没有速度差。
- python不在(1, 2, 3)元组中实习。你看的是不断折叠,而不是实习。
- @用户2357112为什么对{1, 2, 3}不做同样的事情?或者这里也做了持续的折叠,但是有更大的开销?
- @在infmagic2047中,我使用python3.6在我的2013 Mac上测试了这个。我为此添加了一个Timeit测试。
- @冷速:{1, 2, 3}是可变的。
- @用户2357112不敢相信这么简单。学到了一些新东西(并做了适当的编辑),谢谢!
- @coldspeed:注意:如果使用set文字作为in关键字的右侧(例如循环for x in {1,2,3}:或成员测试la if 1 in {1, 2, 3}:),它将执行常量折叠,在函数的常量数组中存储集合的frozenset版本。它可以做到这一点,因为这些操作不能改变设置(除非是疯狂的反射/检查胡说八道),所以冻结一个副本并重用它是合理的。纯粹的实现细节(例如,python 2没有这样做,在python 3的一个版本中进行了更改)。