21  المعاملات المخصصة

تأمل التالي وتوقَّع النتيجة وعلل إجابتك. ما هي نتيجة:

القطعة الأولى:

[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 + v2
<4, 6>
v1 - v2
<-2, -2>
v1 * v2
<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):

v1 = Vector2D(1, 2)
v1 + 3
Vector(4, 5)

لكن لاحظ أنك لو وضعت العدد أولاً فسيظهر خطأ:

3 + v1
---------------------------------------------------------------------------
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

والآن كلاهما يعمل بشكل صحيح:

v1 = Vector2D(1, 2)
3 + v1
Vector(4, 5)
v1 + 3
Vector(4, 5)

إطلاق العناصر

وهذا مثال لعدم تقييد المتجَّه بعنصرين (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

والآن يمكننا إنشاء متجهين ووضع المعاملات بينهما:

v1 = Vector((1, 2))
v2 = Vector((3, 4))

print(v1 + v2)
print(v1 - v2)
<4, 6>
<-2, -2>
v1 * v2
<3, 8>
v1 * 2
<2, 4>
2 * v1
<2, 4>

معاملات الحاويات

وقد تعرَّفنا على القائمة (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 Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
    
    # التمثيل
    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"
    
    # حساب المسافة
    @staticmethod
    def distance(p1: 'Point', p2: 'Point') -> float:
        return ((p1.x - p2.x)**2 + (p1.y - p2.y)**2)**0.5

ثم نعرف المضلَّع أنه نوع حاوية لمجموعة من النقاط:

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)

لاحظ استعمال الأفعال المخصوصة بالحاويات.

الآن ننشيء مضلَّع، بسلسلة من النقاط، ونرى كيف يتم تمثيله:

poly = Polygon([
    Point(0, 0),
    Point(3, 0),
    Point(2, 2),
    Point(0, 6),
])
poly

+---------+
|         |
|         |
|*        |
|         |
|         |
|         |
|  *      |
|         |
|*  *     |
+---------+

ثم الإشارة برقم أو بشريحة:

print(poly[0])
print(poly[-1])
print(poly[1:3])
print(poly[::-1])
Point(0, 0)
Point(0, 6)
[Point(3, 0), Point(2, 2)]
[Point(0, 6), Point(2, 2), Point(3, 0), Point(0, 0)]

ثم التعيين:

poly[0] = Point(10, 10)
poly

+---------+
|        *|
|         |
|*        |
|         |
|         |
|         |
|  *      |
|         |
|   *     |
+---------+

وأخيرًا الحذف:

del poly[0]
poly

+---------+
|         |
|         |
|*        |
|         |
|         |
|         |
|  *      |
|         |
|   *     |
+---------+

المثال الثاني: جلسات المستخدمين

بين أيدينا شيء يخزن بيانات (هي قاموس) لمدة مؤقتة ثم يمحى هذا الشيء المخزَّن. وذلك يستعمل بكثرة في المواقع حيث تخزَّن بيانات التسجيل أثناء جلسة التصفح ليكون الوصول إليها سريعًا بدل الرجوع لقاعدة البيانات في كل مرة.

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())

والآن ننشيء هذا الحاوي ونخزِّن فيه جلسات:

storage = SessionStorage(expires_in=timedelta(days=30))

التعيين:

user_id1 = uuid.uuid4()

storage[user_id1] = {
    'dark_mode': True,
}
user_id2 = uuid.uuid4()

storage[user_id2] = {
    'language': 'English',
    'country': 'Egypt',
}

العد:

len(storage)
2

العضوية:

user_id1 in storage
True

الإشارة:

storage[user_id1]
Session(id=54ab55e8-6506-4804-a63a-b8bdfdbabcf8, expires_at=2025-05-02 17:24:13.293590, data={'dark_mode': True})

لاحظ كيف تظهر هذه الجلسات:

storage
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'})

والحذف:

del storage[user_id1]