Đầu tiên mình mô tả một chút về quản lý và tối ưu bộ nhớ trong Python. Sau khi đặt nền tảng, tôi giải thích sự khác biệt giữa câu lệnh gán, bản sao nông và bản sao sâu. Sau đó tôi tóm tắt sự khác biệt trong một bảng
Nếu bạn thích xem video hơn là đọc một bài viết, bạn có thể tìm thấy video bổ sung tại đây
Quản lý bộ nhớ trong Python
int, float, list, dict, class instance,… chúng đều là đối tượng trong Python. Trong quá trình triển khai CPython, hàm tích hợp sẵn
>>> L1 = [1, 2, 3]>>> id[L1]3 trả về địa chỉ bộ nhớ của một đối tượng —
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]
3061530120
Nếu chúng ta tạo một biến mới
>>> L1 = [1, 2, 3]>>> id[L1]4 tham chiếu đến một đối tượng có cùng giá trị với
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]5, thì
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]4 sẽ có một địa chỉ bộ nhớ mới —
3061530120
>>> L2 = [1, 2, 3]>>> id[L2]
3061527304
Mỗi khi một đối tượng mới được tạo, nó sẽ có một địa chỉ bộ nhớ mới. Ngoại trừ khi nó là -
- một chuỗi rất ngắn
- một số nguyên trong [-5, 256]
- một thùng chứa bất biến rỗng [e. g. bộ dữ liệu]
Hãy xem một ví dụ với một đối tượng số nguyên. Cả
>>> L1 = [1, 2, 3]>>> id[L1]7 và
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]8 đều đề cập đến cùng một giá trị 10. Trong khi
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]5 và
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]4 trong ví dụ trước có hai địa chỉ bộ nhớ khác nhau, thì
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]7 và
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]8 chia sẻ cùng một địa chỉ bộ nhớ —
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]2
3061530120
Đó là bởi vì, trong ba trường hợp ngoại lệ đó, Python tối ưu hóa bộ nhớ bằng cách để biến thứ hai tham chiếu đến cùng một đối tượng trong bộ nhớ, — một số người gọi nó là “đối tượng được chia sẻ”
Hãy ghi nhớ khái niệm về đối tượng được chia sẻ vì chúng ta sẽ cần đến nó sau này khi chúng ta tạo một bản sao sâu của một đối tượng
Chuyển nhượng biến
Nó nói trong tài liệu Python rằng “Các câu lệnh gán trong Python không sao chép các đối tượng, chúng tạo ra các liên kết giữa một mục tiêu và một đối tượng. ” Điều đó có nghĩa là khi chúng ta tạo một biến bằng cách gán, biến mới đề cập đến cùng một đối tượng như biến ban đầu —
>>> L1 = [1, 2, 3]>>> id[L1]3
3061530120
Bởi vì biến mới
>>> L2 = [1, 2, 3]>>> id[L2]3 và biến ban đầu
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]4 chia sẻ cùng một đối tượng [i. e. cùng một danh sách], chúng cũng chứa các phần tử giống nhau —
3061527304
>>> L1 = [1, 2, 3]>>> id[L1]6
3061530120
Như minh họa trong hình bên dưới,
>>> L2 = [1, 2, 3]>>> id[L2]4 và
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]3 chia sẻ cùng một
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]7, i. e. , chúng tham chiếu đến cùng một đối tượng trong bộ nhớ. Và chúng cũng chứa các yếu tố giống nhau
3061527304
Bản sao nông
Khi chúng ta tạo một biến bằng bản sao nông, biến mới đề cập đến một đối tượng mới —
>>> L1 = [1, 2, 3]>>> id[L1]0
3061530120
Mặc dù
>>> L2 = [1, 2, 3]>>> id[L2]4 và
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]9 đề cập đến hai đối tượng khác nhau [i. e. hai danh sách có địa chỉ bộ nhớ khác nhau], các phần tử trong hai danh sách đề cập đến cùng một đối tượng —
3061527304
>>> L1 = [1, 2, 3]>>> id[L1]3
3061530120
Hình bên dưới minh họa cách các phần tử trong
>>> L2 = [1, 2, 3]>>> id[L2]4 đề cập đến các đối tượng giống như các phần tử trong
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]9
3061527304
Sao chép sâu
Tương tự như sao chép nông, khi chúng ta tạo một biến bằng cách sao chép sâu, biến mới tham chiếu đến một đối tượng mới —
>>> L1 = [1, 2, 3]>>> id[L1]6
3061530120
Như được mô tả trong tài liệu Python —
Sự khác biệt giữa sao chép nông và sâu chỉ liên quan đến các đối tượng phức hợp [các đối tượng chứa các đối tượng khác, như danh sách hoặc thể hiện của lớp]
- Một bản sao nông xây dựng một đối tượng phức hợp mới và sau đó [trong phạm vi có thể] chèn vào đó các tham chiếu tới các đối tượng được tìm thấy trong bản gốc
- Một bản sao sâu xây dựng một đối tượng phức hợp mới và sau đó, theo cách đệ quy, chèn các bản sao của các đối tượng được tìm thấy trong bản gốc vào đó
Vì vậy, khác với bản sao nông, các phần tử trong hai danh sách hiện đề cập đến các đối tượng khác nhau -
>>> L1 = [1, 2, 3]>>> id[L1]7
3061530120
Nhưng tại sao
>>> L1 = [1, 2, 3]>>> id[L1]22 và
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]23 chia sẻ cùng một đối tượng [tôi. e. có cùng địa chỉ bộ nhớ]?
3061530120
Hình dưới đây cho thấy
>>> L2 = [1, 2, 3]>>> id[L2]4 và
3061527304
>>> L1 = [1, 2, 3]>>> id[L1]25 đề cập đến hai danh sách khác nhau trong bộ nhớ và các phần tử trong
3061530120
>>> L2 = [1, 2, 3]>>> id[L2]4 đề cập đến các đối tượng khác với các phần tử trong
3061527304
>>> L1 = [1, 2, 3]>>> id[L1]25 ngoại trừ các phần tử số nguyên do tối ưu hóa bộ nhớ
3061530120
Bản tóm tắt
Nếu có một điểm rút ra từ bài viết này, thì đó phải là bảng dưới đây. Phép gán biến không sao chép các đối tượng, vì vậy
>>> L2 = [1, 2, 3]>>> id[L2]4 và
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]3 có cùng địa chỉ bộ nhớ và chứa các phần tử giống nhau. Bản sao nông tạo một đối tượng mới cho
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]9, nhưng các phần tử trong
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]9 vẫn tham chiếu đến các đối tượng giống như các phần tử trong
3061527304
>>> L2 = [1, 2, 3]>>> id[L2]4. Bản sao sâu cũng tạo đối tượng mới cho
3061527304
>>> L1 = [1, 2, 3]>>> id[L1]25 và các phần tử trong
3061530120
>>> L1 = [1, 2, 3]>>> id[L1]25 đề cập đến các đối tượng khác với các phần tử đó trong
3061530120
>>> L2 = [1, 2, 3]>>> id[L2]4 với ba loại ngoại lệ
3061527304