Перейти до вмісту
> 💻 🧠 Код 1001 > 📑 Шпаргалки > 🐍 Шпаргал > Розбір копіювання об’єктів у Python

Розбір копіювання об’єктів у 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), не потрапляючи у нескінченну рекурсію.

Коли що використовувати: головні правила

  1. Використовуйте поверхневе копіювання (copy.copy()), якщо ваш об’єкт містить лише незмінні дані або якщо ви свідомо хочете, щоб копія та оригінал розділяли спільні вкладені об’єкти. Це швидко та ефективно.
  2. Використовуйте глибоке копіювання (copy.deepcopy()), коли вам потрібна повна, стовідсоткова незалежність копії від оригіналу, особливо при роботі зі складними структурами даних (списками, словниками, екземплярами класів).
  3. Для незмінних об’єктів (числа, рядки, кортежі без змінних елементів) копіювання, як правило, не має сенсу — Python і так оптимізує їх використання.
  4. Пам’ятайте про можливість кастомізації через __copy__ та __deepcopy__ для повного контролю над процесом.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *