Python & AI Tutorials Logo
Программирование Python

18. Модель данных и объектов Python: ссылки, сравнения и копии

Понимание того, как Python хранит и управляет данными, критически важно для написания корректных программ. В этой главе мы рассмотрим объектную модель(object model) Python — фундаментальную систему, которая определяет, как работают все данные в Python. Вы узнаете, почему некоторые присваивания создают независимые копии, а другие — общие ссылки(references), как правильно сравнивать объекты и как избегать распространённых ловушек при работе с коллекциями.

Эти знания помогут вам понять неожиданные поведения, с которыми вы могли сталкиваться: например, почему изменение одного списка(list) иногда влияет на другой, или почему сравнение двух списков с == даёт другие результаты, чем сравнение с is.

18.1) В Python всё является объектом

В Python каждый фрагмент данных — это объект(object). Это не просто теоретическая концепция — у неё есть практические последствия для того, как работают ваши программы.

Когда вы создаёте число, строку, список или любое другое значение, Python создаёт объект(object) в памяти. Объект — это контейнер, который содержит:

  • Собственно данные ( значение(value) )
  • Информацию о том, какого типа(type) эти данные
  • Уникальный идентификатор( identity )

Посмотрим это на практике:

python
# Создание объектов разных типов
number = 42
text = "Hello"
items = [1, 2, 3]
 
# Каждая из этих переменных ссылается на объект в памяти
print(number)  # Output: 42
print(text)    # Output: Hello
print(items)   # Output: [1, 2, 3]

Даже простые значения, такие как целые числа, являются объектами. Это означает, что у них есть возможности, выходящие за рамки простого хранения числа:

python
# Целые числа — это объекты с методами
number = 42
print(number.bit_length())  # Output: 6
 
# Строки — это объекты с методами
text = "hello"
print(text.upper())  # Output: HELLO
 
# Списки — это объекты с методами
items = [3, 1, 2]
items.sort()
print(items)  # Output: [1, 2, 3]

Почему это важно? Потому что когда вы присваиваете переменной значение или передаёте данные в функцию(function), вы не копируете объект — вы создаёте ссылку(reference) на тот же самый объект. Это принципиально отличается от того, как работают некоторые другие языки программирования, и понимание этого различия предотвратит множество запутанных багов.

python
# Создание объекта списка
original = [1, 2, 3]
 
# Это не создаёт новый список — это создаёт ещё одну ссылку
# на ТОТ ЖЕ объект списка
another_name = original
 
# Изменение через одну ссылку влияет на другую
another_name.append(4)
 
print(original)      # Output: [1, 2, 3, 4]
print(another_name)  # Output: [1, 2, 3, 4]

И original, и another_name ссылаются на один и тот же объект списка в памяти. Когда мы изменяем список через another_name, мы видим изменение через original, потому что обе переменные «смотрят» на один и тот же объект.

Переменная: original

Объект списка: 1, 2, 3, 4

Переменная: another_name

Такое поведение называется ссылочной семантикой(reference semantics), и это одна из самых важных концепций в программировании на Python. Мы подробно разберём её в течение этой главы.

18.2) Идентичность, тип и значение объектов

У каждого объекта в Python есть три фундаментальные характеристики, которые его определяют: идентичность(identity), тип(type) и значение(value). Понимание этих характеристик помогает рассуждать о том, как ведут себя объекты и как правильно их сравнивать.

18.2.1) Идентичность объекта с id()

Идентичность(identity) объекта — это уникальное число, которое Python присваивает объекту при создании. Эта идентичность никогда не меняется в течение жизни объекта — это как постоянный адрес в памяти.

Вы можете получить идентичность объекта с помощью функции id():

python
# Создание объектов и проверка их идентичностей
x = [1, 2, 3]
y = [1, 2, 3]
z = x
 
print(id(x))  # Output: 140234567890123 (example - actual number varies)
print(id(y))  # Output: 140234567890456 (different from x)
print(id(z))  # Output: 140234567890123 (same as x)

Фактические числа будут другими при каждом запуске программы, но закономерность сохраняется: у x и y разные идентичности, потому что это разные объекты, даже несмотря на то, что они содержат одинаковые значения. В то же время у z такая же идентичность, как у x, потому что z — это просто другое имя для того же объекта.

