Метка: подводные камни

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

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

Поговорим о логических операциях. Допустим у нас есть цепочка из 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 👈 

Умножение списка на число

Студент Макс узнал, что в Python умножать можно не только числа, но и другие объекты, например, строку на число:

>>> "Max" * 3
'MaxMaxMax'

«Вау!» — подумал Макс — «А что если умножить список на число?»:

>>> [42, 26] * 3
[42, 26, 42, 26, 42, 26]

Значит можно создать двумерный массив очень кратко и элегантно?

>>> [[]] * 3
[[], [], []]

Заполнить его:

arr = [[]] * 3
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)

Макс ожидал получить:

[[10], [20], [30]]

А вышло:

[[10, 20, 30], [10, 20, 30], [10, 20, 30]]

😯 Как же так?! Дело в том, что умножение списка на число не копирует сам объект, а лишь ссылку на него. Все три элемента arr ссылаются на один и тот же список. Легко проверить, сравнив адреса объектов:

>>> arr[0] is arr[1]
True
>>> id(arr[0]), id(arr[1])
(4400840776, 4400840776)
Диаграмма: все элементы arr указывают на один и тот же список.

Аналогично в случае классов:

class Dummy: ...
arr = [Dummy()] * 2
arr[0].x = 10
arr[1].x = 20
print(arr[0].x, arr[0] is arr[1])  # 20 True

А вот с числами, строками и кортежами умножение списка будет работать как ожидал Макс, потому что это неизменяемые типы. Вот такая тонкость, которую нужно знать. Максу следовало бы написать так:

arr = [[] for _ in range(3)]  
arr[0].append(10)
arr[1].append(20)
arr[2].append(30)
>>> arr
[[10], [20], [30]]

Менее кратко, но зато работает без сюрпризов: каждую итерацию создается новый пустой список.

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