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

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

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

Паттерн «первый подходящий»: поиск в цикле Python

Три способа найти первый подходящий элемент: цикл с break, next с генератором и ветка «не нашлось» — с дефолтом вместо падения.

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

Половина поисковых задач — не «найди все», а «найди первый»: первый свободный слот на парковке, первый файл с нужным именем, первый незаблокированный пользователь. Если нужен только первый подходящий элемент, цикл обязан останавливаться на нём: перебирать дальше незачем. Разберём три способа: полный перебор и его расточительность, цикл с break и переменной found, функцию next с генератором — и что делать, когда ничего не нашлось.

Цена лишнего перебора

Прежде чем искать способы, стоит понять, сколько стоит лишний перебор. Проверка одного элемента — это сравнение плюс ветвление; на списке из миллиона элементов полный обход это миллион сравнений, а нужный ответ может лежать в первом десятке.

считаем, сколько успели проверить
nums = [3, 8, 15, 22, 31]

checked = 0
for n in nums:
    checked += 1
    if n > 10:
        print('нашли', n, 'за', checked, 'шага')
        break
Вывод
нашли 15 за 3 шага

Три проверки вместо пяти — на пяти элементах разница не видна. На настоящем файле вроде лога на сто тысяч строк она становится ощутимой: полный перебор читает файл до конца, а next с генератором останавливается на первой подходящей строке. Разница в скорости, а в случае с файлом — ещё и в том, что остальной файл вообще не читается.

Задача: первый элемент больше порога

полный перебор печатает всё
temps = [21, 18, 25, 19, 27, 16]

for t in temps:
    if t > 24:
        print('подошло:', t)
Вывод
подошло: 25
подошло: 27

Цикл честно отработал и нашёл оба подходящих значения — но задача была про одно. Двадцать семь проверялись уже после того, как двадцать пять закрыли вопрос: для задачи «первый» это потраченное впустую время, а на списке из миллиона элементов — миллион проверок вместо одной. Нужен способ остановиться.

Цикл с break: переменная found

остановиться на первом
temps = [21, 18, 25, 19, 27, 16]

found = None
for t in temps:
    if t > 24:
        found = t
        break

print(found)
Вывод
25

Переменная found стартует со значением None — «пока ничего не нашли». Как только условие выполнилось, в found ложится элемент и break рвёт цикл: двадцать семь в перебор не попали вовсе. Каркас из трёх частей — стартовое None, условие, break — узнаваем в любом проекте: это канонический поиск первого совпадения, break разбирали подробно в своём уроке.

а если не нашлось
temps = [21, 18, 23, 19]

found = None
for t in temps:
    if t > 24:
        found = t
        break

print(found)
print(found is None)
Вывод
None
True

Ни одно значение не перешагнуло порог: break не случился, и found осталась None. Сравнение found is None — канонический способ проверить, состоялся ли поиск. None здесь не случайность, а честный ответ: специальное значение «ничего», которое легко отличить от настоящих данных. А блок else у цикла из урока про поиск без флагов умеет оформлять ветку «не нашлось» и без переменной-результата.

next: то же самое в одну строку

next с генератором и дефолтом
temps = [21, 18, 25, 19, 27, 16]

found = next(t for t in temps if t > 24)
print(found)

cold = next((t for t in temps if t < 15), 'ни одной')
print(cold)
Вывод
25
ни одной

next принимает генератор — записанную в одну строку проверку «каждый t, который больше 24» — и возвращает первый подходящий элемент, останавливая перебор ровно как break. Второй вызов показывает главный навык: у next есть второй аргумент, значение по умолчанию. Холодных дней в списке нет, и вместо падения next вернул заготовку «ни одной». Обратите внимание на скобки: генератор с дефолтом берётся в свои круглые скобки — next((...), значение).

первый совершеннолетний в словаре
ages = {'Анна': 15, 'Борис': 21, 'Вера': 19}

adult = next((name for name, age in ages.items() if age >= 18), None)
print(adult)
Вывод
Борис

Генератор бегает не только по спискам: обход словаря по items() отдаёт пары «имя — возраст», распаковка разбирает каждую на две переменные, а проверка отбирает первого совершеннолетнего. Перебор идёт в порядке вставки, поэтому ответ Борис: он стоит в словаре раньше Веры, у которой тоже подходящий возраст.

первый файл нужного типа
files = ['отчёт.docx', 'смета.txt', 'договор.pdf', 'заметки.txt']

first_txt = next((f for f in files if f.endswith('.txt')), None)
print(first_txt)
Вывод
смета.txt

Метод endswith проверяет расширение, и next отдаёт первую смету — даже не глядя на файлы после неё. Условие внутри генератора может быть каким угодно сложным: вхождение подстроки, длина, принадлежность списку. Меняются данные и условие, каркас не трогается.

else у цикла: ветка не нашлось
temps = [21, 18, 23]

for t in temps:
    if t > 24:
        print('нашли:', t)
        break
else:
    print('ничего не нашли')
Вывод
ничего не нашли

Блок else у цикла выполняется, только если break не случился, — то есть когда перебор дошёл до конца и совпадений не нашлось. Три способа решают одну задачу: выбирайте по вкусу читателя вашего кода.

Найти не значение, а его номер

Часто нужен не сам элемент, а его позиция: какой по счёту оказался подходящий, чтобы потом взять соседний или вырезать из списка. Для этого к next добавляется enumerate — он даёт генератор пар «номер — элемент».

индекс первого кратного трём
nums = [4, 7, 11, 15]

idx = next((i for i, n in enumerate(nums) if n % 3 == 0), -1)
print('индекс:', idx)
print('значение:', nums[idx])
Вывод
индекс: 3
значение: 15

Пятнадцать стоит под индексом 3 — это первое число, кратное трём. Дефолт -1 выбран не случайно: минус единица заведомо не может быть настоящим индексом, поэтому по ней сразу видно, что совпадения не было. None тут был бы слабее: None в списке индекса не живёт, но проверить его пришлось бы отдельно.

Условие из нескольких правил

Условие внутри генератора не обязано быть простым. Когда правил два, их соединяют через and — и тогда поиск идёт по элементам, удовлетворяющим обоим сразу.

первый, кто проходит обе проверки
data = [('id1', 10), ('id2', 3), ('id3', 7)]

winner = next((i for i, n in data if n > 5), None)
print(winner)
Вывод
id1

id1 проходит: десять больше пяти. id2 отпадает по значению, id3 не подходит — но до него перебор не дошёл. Тот же генератор легко превращается в «найти все подходящие»: замените next на включение, и вы получите список. Разница между «первый» и «все» — один вызов.

первый и все — рядом
nums = [3, 8, 15, 22, 31]

print(next((n for n in nums if n > 10), None))
print([n for n in nums if n > 10])
Вывод
15
[15, 22, 31]

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

Когда next — лишний

У next есть встроенные соседи, и для части задач они справляются лучше. min и max умеют искать по ключу, а list.index — искать позицию. Знать про них полезно: иногда строка с next длиннее, чем встроенный метод.

min и max с ключом
pairs = [('a', 5), ('b', 2), ('c', 9)]

print(min(pairs, key=lambda p: p[1]))
print(max(pairs, key=lambda p: p[1]))
Вывод
('b', 2)
('c', 9)

min с ключом находит пару с минимальным вторым элементом — b с двойкой. Но у min есть важное отличие от next: он не останавливается на первом совпадении, а обходит всё и сравнивает. При равных значениях min вернёт первое, max — последнее, и в этом тоже разница: для «первого максимума» next честнее.

СпособОстанавливается на первомЕсли не нашлось
Цикл с break и foundДаЧто положите сами, обычно None
next с генераторомДаДефолт, а без него StopIteration
Цикл с elseДаВетка else выполняется
фрагмент: включение собирает всё
all_warm = [t for t in temps if t > 24]   # все совпадения: [25, 27]
first_warm = all_warm[0]                  # первый, но перебор уже прошёл целиком
Фрагмент: переменная temps объявлена выше. Включение собирает все совпадения, даже когда нужно одно — для одиночного поиска берите next.
Что выведет код?

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

nums = [7, 3, 8, 4]

first = None
for n in nums:
    if n % 2 == 0:
        first = n
        break
print(first)
words = ['кот', 'пёс', 'ёж']

found = next((w for w in words if len(w) == 2), 'нет')
print(found)
print(next((n for n in [1, 3, 5] if n > 10), -1))
Проверь себя
0 / 8

1. Зачем в поиске первого совпадения нужен break?

2. Что вернёт next с генератором, если подходящих элементов нет, но передан дефолт?

3. Зачем в каркасе поиска переменную found стартуют со значением None?

4. Почему next((...), значение) требует скобок вокруг генератора?

5. Чем next с генератором отличается от спискового включения с [0]?

6. Как найти индекс первого элемента, кратного трём?

7. Почему для индекса дефолтом выбирают -1, а не None?

8. Чем next отличается от max с ключом?

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

В списке emails = ['a@mail.ru', 'b@site.org', 'c@mail.ru'] найди первый адрес на домене site.org и выведи его; если такого нет — выведи строку нет. Используй next с дефолтом.

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

Как найти первый подходящий элемент в списке Python?

Три способа: цикл с break и переменной found, функция next с генератором next(x for x in xs if x > 0) и списковое включение, если нужны все совпадения. Для одиночного поиска next экономит перебор: он останавливается на первом.

Урок 6: break

Что произойдёт, если next не найдёт подходящий элемент?

Без дефолта — исключение StopIteration: вернуть нечего. С дефолтом next((...), значение) вернёт запасное значение. В поисковом коде дефолт задают всегда — это страховка от падения на пустых данных.

Чем None отличается от пустой строки в результате поиска?

None — явный знак «ничего не нашли», а пустая строка может быть настоящим значением: пустой комментарий, пустое имя. Поэтому результат поиска объявляют как None и проверяют found is None.

Как оформить ветку «не нашлось» в том же цикле?

Блоком else у цикла: он выполнится, только если break не случился, то есть совпадений не было вообще. Разбор приёма — в уроке про else у циклов.

Урок 9: else у циклов

Как найти индекс первого подходящего элемента списка?

Через enumerate внутри генератора: next((i for i, n in enumerate(nums) if n > 10), -1). enumerate даёт пары «номер — элемент», поэтому условие проверяет значение, а next возвращает номер. Дефолт -1 выбран как заведомо невозможный индекс.

Урок 4: enumerate

Почему next без дефолта — плохая идея в рабочем коде?

Потому что на пустых или изменившихся данных он бросит StopIteration, и программа упадёт вместо того, чтобы обработать отсутствие совпадения. Дефолт превращает «не нашлось» в обычное значение, с которым дальше работают: None, пустая строка или -1 в зависимости от типа задачи.

Когда полный перебор всё-таки лучше next?

Когда элементов немного и важнее читаемость: any(условие for ...) скажет «есть ли» одной строкой, а next с дефолтом выглядит сложнее. Оптимизировать стоит там, где данных много или цикл выполняется часто — лог-парсер, поток событий.

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

«Если нужен только первый подходящий элемент, цикл обязан останавливаться на нём: перебирать дальше незачем.»

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

TelegramVK

Что дальше

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

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

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