Sao chép nông và sâu trong Python: Hướng dẫn toàn diện

Giới thiệu

Trong Python, câu lệnh gán không tạo ra bản sao của đối tượng - chúng chỉ liên kết tên mới với đối tượng hiện có. Với các đối tượng bất biến (immutable), điều này thường không gây ra vấn đề gì. Tuy nhiên, khi làm việc với các đối tượng có thể thay đổi (mutable) hoặc các cấu trúc lồng nhau, bạn cần tạo ra các bản sao thực sự để có thể修改 mà không ảnh hưởng đến đối tượng gốc.

Bài viết này sẽ hướng dẫn bạn cách sao chép đối tượng trong Python3, phân biệt giữa sao chép nông (shallow copy) và sao chép sâu (deep copy), cũng như cách áp dụng chúng cho các đối tượng tùy ý.

Sao chép các collection có sẵn

Python cung cấp sẵn các hàm factory để sao chép các collection mutable như list, dictionary và set:

danh_sach_moi = list(danh_sach_goc)
tu_dien_moi = dict(tu_dien_goc)
tap_hop_moi = set(tap_hop_goc)

Tuy nhiên, phương pháp này chỉ tạo ra bản sao nông và không áp dụng được cho các đối tượng tùy chỉnh.

Sự khác biệt giữa sao chép nông và sâu

Sao chép nông (Shallow Copy)

Sao chép nông tạo một đối tượng collection mới và điền vào đó các tham chiếu đến các đối tượng con trong collection gốc. Nói cách khác, quá trình sao chép không đệ quy, do đó không tạo bản sao cho các đối tượng con.

Hãy xem ví dụ sau:

danh_sach = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ban_sao_nong = list(danh_sach)

Kiểm tra hai đối tượng:

danh_sach
ban_sao_nong

Kết quả sẽ như sau:

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Thêm một phần tử vào danh sách gốc:

danh_sach.append([10, 11, 12])
danh_sach
ban_sao_nong

Kết quả:

[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Như bạn thấy, việc thêm phần tử mới ở cấp đầu tiên không ảnh hưởng đến bản sao. Tuy nhiên, vì đây là sao chép nông, các đối tượng con vẫn được chia sẻ:

danh_sach[1][0] = 'X'
danh_sach
ban_sao_nong

Kết quả cho thấy cả hai đều bị ảnh hưởng:

[[1, 2, 3], ['X', 5, 6], [7, 8, 9], [10, 11, 12]]
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]

Đây là bản chất của sao chép nông: chỉ sao chép cấp độ đầu tiên, còn các đối tượng con vẫn là tham chiếu chung.

Sao chép sâu (Deep Copy)

Sao chép sâu thực hiện quá trình sao chép một cách đệ quy. Điều này có nghĩa là đầu tiên tạo một đối tượng collection mới, sau đó điền vào đó các bản sao của các đối tượng con được tìm thấy trong collection gốc.

Sử dụng hàm deepcopy() từ module copy:

import copy

danh_sach = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
ban_sao_sau = copy.deepcopy(danh_sach)

Kiểm tra hai đối tượng:

danh_sach
ban_sao_sau

Kết quả:

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Bây giờ hãy thử sửa đổi đối tượng gốc:

danh_sach[1][0] = 'X'
danh_sach
ban_sao_sau

Kết quả cho thấy bản sao sâu hoàn toàn độc lập:

[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Sao chép đối tượng tùy ý

Module copy của Python cung cấp hai hàm chính:

  • copy.copy() - tạo bản sao nông
  • copy.deepcopy() - tạo bản sao sâu

Hãy xem cách sử dụng chúng với các đối tượng tùy chỉnh. Đầu tiên, định nghĩa một lớp Diem đơn giản:

class Diem:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Diem({self.x!r}, {self.y!r})'

Tạo một instance và sao chép nông:

a = Diem(23, 42)
b = copy.copy(a)

Kiểm tra kết quả:

a
b
a is b

Kết quả:

Diem(23, 42)
Diem(23, 42)
False

Lưu ý rằng vì tọa độ sử dụng kiểu bất biến (int), trong trường hợp này sao chép nông và sâu không có khác biệt.

Tiếp theo, tạo một lớp phức tạp hơn - HìnhChuNhat sử dụng các đối tượng Diem:

class HinhChuNhat:
    def __init__(self, goc_tren, goc_duoi):
        self.goc_tren = goc_tren
        self.goc_duoi = goc_duoi

    def __repr__(self):
        return (f'HinhChuNhat({self.goc_tren!r}, '
                f'{self.goc_duoi!r})')

Tạo một hình chữ nhật và thử sao chép nông:

hcn = HinhChuNhat(Diem(0, 1), Diem(5, 6))
hcn_sao_chep = copy.copy(hcn)

Kiểm tra:

hcn
hcn_sao_chep
hcn is hcn_sao_chep

Kết quả:

HinhChuNhat(Diem(0, 1), Diem(5, 6))
HinhChuNhat(Diem(0, 1), Diem(5, 6))
False

Giờ hãy xem điều gì xảy ra khi ta thay đổi đối tượng con:

hcn.goc_tren.x = 999
hcn
hcn_sao_chep

Cả hai đều bị ảnh hưởng vì đây là sao chép nông:

HinhChuNhat(Diem(999, 1), Diem(5, 6))
HinhChuNhat(Diem(999, 1), Diem(5, 6))

Bây giờ tạo bản sao sâu và thử tương tự:

hcn_sau = copy.deepcopy(hcn_sao_chep)
hcn_sau.goc_tren.x = 222
hcn_sau
hcn
hcn_sao_chep

Kết quả cho thấy bản sao sâu hoàn toàn độc lập:

HinhChuNhat(Diem(222, 1), Diem(5, 6))
HinhChuNhat(Diem(999, 1), Diem(5, 6))
HinhChuNhat(Diem(999, 1), Diem(5, 6))

Bảng so sánh

Loại sao chép Mô tả Độc lập
Sao chép nông Chỉ sao chép cấp độ đầu tiên, đối tượng con là tham chiếu Không hoàn toàn
Sao chép sâu Sao chép đệ quy tất cả các cấp Hoàn toàn

Lưu ý về hiệu suất

Sao chép sâu chắc chắn sẽ chậm hơn sao chép nông vì cần duyệt qua toàn bộ cây đối tượng. Hãy cân nhắc điều này khi làm việc với các cấu trúc dữ liệu lớn.

Đối với các collection có sẵn, sử dụng trực tiếp các hàm factory (list(), dict(), set()) được coi là phong cách Python hơn so với việc sử dụng copy.copy().

Đăng vào ngày 16 tháng 9 lúc 04:23