Логические ошибки в программировании: виды и примеры

Логические ошибки в программировании: виды и примеры Полезное

Логическая ошибка — это дефект, при котором программа запускается и завершается без сбоя, но выдает неверный результат, потому что сам алгоритм описан неправильно. В отличие от синтаксической ошибки (код не компилируется) и ошибки времени выполнения (программа падает с исключением), логическая ошибка не дает ни красной строки в редакторе, ни трейсбека: интерпретатор считает код корректным. Ниже — основные виды логических ошибок в программировании и примеры с разбором: неверные границы диапазона, ошибки в условиях, приоритет операций, сравнение вещественных чисел, путаница копии и ссылки. Каждый пример дан тройкой: неверный код, фактический результат, исправление.

Виды ошибок в программировании: где место логической ошибки

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

Вид ошибки Когда проявляется Кто ловит Пример
Синтаксическая до запуска, при разборе кода компилятор / интерпретатор пропущено двоеточие, скобка
Времени выполнения (runtime) во время работы среда выполнения, исключение деление на ноль, обращение за границу списка
Логическая во время работы, молча только тесты и человек неверная формула, перепутанное условие

Ключевое отличие логической ошибки: программа не жалуется. Она честно делает то, что вы написали, — просто вы написали не то, что задумали. Поэтому такую ошибку не найти по сообщению об ошибке, ее ищут сравнением ожидаемого и фактического результата. О синтаксических ошибках подробнее в отдельном разборе (ссылка в конце), а здесь сосредоточимся именно на логических.

Off-by-one: ошибка на единицу в границах

Самая частая логическая ошибка — неверная граница диапазона или индекса. В Python функция range(a, b) не включает правую границу b, и об этом легко забыть.

# Нужно сложить числа от 1 до 10 включительно
total = 0
for i in range(1, 10):
    total += i
print(total)

Фактический результат: 45. Ожидали 55, но range(1, 10) перебирает числа с 1 по 9, десятка не попала в цикл.

# Исправление: правая граница на единицу больше
total = 0
for i in range(1, 11):
    total += i
print(total)  # 55

Тот же класс ошибок — обращение по индексу len(arr) (последний допустимый индекс len(arr) - 1), проверка <= вместо < в границе цикла. Правило простое: всегда проверяйте крайние значения диапазона руками, на первом и последнем элементе.

Неверное логическое условие: and вместо or

Условие, которое читается «правильно» по-русски, легко записать неверно в коде. Классика — перепутать and и or.

# Пропускаем только пользователей от 18 до 65 лет
age = 70
if age > 18 or age < 65:
    print("доступ разрешен")

Фактический результат: строка печатается даже для 70 лет. Любое число удовлетворяет хотя бы одному из условий (70 > 18 истинно), а or требует только одного истинного операнда, поэтому проверка всегда проходит.

# Исправление: оба условия должны выполняться одновременно
age = 70
if age > 18 and age < 65:
    print("доступ разрешен")   # для 70 не печатается

Сюда же относятся инвертированные условия (if not is_valid там, где нужно if is_valid) и сравнение не с той переменной. Такие места стоит покрывать тестом хотя бы на две точки: внутри диапазона и за его пределом.

Приоритет операций: где не хватило скобок

Языки вычисляют выражения по приоритету операторов, а не слева направо. Умножение и деление выполняются раньше сложения, и без скобок формула считает не то.

# Среднее двух чисел
a, b = 4, 6
avg = a + b / 2
print(avg)

Фактический результат: 7.0. Сначала выполнилось b / 2 (это 3.0), потом прибавилось a. Ожидали 5.0.

# Исправление: скобки задают нужный порядок
a, b = 4, 6
avg = (a + b) / 2
print(avg)  # 5.0

Тот же подвох в логических выражениях: and связывает сильнее, чем or, поэтому a or b and c — это a or (b and c). Если сомневаетесь в приоритете, ставьте скобки: они ничего не ломают, зато делают намерение явным.

Сравнение вещественных чисел: почему 0.1 + 0.2 не равно 0.3

Дробные числа хранятся в двоичном формате с плавающей точкой (стандарт IEEE 754), и многие десятичные дроби в нем не представимы точно. Прямое сравнение таких чисел на равенство — логическая ошибка.

x = 0.1 + 0.2
print(x == 0.3)
print(x)

Фактический результат: False и 0.30000000000000004. Сумма получилась чуть больше 0.3 из-за погрешности представления, и строгое равенство не выполнилось.

