الوحدة weakref‎ في بايثون

من موسوعة حسوب


تتيح وحدة wekref إنشاء إشارات ضعيفة weak references للكائنات.

سنستخدم مصطلح الكائن المشار إليه للتعبير عن الكائن الذي يُشار إليه بإشارة ضعيفة.

لا تكون الإشارة الضعيفة إلى كائن معيّن كافية للإبقاء عليه، فعندما لا تبقى إلا الإشارات الضعيفة إلى الكائن المشار إليه، فلا يوجد ما يمنع مجموعة garbage من تدمير الكائن المشار إليه وإعادة استخدام الذاكرة التي يشغلها لأمر آخر. ولكن إلى حين تدمير الكائن فإنّ بإمكان الإشارة الضعيفة أن تعيد الكائن حتى في حال عدم وجود أي إشارات قوية strong إليه.

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

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

تقدّم وحدة weakref الصنفين WeakKeyDictionary و WeakValueDictionary كحل بديل، حيث تُستخدم الإشارات الضعيفة لبناء روابط لا تبقى على الكائنات لمجرّد ظهورها في كائنات الربط.

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

تستخدم WeakKeyDictionary و WeakValueDictionary الإشارات الضعيفة في عملها، حيث يجري إعداد دوال استدعاء خلفي على الإشارات الضعيفة والتي تنبه القواميس الضعيفة عندما تسترجع مجموعة garbage مفتاحًا أو قيمة من القاموس.

تتعامل WeakSet مع المجموعات، ولكنّها تبقي على الإشارات الضعيفة إلى عناصرها، كما هو الحال مع WeakKeyDictionary.

تقدم finalize طريقة مباشرة لتسجيل دوال التنظيف وذلك لاستدعائها عند استرجاع الكائن بواسطة مجموعة garbage، وهذه الطريقة أبسط من إعداد دالة استدعاء خلفي على إشارة ضعيفة، لأنّ الوحدة ستضمن على نحو تلقائي أنّ دالة الإنهاء finalizer ستبقى حيّة إلى حين جمع الكائن بواسطة مجموعة garbage.

يمكن لأنواع الحاويات الضعيفة أو التابع finalize أن تسد احتياجات معظم البرامج، وعادة ما لا تكون هناك حاجة إلى إنشاء إشارات ضعيفة خاصة. تتيح وحدة weakref التعامل مع المستوى الواطئ low-level وذلك لإنجاز مهام متقدّمة ومعقدة.

لا يمكن إنشاء إشارة ضعيفة لجميع الكائنات، ولكن يمكن إنشاء مثل هذه الإشارة لنسخ الأصناف، والدوال المكتوبة في بايثون (وليست مكتوبة في C)، وتوابع النسخ، والمجموعات، والمجموعات المجمّدة، وبعض كائنات الملفات، والمولّدات، وكائنات النوع، والمنافذ sockets، والمصفوفات، وكائنات deque، وكائنات أنماط التعابير النمطية، وكائنات code.

ملاحظة: أصبح بالإمكان إنشاء إشارات ضعيفة لكل من thread.lock و threading.Lock وكائنات code في الإصدار 3.2 من اللغة.

لا تدعم العديد من أنواع البيانات الداخلية -مثل القوائم والقواميس- الإشارات الضعيفة على نحو مباشر، ولكن يمكن ذلك عن طريق التفريع subclassing:

class Dict(dict):

   pass
obj = Dict(red=1, green=2, blue=3) # this object is weak referenceable

وهناك بعض الأنواع الداخلية التي لا تدعم الإشارات الضعيفة حتى مع التفريع مثل الصفوف.

تقدم وحدة weakref الأصناف التالية:

الصنف ref

كائنات الإشارة الضعيفة

لا تمتلك كائنات الإشارة الضعيفة أيّ توابع أو خصائص باستثناء الخاصية ref.__callback__‎. يسمح كائن الإشارة الضعيفة بالوصول إلى الكائن المشار إليه -في حال وجوده- عن طريق استدعائه:

>>> import weakref
>>> class Object:
...     pass
...
>>> o = Object()
>>> r = weakref.ref(o)
>>> o2 = r()
>>> o is o2
True

إن لم يعد الكائن المشار إليه متوفّرًا، يعيد استدعاء كائن الإشارة القيمة None:

>>> del o, o2
>>> print(r())
None

يجب استخدام التعبير ref() is not None للتحقّق من كون كائن الإشارة حيًّا أو لا، وفي العادة يجب أن تتبع الشيفرة التي تكون بحاجة إلى استخدام كائن الإشارة النمط التالي:

# كائن إشارة ضعيف r
o = r()
if o is None:
    # تم جمع الكائن المشار إليه في القمامة
    print("Object has been deallocated; can't frobnicate.")
else:
    print("Object is still live!")
    o.do_something_useful()

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

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

يبين المثال التالي كيفية استخدام صنف متفرّع من الكائن ref لتخزين معلومات إضافية عن الكائن والتأثير على القيمة المعادة عند الوصول إلى الكائن المشار إليه:

