Mutable và Immutable trong python

Mấy hôm code có bug mà tìm mãi mới ra, hoá ra là do mình không để ý tới tính chất mutable với immutable trong python. Giờ thì bug đã fix xong rồi, mình sẽ demo bằng chương trình dưới đây:

Mình có 1 hàm double để x2 từng phần tử của một danh sách, trông qua thì không có vấn đề gì ha.

Mình sẽ sử dụng nó như thế này:

Mình kiểm tra bằng cách lấy phần tử ở danh sách đã được double chia cho phần tử tương ứng ở danh sách gốc. Kết quả mong đợi đáng lẽ ra phải là 2 cho mỗi phép chia. Nhưng kết quả mình nhận được ở đây là:

Toàn là 1 chứ không phải 2 như mong đợi, rõ ràng có gì đó không đúng ở đây. Nếu như print ra nội dung của từng danh sách thì ta dễ dàng thấy được các giá trị của danh sách lstlst_doubled là giống nhau, đều là [2, 4, 6, 8]

Vấn đề này xuất hiện do mình đã không để ý đến việc kiểu list là mutable, có thể thay đổi giá trị sau khi khởi tạo, Python nó truyền tham số kiểu passed by assignment, kiểu này nó sẽ tạo ra một reference tới object mà mình truyền vào, lỡ mà truyền vào cái object mutable, trong hàm ta thay đổi giá trị của object đó, thì ra khỏi hàm giá trị của object đó là giá trị mới chứ không phải là giá trị cũ trước khi được truyền vào hàm nữa.

** Từ hàm ở đây mình bao gồm là function và method luôn nhé.

Vậy mutable, immutable là gì?

– Một object thuộc loại mutable thì nó có thể thay đổi giá trị sau khi được định nghĩa và khởi tạo giá trị

– Một object thuộc loại immutable thì ngược lại, không thể thay thổi giá trị của nó sau khi nó được định nghĩa và khởi tạo giá trị.

Trong python, ta có những object thuộc những kiểu thường gặp sau đây là immutable

int, float, bool, string, decimal, complex, tuple, range, frozenset, bytes

và mutable

list, dict, set, bytearray, user-defined class

Về immutable object

Mình có chương trình

Ta sử dụng

Vì int thuộc loại immutable, nên trong hàm plus_ten, nó chỉ copy giá trị của tham số x vào một biến khác, tính toán và trả về biến đó nên giá trị của đối số truyền vào là a vẫn giữ nguyên. Ta sửa lại một xíu để hiểu hơn.

Bạn xem thêm về hàm id() ở https://docs.python.org/3/library/functions.html#id. Hiểu đơn giản thì nó trả về một số nguyên được xem như là “mã số” của object (thường thì số nguyên này là địa chỉ của object trong bộ nhớ, cái này tuỳ vào cách hiện thực Python và OS)

Từ đây ta thấy rõ ràng x mà hàm plus_ten trả về không phải là x nhận được từ tham số, mà là một x khác được python tạo ra khi tính x = x + 10

Hoặc đơn giản hơn

Cái này có vấn đề gì không ? Thật ra là có 1 cái nhỏ, ta thường hay viết mấy chương trình kiểu như này

Chúng ta cứ ngộ nhận rằng message sẽ cộng thêm cái item_message vào sau nó, nhưng thực chất, cứ mỗi lần lặp thì nó lại tạo ra một message, cái message ở vòng lặp trước trở thành rác. Rõ ràng theo cách này thì ta đang tốn bộ nhớ cho việc tạo rác và dọn rác. Nếu data_source và item_message có length lớn thì rõ là có vấn đề, performance của chương trình chắc chắn bị giảm. Mặc dù máy tính hiện nay có CPU to RAM lớn, nhưng để tối ưu thì những điều này cần nên lưu ý.

Ví dụ như ta có thể giải quyết cái trên bằng cách dùng mutable object, ném các item_message vào 1 list, sau đó join lại thành 1 chuỗi.

Về mutable

Ở đầu bài có ví dụ cho loại này đó, mình thêm cái ví dụ nữa để hiểu hơn nào

Tất cả đều True, vậy thì ta có gì?