Вот практический пример, показывающий, почему идентичность важна:

python
# Два студента с одинаковыми оценками
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
 
# Это разные объекты (разные идентичности)
print(id(student1_grades))  # Output: 140234567890123 (example)
print(id(student2_grades))  # Output: 140234567890456 (different)
 
# Изменение одного не влияет на другой
student1_grades.append(88)
print(student1_grades)  # Output: [85, 90, 92, 88]
print(student2_grades)  # Output: [85, 90, 92]

Теперь рассмотрим другой сценарий:

python
# Оценки одного студента отслеживаются двумя переменными
original_grades = [85, 90, 92]
backup_reference = original_grades
 
# Они ссылаются на ОДИН И ТОТ ЖЕ объект (одинаковая идентичность)
print(id(original_grades))    # Output: 140234567890123 (example)
print(id(backup_reference))   # Output: 140234567890123 (same!)
 
# Изменение через любое имя влияет на оба
backup_reference.append(88)
print(original_grades)     # Output: [85, 90, 92, 88]
print(backup_reference)    # Output: [85, 90, 92, 88]

Ключевая идея: когда две переменные имеют одну и ту же идентичность, они ссылаются на один и тот же объект в памяти. Изменения, сделанные через одну переменную, видны через другую, потому что изменяется только один объект.

18.2.2) Тип объекта с type()

Тип(type) объекта определяет, какого рода данные он хранит и какие операции можно над ним выполнять. Как мы узнали в главе 3, вы можете проверить тип объекта с помощью функции type():

python
# Разные типы объектов
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
 
print(type(number))   # Output: <class 'int'>
print(type(text))     # Output: <class 'str'>
print(type(items))    # Output: <class 'list'>
print(type(mapping))  # Output: <class 'dict'>

Тип объекта никогда не меняется после создания. Нельзя превратить целое число в строку — можно только создать новый строковый объект на основе значения целого числа:

python
# Тип фиксируется при создании
x = 42
print(type(x))  # Output: <class 'int'>
 
# Это не меняет тип x — это создаёт НОВЫЙ строковый объект
# и заставляет x ссылаться на этот новый объект
x = str(x)
# Исходный объект целого числа (42) всё ещё существует в памяти, пока не будет собран сборщиком мусора
# теперь x указывает на совершенно другой объект: строку "42"
 
print(type(x))  # Output: <class 'str'>
print(x)        # Output: 42 (теперь это строка, а не целое число)

Понимание типов критически важно, потому что разные типы поддерживают разные операции:

python
# Списки поддерживают append
grades = [85, 90]
grades.append(92)
print(grades)  # Output: [85, 90, 92]
 
# У строк нет append — они неизменяемые
text = "Hello"
# text.append(" World")  # AttributeError: 'str' object has no attribute 'append'
 
# Но строки поддерживают конкатенацию
text = text + " World"
print(text)  # Output: Hello World

18.2.3) Значение объекта

Значение(value) объекта — это фактические данные, которые он содержит. В отличие от идентичности и типа, значение может меняться у изменяемых(mutable) объектов (например, списков и словарей), но не может меняться у неизменяемых(immutable) объектов (например, целых чисел и строк).

python
# Для изменяемых объектов значение может меняться
shopping_cart = ["milk", "bread"]
print(shopping_cart)  # Output: ['milk', 'bread']
 
shopping_cart.append("eggs")
print(shopping_cart)  # Output: ['milk', 'bread', 'eggs']
# Тот же объект (та же идентичность), другое значение
 
# Для неизменяемых объектов значение не может меняться
count = 5
print(count)  # Output: 5
 
count = count + 1
print(count)  # Output: 6
# Это создало НОВЫЙ объект с новой идентичностью

Вот полный пример, показывающий все три характеристики:

python
# Создание объекта списка
data = [10, 20, 30]
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (example)
print("Type:", type(data))        # Output: Type: <class 'list'>
print("Value:", data)             # Output: Value: [10, 20, 30]
 
# Изменение значения (идентичность и тип остаются прежними)
data.append(40)
 
