Копіювання об’єктів — це одна з фундаментальних концепцій у Python, нерозуміння якої часто призводить до помилок, які важко відстежити. Коли ви змінюєте один об’єкт, і несподівано змінюється інший, проблема, найімовірніше, криється в неправильному копіюванні. Розберімося в цьому раз і назавжди.
🔎 Чому взагалі важливе копіювання об’єктів?
У Python змінні — це не «коробки» з даними, а радше «мітки» або посилання, що вказують на об’єкти в пам’яті. Коли ви виконуєте просте присвоєння, наприклад:
list_a = [1, 2, 3]
list_b = list_a
Ви не створюєте новий список. Ви просто створюєте ще одне посилання (list_b), яке вказує на той самий об’єкт списку.
👉 Будь-яка зміна list_a негайно відобразиться в list_b (і навпаки), тому що це один і той самий об’єкт.
Аналогія: Уявіть, що у вас є документ у Google Docs. Якщо ви надішлете другу посилання на нього, він редагуватиме ваш оригінальний документ. Це і є присвоєння. А ось якщо ви виберете «Файл» → «Створити копію», з’явиться повністю незалежний документ. Саме для створення таких незалежних копій у Python існує модуль copy.
⚙️ Два типи копіювання
Модуль copy надає дві ключові функції:
copy.copy()— створює поверхневу копію (shallow copy).copy.deepcopy()— створює глибоку копію (deep copy).
Основна різниця між ними полягає в тому, як вони обробляють вкладені об’єкти (наприклад, списки всередині списків або словники всередині словників).
🟦 Поверхневе копіювання (shallow copy)
copy.copy() створює новий об’єкт-контейнер, але замість копіювання вкладених об’єктів він просто вставляє в новий контейнер посилання на них.
import copy
original_list = [1, 2, [3, 4]]
shallow_copy = copy.copy(original_list)
# Змінимо вкладений список в оригіналі
original_list[2][0] = 99
print(f"Оригінал: {original_list}") # Вивід: Оригінал: [1, 2, [99, 4]]
print(f"Поверхнева копія: {shallow_copy}") # Вивід: Поверхнева копія: [1, 2, [99, 4]]
Як бачите, зміна вкладеного списку торкнулася і оригіналу, і копії, тому що цей вкладений список у них спільний.
ASCII-діаграма:
original_list → [ 1, 2, ───► list_inner ]
shallow_copy → [ 1, 2, ───► list_inner ]
list_inner → [ 99, 4 ]
🟩 Глибоке копіювання (deep copy)
copy.deepcopy() розв’язує проблему спільних посилань. Ця функція рекурсивно обходить весь об’єкт і створює повні, незалежні копії всього, що зустрічає на своєму шляху, включно з усіма вкладеними об’єктами.
import copy
original_list = [1, 2, [3, 4]]
deep_copy = copy.deepcopy(original_list)
# Змінимо вкладений список в оригіналі
original_list[2][0] = 99
print(f"Оригінал: {original_list}") # Вивід: Оригінал: [1, 2, [99, 4]]
print(f"Глибока копія: {deep_copy}") # Вивід: Глибока копія: [1, 2, [3, 4]]
Тепер deep_copy є повністю незалежною.
ASCII-діаграма:
original_list → [ 1, 2, ───► list_a ]
deep_copy → [ 1, 2, ───► list_b ]
list_a → [ 99, 4 ]
list_b → [ 3, 4 ]
📑 Шпаргалка: copy.copy() vs copy.deepcopy()
| Тип об’єкта | Приклад | copy.copy() (поверхнева) | copy.deepcopy() (глибока) |
|---|---|---|---|
| list (список) | [1, 2, [3, 4]] | Новий список, але вкладений [3, 4] є спільним | Повністю незалежна копія, вкладений список також копіюється |
| dict (словник) | {"a": 1, "b": {"c": 2}} | Новий словник, але вкладений {"c": 2} є спільним | Незалежний словник і всі вкладені словники |
| set (множина) | {1, 2, (3, 4)} | Нова множина, вкладені змінювані елементи є спільними | Нова множина, всі вкладені елементи рекурсивно копіюються |
| tuple (кортеж) | (1, 2, [3, 4]) | Повертає той самий кортеж, але вкладений список є спільним | Новий кортеж з копією вкладеного списку |
| str, int, float, bool | "hi", 42, 3.14, True | Повертає той самий об’єкт (вони незмінні, копіювати немає сенсу) | Те ж саме |
| користувацький клас | Node(1, Node(2)) | Новий об’єкт верхнього рівня, вкладені об’єкти є спільними | Повністю незалежна копія об’єкта та всіх вкладених об’єктів |
об’єкт з __slots__ | Клас із __slots__ | Копіюється лише об’єкт верхнього рівня | Копіюються всі вкладені об’єкти, якщо вони підтримують копіювання |
| файл / сокет / потік | open("file.txt") | Помилка (об’єкт, що не копіюється) | Помилка (об’єкт, що не копіюється) |
🧪 Живі приклади до таблиці
📌 list
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # [1, 2, [99, 4]]
print(f"deepcopy: {c}") # [1, 2, [3, 4]]
📌 dict
import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}") # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}") # {'a': 1, 'b': {'c': 2}}
📌 set
import copy
a = {1, 2, (3, 4)} # Множини можуть містити лише незмінні типи
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (але це два незалежні об'єкти в пам'яті)
📌 tuple
import copy
a = (1, 2, [3, 4]) # Кортеж є незмінним, але його елемент-список — так
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}") # (1, 2, [99, 4])
print(f"deepcopy: {c}") # (1, 2, [3, 4])
📌 Користувацький клас
import copy
class Node:
def __init__(self, value, child=None):
self.value = value
self.child = child
a = Node(1, Node(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.child.value = 99
print(f"copy: {b.child.value}") # 99
print(f"deepcopy: {c.child.value}") # 2
📌 Кастомізація поведінки: __copy__ та __deepcopy__
Ви можете керувати тим, як копіюються ваші власні об’єкти, визначивши в їхньому класі спеціальні методи.
import copy
class Custom:
def __init__(self, x):
self.x = x
def __copy__(self):
print("Викликано __copy__!")
# Повертаємо новий екземпляр, але з тим самим посиланням на внутрішні дані
return Custom(self.x)
def __deepcopy__(self, memo):
# memo — словник для відстеження вже скопійованих об'єктів (захист від рекурсії)
print("Викликано __deepcopy__!")
# Створюємо повністю нову копію, включно з вкладеними даними
return Custom(copy.deepcopy(self.x, memo))
obj = Custom([1, 2])
c1 = copy.copy(obj) # Вивід: Викликано __copy__!
c2 = copy.deepcopy(obj) # Вивід: Викликано __deepcopy__!
⚡ Продуктивність та підводні камені
- Швидкість:
copy.copy()працює значно швидше, оскільки не обходить рекурсивно всю структуру даних.copy.deepcopy()може бути дуже повільним на великих і глибоко вкладених об’єктах. - Об’єкти, що не копіюються: Системні ресурси, такі як відкриті файли, сокети або потоки, скопіювати неможливо. Спроба зробити це викличе помилку.
- Циклічні посилання:
deepcopyдостатньо розумний, щоб обробляти циклічні посилання (коли об’єктAпосилається наB, аB— назад наA), не потрапляючи у нескінченну рекурсію.
Коли що використовувати: головні правила
- Використовуйте поверхневе копіювання (
copy.copy()), якщо ваш об’єкт містить лише незмінні дані або якщо ви свідомо хочете, щоб копія та оригінал розділяли спільні вкладені об’єкти. Це швидко та ефективно. - Використовуйте глибоке копіювання (
copy.deepcopy()), коли вам потрібна повна, стовідсоткова незалежність копії від оригіналу, особливо при роботі зі складними структурами даних (списками, словниками, екземплярами класів). - Для незмінних об’єктів (числа, рядки, кортежі без змінних елементів) копіювання, як правило, не має сенсу — Python і так оптимізує їх використання.
- Пам’ятайте про можливість кастомізації через
__copy__та__deepcopy__для повного контролю над процесом.