Ошибки и отладка
Как узнавать частые ошибки Python по сообщению, читать traceback из нескольких блоков, ловить молчаливые логические баги через print, assert и отладчик VS Code и задавать вопросы так, чтобы на них отвечали.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 0.0.3 вы завели привычку: последняя строка → номер строки → сама строка → гипотеза. Тогда программы были в три строки. Теперь у вас есть функции, файлы и классы, и ошибки стали глубже: сообщение может занимать 30 строк, а причина — прятаться совсем не там, где упало.
А самые опасные ошибки в ML вообще ничего не сообщают. Андрей Карпатый в эссе «A Recipe for Training Neural Networks» формулирует это так: обучение нейросети ломается молча. Перепутали метки, забыли перемешать данные, посчитали loss не по тем токенам — никакого исключения не будет. Модель просто обучится немного хуже, и вы потратите дни, прежде чем заметите. Поэтому отладкаотладкаEN debuggingПоиск и исправление ошибок в программе. Саму ошибку в программе и русские, и китайские разработчики обычно называют просто «баг» (bug). — не досадная помеха, а основная работа инженера: найти, почему программа делает не то, что вы задумали.
В этом уроке — каталог частых ошибок, чтение длинных traceback, три способа ловить молчаливые баги (print, assert,
деление пополам), отладчик VS Code и pdb, метод резиновой уточки, минимальный пример для вопроса и запись бага
в тетрадь ошибок. В конце — охота на баги в шести коротких программах.
Три вида ошибок
Заголовок раздела «Три вида ошибок»Напомним классификацию из урока 0.0.3 — теперь с точным моментом, когда каждая ошибка проявляется.
| Вид | Когда обнаруживается | Что видно | Пример |
|---|---|---|---|
| Синтаксическаясинтаксическая ошибкаEN syntax errorPython не понял запись (скобка, кавычка, двоеточие, отступ). Программа не выполняет ни одной строки. | до запуска: Python читает весь файл и не понимает запись | SyntaxError без слова Traceback; не выполняется ни одна строка | забыта скобка |
| Ошибка выполненияошибка выполненияEN runtime errorЗапись верна, но действие невыполнимо (деление на ноль, нет ключа). Программа падает на этой строке с traceback. | когда выполнение дошло до плохой строки | Traceback ... и тип ошибки; строки выше успели выполниться | деление на ноль |
| Логическаялогическая ошибкаEN logic errorПрограмма работает без сообщений об ошибках, но результат неверный. Ловится только проверками и тестами. | никогда — сама | ничего: программа отработала, ответ неверный | = вместо += |
Проверьте первую строку таблицы сами. В файле из трёх строк print("старт"), x = 1, print("конец" слово «старт»
не напечатается: незакрытая скобка в третьей строке не даёт запуститься всему файлу.
Каталог: ошибки, которые вы точно увидите
Заголовок раздела «Каталог: ошибки, которые вы точно увидите»Сообщения ниже — из Python 3.11. В новых версиях формулировки бывают чуть подробнее, но тип ошибки тот же.
| Ошибка | Код | Сообщение | Что проверить |
|---|---|---|---|
SyntaxError | print("Привет" | '(' was never closed | скобки, кавычки, двоеточия; если строка в порядке — строку выше |
IndentationError | if True: и под ним print(1) без отступа | expected an indented block after 'if' statement on line 1 | после строки с : нужен отступ в 4 пробела |
NameError | print(y) | name 'y' is not defined | опечатка (читайте подсказку Did you mean: ...?); переменная создаётся ниже; в Jupyter не выполнена ячейка |
TypeError | "шаг " + 5 | can only concatenate str (not "int") to str | типы: строка вместо числа, None вместо списка |
ValueError | int("3.5") | invalid literal for int() with base 10: '3.5' | тип верный, значение не подходит |
IndexError | [1, 2, 3][3] | list index out of range | индексы с нуля; последний — len - 1 или -1 |
KeyError | {"кот": 1}["пёс"] | 'пёс' | ключа нет: .get(ключ, по_умолчанию) или проверка in |
ZeroDivisionError | sum([]) / len([]) | division by zero | пустой список, нулевой знаменатель |
AttributeError | x = [3, 1].sort(), потом x.append(4) | 'NoneType' object has no attribute 'append' | откуда в переменной None |
ModuleNotFoundError | import numpi | No module named 'numpi' | опечатка; не активировано окружение (урок 0.1.2) |
Ещё две вы встречали раньше: FileNotFoundError — нет файла по такому пути (урок 0.2.4) и RecursionError — рекурсия
без базы (урок 0.2.2). А если программа не падает, но и не заканчивается, скорее всего, это
бесконечный циклбесконечный циклEN infinite loopЦикл, условие выхода из которого никогда не выполняется. Остановить программу — Ctrl+C.. Нажмите Ctrl+C (в Jupyter — кнопку ■ «Interrupt»): Python
остановится с KeyboardInterrupt, и traceback покажет, на какой строке он крутился.
Главное в сообщении — текст после двоеточия, а не только тип. TypeError бывает очень разным:
# Запускайте каждый пример отдельно: первая же ошибка останавливает программу"шаг " + 5 # TypeError: can only concatenate str (not "int") to strlen(5) # TypeError: object of type 'int' has no len()x = Nonex[0] # TypeError: 'NoneType' object is not subscriptabledef f(a, b): return a + bf(1) # TypeError: f() missing 1 required positional argument: 'b'Traceback из нескольких блоков
Заголовок раздела «Traceback из нескольких блоков»Когда функции вызывают друг друга, traceback показывает всю цепочку. Вот маленький «токенизатор»:
def load_vocab(): return {"<unk>": 0, "кот": 1, "сел": 2, "на": 3}
def encode(words, vocab): ids = [] for w in words: ids.append(vocab[w]) return ids
def main(): vocab = load_vocab() ids = encode("кот сел на мат".split(), vocab) print(ids)
main()Traceback (most recent call last): File "/home/user/tok.py", line 18, in <module> main() File "/home/user/tok.py", line 14, in main ids = encode("кот сел на мат".split(), vocab) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/user/tok.py", line 8, in encode ids.append(vocab[w]) ~~~~~^^^KeyError: 'мат'Заголовок most recent call last значит «последний вызов — внизу». Каждый блок — одна функция, которая на момент
ошибки ещё не закончила работу. Вместе они образуют стек вызововстек вызововEN call stackЦепочка функций, которые вызвали друг друга и ещё не завершились. Traceback — это снимок стека в момент ошибки.:
flowchart TB A["основной код (module), строка 18: main()"] --> B["main, строка 14: encode(...)"] B --> C["encode, строка 8: vocab[w]<br/>💥 KeyError: 'мат'"]
Читаем снизу: KeyError: 'мат' — в словаре нет слова «мат». Упало в encode на строке 8. Но где исправлять —
отдельный вопрос, и ответ зависит от того, что считать правильным поведением. Либо словарь неполный (чинить
load_vocab), либо неизвестные слова должны превращаться в <unk>, как в практике урока 0.2.1: тогда в encode
нужно vocab.get(w, vocab["<unk>"]). Traceback показывает место, где проблема проявилась, а решение принимаете вы.
Когда падает внутри библиотеки
Заголовок раздела «Когда падает внутри библиотеки»Самая частая ошибка в PyTorch выглядит так (пути сокращены, номера строк зависят от версии):
import torchimport torch.nn as nn
layer = nn.Linear(4, 5) # слой ждёт 4 признака на входеx = torch.randn(2, 3) # а мы подаём батч из 2 примеров по 3 признакаy = layer(x)Traceback (most recent call last): File "/home/user/model.py", line 6, in <module> y = layer(x) ^^^^^^^^ File ".../site-packages/torch/nn/modules/module.py", line 1783, in _wrapped_call_impl return self._call_impl(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../site-packages/torch/nn/modules/module.py", line 1794, in _call_impl return forward_call(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File ".../site-packages/torch/nn/modules/linear.py", line 134, in forward return F.linear(input, self.weight, self.bias) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x3 and 4x5)Три нижних блока — код PyTorch, и ошибки там нет: библиотека честно сообщает, что матрицы 2×3 и 4×5 перемножить
нельзя (почему — в уроке о матрицах 1.1). Правило: найдите самый нижний блок со своим файлом — здесь model.py,
строка 6. Исправлять почти всегда там. В реальном обучении таких библиотечных блоков бывает 20–40, и это правило
экономит больше всего времени.
Иногда traceback состоит из двух частей, разделённых фразой The above exception was the direct cause of the following exception (так выглядит raise ... from e из урока 0.2.4) или During handling of the above exception, another exception occurred (ошибка случилась внутри except). Читайте обе части: настоящая причина обычно в первой.
Логические ошибки: Python молчит
Заголовок раздела «Логические ошибки: Python молчит»Функция должна посчитать, сколько всего слов в корпусе:
def count_tokens(texts): total = 0 for text in texts: words = text.split() total = len(words) return total
corpus = ["я люблю python", "python любит меня", "и я"]print(count_tokens(corpus)) # → 2, а в корпусе 3 + 3 + 2 = 8 словНи одной ошибки, и ответ неверный. План действий:
-
Воспроизвести на маленьком входе, где правильный ответ известен заранее. Здесь — 8.
-
Сформулировать гипотезу: «
totalне накапливается» или «splitрежет не так». -
Проверить гипотезу одним из способов ниже — print,
assert, отладчик. -
Исправить одну вещь и запустить снова. Две правки сразу — и вы не узнаете, какая помогла.
print-отладка
Заголовок раздела «print-отладка»Самый простой и самый популярный способ — напечатать промежуточные значения. Удобнее всего f-строка со знаком =
из урока 0.2.1: она печатает и имя, и значение.
for text in texts: words = text.split() total = len(words) print(f"{text=} {len(words)=} {total=}") # временная строка для отладкиtext='я люблю python' len(words)=3 total=3text='python любит меня' len(words)=3 total=3text='и я' len(words)=2 total=2Видно сразу: total не растёт, а перезаписывается длиной текущего текста. Исправление — total += len(words).
Три приёма, которые делают print-отладку сильнее:
reprпоказывает невидимое.print(w)для"кот "и"кот"выглядит одинаково, аprint(repr(w))илиf"{w!r}"покажет'кот 'с пробелом. Так же видны переводы строк:'42\n'. Сравнение"кот " == "кот"даётFalse, и безreprэту ошибку можно искать час.type(x)— когда подозреваете, что вместо числа пришла строка.- Метка. Начинайте отладочные строки с
DEBUG, чтобы потом найти и удалить их все.
Делим пополам
Заголовок раздела «Делим пополам»Программа длинная, данные проходят через пять шагов: чтение → очистка → токенизация → подсчёт → вывод. Где-то по дороге они портятся. Не нужно проверять каждую строку: поставьте проверку посередине, после токенизации. Данные там правильные — баг дальше; неправильные — раньше. Каждая проверка отбрасывает половину кода: 1000 строк → 500 → 250 → 125 → … → 1. Поэтому в программе из 1000 строк место бага находится примерно за 10 проверок (ведь , урок 0.0.2). В математике такое число делений пополам записывают как — логарифмы ждут вас в уроке 1.0.2.
Тот же принцип работает во времени. «Вчера работало, сегодня нет» — Git умеет искать сломавший коммит делением
пополам: команда git bisect среди 100 коммитов найдёт виновника за 7 шагов (Git — урок
0.1.1, пошаговый git bisect run на чужом репозитории — урок
11.1.2). И та же идея — двоичный поискдвоичный поискEN binary searchПоиск делением диапазона пополам. Среди 100 вариантов находит ответ не больше чем за 7 шагов, потому что 2^7 = 128 ≥ 100. —
станет стратегией игры «угадай число» в следующем уроке.
assert: проверки, которые кричат
Заголовок раздела «assert: проверки, которые кричат»assertутверждение (assert)EN assertionassert условие, "сообщение" — проверка, которая падает с AssertionError, если условие ложно. Отключается флагом python -O. вы видели в тестах (урок 0.1.3) и в датаклассе (урок 0.2.3). Это проверка
«здесь обязано быть верно»: если условие ложно, программа сразу падает с AssertionError и вашим сообщением.
probs = [0.2, 0.5, 0.2]assert abs(sum(probs) - 1) < 1e-6, f"сумма вероятностей {sum(probs):.3f}, а должна быть 1"AssertionError: сумма вероятностей 0.900, а должна быть 1Зачем специально падать? Затем, что ошибка у источника в сто раз дешевле ошибки, найденной через три часа обучения.
В ML-коде assert ставят на «инварианты» — то, что должно быть верно всегда: число входов совпадает с числом меток,
вероятности суммируются в 1, в loss нет nan, у тензора нужная формаформа массиваEN shapeКортеж размеров по осям, например (batch, seq_len, d_model).. Китайский учебник
Ляо Сюэфэна прямо советует: где вы печатаете значение, чтобы на него посмотреть, можно поставить assert, и он будет
проверять за вас каждый запуск.
Две ловушки:
- Скобки.
assert (x > 0, "x должен быть положительным")— это проверка кортежа, а непустой кортеж всегда истинен. Python 3.11 предупредит:SyntaxWarning: assertion is always true, perhaps remove parentheses?— но программа продолжит работать, и проверка не сработает никогда. Пишите без скобок. python -Oвыключает всеassert. Поэтому проверять данные от пользователя или из файла нужно черезraise ValueError(...)(урок 0.2.4), аassertоставить для проверок вашей собственной логики.
Отладчик VS Code
Заголовок раздела «Отладчик VS Code»print нужно дописывать в код и перезапускать программу. ОтладчикотладчикEN debuggerПрограмма, которая останавливает ваш код в нужном месте и показывает значения всех переменных. В Python — pdb, в VS Code — встроенный. позволяет поставить программу на паузу в любом месте, увидеть все переменные сразу и выполнять код по одной строке — ничего не меняя в файле.
-
Откройте файл с
count_tokensи щёлкните слева от номера строкиtotal = len(words). Появится красная точка — точка остановаточка остановаEN breakpointСтрока, перед выполнением которой отладчик ставит программу на паузу. В коде — функция breakpoint(). (то же делает клавиша F9). -
Нажмите F5 (Run → Start Debugging). В первый раз VS Code спросит отладчик — выберите Python Debugger, затем Python File (текущий файл). Расширение Python Debugger ставится вместе с расширением Python из урока 0.1.2.
-
Программа остановится перед выполнением отмеченной строки, строка подсветится. Слева, в панели Variables, — все переменные:
text,words,total. -
Нажмите F10: строка выполнится, и вы увидите, как изменился
total. F5 — продолжить до следующей остановки, то есть до следующего прохода цикла. -
В панели Watch можно добавить любое выражение, например
len(words), а в Debug Console — вычислить что угодно прямо на паузе:total + len(words). Панель Call Stack — это живой traceback: те же блоки, только программа не упала, а ждёт вас.
| Клавиша | Действие | Когда нужно |
|---|---|---|
| F9 | поставить или снять точку останова | отметить подозрительную строку |
| F5 | запустить отладку / продолжить до следующей точки | пробежать до интересного места |
| F10 | шаг через (Step Over): выполнить строку целиком | идти по своей функции строка за строкой |
| F11 | шаг внутрь (Step Into): зайти в вызываемую функцию | баг, похоже, внутри вызова |
| Shift+F11 | шаг наружу (Step Out): доработать функцию до конца | зашли не туда |
| Shift+F5 | остановить отладку | всё понятно |
Условная точка останова — главный приём для данных. Цикл идёт по 100 000 примеров, а ломается один. Щёлкните
правой кнопкой по красной точке → Edit Breakpoint → Expression и впишите условие, например text == "и я"
или len(words) == 0. Отладчик остановится только на нужном примере. Там же есть Log Message: точка, которая
не останавливает программу, а печатает сообщение вроде total = {total} — print без правки кода.
pdb: отладчик в терминале
Заголовок раздела «pdb: отладчик в терминале»Если VS Code под рукой нет (например, вы работаете на сервере по SSH), поставьте в код вызов breakpoint().
Программа остановится на этом месте и откроет встроенный отладчик pdb с приглашением (Pdb). Файл dbg.py:
def count_tokens(texts): total = 0 for text in texts: words = text.split() breakpoint() # здесь программа встанет на паузу total = len(words) return total
corpus = ["я люблю python", "python любит меня", "и я"]print(count_tokens(corpus))Запускаем python dbg.py и вводим после (Pdb) команды: p text, total, n, p total, c и так далее.
Python 3.11 отвечает так:
> /home/user/dbg.py(6)count_tokens()-> total = len(words)(Pdb) p text, total('я люблю python', 0)(Pdb) n> /home/user/dbg.py(3)count_tokens()-> for text in texts:(Pdb) p total3(Pdb) c> /home/user/dbg.py(6)count_tokens()-> total = len(words)(Pdb) p text, total('python любит меня', 3)(Pdb) qНа втором проходе total равен 3, и следующая строка запишет в него снова 3, а не 6 — баг пойман.
В Python 3.13 и новее pdb останавливается прямо на строке breakpoint(), а не на следующей, поэтому там понадобится
на одно n больше.
| Команда | Что делает |
|---|---|
p выражение | напечатать значение (print) |
n | выполнить текущую строку (next) |
s | зайти внутрь вызываемой функции (step) |
c | продолжить до следующего breakpoint() (continue) |
l | показать код вокруг текущей строки (list) |
w | показать стек вызовов (where) |
q | выйти; Python напечатает BdbQuit — это нормально |
В Jupyter есть приём ещё проще: ячейка упала — выполните в следующей ячейке %debug, и pdb откроется прямо в месте
падения, со всеми переменными. Не забудьте убрать breakpoint() из кода, когда закончите. В браузерных заданиях
сайта отладчика нет, там работает print.
Резиновая уточка и ИИ-помощник
Заголовок раздела «Резиновая уточка и ИИ-помощник»Метод резиновой уточкиметод резиновой уточкиEN rubber duck debuggingОбъяснить код вслух строка за строкой (хоть игрушечной уточке) — и заметить, где ваше объяснение расходится с кодом. Термин из книги «Программист-прагматик». пришёл из книги «Программист-прагматик» (The Pragmatic
Programmer): программист носил с собой резиновую уточку и, застряв, объяснял ей код строка за строкой. Звучит как шутка,
но работает. Пока код в голове, вы видите то, что хотели написать. Объясняя вслух, вы вынуждены проговорить каждое
допущение: «тут total — сумма слов всех текстов… ой, нет, тут я присваиваю, а не прибавляю».
ИИ-помощник — уточка, которая умеет отвечать. Чтобы он не превратился в решебник (урок 0.0.3), попросите его играть именно уточку:
Я объясню свой код построчно. Не исправляй его и не пиши решение.Задавай вопросы там, где моё объяснение не совпадает с тем, что код делает на самом деле.Минимальный пример и хороший вопрос
Заголовок раздела «Минимальный пример и хороший вопрос»Если 15–20 минут самостоятельных попыток не помогли, пора спрашивать. Самое ценное, что вы можете приложить к вопросу, — минимальный воспроизводимый пример: несколько строк, которые запускаются как есть и показывают ту же ошибку.
Как его сделать:
- Скопируйте код в новый файл.
- Замените настоящие данные (файл на 2 ГБ, датасет) маленьким значением прямо в коде.
- Удаляйте всё, что не нужно для ошибки, по одному куску и запускайте после каждого удаления. Ошибка осталась — удаляйте дальше; пропала — верните кусок, он важен.
Скрипт из 60 строк, который читает корпус, строит словарь и кодирует текст, сжимается до двух:
vocab = {"<unk>": 0, "кот": 1}print(vocab["мат"]) # KeyError: 'мат'Часто на этом шаге вопрос задавать уже не нужно: в двух строках причина видна сама. Если нет — шаблон вопроса для форума или ИИ-помощника:
Цель: хочу закодировать текст словарём токенизатора.Версии: Python 3.11 (python --version), torch 2.x (если вопрос про библиотеку).Минимальный код (запускается как есть): vocab = {"<unk>": 0, "кот": 1} print(vocab["мат"])Полный traceback: <скопированный текст ошибки, не скриншот>Ожидал: id токена <unk>, то есть 0. Получил: KeyError.Уже пробовал: проверил, что слова «мат» нет в словаре.Три запрета: не присылайте код скриншотом — его нельзя запустить; не пишите «не работает» без traceback; не вставляйте в вопрос пароли, API-ключи и личные данные.
Баг → тетрадь ошибок → тест
Заголовок раздела «Баг → тетрадь ошибок → тест»Исправленный баг — ценный материал. Запишите его в свою тетрадь ошибоктетрадь ошибокEN mistake notebookКитайская школьная практика — переписывать каждую ошибку с правильным решением и регулярно к ней возвращаться. На сайте это страница «Тетрадь ошибок», куда сами попадают ошибки из квизов. (журнал
journal/mistakes.md из урока 0.0.3), добавив к прежним четырём полям два новых — как нашёл и тест:
## 2026-10-12 · урок 0.2.5 · тема: циклы, накопление суммы**Ошибка:** count_tokens вернула 2 вместо 8, без сообщений об ошибке**Причина:** total = len(words) перезаписывал сумму на каждом шаге**Как нашёл:** print(f"{total=}") внутри цикла**Исправление:** total += len(words)**Правило на будущее:** накопление в цикле — только +=, начальное значение — до цикла**Тест:** assert count_tokens(["а б", "в"]) == 3Последнее поле — регрессионный тестрегрессионный тестEN regression testТест, написанный на уже исправленный баг, чтобы он не вернулся незаметно.: проверка, написанная на уже исправленный баг.
Положите её в файл с тестами, и если через месяц вы случайно сломаете функцию снова, тест скажет об этом сразу.
Через пару месяцев журнал покажет и ваши слабые места: у кого-то это ошибки на единицуошибка на единицуEN off-by-one errorЦикл или срез делает на один шаг больше или меньше нужного, например range(1, n) вместо range(1, n + 1).
в срезах и range, у кого-то — строки вместо чисел.
flowchart LR A["Воспроизвести<br/>на маленьком входе"] --> B["Локализовать<br/>traceback, пополам"] B --> C["Гипотеза"] C --> D["Проверить<br/>print, assert, отладчик"] D -->|"не подтвердилась"| C D -->|"подтвердилась"| E["Исправить<br/>одну вещь"] E --> F["Тест + запись<br/>в тетрадь ошибок"]
Практика
Заголовок раздела «Практика»Шесть коротких функций, в каждой — ошибка, а в последней две. Докстринг говорит, что функция должна делать.
Нажмите «▶ Запустить»: внизу служебная часть вызывает каждую функцию и печатает ✓ или ✗, а для упавших — тип ошибки
и номер строки. Здесь есть всё из урока: логические ошибки, ошибка на единицу, TypeError, KeyError, None вместо
списка и деление на ноль. Чините по одному багу и запускайте снова; когда все строки станут ✓, нажмите «✓ Проверить».
# Охота на баги: шесть коротких функций, в каждой — ошибка (в последней — две).
# Докстринг каждой функции говорит, как она ДОЛЖНА работать. Код делает не то.
#
# Порядок работы:
# 1. Нажмите «▶ Запустить» и посмотрите на вывод внизу: где неверный ответ, где ошибка и в какой строке.
# 2. Для каждой функции: гипотеза → одно исправление → снова «▶ Запустить».
# 3. Когда все ответы совпадут с ожидаемыми, нажмите «✓ Проверить».
# Переписывать функции целиком не нужно: каждый баг чинится правкой в одной-двух строках.
import traceback
# ─── Баг 1 ───────────────────────────────────────────────────────────────────
def count_long(words, min_len):
"""Сколько слов длиной не меньше min_len.
count_long(["я", "люблю", "python"], 5) -> 2
"""
count = 0
for w in words:
if len(w) >= min_len:
count += 1
return count
# ─── Баг 2 ───────────────────────────────────────────────────────────────────
def mean_last(losses, k):
"""Среднее последних k значений loss (k не больше длины списка).
mean_last([4.0, 3.0, 2.0, 1.0], 2) -> 1.5
"""
tail = losses[-k:-1]
return sum(tail) / k
# ─── Баг 3 ───────────────────────────────────────────────────────────────────
def parse_scores(line):
"""Сумма баллов из строки, где числа записаны через пробел.
parse_scores("10 20 30") -> 60
"""
total = 0
for s in line.split():
total = total + s
return total
# ─── Баг 4 ───────────────────────────────────────────────────────────────────
def char_freq(text):
"""Частоты символов текста; пробелы не считаются.
char_freq("ааб") -> {"а": 2, "б": 1}
"""
counts = {}
for ch in text:
if ch != " ":
counts[ch] += 1
return counts
# ─── Баг 5 ───────────────────────────────────────────────────────────────────
def top3(scores):
"""Три лучших результата по убыванию. Исходный список НЕ должен меняться.
top3([5, 1, 9, 7]) -> [9, 7, 5]
"""
scores = scores.sort(reverse=True)
return scores[:3]
# ─── Баг 6 (их тут два) ──────────────────────────────────────────────────────
def percent(part, whole):
"""Доля part от whole в процентах. Если whole == 0, вернуть 0.0.
percent(3, 5) -> 60.0
"""
return part // whole * 100
# ─── Служебная часть: менять не нужно ────────────────────────────────────────
def show(call, func, *args, expected):
"""Вызывает функцию и печатает результат или ошибку с номером строки."""
try:
got = func(*args)
mark = "✓" if got == expected else "✗"
print(f"{mark} {call} → {got!r} (ожидалось {expected!r})")
except Exception as e:
line = traceback.extract_tb(e.__traceback__)[-1].lineno
print(f"✗ {call} → {type(e).__name__}: {e} (строка {line})")
show('count_long(["я", "люблю", "python"], 5)', count_long, ["я", "люблю", "python"], 5, expected=2)
show("mean_last([4.0, 3.0, 2.0, 1.0], 2)", mean_last, [4.0, 3.0, 2.0, 1.0], 2, expected=1.5)
show('parse_scores("10 20 30")', parse_scores, "10 20 30", expected=60)
show('char_freq("ааб")', char_freq, "ааб", expected={"а": 2, "б": 1})
show("top3([5, 1, 9, 7])", top3, [5, 1, 9, 7], expected=[9, 7, 5])
show("percent(3, 5)", percent, 3, 5, expected=60.0)
show("percent(1, 0)", percent, 1, 0, expected=0.0)
Проверь себя
Заголовок раздела «Проверь себя»- Синтаксическая ошибка не даёт запуститься всему файлу, ошибка выполнения останавливает программу на своей строке, логическая молчит. В сообщении важен не только тип, но и текст после двоеточия.
- Traceback читают снизу вверх; исправляют обычно в самом нижнем блоке со своим файлом.
NoneTypeв сообщении — ищите, откуда в переменной взялсяNone. - Логические баги ловят на маленьком входе с известным ответом:
print(f"{x=}")иrepr,assertна инварианты, деление кода пополам. - Отладчик VS Code (F9, F5, F10, F11, условные точки) и
breakpoint()с pdb показывают все переменные, не меняя код. - Застряли — уточка, потом минимальный пример и вопрос по шаблону. Исправили — запись в тетрадь ошибок и регрессионный тест.
Дальше: урок 0.2.6 «Мини-проекты». Соберём всё вместе в три программы — «угадай число», частотный словарь русского текста и трекер привычек с сохранением в JSON — и научимся разбивать задачу на функции.