python语法中的左值、右值和字符

位置决定语义

在下面的python代码中,忽略掉语法错误,源码中同样一个单词tsecer在不同的位置有不同的意义

  • import之后

在import之后的tsecer是作为一个简单的字面字符串来处理:这里的意思是这个tsecer不会有任何变量(及相关展开)的意义,它更类似于C语言中的字符串,也就是字面量,在源代码中看到是什么显示就是什么。

import tsecer
tsecer = tsecer + 1
  • 等号右侧

这个让人不禁想起C语言中左值/右值的概念:在等号的右侧,同样的tsecer表示的并不是tsecer字符串本身,编译器需要将这个变量展开为对其具体内容的引用。由于这里只是引用,所以需要保证这个变量之前一定已经定义过。

  • 等号左边

这个和右边取内容(dereference)的操作不同,这里更类似于是一个取地址的操作,也就是需要更新tsecer变量地址中的地址。或者对于赋值操作 lhs = rhs来说,它的语义是把rhs的内容存储/更新到lhs代表的地址中。

syntax tree visit

和通常的语言处理逻辑相同:python的语法树处理同样经过了符号表和语法分析,其中的符号表处理在symtable.c文件,而语法分析则在compile.c文件。这两个文件可以说是整个python语法分析最为关键的两个部分,这两个文件中的主体函数都是各种的visit函数,例如在符号表中有symtable_visit_expr函数,在语法分析中的compiler_visit_expr函数,两个函数的第二个参数都是相同的。从C++的视角来看,如果把第一个参数看做是类的话,那么它们操作的都是相同的数据类型。

symtable_visit_expr(struct symtable *st, expr_ty e)
compiler_visit_expr(struct compiler *c, expr_ty e)

import代码生成

对于import的生成,它生成的字节码只是从consts中读取内容,也就是把字后的NAME作为一个const(类似于C语言的字面字符串)处理,不会进行任何的变量展开。

static int
compiler_import(struct compiler *c, stmt_ty s)
{
    /* The Import node stores a module name like a.b.c as a single
       string.  This is convenient for all cases except
         import a.b.c as d
       where we need to parse that string to extract the individual
       module names.
       XXX Perhaps change the representation to make this case simpler?
     */
    Py_ssize_t i, n = asdl_seq_LEN(s->v.Import.names);

    for (i = 0; i < n; i++) {
        alias_ty alias = (alias_ty)asdl_seq_GET(s->v.Import.names, i);
        int r;
        PyObject *level;

        level = PyLong_FromLong(0);
        if (level == NULL)
            return 0;

        ADDOP_O(c, LOAD_CONST, level, consts);
        Py_DECREF(level);
        ADDOP_O(c, LOAD_CONST, Py_None, consts);
        ADDOP_NAME(c, IMPORT_NAME, alias->name, names);

        if (alias->asname) {
            r = compiler_import_as(c, alias->name, alias->asname);
            if (!r)
                return r;
        }
        else {
            identifier tmp = alias->name;
            Py_ssize_t dot = PyUnicode_FindChar(
                alias->name, '.', 0, PyUnicode_GET_LENGTH(alias->name), 1);
            if (dot != -1) {
                tmp = PyUnicode_Substring(alias->name, 0, dot);
                if (tmp == NULL)
                    return 0;
            }
            r = compiler_nameop(c, tmp, Store);
            if (dot != -1) {
                Py_DECREF(tmp);
            }
            if (!r)
                return r;
        }
    }
    return 1;
}

一个全局变量的load/store为例

下面是python字节码中对于全局变量存储和读取的操作,典型的场景下这个对应的赋值操作左侧和右侧变量访问时对应的机器码。这个代码其实也很直观,如果把字典看做一个map的话,load的时候是map.find(key),而store则是map.insert(key, value);

/// @file: Python-3.6.0\Python\ceval.c
PyObject *
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
///....
        TARGET(STORE_GLOBAL) {
            PyObject *name = GETITEM(names, oparg);
            PyObject *v = POP();
            int err;
            err = PyDict_SetItem(f->f_globals, name, v);
            Py_DECREF(v);
            if (err != 0)
                goto error;
            DISPATCH();
        }
///....
        TARGET(LOAD_GLOBAL) {
            PyObject *name = GETITEM(names, oparg);
            PyObject *v;
            if (PyDict_CheckExact(f->f_globals)
                && PyDict_CheckExact(f->f_builtins))
            {
                v = _PyDict_LoadGlobal((PyDictObject *)f->f_globals,
                                       (PyDictObject *)f->f_builtins,
                                       name);
                if (v == NULL) {
                    if (!_PyErr_OCCURRED()) {
                        /* _PyDict_LoadGlobal() returns NULL without raising
                         * an exception if the key doesn't exist */
                        format_exc_check_arg(PyExc_NameError,
                                             NAME_ERROR_MSG, name);
                    }
                    goto error;
                }
                Py_INCREF(v);
            }
            else {
                /* Slow-path if globals or builtins is not a dict */

                /* namespace 1: globals */
                v = PyObject_GetItem(f->f_globals, name);
                if (v == NULL) {
                    if (!PyErr_ExceptionMatches(PyExc_KeyError))
                        goto error;
                    PyErr_Clear();

                    /* namespace 2: builtins */
                    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();
        }
///...
}

为什么 tsecer + 1 = 1会有语法错误

从虚拟机实现来看,tsecer+1这个表达式在内部也是有唯一确定的内存地址的,所以这个赋值在操作上并不是实现不了,只是编译器在语法分析的时候拒绝了这种情况。

根本的原因在于虽然tsecer+1有一个唯一的内部变量地址,但是如果不把它放到一个可以再次访问的位置(例如变量中),下次如何再访问这个修改的值呢?也就是tsecer+1这个操作不是可重入的,下次执行它返回变量的地址可能就已经变化,所以前一次赋值并没有意义。

我们通常说,python这种动态语言的变量不需要声明就可以直接使用,其实从某种角度看,它还是需要声明(至少是一个形式上)的:声明的方式就是变量必须先在赋值之类的store操作中作为左值出现。

回到正题

由于import后面的内容只是字面量(而不会展开),所以如果想通过import来动态加载一个模块无法实现,也就是下面的语法是无法达到目的的。

tsecer@harry: cat -n import_var_mod.py 
     1  import sys
     2
     3  print(sys.argv)
     4
     5  modname = sys.argv[1]
     6  import modname
tsecer@harry: python import_var_mod.py tsecer
['import_var_mod.py', 'tsecer']
Traceback (most recent call last):
  File "import_var_mod.py", line 6, in <module>
    import modname
ImportError: No module named modname
tsecer@harry:

这里给出了动态加载的操作方法

import importlib
function_string = 'mypackage.mymodule.myfunc'
mod_name, func_name = function_string.rsplit('.',1)
mod = importlib.import_module(mod_name)
func = getattr(mod, func_name)
result = func()

posted on 2022-12-09 20:34  tsecer  阅读(288)  评论(0编辑  收藏  举报

导航