前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过内置对象理解 Python(二)

通过内置对象理解 Python(二)

作者头像
老齐
发布2021-11-04 16:36:16
3900
发布2021-11-04 16:36:16
举报
文章被收录于专栏:老齐教室

★本文是《通过内置对象理解 Python》系列文章第二部分 ”

逐一探讨所有的内置函数

在上一节的基础上,下面从一些最有趣的内容开始,这些内容构建了 Python 作为一种语言的基础,逐一对内置函数进行探讨。

compile, execeval 的工作原理

以下面的代码为例:

代码语言:javascript
复制
x = [1, 2]
print(x)

可以将此代码保存到一个文件中并运行,或者在 Python 交互模式中键入它。在这两种情况下,得到的输出结果都将是 [1, 2]

第三中情况,可以将程序以字符串形式传给 Python 的内置函数 exec()

代码语言:javascript
复制
>>> code = '''
... x = [1, 2]
... print(x)
... '''
>>> exec(code)
[1, 2]

exec() (函数名称是 execute 的缩写)以字符串形式接收一些 Python 代码,并将其作为 Python 代码运行。默认情况下,exec() 将和其余代码在相同的作用域中运行。这意味着,它可以读取和操作变量,就像 Python 文件中的任何其他代码片段一样。

代码语言:javascript
复制
>>> x = 5
>>> exec('print(x)')
5

exec() 允许在执行真正的动态代码。例如,可以从互联网上下载一个 Python 文件,将其内容传给 exec() ,它会运行该文件中的程序(但请千万不要这么做) 。

