20  استمداد الأنواع

تستمد الأنواع بأحد طريقتين:

الأولى: التخصيص؛ كأن تقول المبرَّبع نوع خاص من المستطيلات. وكذلك تقول: المستطيل نوع خاص من الأشكال. وبالتالي فإن علاقة المربَّع بالشكل هي علاقة خاص من عام.

الثاني: التركيب؛ كأن تقول: الإنسان حيّ والجمل حيّ فكلاهما حيّ. وتقول: الجمل مركوب والسيارة مركوبة؛ فكلاهما مركوبٌ. فيكون الجملُ مركَّبًا بالاعتبارين معًا: (مركوبًا حيًّا).

والتصخيص يسمى الوراثة؛ وهو يحتاج لرعاية جميع السلسلة الوراثية ليكون التفريع عنها ذا معنى؛ وذلك صعبٌ ضبطه في الواقع. أما التصنيف الاعتباري فاستعمالها أسهل لكوْن التصنيف المتأخِّر لا يحتاج مراعاة سلسلة معقَّدة أو طويلة من الوراثة.

الوراثة

التخصيص بالوراثة (Inheritence): أن يندرج النوع تحت نوعٍ آخر؛ فهو يستمد منه ويزيد عليه صفةً أو فعلاً (أو أكثر). وهو اسميٌّ لأن المعتبر هو أسماء الأنواع؛ فتقول:

  • الشكل: ما كان له محيط
  • والمستطيل شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: طولًا وعرضًا ومساحة
  • ووالمثلث شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: ثلاثةَ أضلاعٍ ومساحة
  • أما المربع فهو مستطيل (إذًا له محيط لأن المستطيل شكل، وله طول وعرض ومساحة): لكن عرضه وطوله متساويان

وهذه شجرة التوارث للأنواع المذكورة:

flowchart BT
  Shape
  Rectangle -- "is a" --> Shape
  Square -- "is a" --> Rectangle
  Triangle -- "is a" --> Shape

تذكَّر أن علامة النجمة * في وصف متغيرات الإجراء المُنشئ __init__ (*sides) تجعل عدد عناصره لا محدودة؛ وذلك لأننا لا نريد حصر عدد أضلاع الشكل إلا لاحقًا عند تخصيصه؛ ففي المربَّع سيكون أربعة، وفي المثلث يسكون ثلاثة، وهكذا.

class Shape:
    def __init__(self, *sides):
        self.sides = sides
    
    @property
    def perimeter(self):
        return sum(self.sides)
    
    @property
    def area(self):
        pass

وليس ثمة شيء هو شكلٌ فقط، ولذلك نعتبر هذا النوع، نوعًا مُجرَّدًا (Abstract)، إذ لن نستعمله مباشرةً أبدًا، بل سنخصصه أولاً. فأول نوع سيرث منه هو المستطيل (Rectangle).

ولاحظ في Rectangle استعمال الإجراء الخاص super() وهو يشير إلى الموروث Shape؛ فيصير معنى الجملة ( super().__init__()) وكوْنها في أوَّل سطرٍ من جملة إجراء الإنشاء: الإنشاء فوق الإنشاء الموروث.

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__(width, height, width, height)
    
    @property
    def width(self):
        return self.sides[0]
    
    @property
    def height(self):
        return self.sides[1]
    
    @property
    def area(self):
        return self.width * self.height

أما المربع، فهو نوعٌ خاص من المستطيل:

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

وأما المثلث، فهو من الشكل:

class Triangle(Shape):
    def __init__(self, a, b, c):
        super().__init__(a, b, c)

    @property
    def a(self):
        return self.sides[0]
    
    @property
    def b(self):
        return self.sides[1]
    
    @property
    def c(self):
        return self.sides[2]
    
    @property
    def area(self):
        s = self.perimeter / 2
        return (s * (s - self.a) * (s - self.b) * (s - self.c))**0.5

والآن ستلاحظ إمكانية استعمال الشيئين المختلفين (المثلث والمستطيل) باعتبار المشترك بينهما (الشكل). ويتبين ذلك إذا كررنا عليهما في قائمة:

x = Triangle(10, 10, 10)
y = Rectangle(10, 20)
shapes = [x, y]

for sh in shapes:
    print(sh.__class__.__name__)
    print("Perimeter:", sh.perimeter)
    print("Area:", round(sh.area, 2))
    print('='*40)
Triangle
Perimeter: 30
Area: 43.3
========================================
Rectangle
Perimeter: 60
Area: 200
========================================

واستعمال instance.__class__.__name__ يعطي اسم النوع الذي ينتمي إليه الشيء.

لكنهما يفترقان في بعض الصفات إذ:

  • المستطيل له طول وعرض
  • المثلث له ثلاثة أضلاع

ويمكن فحص النوع باستعمال الإجراء isinstance(instance, class) لمعرفة ما إذا كان الشيء ينتمي إلى ذلك النوع أو لا.

for sh in shapes:
    if isinstance(sh, Rectangle):
        print(f"Sides: width={sh.width}, height={sh.height}")
    elif isinstance(sh, Triangle):
        print(f"Sides: a={sh.a}, b={sh.b}, c={sh.c}")
Sides: a=10, b=10, c=10
Sides: width=10, height=20

