Python call by gì?

written by HVN on 2017-05-31

Một câu hỏi nhạt toẹt của nhà phỏng vấn có thể đưa ra (với người phỏng vấn thường có kiến thức từ ngôn ngữ khác như Java, PHP hay C++, hoặc một chuyên gia Python thực sự và hỏi để check xem bạn có mắc bẫy không - loại này thì hiếm, và không rảnh 🙄).

Python dùng call-by-value hay call-by-reference?

Hai khái niệm này thực ra không tồn tại trong Python, bạn có thể đào tung cả trang document của Python cũng sẽ không thấy nói gì về khái niệm này. Tức là: nó không có thật! Nó không cần thiết! bạn chỉ cần hiểu function hoạt động thế nào, cách Python sử dụng "name binding". Còn không nên ngồi cãi nhau về "call-by-reference", "call-by-value" làm gì cho tốn thời gian, vô tác dụng.

Call by XYZ là cái gì?

Mọi khái niệm viết sau đây không tồn tại trong Python, các thuật ngữ được viết với "từ vựng" của ngôn ngữ lập trình khác.

Trong một số ngôn ngữ như C, C++, khi gọi function ta có truyền vào các "tham số" (pass argument), nếu function đó nhận vào một array (trong Python hiểu nôm na là list), thì trong function, ta sẽ xử lý chính array đó hay một bản copy của nó?

Kiểu 1

update(danh_sach)

Kiểu 2

update(&danh_sach)

Với câu gọi function (call function) thứ nhất ta gọi function với giá trị (value) của biến danh_sach. Mọi thay đổi trong function thực hiện trên argument được gọi là thực hiện trên bản copy của danh_sach.

Câu gọi function thứ hai ta gọi function với con trỏ (pointer) đến biến danh_sach hay còn gọi là reference. Mọi thay đổi sẽ thay đổi trực tiếp trên danh_sach.

Những ngôn ngữ lập trình bị ảnh hưởng bởi C thường cắt giảm khái niệm "pointer" để tránh gây phức tạp, vì vậy khi gọi function sẽ mặc định dùng 1 trong 2 kiểu trên. Hoặc dùng một cơ chế khác hoàn toàn.

Ví dụ:

Call by và pass by

  • Khi nói function call by , người ta đang lấy function làm trọng tâm của câu chuyện.
  • Khi nói argument pass by, người ta đang lấy argument làm trọng tâm của câu chuyện.

Chỉ là hai cách tập trung khác nhau vào một việc: call function với các argument.

Call function trong Python hoạt động thế nào?

Nếu bắt phải đưa ra một từ khoá, thì đó là các argument sẽ được "pass by assignment". Để thực sự hiểu Python assignment làm gì, bạn cần hiểu về khái niệm namebinding trong Python.

Cách hoạt động của name và binding

x = 4
y = x
x = x + 1
print(x, y)

x là mấy và y là mấy?

Code này đọc theo thuật ngữ của Python như sau:

  • bind name x vào object 4
  • bind name y vào object mà x HIỆN TẠI đang bind vào (tức 4)
  • bind name x vào object tạo ra bởi x+1 (tức 5) do x là 4, khi +1, nó sinh ra một object mới là 5.

Vậy rõ ràng kết quả ở đây: x là 5, y là 4. Chú ý rằng y không đi theo x, cũng không bind vào x, nó bind vào object mà x bind tại thời điểm đó.

Xem ví dụ sau tương tự, nhưng khác hẳn:

L = [1,2,3]
K = L
L[0] = 9
print(L, K)

L là mấy? và K là mấy?

Đây nhẽ ra, phải là câu hỏi phỏng vấn mặc định cho lập trình viên Python chứ không phải mấy câu vớ vẩn như tiêu đề bài này. Vì sao? vì nếu lập trình viên trả lời đúng, tức là anh ta hiểu cách Python hoạt động, và sẽ không tạo ra những bug rất cơ bản trong chương trình.

Hãy đọc đoạn code này theo thuật ngữ Python:

  • bind name L vào list object chứa 1,2,3
  • bind name K vào object mà L hiện tại đang bind vào (tức list 1,2,3)
  • Thay đổi phần tử đầu tiên của list, gán cho giá trị bằng 9

Hãy nhớ LK chỉ là 2 cái tên cùng bind vào một object, khi object này bị thay đổi, dù dùng qua tên L hay tên K, thì thay đổi đó là thực hiện trên object đó. Tức kết quả sẽ cho LK giống nhau (hay chính xác hơn, chúng là một). Dùng function id sẽ giúp hiểu rõ hơn khi nói về object và name:

