Python 性能小习惯,能用in set就不用in list
list在python中表示数组,为一组元素的整合。set为集合,同list一样可以用来保存一组数据,但是两者却不尽相同。本文主要介绍为什么in set的性能优于 in list。
源码部分基于python3.10.4。
Set
set具有两个特点:
- 无序
- 唯一
无序,set中元素的保存是没有顺序的,不想栈和队列,满足先入先出或者先入后出的顺序。
s = set()
s.add(1)
s.add(3)
s.add(2)
print(s)
while len(s):
print(s.pop())
# 输出
{1, 2, 3}
1
2
3
从上面可以看出来,按1-3-2的顺序加入集合的元素,取出的顺序却是1-2-3。
唯一,set中的相同的元素不会重复保存,list中可以保存多个重复值的元素,但是set中不行。
s = set()
for i in range(5):
s.add(i)
s.add(3)
print(s)
# 输出
{0, 1, 2, 3, 4}
如上,在集合中加上0到4之后,在往集合中写入3。虽然不会报错,但是从输出结果可以看到,set中只保留了一个3。
性能对比
这里主要介绍in list和in set的性能对比。
import random
import time
def count_time(fun):
def warpper(*args):
s_time = time.time()
fun(*args)
print('%s耗时:%s' % (fun.__name__, time.time() - s_time))
return warpper
@count_time
def in_list(times, size):
larget_list = list(range(size))
count = 0
for i in range(times):
num = random.randint(0, size)
if num in larget_list:
count += 1
print(count)
@count_time
def in_set(times, size):
larget_set = set(range(size))
count = 0
for i in range(times):
num = random.randint(0, size)
if num in larget_set:
count += 1
print(count)
if __name__ == '__main__':
times = 100000
size = 10000
in_set(times, size)
in_list(times, size)
如上,分别生成一个0-9999的list和set。再利用random生成一个随机数,利用in来判断这个元素是否在list和set中。
输出结果:
99993
in_set耗时:0.09873580932617188
99991
in_list耗时:4.9168860912323
从上面的运行结果,可以明显的看出。in set的实际性能明显优于in list,那么都是有来保存一组元素的类型,为什么会有这么大的不同呢?
List查找
[Objects/listobject.c]
static PyObject *
list_index_impl(PyListObject *self, PyObject *value, Py_ssize_t start,
Py_ssize_t stop)
/*[clinic end generated code: output=ec51b88787e4e481 input=40ec5826303a0eb1]*/
{
Py_ssize_t i;
if (start < 0) {
start += Py_SIZE(self);
if (start < 0)
start = 0;
}
if (stop < 0) {
stop += Py_SIZE(self);
if (stop < 0)
stop = 0;
}
for (i = start; i < stop && i < Py_SIZE(self); i++) {
PyObject *obj = self->ob_item[i];
Py_INCREF(obj);
int cmp = PyObject_RichCompareBool(obj, value, Py_EQ);
Py_DECREF(obj);
if (cmp > 0)
return PyLong_FromSsize_t(i);
else if (cmp < 0)
return NULL;
}
PyErr_Format(PyExc_ValueError, "%R is not in list", value);
return NULL;
}
这是python源码中,实现的从list中查找一个元素是否存在,并返回这个元素第一次出现下标的具体实现。可以看到这里是使用for循环,从头到尾的去寻找这个元素,如果存在就返回下标,不然的话返回null,这里的时间复杂度为O(n)。
Set查找
static setentry *
set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)
{
setentry *table;
setentry *entry;
size_t perturb = hash;
size_t mask = so->mask;
size_t i = (size_t)hash & mask; /* Unsigned for defined overflow behavior */
int probes;
int cmp;
while (1) {
entry = &so->table[i];
probes = (i + LINEAR_PROBES <= mask) ? LINEAR_PROBES: 0;
do {
if (entry->hash == 0 && entry->key == NULL)
return entry;
if (entry->hash == hash) {
PyObject *startkey = entry->key;
assert(startkey != dummy);
if (startkey == key)
return entry;
if (PyUnicode_CheckExact(startkey)
&& PyUnicode_CheckExact(key)
&& _PyUnicode_EQ(startkey, key))
return entry;
table = so->table;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0)
return NULL;
if (table != so->table || entry->key != startkey)
return set_lookkey(so, key, hash);
if (cmp > 0)
return entry;
mask = so->mask;
}
entry++;
} while (probes--);
perturb >>= PERTURB_SHIFT;
i = (i * 5 + 1 + perturb) & mask;
}
}
这是python源码中,在set中查找某一个元素是否存在的实现函数。但是不同的是,set中元素的查找是通过hash来进行的,所以in set的时间复杂度只有差不多O(1)。
这里和很多人说的不太一样,很多人都说python中的set对象具有O(1)成员关系检查。那现在通过set源码的熟悉,可以知道O(1)其实是最优的情况下。因为在发生了散列碰撞的情况下,元素查找的时间会增加,这也是源码中while1的原因。在最后的情况下,当所有散列值碰撞时,成员检查是O(n)。
维基百科指出,不调整大小的散列表的最佳时间复杂度为O(1 + k/n) 。 由于Python集使用调整大小的哈希表,因此此结果不直接应用于Python集。
对于平均情况,假设一个简单的均匀哈希函数,时间复杂度为O(1/(1-k/n)) ,其中k/n可以由常数c<1 。
Big-O仅指作为n→∞的渐近行为。 由于k / n可以由常数限定,c <1, 与n无关 ,
O(1/(1-k/n))不大于等于O(constant) = O(1) O(1/(1-c)) O(1)` 。
因此,假设统一简单散列, 平均来说,Python集合的成员资格检查是O(1) 。