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)
21 المعاملات المخصصة
تأمل التالي وتوقَّع النتيجة وعلل إجابتك. ما هي نتيجة:
القطعة الأولى:
القطعة الثانية:
القطعة الثالثة:
القطعة الرابعة:
معاملات الأرقام
كل الذي سبق، قد تم تعريفه في بايثون لهذه الأنواع التي تراها بالتحديد عن طريق إجراءات مخصصة. وإليك هذا الجدول للمعاملات المخصصة:
مثال | الإجراء |
---|---|
self + other |
__add__ |
self - other |
__sub__ |
self * other |
__mul__ |
self / other |
__truediv__ |
self // other |
__floordiv__ |
self % other |
__mod__ |
self ** other |
__pow__ |
(وانظر توثيق بايثون لمحاكاة العمليات الرقمية).
فنستطيع تعريف نوع المتجَّه (Vector2D) على النحو التالي.
ففي الجمع والطرح والضرب، يكون العائد متجهًا جديدًا هو حاصل العملية على أفراد العناصر المتقابلة بين المتجهين self
و other
. حيث يمثل الأوَّل (self
) المتجَّه في الطرف الأيسر من المعامل، والثاني (other
) في الطرف الأيمن.
والآن يمكننا إنشاء متجهين ووضع المعاملات بينهما:
ماذا لو أردنا إضافة عمليات بين المتجه والعدد، نحو: 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
):
لكن لاحظ أنك لو وضعت العدد أولاً فسيظهر خطأ:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[7], 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
والآن كلاهما يعمل بشكل صحيح:
إطلاق العناصر
وهذا مثال لعدم تقييد المتجَّه بعنصرين (x, y
) بل نستطيع إطلاق عدد العناصر باستعمال التسلسل وجعل عناصره عشرية: Sequence[float]
ثم تحويل أي تسلسل إلى قائمة: list(components)
في إجراء الإنشاء: init
على النحو التالي:
from typing import Sequence
class Vector:
def __init__(self, components: Sequence[float]):
self.components = list(components)
def __repr__(self):
return f"<{', '.join(str(c) for c in self.components)}>"
# جمع متجهين أو متجه وعدد
def __add__(self, other):
# لتخزين الناتج: ننشئ قائمة من الأصفار بنفس طول المتجه
result = Vector([0.0] * len(self.components))
# إذا كان المدخل متجهًا
if isinstance(other, Vector):
# يجب أن يكون لهما نفس عدد العناصر
assert len(self.components) == len(other.components), f"Vectors must have the same number of components: {len(self.components)} != {len(other.components)}"
# نجمع العناصر المتقابلة
for i in range(len(self.components)):
result.components[i] = self.components[i] + other.components[i]
# إذا كان المدخل عددًا
elif isinstance(other, (float, int)):
# نجمع العدد إلى كل عنصر
for i in range(len(self.components)):
result.components[i] = self.components[i] + other
else:
raise TypeError(f"Unsupported operand type(s) for +: 'Vector' and '{type(other)}'")
return result
# الجمع حيث يكون المتجه ثانيًا
def __radd__(self, other):
return self + other
# وضع علامة السالب للمتجه
def __neg__(self):
return Vector([-c for c in self.components])
# الطرح: تطبيقه عن طريق الجمع مع عكس الثاني
def __sub__(self, other):
return self + (-other)
# الطرح حيث يكون المتجه ثانيًا
def __rsub__(self, other):
return other + (-self)
# ضرب متجهين أو متجه وعدد
def __mul__(self, other):
# لتخزين الناتج: ننشئ قائمة من الأصفار بنفس طول المتجه
result = Vector([0.0] * len(self.components))
if isinstance(other, Vector):
# يجب أن يكون لهما نفس عدد العناصر
assert len(self.components) == len(other.components), f"Vectors must have the same number of components: {len(self.components)} != {len(other.components)}"
# نضرب العناصر المتقابلة
for i in range(len(self.components)):
result.components[i] = self.components[i] * other.components[i]
# إذا كان المدخل عددًا
elif isinstance(other, (float, int)):
# نضرب العدد في كل عنصر
for i in range(len(self.components)):
result.components[i] = self.components[i] * other
else:
raise TypeError(f"Unsupported operand type(s) for *: 'Vector' and '{type(other)}'")
return result
# الضرب حيث يكون العدد أولًا
def __rmul__(self, other):
return self * other
والآن يمكننا إنشاء متجهين ووضع المعاملات بينهما:
معاملات الحاويات
وقد تعرَّفنا على القائمة (dict
) التي تقبل مثل هذه العمليات. فهل نستطيع أن نعرِّف هذه العمليات لنوع جديد؟ الجواب: نعم. تسمي بايثون الحروف والعلامات المستعملة مع أنواع الجموع: معاملات الحاويات (أي: التي تحوي عناصر). وهذ ملخصها من توثيق بايثون (محاكاة أنواع الحاويات):
- العد:
len(s)
يُسمَّى فعله:__len__
- العضويَّة:
x in s
يُسمَّى فعله:__contains__
- التكرار:
for x in s
يُسمَّى فعله:__iter__
- الإشارة:
s[i]
يُسمَّى فعلها:__getitem__
- التعيين:
s[i] = p
يُسمَّى فعله:__setitem__
- الحذف:
del s[i]
يُسمَّى فعله:__delitem__
المثال الأول: المضلع
يتكون المضلَّح من نقاط. فنحتاج أولاً لتعريف النقطة:
ثم نعرف المضلَّع أنه نوع حاوية لمجموعة من النقاط:
class Polygon:
def __init__(self, points: list[Point]):
self.points = points
# العد
def __len__(self) -> int:
return len(self.points)
# الإشارة
def __getitem__(self, i: int) -> Point:
return self.points[i]
# التعيين
def __setitem__(self, i: int, p: Point):
self.points[i] = p
# الحذف
def __delitem__(self, i: int):
del self.points[i]
# حساب المحيط
def perimeter(self) -> float:
n = len(self.points)
s = 0
for i in range(n):
s += Point.distance(self.points[i], self.points[(i+1)%n])
return s
# التمثيل
def __repr__(self) -> str:
# إنشاء لوحة 9x9
grid = [[' ' for _ in range(9)] for _ in range(9)]
# وضع النجوم في المواقع المناسبة
for point in self.points:
x = min(max(round(point.x), 0), 8) # ضبط الموقع بين 0 و8
y = min(max(round(point.y), 0), 8) # ضبط الموقع بين 0 و8
grid[y][x] = '*'
# بناء التمثيل النصي
result = []
for row in reversed(grid): # عكس الصفوف لتطابق الإحداثيات الرياضية
result.append('|' + ''.join(row) + '|')
# إضافة الحدود العليا والسفلى
border = '+' + '-'*9 + '+'
result.insert(0, border)
result.append(border)
return '\n' + '\n'.join(result)
لاحظ استعمال الأفعال المخصوصة بالحاويات.
الآن ننشيء مضلَّع، بسلسلة من النقاط، ونرى كيف يتم تمثيله:
+---------+
| |
| |
|* |
| |
| |
| |
| * |
| |
|* * |
+---------+
ثم الإشارة برقم أو بشريحة:
Point(0, 0)
Point(0, 6)
[Point(3, 0), Point(2, 2)]
[Point(0, 6), Point(2, 2), Point(3, 0), Point(0, 0)]
ثم التعيين:
وأخيرًا الحذف:
المثال الثاني: جلسات المستخدمين
بين أيدينا شيء يخزن بيانات (هي قاموس) لمدة مؤقتة ثم يمحى هذا الشيء المخزَّن. وذلك يستعمل بكثرة في المواقع حيث تخزَّن بيانات التسجيل أثناء جلسة التصفح ليكون الوصول إليها سريعًا بدل الرجوع لقاعدة البيانات في كل مرة.
import uuid
from datetime import datetime
class Session:
def __init__(self, expires_at: datetime, data: dict):
self.id = uuid.uuid4()
self.expires_at = expires_at
self.data = data
def __repr__(self):
return f"Session(id={self.id}, expires_at={self.expires_at}, data={self.data})"
def is_expired(self):
return datetime.now() > self.expires_at
- نستورد وحدة
uuid
لإنشاء معرِّفات فريدة - نستورد وحدة
datetime
للتعامل مع التاريخ والوقت - نعرِّف الصنف
Session
الذي يمثِّل جلسة تسجيل الدخول - في إجراء الإنشاء
init
- نعطي الجلسة نفسها معرِّفًا فريدًا:
self.id = uuid.uuid4()
- نُسنِد المتغيرات الأخرى كما هي:
self.expires_at = expires_at
وself.data = data
- نعطي الجلسة نفسها معرِّفًا فريدًا:
- في الفعل
repr
نتحكم بطريقة عرض الجلسة عندما نستعمل مثلاً:print
- في الفعل
is_expired
تحقق من انتهاء صلاحية الجلسة
والآن سنعرِّف الحاوي الذي يخزَّن جلسات المستخدمين، فهو قاموس:
- مفتاحه معرِّف المستخدم
- وقيمته الجلسة التي عرفناها في الأعلى
from datetime import timedelta
class SessionStorage:
def __init__(self, expires_in: timedelta):
# قاموس يخزَّن الجلسات
self.sessions = {}
# عمر الجلسة الواحدة
self.expires_in = expires_in
# العد
def __len__(self):
return len(self.sessions)
# نتحقق: هل الجلسة موجودة وصالحة؟
def is_active(self, key: uuid.UUID):
# إن كان معرِّف المستخدم غير موجود فلا توجد جلسة
if key not in self.sessions:
return False
# فإن وجدت جلسة، فتحقق من صلاحيتها
# لاحظ استعمال .is_expired() الذي هو فعل الجلسة
now = datetime.now()
if self.sessions[key].is_expired():
del self.sessions[key]
return False
return True
# العضويَّة
def __contains__(self, key: uuid.UUID):
return self.is_active(key)
# الإشارة
def __getitem__(self, key: uuid.UUID):
if not self.is_active(key):
raise KeyError(f"Session has expired: key={key}")
return self.sessions[key]
# التعيين
def __setitem__(self, key: uuid.UUID, value: dict):
# نتأكد من النوعين المدخليْن
assert isinstance(key, uuid.UUID)
assert isinstance(value, dict)
# إن لم تكن الجلسة موجودة فإننا ننشئها
if not self.is_active(key):
session = Session(
expires_at=datetime.now() + self.expires_in,
data=value,
)
else:
# إن كانت موجودة فنحدِّث البيانات
session = self.sessions[key]
session.data = value
# نحدِّث قاموس الجلسات
self.sessions[key] = session
# الحذف
def __delitem__(self, key: uuid.UUID):
del self.sessions[key]
# التمثيل / العرض
def __repr__(self):
# تخصيص سطر لكل جلسة
return "\n".join(f"{k}: {v}" for k, v in self.sessions.items())
والآن ننشيء هذا الحاوي ونخزِّن فيه جلسات:
التعيين:
العد:
العضوية:
الإشارة:
Session(id=54ab55e8-6506-4804-a63a-b8bdfdbabcf8, expires_at=2025-05-02 17:24:13.293590, data={'dark_mode': True})
لاحظ كيف تظهر هذه الجلسات:
3ae3b096-246b-4948-a0c2-291e74b8be81: Session(id=54ab55e8-6506-4804-a63a-b8bdfdbabcf8, expires_at=2025-05-02 17:24:13.293590, data={'dark_mode': True})
5eb707ed-be91-4f3b-9de2-87d5d6288534: Session(id=4106e04f-b4c7-4818-976e-840eb327588c, expires_at=2025-05-02 17:24:13.305478, data={'language': 'English', 'country': 'Egypt'})
والحذف: