= []
xs1 = list()
xs2 print(xs1 == xs2)
True
رأينا فيما تقدَّم طريقةً لتبعيض مسارات البرنامج لأجزاء تعمل على معطيَات محددة (الدالة / الإجراء)، بحيث يُمكن استدعاؤها بمعطيات مختلفة، ومرات متعددة. وكل جزء منها فيه سير للأوامر من بدايته حتى أحد النهايات بجملة الرجوع (return
).
والآن نتعرَّف على طريقة لتبعيض البرنامج إلى برامج جزئية؛ لها مساراتها وأيضًا متغيرتها الخاصَّة. وتسمى هذه البرمجة الكائنية (Object-oriented Programming - OOP) حيث يحتفظ كل كائن بأمرين:
وبالمثال يتضح المقال.
القائمة (list
) يتم إنشاؤها بالقوسين المربعين []
، وهذا من اختصارات بايثون. والأصل أن الكائن يتمُّ إنشاؤه بذكر نوعه مع القوسين ()
ليتم استدعاء الإجراء المُنشئ للكائن، على هذا النحو:
= []
xs1 = list()
xs2 print(xs1 == xs2)
True
فأما حال القائمة:
ويُمكِنُ قراءة (Read) الحال بالإجراءات التالية:
print(xs[0])
len(xs)
.index(10)
10 in xs
for x in xs
xs == ys
وأما الانتقال من حالٍ إلى حال، فيتم بأحد إجراءات الكتابة (Write):
xs.append(50)
xs.remove(50)
xs[0] = 30
وكل ما سبق عمليَّات تمثَّل بإجراءات، لكنَّ قد يحلّ محل اسم الإجراء عامل يدلُّ عليه:
.append()
و .remove()
. وإذا أُسنِدَ الإجراء للكائن فهو مما يختصُّ به لا بغيره؛ فالمفعول به هو هذا الكائن، فيكون مُعطىً مُقدَّر لا يحتاج لتمريره بين القوسين.print()
و len()
.xs[0]
فهو إجراء قراءة اسمه: (__getitem__
) لكن بايثون تخصُّه بهذه الكتابة للاختصار. ومثله أيضًا: xs == ys
فهو يستدعي إجراء المقارنة (__eq__
) وكذلك البحث (__contains__
). وأما الكر فيتطلب وجود إجراء (__iter__
) يُرجِع كائنًا له الإجراء (__next__
).وهذه الأسماء تهمُّنا عند تعريف أنواع بأنفسنا.
تأمل المثال التالي:
class Counter:
def __init__(self, count):
self.count = count
def increment(self, by=1):
self.count += by
Counter
self
وهي هنا: count
فقطincrement
فقطأما إجراء __init__
(بشرطتين قبل وشرطتين بعد __
) يرمز للكلمة (Initialization) وتعنى الإنشاء؛ ويتم استدعاؤُها فوْر ذِكر اسم النوع كدالة لإنشائه في نحو: Counter(0)
.
تتقدَّم self
(نفس) كمعطى في الابتداء في جميع الأفعال؛ والإسنادُ إليها إسنادٌ لكائن مُضمَر أنشئ من هذا النوع.
تتم كتابة اسم الصنف، ثم القوسان (عامل الاستدعاء) وبينهما المعطيات لطريقة الإنشاء __init__
على النحو التالي:
= Counter(10) c1
وللوصول إلى خاصية أو طريقة ما فإننا نتسعمل عامل الوصول، النقطة (.
) على النحو التالي:
print(c1.count)
2)
c1.increment(3)
c1.increment(print(c1.count)
10
15
وهذا معيَّن آخر من نفس الصنف:
= Counter(0) c2
وكل واحد منهما له حال خاصَّة به:
print(c1.count)
print(c2.count)
15
0
وكي يتضح مفهوم الأصناف، فإننا سنمثل المجموعة الرياضية (Set
) بأنفسنا، وإن كانت جودودة في بايثون أصلاً.
تسمي بايثون الحروف والعلامات المستعملة مع أنواع الجموع: عوامل الحاوية (Container Operators).
ونمثل بتعريف صنف المجموعة الرياضية، حيث تقبل:
len(s)
يُسمَّى فعله: __len__
x in s
يُسمَّى فعله: __contains__
for x in s
يُسمَّى فعله: __iter__
.remove()
ومن سماتها أن العنصر فيها لا يتكرر.
ومن طرائقها: منطق المجموعة الرياضية.
المجموعة في الرياضيات لها بعض المفاهيم المتعلقة بها وهي:
وتمثلها بايثون باستعمال القوسين المعقوفين ({}
) على هذا النحو:
= {1, 2, 3, 4, 5}
set1 = {4, 5, 6, 7, 8} set2
وفيما يلي نشرح هذه العمليات، ثم ننتقل إلى كيفية تمثيل ذلك في بايثون.
set1.union(set2)
{1, 2, 3, 4, 5, 6, 7, 8}
set1.intersection(set2)
{4, 5}
set1.difference(set2)
{1, 2, 3}
set2.difference(set1)
{6, 7, 8}
set1.symmetric_difference(set2)
{1, 2, 3, 6, 7, 8}
= {1, 2, 3}
A = {1, 2, 3, 4, 5, 6} B
وهذا مثال لاستعمالها كما في الجدول:
print(A.issubset(B))
print(B.issuperset(A))
True
True
وأما الانفصال، فهو عدم وجود أدنى تقاطع بين المجموعتين:
= {'Apple', 'Banana'}
C print(C.isdisjoint(A))
print(C.isdisjoint(B))
True
True
ونعرف المجموعة الرياضيَّة (Set
) بأنفسنا لغرض تعليمي وإن كانت نوعًا أصليًّا في بايثون اسمه (set
). ولاحظ أن التمثيل الداخلي (Internal Representation) الذي سنعتمد عليه هو: القائمة (list
) الممثلة بالمتغيِّر: self.elements
.
class Set:
# طريقة الإنشاء
def __init__(self, elements):
self.elements = elements
# طريقة التمثيل
def __repr__(self):
return f"Set({self.elements})"
# طريقة العد تمررها كما هي
def __len__(self):
return len(self.elements)
# طريقة العضويَّة تمررها كما هي
def __contains__(self, x):
return x in self.elements
# طريقة التكرار تمررها كما هي
def __iter__(self):
return iter(self.elements)
# طريقة الإضافة تتحقق أولاً من عدم وجود العنصر
def add(self, x):
if x not in self.elements:
self.elements.append(x)
# طريقة الحذف تتحقق أولاً من وجود العنصر
def remove(self, x):
if x in self.elements:
self.elements.remove(x)
# طريفة الاتحاد
def union(self, other):
= []
result for x in self.elements + other.elements:
if x not in result:
result.append(x)return Set(result)
# طريقة التقاطع
def intersection(self, other):
= []
result for x in self.elements:
if x in other.elements:
result.append(x)return Set(result)
# طريقة الفرق
def difference(self, other):
= []
result for x in self.elements:
if x not in other.elements:
result.append(x)return Set(result)
# طريقة الفرق التماثلي
def symmetric_difference(self, other):
= self.difference(other)
d1 = other.difference(self)
d2 return d1.union(d2)
# طريقة الانفصال
def isdisjoint(self, other):
return len(self.intersection(other)) == 0
# طريقة الجزئية
def issubset(self, other):
for x in self.elements:
if x not in other.elements:
return False
return True
# طريقة الشمول
def issuperset(self, other):
return other.issubset(self)
ولاحظ في المثال السابق عدة أمور:
الطريقة (__repr__
) تعني التمثيل (Representation)، وهي تظهر حين نعرض الكائن مثلاً بأمر الطباعة print()
.
أن بعض الطرائق تغير التمثيل الداخلي (elements
) مباشرةً، مثل: add
و remove
.
وبعض الطرائق يُنشئ نُسخة جديدة منه، ويجري العمليَّة عليه، ثم يُنشئ كائن مجموعة (Set
) ويمرر إليه هذا التمثيل ويرجع به: return Set(result)
. والفائدة من هذه الحركة: هي إمكانيَّة استعمال هذه الطرائق ضمن طرائق أخرى. كما ترى في .symmetric_difference()
و .isdisjoint()
. فلو لم نقم بهذه الحركة، لم يمكن ذلك.
ثم الإنشاء والاستعمال على النحو التالي:
= Set([1, 2, 3])
s1 = Set([3, 4, 5]) s2
print(s1.union(s2))
print(s1.intersection(s2))
print(s1.difference(s2))
print(s2.difference(s1))
print(s1.symmetric_difference(s2))
Set([1, 2, 3, 4, 5])
Set([3])
Set([1, 2])
Set([4, 5])
Set([1, 2, 4, 5])
print(Set([1, 2]).isdisjoint(Set([3, 4])))
print(Set([1, 2]).issubset(Set([1, 2, 3])))
print(Set([1, 2, 3]).issuperset(Set([1, 2])))
True
True
True
تأمل التالي وتوقَّع النتيجة وعلل إجابتك. ما هي نتيجة:
القطعة الأولى:
1, 2, 3] + [4, 5, 6] [
القطعة الثانية:
1, 2, 3] * [4, 5, 6] [
القطعة الثالثة:
1, 2, 3] * 5 [
القطعة الرابعة:
1, 2, 3] - 3 [
كل الذي سبق، قد تم تعريفه في بايثون لهذه الأنواع التي تراها بالتحديد عن طريق إجراءات مخصصة. وإليك هذا الجدول للعوامل المخصصة:
مثال | الإجراء |
---|---|
self + other |
__add__ |
self - other |
__sub__ |
self * other |
__mul__ |
self / other |
__truediv__ |
self // other |
__floordiv__ |
self % other |
__mod__ |
self ** other |
__pow__ |
(وانظر مرجع بايثون لمحاكاة العمليات الرقمية).
فنستطيع تعريف نوع المتجَّه (Vector2D) على النحو التالي.
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"<{self.x}, {self.y}>"
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector2D(self.x - other.x, self.y - other.y)
def __mul__(self, other):
return Vector2D(self.x * other.x, self.y * other.y)
وفي الجمع والطرح والضرب، يكون العائد متجهًا جديدًا هو حاصل العملية على أفراد العناصر المتقابلة بين المتجهين self
و other
. حيث يمثل الأوَّل (self
) المتجَّه في الطرف الأيسر من العامل، والثاني (other
) في الطرف الأيمن.
والآن يمكننا إنشاء متجهين ووضع العوامل بينهما:
= Vector2D(1, 2)
v1 = Vector2D(3, 4)
v2
+ v2 v1
<4, 6>
- v2 v1
<-2, -2>
* v2 v1
<3, 8>
ماذا لو أردنا إضافة عمليات بين المتجه والعدد، نحو: v1 + 3
? يتطلب ذلك إضافة شرط لفحص النوع، وهو isinstance
كالتالي:
class Vector2D:
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, Vector2D):
return Vector2D(self.x + other.x, self.y + other.y)
else:
return Vector2D(self.x + other, self.y + other)
def __sub__(self, other):
if isinstance(other, Vector2D):
return Vector2D(self.x - other.x, self.y - other.y)
else:
return Vector2D(self.x - other, self.y - other)
def __mul__(self, other):
if isinstance(other, Vector2D):
return Vector2D(self.x * other.x, self.y * other.y)
else:
return Vector2D(self.x * other, self.y * other)
وهكذا يصبح التفاعل بين المتجَّه والعدد، وهما نوعان مختلفان (int
و Vector
):
= Vector2D(1, 2)
v1 + 3 v1
Vector(4, 5)
لكن لاحظ أنك لو وضعت العدد أولاً فسيظهر خطأ:
3 + v1
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[100], line 1 ----> 1 3 + v1 TypeError: unsupported operand type(s) for +: 'int' and 'Vector2D'
هذا لأن عملية الجمع الآن لا تنظر في نوع العدد (int
) ولا تجد فيه قبولاً للمتجه (فقد عرفناه للتو). ولحل هذه المشكلة توفر بايثون لكل فعل مخصص مقابل يبدأ بحرف r
على النحو التالي:
العامل | الإجراء |
---|---|
other + self |
__radd__ |
other - self |
__rsub__ |
other * self |
__rmul__ |
نعدل الإجراء بحيث نضيف إليه المقابل:
class Vector2D:
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, Vector2D):
return Vector2D(self.x + other.x, self.y + other.y)
else:
return Vector2D(self.x + other, self.y + other)
def __radd__(self, other):
return self + other
والآن كلاهما يعمل بشكل صحيح:
= Vector2D(1, 2)
v1 3 + v1
Vector(4, 5)
+ 3 v1
Vector(4, 5)
يُمكن جعل علاقة بين نوع ونوع. ومن ذلك الوراثة (Inheritence) وهي عمليَّة تخصيص (Sub-classing) بحيث يستمد النوع خصائصه وطرائقه من النوع الأعم.
ومثاله في بايثون أنواع الرقم:
flowchart BT Number[<b>رقم</b><br>Number] Integral[<b>كامل</b><br>Integral] Integral --> Number Real[<b>حقيقي</b><br>Real] Real --> Number float[<b>عشري</b><br>float] float --> Real Complex[<b>مركب</b><br>Complex] Complex --> Number int[<b>صحيح</b><br>int] int --> Integral bool[<b>منطقي</b><br>bool] bool --> Integral
ونمثل بمثال فتقول المربع نوع خاص من المستطيلات. وكذلك تقول: المستطيل نوع خاص من الأشكال. وبالتالي فإن علاقة المربَّع بالشكل هي علاقة تخصيص عام. على نحو هذا المثال:
وهذه شجرة التوارث للأنواع المذكورة:
flowchart BT Shape Rectangle -- "is a" --> Shape Square -- "is a" --> Rectangle Triangle -- "is a" --> Shape
وهذا تعريف صنف الشكل:
class Shape:
def __init__(self, sides):
self.sides = sides
@property
def perimeter(self):
return sum(self.sides)
@property
def area(self):
pass
لاحظ استعمال المعدِّل (Decorator) @property
(يعني: خاصيَّة) وهي تجعل طريقة الوصول لا تحتاج إلى قوسي استدعاء (()
) كما هو الأصل. فبمجرد كتابة .perimeter
فإن الطريقة تعمل لتأتيك بالنتيجة، وكأنها متغير. وهذا يستعمل في طرائق القراءة عادة، لا في طرائق الكتابة.
والأمر الثاني هو استعمالنا كلمة 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
لاحظ القوسين الإضافيين حول المتغيرات المرسلة إلى الأب (super().__init__()
)، وهذا يعني أنها كلها ستعيَّن للمعطى الأوَّل كصفّ (tuple
)، وهو نوع تسلسل مثل القائمة لكنه جامد لا يقبل التغيير.
أما المربع، فهو نوعٌ خاص من المستطيل:
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)
t = Rectangle(10, 20)
r = [t, r]
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(shape):
print(shape.__class__.__name__)
print("Perimeter:", shape.perimeter)
print("Area:", round(shape.area, 2))
وفي التمرير خاص:
= Triangle(10, 10, 10)
x show(x)
Triangle
Perimeter: 30
Area: 43.3
= Rectangle(10, 20)
y show(y)
Rectangle
Perimeter: 60
Area: 200
للمزيد راجع ملحق البرمجة الكائنية.