الأصناف المجرّدة والواجهات
درس 3 من 5
3الأصناف المجرّدة والواجهات (Abstract Classes & Interfaces)
الصورة الكبيرةكيف تفرض عقدًا على كل من يرث منك، والفرق بين الصنف المجرّد والواجهة، ولماذا يرفض بايثون إنشاء كائن من صنف مجرّد.
ماذا تشاهد في الرسم: Cash يكتب pay فيُصنع، وBroken ينساها فيرفضه بايثون بـTypeError قبل أن يُستخدم. وWallet لا يرث أصلًا لكنه يملك pay فيعمل حيث تُطلب.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الصنف المجرّد مثل عقد عمل: يكتب «كل موظف يجب أن يقدّم تقريرًا أسبوعيًا» دون أن يكتب التقرير نفسه. لا تستطيع أن توظّف «العقد»، بل موظفًا حقيقيًا وقّع عليه والتزم بما فيه. هكذا تضمن أن كل من يأتي لاحقًا يملك ما تحتاجه.
ABC و@abstractmethod من وحدة abc يجعلان التابع إلزاميًا، وبايثون يرفض إنشاء كائن من صنف لم ينفّذ كل التوابع المجرّدة. والواجهة عقد بلا تنفيذ، وفي بايثون يُعبّر عنها أيضًا بـtyping.Protocol الذي يعتمد على الشكل duck typing لا على الوراثة.
قبل أن تبدأ
🧬درس الوراثة
تعرف كيف يعيد الابن تعريف طريقة أبيه.
📝فكرة العقد
اتفاق مكتوب: من يوقّع يلتزم بما فيه.
الفكرة الجوهرية
أحيانًا تريد أن تقول: كل شكل يجب أن يعرف مساحته، لكن لا أعرف كيف يحسبها كل شكل. هذا الجزء عن كتابة العقد قبل التنفيذ، بثلاثة أمثلة.
1أب لا يعرف الجواب
صنف Shape يعلن area لكنه يرفع خطأً إن نوديت عليه مباشرة. الأبناء مثل Square يكتبون الحساب الحقيقي.
class Shape:
def area(self):
raise NotImplementedError("كل شكل يحسب مساحته بنفسه")
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
print(Square(3).area())
try:
print(Shape().area())
except NotImplementedError as error:
print("خطأ:", error)
هذا أبسط شكل للعقد: الأب يقول «يجب أن تكتب هذه»، والابن يكتبها. لكن الخطأ يظهر متأخرًا، عند النداء لا عند الإنشاء.
2اجعل العقد إلزاميًّا
ABC وabstractmethod من وحدة abc يجعلان بايثون يمنع إنشاء Shape أصلًا، ويمنع أي ابن لم يكتب area.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return round(3.14159 * self.r * self.r, 1)
print(Circle(2).area())
try:
Shape()
except TypeError:
print("لا يمكن إنشاء شكل مجرّد")
الفرق عن الخطوة السابقة: الخطأ يظهر مبكرًا، لحظة محاولة الإنشاء، وهذا أفضل لأنك تكتشفه قبل أن يصل إلى مستخدم.
3عقد بلا وراثة
Duck وRobot لا يرث أحدهما من الآخر ولا من أب مشترك، لكن كليهما عنده sound، فدالة play تعمل مع كليهما.
class Duck:
def sound(self):
return "كواك"
class Robot:
def sound(self):
return "بيب"
def play(thing):
print(thing.sound())
play(Duck())
play(Robot())
بايثون تهتم بما يستطيع الكائن فعله لا بنوعه. هذا يسمّى «كتابة البط»، وهو السبب في أن الواجهات في بايثون أخفّ مما هي في لغات أخرى.
Child()، والرسالة تذكر اسم الطريقة الناقصة. اقرأها فهي تخبرك بالضبط ماذا تكتب.كيف يعمل، خطوة بخطوة: متى يمنعك بايثون من صنع كائن
الصنف المجرّد يعلن طرقًا بـ@abstractmethod بلا جسم حقيقي.
صنف يرث منه ويكتب بعض الطرق أو كلها.
عند Cash() يفحص بايثون: هل بقيت طريقة مجرّدة بلا تعريف؟
إن بقيت واحدة رفض بـTypeError، وإلا صنع الكائن.
أعمق قليلًا
أحيانًا تريد أن تفرض على كل الأصناف الوارثة تنفيذ ميثود معيّن، دون أن تكتب أنت تنفيذه في الصنف الأب. هنا يأتي دور الأصناف المجرّدة.
Abstract Base Class (ABC)
صنف لا يمكن إنشاء كائن منه مباشرة؛ يُعرَّف بوراثة ABC من وحدة abc.
@abstractmethod
ميثود بلا تنفيذ في الصنف الأب، أي صنف وارث لا ينفّذها يبقى مجرّدًا هو الآخر، ولا يمكن إنشاء كائن منه.
Duck Typing
بايثون لا تفرض وراثة صريحة أحيانًا: "إن كان الكائن يملك الميثود المطلوبة، فهو صالح للاستخدام"، بغض النظر عن أصله.
Mixin
صنف صغير يضيف سلوكًا واحدًا محددًا (كالتسجيل Logging) عبر الوراثة المتعددة، دون أن يمثّل مفهومًا كاملاً بذاته.
الرسم 7: PaymentMethod مجرّد يفرض وجود pay(). LoggedCreditCardPayment يرث من صنفين معًا: السلوك الأساسي + سلوك Mixin إضافي.
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
"""يجب أن ينفّذ كل صنف وارث هذه الميثود بطريقته."""
class CreditCardPayment(PaymentMethod):
def pay(self, amount):
return f"تم الدفع {amount} عبر البطاقة الائتمانية"
class CashPayment(PaymentMethod):
def pay(self, amount):
return f"تم الدفع {amount} نقدًا"
# PaymentMethod() -> TypeError: لا يمكن إنشاء كائن من صنف مجرّد
## Duck Typing: أي كائن يملك pay() صالح، بلا حاجة لوراثة PaymentMethod
class GiftCard:
def pay(self, amount):
return f"تم خصم {amount} من بطاقة الهدية"
def checkout(payment_method, amount):
print(payment_method.pay(amount)) # لا يهمها نوع الكائن، فقط أن لديه pay()
checkout(CreditCardPayment(), 200)
checkout(GiftCard(), 50) # يعمل تمامًا رغم عدم وراثتها من PaymentMethod
## Mixin: إضافة سلوك تسجيل بالوراثة المتعددة
class LoggableMixin:
def log_action(self, msg):
print(f"[LOG] {msg}")
class LoggedCreditCardPayment(CreditCardPayment, LoggableMixin):
def pay(self, amount):
self.log_action(f"محاولة دفع {amount}")
return super().pay(amount)
| الأداة | متى تستخدمها |
|---|---|
Abstract Class + @abstractmethod | تريد فرض عقد صارم: "كل من يرث هذا يجب أن ينفّذ هذه الميثودات" |
| Duck Typing | تريد مرونة قصوى دون ربط الكود بوراثة محددة؛ "يكفي أن يعمل بنفس الطريقة" |
| Mixin | تريد إضافة سلوك واحد قابل لإعادة الاستخدام عبر أصناف غير مرتبطة (تسجيل، مقارنة، تحويل JSON...) |
@abstractmethod فوق ميثود في صنف يرث ABC، بدونها، بايثون تعتبر الصنف عاديًا ويمكن إنشاء كائن منه مباشرة، فيفشل هدف "فرض العقد" بصمت.
اختبر فهمك: لماذا تعمل دالة checkout() مع GiftCard رغم أنها لا ترث من PaymentMethod إطلاقًا؟
بسبب Duck Typing: بايثون لا تتحقق من نوع الكائن أو شجرة وراثته عند استدعاء payment_method.pay(amount)، تتحقق فقط أن الكائن يملك ميثودًا اسمها pay تقبل هذا العدد من المدخلات، وهذا كافٍ لتنجح العملية.
مثال محلول
class Payment(ABC) فيه @abstractmethod def pay(self, amount)، وكل وسيلة صنف يرث منه. كود الدفع يتعامل مع Payment فقط، والوسيلة الجديدة لا تغيّره.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| الصنف المجرّد لا فائدة منه لأنك لا تصنع منه كائنًا. | فائدته أنه يفرض على كل ابن أن يكتب الطرق المطلوبة، فيكتشف بايثون النقص لحظة الإنشاء لا في منتصف التشغيل. |
| بايثون يحتاج وراثة لكي يقبل كائنًا في مكان ما. | يكفي أن يملك الكائن الطريقة المطلوبة؛ هذه بطة بايثون: إن مشى كالبطة وصاح كالبطة فهو بطة. |
| Mixin صنف عادي بلا فرق. | Mixin صنف صغير يضيف قدرة واحدة مثل الطباعة بصيغة JSON، ولا يُستخدم وحده أبدًا بل يُخلط مع أصناف أخرى. |
جرّب بنفسك
صنف Base(ABC) فيه طريقتان مجرّدتان. صنف Child(Base) كتب واحدة فقط. ماذا يحدث عند Child()؟
الصنف يبقى مجرّدًا ما دامت فيه طريقة مجرّدة واحدة لم تُكتب، وبايثون ترفض إنشاء كائن منه برسالة تذكر اسم الطريقة الناقصة. التعريف نفسه يمرّ بلا خطأ، والرفض يحدث عند الإنشاء.
بطاقات مراجعة سريعة
تحقق من فهمك
متى يرفع بايثون TypeError مع صنف مجرّد؟
عند محاولة صنع كائن من صنف ما زالت فيه طريقة مجرّدة بلا تعريف.
ما الفرق بين ABC وبطة بايثون؟
ABC عقد صريح يُفحص عند الإنشاء، والبطة اتفاق ضمني: يكفي وجود الطريقة وقت النداء.
لماذا لا يُستخدم Mixin وحده؟
لأنه يضيف قدرة واحدة ويعتمد على بيانات يوفّرها الصنف الذي يُخلط معه.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي3
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.