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ôngcopy.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().