教你閱讀 Cpython 的源碼(二) -Python解釋器進程

第二部分:Python解釋器進程

在上節教你閱讀 Cpython 的源碼(一)中,咱們從編寫Python到執行代碼的過程當中看到Python語法和其內存管理機制。 在本節,咱們將從代碼層面去討論 ,Python的編譯過程。 調用Python二進制文件能夠經過如下五種方式: 1.使用-c和Python命令運行單個命令 2.使用-m和模塊名稱啓動模塊 3.使用文件名運行文件 4.使用shell管道運行stdin輸入 5.啓動REPL並一次執行一個命令html

整個運行過程你能夠經過檢查下面三個源文件進行了解: 1.Programs/python.c是一個簡單的入口文件。 2.Modules/main.c聚集加載配置,執行代碼和清理內存整個過程的代碼文件。 3.Python/initconfig.c從系統環境加載配置,並將其與任何命令行標誌合併。 此圖顯示瞭如何調用每一個函數: 執行模式由配置肯定。node

CPython源代碼樣式:

與Python代碼的PEP8樣式指南相似,CPython C代碼有一個官方樣式指南,最初於2001年設計並針對現代版本進行了更新。python

這裏有一些命名標準方便你調試跟蹤源代碼:git

  • 對公共函數使用Py前綴,靜態函數不使用。Py_前綴保留用於Py_FatalError等全局服務例程。特定的對象(如特定的對象類型API)使用較長的前綴,例如PyString_用於字符串函數。
  • 公衆函數和變量,使用首寫字母大寫,單詞之間下劃線分割的形式, 例如:PyObject_GetAttr, Py_BuildValue, PyExc_TypeError
  • 有時,加載器必須可以看到內置函數。 咱們使用_Py前綴,例如_PyObject_Dump
  • 宏應具備混合字母前綴,首字母大寫,例如PyString_AS_STRINGPy_PRINT_RAW

建立運行環境的配置

經過上圖能夠看到,在執行Python代碼以前,首先會創建配置。在文件Include/cpython/initconfig.h中名爲PyConfig的對象會定義一個配置的數據結構。 配置數據結構包括如下內容:github

  • 各類模式的運行時標誌,如調試和優化模式
  • 執行模式,例如是否傳遞了文件名,提供了stdin或模塊名稱
  • 擴展選項,由-X <option>指定
  • 運行時設置的環境變量 配置數據主要是CPython在運行時用於啓用和禁用各類功能。Python還附帶了幾個命令行界面選項。 在Python中,你可使用-v標誌啓用詳細模式。在詳細模式下,Python將在加載模塊時將消息打印到屏幕:
$ ./python.exe -v -c "print('hello world')"
# installing zipimport hook
import zipimport # builtin
# installed zipimport hook
...

你能夠在PyConfig的struct中的Include/cpython/initconfig.h中看到此標誌的定義:web

/* --- PyConfig ---------------------------------------------- */

