Если у класса нет __contains__, каким запасным протоколом Python проверяет вхождение через оператор in?
Сначала Python ищет специальный метод __contains__. Если его нет, проверка выполняется через получение итератора, то есть обычно с помощью __iter__; если и этого протокола нет, используется старый последовательностный протокол через __getitem__ с индексами, начиная с нуля.
Проверка завершается при первом равном элементе. Для варианта с __getitem__ окончанием последовательности считается исключение IndexError.
Модель данных Python поддерживает несколько уровней протоколов, чтобы разные типы объектов могли участвовать в одинаковых операциях. Оператор in появился как единый интерфейс для контейнеров, но контейнеры могут быть реализованы по-разному: напрямую проверять наличие, перебираться или предоставлять доступ по индексам.
Поддержка __getitem__ как запасного варианта сохраняет совместимость с объектами, реализующими старый последовательностный протокол без явного __iter__.
Неверно считать, что оператор in всегда вызывает __contains__. Это приведёт к ошибкам при анализе пользовательских классов: объект может поддерживать проверку вхождения, даже если такого метода в его исходном коде нет.
Особенно важно понимать последствия реализации только __getitem__. Такой объект должен корректно завершать обращение по индексам исключением IndexError; иначе проверка вхождения может выполняться бесконечно или завершаться другой ошибкой.
Алгоритм имеет следующую концептуальную последовательность:
__contains__, Python вызывает его и приводит результат к логическому значению.__contains__ отсутствует, Python пытается получить итератор через __iter__ и последовательно сравнивает элементы с искомым значением.__iter__ нет, используется итерация по индексам через __getitem__, начиная с нулевого индекса.Наличие __contains__ обычно предпочтительно для контейнеров, способных выполнять поиск эффективнее полного перебора. Например, множество может проверять принадлежность примерно за константное среднее время, тогда как универсальный перебор требует просмотра элементов.
Минимальный пример запасного протокола:
При проверке Python обращается к names[0], затем к names[1]. Обращение к names[2] вызывает IndexError, после чего Python считает последовательность законченной и возвращает True, поскольку нужное значение уже найдено.
Если __iter__ определён, но во время работы выбрасывает исключение, Python не обязан переключаться на __getitem__: наличие метода и успешное выполнение итератора — разные условия. Исключение из выбранного протокола обычно передаётся вызывающему коду.
Сравнение каждого элемента выполняется с учётом обычной семантики равенства. Поэтому побочные эффекты или дорогая реализация __eq__ также могут влиять на стоимость и поведение проверки вхождения.
В проекте был объект, представляющий страницы удалённого API. Разработчик реализовал только __getitem__, чтобы поддержать обращение к страницам по индексам, а затем использовал оператор in для поиска идентификатора записи.
Рассматривались два варианта. Сохранить только __getitem__ было проще, но это означало бы последовательные сетевые запросы и риск бесконечной проверки при ошибочном завершении индексации. Реализовать __contains__ отдельно сложнее, зато можно направить поиск в специализированный API-запрос.
Выбран второй вариант: __getitem__ оставили для постраничного доступа, а __contains__ реализовали через endpoint поиска. Это сделало операцию предсказуемой по стоимости и не смешало семантику перебора страниц с эффективной проверкой принадлежности.
Что произойдёт, если __getitem__ при переборе выбросит не IndexError, а другое исключение?
Такое исключение не считается признаком конца последовательности. Оно обычно распространяется наружу, поэтому проверка in завершается ошибкой. Для индексного протокола именно IndexError обозначает, что элементов больше нет.
Переключится ли Python на __getitem__, если существующий __iter__ выбросит TypeError?
Нет, автоматическое наличие резервного протокола не означает повторную попытку после любой ошибки. Если Python уже выбрал __iter__, ошибка его выполнения не превращает объект автоматически в индексную последовательность. Поэтому __iter__ должен либо корректно возвращать итератор, либо явно сообщать об ошибке контракта.
Почему явный __contains__ может быть важнее, чем просто удобство API?
Он позволяет задать отдельную стратегию поиска. Перебор может быть линейным, запускать побочные эффекты или загружать данные по одному элементу, тогда как __contains__ способен использовать индекс, хеш-таблицу, базу данных или удалённый запрос. Кроме того, явная реализация делает намерение класса очевидным и предотвращает случайное использование дорогого резервного протокола.