Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 5 из 20 Начальный 15 мин 100 XP

in в Python: мгновенная проверка принадлежности множеству

Один оператор, два механизма: in у множества отвечает мгновенно, у списка — перебором. Почему на миллионе элементов разница становится днями и секундами.

Редакция Питоники

Вопрос есть ли элемент внутри — самый частый вопрос к коллекции. Был ли гость в списке приглашённых, входит ли слово в стоп-лист, разрешено ли действие. Оператор in отвечает на все эти вопросы, но скорость ответа зависит от структуры под ним: у множества он мгновенный, у списка — перебор. В этом уроке — как проверять, почему множество ищет прыжком, и где эта разница превращается в часы работы программы.

Оператор in на множестве

in ставится между значением и множеством и отвечает булевым значением: True, если элемент есть, False, если нет:

проверка на стоп-листе
banned = {'spam', 'troll'}
user = 'troll'
print(user in banned)
print('anna' in banned)
print('guest' not in banned)
Вывод
True
False
True

Три вопроса — три честных ответа: тролль в стоп-листе, anna нет, а гостю вход разрешён. Оператор работает с любыми значениями множества — строками и числами одинаково уверенно, и ничего не меняет в самой коллекции.

not in: гарантия отсутствия

Обратный вопрос — нет ли элемента — задаётся оператором not in. Часто он читается естественнее двойного отрицания и удобен для условий:

not in на живом множестве
banned = {'spam', 'troll'}
user = 'guest'
print(user not in banned)
banned.add(user)
print(user not in banned)
Вывод
True
False

Гость был вне стоп-листа — True; после добавления — уже False. not in — это просто перевёрнутый in: оба оператора спрашивают у множества одно и то же, отличаются лишь ожидаемый ответ. Оператор работает и с числами — вопрос тот же: есть или нет:

проверка на числах
pages = {10, 20, 30}
print(20 in pages)
print(40 in pages)
print(20 not in pages)
Вывод
True
False
False

Двадцатая страница учтена, сороковой нет, а not in подтвердил отсутствие. Тип значения не важен: множество проверяет любые свои элементы одинаково мгновенно.

in у списка: ответ тот же

Оператор in работает и со списками — синтаксис тот же, ответы те же:

тот же in у списка
banned_list = ['spam', 'troll']
print('troll' in banned_list)
print('anna' in banned_list)
Вывод
True
False

Снаружи неотличимо: True и False там, где и ожидалось. Разница — внутри. Чтобы ответить про список, Python перебирает элементы по очереди, сравнивая каждый с искомым: нашёл на второй позиции — сделал два сравнения, не нашёл — прошёл весь список до конца. У множества механизм совсем другой.

Почему множество отвечает мгновенно

Множество устроено как хеш-таблица. При добавлении элемент Python вычисляет его хеш — число-отпечаток — и кладёт значение в полку по этому номеру. При проверке in достаточно вычислить хеш искомого и заглянуть в одну конкретную полку: перебирать всё множество не нужно. Поэтому проверка занимает одно и то же время — и на трёх элементах, и на миллионе. Список ищет перебором за O(n), множество — прыжком за O(1); большая О — это рост времени при росте данных: у перебора он линейный, у хеша — почти нулевой.

миллион элементов: оба отвечают True
big_set = set(range(1000000))
big_list = list(big_set)
print(999999 in big_set)
print(999999 in big_list)
print(len(big_set))
Вывод
True
True
1000000

Ответы одинаковы — True и True. Разница во времени: множество посчитало хеш числа и загляло в одну полку, список перебрал до миллиона значений. В единичной проверке разница невидима, но проверок в программе обычно много — и там перебор накапливается.

антипаттерн: поиск по списку внутри цикла
# all_users — 100 000 пользователей
# banned_list — 10 000 стоп-имен
for user in all_users:
    if user in banned_list:
        block(user)
В песочнице списках нет — на настоящих данных такой цикл делает до миллиарда сравнений. Тот же цикл по множеству banned_set делает ровно 100 000 мгновенных проверок.

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

Практика: фильтр разрешённых действий

Соберём приём в рабочую программу: роли с разрешениями — классическая задача, где принадлежность проверяется на каждом шаге:

