الوحدة pickle في بايثون

من موسوعة حسوب
< Python
مراجعة 19:27، 20 سبتمبر 2018 بواسطة Mohammed Taher (نقاش | مساهمات)
(فرق) → مراجعة أقدم | المراجعة الحالية (فرق) | مراجعة أحدث ← (فرق)
اذهب إلى التنقل اذهب إلى البحث

تطبّق وحدة pickle بروتوكولات ثنائية لغرض سَلسلَة وإلغاء سَلسَلَة بنية كائنات بايثون. تطلق تسمية Pickling على العملية التي يتحوّل فيها تسلسل هرمي لكائن بايثون إلى تدفق بايتات byte stream، وتطلق تسمية Unpickling على العملية العكسية والتي يتحوّل فيها تدفّق بايتات (من ملف ثنائي أو كائن شبيه بالبايتات) إلى تسلسل هرمي لكائن بايثون.

تحمل هاتان العمليتان (Pickling و Unpickling) أسماءً أخرى مثل السَلسَلَة "serialization"، والترتيب "marshalling" (ليس المقصود هنا وحدة marshal) والتسطيح "falttening".

سنستخدم مصطلحي السلسلة وإلغاء السلسلة في هذا التوثيق كتعريب لمصطلحي pickling و unpickling.

تحذير: وحدة pickle غير محمية تجاه البيانات المبنيّة على نحو خاطئ أو البيانات الخبيثة؛ لذا لا تلغِ سَلسَلة أي بيانات قادمة من مصادر غير موثوقة.

العلاقة التي تربط هذه الوحدة مع وحدات بايثون الأخرى

مقارنة الوحدة مع وحدة marshal

تمتلك بايثون وحدة سلسلة أكثر بساطة من وحدة pickle وتدعى وحدة marshal، ولكن يفضّل استخدام وحدة pickle بصورة عامة لإلغاء سَلسَلة كائنات بايثون، أما الهدف الرئيسي من وجود وحدة marshal هو دعم ملفات بايثون ذات الامتداد ‎.pyc.

تختلف وحدة pickle عن وحدة marshal في عدة نقاط:

  • تتابع وحدة pickle الكائنات التي تقوم بسلسلتها، وبهذا لن تؤدي الإشارة إلى الكائن نفسه إلى إعادة عملية السلسلة مرة أخرى. أما وحدة marshal لا تدعم ذلك. لهذه الميزة تطبيقات في الكائنات التعاودية recursive وفي مشاركة الكائنات. الكائنات التعاودية هي كائنات تتضمن إشارات إلى نفسها، ولا يمكن التعامل مع مثل هذه الكائنات بواسطة وحدة marshal، وفي الواقع يؤدي استخدام هذه الوحدة مع الكائنات التعاودية إلى انهيار مفسّر بايثون. تحدث عملية مشاركة الكائن عند وجود إشارات متعددة لنفس الكائن في مواضع مختلفة من تسلسل الكائن الذي تجري سلسلته. تخزّن وحدة pickle مثل هذه الكائنات مرة واحدة، وتضمن أنّ جميع الإشارات الأخرى موجّهة نحو النسخة الأساسية. الكائنات المشتركة تبقى مشتركة، وهو أمر قد يكون في غاية الأهمية للكائنات القابلة للتعديل.
  • لا يمكن استخدام وحدة marshal لسلسلة الأصناف المعرّفة من قبل المستخدم إضافة إلى النسخ التابعة لها، أما وحدة pickle فبمقدورها أن تحفظ وتسترجع نسخ الصنف بطريقة واضحة، ولكن يجب أن يكون تعريف الصنف قابلًا للاستيراد وأن يكون موجودًا في نفس الوحدة كما هو الحال عند حفظ الكائن. لا يمكن ضمان نقل صيغة السَلسَلة المعتمدة في وحدة marshal بين إصدارات بايثون المختلفة، والسبب في ذلك يعود إلى أنّ المهمّة الرئيسية لهذه الوحدة هو دعم ملفات ‎.pyc، وما من شيء يمكنه أن يفرض على مستخدمي بايثون عدم تغيير صيغة السَلسَلة بطريقة لا تكون متوافقة مع الإصدارات السابقة. في حين يمكن ضمان توافقية صيغة السَلسَلة المعتمدة في وحدة pickle مع الإصدارات السابقة من بايثون.

مقارنة pickle مع json

هناك عدد من الفروقات الجوهرية بين بروتوكول pickle وJSON (اختصار JavaScript Object Notation):

  • JSON هي صيغة لسلسلة النصوص (مخرجاتها هي نصوص بترميز unicode، وترمّز لاحقًا في معظم الأحيان إلى الترميز utf-8) في حين أنّ pickle هي صيغة لسلسلة البيانات الثنائية.
  • JSON قابلة للقراءة من قبل البشر، أما pickle فلا.
  • JSON واسعة الانتشار خارج بيئة بايثون، أما pickle فمخصّصة للغة بايثون فقط.
  • يمكن لـ JSON أن تمثّل -على نحو افتراضي- جزءًا من أنواع بايثون الداخلية، ولا تدعم الأصناف المخصصة، في حين أنّ بمقدور وحدة pickle أن تمثّل عددًا كبيرًا جدًا من أنواع بايثون (يمكن تمثيل الكثير من الأنواع تلقائيًا بواسطة بعض الأدوات التي تقدّمها بايثون، ويمكن تمثيل الحالات المعقّدة عن طريق تطبيق واجهات برمجية خاصّة ببعض الكائنات).

صيغة تدفق البيانات

إنّ صيغة البيانات التي تستخدمها وحدة pickle خاصّة ببايثون، ومن حسنات ذلك عدم وجود أي قيود يمكن أن تفرض من مصادر خارجية مثل JSON أو XDR (والذي لا يمكنه تمثيل عملية مشاركة المؤشر)، ولكن ذلك يعني عدم قدرة البرامج المكتوبة بلغات أخرى من إعادة بناء كائنات بايثون المسَلسَلة.

تستخدم صيغة بيانات pickle تمثيلًا ثنائيًا مضغوطًا نسبيًا، وإن كنت مهتمًا بمسألة الحجم فيمكن ضغط البيانات المسَلسَلة بكفاءة عالية.

تتضمن وحدة pickletools أدوات لتحليل تدفق البيانات الناتجة من وحدة pickle، وتحتوي الشيفرة المصدرية لوحدة pickletools على تعليقات مكثفة حول شيفرات العمليات المستخدمة في بروتوكول pickle.

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

  • البروتوكول رقم 0 هو البروتوكول الأصلي الذي يمكن قراءته من قبل البشر وهو متوافق مع النسخ الأولى من بايثون.
  • البروتوكول رقم 1 هو صيغة ثنائية قديمة، وهو متوافق أيضًا مع النسخ الأولى من بايثون.
  • ُقدّم الإصدار 2.3 من بايثون البروتوكول رقم 2، ويوفّر هذا البروتوكول أصنافًا ذات شكل جديد لإجراء عمليات سلسلة ذات كفاءة أعلى. يمكن الرجوع إلى PEP 307 للاطلاع على المزيد من المعلومات حول التحسينات التي أتى بها البروتوكول رقم 2.
  • أضيف البروتوكول رقم 3 في الإصدار 3.0 من بايثون. يدعم هذا البروتوكول كائنات bytes صراحةً، ولا يمكن إلغاء سلسلته في إصدارات ‎2.x من اللغة، وهو البروتوكول الافتراضي وينصح باستخدامه عندما يكون التوافق مع إصدارات بايثون 3 الأخرى أمرًا مطلوبًا.
  • أضيف البروتوكول رقم 4 في الإصدار 3.4 من بايثون، ويدعم الكائنات ذات الأحجام الكبيرة، ويُتيح سَلسَلة أنواع إضافية من الكائنات، ويقدّم بعض التحسينات على صيغة البيانات. يمكن مراجعة PEP 3154 للاطلاع على التحسينات التي أتى بها البروتوكول رقم 4.

ملاحظة: إنّ مفهوم السَلسَلة Serialization مفهوم أكثر بدائية من الاستمرارية persistence، فبالرغم من أن pickle تقرأ كائنات الملفات وتكتب فيها، إلّا أنّها لا تعالج مسألة تسمية الكائنات المستمرة، ولا تعالج مشكلة الوصول المتزامن للكائنات المستمرة (وهي مشكلة أعقد بكثير).

يمكن لوحدة pickle أن تحوّل كائنًا معقدًا إلى تدفق بايتات وبمقدورها أن تحول تدفق بايتات إلى كائن يحمل البنية الداخلية نفسها، ولعلّ أوضح ما يمكن فعله بتدفق البايتات هذا هو كتابته في ملف، ولكن يمكن إرساله عبر شبكة معيّنة أو تخزينه في قاعدة بيانات. تقدّم وحدة shelve واجهة بسيطة لسَلسَلة وإلغاء سلسلة الكائنات في ملف قاعدة بيانات بنمط DBM.

الواجهة البرمجية للوحدة

لا تتطلّب عملية سَلسَلة كائن معيّن سوى استدعاء الدالة dumps()‎، وكذلك الأمر في عملية إلغاء سَلسَلة تدفق بيانات معيّن، إذ يكفي استدعاء الدالة loads()‎. ولكن إن كنت بحاجة إلى التحكم بصورة أكبر على عملية السلسلة وإلغاء السلسلة، فيمكن إنشاء كائن Pickler أو Unpickler على التوالي.

تقدّم وحدة pickle الثوابت التالية:

pickle.HIGHEST_PROTOCOL

هذا الثابت هو عدد صحيح يعرض أعلى رقم متاح للبروتوكول، ويمكن تمرير هذا الثابت كقيمة للمعامل protocol في الدالتين dump()‎ و dumps()‎ إلى جانب الدالة البانية للكائن Pickler.

pickle.DEFAULT_PROTOCOL

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

استثناءات الوحدة pickle

تعرّف وحدة pickle ثلاثة استثناءات هي:

الاستثناء pickle.PickleError

صنف أساسي مشترك لجميع الاستثناءات الأخرى في عملية السلسلة. يتفرّع هذا الصنف من الصنف Exception.

الاستثناء pickle.PicklingError

يُطلَق هذا الاستثناء عند ما يُقابل المُسَلسِل Pickler كائنٌ غير قابل للسَلسَلة. هذا الصنف متفرّع من الصنف PickleError.

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

الاستثناء pickle.UnpicklingError

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

يجب الانتباه إلى أنّ هناك استثناءات أخرى قد تُطلق أثناء عملية إلغاء السلسلة، منها (على سبيل المثال وليس الحصر) الاستثناءات AttributeError و EOFError و ImportError و IndexError.

أصناف الوحدة pickle

تصدّر الوحدة pickle صنفين هما:

الصنف Pickler

يستخدم هذا الصنف ملفًّا يكتب فيه تدفق البيانات المسلسلة.

الصنف Unpickler

يستخدم هذا الصنف ملفًّا ثنائيًا يقرأ منه تدفق البيانات المسلسلة.

ما هي الكائنات القابلة للسَلسَلة؟

يمكن سَلسَلة الأنواع التالية:

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

تؤدي كذلك محاولة سَلسَلة بيانات ذات تعاودية كبيرة جدًّا إلى درجة تتجاوز عمق التعاود المسموح به، إلى إطلاق الاستثناء RecursionError. يمكن زيادة عمق التعاود باستخدام الدالة sys.setrecursionlimit()‎ ولكن يجب توخّي الحذر عند القيام بذلك.

يجب الانتباه إلى أن الدوال (سواء أكانت داخلية أم معرّفة من قبل المستخدم) تُسلسل بواسطة اسم الإشارة "المؤهّل بالكامل" لا بواسطة القيمة (لهذا السبب لا يمكن سلسلة دوال lambda، إذ تشترك جميع هذه الدوال بالاسم <lambda>). وهذا يعني أنّ السَلسَلة تشمل اسم الدالة فقط إضافة إلى اسم الوحدة التي جرى تعريف الدالة فيها، أما الشيفرة أو الخصائص التي تعرّفها الدالة فلا تُسلسل على الإطلاق؛ ولهذا يجب أن تكون الوحدة التي تعرّف هذه الدوال قابلة للاستيراد في البيئة غير المسلسلة، ويجب أن تحتوي الوحدة على كائنات مسماة، وإلا فإنّ اللغة ستطلق استثناءً. (تطلق اللغة على الأرجح الاستثناء ImportError أو AttributeError ولكن قد تطلق استثناءات أخرى).

تُسَلسَل الأصناف بنفس الطريقة بواسطة الإشارات المسماة؛ لذا تُفرض القيود ذاتها في البيئة غير المسلسلة. ويجب التنبيه إلى أنّ شيفرة الصنف أو بياناته لا تُسلسل إطلاقًا، ولهذا لا يمكن استرجاع الخاصية attr في المثال التالي ضمن البيئة غير المسلسلة:

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

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

وبنفس الطريقة، لا تُسلسل شيفرة الصنف أو بياناته عند سلسلة نسخ الصنف، وما يتم سلسلته هو بيانات النسخة فقط، والهدف من ذلك هو إمكانية إصلاح الأخطاء في صنف معين أو إضافة توابع جديدة إليه مع إمكانية تحميل الكائنات التي جرى إنشاؤها بواسطة النسخة الأقدم من ذلك الصنف. إن كنت تخطط لاستخدام كائنات على المدى الطويل ترتبط بإصدارات متعددة من صنف معيّن، فمن الأجدر بك أن تضيف رقم إصدار إلى الكائنات ليكون بالإمكان إجراء التحويلات المناسبة بواسطة التابع ‎__setstate__()‎.

سلسلة نسخ الأصناف

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

لن تكون بحاجة إلى شيفرات إضافية لإنشاء نسخ قابلة للسلسلة في معظم الحالات. فوحدة pickle ستسترجع افترضيًا الصنف والخصائص المرتبطة عن طريق الفحص الذاتي. عند إلغاء سلسلة نسخة صنف معيّن لا يجري تنفيذ التابع ‎__init__()‎ في العادة، ويكون السلوك الافتراضي هو إنشاء نسخة غير مهيّئة ثم استرجاع الخصائص المحفوظة. يبيّن المثال التالي تطبيقًا لهذا السلوك:

def save(obj):
    return (obj.__class__, obj.__dict__)

def load(cls, attributes):
    obj = cls.__new__(cls)
    obj.__dict__.update(attributes)
    return obj

يمكن للأصناف أن تغيّر السلوك الافتراضي عن طريق تقديم تابع خاصّ أو أكثر:

التابع object.__getnewargs_ex__()‎

في البروتوكول رقم 2 وما بعده، يمكن للأصناف التي تطبّق التابع ‎‎__getnewargs_ex__()‎ أن تملي القيم الممرّرة على التابع ‎__new__()‎ خلال عملية إلغاء السلسلة.

يجب أن يعيد التابع زوج (معاملات موقعية، معاملات مفتاحية) (args, kwargs) تكون فيه args صفًّا من المعاملات الموقعية، وkwargs قاموسًا يتضمن معاملات مسمّاة تستخدم لبناء الكائن، وتمرر المعاملات هذه بنوعيها إلى التابع ‎__new__()‎ أثناء عملية إلغاء السلسلة.

يجب تطبيق هذا التابع إن كان تابع ‎__new__()‎ يتطلّب استخدام معاملات مفتاحية فقط، أما فيما عدا ذلك فينصح بتطبيق التابع ‎__getnewargs__()‎ لضمان التوافق مع بقية إصدارات بايثون.

ملاحظة: في الإصدار 3.6 من بايثون أصبح التابع ‎__getnewargs_ex__()‎ مستخدمًا في البروتوكولين رقم 2 ورقم 3.

التابع object.__getnewargs__()‎

يؤدي هذا التابع نفس الوظيفة التي يؤدّيها التابع ‎__getnewargs_ex__()‎ ولكنّه يدعم المعاملات الموقعية فقط. يجب أن يعيد التابع صفًّا من المعاملات الموقعية والتي ستمرّر إلى التابع ‎__new__()‎ أثناء عملية إلغاء السلسلة.

لن يُستدعى التابع ‎__getnewargs__()‎ إن كان التابع ‎__getnewargs_ex__()‎ معرّفًا.

ملاحظة: قبل الإصدار 3.6 من بايثون، كان التابع ‎‎__getnewargs__()‎ يُستدعى عوضًا عن التابع ‎__getnewargs_ex__()‎ في البروتوكولين رقم 2 و 3.

التابع object.__getstate__()‎

يمكن للأصناف أن تؤثّر في طريقة المتّبعة لسلسلة نسخها، فإن عرّف الصنف التابع ‎__getstate__()‎ فإنّ التابع سيُستدعى وستجري سلسلة الكائن المعاد كمحتوى لنسخة الصنف عوضًا عن محتوى القاموس الخاصّ بنسخة الصنف. وفي حال غياب التابع ‎‎__getstat__()‎ تجري سَلسَلة ‎__dict__‎ الخاصّ بنسخة الصنف.