typedef struct {
    int _config_version;  /* Internal configuration version,
                             used for ABI compatibility */
    int _config_init;     /* _PyConfigInitEnum value */

    ...

    /* If greater than 0, enable the verbose mode: print a message each time a
       module is initialized, showing the place (filename or built-in module)
       from which it is loaded.

       If greater or equal to 2, print a message for each file that is checked
       for when searching for a module. Also provides information on module
       cleanup at exit.

       Incremented by the -v option. Set by the PYTHONVERBOSE environment
       variable. If set to -1 (default), inherit Py_VerboseFlag value. */
    int verbose;

Python/initconfig.c中,創建了從環境變量和運行時命令行標誌讀取設置的邏輯。 在config_read_env_vars函數中,讀取環境變量並用於爲配置設置分配值:shell

static PyStatus
config_read_env_vars(PyConfig *config)
{
    PyStatus status;
    int use_env = config->use_environment;

    /* 獲取環境變量 */
    _Py_get_env_flag(use_env, &config->parser_debug, "PYTHONDEBUG");
    _Py_get_env_flag(use_env, &config->verbose, "PYTHONVERBOSE");
    _Py_get_env_flag(use_env, &config->optimization_level, "PYTHONOPTIMIZE");
    _Py_get_env_flag(use_env, &config->inspect, "PYTHONINSPECT");

對於詳細設置,你能夠看到若是PYTHONVERBOSE存在,PYTHONVERBOSE的值用於設置&config-> verbose的值,若是環境變量不存在,則將保留默認值-1。 而後再次在initconfig.c中的config_parse_cmdline函數中,用命令行標誌來設置值:express

static PyStatus
config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions,
                     Py_ssize_t *opt_index)
{
...

        switch (c) {
...

        case 'v':
            config->verbose++;
            break;
...
        /* This space reserved for other options */

        default:
            /* unknown argument: parsing failed */
            config_usage(1, program);
            return _PyStatus_EXIT(2);
        }
    } while (1);

此值以後由_Py_GetGlobalVariablesAsDict函數複製到全局變量Py_VerboseFlag。 在Python中,可使用具名元組類型的對象sys.flags訪問運行時標誌,如詳細模式,安靜模式。-X標誌在sys._xoptions字典中均可用。瀏覽器

$ ./python.exe -X dev -q       

>>> import sys
>>> sys.flags
sys.flags(debug=0, inspect=0, interactive=0, optimize=0, dont_write_bytecode=0, 
 no_user_site=0, no_site=0, ignore_environment=0, verbose=0, bytes_warning=0, 
 quiet=1, hash_randomization=1, isolated=0, dev_mode=True, utf8_mode=0)

>>> sys._xoptions
{'dev': True}

除了initconfig.h中的運行時配置外,還有構建配置,它位於根文件夾中的pyconfig.h內。 此文件在構建過程的配置步驟中動態建立,或由Visual Studio for Windows系統動態建立。 能夠經過運行如下命令查看構建配置:緩存

$ ./python.exe -m sysconfig

讀取文件/輸入

一旦CPython具備運行時配置和命令行參數,就能夠肯定它須要執行的內容了。 此任務由Modules/main.c中的pymain_main函數處理。 根據新建立的配置實例,CPython如今將執行經過多個選項提供的代碼。

經過-c輸入

最簡單的是爲CPython提供一個帶-c選項的命令和一個帶引號的Python代碼。 例如:

$ ./python.exe -c "print('hi')"
hi

下圖是整個過程的流程圖 首先,在modules/main.c中執行pymain_run_command函數,將在-c中傳遞的命令做爲C程序中wchar_t *的參數。 wchar_t*類型一般被用做Cpython中Unicode的低級存儲數據類型,由於該類型的大小能夠存儲utf8字符。 將wchar_t *轉換爲Python字符串時,Objects/unicodetype.c文件有一個輔助函數PyUnicode_FromWideChar,它會返回一個PyObject,其類型爲str。而後,經過PyUnicode_AsUTF8String,完成對UTF8的編碼,並將Python中的str對象轉換爲Python字節類型。 完成後,pymain_run_command會將Python字節對象傳遞給PyRun_SimpleStringFlags執行,但首先會經過 PyBytes_AsString將字節對象再次轉換爲str類型。

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
    PyObject *unicode, *bytes;
    int ret;

    unicode = PyUnicode_FromWideChar(command, -1);
    if (unicode == NULL) {
        goto error;
    }

    if (PySys_Audit("cpython.run_command", "O", unicode) < 0) {
        return pymain_exit_err_print();
    }

    bytes = PyUnicode_AsUTF8String(unicode);
    Py_DECREF(unicode);
    if (bytes == NULL) {
        goto error;
    }

    ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
    Py_DECREF(bytes);
    return (ret != 0);

error:
    PySys_WriteStderr("Unable to decode the command from the command line:\n");
    return pymain_exit_err_print();
}

wchar_t *轉換爲Unicode,字節,而後轉換爲字符串大體至關於如下內容:

unicode = str(command)
bytes_ = bytes(unicode.encode('utf8'))
# call PyRun_SimpleStringFlags with bytes_

PyRun_SimpleStringFlags函數是Python/pythonrun.c的一部分。它的目的是將這個簡單的命令轉換爲Python模塊,而後將其發送以執行。因爲Python模塊須要將__main__做爲獨立模塊執行,所以它會自動建立。

