+1

Vì sao gán b = a lại làm đổi luôn cả a? Gốc rễ mô hình Object trong Python

Gán không copy: vì sao b = a rồi sửa ba cũng đổi

Bài 2 - series "Python cho dân backend: nền tảng để lên level".

Mở màn bằng một câu đố như lần trước nghen:

a = [1, 2, 3]
b = a
b.append(4)
print(a)   # [1, 2, 3, 4] - ủa, tao có đụng a đâu?

Bây gán b = a, sửa b, mà a cũng dính. Tưởng b = a là sao chép một bản riêng cho b -> trật. Hiểu cái này là hiểu gốc rễ của nửa số bug "tự nhiên dữ liệu bị đổi" trong Python. image.png

Đa số nghĩ: biến là cái hộp chứa giá trị, gán là copy

Trật lất nghe bây. Trong Python:

Biến là cái NHÃN (tên) gắn vào một object, không phải cái hộp chứa giá trị. b = a chỉ dán thêm nhãn b vào cùng object mà a đang trỏ tới.

Nên ab là hai cái nhãn trên một cái list. Sửa cái list qua nhãn b thì nhãn a "thấy" liền - vì có một cái list thôi. Kiểm bằng id(a) == id(b) -> True (cùng identity).

Mutable vs immutable - chia hai thế giới

Hình dung cho dễ:

  • Mutable (list, dict, set) giống cái rổ - bỏ thêm trái cây vô thì vẫn là cái rổ cũ, chỉ là trong rổ có thêm đồ. Sửa-tại-chỗ được, nên mọi nhãn trỏ tới nó đều thấy thay đổi. Đây là chỗ gây bất ngờ.
  • Immutable (int, str, tuple, bool, None) giống số khắc trên đá - muốn số khác phải lấy cục đá khác. Không sửa-tại-chỗ được: "sửa" thực ra là tạo object mới rồi dời nhãn sang đó -> nhãn kia không hề hấn:
a = 5
b = a
b = b + 1     # b trỏ object mới (6); a vẫn 5
print(a, b)   # 5 6

a += b khác a = a + b (với list)

Cái này hay làm người ta vấp:

a = [1, 2]
b = a
a += [3]       # SỬA TẠI CHỖ list cũ (như extend) -> b cũng thấy
print(b)       # [1, 2, 3]

a = [1, 2]
b = a
a = a + [3]    # TẠO list mới, dời nhãn a sang đó -> b giữ list cũ
print(b)       # [1, 2]

+= trên mutable là sửa tại chỗ; a = a + b là tạo mới.

Truyền vào hàm: "pass by object reference"

Python không pass-by-value như Go, cũng không pass-by-reference kiểu C++ - nó truyền tham chiếu tới object (gọi là pass-by-assignment). Hệ quả thực tế:

def sua(lst):
    lst.append(99)     # SỬA object -> bên ngoài THẤY

def gan_lai(lst):
    lst = [0]          # GÁN LẠI tên cục bộ -> bên ngoài KHÔNG thấy

x = [1, 2]
sua(x);      print(x)  # [1, 2, 99]
gan_lai(x);  print(x)  # [1, 2, 99] (không đổi - chỉ dời nhãn cục bộ)

Sửa nội dung object thì bên ngoài thấy; gán lại cái tên (tham số) thì chỉ đổi nhãn cục bộ trong hàm, bên ngoài không hề hấn.

Hệ quả & liên hệ Go

Đây là nguồn của loại bug "dữ liệu bị đổi mà không biết ai đổi". Muốn b độc lập với a thì phải copy thật (bài sau) - mà copy cũng có bẫy shallow/deep.

Liên hệ Go (series Golang, bài 1): Go copy value khi truyền tham số; Python truyền tham chiếu tới object. Hai ngôn ngữ ngược nhau, mà cùng một bài học sống còn: luôn biết cái gì đang được chia sẻ, cái gì là bản riêng. image.png

Checklist: bây nắm chưa?

  • Biến trong Python là hộp giá trị hay nhãn trỏ object? (Nhãn trỏ object.)
  • b = a có copy hông? (Hông à nha - chỉ thêm nhãn vào cùng object.)
  • Mutable vs immutable khác gì khi gán/sửa? (Mutable = rổ, sửa-tại-chỗ -> mọi nhãn đều thấy; immutable = số khắc đá, "sửa" là tạo mới -> dời nhãn.)
  • a += [3] khác a = a + [3] chỗ nào? (+= sửa tại chỗ list cũ; a = a + ... tạo list mới.)
  • Sửa object trong hàm, bên ngoài có thấy hông? Gán lại tên thì sao? (Sửa object: thấy; gán lại tên: không.)
  • Kiểm hai nhãn cùng object bằng gì? (id() / is.)

Bài tới: "is vs == - và vì sao 256 is 256 cho True mà 257 is 257 có thể False." Bản gốc đăng tại Substack: https://quakebaynghe.substack.com/p/python-object-model-mutable-immutable


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí