يمكن أن تنشأ مشكلة في الذاكرة عندما تحتاج إلى وجود عدد كبير من الكائنات أثناء تنفيذ البرنامج ، خاصة إذا كانت هناك قيود على الحجم الكلي لذاكرة الوصول العشوائي المتاحة.
فيما يلي نظرة عامة على بعض الطرق لتقليل حجم الكائنات ، والتي يمكن أن تقلل بشكل كبير من مقدار ذاكرة الوصول العشوائي المطلوبة للبرامج في بيثون النقي.
للبساطة ، سننظر في الهياكل في 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) كانت لديه فرص حقيقية لتقليل حجم الذاكرة التي تستخدمها الكائنات بشكل كبير.