Python语言以简易明了著称,但初次学习Python,却被不少语法搞的昏头涨脑。List comprehension绝对是其中之一。 python
1、困惑 正则表达式
问题一:列表推导式中的变量,是全局变量?仍是局部变量?仍是闭包变量? 数组
注:一个简单的列表推导式,以下: 闭包
a = [ x for x in range(10) ]
这里的变量x,是局部变量吗?在列表推导式结束后,还能访问变量x吗? app
问题二:列表推导式,推导过程究竟是从左往右?仍是从右往左? 函数
注:一个简单的列表推导式,以下: 学习
a = [ (x, y) for x in range(2) for y in range(3) ]
若是写成伪码,是: spa
for x in range(10): for y in range(10: a.append((x, y))
仍是: code
for y in range(10): for x in range(10): a.append((x, y))
虽然这个问题看起来很奇葩,但让我很困惑。 对象
问题三:列表推导式中,for语句和if语句之间的关系是什么呢?
注:不光是for语句和if语句之间的关系,多个for语句之间,多个if语句之间,for语句和if语句之间,它们的关系又是什么样的呢?
问题四:列表推导式,究竟是怎么运行的呢?
注:虽然看到列表推导式,凭借猜想,也能猜个八九不离十。但总不知道List推导式具体如何运行的,心中总不踏实。
2、释惑
1. 关于GET_ITER、FOR_ITER字节码(opcode)
Python中有迭代的概念,最经常使用的要数for循环了。一个简单的for循环以下:
1: for x in A: 2: do_something 3:
对于这个for循环,Python会大体生成以下字节码:
1: iter = GET_ITER( A ) 2: x = FOR_ITER(iter) 3: if not x : jump to 6 4: do_something… 5: jump to 2 6: (The End)
在这里面,起到重要做用的,就是GET_ITER、FOR_ITER这2个字节码。
其中,GET_ITER是取出一个Object的迭代器(调用PyObject_GetIter(),若是是一个类的对象,调用其__iter__方法);
以后,就会不断对这个迭代器执行FOR_ITER指令(若是是一个类的对象,调用其next方法);
若是FOR_ITER指令迭代不到下一项了(一般是遇到StopIteration异常了),就跳出for循环,不然会一直迭代下去。
2. 关于POP_JUMP_IF_FALSE指令
这个指令比较长,第一次见有些被吓到,不知是干什么的。
可是仔细一看,这个指令仍是比较好理解的:
首先,Python的每一个函数都有本身的运行栈,全部的临时运算结果都要放在这个栈上的,例如如下简单代码:
1: if a > 0: 2: do_something这里的变量a,若是是个全局变量,那么它存在全局变量Dictionary里;若是它是个局部变量,那么它存放在局部变量数组里;若是它是个闭包变量,那么它存放在闭包变量数组里,总之,它是不在函数运行栈里的。
可是,变量a和常量0的逻辑比较的结果,是一个临时的运算结果,这个运算结果是要放在函数运行栈里的,以方便后面if判断时使用。
那么,Python对这么个简单的if代码,会大体生成如下字节码:
1: 进行a > 0判断, 2: Push(结果) 3: POP_JUMP_IF_FALSE 4: do_something… 5: (The end)
Python会在逻辑运算后,将逻辑运算的结果自动Push进函数的运行栈内。那么,在执行指令POP_JUMP_IF_FALSE时,会进行下面的操做:
// POP_JUMP_IF_FALSE 一、x = POP() 二、if not x : jump
POP_JUMP_IF_FALSE指令,其实就是将栈顶的元素(通常是刚进行逻辑运算的结果)Pop出来,而后判断其是否为False,若是是False,那么就跳转,不然什么事也不作。
3. List comprehension的语法
在刚看到List Comprehension时,很不能理解这个语法,总会有一个疑问:在List Comprehesion中,是否只能写for和if语句?可否写while语句?可否写try-except-finally语句?并且for语句和if语句之间的关系是什么?
有不少疑问,最终还得看Grammar/Grammer这个文件中,定义的语法规则。
其中,List comprehension的规则,在Grammar文件中,称为listmaker。
listmaker分为2种,最简单的一种,以下:
a = [1, 2, 3, 4, 5]
也就是直接列出List中的全部元素。这种方式最简单,也最好理解。
第二种就是本文所说的List Comprehension了,语法以下:
一、listmaker: test list_for 二、list_for: for’ explist ‘in’ testlist_safe [list_iter] 三、list_iter: list_for | list_if 四、list_if: if’ old_test [list_iter]
语法文件全是正则表达式,并且先后相互引用,读起来很是吃力。不过在上面所列的这4行语法规则中,能够看到:list comprehension中,只能使用for和if这2种语句。这也解决了一大部分疑问。
并且能够从上面的语法中看出,每一个for语句后面,还能够接一个for语句或者一个if语句;每一个if语句后面,也能够接一个for语句或者一个if语句;而且没有对for语句、if语句的个数有任何限制。
若是注意看上面关于list_for语法的规则,能够发现里面有一个叫testlist_safe的东西,这里要和Dictionary的推导语法规则对照一下,会发现颇有趣的现象。
Dictionary推导式的一部分语法规则以下:
comp_for: 'for' exprlist 'in' or_test [ comp_for ]
这里的comp_for语法规则,几乎和上面的list_for语法规则相同,惟一不一样的是在list_for语法规则中的testlist_safe位置上,变成了or_test。
只从字面看来,testlist_safe和or_test相比,中间有一个’list’单词,也就是说:testlist_safe能够是一个列表,而or_test不能够,举例以下:
a = [ x for x in 1, 2, 3, 4 ]
在构造列表a时,能够直接在for … in …中,列出全部的元素,即上面的“1, 2, 3, 4”;可是,若是是在构造一个Dictionary(或Set),以下:
a = { x for x in 1, 2, 3, 4 }
为何会有这种不一样的语法呢?我还没搞明白!
另外,还会发现testlist_old,里面还一个”old”单词,这个很容易引发头疼,由于加了”old”这个单词,颇有多是为了和老版本兼容而出现的语法规则。而历史遗留问题,是最让人头疼的问题了。
在Python的语法规则里面,还有一个叫作testlist的语法规则,那么这个testlist_old中的”old”究竟是什么意思呢?
通过几番考究,原来以下,一个简单的例子:
a = 5 if b > 3 else 2
这个语法,就是没有”old”的语法。而在testlist_old语法中,这种带有if-else的语法是不容许的。
为何呢?例如,在list comprehension中,能够写成这样:
a = [ x for x in 5 if b > 3 else 2, 3 ]
为了杜绝这种歧义,Python在语法规则中,就使用testlist_old而不是testlist,使得if-expr语句不能出如今for…in…的元素列表中。
4. List Comprehension中,for语句和if语句是什么关系呢?
Python的List Comprehension中,可使用无限多个for、if语句,该怎么去理解这些for、if语句呢?它们之间的关系是什么呢?
Python的语法解析、字节码生成,大约分为3个阶段:
一、将.py源代码,解析成语法树
二、将语法树,解析成AST树
三、根据AST树,生成字节码
对于List Comprehension,能够在第2阶段,即Python/Ast.c这个源文件中,发现for语句和if语句之间的关系。
Python在从语法树生成AST的过程当中,会将List Comprehension中的for分离开来,并将每一个if语句,所有归属于离他最近的左边的for语句,例如:
a = [ (x, y) for x in range(10) if x % 2 if x > 3 for y in range(10) if y > 7 if y != 8 ]
上面这段代码中,有2个for和4个if,分别以下:
一、for x in range(10)
二、for y in range(10)
三、if x % 2
四、if x > 3
五、if y > 7
六、if y != 8
在AST的过程当中,Python会按照for语句将上面的语句拆成2部分,分别以下:
每一个if语句,从属于离他最近的左边的for语句。
下面看语法解析的第三阶段,即:经过AST生成字节码,在源代码Python/Compiler.c文件中。
在Python/Compiler.c源文件中,处理List Comprehension的代码,主要是2592行的compiler_listcomp_generator(…)函数。
这个函数,首先会生成字节码:BUILD_LIST,即生成一个新的List;而后经过自身的递归,从左到右,依次处理AST过程生成的for语句及其从属的if语句。
其中对于每个for语句,大抵生成如下字节码:
1: GET_ITER 2: FOR_ITER
而后从左到右,依次处理从属与这个for语句的if语句,大抵生成如下字节码:
1: 进行逻辑判断并将结果Push进函数栈 2: POP_JUMP_IF_FALSE(若是结果为False,则跳转到XX处) XX: 跳转到for语句的FOR_ITER处执行,至关于continue语句
若是全部的if都判断为True,才进入后续的for语句中执行(后续的for语句都嵌套在以前的for语句中),直到最后一个for语句执行结束(从属的if都判断为True),这时才向list中append一个新的元素。
整个过程的伪码能够以下:
for xx in xxx: if not xxxx: continue; if not xxxxx: continue; ........ for xx in xxx: if not xxxx: continue; ........ // 到了最后一个for for xx in xxx: ....... List.append(value)
至此,List Comprehension的内部运行过程就搞明白了。
5. 最后一个问题,列表推导式中的变量,是局部变量吗?
例如,一个简单的例子:
a = [ x for x in range(10) ]
这里面的变量x,是局部变量吗?在列表推导式结束后,还能够访问变量x吗?
Python的变量做用域,是在源代码Python/Symtable.c中实现的。
关于Python的变量做用域,打算再写一篇另外的文章介绍。
这里的结果是:若是变量x没有被使用过,那么变量x会成为一个局部变量,当列表推导式结束后,还能够访问变量x;不然,变量x原来的做用域是什么,如今仍是什么。