Cách sao chép List trong Python
Python Tutorial | by
Khi làm việc với List, một lỗi phổ biến mà người mới học Python hay gặp phải là vô tình thay đổi List gốc khi tưởng rằng mình đang thao tác trên một bản sao. Điều này xảy ra bởi cách Python quản lý biến và bộ nhớ. Bài viết này sẽ giúp bạn hiểu rõ tầm quan trọng của việc sao chép List đúng cách và tìm hiểu các phương pháp hiệu quả để tạo ra những bản sao độc lập, tránh những "bất ngờ" không mong muốn trong code của bạn!
Tại sao cần sao chép List?
Trong Python, khi bạn gán một List cho một biến khác bằng toán tử =
, bạn không tạo ra một bản sao mới của List. Thay vào đó, bạn chỉ tạo ra một tham chiếu (reference) mới đến cùng một List trong bộ nhớ. Điều này có nghĩa là cả hai biến (cũ và mới) đều trỏ đến cùng một đối tượng List.
Vấn đề tham chiếu và thay đổi List gốc ngoài ý muốn: Khi cả hai biến cùng trỏ đến một List, nếu bạn thay đổi List thông qua một trong các biến, thì thay đổi đó sẽ hiển thị khi bạn truy cập List thông qua biến còn lại. Đây là một lỗi phổ biến và thường gây nhầm lẫn cho người mới học.
Ví dụ minh họa vấn đề gán biến (tham chiếu):
original_list = [1, 2, 3] # Gán biến: new_reference_list bây giờ là một tham chiếu đến cùng List với original_list new_reference_list = original_list print(f"Original List: {original_list}") # Output: Original List: [1, 2, 3] print(f"New Reference List: {new_reference_list}") # Output: New Reference List: [1, 2, 3] # Thay đổi List thông qua new_reference_list new_reference_list.append(4) print(f"Original List sau khi thay đổi new_reference_list: {original_list}") # Output: Original List sau khi thay đổi new_reference_list: [1, 2, 3, 4] print(f"New Reference List sau khi thay đổi: {new_reference_list}") # Output: New Reference List sau khi thay đổi: [1, 2, 3, 4] # Kiểm tra xem chúng có cùng một đối tượng trong bộ nhớ không print(f"original_list is new_reference_list: {original_list is new_reference_list}") # Output: original_list is new_reference_list: True
Như bạn thấy, khi new_reference_list
được thay đổi, original_list
cũng bị thay đổi theo vì chúng thực sự là một và cùng một đối tượng List trong bộ nhớ. Để tránh điều này và làm việc độc lập với dữ liệu, bạn cần sao chép List.
Hai loại sao chép chính: Shallow Copy và Deep Copy
Khi cần sao chép List, có hai khái niệm quan trọng bạn cần hiểu:
Sao chép nông (Shallow Copy):
-
Tạo ra một List mới.
-
Các phần tử của List mới này sẽ là tham chiếu đến các phần tử của List gốc.
-
Nếu các phần tử của List gốc là các kiểu dữ liệu không thể thay đổi (immutable - như số, chuỗi, tuple), thì thay đổi bản sao sẽ không ảnh hưởng đến bản gốc.
-
Tuy nhiên, nếu các phần tử của List gốc là các đối tượng có thể thay đổi (mutable - như List khác, dictionary, set), thì thay đổi các phần tử này trong bản sao sẽ ảnh hưởng đến bản gốc (và ngược lại) vì cả hai vẫn trỏ đến cùng một đối tượng con.
Sao chép sâu (Deep Copy):
-
Tạo ra một List mới.
-
Đồng thời, nó cũng tạo ra các bản sao độc lập của tất cả các đối tượng con có thể thay đổi trong List gốc (và các đối tượng con của đối tượng con, v.v.).
-
Điều này đảm bảo rằng List mới và tất cả các phần tử lồng nhau của nó hoàn toàn độc lập với List gốc. Thay đổi bất kỳ thứ gì trong bản sao sẽ không ảnh hưởng đến bản gốc, và ngược lại.
Chúng ta sẽ đi sâu vào từng loại trong các phần tiếp theo.
Sao chép nông (Shallow Copy) trong Python
Khái niệm: 얕은 복사 (Shallow Copy)
Sao chép nông tạo ra một List mới độc lập với List gốc. Tuy nhiên, nó chỉ sao chép "mức độ đầu tiên" của các phần tử. Điều này có nghĩa là:
-
Nếu List gốc chỉ chứa các phần tử không thể thay đổi (ví dụ: số nguyên, số thực, chuỗi, tuple), thì bản sao nông sẽ hoạt động giống như một bản sao sâu, vì các phần tử này không thể thay đổi được.
-
Nếu List gốc chứa các đối tượng có thể thay đổi (ví dụ: List lồng nhau, dictionary, các đối tượng tùy chỉnh), thì bản sao nông sẽ tạo ra một List mới, nhưng các phần tử bên trong List mới này vẫn là tham chiếu đến chính các đối tượng có thể thay đổi trong List gốc. Thay đổi các đối tượng con này trong bản sao sẽ ảnh hưởng đến bản gốc, và ngược lại.
Khi nào dùng Shallow Copy?
Bạn nên sử dụng sao chép nông khi:
-
List của bạn chỉ chứa các phần tử không thể thay đổi (số, chuỗi, tuple). Trong trường hợp này, shallow copy là đủ và hiệu quả.
-
Bạn chấp nhận rằng nếu List của bạn có các List lồng nhau hoặc các đối tượng có thể thay đổi khác, việc thay đổi chúng trong bản sao sẽ ảnh hưởng đến bản gốc (vì chúng vẫn là cùng một đối tượng).
Các phương pháp tạo Shallow Copy
Có nhiều cách để tạo một bản sao nông của List:
Sử dụng toán tử Slice ([:]
)
-
Cú pháp:
new_list = original_list[:]
-
Giải thích: Đây là cách rất "Pythonic" để tạo một bản sao nông. Bạn đang tạo một lát cắt (slice) của toàn bộ List, bắt đầu từ đầu đến cuối, và kết quả của lát cắt đó là một List mới.
-
Ưu điểm: Ngắn gọn, dễ đọc, và hiệu quả.
-
Nhược điểm: Chỉ là sao chép nông.
-
Ví dụ minh họa: Sao chép List số, List chuỗi.
# List chứa các số (immutable elements) numbers_original = [1, 2, 3] numbers_copy = numbers_original[:] print(f"Original numbers: {numbers_original}") # Output: Original numbers: [1, 2, 3] print(f"Copy numbers: {numbers_copy}") # Output: Copy numbers: [1, 2, 3] print(f"numbers_original is numbers_copy: {numbers_original is numbers_copy}") # Output: False (là đối tượng khác) numbers_copy.append(4) # Thay đổi bản sao print(f"Original numbers sau thay đổi copy: {numbers_original}") # Output: Original numbers sau thay đổi copy: [1, 2, 3] (Không bị ảnh hưởng) print(f"Copy numbers sau thay đổi: {numbers_copy}") # Output: Copy numbers sau thay đổi: [1, 2, 3, 4] # List chứa các chuỗi (immutable elements) words_original = ["apple", "banana"] words_copy = words_original[:] print(f"Original words: {words_original}") print(f"Copy words: {words_copy}") words_copy[0] = "orange" # Thay đổi phần tử chuỗi trong bản sao print(f"Original words sau thay đổi copy: {words_original}") # Output: Original words sau thay đổi copy: ['apple', 'banana'] (Không bị ảnh hưởng) print(f"Copy words sau thay đổi: {words_copy}") # Output: Copy words sau thay đổi: ['orange', 'banana']
Sử dụng hàm list()
-
Cú pháp:
new_list = list(original_list)
-
Giải thích: Hàm
list()
có thể được dùng để chuyển đổi mộtiterable
(bao gồm cả List) thành một List mới. Khi bạn truyền một List vàolist()
, nó sẽ tạo ra một bản sao nông của List đó. -
Ưu điểm: Rõ ràng, dễ hiểu.
-
Nhược điểm: Chỉ là sao chép nông.
Ví dụ minh họa: Sao chép List số, List chuỗi.
# List chứa các số scores_original = [90, 85, 92] scores_copy = list(scores_original) print(f"Original scores: {scores_original}") # Output: Original scores: [90, 85, 92] print(f"Copy scores: {scores_copy}") # Output: Copy scores: [90, 85, 92] print(f"scores_original is scores_copy: {scores_original is scores_copy}") # Output: False scores_copy.pop() # Xóa phần tử cuối cùng trong bản sao print(f"Original scores sau thay đổi copy: {scores_original}") # Output: Original scores sau thay đổi copy: [90, 85, 92] (Không bị ảnh hưởng) print(f"Copy scores sau thay đổi: {scores_copy}") # Output: Copy scores sau thay đổi: [90, 85]
Sử dụng List Comprehension
-
Cú pháp:
new_list = [item for item in original_list]
-
Giải thích: Như đã học, List Comprehension tạo ra một List mới. Khi bạn dùng cú pháp đơn giản nhất
[item for item in original_list]
, bạn đang duyệt qua từng phần tử củaoriginal_list
và thêm chính phần tử đó vàonew_list
. Điều này cũng tạo ra một bản sao nông. -
Ưu điểm: Rất linh hoạt (bạn có thể biến đổi các phần tử hoặc lọc chúng trong khi sao chép).
-
Nhược điểm: Chỉ là sao chép nông.
Ví dụ minh họa: Sao chép List số, List chuỗi.
# List chứa các chuỗi colors_original = ["red", "green", "blue"] colors_copy = [c for c in colors_original] print(f"Original colors: {colors_original}") # Output: Original colors: ['red', 'green', 'blue'] print(f"Copy colors: {colors_copy}") # Output: Copy colors: ['red', 'green', 'blue'] print(f"colors_original is colors_copy: {colors_original is colors_copy}") # Output: False colors_copy[1] = "yellow" # Thay đổi bản sao print(f"Original colors sau thay đổi copy: {colors_original}") # Output: Original colors sau thay đổi copy: ['red', 'green', 'blue'] (Không bị ảnh hưởng) print(f"Copy colors sau thay đổi: {colors_copy}") # Output: Copy colors sau thay đổi: ['red', 'yellow', 'blue']
Minh họa vấn đề của Shallow Copy với List lồng nhau/Đối tượng (Mutable elements)
Đây là điểm quan trọng nhất để hiểu sự khác biệt giữa shallow copy và deep copy. Khi List gốc chứa các đối tượng có thể thay đổi (ví dụ: các List con - nested lists, dictionary, set, hoặc các instance của class), shallow copy sẽ không tạo bản sao của các đối tượng con đó. Thay vào đó, nó tạo ra các tham chiếu đến các đối tượng con giống như trong List gốc.
Ví dụ: List chứa List con (nested list).
# List gốc chứa các List con (các List con là mutable objects) original_nested_list = [1, 2, [3, 4]] # Tạo shallow copy bằng toán tử slice shallow_copy_list = original_nested_list[:] print(f"Original nested list: {original_nested_list}") # Output: Original nested list: [1, 2, [3, 4]] print(f"Shallow copy list: {shallow_copy_list}") # Output: Shallow copy list: [1, 2, [3, 4]] # Thay đổi một phần tử không thể thay đổi trong List con của bản sao # Thay đổi List con (index 2), phần tử index 0 của List con shallow_copy_list[2][0] = 99 print(f"Original nested list sau thay đổi copy: {original_nested_list}") # Output: Original nested list sau thay đổi copy: [1, 2, [99, 4]] (Bị ảnh hưởng!) print(f"Shallow copy list sau thay đổi: {shallow_copy_list}") # Output: Shallow copy list sau thay đổi: [1, 2, [99, 4]] # Kiểm tra xem các List con có cùng một đối tượng trong bộ nhớ không print(f"Original nested sub-list is shallow_copy_list sub-list: {original_nested_list[2] is shallow_copy_list[2]}") # Output: Original nested sub-list is shallow_copy_list sub-list: True
Như bạn thấy, dù shallow_copy_list
là một đối tượng List riêng biệt (original_nested_list is shallow_copy_list
là False
), nhưng List con [3, 4]
bên trong nó vẫn là cùng một đối tượng với List con trong original_nested_list
. Khi shallow_copy_list[2][0]
được thay đổi, original_nested_list[2][0]
cũng bị thay đổi theo.
Sao chép sâu (Deep Copy) trong Python
Khi làm việc với các List phức tạp, đặc biệt là những List chứa các đối tượng có thể thay đổi khác như List con (nested lists), dictionaries, hoặc các instance của class, sao chép nông có thể không đủ để đảm bảo tính độc lập. Đây là lúc sao chép sâu phát huy tác dụng
Khái niệm: Sao chép sâu (Deep Copy)
Sao chép sâu (Deep Copy) tạo ra một bản sao hoàn toàn độc lập của List gốc. Điều này có nghĩa là:
-
Nó không chỉ tạo một List mới ở cấp độ đầu tiên.
-
Mà nó còn tái tạo (sao chép độc lập) tất cả các đối tượng con có thể thay đổi được (như List, dictionary, set, v.v.) bên trong List gốc, ở mọi cấp độ lồng nhau.
-
Kết quả là, bản sao mới và bản gốc sẽ không chia sẻ bất kỳ tham chiếu nào đến cùng một đối tượng có thể thay đổi nào. Mọi thay đổi trong bản sao sẽ không bao giờ ảnh hưởng đến bản gốc, và ngược lại.
Khi nào dùng Deep Copy?
Bạn nên sử dụng sao chép sâu khi:
-
List của bạn chứa các đối tượng có thể thay đổi lồng nhau (như List con, dictionary, instance của class).
-
Bạn muốn đảm bảo rằng bản sao mới hoàn toàn độc lập với bản gốc, và mọi thay đổi đối với bản sao (dù ở cấp độ nào) sẽ không ảnh hưởng đến bản gốc.
-
Bạn cần một "ảnh chụp nhanh" (snapshot) của cấu trúc dữ liệu tại một thời điểm nhất định mà không muốn nó bị thay đổi bởi các thao tác sau này trên bản gốc.
Sử dụng module copy
và hàm copy.deepcopy()
Để thực hiện sao chép sâu, bạn cần sử dụng module copy
của Python, cụ thể là hàm deepcopy()
.
-
Cách dùng: Bạn phải
import copy
ở đầu file Python của mình để sử dụng hàm này.
Cú pháp:
import copy new_list = copy.deepcopy(original_list)
Ví dụ minh họa: Sao chép List chứa List con. Chúng ta sẽ sử dụng lại ví dụ về List lồng nhau để thấy sự khác biệt rõ rệt so với shallow copy.
import copy # List gốc chứa các List con (các List con là mutable objects) original_nested_list = [1, 2, [3, 4]] print(f"Original nested list: {original_nested_list}") # Output: Original nested list: [1, 2, [3, 4]] # Tạo deep copy deep_copy_list = copy.deepcopy(original_nested_list) print(f"Deep copy list: {deep_copy_list}") # Output: Deep copy list: [1, 2, [3, 4]] # Kiểm tra xem List ở cấp độ đầu tiên có phải là đối tượng khác không print(f"original_nested_list is deep_copy_list: {original_nested_list is deep_copy_list}") # Output: original_nested_list is deep_copy_list: False # Thay đổi một phần tử trong List con của bản sao # Thay đổi List con (index 2), phần tử index 0 của List con deep_copy_list[2][0] = 99 print(f"Original nested list sau thay đổi deep copy: {original_nested_list}") # Output: Original nested list sau thay đổi deep copy: [1, 2, [3, 4]] (Không bị ảnh hưởng!) print(f"Deep copy list sau thay đổi: {deep_copy_list}") # Output: Deep copy list sau thay đổi: [1, 2, [99, 4]] # Kiểm tra xem các List con có cùng một đối tượng trong bộ nhớ không print(f"Original nested sub-list is deep_copy_list sub-list: {original_nested_list[2] is deep_copy_list[2]}") # Output: Original nested sub-list is deep_copy_list sub-list: False
Như bạn thấy, khi deep_copy_list[2][0]
được thay đổi, original_nested_list
vẫn hoàn toàn không bị ảnh hưởng. Điều này là do copy.deepcopy()
đã tạo ra một bản sao độc lập của List con [3, 4]
, vì vậy chúng không còn trỏ đến cùng một đối tượng trong bộ nhớ nữa.
Ưu điểm:
-
Tạo bản sao hoàn toàn độc lập, đảm bảo không có sự thay đổi ngoài ý muốn giữa bản gốc và bản sao, ngay cả với các đối tượng lồng nhau.
-
Rất an toàn khi bạn làm việc với các cấu trúc dữ liệu phức tạp.
Nhược điểm:
-
Tốn kém tài nguyên hơn Shallow Copy: Việc sao chép sâu đòi hỏi Python phải duyệt qua toàn bộ cấu trúc dữ liệu lồng nhau và tạo bản sao cho từng đối tượng có thể thay đổi. Điều này có thể tốn nhiều thời gian xử lý và bộ nhớ hơn, đặc biệt với các List rất lớn và phức tạp.
-
Cần
import module
: Bạn phải import modulecopy
trước khi sử dụng.
Kết bài
Vậy là bạn đã nắm được các phương pháp quan trọng để sao chép List trong Python rồi! Hiểu rõ sự khác biệt giữa sao chép nông và sao chép sâu là chìa khóa để tránh những lỗi không mong muốn khi làm việc với dữ liệu.
Sao chép nông (Shallow Copy):
-
Tạo ra một List mới.
-
Nhưng các phần tử bên trong (đặc biệt là các đối tượng có thể thay đổi như List con, dict) vẫn tham chiếu đến cùng một đối tượng với bản gốc.
-
Các cách tạo: Slice
[:]
,list()
, List Comprehension. -
Thích hợp khi List chỉ chứa số, chuỗi, tuple hoặc khi bạn không ngại việc thay đổi các List con sẽ ảnh hưởng đến bản gốc.
Sao chép sâu (Deep Copy):
-
Tạo ra một List mới hoàn toàn độc lập với bản gốc.
-
Tất cả các đối tượng lồng nhau (ở mọi cấp độ) cũng được sao chép độc lập.
-
Cách tạo: Sử dụng
copy.deepcopy()
từ modulecopy
. -
Cần thiết khi List chứa các List con, dictionary, hoặc các đối tượng phức tạp có thể thay đổi, và bạn muốn bản sao hoàn toàn không liên kết với bản gốc.
Việc lựa chọn phương pháp sao chép phù hợp giúp code của bạn hoạt động đúng như mong đợi và tránh được những "bất ngờ" không đáng có.