View on GitHub

Methodics of Python Intro

Practical materials for learning Python

Данная глава посвящена логическим выражениям, условным операторам и циклам. Говоря о логике в 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 построена алгебра логики, представленная тремя классическими логическими операциями:

Таблица алгебры логики вытекает отсюда сама собой:

| | 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 для цикла:

>>> 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  
>>>