Câu 1Sau khi chạy code này, giá trị của x là gì?def f(value):
value += 10
x = 5
f(x)
print(x)
Đối số hàm và tham chiếu đối tượng — Mutable bị thay đổi bên trong hàm
Học cách đối số hàm Python hoạt động khác nhau với kiểu mutable và immutable, kèm bẫy default argument hay gặp.
Khi bạn gọi một hàm, hành vi thay đổi tùy theo đối số là kiểu mutable (list / dict / set) hay kiểu immutable (int / str / tuple). Bài viết này đi qua sự khác biệt đó và các pattern cơ bản để viết hàm an toàn.
Đối số immutable — thay đổi bên trong không lọt ra ngoài
Số nguyên, số thực, chuỗi và tuple là immutable (không thể thay đổi tại chỗ). Nhận một trong số đó làm đối số và viết lại với += 10 hoặc = some_other_value, và biến của bên gọi không bị động đến.
Dưới mui xe, += 10 tạo ra một giá trị mới và chỉ gán lại tên đối số bên trong — tên của bên gọi vẫn trỏ về giá trị gốc.
def try_modify_number(value):
value += 10
print(f"bên trong: {value}")
x = 5
try_modify_number(x)
print(f"bên ngoài: {x}")
# bên trong: 15
# bên ngoài: 5 <- vẫn là giá trị gốc
# Câu chuyện tương tự với chuỗi và tuple
def try_modify_text(text):
text = text + " world"
print(f"bên trong: {text}")
message = "hello"
try_modify_text(message)
print(f"bên ngoài: {message}") # hello
Đối số mutable — thay đổi bên trong lọt ra ngoài
Mặt khác, khi bạn truyền một kiểu mutable như list / dict / set và gọi gì đó như .append() hoặc .update() để thay đổi nội dung tại chỗ, biến của bên gọi cũng thấy cùng thay đổi.
Đó là vì khoảnh khắc đối số được truyền, tên của bên gọi và tên đối số bên trong cùng chia sẻ một chiếc hộp. Đây là cùng cơ chế như y = x từ bài viết trước về mutable vs immutable — giờ xảy ra ở ranh giới lời gọi.
def try_modify_list(items):
items.append(100)
print(f"bên trong: {items}")
my_list = [1, 2, 3]
try_modify_list(my_list)
print(f"bên ngoài: {my_list}")
# bên trong: [1, 2, 3, 100]
# bên ngoài: [1, 2, 3, 100] <- bên ngoài cũng đổi
# Tương tự với dict
def set_role(user, role):
user["role"] = role
admin = {"name": "Minh"}
set_role(admin, "admin")
print(admin) # {'name': 'Minh', 'role': 'admin'}
Khi thay đổi bên trong lọt ra ngoài, nguyên nhân khó truy
cart.append(...) trông hoàn toàn có chủ ý ở dòng riêng của nó. Nhưng vì append bên trong hàm cũng tới được my_list bên ngoài, bạn có thể gặp triệu chứng "list âm thầm tăng lên" xuất hiện ở nơi hoàn toàn không liên quan.
Chỉ thay đổi bên trong hàm — bảo vệ đối số với .copy()
Khi bạn muốn để yên dữ liệu của bên gọi và chỉ thay đổi bên trong hàm, chỉ cần đặt .copy() ở đầu hàm. Sao chép list, dict hoặc set đầu vào sang một chiếc hộp riêng trước khi thay đổi nó, và bên gọi không bị ảnh hưởng.
Trả về phiên bản đã thay đổi với return, và bên gọi có thể giữ cả cart gốc và cart mới cùng lúc. Khi pattern này thành phản xạ cơ bắp, bạn sẽ viết được các hàm an toàn không có side effect (không sửa đổi bên gọi).
Toàn bộ luồng: ① sao chép đối số với .copy() sang chiếc hộp riêng → ② chỉnh sửa bản sao → ③ trả về kết quả. Ghi nhớ ba bước này và bạn có thể thiết kế an toàn bất kỳ hàm nào nhận đối số mutable.
def add_item_safely(cart, item):
items = cart.copy() # sao chép sang chiếc hộp riêng trước khi chỉnh sửa
items.append(item)
return items # gửi kết quả về qua return
my_cart = ["sữa", "bánh mì"]
new_cart = add_item_safely(my_cart, "trứng")
print(my_cart) # ['sữa', 'bánh mì'] <- không bị động đến
print(new_cart) # ['sữa', 'bánh mì', 'trứng'] <- một list riêng
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 code này, giá trị của my_list là gì?def g(items):
items.append(100)
my_list = [1, 2, 3]
g(my_list)
print(my_list)
Câu 3Khi bạn muốn trả về một list mới mà không sửa đổi list của bên gọi, điều đầu tiên cần làm là gì?