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

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

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

Ошибки в цикле Python: try внутри for и продолжение после сбоя

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

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

Настоящие данные почти всегда приходят с мусором: в файле цен затесалось слово, в таблице пустая ячейка, в выгрузке число с пробелом посреди. Урок 2 показал try и except на одной операции — здесь та же пара встаёт на самое рабочее место программы: внутрь цикла. Задача урока — превратить цикл, который падает на первом же мусоре, в цикл, который переживает сбой, считает его и идёт дальше.

Сбой посреди цикла

Сначала посмотрим на проблему в чистом виде. Цикл разбирает список цен, и одна из четырёх строк — не число:

цикл без защиты
# Так не запускается: на третьем элементе int упадёт
prices = ['100', '200', 'ноль', '400']

for price in prices:
    value = int(price)
    print('цена:', value)
Первые две цены напечатаются, а на третьей программа упадёт: ValueError: invalid literal for int() with base 10: 'ноль'. Строка 400 так и не будет обработана — сбой посередине роняет весь проход.

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

try внутри for: сбой одного элемента

Решение — перенести try внутрь тела цикла и сузить до одной операции. Каждый шаг цикла получает собственный запасной план:

try вокруг одного элемента
prices = ['100', '200', 'ноль', '400']

for price in prices:
    try:
        value = int(price)
        print('цена:', value)
    except ValueError:
        print('пропущено:', price)

print('цикл закончился')
Вывод
цена: 100
цена: 200
пропущено: ноль
цена: 400
цикл закончился

Это главный шаблон урока: одна итерация — одна попытка. try стоит внутри тела, поэтому падает не цикл, а только разбор текущего элемента; except его обрабатывает, и цикл автоматически продолжается со следующего. Отдельный continue не нужен: тело и так подошло к концу. А break здесь не появился бы и с ним — обрывать проход из-за одной строки ни к чему.

Ловим не всё, а конкретное

Внутри цикла соблазн widest-перехвата выше, чем где-либо: тысяча итераций, тысяча шансов на неожиданное. Но именно в цикле широкая сетка из прошлого урока опаснее всего — она спрячет и опечатку в теле, промахнувшуюся мимо данных. Правило то же: ловить конкретный тип, а разные риски разбирать разными except-блоками из урока 4:

два риска - два обработчика
rows = [
    {'name': 'Анна', 'age': '30'},
    {'age': '41'},
    {'name': 'Вера', 'age': 'abc'},
]

for row in rows:
    try:
        print(row['name'], '-', int(row['age']))
    except KeyError as e:
        print('нет ключа:', e)
    except ValueError:
        print('возраст не число')
Вывод
Анна - 30
нет ключа: 'name'
возраст не число

У каждой строки — свой исход: у второй нет ключа, у третьей мусор вместо возраста. Цепочка except разбирает оба случая по именам, и цикл ни разу не споткнулся. Заметьте: сообщение KeyError печатается через переменную e из урока 5 — оно само и есть имя потерянного ключа, взятое в кавычки.

Счётчики: успешные и сбои

Печать по ходу удобна в моменте, но отчёту нужны числа: сколько строк разобралось, сколько поломалось. Это работа для счётчиков-аккумуляторов из урока 11 циклов — по одному на исход:

два счётчика на два исхода
values = ['10', 'x', '20', '', '30']
ok = 0
bad = 0

for text in values:
    try:
        int(text)
        ok += 1
    except ValueError:
        bad += 1

print('разобрано:', ok)
print('сбоев:', bad)
Вывод
разобрано: 3
сбоев: 2

Пустая строка тоже не разобралась — int('') падает с тем же ValueError, отдельной проверки она не потребовала. Доля сбоев — это диагноз данных: две из пяти — терпимо и обрабатываем, пятьдесят из ста — источник сломан, о чём чуть ниже. Важно, где стоят счётчики: ok увеличивается внутри try только после успешно выполненной операции — порядок строк решает, честен ли счёт.

else: успех как отдельная ветка

else из урока 6 внутри цикла разделяет попытку и результат: try — только рискованная строка, else — то, что делается с добытым значением:

else после except в цикле
values = ['10', 'x', '30']

for text in values:
    try:
        number = int(text)
    except ValueError:
        print('сбой:', text)
    else:
        print('квадрат:', number * number)
Вывод
квадрат: 100
сбой: x
квадрат: 900

Журнал сбоев с номерами

Пропущено в моменте — хорошо, а какой именно элемент пропущен — знает только тот, кто стоял у экрана. Отчёт собирает это сам: номер, значение и сообщение ошибки. Нумерацию даёт enumerate со стартом с единицы, сообщение — переменная из except ... as e:

журнал собирается, отчёт печатается после
values = ['10', 'x', '20', '3.5']
errors = []

for n, text in enumerate(values, 1):
    try:
        int(text)
    except ValueError as e:
        errors.append(f'строка {n}: {text!r} - {e}')

for line in errors:
    print(line)
Вывод
строка 2: 'x' - invalid literal for int() with base 10: 'x'
строка 4: '3.5' - invalid literal for int() with base 10: '3.5'

Спецификация {text!r} печатает значение в кавычках — так видно и пустую строку, и строку с пробелами, которые в обычной печати растворились бы. Журнал здесь собран списком строк, и в финальном проекте раздела этот приём дорастёт до настоящего отчёта с первыми тремя ошибками и сверкой.

Когда переставать

Продолжать после сбоя — правило по умолчанию, но не догма. Если сбоев половина, источник данных сломан целиком, и молотить его дальше — тратить время и плодить журнал. Для этого случая у цикла есть порог: счётчик сбоев дотянул до лимита — break из урока про while останавливает проход:

порог сбоев и остановка
values = ['10', '20', 'ошибка', 'ошибка', '30']
ok = 0
bad = 0

for text in values:
    try:
        int(text)
        ok += 1
    except ValueError:
        bad += 1
        if bad >= 2:
            print('слишком много сбоев, останавливаюсь')
            break

print(f'разобрано: {ok}, сбоев: {bad}')
Вывод
слишком много сбоев, останавливаюсь
разобрано: 2, сбоев: 2

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

Что случилось с элементомЧто делает цикл
Разобрался штатноОбрабатывает и идёт дальше
Не разобрался — редкий мусорexcept: логирует и продолжает
Сбоев стало слишком многоПорог + break: быстрый отказ
Ошибка вне данных — опечатка в телеНе ловить: пусть падает и показывает строку
так pitfall выглядит в коде
# try обнимает всё тело: какая строка упала - неизвестно
for row in rows:
    try:
        name = row['name']
        age = int(row['age'])
        print(name, age)
    except Exception:
        pass
Цикл переживает любой сбой — и грязные данные, и собственные опечатки. Кто из них встретился на строке 742, не скажет никто: pass не оставляет следов. Правильная ширина try — одна рискованная операция, int(row['age']), с конкретным except ValueError.
фрагмент: тот же журнал через logging
import logging

logging.warning('строка %s: не разобралось - %s', n, e)
В настоящих программах вместо print используют модуль logging: формат тот же — номер строки и сообщение ошибки. Схема из урока не меняется, меняется канал: журнал собирается не в консоль, а в лог.

Комплект собран: try вокруг одного элемента, конкретный тип, счётчики двух исходов, журнал с номерами, порог для остановки. Следующий урок отвечает на вопрос, который встаёт, когда сбои нужно не гасить, а перевыпускать: raise from — цепочка причин, где новая ошибка несёт с собой старую.

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

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

values = ['4', '', '6']
ok = 0
for text in values:
    try:
        int(text)
        ok += 1
    except ValueError:
        pass
print(ok)
for text in ['a', '3']:
    try:
        n = int(text)
    except ValueError:
        print('сбой:', text)
    else:
        print('число:', n)
print('конец')
ok = 0
bad = 0
for text in ['1', 'x']:
    ok += 1
    try:
        int(text)
    except ValueError:
        bad += 1
print(ok, bad)
Проверь себя
0 / 7

1. Куда поставить try, чтобы сбой одного элемента не ронял цикл?

2. Какие счётчики заводят для цикла с разбором данных?

3. Что даёт enumerate(values, 1) в цикле с журналом сбоев?

4. Когда в цикле с обработкой ошибок уместен break?

5. Почему в цикле не советуют except Exception вместо конкретного типа?

6. Нужен ли continue после except внутри for?

7. Что напечатает {text!r} для строки с пробелами по краям?

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

Дан список readings с показаниями датчиков: среди чисел затесались пустая строка и слово сбой. Пройдите циклом с try внутри: каждое значение разберите int-ом, успешные складывайте в numbers, сбои считайте счётчиком bad. В конце напечатайте список чисел и строку сбоев: N.

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

Как обработать ошибку в цикле Python и не остановить цикл?

Поставить try внутрь тела цикла вокруг операции с одним элементом: for text in values: try: int(text) except ValueError: ... Сбой обрабатывается в except, и цикл сам продолжается со следующего элемента — continue не нужен.

Урок 2: try и except

Как пропустить битые строки и посчитать их?

Завести счётчик bad с нуля до цикла и увеличивать его в except, а успешные значения складывать в отдельный список. После цикла счётчики покажут состав данных: сколько разобралось, сколько поломалось.

Урок 11 циклов: счётчики и аккумуляторы

Как узнать, на какой строке списка произошёл сбой?

Идти циклом с enumerate(values, 1) — он отдаёт пары номер-значение с нумерацией от единицы. В except номер и значение дописываются в журнал: errors.append(f'строка {n}: {text}') — и отчёт показывает все места сбоев сразу.

Когда цикл с обработкой ошибок стоит остановить принудительно?

Когда сбоев слишком много: если половина элементов не разбирается, источник сломан целиком и продолжение только тратит время. Заводят порог — if bad >= LIMIT: break — и проход останавливается на первом превышении.

Урок 5: цикл while и break

Что лучше в цикле: except Exception или конкретный тип?

Конкретный тип: в теле цикла рядом с данными живёт и код, и широкая сетка спрячет опечатку наравне с ожидаемым мусором. Разные риски одной строки — разные except-блоки: KeyError отдельно, ValueError отдельно.

Урок 15: except Exception

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

«Считать успехом можно только завершённую работу — инкремент ставится после рискованной операции или в else-ветке.»

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

TelegramVK

Что дальше

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

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

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