هل تساءلت يومًا كيف تجعل بايثون الكائنات تعمل مع العوامل مثل + أو -؟ أو كيف تعرف كيفية عرض الكائنات عند طباعتها؟ الجواب يكمن في طرق بايثون السحرية، المعروفة أيضًا باسم طرق دندر (double under).

طرق السحر هي طرق خاصة تتيح لك تعريف كيفية تصرف كائناتك استجابةً لعمليات مختلفة ووظائف مدمجة. إنها ما يجعل برمجة بايثون الشيئية قوية وبديهية للغاية.

في هذا الدليل، ستتعلم كيفية استخدام طرق السحر لإنشاء كود أكثر أناقة وقوة. سترى أمثلة عملية توضح كيفية عمل هذه الطرق في سيناريوهات العالم الحقيقي.

المتطلبات المسبقة

  • فهم أساسي لقواعد بايثون ومفاهيم البرمجة الشيئية.

  • إلمام بالفئات، الكائنات، والوراثة.

  • معرفة بأنواع البيانات المدمجة في بايثون (القوائم، القواميس، وهكذا).

  • يوصى بوجود تثبيت بايثون 3 يعمل للتفاعل بنشاط مع الأمثلة هنا.

فهرس المحتويات

  1. ما هي الطرق السحرية؟

  2. تمثيل الكائن

  3. تحميل المشغل

  4. طرق الحاوية

  5. الوصول إلى الخصائص

  6. مديري السياق

  7. كائنات الاستدعاء

  8. طرق السحر المتقدمة

  9. اعتبارات الأداء

  10. أفضل الممارسات

  11. الاختتام

  12. المراجع

ما هي الطرق السحرية؟

الطرق السحرية في بايثون هي طرق خاصة تبدأ وتنتهي بشرطتين سفليتين (__). عندما تستخدم عمليات أو دوال معينة على كائناتك، تستدعي بايثون هذه الطرق تلقائيًا.

على سبيل المثال، عندما تستخدم عامل + على كائنين، تبحث بايثون عن طريقة __add__ في المعامل الأيسر. إذا وجدتها، تستدعي تلك الطريقة مع المعامل الأيمن كوسيط.

إليك مثال بسيط يوضح كيف يعمل هذا:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2  # هذا يستدعي p1.__add__(p2)
print(p3.x, p3.y)  # الناتج: 4 6

دعنا نفصل ما يحدث هنا:

  1. نقوم بإنشاء فئة Point التي تمثل نقطة في الفضاء ثنائي الأبعاد

  2. تقوم طريقة __init__ بتهيئة إحداثيات x و y

  3. تحدد طريقة __add__ ما يحدث عندما نضيف نقطتين

  4. عندما نكتب p1 + p2، تستدعي بايثون تلقائيًا p1.__add__(p2)

  5. النتيجة هي نقطة جديدة Point بإحداثيات (4، 6)

هذه مجرد البداية. لدى بايثون العديد من الطرق السحرية التي تتيح لك تخصيص كيفية تصرف الكائنات الخاصة بك في مواقف مختلفة. دعنا نستكشف بعضًا من أكثرها فائدة.

تمثيل الكائن

عند العمل مع الكائنات في بايثون، تحتاج غالبًا إلى تحويلها إلى سلاسل نصية. يحدث هذا عندما تقوم بطباعة كائن أو تحاول عرضه في وحدة التحكم التفاعلية. يوفر بايثون طريقتين سحريتين لهذا الغرض: __str__ و __repr__.

str مقابل repr

تخدم طريقتا __str__ و __repr__ أغراضًا مختلفة:

  • __str__: يتم استدعاؤها بواسطة دالة str() ودالة print(). يجب أن تعيد سلسلة نصية قابلة للقراءة للمستخدمين النهائيين.

  • __repr__: يتم استدعاؤها بواسطة دالة repr() وتستخدم في وحدة التحكم التفاعلية. يجب أن تعيد سلسلة نصية يمكن، في المثالي، استخدامها لإعادة إنشاء الكائن.

إليك مثال يوضح الفرق:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __str__(self):
        return f"{self.celsius}°C"

    def __repr__(self):
        return f"Temperature({self.celsius})"

temp = Temperature(25)
print(str(temp))      # الناتج: 25°C
print(repr(temp))     # الناتج: Temperature(25)

في هذا المثال:

  • __str__ يعيد سلسلة سهلة القراءة تُظهر درجة الحرارة مع رمز الدرجة

  • __repr__ يعيد سلسلة تُظهر كيفية إنشاء الكائن، مما يكون مفيدًا في تصحيح الأخطاء

يصبح الفرق واضحًا عندما تستخدم هذه الكائنات في سياقات مختلفة:

  • عندما تطبع درجة الحرارة، ترى النسخة السهلة القراءة: 25°C

  • عندما تفحص الكائن في وحدة تحكم بايثون، ترى النسخة التفصيلية: Temperature(25)

مثال عملي: فئة خطأ مخصصة

دعنا ننشئ فئة خطأ مخصصة تقدم معلومات أفضل لتصحيح الأخطاء. هذا المثال يوضح كيف يمكنك استخدام __str__ و__repr__ لجعل رسائل الخطأ الخاصة بك أكثر فائدة:

class ValidationError(Exception):
    def __init__(self, field, message, value=None):
        self.field = field
        self.message = message
        self.value = value
        super().__init__(self.message)

    def __str__(self):
        if self.value is not None:
            return f"Error in field '{self.field}': {self.message} (got: {repr(self.value)})"
        return f"Error in field '{self.field}': {self.message}"

    def __repr__(self):
        if self.value is not None:
            return f"ValidationError(field='{self.field}', message='{self.message}', value={repr(self.value)})"
        return f"ValidationError(field='{self.field}', message='{self.message}')"

# الاستخدام
try:
    age = -5
    if age < 0:
        raise ValidationError("age", "Age must be positive", age)
