
Chào các 'dev-er' tương lai! Anh Creyt lại lên sóng với một chủ đề mà nghe thì tưởng đơn giản, nhưng lại là cái bẫy 'ngọt ngào' khiến bao nhiêu anh tài phải 'vò đầu bứt tai' khi mới vào nghề: copy và deepcopy trong Python. Nghe tên là thấy mùi sao chép rồi đúng không? Nhưng tin anh đi, không phải cái 'copy-paste' thần thánh mà các em hay dùng đâu!
1. copy và deepcopy là gì? (Hay: Khi nào dữ liệu của em 'nhân bản vô tính'?)
Để dễ hình dung, các em cứ tưởng tượng thế này:
a. Tham Chiếu (Reference) - 'Chỉ đường' chứ không 'nhân bản'
Trước khi nói đến copy và deepcopy, anh em mình phải hiểu cái này trước đã. Khi các em gán b = a trong Python (với a là một đối tượng có thể thay đổi như list hay dict), các em không hề tạo ra một bản sao của a đâu nhé. Thực ra, các em chỉ đang tạo ra một cái tên khác (b) cùng trỏ vào một 'địa chỉ nhà' (object) trong bộ nhớ với cái tên a mà thôi.
Giống như việc các em và bạn cùng lưu một đường link Google Drive về bức ảnh 'tự sướng' của cả nhóm. Nếu một đứa vào chỉnh sửa ảnh gốc trên Drive, thì đứa kia mở lên cũng thấy bản đã sửa. Đó là tham chiếu!
list_goc = [1, 2, 3]
list_tham_chieu = list_goc # list_tham_chieu và list_goc cùng trỏ về 1 đối tượng
list_tham_chieu.append(4)
print(f"List gốc sau khi sửa: {list_goc}") # Output: [1, 2, 3, 4] - A hú!
print(f"List tham chiếu: {list_tham_chieu}") # Output: [1, 2, 3, 4]
b. copy (Shallow Copy) - 'Sao chép nông' hay 'Nhân bản cấp 1'
Bây giờ đến copy. Cái này giống như các em chụp ảnh màn hình một bài post trên Instagram. Các em có một bản sao của bài post đó trên máy mình. Các em có thể chỉnh sửa, vẽ vời lên bản ảnh chụp màn hình đó mà không ảnh hưởng đến bài post gốc trên Instagram của đứa bạn.
NHƯNG! Nếu bài post đó lại là một album ảnh (tức là cấu trúc lồng nhau), và các em chụp ảnh màn hình cả album. Các em có thể đổi tên album đã chụp, di chuyển nó, nhưng các bức ảnh CON bên TRONG album đó thì sao? Chúng vẫn là CÙNG MỘT BỨC ẢNH với các bức ảnh con trong album gốc. Nếu đứa bạn sửa một bức ảnh trong album gốc, thì bức ảnh đó trong album đã chụp màn hình của các em cũng... thay đổi theo! Hơi 'lú' đúng không?
Đó chính là shallow copy (sao chép nông). Nó tạo ra một đối tượng MỚI cho cấu trúc chính (list, dict, set), nhưng với các phần tử CON bên trong (nếu các phần tử đó là các đối tượng có thể thay đổi như list, dict khác), nó vẫn giữ nguyên các tham chiếu đến các đối tượng con gốc. Tức là, thay đổi các đối tượng con trong bản sao sẽ ảnh hưởng đến các đối tượng con trong bản gốc.
Khi nào dùng? Khi các em chỉ cần một bản sao của cấu trúc cấp độ đầu tiên, và các phần tử bên trong là các kiểu dữ liệu bất biến (số, chuỗi, tuple không chứa mutable) hoặc các em chấp nhận việc chúng vẫn chia sẻ tham chiếu.
c. deepcopy (Deep Copy) - 'Sao chép sâu' hay 'Nhân bản vô tính toàn diện'
Đây mới là 'vô tính' thực sự! deepcopy giống như việc các em tải nguyên cả một bộ phim về máy tính cá nhân. Các em có một bản sao hoàn toàn độc lập, từ cái vỏ bên ngoài đến từng khung hình, từng pixel bên trong. Các em có thể cắt ghép, chỉnh sửa, xóa cảnh nào đó trong bản phim của mình mà không một chút tẹo nào ảnh hưởng đến bản gốc của nhà sản xuất phim.
deepcopy tạo ra một đối tượng MỚI và đệ quy tạo ra các đối tượng MỚI cho tất cả các phần tử con, cháu, chắt... bên trong nó. Tức là, mọi thứ đều độc lập hoàn toàn. Chỉnh sửa bản sao không bao giờ ảnh hưởng đến bản gốc, và ngược lại. Đây là 'sao chép toàn diện'.
Khi nào dùng? Khi các em cần một bản sao hoàn toàn độc lập, không có bất kỳ sự chia sẻ tham chiếu nào với đối tượng gốc, đặc biệt với các đối tượng chứa các đối tượng có thể thay đổi (mutable objects) lồng nhau.
2. Code Ví Dụ Minh Họa Đàng Hoàng
Để 'sáng mắt' hơn, chúng ta cùng xem vài ví dụ code nhé. Nhớ import copy khi dùng deepcopy!
import copy
print("\n--- Ví dụ 1: Đối tượng đơn giản (Mutable) ---")
list_goc = [1, 2, 3]
# Gán (Reference)
list_tham_chieu = list_goc
list_tham_chieu.append(4)
print(f"Gán (Reference) - List gốc: {list_goc}, List tham chiếu: {list_tham_chieu}") # Cả hai đều thay đổi
# Shallow Copy (list.copy() hoặc list() hoặc list[:])
list_goc_2 = [10, 20, 30]
list_shallow = list_goc_2.copy()
list_shallow.append(40)
print(f"Shallow Copy - List gốc: {list_goc_2}, List shallow: {list_shallow}") # List gốc không thay đổi
# Deep Copy (Không cần thiết lắm với list đơn giản này, nhưng để so sánh)
list_goc_3 = [100, 200, 300]
list_deep = copy.deepcopy(list_goc_3)
list_deep.append(400)
print(f"Deep Copy - List gốc: {list_goc_3}, List deep: {list_deep}") # List gốc không thay đổi
print("\n--- Ví dụ 2: Đối tượng lồng nhau (List of Lists) ---")
list_lon_goc = [[1, 2], [3, 4]]
# Gán (Reference)
list_lon_tham_chieu = list_lon_goc
list_lon_tham_chieu[0].append(5) # Sửa phần tử con
print(f"Gán (Reference) - List lồng gốc: {list_lon_goc}, List lồng tham chiếu: {list_lon_tham_chieu}")
# Output: [[1, 2, 5], [3, 4]], [[1, 2, 5], [3, 4]] -> Cả hai đều thay đổi
print("\n--- Ví dụ 3: Shallow Copy với đối tượng lồng nhau ---")
list_lon_goc_2 = [[10, 20], [30, 40]]
list_lon_shallow = list_lon_goc_2.copy() # Tạo bản sao của list ngoài
list_lon_shallow.append([50, 60]) # Thêm phần tử mới vào list ngoài của bản sao -> Không ảnh hưởng gốc
list_lon_shallow[0].append(25) # Sửa phần tử con của list ngoài bản sao -> ẢNH HƯỞNG GỐC!
print(f"Shallow Copy - List lồng gốc: {list_lon_goc_2}")
# Output: [[10, 20, 25], [30, 40]] -> Phần tử con trong gốc bị sửa!
print(f"Shallow Copy - List lồng shallow: {list_lon_shallow}")
# Output: [[10, 20, 25], [30, 40], [50, 60]]
print("\n--- Ví dụ 4: Deep Copy với đối tượng lồng nhau ---")
list_lon_goc_3 = [['A', 'B'], ['C', 'D']]
list_lon_deep = copy.deepcopy(list_lon_goc_3) # Tạo bản sao hoàn toàn độc lập
list_lon_deep.append(['E', 'F'])
list_lon_deep[0].append('X') # Sửa phần tử con của list ngoài bản sao -> KHÔNG ẢNH HƯỞNG GỐC!
print(f"Deep Copy - List lồng gốc: {list_lon_goc_3}")
# Output: [['A', 'B'], ['C', 'D']] -> Hoàn toàn nguyên vẹn!
print(f"Deep Copy - List lồng deep: {list_lon_deep}")
# Output: [['A', 'B', 'X'], ['C', 'D'], ['E', 'F']]

