Python: is. Равенство и эквивалентность
Новички часто путаются в конструкциях is и == . Давайте разберемся, что к чему.
Сразу к сути: == (и его антагонист != ) применяются для проверки равенства (неравенства) значения двух объектов. Значение, это непосредственно то, что лежит в переменной. Значение числа 323235 – собственно число 323235. Тавтология. Но на примерах станет яснее.
Оператор is (и его антагонист is not ) применяются проверки равенства (неравенства) ссылок на объект. Сразу отметим то, что на значение (допустим 323235) может быть копировано и храниться в разных местах (в разных объектах в памяти).
Видите, значение переменных равны по значению, но они ссылаются на разные объекты. Я не случайно взял большое число 323235. Дело в том, что в целях оптимизации интерпретатор Python при старте создает некоторые количество часто-используемых констант (от -5 до 256 включительно).
Следите внимательно за ловкостью рук:
Поэтому новички часто совершают ошибку, считая, что писать == – это как-то не Python-way, а is – Python-way. Это ошибочное предположение может быть раскрыто не сразу.
Python старается кэшировать и переиспользовать строковые значения. Поэтому весьма вероятно, что переменные, содержащие одинаковые строки, будут содержать ссылки на одинаковые объекты. Но это не факт! Смотрите последний пример:
Мы составили строку из двух частей и она попала в другой объект. Python не догадался (и правильно) поискать ее в существующих строках.
Суть is (id)
В Python есть встроенная функция id . Она возвращает идентификатор объекта – некоторое число. Гарантируется, что оно будет различно для различных объектах в пределах одного интерпретатора. В реализации CPython – это просто адрес объекта в памяти интерпретатора.
Это тоже самое, что:
И все! Пример для проверки:
Значения переменных равны, но их id – разные, и is выдает False . Как только мы к x привязали y , то ссылки стали совпадать.
Для чего можно применять is?
Если мы точно знаем уверены, что хотим проверять именно равенство ссылок на объекты (один ли это объект в памяти или разные).
Еще можно применять is для сравнения с None . None – это встроенная константа и двух None быть не может.
Также для Ellipsis:
Я не рекомендую применять is для True и False .
Потому что короче писать if x: , чем if x is True: .
Можно применять is для сравнения типов с осторожностью (без учета наследования, т. е. проверка на точное совпадение типов):
С наследованием может быть конфуз:
Не смотря на то, что Bar – наследник Foo , типы переменных foo и bar не совпадают. Если нам важно учесть наcледование, то пишите isinstance .
Нюанс: is not против is (not)
Важно знать, что is not – это один целый оператор, аналогичный id(x) != id(y) . А в конструкции x is (not y) – у нас сначала будет логическое отрицание y , а потом просто оператор is .
Сравнение пользовательских классов
Далее речь пойдет об обычных == и != . Можно определить магический метод __eq__ , который обеспечит поведение при сравнении классов. Если он не реализован, то объекты будет сравниваться по ссылкам (как при is ).
Если он реализован, то будет вызван метод __eq__ для левого операнда.
Метод __ne__ отвечает за реализацию != . По умолчанию он вызывает not x.__eq__(y) . Но рекомендуется реализовывать их оба вручную, чтобы поведение сравнения было согласовано и явно.
Вопрос к размышлению: что будет если мы сравним объекты разных классов, причем оба класса реализуют __eq__ ?
Что будет, если мы реализуем __ne__ , но не реализуем __eq__ ?
А еще есть метод __cmp__ . Это уже выходит за рамки статьи про is . Почитайте самостоятельно…
Операции сравнения в Python, цепочки сравнений.
В Python есть шесть операций сравнения. Все они имеют одинаковый приоритет, который выше, чем у логических операций.
Разрешенные операции сравнения:
- x < y — строго x меньше y ,
- x <= y — x меньше или равно y ,
- x > y — строго x больше y ,
- x >= y — x больше или равно y ,
- x == y — x равно y ,
- x != y — x не равно y .
Внимание!. Комплексным числам (тип complex ) недоступны операции: x < y , x <= y , x > y и x >= y .
Сравнения могут быть связаны произвольно и записаны в цепочки сравнений, в которых для соединения сравнений используются неявные логические операторы and .
В примере выше y вычисляется только один раз. Если x < y оказывается ложным, то в обоих случаях, приведенных выше z не оценивается вообще.
В такой форме сравнения легче читаются, и каждое подвыражение вычисляется по крайней мере один раз.
Объекты разных типов, за исключением различных числовых типов, никогда не будут равными.
Оператор == всегда определен, но для некоторых типов объектов, например объектов класса, эквивалентен оператору идентичности is .
Операторы < , <= , > и >= применяются только там, где они имеют смысл, например они вызывают исключение TypeError , когда один из аргументов является комплексным числом.
Неидентичные экземпляры класса обычно при сравнении будут неравны, если только класс не определяет метод __eq__() .
Экземпляры класса не могут быть упорядочены относительно других экземпляров того же класса или других типов объектов, если класс не определяет достаточное количество методов __lt__() , __le__() , __gt__() и __ge__() . В общем случае определение методов __lt__() и __eq__() для этих целей бывает достаточно.
Поведение встроенных типов в операциях сравнения:
Числа встроенных числовых типов int , float , complex и стандартных библиотечных типов fractions.Fraction и decimal.Decimal можно сравнивать внутри и между их типами, с ограничением, что комплексные числа не поддерживают сравнение порядка. В пределах задействованных типов они сравнивают математически (алгоритмически) правильно без потери точности.
Нечисловые значения float('NaN') и decimal.Decimal('NaN') являются особыми. Любое упорядоченное сравнение числа с нечисловым значением неверно. Нечисловые значения не равны самим себе. Например, если x = float('NaN') , 3 < x , x < 3 и x == x все ложны, а x! = X истинно. Это поведение соответствует стандарту IEEE 754.
None и NotImplemented являются одиночными. PEP 8 советует, что сравнения для одиночных экземпляров всегда должны выполняться с использованием или нет, а не с операторами равенства.
Двоичные последовательности (экземпляры bytes или bytearray ) можно сравнивать внутри и между их типами. Они сравнивают лексикографически, используя числовые значения своих элементов.
Строки (экземпляры str ) сравниваются лексикографически с использованием числовых кодовых точек Unicode (результат встроенной функции ord() ) их символов.
Строки и двоичные последовательности напрямую сравнивать нельзя.
Последовательности (экземпляры tuple , list или range ) можно сравнивать только в пределах каждого из их типов с ограничением, что диапазоны range не поддерживают сравнение порядка (сортировку). Оператор == между этими типами приводит к неравенству, а сравнение порядка между этими типами вызывает исключение TypeError .
Последовательности сравнивают лексикографически с помощью сравнения соответствующих элементов. Встроенные контейнеры обычно предполагают, что идентичные объекты равны самим себе. Это позволяет им обходить тесты на равенство для идентичных объектов, чтобы повысить производительность и сохранить свои внутренние инварианты.
Лексикографическое сравнение встроенных коллекций работает следующим образом:
Чтобы две коллекции были равными, они должны быть одного типа, иметь одинаковую длину и каждая пара соответствующих элементов должна быть равной. Например [1,2] == (1,2) ложно, потому что типы последовательностей разные.
Коллекции, поддерживающие сравнение порядка (сортировку), упорядочиваются также, как их первые неравные элементы, например [1,2, x] <= [1,2, y] имеет то же значение, что и x <= y . Если соответствующий элемент не существует, то более короткая коллекция при сортировке встанет первой, например [1,2] < [1,2,3] истинно).
Множества (экземпляры set или frozenset ) можно сравнивать внутри и между их типами.
Они определяют операторы сравнения порядка для обозначения тестов подмножества и надмножества. Эти отношения не определяют общий порядок. Например два множества <1,2>и <2,3>не равны, ни подмножества друг друга, ни надмножества друг друга. Соответственно, наборы не являются подходящими аргументами для функций, которые зависят от общего упорядочения. Например min() , max() и sorted() дают неопределенные результаты при наличии списка множеств в качестве входных данных.
Большинство других встроенных типов не имеют реализованных методов сравнения, поэтому они наследуют поведение сравнения по умолчанию.
Подводные камни (ловушки цепочек сравнения).
Несмотря на то, что цепочки сравнения выглядят очень разумно, есть пара подводных камней, на которые необходимо обратить внимание.
Нетранзитивные операторы.
Чтобы проверить, совпадают ли a , b и c , можно использовать цепочку сравнения a == b == c . А как проверить, ВСЕ ли они разные? Первое, что приходит в голову — это a != b != c , и мы попадаем в первую ловушку!
Но это не так. Они не все разные, ведь a == c . Проблема здесь в том, что a != b != c — это a != b and b != c , что проверяет, что b отличается от a и от c , но ничего не говорит о том, как связаны a и c`.
С математической точки зрения, != не является транзитивным, т. е. знание того, как a относится к b , и знание того, как b относится к c , не говорит о том, как a относится к c . Что касается транзитивного примера, можно взять оператор равенства == . Если a == b and b == c , то также верно, что a == c`.
Непостоянное значение в выражении.
Напомним, что в цепочке сравнений, таких как a < b < c , значение b в середине выражения вычисляется только один раз, тогда как в расширенном выражении a < b and b < c значение b вычисляется дважды.
Если b содержит что-то непостоянное или выражение с побочными эффектами, то эти два выражения не эквивалентны.
Этот пример показывает разницу в количестве оценок значения в середине выражения:
Следующий пример показывает, что выражение типа 1 < f() < 0 может принимать значение True , когда оно записано развернуто:
Синтаксис lst[::-1] — это срез, который переворачивает список.
Конечно, 1 < f () < 0 никогда не будет быть истинным, пример просто показывает, что цепочка сравнения и развернутое сравнение не всегда эквивалентны.
Плохо читаемые цепочки сравнения.
Цепочки сравнения выглядят действительно естественно, но в некоторых конкретных случаях она не так хороша. Это довольно субъективный вопрос, но лучше избегать цепочки, в которых операторы не "выровнены", например:
- a < b > c ;
- a <= b > c ;
- a < b >= c ;
Можно утверждать, например, что a < b > c читается как "проверим, b больше, чем a и c ?", но лучше эту цепочку записать так max(a, c) < b или b > max(a, c) .
Есть некоторые другие цепочки, которые просто сбивают с толку:
- a < b is True ;
- a == b in lst ;
- a in lst is True ;
В Python операторы is , is not , in и not in являются операторами сравнения, следовательно их также можно связать с другими операторами. Это создает странные ситуации, такие как:
Примеры использования цепочек сравнения.
Когда Python видит два оператора сравнения подряд, как в a < b < c , он ведет себя так, как если бы было написано что-то вроде a < b and b < c , за исключением того, что b вычисляется только один раз. Такое поведение актуально, если, например, b является выражением, подобным вызову функции.
Другой пример использования — когда необходимо убедиться, что все три значения одинаковы:
На самом деле можно связать произвольное количество операторов сравнения в цепочку? Например, a == b == c == d == e проверяет, совпадают ли все пять переменных, в то время как a < b < c < d < e проверяет, есть ли строго возрастающая последовательность.
Числа типа float не являются десятичными дробями и используют двоичную арифметику, поэтому иногда выражения могут вычисляться с ничтожно малыми погрешностями. Из-за этих погрешностей операции сравнения работают не так как ожидается.
Логический тип
Не строгое, но интуитивно понятное определение логического выражения таково: логическое выражение — это последовательность первичных логических выражений, объединенных скобками и знаками логических операций. Заметьте, что точно также можно определить понятие арифметического или строкового выражения. Разница лишь в задании первичных выражений и операций над ними.
Какие же логические операции применяются при построении логических выражений в логике, в языках программирования и, в частности, в Python?
Давайте совершим небольшой экскурс в основы логики. Логические операции — функции, определенные над логическими переменными, результат которых также имеет тип bool . Уникальной особенностью логических функций является существование базиса — конечного набора функций, позволяющего любую логическую функцию с любым числом переменных задать формулой (логическим выражением), использующим только функции базиса. Можно доказать, что таким базисом является набор из трех функций: отрицание, конъюнкция, дизъюнкция.
Отрицание (в Python существуют два варианта этой операции — not и
) — это унарная операция, изменяющая значение аргумента на противоположное — True на False и обратно.
Конъюнкция, называемая также операцией И , логическим умножением, (в Python существуют два варианта этой операции — and и & ) — это бинарная операция, истинная тогда и только тогда, когда оба операнда операции истинны.
Дизъюнкция, называемая также операцией ИЛИ , логическим сложением, (в Python существуют два варианта этой операции — or и | ) — это бинарная операция, ложная тогда и только тогда, когда оба операнда операции ложны.
Эти операции имеют разный приоритет при вычислении логических выражений. Высший приоритет имеет операция отрицания, затем логическое умножение (конъюнкция), затем логическое сложение (дизъюнкция).
Из логики известно, что базис можно сократить до одной функции. Теоретически это интересно, но практически нецелесообразно из-за растущей сложности формул, задающих логическое выражение. Поэтому в языках программирования базис обычно расширяется, в него добавляется эквивалентность (в Python — это == ) и ее отрицание, операция, называемая Исключающее Или, сложение по модулю 2 (в Python — это ^ ). Таким образом базис логических функций, используемых в языке Python, состоит из 5 функций, три из которых существуют в двух вариантах.
Отметим одну важную особенность логических выражений. Числа сами по себе не являются первичными логическими выражениями, но в Python при построении логических выражений можно использовать числа в качестве первичных выражений. В этом случае арифметическое выражение, имеющее значение 0, интерпретируется как False , все остальные значения — True . Заметьте, арифметическое выражение в этом случае не обязательно имеет тип int , допустим любой арифметический тип. Таким образом понятие логического выражения в Python расширяется, — оно строится из первичных логических выражений, арифметических выражений, объединяя их скобками и знаками операций. Как следствие, операнды логических операций могут иметь как тип bool , так и арифметический тип. Это еще одна сложность, затуманивающая понимание логических операций, допустимых в Python. Особенно неприятно, когда операнды имеют разные типы. Еще одно следствие, — логическое выражение не обязательно имеет тип bool , — его значением может быть число арифметического типа.
Поговорим теперь о том, почему базисные логические операции существуют в двух вариантах. Это не является уникальной особенностью Python. Практически такая ситуация существует в большинстве языков программирования. Объясняется это тем, что в программировании логические выражения имеют не два, а три значения — истина, ложь, неопределенность. Неопределенность не есть признак ошибки, а признак некоторой распознаваемой ситуации. Поэтому вместо классической конъюнкции и дизъюнкции полезно иметь их варианты — условные операции, учитывающие возможность неопределенности операнда, не приводящие к возникновению ошибки в работе программы.
Есть еще одна причина, по которой полезно иметь логические операции в двух вариантах, один из которых предполагает выполнение логических операций над булевскими операндами, что соответствует классической логике, другой — предполагает выполнение операций над целыми числами, когда целое число рассматривается как строка битов, представляющих число в двоичной системе счисления. Тогда операция выполняется над соответствующими парами бит операндов, формируя новое число. Такая возможность позволяет эффективно решать целый ряд задач, возникающих в программировании. Обычная практика, когда операция применяется либо к целым числам, либо к булевским выражениям. В языке Python эти два способа работы смешиваются, что будет показано на примерах.