xs1 = []
xs2 = list()
print(xs1 == xs2)البرمجة بالكائنات
رأينا فيما تقدَّم طريقةً لتبعيض مسارات البرنامج لأجزاء تعمل على معطيَات محددة (الدالة / الإجراء)، بحيث يُمكن استدعاؤها بمعطيات مختلفة، ومرات متعددة. وكل جزء منها فيه سير للأوامر من بدايته حتى أحد النهايات بجملة الرجوع (return).
والآن نتعرَّف على طريقة لتبعيض البرنامج إلى برامج جزئية؛ لها مساراتها وأيضًا متغيرتها الخاصَّة. وتسمى هذه البرمجة الكائنية (Object-oriented Programming - OOP) حيث يحتفظ كل كائن بأمرين:
- الحال (State) وتمثِّلُها المتغيرات، والإجراءات التي تقرأ من تلك المتغيرات.
- الانتقال (Transition) وتمثِّلُها الإجراءات التي تكتب في تلك المتغيرات (أو تكتب في منافذ المخرجات)
وبالمثال يتضح المقال.
القائمة (list) يتم إنشاؤها بالقوسين المربعين []، وهذا من اختصارات بايثون. والأصل أن الكائن يتمُّ إنشاؤه بذكر نوعه مع القوسين () ليتم استدعاء الإجراء المُنشئ للكائن، على هذا النحو:
فأما حال القائمة:
- قيَم العناصر التي تحتويها
- موضِع كل عنصر فيها بالترتيب
- عدد العناصر
ويُمكِنُ قراءة (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- الصنف (Class):
Counter - الخصائص (Properties): هي كل ما تم تعيينه وإسنادُه إلى
selfوهي هنا:countفقط - الطرائق (Methods): وهي كل إجراء تم تعريفُه في داخل تعريف النوع، وهي هنا:
incrementفقط
أما إجراء __init__ (بشرطتين قبل وشرطتين بعد __) يرمز للكلمة (Initialization) وتعنى الإنشاء؛ ويتم استدعاؤُها فوْر ذِكر اسم النوع كدالة لإنشائه في نحو: Counter(0).
تتقدَّم self (نفس) كمعطى في الابتداء في جميع الأفعال؛ والإسنادُ إليها إسنادٌ لكائن مُضمَر أنشئ من هذا النوع.
إنشاء الكائنات
تتم كتابة اسم الصنف، ثم القوسان (عامل الاستدعاء) وبينهما المعطيات لطريقة الإنشاء __init__ على النحو التالي:
c1 = Counter(10)وللوصول إلى خاصية أو طريقة ما فإننا نتسعمل عامل الوصول، النقطة (.) على النحو التالي:
print(c1.count)
c1.increment(2)
c1.increment(3)
print(c1.count)وهذا معيَّن آخر من نفس الصنف:
c2 = Counter(0)وكل واحد منهما له حال خاصَّة به:
print(c1.count)
print(c2.count)عوامل الحاويات
وكي يتضح مفهوم الأصناف، فإننا سنمثل المجموعة الرياضية (Set) بأنفسنا، وإن كانت جودودة في بايثون أصلاً.
تسمي بايثون الحروف والعلامات المستعملة مع أنواع الجموع: عوامل الحاوية (Container Operators).
ونمثل بتعريف صنف المجموعة الرياضية، حيث تقبل:
- العد:
len(s)يُسمَّى فعله:__len__ - العضويَّة:
x in sيُسمَّى فعله:__contains__ - الكر:
for x in sيُسمَّى فعله:__iter__ - الحذف:
.remove()
ومن سماتها أن العنصر فيها لا يتكرر.
ومن طرائقها: منطق المجموعة الرياضية.
منطق المجموعة الرياضية
المجموعة في الرياضيات لها بعض المفاهيم المتعلقة بها وهي:
- التقاطع والاتحاد والفرق، والفرق التماثلي
- وكذلك تحقق: (الجزئية والشمول والانفاصل).
وتمثلها بايثون باستعمال القوسين المعقوفين ({}) على هذا النحو:
set1 = {1, 2, 3, 4, 5}
set2 = {4, 5, 6, 7, 8}وفيما يلي نشرح هذه العمليات، ثم ننتقل إلى كيفية تمثيل ذلك في بايثون.
العمليات على المجموعات
الاتحاد
set1.union(set2)التقاطع
graph TD
subgraph Universal_Set [Universal Set U]
A((Set A))
B((Set B))
A --- AB(A ∩ B)
B --- AB
end
set1.intersection(set2)الفرق
set1.difference(set2)set2.difference(set1)الفرق التماثلي
set1.symmetric_difference(set2)العلاقات بين المجموعات
الجزئية والشمول
A = {1, 2, 3}
B = {1, 2, 3, 4, 5, 6}وهذا مثال لاستعمالها كما في الجدول:
print(A.issubset(B))
print(B.issuperset(A))الانفصال
وأما الانفصال، فهو عدم وجود أدنى تقاطع بين المجموعتين:
C = {'Apple', 'Banana'}
print(C.isdisjoint(A))
print(C.isdisjoint(B))تعريف المجموعة الرياضية
ونعرف المجموعة الرياضيَّة (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):
d1 = self.difference(other)
d2 = other.difference(self)
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(). فلو لم نقم بهذه الحركة، لم يمكن ذلك.
ثم الإنشاء والاستعمال على النحو التالي:
s1 = Set([1, 2, 3])
s2 = Set([3, 4, 5])print(s1.union(s2))
print(s1.intersection(s2))
print(s1.difference(s2))
print(s2.difference(s1))
print(s1.symmetric_difference(s2))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])))تحديد عمل العوامل
تأمل التالي وتوقَّع النتيجة وعلل إجابتك. ما هي نتيجة:
القطعة الأولى:
[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) في الطرف الأيمن.
والآن يمكننا إنشاء متجهين ووضع العوامل بينهما:
v1 = Vector2D(1, 2)
v2 = Vector2D(3, 4)
v1 + v2v1 - v2v1 * v2ماذا لو أردنا إضافة عمليات بين المتجه والعدد، نحو: 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):
v1 = Vector2D(1, 2)
v1 + 3لكن لاحظ أنك لو وضعت العدد أولاً فسيظهر خطأ:
3 + v1هذا لأن عملية الجمع الآن لا تنظر في نوع العدد (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والآن كلاهما يعمل بشكل صحيح:
v1 = Vector2D(1, 2)
3 + v1v1 + 3التخصيص بالوراثة
يُمكن جعل علاقة بين نوع ونوع. ومن ذلك الوراثة (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):
s = self.perimeter / 2
return (s * (s - self.a) * (s - self.b) * (s - self.c))**0.5والآن ستلاحظ إمكانية استعمال الشيئين المختلفين (المثلث والمستطيل) باعتبار المشترك بينهما (الشكل). ويتبين ذلك إذا كررنا عليهما في قائمة:
t = Triangle(10, 10, 10)
r = Rectangle(10, 20)
shapes = [t, r]
for sh in shapes:
print(sh.__class__.__name__)
print("Perimeter:", sh.perimeter)
print("Area:", round(sh.area, 2))
print('='*40)واستعمال 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}")وذلك ينطبق في تعريف الإجراءات. فإنك تستطيع تحديد النوع الأعم وتمرير النوع الأخص.
فهو في التعريف عام:
def show(shape):
print(shape.__class__.__name__)
print("Perimeter:", shape.perimeter)
print("Area:", round(shape.area, 2))وفي التمرير خاص:
x = Triangle(10, 10, 10)
show(x)y = Rectangle(10, 20)
show(y)للمزيد راجع ملحق البرمجة الكائنية.






