Python OOP
Đóng gói (Encapsulation)

Đóng gói (Encapsulation)

Encapsulation là gì?

Encapsulation (Đóng gói) là việc:

  1. Gom nhóm dữ liệu (thuộc tính) và các hành động (phương thức) liên quan vào một đơn vị (class)
  2. Che giấu chi tiết bên trong, chỉ hiển thị những gì cần thiết ra bên ngoài
  3. Bảo vệ dữ liệu khỏi việc truy cập và thay đổi trực tiếp không mong muốn

Tại sao cần Encapsulation?

1. Bảo vệ dữ liệu

Ngăn chặn việc thay đổi dữ liệu một cách tùy tiện từ bên ngoài.

2. Dễ bảo trì

Thay đổi cách thức hoạt động bên trong mà không ảnh hưởng code bên ngoài.

3. Kiểm soát truy cập

Kiểm tra và validate dữ liệu trước khi thay đổi.

4. Giảm độ phức tạp

Người dùng chỉ cần biết "cái gì" mà không cần biết "như thế nào".

Ví dụ minh họa

Hãy tưởng tượng một chiếc điện thoại:

  • Bạn chỉ cần nhấn nút để gọi điện (interface công khai)
  • Bạn không cần biết bên trong hoạt động thế nào (chi tiết được che giấu)
  • Bạn không thể trực tiếp can thiệp vào mạch điện (dữ liệu được bảo vệ)

Access Modifiers trong Python

Python sử dụng quy ước đặt tên để phân biệt mức độ truy cập:

LoạiKý hiệuTruy cậpÝ nghĩa
PublicnameMọi nơiCông khai, ai cũng dùng được
Protected_nameTrong class và subclassBảo vệ, chỉ nên dùng trong class
Private__nameChỉ trong classRiêng tư, chỉ class đó dùng

Lưu ý: Đây chỉ là quy ước, Python không thực sự ngăn chặn hoàn toàn việc truy cập.

1. Public Members (Thành viên công khai)

Có thể truy cập từ mọi nơi.

class Person:
    def __init__(self, name, age):
        self.name = name  # Public
        self.age = age    # Public
 
    def introduce(self):  # Public method
        print(f"Tôi là {self.name}, {self.age} tuổi")
 
person = Person("An", 20)
print(person.name)  # Truy cập trực tiếp - OK
person.age = 25     # Thay đổi trực tiếp - OK
person.introduce()  # Tôi là An, 25 tuổi

2. Protected Members (Thành viên bảo vệ)

Sử dụng _ (một dấu gạch dưới) ở đầu tên.

class BankAccount:
    def __init__(self, account_number, balance):
        self._account_number = account_number  # Protected
        self._balance = balance                # Protected
 
    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            print(f"Đã nạp {amount:,}đ")
 
    def _validate_amount(self, amount):  # Protected method
        return amount > 0
 
account = BankAccount("123456", 1000000)
 
# Vẫn có thể truy cập nhưng KHÔNG NÊN
print(account._balance)  # 1000000 - Không khuyến khích!
 
# Nên dùng qua method
account.deposit(500000)

Quy ước: _name có nghĩa là "Đây là internal, bạn không nên dùng trực tiếp từ bên ngoài".

3. Private Members (Thành viên riêng tư)

Sử dụng __ (hai dấu gạch dưới) ở đầu tên.

class BankAccount:
    def __init__(self, account_number, balance):
        self.__account_number = account_number  # Private
        self.__balance = balance                # Private
 
    def deposit(self, amount):
        if self.__validate_amount(amount):
            self.__balance += amount
            print(f"Đã nạp {amount:,}đ. Số dư: {self.__balance:,}đ")
 
    def withdraw(self, amount):
        if self.__validate_amount(amount):
            if amount <= self.__balance:
                self.__balance -= amount
                print(f"Đã rút {amount:,}đ. Số dư: {self.__balance:,}đ")
            else:
                print("Số dư không đủ!")
 
    def get_balance(self):  # Public method để truy cập private data
        return self.__balance
 
    def __validate_amount(self, amount):  # Private method
        return amount > 0
 
account = BankAccount("123456", 1000000)
 
# Không thể truy cập trực tiếp
# print(account.__balance)  # Lỗi AttributeError!
 
# Phải dùng qua public method
print(account.get_balance())  # 1000000
account.deposit(500000)       # Đã nạp 500,000đ. Số dư: 1,500,000đ

Name Mangling

Python thực hiện "name mangling" với private attributes: __name_ClassName__name

class MyClass:
    def __init__(self):
        self.__private = "Private value"
 
obj = MyClass()
# print(obj.__private)  # Lỗi!
 
# Vẫn có thể truy cập bằng cách này (nhưng KHÔNG NÊN!)
print(obj._MyClass__private)  # Private value

Getter và Setter Methods

Cách truyền thống để truy cập và thay đổi private attributes.

class Student:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        self.__gpa = 0.0
 
    # Getter methods
    def get_name(self):
        return self.__name
 
    def get_age(self):
        return self.__age
 
    def get_gpa(self):
        return self.__gpa
 
    # Setter methods
    def set_name(self, name):
        if len(name) > 0:
            self.__name = name
        else:
            print("Tên không hợp lệ!")
 
    def set_age(self, age):
        if 0 < age < 150:
            self.__age = age
        else:
            print("Tuổi không hợp lệ!")
 
    def set_gpa(self, gpa):
        if 0.0 <= gpa <= 4.0:
            self.__gpa = gpa
        else:
            print("GPA phải từ 0.0 đến 4.0!")
 
student = Student("An", 20)
print(student.get_name())  # An
 
student.set_gpa(3.5)
print(student.get_gpa())   # 3.5
 
student.set_gpa(5.0)       # GPA phải từ 0.0 đến 4.0!

Property Decorator (Cách Pythonic)

Cách hiện đại và được khuyên dùng trong Python.

class Student:
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        self.__gpa = 0.0
 
    @property
    def name(self):
        """Getter cho name"""
        return self.__name
 
    @name.setter
    def name(self, value):
        """Setter cho name"""
        if len(value) > 0:
            self.__name = value
        else:
            raise ValueError("Tên không được rỗng!")
 
    @property
    def age(self):
        """Getter cho age"""
        return self.__age
 
    @age.setter
    def age(self, value):
        """Setter cho age"""
        if 0 < value < 150:
            self.__age = value
        else:
            raise ValueError("Tuổi không hợp lệ!")
 
    @property
    def gpa(self):
        """Getter cho gpa"""
        return self.__gpa
 
    @gpa.setter
    def gpa(self, value):
        """Setter cho gpa"""
        if 0.0 <= value <= 4.0:
            self.__gpa = value
        else:
            raise ValueError("GPA phải từ 0.0 đến 4.0!")
 
# Sử dụng như thuộc tính thông thường
student = Student("An", 20)
print(student.name)  # An (gọi getter)
 
student.gpa = 3.5    # Gọi setter
print(student.gpa)   # 3.5
 
try:
    student.gpa = 5.0  # ValueError: GPA phải từ 0.0 đến 4.0!
except ValueError as e:
    print(f"Lỗi: {e}")

Read-only Property

Tạo thuộc tính chỉ đọc (không có setter).

class Circle:
    def __init__(self, radius):
        self.__radius = radius
 
    @property
    def radius(self):
        return self.__radius
 
    @radius.setter
    def radius(self, value):
        if value > 0:
            self.__radius = value
        else:
            raise ValueError("Bán kính phải > 0")
 
    @property
    def area(self):
        """Read-only property - chỉ có getter"""
        return 3.14159 * self.__radius ** 2
 
    @property
    def circumference(self):
        """Read-only property"""
        return 2 * 3.14159 * self.__radius
 
circle = Circle(5)
print(f"Bán kính: {circle.radius}")           # 5
print(f"Diện tích: {circle.area:.2f}")        # 78.54
print(f"Chu vi: {circle.circumference:.2f}")  # 31.42
 