print("Identity:", id(data))      # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data))        # Output: Type: <class 'list'> (unchanged)
print("Value:", data)             # Output: Value: [10, 20, 30, 40] (changed)

Объект

Идентичность: уникальный ID

Тип: class 'list'

Значение: 10, 20, 30, 40

Никогда не меняется

Никогда не меняется

Может меняться для изменяемых типов

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

18.3) Изменяемые и неизменяемые типы

Одно из самых важных различий в Python — это различие между изменяемыми(mutable) и неизменяемыми(immutable) типами. Это различие влияет на то, как ведут себя объекты, когда вы пытаетесь их изменить, и понимание этого предотвращает многие распространённые ошибки программирования.

18.3.1) Неизменяемые типы: значения, которые нельзя изменить

Неизменяемый(immutable) объект — это объект, значение которого нельзя изменить после создания. Когда вы выполняете операцию, которая кажется изменяющей неизменяемый объект, Python на самом деле создаёт новый объект с изменённым значением.

К неизменяемым типам Python относятся:

  • Целые числа (int)
  • Числа с плавающей точкой (float)
  • Строки (str)
  • Кортежи (tuple)
  • Логические значения (bool)
  • None (NoneType)

Посмотрим, как неизменяемость проявляется на примере целых чисел:

python
# Создание целого числа
x = 100
print("Original x:", x)           # Output: Original x: 100
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890123 (example)
 
# Это выглядит так, будто мы изменяем x, но на самом деле мы создаём новый объект
x = x + 1
print("Modified x:", x)           # Output: Modified x: 101
print("Identity of x:", id(x))    # Output: Identity of x: 140234567890456 (different!)

Идентичность изменилась, потому что x = x + 1 создало полностью новый объект целого числа со значением 101. Исходный объект со значением 100 всё ещё существует (до тех пор, пока сборщик мусора Python не удалит его), но теперь x ссылается на другой объект.

Строки демонстрируют неизменяемость ещё более наглядно:

python
# Создание строки
message = "Hello"
print("Original:", message)        # Output: Original: Hello
print("Identity:", id(message))    # Output: Identity: 140234567890789 (example)
 
# Методы строк не изменяют исходную строку — они возвращают новые строки
uppercase = message.upper()
print("Original:", message)        # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase)     # Output: Uppercase: HELLO
print("Identity of original:", id(message))    # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)

Даже операции, которые выглядят так, будто изменяют строку, на самом деле создают новые строковые объекты:

python
# Построение строки с помощью конкатенации
text = "Python"
print("Before:", text, "- ID:", id(text))  # Output: Before: Python - ID: 140234567891234 (example)
 
text = text + " Programming"
print("After:", text, "- ID:", id(text))   # Output: After: Python Programming - ID: 140234567891567 (different)

Почему неизменяемость важна: неизменяемые объекты безопасно разделять между разными частями программы, потому что ни одна часть не может случайно изменить их. Это делает код более предсказуемым и более простым для понимания.

18.3.2) Изменяемые типы: значения, которые могут изменяться

Изменяемый(mutable) объект — это объект, значение которого можно изменить после создания без создания нового объекта. Идентичность объекта остаётся прежней, но его содержимое можно модифицировать.

К изменяемым типам Python относятся:

  • Списки (list)
  • Словари (dict)
  • Множества (set)

Посмотрим на изменяемость на примере списков:

python
# Создание списка
numbers = [1, 2, 3]
print("Original:", numbers)        # Output: Original: [1, 2, 3]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (example)
 
# Изменение списка — тот же объект, другое значение
numbers.append(4)
print("Modified:", numbers)        # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers))    # Output: Identity: 140234567892345 (same!)

Идентичность не изменилась, потому что мы изменили существующий объект списка, а не создали новый. Это принципиально отличается от того, как работают неизменяемые типы.

Словари и множества тоже являются изменяемыми:

python
# Пример со словарём
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student))  # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
 
student["grade"] = 90  # Изменение словаря
print("After:", student, "- ID:", id(student))   # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
 
# Пример с множеством
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers))  # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
 
unique_numbers.add(4)  # Изменение множества
print("After:", unique_numbers, "- ID:", id(unique_numbers))   # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)

18.3.3) Почему изменяемость важна на практике

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