>>> L = [1,2,3]
>>> K = L
>>> L[0] = 9
>>> print(L, K)
[9, 2, 3] [9, 2, 3]
>>> id(L)
4326049992
>>> id(K)
4326049992
>>> L is K
True

id sẽ trả về một số ID (đảm bảo là duy nhất) gắn liền với object mà cái name đó đang bind.

Cơ chế hoạt động của function

>>> def change(numb, ns):
...     numb = numb + 1
...     ns[0] = 0
...     return None
...
>>> N = 9
>>> L = [1,2,3]
>>> change(N, L)
>>> print(N, L)
9 [0, 2, 3]

Cơ chế gọi function của Python hoạt động như sau:

  • bind name numb vào argument thứ nhất, tức numb = N
  • bind name ns vào argument thứ hai, tức ns = L
  • bind numb vào object tạo ra bởi numb + 1, tức 10
  • thay đổi object mà name ns đang bind tới, tức list [1,2,3], thay đổi giá trị ứng với index 0, tức sẽ có [0,2,3]
  • nên nhớ rằng nsL cùng bind đến 1 object. Vì vậy giờ L cũng đã bị thay đổi

Vậy làm sao để không thay đổi list L và thu về một list mới sau khi gọi function? Hãy tạo một bản copy của list rồi dùng bản copy đó.

>>> def change(L):
...     L = L[:] # slicing là một cách đơn giản để tạo một bản shallow copy
...     L[0] = 0
...     return L
...
>>> newL = change(L)
>>> print(L)
[1, 2, 3]
>>> print(newL)
[0, 2, 3]

CHÚ Ý: Nếu list L chứa các mutable object (xem ở dưới), cần sử dụng function copy.deepcopy trong standard library copy.

Một số kiểu dữ liệu trong Python không cho phép thay đổi giá trị của nó sau khi tạo ra (immutable) như int, float, str, NoneType, bool, tuple. Vì vậy mỗi lần "thay đổi", ta sẽ tạo ra một object mới chứa giá trị mới:

>>> N = 9
>>> id(N)
4297624192
>>> N = N + 2
>>> id(N)
4297624256
>>> N
11

>>> T1 = (1,2,3)
>>> id(T1)
4326100280
>>> T1 = T1 + (3,4,5)
>>> id(T1)
4325883048
>>> print(T1)
(1, 2, 3, 3, 4, 5)

Các kiểu dữ liệu khác cho phép thay đổi sau khi tạo ra (mutable): list, dict, set.

>>> L = [1,2,3]
>>> id(L)
4326050504
>>> L.extend([3,4,5])
>>> id(L)
4326050504
>>> print(L)
[1, 2, 3, 3, 4, 5]

Với kiểu mutable, cần đặc biệt chú ý phép toán nào trả về object mới, phép toán nào thay đổi object hiện tại:

>>> L = [1,2,3]
>>> id(L)
4326050568
>>> L = L + [3,4,5]
>>> id(L)
4326095240
>>> L
[1, 2, 3, 3, 4, 5]

append, extend của list đều thay đổi object gọi method. Phép + list với list, sinh ra một list mới.

Python dùng pass by assignment, call by object reference

Pass by assignment là khái niệm lấy từ Python FAQ, khi mà người ta bắt buộc phải giải thích khái niệm này cho những người đã biết ngôn ngữ lập trình khác. Các name trong function sẽ bind tới các object mà argument đang bind tới.

Trong tutorial Python, có viết "các argument được pass sử dụng call by value, với các value luôn luôn là các object reference chứ không phải value của object." - yup, I lied 😒

Định nghĩa name trong Python:

Names refer to objects.

Liệu bạn có thể bịa ra một thuật ngữ nữa là "call by name" ??!!!

Chốt, thông

Để khỏi đau đầu về những điều này, chỉ cần nắm chắc các khái niệm name, binding, mutable, immutable là đủ biết chương trình chạy thế nào. Bạn không cần biết gọi tên Python là call-by-gì? Nếu cần thể hiện thì hãy nói "call-by-object-reference" và gửi kèm link doc.

Trên internet có nhiều nơi hỏi đáp, cố đưa ra một cái tên, nhưng những cái tên đó không có trong tài liệu chính thống của Python.

Tham khảo