flowchart BT Shape Rectangle -- "is a" --> Shape Square -- "is a" --> Rectangle Triangle -- "is a" --> Shape
21 استمداد الأنواع
تستمد الأنواع بأحد طريقتين:
الأولى: التخصيص (Sub-typing) ؛ كأن تقول المبرَّبع نوع خاص من المستطيلات. وكذلك تقول: المستطيل نوع خاص من الأشكال. وبالتالي فإن علاقة المربَّع بالشكل هي علاقة خاص من عام.
الثاني: التركيب (Composition) ؛ كأن تقول: الإنسان حيّ والجمل حيّ فكلاهما حيّ. وتقول: الجمل مركوب والسيارة مركوبة؛ فكلاهما مركوبٌ. فيكون الجملُ مركَّبًا بالاعتبارين معًا: (مركوبًا حيًّا).
الفرق بينهما:
التخصيص يسمى الوراثة؛ وهو يحتاج لرعاية جميع السلسلة الوراثية ليكون التفريع عنها ذا معنى؛ وذلك صعبٌ ضبطه في الواقع. أما التركيب فنوعُ من التصنيف الاعتباري؛ فاستعمالها أسهل لكوْن التصنيف المتأخِّر لا يحتاج مراعاة سلسلة معقَّدة أو طويلة من الوراثة.
الوراثة
التخصيص بالوراثة (Inheritence): أن يندرج النوع تحت نوعٍ آخر؛ فهو يستمد منه ويزيد عليه صفةً أو فعلاً (أو أكثر). وهو اسميٌّ لأن المعتبر هو أسماء الأنواع؛ فتقول:
- الشكل: ما كان له محيط
- والمستطيل شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: طولًا وعرضًا ومساحة
- ووالمثلث شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: ثلاثةَ أضلاعٍ ومساحة
- أما المربع فهو مستطيل (إذًا له محيط لأن المستطيل شكل، وله طول وعرض ومساحة): لكن عرضه وطوله متساويان
وهذه شجرة التوارث للأنواع المذكورة:
تذكَّر أن علامة النجمة *
في وصف متغيرات الإجراء المُنشئ __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):
= self.perimeter / 2
s return (s * (s - self.a) * (s - self.b) * (s - self.c))**0.5
والآن ستلاحظ إمكانية استعمال الشيئين المختلفين (المثلث والمستطيل) باعتبار المشترك بينهما (الشكل). ويتبين ذلك إذا كررنا عليهما في قائمة:
= Triangle(10, 10, 10)
x = Rectangle(10, 20)
y = [x, y]
shapes
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))
وفي التمرير خاص:
= Triangle(10, 10, 10)
x
show(x)
= Rectangle(10, 20)
y show(y)
Triangle
Perimeter: 30
Area: 43.3
Rectangle
Perimeter: 60
Area: 200
أمثلة على الوراثة
وتجد في شجرة المجموعات شكل A.1 أن المجموعات المتغيرة نوع مخصص من الأنواع الجامدة:
- القائمة (
list
) مخصصة من التسلسل المتغير (MutableSequence
) الذي هو من نوع التسلسل (Sequence
). - المجموعة (
set
) مخصصة من المجموعة المتغيرة (MutableSet
) التي هي من نوع المجموعة (Set
). - القاموس (
dict
) مخصص من المقابلة المتغيرة (MutableMapping
) الذي هو من نوع المقابلة (Mapping
).
التركيب
التركيب (Composition) ويسمى بالإنجليزية أحيانًا (Duck Typing) إشارة إلى العبارة: “إذا كان الشيء يمشي مثل البطة، ويبطبط مثلها؛ فإنه يعامَل كالبطة!”.
ومثال التركيب في نفس شجرة المجموعات في بايثون شكل A.1 هو نوع الجمع (Collection
)، حيث له ثلاثة اعتبارات:
- الجمع ذو أعضاء: كونه يقبل العضويَّة:
x in s
وفعلها هو:__contains__
؛ وبذلك يصنَّف أنه حاوٍ (Container
) - الجمع ذو تكرار: كونه يقبل التكرار:
for x in s
وفعله هو:__iter__
؛ وذبك يصنف أنه مكرَّر (Iterable
) - الجمع ذو عدد: كونه يقبل العد:
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
فإنه يتصرف كغيره من الجموع:
= Vector(10, 20)
v print(len(v))
= [10, 20, 30]
xs print(len(xs))
2
3
بل إنك تستطيع استعمال الفعل المخصوص نفسه (بالشرطتين):
assert len(v) == v.__len__()
assert len(xs) == xs.__len__()
والفعل الذي يكون اسمه محصورًا بشرطتين سفليتين نحو: __add__
فإنَّه من الأفعال المخصوصة بشيء من التراكيب اللغوية في بايثون. فهذا الفعل -مثلاً- ينوب عنه في لغة بايثون علامة الجمع: +
وسيأتي بيانه في الفصل التالي: تعريف العوامل.