Câu 1Nhóm nào sau đây chỉ gồm các kiểu mutable?
Kiểu mutable và immutable
Phân biệt kiểu mutable và immutable trong Python, bẫy chia sẻ tham chiếu khi gán và cách tạo bản sao độc lập an toàn.
Tại sao bạn cần học phần này
Bài viết này không dành cho người mới hoàn toàn, nhưng chủ đề này không thể bỏ qua nếu bạn muốn hiểu sâu về lập trình. Chuyện rất phổ biến: người mới gặp phải một bug liên quan đến mutability và mất hàng giờ để debug.
Trong Python, bạn có thể nghĩ y = x chỉ đơn giản là nối hai biến bằng dấu bằng, nhưng rồi phát hiện ra chỉnh sửa một bên lại ghi đè luôn bên kia. Đó chính là mutability.
Chương này làm rõ sự khác biệt giữa "mutable" (có thể thay đổi) và "immutable" (không thể thay đổi), và chỉ cho bạn cách sao chép an toàn.
Một kiểu cho phép ghi đè chính biến đó (qua append, gán phần tử, update, v.v.) là mutable; kiểu không cho phép là immutable.
list / dict / set là mutable, còn lại (int / float / str / bool / tuple) là immutable.
Kiểu immutable — đổi một bên không ảnh hưởng bên kia
Với một kiểu immutable (kiểu không thể thay đổi), sau khi bạn đã truyền giá trị bằng y = x, việc thay đổi x về sau không ảnh hưởng đến y.
Ví dụ, sau x += 1, x trở thành 11, nhưng y vẫn giữ nguyên giá trị ban đầu là 10.
# int là immutable
x = 10
y = x
x += 1
print(x) # 11
print(y) # 10 <- không đổi
# str cũng immutable
x = "hello"
y = x
x = x + " world"
print(x) # hello world
print(y) # hello
# tuple cũng immutable
x = ("a", "b")
y = x
x = ("c", "d")
print(x) # ('c', 'd')
print(y) # ('a', 'b')
Biến trong Python là "nhãn tên dán lên hộp"
Một biến trong Python không tự giữ giá trị — nó giống như một nhãn tên trỏ tới dữ liệu (một chiếc hộp) trong bộ nhớ. Khi bạn viết y = x, Python không tạo hộp mới, mà chỉ dán thêm một nhãn tên (y) lên đúng chiếc hộp mà x đang trỏ tới.
- Kiểu immutable: gán lại như x = 11 chỉ chuyển nhãn tên của x sang một chiếc hộp khác. y vẫn trỏ tới chiếc hộp ban đầu, nên giá trị của nó vẫn độc lập.
- Kiểu mutable: thao tác tại chỗ như x.append(...) làm thay đổi chính chiếc hộp được chia sẻ, nên thay đổi cũng nhìn thấy được qua y.
Đây chính là ranh giới chia tách immutable với mutable.
Kiểu mutable — với y = x, đổi một bên thì bên kia cũng đổi
Nhưng với kiểu mutable (list / dict / set), viết y = x khiến y và x cùng trỏ tới một chiếc hộp.
Nếu sau đó bạn thực hiện một thao tác ghi đè lên nội dung, ví dụ x.append(...), thay đổi đó sẽ hiện ra cả ở phía y.
y = x không tạo một bản sao riêng — cả hai cái tên đều trỏ về cùng một chỗ.
Các thao tác ghi đè nội dung như append làm thay đổi chiếc hộp chung mà cả hai cái tên cùng trỏ tới, nên thay đổi cũng hiện ra ở phía y.
# list là mutable
x = ["a", "b"]
y = x # chỉ là dán thêm một cái tên y lên cùng danh sách
x.append("c") # ghi đè trực tiếp lên nội dung
print(x) # ['a', 'b', 'c']
print(y) # ['a', 'b', 'c'] <- y cũng tăng theo!
# remove cũng vậy
x.remove("b")
print(y) # ['a', 'c'] <- biến mất khỏi y luôn
# dict và set có cùng hiện tượng
d = {"k": 1}
e = d
e["new"] = 99
print(d) # {'k': 1, 'new': 99} <- d cũng tăng theo
Vì sao mặc định lại là chia sẻ?
Nếu y = x sao chép toàn bộ nội dung mỗi lần, thì khi, ví dụ, x chứa hàng triệu bản ghi lấy từ cơ sở dữ liệu, bộ nhớ và thời gian chạy sẽ phình lên rất nhanh.
Vì vậy, trong Python tên biến không phải là giá trị, mà là một cái nhãn trỏ tới một vị trí trong bộ nhớ.
Cơ chế này không thể thay đổi, nên bạn (người viết code) phải tự cẩn thận khi dùng.
Dùng copy() để có phiên bản độc lập
Khi bạn muốn giữ nguyên dữ liệu gốc và tạo ra một phiên bản riêng, hãy dùng phương thức copy().
Viết y = x.copy() sẽ chép nội dung sang một chiếc hộp mới rồi giao cho y, nên những thay đổi trên x về sau sẽ không ảnh hưởng đến y.
Ngay khi viết y = x.copy(), một vùng nhớ mới được cấp phát.
Sau đó, ghi đè nội dung của x bằng x.append(...) hay tương tự không ảnh hưởng gì tới y.
# list / dict / set đều có .copy()
x = ["apple", "lemon"]
y = x.copy()
x.append("grape")
print(x) # ['apple', 'lemon', 'grape']
print(y) # ['apple', 'lemon'] <- không ảnh hưởng
# dict.copy() cũng thế
d = {"a": 1, "b": 2}
e = d.copy()
d["c"] = 3
print(d) # {'a': 1, 'b': 2, 'c': 3}
print(e) # {'a': 1, 'b': 2} <- không ảnh hưởng
# set.copy() cũng thế
s = {1, 2}
t = s.copy()
s.add(3)
print(s) # {1, 2, 3}
print(t) # {1, 2} <- không ảnh hưởng
# list còn có vài cách copy khác
x = ["apple", "lemon"]
y1 = list(x) # đi qua constructor cũng cho ra list mới
y2 = x[:] # copy toàn bộ bằng slice (từ đầu đến cuối)
Cẩn thận khi list chứa list bên trong
copy() chỉ tạo chiếc hộp ngoài mới. Những giá trị mutable nằm bên trong (như list trong list) vẫn được chia sẻ. Người ta gọi đây là shallow copy (sao chép nông).
Hãy cẩn thận với dữ liệu lồng nhau.
Trong bài này, bạn đã học sự khác biệt giữa mutable và immutable và cách copy() cho bạn một phiên bản độc lập.
Ý tưởng rằng tên biến không phải là giá trị, mà là cái nhãn trỏ tới một vị trí trong bộ nhớ cũng đúng với các ngôn ngữ khác chứ không chỉ Python. Khi làm việc với kiểu mutable, hãy chèn một copy().
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Sau khi chạy đoạn code sau, giá trị của b là gì?
``
a = [1, 2, 3]
b = a
a.append(4)
Câu 3Bạn muốn giữ nguyên nội dung của list a ban đầu và tạo ra một phiên bản độc lập b. Cách viết nào phù hợp nhất?