int
PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags)
{
    PyObject *m, *d, *v;
    m = PyImport_AddModule("__main__"); #建立__main__模塊
    if (m == NULL)
        return -1;
    d = PyModule_GetDict(m);
    v = PyRun_StringFlags(command, Py_file_input, d, d, flags);
    if (v == NULL) {
        PyErr_Print();
        return -1;
    }
    Py_DECREF(v);
    return 0;
}

一旦PyRun_SimpleStringFlags建立了一個模塊和一個字典,它就會調用PyRun_StringFlags函數,它會建立一個僞文件名,而後調用Python解析器從字符串建立一個AST並返回一個模塊,mod。 你將在下一節中深刻研究AST和Parser代碼。

經過-m輸入

執行 Python 命令的另外一個方法,經過使用 -m 而後知道一個模塊名。一個典型的例子是python -m unittest,運行一個unittest測試模塊。使用-m標誌意味着在模塊包中,你想要執行__main__中的任何內容。它還意味着你要在sys.path中搜索指定的模塊。因此,使用這種搜索機制以後,你不須要去記憶unittest模塊它位於那個位置。 爲何會這樣呢?接下來就讓咱們一塊兒看看緣由。

Modules/main.c中,當使用-m標誌運行命令行時,它會調用pymain_run_module函數,並將傳入模塊的名稱做爲modname參數傳遞。 而後CPython將導入標準庫模塊runpy,並經過PyObject_Call函數執行它。導入模塊的操做是在函數PyImport_ImportModule進行的。

static int
pymain_run_module(const wchar_t *modname, int set_argv0)
{
    PyObject *module, *runpy, *runmodule, *runargs, *result;
    runpy = PyImport_ImportModule("runpy");
 ...
    runmodule = PyObject_GetAttrString(runpy, "_run_module_as_main");
 ...
    module = PyUnicode_FromWideChar(modname, wcslen(modname));
 ...
    runargs = Py_BuildValue("(Oi)", module, set_argv0);
 ...
    result = PyObject_Call(runmodule, runargs, NULL);
 ...
    if (result == NULL) {
        return pymain_exit_err_print();
    }
    Py_DECREF(result);
    return 0;
}

在這個函數中,您還將看到另外兩個C API函數:PyObject_CallPyObject_GetAttrString。 由於PyImport_ImportModule返回一個核心對象類型PyObject *,因此須要調用特殊函數來獲取屬性並調用它。 在Python中,若是你須要調用某個函數屬性,你可使用getattr()函數。相似的,在C API中,它將調用Objects/object.c文件中的 PyObject_GetAttrString方法。若是你要在python中運行一個callable類型的對象,你須要使用括號運行它,或者調用其__call__()屬性。在Objects/object.c中對__call__()進行了實現。

hi = "hi!"
hi.upper() == hi.upper.__call__()  # this is the same

runpy模塊就在Lib/runpy.py,它是純Python寫的。 執行python -m <module>至關於運行python -m runpy <module>。 建立runpy模塊是爲了抽象在操做系統上定位和執行模塊的過程。 runpy作了一些事情來運行目標模塊:

  • 爲你提供的模塊名稱調用\__import __()
  • \__name__(模塊名稱)設置爲名爲\__main__的命名空間
  • \__main__命名空間內執行該模塊

runpy模塊還支持執行目錄和zip文件。

經過文件名輸入

若是Python命令的第一個參數是文件名,例如,python test.py。Cpython會打開一個文件的句柄,相似咱們在Python中使用open(),並將句柄傳遞給Python/pythonrun.c. 文件裏的PyRun_SimpleFileExFlags()。 這裏有三種方式: 1.若是文件後綴是.pyc,就會調用run_pyc_file()。 2.若是文件後綴是.py,將調用PyRun_FileExFlags()。 3.若是文件路徑是stdin,用戶運行了命令| python會將stdin視爲文件句柄並運行PyRun_FileExFlags()

下面是上述過程的C代碼

