पायथन वर्चुअल मशीन के अंदर। भाग 1



सभी को नमस्कार। मैंने अंततः यह पता लगाने का निर्णय लिया कि पायथन दुभाषिया कैसे काम करता है। ऐसा करने के लिए, उन्होंने एक लेख-पुस्तक का अध्ययन करना शुरू किया और उसी समय रूसी में अनुवाद करने की कल्पना की। तथ्य यह है कि अनुवाद आपको एक असंगत वाक्य को याद करने की अनुमति नहीं देते हैं और सामग्री की आत्मसात की गुणवत्ता बढ़ जाती है)।मैं संभव अशुद्धि के लिए अग्रिम में माफी मांगता हूं। मैं हमेशा यथासंभव सही अनुवाद करने की कोशिश करता हूं, लेकिन मुख्य समस्याओं में से एक: रूसी समकक्ष में कुछ शर्तों का बस कोई उल्लेख नहीं है।

अनुवाद नोट
Python , «code object», ( ) . , .

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

परिचय


पायथन प्रोग्रामिंग भाषा लगभग कुछ समय के लिए रही है। पहले संस्करण का विकास गुइडो वान रोसुम द्वारा 1989 में शुरू किया गया था, और तब से भाषा बढ़ी है और सबसे लोकप्रिय में से एक बन गई है। पायथन का उपयोग विभिन्न अनुप्रयोगों में किया जाता है: ग्राफिकल इंटरफेस से लेकर डेटा विश्लेषण अनुप्रयोगों तक।

इस लेख का उद्देश्य दुभाषिया के दृश्यों के पीछे जाना और एक वैचारिक अवलोकन प्रदान करना है कि पायथन में लिखे गए प्रोग्राम को कैसे निष्पादित किया जाता है। लेख में सीपीथॉन पर विचार किया जाएगा, क्योंकि लेखन के समय, यह सबसे लोकप्रिय और बुनियादी पायथन कार्यान्वयन है।

इस पाठ में पायथन और सीपीथॉन को समानार्थक शब्द के रूप में प्रयोग किया जाता है, लेकिन पायथन के किसी भी उल्लेख का अर्थ है सीपीथॉन (सी में लागू अजगर संस्करण)। अन्य कार्यान्वयन में PyPy (अजगर को सीमित उपसमूह में लागू किया गया अजगर), Jython (जावा वर्चुअल मशीन पर कार्यान्वयन), आदि शामिल हैं।

मैं एक पायथन कार्यक्रम के निष्पादन को दो या तीन मुख्य चरणों (नीचे सूचीबद्ध) में विभाजित करना पसंद करता हूं, यह इस बात पर निर्भर करता है कि दुभाषिया को कैसे बुलाया जाता है। इस लेख में इन चरणों को अलग-अलग डिग्री में कवर किया जाएगा:

  1. इनिशियलाइज़ेशन - इस कदम में अजगर प्रक्रिया द्वारा आवश्यक विभिन्न डेटा संरचनाओं को स्थापित करना शामिल है। सबसे अधिक संभावना है कि यह तब होगा जब कार्यक्रम दुभाषिया के खोल के माध्यम से गैर-इंटरैक्टिव मोड में निष्पादित किया जाता है।
  2. — , : , , .
  3. — .

"पार्सिंग" पेड़ों के साथ-साथ अमूर्त सिंटैक्स पेड़ों (एएसडी) को उत्पन्न करने का तंत्र भाषा स्वतंत्र है। इसलिए, हम इस विषय को बहुत अधिक कवर नहीं करेंगे, क्योंकि पायथन में उपयोग किए जाने वाले तरीके अन्य प्रोग्रामिंग भाषाओं के तरीकों के समान हैं। दूसरी ओर, पायथन में एडीएस से प्रतीक तालिकाओं और कोड ऑब्जेक्ट्स के निर्माण की प्रक्रिया अधिक विशिष्ट है, इसलिए, यह विशेष ध्यान देने योग्य है। यह संकलित कोड ऑब्जेक्ट्स और अन्य सभी डेटा संरचनाओं की व्याख्या पर भी चर्चा करता है। हमारे द्वारा कवर किए गए विषयों में शामिल होंगे, लेकिन यह सीमित नहीं हैं: प्रतीक तालिकाओं के निर्माण और कोड ऑब्जेक्ट्स, पायथन ऑब्जेक्ट्स, फ़्रेम ऑब्जेक्ट्स, कोड ऑब्जेक्ट्स, फ़ंक्शनल ऑब्जेक्ट्स, ऑपकोड, इंटरप्रेटर लूप्स, जनरेटर और उपयोगकर्ता वर्ग बनाने की प्रक्रिया।