# Исправление: сравнивать с допуском
import math
print(math.isclose(0.1 + 0.2, 0.3))  # True

Функция math.isclose (доступна с Python 3.5) сравнивает числа с относительным допуском. Для денежных расчетов вместо float берут тип Decimal, где 0.1 + 0.2 дает ровно 0.3. Правило: вещественные числа не сравнивают через ==.

Копия против ссылки: изменили одно, поменялось все

В Python переменная хранит не сам объект, а ссылку на него. Если присвоить один список двум именам, они указывают на одни и те же данные, и правка через одно имя видна через другое.

# Хотим две независимые строки таблицы
row = [0, 0, 0]
grid = [row, row]
grid[0][1] = 5
print(grid)

Фактический результат: [[0, 5, 0], [0, 5, 0]]. Обе «строки» — это один и тот же список, поэтому пятерка появилась в обеих.

# Исправление: создать отдельный список для каждой строки
grid = [[0, 0, 0] for _ in range(2)]
grid[0][1] = 5
print(grid)  # [[0, 5, 0], [0, 0, 0]]

Когда нужна именно независимая копия вложенных структур, применяют copy.deepcopy. Поверхностное копирование (list(x) или срез x[:]) дублирует только верхний уровень, а вложенные списки останутся общими — это отдельная ловушка того же рода.

Изменение коллекции во время перебора

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

nums = [2, 4, 6]
# Хотим удалить все четные, ожидаем пустой список
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(nums)

Фактический результат: [4]. После удаления двойки список стал короче, перебор «перескочил» четверку и не проверил ее.

# Исправление: строим новый список, а старый не трогаем
nums = [2, 4, 6]
nums = [n for n in nums if n % 2 != 0]
print(nums)  # []

Можно перебирать копию (for n in nums[:]), но генератор списка обычно чище и быстрее. Общее правило: не меняйте коллекцию, по которой прямо сейчас идет цикл.

Как ловить логические ошибки

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

Прием Что дает Против каких ошибок
Юнит-тесты сравнение факта с эталоном все виды логических
Отладчик, точки останова пошаговый просмотр значений неверные условия, границы
print и логирование видно фактические данные формулы, приоритет операций
Проверка граничных значений первый и последний элемент off-by-one
Ревью кода коллегой свежий взгляд на логику перепутанные условия, знаки
Аннотации типов и линтер часть ошибок до запуска типы, очевидные опечатки

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

Выводы

  • Логическая ошибка не вызывает ни ошибки компиляции, ни исключения: программа отрабатывает и выдает неверный результат, поэтому ловится только сравнением факта с ожиданием.
  • Самые частые виды — off-by-one в границах, перепутанные and/or, нехватка скобок из-за приоритета операций, сравнение float через ==, путаница копии и ссылки, изменение коллекции во время перебора.
  • Границы диапазонов и циклов всегда проверяются на крайних значениях: первый и последний элемент, пустой вход.
  • Вещественные числа сравнивают с допуском (math.isclose) или считают в Decimal, а не через строгое равенство.
  • В Python присваивание копирует ссылку, а не данные: для независимых структур нужен явный новый объект или deepcopy.
  • Юнит-тесты, отладчик, проверка границ и ревью дополняют друг друга; надежнее всего они работают вместе.

Где применяется / связь с практикой

Освойте тему на практике

Логические ошибки — повседневная часть работы любого разработчика, и умение их находить важнее, чем знание синтаксиса: язык можно подсмотреть, а вот отличить «код работает» от «код дает верный результат» приходится самому. Начинающим удобнее всего тренировать этот навык на Python: понятный синтаксис не отвлекает, и внимание остается на самой логике. Разобраться с основами языка и приемами отладки помогает курс Python Basic, где разбор ошибок и тестирование идут вместе с практикой. Посмотреть формат занятий и задать вопрос преподавателю можно на бесплатных вебинарах — там же разбирают типовые ошибки новичков.

Смежные темы: Python и синтаксические ошибки, Какими бывают ошибки в приложениях, Ошибки в PHP: виды и логирование.

FAQ

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

Какие инструменты помогают находить логические ошибки?
Юнит-тесты фиксируют ожидаемый ответ, отладчик показывает значения переменных по шагам, логирование выводит фактические данные, а линтер и аннотации типов отсекают часть ошибок еще до запуска. Ревью кода коллегой добавляет свежий взгляд.

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

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)