Python如何工作
Python如何工作
Python经常被描述为一种解释型语言 - 当程序运行时,你的源代码被翻译成本地CPU指令 - 但这只是部分正确的。与许多解释型语言一样,Python实际上将源代码编译为虚拟机的一组指令,Python解释器是该虚拟机的一个实现。这种中间格式被称为“字节码”。
因此,.pycPython留下的文件不仅仅是源代码的一些“更快”或“优化”版本; 它们是在您的程序运行时将由Python的虚拟机执行的字节码指令。
我们来看一个例子。这是一个经典的“你好,世界!” 用Python编写:
- def hello()
- print("Hello, World!")
这里是它变成的字节码(翻译成人类可读的形式):
- 0 LOAD_GLOBAL 0 (print)
- 2 LOAD_CONST 1 ('Hello, World!')
- 4 CALL_FUNCTION 1
如果您键入该hello()函数并使用CPython解释器运行它,则上面的列表是Python将执行的内容。不过,它可能看起来有点奇怪,所以让我们深入了解发生了什么。
Python虚拟机中
CPython使用基于堆栈的虚拟机。也就是说,它完全围绕着堆栈数据结构(您可以将一个项目“推”到该结构的“顶部”,或者从“顶部”“弹出”一个项目)。
CPython使用三种类型的堆栈:
1、该调用堆栈。这是一个正在运行的Python程序的主要结构。它有一个项目 - 一个“框架” - 用于每个当前活动的函数调用,堆栈的底部是程序的入口点。每个函数调用都会将新框架推入调用堆栈,并且每次函数调用返回时,其框架都会弹出。
在每一帧中,都有一个评估堆栈(也称为数据堆栈)。这个栈是执行Python函数的地方,执行Python代码主要包括把东西推到这个栈上,操作它们,并将它们弹回去。
2、同样在每一帧中,都有一个块堆栈。Python使用它来跟踪某些类型的控制结构:循环,try/ except块和with块将所有条目都压入块堆栈,并且块堆栈在您退出其中一个结构时弹出。这有助于Python知道哪些块在任何特定时刻处于活动状态,例如,a continue或break语句可能会影响正确的块。
3、尽管有一些指令可以执行其他操作(如跳转到特定指令或操作块堆栈),但Python的大部分字节码指令操作当前调用堆栈帧的评估堆栈。
为了感受这一点,假设我们有一些调用函数的代码,如下所示:my_function(my_variable, 2)。Python会将其转换为四个字节码指令序列:
1、一条 LOAD_NAME那查找函数对象指令my_function并将其推到评价堆栈的顶部
2、另一条LOAD_NAME查找变量my_variable并将其推到评估堆栈顶部的指令
3、一条 LOAD_CONST指令推字面整数值2对评价堆栈的顶部
4、一条CALL_FUNCTION指令
该CALL_FUNCTION指令的参数为2,表示Python需要从堆栈顶部弹出两个位置参数; 那么调用的函数将处于最前面,并且它也可以被弹出(对于涉及关键字参数的函数,使用了不同的指令CALL_FUNCTION_KW- - 但具有类似的操作原理,并且第三条指令CALL_FUNCTION_EX用于函数涉及与*或**运营商拆开包装的调用)。一旦Python拥有了所有这些功能,它将在调用堆栈上分配一个新框架,为函数调用填充局部变量,并执行该my_function框架内的字节码。一旦完成,框架将从调用堆栈中弹出,并在原始框架中返回值my_function 将被推到评估堆栈顶部。
访问和理解Python字节码
如果您想要使用这个,那么Python标准库中的dis模块是一个巨大的帮助;dis模块为Python字节码提供了一个“反汇编器”,使其易于获得人类可读的版本,并查找各种字节码指令。dis模块的文档会检查它的内容。提供一个完整的字节码指令列表,以及它们所做的和它们的参数。
例如,要获取上述hello()函数的字节码列表,我将它键入Python解释器,然后运行:
- import dis
- dis.dis(hello)
该函数dis.dis()将反汇编函数,方法,类,模块,编译的Python代码对象或包含源代码的字符串文本,并打印出可读的版本。dis模块中另一个方便的功能是distb()。您可以将它传递给Python traceback对象,或者在引发异常之后调用它,并且它会在异常时分解调用堆栈中的最顶层函数,打印其字节码,并向引发该指令的指令插入一个指针例外。
查看Python为每个函数编译的编译代码对象也很有用,因为执行一个函数会利用这些代码对象的属性。以下是查看该hello()功能的示例:
- >>> 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:将索引1处的字面值取出co_consts并将其压入(索引0处的值是文字None,co_consts因为None如果没有return达到明确声明,Python函数调用将具有隐式返回值)
CALL_FUNCTION 1:告诉Python调用一个函数; 它需要从堆栈中弹出一个位置参数,然后新的堆栈顶部将是要调用的函数。
"raw"字节码 - 作为非人类可读的字节 - 在代码对象上也可用作属性co_code。dis.opname如果您想尝试手动反汇编函数,则可以使用列表从其小数字节值中查找字节码指令的名称。
使用字节码
现在你已经读了这么多,你可能会想:“好吧,我猜这很酷,但是知道这个的实际价值是什么?” 为了好奇而抛开好奇心,理解Python字节码在几个方面是有用的。
首先,理解Python的执行模型可以帮助你推理你的代码。人们喜欢开玩笑说C是一种“便携式汇编程序”,你可以很好地猜测C语言源代码中哪些机器指令会变成什么样。理解字节码将为您提供与Python相同的功能 - 如果您可以预测Python源代码转换为什么字节码,则可以更好地决定如何编写和优化它。
其次,了解字节码是回答有关Python的问题的有用方法。例如,我经常看到较新的Python程序员想知道为什么某些结构比其他结构更快(比如为什么{}要快dict())。知道如何访问和读取Python字节码可以让你找出答案(尝试:dis.dis("{}")与dis.dis("dict()"))。
最后,理解字节码以及Python如何执行它,对于Python程序员不经常参与的特定类型的编程提供了一个有用的视角:面向堆栈的编程。如果您曾经使用过像FORTH或Factor这样的面向堆栈的语言,这可能是一个旧消息,但如果您不熟悉这种方法,那么了解Python字节码并了解其面向堆栈的编程模型如何工作就是一个整洁扩大你的编程知识的方法。