-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
df756ec
commit 039216c
Showing
2 changed files
with
279 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
## 楔子 | ||
|
||
上一篇文章我们介绍了 PyCodeObject 对象,但是还遗漏了一些内容,这里再单独补充一下。 | ||
|
||
## 内置函数 compile | ||
|
||
之前通过函数的 \_\_code\_\_ 属性获取了该函数的 PyCodeObject 对象,但是还有没有其它的方法呢?显然是有的,答案是通过内置函数 compile,不过在介绍 compile 之前,先介绍一下 eval 和 exec。 | ||
|
||
<font color="darkblue">**eval:传入一个字符串,然后把字符串里面的内容当做表达式。**</font> | ||
|
||
~~~Python | ||
a = 1 | ||
# 所以 eval("a") 就等价于 a | ||
print(eval("a")) # 1 | ||
print(eval("1 + 1 + 1")) # 3 | ||
~~~ | ||
|
||
注意:eval 是有返回值的,返回值就是字符串里面的内容。所以 eval 接收的字符串里面一定是一个表达式,表达式计算之后是一个具体的值,比如 <font color="blue">a = eval("1 + 2")</font>,等价于 <font color="blue">a = 3</font>。 | ||
|
||
但如果是语句的话,比如 <font color="blue">a = eval("b = 3")</font>,这样等价于 <font color="blue">a = (b = 3)</font>,显然这会出现语法错误。因此 eval 函数把字符串两边的引号剥掉之后,得到的一定是一个普通的值。 | ||
|
||
~~~Python | ||
try: | ||
print(eval("xxx")) | ||
except NameError as e: | ||
print(e) # name 'xxx' is not defined | ||
~~~ | ||
|
||
此时等价于 print(xxx),但是 xxx 没有定义,所以报错。 | ||
|
||
~~~Python | ||
# 此时是合法的,等价于 print('xxx') | ||
print(eval("'xxx'")) # xxx | ||
~~~ | ||
|
||
以上就是 eval 函数,使用起来还是很方便的。 | ||
|
||
<font color="darkblue">**exec:传入一个字符串,把字符串里面的内容当成语句来执行,这个是没有返回值的,或者说返回值是 None。**</font> | ||
|
||
~~~Python | ||
# 相当于 a = 1 | ||
exec("a = 1") | ||
print(a) # 1 | ||
|
||
statement = """ | ||
a = 123 | ||
if a == 123: | ||
print("a 等于 123") | ||
else: | ||
print("a 不等于 123") | ||
""" | ||
exec(statement) # a 等于 123 | ||
~~~ | ||
|
||
注意:<font color="blue">a 等于 123</font> 并不是 exec 返回的,而是把上面那坨字符串当成普通代码执行的时候 print 出来的。这便是 exec 的作用,将字符串当成语句来执行。 | ||
|
||
所以使用 exec 可以非常方便地创建多个变量。 | ||
|
||
~~~Python | ||
import random | ||
|
||
for i in range(1, 5): | ||
exec(f"a{i} = {random.randint(1, 100)}") | ||
|
||
print(a1) # 72 | ||
print(a2) # 21 | ||
print(a3) # 38 | ||
print(a4) # 32 | ||
~~~ | ||
|
||
那么 exec 和 eval 的区别就显而易见了,eval 是要求字符串里面的内容能够当成一个值,并且该值就是 eval 函数的返回值。而 exec 则是直接执行里面的内容,返回值是 None。 | ||
|
||
~~~Python | ||
print(eval("1 + 1")) # 2 | ||
print(exec("1 + 1")) # None | ||
|
||
# 相当于 a = 2 | ||
exec("a = 1 + 1") | ||
print(a) # 2 | ||
|
||
try: | ||
# 相当于 a = 2,但很明显 a = 2 是一个语句 | ||
# 它无法作为一个值,因此放到 eval 里面就报错了 | ||
eval("a = 1 + 1") | ||
except SyntaxError as e: | ||
print(e) # invalid syntax (<string>, line 1) | ||
~~~ | ||
|
||
还是很好区分的,但是 eval 和 exec 在生产中尽量要少用。另外,eval 和 exec 还可以接收第二个参数和第三个参数,我们在介绍名字空间的时候再说。 | ||
|
||
<font color="darkblue">**compile:关键来了,它执行后返回的就是一个 PyCodeObject 对象。**</font> | ||
|
||
这个函数接收哪些参数呢? | ||
|
||
- 参数一:当成代码执行的字符串 | ||
- 参数二:可以为这些代码起一个文件名 | ||
- 参数三:执行方式,支持三种,分别是 exec、single、eval | ||
|
||
我们演示一下。 | ||
|
||
~~~Python | ||
# exec:将源代码当做一个模块来编译 | ||
# single:用于编译一个单独的 Python 语句(交互式) | ||
# eval:用于编译一个 eval 表达式 | ||
statement = "a, b = 1, 2" | ||
# 这里我们选择 exec,当成一个模块来编译 | ||
co = compile(statement, "古明地觉的编程教室", "exec") | ||
|
||
print(co.co_firstlineno) # 1 | ||
print(co.co_filename) # 古明地觉的编程教室 | ||
print(co.co_argcount) # 0 | ||
# 我们是以 a, b = 1, 2 这种方式赋值 | ||
# 所以 (1, 2) 会被当成一个元组加载进来 | ||
# 因此从这里可以看出,元组在编译阶段就已经确定好了 | ||
print(co.co_consts) # ((1, 2), None) | ||
|
||
statement = """ | ||
a = 1 | ||
b = 2 | ||
""" | ||
co = compile(statement, "<file>", "exec") | ||
print(co.co_consts) # (1, 2, None) | ||
print(co.co_names) # ('a', 'b') | ||
~~~ | ||
|
||
我们后面在分析 PyCodeObject 的时候,会经常使用 compile 函数。 | ||
|
||
然后 compile 还可以接收一个 flags 参数,也就是第四个参数,它的默认值为 0,表示按照标准模式进行编译,就是之前说的那几步。 | ||
|
||
- 对文本形式的源代码进行分词,将其切分成一个个的 Token; | ||
- 对 Token 进行语法解析,生成抽象语法树(AST); | ||
- 将 AST 编译成 PyCodeObject 对象,简称 code 对象或者代码对象; | ||
|
||
但如果将 flags 指定为 1024,那么 compile 函数在生成 AST 之后会直接停止,然后返回一个 _ast.Module 对象。 | ||
|
||
~~~Python | ||
print( | ||
compile("a = 1", "<file>", "exec").__class__ | ||
) # <class 'code'> | ||
|
||
print( | ||
compile("a = 1", "<file>", "exec", flags=1024).__class__ | ||
) # <class '_ast.Module'> | ||
~~~ | ||
|
||
_ast 模块是和 Python 的抽象语法树相关的,那么问题来了,这个 _ast.Module 对象能够干什么呢?别着急,我们后续在介绍栈帧的时候说。不过由于抽象语法树比较底层,因此知道 compile 的前三个参数的用法即可。 | ||
|
||
## 字节码与反编译 | ||
|
||
关于 Python 的字节码,是后面剖析虚拟机的重点,现在先来看一下。我们知道执行源代码之前会先编译得到 PyCodeObject 对象,里面的 co_code 字段指向了字节码序列,或者说字节码指令集。 | ||
|
||
虚拟机会根据这些指令集来进行一系列的操作(当然也依赖其它的静态信息),从而完成对程序的执行。关于指令,解释器定义了 200 多种,我们大致看一下。 | ||
|
||
~~~C | ||
// Include/opcode.h | ||
#define POP_TOP 1 | ||
#define ROT_TWO 2 | ||
#define ROT_THREE 3 | ||
#define DUP_TOP 4 | ||
#define DUP_TOP_TWO 5 | ||
#define ROT_FOUR 6 | ||
#define NOP 9 | ||
#define UNARY_POSITIVE 10 | ||
#define UNARY_NEGATIVE 11 | ||
#define UNARY_NOT 12 | ||
#define UNARY_INVERT 15 | ||
#define BINARY_MATRIX_MULTIPLY 16 | ||
#define INPLACE_MATRIX_MULTIPLY 17 | ||
#define BINARY_POWER 19 | ||
#define BINARY_MULTIPLY 20 | ||
#define BINARY_MODULO 22 | ||
#define BINARY_ADD 23 | ||
#define BINARY_SUBTRACT 24 | ||
#define BINARY_SUBSCR 25 | ||
#define BINARY_FLOOR_DIVIDE 26 | ||
#define BINARY_TRUE_DIVIDE 27 | ||
#define INPLACE_FLOOR_DIVIDE 28 | ||
#define INPLACE_TRUE_DIVIDE 29 | ||
#define GET_AITER 50 | ||
#define GET_ANEXT 51 | ||
#define BEFORE_ASYNC_WITH 52 | ||
#define BEGIN_FINALLY 53 | ||
#define END_ASYNC_FOR 54 | ||
#define INPLACE_ADD 55 | ||
#define INPLACE_SUBTRACT 56 | ||
#define INPLACE_MULTIPLY 57 | ||
#define INPLACE_MODULO 59 | ||
#define STORE_SUBSCR 60 | ||
#define DELETE_SUBSCR 61 | ||
#define BINARY_LSHIFT 62 | ||
#define BINARY_RSHIFT 63 | ||
#define BINARY_AND 64 | ||
#define BINARY_XOR 65 | ||
#define BINARY_OR 66 | ||
#define INPLACE_POWER 67 | ||
#define GET_ITER 68 | ||
#define GET_YIELD_FROM_ITER 69 | ||
#define PRINT_EXPR 70 | ||
#define LOAD_BUILD_CLASS 71 | ||
#define YIELD_FROM 72 | ||
#define GET_AWAITABLE 73 | ||
#define INPLACE_LSHIFT 75 | ||
#define INPLACE_RSHIFT 76 | ||
// ... | ||
~~~ | ||
|
||
所谓字节码指令其实就是个整数,多个指令组合在一起便是字节码指令集(字节码序列),它是一个 bytes 对象。当然啦,指令集里面不全是指令,索引(偏移量)为偶数的字节表示指令,索引为奇数的字节表示指令参数,后续会细说。 | ||
|
||
然后我们可以通过反编译的方式查看每行 Python 代码都对应哪些操作指令。 | ||
|
||
~~~Python | ||
# Python 的 dis 模块专门负责干这件事情 | ||
import dis | ||
|
||
def foo(a, b): | ||
c = a + b | ||
return c | ||
|
||
# 里面接收 PyCodeObject 对象 | ||
# 当然函数也是可以的,会自动获取 co_code | ||
dis.dis(foo) | ||
""" | ||
2 0 LOAD_FAST 0 (a) | ||
2 LOAD_FAST 1 (b) | ||
4 BINARY_ADD | ||
6 STORE_FAST 2 (c) | ||
3 8 LOAD_FAST 2 (c) | ||
10 RETURN_VALUE | ||
""" | ||
~~~ | ||
|
||
字节码反编译后的结果多么像汇编语言,其中第一列是源代码行号,第二列是字节码偏移量,第三列是字节码指令(也叫操作码),第四列是指令参数(也叫操作数)。Python 的字节码指令都是成对出现的,每个指令会带有一个指令参数。 | ||
|
||
另外查看字节码也可以使用 opcode 模块: | ||
|
||
~~~Python | ||
from opcode import opmap | ||
|
||
opmap = {v: k for k, v in opmap.items()} | ||
|
||
def foo(a, b): | ||
c = a + b | ||
return c | ||
|
||
code = foo.__code__.co_code | ||
for i in range(0, len(code), 2): | ||
print("操作码: {:<12} 操作数: {}".format( | ||
opmap[code[i]], code[i+1] | ||
)) | ||
""" | ||
操作码: LOAD_FAST 操作数: 0 | ||
操作码: LOAD_FAST 操作数: 1 | ||
操作码: BINARY_ADD 操作数: 0 | ||
操作码: STORE_FAST 操作数: 2 | ||
操作码: LOAD_FAST 操作数: 2 | ||
操作码: RETURN_VALUE 操作数: 0 | ||
""" | ||
~~~ | ||
|
||
总之字节码就是一段字节序列,转成列表之后就是一堆数字。偶数位置表示指令本身,而每个指令后面都会跟一个指令参数,也就是奇数位置表示指令参数。 | ||
|
||
所以指令本质上只是一个整数,而虚拟机会根据不同的指令执行不同的逻辑。说白了 Python 虚拟机执行字节码的逻辑就是把自己想象成一颗 CPU,并内置了一个巨型的 switch case 语句,其中每个指令都对应一个 case 分支。然后遍历整条字节码,拿到每一个指令和指令参数。接着对指令进行判断,不同的指令进入不同的 case 分支,执行不同的处理逻辑,直到字节码全部执行完毕或者程序出错。 | ||
|
||
关于执行字节码的具体流程,等介绍栈帧的时候细说。 | ||
|
||
------ | ||
|
||
| ||
|
||
**欢迎大家关注我的公众号:古明地觉的编程教室。** | ||
|
||
![](./images/qrcode_for_gh.jpg) | ||
|
||
**如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。** | ||
|
||
![](./images/supports.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters