class Account:
def __init__(self, name, balance):
self.name = name
self.__balance = balance
# الإيداع
def deposit(self, amount):
# قبليات الإيداع
if amount <= 0:
print("must be positive")
return
# الإيداع نفسه
self.__balance += amount
# بعديات الإيداع
print('time of deposit:', '2027-07-07')
@property
def balance(self):
return self.__balance
ملحق O — البرمجة الكائنية
التغليف
التغليف (Encapsulation) آلية لإخفاء عمليات تفصيلية معقدة على البيانات المكنونة خلف إجراءات بسيطة.
فحينما عرفنا المتغير كصفة: count
في الشيء Counter
، أبرزنا من الإجراءات: increment
فقط. وكأننا نقول: لا يمكن أن يعدِّل المستفيد على المتغير count
إلا عن طريق الإجراء increment
. وصحيح أنك تستطيع الوصول من الخارج إلى صفة المعيَّن: c1.count
لتعديلها مباشرةً، إلا أن ذلك يخالف آلية التغليف.
في المثال التالي لا نريد للمستفيد أن يعدِّل على الرصيد balance
إلا عن طريق الإجراء deposit
الذي يضمن أمرين:
- قبليات (pre-conditions): كاشتراط أن الزيادة تكون موجبة قبل البدء
- بعديات (post-conditions): ضمان تسجيل تاريخ العملية بعد التمام
يتم ذلك في بايثون عن طريق جعل الصفة نفسها (balance
) مخفيَّة، ونبدلها بصفة محميَّة: __balance
بشرطتين سفليتين متقدِّمة. ثم نُبرِزُ الصفة عن طريق فعل قراءة؛ وذلك باستعمال المعدِّل @property
، وهو يجعل الفعل balance
يبرز كصفة بلا قوسين للاستدعاء balance()
وذلك يعني أن مجرَّد عملية الوصول إلى الصفة (بعلامة النقطة: .
) هو في الحقيقة استدعاء لفعل يعود بقيمة، لا بالصفة التي يمكن تعديلها.
نقوم الآن بإنشاء هذا الشيء الذي يمثِّل حساب المستخدم (Account
)، ثم نصِل إلى الصفة بعلامة النقطة: a1.balance
؛ فيكون ذلك استدعاءً للإجراء الذي تم وضع المعدِّل @property
عليه ليكون الوصول إليه كالصفة:
= Account('Adam', 100)
a1 a1.balance
100
ولو حاولت التغيير مباشرة على الصفة فإنك أصلاً لن تجد اسمها مُسندًا إلى الشيء:
= 1000 a1.balance
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[3], line 1 ----> 1 a1.balance = 1000 AttributeError: property 'balance' of 'Account' object has no setter
لكن يجوز فقط قراءة الصفة (قيمتها) لا التعديل عليها، هكذا (بعد الإيداع مثلاً):
100)
a1.deposit( a1.balance
time of deposit: 2027-07-07
200
وليظهر أثر الحماية، نجرِّب إيداع مبلغ سالب، ولاحظ رسالة الخطأ:
-44) a1.deposit(
must be positive
وهكذا نتصوَّر الشيء كأنه آلة مزوَّّدة بآليات عامَّة يسهل استعمالها في ظروف كثيرة بحيث تغير هذه الآليات من حالة الشيء في كل مرة ليؤدي وظائف معقَّدة لا تتم بسهول بمجرَّد إجراء ذو خطوات محددة دائمًا بتسلسل واحد.
العَرض
نريد أن نعرِّف نوعًا جديدًا للإحداثيات (Point) ونريد أن نعرضها في جملة print
بتمثيل يعبِّر عنها كإحداثيات.
يستعمل الفعل الخاص __repr__
، ويعني التمثيل (Representation) لتخصيص طريقة عرض الشيء؛ سواءٌ إذا تم تمرير في print
أو في آخر سطر من الخلية. كل ما عليك هو إعادة قيمة نصيَّة من ذلك الفعل.
class Point:
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})"
والآن إن عرفنا نقطة جديدة، ووضعناها على السطر لوحدها ، ستظهر لنا الإحداثيات، لا عنوانها في الذاكرة:
= Point(3, 4)
p 7, 6)
p.move( p
Point(10, 10)
الإجراء المُثبَت
الإجراء المُثبَت (Static Method): هو الإجراء المسند إلى النوع لا للمعيَّن.
فيمكن إسناد الإجراء للجنس لا للشيء الواحد، وذلك بإضافة المعدِّل @staticmethod
عليه. ونمثل لذلك بإجراء حساب المسافة بين نقطتين: distance
. ولاحظ عدم وجود self
فيه:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})"
@staticmethod
def distance(p1, p2, distance_type='euclidean'):
if distance_type == 'euclidean':
return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5
elif distance_type == 'manhattan':
return abs(p1.x - p2.x) + abs(p1.y - p2.y)
ثم يستعمل كأي إجراء، لكن بإسناده إلى النوع:
= Point(3, 4)
p1 = Point(7, 6)
p2 = Point.distance(p1, p2, 'manhattan')
d print('distance:', d)
distance: 6
المتغير المُثبَت
المتغير المُثبَت (Static Variable): هو المتغير المسند إلى النوع لا للمعيَّن.
يُسنَد المتغير للنوع بتعيينه بمحاذاة غيره من الإجراءات نحو ما فعلنا هنا بالمتغير decor
. ولاحظ استعماله في الإجراء __repr__
في جملة if-else
من غير استعمال self
لأننا لا نشير إلى معيَّن.
class Point:
= '<>'
decor
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):
if Point.decor == '<>':
return f"<{self.x}, {self.y}>"
elif Point.decor == '[]':
return f"[{self.x}, {self.y}]"
else:
return f"({self.x}, {self.y})"
فيمكن الآن تعديل المتغير decor
للنوع:
= Point(3, 4)
p1 = Point(7, 6)
p2 print(p1, p2)
= '[]'
Point.decor print(p1, p2)
<3, 4> <7, 6>
[3, 4] [7, 6]
هيكل البيانات
تختلف كفاءة استرجاع البيانات وكتابتها والتعديل عليها بحسب شكل هذه البيانات في الذاكرة وشكلها على جهاز التخزين فعليًّا. وهذا ما نسميه بهيكلة البيانات (Data Structure). ومن أمثلة ذلك:
- المصفوفة (Array)
- الشجرة (Tree)
- الرسم (Graph)
وغيرها كثير. وسنمثل على ذلك بتشكيل الكومة في بايثون:
الكومة (Stack
)
نريد أن نعرِّف نوعًا من هياكل البيانات يسمى الكومة (Stack)، وكأنه يمثِّل مجموعة مكدسة من الأوراق؛ إذ له فعلين:
- وضع ورقة:
push(item)
وكأنك تضع ورقة فوق الأوراق السابقة - سحب ورقة:
pop()
وكأنك تسحب ورقةً من الأعلى - لمحة سريعة:
peek()
وكأنك تنظر إلى الورقة العليا دون سحبها
وأقرب شيء له في بايثون هو القائمة (list
) ولذلك ستكون هي تمثيلها الداخلي.
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def peek(self):
return self.items[-1]
def __len__(self):
return len(self.items)
def __repr__(self):
return f"|{'|'.join(str(item) for item in self.items)}>"
- فعل الإنشاء يُسنِد قائمة فارغة إلى الشيء (
self
) - فعل
push
يضيف إلى آخر هذه القائمة بالفعلappend
- فعل
pop
يأخذ آخر عنصر من القائمة بالفعلpop
(وهو إزالة مع أخذ) - فعل
__len__
لمعرفة عدد العنصار في هذا الهيكل - فعل
__repr__
يعطينا تمثيلاً للكومة بالاعتماد على تمثيل القائمة
نجرب الآن أن ننشئها ثم نضع أربعة عناصر فيها ونظهرها:
= Stack()
s 10)
s.push(20)
s.push(30)
s.push(400)
s.push(print(s)
|10|20|30|400>
والآن نسحب عنصرًا (من الأخير)، ونظهرها:
s.pop()print(s)
|10|20|30>
ويمكن النظر في العنصر الأعلى دون سحبه بالفعل peek
:
print(s.peek())
30
ونستطيع أن نعدها:
len(s)
3
استعمال الكومة
إذا كان لديك نص تريد التحقق من توازن الأقواس فيه، بحيث يكون لكل قوسٍ مفتوح قوسٌ يغلقه، فالكومة تساعدك في حل مثل هذه المسألة (انظر التوكيدات في الأسفل):
def is_balanced(text: str) -> bool:
"""Check if the text has balanced parentheses."""
= Stack()
stack for character in text:
if character == '(':
stack.push(character)elif character == ')':
if len(stack) == 0:
return False
stack.pop() return len(stack) == 0
assert is_balanced('()')
assert is_balanced('(())')
assert not is_balanced('(()')
assert not is_balanced('((()')
assert not is_balanced('())))')