12 الأنواع
تتخذ بايثون نموذجًا في البرمجة يسمى البرمجة الشيئية؛ فالمتغيرات أسماء تشير لأشياء: فالرقم شيء، والنص شيء، والقائمة شيء، والمصفوفة شيء، وهلم جرا.
والشيء هو ما تُسنَدُ إليه: صفات (وهي المتغيرات) وأفعال؛ ويندرج تحت نوع ما.
وأشملُ نوعٍ في بايثون هو الشيء (Object).
12.1 تعريف النوع
يُعرَّف النوع بالكلمة class
ويُبتدأُ غالبًا بتعريف فعل الإنشاء __init__
ليتم تعيين الصفات فيه بالإسناد للاسم self
الذي يشير إلى الشيء المعين الذي يتم إنشاؤه الآن.
أما تعريف الأفعال فكالأفعال مثل def move
بزيادة self
في الابتداء.
تأمل المثال:
والآن نستطيع إنشاء معيَّنات من هذا النوع ونمرر القيم x, y
بحسب ما هو موجود في الفعل المخصص للإنشاء: __init__
على النحو التالي:
نستعمل حرف النقطة .
بعد اسم المعيَّن للإشارة لصفةٍ أو فعل، نحو: p1.x
:
x_diff = abs(p1.x - p2.x)
y_diff = abs(p1.y - p2.y)
print(f'X difference is {x_diff} and Y difference is {y_diff}.')
X difference is 4 and Y difference is 3.
وهذا مثال لطلب الفعل: p1.move()
لو أردنا طباعة النقطة، كيف تظهر؟
لتخصيص طريقة عرض الشيء، بحيث لو طبعناه أو ذكرناه في آخر السطر يظهر البشكل الذي نريده، يمكن تغيير الفعل المخصص __repr__
أي: التمثيل .. هكذا:
والآن إن عرفنا نقطة جديدة، ووضعناها على السطر لوحدها ، ستظهر لنا الإحداثيات، لا عنوانها الذاكري:
الفعل الثابت هو ما يُسنَدُ للنوع نفسه لا للأعيان. ويتم تثبيت الفعل بعلامة المزيِّن @staticmethod
. لاحظ عدم وجود self
في الفعل الجديد distance
لأنه ثابت:
ويتم استدعاؤه بذكر اسم النوع والنقطة .
:
كذلك يُجعل المتغير من النوع الثابت بتعيينه بمحاذاة غيره من الأفعال نحو ما فعلنا هنا بالمتغير distance_type
. ولاحظ استعماله في الفعل distance
في جملة if-else
من غير استعمال self
لأننا لا نشير إلى معيَّن:
class Point:
distance_type = 'euclidean'
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy
def __repr__(self):
return f"Point({self.x}, {self.y})"
@staticmethod
def distance(p1, p2):
if Point.distance_type == 'euclidean':
return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5
elif Point.distance_type == 'manhattan':
return abs(p1.x - p2.x) + abs(p1.y - p2.y)
12.2 التغليف (Encapsulation)
فكرة التغليف في البرمجة الشيئية هي إبعاد التفاصيل عن المستفيد.
في المثال التالي لا نريد للمستفيد أن يعدِّل على الرصيد balance
إلا عن طريق الفعل deposit
الذي يضمن أن الزيادة تكون موجبة.
ولاحظ أن استعمال __balance
عرفٌ بين المبرمجين في بايثون لنقول أن الصفة مخفيَّة عن المستفيد، وأنه يجب أن لا يغيرها مباشرةً.
أما المزيِّن @property
في الفعل balance
فإنه يرجع بقيمة الصفة لا بالصفة نفسها. وذلك ليمنع التعديل عليها.
ولو حاولت التغيير مباشرة فلن تستطيع (من غير الشرطتين السفليتين):
12.3 تصنيف الأنواع
تصنف الأنواع في البرمجة بطريقتين:
- تصنيف اسمي: حيث يتم التصريح بأن النوع فلان، يستمد من النوع فلان (وقد يتعدد). وتتم بالوراثة (Inheritance)
- تصنيف عملي: حيث يكون اعتبار التصنيف بحسب أفعال النوع. وتتم بالواجهة (Interface) وهو نوعٌ من التركيب (Composition)
12.4 التصنيف الاسمي: الوراثة (Inheritance)
الوراثة: أن يندرج النوع تحت نوعٍ آخر؛ فهو يستمد منه ويزيد عليه صفةً أو فعلاً (أو أكثر).
وهو اسميٌّ لأن المعتبر هو أسماء الأنواع؛ فتقول:
- الشكل هو ما كان له محيط
- المستطيل شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: طولًا وعرضًا ومساحة
- والمثلث شكلٌ (إذًا له محيط) و فوق ذلك فإنه له: ثلاثةَ أضلاعٍ ومساحة
- أما المربع فهو مستطيل (إذًا له محيط لأن المستطيل شكل، وله طول وعرض ومساحة): لكن عرضه وطوله متساويان
وهذه شجرة التوارث للأنواع المذكورة:
ويستعمل الإجراء super()
لإعمال إنشاء المُستمدِّ منه في __init__
. وتذكر أن علامة النجمة *
في وصف متغيرات الإجراء (*sides
) تجعل عدد عناصره لا محدودة.
class Shape:
def __init__(self, *sides):
self.sides = sides
@property
def perimeter(self):
return sum(self.sides)
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 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
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
نعين مثلثًا ونطبع على الشاشة:
x = Triangle(10, 10, 10)
print("Triangle")
print("Perimeter:", x.perimeter)
print("Area:", round(x.area, 2))
print(f"Sides: a={x.a}, b={x.b}, c={x.c}")
Triangle
Perimeter: 30
Area: 43.3
Sides: a=10, b=10, c=10
نعين مستطيلًا ونطبع على الشاشة:
y = Rectangle(10, 20)
print("Rectangle")
print("Perimeter:", y.perimeter)
print("Area:", round(y.area, 2))
print(f"Sides: width={y.width}, height={y.height}")
Rectangle
Perimeter: 60
Area: 200
Sides: width=10, height=20
وتجد في شجرة المجموعات شكل 1 أن المجموعات المتغيرة نوع مخصص من الأنواع الجامدة:
- القائمة (
list
) مخصصة من التسلسل المتغير (MutableSequence
) الذي هو من نوع التسلسل (Sequence
). - المجموعة (
set
) مخصصة من المجموعة المتغيرة (MutableSet
) التي هي من نوع المجموعة (Set
). - القاموس (
dict
) مخصص من القاموس المتغير (MutableMapping
) الذي هو من نوع القاموس (Mapping
).
يفضل كثير من الممارسين التصنيف العملي على التصنيف الاسمي في أغلب الأحيان. لمرونته التي نحتاجها في التطوير المستمر للبرنامج، بينما التصنيف الاسمي جامد، وسرعانما تُخرَق حدوده عند التغيير. وكثيرٌ مما ينظَّرُ له في التصنيف الاسمي يمكن تحقيقه بجملة if-elif-else
أو جملة match-case
وتنتهي القصة. ولذلك يجب استعماله في حالات قليلة يكون الاستمداد فيها من حيثية محددة: مثل التغيُّر (Mutability) كما في الثلاثة السابقة.
12.5 التصنيف العملي: التركيب (Composition)
ومثاله في نفس شجرة المجموعات شكل 1 هو نوع الجمع (Collection
)، حيث له ثلاثة اعتبارات:
- كونه يقبل العضويَّة:
x in s
وفعله هو:__contains__
يصنف أنه حاوٍ (Container
) - كونه يقبل التكرار:
for x in s
وفعله هو:__iter__
يصنف أنه متوالي (Iterable
) - كونه يقبل العد:
len(s)
وفعله هو:__len__
يصنف أنه محجَّم (Sized
)
وكذلك المتسلسلة (Sequence
) تقبل العكس، وفعلها المخصص هو: __reversed__
؛ فبهذا الاعتبار هي من النوع القابل للعكس (Reversible
).
فمثلاً: أيُّ شيءٍ يعرِّفُ الفعل len(sized)__ -> int__
فإنه ينتمي لنوع ذوات الحجم (Sized
)، ونمثل لذلك بنوع المتجه في فضاء ثنائي الأبعاد حيث يتكون من عنصرين:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __len__(self):
return 2
v1 = Vector(10, 20)
len(v1)
2
ولاحظ عدم وجود ذكر للنوع Sized
البتة، بل المُعتَبَرُ وجود الفعل __len__
حتى ينطبق التصنيف.
ويكثر فيه المجرَّداتُ ذات الفعل الواحد أو الفعلين. لأننا نريد أشياء كثيرة تنتمي بحسب ما يكون فيها من أفعال.
12.6 المعاملات (Operators)
تأمل التالي وتوقَّع النتيجة وعلل إجابتك. ما هي نتيجة:
الأول:
الثاني:
الثالث:
الرابع:
كل الذي سبق، قد تم تعريفه في بايثون لهذه الأنواع التي تراها بالتحديد عن طريق أفعال مخصصة. وإليك هذا الجدول للمعاملات المخصصة:
مثال معامله | الفعل |
---|---|
self + other |
__add__ |
self - other |
__sub__ |
self * other |
__mul__ |
self / other |
__truediv__ |
self // other |
__floordiv__ |
self % other |
__mod__ |
self ** other |
__pow__ |
(وانظر توثيق بايثون لمحاكاة العمليات الرقمية).
فنستطيع تعريف نوع متجَّه على النحو التالي.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, other):
return Vector(self.x * other.x, self.y * other.y)
والآن يمكننا إنشاء متجهين وفعل العمليات المخصصة:
ماذا لو أردنا إضافة عمليات بين المتجه والعدد، نحو: v1 + 3
? يتطلب ذلك إضافة شرط لفحص النوع، وهو isinstance
كالتالي:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
else:
return Vector(self.x + other, self.y + other)
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
else:
return Vector(self.x - other, self.y - other)
def __mul__(self, other):
if isinstance(other, Vector):
return Vector(self.x * other.x, self.y * other.y)
else:
return Vector(self.x * other, self.y * other)
والآن يمكننا فعل العمليات المخصصة:
لكن لاحظ أنك لو وضعت العدد أولاً فسيظهر خطأ:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[25], line 1 ----> 1 3 + v1 TypeError: unsupported operand type(s) for +: 'int' and 'Vector'
هذا لأن عملية الجمع الآن لا تنظر في نوع العدد (int
) ولا تجد فيه قبولاً للمتجه (فقد عرفناه للتو). ولحل هذه المشكلة توفر بايثون لكل فعل مخصص مقابل يبدأ بحرف r
على النحو التالي:
المعامل | الفعل |
---|---|
other + self |
__radd__ |
other - self |
__rsub__ |
other * self |
__rmul__ |
نعدل الفعل بحيث نضيف إليه المقابل:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
else:
return Vector(self.x + other, self.y + other)
def __radd__(self, other):
return self + other
والآن كلاهما يعمل بشكل صحيح:
12.7 الإجمال ثم التفصيل
ويُحبَّذُ في الإنشاء الإجمالُ ثم التفصيل. ونبين ذلك في تعريف الأفعال، وينطبق كذلك في تعريف الأنواع:
- كتابة اسم معبِّرٍ عن وظيفة الفعل، مع كتابة نوع ما يقبل ونوع ما ينتج
- وهي جُملة التعريف:
def
- وهي جُملة التعريف:
- كتابة وصف -باللغة الإنجليزية- يحدد سلوكه العام بناءً على عوامله يصف ما يَقبل وما يُنتج (إذْ لغة البرمجة عاجزة عن بيان كل ما يحصل)
- وهو النص الملحَقُ:
docstring
(ويكونُ أولَّ شيء فيه قبل أي أمر، وهو المحاط بـ:"""
) والقاعدة العامة فيه: ألا يصف إلا ما يفيد المستفيد من الفعل. أما التفاصيل التي تفيد مطوِّر الفعل فإنها تكون تعليقات بعلامة#
في ثناياه. فالذي يظهر عند المساعدةhelp(min_max)
هو النص الملحق، لا التعليقات.
- وهو النص الملحَقُ:
- بعد ذلك نكتب الاختبارات التي بمجموعها تصف سلوك الفعل من الخارج
- ونستعمل لها جملة التوكيد:
assert
- ولن تكون حقيقيةً لأن تفاصيل الفعل لم تُكتب بعد، لذا قد تستعمل كلمة
pass
لإرضاء المترجم إلى ذلك الحين
- ونستعمل لها جملة التوكيد:
- وبعد ذلك نأتي لكتابة التفاصيل: التي هي قطعة الكود داخل الفعل.
وهكذا تظهر المساعدة، مثلما لو وضعت المؤشر على اسم الفعل (فإن غالب المحررات تُظهر لك شيئًا):
Help on function min_max in module __main__:
min_max(numbers: list[float]) -> tuple[float, float]
Return the minimum and maximum values in the list.
لاحظ أن الظاهر هو ماهية الفعل لا كيفية عمله. وهذا هو المهم.
ثم نكتب اختبارات تصف السلوك الذي نريده منه:
# assert min_max([10, 20, 30, 40, 50]) == (10, 50)
# assert min_max([50, 40, 30, 20, 10]) == (10, 50)
# assert min_max([10, 10, -900, 10, 10]) == (-900, 10)
ثم الآن نعدل الفعل الذي كتبناه:
- تستعمل التعليقات المبتدأة بحرف
#
لتبيين ما قد يُشكل من التفاصيل - ذكر النوع
tuple[float, float]
يعني أن الفعل يرجع بزوج، لا بقيمة واحدة (وهذا يسمَّى نوع الصف، وسيأتي الحديث عنه)
def min_max(numbers: list[float]) -> tuple[float, float]:
"""Return the minimum and maximum values in the list."""
if len(numbers) == 0:
return None, None
# Assign the first element to minimum and maximum
minimum = numbers[0]
maximum = numbers[0]
for num in numbers:
if num < minimum:
minimum = num
# elif is used because maximum will never be less than minimum
elif num > maximum:
maximum = num
return minimum, maximum
ونشغل الاختبارات، ونتوقع أن لا يظهر منها خطأ:
راجع ثوثيق بايثون في العوامل الخاصة للمزيد.