Ở chỗ list_2 = list_1, là ta cho list_2 trỏ về object mà list_1 trỏ tới, như vậy hai đứa cùng trỏ đến một object, vì kiểu list là mutable nên khi ta thực hiện thay đổi (thêm một phần tử) thì nó không tạo ra một object mới như immutable mà thực hiện thay đổi ngay trên object đó luôn, điều này dẫn đến list_1list_2 vẫn trỏ vào chỗ cũ.

Ta cứ hay viết code kiểu

Nhiều khi ta không để ý thì lại đi viết code như thế này, tham số sẽ là một kiểu list, là mutable, rõ là sau khi remove_something thì original_data đã bị đổi, sau đó lại gọi add_something thì rõ ràng đang chạy sai dữ liệu so với ý ta muốn. Ta viết code kiểu này thì lại làm hàm có hai ngõ ra, một chỗ return, một chỗ là tham số, sử dụng không cẩn thận thì lại lòi ra bug rồi ngồi fix sấp mặt.

Như vậy chỗ này để chắc ăn thì ta nên copy data_list ở tham số ra một list khác và thực hiện thay đổi trên list đó rồi trả về, như vậy sẽ bảo toàn được list mà mình truyền vào để dùng tiếp ở những chỗ khác (copy thì để ý shallow và deep vì nó có nhiều vấn đề với những cái phần tử của list nữa). Không copy thì ta có thể tạo ra một list mới để chứa những giá trị cần trả về, khi ta thao tác với một phần tử của list gốc thì ném cái giá trị mới vào list để trả về này. Nói chung là nếu còn dùng lại list gốc thì trong hàm đừng thay đổi nó, đối với các kiểu mutable khác cũng vậy. Nếu dùng có ý đồ thì comment cho rõ ràng để thằng vào sau nó không xài sai rồi ngồi debug.

Ta đến với một chỗ sai khác

Khi gọi append_list(), không truyền đối số, thì sử dụng đối số mặc định là list rỗng [], điều này không có gì bàn cãi, và ta nghĩ rằng append_list("item 2") sẽ trả về ['item 2'], vì hai lời gọi hàm nhìn như chả liên quan mẹ gì nhau cả.

Có vấn đề gì không? Không, chỉ là ta chưa biết cách mà Python làm việc với những tham số mặc định.

Khi ta sử dụng giá trị mặc định cho tham số thì khi hàm được định nghĩa, tham số đó cũng được khởi tạo với giá trị tương ứng. Hơn nữa, giá trị này được sử dụng chung cho mỗi lần gọi hàm mà không truyền đối số vào. Có nghĩa là lúc định nghĩa hàm, nó được tạo ra và nằm đó, thằng nào gọi hàm mà không truyền tham số thì lấy nó lên mà xài chứ không có tạo lại. Đây sẽ là vấn đề nếu như ta sử dụng mutable object đem đi làm tham số mặc định như ví dụ trên.

=> Hạn chế sử dụng mutable object như list, dict, … để làm giá trị mặc định cho tham số, nếu bạn muốn dùng nó thì hãy làm cho nó chỉ được gán giá trị khi hàm được gọi.

Thay vì viết như trên thì ta có thể viết

Nâng cao một xíu, ta xét lại kiểu tuple, tuple là một kiểu Containers trong Python: https://docs.python.org/3/library/stdtypes.html#tuple, tuple thuộc loại immutable. Xét đoạn code sau

Ta có tup là một tuple, nó chứa 2 số nguyên, 1 chuỗi, 1 danh sách, và ta đã thêm vào danh sách đó một phần tử, rồi ta thấy tup sau khi thêm phần tử vào list khác so với tup ban đầu. Any problem ??

No, thực chất là tup vẫn không đổi, cái bị thay đổi là cái danh sách, và nó là một mutable object, tup nó chỉ chứa một reference đến danh sách kia thôi.

Ta có thể kiểm tra lại

Rõ ràng tup không bị thay đổi. Lưu ý ở chỗ này :p

======================================

Ok, đến đây là hết rồi nè :p, mình dừng phím ở đây nhé 😐

Leave a Reply

Your email address will not be published. Required fields are marked *