python
# Пример с неизменяемым объектом — безопасное разделение
x = "Hello"
y = x  # y ссылается на тот же объект строки
 
# «Изменение» x создаёт новый объект
x = x + " World"
 
print(x)  # Output: Hello World
print(y)  # Output: Hello (unchanged - y still refers to the original)
python
# Пример с изменяемым объектом — общие изменения
list1 = [1, 2, 3]
list2 = list1  # list2 ссылается на ТОТ ЖЕ объект списка
 
# Изменение через list1 влияет на list2
list1.append(4)
 
print(list1)  # Output: [1, 2, 3, 4]
print(list2)  # Output: [1, 2, 3, 4] (also changed!)

Неизменяемые типы

int, float, str, tuple, bool, None

Значение не может изменяться

Операции создают новые объекты

Безопасно разделять

Изменяемые типы

list, dict, set

Значение может изменяться

Операции изменяют существующий объект

При разделении нужна осторожность

Понимание изменяемости необходимо для:

  1. Предсказания поведения: понимания, создаёт ли операция новый объект или изменяет существующий
  2. Избежания багов: предотвращения непреднамеренных изменений при совместном использовании объектов
  3. Написания эффективного кода: выбора правильного типа под вашу задачу
  4. Понимания поведения функций: понимания, когда параметры функций могут быть изменены

В следующих разделах мы рассмотрим, как работает присваивание с этими разными типами и как при необходимости создавать независимые копии.

18.4) Как работает присваивание с объектами

Присваивание в Python не копирует объекты — оно создаёт ссылки(references) на объекты. Понимание этого различия критически важно для написания корректных программ, особенно при работе с изменяемыми типами.

18.4.1) Присваивание создаёт ссылки, а не копии

Когда вы пишете x = y, Python не создаёт копию объекта, на который ссылается y. Вместо этого он делает так, чтобы x ссылалась на тот же объект, на который ссылается y. Обе переменные становятся именами одного и того же объекта в памяти.

Сначала посмотрим на это на неизменяемых объектах:

python
# Присваивание с целыми числами (неизменяемые)
a = 100
b = a  # b теперь ссылается на тот же объект целого числа, что и a
 
print("a:", a)           # Output: a: 100
print("b:", b)           # Output: b: 100
print("Same object?", id(a) == id(b))  # Output: Same object? True
 
# «Изменение» a создаёт новый объект
a = a + 1
 
print("a:", a)           # Output: a: 101
print("b:", b)           # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b))  # Output: Same object? False

С неизменяемыми объектами такое поведение обычно безопасно, потому что вы не можете изменить исходный объект. Когда вы выполняете операцию, которая меняет значение, Python создаёт новый объект.

Однако с изменяемыми объектами поведение совсем другое:

python
# Присваивание со списками (изменяемые)
list1 = [1, 2, 3]
list2 = list1  # list2 ссылается на ТОТ ЖЕ объект списка, что и list1
 
print("list1:", list1)   # Output: list1: [1, 2, 3]
print("list2:", list2)   # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2))  # Output: Same object? True
 
# Изменение через list1 влияет на list2
list1.append(4)
 
print("list1:", list1)   # Output: list1: [1, 2, 3, 4]
print("list2:", list2)   # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2))  # Output: Same object? True

И list1, и list2 — это имена одного и того же объекта списка. Когда вы изменяете список через любое имя, вы видите изменение через оба имени, потому что список только один.

Присваивание с неизменяемыми типами

Обе переменные сначала ссылаются на один и тот же объект

Операции создают новые объекты

Переменные становятся независимыми

Присваивание с изменяемыми типами

Обе переменные ссылаются на один и тот же объект

Операции изменяют общий объект

Изменения видны через обе переменные

Вот практический пример, показывающий, почему это важно:

python
# Управление оценками студента
alice_grades = [85, 90, 92]
backup_grades = alice_grades  # Попытка создать резервную копию
 
print("Original:", alice_grades)  # Output: Original: [85, 90, 92]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92]
 
# Добавление новой оценки
alice_grades.append(88)
 
# «Резервная копия» тоже изменилась!
print("Original:", alice_grades)  # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades)   # Output: Backup: [85, 90, 92, 88]

