flowchart BT Shape Rectangle -- "is a" --> Shape Square -- "is a" --> Rectangle Triangle -- "is a" --> Shape
20 استمداد الأنواع
تستمد الأنواع بأحد طريقتين:
الأولى: التخصيص؛ كأن تقول المبرَّبع نوع خاص من المستطيلات. وكذلك تقول: المستطيل نوع خاص من الأشكال. وبالتالي فإن علاقة المربَّع بالشكل هي علاقة خاص من عام.
الثاني: التركيب؛ كأن تقول: الإنسان حيّ والجمل حيّ فكلاهما حيّ. وتقول: الجمل مركوب والسيارة مركوبة؛ فكلاهما مركوبٌ. فيكون الجملُ مركَّبًا بالاعتبارين معًا: (مركوبًا حيًّا).
والتصخيص يسمى الوراثة؛ وهو يحتاج لرعاية جميع السلسلة الوراثية ليكون التفريع عنها ذا معنى؛ وذلك صعبٌ ضبطه في الواقع. أما التصنيف الاعتباري فاستعمالها أسهل لكوْن التصنيف المتأخِّر لا يحتاج مراعاة سلسلة معقَّدة أو طويلة من الوراثة.
الوراثة
التخصيص بالوراثة (Inheritence): أن يندرج النوع تحت نوعٍ آخر؛ فهو يستمد منه ويزيد عليه صفةً أو فعلاً (أو أكثر). وهو اسميٌّ لأن المعتبر هو أسماء الأنواع؛ فتقول:
- الشكل: ما كان له محيط
- والمستطيل شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: طولًا وعرضًا ومساحة
- ووالمثلث شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: ثلاثةَ أضلاعٍ ومساحة
- أما المربع فهو مستطيل (إذًا له محيط لأن المستطيل شكل، وله طول وعرض ومساحة): لكن عرضه وطوله متساويان
وهذه شجرة التوارث للأنواع المذكورة:
تذكَّر أن علامة النجمة *
في وصف متغيرات الإجراء المُنشئ __init__
(*sides
) تجعل عدد عناصره لا محدودة؛ وذلك لأننا لا نريد حصر عدد أضلاع الشكل إلا لاحقًا عند تخصيصه؛ ففي المربَّع سيكون أربعة، وفي المثلث يسكون ثلاثة، وهكذا.
وليس ثمة شيء هو شكلٌ فقط، ولذلك نعتبر هذا النوع، نوعًا مُجرَّدًا (Abstract)، إذ لن نستعمله مباشرةً أبدًا، بل سنخصصه أولاً. فأول نوع سيرث منه هو المستطيل (Rectangle).
ولاحظ في Rectangle
استعمال الإجراء الخاص super()
وهو يشير إلى الموروث Shape
؛ فيصير معنى الجملة ( super().__init__()
) وكوْنها في أوَّل سطرٍ من جملة إجراء الإنشاء: الإنشاء فوق الإنشاء الموروث.
أما المربع، فهو نوعٌ خاص من المستطيل:
وأما المثلث، فهو من الشكل:
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
وذلك ينطبق في تعريف الإجراءات. فإنك تستطيع تحديد النوع الأعم وتمرير النوع الأخص.
فهو في التعريف عام:
وفي التمرير خاص:
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
)، حيث له ثلاثة اعتبارات:
- الجمع ذو أعضاء: كونه يقبل العضويَّة:
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) المجرَّداتُ ذات الإجراء الواحد أو الإجراءين. لأننا نريد أشياء كثيرة تنتمي بحسب ما يكون فيها من إجراءات.
تكوين نوع مركَّب
ونمثل لذلك بصنف المتجَّه على النحو التالي:
فبتعريفنا لفعل معرفة الطول (__len__
)، يُعتبر المتجَّه من جنس المحجَّمات (Sized
) ولذلك يقبل تمريره إلى إجراء العد (len(Sized)
). وانضم بذلك إلى المجموعات مثل القائمة (list
) والمجموعة (set
) وغيرهما. ولاحظ عدم وجود ذكر للنوع Sized
البتة، بل المُعتَبَرُ وجود الإجراء __len__
حتى يُقبل الشيء في len
.
فحين ننشيئ معيَّنًا من هذا الصنف، ثم نمرره للإجراء len
فإنه يتصرف كغيره من الجموع:
ولتحقق أكثر، لاحظ أنك تستطيع استعمال الفعل المخصوص نفسه (بالشرطتين):
والفعل الذي يكون اسمه محصورًا بشرطتين سفليتين نحو: __add__
فإنَّه من الأفعال المخصوصة بشيء من التراكيب اللغوية في بايثون. فهذا الفعل -مثلاً- ينوب عنه في لغة بايثون علامة الجمع: +
.