جهاز CPython. تقرير ياندكس

ننشر ملخصًا للمحاضرة التمهيدية لدورة الفيديو "تطوير الخلفية في بايثون" . فيه ايجور اوفشارينكوegorovcharenko، رئيس فريق في Yandex.Taxi ، تحدث عن الهيكل الداخلي لمترجم CPython.





- باختصار ، ما هي الخطة التي لدينا؟ أولاً ، سنتحدث عن سبب تعلمنا بايثون. ثم دعونا نرى كيف يعمل مترجم CPython بمزيد من العمق ، وكيف يدير الذاكرة ، وكيف يعمل نظام الكتابة في Python ، والقواميس ، والمولدات ، والاستثناءات. أعتقد أن الأمر سيستغرق حوالي ساعة.





لماذا بايثون؟





* insights.stackoverflow.com/survey/2019

**

تفسير شخصي جدًا *** تفسير بحثي

**** للبحث




لنبدأ. لماذا بايثون؟ تعرض الشريحة مقارنة بين العديد من اللغات المستخدمة حاليًا في تطوير الواجهة الخلفية. لكن باختصار ، ما هي ميزة بايثون؟ يمكنك كتابة التعليمات البرمجية عليها بسرعة. هذا ، بالطبع ، شخصي للغاية - يمكن للأشخاص الذين يكتبون C ++ أو Go أن يجادلوا في هذا. لكن في المتوسط ​​، تكون الكتابة بلغة بايثون أسرع.



ما هي العيوب؟ العيب الأول وربما الرئيسي هو أن بايثون أبطأ. يمكن أن يكون أبطأ 30 مرة من اللغات الأخرى ، وإليكدراسةحول هذا الموضوع. لكن سرعته تعتمد على المهمة. هناك فئتان من المهام:



- مقيدة بوحدة المعالجة المركزية ، مهام مرتبطة بوحدة المعالجة المركزية ، وحدة المعالجة المركزية ملزمة



- I / O ملزمة ، مهام محدودة بمدخلات ومخرجات: إما عبر الشبكة أو في قواعد البيانات.



إذا كنت تحل مشكلة مرتبطة بوحدة المعالجة المركزية ، إذن نعم ، ستكون بايثون أبطأ. إذا كانت I / O ملزمة ، وهذه فئة كبيرة من المهام ، إذن لفهم سرعة التنفيذ ، تحتاج إلى تشغيل المعايير. وربما عند مقارنة Python باللغات الأخرى ، فلن تلاحظ فرق الأداء.



بالإضافة إلى ذلك ، يتم كتابة Python ديناميكيًا: لا يتحقق المترجم الفوري من الأنواع في وقت الترجمة. في الإصدار 3.5 ، ظهرت تلميحات الكتابة ، مما يسمح لك بتحديد الأنواع بشكل ثابت ، لكنها ليست صارمة للغاية. أي أنك ستكتشف بعض الأخطاء الموجودة بالفعل في الإنتاج ، وليس في مرحلة التجميع. اللغات الشائعة الأخرى للواجهة الخلفية - Java و C # و C ++ و Go - بها كتابة ثابتة: إذا مررت الكائن الخطأ في الكود ، فسيخبرك المترجم بذلك.



أكثر واقعية ، كيف يتم استخدام Python في تطوير منتجات سيارات الأجرة؟ نحن نتجه نحو بنية الخدمات المصغرة. لدينا بالفعل 160 خدمة مصغرة ، وهي البقالة - 35 منها في Python و 20 - على الإيجابيات. أي أننا نكتب الآن إما بلغة بايثون فقط أو عن الإيجابيات.



كيف نختار اللغة؟ الأول هو متطلبات التحميل ، أي أننا نرى ما إذا كان بإمكان Python التعامل معها أم لا. إذا انسحب ، فإننا ننظر إلى كفاءة مطوري الفريق.



الآن أريد أن أتحدث عن المترجم. كيف يعمل CPython؟



جهاز المترجم الفوري



قد يطرح السؤال: لماذا نحتاج إلى معرفة كيفية عمل المترجم الفوري. السؤال صحيح. يمكنك كتابة الخدمات بسهولة دون معرفة ما هو تحت الغطاء. قد تكون الإجابات على النحو التالي:



1. تحسين الحمل العالي. تخيل أن لديك خدمة بايثون. إنه يعمل ، الحمل منخفض. ولكن في يوم من الأيام تأتيك المهمة - كتابة قلم جاهز لتحمل ثقيل. لا يمكنك الابتعاد عن هذا ، لا يمكنك إعادة كتابة الخدمة بأكملها في C ++. لذلك ، أنت بحاجة إلى تحسين الخدمة من أجل التحميل العالي. يمكن أن يساعد فهم كيفية عمل المترجم الفوري في ذلك.



2. تصحيح الحالات المعقدة. لنفترض أن الخدمة قيد التشغيل ، لكن الذاكرة تبدأ في "التسرب" فيها. في Yandex.Taxi ، لدينا مثل هذه الحالة مؤخرًا. استهلكت الخدمة 8 جيجا بايت من الذاكرة كل ساعة وتعطلت. نحن بحاجة لمعرفة ذلك. يتعلق الأمر باللغة ، بايثون. مطلوب معرفة كيفية عمل إدارة الذاكرة في بايثون.



