Python中列表类的__contains__方法是如何实现的?

6

假设我定义了以下变量:

mode = "access"
allowed_modes = ["access", "read", "write"]

我目前有一个类型检查语句,如下所示:

assert any(mode == allowed_mode for allowed_mode in allowed_modes)

然而,看起来我可以简单地用以下内容替换它
assert mode in allowed_modes

根据 ThiefMasterPython List Class __contains__ Method Functionality 中的回答,这两者应该是等价的。这是否确实如此?我如何通过查看 Python 的源代码轻松验证这一点?

1
我找到了这个:https://github.com/python/cpython/blob/master/Objects/listobject.c。请看第402行。 - 0Tech
是的,它们是等价的。第二个(更短)版本应该会稍微快一点。您可以查看包含的源代码和列表迭代器的next - wildwilhelm
3个回答

12

不,它们不是等价的。例如:

>>> mode = float('nan')
>>> allowed_modes = [mode]
>>> any(mode == allowed_mode for allowed_mode in allowed_modes)
False
>>> mode in allowed_modes
True

请查看Membership test operations获取更多详细信息,包括以下声明:

  

对于容器类型(如列表、元组、集合、冻结集、字典或collections.deque),表达式x in y等效于any(x is e or x == e for e in y)


7

Python列表是由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_EQ)比较elPyList_GET_ITEM(a,i)返回1时停止。

2
我认为这是误导性的,因为你让它看起来只检查“相等性”,所以OP的两个片段将是等价的。但它们并不是等价的,因为还检查了身份,这很重要。请参见我的答案和PyObject_RichCompareBool的注释,其中说:“如果o1和o2是同一个对象,则PyObject_RichCompareBool()对于Py_EQ始终返回1,对于Py_NE始终返回0”。 - Stefan Pochmann

5

由于“any”需要额外的函数调用、生成器表达式和其他因素,所以它并不等同。

>>> mode = "access"
>>> allowed_modes =["access", "read", "write"]
>>> 
>>> def f1():
...    mode in allowed_modes
... 
>>> def f2():
...    any(mode == x for x in allowed_modes)
... 
>>> 
>>> 
>>> import dis
>>> dis.dis
dis.dis(          dis.disassemble(  dis.disco(        dis.distb(        
>>> dis.dis(f1)
  2           0 LOAD_GLOBAL              0 (mode)
              3 LOAD_GLOBAL              1 (allowed_modes)
              6 COMPARE_OP               6 (in)
              9 POP_TOP
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE
>>> dis.dis(f2)
  2           0 LOAD_GLOBAL              0 (any)
              3 LOAD_CONST               1 (<code object <genexpr> at 0x7fb24a957540, file "<stdin>", line 2>)
              6 LOAD_CONST               2 ('f2.<locals>.<genexpr>')
              9 MAKE_FUNCTION            0
             12 LOAD_GLOBAL              1 (allowed_modes)
             15 GET_ITER
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE
>>> 

这比 Python 方法本身更具说明性,但此处是列表__contains__的源代码,循环是用 C 编写的,可能比 Python 循环更快。
一些时间数值证实了这一点。
>>> import timeit
>>> timeit.timeit(f1)
0.18974408798385412
>>> timeit.timeit(f2)
0.7702703149989247
>>> 

“不等价”取决于您如何定义“等价”。从功能上讲,这两个解决方案在产生相同结果方面是等价的。 - bruno desthuilliers
真的,但我的意图是尽可能找出差异并突出它们。之后,您可以丢弃那些不相关的内容,然后考虑这两个是否等效。对我来说,在这种情况下应该使用f1而不是f2是很清楚的。 - Noufal Ibrahim
我已�很好地�解了你的�图😉 - �是想�确一点,两�解决方案都会产生相�的结�(因为OP没有定义"等效"),并且两者都会进行顺�查找和相等性测试。当然,包�性测试是显而易�的Pythonic解决方案(也是最快的解决方案)。 - bruno desthuilliers
@brunodesthuilliers 它们并不总是产生相同的结果。 - Stefan Pochmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接