Kodax

Abstract Classes — البرمجة الكائنية

الأصناف المجردة (Abstract Classes)

أحياناً تريد صنفاً أباً يحدد ما يجب أن يفعله كل ابن، دون أن يكون له معنى إنشاؤه وحده. "شكل" عام ليس له مساحة؛ الدائرة والمربع لهما. الصنف المجرد هو عقد: كل صنف يرث منه ملزم بتنفيذ دوال معينة.

abc و @abstractmethod

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

    def describe(self):              # دالة عادية مشتركة
        return f"المساحة: {self.area():.2f}"

ماذا تضيف فعلياً؟

1. لا يمكن إنشاء الصنف المجرد: Shape() ترمي TypeError.

2. لا يمكن إنشاء ابن لم ينفذ كل الدوال المجردة:

class Circle(Shape):
    pass

Circle()   # TypeError: Can't instantiate abstract class Circle ...

الخطأ يظهر عند الإنشاء وليس لاحقاً عند استدعاء الدالة الناقصة، فتكتشف النسيان مبكراً.

مقارنة مع raise NotImplementedError

في درس تعدد الأشكال كتبنا raise NotImplementedError في دالة الأب. هذا يعمل، لكن الخطأ يظهر فقط عند استدعاء الدالة، وقد يكون ذلك بعد ساعات من التشغيل. ABC تكتشفه فوراً.

الدوال العادية في الصنف المجرد

الصنف المجرد يمكن أن يحتوي دوالاً كاملة تعتمد على المجردة (مثل describe أعلاه): الأب يحدد الخطوات العامة، والأبناء يملؤون التفاصيل.

متى تستخدمه؟

- عندما تكتب إطاراً أو مكتبة وتريد إجبار المستخدمين على تنفيذ دوال معينة.

- عندما يكون لديك عدة تنفيذات لنفس المفهوم (مصادر بيانات: ملف، قاعدة بيانات، API).

Duck typing أم ABC؟

في Python كثيراً ما تكفي duck typing. استخدم ABC عندما يكون وجود العقد بوضوح مهماً، أو عندما تكون الأخطاء الناتجة عن دالة ناقصة مكلفة.

تمرين

أنشئ صنفاً مجرداً Notifier بدالة مجردة send(message) ودالة عادية notify_all(users, message)، ثم EmailNotifier و SmsNotifier. جرّب إنشاء صنف ابن لا ينفذ send ولاحظ الخطأ.

from abc import ABC, abstractmethod

# Abstract Class
class Animal(ABC):
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def speak(self):
        """يجب أن ينفذها كل حيوان"""
        pass
    
    @abstractmethod
    def move(self):
        """يجب أن ينفذها كل حيوان"""
        pass
    
    # دالة عادية (غير مجردة)
    def info(self):
        return f"أنا {self.name}"

# لا يمكن إنشاء Animal مباشرة
# animal = Animal("x")  # TypeError!

class Dog(Animal):
    def speak(self):
        return "هاو هاو!"
    
    def move(self):
        return "يمشي على 4 أرجل"

class Bird(Animal):
    def speak(self):
        return "تغريد!"
    
    def move(self):
        return "يطير بجناحيه"

# الآن يمكن الإنشاء
dog = Dog("بادي")
bird = Bird("تويتي")

print(dog.speak())    # هاو هاو!
print(bird.move())    # يطير بجناحيه
print(dog.info())     # أنا بادي

# واجهة للأشكال
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    @property
    @abstractmethod
    def name(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    @property
    def name(self):
        return "مستطيل"
    
    def area(self):
        return self._width * self._height
    
    def perimeter(self):
        return 2 * (self._width + self._height)

rect = Rectangle(4, 5)
print(f"{rect.name}: المساحة={rect.area()}, المحيط={rect.perimeter()}")

# التحقق
print(isinstance(rect, Shape))  # True
print(isinstance(rect, Rectangle))  # True
اختبر فهمك