3. Mẹo (Best Practices) để ghi nhớ và dùng thực tế
- Quy tắc 'Ngón Tay Cái' (Rule of Thumb):
- Nếu đối tượng của em chỉ chứa các phần tử 'bất biến' (immutable) như số, chuỗi, tuple (mà tuple đó không chứa các đối tượng mutable bên trong), thì
copyhaydeepcopyđều cho kết quả như nhau (và thường chỉ cầncopyhoặc thậm chí gán rồi tạo đối tượng mới cho mutable là đủ). - Nếu đối tượng của em chứa các phần tử 'có thể thay đổi' (mutable) như
list,dict,set, hoặc cáccustom objectskhác, thì hãy CẨN THẬN!copychỉ sao chép cấp độ đầu tiên. Nếu em muốn tất cả các cấp đều độc lập hoàn toàn, không liên quan gì đến nhau, thì DÙNGdeepcopy.
- Nếu đối tượng của em chỉ chứa các phần tử 'bất biến' (immutable) như số, chuỗi, tuple (mà tuple đó không chứa các đối tượng mutable bên trong), thì
- Hiệu suất là vấn đề:
deepcopytốn tài nguyên (CPU và bộ nhớ) hơncopyrất nhiều vì nó phải 'lặn sâu' vào từng ngóc ngách của đối tượng để tạo bản sao. Chỉ dùng khi thực sự cần sự độc lập hoàn toàn. Đừng lạm dụng! - Nhớ
import copy: Muốn dùngdeepcopy, đừng quên dòngimport copyở đầu file nhé. - Hình dung bằng sơ đồ cây: Hãy tưởng tượng đối tượng của em là một cái cây.
copygiống như việc em chặt cái cây đó ở gốc và trồng nó vào một chậu mới, nhưng các cành con (đối tượng con) trên cây đó vẫn là cành cũ, vẫn dính vào nhau. Còndeepcopylà em nhổ cả rễ, sao chép cả cây lẫn từng chiếc lá, từng cái rễ con, rồi trồng một cây hoàn toàn mới, không liên quan gì đến cây cũ nữa.
4. Ứng dụng thực tế: Khi nào thì 'nhân bản vô tính' phát huy tác dụng?
copy và deepcopy không phải là những thứ 'trên trời rơi xuống' đâu, chúng được ứng dụng rất nhiều trong các hệ thống thực tế:
- Phát triển Game: Khi người chơi lưu game (save game), hệ thống cần tạo một bản sao hoàn toàn độc lập của trạng thái game hiện tại (vị trí nhân vật, vật phẩm, nhiệm vụ...). Nếu chỉ dùng
copymà trạng thái game có cấu trúc lồng nhau, khi người chơi tiếp tục chơi và thay đổi trạng thái, bản save game cũng bị thay đổi theo, và đó là một 'bug' cực kỳ khó chịu! - Chức năng Undo/Redo: Trong các trình chỉnh sửa văn bản, ảnh (như Photoshop), video... mỗi khi các em thực hiện một thao tác, hệ thống cần lưu lại trạng thái trước đó để các em có thể 'undo'. Để đảm bảo việc 'undo' không làm hỏng trạng thái hiện tại hoặc các trạng thái đã lưu khác, việc tạo một
deepcopycủa trạng thái là cực kỳ quan trọng. - Quản lý cấu hình (Configuration Management): Một ứng dụng có thể đọc một file cấu hình và tạo ra một đối tượng cấu hình. Nếu các module khác nhau trong ứng dụng cần tùy chỉnh cấu hình này cho riêng mình mà không muốn ảnh hưởng đến cấu hình gốc hoặc cấu hình của các module khác, họ sẽ cần một
deepcopycủa đối tượng cấu hình đó. - Machine Learning/AI: Khi huấn luyện các mô hình, các nhà khoa học dữ liệu có thể muốn thử nghiệm nhiều bộ tham số khác nhau trên cùng một tập dữ liệu hoặc trên một bản sao của mô hình mà không làm hỏng bản gốc, để có thể so sánh và quay lại các phiên bản trước đó.
5. Thử nghiệm và Hướng dẫn nên dùng cho case nào
Anh Creyt ngày xưa cũng đã từng 'ăn hành' vì nhầm lẫn giữa gán và copy đấy các em. Nhất là khi làm việc với cấu trúc dữ liệu lồng nhau, cứ tưởng copy rồi mà sửa chỗ này nó lại ảnh hưởng chỗ kia, ngáo ngơ cả buổi không hiểu tại sao. Đó là bài học xương máu về shallow copy!
Khi nào nên dùng copy (Shallow Copy)?
- Khi đối tượng của em chỉ chứa các phần tử immutable (số, chuỗi, tuple không chứa mutable). Trong trường hợp này,
copyhaydeepcopyđều cho kết quả tương tự về mặt chức năng, vàcopysẽ hiệu quả hơn. - Khi em chỉ cần sao chép cấp độ đầu tiên của một đối tượng mutable (ví dụ: một list các số), và em không quan tâm đến việc các phần tử bên trong có thể vẫn chia sẻ tham chiếu (vì chúng là immutable hoặc em có chủ đích muốn vậy).
- Khi em cần hiệu suất tốt hơn và không có cấu trúc lồng nhau mutable.
Khi nào nên dùng deepcopy (Deep Copy)?
- Khi đối tượng của em là một cấu trúc lồng nhau phức tạp (list of lists, dict of lists, custom objects chứa các đối tượng khác). Đây là lúc
deepcopythực sự tỏa sáng. - Khi em cần đảm bảo rằng bản sao hoàn toàn độc lập với bản gốc, mọi thay đổi trên bản sao sẽ không bao giờ ảnh hưởng đến bản gốc, và ngược lại.
- Khi em đang làm việc với các hệ thống cần sự 'tách biệt' hoàn toàn giữa các phiên bản dữ liệu (như các ví dụ về game saves, undo/redo, snapshot hệ thống).
Nhớ kỹ nhé các em, hiểu rõ copy và deepcopy không chỉ giúp các em tránh được những 'bug' khó nhằn mà còn thể hiện sự chuyên nghiệp và tư duy lập trình vững chắc. Cứ thực hành nhiều vào, rồi các em sẽ 'master' được thôi!
Thuộc Series: Python
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!