Python
Thứ Ba, 15/01/2019, 00:01
Python: Presto-Chango là gì? Hiểu đúng về Name và Value trong Python

Python: Presto-Chango là gì? Hiểu đúng về Name và Value trong Python

**Hiểu chưa kỹ về cách sử dụng tên và cách thay đổi giá trị của biến trong Python là một trong những nguyên nhân khiến cho những người mới bắt đầu sử dụng Python cảm thấy rất lúng túng. Kể cả khi bạn đã "có kinh nghiệm" với Python, đôi khi bạn vẫn phải mất thời gian tìm lỗi với một số tình huống gặp phải.
Ví dụ, rõ ràng là bạn vừa mới "thay đổi" giá trị của một biến rồi, nhưng khi kiểm tra lại bằng cách in biến đó ra thì giá trị của nó vẫn là giá trị ban đầu !? Hoặc là tình huống sau: bạn chẳng tác động gì đến một biến vừa tạo ra, nhưng một lát sau biến đó lại bị thay đổi giá trị ban đầu!?
Nguyên nhân tại đâu? Bài viết này sẽ cố gắng trình bày dễ hiểu nhất về Name và Value trong Python để giúp bạn tránh gặp phải tình huống tương tự trong tương lai.

1. Name và value trong Python

Giống như nhiều ngôn ngữ lập trình khác, Python dùng biến (variable) để quản lí dữ liệu và điều khiển mọi hoạt động của chương trình. Mỗi biến là một đối tượng và có một tên (name) riêng. Khi ta gán cho biến một giá trị (value) thì tên của biến sẽ trỏ đến một địa chỉ ô nhớ trên RAM. Để đơn giản, có thể đồng nhất variable và name là một. Ví dụ, khi ta khai báo
>>> x = 1 >>> print(x) 1
thì Python sẽ tạo ra một ô nhớ trên RAM mang giá trị là 1 và một biến tên là **x **chỉ đến ô nhớ vừa tạo ra. Mỗi khi ta gọi x thì Python sẽ trả về giá trị trên ô nhớ mà x đang chỉ đến. Như mô tả ở hình dưới đây
notion image
Sau đây là những điểm quan trọng về name và value trong Python.

2. Một value có thể có nhiều names

Tiếp tục ví dụ trên, nếu ta gán
>>> y = x >>> print(y) 1
thì tức là ta đang tạo thêm tên mới cho ô nhớ mà biến x đang chỉ đến. Cụ thể hơn, lúc này y cũng sẽ mang giá trị là 1, tuy nhiên không có thêm ô nhớ mới nào được tạo ra, mà thay vào đó, y sẽ trỏ đến cùng một ô nhớ với x, xem hình dưới đây:
notion image
Điều này cũng giống như một người có 2 tên: tên ở nhà và tên ở trường, nhưng về bản chất thì vẫn là một người.

3. Các biến có thể được gán lại giá trị một cách độc lập

Tiếp tục ví dụ trên, nếu sau đó ta gán lại giá trị của y
>>> y = 1
thì y sẽ không còn trỏ đến ô nhớ cũ nữa, mà sẽ trỏ đến một ô nhớ mới hoàn toàn (mặc dù cả hai ô nhớ vẫn mang giá trị là 1), như hình dưới đây:
notion image
Đến đây mọi chuyện có vẻ vẫn ổn. Tuy nhiên, khi ta làm việc với một danh sách (list) thì mọi chuyện sẽ bắt đầu trở nên rắc rối.
Bây giờ, ta tạo một list như sau
>>> A = [1,2,3] >>> print(A) [1, 2, 3]
thì Python sẽ tạo ra một dãy ô nhớ trên RAM mang giá trị lần lượt là 1, 2, 3 và một biến tên là A trỏ đến ô nhớ đầu tiên của dãy ô nhớ vừa tạo ra
notion image
Như vậy, A chỉ quan tâm đến "thằng" đầu tiên của dãy ô nhớ, không cần biết tiếp sau là gì. Đoán xem chuyện gì xảy ra nếu ta thêm một thành viên mới sau số 3? Đúng vậy, địa chỉ của biến A sẽ không bị thay đổi, nhưng rõ ràng danh sách A thì có. Đây chính là tính chất mutable của biến kiểu **list **(Xem bài Python: Phân biệt Mutable vs Immutable).

4. Quan trọng: không thể copy dữ liệu bằng lệnh gán

Tiếp tục ví dụ trên, nếu ta tạo ra một biến mới tên B và muốn B có dữ liệu giống như A thì một cách tự nhiên ta thực hiện lệnh "copy" như dưới đây:
>>> B = A >>> print(A) [1, 2, 3] >>> print(B) [1, 2, 3]
Tuy nhiên, nhớ lại vị dụ phía trên, về bản chất ta chỉ đang tạo ra tên mới cho dãy ô nhớ mà biến A đang chỉ đến, như mô tả dưới đây
notion image
Như ta thấy, cả A và B đang cùng trỏ về một danh sách. Cho nên, nếu sau đó ta làm thay đổi giá trị của A hoặc B thì những thay đổi đó cũng sẽ thấy được trên biến còn lại (tức là biến còn lại cũng bị thay đổi theo). Ví dụ:
>>> A.append(4) >>> print(A) [1, 2, 3, 4] >>> print(B) [1, 2, 3, 4]
Rõ ràng trong đoạn code trên, sau khi khởi tạo B, ta chưa tác động gì đến nó, nhưng giá trị của nó lại bị thay đổi. Hiện tượng này có một cái hẳn một cái tên, gọi là: Presto-Chango.
notion image
Tuy nhiên, không phải bất cứ khi nào A thay đổi thì B sẽ thay đổi theo (hãy tượng tượng việc lập trình sẽ rắc rối như thế nào khi điều này xảy ra!). Thật may là: B chỉ thay đổi theo A nếu A thực hiện những thay đổi tại chỗ, tức là A thực hiện những phương thức của chính A, trực tiếp làm thay đổi giá trị của A.
Ví dụ, nếu ta thêm số 5 vào danh sách **A **bằng lệnh sau thì **B **sẽ không bị thay đổi theo:
>>> A = [1, 2, 3, 4, 5] >>> print(A) [1, 2, 3, 4, 5] >>> print(B) [1, 2, 3, 4]
Điều này cũng dễ hiểu, bởi ta đã gán cho A một giá trị hoàn toàn mới trên một dãy ô nhớ hoàn toàn mới. Lúc này Python sẽ xem AB là hai biến hoàn toàn độc lập với nhau.
Như đã nói ở trên, không phải lúc nào lệnh gán cũng gây ra **Presto-Chango, **chúng chỉ xảy ra khi kiểu dữ liệu là **mutable **(tức là dữ liệu có thể thay đổi tại chỗ được). List là một kiểu mutable. Để rõ hơn về mutable và immutable, xin xem bài viết:
Tóm lại, khi bạn thực sự muốn copy dữ liệu để tạo ra 2 biến độc lập thì không được dùng lệnh gán đơn thuần.

5. Ngoài tên riêng, Python còn nhiều cách khác để trỏ đến giá trị

Trở lại ví dụ về danh sách A chứa các giá trị **1, 2, 3 **như hình phía trên. Thực chất, bản thân các phần tử của A, (tức là A[0], A[1], A[2]) chính là các cái tên (names) dùng để trỏ về các ô nhớ mang các giá trị **1, 2, 3. **Điều này có thể gây khó hiểu. Hãy xem mô tả dưới đây:
notion image
Như vậy, một lần nữa, khi gọi một giá trị trong mảng bằng index thì thưc ra ta đang dùng một cái tên để gọi đến một ô nhớ.
Tương tự như gọi phần tử của mảng bằng index, những cách gọi sau cũng có bản chất tương tự:
  • dict[key]
  • object.attr
  • object.attr[index],...
Nhắc lại rằng, vì các cách truy cập trên đều cùng là: dùng tên để trỏ đến giá trị nên chúng hoàn toàn có thể xảy ra hiện tượng Presto-Chango.

6. Có nhiều kiểu gán giá trị trong Python