التابع object.__setstate__(state)‎

إنّ عرف الصنف التابع ‎__setstate__()‎ أثناء عملية إلغاء السلسلة، فإنّه سيستدعى في حالة إلغاء السلسلة، وليس من الضروري في مثل هذه الحالة أن يكون كائن state قاموسًا. أما في الحالات الأخرى فيجب أن تكون الحالة المُسلسَلة قاموسًا تُسند عناصره إلى قاموس نسخة الصنف الجديدة.

ملاحظة:

إن أعاد التابع ‎__getstat__()‎ القيمة False، فلن يُستدعى التابع ‎__setstate__()‎ أثناء عملية إلغاء السلسلة.

راجع قسم "التعامل مع الكائنات ذات الحالة" للمزيد من المعلومات حول كيفية استخدام التابعين ‎__getstate__()‎ و ‎__setstate__()‎.

ملاحظة:

عند إجراء عملية إلغاء السلسلة، قد تُستدعى بعض التوابع مثل ‎__getattr__()‎ أو ‎__getattribute__()‎ أو ‎__setattr__()‎ على نسخة الصنف. وفي حال اعتماد هذه التوابع على كون بعض الثوابت الداخلية ذات قيمة صحيحة، فيجب على النوع حينئذٍ أن يطبّق التابع ‎__getnewargs__()‎ أو ‎__getnewargs_ex__()‎ لإنشاء مثل هذه الثوابت، وإلّا فلن يُستدعى التابع ‎__new__()‎ أو ‎__init__()‎.

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

يقدّم بروتوكول النسخ واجهة محدّة لاسترجاع البيانات اللازمة لسَلسَلة الكائنات ونسخها، وتستخدم وحدة copy هذا البروتوكول لإجراء عمليات النسخ السطحية والعميقة.

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

التابع object.__reduce__()‎

تعمل الواجهة في الوقت الحاضر بالشكل التالي. لا يأخذ التابع ‎‎__reduce__()‎ أي معاملات ويجب أن يعيد إما سلسلة نصية أو صفًّا (وهو الأفضل). يُطلق على الكائن المعاد في أغلب الأحيان تسمية "قيمة الاختزال reduce value".

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

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

تمتلك العناصر الخمسة في الصفّ الدلالات التالية وبحسب الترتيب:

  • كائن callable سيُستدعى لإنشاء النسخة الأوّلية من الكائن.
  • صفّ من المعاملات الخاصّة بكائن callable. يجب إعطاء صفّ فارغ إن كان كائن callable لا يستقبل أيّ معاملات.
  • عنصر اختياري، يمثّل حالة الكائن والتي ستمرّر إلى التابع ‎__setstate__()‎ الخاصّ بالكائن كما هو مبيّن سابقًا. يجب أن تكون قيمة هذا العنصر قاموسًا إن لم يمتلك الكائن مثل هذا التابع، وستضاف القيمة إلى الخاصية ‎__dict__‎ في الكائن.
  • عنصر اختياري، وهو مكرِّر (وليس تسلسل) ينتج عناصر متعاقبة ستُلحق بالكائن إمّا بواسطة التابع ‎obj.append(item)‎، أو دفعة واحدة بواسطة التابع obj.extend(list_of_items)‎. يستخدم هذا العنصر بصورة أساسية مع الأصناف المتفرّعة من القوائم، ولكن يمكن استخدامه مع الأصناف الأخرى ما دامت تملك التابعين append()‎ و extend()‎ مع التوقيع المناسب. (يعتمد الاختيار بين التابعين append()‎ و extend()‎ لإضافة العناصر على رقم بروتوكول السلسلة المستخدم، وعلى عدد العناصر المضافة؛ لذا يجب توفّر التابعين في الأصناف الأخرى).
  • عنصر اختياري، وهو مكرّر (وليس تسلسلًا) ينتج أزواج مفتاح-قيمة متعاقبة. تُخزّن هذه العناصر في الكائن بواسطة التعبير obj[key] = value. يستخدم هذا العنصر بصورة اساسية مع الأصناف المتفرعة من القواميس، ولكن يمكن استخدامه مع الأصناف الأخرى ما دامت تستخدم التابع ‎__setitem__()‎.

التابع object.__reduce_ex__(protocol)‎

