Hiểu Về Tính Biến Đổi Trong Python
Python thường mang đến những bất ngờ thú vị. Một trong những khái niệm gây ngạc nhiên nhất là tính biến đổi (mutability). Ban đầu, có vẻ như đây chỉ là một thuật ngữ mà bạn có thể bỏ qua. Nhưng nó quyết định cách mà mã của bạn hoạt động theo những cách rất tinh vi: tại sao một cập nhật biến lại lan truyền nhanh chóng, trong khi một biến khác lại đứng yên như đá.
Hãy cùng khám phá điều gì thực sự đang diễn ra.
Tính Biến Đổi Là Gì?
Một đối tượng được coi là biến đổi (mutable) khi nó có thể thay đổi tại chỗ. Giống như đất sét, bạn có thể định hình lại nó mà không cần phải vứt bỏ.
- Danh sách là biến đổi. Bạn có thể thêm, mở rộng hoặc xóa - nó vẫn là danh sách đó, chỉ khác đi một chút.
- Từ điển cũng là biến đổi. Bạn có thể thêm hoặc xóa các khóa và đối tượng sẽ vẫn tồn tại.
Ngược lại, các đối tượng bất biến (immutable) giống như thủy tinh: chúng không thể uốn cong. Nếu bạn muốn điều gì đó khác, bạn phải tạo ra một cái mới hoàn toàn.
- Chuỗi là bất biến. Bạn không thể thay thế một ký tự duy nhất trực tiếp.
- Tuple cũng là bất biến. Một khi đã được thiết lập, nội dung của nó không thể được gán lại.
Kiểm Tra Danh Tính: Cùng Một Đối Tượng Hay Chỉ Là Giống Nhau?
Các biến trong Python không giữ các đối tượng; chúng tham chiếu đến chúng. Hai biến có thể chỉ đến cùng một đối tượng hoặc đến các đối tượng khác nhau. Bạn có thể kiểm tra điều này với hàm tích hợp id().
python
numbers = [1, 2, 3]
alias = numbers
alias.append(4)
print(numbers) # [1, 2, 3, 4]
print(alias) # [1, 2, 3, 4]
print(id(numbers) == id(alias)) # True
Cả hai biến đều trỏ đến cùng một đối tượng danh sách. Nếu bạn thay đổi một cái, bạn sẽ thay đổi cả hai.
Bây giờ về chuỗi:
python
greeting = "hello"
copy = greeting
print(id(greeting) == id(copy)) # True (cùng một đối tượng)
copy += " world"
print(greeting) # "hello"
print(copy) # "hello world"
print(id(greeting) == id(copy)) # False (đối tượng mới được tạo)
Sự khác biệt không nằm ở việc gán - cả alias = numbers và copy = greeting ban đầu đều trỏ đến cùng một đối tượng. Điểm phân tách xuất hiện khi bạn thay đổi chúng: danh sách biến đổi tại chỗ, trong khi chuỗi tạo ra các đối tượng mới và gán lại biến.
Tại Sao Tính Biến Đổi Khiến Người Mới Bất Ngờ?
Dưới đây là cái bẫy đã gây ra hàng ngàn báo cáo lỗi:
python
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] <- bất ngờ!
Danh sách mặc định không được đặt lại giữa các lần gọi. Python tạo nó một lần và tiếp tục sử dụng lại. Cách khắc phục là đặt mặc định thành None và xây dựng danh sách bên trong:
python
def add_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Các tham số mặc định mà có thể biến đổi trông có vẻ vô hại nhưng thực sự tồn tại vĩnh viễn.
Sức Mạnh Tinh Tế Của Tính Bất Biến
Tính bất biến ban đầu có vẻ hạn chế. Nhưng nó có những lợi ích:
- An toàn khi chia sẻ. Gửi một đối tượng bất biến cho một hàm và bạn biết nó sẽ không bị thay đổi.
- Có thể băm. Các đối tượng bất biến có thể tồn tại trong tập hợp hoặc làm khóa từ điển. Danh sách thì không thể.
- Dễ dự đoán. Các tuple và chuỗi không bao giờ thay đổi dưới chân bạn.
Tóm lại: tính bất biến giống như những rào chắn mà bạn không biết là mình cần.
Khi Nào Tính Biến Đổi Là Điều Bạn Cần
Ngược lại, tính biến đổi rất hiệu quả cho các tập hợp đang phát triển:
- Dữ liệu đang phát triển (danh sách, từ điển, tập hợp)
- Theo dõi trạng thái thay đổi theo thời gian (giỏ hàng, bảng xếp hạng)
- Tránh chi phí của việc tạo đối tượng liên tục
Đó giống như việc khuấy một nồi súp: bạn cứ thêm nguyên liệu cho đến khi hoàn thành.
Thông Điệp Lẫn Lộn: Các Bộ Chứa Bất Biến Với Nội Dung Biến Đổi
Một tuple là bất biến, nhưng nó có thể chứa các đối tượng biến đổi:
python
t = ([1, 2], [3, 4])
t[0].append(99)
print(t) # ([1, 2, 99], [3, 4])
Tuple bản thân không thể được gán lại, nhưng danh sách bên trong nó có thể được thay đổi. Nó giống như thủy tinh bên ngoài, đất sét bên trong.
Cách Nghĩ Về Tính Biến Đổi Mà Không Bị Rối
Dưới đây là một mô hình tư duy hữu ích:
- Nếu một đối tượng có thể thay đổi mà không tạo ra một cái mới, nó là biến đổi.
- Nếu bất kỳ “thay đổi” nào tạo ra một đối tượng mới, nó là bất biến.
Và đây là quy tắc vàng: biết loại đối tượng mà bạn đang truyền xung quanh. Các lỗi xảy ra khi bạn vô tình đưa ra một quả lựu đạn thay vì một bức ảnh chụp.
Những Điều Rút Ra Để Ghi Nhớ
- Danh sách, từ điển và tập hợp là biến đổi. Chuỗi, tuple và frozenset không phải vậy.
- Tính biến đổi có thể tiết kiệm thời gian và bộ nhớ, nhưng cũng có thể gây ra những tác động không lường trước.
- Tính bất biến mang lại sự an toàn và làm cho các đối tượng có thể sử dụng như các thành viên trong tập hợp hoặc khóa từ điển.
- Các tham số mặc định có thể biến đổi trong các hàm là một cái bẫy - hãy sử dụng
Nonethay vào đó. - Tuple có thể ẩn chứa nội dung biến đổi, vì vậy đừng giả định rằng “bất biến” có nghĩa là không thể chạm vào.
Tính biến đổi là một trong những ý tưởng của Python mà bạn không thể tránh khỏi. Càng sớm bạn bắt đầu nghĩ về danh sách như đất sét và chuỗi như thủy tinh, bạn sẽ ít có khả năng phải quét những mảnh vỡ trong mã của mình hơn.
*Aaron Rose là kỹ sư phần mềm và nhà văn công nghệ tại tech-reader.blog và là tác giả của sách Think Like a Genius.