circle.radius = 10  # OK - có setter
print(f"Diện tích mới: {circle.area:.2f}")    # 314.16
 
# circle.area = 100  # Lỗi! area là read-only

Ví dụ thực tế: Class BankAccount

class BankAccount:
    __interest_rate = 0.05  # Private class attribute
 
    def __init__(self, account_number, owner_name, balance=0):
        self.__account_number = account_number  # Private
        self.__owner_name = owner_name          # Private
        self.__balance = balance                # Private
        self.__transaction_history = []         # Private
 
    @property
    def account_number(self):
        """Số tài khoản - chỉ đọc"""
        return self.__account_number
 
    @property
    def owner_name(self):
        """Tên chủ tài khoản"""
        return self.__owner_name
 
    @owner_name.setter
    def owner_name(self, name):
        if len(name) > 0:
            self.__owner_name = name
        else:
            raise ValueError("Tên không được rỗng!")
 
    @property
    def balance(self):
        """Số dư - chỉ đọc"""
        return self.__balance
 
    def deposit(self, amount):
        """Nạp tiền"""
        if self.__validate_amount(amount):
            self.__balance += amount
            self.__add_transaction("Nạp tiền", amount)
            print(f"Đã nạp {amount:,}đ. Số dư mới: {self.__balance:,}đ")
            return True
        return False
 
    def withdraw(self, amount):
        """Rút tiền"""
        if not self.__validate_amount(amount):
            return False
 
        if amount > self.__balance:
            print("Số dư không đủ!")
            return False
 
        self.__balance -= amount
        self.__add_transaction("Rút tiền", amount)
        print(f"Đã rút {amount:,}đ. Số dư còn lại: {self.__balance:,}đ")
        return True
 
    def transfer(self, other_account, amount):
        """Chuyển tiền"""
        if self.withdraw(amount):
            other_account.deposit(amount)
            print(f"Đã chuyển {amount:,}đ cho TK {other_account.account_number}")
            return True
        return False
 
    def add_interest(self):
        """Tính lãi"""
        interest = self.__balance * BankAccount.__interest_rate
        self.__balance += interest
        self.__add_transaction("Lãi suất", interest)
        print(f"Đã cộng lãi {interest:,}đ. Số dư mới: {self.__balance:,}đ")
 
    def get_transaction_history(self):
        """Xem lịch sử giao dịch"""
        print(f"\n=== LỊCH SỬ GIAO DỊCH - TK {self.__account_number} ===")
        for transaction in self.__transaction_history:
            print(f"{transaction['type']}: {transaction['amount']:,}đ")
 
    def __validate_amount(self, amount):
        """Private method: Kiểm tra số tiền hợp lệ"""
        if amount <= 0:
            print("Số tiền phải lớn hơn 0!")
            return False
        return True
 
    def __add_transaction(self, transaction_type, amount):
        """Private method: Thêm giao dịch vào lịch sử"""
        self.__transaction_history.append({
            'type': transaction_type,
            'amount': amount
        })
 
    @classmethod
    def set_interest_rate(cls, rate):
        """Class method: Đặt lãi suất chung"""
        if 0 <= rate <= 1:
            cls.__interest_rate = rate
            print(f"Đã đặt lãi suất mới: {rate*100}%")
        else:
            print("Lãi suất không hợp lệ!")
 
# Sử dụng
account1 = BankAccount("001", "Nguyễn Văn A", 5000000)
account2 = BankAccount("002", "Trần Thị B", 3000000)
 
# Truy cập qua property
print(f"Số TK: {account1.account_number}")  # 001 - read-only
print(f"Chủ TK: {account1.owner_name}")     # Nguyễn Văn A
print(f"Số dư: {account1.balance:,}đ")      # 5,000,000đ - read-only
 
# Không thể thay đổi trực tiếp
# account1.balance = 10000000  # Lỗi! balance là read-only
 
# Phải dùng qua method
account1.deposit(1000000)
account1.withdraw(500000)
account1.transfer(account2, 1000000)
account1.add_interest()
 
