由逗号、赋值优先级想到的语法结构

逗号的意义

在C语言中,逗号用来表示一个中间的表达式;而在lua和Python中,逗号通常用来作为多值赋值的一种语法。这就导致一个有意思的现象,相同的语句在C/LUA语言中不同的意义。例如下面的句子

x , y = z, u

在lua中,这个表示x=z切y=u,而在C中则只表示y=z。在脚本语言将逗号从C语言的"表达式"中解放出来之后,逗号和赋值的表达式优先级如何处理?

c

C语言语法中,由于逗号表达式在最语法顶层(高于assignment-expression)定义,所以它在表达式(expression)中优先级最低:在等号和逗号同时出现的时候,等号(赋值)的优先级更高(语法通常都是在移进/规约的时候执行匹配的动作,因为顶层的语法单位是最后被规约的,所以它的动作最后被执行,从语言的效果来看就是优先级最低。)。

<statement> ::= <labeled-statement>
              | <expression-statement>
              | <compound-statement>
              | <selection-statement>
              | <iteration-statement>
              | <jump-statement>
<expression> ::= <assignment-expression>
               | <expression> , <assignment-expression>

lua

在lua的语法定义中,

	chunk ::= {stat [`;´]} [laststat [`;´]]

	block ::= chunk

	stat ::=  varlist `=´ explist | 
		 functioncall | 
		 do block end | 
		 while exp do block end | 
		 repeat block until exp | 
		 if exp then block {elseif exp then block} [else block] end | 
		 for Name `=´ exp `,´ exp [`,´ exp] do block end | 
		 for namelist in explist do block end | 
		 function funcname funcbody | 
		 local function Name funcbody | 
		 local namelist [`=´ explist] 

	laststat ::= return [explist] | break

	funcname ::= Name {`.´ Name} [`:´ Name]

	varlist ::= var {`,´ var}

	var ::=  Name | prefixexp `[´ exp `]´ | prefixexp `.´ Name 

	namelist ::= Name {`,´ Name}

	explist ::= {exp `,´} exp

	exp ::=  nil | false | true | Number | String | `...´ | function | 
		 prefixexp | tableconstructor | exp binop exp | unop exp 

	prefixexp ::= var | functioncall | `(´ exp `)´

	functioncall ::=  prefixexp args | prefixexp `:´ Name args 

	args ::=  `(´ [explist] `)´ | tableconstructor | String 

	function ::= function funcbody

	funcbody ::= `(´ [parlist] `)´ block end

	parlist ::= namelist [`,´ `...´] | `...´

	tableconstructor ::= `{´ [fieldlist] `}´

	fieldlist ::= field {fieldsep field} [fieldsep]

	field ::= `[´ exp `]´ `=´ exp | Name `=´ exp | exp

	fieldsep ::= `,´ | `;´

	binop ::= `+´ | `-´ | `*´ | `/´ | `^´ | `%´ | `..´ | 
		 `<´ | `<=´ | `>´ | `>=´ | `==´ | `~=´ | 
		 and | or

	unop ::= `-´ | not | `#´

可以看到,赋值表达式('=')分割只能在一个statement中出现一次,所以类似于C语言中的

x , y = z=u

这样的语句在lua中是非法的。这是因为lua语法比较简单,一个语句只会允许出现一个赋值操作符。从语法的角度看,exp语法单位中不允许出现赋值表达式(表格定义中的field初始化除外)。或者再通俗的说,一个赋值语句除了表格定义中的field初始化外,最多只能有一个赋值动作。

下面是lua解析的相关代码:

static void statement (LexState *ls) {
  int line = ls->linenumber;  /* may be needed for error messages */
  enterlevel(ls);
  switch (ls->t.token) {
    case ';': {  /* stat -> ';' (empty statement) */
      luaX_next(ls);  /* skip ';' */
      break;
    }
    case TK_IF: {  /* stat -> ifstat */
      ifstat(ls, line);
      break;
    }
    case TK_WHILE: {  /* stat -> whilestat */
      whilestat(ls, line);
      break;
    }
    case TK_DO: {  /* stat -> DO block END */
      luaX_next(ls);  /* skip DO */
      block(ls);
      check_match(ls, TK_END, TK_DO, line);
      break;
    }
    case TK_FOR: {  /* stat -> forstat */
      forstat(ls, line);
      break;
    }
    case TK_REPEAT: {  /* stat -> repeatstat */
      repeatstat(ls, line);
      break;
    }
    case TK_FUNCTION: {  /* stat -> funcstat */
      funcstat(ls, line);
      break;
    }
    case TK_LOCAL: {  /* stat -> localstat */
      luaX_next(ls);  /* skip LOCAL */
      if (testnext(ls, TK_FUNCTION))  /* local function? */
        localfunc(ls);
      else
        localstat(ls);
      break;
    }
    case TK_DBCOLON: {  /* stat -> label */
      luaX_next(ls);  /* skip double colon */
      labelstat(ls, str_checkname(ls), line);
      break;
    }
    case TK_RETURN: {  /* stat -> retstat */
      luaX_next(ls);  /* skip RETURN */
      retstat(ls);
      break;
    }
    case TK_BREAK:   /* stat -> breakstat */
    case TK_GOTO: {  /* stat -> 'goto' NAME */
      gotostat(ls, luaK_jump(ls->fs));
      break;
    }
    default: {  /* stat -> func | assignment */
      exprstat(ls);
      break;
    }
  }
  lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
             ls->fs->freereg >= ls->fs->nactvar);
  ls->fs->freereg = ls->fs->nactvar;  /* free registers */
  leavelevel(ls);
}
static void exprstat (LexState *ls) {
  /* stat -> func | assignment */
  FuncState *fs = ls->fs;
  struct LHS_assign v;
  suffixedexp(ls, &v.v);
  if (ls->t.token == '=' || ls->t.token == ',') { /* stat -> assignment ? */
    v.prev = NULL;
    assignment(ls, &v, 1);
  }
  else {  /* stat -> func */
    check_condition(ls, v.v.k == VCALL, "syntax error");
    SETARG_C(getinstruction(fs, &v.v), 1);  /* call statement uses no results */
  }
}

python

python的语法相对lua更加完善,定义更加规范,功能也更多(所以也没有lua轻便)。

测试代码

对于逗号和赋值混编的形式,python给出了一个有些莫名其妙的错误提示

tsecer@harry: python3 --version 
Python 3.6.8
tsecer@harry: cat -n assign.py     
     1  x = y = z = u = 1
     2  x, y = z = u
     3
tsecer@harry: python3 assign.py 
Traceback (most recent call last):
  File "assign.py", line 2, in <module>
    x, y = z = u
TypeError: 'int' object is not iterable
tsecer@harry: 

语法定义

在python的语法定义中,一个语句(statement)中可以多次出现赋值表达式( 对应语法文件中的('=' (yield_expr|testlist_star_expr))*描述 )。同样,由于赋值是在最顶层,所以它的优先级是最低的。

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
annassign: ':' test ['=' test]

语法解析

对于一个语句中多个赋值的处理,在ast_for_expr_stmt函数的最后一个分支中,处理逻辑是将除了最后一个表达式(循环for (i = 0; i < NCH(n) - 2; i += 2)中)都放入targets中,剩余的最后一个作为value,生成一个赋值动作。这个通常是我们期望的效果:在C语言中,这种连等也是所有的表达式具有相同的内中。

///@file: Python-3.6.0\Python\ast.c
static stmt_ty
ast_for_expr_stmt(struct compiling *c, const node *n)
{
    REQ(n, expr_stmt);
    /* expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                            ('=' (yield_expr|testlist_star_expr))*)
       annassign: ':' test ['=' test]
       testlist_star_expr: (test|star_expr) (',' test|star_expr)* [',']
       augassign: '+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^='
                | '<<=' | '>>=' | '**=' | '//='
       test: ... here starts the operator precedence dance
     */

    if (NCH(n) == 1) {
	///....
    }
    else if (TYPE(CHILD(n, 1)) == augassign) {
	///....
    }
    else if (TYPE(CHILD(n, 1)) == annassign) {
	///....
    }
    else {
        int i;
        asdl_seq *targets;
        node *value;
        expr_ty expression;

        /* a normal assignment */
        REQ(CHILD(n, 1), EQUAL);
        targets = _Py_asdl_seq_new(NCH(n) / 2, c->c_arena);
        if (!targets)
            return NULL;
        for (i = 0; i < NCH(n) - 2; i += 2) {
            expr_ty e;
            node *ch = CHILD(n, i);
            if (TYPE(ch) == yield_expr) {
                ast_error(c, ch, "assignment to yield expression not possible");
                return NULL;
            }
            e = ast_for_testlist(c, ch);
            if (!e)
              return NULL;

            /* set context to assign */
            if (!set_context(c, e, Store, CHILD(n, i)))
              return NULL;

            asdl_seq_SET(targets, i / 2, e);
        }
        value = CHILD(n, NCH(n) - 1);
        if (TYPE(value) == testlist_star_expr)
            expression = ast_for_testlist(c, value);
        else
            expression = ast_for_expr(c, value);
        if (!expression)
            return NULL;
        return Assign(targets, expression, LINENO(n), n->n_col_offset, c->c_arena);
    }
}

python内置了语法树分析结构,可以通过该功能展示python是如何解析这个简单脚本的

tsecer@harry: python 
>>> import sys, ast
>>> f = open('assign.py', 'r')    
>>> print(ast.dump(ast.parse(f.read())))    
Module(body=[Assign(targets=[Name(id='x', ctx=Store()), Name(id='y', ctx=Store()), Name(id='z', ctx=Store()), Name(id='u', ctx=Store())], value=Num(n=1)), Assign(targets=[Tuple(elts=[Name(id='x', ctx=Store()), Name(id='y', ctx=Store())], ctx=Store()), Name(id='z', ctx=Store())], value=Name(id='u', ctx=Load()))])
>>> 

可以看到,内部主要有两个Assign组成,第二个Assign的targets包括了一个包括x,y的元组(Tuple)还有一个单独的z元素,value只有一个单独的u。和前面的分析类似。

错误原因

回到刚开始的语法错误:由于

 x, y = z = u

表示将u赋值给z和(x,y)元组,由于元组的初始化需要迭代变量,而u是一个int类型而不是一个迭代变量,所以给出语法错误提示。

Assignment expressions

查看3.6的语法,python的赋值也只能出现在最顶层的连续赋值中,在子表达式中也同样不能使用C语言中的中间赋值语法。例如下面语法就是错误的

>>> x = (y = 2) + 1
  File "<stdin>", line 1
    x = (y = 2) + 1
           ^
SyntaxError: invalid syntax
>>> 

为了解决这个问题,python在3.8版本中引入了walrus表达式,语法是 x := y类型,它允许出现在子表达式中,模拟了C语言的子表达式。

annotate assign

在python的语法文件中,还可以看到一个annotate assign语法

annassign: ':' test ['=' test]

这个语法和名字暗示的一样,主要是为了注释前面表达式,和注释的区别就是在于annotate中的注释是需要经过语法检查的,如果不合法会在解析时报错。
annotate通常是描述它的类型,但是从语法定义中可以看到,它只是要求是一个test。这就是说它可以是任何表达式,可以是类型,也可以是任意表达式。这就引出了一个之前忽略的问题:在python的语法中并没有地方描述哪些是类型(type),哪些是表达式。
事实上这个还是经常进行C语言开发导致的习惯思维,在动态语言中,类型也是一个对象,所以它也可以是一个表达式;或者说表达式本身就包括了对象。
下面是测试代码,其中int类型可以赋值给变量y;annotate中的注释可以为(x+1)这种非类型的表达式;但是不能是nop这种非法的表达式。

tsecer@harry: cat -n annoassign.py 
     1  x = 1
     2  y = int
     3  z : (x + 1) = y(2)
     4  z : nop = y(2)
     5
tsecer@harry: python3 annoassign.py
Traceback (most recent call last):
  File "annoassign.py", line 4, in <module>
    z : nop = y(2)
NameError: name 'nop' is not defined
tsecer@harry: 

posted on 2023-02-03 19:55  tsecer  阅读(83)  评论(0编辑  收藏  举报

导航