यह सामग्री सीखने के इच्छुक किसी व्यक्ति के लिए अभिप्रेत है कि सीपीथॉन वर्चुअल मशीन कैसे काम करती है। यह माना जाता है कि उपयोगकर्ता पहले से ही अजगर से परिचित है और भाषा की मूल बातें समझता है। वर्चुअल मशीन की संरचना का अध्ययन करते समय, हम सी-कोड की एक महत्वपूर्ण राशि का सामना करेंगे, इसलिए यह उस उपयोगकर्ता के लिए आसान होगा, जिसके पास सामग्री को समझने के लिए सी की प्रारंभिक समझ है। और इसलिए, मूल रूप से, आपको इस सामग्री से परिचित होने की आवश्यकता है: सीपीथॉन वर्चुअल मशीन के बारे में अधिक जानने की इच्छा।

यह लेख दुभाषिया के आंतरिक कार्य के अध्ययन में किए गए व्यक्तिगत नोट्स का एक विस्तारित संस्करण है। PyCon वीडियो , स्कूल लेक्चर और इस ब्लॉग में बहुत सारे गुणवत्ता वाले सामान हैं ज्ञान के इन शानदार स्रोतों के बिना मेरा काम पूरा नहीं होता।

इस पुस्तक के अंत में, पाठक यह समझने में सक्षम होंगे कि पायथन दुभाषिया आपके कार्यक्रम को कैसे निष्पादित करता है। इसमें प्रोग्राम निष्पादन और डेटा संरचनाओं के विभिन्न चरण शामिल हैं जो प्रोग्राम में महत्वपूर्ण हैं। शुरू करने के लिए, हम एक पक्षी का नज़रिया लेंगे जो तब होता है जब एक तुच्छ कार्यक्रम निष्पादित किया जाता है, जब कमांड लाइन पर मॉड्यूल का नाम दुभाषिया के पास जाता है। पायथन डेवलपर गाइड के अनुसरण में CPython निष्पादन योग्य कोड स्रोत से स्थापित किया जा सकता है

यह पुस्तक पायथन 3 संस्करण का उपयोग करती है।

30,000 फुट का दृश्य


यह अध्याय इस बारे में बात करता है कि दुभाषिया पायथन कार्यक्रम को कैसे निष्पादित करता है। निम्नलिखित अध्यायों में, हम इस "पहेली" के विभिन्न भागों की जाँच करेंगे और प्रत्येक भाग का अधिक विस्तृत विवरण प्रदान करेंगे। पाइथन में लिखे गए एक कार्यक्रम की जटिलता के बावजूद, यह प्रक्रिया हमेशा समान होती है। यानिव इंटरनैशनल पर अपने लेखों की श्रृंखला में यानिव अकिन द्वारा दी गई उत्कृष्ट व्याख्या हमारी चर्चा के लिए विषय निर्धारित करती है।

स्रोत मॉड्यूल टेस्टहोम को कमांड लाइन से निष्पादित किया जा सकता है (जब इसे पायथन इंटरप्रेटर प्रोग्राम के लिए $ अजगर अजगर के रूप में तर्क के रूप में पारित किया जाता है)। यह पायथन निष्पादन योग्य को लागू करने का सिर्फ एक तरीका है। हम एक इंटरएक्टिव इंटरप्रेटर भी लॉन्च कर सकते हैं, कोड के रूप में किसी फ़ाइल की लाइनों को निष्पादित कर सकते हैं, आदि। लेकिन यह और अन्य तरीके हमें रूचि नहीं देते हैं। यह निष्पादन योग्य फ़ाइल (चित्रा 2.1) के लिए एक तर्क (कमांड लाइन के अंदर) के रूप में मॉड्यूल का स्थानांतरण है जो कोड के वास्तविक निष्पादन में शामिल विभिन्न कार्यों के प्रवाह को सबसे अच्छा दर्शाता है।


चित्र 2.1: रनटाइम पर स्ट्रीम करें।

अजगर निष्पादन योग्य एक नियमित सी कार्यक्रम है, इसलिए जब इसे बुलाया जाता है, तो उन प्रक्रियाओं के समान प्रक्रियाएं होती हैं जो मौजूद हैं, उदाहरण के लिए, लिनक्स कर्नेल या सरल हैलो वर्ल्ड प्रोग्राम में, होते हैं। समझने के लिए अपने समय का एक मिनट लें: अजगर का निष्पादन केवल एक और कार्यक्रम है जो आपकी खुद की शुरूआत करता है। ऐसे "संबंध" सी भाषा और कोडांतरक (या llvm) के बीच मौजूद हैं। मानक इनिशियलाइज़ेशन प्रक्रिया (जो उस प्लेटफ़ॉर्म पर निर्भर करती है जहाँ निष्पादन होता है) तब शुरू होता है जब अजगर निष्पादन योग्य को एक तर्क के रूप में मॉड्यूल नाम से पुकारा जाता है।

यह आलेख मानता है कि आप यूनिक्स-आधारित ऑपरेटिंग सिस्टम का उपयोग कर रहे हैं, इसलिए कुछ विशेषताएं विंडोज पर भिन्न हो सकती हैं।

स्टार्टअप पर सी भाषा प्रारंभ के सभी "जादू" को निष्पादित करती है - यह पुस्तकालयों को लोड करती है, पर्यावरण चर को चेक / सेट करती है, और उसके बाद, अजगर निष्पादन योग्य का मुख्य तरीका किसी भी अन्य सी प्रोग्राम की तरह ही लॉन्च किया जाता है। पायथन की मुख्य निष्पादन योग्य फ़ाइल ./Programs/python.c में स्थित है और कुछ इनिशियलाइज़ेशन (जैसे प्रोग्राम कमांड लाइन के तर्कों की प्रतियां जो मॉड्यूल को पारित की गई थीं) करती है। मुख्य कार्य तो कॉल Py_Main समारोह में स्थित ./Modules/main.c । यह दुभाषिया की प्रारंभिक प्रक्रिया को संसाधित करता है: कमांड लाइन तर्क का विश्लेषण करता है, झंडे सेट करता है, पर्यावरण चर पढ़ता है, हुक निष्पादित करता है, हैश कार्यों को यादृच्छिक करता है, आदि। यह भी कहा जाता हैPy_Initialize से pylifecycle.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: दुभाषिया राज्य डेटा संरचना

जो कोई भी लंबे समय से पायथन प्रोग्रामिंग भाषा का उपयोग करता है, वह इस संरचना (sysdict, buildins, codec) में वर्णित कई क्षेत्रों को पहचान सकता है।

  1. * अगला क्षेत्र दुभाषिया के एक और उदाहरण का संदर्भ है, क्योंकि कई पायथन दुभाषिए एक ही प्रक्रिया के भीतर मौजूद हो सकते हैं।
  2. * Tstate_head क्षेत्र निष्पादन के मुख्य थ्रेड इंगित करता है (यदि कार्यक्रम मल्टी-थ्रेडेड है, तो दुभाषिया कार्यक्रम द्वारा बनाई गई सभी धागे के लिए आम है)। हम जल्द ही इस पर और विस्तार से चर्चा करेंगे।
  3. मॉड्यूल, mod_by_index, sysdict, buildins और importlib खुद के लिए बोलते हैं। उन सभी को PyObject के उदाहरणों के रूप में परिभाषित किया गया है , जो पायथन वर्चुअल मशीन में सभी वस्तुओं के लिए मूल प्रकार है। निम्नलिखित अध्यायों में अजगर वस्तुओं पर अधिक विस्तार से चर्चा की जाएगी।
  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: धारा राज्य डेटा

संरचना का हिस्सा इंटरप्रेटर डेटा संरचनाएं और स्ट्रीम स्टेट्स निम्नलिखित अध्यायों में अधिक विस्तार से चर्चा की गई है। इनिशियलाइज़ेशन प्रक्रिया आयात तंत्र के साथ-साथ प्राथमिक stdio भी स्थापित करती है।

सभी आरंभ को पूरा करने के बाद, Py_Main run_file फ़ंक्शन (main.c मॉड्यूल में भी स्थित) को कॉल करता है । निम्नलिखित फ़ंक्शन कॉल की एक श्रृंखला है: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromExileObject। PyRun_SimpleFileExFlags__main__ नामस्थान बनाता है जिसमें फ़ाइल की सामग्री निष्पादित की जाएगी। यह भी जाँचता है कि क्या फ़ाइल का pyc संस्करण मौजूद है (pyc फ़ाइल एक साधारण फ़ाइल है जिसमें स्रोत कोड का पहले से संकलित संस्करण है)। यदि एक pyc संस्करण मौजूद है, तो इसे बाइनरी फ़ाइल के रूप में पढ़ने का प्रयास किया जाएगा, और फिर इसे चलाएं। यदि pyc फ़ाइल गुम है, तो PyRun_FileExFlags को कहा जाता है, आदि। PyParser_ASTFromFileObject फ़ंक्शन PyParser_ParseFileObject को कॉल करता है , जो मॉड्यूल की सामग्री को पढ़ता है और इससे पार्सिंग पेड़ बनाता है। फिर, बनाया गया पेड़ PyParser_ASTFromNodeObject को दिया जाता है , जो इससे एक अमूर्त सिंटैक्स ट्री बनाता है।

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

एएसटी उत्पन्न होता है जब run_mod कहा जाता है । यह फ़ंक्शन PyAST_CompileObject को कॉल करता है , जो एएसटी से कोड ऑब्जेक्ट बनाता है। ध्यान दें कि PyAST_CompileObject कॉल के दौरान उत्पन्न बायटेकोड को सरल peephole ऑप्टिमाइज़र के माध्यम से पारित किया जाता है , जो कोड ऑब्जेक्ट्स बनाने से पहले उत्पन्न बायटेकोड का कम अनुकूलन करता है। Run_mod समारोह तो लागू होता है PyEval_EvalCode समारोह से ceval.c फ़ाइल कोड वस्तु के लिए। यह फ़ंक्शन कॉल की एक और श्रृंखला की ओर जाता है: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalCrameEx। कोड ऑब्जेक्ट को एक या दूसरे रूप में इनमें से अधिकांश कार्यों के तर्क के रूप में पारित किया जाता है। _PyEval_EvalFrameEx- यह एक सामान्य दुभाषिया लूप है जो कोड ऑब्जेक्ट के निष्पादन की प्रक्रिया करता है। हालाँकि, इसे न केवल कोड ऑब्जेक्ट के साथ तर्क के रूप में कहा जाता है, बल्कि फ़्रेम ऑब्जेक्ट के साथ, जिसमें एक विशेषता के रूप में एक फ़ील्ड है जो कोड ऑब्जेक्ट को संदर्भित करता है। यह फ़्रेम कोड ऑब्जेक्ट के निष्पादन के लिए संदर्भ प्रदान करता है। सरल शब्दों में: इंटरप्रेटर लूप लगातार निर्देशों के सरणी से निर्देश काउंटर द्वारा इंगित अगले निर्देश को पढ़ता है। तब यह इस निर्देश को निष्पादित करता है: यह प्रक्रिया में मूल्य स्टैक से वस्तुओं को जोड़ता है या हटाता है जब तक कि इसे निष्पादित किए जाने वाले निर्देशों के सरणी में खाली नहीं किया जाता है (अच्छी तरह से, या कुछ असाधारण होता है जो लूप को बाधित करता है)।

पायथन कार्यों का एक सेट प्रदान करता है जिसका उपयोग आप वास्तविक कोड ऑब्जेक्ट्स की जांच करने के लिए कर सकते हैं। उदाहरण के लिए, एक साधारण प्रोग्राम को एक कोड ऑब्जेक्ट में संकलित किया जा सकता है और अजगर के वर्चुअल मशीन द्वारा निष्पादित किए जाने वाले ऑपकोड प्राप्त करने के लिए disassembled किया जा सकता है। यह लिस्टिंग 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: पायथन में एक फ़ंक्शन को डिसेबल करना

हेडर फ़ाइल ।/Include/opcodes.h में पायथन वर्चुअल मशीन के सभी निर्देशों / ऑपकोड की पूरी सूची है। ओपकोड बहुत सरल हैं। हमारे उदाहरण को Listing 2.3 में लें, जिसमें चार निर्देशों का एक सेट है। LOAD_FAST मान स्टैक पर इसके तर्क (इस मामले में x) के मूल्य को लोड करता है। पायथन वर्चुअल मशीन स्टैक-आधारित है, इसलिए ओपकोड संचालन के लिए मान स्टैक से "पॉपप" किए जाते हैं, और अन्य ओपकोड द्वारा आगे के उपयोग के लिए गणना परिणामों को स्टैक पर वापस धकेल दिया जाता है। तब BINARY_MULTIPLY स्टैक से दो आइटम पॉप करता है, दोनों मानों का द्विआधारी गुणन करता है, और परिणाम को स्टैक पर वापस धकेलता है। RETURN VALUE निर्देशस्टैक से एक मान प्राप्त करता है, इस मान के ऑब्जेक्ट के लिए रिटर्न वैल्यू सेट करता है, और इंटरप्रेटर लूप को बाहर निकालता है। यदि आप लिस्टिंग 2.3 को देखते हैं, तो यह स्पष्ट है कि यह एक बहुत मजबूत सरलीकरण है।

दुभाषिया लूप की वर्तमान व्याख्या कई विवरणों को ध्यान में नहीं रखती है, जिसकी चर्चा बाद के अध्यायों में की जाएगी। उदाहरण के लिए, यहां वे प्रश्न हैं जिनका हमें उत्तर नहीं मिला:

  • LOAD_FAST कथन द्वारा लोड किए गए मान कहां से आए?
  • तर्क कहाँ से आते हैं, जो निर्देशों के हिस्से के रूप में उपयोग किए जाते हैं?
  • नेस्टेड फंक्शन और मेथड कॉल्स को कैसे हैंडल किया जाता है?
  • इंटरप्रेटर लूप अपवादों को कैसे संभालता है?

सभी निर्देशों को पूरा करने के बाद, Py_Main फ़ंक्शन निष्पादन जारी रखता है, लेकिन इस बार सफाई की प्रक्रिया शुरू होती है। यदि इंटरप्रेटर की शुरुआत के दौरान प्रारंभ करने के लिए Py_Initialize को कॉल किया जाता है , तो Py_FinalizeEx को क्लीनअप करने के लिए कहा जाता है। इस प्रक्रिया में थ्रेड्स से बाहर निकलने की प्रतीक्षा करना, किसी भी निकास हैंडलर को कॉल करना, साथ ही इंटरप्रेटर द्वारा आवंटित अभी भी उपयोग की गई मेमोरी को मुक्त करना शामिल है।

और इसलिए, हमने पायथन में होने वाली प्रक्रियाओं के "उच्च स्तर" विवरण को देखा, जब कोई स्क्रिप्ट चलती है। जैसा कि पहले उल्लेख किया गया है, कई सवाल हैं जिनका उत्तर दिया जाना बाकी है। भविष्य में, हम दुभाषिया के अध्ययन में देरी करेंगे और प्रत्येक चरण पर विस्तार से विचार करेंगे। और हम अगले अध्याय में संकलन प्रक्रिया का वर्णन करके शुरू करेंगे।

All Articles