int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{
 ...
    m = PyImport_AddModule("__main__");
 ...
    if (maybe_pyc_file(fp, filename, ext, closeit)) {
 ...
        v = run_pyc_file(pyc_fp, filename, d, d, flags);
    } else {
        /* When running from stdin, leave __main__.__loader__ alone */
        if (strcmp(filename, "<stdin>") != 0 &&
            set_main_loader(d, filename, "SourceFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            goto done;
        }
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
    }
 ...
    return ret;
}

使用PyRun_FileExFlags()經過文件輸入

對於使用stdin和腳本文件方式,CPython會將文件句柄傳遞給位於pythonrun.c文件中的PyRun_FileExFlags()。PyRun_FileExFlags()的目的相似於用於-c輸入PyRun_SimpleStringFlags(),Cpython會把文件句柄加載到PyParser_ASTFromFileObject()中。

咱們將在下一節介紹Parser和AST模塊 由於這是一個完整的腳本,因此它不用像使用-c的方式須要經過PyImport_AddModule("__main__")建立__main__模塊。 與PyRun_SimpleStringFlags相同,一旦PyRun_FileExFlags()從文件建立了一個Python模塊,它就會將它發送到run_mod()來執行。 run_mod()能夠在Python/pythonrun.c中找到,並將模塊發送到AST以編譯成代碼對象,代碼對象是用於存儲字節碼操做的格式,並保存到.pyc文件中。

C代碼片斷

static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    PyCodeObject *co;
    PyObject *v;
    co = PyAST_CompileObject(mod, filename, flags, -1, arena);
    if (co == NULL)
        return NULL;

    if (PySys_Audit("exec", "O", co) < 0) {
        Py_DECREF(co);
        return NULL;
    }

    v = run_eval_code_obj(co, globals, locals);
    Py_DECREF(co);
    return v;
}

咱們將在下一節中介紹CPython編譯器和字節碼。 對run_eval_code_obj()的調用是一個簡單的包裝函數,而後它會調用Python/eval.c文件中的PyEval_EvalCode()函數。PyEval_EvalCode()函數是CPython的主要評估循環,它會迭代每一個字節碼語句並在本地機器上執行它。

使用run_pyc_file() 經過編譯字節碼輸入

PyRun_SimpleFileExFlags()中,有一個判斷子句爲用戶提供了.pyc文件的文件路徑。若是文件路徑以.pyc結尾,則不是將文件做爲純文本文件加載並解析它,它會假定.pyc文件的內容是字節碼,並保存到磁盤中。 文件Python/pythonrun.c中的run_py_file()方法,使用文件句柄從.pyc文件中編組(marshals)代碼對象。編組(Marshaling)是一個技術術語,做用是將文件內容複製到內存中並將其轉換爲特定的數據結構。磁盤上的代碼對象數據結構是CPython編譯器緩存已編譯代碼的方式,所以每次調用腳本時都不須要解析它。

C代碼

static PyObject *
run_pyc_file(FILE *fp, const char *filename, PyObject *globals,
             PyObject *locals, PyCompilerFlags *flags)
{
    PyCodeObject *co;
    PyObject *v;
  ...
    v = PyMarshal_ReadLastObjectFromFile(fp);
  ...
    if (v == NULL || !PyCode_Check(v)) {
        Py_XDECREF(v);
        PyErr_SetString(PyExc_RuntimeError,
                   "Bad code object in .pyc file");
        goto error;
    }
    fclose(fp);
    co = (PyCodeObject *)v;
    v = run_eval_code_obj(co, globals, locals);
    if (v && flags)
        flags->cf_flags |= (co->co_flags & PyCF_MASK);
    Py_DECREF(co);
    return v;
}

一旦代碼對象被封送到內存,它就被髮送到run_eval_code_obj(),它會調用Python/ceval.c來執行代碼。

詞法分析(Lexing)和句法分析(Parsing)

在閱讀和執行 Python 文件的過程當中,咱們深刻了解了解析器和AST模塊,並對函數PyParser_ASTFromFileObject()函數進行了調用。

咱們繼續看Python/pythonrun.c,該文件的PyParser_ASTFromFileObject()方法將拿到一個文件句柄,編譯器標誌和PyArena實例,並使用PyParser_ParseFileObject()將文件對象轉換爲節點對象。

節點對象將使用AST函數PyAST_FromNodeObject轉換爲模塊。 C代碼

mod_ty
PyParser_ASTFromFileObject(FILE *fp, PyObject *filename, const char* enc,
                           int start, const char *ps1,
                           const char *ps2, PyCompilerFlags *flags, int *errcode,
                           PyArena *arena)
{
    ...
    node *n = PyParser_ParseFileObject(fp, filename, enc,
                                       &_PyParser_Grammar,
                                       start, ps1, ps2, &err, &iflags);
    ...
    if (n) {
        flags->cf_flags |= iflags & PyCF_MASK;
        mod = PyAST_FromNodeObject(n, flags, filename, arena);
        PyNode_Free(n);
    ...
    return mod;
}

談到了PyParser_ParseFileObject()函數,咱們須要切換到Parser/parsetok.c文件以及談談CPython解釋器的解析器-標記化器階段。 此函數有兩個重要任務: 1.在Parser/tokenizer.c中使用PyTokenizer_FromFile()實例化標記化器狀態tok_state結構體。 2.使用Parser/parsetok.c中的parsetok()將標記(tokens)轉換爲具體的解析樹(節點列表)。

node *
PyParser_ParseFileObject(FILE *fp, PyObject *filename,
                         const char *enc, grammar *g, int start,
                         const char *ps1, const char *ps2,
                         perrdetail *err_ret, int *flags)
{
    struct tok_state *tok;
...
    if ((tok = PyTokenizer_FromFile(fp, enc, ps1, ps2)) == NULL) {
        err_ret->error = E_NOMEM;
        return NULL;
    }
...
    return parsetok(tok, g, start, err_ret, flags);
}

tok_state(在Parser/tokenizer.h中定義)是存儲由tokenizer生成的全部臨時數據的數據結構。它被返回到解析器-標記器(parser-tokenizer),由於parsetok()須要數據結構來開發具體的語法樹。 在parsetok()的內部,他會調用結構體tok_state,在循環中調用tok_get(),直到文件耗盡而且找不到更多的標記(tokens)爲止。 tok_get()位於Parser/tokenizer.c文件,其實爲類型迭代器(iterator),它將繼續返回解析樹中的下一個token。 tok_get()是整個CPython代碼庫中最複雜的函數之一。它有超過640行的代碼,包括數十年的邊緣案例,以及新語言功能和語法。 其中一個比較簡單的例子是將換行符轉換爲NEWLINE標記的部分:

static int
tok_get(struct tok_state *tok, char **p_start, char **p_end)
{
...
    /* Newline */
    if (c == '\n') {
        tok->atbol = 1;
        if (blankline || tok->level > 0) {
            goto nextline;
        }
        *p_start = tok->start;
        *p_end = tok->cur - 1; /* Leave '\n' out of the string */
        tok->cont_line = 0;
        if (tok->async_def) {
            /* We're somewhere inside an 'async def' function, and
               we've encountered a NEWLINE after its signature. */
            tok->async_def_nl = 1;
        }
        return NEWLINE;
    }
...
}

在這個例子裏,NEWLINE是一個標記(tokens),其值在Include/token.h中定義。 全部標記都是常量int值,而且在咱們運行make regen-grammar時生成了Include/token.h文件。 PyParser_ParseFileObject()返回的node類型對下一階段相當重要,它會將解析樹轉換爲抽象語法樹(AST)。

typedef struct _node {
    short               n_type;
    char                *n_str;
    int                 n_lineno;
    int                 n_col_offset;
    int                 n_nchildren;
    struct _node        *n_child;
    int                 n_end_lineno;
    int                 n_end_col_offset;
} node;

因爲CST多是語法,令牌ID或者符號樹,所以編譯器很難根據Python語言作出快速決策。 這就是下一階段將CST轉換爲更高層次結構的AST的緣由。此任務由Python/ast.c模塊執行,該模塊具備C版和Python API版本。在跳轉到AST以前,有一種方法能夠從解析器階段訪問輸出。CPython有一個標準的庫模塊parser,它使用Python API去展現C函數的內容。該模塊被記錄爲CPython的實現細節,所以你不會在其餘Python解釋器中看到它。此外,函數的輸出也不容易閱讀。輸出將採用數字形式,使用make regen-grammar階段生成的symbol和token編號,存儲在Includ/token.hInclude/symbol.h中。

>>> from pprint import pprint
>>> import parser
>>> st = parser.expr('a + 1')
>>> pprint(parser.st2list(st))
[258,
 [332,
  [306,
   [310,
    [311,
     [312,
      [313,
       [316,
        [317,
         [318,
          [319,
           [320,
            [321, [322, [323, [324, [325, [1, 'a']]]]]],
            [14, '+'],
            [321, [322, [323, [324, [325, [2, '1']]]]]]]]]]]]]]]]],
 [4, ''],
 [0, '']]

爲了便於理解,你能夠獲取symbol和token模塊中的全部數字,將它們放入字典中,並使用名稱遞歸替換parser.st2list()輸出的值。

import symbol
import token
import parser

def lex(expression):
    symbols = {v: k for k, v in symbol.__dict__.items() if isinstance(v, int)}
    tokens = {v: k for k, v in token.__dict__.items() if isinstance(v, int)}
    lexicon = {**symbols, **tokens}
    st = parser.expr(expression)
    st_list = parser.st2list(st)

    def replace(l: list):
        r = []
        for i in l:
            if isinstance(i, list):
                r.append(replace(i))
            else:
                if i in lexicon:
                    r.append(lexicon[i])
                else:
                    r.append(i)
        return r

    return replace(st_list)

你可使用簡單的表達式運行lex(),例如a+ 1,查看它如何表示爲解析器樹:

>>> from pprint import pprint
>>> pprint(lex('a + 1'))

['eval_input',
 ['testlist',
  ['test',
   ['or_test',
    ['and_test',
     ['not_test',
      ['comparison',
       ['expr',
        ['xor_expr',
         ['and_expr',
          ['shift_expr',
           ['arith_expr',
            ['term',
             ['factor', ['power', ['atom_expr', ['atom', ['NAME', 'a']]]]]],
            ['PLUS', '+'],
            ['term',
             ['factor',
              ['power', ['atom_expr', ['atom', ['NUMBER', '1']]]]]]]]]]]]]]]]],
 ['NEWLINE', ''],
 ['ENDMARKER', '']]

在輸出中,你能夠看到小寫的符號(symbols),例如'test'和大寫的標記(tokens),例如'NUMBER'。

抽象語法樹

CPython解釋器的下一個階段是將解析器生成的CST轉換爲能夠執行的更合理的結構。 該結構是代碼的更高級別表示,稱爲抽象語法樹(AST)。 AST是使用CPython解釋器進程內聯生成的,但你也可使用標準庫中的ast模塊以及C API在Python中生成它們。 在深刻研究AST的C實現以前,理解一個簡單的Python代碼的AST是頗有用的。 爲此,這裏有一個名爲instaviz的簡單應用程序。能夠在Web UI中顯示AST和字節碼指令(稍後咱們將介紹)。

小插曲

這裏我須要說下,由於我按照原文的例子去照着作,發現根本就運行不起來,因此我就和你們說個人作法。 首先,咱們不能經過pip的方式去安裝運行,而是從github上把他的源碼下載下來,而後在其文件下建立一個文件。 該程序須要在Python3.6+的環境下運行,包含3.6。 1.下載

https://github.com/tonybaloney/instaviz.git

2.寫腳本 隨意命名,好比example.py,代碼以下

import instaviz
def example():
    a = 1
    b = a + 1
    return b


if __name__ == "__main__":
    instaviz.show(example)

3.目錄結構以下 4.修改文件web.py 將原來的server_static函數和home函數用下面的代碼替換

@route("/static/<filename>")
def server_static(filename):
    return static_file(filename, root="./static/")


@route("/", name="home")
@jinja2_view("home.html", template_lookup=["./templates/"])
def home():
    global data
    data["style"] = HtmlFormatter().get_style_defs(".highlight")
    data["code"] = highlight(
        "".join(data["src"]),
        PythonLexer(),
        HtmlFormatter(
            linenos=True, linenostart=data["co"].co_firstlineno, linespans="src"
        ),
    )
    return data

5.運行 好了,如今能夠運行example.py文件了,運行以後會生成一個web服務(由於這個模塊是基於bottle框架的),而後瀏覽器打開 http://localhost:8080/ 6.展現頁面 好了,咱們繼續原文的思路。 這裏就到了展現圖了 左下圖是咱們聲明的example函數,表示爲抽象語法樹。 樹中的每一個節點都是AST類型。它們位於ast模塊中,繼承自_ast.AST。 一些節點具備將它們連接到子節點的屬性,與CST不一樣,後者具備通用子節點屬性。 例如,若是單擊中心的Assign節點,則會連接到b = a + 1行: 它有兩個屬性:

  • targets是要分配的名稱列表。它是一個列表,由於你可使用解包來使用單個表達式分配多個變量。
  • value是要分配的值,在本例中是BinOp語句,a+ 1。 若是單擊BinOp語句,則會顯示相關屬性: left:運算符左側的節點 op:運算符,在本例,是一個Add節點(+) right:運算符右側的節點 看一下圖就瞭解了 。 在C中編譯AST並非一項簡單的任務,所以Python/ast.c模塊超過5000行代碼。 有幾個入口點,構成AST的公共API的一部分。 在詞法分析(Lexing)和句法分析(Parsing)的最後一節中,咱們講到了對PyAST_FromNodeObject()的調用。在此階段,Python解釋器進程以node * tree的格式建立了一個CST。而後跳轉到Python/ast.c中的PyAST_FromNodeObject(),你能夠看到它接收node * tree,文件名,compiler flags和PyArena。 此函數的返回類型是定義在文件Include/Python-ast.h的mod_ty函數。 mod_ty是Python中5種模塊類型之一的容器結構: 1.Module 2.Interactive 3.Expression 4.FunctionType 5.Suite 在Include/Python-ast.h中,你能夠看到Expression類型須要一個expr_ty類型的字段。expr_ty類型也是在Include/Python-ast.h中定義。
enum _mod_kind {Module_kind=1, Interactive_kind=2, Expression_kind=3,
                 FunctionType_kind=4, Suite_kind=5};
struct _mod {
    enum _mod_kind kind;
    union {
        struct {
            asdl_seq *body;
            asdl_seq *type_ignores;
        } Module;

        struct {
            asdl_seq *body;
        } Interactive;

        struct {
            expr_ty body;
        } Expression;

        struct {
            asdl_seq *argtypes;
            expr_ty returns;
        } FunctionType;

        struct {
            asdl_seq *body;
        } Suite;

    } v;
};

AST類型都列在Parser/Python.asdl中,你將看到全部列出的模塊類型,語句類型,表達式類型,運算符和結構。本文檔中的類型名稱與AST生成的類以及ast標準模塊庫中指定的相同類有關。 Include/Python-ast.h中的參數和名稱與Parser/Python.asdl中指定的參數和名稱直接相關:

-- ASDL's 5 builtin types are:
-- identifier, int, string, object, constant

module Python
{
    mod = Module(stmt* body, type_ignore *type_ignores)
        | Interactive(stmt* body)
        | Expression(expr body)
        | FunctionType(expr* argtypes, expr returns)

由於C頭文件和結構在那裏,所以Python/ast.c程序能夠快速生成帶有指向相關數據的指針的結構。查看PyAST_FromNodeObject(),你能夠看到它本質上是一個switch語句,根據TYPE(n)的不一樣做出不一樣操做。TYPE()是AST用來肯定具體語法樹中的節點是什麼類型的核心函數之一。在使用PyAST_FromNodeObject()的狀況下,它只是查看第一個節點,所以它只能是定義爲Module,Interactive,Expression,FunctionType的模塊類型之一。TYPE()的結果要麼是符號(symbol)類型要麼是標記(token)類型。 對於file_input,結果應該是Module。Module是一系列語句,其中有幾種類型。 遍歷n的子節點和建立語句節點的邏輯在ast_for_stmt()內。若是模塊中只有1個語句,則調用此函數一次,若是有多個語句,則調用循環。而後使用PyArena返回生成的Module。 對於eval_input,結果應該是Expression,CHILD(n,0)(n的第一個子節點)的結果傳遞給ast_for_testlist(),返回expr_ty類型。而後使用PyArena將此expr_ty發送到Expression()以建立表達式節點,而後做爲結果傳回:

mod_ty
PyAST_FromNodeObject(const node *n, PyCompilerFlags *flags,
                     PyObject *filename, PyArena *arena)
{
    ...
    switch (TYPE(n)) {
        case file_input:
            stmts = _Py_asdl_seq_new(num_stmts(n), arena);
            if (!stmts)
                goto out;
            for (i = 0; i < NCH(n) - 1; i++) {
                ch = CHILD(n, i);
                if (TYPE(ch) == NEWLINE)
                    continue;
                REQ(ch, stmt);
                num = num_stmts(ch);
                if (num == 1) {
                    s = ast_for_stmt(&c, ch);
                    if (!s)
                        goto out;
                    asdl_seq_SET(stmts, k++, s);
                }
                else {
                    ch = CHILD(ch, 0);
                    REQ(ch, simple_stmt);
                    for (j = 0; j < num; j++) {
                        s = ast_for_stmt(&c, CHILD(ch, j * 2));
                        if (!s)
                            goto out;
                        asdl_seq_SET(stmts, k++, s);
                    }
                }
            }

            /* Type ignores are stored under the ENDMARKER in file_input. */
            ...

            res = Module(stmts, type_ignores, arena);
            break;
        case eval_input: {
            expr_ty testlist_ast;

            /* XXX Why not comp_for here? */
            testlist_ast = ast_for_testlist(&c, CHILD(n, 0));
            if (!testlist_ast)
                goto out;
            res = Expression(testlist_ast, arena);
            break;
        }
        case single_input:
            ...
            break;
        case func_type_input:
            ...
        ...
    return res;
}

在ast_for_stmt()函數裏,也有一個switch語句,它會判斷每一個可能的語句類型(simple_stmt,compound_stmt等),以及用於肯定節點類的參數的代碼。 再來一個簡單的例子,2**42的4次冪。這個函數首先獲得ast_for_atom_expr(),這是咱們示例中的數字2,而後若是有一個子節點,則返回原子表達式.若是它有多個字節點,使用Pow操做符以後,左節點是一個e(2),右節點是一個f(4)。

static expr_ty
ast_for_power(struct compiling *c, const node *n)
{
    /* power: atom trailer* ('**' factor)*
     */
    expr_ty e;
    REQ(n, power);
    e = ast_for_atom_expr(c, CHILD(n, 0));
    if (!e)
        return NULL;
    if (NCH(n) == 1)
        return e;
    if (TYPE(CHILD(n, NCH(n) - 1)) == factor) {
        expr_ty f = ast_for_expr(c, CHILD(n, NCH(n) - 1));
        if (!f)
            return NULL;
        e = BinOp(e, Pow, f, LINENO(n), n->n_col_offset,
                  n->n_end_lineno, n->n_end_col_offset, c->c_arena);
    }
    return e;
}

若是使用instaviz模塊查看上面的函數

>>> def foo():
       2**4
>>> import instaviz
>>> instaviz.show(foo)

在UI中,你還能夠看到其相應的屬性: 總之,每一個語句類型和表達式都是由一個相應的ast_for_*()函數來建立它。 參數在Parser/Python.asdl中定義,並經過標準庫中的ast模塊公開出來。 若是表達式或語句具備子級,則它將在深度優先遍歷中調用相應的ast_for_*子函數。

結論

CPython的多功能性和低級執行API使其成爲嵌入式腳本引擎的理想候選者。 你將看到CPython在許多UI應用程序中使用,例如遊戲設計,3D圖形和系統自動化。 解釋器過程靈活高效,如今你已經瞭解它的工做原理。 在這一部分中,咱們瞭解了CPython解釋器如何獲取輸入(如文件或字符串),並將其轉換爲邏輯抽象語法樹。咱們尚未處於能夠執行此代碼的階段。接下來,咱們將繼續深刻,了將抽象語法樹轉換爲CPU能夠理解的一組順序命令的過程。 -後續-

更多技術內容,關注公衆號:python學習開發

相關文章
相關標籤/搜索