Функциональность Python `in` vs.` __contains__`

Я реализовал метод __contains__ для класса в первый раз на днях, и поведение не было тем, что я ожидал. Я подозреваю, что есть какая-то тонкость в in, который я не понимаю, и я надеялся, что кто-то сможет просветить меня.

Мне кажется, что оператор in не просто переносит метод объекта __contains__, но также пытается принудительно вывести вывод __contains__ в boolean. Например, рассмотрим класс

class Dummy(object):
    def __contains__(self, val):
        # Don't perform comparison, just return a list as
        # an example.
        return [False, False]

Оператор in и прямой вызов метода __contains__ возвращают очень разные результаты:

>>> dum = Dummy()
>>> 7 in dum
True
>>> dum.__contains__(7)
[False, False]

Опять же, похоже, что in вызывает __contains__, но затем принуждает результат к bool. Я не могу найти это поведение где-либо, кроме факта, что __contains__ документация говорит, что __contains__ должен возвращать только True или False.

Я счастлив, следуя конвенции, но может ли кто-нибудь сказать мне точную связь между in и __contains__?

Эпилог

Я решил выбрать ответ @eli-korvigo, но все должны смотреть на @ashwini-chaudhary comment о bug ниже.

Ответ 1

Используйте источник, Люк!

Проследить реализацию оператора in

>>> import dis
>>> class test(object):
...     def __contains__(self, other):
...         return True

>>> def in_():
...     return 1 in test()

>>> dis.dis(in_)
    2           0 LOAD_CONST               1 (1)
                3 LOAD_GLOBAL              0 (test)
                6 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
                9 COMPARE_OP               6 (in)
               12 RETURN_VALUE

Как вы можете видеть, оператор in становится командой COMPARE_OP виртуальной машины. Вы можете найти это в ceval.c

TARGET(COMPARE_OP)
    w = POP();
    v = TOP();
    x = cmp_outcome(oparg, v, w);
    Py_DECREF(v);
    Py_DECREF(w);
    SET_TOP(x);
    if (x == NULL) break;
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    DISPATCH(); 

Взгляните на один из переключателей в cmp_outcome()

case PyCmp_IN:
    res = PySequence_Contains(w, v);
    if (res < 0)
         return NULL;
    break;

Здесь мы имеем вызов PySequence_Contains

int
PySequence_Contains(PyObject *seq, PyObject *ob)
{
    Py_ssize_t result;
    PySequenceMethods *sqm = seq->ob_type->tp_as_sequence;
    if (sqm != NULL && sqm->sq_contains != NULL)
        return (*sqm->sq_contains)(seq, ob);
    result = _PySequence_IterSearch(seq, ob, PY_ITERSEARCH_CONTAINS);
    return Py_SAFE_DOWNCAST(result, Py_ssize_t, int);
}

Это всегда возвращает int (boolean).

P.S.

Спасибо Martijn Pieters за предоставление , чтобы найти реализацию оператора in.

Ответ 2

В ссылка Python для __contains__ написано, что __contains__ должен возвращать True или False.

Если возвращаемое значение не является логическим, оно преобразуется в boolean. Вот доказательство:

class MyValue:
    def __bool__(self):
        print("__bool__ function runned")
        return True

class Dummy:
    def __contains__(self, val):
        return MyValue()

Теперь напишите в оболочке:

>>> dum = Dummy()
>>> 7 in dum
__bool__ function runned
True

И bool() непустого списка возвращает True.

Edit:

Это только документация для __contains__, если вы действительно хотите увидеть точное отношение, вы должны рассмотреть возможность поиска исходного кода, хотя я не уверен, где именно, но он уже ответил. В документации для сравнения написано:

Однако эти методы могут возвращать любое значение, поэтому, если оператор сравнения используется в булевом контексте (например, в условии оператора if), Python будет вызывать bool(), чтобы определить, является ли результат истинным или ложным.

Итак, вы можете догадаться, что это похоже на __contains__.