داخل الجهاز الظاهري Python. الجزء الأول



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

ملاحظة الترجمة
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

المقدمة


كانت لغة برمجة Python موجودة لبعض الوقت. بدأ تطوير الإصدار الأول من قبل Guido Van Rossum في عام 1989 ، ومنذ ذلك الحين نمت اللغة وأصبحت واحدة من الأكثر شعبية. يتم استخدام Python في تطبيقات مختلفة: من الواجهات الرسومية إلى تطبيقات تحليل البيانات.

الغرض من هذه المقالة هو الذهاب وراء كواليس المترجم وتقديم نظرة مفاهيمية عن كيفية تنفيذ برنامج مكتوب بلغة Python. سيتم النظر في CPython في المقالة ، لأنه في وقت كتابة هذا التقرير ، كان تطبيق Python الأكثر شيوعًا والأساسي.

يتم استخدام Python و CPython كمرادفات في هذا النص ، ولكن أي ذكر لـ Python يعني CPython (نسخة python التي تم تنفيذها في C). تشمل التطبيقات الأخرى PyPy (يتم تنفيذ python في مجموعة فرعية محدودة من Python) و Jython (التنفيذ على Java Virtual Machine) ، إلخ.

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

  1. التهيئة - تتضمن هذه الخطوة إعداد هياكل البيانات المختلفة التي تتطلبها عملية الثعبان. على الأرجح سيحدث هذا عندما يتم تنفيذ البرنامج في وضع غير تفاعلي من خلال غلاف المترجم.
  2. — , : , , .
  3. — .

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

هذه المادة مخصصة لأي شخص مهتم بمعرفة كيفية عمل جهاز CPython الظاهري. من المفترض أن المستخدم يعرف بالفعل الثعبان ويفهم أساسيات اللغة. عند دراسة هيكل الآلة الافتراضية ، سنواجه كمية كبيرة من C-code ، لذلك سيكون من الأسهل على المستخدم الذي لديه فهم أولي للغة C أن يفهم المادة. وهكذا ، في الأساس ، ما تحتاجه للتعرف على هذه المواد: الرغبة في معرفة المزيد عن الجهاز الافتراضي CPython.

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

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

يستخدم هذا الكتاب إصدار Python 3.

عرض 30،000 قدم


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

يمكن تنفيذ الوحدة المصدر المصدر test.py من سطر الأوامر (عند تمريرها كوسيطة لبرنامج مترجم Python في شكل $ python test.py). هذه مجرد طريقة لاستدعاء ملف Python القابل للتنفيذ. يمكننا أيضًا تشغيل مترجم تفاعلي ، وتنفيذ أسطر الملف كرمز ، إلخ. لكن هذه وغيرها من الأساليب لا تهمنا. إن نقل الوحدة كحجة (داخل سطر الأوامر) إلى الملف القابل للتنفيذ (الشكل 2.1) هو أفضل ما يعكس تدفق الإجراءات المختلفة التي تنطوي عليها التنفيذ الفعلي للتعليمة البرمجية.


الشكل 2.1: دفق في وقت التشغيل.

برنامج الثعبان القابل للتنفيذ هو برنامج C عادي ، لذلك عندما يتم استدعاؤه ، تحدث عمليات مشابهة لتلك الموجودة ، على سبيل المثال ، في لينكس نواة أو برنامج مرحبا العالم البسيط. خذ دقيقة من وقتك لتتفهم: ملف الثعبان القابل للتنفيذ هو مجرد برنامج آخر يطلق برنامجك الخاص. توجد مثل هذه "العلاقات" بين لغة C والمجمع (أو llvm). تبدأ عملية التهيئة القياسية (التي تعتمد على النظام الأساسي الذي يتم فيه التنفيذ) عندما يتم استدعاء الملف التنفيذي بيثون باسم الوحدة النمطية كوسيطة.

تفترض هذه المقالة أنك تستخدم نظام تشغيل يستند إلى Unix ، لذلك قد تختلف بعض الميزات في Windows.

تقوم لغة C عند بدء التشغيل بتنفيذ "سحر" التهيئة الخاص بها - فهي تقوم بتحميل المكتبات ، وفحص / تعيين متغيرات البيئة ، وبعد ذلك ، يتم إطلاق الطريقة الرئيسية لملف بيثون القابل للتنفيذ تمامًا مثل أي برنامج C آخر. الملف الرئيسي القابل للتنفيذ في Python موجود في ./Programs/python.c ويقوم ببعض التهيئة (مثل عمل نسخ من وسيطات سطر أوامر البرنامج التي تم تمريرها إلى الوحدة). تستدعي الوظيفة الرئيسية بعد ذلك وظيفة Py_Main الموجودة في ./Modules/main.c . يعالج عملية التهيئة للمترجم: يحلل وسيطات سطر الأوامر ، ويضع الأعلام ، ويقرأ متغيرات البيئة ، وينفذ الخطافات ، ويجعل وظائف التجزئة عشوائية ، إلخ. أيضا يسمىPy_Initialize من pylifecycle.c ، الذي يتعامل مع تهيئة بنيات بيانات المترجم وحالة التدفق ، هي بنيات بيانات مهمة للغاية.

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

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

قائمة التعليمات البرمجية 2.1: بنية بيانات حالة المترجم يمكن

