Cat

== != is

Поговорим о разнице между оператором "==" и ключевым словом "is". И про коробочки и бирочки не забудем.

Нюансы Python rusheslav 14 Ноябрь 2023 Просмотров: 402

Поговорим о разнице между оператором "==" и ключевым словом "is". Если до этого вы не читали мой пост о переменных в питоне и коробочках, сейчас самое время сделать это, потому что мы будем продолжать всё ту же аналогию.

Итак, вспомним, что у нас есть:

  • Переменная - она же бирка на коробочке
  • Коробочка - она же ячейка памяти
  • Заводской номер коробочки - он же адрес ячейки памяти
  • Содержимое коробки - значение
  • Склад - память в целом.

Вспомнили? Отлично. Есть шанс, что если с разработкой не срастётся, всегда можно будет устроиться работать на склад. Шутка, всё обязательно получится, для этого мы здесь.

Оператор "=="

Итак, для чего нужен оператор "=="? Он позволяет проверить, совпадает ли содержимое двух коробок на складе (значения, хранящиеся в двух ячейках памяти). Он похож на работника склада, который умеет выполнять только эту задачу и никакую другую. Негусто, конечно, но зато справляется он со своей функцией на ура.

Как поставить работнику задачу? Дать ему данные с двух бирок (переменные) и попросить его сравнить содержимое коробок, на которых они наклеены. Рассмотрим на примере.

name = "Стэн"
same_name = "Стэн"
print(name == same_name)

Наш трудяга стремглав летит проверять, что у нас лежит в коробках с бирками name и same_name. А лежит у нас и там, и там одинаковая строка "Стэн". Оператор радостно нам сообщает об этом: True!

Закрепим - оператор "==" сравнивает переменные по значению (содержимое двух коробок с бирками, которые мы ему передали).

Ключевое слово "is"

Помимо работника, у нас в команде есть ключевое слово "is" (пускай на этот раз будет сотрудница). Умеет опять-таки немного, но работу свою выполняет чётко. Ей мы также даём информацию о том, что написано на двух бирках (названия переменных), а она проверяет, наклеены ли они на одну и ту же коробку (ссылаются ли переменные на одну и ту же ячейку памяти).

Рассмотрим на примере.

name = “Стэн"
print(id(name))
same_name = “Стэн"
print(id(same_name))
print(name == same_name)
print(name is same_name)

Из поста про переменные мы помним, что две одинаковые строки, которые мы кладем в две разные переменные, на самом деле будут лежать в одной и той же ячейке памяти, что и видно по адресу, который мы в обоих случаях вывели в консоль print’ом. И нашей сотруднице это видно, о чём она нам и заявляет: True.

То есть ещё раз: оператор "==" показывает, одинаковое ли значение (содержимое) хранится в двух ячейках памяти (коробках), на которые ссылаются две переменные (наклеены две бирки). Ключевое слово "is" показывает, ссылаются ли две переменные (наклеены ли две бирки) на одну и ту же ячейку памяти (коробку), или нет.

А зачем это вообще нужно?

Разве бывает так, чтобы ответы наших работников по поводу одинакового содержимого двух разных переменных различались?

Бывает. Рассмотрим на примере:

a = "Стэн"
b = "Стэн"
c = "Ст" + "эн"
d = input('Введите слово "Стэн": ')  # Введем слово "Стэн" (без кавычек)
print(id(a))  # 4352025456
print(id(b))  # 4352025456
print(id(c))  # 4352025456
print(id(d))  # 4353014544
print(a == b)  # True
print(a is b)  # True
print(a == c)  # True
print(a is c)  # True
print(a == d)  # True
print(a is d)  # False

Несложно заметить, что во всех четырёх наших переменных в итоге окажется одна и та же строка (если мы честно ввели слово “Стэн” без кавычек, когда нас об этом попросили). Но в первых трех случаях эта строка оказалась в одной и той же ячейке, на которую “наклеились” три переменные: a, b, c.

