Python 源码阅读: list 中的 __len__() 和 in

Python 源码阅读: list 中的 _len_() 和 in

本文内容为博主阅读源码和官方文档以及其他相关文章后自己的理解, 不保证正确性。

昨天做 leetcode 的时候, 一道 K-Sum 的题目, 同样是 O(n²) 的复杂度, Java 能过, Python 超时, 我想可能是我调用了两次 _len_() 和加了一个 if not in 的判断的原因(最后发现是后者)。

阅读了一下 Python 的源码, list 的 __len__()in 实现如下:

_len_()

有关 list 定义源码位置在 Python 目录下的 include/listobject.h 内, 代码如下:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

其中 ob_item 是指向列表元素的指针数组, list[0] 即 ob_item[0], allocated 是列表的空间大小。在 PyObject_VAR_HEAD 中, 拥有一个 ob_size 变量。

下面是 Python 目录下的 include/object.h 中的相关代码:

#define PyObject_VAR_HEAD      PyVarObject ob_base;
...
...
typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

ob_size 变量存储的就是对象的长度, 所以每次调用 _len_() 方法的时候, 返回的是一个已经存储好了的变量, 并没有对列表进行遍历操作, 时间复杂度是 O(1)。

这个不是造成题目超时的原因。但是从代码规范来讲的话, len(xxx) 这个变量我使用超过了两次, 应该先赋值给一个变量代替的, 只是刷题的时候没讲究这么多, 这也是题外话了。

in

既然 _len_() 方法复杂度是 O(1), 那么问题就应该出在 if xxx in xxx 时调用的 _contains_() 方法里了:

查看官方 CPython 源码, 在 cpython/Object/listobject.c 中有这几行代码:

static int
list_contains(PyListObject *a, PyObject *el)
{
    Py_ssize_t i;
    int cmp;

    for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i)
        cmp = PyObject_RichCompareBool(el, PyList_GET_ITEM(a, i),
                                           Py_EQ);
    return cmp;
}

其中, Py_ssize_t 可在 PEP 353 中看到, 它是一个有符号的整形, 它所占字节数与编译器的 size_t 类型相同。

cpython/Object/object.c 可找到 PyObject_RichCompareBool 的定义代码:

int
PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
{
    PyObject *res;
    int ok;

    /* Quick result when objects are the same.
       Guarantees that identity implies equality. */
    if (v == w) {
        if (op == Py_EQ)
            return 1;
        else if (op == Py_NE)
            return 0;
    }

    res = PyObject_RichCompare(v, w, op);
    if (res == NULL)
        return -1;
    if (PyBool_Check(res))
        ok = (res == Py_True);
    else
        ok = PyObject_IsTrue(res);
    Py_DECREF(res);
    return ok;
}

PyObject_RichCompareBool 比较传入的 v 和 w 的值, 具体比较内容由 op 决定, op 必须是 Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, 或者 Py_GE 的其中一个, 分别对应 <, <=, ==, !=, >, 和 >=。在 list_contains(PyListObject *a, PyObject *el) 的 for 循环中, 传入的是 PyEQ, 也就是判断是否相等。整个函数的步骤如下:

如果 vw 相等的话, 就根据 '==' 和 '!=' 返回相应的值; 否则调用 PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid)

PyObject_RichCompare 的源码同样在 cpython/Object/object.c 中:

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;

    assert(Py_LT <= op && op <= Py_GE);
    if (v == NULL || w == NULL) {
        if (!PyErr_Occurred())
            PyErr_BadInternalCall();
        return NULL;
    }
    if (Py_EnterRecursiveCall(" in comparison"))
        return NULL;
    res = do_richcompare(v, w, op);
    Py_LeaveRecursiveCall();
    return res;
}

这个函数也是根据第 3 个参数比较传入的值, 成功时返回比较值, 失败则返回 NULL。

PyObject_RichCompare 执行完后调用 PyBool_Check 来验证 res 是否是一个 PyBool_Type 类型, 是的话判断布尔值是否为 Py_True, 否则调用 PyObject_IsTrue() 判断 res 对象是否为真, 为真返回 1 否则返回 0。

最后调用 Py_DECREF(res) 来减少 res 的 reference counts, 当 reference counts 减到 0 时将对象释放。这里要注意的是, 它并不会直接调用 free(), 而是通过 PyObject 中的对象 ob_type 的函数指针来调用 free()

这样一来 if xxx in list 的过程就明了了, 它会使用线性查找, 遍历整个列表来判断值是否存在于列表中。时间复杂度为 O(n)。

虽然只是 O(n) 的复杂度, 但是在 K-sum 的题目中如果存入的单个解是一个数组的话, 每次调用 PyObject_RichCompareBool() 所耗的时间会更多, 这大概就是超时的原因吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom阅读 2,689评论 0 3
  • 用于python面试整理,主要来源于http://www.cnblogs.com/skiler/p/6952707...
    十里江城阅读 2,322评论 0 13
  • 实际上,无论看了这问的人都要承认,我们大家都是矛盾体,在希望和失望里矛盾自己。 前阵子又看了一次黑客帝国,实际上我...
    呼吸的鲸鱼阅读 187评论 0 0
  • 每日打卡。 来评论区记录下今天的收获和成长吧!
    树洞君阅读 237评论 14 1
  • 新的开始 文/薰子 平淡的一生中 总需要无数个这样新的开始 从前的失意、惨败以及不堪回首 都可以被埋葬,被掩盖 在...
    會寫詩的小妮子阅读 155评论 0 1