except ValidationError as e:
    print(e)  # الناتج: خطأ في الحقل 'العمر': يجب أن يكون العمر موجبًا (القيمة: -5)

توفر هذه الفئة المخصصة من الأخطاء عدة فوائد:

  1. تشمل اسم الحقل الذي حدث فيه الخطأ

  2. تظهر القيمة الفعلية التي تسببت في الخطأ

  3. توفر رسائل خطأ سهلة الاستخدام ومفصلة

  4. تسهل تصحيح الأخطاء من خلال تضمين جميع المعلومات ذات الصلة

تحميل المشغل

يعتبر تحميل المشغل أحد أقوى ميزات طرق السحر في بايثون. يسمح لك بتعريف كيفية تصرف كائناتك عند استخدامها مع مشغلين مثل +، -، *، و==. وهذا يجعل كودك أكثر سهولة في الفهم وقراءة.

المشغلين الحسابيين

توفر بايثون طرق سحرية لجميع العمليات الحسابية الأساسية. إليك جدول يوضح أي طريقة تتوافق مع أي مشغل:

المشغل طريقة السحر الوصف
+ __add__ الجمع
- __sub__ الطرح
* __mul__ الضرب
/ __truediv__ القسمة
// __floordiv__ القسمة الصحيحة
% __mod__ الباقي
** __pow__ الأس

عمليات المقارنة

بالمثل، يمكنك تحديد كيفية مقارنة الكائنات الخاصة بك باستخدام هذه الأساليب السحرية:

العامل الطريقة السحرية الوصف
== __eq__ مساو لـ
!= __ne__ غير مساو لـ
< __lt__ أقل من
> __gt__ أكبر من
<= __le__ أقل من أو يساوي
>= __ge__ أكبر من أو يساوي

مثال عملي: فئة النقود

دعونا ننشئ فئة Money التي تتعامل مع عمليات العملات بشكل صحيح. توضح هذه المثال كيفية تنفيذ عدة عمليات والتعامل مع الحالات الحدية:

from functools import total_ordering
from decimal import Decimal

@total_ordering  # تنفذ جميع طرق المقارنة بناءً على __eq__ و __lt__
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = Decimal(str(amount))
        self.currency = currency

    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot add different currencies: {self.currency} and {other.currency}")
        return Money(self.amount + other.amount, self.currency)

    def __sub__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract different currencies: {self.currency} and {other.currency}")
        return Money(self.amount - other.amount, self.currency)

    def __mul__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount * Decimal(str(other)), self.currency)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, (int, float, Decimal)):
            return Money(self.amount / Decimal(str(other)), self.currency)
        return NotImplemented

    def __eq__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.currency == other.currency and self.amount == other.amount

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        if self.currency != other.currency:
            raise ValueError(f"Cannot compare different currencies: {self.currency} and {other.currency}")
        return self.amount < other.amount

    def __str__(self):
        return f"{self.currency} {self.amount:.2f}"

    def __repr__(self):
        return f"Money({repr(float(self.amount))}, {repr(self.currency)})"

دعونا نفصل الميزات الرئيسية لهذه الفئة Money:

  1. التعامل مع الدقة: نستخدم Decimal بدلاً من float لتجنب مشاكل دقة النقطة العائمة في حسابات المال.

  2. سلامة العملات: تمنع الفئة العمليات بين العملات المختلفة لتجنب الأخطاء.

  3. التحقق من النوع: تتحقق كل طريقة مما إذا كان المعامل الآخر من النوع الصحيح باستخدام isinstance().

  4. NotImplemented: عندما لا يكون للعملية معنى، نعيد NotImplemented لندع بايثون يحاول العملية العكسية.

  5. @total_ordering: يقوم هذا المزين تلقائيًا بتنفيذ جميع طرق المقارنة بناءً على __eq__ و __lt__.

إليك كيفية استخدام فئة Money:

# الحساب الحسابي الأساسي
wallet = Money(100, "USD")
expense = Money(20, "USD")
remaining = wallet - expense
print(remaining)  # الناتج: USD 80.00

# العمل مع عملات مختلفة
salary = Money(5000, "USD")
bonus = Money(1000, "USD")
total = salary + bonus
print(total)  # الناتج: USD 6000.00

# القسمة على عدد محدد
weekly_pay = salary / 4
print(weekly_pay)  # الناتج: USD 1250.00

# المقارنات
print(Money(100, "USD") > Money(50, "USD"))  # الناتج: True
print(Money(100, "USD") == Money(100, "USD"))  # الناتج: True

# التعامل مع الأخطاء
try:
    Money(100, "USD") + Money(100, "EUR")
except ValueError as e:
    print(e)  # الناتج: لا يمكن إضافة عملات مختلفة: USD و EUR

توضح فئة Money العديد من المفاهيم الهامة:

  1. كيفية التعامل مع أنواع مختلفة من العناصر

  2. كيفية تنفيذ معالجة الأخطاء الصحيحة

  3. كيفية استخدام مزين @total_ordering

  4. كيفية الحفاظ على الدقة في الحسابات المالية

  5. كيفية توفير طرق السلسلة والتمثيل

طرق الحاوية

تتيح لك طرق الحاوية جعل كائناتك تتصرف مثل الحاويات المدمجة مثل القوائم أو القواميس أو المجموعات. هذا مفيد بشكل خاص عندما تحتاج إلى سلوك مخصص لتخزين واسترجاع البيانات.

بروتوكول التسلسل

لجعل كائنك يتصرف مثل تسلسل (مثل قائمة أو مجموعة)، تحتاج إلى تنفيذ هذه الطرق:

الطريقة الوصف مثال على الاستخدام
__len__ يعيد طول الحاوية len(obj)
__getitem__ يسمح بالترميز باستخدام obj[key] obj[0]
__setitem__ يسمح بالتعيين باستخدام obj[key] = value obj[0] = 42
__delitem__ يسمح بالحذف باستخدام del obj[key] del obj[0]
__iter__ يعيد مكررًا للحاوية for item in obj:
__contains__ تنفيذ عامل in 42 in obj

بروتوكول الرسم

للحصول على سلوك شبيه بالقاموس ، ستحتاج إلى تنفيذ هذه الطرق:

الطريقة الوصف استخدام مثالي
__getitem__ الحصول على القيمة بواسطة المفتاح obj["key"]
__setitem__ تعيين القيمة بواسطة المفتاح obj["key"] = value
__delitem__ حذف زوج المفتاح-القيمة del obj["key"]
__len__ الحصول على عدد أزواج المفتاح-القيمة len(obj)
__iter__ التكرار عبر المفاتيح for key in obj:
__contains__ التحقق مما إذا كان المفتاح موجودًا "key" in obj

مثال عملي: ذاكرة مخصصة

لنقم بتنفيذ ذاكرة معتمدة على الوقت تنتهي صلاحيتها تلقائيًا. يوضح هذا المثال كيفية إنشاء حاوية مخصصة تتصرف مثل قاموس ولكن بوظائف إضافية:

import time
from collections import OrderedDict

class ExpiringCache:
    def __init__(self, max_age_seconds=60):
        self.max_age = max_age_seconds
        self._cache = OrderedDict()  # {المفتاح: (القيمة، الطابع الزمني)}

    def __getitem__(self, key):
        if key not in self._cache:
            raise KeyError(key)

        value, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            raise KeyError(f"Key '{key}' has expired")

        return value

    def __setitem__(self, key, value):
        self._cache[key] = (value, time.time())
        self._cache.move_to_end(key)  # انتقل إلى النهاية للحفاظ على ترتيب الإدخال

    def __delitem__(self, key):
        del self._cache[key]

    def __len__(self):
        self._clean_expired()  # نظف العناصر المنتهية قبل الإبلاغ عن الطول
        return len(self._cache)

    def __iter__(self):
        self._clean_expired()  # نظف العناصر المنتهية قبل التكرار
        for key in self._cache:
            yield key

    def __contains__(self, key):
        if key not in self._cache:
            return False

        _, timestamp = self._cache[key]
        if time.time() - timestamp > self.max_age:
            del self._cache[key]
            return False

        return True

    def _clean_expired(self):
        """Remove all expired entries from the cache."""
        now = time.time()
        expired_keys = [
            key for key, (_, timestamp) in self._cache.items()
            if now - timestamp > self.max_age
        ]
        for key in expired_keys:
            del self._cache[key]

دعونا نفصل كيف يعمل هذا التخزين المؤقت:

  1. التخزين: يستخدم التخزين المؤقت OrderedDict لتخزين أزواج المفتاح والقيمة مع الطوابع الزمنية.

  2. الانتهاء: يتم تخزين كل قيمة كزوج مرتب من (القيمة، الطابع الزمني). عند الوصول إلى قيمة، نتحقق مما إذا كانت قد انتهت صلاحيتها.

  3. طرق الحاوية: تقوم الفئة بتنفيذ جميع الطرق اللازمة لتتصرف كقاموس:

    • __getitem__: يسترجع القيم ويتحقق من انتهاء الصلاحية

    • __setitem__: يخزن القيم مع الطابع الزمني الحالي

    • __delitem__: يزيل الإدخالات

    • __len__: يُرجع عدد الإدخالات غير المنتهية

    • __iter__: يتكرر على المفاتيح غير المنتهية

    • __contains__: يتحقق مما إذا كان المفتاح موجودًا

إليك كيفية استخدام الذاكرة المؤقتة:

# إنشاء ذاكرة تخزين مؤقتة بانتهاء صلاحية بعد ثانيتين
cache = ExpiringCache(max_age_seconds=2)

# تخزين بعض القيم
cache["name"] = "Vivek"
cache["age"] = 30

# الوصول إلى القيم
print("name" in cache)  # الناتج: صحيح
print(cache["name"])    # الناتج: فيفيك
print(len(cache))       # الناتج: 2

# انتظر انتهاء الصلاحية
print("Waiting for expiration...")
time.sleep(3)

# التحقق من القيم المنتهية الصلاحية
print("name" in cache)  # الناتج: غير صحيح
try:
    print(cache["name"])
except KeyError as e:
    print(f"KeyError: {e}")  # الناتج: KeyError: 'name'

print(len(cache))  # الناتج: 0

توفر هذه الطريقة لتخزين البيانات المؤقتة عدة فوائد:

  1. انتهاء تلقائي للبيانات القديمة

  2. واجهة شبيهة بالقواميس لسهولة الاستخدام

  3. كفاءة الذاكرة عن طريق إزالة البيانات المنتهية الصلاحية

  4. عمليات آمنة للموضوع (بشرط الوصول من خلال خط واحد)

  5. الحفاظ على ترتيب الإدخالات

الوصول إلى السمات

تتيح لك طرق الوصول إلى السمات التحكم في كيفية معالجة كائناتك للحصول على السمات وتعيينها وحذفها. هذا مفيد بشكل خاص لتنفيذ الخصائص والتحقق من صحة البيانات وتسجيل الأحداث.

getattr و getattribute

توفر بايثون طريقتين للتحكم في الوصول إلى السمات:

  1. __getattr__: يتم استدعاؤها فقط عندما يفشل البحث عن سمة (أي عندما لا توجد السمة)

  2. __getattribute__: يتم استدعاؤها لكل وصول للسمة، حتى بالنسبة للسمات الموجودة

الفرق الرئيسي هو أن __getattribute__ يتم استدعاؤها لكل وصول للسمة، بينما يتم استدعاء __getattr__ فقط عندما لا يتم العثور على السمة من خلال الوسائل العادية.

إليك مثال بسيط يوضح الفرق:

class AttributeDemo:
    def __init__(self):
        self.name = "Vivek"

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        return f"Default value for {name}"

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

demo = AttributeDemo()
print(demo.name)      # المخرج: تم استدعاء __getattribute__ للاسم
                      #        Vivek
print(demo.age)       # المخرج: تم استدعاء __getattribute__ للعمر
                      #        تم استدعاء __getattr__ للعمر
                      #        القيمة الافتراضية للعمر

setattr و delattr

وبالمثل، يمكنك التحكم في كيفية تعيين السمات وحذفها:

  1. __setattr__: يتم استدعاؤها عند تعيين سمة

  2. __delattr__: يتم استدعاؤها عند حذف سمة

تتيح لك هذه الأساليب تنفيذ التحقق من الصحة وتدقيق السجلات أو السلوك المخصص عند تعديل السمات.

مثال عملي: خصائص التسجيل التلقائي

لنقم بإنشاء فئة تقوم تلقائيًا بتسجيل جميع تغييرات الخاصية. هذا مفيد لأغراض تصحيح الأخطاء والتدقيق وتتبع تغييرات حالة الكائن:

import logging

# إعداد التسجيل
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class LoggedObject:
    def __init__(self, **kwargs):
        self._data = {}
        # تهيئة السمات دون تنشيط __setattr__
        for key, value in kwargs.items():
            self._data[key] = value

    def __getattr__(self, name):
        if name in self._data:
            logging.debug(f"Accessing attribute {name}: {self._data[name]}")
            return self._data[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __setattr__(self, name, value):
        if name == "_data":
            # السماح بتعيين سمة _data مباشرة
            super().__setattr__(name, value)
        else:
            old_value = self._data.get(name, "<undefined>")
            self._data[name] = value
            logging.info(f"Changed {name}: {old_value} -> {value}")

    def __delattr__(self, name):
        if name in self._data:
            old_value = self._data[name]
            del self._data[name]
            logging.info(f"Deleted {name} (was: {old_value})")
        else:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

لنقم بتفكيك كيفية عمل هذه الفئة:

  1. التخزين: تستخدم الفئة قاموسًا خاصًا _data لتخزين قيم السمات.

  2. الوصول إلى السمات:

    • __getattr__: يعيد القيم من _data ويسجل رسائل التصحيح

    • __setattr__: يخزن القيم في _data ويسجل التغييرات

    • __delattr__: يقوم بإزالة القيم من _data ويسجل عمليات الحذف

  3. معالجة خاصة: يتم التعامل مع خاصية _data بشكل مختلف لتجنب التكرار اللانهائي.

إليك كيفية استخدام الفئة:

# إنشاء كائن مسجل بقيم أولية
user = LoggedObject(name="Vivek", email="[email protected]")

# تعديل الخصائص
user.name = "Vivek"  # السجلات: تم تغيير الاسم: Vivek -> Vivek
user.age = 30         # السجلات: تم تغيير العمر: <غير محدد> -> 30

# الوصول إلى الخصائص
print(user.name)      # الناتج: Vivek

# حذف الخصائص
del user.email        # السجلات: تم حذف البريد الإلكتروني (كان: [email protected])

# حاول الوصول إلى الخاصية المحذوفة
try:
    print(user.email)
except AttributeError as e:
    print(f"AttributeError: {e}")  # الناتج: AttributeError: لا يحتوي كائن 'LoggedObject' على خاصية 'email'

يوفر هذا التنفيذ العديد من الفوائد:

  1. تسجيل تلقائي لجميع تغييرات الخصائص

  2. تسجيل على مستوى تصحيح الأخطاء للوصول إلى الخصائص

  3. رسائل خطأ واضحة لغياب الخصائص

  4. تتبع سهل لتغييرات حالة الكائن

  5. مفيد لتصحيح الأخطاء والتدقيق

مديري السياق

تعد مدراء السياقات ميزة قوية في لغة البرمجة Python تساعدك في إدارة الموارد بشكل صحيح. إنها تضمن الحصول على الموارد بشكل صحيح وإطلاقها، حتى إذا حدث خطأ. تعتبر البيانة with هي الطريقة الأكثر شيوعًا لاستخدام مدراء السياقات.

دخول وخروج

لإنشاء مدير سياق، تحتاج إلى تنفيذ طريقتين سحريتين:

  1. __enter__: تُستدعى عند دخول البيانة with. يجب أن تقوم بإرجاع المورد الذي يجب إدارته.

  2. __exit__: تُستدعى عند خروج البيانة with، حتى إذا حدث استثناء. يجب أن تتعامل مع النظافة.

تتلقى طريقة __exit__ ثلاثة معاملات:

  • exc_type: نوع الاستثناء (إذا كان هناك)

  • exc_val: نسخة الاستثناء (إذا كان هناك)

  • exc_tb: سجل التتبع (إذا كان هناك)

مثال عملي: مدير اتصال قاعدة بيانات

لنقم بإنشاء مدير سياق لاتصالات قاعدة البيانات. يوضح هذا المثال كيفية إدارة الموارد الخاصة بقاعدة البيانات بشكل صحيح ومعالجة العمليات:

import sqlite3
import logging

# إعداد تسجيل الأحداث
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

class DatabaseConnection:
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
        self.cursor = None

    def __enter__(self):
        logging.info(f"Connecting to database: {self.db_path}")
        self.connection = sqlite3.connect(self.db_path)
        self.cursor = self.connection.cursor()
        return self.cursor

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            logging.error(f"An error occurred: {exc_val}")
            self.connection.rollback()
            logging.info("Transaction rolled back")
        else:
            self.connection.commit()
            logging.info("Transaction committed")

        if self.cursor:
            self.cursor.close()
        if self.connection:
            self.connection.close()

        logging.info("Database connection closed")

        # إرجاع False لنشر الاستثناءات، و True لإخفائها
        return False

لنقم بتفكيك كيفية عمل هذا المدير السياق:

  1. التهيئة:

    • يستغرق الفئة مسار قاعدة البيانات

    • يقوم بتهيئة الاتصال والمؤشر على أنهما None

  2. طريقة الدخول:

    • ينشئ اتصالًا بقاعدة البيانات

    • ينشئ مؤشرًا

    • يعيد المؤشر للاستخدام في كتلة with

  3. طريقة الخروج:

    • تدير إدارة المعاملات (التأكيد/التراجع)

    • تغلق المؤشر والاتصال

    • تسجيل جميع العمليات

    • تعيد False لنشر الاستثناءات

هنا كيفية استخدام مدير السياق:

# أنشئ قاعدة بيانات اختبار في الذاكرة
try:
    # معاملة ناجحة
    with DatabaseConnection(":memory:") as cursor:
        # أنشئ جدولًا
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)

        # أدخل البيانات
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Vivek", "[email protected]")
        )

        # استعلم عن البيانات
        cursor.execute("SELECT * FROM users")
        print(cursor.fetchall())  # الناتج: [(1 ، 'Vivek' ، '[email protected]')]

    # قم بإظهار التراجع في المعاملة في حالة حدوث خطأ
    with DatabaseConnection(":memory:") as cursor:
        cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)
        cursor.execute(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            ("Wewake", "[email protected]")
        )
        # هذا سيتسبب في خطأ - الجدول 'nonexistent' غير موجود
        cursor.execute("SELECT * FROM nonexistent")
except sqlite3.OperationalError as e:
    print(f"Caught exception: {e}")

يوفر مدير السياق هذا عدة فوائد:

  1. يتم إدارة الموارد تلقائيًا (على سبيل المثال: يتم إغلاق الاتصالات دائمًا).

  2. مع سلامة المعاملات، يتم الالتزام بالتغييرات أو التراجع عنها بشكل مناسب.

  3. يتم التقاط الاستثناءات ومعالجتها بشكل سلس

  4. يتم تسجيل جميع العمليات لأغراض تصحيح الأخطاء

  5. تجعل عبارة with الكود واضحًا وموجزًا

الكائنات القابلة للاستدعاء

تتيح لك طريقة السحر __call__ جعل كائنات فئتك تعمل مثل الدوال. هذا مفيد لإنشاء كائنات تحافظ على الحالة بين الاستدعاءات أو لتنفيذ سلوك مشابه للدوال مع ميزات إضافية.

استدعاء

يتم استدعاء طريقة __call__ عندما تحاول استدعاء كائن من فئتك كما لو كان دالة. إليك مثال بسيط:

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

# إنشاء كائنات تعمل مثل الدوال
double = Multiplier(2)
triple = Multiplier(3)

print(double(5))  # المخرجات: 10
print(triple(5))  # المخرجات: 15

يظهر هذا المثال كيف تتيح لك __call__ إنشاء كائنات تحافظ على الحالة (العامل) بينما يمكن استدعاؤها مثل الدوال.

مثال عملي: ديكور الميمو

لنقم بتنفيذ مزين memoization باستخدام __call__. سيقوم هذا المزين بتخزين نتائج الدالة لتجنب الحسابات الزائدة:

import time
import functools

class Memoize:
    def __init__(self, func):
        self.func = func
        self.cache = {}
        # الحفاظ على بيانات الدالة (الاسم، الوثيقة، إلخ)
        functools.update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        # إنشاء مفتاح من الوسائط
        # لأسباب بساطة، نفترض أن جميع الوسائط قابلة للتجزئة
        key = str(args) + str(sorted(kwargs.items()))

        if key not in self.cache:
            self.cache[key] = self.func(*args, **kwargs)

        return self.cache[key]

# الاستخدام
@Memoize
def fibonacci(n):
    """Calculate the nth Fibonacci number recursively."""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# قياس وقت التنفيذ
def time_execution(func, *args, **kwargs):
    start = time.time()
    result = func(*args, **kwargs)
    end = time.time()
    print(f"{func.__name__}({args}, {kwargs}) took {end - start:.6f} seconds")
    return result

# بدون memoization، سيكون هذا بطيئًا للغاية
print("Calculating fibonacci(35)...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

# الاستدعاء الثاني يكون فوريًا بسبب memoization
print("\nCalculating fibonacci(35) again...")
result = time_execution(fibonacci, 35)
print(f"Result: {result}")

لنقم بتفكيك كيف يعمل مزين memoization هذا:

  1. التهيئة:

    • يأخذ دالة كوسيط

    • ينشئ قاموس ذاكرة مؤقتة لتخزين النتائج

    • يحافظ على بيانات الدالة باستخدام functools.update_wrapper

  2. طريقة الاستدعاء:

    • ينشئ مفتاحًا فريدًا من وسائط الدالة

    • يتحقق مما إذا كانت النتيجة مخزنة في الذاكرة المؤقتة

    • إذا لم تكن مخزنة، يحسب النتيجة ويقوم بتخزينها

    • يعيد النتيجة المخزنة في الذاكرة المؤقتة

  3. الاستخدام:

    • تطبيقه كزخرفة على أي دالة

    • يخزن النتائج تلقائيًا للاستدعاءات المتكررة

    • يحتفظ ببيانات الدالة وسلوكها

من فوائد هذا التنفيذ:

  1. أداء أفضل، حيث يتجنب الحسابات الزائدة

  2. أفضل، شفافية، حيث يعمل بدون تعديل الوظيفة الأصلية

  3. إنه مرن ويمكن استخدامه مع أي وظيفة

  4. إنه فعال من حيث استخدام الذاكرة ويخزن النتائج لإعادة الاستخدام

  5. إنه يحافظ على وثائق الوظيفة

طرق السحر المتقدمة

لنستكشف الآن بعض طرق السحر المتقدمة في لغة Python. توفر هذه الطرق التحكم الدقيق في إنشاء الكائنات واستخدام الذاكرة وسلوك القاموس.

الطريقة الجديدة لإنشاء الكائنات

تُستدعى الطريقة __new__ قبل __init__ وتكون مسؤولة عن إنشاء وإرجاع نسخة جديدة من الفئة. وهذا مفيد لتنفيذ أنماط مثل الكائنات المفردة أو الكائنات غير القابلة للتغيير.

فيما يلي مثال على نمط الكائن المفرد باستخدام __new__:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name=None):
        # سيتم استدعاء هذا في كل مرة يتم فيها استدعاء Singleton()
        if name is not None:
            self.name = name

# الاستخدام
s1 = Singleton("Vivek")
s2 = Singleton("Wewake")
print(s1 is s2)  # الناتج: True
print(s1.name)   # الناتج: Wewake (المرة الثانية للتهيئة قامت بالكتابة فوق الأولى)

لنقم بتفكيك كيف يعمل هذا الكائن المفرد:

  1. متغير الفئة: _instance يخزن النسخة الوحيدة من الفئة

  2. طريقة الجديدة:

    • يتحقق مما إذا كانت النسخة موجودة

    • ينشئ واحدة إذا لم تكن موجودة

    • يعيد النسخة الموجودة إذا كانت موجودة

  3. طريقة init:

    • تُستدعى في كل مرة يتم فيها استخدام المُنشئ

    • تحديث سمات النسخة

المنافذ لتحسين الذاكرة

متغير الفئة __slots__ يقيد السمات التي يمكن أن تمتلكها النسخة، مما يوفر الذاكرة. هذا مفيد بشكل خاص عندما يكون لديك العديد من النسخ من فئة تحتوي على مجموعة ثابتة من السمات.

إليك مقارنة بين الفئات العادية والفئات ذات المنافذ:

import sys

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

# قارن استخدام الذاكرة
regular_people = [RegularPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]
slotted_people = [SlottedPerson("Vivek" + str(i), 30, "[email protected]") for i in range(1000)]

print(f"Regular person size: {sys.getsizeof(regular_people[0])} bytes")  # الناتج: حجم الشخص العادي: 48 بايت
print(f"Slotted person size: {sys.getsizeof(slotted_people[0])} bytes")  # الناتج: حجم الشخص المفصّل: 56 بايت
print(f"Memory saved per instance: {sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])} bytes")  # الناتج: توفير الذاكرة لكل نسخة: -8 بايت
print(f"Total memory saved for 1000 instances: {(sys.getsizeof(regular_people[0]) - sys.getsizeof(slotted_people[0])) * 1000 / 1024:.2f} KB")  # الناتج: إجمالي توفير الذاكرة لـ 1000 نسخة: -7.81 كيلوبايت

تشغيل هذا الكود ينتج نتيجة مثيرة للاهتمام:

Regular person size: 48 bytes
Slotted person size: 56 bytes
Memory saved per instance: -8 bytes
Total memory saved for 1000 instances: -7.81 KB

من المدهش، في هذا المثال البسيط، يكون حجم النسخة المفصلة في الواقع أكبر بـ 8 بايت من النسخة العادية! هذا يبدو يتعارض مع النصيحة الشائعة حول توفير الذاكرة باستخدام __slots__.

فما الذي يحدث هنا؟ توفير الذاكرة الفعلي من __slots__ يأتي من:

  1. التخلص من القواميس: تخزن كائنات Python العادية سماتها في قاموس (__dict__)، والذي يحمل تكاليف إضافية. دالة sys.getsizeof() لا تأخذ في الاعتبار حجم هذا القاموس.

  2. تخزين السمات: بالنسبة للكائنات الصغيرة ذات عدد قليل من السمات، فإن تكاليف وصفات الفتحات قد تفوق توفير القاموس.

  3. قابلية التوسع: الفائدة الحقيقية تظهر عندما:

    • لديك العديد من الكائنات (آلاف أو ملايين)

    • لديك العديد من السمات في كائناتك

    • أنت تضيف سمات بشكل ديناميكي

دعنا نرى مقارنة أكثر شمولاً:

# قياس ذاكرة أكثر دقة
import sys

def get_size(obj):
    """Get a better estimate of the object's size in bytes."""
    size = sys.getsizeof(obj)
    if hasattr(obj, '__dict__'):
        size += sys.getsizeof(obj.__dict__)
        # إضافة حجم محتويات القاموس
        size += sum(sys.getsizeof(v) for v in obj.__dict__.values())
    return size

class RegularPerson:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

class SlottedPerson:
    __slots__ = ['name', 'age', 'email']

    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

regular = RegularPerson("Vivek", 30, "[email protected]")
slotted = SlottedPerson("Vivek", 30, "[email protected]")

print(f"Complete Regular person size: {get_size(regular)} bytes")  # الناتج: حجم شخص عادي كامل: 610 بايت
print(f"Complete Slotted person size: {get_size(slotted)} bytes")  # الناتج: حجم شخص مقسم كامل: 56 بايت

مع هذا القياس الأكثر دقة، سترى أن الكائنات المقسمة تستخدم عادة ذاكرة أقل بشكل إجمالي، خاصة مع إضافة المزيد من السمات.

النقاط الرئيسية حول __slots__:

  1. فوائد ذاكرة حقيقية: تأتي التوفير الأساسي في الذاكرة من القضاء على __dict__ الخاصة بالنسخة

  2. القيود الديناميكية: لا يمكنك إضافة سمات تعسفية إلى الكائنات المدرجة

  3. اعتبارات الوراثة: استخدام __slots__ مع الوراثة يتطلب تخطيطًا دقيقًا

  4. حالات الاستخدام: الأفضل للفئات ذات العديد من الحالات والسمات الثابتة

  5. مكافأة الأداء: يمكن أيضًا توفير وصول أسرع للسمات في بعض الحالات

المفقود لقيم القاموس الافتراضية

يتم استدعاء الطريقة __missing__ بواسطة الفئات الفرعية للقواميس عند عدم العثور على مفتاح. هذا مفيد لتنفيذ القواميس بقيم افتراضية أو إنشاء مفاتيح تلقائيًا.

إليك مثال على قاموس يقوم بإنشاء قوائم فارغة تلقائيًا للمفاتيح المفقودة:

class AutoKeyDict(dict):
    def __missing__(self, key):
        self[key] = []
        return self[key]

# الاستخدام
groups = AutoKeyDict()
groups["team1"].append("Vivek")
groups["team1"].append("Wewake")
groups["team2"].append("Vibha")

print(groups)  # الناتج: {'فريق1': ['فيفيك', 'ويويك'], 'فريق2': ['فيبها']}

يوفر هذا التنفيذ العديد من الفوائد:

  1. لا حاجة للتحقق من وجود مفتاح، مما يجعل الأمر أكثر راحة.

  2. التهيئة التلقائية تنشئ القيم الافتراضية حسب الحاجة.

  3. تقلل النص الزائد لتهيئة القاموس.

  4. أكثر مرونة ويمكن تنفيذ أي منطق قيم افتراضية.

  5. ينشئ القيم فقط عند الحاجة، مما يجعله أكثر كفاءة في الذاكرة.

اعتبارات الأداء

على الرغم من أن الطرق السحرية قوية، إلا أنها يمكن أن تؤثر على الأداء إذا لم تستخدم بعناية. دعونا نستكشف بعض الاعتبارات الشائعة للأداء وكيفية قياسها.

تأثير الطرق السحرية على الأداء

الطرق السحرية المختلفة لها تأثيرات مختلفة على الأداء:

طرق الوصول إلى السمات:

  • تُستدعى __getattr__، __getattribute__، __setattr__، و __delattr__ بشكل متكرر

  • عمليات معقدة في هذه الطرق يمكن أن تبطئ بشكل كبير من أداء الكود

طرق الحاويات:

  • __getitem__، __setitem__، و __len__ يتم استدعاؤها في الحلقات بشكل متكرر

  • التنفيذ غير الفعال يمكن أن يجعل الحاوية الخاصة بك أبطأ بكثير من أنواع البناء الداخلية

زيادة العمليات:

  • يتم استخدام المشغلات الحسابية والمقارنة بشكل متكرر

  • يمكن أن تجعل التنفيذات المعقدة العمليات البسيطة بطيئة بشكل غير متوقع

لنقيس تأثير الأداء لـ __getattr__ مقابل الوصول المباشر إلى السمات:

import time

class DirectAccess:
    def __init__(self):
        self.value = 42

class GetAttrAccess:
    def __init__(self):
        self._value = 42

    def __getattr__(self, name):
        if name == "value":
            return self._value
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

# قياس الأداء
direct = DirectAccess()
getattr_obj = GetAttrAccess()

def benchmark(obj, iterations=1000000):
    start = time.time()
    for _ in range(iterations):
        x = obj.value
    end = time.time()
    return end - start

direct_time = benchmark(direct)
getattr_time = benchmark(getattr_obj)

print(f"Direct access: {direct_time:.6f} seconds")
print(f"__getattr__ access: {getattr_time:.6f} seconds")
print(f"__getattr__ is {getattr_time / direct_time:.2f}x slower")

تشير هذه الاختبارات الحية إلى اختلافات أداء كبيرة:

Direct access: 0.027714 seconds
__getattr__ access: 0.060646 seconds
__getattr__ is 2.19x slower

كما يمكنك أن ترى، استخدام __getattr__ بطيء أكثر من ضعفين مقارنةً بالوصول المباشر إلى السمات. قد لا يهم هذا في السمات التي يتم الوصول إليها بشكل زائد، ولكنه يمكن أن يصبح مهمًا في الشفرة الحاسوبية ذات الأداء الحرج التي تقوم بالوصول إلى السمات في حلقات ضيقة.

استراتيجيات التحسين

من الحسن حظنا أن هناك طرق مختلفة يمكنك من خلالها تحسين الأساليب السحرية.

  1. استخدم الفتحات لكفاءة الذاكرة: يقلل هذا من استخدام الذاكرة ويحسن سرعة الوصول إلى السمات. إنه الأفضل للفئات ذات العديد من الحالات.

  2. خزن القيم المحسوبة: يمكنك تخزين نتائج العمليات المكلفة وتحديث التخزين المؤقت فقط عند الضرورة. استخدم @property للسمات المحسوبة.

  3. قلل من استدعاءات الأساليب: تأكد من تجنب استدعاءات الأساليب السحرية غير الضرورية واستخدم الوصول المباشر إلى السمات عند الإمكان. ضع في اعتبارك استخدام __slots__ للسمات التي يتم الوصول إليها بشكل متكرر.

أفضل الممارسات

عند استخدام الأساليب السحرية، اتبع هذه الممارسات الأفضل لضمان أن يكون الكود الخاص بك قابل للصيانة، فعال، وموثوق.

1. كن متسقًا

عند تنفيذ الأساليب السحرية ذات الصلة، حافظ على الاتساق في السلوك:

from functools import total_ordering

@total_ordering
class ConsistentNumber:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value == other.value

    def __lt__(self, other):
        if not isinstance(other, ConsistentNumber):
            return NotImplemented
        return self.value < other.value

2. ارجع NotImplemented

عندما لا تكون العملية منطقية، ارجع NotImplemented للسماح لـ Python بمحاولة العملية المعاكسة:

class Money:
    def __add__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        # ... بقية التنفيذ

3. اجعلها بسيطة

يجب أن تكون الطرق السحرية بسيطة وقابلة للتنبؤ. تجنب المنطق المعقد الذي قد يؤدي إلى سلوك غير متوقع:

# جيد: بسيط وقابل للتنبؤ
class SimpleContainer:
    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]

# سيء: معقد وقد يكون مربكًا
class ComplexContainer:
    def __init__(self):
        self.items = []
        self.access_count = 0

    def __getitem__(self, index):
        self.access_count += 1
        if self.access_count > 100:
            raise RuntimeError("Too many accesses")
        return self.items[index]

4. توثيق السلوك

قم بتوثيق كيفية عمل الطرق السحرية الخاصة بك بوضوح، خاصة إذا كانت تنحرف عن التوقعات القياسية:

class CustomDict(dict):
    def __missing__(self, key):
        """
        Called when a key is not found in the dictionary.
        Creates a new list for the key and returns it.
        This allows for automatic list creation when accessing
        non-existent keys.
        """
        self[key] = []
        return self[key]

5. النظر في الأداء

كن واعيًا لتداعيات الأداء، خاصة بالنسبة للطرق التي يتم استدعاؤها بشكل متكرر:

class OptimizedContainer:
    __slots__ = ['items']  # استخدم __slots__ لتحسين الأداء

    def __init__(self):
        self.items = []

    def __getitem__(self, index):
        return self.items[index]  # الوصول المباشر أسرع

6. التعامل مع الحالات الحدودية

دائمًا اعتبر الحالات الحدودية وتعامل معها بشكل مناسب:

class SafeContainer:
    def __getitem__(self, key):
        if not isinstance(key, (int, slice)):
            raise TypeError("Index must be integer or slice")
        if key < 0:
            raise ValueError("Index cannot be negative")
        # ... بقية التنفيذ

الخلاصة

توفر الطرق السحرية في بايثون وسيلة قوية لجعل الفئات الخاصة بك تتصرف مثل الأنواع المدمجة، مما يمكّن من كتابة كود أكثر حدسية وتعبيرية. على مدار هذا الدليل، استكشفنا كيفية عمل هذه الطرق وكيفية استخدامها بشكل فعال.

النقاط الأساسية

  1. تمثيل الكائنات:

    • استخدم __str__ للإخراج الصديق للمستخدم

    • استخدم __repr__ للتصحيح والتطوير

  2. تحميل المشغل:

    • قم بتنفيذ العمليات الحسابية وعمليات المقارنة

    • قم بإرجاع NotImplemented للعمليات غير المدعومة

    • استخدم @total_ordering لمقارنات متسقة

  3. سلوك الحاوية:

    • قم بتنفيذ بروتوكولات التسلسل والتخطيط

    • ضع في اعتبارك الأداء للعمليات المستخدمة بشكل متكرر

    • تعامل مع الحالات الحدية بشكل مناسب

  4. إدارة الموارد:

    • استخدم مديري السياق للتعامل المناسب مع الموارد

    • قم بتنفيذ __enter__ و __exit__ للتنظيف

    • تعامل مع الاستثناءات في __exit__

  5. تحسين الأداء:

    • استخدم __slots__ لكفاءة الذاكرة

    • قم بتخزين القيم المحسوبة عند الاقتضاء

    • قلل من استدعاءات الطرق في الشيفرة المستخدمة بشكل متكرر

متى تستخدم طرق السحر

تكون طرق السحر الأكثر فائدة عندما تحتاج إلى:

  1. إنشاء هياكل بيانات مخصصة

  2. تنفيذ أنواع محددة للنطاق

  3. إدارة الموارد بشكل صحيح

  4. إضافة سلوك خاص إلى الفئات الخاصة بك

  5. جعل رمزك أكثر تقنية بايثون

متى يجب تجنب الأساليب السحرية

تجنب الأساليب السحرية عندما:

  1. يكون الوصول البسيط إلى السمات كافياً

  2. سيكون السلوك غامضًا أو غير متوقع

  3. الأداء هو أمر حاسم وسيتسبب الأساليب السحرية في زيادة العبء

  4. سيكون التنفيذ معقدًا جدًا

تذكر أن القوة الكبيرة تأتي مع مسؤولية كبيرة. استخدم الأساليب السحرية بحكمة، مع مراعاة آثار الأداء ومبدأ الأدنى من المفاجآت. عند استخدامها بشكل مناسب، يمكن للأساليب السحرية أن تعزز بشكل كبير قابلية قراءة رمزك وتعبيرية.

المراجع والقراءة الإضافية

الوثائق الرسمية لبايثون

  1. نموذج البيانات في بايثون – الوثائق الرسمية – دليل شامل عن نموذج البيانات في بايثون والأساليب السحرية.

  2. functools.total_ordering – وثائق لديكوريتور total_ordering الذي يملأ تلقائيا الأساليب المفقودة للمقارنة.

  3. أسماء الأساليب الخاصة في بايثون – الإشارة الرسمية لمعرفات الأساليب الخاصة في بايثون.

  4. الفصول المجردة للصنف التجميعات – تعرف على الفصول المجردة للحاويات التي تحدد الواجهات التي يمكن لصنف الحاوية الخاص بك تنفيذها.

مصادر المجتمع

  1. دليل لطرق السحر في لغة بايثون – ريف كتلر – أمثلة عملية للطرق السحرية وحالات الاستخدام الشائعة.

قراءة إضافية

إذا كنت قد استمتعت بهذه المقالة، فقد تجد المقالات المتعلقة بلغة بايثون على مدونتي الشخصية مفيدة:

  1. تجارب عملية لتحسين استعلامات Django ORM – تعلم كيفية تحسين استعلامات Django ORM الخاصة بك من خلال أمثلة وتجارب عملية.

  2. تكلفة عالية لمعالجة uWSGI المتزامنة – فهم تأثيرات الأداء للمعالجة المتزامنة في uWSGI وكيف يؤثر ذلك على تطبيقات الويب بلغة Python الخاصة بك.