Это вовсе не резервная копия — обе переменные ссылаются на один и тот же список. Чтобы создать настоящую резервную копию, нужно создать копию (мы рассмотрим это в разделе 18.8).

18.4.2) Присваивание при вызовах функций

Когда вы передаёте аргумент в функцию, Python использует ту же ссылочную семантику(reference semantics). Параметр становится ещё одним именем для того же объекта:

python
# Функция с неизменяемым параметром
def increment(number):
    number = number + 1  # Создаёт новый объект
    return number
 
value = 5
result = increment(value)
 
print("Original value:", value)    # Output: Original value: 5 (unchanged)
print("Returned result:", result)  # Output: Returned result: 6

Параметр number изначально ссылается на тот же объект целого числа, что и value. Когда мы делаем number = number + 1, мы создаём новый объект целого числа и заставляем number ссылаться на него. Исходный объект (и value) остаются неизменными.

С изменяемыми объектами поведение другое:

python
# Функция с изменяемым параметром
def add_item(items, new_item):
    items.append(new_item)  # Изменяет исходный список
 
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
 
print("Original list:", shopping_list)  # Output: Original list: ['milk', 'bread', 'eggs']

Параметр items ссылается на тот же объект списка, что и shopping_list. Когда мы изменяем список через items, мы изменяем исходный список.

Вот распространённая ошибка и способ её избежать:

python
# ОШИБКА: непреднамеренное изменение исходного
def process_grades(grades):
    grades.append(100)  # Изменяет исходный!
    return grades
 
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]
 
# ПРАВИЛЬНО: создайте копию, если не хотите изменять исходный объект
def process_grades_safely(grades):
    # Создаём новый список с теми же элементами
    result = grades + [100]  # Конкатенация создаёт новый список
    return result
 
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
 
print("Original:", student_grades)  # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed)      # Output: Processed: [85, 90, 92, 100]

Важное примечание о изменяемых аргументах по умолчанию: связанная распространённая ловушка — использование изменяемых объектов в качестве значений параметров по умолчанию (например, def func(items=[]):). Параметры по умолчанию создаются один раз при определении функции, а не при каждом её вызове, что может приводить к неожиданному поведению, когда список по умолчанию накапливает значения между несколькими вызовами функции. Мы подробно рассмотрим это в главе 20, но имейте в виду, что это частый источник багов при работе с изменяемыми параметрами.

18.5) Ссылочная семантика и алиасинг объектов

Ссылочная семантика(reference semantics) означает, что переменные в Python — это имена, которые ссылаются на объекты, а не контейнеры, которые хранят значения. Когда несколько переменных ссылаются на один и тот же объект, это называют алиасингом(aliasing). Понимание алиасинга важно для предсказания поведения ваших программ.

18.5.1) Что такое алиасинг?

Алиасинг(aliasing) возникает, когда две или более переменных ссылаются на один и тот же объект в памяти. Эти переменные являются «алиасами» друг для друга — разными именами одного и того же.

Посмотрим на алиасинг на простом примере:

python
# Создание списка и алиаса
original = [1, 2, 3]
alias = original  # alias ссылается на тот же список, что и original
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Alias:", alias)        # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias))  # Output: Same object? True
 
# Изменение через алиас
alias.append(4)
 
# Изменение видно через оба имени
print("Original:", original)  # Output: Original: [1, 2, 3, 4]
print("Alias:", alias)        # Output: Alias: [1, 2, 3, 4]

В памяти есть только один объект списка, но у него два имени: original и alias. Любое изменение, сделанное через любое из имён, влияет на один и тот же базовый объект.

Вот более реалистичный пример с записями студентов:

python
# База данных студентов с алиасингом
students = {
    "alice": {"name": "Alice", "grade": 85},
    "bob": {"name": "Bob", "grade": 90}
}
 
# Создание алиаса на запись Alice
alice_record = students["alice"]
 
print("Alice's grade:", alice_record["grade"])  # Output: Alice's grade: 85
 
# Изменение через алиас
alice_record["grade"] = 95
 
# Изменение видно в исходном словаре
print("Updated grade:", students["alice"]["grade"])  # Output: Updated grade: 95

