يمكن أن تنشأ مشكلة في الذاكرة عندما تحتاج إلى وجود عدد كبير من الكائنات أثناء تنفيذ البرنامج ، خاصة إذا كانت هناك قيود على الحجم الكلي لذاكرة الوصول العشوائي المتاحة.
فيما يلي نظرة عامة على بعض الطرق لتقليل حجم الكائنات ، والتي يمكن أن تقلل بشكل كبير من مقدار ذاكرة الوصول العشوائي المطلوبة للبرامج في بيثون النقي.
للبساطة ، سننظر في الهياكل في Python لتمثيل النقاط مع إحداثيات x و y و z مع إمكانية الوصول إلى تنسيق القيم بالاسم.
ديكت
في البرامج الصغيرة ، وخاصة البرامج النصية ، من السهل جدًا استخدام الأمر المضمن لتمثيل المعلومات الهيكلية:
>>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y
مع ظهور تطبيق أكثر "ضغطًا" في Python 3.6 مع مجموعة مفاتيح مطلوبة ، أصبح الأمر أكثر جاذبية. ومع ذلك ، انظر إلى حجم تتبعه في ذاكرة الوصول العشوائي:
>>> print(sys.getsizeof(ob)) 240
يستغرق الكثير من الذاكرة ، خاصةً إذا كنت بحاجة فجأة إلى إنشاء عدد كبير من الحالات:
مثيل فئة
بالنسبة لأولئك الذين يحبون إمساك كل شيء في الفصول الدراسية ، من الأفضل تعريفه كفئة مع وصول باستخدام اسم السمة:
class Point: # def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> x = ob.x >>> ob.y = y
هيكل مثيل الطبقة مثير للاهتمام:
هنا __weakref__ هو ارتباط بقائمة من المراجع الضعيفة __dict__ لهذا الكائن ، الحقل __dict__ هو رابط لقاموس المثيل للفئة التي تحتوي على قيم سمات المثيل (لاحظ أن الروابط على نظام أساسي 64 بت تشغل 8 بايت). بدءًا من Python 3.3 ، يتم استخدام مساحة مفتاح القاموس المشترك لجميع مثيلات الفصل. هذا يقلل من حجم تتبع مثيل في الذاكرة:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
نتيجة لذلك ، يترك عدد كبير من مثيلات الفصل مساحة أصغر في الذاكرة من القاموس العادي ( dict ):
من السهل معرفة أن تتبع المثيل في الذاكرة لا يزال كبيرًا نظرًا لحجم قاموس المثيل.
نسخة من الفصل مع __slots__
يتم تحقيق انخفاض كبير في تتبع مثيل في الذاكرة عن طريق التخلص من __dict__ و __weakref__ . هذا ممكن مع "الخدعة" مع __slots__ :
class Point: __slots__ = 'x', 'y', 'z' def __init__(self, x, y, z): self.x = x self.y = y self.z = z >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 64
أصبح التتبع في الذاكرة أكثر إحكاما:
يؤدي استخدام __slots__ في تعريف الفئة إلى تقليل أثر عدد كبير من الحالات في الذاكرة بشكل كبير:
في الوقت الحالي ، هذه هي الطريقة الرئيسية لتقليل تتبع مثيل فئة في ذاكرة البرنامج بشكل ملحوظ.
يتحقق هذا التخفيض من خلال حقيقة أنه في الذاكرة بعد عنوان الكائن ، يتم تخزين الإشارات إلى الكائنات ، ويتم الوصول إليها باستخدام واصفات خاصة موجودة في قاموس الفئة:
>>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>})
هناك مكتبة قائمة __slots__ لأتمتة عملية إنشاء فصل باستخدام __slots__ . تقوم الدالة namedlist.namedlist بإنشاء بنية فئة مماثلة للفئة بـ __slots__ :
>>> Point = namedlist('Point', ('x', 'y', 'z'))
تسمح لك حزمة attrs أخرى بأتمتة عملية إنشاء الفصول مع وبدون __slots__ .
الصفوف (tuple)
لدى Python أيضًا نوع tuple مضمن لتمثيل مجموعات البيانات. Tuple هو هيكل ثابت أو سجل ، ولكن بدون أسماء الحقول. للوصول إلى الحقل ، يتم استخدام فهرس الحقل. ترتبط حقول tuple مرة واحدة وإلى الأبد بكائنات القيمة في وقت إنشاء tuple:
>>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y #
مثيلات Tuple مضغوطة تمامًا:
>>> print(sys.getsizeof(ob)) 72
تشغلها 8 بايتات في الذاكرة أكثر من مثيلات الفئة مع __slots__ ، حيث يحتوي تتبع tuple في الذاكرة أيضًا على عدد الحقول:
Namedtuple
نظرًا لاستخدام tuple على نطاق واسع جدًا ، كان هناك يوم واحد طلب لاستمرار الوصول إلى الحقول بالاسم أيضًا. كانت الاستجابة لهذا الطلب هي الوحدة النمطية collections.namedtuple .
تم namedtuple وظيفة namedtuple لأتمتة عملية إنشاء هذه الفئات:
>>> Point = namedtuple('Point', ('x', 'y', 'z'))
يخلق فئة فرعية من tuple ، والتي تحدد مقابض للوصول إلى الحقول بالاسم. على سبيل المثال ، سيبدو بشيء من هذا القبيل:
class Point(tuple): # @property def _get_x(self): return self[0] @property def _get_y(self): return self[1] @property def _get_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z))
جميع مثيلات هذه الفئات لها أثر في الذاكرة مطابقة للقيمة. يترك عدد كبير من الحالات بصمة ذاكرة أكبر قليلاً:
Recordclass: تم تغيير اسمهtuple بدون GC
نظرًا لأن tuple و ، بناءً على ذلك ، namedtuple فئات namedtuple كائنات غير قابلة للتغيير ، بمعنى أنه لم يعد بالإمكان ربط كائن قيمة ob.x بكائن قيمة آخر ، فقد نشأ طلب للحصول على متغير تم تغيير اسمه باسم. نظرًا لأن Python لا يحتوي على نوع مضمن مماثل لنمط tuple الذي يدعم المهام ، فقد تم إنشاء العديد من الاختلافات. سوف نركز على فئة السجل ، التي حصلت على تصنيف stackoverflow . بالإضافة إلى ذلك ، يمكن استخدامه لتقليل حجم تتبع كائن في الذاكرة مقارنة بحجم تتبع كائنات من نوع tuple .
في حزمة recordclass ، يتم تقديم type recordclass.mutabletuple ، وهو مماثل تقريبًا لل tuple ولكنه يدعم أيضًا الواجبات. على أساسها ، يتم إنشاء فئات فرعية مماثلة تقريبًا للعدد المسمى ، ولكنها تدعم أيضًا تعيين قيم جديدة للحقول (بدون إنشاء مثيلات جديدة). تعمل وظيفة recordclass ، مثل وظيفة namedtuple ، على إنشاء مثل هذه الفئات تلقائيًا:
>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)
تحتوي مثيلات الفصل على نفس بنية tuple ، ولكن بدون PyGC_Head فقط:
recordclass للإعدادات الافتراضية ، recordclass دالة recordclass فئة غير متضمنة في آلية تجميع البيانات المهملة. عادةً ما يتم استخدام كل من recordclass و recordclass لتفرخ الفئات التي تمثل السجلات أو هياكل البيانات البسيطة (غير المتكررة). استخدامها الصحيح في بايثون لا يولد مراجع دائرية. لهذا السبب ، فإن تتبع مثيلات الفئات التي تم إنشاؤها بواسطة فئة PyGC_Head الافتراضية PyGC_Head جزء PyGC_Head ، وهو ضروري للفئات التي تدعم آلية تجميع البيانات المهملة الدورية (بشكل أكثر دقة: علامة PyTypeObject غير محددة في حقل flags في هيكل PyTypeObject المطابق للفئة التي يتم إنشاؤها).
حجم التتبع لعدد كبير من المثيلات أصغر من حجم مثيلات فئة __slots__ :
Dataobject
يستند حل آخر مقترح في مكتبة recordclass إلى الفكرة: استخدام بنية التخزين في الذاكرة ، كما في حالات الفئات التي تحتوي على __slots__ ، ولكن ليس للمشاركة في آلية تجميع البيانات المهملة الدورية. تم إنتاج الفصل باستخدام دالة recordclass.make_dataclass :
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
الفئة الافتراضية التي تم إنشاؤها بهذه الطريقة تنشئ مثيلات متحولة.
هناك طريقة أخرى لاستخدام إعلان الفئة عن طريق الوراثة من recordclass.dataobject :
class Point(dataobject): x:int y:int z:int
ستنشئ الفئات التي تم إنشاؤها بهذه الطريقة مثيلات لا تشارك في آلية تجميع البيانات المهملة الدائرية. بنية المثيل في الذاكرة هي نفسها مع __slots__ ، ولكن بدون رأس PyGC_Head :
>>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40
للوصول إلى الحقول ، يتم استخدام واصفات خاصة أيضًا للوصول إلى الحقل من خلال الإزاحة بالنسبة إلى بداية الكائن ، والتي يتم وضعها في قاموس الفصل:
mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>, ....................................... 'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>, 'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>, 'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>})
يعد حجم التتبع لعدد كبير من الحالات هو أصغر حجم ممكن لـ CPython:
Cython
هناك نهج واحد يعتمد على استخدام Cython . وتتمثل الميزة في أنه يمكن للحقول أن تأخذ قيمًا لأنواع لغة C. يتم إنشاء واصفات للوصول إلى الحقول من Python الخالص تلقائيًا. على سبيل المثال:
cdef class Python: cdef public int x, y, z def __init__(self, x, y, z): self.x = x self.y = y self.z = z
في هذه الحالة ، يكون للمثيلين حجم ذاكرة أصغر:
>>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 32
يحتوي تتبع مثيل في الذاكرة على البنية التالية:
حجم التتبع لعدد كبير من النسخ أصغر:
ومع ذلك ، يجب أن نتذكر أنه عند الوصول إلى رمز Python ، سيتم إجراء التحويل من int إلى كائن Python والعكس بالعكس في كل مرة.
نمباي
استخدام صفائف متعددة الأبعاد أو سجل لكميات كبيرة من البيانات يعطي مكسب في الذاكرة. ومع ذلك ، لمعالجة فعالة في بيثون النقي ، يجب عليك استخدام أساليب المعالجة التي تركز على استخدام وظائف من حزمة numpy .
>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])
يتم إنشاء صفيف وعناصر N التي تم تهيئتها باستخدام الأصفار باستخدام الوظيفة:
>>> points = numpy.zeros(N, dtype=Point)
حجم الصفيف هو أصغر ممكن:
يتطلب الوصول المنتظم إلى عناصر الصفيف والسلاسل تحويل كائن Python
في قيمة C int والعكس بالعكس. يؤدي استخراج صف واحد إلى صفيف يحتوي على عنصر واحد. مساره لن يكون مضغوطًا جدًا:
>>> sys.getsizeof(points[0]) 68
لذلك ، كما ذكر أعلاه ، في شفرة Python ، من الضروري معالجة المصفوفات باستخدام وظائف من الحزمة numpy .
استنتاج
باستخدام مثال واضح وبسيط ، كان من الممكن التحقق من أن مجتمع المطورين ومستخدمي لغة برمجة بيثون (CPython) كانت لديه فرص حقيقية لتقليل حجم الذاكرة التي تستخدمها الكائنات بشكل كبير.