Học bằng cách đọc theo thứ tự

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đ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.

0 / 1 / 2 dấu gạch dưới truyền tải ý định
name(không có)thuộc tínhpublicdùng tự dotừ bên ngoài_name(1)quy ước:privateđừng đụng__name(2)
Số dấu gạch dưới không thay đổi việc thi hành; nó chỉ là nhãn ra hiệu ý định.

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

Dùng class BlogPost để cảm nhận _ có nghĩa gì (đây là tình huống khác với mẫu trên — cùng cấu trúc, khác trường).

① Định nghĩa class BlogPost: và gán self._titleself._views trong __init__(self, title, views).

② Định nghĩa summary(self) để return {"title": self._title, "views": self._views}.

③ Tạo post = BlogPost("Bắt đầu với Python", 100). Trước tiên, truy cập trực tiếp với post._viewsprint ra (chạy được nhưng là pattern bị cấm).

④ Sau đó dùng pattern khuyến khíchprint(post.summary()).

(Khi chạy đúng, phần giải thích sẽ hiện ra.)

Python Editor

Chạy code để xem đầu ra

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 manglingobj.__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.

__pin được viết lại thành _Account__pin nội bộ
self.__pin= 1234Python viếtlại tên_Account__pin= 1234obj.__pin-> Errorobj._Account__pin-> 1234lưu
Thuộc tính hai dấu gạch dưới được lưu dưới «gạch dưới + tên class + tên gốc». Đọc 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

Dùng class LoginForm để xác minh rằng __password thực sự được viết lại (tình huống khác với mẫu Account, hành vi giống nhau).

① Định nghĩa class LoginForm: và trong __init__(self, username, password) gán self._username = usernameself.__password = password.

② Tạo form = LoginForm("minh", "p@ssw0rd"). Dùng print(form._username) để xác nhận bên một dấu gạch dưới đọc bình thường.

③ Dùng print(form._LoginForm__password) để lấy password qua tên đã mangling.

④ Chạy print([n for n in dir(form) if not n.startswith('__')]) để dump tên thuộc tính thực sự lưu trên instance và thấy _LoginForm__password có trong danh sách.

Python Editor

Chạy code để xem đầu ra

«__» 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 đó?

Dồn đọc/ghi qua một method
tính nhất quánbị phághi trực tiếp_price = -100❌ ngoàiproduct._price= -100tính nhất quánđược giữset_price()validate✅ ngoàiproduct.set_price(100)y nguyênchỉ khi OK
Ghi trực tiếp cho phép bất kỳ giá trị nào hạ cánh 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

Thêm getter và setter chỉ cho age vào class form đăng ký UserProfile. Tuổi không thể âm hoặc 200+, nên setter phải bảo vệ phạm vi.

① Định nghĩa class UserProfile: và trong __init__(self, name, age) gán _name_age.

② Định nghĩa get_age(self)return self._age.

③ Định nghĩa set_age(self, age). Chỉ khi age là int và 0 <= age <= 150 mới chạy self._age = age; nếu không raise ValueError("age must be an integer in [0, 150]").

④ Tạo user = UserProfile("Minh", 30), print user.get_age(), sau đó user.set_age(31)print lại kết quả.

⑤ Xác nhận đường từ chối: bọc user.set_age(-5) trong try / except ValueError as e:print("NG:", e).

Python Editor

Chạy code để xem đầu ra

@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@xxx.setter.

Với chúng, nơi gọi giữ nguyên product.price / product.price = 2000truy 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.

product.price trông y như cũ; method chạy bên dưới
product.price@propertydef pricereturnself._priceproduct.price= 2000@price.setterdef pricevalidate, rồicập nhật self._priceđọcghi
Cú pháp truy cập thuộc tính giữ nguyên; @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.

Viết lại cùng UserProfile từ Thực hành 3 dùng @property / @age.setter, và thêm một computed property age_group (under 18 / adult / senior).

① Định nghĩa class UserProfile: với __init__(self, name, age) gán _name / _age.

② Dùng @property def age(self): để return self._age.

③ Dùng @age.setter def age(self, value):; chỉ khi isinstance(value, int) and 0 <= value <= 150 mới gán self._age = value, nếu không raise ValueError("age must be an integer in [0, 150]").

④ Dùng @property def age_group(self): để trả "under 18" nếu self._age < 18, "adult" nếu self._age < 65, ngược lại "senior" (không setter — chỉ đọc).

⑤ Tạo user = UserProfile("Minh", 30), print user.ageuser.age_group, sau đó user.age = 70print age_group thêm một lần nữa.

Python Editor

Chạy code để xem đầu ra

Ba trụ cột của OOP

Đóng gói nâng đỡ những gì
Đóng gói
  • 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ữ
Kế thừa
  • Tái sử dụng cơ chế của lớp cha
Đa hình
  • Cùng tên method, hành vi khác theo type
Đóng gói
  • Dồn truy cập từ ngoài qua một bộ cửa nhỏ
Kế thừa, đa hình và đóng gói là ba trụ cột của OOP. Kế thừa tái sử dụng, đa hình thống nhất nơi gọi, và đóng gói khiến cả hệ thống khó bị phá — gán mỗi vai trò một cách có ý thức giữ thiết kế class sạch.
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Phát biểu nào chính xác nhất về biến private trong Python?

Câu 2Lợi ích lớn nhất của việc dùng @property@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?