account1.get_transaction_history()

Ví dụ: Class Temperature với Validation

class Temperature:
    """Class nhiệt độ với validation"""
    ABSOLUTE_ZERO = -273.15  # Public constant
 
    def __init__(self, celsius=0):
        self.__celsius = None
        self.celsius = celsius  # Dùng setter để validate
 
    @property
    def celsius(self):
        """Nhiệt độ Celsius"""
        return self.__celsius
 
    @celsius.setter
    def celsius(self, value):
        """Setter với validation"""
        if value < Temperature.ABSOLUTE_ZERO:
            raise ValueError(
                f"Nhiệt độ không thể thấp hơn {Temperature.ABSOLUTE_ZERO}°C"
            )
        self.__celsius = value
 
    @property
    def fahrenheit(self):
        """Nhiệt độ Fahrenheit - tính từ Celsius"""
        return self.__celsius * 9/5 + 32
 
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Đặt nhiệt độ từ Fahrenheit"""
        celsius = (value - 32) * 5/9
        self.celsius = celsius  # Dùng setter của celsius để validate
 
    @property
    def kelvin(self):
        """Nhiệt độ Kelvin"""
        return self.__celsius + 273.15
 
    @kelvin.setter
    def kelvin(self, value):
        """Đặt nhiệt độ từ Kelvin"""
        if value < 0:
            raise ValueError("Nhiệt độ Kelvin không thể âm!")
        self.celsius = value - 273.15
 
    def __str__(self):
        return (f"{self.celsius:.2f}°C = "
                f"{self.fahrenheit:.2f}°F = "
                f"{self.kelvin:.2f}K")
 
# Sử dụng
temp = Temperature(25)
print(temp)  # 25.00°C = 77.00°F = 298.15K
 
temp.fahrenheit = 98.6
print(temp)  # 37.00°C = 98.60°F = 310.15K
 
temp.kelvin = 273.15
print(temp)  # 0.00°C = 32.00°F = 273.15K
 
try:
    temp.celsius = -300  # ValueError!
except ValueError as e:
    print(f"Lỗi: {e}")

Computed Properties

Properties được tính toán từ các thuộc tính khác.

class Rectangle:
    def __init__(self, width, height):
        self.__width = width
        self.__height = height
 
    @property
    def width(self):
        return self.__width
 
    @width.setter
    def width(self, value):
        if value > 0:
            self.__width = value
        else:
            raise ValueError("Chiều rộng phải > 0")
 
    @property
    def height(self):
        return self.__height
 
    @height.setter
    def height(self, value):
        if value > 0:
            self.__height = value
        else:
            raise ValueError("Chiều cao phải > 0")
 
    @property
    def area(self):
        """Computed property"""
        return self.__width * self.__height
 
    @property
    def perimeter(self):
        """Computed property"""
        return 2 * (self.__width + self.__height)
 
    @property
    def diagonal(self):
        """Computed property"""
        return (self.__width ** 2 + self.__height ** 2) ** 0.5
 
rect = Rectangle(5, 3)
print(f"Diện tích: {rect.area}")      # 15
print(f"Chu vi: {rect.perimeter}")    # 16
print(f"Đường chéo: {rect.diagonal:.2f}")  # 5.83
 
# Khi thay đổi width/height, các computed properties tự động cập nhật
rect.width = 10
print(f"Diện tích mới: {rect.area}")  # 30

Tổng kết

  • Encapsulation giúp bảo vệ dữ liệu và kiểm soát truy cập
  • Python sử dụng quy ước đặt tên:
    • name: Public (công khai)
    • _name: Protected (bảo vệ)
    • __name: Private (riêng tư)
  • Property decorator là cách Pythonic để tạo getter/setter
  • Dùng validation trong setter để đảm bảo dữ liệu hợp lệ
  • Computed properties tính toán giá trị từ các thuộc tính khác
  • Encapsulation làm code dễ bảo trì và an toàn hơn

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về Polymorphism (Đa hình)!


Lập trình Python - Bumbii Academy