• python为什么需要符号表


    一、问题

    从python的源代码可以看到,python中有符号表(symtable.c)相关代码。但是作为一种动态语言,它为什么需要符号表呢?
    猜测可能有下面的原因:

    1、词法去重

    对于重复出现的变量名,在生成字节码的时候使用相同的字符串。例如
    x = x + 1
    x出现了两次,有了符号表,在生成的字节码中,x就可以只出现一次(而且必须只出现一次)。进而,同一个源代码中的所有相同字符串不管它们的语法意义都使用相同的字符串,从而节省生成文件大小,类似于C语言中调试信息中使用的字符串hash表。

    2、语义生成

    可能在语法分析阶段,判断符号之间的引用,进而会做一些语法检查,或者根据变量生成不同的字节码指令。

    二、词法分析

    由于字符串、整数、操作符等常见类型的特征都非常明显,更加常见的是从源代码级别上看到的标识符变量。例如,
    x.y = z
    在这个表达式中,x、y、z三个都是常规理解上的标识符类型,这个是python代码中最常见的元素。在python的源代码中,这种类型的标识符(token)标记为NAME类型。
    Python-3.6.0\Parser\tokenizer.c
    static int
    tok_get(struct tok_state *tok, char **p_start, char **p_end)
    {
    ……
    /* Identifier (most frequent token!) */
    nonascii = 0;
    if (is_potential_identifier_start(c)) {
    ……
    return NAME;
    }
    ……
    }

    三、生成语法树(abstract syntax tree)

    和常规的语法编译器一样,词法分析之后就需要进行语法分析,进而生成语法树。python语法树对应的状态机根据Python-3.6.0\Grammar\Grammar文件生成,在源代码中它对应的语法结构是Python-3.6.0\Python\graminit.c文件中的
    grammar _PyParser_Grammar = {
    86,
    dfas,
    {176, labels},
    256
    };
    在PyNode_AddChild函数中,会根据token之后的字符串类型生成node节点,这些node节点就是语法树上的node节点。可以看到:
    1、在这个函数中,随着子节点的增加,节点会动态调整子节点数组的大小。
    2、记录了变量在源代码中的行号,列号。
    3、所有词法单位都是以字符串形式记录的,但是这个字符串如何解析需要根据type来进行。例如对于num类型(例如1111),这里依然是当做字符串("1111")保存的。
    4、字符串依然是单独一份,没有共用。也就是 x = x + 1,两个x对应的是两个不同的字符串,两个字符串有各自的内存空间。
    Python-3.6.0\Parser\parser.c
    int
    PyNode_AddChild(node *n1, int type, char *str, int lineno, int col_offset)
    {
    const int nch = n1->n_nchildren;
    int current_capacity;
    int required_capacity;
    node *n;

    if (nch == INT_MAX || nch < 0)
    return E_OVERFLOW;

    current_capacity = XXXROUNDUP(nch);
    required_capacity = XXXROUNDUP(nch + 1);
    if (current_capacity < 0 || required_capacity < 0)
    return E_OVERFLOW;
    if (current_capacity < required_capacity) {
    if ((size_t)required_capacity > SIZE_MAX / sizeof(node)) {
    return E_NOMEM;
    }
    n = n1->n_child;
    n = (node *) PyObject_REALLOC(n,
    required_capacity * sizeof(node));
    if (n == NULL)
    return E_NOMEM;
    n1->n_child = n;
    }

    n = &n1->n_child[n1->n_nchildren++];
    n->n_type = type;
    n->n_str = str;
    n->n_lineno = lineno;
    n->n_col_offset = col_offset;
    n->n_nchildren = 0;
    n->n_child = NULL;
    return 0;
    }

    四、从字符串到具体类型的转换

    正如前面所说的,ast中存储的依然是字符串形式,接下来会根据ast生成语法结构:模块(mod_ty)、语句(stmt_ty)、表达式(expr_ty)。在这种结构中,字符串形式的NUMBER被转换为int类型的整数(例如PyLong_Type类型变量)。
    不过,这里相同的字符串依然没有合并,也还没有处理符号表。
    Python-3.6.0\Python\ast.c
    static expr_ty
    ast_for_atom(struct compiling *c, const node *n)
    {
    /* atom: '(' [yield_expr|testlist_comp] ')' | '[' [testlist_comp] ']'
    | '{' [dictmaker|testlist_comp] '}' | NAME | NUMBER | STRING+
    | '...' | 'None' | 'True' | 'False'
    */
    node *ch = CHILD(n, 0);

    switch (TYPE(ch)) {
    case NAME: {
    ……
    case NUMBER: {
    PyObject *pynum = parsenumber(c, STR(ch));
    if (!pynum)
    return NULL;

    if (PyArena_AddPyObject(c->c_arena, pynum) < 0) {
    Py_DECREF(pynum);
    return NULL;
    }
    return Num(pynum, LINENO(n), n->n_col_offset, c->c_arena);
    }
    ……
    }
    }

    五、在符号表中添加新符号

    1、符号表的生成

    在生成mod_ty类型之后,进行语义处理之前,先构建该模块的符号表,也就是递归遍历表达式中的各个变量。
    PyCodeObject *
    PyAST_CompileObject(mod_ty mod, PyObject *filename, PyCompilerFlags *flags,
    int optimize, PyArena *arena)
    {
    ……
    c.c_st = PySymtable_BuildObject(mod, filename, c.c_future);
    if (c.c_st == NULL) {
    if (!PyErr_Occurred())
    PyErr_SetString(PyExc_SystemError, "no symtable");
    goto finally;
    }

    co = compiler_mod(&c, mod);

    finally:
    compiler_free(&c);
    assert(co || PyErr_Occurred());
    return co;
    }

    2、符号表添加符号

    在向符号表添加符号的时候,根据表达式树的结构,就有了作用域的概念,进而知道它的作用域。有了作用域之后就有了变量的类型,这里变量的类型其实主要是和函数相关的:包括函数的参数、局部变量、lamda表达式使用的变量等。
    也就是说,在这个时候,字符串格式的变量开始进行去重和确定作用域,而作用域进而决定了使用的字节码指令。
    Python-3.6.0\Python\symtable.c
    static int
    symtable_add_def(struct symtable *st, PyObject *name, int flag)
    {
    ……
    }
    symtable_add_def函数添加符号的时候就已经包含了符号的类型,并且通过struct symtable *st也可以完成去重,这样字符串形式的变量就有了作用域、类型的概念,并且完成了去重。
    /* Flags for def-use information */

    #define DEF_GLOBAL 1 /* global stmt */
    #define DEF_LOCAL 2 /* assignment in code block */
    #define DEF_PARAM 2<<1 /* formal parameter */
    #define DEF_NONLOCAL 2<<2 /* nonlocal stmt */
    #define USE 2<<3 /* name is used */
    #define DEF_FREE 2<<4 /* name used but not defined in nested block */
    #define DEF_FREE_CLASS 2<<5 /* free variable from class's method */
    #define DEF_IMPORT 2<<6 /* assignment occurred via import */
    #define DEF_ANNOT 2<<7 /* this name is annotated */

    3、典型的符号场景包括

    a、函数参数

    static int
    symtable_visit_params(struct symtable *st, asdl_seq *args)
    {
    int i;

    if (!args)
    return -1;

    for (i = 0; i < asdl_seq_LEN(args); i++) {
    arg_ty arg = (arg_ty)asdl_seq_GET(args, i);
    if (!symtable_add_def(st, arg->arg, DEF_PARAM))
    return 0;
    }

    return 1;
    }

    b、根据访问类型处理

    如果变量在赋值类型指令左边,则默认是局部变量定义;右边默认是引用。例如
    tsecer=harry
    harry在右侧,上下文为Load,所以是USE
    tsecer在左侧,类型为局部变量定义。
    static int
    symtable_visit_expr(struct symtable *st, expr_ty e)
    {
    ……
    case Name_kind:
    if (!symtable_add_def(st, e->v.Name.id,
    e->v.Name.ctx == Load ? USE : DEF_LOCAL))
    VISIT_QUIT(st, 0);
    ……
    }

    六、不同类型对存储位置的影响

    函数参数,是调用者传入的,正常来说它们应该是在栈帧的堆栈里的。
    函数局部变量,这些是随着函数栈帧的存在而存在,函数调用返回之后它们对应的空间也会被销毁。
    全局变量,它们在函数退出之后依然存在,也就是函数内修改的内容在函数返回之后依然生效。
    未知类型,可能是外部变量,例如print函数,它是python内置符号,所以可以在运行时尝试从多个地方查找。

    不同的作用域使用的函数
    >>> def tsecer(parm):
    ... global gInTsecer;
    ... gInTsecer = 1
    ... lInTsecer = 2
    ... harry(parm, gInTsecer, lInTsecer, iInTsecer)
    ...
    >>> dis.dis(tsecer)
    3 0 LOAD_CONST 1 (1)
    3 STORE_GLOBAL 0 (gInTsecer)

    4 6 LOAD_CONST 2 (2)
    9 STORE_FAST 1 (lInTsecer)

    5 12 LOAD_GLOBAL 1 (harry)
    15 LOAD_FAST 0 (parm)
    18 LOAD_GLOBAL 0 (gInTsecer)
    21 LOAD_FAST 1 (lInTsecer)
    24 LOAD_GLOBAL 2 (iInTsecer)
    27 CALL_FUNCTION 4
    30 POP_TOP
    31 LOAD_CONST 0 (None)
    34 RETURN_VALUE
    >>>

    如果不是在函数内定义,不识别的通过LOAD_NAME
    tsecer@harry: cat symtable.py
    harry(x)

    tsecer@harry: python -m dis symtable.py
    1 0 LOAD_NAME 0 (harry)
    3 LOAD_NAME 1 (x)
    6 CALL_FUNCTION 1
    9 POP_TOP
    10 LOAD_CONST 0 (None)
    13 RETURN_VALUE
    tsecer@harry:
    这个指令对应的逻辑是自内向外逐层查找变量定义,这也就相当于“运行时链接”
    PyObject *
    _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
    {
    ……
    TARGET(LOAD_NAME) {
    PyObject *name = GETITEM(names, oparg);
    PyObject *locals = f->f_locals;
    PyObject *v;
    if (locals == NULL) {
    PyErr_Format(PyExc_SystemError,
    "no locals when loading %R", name);
    goto error;
    }
    if (PyDict_CheckExact(locals)) {
    v = PyDict_GetItem(locals, name);
    Py_XINCREF(v);
    }
    else {
    v = PyObject_GetItem(locals, name);
    if (v == NULL) {
    if (!PyErr_ExceptionMatches(PyExc_KeyError))
    goto error;
    PyErr_Clear();
    }
    }
    if (v == NULL) {
    v = PyDict_GetItem(f->f_globals, name);
    Py_XINCREF(v);
    if (v == NULL) {
    if (PyDict_CheckExact(f->f_builtins)) {
    v = PyDict_GetItem(f->f_builtins, name);
    if (v == NULL) {
    format_exc_check_arg(
    PyExc_NameError,
    NAME_ERROR_MSG, name);
    goto error;
    }
    Py_INCREF(v);
    }
    else {
    v = PyObject_GetItem(f->f_builtins, name);
    if (v == NULL) {
    if (PyErr_ExceptionMatches(PyExc_KeyError))
    format_exc_check_arg(
    PyExc_NameError,
    NAME_ERROR_MSG, name);
    goto error;
    }
    }
    }
    }
    PUSH(v);
    DISPATCH();
    }
    ……
    }

  • 相关阅读:
    chrome浏览器解析xml
    CuteEditor报错 空引用错误
    猫哥网络编程系列:HTTP PEM 万能调试法
    猫哥网络编程系列:详解 BAT 面试题
    全新 Mac 安装指南(编程篇)(环境变量、Shell 终端、SSH 远程连接)
    全新 Mac 安装指南(通用篇)(推荐设置、软件安装、推荐软件)
    魅族手机浏览器兼容性调优最佳实践
    使用 nvm 管理不同版本的 node 与 npm
    一种让 IE6/7/8 支持 media query 响应式设计的方法
    排列组合算法的javascript实现
  • 原文地址:https://www.cnblogs.com/tsecer/p/15799485.html
Copyright © 2020-2023  润新知