Как Python кричит на вас и почему это хорошо

Python Николай

Оглавление

Ошибки…

Что вы чувствуете, когда, запустив программу, увидели, что она выдала ошибку? Скорее всего, это злость и непонимание. Очень хочется поскорее избавиться от нее и получить ответ. Но ошибки — наши лучшие друзья. Ведь именно они защищают ваши баллы.

Думаю, самая частая ошибка в программировании — выход за пределы массива. IndexError (Python), ArrayIndexOutOfBoundsException (Java), IndexOutOfRangeException (C#) видел каждый программист и не раз. Это очень частая ошибка. И, к слову, это самая лучшая ошибка, которую можно получить в программе. Потому что она предотвращает непредвиденное поведение программы.

Синтаксические ошибки vs ошибки во время выполнения

Ошибки принято называть exceptions (они же исключения)

В программировании существует несколько видов ошибок. Первый — ошибки компиляции, они же синтаксические ошибки. Когда вы забыли закрыть скобку, не поставили двоеточие или не поставили отступ. Это самые безобидные ошибки, которые очень быстро и легко фиксятся.

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

Такой код спокойно запустится, в нем никаких синтаксических ошибок нет. Но во время выполнения вы получите exception деления на ноль. Чаще всего программисты сталкиваются именно со вторым видом ошибок, потому что их сложнее всего предсказать до выполнения программы. (Для этого вообще профессия отдельная есть, QA-тестер)

Выход за пределы массива

Попробую объяснить, почему выбрасывание исключений (вместо их игнорирования) хорошо и полезно для вашей программы. Возьмем в качестве примера внутреннее устройство массива. Под капотом массив — это просто выделенный кусок оперативной памяти фиксированного размера. К примеру, возьмем список 10 чисел типа int.

Схематичное устройство оперативной памяти, разделенной на ячейки

Где-то мы выделили 10 ячеек. Наши данные, которые мы сохранили в массив, лежат в конкретном месте в памяти. Это адреса с 0x005 до 0x00E (адреса здесь схематичные, в реальных ОС они имеют 64-битный формат). Что будет, если мы обратимся к массиву по индексу 15? Мы получим доступ к памяти, которая нам не принадлежит. Зачастую там будет лежать мусор.

Можете проверить это: написать программу на С (или С++), создать массив на 10 элементов и попробовать вывести значение массива по индексу 11 или 111. Ошибки у вас никакой не будет, просто выведется какое-то мусорное значение, а если повезет, то программа упадет с segmentation fault, когда обратится к памяти, которую ОС не выделяла. Но иногда вместо мусора это может быть важная информация. Пароли, строки подключения к базам данных, возможность использовать вредоносный код. (Можете почитать в интернете про уязвимость в OpenSSL).

На первый взгляд, это все кажется мелочью. Ну что такого страшного в этих ваших пределах массива, я-то все помню, никогда не выйду за его пределы. Ну, выйдете, а если будете работать с динамической памятью, то и утечки памяти допустите. Это когда вы выделили память, потеряли к ней доступ, и теперь эта часть “вечно” занята вашей программой. Очень часто встречалось в играх на Unreal Engine, потому что там есть много выделений динамической памяти, и не очень хорошие программисты ее не чистили. Чем дольше играете — тем больше лагов и фризов. В программах посложнее hello world очень редко удается удержать все возможные сценарии в голове. Поэтому когда программа вылетает с ошибкой Index out of range exception, это спасение от непредвиденных последствий.

Худший сценарий на экзамене

Представим ситуацию: вы на экзамене пишете задание номер 17. Вы забыли написать - 1 в range и получили вот такой код:

for i in range(len(nums)):
    x1 = nums[i]
    x2 = nums[i + 1]

Ваша программа обратилась к памяти, которая ей не принадлежит. Вам “повезло”, программа не вылетела, в x2 записалось мусорное значение, if выполнился, и у вас получился ответ на 1 больше. И такие сценарии будут происходить во всех задачах. Ни одной ошибки не выбросится. Вы получаете 28 баллов за экзамен, потому что почти все задачи решены неправильно. А вы и знать не знаете, ведь все программы “сработали”. Ни одной ошибки.

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

Учимся читать exception

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

Разберемся на примере, будем вместе анализировать и исправлять ошибку в программе.

Дана программа ниже:

def average(numbers):
    total = sum(numbers)
    return total / len(numbers)


def average_of_positive(numbers):
    positive = [x for x in numbers if x > 0]
    return average(positive)


def analyze(data):
    result = average_of_positive(data)
    print(f"Среднее положительных: {result}")


analyze([-5, -3, -1])

При запуске она выдает вот такое:

Traceback (most recent call last):
  File "solution.py", line 15, in <module>
    analyze([-5, -3, -1])
    ~~~~~~~^^^^^^^^^^^^^^
  File "solution.py", line 11, in analyze
    result = average_of_positive(data)
             ~~~~~~~~~~~~~~~~~~~^^^^^^
  File "solution.py", line 7, in average_of_positive
    return average(positive)
           ~~~~~~~^^^^^^^^^^
  File "solution.py", line 3, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

Много непонятного и страшного текста, к тому же подсвеченного ярко-красным цветом (в IDLE/PyCharm). Давайте разбираться.

Ошибки читаются снизу вверх. Самая нижняя строчка говорит: ZeroDivisionError: division by zero, что можно перевести как “Ошибка деления на ноль: деление на ноль”. Прочитав ошибку, уже понятно, что именно в программе пошло не так. Но теперь нужно понять, в какой именно строке произошла ошибка? Читаем строчку чуть выше:

  File "solution.py", line 3, in average
    return total / len(numbers)
           ~~~~~~^~~~~~~~~~~~~~

Здесь уже говорится, в какой именно строке (line 3) произошла ошибка. Современные версии интерпретатора к тому же показывают именно то место, которое вызвало ошибку, в нашем случае это оператор деления. Также написано конкретное имя функции, которая вызвала ошибку. Это находится в этой строке: File "solution.py", line 3, in average, а точнее в in average. То есть функция, которая вызвала ошибку, была average. Давайте посмотрим, что делает эта функция:

def average(numbers):
    total = sum(numbers)
    return total / len(numbers)

Она просто считает сумму чисел и делит ее на количество. Получается, что количество чисел было равно нулю. Странно… Давайте почитаем ошибку дальше.

  File "solution.py", line 7, in average_of_positive
    return average(positive)
           ~~~~~~~^^^^^^^^^^

В этой строчке уже можно увидеть, кто именно вызвал функцию average, — это функция average_of_positive. Давайте посмотрим на определение функции average_of_positive:

def average_of_positive(numbers):
    positive = [x for x in numbers if x > 0]
    return average(positive)

Эта функция принимает числа, создает новый список, в который сохраняет только положительные элементы, и вызывает функцию average. Хорошо, а какие тогда исходные данные пришли в эту функцию? Читаем дальше…

  File "solution.py", line 11, in analyze
    result = average_of_positive(data)
             ~~~~~~~~~~~~~~~~~~~^^^^^^

Функцию average_of_positive вызвала функция analyze. Смотрим на определение функции analyze

def analyze(data):
    result = average_of_positive(data)
    print(f"Среднее положительных: {result}")

Замечаем, что она просто принимает data и сохраняет в result результат работы функции average_of_positive. Но какие именно данные были переданы в эту функцию? Читаем дальше:

  File "solution.py", line 15, in <module>
    analyze([-5, -3, -1])
    ~~~~~~~^^^^^^^^^^^^^^

Вот мы и нашли виновника торжества. В функцию analyze поступил список из отрицательных чисел, дальше эти числа попали в функцию average_of_positive, где она отфильтровала только положительные числа, которых у нас нет, и функция average выбросила ошибку, потому что len(numbers) = 0. Надеюсь, вы уловили все шаги “дебаггинга”.

Из этого примера можно сделать вывод, что не всегда место, которое выбросило исключение, является виновником самого исключения. Возможно, исходные данные были некорректны и их нужно изменить. Не бойтесь ошибок. Они достаточно четко показывают вам, в каком месте и почему у вас произошло исключение (тем более в Python).

Тренировка

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

Traceback (most recent call last):
  File "solve.py", line 24, in <module>
    solve([12, 3456, 78, 999])
    ~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "solve.py", line 20, in solve
    result = digit_sum(big)
  File "solve.py", line 5, in digit_sum
    for ch in n:
              ^
TypeError: 'int' object is not iterable
Квиз

Что выдаст исправленная программа?

Популярные ошибки и способы их устранить

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

Чтобы полностью закрепить материал, сначала запустите программу, попробуйте сами исправить ее, а только потом смотрите исправленный вариант.

Синтаксические ошибки

1. Забыть поставить двоеточие после if, for, while, def.

Исправление
n = 10
if n > 0: # было пропущеное двоеточие
    print(n)

2. Неправильные отступы.

Исправление
n = 5
print(n)

if n > 0:
    print("положительное")
    print(n) # добавил пробел

3. Одно равно вместо двух в сравнении

Исправление
n = 15 % 10
if n == 5: # ==, вместо =
    print("пять")

4. Незакрытые скобки

Исправление
print(sum([x for x in range(10) if x % 2 == 0]))
# были не закрыты квадратная и круглая скобки

5. Использовать зарезервированные имена в качестве переменных

если после изменения кода все равно выдает ошибку, перезагрузите страницу и перед запуском сразу исправьте ошибку, какой-то баг питона

Пример 1:

Исправление
n = [1, 2, 3, 4]
maxx = 10 # дали переменной имя, отличное от встроенных в Python функций
print(max(n))

Пример 2:

Исправление
my_list = [1, 2, 3] # дал переменной имя, отличное от list
a = list(range(5))

Ошибки во время выполнения

1. Выход за пределы списка

Исправление
a = [10, 20, 30]
for i in range(len(a) - 1): # добавили -1 в range
    print(a[i], a[i + 1])

2. Обращение к несуществующей переменной

Исправление
summa = 0
for i in range(10):
    summa += i
print(summa) #  было summ, а переменная называется summa

3. Сложение строк и целых чисел

Исправление
n = 67
n2 = ""
while n > 0:
    n2 += str(n % 2) # переводим в str и складываем
    n = n // 2

n2 = n2[::-1]
print("Ответ:", n2)

4. Сравнение строк и чисел

Исправление
n = '67'
if int(n) > 10: # переводим в int для корректного сравнения
    print("много")

Внимание! Нужно быть осторожным, потому что операторы == и != не выдают ошибку, когда сравнивают строку и число. Из-за такой ошибки многие теряют баллы, потому что очень часто приходится работать со строками, но сравнивать их с числами. Будьте внимательны, особенно в номере 8, когда пишете if num[0] != 0 — это неправильно! Обязательно нужно сравнивать строку со строкой, иначе данное условие всегда будет истинно! Как было бы хорошо, если бы Python выдавал ошибку на такую запись…

5. Итерация по числу

Исправление
n = 123
sum_digits = 0
for i in str(n): # проходимся теперь по строке
    sum_digits += int(i) # прибавляем целое число
print("Сумма цифр:", sum_digits)

6. Невалидный литерал для 10 системы счисления

Исправление
n = 123
n2 = bin(n)[2:]
n2 = n2 + bin(n2.count('1'))[2:] # взять срез
print(int(n2, 2))

В ошибке говорилось ValueError: invalid literal for int() with base 2: '11110110b110' — показана часть строки, на которую ругается интерпретатор. Мы видим букву b, которую мы не можем конвертировать из 2-ой в 10-ую.

Такая ошибка может возникнуть не только после перевода из 2-ой в 10-ую, но суть ее всегда одна. Функция int пытается перевести строку в 10-ую из той, которую вы укажете (по умолчанию это 10-ая), но сталкивается с проблемой, что такого литерала нет в такой системе счисления, и выбрасывает ошибку. Если бы в примере система была не 2-ичная, а, допустим, 12-ричная, то ошибки бы не было, потому что цифра b есть в алфавите 12-ричной системы счисления (можете проверить, вернув код, каким он был, и поменять в int цифру с 2 на 12)

7. Деление на ноль

К примеру, возьмем алгоритм поиска делителей, типичное задание 25.

Исправление
n = 12
for d in range(1, n + 1): # перебираем от 1 и до числа включительно
    if n % d == 0:
        print(d)

Деление такое %, и такое /, и даже такое // — все одно и то же деление, и на ноль нельзя делить никаким делением 🤓. Причем каждое из делений выдает разные ошибки:

/ => ZeroDivisionError: division by zero

// => ZeroDivisionError: integer division or modulo by zero

% => ZeroDivisionError: integer modulo by zero

8. Ошибка глубины рекурсии

Исправление
from functools import *

@lru_cache()
def F(n):
    if n <= 1:
        return 1
    if n > 1:
        return F(n - 1) + n

for n in range(1,100000):
    F(n)

print(F(100000))

ОЧЕНЬ ВАЖНО! Если в задаче ЕГЭ у вас выскочила ошибка глубины рекурсии, то НЕ НАДО: а) повышать ее через sys.setrecursionlimit и б) писать @lru_cache(None). Никогда не пишите None внутри @lru_cache(). По умолчанию @lru_cache() сохраняет последние 128 значений в своем кэше, а когда вы пишете None, вы сохраняете АБСОЛЮТНО ВСЕ значения, что, вероятно, переполнит вашу оперативную память и крашнет компьютер. В теории, если по умолчанию @lru_cache() программа все равно выдает ошибку глубины рекурсии, то можно понемногу повышать значение внутри @lru_cache(), начать можно с 200, 300, 400, … и т. д.