Переменная alice_record — это алиас для словаря, хранящегося по ключу students["alice"]. Когда мы изменяем alice_record, мы изменяем тот же словарь, который хранится внутри словаря students.

18.5.2) Обнаружение алиасинга с помощью оператора is

Вы можете проверить, являются ли две переменные алиасами (то есть ссылаются ли они на один и тот же объект), с помощью оператора is:

python
# Проверка алиасинга
list1 = [1, 2, 3]
list2 = list1      # Алиас
list3 = [1, 2, 3]  # Другой объект с тем же значением
 
print("list1 is list2:", list1 is list2)  # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3)  # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3)  # Output: list1 == list3: True (same value)

Оператор is проверяет идентичность(identity) (ссылаются ли две переменные на один и тот же объект), тогда как == проверяет значение(value) (имеют ли два объекта одинаковое содержимое). Мы подробно разберём это различие в разделе 18.6.

18.5.3) Алиасинг в коллекциях

Алиасинг становится более сложным, когда объекты хранятся в коллекциях:

python
# Создание списка списков
row = [0, 0, 0]
grid = [row, row, row]  # Все три элемента — алиасы одного и того же списка!
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Изменение одного элемента влияет на все строки
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]

Это распространённая ошибка при попытке создать двумерную сетку. Все три строки — алиасы одного и того же списка, поэтому изменение одной строки изменяет их все.

Правильный способ создать независимые строки:

python
# Создание независимых строк
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]  # Каждая строка — отдельный список
 
print("Grid:")
for r in grid:
    print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
 
# Теперь изменение одного элемента влияет только на эту строку
grid[0][0] = 1
 
print("\nAfter modification:")
for r in grid:
    print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]

18.6) Равенство, идентичность и принадлежность (==, is и in) для разных типов

Python предоставляет три фундаментальных оператора для сравнения и проверки отношений между объектами: == для равенства(equality), is для идентичности(identity) и in для принадлежности(membership). Понимание того, когда использовать каждый оператор, критически важно для написания корректных программ.

18.6.1) Равенство с == (сравнение значений)

Оператор == проверяет, имеют ли два объекта одно и то же значение(value). Неважно, является ли это один и тот же объект в памяти — важна только равенство содержимого.

python
# Сравнение значений с помощью ==
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 == list2)  # Output: True (same values)
print(list1 == list3)  # Output: True (same values)

Даже если list1 и list2 — разные объекты в памяти, у них одинаковое значение, поэтому == возвращает True.

Вот как == работает с разными типами:

python
# Равенство для разных типов
print(42 == 42)              # Output: True (same integer value)
print(42 == 42.0)            # Output: True (integer equals float with same value)
print("hello" == "hello")    # Output: True (same string value)
print([1, 2] == [1, 2])      # Output: True (same list contents)
print({"a": 1} == {"a": 1})  # Output: True (same dictionary contents)
 
# Разные значения
print(42 == 43)              # Output: False
print("hello" == "Hello")    # Output: False (case-sensitive)
print([1, 2] == [2, 1])      # Output: False (order matters)

Для коллекций == выполняет глубокое сравнение(deep comparison) — оно проверяет, равны ли все элементы:

python
# Глубокое сравнение с вложенными структурами
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True (all nested elements are equal)
 
# Даже если внутренние списки — разные объекты
print(id(list1[0]) == id(list2[0]))  # Output: False (different objects)
print(list1[0] == list2[0])          # Output: True (same values)

18.6.2) Идентичность с is (сравнение идентичности объектов)

Оператор is проверяет, ссылаются ли две переменные на один и тот же объект в памяти. Он сравнивает идентичность(identity), а не значение.

python
# Сравнение идентичностей с помощью is
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
 
print(list1 is list2)  # Output: False (different objects)
print(list1 is list3)  # Output: True (same object)
 
# Подтверждаем через id()
print(id(list1) == id(list2))  # Output: False
print(id(list1) == id(list3))  # Output: True

Когда использовать is: наиболее распространённое применение is — проверка на None:

python
# Проверка на None (правильный способ)
def find_student(name, students):
    """Return student record or None if not found."""
    for student in students:
        if student["name"] == name:
            return student
    return None
 
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 90}
]
 
