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) 。

posted @ 2022-09-15 16:32  红雨520  阅读(695)  评论(0编辑  收藏  举报