Данная глава посвящена логическим выражениям, условным операторам и циклам. Говоря о логике в Python, хочется отдельно сказать про элегантность и изящество, с которым разработчики подошли при решении даже таких простых вещей. Логические выражения заполоняют Python всюду, на них строятся достаточно сложные конструкции, производятся вычисления и основываются решения интерпретатора о состоянии объектов.
Быстрый поиск
Алгебра логики
Как и в большинстве языков, результат логического выражения в Python имеет отдельный логический тип bool, представленный двумя объектами — True и False:
>>> 213 == 234
False
>>> res = 123 < 67
>>> res
False
>>> type(res)
<class 'bool'>
>>>
Из Си в Python перетянулось соответствие bool числовому протоколу — тип обладает всеми свойствами, которые, по правилам Python, обязаны быть у числовых типов.
>>> res + 1
1
>>> int(True)
1
>>> int(False)
0
>>>
В частности, объект типа bool обладает методами, свойственными числовым типам (строго говоря, это почти наверное достаточное требование; подробнее о протоколах будет рассказано в будущих главах)
>>> dir(res)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
>>>
Над объектами типа bool построена алгебра логики, представленная тремя классическими логическими операциями:
- not — отрицание,
- and — логическое «И», конъюнкция,
- or — логическое «ИЛИ», дизъюнкция.
Таблица алгебры логики вытекает отсюда сама собой:
| | or | | | | and | | | —– | —— | ————– | — | —– | ——- | ————— | | A | B | A or B | | A | B | A and B | | False | False | False | | False | False | False | | False | True | True | | False | True | False | | True | False | True | | True | False | False | | True | True | True | | True | True | True | У логических операций приоритет ниже, чем у арифметических.
Особенность логической интерпретации объектов в Python
При работе с объектами не всегда удобно явно задавать объекты типа bool. Здесь открывается одна особенность Python: все объекты могут быть проинтерпретированы как «пустые» и «непустые», и с ними могут быть связаны значения True или False соответственно.
>>> not 123
False
>>> not 0
True
>>> not "q2w3er"
False
>>> not ""
True
>>> not {}
True
>>>
Насколько все объекты могут быть проинтерпретированы — даже те, которые не имеют метода __bool__, например, функции:
>>> def mul(a,b):
... return a * b
...
>>> mul
<function mul at 0x7fe0632c3ce0>
>>> if mul:
... print("YES")
...
YES
>>> dir(mul)
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__','__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__']
>>> bool(mul)
True
>>>
Кроме всегда «верных» типов объектов существуют и всегда «ложные». К таким типам относится, в частности, None:
>>> type(None)
<class 'NoneType'>
>>> dir(None)
['__bool__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> bool(None)
False
>>>
Важно, что при сравнении объектов возвращаемым значением становятся не булевы значения, а сами объекты в зависимости от того, на котором из них стало однозначным определение значения всего выражения.
| or | and | |||||
|---|---|---|---|---|---|---|
| A | B | A or B | A | B | A and B | |
| Пусто | Пусто | B | Пусто | Пусто | A | |
| Пусто | Непусто | B | Пусто | Непусто | A | |
| Непусто | Пусто | A | Непусто | Пусто | B | |
| Непусто | Непусто | A | Непусто | Непусто | B |
Эта логика буквально построена и описывает ленивые вычисления: при однозначности результата выражения оставшиеся операнды не вычисляются:
>>> 123 and "WER"
'WER'
>>> 123 or "WER"
123
>>> 0 or "WER"
'WER'
>>> 0 and "WER"
0
>>>
За счёт ленивой логики становятся возможными частичные вычисления, позволяющие писать конструкции, некорректные на первый взгляд. Рассмотрим пример вычисления частного двух чисел. В общем случае необходимо вводить отдельную проверку на ненулевое значение знаменателя. Но благодаря ленивым вычислениям размер кода можно резко уменьшить, получив аналог условного оператора:
>>> a, b = 1, 2
>>> if b != 0:
... print(a/b)
... else:
... print("Zero Division")
...
0.5
>>> a, b = 1, 0
>>> if b != 0:
... print(a/b)
... else:
... print("Zero Division")
...
Zero Division
>>> a, b = 1, 2
>>> print(b and a/b or "Zero Division")
0.5
>>> a, b = 1, 0
>>> print(b and a/b or "Zero Division")
Zero Division
>>>
Однако у такого «условного оператора» есть проблема: при «пустом» значении выражения в «if-блоке» в выражении продолжается вычисление и, соответственно, выполнение «else-блока»:
>>> a, b = 0, 2
>>> print(b and a/b or "Zero Division")
Zero Division
>>>
Во избежание данных проблем правильнее использовать «тернарный оператор», представленный в формате, похожем на промежуточный вариант между логическим условным выражением и условным оператором:
>>> a, b = 0, 2
>>> print(a/b if b else "Zero Division")
0.0
>>>
Сравнения
В Python, естественно, существуют базовые операции сравнения. Операции сравнения возвращают исключительно True-False.
К операциям сравнения относится и проверка идентичности c помощью is:
>>> a = b = 12345
>>> c = 12345
>>> a == b
True
>>> a == c
True
>>> a is b
True
>>> a is c
False
>>>
Без вызова id или использования is различить равные объекты невозможно.
К операциям сравнения также относится проверка на принадлежность in:
>>> 2 in [1, 2, 3, 4, 5]
True
>>> 12 in [1, 2, 3, 4, 5]
False
>>>
Многоместные сравнения
Одна из ярко выделяющих Python особенность языка — многоместные сравнения. Они имеют вид множественных «последовательных» сравнений объектов.
Почему же в кавычках? Попробуем на простом примере разобрать семантику и логику этих выражений:
>>> 1 < 2 < 3
True
>>> (1 < 2) < 3
True
>>> 3 > 2 > 1
True
>>> (3 > 2) > 1
False
>>>
Си-интерпретация данных выражений оказалось неправильной: здесь не зашифрованы зависимые сравнения. На самом деле такой формат сравнений — синтаксический сахар для множества сравнений через and:
>>> 3 > 2 > 1
True
>>> 3 > 2 and 2 > 1
True
>>>
И, поскольку сравнения друг на друге не завязаны, многоместные операции могут содержать разные операции сравнения:
>>> 1 < 2 > -2
True
>>>
Условные действия
Мы уже упомянули как логические частичные вычисления (Condition and True_action or False_action) , так и «тернарный оператор» (который в Python правильнее называть условной операцией) (True_Value if Condition else False_Value). Оба варианта подходят для быстрых вычислений на месте:
>>> a, b, c = 1,2, 3
>>> a if b else c
1
>>> (a if b else c) << 4
16
>>> bin((a if b else c) << 4)
'0b10000'
>>>
Условные операторы в Python также есть. При этом у if-клаузы могут быть как итоговая альтернатива else, так и промежуточные альтернативы elif:
>>> a = 2
>>> if a > 1:
... print("qqq")
...
qqq
>>> a = 0
>>> if a > 1:
... print("qqq")
... else:
... print("AAA")
...
AAA
>>> a = 3
>>> if a > 4:
... print("qqq")
... elif a > 2:
... print("QKRQ")
... else:
... print("AAA")
...
QKRQ
>>>
Заметим, что в Python для выделения вложенных блоков кода операторных скобок нет, здесь их роль играют отступы, что делает код визуально более понятным.
Оператор сопоставления (введение)
Вспоминая Си и его оператор switch-case, в Python можно обнаружить похожий на него визуально оператор сопоставления match-case. Вообще говоря, это отдельный сложный оператор сопоставления шаблонов со своим непитоновским синтаксисом. Однако в самом простом случае match-case может играть роль switch-case.
Из достаточно простых особенностей данного оператора можно выделить возможность не просто задавать явные значения параметров сравнения, но и описывать условия над связанными переменными:
>>> a = "w"
>>> match a:
... case "":
... print("Empty")
... case "a":
... print("Just a")
... case var if len(var) > 3:
... print("Long smth")
... case _:
... print("CHto-to")
...
CHto-to
>>> a = ""
>>> match a:
... case "":
... print("Empty")
... case "a":
... print("Just a")
... case var if len(var) > 3:
... print("Long smth")
... case _:
... print("CHto-to")
...
Empty
>>> a = "a"
>>> match a:
... case "":
... print("Empty")
... case "a":
... print("Just a")
... case var if len(var) > 3:
... print("Long smth")
... case _:
... print("CHto-to")
...
Just a
>>> a = "12345"
>>> match a:
... case "":
... print("Empty")
... case "a":
... print("Just a")
... case var if len(var) > 3:
... print("Long smth")
... case _:
... print("CHto-to")
...
Long smth
>>>
При этом, поскольку в Python при работе с вложенными блоками в общем случае не создаётся отдельного локального пространства имён, в глобальном пространстве имён возникают связанные переменные:
>>> var
'12345'
>>>
Цикл с предусловием
В Python циклы в привычном понимании нами этой структуры представлены одним вариантом while с предусловием (формат с постусловием множество раз обсуждался сообществом в PEP-ах, однако всегда отклонялся; реализация построчной обработки циклов такого формата затруднительна, как минимум, со стороны правильной интерпретации и анализом текста программы).
Цикл всегда должен иметь 4 основных элемента:
инициализация
while условие:
тело
изменение условий
Отсутствие какого-либо из этих элементов ведёт к точно некорректной работе программы, как, например, в коде ниже: инициализация внутреннего цикла на шагах i > 0 отсутствует.
>>> i, j = 1, 2
>>> while i < 4:
... while j < 4:
... print(i, j)
... j += 1
... i += 1
...
1 2
1 3
>>>
Как и любые базовые циклы в Си, while поддерживает работу с операторами break и continue. Однако вечная проблема при работе с данными операторами — неразличимость выхода из цикла по условию while или по break.
>>> a = "QWE"
>>> while a:
... if a == "QQQ":
... break
... print(a)
... a = input()
...
QWE
qwer
qwer
awerf
awerf
>>> a = "QWE"
>>> while a:
... if a == "QQQ":
... break
... print(a)
... a = input()
...
QWE
QQQ
>>>
В обычных языках программирования для решения используют флаговые переменные. Python же предлагает клаузу else для цикла:
- Если выход из цикла произошёл по условию, после выполнения цикла срабатывает клауза else;
- Если выход из цикла был сделан через break, клауза else будет проигнорирована.
>>> a = "QWE"
>>> while a:
... if a == "QQQ":
... break
... print(a)
... a = input()
... else:
... print("no QQQ")
...
QWE
qwer
qwer
qwer
qwer
no QQQ
>>> a = "QWE"
>>> while a:
... if a == "QQQ":
... break
... print(a)
... a = input()
... else:
... print("no QQQ")
...
QWE
QQQ
>>>
Работа с циклом while — одно из ключевых мест использования walrus operator := для объединения инициализации, условия и изменения условия в циклах по вводу:
>>> while (a := input()) != "QUIT":
... print(a)
...
qwer
qwer
asdf
asdf
QUIT
>>>