result = find_student("Charlie", students)
 
# Используйте 'is' для проверки на None
if result is None:
    print("Student not found")  # Output: Student not found
else:
    print(f"Found: {result}")

18.6.3) Принадлежность с in (проверка вхождения)

Оператор in проверяет, содержится ли значение в коллекции. Он работает со строками, списками, кортежами, множествами и словарями:

python
# Принадлежность в разных типах
print(2 in [1, 2, 3])           # Output: True
print("hello" in "hello world")  # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5})     # Output: True

Для словарей in проверяет, существует ли ключ:

python
# Проверка принадлежности в словаре
student = {"name": "Alice", "grade": 85, "age": 20}
 
print("name" in student)    # Output: True (key exists)
print("Alice" in student)   # Output: False (value, not key)
print("grade" in student)   # Output: True (key exists)
 
# Для проверки значений нужно использовать .values()
print("Alice" in student.values())  # Output: True

Оператор not in проверяет отсутствие:

python
# Проверка отсутствия
shopping_list = ["milk", "bread", "eggs"]
 
if "butter" not in shopping_list:
    print("Don't forget to buy butter!")  # Output: Don't forget to buy butter!

Сводка, когда использовать каждый оператор:

  • Используйте ==, когда хотите проверить, имеют ли два объекта одинаковое значение
  • Используйте is, когда хотите проверить, ссылаются ли две переменные на один и тот же объект (чаще всего с None или при отладке алиасинга)
  • Используйте in, когда хотите проверить, содержится ли значение в коллекции

Понимание этих различий помогает писать более точные и корректные сравнения в ваших программах.

18.7) Сравнение объектов, которые содержат другие объекты

Когда объекты содержат другие объекты (например, списки внутри списков или словари, содержащие списки), сравнения становятся более тонкими. Понимание того, как Python сравнивает вложенные структуры, важно при работе со сложными данными.

18.7.1) Как работает == с вложенными структурами

Оператор == выполняет рекурсивное сравнение(recursive comparison) для вложенных структур. Он сравнивает не только внешний контейнер, но и все вложенные объекты:

python
# Сравнение вложенных списков
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
 
print(list1 == list2)  # Output: True
 
# Хотя внутренние списки — разные объекты
print(id(list1[0]) == id(list2[0]))  # Output: False
print(list1[0] == list2[0])          # Output: True

Python рекурсивно сравнивает каждый элемент. Чтобы list1 == list2 было True, каждый соответствующий элемент должен быть равен, включая вложенные элементы.

Вот более сложный пример:

python
# Вложенная структура с несколькими уровнями
data1 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
data2 = {
    "students": [
        {"name": "Alice", "grades": [85, 90, 92]},
        {"name": "Bob", "grades": [88, 91, 87]}
    ],
    "class": "Python 101"
}
 
print(data1 == data2)  # Output: True

Python сравнивает:

  1. Ключи и значения словаря на верхнем уровне ("students" и "class")
  2. Список студентов
  3. Словарь каждого студента (с ключами "name" и "grades")
  4. Список оценок каждого студента
  5. Каждую отдельную оценку

Все уровни должны совпасть, чтобы сравнение вернуло True.

18.7.2) Порядок важен для последовательностей

Для последовательностей (списков и кортежей) порядок элементов важен:

python
# В списках порядок важен
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
 
print(list1 == list2)  # Output: False (different order)
 
# Но для множеств порядок не важен
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
 
print(set1 == set2)  # Output: True (sets are unordered)

18.7.3) Сравнение коллекций разных типов

Разные типы коллекций (list, tuple, set) никогда не равны друг другу, даже если они содержат одинаковые элементы:

python
# Сравнение разных типов
print([1, 2, 3] == (1, 2, 3))  # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3})  # Output: False (list vs set)
 
# Даже при одинаковых элементах
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
 
print(list_version == tuple_version)  # Output: False
print(list_version == set_version)    # Output: False
print(tuple_version == set_version)   # Output: False

18.8) Поверхностные копии списков, словарей и множеств