يمكن استخدام التابع ‎__reduce_ex__()‎ عوضًا عن التابع ‎__reduce__()‎، والفرق الوحيد بينهما هو أنّ الأوّل يأخذ معاملًا عدديًا واحدًا فقط، وهو رقم البروتوكول المستخدم. تستخدم الوحدة pickle هذا التابع في حال تعريفه مع التابع ‎__reduce__()‎، ويصبح الأخير مرادفًا للنسخة الموسّعة من التابع. إن الاستخدام الأساسي لهذا التابع هو تقديم قيمة اختزال متوافقة مع الإصدارات القديمة من بايثون.

استمرارية الكائنات الخارجية

تدعم وحدة pickle الإشارة إلى كائن خارج تدفق البيانات المسلسلة وذلك لتحقيق استمرارية الكائن object persistence. ويشار إلى مثل هذه الكائنات بمعرّف مستمر persistent ID، والذي يجب أن يكون سلسلة نصية مكوّنة من محارف حرفية-رقمية alphanumeric characters (عند استخدام البروتوكول رقم 0) أو كائن عادي (للبروتوكولات الأحدث).

ملاحظة: إن سبب تحديد السلسلة النصية بالمحارف الحرفية-الرقمية يعود إلى أنّ المعرّفات المستمرة في البروتوكول رقم 0 تُفصل عن بعضها البعض بواسطة محرف السطر الجديد. لذا، يؤدي وجود أيّ نوع من أنواع محارف السطر الجديد في المعرّفات المستمرة إلى جعل نتيجة السَلسَلة غير قابلة للقراءة.

لا تعرّف عملية تحليل المعرّفات المستمرة بواسطة الوحدة pickle، بل تفوّض الوحدة هذه العملية إلى التوابع المعرّفة من قبل المستخدم في التابع persistent_id()‎ في الكائن Pickler، والتابع persistent_load()‎ في الكائن Unpickler.

تتطلب سلسلة كائن يمتلك معرّفًا مستمرًّا خارجيًّا أن يمتلك المسلسِل تابع persistent_id()‎ خاصًّا يأخذ كائنًا كمعامل ويعيد القيمة None أو المعرّف المستمرّ الخاصّ بذلك الكائن. إذا أعيدت القمية None، يُسلسل المسلسِل الكائن بصورة طبيعية. وإذا أعيدت سلسلة نصية تتضمن المعرّف المستمر فإنّ المسلسِل سيُسلسل ذلك الكائن مع علامة خاصة ليتمكّن ملغي السلسلة من تمييزه كمعرّف مستمرّ.

يتطلّب إلغاء سلسلة الكائنات الخارجية أن يمتلك ملغي السلسلة تابع persistent_load()‎ خاصًّا يأخذ كائن معرّف مستمر كمعامل ويعيد الكائن المشار إليه.

يبين المثال التالي طريقة استخدام المعرّف المستمر لسلسلة كائنات خارجية عن طريق الإشارة:

# مثال بسيط يبين طريقة استخدام المعرّف المستمر
# لسلسلة كائنات خارجية عن طريق الإشارة

import pickle
import sqlite3
from collections import namedtuple

# صنف بسيط يمثّل سجلًّا في قاعدة البيانات
MemoRecord = namedtuple("MemoRecord", "key, task")

class DBPickler(pickle.Pickler):

    def persistent_id(self, obj):
        # عوضًا عن سلسلة MemoRecord كنسخة صنف عادية، سننشئ معرّفًا مستمرًّا
        if isinstance(obj, MemoRecord):
            # المعرّف المستمر هنا هو صفّ يتضمن وسمًا ومفتاحًا
            # ويشير إلى سجلّ معيّن في قاعدة البيانات.
            return ("MemoRecord", obj.key)
        else:
            # إن لم يمتلك الكائن معرّفًا مستمرّاً، نعيد القيمة None.
            # هذا يعني أنه تجب سلسلة الكائن كما هو معتاد.
            return None


