由逗号、赋值优先级想到的语法结构
逗号的意义
在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: