通过内置对象理解 Python(三)
2021-11-03
逐一探讨所有的内置函数
在上一节的基础上,下面从一些最有趣的内容开始,这些内容构建了 Python 作为一种语言的基础,逐一对内置函数进行探讨。
compile
, exec
和 eval
的工作原理
以下面的代码为例:
1 | x = [1, 2] |
可以将此代码保存到一个文件中并运行,或者在 Python 交互模式中键入它。在这两种情况下,得到的输出结果都将是 [1, 2]
。
第三中情况,可以将程序以字符串形式传给 Python 的内置函数 exec()
:
1 | ''' code = |
exec()
(函数名称是 execute 的缩写)以字符串形式接收一些 Python 代码,并将其作为 Python 代码运行。 默认情况下,exec()
将和其余代码在相同的作用域中运行。这意味着,它可以读取和操作变量,就像 Python 文件中的任何其他代码片段一样。
1 | 5 x = |
exec()
允许在执行真正的动态代码。 例如,可以从互联网上下载一个 Python 文件,将其内容传给 exec()
,它会运行该文件中的程序(但请千万不要这么做) 。
在大多数情况下,编写代码时并不真的需要 exec()
。 它用于实现一些真正的动态行为(如在运行时创建动态类,就像 collections.namedtuple
所作的那样,或修改正在从 Python 文件读取的代码(如在 zxpy中)。不过,这里不重点讨论这个,下面要探讨的是 exec()
的执行过程。
exec()
不仅可以接收字符串并将其作为代码运行,还可以接收代码对象,即 Python 程序编译后的“字节码”版本的程序。它们不仅包含从 Python 代码中生成的精确指令,还存储了代码中使用的变量和常量等。
代码对象是从 ASTs(abstract syntax trees,抽象语法树)生成的,ASTs 本身是由运行在代码串上的解析器生成的。
下面,通过例子来了解其内涵。 首先,导入 ast
模块,用于生成一个 AST。
1 | import ast |
看着有点难,接下来抽丝剥茧。
可以将 AST
视为一个 Python 模块。
1 | 2)) print(ast.dump(tree, indent= |
该模块的 body 部分含有两个子模块(两个语句):
第一个是
Assign
语句 …1
2Assign(
...赋值给
x
…1
2
3targets=[
Name(id='x', ctx=Store())],
...包含两个常量
1
和2
的列表List
的值。1
2
3
4
5
6value=List(
elts=[
Constant(value=1),
Constant(value=2)],
ctx=Load())),
),第二个是
Expr
语句,在本例中是调用一个函数 …1
2
3Expr(
value=Call(
...它的函数名称为
print
,参数值为x
。1
2
3func=Name(id='print', ctx=Load()),
args=[
Name(id='x', ctx=Load())],
所以 Assign
部分描述的是 x = [1, 2]
,而 Expr
描述的是 print(x)
。 现在看起来没那么难,对吧?
补充知识: Tokenizer
实际上,在将代码解析为 AST 之前,还需要执行名为 Lexing 的一个步骤。
它指的是根据 Python 语法将源代码转换为 token。从下面的内容中可以看到 Python 如何将源码 token 化。这里使用了 tokenize
模块:
1 | cat code.py |
所谓 token 化,就是将源码转换为最基本的标记符(token),比如变量名、括号、字符串和数字。 它还跟踪每个 token 的行号和位置,这有助于指向错误信息的确切位置。
这个“ token 流”就被解析为 AST 的内容。
(补充知识完毕)
现在有了一个 AST 对象,接下来使用内置的编译器将它编译成代码对象,并且用 exec()
函数执行代码对象,其效果就如同之前的运行结果一样:
1 | import ast>>> code = '''... x = [1, 2]... print(x)... '''>>> tree = ast.parse(code)>>> code_obj = compile(tree, 'myfile.py', 'exec')>>> exec(code_obj)[1, 2] |
但是现在,可以看看代码对象是什么样子的,先看它的一些属性:
1 | import ast |
可以看到代码中使用的变量 x
和 print
,以及常数 1
和 2
。此外,关于源码文件的更多信息都可以在代码对象中找到。 它包含了直接在 Python 虚拟机中运行所需的所有信息,以便生成输出。
如果你想深入了解字节码的含义,下面关于 dis
模块的补充知识可以参考。
补充知识::dis
模块
Python 中的 dis
模块可以把字节码以人类能理解的方式可视化地表达出来,以帮助弄清 Python 在幕后做什么。 它接收字节码、常量和变量信息,并产生如下结果:
1 | import dis |
这表明:
- 第1行创建了4个字节码,将两个常数
1
和2
加到栈上,并从栈最上面的2
构建一个列表,将其存储到变量x
中。 - 第2行创建了6个字节码,它将
print
和x
加到栈上,并以栈最上面的1
为参数调用函数(意思是,将参数x
传给print()
函数并执行此函数)。 然后通过执行POP_TOP
删除函数的返回值,因为我们没有应用或存储print(x)
的返回值。最后的两行从文件执行的末尾返回None
,这不起任何作用。
第1行中的 LOADC_ONST
称为一个操作码(opcode),观察操作码左半边的数字,相邻的两个数字之间差距为2,这是因为把对象存储为操作码时,每个字节码的字长是 2。由此也可知,上述示例中的字符串长度为20个字节常。
1 | ''' code_obj = compile( |
可以确认生成的字节码正好是20字节。
(补充知识完毕)
函数 eval()
非常类似于 exec()
,只是它只接受表达式作为参数,不能像 exec()
那样以一条或者一组语句为参数。而且,与 exec()
的另一个不同之处是,它返回参数中表达式的结果。
例如:
1 | '1 + 1') result = eval( |
You can also go the long, detailed route with eval
, you just need to tell ast.parse
and compile
that you’re expecting to evaluate this code for its value, instead of running it like a Python file.
函数对象 eval
可以应用于很多地方,比如在 ast.parse
和 compile
中,如果要执行表达式,但不是类似 Python 文件那样,可以用下面的方式:
1 | '1 + 1', mode='eval') expr = ast.parse( |
globals
和 locals
:包含所有
虽然生成的代码对象和定义的常量有类似的存储逻辑,但变量的值没有存储,比如:
1 | def double(number): |
这个函数将存储常量 2
以及变量名 number
,但显然它不能包含 number
的实际值,因为只有在函数实际运行时才会给该参数赋值。
那么,这是为什么呢? 答案是 Python 将所有东西都存储在与每个局部作用域关联的字典中。 这意味着每段代码都有自己定义的“局部作用域”,“局部作用域”在代码中使用 locals()
访问,它包含对应局部作用域的变量名和值。
让我们来看看实际情况:
1 | 5 value = |
看最后一行:不仅 value
存储在 locals()
返回的字典中,函数 double()
本身也存储在那里!这就是 Python 存储数据的方式。
globals()
也与此类似,只不过它是指向全局作用域。所以像这样的代码:
1 | magic_number = 42 |
locals()
只包含 x
和 y
,而 globals()
则包含 magic_number
和 function
本身。
input
和 print
:基本功能
input
和 print
可能是 Python 中用得最广泛的两个函数,很多学习者初学之时,经常用它们,看起来很简单,比如 input
接收一行文本,print
把文本打印出来,就这么简单。 对吧?
实际上,可能比你知道得更复杂。
下面是 print()
函数的完整格式:
1 | print(*values, sep=' ', end='\n', file=sys.stdout, flush=False) |
参数 *values
意味着可以提供任意数量的位置参数,print
都能正确地打印出它们,默认情况所打印的位置参数引用对象之间用空格分隔。
如果想让分隔符有所不同,例如,想把每个项目打印在不同的行上,可以相应地设置 sep
的值,比如为 \n
:
1 | 1, 2, 3, 4) print( |
还有 end
参数,通过它可以修改行末的设置,默认是行末端为 \n
,即换行,如果不换行,可以修改为 end=''
:
1 | for i in range(10): |
再看 print
另外两个参数:file
和 flush
。file
指将打印的内容输出到指定“文件”,默认值是 sys.stdout
,即打印到标准输出文件,也就是打印到控制台, 如下,也可以设置为一个具体的文件。
1 | with open('myfile.txt', 'w') as f: |
补充知识:使用上下文管理器
在函数 print()
中,默认情况下 file=sys.stdout
,则会将所要打印的内容输出到控制台,如果将 sys.stdout
引用一个文件,则会将打印的内容输出到该文件中。例如:
1 | import sys |
但是,如果这样做了,就无法再回到控制台,除非关闭当前交互模式,重新进入。如果关闭文件,也会遇到问题。例如:
1 | import sys |
为了避免此问题,可以使用上下文管理器做装饰器,以确保在完成任务后还原 sys.stdout
。
1 | import sys |
可以这样使用它:
1 | with print_writer('myfile.txt'): |
打印到文件或 IO 对象是一个常见的用例,contextlib
有一个函数 redirect_stdout
:
1 | from contextlib import redirect_stdout |
(补充知识完毕)
参数 flush=False
用于标记 print()
的文本内容发送到控制台/文件,而不是将其放入缓存。 这通常没什么区别,但如果在控制台打印一个非常长的字符串,可能要将它设置为 True
,以避免在向用户显示输出时出现延迟。
而对于 input()
函数,就没什么秘密而言了,它只是接受一个字符串作为提示符显示。
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能