class DBUnpickler(pickle.Unpickler):

    def __init__(self, file, connection):
        super().__init__(file)
        self.connection = connection

    def persistent_load(self, pid):
            # ينفّذ هذا التابع مع كلّ معرّف مستمر.
            # pid هنا هو الصفّ المعاد من DBPickler.
        cursor = self.connection.cursor()
        type_tag, key_id = pid
        if type_tag == "MemoRecord":
            # جلب السجلّ المشار إليه من قاعدة البيانات وإعادة قيمته.
            cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
            key, task = cursor.fetchone()
            return MemoRecord(key, task)
        else:
            # يجب دائمًا إطلاق خطأ في حال عدم القدرة على إعادة الكائن الصحيح.
            # وإلّا فإن ملغي السلسلة سيظنّ أن القيمة None هي الكائن المشار إليه
            # بواسطة المعرّف المستمر.
            raise pickle.UnpicklingError("unsupported persistent object")


def main():
    import io
    import pprint

    # تهيئة قاعدة البيانات وإضافة البيانات إليها
    conn = sqlite3.connect(":memory:")
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE memos(key INTEGER PRIMARY KEY, task TEXT)")
    tasks = (
        'give food to fish',
        'prepare group meeting',
        'fight with a zebra',
        )
    for task in tasks:
        cursor.execute("INSERT INTO memos VALUES(NULL, ?)", (task,))

    # جلب السجل المراد سلسلته.
    cursor.execute("SELECT * FROM memos")
    memos = [MemoRecord(key, task) for key, task in cursor]
    # حفظ السجلات باستخدام الصنف DBPickler.
    file = io.BytesIO()
    DBPickler(file).dump(memos)

    print("Pickled records:")
    pprint.pprint(memos)

    # تحديث سجلّ للتأكد فقط من سير الأمور على ما يرام.
    cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")

    # تحميل السجلات من تدفق البيانات المسلسلة.
    file.seek(0)
    memos = DBUnpickler(file, conn).load()

    print("Unpickled records:")
    pprint.pprint(memos)


if __name__ == '__main__':
    main()

جداول الإرسال Dispatch Tables

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

جدول الإرسال العامّ والذي تتحكّم به الوحدة copyreg متوفّر عن طريق الثابت copyreg.dispatch_table؛ لهذا يمكن استخدام نسخة معدّلة من copyreg.dispatch_table كجدول إرسال خاص.

فعلى سبيل المثال:

f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass

تنشئ الشيفرة السابقة نسخة من الصنف pickle.Pickler مع جدول إرسال خاص يتعامل مع الصنف SomeClass بصورة خاصة. أما الشيفرة التالية:

class MyPickler(pickle.Pickler):
    dispatch_table = copyreg.dispatch_table.copy()
    dispatch_table[SomeClass] = reduce_SomeClass
f = io.BytesIO()
p = MyPickler(f)

فتؤدي الوظيفة نفسها، ولكن ستتشارك جميع نسخ الصنف MyPickler جدول الإرسال ذاته افتراضيًا. فيما يلي الشيفرة المكافئة التي تستخدم وحدة copyreg:

copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)

التعامل مع الكائنات ذات الحالة Stateful objects

يبين المثال التالي طريقة تعديل عملية سلسلة الأصناف. يفتح الصنف TextReader ملفًّا نصّيًّا، ويعيد رقم السطر ومحتوى السطر عند كل استدعاء للتابع readline()‎. إن جرت سلسلة نسخة من الصنف TextReader فإنّ جميع الخصائص ستحفظ باستثناء كائن الملف. وعند إلغاء سلسلة نسخة الصنف، يُعاد فتح الملف، وتستمر عملية القراءة من الموقع الأخير. يُستخدم التابعان ‎__setstate__()‎ و ‎__‎getstate__()‎ لتطبيق هذا السلوك.

class TextReader:
    """Print and number lines in a text file."""

    def __init__(self, filename):
        self.filename = filename
        self.file = open(filename)
        self.lineno = 0

    def readline(self):
        self.lineno += 1
        line = self.file.readline()
        if not line:
            return None
        if line.endswith('\n'):
            line = line[:-1]
        return "%i: %s" % (self.lineno, line)

    def __getstate__(self):
        # ينسخ التابع حالة الكائن من الخاصية self.__dict__‎ والتي تتضمن
        # جميع خصائص النسخة. استخدم التابع dict.copy()‎ دائمًا
        # لتتجنّب تعديل الحالة الأصلية.
        state = self.__dict__.copy()
        # إزالة العناصر غير القابلة للسلسلة.
        del state['file']
        return state

    def __setstate__(self, state):
        # استعادة خصائص النسخة (أي filename و lineno).
        self.__dict__.update(state)
        # استعادة الحالة السابقة للملف المفتوح. للقيام بذلك يجب
        # إعادة فتح الملف والقراءة منه إلى حين استرجاع رقم السطر.
        file = open(self.filename)
        for _ in range(self.lineno):
            file.readline()
        # وأخيرًا، حفظ الملف.
        self.file = file

