Некоторые темы в программировании скучны, и приоритет операторов Python — одна из них. Давайте это исправим!
Это была работа мечты Дэна. Но его первый рабочий день не задался. Скажем прямо, фиговый был день.
Казалось, прошла вечность с тех пор, как он узнал, что получил должность личного помощника мэра Пайтауна. Радость, воодушевление и гордость того дня сменились отчаянием, безысходностью и тревогой.
Все началось с того, что мэр поприветствовал Дэна на новой должности. Дэн и так уже нервничал.
«После вас», — вежливо сказал мэр, приглашая Дэна легким взмахом руки.
«Вы идете первым, сэр», — заикнулся Дэн.
«Нет, я настаиваю, вы идете первым».
Мы все оказывались в подобной ситуации. Или в ее альтернативной форме — вы наверняка узнаете ее! Вы идете по городу и встречаетесь взглядом с другим пешеходом, идущим вам навстречу. Вы оба понимаете, что вот-вот столкнетесь, и оба слегка меняете направление. Но в одну сторону. Вы улыбаетесь друг другу и снова смещаетесь. И опять в одну сторону. Теперь улыбки становятся шире, поскольку вы узнаёте этот танец “влево-вправо”. И наконец один из вас делает более широкий и четкий шаг в сторону. Ничего страшного.
Дэну повезло меньше. Когда он наконец решил войти в дверь, в нее решил войти и мэр. Авария. Кофе мэра, горячий и черный, пролился на его костюм (т.е. костюм мэра). Дэн не пострадал, что еще больше усилило его смущение.
Чтобы успокоить его, мэр сразу же дал Дэну первое задание.
«Принесите мне апельсиновый сок или кофе с молоком, пожалуйста».
Дэн все еще находился в оцепенении. Охранник на входе указал ему, где находится кухня. Дэн налил полстакана апельсинового сока и ничтоже сумняшеся долил его молоком. Он отнес этот напиток мэру и понял, что натворил, только когда увидел, что мэр вежливо делает вид, что больше не хочет пить.
«Сегодня мне нужно кое-что сделать, и вы можете помочь», — сказал мэр, надеясь успокоить Дэна.
«Вот папка со всеми сотрудниками мэрии. Для каждого сотрудника рассчитайте премию: 10 % от базового оклада плюс сверхурочные часы, умноженные на почасовую ставку, и все специальные надбавки».
Дэн глубоко вздохнул, уверенно улыбнулся, взял папку и с подобающим почтением вышел из кабинета мэра. Он сел за свой стол. Он сделал паузу. «Рассчитайте премию как 10 % от базового оклада плюс сверхурочные часы, умноженные на почасовую ставку, и все специальные надбавки». В спокойном состоянии Дэн догадался бы, что имел в виду мэр. Но, несмотря на все усилия Дэна изобразить спокойствие, внутри было неспокойно. Он вычислил 10 % от зарплаты каждого. Затем он добавил оплату за сверхурочные часы и специальные надбавки. Премия получилась огромной, ведь многие сотрудники работали сверхурочно! Можете представить реакцию мэра, когда он увидел итоговую сумму.
Приоритет операторов
Мэр мог бы выразиться яснее, когда давал указания Дэну. «Принесите мне, пожалуйста, апельсиновый сок или вместо него кофе с молоком». Я уверен, что вам случалось уточнять, если что-то из сказанного вам было двусмысленным.
Но как Python решает эти проблемы? В нем есть иерархия операторов, и те, что выше в списке, выполняются раньше, чем те, что ниже.
Давайте рассмотрим несколько примеров.
Вот несколько персонажей из книг и фильмов, которые я недавно читал или смотрел:
characters = [ "Jean-Luc Picard", "Harry Potter", "Sherlock Holmes", "Yoda", "Bilbo Baggins", "Katniss Everdeen", ]
Давайте напишем код, который найдет все имена, начинающиеся с «B» или «Y», которые короче восьми символов. Чтобы не усложнять задачу, мы будем использовать символы, а не буквы, поэтому включим пробелы и дефисы в те имена, в которых они есть.
Первая попытка
Вот первая попытка:
characters = [ "Jean-Luc Picard", "Harry Potter", "Sherlock Holmes", "Yoda", "Bilbo Baggins", "Katniss Everdeen", ] selection = [ name for name in characters if name[0] == "B" or name[0] == "Y" and len(name) < 8 ]
Вы создаете выборку (selection
) с помощью представления списка. Я разделил представление списка на три строки, чтобы сделать его более читабельным. Последняя строка — это условие, которое используется для фильтрации имен, которые вы хотите оставить:
if name[0] == "B" or name[0] == "Y" and len(name) < 8
Давайте рассмотрим эту строку:
- Она начинается с ключевого слова
if
. - В ней четыре имени (или идентификатора):
name
встречается три раза, аlen
— один. - В ней есть две пары квадратных скобок. В обоих случаях они следуют сразу после имени (идентификатора).
- Сразу после имени функции
len
есть пара круглых скобок. - Есть также операторы сравнения и равенства:
==
встречается дважды, а<
— один раз. - В строке есть операторы
or
иand
. - В ней есть два строковых литерала, «B» и «Y», и целочисленный литерал, 8.
Чтобы не разбираться со всем этим сразу, давайте сосредоточимся на булевых операторах and
и or
.
Должен ли Python сначала проверить, является ли первая буква имени «B» или «Y», а затем проверить, не короче ли это имя восьми символов? Или сначала нужно проверить, начинается ли имя с буквы «Y» и короче ли оно восьми символов, а затем проверить, начинается ли оно с буквы «B»?
Python не может читать ваши мысли. Он не знает, каковы ваши намерения. Итак, давайте выполним этот код и посмотрим, что произойдет:
['Yoda', 'Bilbo Baggins']
Оба имени начинаются либо с «B», либо с «Y». Пока все хорошо. Но в имени Бильбо Бэггинса 13 символов, включая пробел. Это больше, чем восемь. Бильбо Бэггинса не должно быть в списке.
Это потому, что and
имеет более высокий приоритет, чем or
. Поэтому, несмотря на то что or
стоит первым в строке кода, Python сначала выполняет сравнение and
. Давайте разберемся, что это значит, на примере Йоды и Бильбо Бэггинса.
Давайте преобразуем нашу строку кода шаг за шагом. Следующие шаги предназначены только для демонстрации, и это не тот код, который стоит писать в своих программах:
if name[0] == "B" or name[0] == "Y" and len(name) < 8
Когда name = "Yoda"
, вы можете представить эту строку следующим образом:
if "Y" == "B" or "Y" == "Y" and len("Yoda") < 8
Операция and
выполняется первой. Поэтому Python проверяет, что "Y" == "Y" and len("Yoda") < 8
. Оба эти выражения равны True. Выражение and
оценивается как True:
if "Y" == "B" or True
Остается выражение or
, у которого два операнда:
"Y" == "B"
- True
Первое выражение оценивается как False, а второе как True. Следовательно, or оценивается как True.
Йода прошел отбор!
Давайте повторим процесс с Бильбо Бэггинсом, начиная с полной строки кода:
if name[0] == "B" or name[0] == "Y" and len(name) < 8
Когда name = "Bilbo Baggins"
, строку можно представить следующим образом:
if "B" == "B" or "B" == "Y" and len("Bilbo Baggins") < 8
Первое выражение, которое будет вычислено, — это "B" == "Y" and len("Bilbo Baggins") < 8
, поскольку and
имеет приоритет над or
. Оба операнда равны False, поскольку «B» не равно «Y» и в полном имени Бильбо слишком много символов:
if "B" == "B" or False
Теперь очередь оператора or
. Один из операндов, "B" == "B"
, оценивается как True. Поскольку для оператора or
достаточно, чтобы один из его операндов был True (или истинным), все выражение or
оценивается как True, даже если второй операнд — False.
Бильбо Бэггинс также попадает в список. Но это не желаемый результат.
Вторая попытка
Мы начали строить иерархию старшинства операторов. Пока что мы определили, что and
стоит выше or
. Давайте поднимемся на вершину иерархии, чтобы найти инструмент, который можно использовать во второй попытке.
Круглые скобки находятся на самом верху. Они имеют приоритет над всем остальным. Такой же приоритет имеют и другие типы скобок, например []
для создания списков и {}
для создания словарей или множеств.
Поэтому, если вы хотите, чтобы выражение or
опередило and
в порядке выполнения операций, вы можете заключить выражение or
в круглые скобки. Круглые скобки поднимают это выражение на вершину иерархии:
characters = [ "Jean-Luc Picard", "Harry Potter", "Sherlock Holmes", "Yoda", "Bilbo Baggins", "Katniss Everdeen", ] selection = [ name for name in characters if (name[0] == "B" or name[0] == "Y") and len(name) < 8 ] print(selection)
Обратите внимание на дополнительные круглые скобки в предложении if
. Теперь вывод такой:
['Yoda']
Йода остался, а Бильбо Бэггинс — нет. Давайте используем ту же технику, что и в предыдущем разделе, и посмотрим, почему Йода попал в список:
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
Когда name = "Yoda"
:
if ("Y" == "B" or "Y" == "Y") and len("Yoda") < 8
Выражение в круглых скобках выполняется первым, и поскольку один из операндов равен True, выражение or
оценивается как True:
if True and len("Yoda") < 8
А имя Йоды содержит только четыре символа, поэтому оба выражения оцениваются как True в выражении and. Йода попадает в список.
А как насчет Бильбо Бэггинса? Давайте начнем с начала:
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
Теперь name = "Bilbo Baggins"
:
if ("B" == "B" or "B" == "Y") and len("Bilbo Baggins") < 8
Поскольку выражение or
в скобках стоит на первом месте, вы получаете:
if True and len("Bilbo Baggins") < 8
Выражение or
равно True, поскольку один из операндов равен True. Но у Бильбо Бэггинса длинное полное имя, поэтому второй операнд в выражении and
— False. Поэтому выражение and
оценивается как False. Бильбо не попадает в список.
Что мы можем сказать о приоритете других операторов?
Давайте вернемся к строке в представлении списка, которая содержит предложение if
:
if (name[0] == "B" or name[0] == "Y") and len(name) < 8
Вы видели, что круглые скобки, используемые для группировки выражений, находятся на вершине иерархии. Квадратные или фигурные скобки для группировки элементов в списке, словаре или множестве находятся на том же уровне. Вторая позиция в иерархии операторов тоже включает круглые и квадратные скобки — те, которые следуют сразу после объекта для вызова функции или класса или для подзаписи объекта (например, для обращения к элементу по индексу, среза или получения значения из словаря).
Но вернемся к нашей строке кода. В выражении в круглых скобках первыми выполняются две операции обращения к элементу по индексу, name[0]
. Это может показаться очевидным, но это результат того, что приоритет этих операций очень высок.
И если вы знакомы с использованием or
, то можете сделать вывод, что приоритет оператора равенства ==
выше, чем приоритет or
. Если бы это было не так, то код в круглых скобках выглядел бы как следующее выражение: name[0] == ("B" or name[0]) == "Y"
. Это выражение всегда оценивается как False. Строка «B» не пуста и поэтому истинна. Выражение or
всегда оценивается по первому операнду, если он истинный. Но "B" == "Y"
— всегда False!
Переходим к тому моменту, когда программа смотрит на второй операнд в выражении and
, который равен len(name) < 8
. Поскольку круглые скобки после идентификатора len
в списке старшинства уступают только самостоятельным скобкам, а в этом выражении нет самостоятельных скобок, то вызов функции происходит раньше оператора <
. Сюрприза тут нет, я знаю.
Вот порядок старшинства операторов, с которым вы познакомились на данный момент. Я пронумеровал список, но обратите внимание, что в нем есть пробелы. Это не опечатка! Есть и другие операторы, которые мы должны будем вставить в список позже, но я не буду пытаться рассмотреть их все в этой статье. Иначе мы останемся здесь навсегда!
1. Круглые скобки для группировки выражений и квадратные и фигурные скобки для создания списков, словарей и множеств:
(выражения...), [выражения...], {ключ: значение}, {выражения...}.
2. Скобки и круглые скобки для вызова, срезов, обращения по индексу. Точечная нотация для доступа к атрибутам объектов одного иерархического уровня:
obj[index_or_key], obj[start:stop], some_function(arguments...), some_object.attribute
12. Операторы равенства и сравнения. Тесты принадлежности и тождества также находятся на этом уровне:
in, not in, is, is not, <, <=, >, >=, !=, ==
14. Оператор and
15. Оператор or
Дэн, мэр и правила приоритетов в Python
«Принесите мне апельсиновый сок или кофе с молоком, пожалуйста».
Если бы в мэрии следовали правилам приоритетности операторов Python, согласно которым and идет перед or, Дэн не допустил бы никаких ошибок.
А как насчет второй просьбы мэра?
«Рассчитайте премию как 10 % от базовой зарплаты плюс сверхурочные часы, умноженные на почасовую ставку, и добавьте любые специальные бонусные надбавки».
На языке Python это будет выглядеть так:
0.1 * base_salary + overtime_hours * hourly_rate + special_allowances
Эта строка включает два оператора: *
и +
. Как обычно бывает в математике, умножение имеет приоритет над сложением. Это приводит к той же ошибке, что и у Дэна: оплата сверхурочных полностью добавляется к премии, что не очень хорошо для казначея мэрии. Мэр хотел сказать, что Дэн должен сначала сложить базовую зарплату и сверхурочные, а затем вычислить 10 % от суммы и в конце добавить специальные бонусные надбавки.
Не зря круглые скобки находятся в верхней части списка приоритетов. Они полезны в таких ситуациях, когда нужно изменить порядок операций:
0.1 * (base_salary + overtime_hours * hourly_rate) + special_allowances
Скобки гарантируют, что сначала к базовой зарплате будет добавлена оплата сверхурочных работ, а затем полученная сумма будет умножена на 0,1. При этом overtime_hours * hourly_rate
не нужно заключать в дополнительные скобки, поскольку *
стоит выше +
в иерархии.
Но мне теперь интересно посмотреть весь список приоритетов!
Ни один уважающий себя автор не может закончить статью об иерархии операторов в Python, не приведя таблицу приоритетов полностью. Но я, понятное дело, к таковым авторам не отношусь. Я мог бы скопировать эту таблицу из документации, но лучше просто оставлю ссылку на нее:
Заключение
Дэн с мэром в конечном итоге поговорили и все выяснили. Оба поняли, что нужно поработать над коммуникацией. Возможно, им стоит распечатать себе табличку с приоритетом операторов Python и повесить ее где-нибудь на видном месте!
Перевод статьи “After You. No, I Insist, You Go First • Python’s Operator Precedence”.