3. هذا مفيد إذا كنت ستكتب مكتبات معقدة أو تعليمات برمجية معقدة.



4. وبشكل عام - من الجيد معرفة الأداة التي تعمل بها على مستوى أعمق ، وليس فقط كمستخدم. هذا هو موضع تقدير في Yandex.



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







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



Bytecode هو نوع من الكود الوسيط يتم الحصول عليه من الأصل. إنه غير مرتبط بالمنصة ويعمل على جهاز افتراضي. لماذا الافتراضية؟ هذه ليست سيارة حقيقية ، لكنها نوع من التجريد.







ما أنواع الأجهزة الافتراضية الموجودة؟ التسجيل والمكدس. لكن هنا لا يجب أن نتذكر هذا ، ولكن حقيقة أن بايثون هي آلة مكدس. بعد ذلك ، سنرى كيف يعمل المكدس.



وهناك تحذير آخر: سنتحدث هنا فقط عن CPython. CPython هو تطبيق مرجعي لـ Python ، مكتوب ، كما قد تتخيل ، في C. يستخدم كمرادف: عندما نتحدث عن Python ، نتحدث عادةً عن CPython.



ولكن هناك مترجمون آخرون أيضًا. هناك PyPy ، الذي يستخدم تجميع JIT ويسرع حوالي خمس مرات. نادرا ما تستخدم. أنا بصراحة لم أقابل. هناك JPython ، وهناك IronPython ، الذي يترجم الرمز الثانوي لـ Java Virtual Machine وجهاز Dotnet. هذا خارج نطاق محاضرة اليوم - لأكون صادقًا ، لم أجدها. لذلك دعونا نلقي نظرة على CPython.







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







علاوة على ذلك ، فإن شجرة التركيب المجردة ، AST مبنية من هذه الرموز المميزة. أيضًا ، لا تهتم بعد ، فهذه مجرد بعض الأشجار ، في العقد التي تجري فيها عمليات. دعنا نقول في حالتنا أن هناك BinOp ، عملية ثنائية. العملية - الأس ، المعاملات: العدد المطلوب رفعه ، والقدرة على الزيادة.



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



دعونا نرى بمزيد من التفصيل هنا. Bytecode ، كما يخبرنا الاسم ، هو رمز مكون من بايت. وفي Python ، بدءًا من 3.6 ، يكون رمز بايت هو 2 بايت.







البايت الأول هو المشغل نفسه ، ويسمى كود التشغيل. البايت الثاني هو حجة oparg. يبدو أن لدينا من فوق. هذا هو ، تسلسل البايت. لكن لدى Python وحدة تسمى dis ، من Disassembler ، والتي يمكننا من خلالها رؤية تمثيل أكثر قابلية للقراءة من قبل الإنسان.



كيف تبدو؟ يوجد رقم سطر للمصدر - أقصى اليسار. العمود الثاني هو العنوان. كما قلت ، فإن



الرمز الثانوي في Python 3.6 يأخذ 2 بايت ، لذا فإن كل العناوين متساوية ونرى 0 ، 2 ، 4 ... Load.name ، Load.const هي بالفعل خيارات الكود نفسها ، أي رموز تلك العمليات التي يجب أن تنفذ بايثون. 0 ، 0 ، 1 ، 1 هي oparg ، أي وسيطات هذه العمليات. دعونا نرى كيف يتم ذلك بعد ذلك.



(...) دعونا نرى كيف يتم تنفيذ الرمز الثنائي في Python ، ما هي الهياكل الموجودة لهذا الغرض.







إذا كنت لا تعرف C ، فلا بأس. الحواشي هي من أجل الفهم العام.



لدى Python هيكلين يساعداننا في تنفيذ الرمز الثانوي. الأول هو CodeObject ، يمكنك رؤية ملخصه. في الواقع ، الهيكل أكبر. هذا رمز بدون سياق. هذا يعني أن هذه البنية تحتوي ، في الواقع ، على الرمز الثانوي الذي رأيناه للتو. تحتوي على أسماء المتغيرات المستخدمة في هذه الوظيفة ، إذا كانت الوظيفة تحتوي على مراجع للثوابت ، أسماء الثوابت ، شيء آخر.







الهيكل التالي هو FrameObject. هذا هو بالفعل سياق التنفيذ ، الهيكل الذي يحتوي بالفعل على قيمة المتغيرات ؛ إشارات إلى المتغيرات العالمية ؛ حزمة التنفيذ ، والتي سنتحدث عنها بعد قليل ، والكثير من المعلومات الأخرى. دعنا نقول رقم تنفيذ التعليمات.



كمثال: إذا كنت تريد استدعاء دالة عدة مرات ، فسيكون لديك نفس CodeObject ، وسيتم إنشاء FrameObject جديد لكل مكالمة. سيكون لها حججها الخاصة ، مكدسها الخاص. لذا فهم مترابطون.







ما هي حلقة المترجم الرئيسية ، كيف يتم تنفيذ الرمز الثانوي؟ رأيت أن لدينا قائمة برموز التشغيل هذه مع oparg. كيف يتم كل هذا؟ لدى Python ، مثل أي مترجم ، حلقة تنفذ هذا الرمز الثانوي. وهذا يعني أن الإطار يدخله ، وتنتقل Python ببساطة من خلال الرمز الثانوي بالترتيب ، وتنظر إلى أي نوع من الخيارات ، وتنتقل إلى معالجها باستخدام مفتاح ضخم. يتم عرض كود تشغيل واحد فقط هنا على سبيل المثال. على سبيل المثال ، لدينا هنا عملية طرح ثنائية ، وطرح ثنائي ، لنفترض أن "AB" سيتم إجراؤه في هذا المكان.



لنوضح كيف يعمل الطرح الثنائي. بسيط جدًا ، هذا أحد أبسط الرموز. تأخذ الدالة TOP أعلى قيمة من المكدس ، وتخرجها من أعلى قيمة ، ولا تكتفي بإخراجها من المكدس ، ثم يتم استدعاء وظيفة PyNumber_Subtract. النتيجة: يتم دفع وظيفة الشرطة المائلة SET_TOP إلى المكدس. إذا لم يكن واضحًا بشأن المكدس ، فسيتبع ذلك مثال.







باختصار شديد عن GIL. GIL هو كائن على مستوى العملية في Python يأخذ كائن المزامنة هذا في حلقة المترجم الرئيسية. وفقط بعد ذلك يبدأ تنفيذ الرمز الثانوي. يتم ذلك بحيث يقوم مؤشر ترابط واحد فقط بتنفيذ الرمز الثانوي في كل مرة من أجل حماية البنية الداخلية للمترجم.



دعنا نقول ، بالمضي قدماً قليلاً ، أن جميع الكائنات في Python لها عدد من الإشارات إليها. وإذا غيّر موضوعان هذا العدد من الروابط ، فسيتعطل المترجم الفوري. لذلك هناك جيل.



سيتم إخبارك عن هذا في محاضرة حول البرمجة غير المتزامنة. كيف يمكن أن يكون هذا مهمًا بالنسبة لك؟ لا يتم استخدام Multithreading ، لأنه حتى إذا قمت بإنشاء العديد من سلاسل الرسائل ، فعندئذٍ بشكل عام سيكون لديك واحد فقط منهم قيد التشغيل ، وسيتم تنفيذ الرمز الثانوي في أحد سلاسل الرسائل. لذلك ، استخدم إما المعالجة المتعددة ، أو امتداد sish ، أو أي شيء آخر.







مثال سريع. يمكنك استكشاف هذا الإطار بأمان من Python. هناك وحدة sys لها وظيفة تسطير أسفل السطر get_frame. يمكنك الحصول على إطار ومعرفة المتغيرات الموجودة. هناك تعليمات. هذا أكثر للتدريس ، في الحياة الواقعية لم أستخدمه.



دعنا نحاول أن نرى كيف يعمل مكدس آلة Python الافتراضية للفهم. لدينا بعض التعليمات البرمجية ، بسيطة للغاية ، والتي لا تفهم ما تفعله.







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



ماذا تفعل بايثون؟ يتم تنفيذه فقط بالترتيب ، رمز بايت ، في العمود الأوسط ، ويعمل مع المكدس.







لقد قمنا بتشغيل أول كود تشغيل يسمى LOAD_CONST. يحمل ثابتًا. لقد تخطينا الجزء ، تم إنشاء CodeObject هناك ، وكان لدينا CodeObject في مكان ما في الثوابت. قام Python بتحميله على المكدس باستخدام LOAD_CONST. لدينا الآن CodeObject على المكدس في هذا الإطار. يمكننا المضي قدما.







ثم تقوم بايثون بتشغيل كود التشغيل MAKE_FUNCTION. من الواضح أن MAKE_FUNCTION تصنع وظيفة. يتوقع أن يكون لديك CodeObject على المكدس. يقوم ببعض الإجراءات ، ويخلق وظيفة ، ويدفع الوظيفة مرة أخرى إلى المكدس. الآن لديك FUNCTION بدلاً من CodeObject التي كانت موجودة في حزمة الإطارات. والآن يجب وضع هذه الوظيفة في متغير to_power حتى تتمكن من الرجوع إليها.







يتم تنفيذ كود التشغيل STORE_NAME ، ويتم وضعه في متغير to_power. كانت لدينا وظيفة في المكدس ، وهي الآن متغير to_power ، يمكنك الرجوع إليها.



بعد ذلك ، نريد طباعة 10 + قيمة هذه الوظيفة.







ماذا تفعل بايثون؟ تم تحويل هذا إلى رمز بايت. أول كود تشغيل لدينا هو LOAD_CONST. نقوم بتحميل العشرة الأوائل على المكدس. ظهر العشرات على المكدس. الآن نحن بحاجة إلى تنفيذ to_power.







يتم تنفيذ الوظيفة على النحو التالي. إذا كانت تحتوي على وسيطات موضعية - لن ننظر إلى الباقي في الوقت الحالي - فإن Python أولاً تضع الوظيفة نفسها في المكدس. ثم يضع في جميع الوسائط ويستدعي CALL_FUNCTION برقم وسيطة وسيطات الدالة.







قمنا بتحميل المتغير الأول على المكدس ، هذه دالة.







