💻
Разработка в IT
Опубликовано:
14.04.2026
Обновлено:
14.04.2026

Вычисление логических выражений по сокращённой схеме в Python

Алексей Иванов

Когда я впервые столкнулся с делением на ноль внутри условия if, отладка заняла полчаса. Проблема решилась одной перестановкой операндов. Причина: Python не вычисляет второй операнд, если результат уже понятен по первому. Такой механизм называют вычислением по сокращённой схеме, или short-circuit evaluation.

Эта статья разбирает, как именно Python обрабатывает логические выражения с операторами and, or и not, почему порядок операндов влияет на результат и безопасность кода, и где сокращённая схема создаёт ловушки для новичков.

Что такое short-circuit evaluation

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

Зачем это нужно

Сокращённая схема решает две задачи. Первая: экономия ресурсов. Если левый операнд уже определил результат, вычислять правый бессмысленно, особенно когда правый операнд содержит вызов функции или обращение к базе данных. Вторая: защита от ошибок. Код x != 0 and 10 / x > 2 не вызовет ZeroDivisionError, потому что при x == 0 Python остановится на первом операнде и не дойдёт до деления.

Как это выглядит на уровне интерпретатора

Python вычисляет выражение A and B так:

  1. Вычислить A.
  2. Если A ложно, вернуть A (без вычисления B).
  3. Если A истинно, вычислить и вернуть B.

Для A or B алгоритм зеркальный:

  1. Вычислить A.
  2. Если A истинно, вернуть A.
  3. Если A ложно, вычислить и вернуть B.

Обратите внимание: операторы and и or возвращают не True/False, а сам объект-операнд. Об этом подробнее ниже.

Как операторы and и or возвращают значения

Многие думают, что and и or возвращают булево значение. Это не так. Оператор and возвращает первый falsy-объект, а если все операнды truthy, возвращает последний. Оператор or возвращает первый truthy-объект, а если все falsy, возвращает последний.

Что считается truthy и falsy

В Python следующие значения интерпретируются как ложные (falsy):

  • False
  • None
  • числовой ноль: 0, 0.0, 0j
  • пустые коллекции: "", [], (), {}, set(), frozenset()
  • объекты, у которых метод __bool__ возвращает False или __len__ возвращает 0

Все остальные значения считаются истинными (truthy).

Примеры с and

# and возвращает первый falsy или последний операнд
print(1 and 2 and 3)  # 3 (все truthy, вернул последний)
print(1 and 0 and 3)  # 0 (первый falsy)
print([] and 'hello')  # [] (пустой список - falsy)

Примеры с or

# or возвращает первый truthy или последний операнд
print(0 or '' or 'default')  # 'default' (первый truthy)
print(0 or '' or [])  # [] (все falsy, вернул последний)
print('data' or 'fallback')  # 'data' (первый truthy)

Такое поведение позволяет использовать or для значений по умолчанию:

username = input_value or 'anonymous'

Если input_value пустая строка или None, переменная username получит значение "anonymous".

Паттерн защитного выражения (guardian pattern)

Защитное выражение использует short-circuit, чтобы предотвратить выполнение опасной операции. Левый операнд проверяет условие-предохранитель, правый содержит операцию, которая без проверки могла бы вызвать ошибку.

Классический пример: деление на ноль

x = 6
y = 2
# Безопасно: если x < 2, деление не выполнится
result = x >= 2 and (x / y) > 2
print(result)  # True

y = 0
# Без guardian pattern:
# result = (x / y) > 2  # ZeroDivisionError!

# С guardian pattern:
result = y != 0 and (x / y) > 2
print(result)  # False, деления не было

Почему порядок операндов критичен

x = 1
y = 0

# Вариант 1: guardian слева - безопасно
print(x >= 2 and y != 0 and (x / y) > 2)  # False

# Вариант 2: guardian справа - ошибка!
# print(x >= 2 and (x / y) > 2 and y != 0) # ZeroDivisionError

В первом варианте x >= 2 вернёт False, Python остановится. Во втором, если x = 6, Python дойдёт до x / y раньше проверки y != 0 и выбросит исключение. Защитное условие всегда ставят левее опасной операции.

Guardian pattern со списками

data = []

# Проверяем, что список не пуст, перед обращением к элементу
if data and data[0] > 10:
    print('Первый элемент больше 10')
# Без ошибки IndexError

Сокращённая схема в функциях all() и any()

Встроенные функции all() и any() тоже используют short-circuit evaluation. Функция all() перебирает элементы итерируемого объекта и возвращает False, как только встретит первый falsy-элемент. Функция any() возвращает True на первом truthy-элементе.

Практический пример
def check_positive(n):
    print(f'Проверяю {n}')
    return n > 0


numbers = [3, 7, -1, 5, 2]

# any() остановится на первом True (элемент 3)
print(any(check_positive(n) for n in numbers))
# Вывод:
# Проверяю 3
# True

# all() остановится на первом False (элемент -1)
print(all(check_positive(n) for n in numbers))
# Вывод:
# Проверяю 3
# Проверяю 7
# Проверяю -1
# False

Типичные ошибки при работе с short-circuit

Новички допускают несколько повторяющихся ошибок. Разберём каждую.

Побочные эффекты в правом операнде

log_messages = []


def log_and_check(value):
    log_messages.append(value)
    return value > 0


x = -5
# log_and_check(10) НЕ выполнится, лог не запишется
result = (x > 0) and log_and_check(10)
print(log_messages)  # []

Если правый операнд содержит побочный эффект (запись в лог, отправка запроса, изменение переменной), при short-circuit этот эффект не произойдёт. Код становится непредсказуемым. Побочные эффекты лучше выносить за пределы логических выражений.