يمكن استخدام الصنف بالطريقة التالية:

>>> reader = TextReader("hello.txt")
>>> reader.readline()
'1: Hello world!'
>>> reader.readline()
'2: I am line number two.'
>>> new_reader = pickle.loads(pickle.dumps(reader))
>>> new_reader.readline()
'3: Goodbye!'

تقييد النطاقات العامة Globals

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

لاحظ ما يفعله تدفق البيانات المكتوب يدويًا في هذا المثال عند تحميله:

>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
0

يستورد ملغي السلسلة في هذا المثال الدالة os.system()‎ ثم يطبّق السلسلة النصية "echo hello world". على الرغم من أنّ هذا المثال لا يتضمن شيفرة مؤذية، إلاّ أنّه يمكن وبكلّ سهولة إضافة شيفرات تتسبب في إلحاق أضرار جسيمة بالنظام.

ولهذا السبب يجب التحكم فيما يجري إلغاء سلسلته عن طريق تخصيص هذه العملية بواسطة التابع Unpickler.find_class()‎. وعلى عكس ما يوحي به اسم هذا التابع، فإنّه يُستدعى عند طلب أيّ شيء من النطاق العام (أي صنف أو دالة). وهكذا يصبح بالإمكان إلغاء النطاق العام تمامًا، أو تقييده بمجموعة آمنة.

يقدّم المثال التالي ملغيَ سلسلة يسمح بتحميل بعض الأصناف الآمنة من الوحدة builtins:

import builtins
import io
import pickle

safe_builtins = {
    'range',
    'complex',
    'set',
    'frozenset',
    'slice',
}

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        # السماح باستيراد الأصناف الآمنة فقط.
        if module == "builtins" and name in safe_builtins:
            return getattr(builtins, name)
        # منع استيراد أي شيء آخر.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))

def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()

يبين المثال التالي طريقة بسيطة لاستخدام ملغي السلسلة الذي أعددناه في المثال السابق:

>>> restricted_loads(pickle.dumps([1, 2, range(15)]))
[1, 2, range(0, 15)]
>>> restricted_loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
Traceback (most recent call last):
  ...
pickle.UnpicklingError: global 'os.system' is forbidden
>>> restricted_loads(b'cbuiltins\neval\n'
...                  b'(S\'getattr(__import__("os"), "system")'
...                  b'("echo hello world")\'\ntR.')
Traceback (most recent call last):
  ...
pickle.UnpicklingError: global 'builtins.eval' is forbidden

يجب توخي الحذر في اختيار ما سيجري إلغاء سلسلته كما هو موضّح في المثال السابق. لذا إن كان مهتمًّا بالجانب الأمني فيجدر بك أن تستخدم بدائل أخرى مثل الواجهة البرمجية لعملية الترتيب marshalling API في xmlrpc.client أو استخدام حلول من طرف ثالث.

الأداء

تمتاز الإصدارات الحديثة من بروتوكولات السلسلة (البروتوكول رقم 2 وما بعده) بالكفاءة العالية في الترميز الثنائي للعديد من الخصائص والأنواع الداخلية الشائعة في بايثون، إلى جانب أنّ وحدة pickle تتضمّن محسّن أداء مكتوب بلغة C.

أمثلة

يمكن استخدم الدالتين dump()‎ و load()‎ في الشيفرات البسيطة:

import pickle

# مجموعة متنوعة من الأنواع القابلة للسلسلة
data = {
    'a': [1, 2.0, 3, 4+6j],
    'b': ("character string", b"byte string"),
    'c': {None, True, False}
}

with open('data.pickle', 'wb') as f:
    # سلسلة القاموس 'data' باستخدام أعلى إصدار متوفّر من بروتوكولات السلسلة.
    pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)

يقرأ المثال التالي البيانات الناتجة من عملية السلسلة:

import pickle

with open('data.pickle', 'rb') as f:
    # تكشف الدالة عن البروتوكول المستخدم تلقائيًا، ولا حاجة لتحديده.
    data = pickle.load(f)

انظر أيضًا

مصادر