在大多数情况下,编写代码时并不真的需要 exec() 。它用于实现一些真正的动态行为(如在运行时创建动态类,就像 collections.namedtuple 所作的那样,或修改正在从 Python 文件读取的代码(如在 zxpy中)。不过,这里不重点讨论这个,下面要探讨的是 exec() 的执行过程。

exec() 不仅可以接收字符串并将其作为代码运行,还可以接收代码对象,即 Python 程序编译后的“字节码”版本的程序。它们不仅包含从 Python 代码中生成的精确指令,还存储了代码中使用的变量和常量等。

代码对象是从 ASTs(abstract syntax trees,抽象语法树)生成的,ASTs 本身是由运行在代码串上的解析器生成的。

下面,通过例子来了解其内涵。首先,导入 ast 模块,用于生成一个 AST。

代码语言:javascript
复制
>>> import ast
>>> code = '''
... x = [1, 2]
... print(x)
... '''
>>> tree = ast.parse(code)
>>> print(ast.dump(tree, indent=2))
Module(
  body=[
    Assign(
      targets=[
        Name(id='x', ctx=Store())],
      value=List(
        elts=[
          Constant(value=1),
          Constant(value=2)],
        ctx=Load())),
    Expr(
      value=Call(
        func=Name(id='print', ctx=Load()),
        args=[
          Name(id='x', ctx=Load())],
        keywords=[]))],
  type_ignores=[])

看着有点难,接下来抽丝剥茧。

可以将 AST 视为一个 Python 模块。

代码语言:javascript
复制
>>> print(ast.dump(tree, indent=2))
Module(
  body=[
    ...

该模块的 body 部分含有两个子模块(两个语句):

第一个是 Assign 语句 …

代码语言:javascript
复制
Assign(
    ...

赋值给 x

代码语言:javascript
复制
    targets=[
      Name(id='x', ctx=Store())],
    ...

包含两个常量 12 的列表 List 的值。

代码语言:javascript
复制
    value=List(
      elts=[
        Constant(value=1),
        Constant(value=2)],
      ctx=Load())),
  ),

第二个是 Expr 语句,在本例中是调用一个函数 …

代码语言:javascript
复制
  Expr(
    value=Call(
      ...

它的函数名称为 print ,参数值为 x

代码语言:javascript
复制
    func=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 模块:

代码语言:javascript
复制
$ cat code.py
x = [1, 2]
print(x)

$ py -m tokenize code.py
0,0-0,0:            ENCODING       'utf-8'
1,0-1,1:            NAME           'x'
1,2-1,3:            OP             '='
1,4-1,5:            OP             '['
1,5-1,6:            NUMBER         '1'
1,6-1,7:            OP             ','
1,8-1,9:            NUMBER         '2'
1,9-1,10:           OP             ']'
1,10-1,11:          NEWLINE        '
'
2,0-2,5:            NAME           'print'
2,5-2,6:            OP             '('
2,6-2,7:            NAME           'x'
2,7-2,8:            OP             ')'
2,8-2,9:            NEWLINE        '
'
3,0-3,0:            ENDMARKER      ''

所谓 token 化,就是将源码转换为最基本的标记符(token),比如变量名、括号、字符串和数字。它还跟踪每个 token 的行号和位置,这有助于指向错误信息的确切位置。

这个“ token 流”就被解析为 AST 的内容。

(补充知识完毕)


现在有了一个 AST 对象,接下来使用内置的编译器将它编译成有字节码组成的代码对象,并且用 exec() 函数执行代码对象,其效果就如同之前的运行结果一样:

代码语言:javascript
复制
>>> import ast
>>> code = '''
... x = [1, 2]
... print(x)
... '''
>>> tree = ast.parse(code)
>>> code_obj = compile(tree, 'myfile.py', 'exec')
>>> exec(code_obj)
[1, 2]

但是现在,可以看看代码对象是什么样子的,先看它的一些属性:

代码语言:javascript
复制
>>> code_obj.co_code
b'd\x00d\x01g\x02Z\x00e\x01e\x00\x83\x01\x01\x00d\x02S\x00'
>>> code_obj.co_filename
'myfile.py'
>>> code_obj.co_names
('x', 'print')
>>> code_obj.co_consts
(1, 2, None)

可以看到代码中使用的变量 xprint ,以及常数 12 。此外,关于源码文件的更多信息都可以在代码对象中找到。它包含了直接在 Python 虚拟机中运行所需的所有信息,以便生成输出。

如果你想深入了解字节码的含义,下面关于 dis 模块的补充知识可以参考。


补充知识:dis 模块

Python 中的 dis 模块可以把字节码以人类能理解的方式可视化地表达出来,以帮助弄清 Python 在幕后做什么。它接收字节码、常量和变量信息,并产生如下结果:

代码语言:javascript
复制
>>> import dis
>>> dis.dis('''
... x = [1, 2]
... print(x)
... ''')
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 BUILD_LIST               2
              6 STORE_NAME               0 (x)

  2           8 LOAD_NAME                1 (print)
             10 LOAD_NAME                0 (x)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               2 (None)
             18 RETURN_VALUE
>>>

这表明:

  • 第1行创建了4个字节码,将两个常数 12 加到栈上,并从栈最上面的 2 构建一个列表,将其存储到变量 x 中。
  • 第2行创建了6个字节码,它将 printx 加到栈上,并以栈最上面的 1 为参数调用函数(意思是,将参数 x 传给 print() 函数并执行此函数)。然后通过执行 POP_TOP 删除函数的返回值,因为我们没有应用或存储 print(x) 的返回值。最后的两行从文件执行的末尾返回 None ,这不起任何作用。

第1行中的 LOADC_ONST 称为一个操作码(opcode),观察操作码左半边的数字,相邻的两个数字之间差距为2,这是因为把对象存储为操作码时,每个字节码的字长是 2。由此也可知,上述示例中的字符串长度为20个字节常。

代码语言:javascript
复制
>>> code_obj = compile('''
... x = [1, 2]
... print(x)
... ''', 'test', 'exec')
>>> code_obj.co_code
b'd\x00d\x01g\x02Z\x00e\x01e\x00\x83\x01\x01\x00d\x02S\x00'
>>> len(code_obj.co_code)
20

可以确认生成的字节码正好是20字节。

(补充知识完毕)


函数 eval() 非常类似于 exec() ,只是它只接受表达式作为参数,不能像 exec() 那样以一条或者一组语句为参数。而且,与 exec() 的另一个不同之处是,它返回参数中表达式的结果。

例如:

代码语言:javascript
复制
>>> result = eval('1 + 1')
>>> result
2

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.parsecompile 中,如果要执行表达式,但不是类似 Python 文件那样,可以用下面的方式:

代码语言:javascript
复制
>>> expr = ast.parse('1 + 1', mode='eval')
>>> code_obj = compile(expr, '<code>', 'eval')
>>> eval(code_obj)
2

【未完,待续】

其他系列:

点击【阅读原文】查看更多内容

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 逐一探讨所有的内置函数
    • compile, exec 和 eval 的工作原理
    相关产品与服务
    对象存储
    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档