13  التجريد

ومن الخصائص الممتعة في بناء البرمجيات: العلاقات بين المجردات في المستويات المختلفة من التجريد. وسنمثل لذلك بـ:

  1. التعميم: بحيث يكون النوع غير مقيَّد بحالة معيَّنة، بل يُخصص عند التعيين.
  2. التركيب: حيث نعرِّف صِنف المضلَّع أنه مركَّب من سلسلة + نقاط ونركِّب من عوامل اللغة ما يجعله يشبه المجموعات
  3. التخصيص: حيث نخفي أفعال السلسلة (الحذف والإضافة والتعديل ونحوها) وراء أفعال محددَّة في نوع جديد نسميه الطابور
  4. التسلسل: وهو النوع الذي يرتبط بمثله. كالشجرة، فروعها أصولٌ لفروع، وهكذا يتسلسل حتى نصل إلى الثمار.

13.1 التعميم

نريد أن نعمم نوع المتجه بحيث لا يتقيد بعنصرين x,y بل يقبل أن يكون أكثر من ذلك:

class Vector:
    def __init__(self, *args):
        self.elements = args
    
    def __add__(self, other):
        result = []
        if isinstance(other, Vector):
            for i in range(len(self.elements)):
                result.append(self.elements[i] + other.elements[i])
        else:
            for i in range(len(self.elements)):
                result.append(self.elements[i] + other)        

        return Vector(*result)
    
    def __radd__(self, other):
        return self + other
    
    def __repr__(self):
        return f"Vector({self.elements})"
  • لاحظ استعمال النجمة في فعل الإنشاء *args وذلك لتكون عدد العناصر غير مقيدة بحد
  • وفي __add__ لنا حالتان: ما إذا كان المدخل عبارة عن متجهًا أو عددًا
  • أما السطر الأخير منها: return Vector(*result) فعلامة النجمة تفك القائمة بحيث تمرر عناصرها كمعيَّنات منفصلة لفعل الإنشاء الذي تم تعريفه في الأعلى.
v1 = Vector(10, 20, 30)
v2 = Vector(60, 50, 40)

v1 + v2
Vector((70, 70, 70))
v1 + 10
Vector((20, 30, 40))
10 + v1
Vector((20, 30, 40))

13.2 التركيب

لو كان عندنا تعريف نوع نقطة ..

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
    
    @staticmethod
    def distance(p1, p2):
        return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5

يمكننا تركيب شيء مكون من النقاط، وهو المضلَّع، على النحو التالي:

  • يأخذ فعل الإنشاء __init__ سلسلة من النقاط ويعيِّنُها لمتغير اسمه points
  • يعرض فعل التمثيل __repr__ كلمة Polygon وداخل أقواسها يضع تمثيل النقاط
  • لاحظ أن فعل perimeter يُنتج قُطر المضلَّع؛ وهو مجموع المسافات بين النقاط المتوالية
class Polygon:
    def __init__(self, points):
        self.points = points

    def __repr__(self):
        return f"Polygon({self.points})"

    def perimeter(self):
        n = len(self.points)
        s = 0
        for i in range(n):
            s += Point.distance(self.points[i], self.points[(i+1)%n])
        return s

لاحظ أن إنشاء المضلَّع يتطلَّب إنشاء نقاط. ولاحظ أننا جعلنا كل نقطة في سطر، لكنها في الحقيقة كلها عناصر لقائمة، وهو واضحٌ بملاحظة القوسين المربعين [ ]:

poly = Polygon([
    Point(0, 0),
    Point(5, 0),
    Point(5, 5),
    Point(0, 5),
])
poly
Polygon([Point(0, 0), Point(5, 0), Point(5, 5), Point(0, 5)])

والآن يمكن استعمال فعل حساب المحيط:

poly.perimeter()
20.0

ولأن هذا المركَّب مكوَّن من أفراد، فنريده أن يُشابه الأنواع الحاوية، التي لها أفعال معيَّنة. وذلك ممكنٌ باستعمال الأفعال المخصصة:

  • العد: len(s) يُسمَّى فعله: __len__
  • الإشارة: s[i] يُسمَّى فعله: __getitem__
  • التعيين: s[i] = p يُسمَّى فعله: __setitem__
  • الحذف: del s[i] يُسمَّى فعله: __delitem__

انظر التوثيق الرسمي لبايثون حول محاكاة أنواع الحاويات لمزيد من المعلومات.

class Polygon:
    def __init__(self, points):
        self.points = points

    def __repr__(self):
        return f"Polygon({self.points})"

    def __len__(self):
        return len(self.points)
    
    def __getitem__(self, i):
        return self.points[i]

    def __setitem__(self, i, p):
        self.points[i] = p

    def __delitem__(self, i):
        del self.points[i]
    
    def perimeter(self):
        n = len(self.points)
        s = 0
        for i in range(n):
            s += Point.distance(self.points[i], self.points[(i+1)%n])
        return s
poly = Polygon([
    Point(0, 0),
    Point(5, 0),
    Point(5, 5),
    Point(0, 5),
])
poly
Polygon([Point(0, 0), Point(5, 0), Point(5, 5), Point(0, 5)])

والآن نستطيع عدَّ النقاط بالفعل المعروف len الذي بدوْره يستدعي الفعل المتعلق بالشيء __len__ (فُهما سواء):

assert len(poly) == poly.__len__()
len(poly)
4

بتعريفنا لفعل معرفة الطول، انسجم هذا النوع مع المجموعات مثل القائمة ومجموعة الفرائد والروابط والسلسلة النصية وغيرها.