Путаница с возвращаемым типом

value = '' or 0 or None
print(value)  # None
print(type(value))  # <class 'NoneType'>

Ожидание False здесь ошибочно. Оператор or перебрал все falsy-значения и вернул последнее. Если нужен булев результат, используйте bool():

value = bool('' or 0 or None)
print(value)  # False

Подмена or вместо значения по умолчанию

# Опасно: 0 - допустимое значение, но оно falsy
count = user_input or 10
# Если user_input == 0, count станет 10, а не 0

Для таких случаев безопаснее использовать явную проверку:

count = user_input if user_input is not None else 10

Сравнение short-circuit в Python и других языках

Сокращённая схема вычислений есть не только в Python. Но реализации отличаются.

Язык

Операторы

Возвращаемый тип

Short-circuit

Python

and, or

Сам операнд (любой тип)

Да

JavaScript

&&, ||

Сам операнд (любой тип)

Да

C / C++

&&, ||

int (0 или 1)

Да

Java

&&, ||

boolean

Да

Java

&, |

boolean

Нет (вычисляет оба)

Rust

&&, ||

bool

Да

В Java операторы & и | (без удвоения) вычисляют оба операнда. Python такой формы не имеет: and и or всегда работают по сокращённой схеме. Если нужно гарантировать вычисление обоих операндов, придётся сохранять результаты в отдельные переменные.

Оператор not и сокращённая схема

Оператор not не участвует в short-circuit: он работает с одним операндом, поэтому пропускать нечего. Но not меняет логику читаемости кода, и при комбинации с and/or порядок вычислений сохраняется.

x = 5
y = 0

# not применяется к результату (x > 3), затем and проверяет остальное
result = not (x > 3) and (10 / y > 1)
print(result)  # False, деление не выполнилось
# not (x > 3) вернул False, and остановился

Приоритет операторов: not > and > or. Скобки помогают избежать путаницы:

# Без скобок
not True or False  # False or False -> False

# Со скобками - другой результат
not (True or False)  # not True -> False

Практический пример: безопасный парсинг данных

Рассмотрим задачу: чтение настроек из словаря с вложенной структурой. Некоторые ключи могут отсутствовать.

config = {
    'database': {
        'host': 'localhost', 
        'port': 5432
    }
}

# С short-circuit - одна строка
db_host = config.get('database') and config['database'].get('host')
print(db_host)  # 'localhost'

# Если ключ отсутствует
db_ssl = config.get('database') and config['database'].get('ssl')
print(db_ssl)  # None (безопасно, без KeyError)

# Если весь блок отсутствует
cache_host = config.get('cache') and config['cache'].get('host')
print(cache_host)  # None

Метод config.get("cache") вернёт None, и оператор and остановится. Обращения к несуществующему ключу не произойдёт.

Для сложной вложенности лучше использовать отдельную функцию:

def safe_get(data, *keys):
    '''Безопасное извлечение значения из вложенного словаря.'''
    for key in keys:
        if isinstance(data, dict):
            data = data.get(key)
        else:
            return None
    return data


host = safe_get(config, 'database', 'host')
print(host)  # 'localhost'

missing = safe_get(config, 'cache', 'host')
print(missing)  # None

Неочевидные факты о short-circuit в Python

Несколько деталей, которые редко упоминают в руководствах.

Первый факт: цепочка and из нескольких операндов возвращает первый falsy или последний truthy. Это работает для любого количества операндов, не только для двух. Выражение a and b and c and d остановится на первом ложном значении.

Второй факт: генераторные выражения внутри all() и any() используют ленивое вычисление вместе с short-circuit. Генератор выдаёт элементы по одному, и функция останавливается, не запрашивая следующий элемент. Список-comprehension создаст все элементы сразу, до передачи в all()/any().

# Генератор - short-circuit работает полноценно
any(x > 100 for x in range(1_000_000))  # быстро: остановится на 101

# Список - сначала создаст миллион элементов
any([x > 100 for x in range(1_000_000)])  # медленно

Третий факт: тернарный оператор a if condition else b тоже использует short-circuit. Вычисляется только одна из ветвей.

Четвёртый факт: в выражении False and func() функция func не только не выполнится, но и не будет найдена в пространстве имён. Если func не определена, ошибки NameError не возникнет.

result = False and func_undefined()
print(result)  # False

FAQ

Работает ли short-circuit с побитовыми операторами & и |?

Нет. Побитовые операторы & и | всегда вычисляют оба операнда . Они выполняют поразрядные операции над числами. Для short-circuit используйте and и or.

Можно ли отключить сокращённую схему?

Нет встроенного способа. Если нужно гарантировать вычисление обоих операндов, сохраните результаты в переменные:

a = some_check()
b = other_check()
result = a and b

Как short-circuit влияет на производительность?

При большом количестве условий экономия ощутима. Ставьте самые "дешёвые" и часто ложные проверки левее в выражениях с and. Для or левее размещайте проверки, которые чаще истинны.

Работает ли short-circuit внутри lambda?

Да. Lambda содержат обычные Python-выражения, и все правила short-circuit применяются без изменений.

safe_div = lambda x, y: y != 0 and x / y
print(safe_div(10, 0))  # False
print(safe_div(10, 2))  # 5.0

В чём разница между if x: и if x is not None:?

Проверка if x: использует truthiness: она вернёт False для 0, пустой строки, пустого списка и None. Проверка if x is not None: отсекает только None. Когда 0 или пустая строка допустимы, используйте явное сравнение с None.

Мой совет: привыкайте читать логические выражения слева направо и задавать вопрос "если левая часть определила результат, что справа не выполнится?" Эта привычка экономит часы отладки и помогает писать защитные выражения правильно с первого раза.

Читайте также