قمنا بتحميل وسيطين إضافيين إلى المكدس - 30 و 2. لدينا الآن دالة ووسيطتان في المكدس. الجزء العلوي من المكدس في الأعلى. CALL_FUNCTION في انتظارنا. نقول: CALL_FUNCTION (2) ، أي لدينا دالة ذات وسيطين. يتوقع CALL_FUNCTION وجود وسيطتين في المكدس ، متبوعين بدالة. لدينا: 2 ، 30 ووظيفة.



كود التشغيل قيد التقدم.







بالنسبة لنا ، وفقًا لذلك ، يترك هذا المكدس ، يتم إنشاء وظيفة جديدة ، حيث سيتم التنفيذ الآن.



الإطار له كومة خاصة به. تم إنشاء إطار جديد لوظيفته. لا تزال فارغة.







مزيد من التنفيذ يحدث. إنه بالفعل أسهل هنا. نحتاج إلى رفع A إلى أس. نقوم بتحميل قيمة المتغير A - 30 على المكدس .. قيمة القوة المتغيرة - 2.







ويتم تنفيذ كود التشغيل BINARY_POWER.







نرفع رقمًا إلى قوة آخر ونعيده إلى المكدس. اتضح 900 في مكدس الوظائف.



سيعيد رمز التشغيل التالي RETURN_VALUE القيمة من الحزمة إلى الإطار السابق.







هذه هي الطريقة التي يتم بها الإعدام. اكتملت الوظيفة ، ومن المرجح أن يكون الإطار واضحًا إذا لم يكن به مراجع وسيكون هناك رقمان في إطار الوظيفة السابقة.







ثم كل شيء هو نفسه تقريبا. الإضافة تحدث.







(...) دعنا نتحدث عن الأنواع و PyObject.



الكتابة







الكائن هو هيكل sish ، حيث يوجد حقلين رئيسيين: الأول هو عدد المراجع لهذا الكائن ، والثاني هو نوع الكائن ، بالطبع ، إشارة إلى نوع الكائن.



ترث الكائنات الأخرى من PyObject من خلال إرفاقها. أي ، إذا نظرنا إلى عدد عشري ، رقم فاصلة عائمة ، فإن البنية الموجودة هناك هي PyFloatObject ، ثم تحتوي على HEAD ، وهي بنية PyObject ، بالإضافة إلى البيانات ، أي ob_fval مزدوج ، حيث يتم تخزين قيمة هذا الطفو نفسه.







وهذا هو نوع الجسم. لقد نظرنا للتو إلى النوع في PyObject ، إنه هيكل يشير إلى نوع. في الواقع ، هذا أيضًا بنية C تحتوي على مؤشرات للوظائف التي تنفذ سلوك هذا الكائن. أي أن هناك هيكل كبير جدًا هناك. يحتوي على وظائف محددة تسمى إذا ، على سبيل المثال ، تريد إضافة كائنين من هذا النوع. أو تريد طرح هذا الكائن أو تسميته أو إنشائه. يجب تحديد أي شيء يمكنك القيام به مع الأنواع في هذه البنية.







على سبيل المثال ، لنلقِ نظرة على الأعداد الصحيحة int ، في Python. أيضا نسخة مختصرة جدا. ما الذي قد نهتم به؟ كثافة العمليات لديها tp_name. يمكنك أن ترى أن هناك tp_hash ، يمكننا الحصول على التجزئة int. إذا استدعينا hash on int ، فسيتم استدعاء هذه الوظيفة. tp_call لدينا صفر ، غير محدد ، وهذا يعني أنه لا يمكننا استدعاء int. tp_str - لم يتم تعريف سلسلة السلسلة. Python لديها وظيفة str التي يمكن أن تتحول إلى سلسلة.



لم تظهر على الشريحة ، لكنكم تعلمون جميعًا أنه لا يزال من الممكن طباعة int. لماذا الصفر هنا؟ نظرًا لوجود tp_repr أيضًا ، فإن Python لديها وظيفتان لتمرير السلسلة: str و repr. صب أكثر تفصيلا لسلسلة. لقد تم تعريفه بالفعل ، ولكنه لم يصل إلى الشريحة ، وسيتم استدعاؤه إذا كنت ، في الواقع ، تقود إلى سلسلة.



في النهاية ، نرى tp_new - دالة يتم استدعاؤها عند إنشاء هذا الكائن. tp_init لدينا صفر. نعلم جميعًا أن int ليست نوعًا متغيرًا ، غير قابل للتغيير. بعد إنشائه ، لا جدوى من تغييره ، أو تهيئته ، بحيث يكون هناك صفر.







لنلقِ نظرة أيضًا على Bool على سبيل المثال. كما يعلم البعض منكم ، فإن Bool في Python يرث في الواقع من int. وهذا يعني أنه يمكنك إضافة Bool ومشاركتها مع بعضها البعض. هذا ، بالطبع ، لا يمكن القيام به ، لكنه ممكن.



نرى أن هناك tp_base - مؤشر على الكائن الأساسي. كل شيء بجانب tp_base هي الأشياء الوحيدة التي تم تجاوزها. أي أن لها اسمها الخاص ، ووظيفة العرض الخاصة بها ، حيث لا تكون رقمًا مكتوبًا ، ولكنها صحيحة أو خاطئة. التمثيل كرقم ، يتم تجاوز بعض الوظائف المنطقية هناك. Docstring هو خاص بها وخلقها. كل شيء آخر يأتي من int.







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