Ta thường dùng dấu bằng "=" để gán biến bên phải cho biến bên trái. Ngoài ra, còn có nhiều kiểu gán khác trong Python. Chính vì lệnh gán không thể copy dữ liệu, nên ta cần phải cẩn thận để tránh Presto-Chango. Sau đây là những lệnh gán khác (mỗi dòng đều là một lệnh gán cho X)
for X in ... [... for X in ...] (... for X in ...) {... for X in ...} class X(...): def X(...): def fn(X): ... ; fn(12) with ... as X: except ... as X: import X from ... import X import ... as X from ... import ... as X
Trong các lệnh gán trên, đáng chú ý là lệnh gọi hàm: Khi gọi hàm thì Python sẽ gán giá trị vào tham số. Tức là nếu trong hàm, tham số bị thay đổi giá trị thì nhiều khả năng sẽ xảy ra Presto-Chango.

7. Sai sót dễ mắc phải khi truyền tham số vào hàm

Giả sử ta cần viết một hàm nhận vào một list và một số integer, hàm này trả về một list giống với list nhận vào nhưng có kèm theo số integer ở cuối cùng. Ví dụ: nếu input là [1, 2, 3] và số 100 thì output sẽ là [1 ,2, 3, 100]. Một cách tự nhiên, ta thường định nghĩa hàm như sau:
# Dinh nghia mot ham ten la "them so" nhu sau def themso(lis, num): out = lis out.append(num) return out
Cách định nghĩa phía trên tương đối dễ hiểu:
  • Đầu tiên ta "sao chép" danh sách vào biến out
  • Tiếp theo ta thêm số num vào danh sách out
  • Cuối cùng ta trả về biến out (đã được thêm numvào cuối danh sách)
Bây giờ hãy thử test hàm trên với input cụ thể
>>> A = [1, 2, 3] >>> B = themso(A, 99) >>> C = themso(A, 100) >>> print(B) >>> print(C) >>> print(A)
Bạn thử đoán xem ba lệnh print phía trên sẽ cho ra kết quả gì?
Như đã nói ở phần trên, Python xem việc truyền tham số tương đương với một phép gán giá trị. Cho nên khi ta gọi lệnh themso(A,99) thì danh sách A sẽ được gán cho biến lis, sau đó, bên trong hàm, biến lis sẽ tiếp tục được gán cho out. Điều này có nghĩa là A, lisout đang cùng trỏ về một dãy ô nhớ trên RAM (dãy ô nhớ này mang các giá trị 1, 2, 3). Hay nói cách khác thì A, lis, out chỉ là những cái tên khác nhau của một danh sách.
Vì hàm này trả về out, nên khi gọi B=themso(A,99) thì lập tức B cũng trỏ tới cùng danh sách với A (lis và out đã bị xóa sau khi ra khỏi hàm). Lúc này B sẽ là [1, 2, 3, 99] như chúng ta mong đợi, tuy nhiên A cũng bị thay đổi theo (vì thật ra A với B là một).
Tiếp tục gọi lệnh C=themso(A,100). Vì lúc này A đã bị biến đổi thành [1, 2, 3, 99], nên C sẽ là [1, 2, 3, 99, 100]. Tuy nhiên, tương tự như trên, A lại bị biến đổi theo C, trở thành [1, 2, 3, 99, 100]
Cuối cùng, vì như đã nói, BA là một, nên B lúc này cũng sẽ mang giá trị [1, 2, 3, 99, 100].
Vậy kết quả của ba lệnh in trên sẽ là:
>>> print(B) [1, 2, 3, 99, 100] >>> print(C) [1, 2, 3, 99, 100] >>> print(A) [1, 2, 3, 99, 100]

8. Kết luận

Python dùng name để trỏ đến value. Một value có thể có nhiều names.
Trong Python, không thể copy dữ liệu bằng lệnh gán. Lệnh gán chỉ tạo ra một cái tên khác mà thôi.
Bên cạnh lệnh gán thông thường (ví dụ A=B), Python còn nhiều kiểu gán khác.
Python xem việc truyền tham số là lệnh gán. Do đó phải cẩn thận khi thiết kế hàm để tránh Presto-Chango không mong muốn.

9. Tài liêu tham khảo

notion image
 
TEXmath
 
Cùng chủ đề