了解 Python 字节码是什么,Python 如何使用它来执行你的代码,以及知道它是如何帮到你的。html
若是你曾经编写过 Python,或者只是使用过 Python,你或许常常会看到 Python 源代码文件——它们的名字以 .py
结尾。你可能还看到过其它类型的文件,好比以 .pyc
结尾的文件,或许你可能据说过它们就是 Python 的 “字节码bytecode” 文件。(在 Python 3 上这些可能不容易看到 —— 由于它们与你的 .py
文件不在同一个目录下,它们在一个叫 __pycache__
的子目录中)或者你也据说过,这是节省时间的一种方法,它能够避免每次运行 Python 时去从新解析源代码。python
可是,除了 “噢,原来这就是 Python 字节码” 以外,你还知道这些文件能作什么吗?以及 Python 是如何使用它们的?linux
若是你不知道,那你走运了!今天我将带你了解 Python 的字节码是什么,Python 如何使用它去运行你的代码,以及知道它是如何帮助你的。git
Python 常常被介绍为它是一个解释型语言 —— 其中一个缘由是在程序运行时,你的源代码被转换成 CPU 的原生指令 —— 但这样的见解只是部分正确。Python 与大多数解释型语言同样,确实是将源代码编译为一组虚拟机指令,而且 Python 解释器是针对相应的虚拟机实现的。这种中间格式被称为 “字节码”。程序员
所以,这些 .pyc
文件是 Python 悄悄留下的,是为了让它们运行的 “更快”,或者是针对你的源代码的 “优化” 版本;它们是你的程序在 Python 虚拟机上运行的字节码指令。github
咱们来看一个示例。这里是用 Python 写的经典程序 “Hello, World!”:编程
1
2
|
def hello()
print("Hello, World!")
|
下面是转换后的字节码(转换为人类可读的格式):数据结构
1
2
3
|
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Hello, World!')
4 CALL_FUNCTION 1
|
若是你输入那个 hello()
函数,而后使用 CPython 解释器去运行它,那么上述列出的内容就是 Python 所运行的。它看起来可能有点奇怪,所以,咱们来深刻了解一下它都作了些什么。编程语言
CPython 使用一个基于栈的虚拟机。也就是说,它彻底面向栈数据结构的(你能够 “推入” 一个东西到栈 “顶”,或者,从栈 “顶” 上 “弹出” 一个东西来)。函数
CPython 使用三种类型的栈:
try
/ except
块、以及 with
块,所有推入到块栈中,当你退出这些控制结构时,块栈被销毁。这将帮助 Python 了解任意给定时刻哪一个块是活动的,好比,一个 continue
或者 break
语句可能影响正确的块。大多数 Python 字节码指令操做的是当前调用栈帧的计算栈,虽然,还有一些指令能够作其它的事情(好比跳转到指定指令,或者操做块栈)。
为了更好地理解,假设咱们有一些调用函数的代码,好比这个:my_function(my_variable, 2)
。Python 将转换为一系列字节码指令:
LOAD_NAME
指令去查找函数对象 my_function
,而后将它推入到计算栈的顶部LOAD_NAME
指令去查找变量 my_variable
,而后将它推入到计算栈的顶部LOAD_CONST
指令去推入一个实整数值 2
到计算栈的顶部CALL_FUNCTION
指令这个 CALL_FUNCTION
指令将有 2 个参数,它表示那个 Python 须要从栈顶弹出两个位置参数;而后函数将在它上面进行调用,而且它也同时被弹出(对于函数涉及的关键字参数,它使用另外一个不一样的指令 —— CALL_FUNCTION_KW
,但使用的操做原则相似,以及第三个指令 —— CALL_FUNCTION_EX
,它适用于函数调用涉及到参数使用 *
或 **
操做符的状况)。一旦 Python 拥有了这些以后,它将在调用栈上分配一个新帧,填充到函数调用的本地变量上,而后,运行那个帧内的 my_function
字节码。运行完成后,这个帧将被调用栈销毁,而在最初的帧内,my_function
的返回值将被推入到计算栈的顶部。
若是你想玩转字节码,那么,Python 标准库中的 dis
模块将对你有很是大的帮助;dis
模块为 Python 字节码提供了一个 “反汇编”,它可让你更容易地获得一我的类可读的版本,以及查找各类字节码指令。dis
模块的文档 可让你遍历它的内容,而且提供一个字节码指令可以作什么和有什么样的参数的完整清单。
例如,获取上面的 hello()
函数的列表,能够在一个 Python 解析器中输入以下内容,而后运行它:
1
2
|
import dis
dis.dis(hello)
|
函数 dis.dis()
将反汇编一个函数、方法、类、模块、编译过的 Python 代码对象、或者字符串包含的源代码,以及显示出一我的类可读的版本。dis
模块中另外一个方便的功能是 distb()
。你能够给它传递一个 Python 追溯对象,或者在发生预期外状况时调用它,而后它将在发生预期外状况时反汇编调用栈上最顶端的函数,并显示它的字节码,以及插入一个指向到引起意外状况的指令的指针。
它也能够用于查看 Python 为每一个函数构建的编译后的代码对象,由于运行一个函数将会用到这些代码对象的属性。这里有一个查看 hello()
函数的示例:
1
2
3
4
5
6
7
8
|
>>> hello.__code__
<code object hello at 0x104e46930, file "<stdin>", line 1>
>>> hello.__code__.co_consts
(None, 'Hello, World!')
>>> hello.__code__.co_varnames
()
>>> hello.__code__.co_names
('print',)
|
代码对象在函数中能够以属性 __code__
来访问,而且携带了一些重要的属性:
co_consts
是存在于函数体内的任意实数的元组co_varnames
是函数体内使用的包含任意本地变量名字的元组co_names
是在函数体内引用的任意非本地名字的元组许多字节码指令 —— 尤为是那些推入到栈中的加载值,或者在变量和属性中的存储值 —— 在这些元组中的索引做为它们参数。
所以,如今咱们可以理解 hello()
函数中所列出的字节码:
LOAD_GLOBAL 0
:告诉 Python 经过 co_names
(它是 print
函数)的索引 0 上的名字去查找它指向的全局对象,而后将它推入到计算栈LOAD_CONST 1
:带入 co_consts
在索引 1 上的字面值,并将它推入(索引 0 上的字面值是 None
,它表示在 co_consts
中,由于 Python 函数调用有一个隐式的返回值 None
,若是没有显式的返回表达式,就返回这个隐式的值 )。CALL_FUNCTION 1
:告诉 Python 去调用一个函数;它须要从栈中弹出一个位置参数,而后,新的栈顶将被函数调用。“原始的” 字节码 —— 是非人类可读格式的字节 —— 也能够在代码对象上做为 co_code
属性可用。若是你有兴趣尝试手工反汇编一个函数时,你能够从它们的十进制字节值中,使用列出 dis.opname
的方式去查看字节码指令的名字。
如今,你已经了解的足够多了,你可能会想 “OK,我认为它很酷,可是知道这些有什么实际价值呢?”因为对它很好奇,咱们去了解它,可是除了好奇以外,Python 字节码在几个方面仍是很是有用的。
首先,理解 Python 的运行模型能够帮你更好地理解你的代码。人们都开玩笑说,C 是一种 “可移植汇编器”,你能够很好地猜想出一段 C 代码转换成什么样的机器指令。理解 Python 字节码以后,你在使用 Python 时也具有一样的能力 —— 若是你能预料到你的 Python 源代码将被转换成什么样的字节码,那么你能够知道如何更好地写和优化 Python 源代码。
第二,理解字节码能够帮你更好地回答有关 Python 的问题。好比,我常常看到一些 Python 新手困惑为何某些结构比其它结构运行的更快(好比,为何 {}
比 dict()
快)。知道如何去访问和阅读 Python 字节码将让你很容易回答这样的问题(尝试对比一下: dis.dis("{}")
与 dis.dis("dict()")
就会明白)。
最后,理解字节码和 Python 如何运行它,为 Python 程序员不常用的一种特定的编程方式提供了有用的视角:面向栈的编程。若是你之前历来没有使用过像 FORTH 或 Fator 这样的面向栈的编程语言,它们可能有些古老,可是,若是你不熟悉这种方法,学习有关 Python 字节码的知识,以及理解面向栈的编程模型是如何工做的,将有助你开拓你的编程视野
转自:http://python.jobbole.com/89232/ ,原文出处: James Bennett 译文出处:linux中国—qhwdw