Học bằng cách đọc theo thứ tự

Đố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.

Đối số immutable
phía gọix = 5trong hàmx = 5x = 5(không đổi)x = 15(giá trị mới)truyềnx += 10
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

Hãy viết một hàm chuyển đổi thuế và xác nhận rằng thay đổi đối số int không ảnh hưởng đến bên ngoài.

① Định nghĩa def add_tax(price):, đặt price = int(price * 1.1), rồi in f"bên trong: {price}". (Lần này không có giá trị trả về.)

② Đặt base_price = 1000, gọi add_tax(base_price), rồi in f"bên ngoài: {base_price}".

Thành công nghĩa là base_price bên ngoài không thay đổi.

(Phần giải thích xuất hiện khi chạy đúng.)

Python Editor

Chạy code để xem đầu ra

Đố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.

Đối số mutable
phía gọimy_list = [1, 2, 3]trong hàmitems = [1, 2, 3]my_list =[1, 2, 3, 100](cũng đổi theo)items =[1, 2, 3, 100]truyền cùng chiếc hộpitems.append(100)phản ánh
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.

Hãy cảm nhận tận tay cách dữ liệu bên ngoài bị thay đổi không cố ý.

① Định nghĩa def add_item(cart, item):, chạy cart.append(item), rồi in f"bên trong: {cart}". (Không return.)

② Chuẩn bị my_cart = ["sữa", "bánh mì"], gọi add_item(my_cart, "trứng"), rồi in f"bên ngoài: {my_cart}".

Xác nhận rằng "trứng" cũng kết thúc trong my_cart bên ngoài (đây là sự khác biệt với trường hợp immutable ở phần trước).

Python Editor

Chạy code để xem đầu ra

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ốccart 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.

Tạo chiếc hộp mới với .copy() trước khi thay đổi
items = cart.copy()items.append(x)cart gốcvẫn nguyênkhông ảnh hưởng
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

Hãy viết lại hàm từ phần trước thành phiên bản an toàn không sửa đổi bên gọi.

① Định nghĩa def add_item_safely(cart, item):, sao chép đầu vào với items = cart.copy(), chạy items.append(item), và kết thúc với return items.

② Chuẩn bị my_cart = ["sữa", "bánh mì"] và bắt kết quả với new_cart = add_item_safely(my_cart, "trứng").

print() cả my_cartnew_cart và xác nhận rằng my_cart gốc không thay đổi trong khi chỉ new_cart được thêm "trứng".

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

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)

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ì?