بايثون يستهلك الكثير من الذاكرة أو كيفية تقليل حجم الأشياء؟

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


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


للبساطة ، سننظر في الهياكل في 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 

يستغرق الكثير من الذاكرة ، خاصةً إذا كنت بحاجة فجأة إلى إنشاء عدد كبير من الحالات:


عدد النسخحجم التتبع
1000000240 ميجا بايت
100000002.40 غيغابايت
10000000024 جيجابايت

مثيل فئة


بالنسبة لأولئك الذين يحبون إمساك كل شيء في الفصول الدراسية ، من الأفضل تعريفه كفئة مع وصول باستخدام اسم السمة:


 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 

هيكل مثيل الطبقة مثير للاهتمام:


المجالالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
__weakref__8
__dict__8
المجموع:56

هنا __weakref__ هو ارتباط بقائمة من المراجع الضعيفة __dict__ لهذا الكائن ، الحقل __dict__ هو رابط لقاموس المثيل للفئة التي تحتوي على قيم سمات المثيل (لاحظ أن الروابط على نظام أساسي 64 بت تشغل 8 بايت). بدءًا من Python 3.3 ، يتم استخدام مساحة مفتاح القاموس المشترك لجميع مثيلات الفصل. هذا يقلل من حجم تتبع مثيل في الذاكرة:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

نتيجة لذلك ، يترك عدد كبير من مثيلات الفصل مساحة أصغر في الذاكرة من القاموس العادي ( dict ):


عدد النسخحجم التتبع
1000000168 ميجا بايت
100000001.68 جيجابايت
10000000016.8 غيغابايت

من السهل معرفة أن تتبع المثيل في الذاكرة لا يزال كبيرًا نظرًا لحجم قاموس المثيل.


نسخة من الفصل مع __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 

أصبح التتبع في الذاكرة أكثر إحكاما:


المجالالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
س8
ذ8
ض8
المجموع:64

يؤدي استخدام __slots__ في تعريف الفئة إلى تقليل أثر عدد كبير من الحالات في الذاكرة بشكل كبير:


عدد النسخحجم التتبع
100000064 ميغابايت
10000000640 ميجابايت
1000000006.4 جيجابايت

في الوقت الحالي ، هذه هي الطريقة الرئيسية لتقليل تتبع مثيل فئة في ذاكرة البرنامج بشكل ملحوظ.


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


 >>> 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 في الذاكرة أيضًا على عدد الحقول:


المجالالحجم (بايت)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
المجموع:72

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)) 

جميع مثيلات هذه الفئات لها أثر في الذاكرة مطابقة للقيمة. يترك عدد كبير من الحالات بصمة ذاكرة أكبر قليلاً:


عدد النسخحجم التتبع
100000072 ميجا بايت
10000000720 ميجا بايت

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 فقط:


المجالالحجم (بايت)
PyObject_HEAD16
ob_size8
س8
ذ8
ذ8
المجموع:48

recordclass للإعدادات الافتراضية ، recordclass دالة recordclass فئة غير متضمنة في آلية تجميع البيانات المهملة. عادةً ما يتم استخدام كل من recordclass و recordclass لتفرخ الفئات التي تمثل السجلات أو هياكل البيانات البسيطة (غير المتكررة). استخدامها الصحيح في بايثون لا يولد مراجع دائرية. لهذا السبب ، فإن تتبع مثيلات الفئات التي تم إنشاؤها بواسطة فئة PyGC_Head الافتراضية PyGC_Head جزء PyGC_Head ، وهو ضروري للفئات التي تدعم آلية تجميع البيانات المهملة الدورية (بشكل أكثر دقة: علامة PyTypeObject غير محددة في حقل flags في هيكل PyTypeObject المطابق للفئة التي يتم إنشاؤها).


حجم التتبع لعدد كبير من المثيلات أصغر من حجم مثيلات فئة __slots__ :


عدد النسخحجم التتبع
100000048 ميجا بايت
10000000480 ميجا بايت
1000000004.8 غيغابايت

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 :


المجالالحجم (بايت)
PyObject_HEAD16
س8
ذ8
ذ8
المجموع:40

 >>> 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:


عدد النسخحجم التتبع
100000040 ميجا بايت
10000000400 ميجا بايت
1000000004.0 غيغابايت

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 

يحتوي تتبع مثيل في الذاكرة على البنية التالية:


المجالالحجم (بايت)
PyObject_HEAD16
س4
ذ4
ذ4
فارغ4
المجموع:32

حجم التتبع لعدد كبير من النسخ أصغر:


عدد النسخحجم التتبع
100000032 ميجا بايت
10000000320 ميجا بايت
1000000003.2 جيجابايت

ومع ذلك ، يجب أن نتذكر أنه عند الوصول إلى رمز Python ، سيتم إجراء التحويل من int إلى كائن Python والعكس بالعكس في كل مرة.


نمباي


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


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

يتم إنشاء صفيف وعناصر N التي تم تهيئتها باستخدام الأصفار باستخدام الوظيفة:


  >>> points = numpy.zeros(N, dtype=Point) 

حجم الصفيف هو أصغر ممكن:


عدد النسخحجم التتبع
100000012 ميجا بايت
10000000120 ميجا بايت
1000000001.20 غيغابايت

يتطلب الوصول المنتظم إلى عناصر الصفيف والسلاسل تحويل كائن Python
في قيمة C int والعكس بالعكس. يؤدي استخراج صف واحد إلى صفيف يحتوي على عنصر واحد. مساره لن يكون مضغوطًا جدًا:


  >>> sys.getsizeof(points[0]) 68 

لذلك ، كما ذكر أعلاه ، في شفرة Python ، من الضروري معالجة المصفوفات باستخدام وظائف من الحزمة numpy .


استنتاج


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

Source: https://habr.com/ru/post/ar455722/


All Articles