Kodax

التغليف (Encapsulation) — البرمجة الكائنية

التغليف (Encapsulation) والخصائص

التغليف يعني أن الكائن يتحكم في بياناته: بقية البرنامج تتعامل معه عبر دوال واضحة، بدلاً من تعديل خصائصه الداخلية مباشرة إلى قيم غير صالحة.

المشكلة

class Account:
    def __init__(self):
        self.balance = 0

account = Account()
account.balance = -5000   # لا شيء يمنع هذا!

مستويات الوصول في Python

Python لا تملك private حقيقية مثل Java، بل اصطلاحات:

| الكتابة | المعنى |

|---|---|

| balance | عام: جزء من واجهة الصنف |

| _balance | داخلي بالاتفاق: "لا تستخدمه من الخارج" لكنه متاح |

| __balance | تشويه الاسم (name mangling): يصبح _Account__balance |

__balance ليست مخفية فعلاً: Python تغيّر اسمها داخلياً إلى _Account__balance لتجنب التصادم مع أسماء في الأصناف الوارثة، ويمكن الوصول إليها بهذا الاسم. الاصطلاح الشائع في Python هو _ واحدة.

@property: خاصية بقواعد

property تجعل دالة تُستخدم كأنها خاصية، فتستطيع التحقق عند القراءة والكتابة دون تغيير طريقة الاستخدام:

class Account:
    def __init__(self, balance=0):
        self._balance = balance

    @property
    def balance(self):           # القراءة: account.balance
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("المبلغ يجب أن يكون موجباً")
        self._balance += amount

بدون setter، تصبح balance للقراءة فقط: account.balance = 10 ترمي AttributeError. الرصيد يتغير فقط عبر deposit و withdraw اللتين تطبقان القواعد.

setter عند الحاجة

@property
def age(self):
    return self._age

@age.setter
def age(self, value):
    if value < 0:
        raise ValueError("عمر غير صالح")
    self._age = value

استدعِ الـ setter داخل __init__ أيضاً (self.age = age) حتى يُطبَّق التحقق من البداية.

خصائص محسوبة

@property
def full_name(self):
    return f"{self.first} {self.last}"

لا تُخزَّن، بل تُحسب عند كل قراءة، فتبقى صحيحة دائماً.

لماذا لا نكتب get_balance() و set_balance()؟

في Python ابدأ بخاصية عامة بسيطة؛ إذا احتجت لاحقاً قواعد، حوّلها إلى @property دون أن يتغير أي كود يستخدمها. هذا أحد أجمل جوانب Python.

تمرين

أنشئ صنف Temperature يخزن الحرارة بالمئوية في _celsius مع setter يرفض القيم الأقل من -273.15، وخاصية محسوبة fahrenheit.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner           # Public
        self._account_type = "عادي"  # Protected (اصطلاح)
        self.__balance = balance     # Private
    
    # Getter
    @property
    def balance(self):
        return self.__balance
    
    # Setter مع تحقق
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("الرصيد لا يمكن أن يكون سالباً!")
        self.__balance = value
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return f"تم إيداع {amount}. الرصيد: {self.__balance}"
        return "مبلغ غير صالح"
    
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return f"تم سحب {amount}. الرصيد: {self.__balance}"
        return "رصيد غير كافٍ أو مبلغ غير صالح"

# استخدام
account = BankAccount("أحمد", 1000)

# الوصول للـ public
print(account.owner)  # أحمد

# الوصول عبر property
print(account.balance)  # 1000

# محاولة تعديل private مباشرة - لا تعمل!
# print(account.__balance)  # AttributeError!

# الطريقة الصحيحة
account.deposit(500)
print(account.balance)  # 1500

# Setter مع تحقق
try:
    account.balance = -100  # ValueError!
except ValueError as e:
    print(e)

# مثال آخر: شخص
class Person:
    def __init__(self, name, age):
        self._name = name
        self.__age = age
    
    @property
    def age(self):
        return self.__age
    
    @age.setter
    def age(self, value):
        if 0 < value < 150:
            self.__age = value
        else:
            raise ValueError("عمر غير صالح")
    
    # Property للقراءة فقط
    @property
    def is_adult(self):
        return self.__age >= 18

person = Person("محمد", 25)
print(person.is_adult)  # True
# person.is_adult = False  # Error! قراءة فقط
اختبر فهمك