وذلك ينطبق في تعريف الإجراءات. فإنك تستطيع تحديد النوع الأعم وتمرير النوع الأخص.

فهو في التعريف عام:

def show(sh: Shape):
    print(sh.__class__.__name__)
    print("Perimeter:", sh.perimeter)
    print("Area:", round(sh.area, 2))

وفي التمرير خاص:

x = Triangle(10, 10, 10)
show(x)

y = Rectangle(10, 20)
show(y)
Triangle
Perimeter: 30
Area: 43.3
Rectangle
Perimeter: 60
Area: 200

أمثلة على الوراثة

وتجد في شجرة المجموعات شكل 1 أن المجموعات المتغيرة نوع مخصص من الأنواع الجامدة:

  • القائمة (list) مخصصة من التسلسل المتغير (MutableSequence) الذي هو من نوع التسلسل (Sequence).
  • المجموعة (set) مخصصة من المجموعة المتغيرة (MutableSet) التي هي من نوع المجموعة (Set).
  • القاموس (dict) مخصص من المقابلة المتغيرة (MutableMapping) الذي هو من نوع المقابلة (Mapping).

التركيب

التركيب (Composition) ويسمى بالإنجليزية أحيانًا (Duck Typing) إشارة إلى العبارة: “إذا كان الشيء يمشي مثل البطة، ويبطبط مثلها؛ فإنه يعامَل كالبطة!”.

ومثال التركيب في نفس شجرة المجموعات في بايثون شكل 1 هو نوع الجمع (Collection)، حيث له ثلاثة اعتبارات:

  1. الجمع ذو أعضاء: كونه يقبل العضويَّة: x in s وفعلها هو: __contains__؛ وبذلك يصنَّف أنه حاوٍ (Container)
  2. الجمع ذو تكرار: كونه يقبل التكرار: for x in s وفعله هو: __iter__؛ وذبك يصنف أنه مكرَّر (Iterable)
  3. الجمع ذو عدد: كونه يقبل العد: len(s) وفعله هو: __len__؛ يصنف أنه محجَّم (Sized)

وكذلك التسلسل (Sequence)؛ فهو يقبل العكس، وفعلها المخصص هو: __reversed__؛ فبهذا الاعتبار هو من النوع القابل للعكس (Reversible).

النوع المجرَّد

تعرَّف عناصر المركبات على أنها أنواع مجرَّدة (Abstract Base Classes) ليس لها وجود في ذاتها، لكن ينتمي إليها النوع الذي يحتوي على إجراءاتها.

تأمل المثال المأخوذ من تفاصيل المكتبة الأساسية في بايثون نفسها:

from abc import abstractmethod

class Sized:

    @abstractmethod
    def __len__(self) -> int:
        return 0
    
def len(sized: Sized) -> int:
    return sized.__len__()
  • عرفنا النوع المجرَّد Sized (مُحجَّم) بأن له فعلاً واحدًا مجرَّدًا: __len__ وذلك لوجود المعدِّل @abstractmethod حكمنا أنه مجرد.
  • عرفنا إجراءً ليس في داخل هذا الصنف، بل هو إجراء عام كأي إجراء، وجعلنا عامله يكون من جنس ذلك المجرَّد: Sized
    • وهو بالتالي يقوم باستدعاء الفعل المخصص __len__ للشيء الذي يمرر إليه.

بهذه الطريقة فإننا حين نعرِّف أي صنف جديد ونمرره إلى ذلك الإجراء، فإنَّه يبحث عن الفعل __len__ فيه، ويقوم باستدعائه. وتسمى هذه الخاصيَّة تعدد الأشكال (Polymorphism): أي أن الإجراء الواحد يقبل أنواع مختلفة لوجود فعل مشترك بينها.

ويكثر في لغة (جو: Golang) المجرَّداتُ ذات الإجراء الواحد أو الإجراءين. لأننا نريد أشياء كثيرة تنتمي بحسب ما يكون فيها من إجراءات.

تكوين نوع مركَّب

ونمثل لذلك بصنف المتجَّه على النحو التالي:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __len__(self):
        return 2

فبتعريفنا لفعل معرفة الطول (__len__)، يُعتبر المتجَّه من جنس المحجَّمات (Sized) ولذلك يقبل تمريره إلى إجراء العد (len(Sized)). وانضم بذلك إلى المجموعات مثل القائمة (list) والمجموعة (set) وغيرهما. ولاحظ عدم وجود ذكر للنوع Sized البتة، بل المُعتَبَرُ وجود الإجراء __len__ حتى يُقبل الشيء في len.

فحين ننشيئ معيَّنًا من هذا الصنف، ثم نمرره للإجراء len فإنه يتصرف كغيره من الجموع:

v = Vector(10, 20)
print(len(v))

xs = [10, 20, 30]
print(len(xs))
2
3

ولتحقق أكثر، لاحظ أنك تستطيع استعمال الفعل المخصوص نفسه (بالشرطتين):

assert len(v) == v.__len__()
assert len(xs) == xs.__len__()

والفعل الذي يكون اسمه محصورًا بشرطتين سفليتين نحو: __add__ فإنَّه من الأفعال المخصوصة بشيء من التراكيب اللغوية في بايثون. فهذا الفعل -مثلاً- ينوب عنه في لغة بايثون علامة الجمع: +.