Почему же в четвертом случае (для переменной d) потребовалось новая ячейка в памяти? Всё дело в том, как Python пытается экономить память. На этапе компиляции кода он способен сравнивать строки между собой, а также проверять результат простых операций со строками (вроде конкатенации строк “Ст” и “эн”). Так как этот результат соответствует уже существующей в памяти строке “Стэн”, нам незачем создавать еще один такой же объект. Смело клеим бирку на уже созданную ранее коробку со Стэном. Процесс оптимизации работы со строками и некоторыми другими объектами называется “интернированием”, и мы когда-нибудь уделим ему отдельное внимание.

А вот то, что мы вводим с клавиатуры и передаем в переменную d, попадает в память уже в момент работы кода и поэтому для новой строки подбирается новая ячейка.

Советую не верить мне на слово, а «пощупать» всё в PyCharm самостоятельно.

Ещё один пример

list_one = [1, 3, 5]
list_two = [1, 3, 5]
list_three = list_one
list_four = list_one.copy()

print(id(list_one))  # 4509660416
print(id(list_two))  # 4511778176
print(id(list_three))  # 4509660416
print(id(list_four))  # 4510270528

print(list_one == list_two)  # True
print(list_one is list_two)  # False

print(list_one == list_three)  # True
print(list_one is list_three)  # True

print(list_one == list_four)  # True
print(list_one is list_four)  # False

Пройдёмся сверху вниз по коду примера:

  1. Мы создали список из трех цифр и записали его в переменную list_one.
  2. Далее создали точно такой же список и записали его в переменную list_two.
  3. Затем на ячейку, к которой уже “прикреплена” переменная list_one, "вешаем" еще одну переменную list_three.
  4. Наконец, в переменную list_four записываем результат копирования списка из list_one.

Первое, на что стоит обратить внимание, list_one и list_two записались в разные ячейки памяти, что видно и по номерам этих ячеек, и по работе нашей прекрасной сотрудницы "is", которая выдает “False”, хотя её коллега "==" справедливо подтверждает, что содержатся в обеих переменных идентичные списки. Со строками из предыдущего примера в такой же ситуации результат, как мы помним, был другим. Да, такое поведение напрямую связано с особенностями этих типов данных, о чем мы подробно поговорим в следующий раз.

Второе, на что стоит обратить внимание, list_one и list_three “прицепились” к одной и той же ячейке. Это видно по работе функции id(), а также по показаниям наших сотрудников - "==" и "is".

Третий важный момент: когда мы используем функцию copy() для копирования списка, в переменную list_four попадает идентичный список с числами. Но записывается он уже в новую ячейку памяти, что легко видно по результатам работы id(), "==" и "is". Возможно, вы спросите, для чего вообще нужна эта манипуляция c copy(), если можно было бы, как и в случае с list_two, просто вручную пересоздать точно такой же список. Ну, хорошо, когда это можно сделать “на берегу” до запуска кода. А если список попадает к нам каким-то другим образом? Например, вводится через консоль или получается автоматически с какого-нибудь сайта? Если нам нужна точная его копия в другой ячейке памяти (а иногда это просто-таки необходимо), то copy() - один из возможных выходов из ситуации.

Ну и последнее

list_one = [1, 3, 5]
list_two = [1, 3, 5]
list_three = list_one
list_four = list_one.copy()
list_one.append(7)
print(list_one)  # [1, 3, 5, 7]
print(list_two)  # [1, 3, 5]
print(list_three)  # [1, 3, 5, 7]
print(list_four)  # [1, 3, 5]

Работаем всё с теми же четырьмя переменными, в которых находится всё то же самое. К списку в list_one добавляем еще одно число и выводим в консоль все четыре переменные. Для тех, кто внимательно следил за ходом нашей мысли, не должно стать сюрпризом, что изменения, помимо списка в переменной list_one, коснулись и списка в переменной list_three. Почему? Потому что это один и тот же список, который “лежит” в одной и той же коробке, к которой мы прицепили две бирки: list_one и list_three. Списки из list_two и list_four остались нетронутыми, потому что каждый из них спокойно себе лежит в своей “коробке”.

А вот если бы мы пытались проделать нечто подобное со строками или целыми числами, результат снова был бы отличным, но это уже совсем другая история…

    Нет комментариев

    Реклама