фильтр по белому списку
allowed = {'читать', 'писать'}
actions = ['читать', 'удалять', 'писать', 'настраивать']
for action in actions:
    if action in allowed:
        print(action, '- разрешено')
    else:
        print(action, '- запрещено')
Вывод
читать - разрешено
удалять - запрещено
писать - разрешено
настраивать - запрещено

Белый список — множество, каждое действие проверяется в нём одним in. Направление проверки важно: спрашиваем у разрешений, а не у действий — маленькое множество проверяется быстрее большого списка действий, и код читается как вопрос: разрешено ли это?

проверка перед удалением
tags = {'python', 'code'}
target = 'web'
if target in tags:
    tags.remove(target)
    print(target, '- удалён')
else:
    print(target, '- нет такого')
print(sorted(tags))
Вывод
web - нет такого
['code', 'python']

Здесь in нужен не для скорости, а для сообщения: удаление тега web не состоялось, и программа знает почему. Такой же выбор делали в прошлом уроке между remove и discard — проверка in даёт версию с собственным сообщением.

СтруктураКак ищетСкорость
setПрыжком по хешу в одну полкуO(1): одинаково на любых объёмах
listПеребором по очередиO(n): растёт с длиной списка
dictПо ключу, тот же хешO(1): как у множества

Словарь ищет тем же механизмом, что и множество: хеш ключа и одна полка. Поэтому пара set и dict закрывает почти все задачи быстрого поиска, а списки оставляют перебору там, где порядок нужен сам по себе. Хеш-механизм у обоих общий — урок 17 показывает его изнутри. А если принадлежность проверяется часто, следующий шаг — операции над целыми множествами: объединение множеств собирает всех сразу без перебора.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

letters = {'a', 'b'}
print('b' in letters)
print('c' in letters)
print('c' not in letters)
s = set(range(3))
print(2 in s)
print(len(s))
print(3 in s)
allowed = {'читать'}
actions = ['писать', 'читать']
for a in actions:
    if a in allowed:
        print(a)
Проверь себя
0 / 6

1. Что возвращает выражение 'x' in s?

2. Почему проверка in у множества работает мгновенно?

3. Как список отвечает на проверку in?

4. Что вернёт 'x' in set() — проверка по пустому множеству?

5. Что означает запись user not in banned?

6. Сто тысяч пользователей проверяются по стоп-списку из десяти тысяч. Сколько сравнений в худшем случае у списка и у множества?

Карточки терминов
Запомнено: 0 / 7
Практика

Дано banned = {'spam', 'bot'} и users = ['anna', 'spam', 'max', 'bot']. Напечатайте для каждого пользователя строку имя - ok, если он не в стоп-листе, и имя - блок, если в нём.

practice.py
Вопросы и ответы по уроку

Как проверить, есть ли элемент в множестве Python?

Оператором in: 'x' in s возвращает True или False. Для проверки отсутствия используйте not in. Оператор работает за постоянное время независимо от размера множества — множество устроено как хеш-таблица.

Урок 4: удаление с проверкой

Почему in у set быстрее, чем у list?

Множество хранит элементы в полках по хешу: проверка вычисляет хеш искомого и заглядывает в одну полку — O(1). Список перебирает значения по очереди — O(n): чем длиннее список, тем дольше ответ. На единичных проверках разница невидима, на миллионах — огромна.

Что такое O(1) простыми словами?

Скорость, не зависящая от объёма данных: множество из трёх и из трёх миллионов элементов отвечают одинаково быстро. O(n) — противоположность: время растёт вместе с длиной. Большая О описывает рост времени, а не секунды конкретного запуска.

Как проверить, что элемента нет в множестве?

Оператором not in: user not in banned вернёт True, если пользователя в стоп-листе нет. Это то же самое, что not (user in banned), но читается лучше.

Когда стоит превратить список в множество?

Когда по нему делается много проверок in. Одно превращение set(список) стоит один раз, а каждая последующая проверка становится мгновенной. Классический случай — стоп-лист или белый список, по которому проверяется поток значений.

Урок 2: создание множеств

Понравился урок? Сошлитесь на него

«При проверке in достаточно вычислить хеш искомого и заглянуть в одну конкретную полку: перебирать всё множество не нужно.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.