
Логика
Логические операции. Условные конструкции
Штана Альберт Игоревич
Логика
Кроме арифметических операций в математике есть операции сравнения, например, 6 > 2 или 5 < 10. Они есть и в программировании. Допустим, когда мы заходим на сайт, введенные логин и пароль сравниваются с теми, какие есть в базе. Если они есть, нас пускают внутрь — аутентифицируют. Языки программирования адаптировали все математические операции сравнения в неизменном виде, кроме операторов равенства и неравенства. В математике для этого используется обычное равно =, но в программировании такое встречается редко. Во многих языках символ = используется, чтобы присвоить переменным значения. Поэтому в Python сравнивают с помощью ==.
Список операций сравнения:
- < — меньше
- <= — меньше или равно
- > — больше
- >= — больше или равно
- == — равно
- != — не равно
Эти операции применяются не только к числам. Например, с помощью оператора равенства можно сравнить строки: password == text — это сравнение идентичности строк, которые записаны в разных переменных.
Логический тип
Логическая операция типа 6 > 3 или password == text — это выражение. Его результат — специальное значение True («истина») или False («ложь»). Это тип данных — bool.
result = 6 > 3
print(result) # => True
print('two' != 'two') # => False
Наряду со строками (str) и целыми и рациональными числами, тип bool (булев) — это один из примитивных типов данных в Python. Попробуем написать простую функцию, которая принимает на вход возраст ребенка и определяет, младенец ли он. Младенцами считаются дети до года:
def is_infant(age):
return age < 1
print(is_infant(3)) # => False
Любая операция — это выражение, поэтому единственной строчкой функции пишем: «Вернуть то значение, которое получится в результате сравнения age < 1». В зависимости от того, какой аргумент пришел, сравнение будет истинным (True) или ложным (False), а return вернет этот результат. А теперь проверим ребенка, которому полгода(напишем вызов той же функции только с другим значением):
print(is_infant(0.5)) # => True
Результат операции — True. Значит, ребенок в возрасте пол года — действительно младенец.
Предикаты
Функция is_infant() — это функция-предикат или функция-вопрос. Предикат отвечает на вопрос «да» или «нет», возвращая значение типа bool. Предикаты во всех языках принято именовать особым образом для простоты анализа. В Python предикаты начинаются с префикса is или has, например:
- is_infant() — «младенец ли?»
- has_children() — «есть ли дети?»
- is_empty() — «пустой ли?»
- has_errors() — «есть ли ошибки?»
Функция считается предикатом, если она возвращает булевы значения True или False. Напишем еще одну функцию-предикат. Она принимает строку и проверяет, является ли она словом 'Python':
def is_python(string):
return string.is_capitalize() == 'Python'
print(is_python('C#'))
Комбинирование операций и функций
Логические операции — это выражения, значит, их можно комбинировать с другими выражениями. Например, мы хотим проверить четность числа — кратность двум. В программировании используют такой подход — проверяют остаток от деления на два:
- если остаток 0 — число четное
- если остаток не 0 — число нечетное
Остаток от деления — простая, но важная концепция в арифметике, алгебре, в теории чисел и криптографии. Нужно разделить число на несколько равных групп, и если в конце что-то останется — это остаток от деления. Делим конфеты поровну между людьми:
- 7 конфет, 2 человека: 2 x 3 + остаток 1 — 7 не кратно 2
- 21 конфету, 3 человека: 3 x 7 + остаток 0 — 21 кратно 3
- 19 конфет, 5 человек: 5 x 3 + остаток 4 — 19 не кратно 5
Оператор % вычисляет остаток от деления:
- 7 % 2 → 1
- 21 % 3 → 0
- 19 % 5 → 4
Скомбинируем в одном выражении логический оператор «проверка равенства» == и арифметический оператор % и напишем функцию проверки четности:
def is_even(number):
return number % 2 == 0
print(is_even(8)) # => True
print(is_even(5)) # => False
Приоритет арифметических операций выше логических. Значит, сначала вычисляется арифметическое выражение number % 2, а затем результат сравнивается с нулем и возвращается результат проверки равенства. Теперь напишем функцию, которая принимает строку и проверяет, начинается ли эта строка с латинской буквы A. Алгоритм:
- Получить и записать в переменную первый символ из строки-аргумента;
- Сравнить, равен ли символ латинской букве A;
- Вернуть результат.
def is_first_letter_an_a(string):
first_letter = string[0]
return first_letter == 'A'
print(is_first_letter_an_a('Orange')) # => False
print(is_first_letter_an_a('Apple')) # => True-
Чтобы было понятно, что тут происходит, попробуйте проговорить происходящее аналогично тому, как мы расшифровывали процесс в примере с is_even(). Теперь вы знаете, что операции сравнения применяются в программировании наравне с арифметическими. Но помните, что равенство обозначается ==.
Логические операторы
Логические операторы нужны, чтобы составлять сложные логические условия. Предположим, что сайт при регистрации требует, чтобы пароль был длиннее восьми символов и короче двадцати. В математике бы написали 8 < x < 20, но во многих языках программирования так сделать нельзя. Попробуем написать два отдельных логических выражения и соединим их специальным оператором «И»:
Пароль длиннее 8 символов И пароль короче 20 символов
Напишем функцию, которая принимает пароль и говорит, соответствует ли он условиям (True) или не соответствует (False):
def is_correct_password(password):
length = len(password)
return length > 8 and length < 20
print(is_correct_password('qwerty')) # => False
print(is_correct_password('qwerty1234')) # => True
print(is_correct_password('zxcvbnmasdfghjkqwertyui')) # => False
and — означает «И». В математической логике это называют конъюнкцией.
Все выражение считается истинным, если истинен каждый операнд — каждое из составных выражений.
Иными словами, and означает «и то, и другое». Приоритет этого оператора ниже, чем приоритет операторов сравнения.
Поэтому выражение length > 8 and length < 20 правильно отрабатывает без скобок.
Кроме and часто используется оператор or — «ИЛИ» (дизъюнкция).
Он означает «или то, или другое, или оба». Выражение a or b считается истинным, если хотя бы один из операндов или одновременно все — истинные.
В другом случае выражение ложное. Операторы можно комбинировать в любом количестве и любой последовательности.
Если в коде одновременно встречаются and и or, то приоритет задают скобками.
Ниже пример расширенной функции, которая определяет корректность пароля:
import string
def has_special_chars(string):
# Проверяет, есть ли специальные символы в строке
# ...
def is_strong_password(password):
length = len(password)
# Скобки задают приоритет. Понятно, что к чему относится.
return (length > 8 and length < 20) and has_special_chars(password)
Теперь представим, что мы хотим купить квартиру, которая удовлетворяет таким условиям: площадь от 100 квадратных метров и больше на любой улице ИЛИ площадь от 80 квадратных метров и больше, но на центральной улице Main Street. Напишем функцию, которая проверит квартиру. Она принимает два аргумента: площадь — число и название улицы — строку:
def is_good_apartment(area, street):
return area >= 100 or (area >= 80 and street == 'Main Street')
print(is_good_apartment(91, 'Queens Street')) # => False
print(is_good_apartment(78, 'Queens Street')) # => False
print(is_good_apartment(70, 'Main Street')) # => False
print(is_good_apartment(120, 'Queens Street')) # => True
print(is_good_apartment(120, 'Main Street')) # => True
print(is_good_apartment(80, 'Main Street')) # => True
Раздел математики, в котором изучаются логические операторы, называется булевой алгеброй. Ниже увидите таблицы истинности — по ним можно определить, каким будет результат, если применить оператор.
И and
| A | B | A and B |
|---|---|---|
| True | True | True |
| True | False | False |
| False | True | False |
| False | False | False |
ИЛИ or
| A | B | A or B |
|---|---|---|
| True | True | True |
| True | False | True |
| False | True | True |
| False | False | False |
Отрицание not
Наряду с логическими операторами И и ИЛИ, часто используется операция «инверсия».
Она меняет логическое значение на противоположное.
В программировании отрицанию соответствует унарный оператор not или ! знак:
not True # False
not False # True
Например, если есть функция, которая проверяет четность числа, то с помощью её отрицания можно выполнить проверку нечетности:
def is_even(number):
return number % 2 == 0
print(is_even(10)) # => True
print(not is_even(10)) # => False
В примере выше мы добавили not слева от вызова функции и получили обратное действие.
Отрицание — инструмент, с помощью которого можно выражать задуманные правила в коде и не писать новые функции.
Если написать not not is_even(10), то код сработает даже в таком случае:
print(not not is_even(10)) # => True
В логике двойное отрицание — это отсутствие отрицания:
not not True # True
not not False # False
print(not not is_even(10)) # => True
print(not not is_even(11)) # => False
Итак, с помощью логических операторов можно задавать составные условия из двух и более логических выражений. Более, подробнее по алгебру логики можно почитать по ссылкам: Законы алгебры логики
Результаты логических операций
Посмотрим на правила преобразования и составные выражения с двойным отрицанием.
Правила преобразования
Посмотрите на пример:
print(0 or 1) ## 1
В данном случае число 0 эквивалентно False, а число 1 эквивалентно True. Таким образом, оператор ИЛИ вернет 1, так как это первый аргумент, который может быть преобразован в True. Возьмем пример посложнее:
print(0 or False or '' or [] or 42 or "Hello") ## 42
В данном случае:
- Число 0 эквивалентно False
- Значение False уже является False
- Пустая строка ('') эквивалентна False
- Пустой список эквивалентен False
- Число 42 эквивалентно True
- Строка "Hello" также эквивалентна True
Оператор ИЛИ проверяет значения слева направо, и возвращает первый аргумент, который может быть преобразован в True. В данном примере это число 42. Если ни одно значение не подходит, то вернется последнее значение в цепочке проверок. Пример с оператором И:
print(0 and 1) ## 0
Оператор И работает так, что его выполнение слева направо прерывается и возвращается результат первого аргумента, который можно преобразовать в False. Если такого аргумента нет, возвращается последний — правый.
print(42 and "Hello" and [] and 0) ## []
В данном случае:
- Число 42 эквивалентно True
- Строка "Hello" эквивалентна True
- Пустой список () эквивалентен False
- Число 0 эквивалентно False
Оператор И будет проверять значения слева направо и возвращать первый аргумент, который может быть преобразован в False. В данном примере это пустой список ().
В Python есть два правила преобразования:
- 0, 0.0, '' и None приводятся к False. Эти значения называют falsy.
- Все остальное приводится к True
Этими правилами пользуются в разработке, например, чтобы определить значение по умолчанию:
value = name or ''
# Примеры
234 or '' # 234
'python' or '' # 'python'
None or '' # ''
Если name примет одно из falsy-значений, переменной value будет присвоена пустая строка. В этом случае в последующем коде мы сможем работать с value как со строкой. Но здесь есть потенциальный баг. Если name содержит falsy значение, а переменной value можно присвоить значения типа 0, False, None, то код выше заработает неверно:
# Значение на самом деле есть,
# но оно Falsy, поэтому не выбирается на условии OR
False or '' # ''
0 or '' # ''
None or '' # ''
Составные выражения
Если соединить логические выражения между собой, можно получить довольно интересные способы решения задач с кодом. Допустим, нам нужно реализовать код, в котором в переменную записывается:
- Строка yes, если число четное
- Строка no, если нечетное
Это можно сделать, если использовать знания, полученные выше:
# число четное
result = 10 % 2 == 0 and 'yes' or 'no' # 'yes'
# или сразу печатаем на экране
print(10 % 2 == 0 and 'yes' or 'no') # => 'yes'
# число нечетное
print(11 % 2 == 0 and 'yes' or 'no') # => 'no'
Эти выражения работают согласно порядку и приоритетам. Приоритет присваивания самый низкий, поэтому оно происходит в конце. Приоритет сравнения == выше, чем приоритет логических операторов and и or, поэтому сравнение происходит раньше. Дальше код выполняется слева направо, так как приоритет and выше, чем приоритет or. Рассмотрим по шагам:
# Для четного
# 1 шаг
10 % 2 == 0 # True
# 2 шаг
True and 'yes' # Результат — 'yes'
# 3 шаг
'yes' or 'no' # Проверка на or выполняется, но правая часть не исполняется, так как сразу возвращается 'yes'
# Для нечетного
# 1 шаг
11 % 2 == 0 # False
# 2 шаг
False and 'yes' # Результат — ложь, проверяем дальше
# 3 шаг
False or 'no' # Выбирается и возвращается 'no'
Такую же схему можно использовать с любым выражением в начале:
print(somefunc() and 'yes' or 'no')
Двойное отрицание
Вспомним, как выглядит операция отрицания:
answer = True
print(not answer) # => False
При двойном отрицании итоговое значение равно начальному:
answer = True
print(not not answer) # => True
Оператор not всегда возвращает булево значение, независимо от типа переданного аргумента, а не заменяет значение на противоположное.
Поэтому двойное отрицание тоже вернет булево True/False.
answer = 'python'
print(not answer) # => False
print(not not answer) # => True
Ошибки выбора
Представьте, что нам нужно проверить, равно ли значение одному или другому. Например, переменная value должна содержать одно из двух значений: first или second. Начинающие разработчики иногда записывают это выражение так:
value == ('first' or 'second')
Однако такой код приведет к неверному результату. Необходимо вспомнить приоритет выполнения операций. Первым делом вычисляется все, что указано в скобках — 'first' or 'second'. Если выполнить этот код, то вывод будет таким:
>>> 'first' or 'second'
'first'
Теперь заменим исходное выражение на частично вычисленное:
value == 'first'
Совсем не то, что мы ожидали. А теперь вернемся к началу и напишем проверку правильно:
# Скобки ставить не обязательно,
# потому что приоритет == выше, чем приоритет or
value == 'first' or value == 'second'
Условные конструкции
С помощью условных конструкций можно изменить поведение программы, которое будет зависеть от проверяемых условий.
Если if
Для примера рассмотрим функцию, которая определяет тип переданного предложения. Для начала она будет отличать повествовательные предложения от вопросительных:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
if last_char == '?':
return 'question'
return 'normal'
print(get_type_of_sentence('Python')) # => normal
print(get_type_of_sentence('Python?')) # => question
if — конструкция языка, которая управляет порядком выполнения инструкций.
После слова if ей передается выражение-предикат, и в конце ставится двоеточие.
После этого идет блок кода. Он выполнится, если предикат — истина.
Если предикат — ложь, то блок кода пропускается, и функция выполняется дальше.
В нашем случае следующая строчка кода — return 'normal' — заставит функцию вернуть строку и завершиться.
return может находиться в любом месте функции — даже внутри блока кода с условием.
Иначе else
Теперь изменим функцию из предыдущего примера так, чтобы она возвращала не просто тип предложения, а целую строку Sentence is normal или Sentence is question:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
if last_char == '?':
sentence_type = 'question'
else:
sentence_type = 'normal'
return "Sentence is " + sentence_type
print(get_type_of_sentence('Python')) # => 'Sentence is normal'
print(get_type_of_sentence('Python?')) # => 'Sentence is question'
Мы добавили else и новый блок. Он выполнится, если условие в if — ложь. Еще в блок else можно вкладывать другие условия if.
Else переводится «иначе», «в ином случае».
Оформить конструкцию if-else можно двумя способами. С помощью отрицания можно изменить порядок блоков:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
if last_char != '?':
sentence_type = 'normal'
else:
sentence_type = 'question'
return "Sentence is " + sentence_type
Чтобы конструкцию было легче оформлять, старайтесь выбирать проверку без отрицаний и подстраивайте содержимое блоков под нее.
Конструкция else + if = elif
Функция get_type_of_sentence() различает только вопросительные и повествовательные предложения. Добавим в нее поддержку восклицательных предложений:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
if last_char == '?':
sentence_type = 'question'
if last_char == '!':
sentence_type = 'exclamation'
else:
sentence_type = 'normal'
return 'Sentence is ' + sentence_type
print(get_type_of_sentence('Who?')) # => 'Sentence is normal'
print(get_type_of_sentence('No')) # => 'Sentence is normal'
print(get_type_of_sentence('No!')) # => 'Sentence is exclamation'
Мы добавили проверку восклицательных предложений — exclamation. Технически эта функция работает, но вопросительные предложения трактует неверно. Еще в ней есть проблемы с точки зрения семантики:
- Наличие восклицательного знака проверяется в любом случае, даже если уже обнаружился вопросительный знак;
- Ветка else описана для второго условия, но не для первого. Поэтому вопросительное предложение становится "normal";
Чтобы исправить ситуацию, воспользуемся еще одним элементом условной конструкции:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
if last_char == '?':
sentence_type = 'question'
elif last_char == '!':
sentence_type = 'exclamation'
else:
sentence_type = 'normal'
return 'Sentence is ' + sentence_type
print(get_type_of_sentence('Who?')) # => 'Sentence is question'
print(get_type_of_sentence('No')) # => 'Sentence is normal'
print(get_type_of_sentence('No!')) # => 'Sentence is exclamation'
Теперь все условия выстроились в единую конструкцию.
elif означает — «если не выполнено предыдущее условие, но выполнено текущее». Получается такая схема:
- Если последний символ — ?, то 'question'
- Если последний символ — !, то 'exclamation'
- Остальные варианты — 'normal'
Выполнится только один из блоков кода, который относится ко всей конструкции if.
Тернарный оператор
Посмотрите на определение функции, которая возвращает модуль переданного числа:
def abs(number):
if number >= 0:
return number
return -number
Но можно записать более лаконично. Для этого справа от return должно быть выражение, но if — это инструкция, а не выражение. В Python есть конструкция, которая работает как if-else, но считается выражением. Она называется тернарный оператор — единственный оператор в Python, который требует три операнда:
def abs(number):
return number if number >= 0 else -number
Общий паттерн выглядит так:
def get_type_of_sentence(sentence):
last_char = sentence[-1]
return 'question' if last_char == '?' else 'normal'
print(get_type_of_sentence('Python')) # => normal
print(get_type_of_sentence('Python?')) # => question
Тернарный оператор можно вкладывать в тернарный оператор. Но не нужно так делать, так как такой код тяжело читать и отлаживать.
Отступы и блоки
В Python, в отличие от других языков, блоки кода принято выделять не скобками, а новой строкой с отступом. Отступы обычно состоят из 4 пробелов или одного символа табуляции, который нужно настроить в редакторе на использование пробелов. Все строки в одном блоке должны иметь одинаковый отступ. Увеличение отступа означает начало нового блока, а уменьшение отступа означает конец блока.
if a == 42:
# отступ и начало блока if
# все строки кода с одним отступом выполняются в одном блоке
print('First')
print('Second')
# конец отступа и выход из блока
print('Goodbye!')
На примере использования else видно, как важно не забывать отделять блоки.
# Неправильно
def check_number(number):
if number > 0:
print("Число положительное")
if number > 10:
print("Число больше 10")
else:
print("Число не положительное")
check_number(3)
# => Число положительное
# => Число не положительное
В примере выше мы забыли "вложить" с помощью отступа второй if, потому else теперь относится к нему, а не первому if.
# Правильно
def check_number(number):
if number > 0:
print("Число положительное")
if number > 10:
print("Число больше 10")
else:
print("Число не положительное")
check_number(3)
# => Число положительное
Теперь второй if вложен в первый, а else на одном уровне с первым и противопоставляется ему.
Оператор match
Многие языки в дополнение к условной конструкции if включают в себя switch. С выходом версии Python 3.10 также был добавлен оператор с аналогичной функциональностью — match.
Оператор match
Оператор match — это специализированная версия if, которую создали для особых ситуаций. Например, ее нужно использовать там, где есть цепочка if else с проверками на равенство:
if status == 'processing':
# Делаем раз
elif status == 'paid':
# Делаем два
elif status == 'new':
# Делаем три
else:
# Делаем четыре
У этой составной проверки есть одна отличительная черта: каждая ветка здесь — это проверка значения переменной status. Оператор match позволяет записать этот код короче и выразительнее:
match status:
case 'processing': # status == 'processing'
# Делаем раз
case 'paid': # status == 'paid'
# Делаем два
case 'new': # status == 'new'
# Делаем три
case _: # else
# Делаем четыре
С точки зрения количества элементов match — это сложная конструкция. Она состоит из таких элементов:
- Внешнее описание, в которое входит ключевое слово match. Это переменная, по значениям которой match и будет выбирать поведение
- Конструкции case, внутри которых описывается поведение для разных значений рассматриваемой переменной.
Каждый case соответствует if в примере выше. При этом case _ — это особая ситуация, которая соответствует ветке else в условных конструкциях. Как и else, указывать case _ необязательно. Внутри match допустим только тот синтаксис, который показан выше. Другими словами, там можно использовать case. А вот внутри каждого case ситуация другая. Здесь можно выполнять любой произвольный код:
match count:
case 1:
# Делаем что-то полезное
case 2:
# Делаем что-то полезное
case _:
# Что-то делаем
Два способа вернуть результат
Иногда результат, полученный внутри case — это конец выполнения функции, которая содержит match. В таком случае его нужно как-то вернуть наружу. Есть два способа для решения этой задачи:
- Создать переменную перед match, заполнить ее в case и затем в конце вернуть значение этой переменной наружу:
def count_items(count):
# Объявляем переменную
result = ''
# Заполняем
match count:
case 1:
result = 'one'
case 2:
result = 'two'
case _:
result = None
# Возвращаем
return result
- Вместо создания переменной при работе с case можно делать обычный возврат из функции:
def count_items(count):
match count:
case 1:
return 'one'
case 2:
return 'two'
case _:
return None
Оператор match хоть и встречается в коде, но технически всегда можно обойтись без него. Его главное достоинство в том, что он лучше выражает намерение программиста, когда нужно проверять конкретные значения переменной. Хотя кода и стало физически чуть больше, читать его легче, в отличие от блоков elif.
Несколько значений в case
С помощью оператора | (или) можно объединять несколько значений в один case, чтобы выполнять одну и ту же операцию ветвления. Например:
def match_input(input):
match input:
case 'start' | 'begin':
print('Starting...')
case 'stop' | 'end':
print('Stopping...')
case 'pause':
print('Pausing...')
case _:
print('Invalid input')
match_input('begin') # => Starting...
match_input('stop') # => Stopping...
match_input('pause') # => Pausing...
match_input('test') # => Invalid input
Проверка типов
В операторе case можно использовать функции приведения типов, например, str(), int(). Это нужно, чтобы проверять тип переменной после match:
def get_type(val):
match val:
case str(val):
print(f'It is a string: {val}')
case int(val):
print(f'It is an integer: {val}')
case _:
print("I don't know this type")
get_type('hello') # => It is a string: hello
get_type(123) # => It is an integer: 123
get_type(None) # => I don't know this type
Определение переменной в case
Если определить переменную после case, то ей будет присвоено значение, которое связано с соответствием с match:
def match_input(input):
match input:
case 'start':
message = 'Starting...'
case 'stop':
message = 'Stopping...'
case 'pause':
message = 'Pausing...'
case _:
message = 'Invalid input'
print(message)
match_input('start') # => Starting...
match_input('stop') # => Stopping...
match_input('pause') # => Pausing...
match_input('test') # => Invalid input
Здесь обязательно нужно задать "дефолтный" случай — _. Так как если ни один case не соответствует входному значению, переменная message не будет определена. Это вызовет ошибку NameError.
Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.