Логические операции играют важную роль в программировании. Они используются для создания условных конструкций и составления сложных алгоритмов. В Python для выполнения логических операций используются логические операторы:
-
not
— логическое отрицание -
and
— логическое умножение -
or
— логическое сложение
В этой статье мы поговорим о неочевидных деталях и скрытых особенностях работы логических операторов в Python.
Таблицы истинности логических операторов
Мы привыкли к тому, что обычно в языках программирования логические операторы возвращают значения True
или False
согласно своим таблицам истинности.
Таблица истинности оператора not
:
a |
not a |
---|---|
|
|
|
|
Таблица истинности оператора and
:
a |
b |
a and b |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Таблица истинности оператора or
:
a |
b |
a or b |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Когда операндами логических операторов являются объекты 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
объектами, а объекты, которые оцениваются как False
— falsy
объектами.
К встроенным 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
.
Приоритет логических операторов
Важно помнить о приоритете логических операторов. Ниже логические операторы представлены в порядке уменьшения приоритета (сверху вниз):
-
not
-
and
-
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!🐍
Автор: Тимур Гуев