Gần đây tôi đã có một trải nghiệm thú vị mà hóa ra lại là một bài học tuyệt vời về khả năng thay đổi của các đối tượng, đặc biệt là các chuỗi, trong Python. Trải nghiệm này là kết quả của việc làm rối tung hai bộ não Python, cảm thấy khó chịu về một sự mâu thuẫn có vẻ mâu thuẫn trong chức năng ngôn ngữ, và sau đó đi xuống một hố thỏ hấp dẫn
Brainteaser đầu tiên, lịch sự của Mike Driscoll trên Twitter là
i = j = [3]i += jprint[i, j]
Chương trình sẽ xuất ra cái gì?
Điều này có vẻ như là một điều khá dễ dàng. Các biến và i
và j
đều được chỉ định trỏ vào một danh sách chứa số 3
. Tiếp theo, biến i
được sửa đổi tại chỗ bằng cách thêm biến j
vào nó
Đối với danh sách, toán tử cộng trong Python nối nội dung mới của danh sách thứ hai vào nội dung của danh sách đầu tiên, vì vậy bây giờ là i == [3, 3]
, trong khi j
vẫn là
x = y = “Python”x += “rocks!”print[x, y]0 vì nó không được sửa đổi, vì vậy việc in
i
, j
sẽ in ra i
[ . Vì vậy, câu trả lời cuối cùng phải là x = y = “Python”x += “rocks!”print[x, y]7
Những người quen thuộc với cách Python triển khai các biến dưới dạng con trỏ sẽ nhận ra lỗi của tôi. i
và j
đều trỏ đến cùng một đối tượng trong bộ nhớ, vì vậy mặc dù i
là đối tượng duy nhất được sửa đổi rõ ràng, nhưng khi đối tượng trong bộ nhớ thay đổi, thì giá trị của j
cũng vậy. Vì vậy, câu trả lời đúng là
a = “Hello”id[a]140214243255728a += “!”id[a]2
Ok, rút kinh nghiệm. Vì vậy, khi một ý tưởng khác, trông rất giống, xuất hiện trong nguồn cấp dữ liệu tin tức của tôi [một lần nữa với sự giúp đỡ của Mike Driscoll], tôi nghĩ rằng mình sẽ hiểu đúng.
x = y = “Python”x += “rocks!”print[x, y]
Chương trình sẽ xuất ra cái gì?’’’
Trông gần giống với vấn đề trước đó, phải không? . Do đó,
a = “Hello”id[a]140214243255728a += “!”id[a]3 sẽ trở thành
a = “Hello”id[a]140214243255728a += “!”id[a]4. Và, như tôi đã học được từ brainteaser trước đó, vì
a = “Hello”id[a]140214243255728a += “!”id[a]3 và
a = “Hello”id[a]140214243255728a += “!”id[a]6 trỏ đến cùng một đối tượng trong bộ nhớ, nên
a = “Hello”id[a]140214243255728a += “!”id[a]6 cũng sẽ bị biến đổi thành
a = “Hello”id[a]140214243255728a += “!”id[a]4 phải không?
Sai. Bất kỳ người hâm mộ Python nào cũng sẽ nói với bạn rằng các chuỗi Python là “bất biến”, nghĩa là chúng không thể được sửa đổi trực tiếp. Thay vào đó, khi bạn cố gắng sửa đổi một chuỗi, Python sẽ tạo một đối tượng mới trong bộ nhớ dựa trên đột biến mà bạn muốn áp dụng cho đối tượng ban đầu. Vì vậy, trong khi
a = “Hello”id[a]140214243255728a += “!”id[a]3 biến thành
a = “Hello”id[a]140214243255728a += “!”id[a]4, thì
a = “Hello”id[a]140214243255728a += “!”id[a]6 vẫn là
i
2, vì vậy in chúng cạnh nhau sẽ cho bạn i
3Khỏe. Khó chịu, nhưng đủ đơn giản. Tuy nhiên, trong nghiên cứu của tôi về khả năng biến đổi của chuỗi trong Python, tôi đã tìm thấy một ví dụ khác [được sửa đổi để dễ đọc, lần này là từ Austin Henley] dường như mâu thuẫn với những gì tôi vừa học
a = “Hello”id[a]140214243255728a += “!”id[a]
Bây giờ, vì chúng ta vừa cố gắng thay đổi một chuỗi bất biến, nên sẽ có một đối tượng mới trong bộ nhớ mã hóa i
4 tại một vị trí mới trong bộ nhớ. Do đó, việc gọi i
5 của biến i
6 sẽ in ra một địa chỉ bộ nhớ khác với địa chỉ mà chúng ta đã thấy trước đây, phải không?
SAI MỘT LẦN NỮA. Và điều này thực sự đã ném tôi cho một vòng lặp. Tôi đã phải vật lộn để xử lý tính bất biến của các chuỗi, vì vậy bây giờ biết rằng rõ ràng chúng có thể thay đổi khiến tôi mất niềm tin vào bản chất tất định của máy tính. Làm thế quái nào mà cả ví dụ này và ví dụ trước đều đúng?
Những độc giả chú ý nhiều đến chi tiết hoặc đặc biệt là Python-adroit có thể nhận thấy sự khác biệt giữa ví dụ thứ hai và thứ ba. Trong ví dụ thứ hai, hai biến trỏ đến cùng một đối tượng trong bộ nhớ, trong khi ở ví dụ thứ ba, chỉ một biến duy nhất trỏ đến đối tượng. Tại sao nó quan trọng? . Nếu không có tham chiếu nào khác đến chuỗi, thì nó sẽ cố gắng thay đổi chuỗi thay vì cấp phát một chuỗi mới” [nhấn mạnh của tôi]. Trong trường hợp brainteaser hai, cả
a = “Hello”id[a]140214243255728a += “!”id[a]3 và
a = “Hello”id[a]140214243255728a += “!”id[a]6 đều chỉ vào cùng một đối tượng trong bộ nhớ, do đó, tính bất biến được bảo toàn
Thông minh thực sự. Hoặc chỉ bực bội khủng khiếp. Bởi vì, như Henley tiếp tục chỉ ra rằng ngay cả lời giải thích này cũng hơi đơn giản hóa. Nếu đột biến trên chuỗi ban đầu phát triển quá lớn, đôi khi CPython sẽ thay đổi kích thước bộ đệm, tạo một đối tượng mới trong bộ nhớ và do đó giữ lại hình ảnh “bất biến”. Khi chạy một điểm chuẩn, anh ấy thấy rằng việc thay đổi kích thước bộ đệm khiến địa chỉ trong bộ nhớ thay đổi 46 lần trong số 10.000
Và, lập luận với người bạn của tôi, người mà tôi đã thảo luận về vấn đề này, ngay cả khi chúng tôi bỏ qua trường hợp thay đổi kích thước bộ đệm, đây có phải là hành vi nguy hiểm không?
Sau khi suy nghĩ về điều này nhiều hơn một chút, chúng tôi nhận ra rằng, mặc dù kỳ quặc nhưng hành vi này không thực sự nguy hiểm. Rốt cuộc, các lập trình viên có thể tránh giả định rằng một đối tượng bị đột biến sẽ giữ lại địa chỉ của nó trong bộ nhớ. Nếu hai hoặc nhiều biến trỏ đến cùng một đối tượng, thì sự đột biến được đảm bảo dẫn đến một địa chỉ mới cho đối tượng được trỏ đến bởi biến đã sửa đổi. Nếu một số lượng biến không xác định trỏ đến cùng một đối tượng, bạn có thể cho rằng nó sẽ nhận một địa chỉ mới trong bộ nhớ, nhưng nếu “địa chỉ mới” đó trùng với địa chỉ cũ, thì đó không phải là vấn đề
Nhìn chung, đây là một cách cực kỳ khó khăn để thấm sâu vào đầu tôi khả năng xử lý con trỏ trong Python và khả năng biến đổi. Đây là hy vọng nó dính