Câu 1Phát biểu nào chính xác nhất về biến private trong Python?
Biến private và Đóng gói — Truy cập an toàn qua getter / setter
Học biến private và đóng gói trong Python. Quy ước _x vs name mangling __x, get_xxx / set_xxx để truy cập an toàn, và phong cách Pythonic @property / @xxx.setter — kèm thực hành.
Lần trước ta đã bao quát hai trụ cột đầu tiên của OOP — kế thừa và đa hình. Bài này khép lại trụ cột thứ ba: đóng gói (encapsulation).
Biến private — Python không có «private» thật sự
Java và C++ có từ khóa private chặn truy cập từ bên ngoài ngay khi bạn khai báo. Python không có cơ chế private cấp ngôn ngữ. Thay vào đó, số lượng dấu gạch dưới đầu tên ra hiệu «cái này dùng nội bộ» hoặc «đừng đụng trực tiếp» — đó là quy ước giữa lập trình viên, không phải luật cứng.
Một dấu gạch dưới _x — private chỉ ở mức quy ước
Thêm một _ trước tên là cách bạn nói với cộng đồng Python rằng «thuộc tính này dùng nội bộ class — đừng truy cập trực tiếp từ bên ngoài». Tham số trong __init__ vẫn dùng tên thường; chỉ trường self. mới mang dấu _ đầu.
class UserAccount:
def __init__(self, owner_name, balance):
self._owner_name = owner_name # nội bộ -> tiền tố _
self._balance = balance
def get_info(self): # accessor hướng ra ngoài
return {"owner": self._owner_name, "balance": self._balance}
user = UserAccount("Minh", 50000)
print(user._balance) # 50000 <- chạy được nhưng không khuyến khích
print(user.get_info()) # {'owner': 'Minh', 'balance': 50000} <- khuyến khích
Hai dấu gạch dưới __x — name mangling
Thêm hai dấu _ đầu tên khiến Python viết lại chính tên thuộc tính. Nếu bạn viết self.__pin = 1234 bên trong class Account, tên thật được lưu trở thành _Account__pin. Đó là name mangling — obj.__pin từ bên ngoài không tìm thấy gì, nên truy cập thực sự khó hơn nhiều.
obj.__pin trực tiếp thất bại với AttributeError vì key đó không có ở đó.class Account:
def __init__(self, owner, pin):
self._owner = owner # private chỉ ở quy ước
self.__pin = pin # bị name-mangling (thành _Account__pin)
acc = Account("Minh", 1234)
print(acc._owner) # Minh <- chạy bình thường
# print(acc.__pin) # AttributeError <- không thấy trực tiếp
print(acc._Account__pin) # 1234 <- tên đã mangling tới được
«__» cũng không phải bức tường tuyệt đối
Hai dấu gạch dưới chặn truy cập trực tiếp obj.__pin, mạnh hơn một bậc. Nhưng ai biết tên đã mangling obj._Account__pin vẫn với tới được. Đó không phải private thật sự. Trong dự án thực, một dấu gạch dưới _x phổ biến hơn nhiều trừ khi có lý do cụ thể để dùng mangling.
Đóng gói — giới hạn truy cập qua method dành riêng
Đóng gói (encapsulation) là tư tưởng thiết kế «gói các thuộc tính dữ liệu và các method thao tác chúng vào một class, và buộc code bên ngoài đi qua một bộ điểm vào nhỏ đã công bố». Vậy làm sao ta dựng các điểm vào đó?
_price không kiểm tra. Đi qua method nghĩa là setter validate kiểu và phạm vi ở một chỗ duy nhất.Phong cách cơ bản nhất là tự viết các method get_xxx / set_xxx bằng tay. Bên trong setter, làm kiểm tra isinstance cho kiểu và kiểm tra phạm vi, và raise ValueError(...) nếu có gì không ổn. Với điều đó tại chỗ, không có giá trị rác nào tới được _price.
class Product:
def __init__(self, name, price, stock):
self._name = name
self._price = price
self._stock = stock
def get_price(self):
return self._price
def set_price(self, price):
if isinstance(price, int) and price >= 0:
self._price = price
else:
raise ValueError("price must be a non-negative integer")
product = Product("T-shirt", 1500, 30)
print(product.get_price()) # 1500
product.set_price(2000)
print(product.get_price()) # 2000
# product.set_price(-100) # ValueError
@property và @xxx.setter
Phong cách get_price() / set_price(...) rõ ràng, nhưng nơi gọi cuối cùng trông kiểu gọi method — không phải sạch nhất. Thành ngữ tinh tế hơn của Python dùng hai decorator: @property và @xxx.setter.
Với chúng, nơi gọi giữ nguyên product.price / product.price = 2000 — truy cập thuộc tính thuần — nhưng bên dưới, method getter và setter được gọi. Đó là cấu trúc hai lớp nơi cú pháp giữ đơn giản nhưng logic vẫn chạy.
@property chuyển hướng đọc và @price.setter chuyển hướng ghi thành lời gọi method. Validation sống bên trong setter.class Product:
def __init__(self, name, price):
self._name = name
self._price = price
@property
def price(self): # getter
return self._price
@price.setter
def price(self, value): # setter — tên phải khớp getter
if not isinstance(value, int) or value < 0:
raise ValueError("price must be a non-negative integer")
self._price = value
@property
def label(self): # computed property — giá trị suy ra
return f"{self._name} ({self._price})"
product = Product("T-shirt", 1500)
print(product.price) # 1500 <- @property được gọi
product.price = 2000 # <- @price.setter được gọi
print(product.price) # 2000
print(product.label) # T-shirt (2000) <- computed property
Giữ tên setter giống hệt tên getter
price trong @price.setter phải khớp tên method từ @property def price trước đó. Python diễn giải decorator như «gắn phiên bản ghi vào cùng object price đã có phiên bản đọc» — nếu tên lệch, chúng bị xem như hai thứ riêng biệt.
Ba trụ cột của OOP
- Bảo vệ dữ liệu — tách trạng thái nội bộ khỏi public API qua
_x - Tính nhất quán — tập trung validation vào setter, một chỗ
- Tự do triển khai — đổi nội bộ mà không đổi public API
- Phong cách Pythonic — quy ước cộng
@property, không phải cưỡng ép cấp ngôn ngữ
- Tái sử dụng cơ chế của lớp cha
- Cùng tên method, hành vi khác theo type
- Dồn truy cập từ ngoài qua một bộ cửa nhỏ
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Lợi ích lớn nhất của việc dùng @property và @xxx.setter là gì?
Câu 3Bên trong class Account: bạn viết self.__pin = 1234. Từ ngoài, acc.__pin báo AttributeError. Tên nào thực sự được lưu trên instance?