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. Часто он читается естественнее двойного отрицания и удобен для условий:
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 работает и со списками — синтаксис тот же, ответы те же:
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); большая О — это рост времени при росте данных: у перебора он линейный, у хеша — почти нулевой.
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)
Сто тысяч пользователей, каждый проверяется по списку из десяти тысяч — до миллиарда сравнений в худшем случае. С множеством тот же код делает сто тысяч мгновенных проверок: разница — между минутами и мгновением.
Практика: фильтр разрешённых действий
Соберём приём в рабочую программу: роли с разрешениями — классическая задача, где принадлежность проверяется на каждом шаге:
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)
1. Что возвращает выражение 'x' in s?
2. Почему проверка in у множества работает мгновенно?
3. Как список отвечает на проверку in?
4. Что вернёт 'x' in set() — проверка по пустому множеству?
5. Что означает запись user not in banned?
6. Сто тысяч пользователей проверяются по стоп-списку из десяти тысяч. Сколько сравнений в худшем случае у списка и у множества?
Дано banned = {'spam', 'bot'} и users = ['anna', 'spam', 'max', 'bot']. Напечатайте для каждого пользователя строку имя - ok, если он не в стоп-листе, и имя - блок, если в нём.
Как проверить, есть ли элемент в множестве 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Множества · Урок 6
Объединение множеств в Python: оператор | и union
Гости субботы плюс гости воскресенья — но каждый посчитан один раз. Оператор | и метод union собирают два множества в одно без дубликатов.
Множества · Урок 4
remove, discard и pop: удаление элементов множества
Три способа убрать элемент: remove падает на отсутствующем, discard молчит, pop достаёт произвольный. Один и тот же набор, три разные реакции на пустоту.
Множества · Урок 17
Множества и словари Python: hash и общие механизмы
Множество и словарь построены на одном механизме: hash назначает каждому элементу полку, и проверка in становится мгновенной. Ключи словаря — готовое множество, над которым работают & и |.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Словари · Урок 7
in: проверка ключа в словаре
Оператор in отвечает на вопрос «есть ли такой ключ»: проверка перед доступом вместо KeyError, not in, грабли с проверкой значений и причина скорости поиска.
проверка ключа перед доступомскорость поиска в словаре python
Циклы · Урок 9
else у циклов в Python: поиск без флагов
У циклов for и while есть собственный else: он срабатывает, если перебор дошёл до конца без break. Поиск «нашли или нет» — в один заход и без флагов.
поиск в списке pythonпоиск элемента в цикле
Множества · Урок 9
Подмножества в Python: issubset и issuperset
Один комплект внутри другого: issubset и issuperset возвращают True или False одной строкой. Строгие знаки < и >, пустое множество, списки вместо множеств и направления проверки.
python проверить множествомножества python проверка