عندما تبدأ باستخدام الكلاسات في بايثون، يكون من السهل وضع كل البيانات داخل الكائن ثم تعديلها مباشرة من أي مكان في البرنامج. في مثال صغير قد يبدو هذا طبيعيًا، لكن المشكلة تظهر عندما تكبر الكلاسات وتصبح بعض القيم لها قواعد يجب احترامها؛ مثل رصيد حساب لا يجب أن يصبح سالبًا، أو سعر منتج لا يقبل قيمة أقل من صفر، أو بيانات داخلية لا نريد أن يعتمد عليها بقية المشروع مباشرة.
هنا يظهر مفهوم التغليف Encapsulation. فكرته ليست «إخفاء كل شيء» بقدر ما هي تنظيم حدود الكائن: ما البيانات التي تعتبر جزءًا من الواجهة العامة؟ وما التفاصيل الداخلية التي لا ينبغي للكود الخارجي الاعتماد عليها؟ ومن المسؤول عن التحقق من صحة القيمة قبل تعديلها؟
في الدرس 38 من سلسلة أساسيات بايثون سنتعلم الفرق بين public و_protected و__private، ونفهم لماذا لا يوجد في بايثون متغير private مغلق تمامًا بالطريقة المعروفة في بعض اللغات، ثم نشرح Name Mangling واستخدام @property و@setter لبناء كائنات أوضح وأكثر أمانًا من ناحية منطق البرنامج.
{alertInfo} الفكرة ببساطة: التغليف يعني أن تجعل الكلاس مسؤولًا عن بياناته وقواعدها، وأن تعرض للخارج واجهة واضحة بدل السماح لبقية البرنامج بالاعتماد على كل تفاصيل التنفيذ الداخلية.
{getToc} $title={محتوى المقال}
ما هو Encapsulation في بايثون؟
التغليف Encapsulation أحد مفاهيم البرمجة الكائنية. ويعني جمع البيانات والعمليات المرتبطة بها داخل كائن، مع تحديد طريقة واضحة للتعامل مع هذه البيانات بدل ترك كل جزء من البرنامج يعدلها بالطريقة التي يريدها.
لنأخذ مثالًا بسيطًا لحساب بنكي:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account = BankAccount("Ali", 1000)
account.balance = -5000
print(account.balance)
الكود يعمل، لكن من ناحية منطق البرنامج لدينا مشكلة: أي جزء من المشروع يستطيع وضع قيمة سالبة غير منطقية في الرصيد.
التغليف يساعدنا على نقل قاعدة «الرصيد لا يتغير إلا عبر عمليات صحيحة» إلى داخل الكلاس نفسه، بدل الاعتماد على أن كل مبرمج سيتذكرها في كل مكان.
ما الفائدة الحقيقية من التغليف؟
- حماية قواعد البيانات الداخلية للكائن من التعديلات غير المنطقية.
- تقليل اعتماد بقية المشروع على تفاصيل التنفيذ.
- تغيير طريقة التخزين الداخلية لاحقًا دون كسر كل الكود الخارجي.
- جعل واجهة الكلاس أوضح: ماذا يسمح للمستخدم أن يقرأ؟ وماذا يسمح له أن يعدل؟
- وضع التحقق Validation في مكان واحد بدل تكراره في أجزاء البرنامج.
وهذا مهم جدًا في المشاريع الكبيرة. فإذا كان لديك 20 مكانًا يعدل السعر، لا تريد كتابة شرط منع السعر السالب 20 مرة. الأفضل أن يكون للكلاس مسار واحد مسؤول عن تعديل السعر.
قبل التفاصيل: لا يوجد private صارم في بايثون
هذه أهم نقطة في الدرس. بايثون لا تملك متغيرات instance خاصة لا يمكن الوصول إليها من خارج الكائن بشكل مطلق. الأسلوب فيها يعتمد بدرجة كبيرة على الاتفاقيات البرمجية.
| الشكل | مثال | المعنى العملي |
|---|---|---|
| عام Public | name | جزء طبيعي من واجهة الكائن ويمكن استخدامه مباشرة. |
| غير عام بالاتفاق | _balance | الشرطة السفلية الواحدة تقول: هذا تفصيل داخلي، لا تعتمد عليه من الخارج إلا لسبب واضح. |
| Name Mangling | __pin | تغير بايثون الاسم داخليًا لتقليل التعارضات العرضية، لكنها لا تجعله سرًا أو غير قابل للوصول نهائيًا. |
أولًا: ما هو public في بايثون؟
أي attribute عادية بدون underscore في البداية تعتبر جزءًا عامًا من واجهة الكائن في التصميم المعتاد.
class Student:
def __init__(self, name):
self.name = name
student = Student("Sara")
print(student.name)
student.name = "Mona"
print(student.name)
هنا name public. الوصول إليه وتعديله مباشرة سلوك طبيعي إذا كان هذا ما صممت الكلاس للسماح به.
متى يكون public هو الاختيار الصحيح؟
استخدم attribute عامة عندما تكون القيمة بسيطة، ولا توجد قواعد معقدة عند قراءتها أو تغييرها، ولا تحتاج إلى إخفاء طريقة تخزينها.
class Book:
def __init__(self, title):
self.title = title
لا توجد فائدة تلقائية من تحويل كل شيء إلى private أو كتابة getter وsetter لكل قيمة. البساطة جزء من كتابة كود جيد.
ثانيًا: ماذا يعني _protected في بايثون؟
عندما يبدأ الاسم بشرطة سفلية واحدة مثل _balance، فإن بايثون لا تمنع الوصول إليه. هذه إشارة اتفاقية للمبرمجين تقول إن هذا الاسم غير عام، وهو من تفاصيل التنفيذ الداخلية وقد يتغير.
class BankAccount:
def __init__(self, balance):
self._balance = balance
account = BankAccount(1000)
print(account._balance)
سيعمل الكود. إذن الشرطة السفلية الواحدة ليست قفلًا.
لكن عندما ترى _balance، ينبغي أن تفهم أن مصمم الكلاس لا يريد منك عادةً الاعتماد على هذه القيمة مباشرة، بل يفضل استخدام methods أو properties مخصصة.
لماذا يسمى أحيانًا protected رغم أنه غير محمي فعليًا؟
في شروحات البرمجة الكائنية يستخدم كثيرون كلمة protected لتقريب الفكرة إلى من يعرف لغات أخرى. لكن في بايثون الأدق أن تقول: non-public by convention.
أي أن الاسم self._balance يقول للمبرمج: «هذه تفاصيل داخلية، لا تستخدمها مباشرة إلا وأنت تعرف ما تفعل». المفسر نفسه لا يمنعك.
مثال أفضل: تعديل الرصيد عبر methods
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("يجب أن يكون مبلغ الإيداع أكبر من صفر")
self._balance += amount
def get_balance(self):
return self._balance
account = BankAccount("Ali", 1000)
account.deposit(250)
print(account.get_balance())
النتيجة 1250. التحسين هنا أن للكلاس واجهة واضحة لإدارة الرصيد، ويمكن وضع قواعد التحقق داخلها.
ثالثًا: ماذا يعني __private في بايثون؟
عندما تكتب اسمًا يبدأ بشرطتين سفليتين مثل __pin داخل class، تطبق بايثون عملية اسمها Name Mangling.
class Account:
def __init__(self):
self.__pin = 1234
account = Account()
إذا حاولت account.__pin ستحصل عادةً على AttributeError لأن الاسم الفعلي داخل الكائن تغير.
ما هو Name Mangling؟
عند استخدام __pin داخل الكلاس Account، تغير بايثون الاسم داخليًا تقريبًا إلى:
_Account__pin
ويمكنك رؤية ذلك عبر __dict__:
print(account.__dict__)
الهدف الأساسي من Name Mangling هو تقليل التعارض العرضي للأسماء، خصوصًا مع الوراثة، وليس إنشاء نظام أمان يخفي أسرار البرنامج.
هل يمكن الوصول إلى __private من الخارج؟
نعم، إذا عرفت الاسم بعد Name Mangling يمكنك الوصول إليه:
print(account._Account__pin)
ولهذا لا تستخدم __name لتخزين كلمة مرور أو مفتاح API ثم تعتبره محميًا. هذه ليست آلية أمنية.
{alertWarning} تنبيه مهم: استخدام شرطتين سفليتين لا يعني أن البيانات أصبحت سرية أو غير قابلة للوصول. لا تعتمد على Encapsulation لحماية أسرار أمنية داخل برنامج العميل.
متى يكون __name مفيدًا فعلًا؟
يصبح مفيدًا عندما تريد تقليل احتمال أن يعرّف subclass اسمًا داخليًا بالصدفة فيتعارض مع اسم تستخدمه الكلاس الأب.
class Base:
def __init__(self):
self.__value = "base"
def show_base(self):
print(self.__value)
class Child(Base):
def __init__(self):
super().__init__()
self.__value = "child"
def show_child(self):
print(self.__value)
obj = Child()
obj.show_base()
obj.show_child()
سيظهر base ثم child لأن اسم الأب واسم الابن تم تغييرهما إلى اسمين داخليين مختلفين.
الفرق بين _name و __name
| الاسم | ماذا تفعل بايثون؟ | متى تستخدمه؟ |
|---|---|---|
name | لا تغيير خاص. | عندما يكون جزءًا عامًا من واجهة الكائن. |
_name | لا تمنع الوصول ولا تغير الاسم. | للدلالة على أنه تفصيل داخلي غير عام. |
__name | تطبق Name Mangling داخل تعريف الكلاس. | عندما تريد تقليل تعارض الاسم مع subclasses، وليس لمجرد «إخفاء» البيانات. |
هل أستخدم getter وsetter مثل اللغات الأخرى؟
يمكنك ذلك، لكن بايثون توفر أسلوبًا أنظف في كثير من الحالات باستخدام @property.
ما هي @property في بايثون؟
@property تحول method إلى واجهة قراءة تشبه attribute.
class Product:
def __init__(self, price):
self._price = price
@property
def price(self):
return self._price
product = Product(50)
print(product.price)
نتعامل مع price كأنها attribute، رغم أن القراءة تمر فعليًا عبر method.
إضافة setter للتحقق من القيمة
class Product:
def __init__(self, price):
self.price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("السعر لا يمكن أن يكون سالبًا")
self._price = value
الآن أي تعديل عام عبر product.price = ... يمر عبر الـsetter ويخضع للتحقق.
لماذا كتبنا self.price داخل __init__ بدل self._price؟
لأن self.price = price يجعل القيمة الأولية تمر أيضًا عبر الـsetter، وبالتالي يطبق عليها شرط التحقق نفسه من لحظة إنشاء الكائن.
مثال خاطئ: التحقق عند الإنشاء فقط
class Product:
def __init__(self, price):
if price < 0:
raise ValueError("السعر غير صحيح")
self.price = price
الإنشاء صحيح، لكن يمكن لاحقًا كتابة product.price = -500. إذا كان السعر يجب أن يبقى غير سالب طوال حياة الكائن، فضع التحقق في المسار الذي يمر منه كل تعديل.
Property للقراءة فقط
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
هنا area تُحسب عند الطلب ولا تحتاج إلى التخزين، ولا نعرّف لها setter.
Encapsulation لا تعني كتابة properties لكل شيء
إذا كانت القيمة public وبسيطة ولا توجد قاعدة خاصة عند تعديلها، فهذه الكتابة كافية:
self.name = name
استخدم property عندما تحتاج فعلًا إلى منطق إضافي أو واجهة مستقرة فوق تنفيذ داخلي قابل للتغيير.
ميزة مهمة: تغيير التنفيذ دون تغيير طريقة الاستخدام
يمكن أن تبدأ بـattribute عامة مثل user.age، ثم تحولها لاحقًا إلى property للتحقق من القيم مع بقاء طريقة الاستخدام الخارجية نفسها.
class User:
def __init__(self, age):
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("العمر لا يمكن أن يكون سالبًا")
self._age = value
الكود الخارجي يظل يستخدم user.age، وهذه إحدى أقوى فوائد التغليف.
مثال عملي كامل: BankAccount بتغليف أوضح
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("الرصيد الأولي لا يمكن أن يكون سالبًا")
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("مبلغ الإيداع يجب أن يكون أكبر من صفر")
self._balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("مبلغ السحب يجب أن يكون أكبر من صفر")
if amount > self._balance:
raise ValueError("الرصيد غير كافٍ")
self._balance -= amount
account = BankAccount("Ali", 1000)
account.deposit(300)
account.withdraw(200)
print(account.owner)
print(account.balance)
الكود الخارجي لا يحتاج إلى معرفة كيف يخزن الحساب الرصيد. يعرف فقط أن لديه balance للقراءة، وdeposit() وwithdraw() لتنفيذ العمليات.
لماذا لم نجعل balance لها setter؟
لأننا لا نريد السماح بتعيين الرصيد مباشرة؛ الرصيد يجب أن يتغير بسبب عملية واضحة مثل إيداع أو سحب. لذلك جعلنا property للقراءة فقط.
هل يستطيع المستخدم تعديل _balance مباشرة رغم ذلك؟
نعم، تقنيًا يمكنه ذلك. وهذا يوضح فلسفة بايثون: الاتفاقية تقول إن _balance تفصيل داخلي ولا ينبغي التعامل معه مباشرة من الاستخدام العادي.
الفرق بين Encapsulation وAbstraction
| المفهوم | السؤال الذي يجيب عنه | مثال |
|---|---|---|
| Encapsulation | كيف أنظم البيانات وقواعد الوصول والتعديل داخل الكائن؟ | تعديل الرصيد عبر deposit() وwithdraw(). |
| Abstraction | ما التفاصيل التي يحتاج المستخدم معرفتها ليستخدم الكائن؟ | المستخدم يطلب withdraw(100) دون معرفة كل خطوات تحديث الرصيد داخليًا. |
الفرق بين Encapsulation وInheritance
الوراثة تتعلق بعلاقة بين الكلاسات، أما التغليف فيتعلق بكيفية إدارة الكلاس لبياناته وواجهته. يمكنك استخدام التغليف بدون أي وراثة.
الفرق بين Encapsulation وPolymorphism
تعدد الأشكال يسمح باستخدام استدعاء موحد مع أنواع مختلفة من الكائنات. التغليف يركز على حدود كل كائن وكيفية التعامل مع حالته الداخلية.
أخطاء شائعة عند تعلم Encapsulation
1. الاعتقاد أن __private لا يمكن الوصول إليه مطلقًا
الشرطتان السفليتان تؤديان إلى Name Mangling، وليستا نظام أمان.
2. استخدام __ لكل attribute
هذا يجعل الكود أعقد دون فائدة. استخدم الاسم العام عندما يكون عامًا، وشرطة واحدة للأجزاء غير العامة، وdouble underscore عند الحاجة الفعلية إلى Name Mangling.
3. كتابة getter وsetter بلا منطق
إذا لم تكن هناك قاعدة أو حاجة خاصة، فقد تكون attribute عامة أبسط وأنسب.
4. تعديل _attribute مباشرة رغم وجود واجهة مخصصة
إذا صممت الكلاس بحيث يتم تعديل الرصيد عبر methods واضحة، فلا تجعل بقية المشروع يكتب القيمة الداخلية مباشرة.
5. وضع التحقق في مسار وترك مسار آخر يتجاوزه
إذا كان السعر لا يقبل قيمة سالبة، تأكد أن كل تعديل عام يمر عبر قاعدة التحقق نفسها.
6. الخلط بين حماية المنطق والحماية الأمنية
Encapsulation تساعد على تنظيم البرنامج، لكنها لا تشفر البيانات ولا تمنع كودًا لديه وصول إلى الكائن من فحص تفاصيله.
متى أستخدم property؟
- عندما تحتاج إلى التحقق من القيمة عند التعديل.
- عندما تحسب قيمة عند القراءة بدل تخزينها.
- عندما تريد قيمة read-only.
- عندما تريد الحفاظ على واجهة attribute مع تغيير التنفيذ الداخلي.
متى لا أحتاج property؟
إذا كانت attribute بسيطة ولا توجد قواعد أو حسابات أو سبب لإخفاء التنفيذ، فالوصول المباشر قد يكون أوضح.
مثال آخر: درجة طالب مع Validation
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not 0 <= value <= 100:
raise ValueError("الدرجة يجب أن تكون بين 0 و100")
self._score = value
بهذا تبقى قاعدة الدرجة داخل الكلاس بدل تكرارها في كل مكان يستخدم الطالب.
تمرين عملي
أنشئ class باسم Temperature تخزن درجة الحرارة في _celsius، وتوفر property باسم celsius تمنع أي قيمة أقل من -273.15، وproperty للقراءة فقط باسم fahrenheit.
حل مقترح
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("القيمة أقل من الصفر المطلق")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
قائمة فحص سريعة لفهم Encapsulation
- هل تعرف أن public هو الشكل الطبيعي للـattributes العامة؟
- هل تفهم أن
_nameconvention لعضو غير عام وليس حماية إجبارية؟ - هل تعرف أن
__nameيفعّل Name Mangling؟ - هل تعرف أن Name Mangling ليس نظام أمان؟
- هل تستطيع استخدام
@propertyللقراءة؟ - هل تستطيع استخدام
@x.setterللتحقق من القيمة؟ - هل تضع قواعد صحة البيانات داخل الكلاس بدل تكرارها خارجه؟
روابط داخلية مفيدة من بايثون العرب
- كورس أساسيات بايثون للمبتدئين من الصفر
- أساسيات بايثون 34: شرح class وobject للمبتدئين
- أساسيات بايثون 35: الفرق بين Class Attributes وInstance Attributes
- أساسيات بايثون 36: شرح الوراثة Inheritance للمبتدئين
- أساسيات بايثون 37: شرح تعدد الأشكال Polymorphism للمبتدئين
مصادر رسمية للتوسع
- شرح المتغيرات غير العامة وName Mangling في توثيق بايثون الرسمي
- توثيق property الرسمي في بايثون
- دليل الكلاسات في توثيق بايثون الرسمي
الخلاصة
التغليف Encapsulation في بايثون لا يعني بناء جدار لا يمكن تجاوزه حول بيانات الكائن. معناه العملي هو تصميم حدود واضحة: ما الذي يعتبر جزءًا عامًا من واجهة الكلاس، وما الذي يعتبر تفصيلًا داخليًا، وما الطرق الصحيحة لقراءة البيانات أو تغييرها.
الاسم العادي مثل name مناسب للواجهة العامة. والاسم الذي يبدأ بشرطة سفلية واحدة مثل _balance يعبّر عن عضو غير عام بالاتفاق. أما الاسم الذي يبدأ بشرطتين مثل __pin فيفعّل Name Mangling لتقليل التعارضات العرضية، وليس لمنع الوصول كليًا.
وعندما تحتاج إلى تنفيذ منطق عند القراءة أو التعديل، توفر @property وsetter طريقة أنيقة للحفاظ على واجهة بسيطة مثل product.price مع وضع التحقق داخل الكلاس.
{alertSuccess} القاعدة المهمة: لا تجعل كل attribute private لمجرد أن التغليف موجود. صمم واجهة الكلاس بأبسط شكل يضمن صحة البيانات ويخفي فقط التفاصيل التي لا ينبغي للكود الخارجي الاعتماد عليها.
أسئلة شائعة
ما هو Encapsulation في بايثون؟
هو تنظيم بيانات الكائن والعمليات المرتبطة بها داخل الكلاس، مع توفير واجهة واضحة للتعامل معها بدل الاعتماد المباشر على كل تفاصيل التنفيذ الداخلية.
ما الفرق بين public وprotected وprivate في بايثون؟
الاسم العام مثل name يستخدم كجزء طبيعي من الواجهة. الاسم _name يشير بالاتفاق إلى عضو غير عام. أما __name فيفعّل Name Mangling، لكنه لا يجعل المتغير مغلقًا تمامًا.
هل يوجد private حقيقي في بايثون؟
لا يوجد private instance variable لا يمكن الوصول إليه من الخارج بشكل مطلق. بايثون تعتمد على الاتفاقيات، وdouble underscore تستخدم Name Mangling أساسًا لتقليل تعارضات الأسماء.
ما معنى الشرطة السفلية الواحدة _ قبل اسم المتغير؟
تعني عادةً أن الاسم تفصيل داخلي وغير عام في واجهة الكلاس. المفسر لا يمنع الوصول إليه.
ماذا تفعل الشرطتان __ قبل الاسم؟
تجعلان بايثون تطبق Name Mangling على الاسم داخل تعريف الكلاس، فيتحول مثلًا __value داخل Account إلى اسم قريب من _Account__value.
هل __private آمنة لتخزين كلمات المرور؟
لا. Name Mangling ليست آلية أمنية، ويمكن الوصول إلى الاسم الداخلي بطرق مختلفة.
ما هي property في بايثون؟
هي أداة تسمح لك بتقديم method كأنها attribute، ويمكن ربطها بـsetter لتنفيذ تحقق عند تعديل القيمة.
متى أستخدم @property؟
عندما تحتاج إلى التحقق من القيمة، أو حسابها عند الطلب، أو جعلها للقراءة فقط، أو الحفاظ على واجهة attribute مع تغيير التنفيذ الداخلي.
هل يجب استخدام getter وsetter لكل attribute؟
لا. إذا كانت attribute بسيطة ولا توجد قواعد خاصة، فقد يكون الوصول المباشر أوضح.
ما الفرق بين Encapsulation وInheritance؟
التغليف يتعلق بإدارة بيانات الكائن وواجهته، بينما الوراثة تتعلق بإنشاء class جديدة مبنية على class أخرى وعلاقة الأنواع ببعضها.