لأي شخص استخدم لغة برمجة Python لفترة طويلة التعرف على العديد من الحقول المذكورة في هذه البنية (sysdict و builtins و codec).

  1. الحقل التالي * هو إشارة إلى مثيل آخر للمترجم ، حيث يمكن أن يوجد العديد من مترجمين Python في نفس العملية.
  2. في مجال tstate_head * يشير إلى الترابط الرئيسي التنفيذ (إذا كان البرنامج متعددة الخيوط، ثم المترجم هو مشترك لجميع المواضيع التي تم إنشاؤها بواسطة برنامج). سنناقش هذا بمزيد من التفصيل قريبا.
  3. الوحدات ، modules_by_index ، sysdict ، Builtins و importlib تتحدث عن نفسها. يتم تعريفهم جميعًا على أنهم مثيلات PyObject ، وهو نوع الجذر لجميع الكائنات في جهاز Python الظاهري. سيتم مناقشة كائنات بايثون بمزيد من التفصيل في الفصول التالية.
  4. تحتوي الحقول المتعلقة ببرنامج الترميز * على معلومات تساعد في تنزيل الترميزات. هذا مهم جداً لفك تشفير البايت.

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

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

الإدراج 2.2: جزء من

بنية بيانات حالة التدفق تتم مناقشة هياكل بيانات المترجم وحالات التدفق بمزيد من التفصيل في الفصول التالية. كما تقوم عملية التهيئة بإعداد آليات الاستيراد وكذلك المرحلة الابتدائية.

بعد الانتهاء من جميع التهيئة ، يستدعي Py_Main وظيفة run_file (الموجودة أيضًا في وحدة main.c). فيما يلي سلسلة من استدعاءات الوظائف: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagsينشئ مساحة الاسم __main__ حيث سيتم تنفيذ محتويات الملف. يتحقق أيضًا من وجود إصدار pyc للملف (ملف pyc هو ملف بسيط يحتوي على نسخة مجمعة بالفعل من التعليمات البرمجية المصدر). في حالة وجود نسخة pyc ، ستتم محاولة قراءتها كملف ثنائي ، ثم تشغيلها. إذا كان ملف pyc مفقودًا ، فسيتم استدعاء PyRun_FileExFlags ، إلخ. تستدعي الدالة PyParser_ASTFromFileObject PyParser_ParseFileObject ، الذي يقرأ محتويات الوحدة النمطية ويقوم ببناء أشجار التحليل منها. بعد ذلك ، يتم تمرير الشجرة التي تم إنشاؤها إلى PyParser_ASTFromNodeObject ، والتي تنشئ شجرة بناء مجردة منها.

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

يتم إنشاء AST عند استدعاء run_mod . تستدعي هذه الوظيفة PyAST_CompileObject ، التي تنشئ كائنات التعليمات البرمجية من AST. لاحظ أن كود البايت الذي تم إنشاؤه أثناء استدعاء PyAST_CompileObject يتم تمريره من خلال مُحسِّن ثقب الباب البسيط ، الذي يؤدي إلى تحسين منخفض للرمز البيني الذي تم إنشاؤه قبل إنشاء كائنات التعليمات البرمجية. ثم تقوم الدالة run_mod بتطبيق دالة PyEval_EvalCode من ملف ceval.c على كائن التعليمات البرمجية. يؤدي ذلك إلى سلسلة أخرى من استدعاءات الوظائف: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. يتم تمرير كائن التعليمات البرمجية كوسيطة لمعظم هذه الوظائف في شكل أو آخر. _PyEval_EvalFrameEx- هذا هو حلقة المترجم العادي الذي يعالج تنفيذ كائنات التعليمات البرمجية. ومع ذلك ، يتم استدعاؤه ليس فقط مع كائن التعليمات البرمجية كوسيطة ، ولكن مع كائن إطار يحتوي على حقل يشير إلى كائن التعليمات البرمجية كسمة. يوفر هذا الإطار السياق لتنفيذ كائن التعليمات البرمجية. بكلمات بسيطة: حلقة المترجم تقرأ باستمرار التعليمات التالية التي يشير إليها عداد التعليمات من مجموعة التعليمات. ثم تنفذ هذه التعليمات: تضيف أو تزيل كائنات من مكدس القيمة في العملية حتى يتم إفراغها في صفيف التعليمات ليتم تنفيذها (جيدًا ، أو يحدث شيء استثنائي يعطل الحلقة).

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

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

قائمة التعليمات البرمجية 2.3: تفكيك دالة في Python يحتوي

ملف الرأس ./Include/opcodes.h على قائمة كاملة بكافة التعليمات / opcodes للجهاز الظاهري Python. Opcode's بسيطة للغاية. خذ مثالنا في القائمة 2.3 ، التي تحتوي على مجموعة من أربعة تعليمات. تقوم LOAD_FAST بتحميل قيمة الوسيطة الخاصة بها (في هذه الحالة س) على مكدس القيمة. تعتمد الآلة الافتراضية لـ python على المكدس ، لذلك يتم "إبراز" قيم عمليات كود التشغيل من المكدس ، ويتم دفع نتائج الحساب مرة أخرى إلى المكدس لمزيد من الاستخدام من قبل opcodes الأخرى. ثم يخرج BINARY_MULTIPLY عنصرين من المكدس ، ويؤدي إلى الضرب الثنائي لكلتا القيمتين ، ويدفع النتيجة مرة أخرى إلى المكدس. عودة قيمة التعليماتيسترجع قيمة من المكدس ، ويعين القيمة المرجعة للكائن على هذه القيمة ، ويخرج من حلقة المترجم. إذا نظرت إلى القائمة 2.3 ، فمن الواضح أن هذا تبسيط قوي جدًا.

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

  • من أين أتت القيم التي تم تحميلها بواسطة عبارة LOAD_FAST؟
  • من أين تأتي الحجج ، والتي يتم استخدامها كجزء من التعليمات؟
  • كيف يتم التعامل مع استدعاءات الوظائف والأسلوب المتداخلة؟
  • كيف تتعامل حلقة المترجم مع الاستثناءات؟

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

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

All Articles