← Назад к вопросам

Зачем нужен __contains__ в итераторе Python?

1.7 Middle🔥 141 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Метод contains в Python

__contains__ — это магический метод который определяет поведение оператора in. Он используется для проверки принадлежности элемента к контейнеру.

Базовое определение

# __contains__ это то что вызывается при использовании 'in'
class MyContainer:
    def __init__(self, items):
        self.items = items
    
    def __contains__(self, item):
        return item in self.items

container = MyContainer([1, 2, 3, 4, 5])

print(3 in container)   # Вызовет container.__contains__(3) → True
print(10 in container)  # Вызовет container.__contains__(10) → False
print(5 not in container)  # False

Как Python выбирает реализацию

Когда пишешь x in container, Python выполняет следующее:

  1. Если есть __contains__ — вызовет его (самый приоритетный)
  2. Если нет __contains__ — попробует использовать __iter__ (итеративный поиск)
  3. Если нет обоих — попробует __getitem__ с индексами (0, 1, 2...)
  4. Если ничего нет — ошибка TypeError
# Пример 1: с __contains__
class FastLookup:
    def __init__(self, items):
        self.items = set(items)  # O(1) поиск
    
    def __contains__(self, item):
        return item in self.items  # Быстро!

fast = FastLookup([1, 2, 3])
print(3 in fast)  # O(1) время

# Пример 2: без __contains__, но есть __iter__
class SlowLookup:
    def __init__(self, items):
        self.items = items
    
    def __iter__(self):
        return iter(self.items)

slow = SlowLookup([1, 2, 3])
print(3 in slow)  # Python итерирует и проверяет каждый элемент O(n)

Производительность: почему это важно

import time

data = list(range(1_000_000))

# Без оптимизации: проверка каждого элемента O(n)
class UnoptimizedContainer:
    def __init__(self, items):
        self.items = items

unopt = UnoptimizedContainer(data)

start = time.perf_counter()
for _ in range(100):
    999_999 in unopt.items  # Проверяет 1M элементов каждый раз
long_time = time.perf_counter() - start

# С оптимизацией: O(1) lookup
class OptimizedContainer:
    def __init__(self, items):
        self.items = set(items)
    
    def __contains__(self, item):
        return item in self.items

opt = OptimizedContainer(data)

start = time.perf_counter()
for _ in range(100):
    999_999 in opt  # Вызовет __contains__ → O(1)
fast_time = time.perf_counter() - start

print(f"Без оптимизации: {long_time:.3f}s")
print(f"С оптимизацией: {fast_time:.3f}s")
print(f"Быстрее в {long_time / fast_time:.0f}x раз!")

Результат: 50-500x ускорение просто добавив __contains__!

Практические примеры

1. Кастомный список с быстрым поиском

class FastList:
    def __init__(self, items=None):
        self.items = list(items) if items else []
        self._set = set(items) if items else set()
    
    def append(self, item):
        self.items.append(item)
        self._set.add(item)
    
    def __contains__(self, item):
        # Используем множество для O(1) поиска
        return item in self._set
    
    def __iter__(self):
        return iter(self.items)  # Порядок сохранён!
    
    def __len__(self):
        return len(self.items)

fast_list = FastList([1, 2, 3, 4, 5])
fast_list.append(6)

print(3 in fast_list)   # O(1) благодаря __contains__
print(list(fast_list))  # [1, 2, 3, 4, 5, 6] порядок есть

2. Диапазон чисел

class Range:
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __contains__(self, item):
        # Не хранит все числа в памяти! O(1) проверка
        return self.start <= item < self.end
    
    def __iter__(self):
        return iter(range(self.start, self.end))

my_range = Range(0, 1_000_000_000)  # Миллиард чисел!

print(500 in my_range)      # O(1) и работает мгновенно
print(2_000_000 in my_range)  # O(1)
# Не может итерировать мгновенно (список бы занял Гб памяти)

3. Граф для проверки наличия вершины

class Graph:
    def __init__(self):
        self.vertices = set()
        self.edges = {}
    
    def add_vertex(self, v):
        self.vertices.add(v)
        self.edges[v] = set()
    
    def add_edge(self, v1, v2):
        self.edges[v1].add(v2)
        self.edges[v2].add(v1)
    
    def __contains__(self, vertex):
        # O(1) проверка наличия вершины
        return vertex in self.vertices
    
    def __iter__(self):
        return iter(self.vertices)

graph = Graph()
graph.add_vertex('A')
graph.add_vertex('B')
graph.add_edge('A', 'B')

print('A' in graph)  # True (быстро)
print('C' in graph)  # False (быстро)

4. Объект с разрешенными значениями

class HTTPStatus:
    ALLOWED_CODES = {200, 201, 204, 301, 302, 400, 401, 403, 404, 500, 502, 503}
    
    def __contains__(self, code):
        return code in self.ALLOWED_CODES

http = HTTPStatus()

if 200 in http:
    print("Valid status code")  # O(1) проверка

if 999 in http:
    print("Invalid")
else:
    print("Code 999 not allowed")

5. Доступ к атрибутам класса

class Person:
    attributes = {'name', 'age', 'email', 'phone'}
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __contains__(self, attr):
        return attr in self.attributes

person = Person("Alice", 30)

if 'name' in person:
    print("Name is allowed attribute")  # O(1)

if 'secret' in person:
    print("Secret is allowed")
else:
    print("Secret attribute not allowed")  # O(1)

Разница: contains vs getitem

class WithGetItem:
    def __init__(self, items):
        self.items = items
    
    def __getitem__(self, index):
        return self.items[index]

# Без __contains__! Python будет перебирать индексы
obj = WithGetItem([1, 2, 3, 4, 5])
print(3 in obj)  # Работает, но O(n): iterates через __getitem__

class WithContains:
    def __init__(self, items):
        self.items = items
    
    def __contains__(self, item):
        # Более эффективная реализация
        return item in self.items

obj2 = WithContains([1, 2, 3, 4, 5])
print(3 in obj2)  # O(n), но явно написано

Когда добавлять contains

ДОБАВЛЯЙ если:

  • ✓ Часто проверяешь наличие элемента (in)
  • ✓ Есть более эффективный способ чем линейный поиск
  • ✓ Можешь кешировать данные (множество, индекс)
  • ✓ Логика поиска сложная (диапазоны, условия)

НЕ ДОБАВЛЯЙ если:

  • ❌ Уже есть __iter__ и линейный поиск OK
  • ❌ Контейнер очень маленький (< 10 элементов)
  • ❌ Проверка используется редко

Вывод

__contains__ позволяет оптимизировать проверку принадлежности элемента. Это особенно важно для больших контейнеров, где можно использовать структуры данных (set, dict, sorted list) для O(1) или O(log n) поиска вместо O(n).

Зачем нужен __contains__ в итераторе Python? | PrepBro