قد تنشأ مشكلة في الذاكرة عندما يكون هناك عدد كبير من الكائنات نشطة في ذاكرة الوصول العشوائي أثناء تنفيذ البرنامج ، خاصةً إذا كانت هناك قيود على إجمالي حجم الذاكرة المتوفرة.
فيما يلي نظرة عامة على بعض طرق تقليل حجم الكائنات ، والتي يمكن أن تقلل بشكل كبير من مقدار ذاكرة الوصول العشوائي اللازمة للبرامج في بيثون النقي.
ملاحظة: هذه هي النسخة الإنجليزية من منصبي الأصلي (باللغة الروسية).
للبساطة ، سننظر في الهياكل في بيثون لتمثيل النقاط مع الإحداثيات x
، y
، z
مع الوصول إلى قيم الإحداثيات بالاسم.
ديكت
في البرامج الصغيرة ، وخاصة في البرامج النصية ، من السهل جدًا والمريح استخدام الأمر المضمن لتمثيل المعلومات الهيكلية:
>>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y
مع ظهور تطبيق أكثر إحكاما في Python 3.6 مع مجموعة من المفاتيح المطلوبة ، أصبح dict
أكثر جاذبية. ومع ذلك ، دعونا نلقي نظرة على حجم بصمتها في ذاكرة الوصول العشوائي:
>>> 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__
هو مرجع لقاموس مثيل الفئة ، والذي يحتوي على قيم سمات المثيل (لاحظ أن منصة مراجع 64 بت تشغل 8 بايت). بدءًا من Python 3.3 ، يتم استخدام المساحة المشتركة لتخزين المفاتيح في القاموس لجميع مثيلات الفصل. هذا يقلل من حجم تتبع مثيل في ذاكرة الوصول العشوائي:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
نتيجةً لذلك ، يكون لعدد كبير من مثيلات الفصل مساحة أصغر في الذاكرة من القاموس العادي ( dict
):
من السهل أن نرى أن حجم المثيل في RAM لا يزال كبيرًا نظرًا لحجم قاموس المثيل.
نسخة من الفصل مع __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
أصبح حجم الكائن في RAM أصغر بكثير:
يؤدي استخدام __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__
، هناك مكتبة [قائمة باسم] ( https://pypi.org/project/namedlist ). namedlist.namedlist
الدالة namedlist.namedlist
فئة مع __slots__
:
>>> Point = namedlist('Point', ('x', 'y', 'z'))
تسمح لك حزمة أخرى [attrs] ( https://pypi.org/project/attrs ) بأتمتة عملية إنشاء فئات مع __slots__
.
الصفوف (tuple)
لدى Python أيضًا tuple
النوع المضمن لتمثيل هياكل البيانات غير القابلة للتغيير. tuple عبارة عن بنية أو سجل ثابت ، ولكن بدون أسماء الحقول. للوصول إلى الحقل ، يتم استخدام فهرس الحقل. ترتبط حقول tuple مرة واحدة وإلى الأبد بكائنات القيمة في وقت إنشاء مثيل tuple:
>>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR
مثيلات tuple مضغوطة تمامًا:
>>> print(sys.getsizeof(ob)) 72
يشغلونها 8 بايت في الذاكرة أكثر من مثيلات الفئات التي تحتوي على __slots__
، نظرًا لأن تتبع tuple في الذاكرة يحتوي أيضًا على عدد من الحقول:
Namedtuple
نظرًا لاستخدام المجموعة على نطاق واسع جدًا ، فقد كان هناك يوم واحد طلب بأنه لا يزال بإمكانك الوصول إلى الحقول وبالاسم أيضًا. كانت الإجابة على هذا الطلب هي 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_z(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z))
جميع مثيلات هذه الفئات لها بصمة ذاكرة مماثلة لتلك الموجودة في tuple. يترك عدد كبير من الحالات بصمة ذاكرة أكبر قليلاً:
Recordclass: muttamedtuple دون دوري GC
نظرًا لأن tuple
و ، بناءً على ذلك ، namedtuple
كائنات ثابتة ، بمعنى أنه لم يعد بالإمكان ربط السمة ob.x
بكائن قيمة آخر ، فقد نشأ طلب لمتغير اسمهtuple قابل للتغيير. نظرًا لعدم وجود أي نوع مضمن في Python مماثل للقيمة التي تدعم المهام ، فقد تم إنشاء العديد من الخيارات. سنركز على [recordclass] ( https://pypi.org/project/recordclass ) ، التي حصلت على تصنيف [stackoverflow] ( https://stackoverflow.com/questions/29290359/existence-of-mutable-named- tuple-in -python / 29419745). بالإضافة إلى أنه يمكن استخدامه لتقليل حجم الكائنات في ذاكرة الوصول العشوائي مقارنة بحجم الكائنات مثل tuple
..
تقدم الحزمة recordclass النوع recordclass.mutabletuple
، والذي يكاد يكون متطابقًا مع tuple ، ولكنه يدعم أيضًا الواجبات. على أساسها ، يتم إنشاء فئات فرعية تكون متطابقة تمامًا تقريبًا معtuples المسماة ، ولكنها تدعم أيضًا تعيين قيم جديدة للحقول (بدون إنشاء مثيلات جديدة). تتيح recordclass
وظيفة recordclass
، مثل وظيفة namedtuple
، أتمتة إنشاء هذه الفئات:
>>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3)
تحتوي مثيلات الفئة على نفس بنية tuple
، ولكن بدون PyGC_Head
فقط:
بشكل افتراضي ، تقوم دالة recordclass
بإنشاء فئة لا تشارك في آلية تجميع البيانات المهملة الدورية. عادةً ما يتم استخدام كل من recordclass
و recordclass
لإنشاء فئات تمثل السجلات أو هياكل بيانات بسيطة (غير متكررة). استخدامها بشكل صحيح في بيثون لا يولد مراجع دائرية. لهذا السبب ، في أعقاب مثيلات الفئات التي تم إنشاؤها بواسطة recordclass
، default, the
fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the
structure, corresponding to the created class, in the
PyTypeObject structure, corresponding to the created class, in the
field, by default, the flag
الإشارات field, by default, the flag
لم يتم تعيين field, by default, the flag
Py_TPFLAGS_HAVE_GC`.
حجم مساحة الذاكرة لعدد كبير من المثيلات أصغر من مثيلات الفئة مع __slots__
:
Dataobject
يستند حل آخر مقترح في مكتبة recordclass إلى الفكرة: استخدم بنية التخزين نفسها في الذاكرة كما في مثيلات الفئة مع __slots__
، لكن لا تشارك في آلية تجميع البيانات المهملة الدورية. يتم إنشاء هذه الفئات باستخدام دالة recordclass.make_dataclass
:
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
الفئة التي تم إنشاؤها بهذه الطريقة ، بشكل افتراضي ، تنشئ مثيلات قابلة للتغيير.
طريقة أخرى - استخدام recordclass.dataobject
الفئة مع الميراث من 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] ( https://cython.org ). ميزته هي أن الحقول يمكن أن تأخذ على قيم الأنواع الذرية للغة C. يتم إنشاء واصفات للوصول إلى الحقول من بيثون النقي تلقائيا. على سبيل المثال:
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
.
استنتاج
في مثال واضح وبسيط ، كان من الممكن التحقق من أن مجتمع لغة برمجة Python (CPython) من المطورين والمستخدمين لديه إمكانيات حقيقية لتخفيض كبير في حجم الذاكرة المستخدمة من قبل الكائنات.