In the standard implementation of Python, in
doesn't quite use __contains__
directly. in
actually uses the C-level sq_contains
function pointer in the struct representing an object's type. For types with a __contains__
implemented in Python, sq_contains
points to a function that looks up and invokes the __contains__
method, but for types with a __contains__
implemented in C, sq_contains
points straight to the C implementation of the method.
Since list.__contains__
is implemented in C, 4 in li
gets to invoke the C implementation directly, without passing through the overhead of a Python method lookup and a Python function call. li.__contains__(4)
has to perform the Python method lookup and Python function call, so it's substantially slower.
If you want to see the code path involved for 4 in li
, you can follow the call hierarchy down from COMPARE_OP
in the bytecode evaluation loop. You'll see that it uses cmp_outcome
:
TARGET(COMPARE_OP)
{
w = POP();
v = TOP();
if (PyInt_CheckExact(w) && PyInt_CheckExact(v)) {
...
}
else {
slow_compare:
x = cmp_outcome(oparg, v, w);
}
cmp_outcome
uses PySequence_Contains
:
static PyObject *
cmp_outcome(int op, register PyObject *v, register PyObject *w)
{
int res = 0;
switch (op) {
...
case PyCmp_IN:
res = PySequence_Contains(w, v);
if (res < 0)
return NULL;
break;
PySequence_Contains
looks up the sq_contains
field of the tp_as_sequence
field of the C struct representing the list type:
int
PySequence_Contains(PyObject *seq, PyObject *ob)
{
Py_ssize_t result;
if (PyType_HasFeature(seq->ob_type, Py_TPFLAGS_HAVE_SEQUENCE_IN)) {
PySequenceMethods *sqm = seq->ob_type->tp_as_sequence;
if (sqm != NULL && sqm->sq_contains != NULL)
return (*sqm->sq_contains)(seq, ob);
This field stores a function pointer to list_contains
, the C function implementing the list containment check. At no point does Python have to perform any dict lookups to find the method, or allocate a method object to represent the method, or build a tuple of arguments to pass to the method.