في Python ، ينمو الحجم مثل 0 ، 4 ، 8 ، 16 ، 25 ، أي وفقًا لنوع من المعادلة التي تسمح لنا بالقيام بالإدخال بشكل مقارب للثابت. ويمكنك أن ترى أن هناك مقتطفًا من وظيفة الإدراج في القائمة. وهذا يعني أننا نقوم بتغيير الحجم. إذا لم يكن لدينا تغيير الحجم ، فإننا نلقي بخطأ ونعين العنصر. في Python ، هذه مصفوفة ديناميكية عادية مطبقة في C.



(...) لنتحدث عن القواميس باختصار. هم موجودون في كل مكان في بايثون.



قواميس



نعلم جميعًا أنه في الكائنات ، يتم تضمين التكوين الكامل للفئات في القواميس. الكثير من الأشياء مبنية عليها. القواميس في بايثون في جدول تجزئة.







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



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



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





ارتباط من الشريحة



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



إذا نظرنا إلى مصفوفة المؤشرات ، فسنجد في المجموعة الأولى لا شيء ، وفي المجموعة الثانية يوجد عنصر بالفهرس 1 من هذه المصفوفة ، وما إلى



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



هناك بعض التحسينات المثيرة للاهتمام التي تستخدمها Python. لكي تعمل جداول التجزئة هذه ، نحتاج إلى عملية مقارنة العناصر. تخيل أننا وضعنا عنصرًا في جدول تجزئة ، ثم نريد أن نأخذ عنصرًا. نأخذ التجزئة ونذهب إلى الدلو. نرى: الدلو ممتلئ ، هناك شيء ما. لكن هل هذا هو العنصر الذي نحتاجه؟ ربما كان هناك تصادم عندما تم وضعه وكان العنصر مناسبًا في الواقع في دلو آخر. لذلك ، يجب أن نقارن المفاتيح. إذا كان المفتاح خاطئًا ، فإننا نستخدم نفس آلية البحث التالية التي يتم استخدامها لحل التصادم. ودعنا ننتقل.





ارتباط من الشريحة



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



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



وفقط في حالة غير محتملة للغاية - إذا كانت تجزئاتنا متساوية ، لكننا لا نعرف ما إذا كان نفس الكائن - عندها فقط نقارن الكائنات نفسها.



شيء صغير مثير للاهتمام: لا يمكنك إدخال أي شيء في المفاتيح أثناء التكرار. هذا خطأ.







تحت الغطاء ، يحتوي القاموس على متغير يسمى الإصدار ، والذي يخزن إصدار القاموس. عندما تقوم بتغيير القاموس ، يتغير الإصدار ، فإن Python تفهم ذلك وتلقي عليك بخطأ.







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



هذا المنطق مكتوب بلغة بايثون. لكي لا تكتب إذا كان ضخمًا من النموذج "إذا كانت حالة الطلب كذا وكذا ، فافعل هذا" ، فهناك إملاء يكون المفتاح فيه هو حالة الطلب. وهناك مجموعة tuple لـ VALUE ، والتي تحتوي على جميع المعالجات التي يجب تنفيذها عند الانتقال إلى هذه الحالة. هذه ممارسة شائعة ، في الواقع ، إنها بديل لمفتاح sish.







عدد قليل من الأشياء حسب النوع. سأخبرك عن غير قابل للتغيير. هذه أنواع بيانات غير قابلة للتغيير ، وهي أنواع قابلة للتغيير على التوالي: إملاءات ، فئات ، مثيلات فئة ، أوراق ، وربما شيء آخر. كل شيء آخر تقريبًا عبارة عن سلاسل وأرقام عادية - فهي غير قابلة للتغيير. ما هي الأنواع المتغيرة؟ أولاً ، تجعل الشفرة أسهل في الفهم. أي ، إذا رأيت في الكود أن شيئًا ما عبارة عن مجموعة ، فهل تفهم أنه لا يتغير أكثر ، وهذا يسهل عليك قراءة الكود؟ فهم ما سيحدث بعد ذلك. في tuple ds لا يمكنك كتابة العناصر. ستفهم هذا ، وسيساعد في قراءتك أنت وجميع الأشخاص الذين سيقرؤون الرمز نيابة عنك.



لذلك ، هناك قاعدة: إذا لم تقم بتغيير شيء ما ، فمن الأفضل استخدام الأنواع غير القابلة للتغيير. كما أنه يؤدي إلى عمل أسرع. هناك نوعان من الثوابت التي يستخدمها tuple: pit_tuple و tap_tuple و max و CC. ما هي النقطة؟ لجميع المجموعات التي يصل حجمها إلى 20 ، يتم استخدام طريقة تخصيص محددة ، مما يجعل هذا التخصيص أسرع. ويمكن أن يكون هناك ما يصل إلى ألفي كائن من كل نوع ، الكثير. هذا أسرع بكثير من الأوراق ، لذلك إذا كنت تستخدم tuple ، فستكون أسرع.



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







كيف تبدو في C؟ مثال. على اليسار ، توجد قائمة عادية على اليمين. هنا ، بالطبع ، ليست كل الاختلافات مرئية ، ولكن فقط تلك التي أردت إظهارها. في القائمة الموجودة في حقل tp_hash لدينا NotImplemented ، أي أن القائمة لا تحتوي على تجزئة. في tuple ، هناك بعض الوظائف التي ستعيد لك علامة التجزئة. هذا هو بالضبط السبب في أن tuple ، من بين أشياء أخرى ، يمكن أن يكون مفتاح dict ، ولا يمكن لـ list.



الشيء التالي المميز هو وظيفة تعيين العنصر ، sq_ass_item. في القائمة ، في tuple يكون صفرًا ، أي أنه لا يمكنك بطبيعة الحال تعيين أي شيء إلى tuple.







شيء اخر. بايثون لا تنسخ أي شيء حتى نطلبه. يجب تذكر هذا أيضًا. إذا كنت تريد نسخ شيء ما ، فاستخدم ، على سبيل المثال ، وحدة النسخ ، التي تحتوي على وظيفة copy.deepcopy. ماهو الفرق؟ نسخ الكائن ، إذا كان كائن حاوية ، مثل قائمة الأشقاء. يتم إدراج كافة المراجع التي كانت موجودة في هذا الكائن في الكائن الجديد. والنسخ العميق ينسخ بشكل متكرر جميع الكائنات داخل هذه الحاوية وخارجها.



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



(...) بعد ذلك ، دعنا نتحدث عن إدارة الذاكرة.



إدارة الذاكرة







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



في الواقع ، تنشئ Python الكثير من الكائنات الصغيرة في وقت التشغيل. وإذا استخدمنا وظيفة malloc القياسية لتخصيصها ، فسنجد أنفسنا سريعًا في حقيقة أن ذاكرتنا مجزأة ، وبالتالي ، فإن تخصيص الذاكرة بطيء.







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



عندما نحاول تخصيص أقل من 512 بايت لكائن ما ، تختار Python بطريقتها الخاصة كتلة مناسبة لهذا الكائن وتضع الكائن في هذه الكتلة.



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







تحرير الذاكرة. في وقت سابق رأينا بنية PyObject. لديها هذا المرجع - عدد المرجع. إنه يعمل بكل بساطة. عندما تشير إلى هذا الكائن ، فإن Python تزيد من عدد المرجع. بمجرد أن يكون لديك كائن ، يختفي المرجع إليه ، وقم بإلغاء تخصيص عدد المرجع.



ما تم تمييزه باللون الأصفر. إذا لم يكن refcnt صفراً ، فإننا نفعل شيئًا هناك. إذا كانت قيمة refcnt تساوي صفرًا ، فسنقوم على الفور بإلغاء تخصيص الكائن. نحن لا ننتظر أي جامع قمامة ، لا شيء ، ولكن في هذه اللحظة نقوم بمسح الذاكرة.



إذا صادفت طريقة del ، فإنها ببساطة تزيل ارتباط المتغير بالكائن. ويتم استدعاء طريقة __del__ ، التي يمكنك تعريفها في الفصل الدراسي ، عند إزالة الكائن فعليًا من الذاكرة. سوف تستدعي del على الكائن ، ولكن إذا كان لا يزال يحتوي على مراجع ، فلن يتم حذف الكائن في أي مكان. ولن يتم استدعاء محضره النهائي __del__. على الرغم من أنها تسمى متشابهة للغاية.



عرض توضيحي قصير عن كيف يمكنك رؤية عدد الروابط. هناك وحدة sys المفضلة لدينا ، والتي لها وظيفة getrefcount. يمكنك رؤية عدد الروابط لكائن ما.







سأخبرك أكثر. كائن مصنوع. عدد الروابط مأخوذ منه. تفاصيل مثيرة للاهتمام: يشير المتغير A إلى TaxiOrder. تأخذ عدد الروابط ، وسوف تحصل على "2" مطبوعة. يبدو لماذا؟ لدينا مرجع كائن واحد. ولكن عند استدعاء getrefcount ، يتم التفاف هذا الكائن حول الوسيطة داخل الدالة. لذلك ، لديك بالفعل مرجعين لهذا الكائن: الأول هو المتغير ، والثاني هو وسيطة الوظيفة. لذلك ، تتم طباعة "2".



الباقي تافه. نقوم بتعيين متغير آخر للكائن ، نحصل على 3. ثم نقوم بإزالة هذا الربط ، نحصل على 2. ثم نقوم بإزالة جميع الإشارات إلى هذا الكائن ، وفي نفس الوقت يتم استدعاء الصيغة النهائية ، والتي ستطبع سطرنا.







(...) هناك ميزة أخرى مثيرة للاهتمام لـ CPython ، والتي لا يمكن البناء عليها ، ويبدو أنه لا يُقال عنها في أي مكان في المستندات. غالبًا ما يتم استخدام الأعداد الصحيحة. سيكون من الإسراف في إعادة إنشائها في كل مرة. لذلك ، الأكثر استخدامًا ، اختار مطورو Python النطاق من –5 إلى 255 ، وهم Singleton. أي ، تم إنشاؤها مرة واحدة ، وتقع في مكان ما في المترجم ، وعندما تحاول الحصول عليها ، تحصل على مرجع لنفس الشيء. أخذنا A و B ، واحد ، وطبعناهم ، وقارننا عناوينهم. حصلت على الحقيقة. ولدينا ، على سبيل المثال ، 105 إشارة إلى هذا الكائن ، ببساطة لأنه يوجد الآن الكثير.



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







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



كيف يعمل؟ أولاً ، سأتحدث بإيجاز عن الأجيال ، ثم عن الخوارزمية.







لتحسين سرعة أداة تجميع القمامة في Python ، فهي تعمل عبر الأجيال ، أي أنها تعمل باستخدام الأجيال. هناك ثلاثة أجيال. لماذا هم بحاجة؟ من الواضح أن تلك الأشياء التي تم إنشاؤها مؤخرًا من المرجح أن تكون غير ضرورية أكثر من الأشياء طويلة العمر. لنفترض أنك قمت بإنشاء شيء ما في سياق الوظائف. على الأرجح ، عند الخروج من الوظيفة ، لن تكون هناك حاجة إليها. الأمر نفسه مع الحلقات ، والمتغيرات المؤقتة. كل هذه الأشياء تحتاج إلى التنظيف في كثير من الأحيان أكثر من تلك التي تعيش لفترة طويلة.



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



افتراضيا هناك 700 ، 10 ، 10. ما هو 700؟ هذا هو عدد إنشاء الكائن مطروحًا منه عدد عمليات الحذف. بمجرد أن يتجاوز 700 ، يبدأ جيل جديد من جمع القمامة. و 10 ، 10 هو عدد مجموعات القمامة في الجيل السابق ، وبعد ذلك نحتاج إلى بدء جمع القمامة في الجيل الحالي.



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







كيف يعمل جمع القمامة في بايثون؟ لنفترض أننا بدأنا جمع القمامة في الجيل 0. لدينا بعض الأشياء ، ولديها دورات. توجد مجموعة من الكائنات على اليسار تشير إلى بعضها البعض ، والمجموعة الموجودة على اليمين تشير أيضًا إلى بعضها البعض. تفاصيل مهمة - تمت الإشارة إليها أيضًا من الجيل 1. كيف تكتشف Python الحلقات؟ أولاً ، يتم إنشاء متغير مؤقت لكل كائن ويتم كتابة عدد المراجع لهذا الكائن فيه. ينعكس هذا على الشريحة. لدينا رابطان للكائن في الأعلى. ومع ذلك ، تتم الإشارة إلى كائن من الجيل 1 من الخارج. تتذكر بايثون هذا. ثم (مهم!) يمر عبر كل كائن داخل الجيل ويحذف ، ويقلل العداد بعدد المراجع داخل هذا الجيل.







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



ماذا تفعل بايثون بعد ذلك؟ إنه ، نظرًا لوجود واحد هنا ، يفهم أن هذه الأشياء تمت الإشارة إليها من الخارج. ولا يمكننا حذف هذا الكائن أو هذا ، وإلا فإننا سننتهي بموقف غير صالح. لذلك ، تنقل Python هذه الكائنات إلى الجيل 1 ، وكل ما تبقى في الجيل 0 ، يتم حذفه وتنظيفه. كل شيء عن جامع القمامة.







(...) استمر. سأخبرك بإيجاز شديد عن المولدات.



مولدات كهرباء







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



ماذا يمكنك ان تفعل بالمولدات؟ يمكنك الحصول على مولد ، وهذا سيعيد القيم لك ، تذكر السياق. يمكنك العودة للمولد. في هذه الحالة ، سيتم طرح تنفيذ StopIteration ، وستحتوي القيمة التي بداخلها على القيمة ، وفي هذه الحالة Y.



حقيقة أقل شهرة: يمكنك إرسال بعض القيم إلى المولد. أي أنك تستدعي طريقة الإرسال على المولد ، وستكون Z - انظر المثال - هي قيمة تعبير العائد الذي سيستدعيه المولد. إذا كنت تريد التحكم في المولد ، يمكنك تمرير القيم هناك.



يمكنك أيضًا طرح الاستثناءات هناك. نفس الشيء: خذ جسم مولد ، ارميه. أنت ترمي خطأ هناك. سيكون لديك خطأ في مكان آخر عائد. وأغلق - يمكنك إغلاق المولد. ثم يتم رفع تنفيذ GeneratorExit ، ومن المتوقع ألا ينتج عن المولد أي شيء آخر.







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







عند تنفيذ وظيفة المولد ، كيف تعرف Python أنك لست بحاجة إلى تنفيذها ، ولكن عليك إنشاء مولد؟ يحتوي CodeObject الذي نظرنا إليه على إشارات. وعندما تستدعي دالة ، تتحقق Python من أعلامها. إذا كانت علامة CO_GENERATOR موجودة ، فإنها تدرك أن الوظيفة لا تحتاج إلى التنفيذ ، ولكنها تحتاج فقط إلى إنشاء مولد. وهو يصنعها. دالة PyGen_NewWithQualName.







كيف يسير الإعدام؟ من GENERATOR_FUNCTION ، يقوم المولد أولاً باستدعاء GENERATOR_Object. ثم يمكنك استدعاء GENERATOR_Object باستخدام التالي للحصول على القيمة التالية. كيف تحدث المكالمة التالية؟ يتم أخذ إطاره من المولد ، ويتم تخزينه في المتغير F. وإرساله إلى الحلقة الرئيسية للمترجم الشفهي EvalFrameEx. يتم إعدامك كما في حالة الوظيفة العادية. يتم استخدام رمز الخريطة YIELD_VALUE للعودة وإيقاف تنفيذ المولد مؤقتًا. يتذكر كل السياق في الإطار ويتوقف عن التنفيذ. كان هذا هو الموضوع قبل الأخير.



(...) تلخيص سريع لماهية الاستثناءات وكيفية استخدامها في بايثون.



استثناءات







الاستثناءات هي طريقة للتعامل مع حالات الخطأ. لدينا كتلة محاولة. يمكننا أن نكتب في محاولة تلك الأشياء التي يمكن أن تلقي استثناءات. لنفترض أنه يمكننا رفع خطأ باستخدام كلمة زيادة. بمساعدة باستثناء يمكننا اكتشاف أنواع معينة من الاستثناءات ، في هذه الحالة SomeError. باستثناء أننا نمسك جميع الاستثناءات بدون تعبير يتم استخدام كتلة else بشكل أقل تكرارًا ، ولكنها موجودة ولن يتم تنفيذها إلا في حالة عدم وجود استثناءات. سيتم تنفيذ الكتلة النهائية على أي حال.



كيف تعمل الاستثناءات في CPython؟ بالإضافة إلى مكدس التنفيذ ، يحتوي كل إطار أيضًا على مجموعة من الكتل. من الأفضل استخدام مثال.











مكدس الكتل هو مكدس تتم كتابة الكتل عليه. كل كتلة لها نوع ، معالج ، معالج. المعالج هو عنوان الرمز الثانوي الذي يجب الانتقال إليه لمعالجة هذه الكتلة. كيف يعمل؟ لنفترض أن لدينا بعض التعليمات البرمجية. لقد أنشأنا كتلة try ، ولدينا كتلة استثناء حيث نلتقط استثناءات RuntimeError ، وكتلة أخيرة ، والتي يجب أن تكون في أي حال.



كل هذا يتدهور في هذا الرمز الثانوي. في بداية كود البايت في كتلة try ، نرى اثنين من كود التشغيل SETUP_FINALLY مع وسيطات لـ 40 و 12. هذه هي عناوين المعالجات. عندما يتم تنفيذ SETUP_FINALLY ، يتم وضع كتلة على مكدس الكتلة ، والتي تقول: لمعالجتي ، انتقل في إحدى الحالات إلى العنوان 40 ، وفي الحالة الأخرى - إلى العنوان الثاني عشر.



12 أسفل المكدس باستثناء السطر الذي يحتوي على آخر خطأ وقت التشغيل. هذا يعني أنه عندما يكون لدينا استثناء ، سننظر في مكدس الكتلة بحثًا عن كتلة من النوع SETUP_FINALLY. ابحث عن الكتلة التي يوجد بها انتقال إلى العنوان 12 ، انتقل إلى هناك. وهناك مقارنة بين الاستثناء والنوع: نتحقق مما إذا كان نوع الاستثناء هو RuntimeError أم لا. إذا كانت متساوية ، فإننا ننفذها ، وإلا فإننا نقفز إلى مكان آخر.



أخيرًا هو الكتلة التالية في مكدس الكتلة. سيتم تنفيذه بالنسبة لنا إذا كان لدينا استثناء آخر. ثم سيستمر البحث في مكدس الكتلة هذا ، وسنصل إلى كتلة SETUP_FINALLY التالية. سيكون هناك معالج يخبرنا ، على سبيل المثال ، العنوان 40. نقفز إلى العنوان 40 - يمكنك أن ترى من الكود أن هذه كتلة أخيرًا.







إنه يعمل بكل بساطة في CPython. لدينا جميع الوظائف التي يمكنها رفع الاستثناءات وإرجاع رمز القيمة. إذا كان كل شيء على ما يرام ، يتم إرجاع 0. إذا كان خطأ ، يتم إرجاع -1 أو NULL ، اعتمادًا على نوع الوظيفة.



خذ مثل هذا الشكل الداخلي في C. نرى كيف يحدث الانقسام. وهناك تحقق من أنه إذا كان B يساوي صفرًا ولا نريد القسمة على صفر ، فإننا نتذكر الاستثناء ونعيد NULL. هذا يعني أنه كان هناك خطأ. لذلك ، يجب أيضًا التخلص من جميع الوظائف الأخرى الأعلى في مكدس الاستدعاءات NULL. سنرى هذا في الحلقة الرئيسية للمترجم ونقفز هنا.







هذا هو فك المكدس. كل شيء كما قلت: نمر عبر مكدس الكتلة بالكامل ونتحقق من أن نوعه SETUP_FINALLY. إذا كان الأمر كذلك ، فقفز فوق Handler ، الأمر بسيط للغاية. هذا ، في الواقع ، كل شيء.



الروابط



مترجم العام:

docs.python.org/3/reference/executionmodel.html

github.com/python/cpython

leanpub.com/insidethepythonvirtualmachine/read



إدارة الذاكرة:

arctrix.com/nas/python/gc

rushter.com/blog/python -memory-managment

instagram-engineering.com/dismissing-python-garbage-collection-at-instagram-4dca40b29172

stackify.com/python-garbage-collection



الاستثناءات:

bugs.python.org/issue17611



All Articles