وقد تتساءل: هل أي كلمة أكتبها بين الشرطتين السفليتين استطيع استعماله مثل len؟ والجواب: للأسف لا؛ وإنما هي أسماء مخصوصة.

أما قبول الفعل len لأنواع مغايرة عن بعضها فتسمَّى خاصيَّة تعدد الأشكال (Polymorphism): أي أن الفعل الواحد يقبل أنواع متعددة في الحقيقة لكنَّها تُظهر نفس السلوك الخارجي. بعبارة أخرى نقول:

  • الظاهر: المدخلات والمخرجات واحدة
  • الحقيقة: الخوارزمية (التفاصيل) قد تختلف اختلافًا جذريًّا !

وهذا كثيرٌ في البرمجيات.

ثم الإشارة برقم أو بشريحة:

print(poly[0])
print(poly[-1])
print(poly[1:3])
print(poly[::-1])
Point(0, 0)
Point(0, 5)
[Point(5, 0), Point(5, 5)]
[Point(0, 5), Point(5, 5), Point(5, 0), Point(0, 0)]

ثم التعيين:

poly[0] = Point(10, 10)
poly
Polygon([Point(10, 10), Point(5, 0), Point(5, 5), Point(0, 5)])

وأخيرًا الحذف:

del poly[0]
poly
Polygon([Point(5, 0), Point(5, 5), Point(0, 5)])

تبين بهذا محاكاة بعض أفعال الحاويات الأساسية مثل list وset وtuple وstr وdict. وقد تقدَّم معنا أن النقطة تستطيع محاكاة الأنواع العددية باستعمال الفعل المخصص للجمع + الذي هو __add__.

13.3 التخصيص

يكثر استعمال الطابور كهيكل بيانات لحل مجموعة من المسائل بطريقة سهلة. نستعرض في هذا المثال كيفية تخصيص القائمة (list) ذات الأفعال الكثيرة من الحذف والإضافة والتعديل، لنوع له أفعال مختلفة لكنها مبنية عليها.

الطابور يُعرَّف بفعلين أساسيين:

  • الإدخال من جهة
  • الإخراج من الجهة المقابلة (بحيث يكون ما دخل أولاً هو الذي يخرج أولاً)

فهو بهذا أخص من القائمة التي تزيد عليه: الإشارة لعنصر برقمٍ وإمكان تعديله أو حذفه أو الإضافة في الوسط، وغيرها.

فهذا مثال لكتابة نوع الطابور:

class Queue:
    def __init__(self):
        self.items = []

    def enqueue(self, item):
        """Add an item to the end of the queue"""
        self.items.append(item)

    def dequeue(self):
        """Remove an item from the front of the queue"""
        if not self.is_empty():
            return self.items.pop(0)
        return None

    def peek(self):
        """Return the item at the front without removing it"""
        if not self.is_empty():
            return self.items[0]
        return None

    def is_empty(self):
        """Check if the queue is empty"""
        return len(self.items) == 0
    
    def __len__(self):
        return len(self.items)

    def __repr__(self):
        """Return the underlying list representation"""
        return repr(self.items)
  • لاحظ أن فعل الإنشاء __init__ يعيِّن قائمة فارغة للشيء self
  • ثم الفعل enqueue يتم بواسطة فعل القائمة append
  • والفعل dequeue يتم بواسطة فعل القائمة pop
  • وكذلك peek باستعمال الإشارة بالرقم للعنصر في الموضع 0
  • ثم is_empty بالنظر لطول القائمة بالفعل len

نختبر ذلك الآن:

q = Queue()

q.enqueue(1)
q.enqueue(2)
q.enqueue(3)

print(q)
[1, 2, 3]

نزيل عُنصرًا:

q.dequeue()
print(q)
[2, 3]

هل يمكننا معرفة عدد العناصر؟:

len(q)
2

صحيح أن القائمة أكثر قدرة (إذْ تتضمن عمليات أكثر)، إلا أن الطابور مخصص لخدمة وظيفة محددة.

13.4 التسلسل

ومن الأنواع ما يتسليل مثل الشجرة، حيث فروعها قد تكون أصولاً لفروع أخرى. وهكذا يتسلسل الأمر حتى نصل إلى الأوراق / الثمار (القيم الأخيرة).

ومثل هذا له استعمالات كثيرة في هيكلة البيانات بأمثل بنية تكون فيها أسرع في البحث والإدراج والحذف ونحو ذلك. لكننا نكتفي بمثال نتأمله، نعرف فيه الشجرة الثنائية:

class BinaryTreeNode:
    def __init__(self,
        value,
        left: 'BinaryTreeNode' = None,
        right: 'BinaryTreeNode' = None):
        self.value = value
        self.left = left
        self.right = right
    
    def __repr__(self):
        if self.left is None and self.right is None:
            return f"{self.value}"
        return f"({self.left}) <-- [{self.value}] --> ({self.right})"
tree = BinaryTreeNode(10)
tree.left = BinaryTreeNode(5)
tree.right = BinaryTreeNode(15)

tree
(5) <-- [10] --> (15)
tree.left.left = BinaryTreeNode(3)
tree.left.right = BinaryTreeNode(7)
tree.right.left = BinaryTreeNode(13)
tree.right.right = BinaryTreeNode(17)

tree
((3) <-- [5] --> (7)) <-- [10] --> ((13) <-- [15] --> (17))

نكتفي بهذا القدر.