Логические операции играют важную роль в программировании. Они используются для создания условных конструкций и составления сложных алгоритмов. В Python для выполнения логических операций используются логические операторы:

  • not — логическое отрицание

  • and — логическое умножение

  • or — логическое сложение

В этой статье мы поговорим о неочевидных деталях и скрытых особенностях работы логических операторов в Python.

Таблицы истинности логических операторов

Мы привыкли к тому, что обычно в языках программирования логические операторы возвращают значения True или False согласно своим таблицам истинности.

Таблица истинности оператора not:

a

not a

False

True

True

False

Таблица истинности оператора and:

a

b

a and b

False

False

False

False

True

False

True

False

False

True

True

True

Таблица истинности оператора or:

a

b

a or b

False

False

False

False

True

True

True

False

True

True

True

True

Когда операндами логических операторов являются объекты True и False, работа логических операторов в Python также соответствует данным таблицам истинности.

Приведенный ниже код:

print(not True)
print(not False)
print(False and True)
print(True and True)
print(False or True)
print(False or False)

выводит:

False
True
False
True
True
False

Однако Python не ограничивает нас только значениями True и False в качестве операндов логических операторов. Операндами операторов not, and и or могут быть объекты любых других типов данных.

Понятия truthy и falsy

Одной из важных особенностей языка Python является концепция truthy и falsy объектов. Любой объект в Python может быть оценен как True или False. При этом объекты, которые оцениваются как True, называются truthy объектами, а объекты, которые оцениваются как Falsefalsy объектами.

К встроенным falsy объектам относятся:

  • значение False

  • значение None

  • нули числовых типов данных: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)

  • пустые последовательности и коллекции: '', (), [], {}, set(), range(0)

Другие объекты встроенных типов данных относятся к truthy объектам. Экземпляры пользовательских классов по умолчанию также являются truthy объектами.

Чтобы привести объекты к значению True или False, используется встроенная функция bool().

Приведенный ниже код:

# falsy объекты
print(bool(False))
print(bool(None))
print(bool(0))
print(bool(0.0))
print(bool([]))
print(bool(''))
print(bool({}))

#truthy объекты
print(bool(True))
print(bool(123))
print(bool(69.96))
print(bool('beegeek'))
print(bool([4, 8, 15, 16, 23, 42]))
print(bool({1, 2, 3}))

выводит:

False
False
False
False
False
False
False
True
True
True
True
True
True

Концепция truthy и falsy объектов в Python позволяет работать с условным оператором в более простой манере.

Например, приведенный ниже код:

if len(data) > 0:
    ...

if value == True:
    ...

if value == False:
    ...  

можно переписать в виде:

if data:
    ...  

if value:
    ...  

if not value:
    ...

На картинке ниже представлены примеры упрощенной записи условного оператора с различными объектами Python согласно концепции truthy и falsy объектов:

Оператор not

Как мы уже знаем, операндом оператора not может быть объект любого типа. Если операнд отличен от значений True и False, он оценивается в соответствии с концепцией truthy и falsy объектов. При этом результатом работы оператора not всегда является значение True или False.

Приведенный ниже код:

print(not False)
print(not None)
print(not 0)
print(not 0.0)
print(not [])
print(not '')
print(not {})

выводит:

True
True
True
True
True
True
True

Приведенный ниже код:

print(not True)
print(not 123)
print(not 69.96)
print(not 'beegeek')
print(not [4, 8, 15, 16, 23, 42])
print(not {1, 2, 3})

выводит:

False
False
False
False
False
False

Операторы and и or

Операндами операторов and и or, как и в случае с not, могут быть объекты любых типов данных. По аналогии с оператором not можно предположить, что результатом работы логических операторов and и or также является значение True или False. Однако на самом деле данные операторы возвращают один из своих операндов. Какой именно — зависит от самого оператора.

Приведенный ниже код:

print(None or 0)
print(0 or 5)
print('beegeek' or None)
print([1, 2, 3] or [6, 9])

print(1 or 'beegeek' or None)
print(0.0 or 'habr' or {'one': 1})
print(0 or '' or [6, 9])
print(0 or '' or [])
print(0 or '' or [] or {})

выводит:

0
5
beegeek
[1, 2, 3]
1
habr
[6, 9]
[]
{}

Как мы видим, оператор or оценивает каждый свой операнд как truthy или falsyобъект, однако возвращает не значение True или False, а сам объект по определенному правилу — первый truthy объект либо последний объект, если truthyобъекты в логическом выражении не найдены.

Аналогично дело обстоит с оператором and.

Приведенный ниже код:

print(None and 10)
print(5 and 0.0)
print('beegeek' and {})
print([1, 2, 3] and [6, 9])

print(1 and 'beegeek' and None)
print('habr' and 0 and {'one': 1})
print(10 and [6, 9] and [])

выводит:

None
0.0
{}
[6, 9]
None
0
[]

Оператор and возвращает первый falsy объект либо последний объект, если falsy объекты в логическом выражении не найдены.

Логические операторы ленивы

Логические операторы в Python являются ленивыми. Это означает, что возвращаемый операнд вычисляется путем оценки истинности всех операндов слева направо до тех пор, пока это остается актуальным:

  • если левый операнд оператора or является truthy объектом, то общим результатом логического выражения является True, независимо от значения правого операнда

  • если левый операнд оператора and является falsy объектом, то общим результатом логического выражения является False, независимо от значения правого операнда

Данный механизм называется вычислением по короткой схеме (short-circuit evaluation) и используется интерпретатором для оптимизации вычислений. Рассмотрим наглядный пример, демонстрирующий данное поведение.

Приведенный ниже код:

def f():
    print('bee')
    return 3
  
if True or f():
    print('geek')

выводит:

geek

Левым операндом оператора or является truthy объект (значение True), значит, для вычисления общего результата логического выражения нет необходимости вычислять правый операнд, то есть вызывать функцию f(). Поскольку вызова функции не происходит, в выводе отсутствует строка bee. Общим результатом логического выражения является значение True, а значит, выполняются инструкции блока кода условного оператора, и в выводе мы видим строку geek.

Напротив, приведенный ниже код:

def f():
    print('bee')
    return 3
  
if True and f():
    print('geek')

выводит:

bee
geek

Левым операндом оператора and является truthy объект (значение True), значит, для вычисления общего результата логического выражения необходимо вычислить и правый операнд, то есть вызвать функцию f(). В результате вызова выполняются инструкции из тела функции, поэтому в выводе мы видим строку bee. Функция возвращает число 3, которое также является truthy объектом. Таким образом, общим результатом логического выражения является число 3, а значит, выполняются инструкции блока кода условного оператора, и в выводе мы видим строку geek.

Приоритет логических операторов

Важно помнить о приоритете логических операторов. Ниже логические операторы представлены в порядке уменьшения приоритета (сверху вниз):

  1. not

  2. and

  3. or

Согласно приоритету логических операторов приведенный ниже код:

a = 0
b = 7
c = 10
print(not a and b or not c)        # 7

эквивалентен следующему:

a = 0
b = 7
c = 10
print(((not a) and b) or (not c))  # 7

По отношению к другим операторам Python (за исключением оператора присваивания =) логические операторы имеют самый низкий приоритет.

Например, приведенный ниже код:

a = 5
b = 7
print(not a == b)  # True

эквивалентен следующему:

a = 5
b = 7
print(not (a == b))  # True

Отметим, что запись вида:

a = 5
b = 7
print(a == not b)

недопустима и приводит к возбуждению исключения SyntaxError.

Для большей наглядности рассмотрим подробно другой пример.

Приведенный ниже код:

print(not 1 == 2 or 3 == 3 and 5 == 6)  

выводит:

True

Согласно приоритету операторов в первую очередь вычисляются выражения 1 == 2, 3 == 3 и 5 == 6, в результате чего исходное выражение принимает вид not False or True and False. Далее выполняется оператор not, возвращая значение True, после него — оператор and, возвращая значение False. Выражение принимает вид True or False. Последним выполняется оператор or, возвращая общий результат выражения — значение True.

Цепочки сравнений

Иногда нам требуется объединить операции сравнения в цепочку сравнений.

Рассмотрим программный код:

a = 5
b = 10
c = 15

print(a < b < c)        # True    
print(a < b and b < c)  # True

Выражения a < b < c и a < b and b < c представляют собой сокращенный и расширенный варианты записи цепочки сравнений и являются эквивалентными, так как на самом деле для объединения сравнений в сокращенном выражении a < b < c оператор and используется неявно

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

Приведенный ниже код:

def f():
    print('bee')
    return 3

if 5 < 1 < f():
    print('geek')
else:
    print('beegeek')

выводит:

beegeek

В примере выше выражение 5 < 1 < f() эквивалентно выражению 5 < 1 and 1 < f(). Сравнение 5 < 1 возвращает False. В результате сравнение 1 < f() не выполняется, и функция f() не вызывается.

Тем не менее между сокращенным и расширенным вариантами записи цепочек сравнений существует важное отличие.

Приведенный ниже код:

def f():
    print('bee')
    return 3

if 1 < f() < 5:
    print('geek')

выводит:

bee
geek

в то время как приведенный ниже код:

def f():
    print('bee')
    return 3

if 1 < f() and f() < 5:
    print('geek')

выводит:

bee
bee
geek

Как мы видим, в сокращенном выражении 1 < f() < 5 функция f() вызывается только один раз, а в расширенном выражении 1 < f() and f() < 5 — два раза. Данную особенность важно учитывать, когда операнд, участвующий в сравнении, возвращает непостоянный результат. 

Например, приведенный ниже код:

from random import randint

def f():
    x = randint(1, 7)
    print(x)
    return x

print(1 < f() < 5)
print(1 < f() and f() < 5)

выводит (результат может отличаться):

4
True
7
5
False

В примере выше в сокращенной записи функция f() вызывается один раз и возвращает значение 4. Однако в расширенной записи функция f() вызывается дважды, возвращая разные значения (7 и 5). Поэтому в данном случае выражения 1 < f() < 5 и 1 < f() and f() < 5 не являются эквивалентными.

Помимо операторов сравнения, в цепочку операторов могут объединяться и другие операторы Python. При этом в некоторых случаях мы можем столкнуться с неожиданным поведением программы из-за аналогичного неявного вызова оператора and.

Например, приведенный ниже код:

lst = [1, 2, 3]
num = 2

print(num in lst == True)

выводит:

False

Можно подумать, что результатом выражения num in lst == True должно быть значение True, однако это не так. Дело в том, что данное выражение на самом деле эквивалентно выражению num in lst and lst == True, которое, в свою очередь, эквивалентно выражению True and False. Следовательно, результатом данной цепочки операторов является значение False.

Рассмотрим еще два примера с неожиданным поведением.

Приведенный ниже код:

a = 5
b = 5
c = 10

print(a < c is True)        
print(a == b in [True])

эквивалентен коду:

a = 5
b = 5
c = 10

print(a < c and c is True)
print(a == b and b in [True])

и выводит:

False
False

Подведем итоги

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

Логические операторы and и or являются ленивыми операторами. Они возвращают один из своих операндов, реализуя вычисления по короткой схеме. В цепочках операторов оператор and может использоваться неявно. Об этом всегда стоит помнить при объединении различных операторов в одно выражение.

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

❤️ Happy Pythoning! ?

Комментарии (31)