Giải Mã Bí Ẩn Python: Bạn Có Thể Giải Được Mã Này Không?
Giới thiệu
Bài viết này sẽ đưa bạn vào một thử thách thú vị liên quan đến một trong những khái niệm cơ bản nhất trong Python. Đây là một câu đố đơn giản nhưng lại chứa đựng nhiều điều bất ngờ mà ngay cả những lập trình viên dày dạn kinh nghiệm cũng có thể bị đánh lừa. Hãy cùng tìm hiểu và kiểm tra khả năng của bạn nhé!
Thử thách
Trước khi đi vào giải thích, hãy bắt đầu với một câu đố. Hãy xem đoạn mã Python sau và dự đoán đầu ra của nó:
python
a = 500
b = a
a = a + 100
list1 = [1, 2]
list2 = list1
list1.append(3)
print(f"b là {b}")
print(f"list2 là {list2}")
Bạn nghĩ đầu ra sẽ là gì?
A) b là 600
và list2 là [1, 2]
B) b là 500
và list2 là [1, 2, 3]
C) b là 500
và list2 là [1, 2]
D) b là 600
và list2 là [1, 2, 3]
Hãy dành một chút thời gian để suy nghĩ về câu trả lời. Đoạn mã có vẻ đơn giản, nhưng có điều gì đó tinh tế đang diễn ra mà ngay cả những lập trình viên có kinh nghiệm cũng có thể gặp khó khăn.
Cuộn xuống khi bạn đã sẵn sàng cho câu trả lời...
Câu trả lời
Câu trả lời đúng là B: b là 500
và list2 là [1, 2, 3]
.
Nếu bạn đã đoán đúng, tuyệt vời! Nếu không, đừng lo lắng—hành vi này làm khó nhiều lập trình viên Python. Chìa khóa để hiểu điều này nằm ở một khái niệm quan trọng mà nhiều bài hướng dẫn thường bỏ qua.
Bí ẩn được tiết lộ
Những hành vi khác nhau trong mã của chúng ta không phải là ngẫu nhiên hay không nhất quán. Chúng là kết quả hợp lý của cách Python xử lý hai loại đối tượng khác nhau: có thể thay đổi (mutable) và không thể thay đổi (immutable).
Hãy cùng phân tích kỹ từng bước một.
Phần 1: Điều bất ngờ với số nguyên
python
a = 500
b = a
a = a + 100
Đây là những gì Python thực sự đang làm:
a = 500
: Tạo một đối tượng số nguyên chứa500
và gán têna
trỏ tới nó.b = a
: Khiếnb
trỏ tới cùng một đối tượng500
màa
đang trỏ tới.a = a + 100
: Đây là dòng quan trọng. Nó đánh giáa + 100
(là600
), tạo một đối tượng số nguyên mới chứa600
, và gán lạia
trỏ tới đối tượng mới này.
Điểm quan trọng: các số nguyên là không thể thay đổi trong Python. Bạn không thể thay đổi giá trị của một đối tượng số nguyên sau khi nó được tạo ra. Khi bạn viết a = a + 100
, Python không sửa đổi đối tượng 500
hiện có—nó tạo ra một đối tượng mới 600
.
Vì b
chưa bao giờ được gán lại, nó vẫn trỏ tới đối tượng 500
ban đầu. Do đó: b là 500
.
Phần 2: Bất ngờ với danh sách
python
list1 = [1, 2]
list2 = list1
list1.append(3)
Bây giờ hãy theo dõi các thao tác với danh sách:
list1 = [1, 2]
: Tạo một đối tượng danh sách chứa[1, 2]
và gánlist1
trỏ tới nó.list2 = list1
: Khiếnlist2
trỏ tới cùng một đối tượng danh sách màlist1
đang trỏ tới.list1.append(3)
: Đây là sự khác biệt—điều này sửa đổi đối tượng danh sách hiện có bằng cách thêm3
vào nó.
Điểm quan trọng: các danh sách có thể thay đổi trong Python. Khi bạn gọi append()
, bạn đang sửa đổi đối tượng danh sách hiện có, không tạo ra một cái mới.
Vì cả list1
và list2
đều trỏ tới cùng một đối tượng danh sách, và đối tượng đó đã được sửa đổi, cả hai tên giờ đây đều tham chiếu tới [1, 2, 3]
. Do đó: list2 là [1, 2, 3]
.
Bức tranh lớn hơn: Biến là tên, không phải hộp
Câu đố này minh họa một sự thật cơ bản về Python mà nhiều lập trình viên hiểu sai: các biến là tên trỏ tới đối tượng, không phải các hộp chứa giá trị.
Hãy nghĩ về nó như thế này:
- Tên biến giống như một mảnh giấy dán có tên viết trên đó
- Đối tượng giống như những chiếc hộp chứa dữ liệu thực tế
- Gán tên dán lên một chiếc hộp
- Nhiều tên có thể trỏ tới cùng một chiếc hộp
Khi bạn hiểu mô hình tư duy này, đoạn mã bí ẩn trở nên rõ ràng:
- Với các đối tượng không thể thay đổi (số nguyên), các thao tác tạo ra các hộp mới và di chuyển nhãn tên tới chúng.
- Với các đối tượng có thể thay đổi (danh sách), các thao tác sửa đổi nội dung của các hộp hiện có trong khi tất cả các nhãn tên trỏ tới hộp đó vẫn giữ nguyên.
Tại sao điều này quan trọng
Hiểu rõ tính thay đổi không chỉ mang tính học thuật—nó ngăn ngừa các lỗi thực sự. Hãy xem xét ví dụ thường gặp này trong Python:
python
def add_item(item, target_list=[]):
target_list.append(item)
return target_list
# Điều này không hoạt động như mong đợi!
list1 = add_item("first")
list2 = add_item("second") # Ôi! list2 giờ đây là ["first", "second"]
Danh sách mặc định []
là có thể thay đổi, vì vậy tất cả các cuộc gọi hàm đều chia sẻ cùng một đối tượng danh sách. Khi bạn hiểu tính thay đổi của đối tượng, hành vi này trở nên dễ dự đoán hơn là bí ẩn.
Kiểm tra hiểu biết của bạn
Bây giờ bạn đã biết bí mật, hãy thử dự đoán đầu ra của đoạn mã này:
python
x = "hello"
y = x
x = x + " world"
dict1 = {"key": "value"}
dict2 = dict1
dict1["key"] = "modified"
print(f"y là '{y}'")
print(f"dict2 là {dict2}")
Câu trả lời: y là 'hello'
và dict2 là {'key': 'modified'}
(chuỗi là không thể thay đổi, từ điển là có thể thay đổi).
Kết luận
Hệ thống biến trong Python rất thanh lịch khi bạn hiểu mô hình cơ bản. Các biến không chứa đối tượng—chúng trỏ tới chúng. Việc đối tượng được trỏ tới có thể sửa đổi hay không phụ thuộc vào tính thay đổi của nó.
Khái niệm này giải thích vô số hành vi của Python có vẻ bí ẩn lúc đầu. Nếu bạn nắm vững nó, bạn sẽ thấy mã Python trở nên dễ dự đoán và dễ gỡ lỗi hơn rất nhiều.
Hiểu rõ tính thay đổi chỉ là một trong nhiều khái niệm thanh lịch khiến Python trở nên mạnh mẽ. Khi bạn thấy được mẫu này, bạn sẽ bắt đầu nhận ra nó ở khắp nơi trong mã của mình.
Còn những bí ẩn Python nào khác làm bạn bối rối không? Hãy chia sẻ ví dụ của bạn trong phần bình luận—có thể có một lời giải thích đơn giản hơn bạn nghĩ! Và nếu bạn thấy điều này hữu ích, hãy theo dõi để nhận thêm thông tin về Python giúp làm sáng tỏ những điều bí ẩn trong mã.
Aaron Rose là kỹ sư phần mềm và nhà viết kỹ thuật tại tech-reader.blog và là tác giả của Think Like Genius.