Короткое замыкание

Провод горит в розетке

Поговорим о логических операциях. Допустим у нас есть цепочка из or

if x() or y() or z():
    print('bingo!')

Чтобы print сработал, нужно, чтобы хотя бы один из трех вызовов давал бы True (или приводился к True). Что если x() сразу вернет True? Тогда, очевидно, все выражение будет равняться True в любом случае и независимо от того, что будет в y() и z(). Если смысл их вычислять? Нет! Python и не вычисляет. Тем самым достигается некоторая оптимизация, которая называется short circuiting (или короткое замыкание).

Это хорошо, только, если в оставшихся логических выражениях нет побочных эффектов, потому они не будут выполнены, если вычисление логического выражение будет остановлено. Давайте проверим:

def check(b):
    print('check.')
    return b

if True or check(True):
    print('ok.')  # ok.

if False or check(True):
    print('ok.')  # check. ok.

В первом случае check не работает, потому что первый операнд True уже предопределит судьбу выражения. А во втором случае – сработает, потому первый операнд False не дает определенности и нужно вычислить check().

Аналогично все с оператором and: как только первый операнд в цепочке вернет False, выполнение прекратиться. 

if True and False and check(True):
    ...  # не выполнится check

Встроенные функции all и any тоже используют короткое замыкание, то есть all перестает проверять на первом False, а any – на первом True.

all(check(i) for i in [1, 1, 0, 1, 1])  # выведет 3 check из 5
any(check(i) for i in [0, 1, 0, 0, 0])  # выведет 2 check из 5

Эту особенность стоит помнить. Лично я сталкивался с алгоритмом, где было что-то вроде:

while step(x, y, True) or step(x, y, False): ...

По задумке оба step должны выполнятся на каждой итерации, но из-за короткого замыкания второй из них иногда не выполнялся; алгоритм работал неверно.

Что если не нужно такое поведение?

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

def check(b):
    print('check.')
    return b

check(False) & check(False)  # & – битовое и
check(True) | check(False)   # | - битовое или

В этом случае оба check сработают!

❗Внимание: есть подводные камни. Этот прием работает корректно только с булевыми типами! Если мы подставим целые числа, то результат может быть не тот, что ожидается. Яркий пример – это числа 1 и 2:

>>> bool(1 and 2)
True
>>> bool(1 & 2)
False
>>> 1 & 2
0

Поэтому, в логическом выражении, если тип операнда не булев, то его нужно привести. Недавний пример должен быть переписан так:

while bool(step(x, y, True)) | bool(step(x, y, False)):
    ...

Второй подводный камень: приоритет операторов | и & гораздо выше, чем у not, and и or. Так что, если миксуем их, то всегда ставим скобки: 

>>> not False or True
True
>>> not False | True
False
>>> (not False) | True
True

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

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Добавить комментарий