При работе с изменяемыми объектами вам часто нужно создавать независимые копии, чтобы избежать непреднамеренных изменений. Например, при резервном копировании данных перед обработкой, создании тестовых сценариев без влияния на продакшен-данные или передаче данных в функции, которые не должны изменять оригинал. Понимание того, как работают механизмы копирования в Python, помогает при необходимости создавать действительно независимые копии.

Однако не все методы копирования создают полностью независимые копии. Понимание разницы между поверхностными(shallow) и глубокими(deep) копиями важно, чтобы избегать тонких багов.

18.8.1) Что такое поверхностная копия?

Поверхностная копия(shallow copy) создаёт новый объект, но не создаёт копии объектов, которые находятся внутри него. Вместо этого новый объект содержит ссылки на те же вложенные объекты, что и оригинал.

Посмотрим на это на простом списке:

python
# Создание поверхностной копии простого списка
original = [1, 2, 3]
copy = original.copy()  # Создаёт поверхностную копию
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3]
 
# Это разные объекты
print("Same object?", original is copy)  # Output: Same object? False
 
# Изменение копии не влияет на оригинал
copy.append(4)
 
print("Original:", original)  # Output: Original: [1, 2, 3]
print("Copy:", copy)          # Output: Copy: [1, 2, 3, 4]

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

Но что происходит со вложенными структурами? Посмотрим, где поверхностные копии проявляют свои ограничения:

python
# Поверхностная копия со вложенными списками
original = [[1, 2], [3, 4]]
copy = original.copy()
 
print("Original:", original)  # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2], [3, 4]]
 
# Внешние списки — разные объекты
print("Same outer list?", original is copy)  # Output: Same outer list? False
 
# Но вложенные списки — ЭТО ТЕ ЖЕ объекты
print("Same nested list?", original[0] is copy[0])  # Output: Same nested list? True
 
# Изменение вложенного списка влияет на оба
copy[0].append(99)
 
print("Original:", original)  # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy)          # Output: Copy: [[1, 2, 99], [3, 4]]

Исходный список

Вложенный список 1: 1, 2, 99

Вложенный список 2: 3, 4

Поверхностная копия

18.8.2) Создание поверхностных копий списков

Есть несколько способов создать поверхностную копию списка:

python
# Метод 1: использование метода copy()
original = [[1, 2], [3, 4]]
copy1 = original.copy()
 
# Метод 2: срез списка
copy2 = original[:]
 
# Метод 3: конструктор list()
copy3 = list(original)
 
# Все три создают поверхностные копии
print(copy1)  # Output: [[1, 2], [3, 4]]
print(copy2)  # Output: [[1, 2], [3, 4]]
print(copy3)  # Output: [[1, 2], [3, 4]]
 
# Внешний список — другой
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
print(original is copy3)  # Output: False
 
# Но внутренние списки РАЗДЕЛЯЮТСЯ
print(original[0] is copy1[0])  # Output: True
print(original[0] is copy2[0])  # Output: True
print(original[0] is copy3[0])  # Output: True

18.8.3) Создание поверхностных копий словарей

Словари тоже поддерживают поверхностное копирование:

python
# Метод 1: использование метода copy()
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
 
# Метод 2: конструктор dict()
copy2 = dict(original)
 
# Оба создают поверхностные копии
print(copy1)  # Output: {'name': 'Alice', 'grade': 85}
print(copy2)  # Output: {'name': 'Alice', 'grade': 85}
 
# Это разные объекты
print(original is copy1)  # Output: False
print(original is copy2)  # Output: False
 
# Изменение копии не влияет на оригинал
copy1["grade"] = 90
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1)         # Output: Copy: {'name': 'Alice', 'grade': 90}

Однако с вложенными структурами действует то же ограничение поверхностной копии:

python
# Поверхностная копия со вложенным словарём
original = {
    "name": "Alice",
    "grades": [85, 90, 92]
}
 
copy = original.copy()
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
 
# Словари — разные объекты
print("Same dict?", original is copy)  # Output: Same dict? False
 
# Но список grades — ЭТО ТОТ ЖЕ объект
print("Same grades list?", original["grades"] is copy["grades"])  # Output: Same grades list? True
 
# Изменение списка grades влияет на оба
copy["grades"].append(88)
 
print("Original:", original)  # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy)          # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
© 2025. Primesoft Co., Ltd.
support@primesoft.ai