import weakref
class ExtendedRef(weakref.ref):
    def __init__(self, ob, callback=None, **annotations):
        super(ExtendedRef, self).__init__(ob, callback)
        self.__counter = 0
        for k, v in annotations.items():
            setattr(self, k, v)
    def __call__(self):
        """Return a pair containing the referent and the number of
        times the reference has been called.
        """
        ob = super(ExtendedRef, self).__call__()
        if ob is not None:
            self.__counter += 1
            ob = (ob, self.__counter)
        return ob

مثال

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

import weakref
_id2obj_dict = weakref.WeakValueDictionary()
def remember(obj):
    oid = id(obj)
    _id2obj_dict[oid] = obj
    return oid
def id2obj(oid):
    return _id2obj_dict[oid]

كائنات الإنهاء

إنّ الفائدة الرئيسة من استخدام finalize هي أنّها تسهّل عملية تسجيل الاستدعاء الخلفي دون الحاجة إلى الاحتفاظ بكائن الإنهاء المعاد. فعلى سبيل المثال:

>>> import weakref
>>> class Object:
...     pass
...
>>> kenny = Object()
>>> weakref.finalize(kenny, print, "You killed Kenny!")  
<finalize object at ...; for 'Object' at ...>
>>> del kenny
You killed Kenny!

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

>>> def callback(x, y, z):
...     print("CALLBACK")
...     return x + y + z
...
>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> assert f.alive
>>> assert f() == 6
CALLBACK
>>> assert not f.alive
>>> f()                     # لا يجري استدعاء الاستدعاء الخلفي لأنّ كائن الإنهاء ميت
>>> del obj                 # لا يجري استدعاء الاستدعاء الخلفي لأنّ كائن الإنهاء ميت

يمكن إلغاء تسجيل كائن إنهاء باستخدام التابع detach()‎. ينهي هذا التابع حياة كائن الإنهاء ويعيد المعاملات الممررة إلى الدالة البانية لحظة إنشائه.

>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> f.detach()                                           
(<...Object object ...>, <function callback ...>, (1, 2), {'z': 3})
>>> newobj, func, args, kwargs = _
>>> assert not f.alive
>>> assert newobj is obj
>>> assert func(*args, **kwargs) == 6
CALLBACK

يستدعى كائن الإنهاء إن كان حيًّا عند إنهاء عمل البرنامج ما لم تعيّن القيمة False للخاصية atexit:

>>> obj = Object()
>>> weakref.finalize(obj, print, "obj dead or exiting")  
<finalize object at ...; for 'Object' at ...>
>>> exit()                                               
obj dead or exiting

مقارنة كائنات الإنهاء مع توابع ‎__del__()‎

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

  • جمع الكائن في القمامة
  • استدعاء التابع remove()‎ الخاص بالكائن.
  • الخروج من البرنامج.

يمكن استخدام التابع ‎__del__()‎ كما يلي:

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
    def remove(self):
        if self.name is not None:
            shutil.rmtree(self.name)
            self.name = None
    @property
    def removed(self):
        return self.name is None
    def __del__(self):
        self.remove()

بدءًا من الإصدار 3.4 من بايثون، لم تعد توابع ‎__del__()‎ تمنع دورات الإشارة من أن تُجمع في القمامة، ولم تعد تجبر الخصائص العامة للوحدة بأن تأخذ القيمة None عند إغلاق المفسّر؛ لهذا ستعمل هذه الشيفرة دون أي مشاكل في CPython.

ولكن طريقة التعامل مع توابع ‎__del__()‎ عائدة إلى طريقة الاستخدام؛ وذلك لأنّها تعتمد على تفاصيل داخلية خاصّة بجامع القمامة الخاصّ بالمفسّر.

البديل الأنسب هو تعريف كائن إنهاء يشير فقط إلى الدوال والكائنات التي يحتاجها، عوضًا عن الوصول إلى الكائن برمّته:

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
        self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
    def remove(self):
        self._finalizer()
    @property
    def removed(self):
        return not self._finalizer.alive

عند تعريف كائن الإنهاء بهذه الطريقة، فإنّه يستقبل فقط إشارة إلى التفاصيل التي يحتاجها لتنظيف المجلّد بصورة ملائمة. وحتى لو لم يُجمع الكائن في القمامة على الإطلاق، فإنّ كائن الإنهاء سيُستدعى عند الخروج من البرنامج. وتمتاز كائنات الإنهاء المبنية على كائنات weakref في إمكانية استخدامها لتسجيل كائنات الإنهاء للأصناف التي يتحكم طرف ثالث في تعريفها، كأن يجري تشغيل شيفرة عندما لا تكون الوحدة محمّلة:

import weakref, sys
def unloading_module():
# إشارة ضمنية إلى الخصائص العامة في الوحدة من داخل الدالة
weakref.finalize(sys.modules[__name__], unloading_module)

ملاحظة:

إن إنشأت كائن إنهاء في daemonic thread لحظة إنهاء البرنامج فمن المحتمل أن لا يُستدعى كائن الإنهاء عند إنهاء البرنامج. ولكن في daemonic thread فإنّ atexit.register()‎ و  try: ... finaly: ...‎ و with: ...‎ لا تضمن تنفيذ حدث التنظيف.

مصادر