9. Ошибка ключа в словаре

Особенно актуально в задании 12 (потому что в других мы словарями не пользуемся), исправлений здесь делать не надо, только посмотреть на то, что выдает программа, когда нашего ключа не находится в словаре.

Вариантов исправления в реальной задаче может быть два, в зависимости от того, какая именно ошибка у вас: 1) вы не дописали все необходимые команды, 2) каким-то образом вы обратились по символу с не тем состоянием, которое надо было. Это при условии, что вы проверили, что все команды были переписаны правильно. Тогда нужно проверить, правильно ли вы указали местоположение головки в начальный момент (i = 0, если в ближайшей слева, и i = len(s) - 1, если в ближайшей справа). Проверить, какое у вас начальное состояние (должно быть q = 0), выходите ли вы из бесконечного цикла, если cmd[1] == 2, точно ли у вас меняется состояние во время выполнения программы (q = cmd[2]), точно ли вы перемещаете головку дальше (i += cmd[1]), точно ли вы меняете символ перед всеми остальными действиями (s[i] = cmd[0])…

10. Ошибка отсутствия файла

Исправить ошибку здесь не получится, поэтому распишу профилактику этой ошибки в реальной ситуации.

  1. Совет! Всегда помещайте файл, который хотите запустить в Python, вместе с самим файлом Python. Тогда в таком случае достаточно лишь прописать только название файла: f = open("17.txt")
задачи/
├── 17.txt
├── 17.py
  1. Если у вас файлы хранятся в отдельной папке, но эта папка находится в одном месте с файлом Python — тогда пишем название папки, слэш, название файла: f = open("files/17.txt")
задачи/
├── files/
│   ├── 17.txt
├── 17.py
  1. Самый нерекомендуемый вариант — указать полный путь до файла. Для этого можно кликнуть ПКМ по файлу, копировать как путь. Получится что-то такое: "C:\Movies\TheBoysS0508.mkv" — и теперь это можно вставить в open. Но этот способ самый нерекомендуемый, потому что при вставке придется экранировать слэши либо ставить букву r перед кавычками. Иначе будет ошибка: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape. Вариант исправления: f = open(r"C:\Movies\TheBoysS0508.mkv")

11. Ошибка с файлом в 27 задании

В некоторых задачах 27 в файле первой строкой встречается X Y, и из-за этого код может выдать вот такую ошибку:

Traceback (most recent call last):
  File "27.py", line 27, in <module>
    x, y = map(float, s.split())
    ^^^^
ValueError: could not convert string to float: 'X'
Исправление

Написать пустой f.readline, который считает первую строку, переместит указатель дальше, и изменения никуда не сохранит.

f = open("Files/23/27_A_23.txt")
f.readline()

Если встречаете ошибку ValueError: could not convert string to float, это значит, что вы пытаетесь конвертировать строковый тип данных в вещественный, при этом строка не является ни числом, ни корректным вещественным числом. Корректным вещественным числом в Python является "3.14", тогда как "3,14" <= неправильная запись.

Исправление
s = "3,14"
s = s.replace(",", ".") # меняем запятую на точку
print(float(s))

12. Вызов метода у типа, который его не имеет

Исправление
a = 1343241234
print(str(a).count("1")) # переводим a в str и считаем количество единиц
# как символ. int не имеет метода count

Обработка ошибок

В программировании ошибки можно обрабатывать, в разных языках для этого своя конструкция. В С-подобных это try/catch, в Python это try/except. Но я сознательно ничего не хочу про это рассказывать, потому что в задачах ЕГЭ это не нужно. Сами узнаете, когда начнете писать свои веб-сервисы или приложения. В решении задач эти конструкции не нужны.

И да, это не противоречит всему, что было сказано выше. Конструкция try/except это не способ заткнуть ошибку, чтобы программа не падала, а способ осознанно обработать ожидаемую проблему (к примеру, пользователь ввел не число, просим его ввести значение заново). Игнорировать ошибки по-прежнему плохо. А оставлять пустой catch/except вообще грех.

Заключение

Если у вас остались какие-либо вопросы или вы хотите добавить список ошибок, которые я упустил, милости прошу писать сюда