说明
《Python 教程》 持续更新中,提供建议、纠错、催更等加作者微信: gairuo123(备注:pandas教程)和关注公众号「盖若」ID: gairuo。跟作者学习,请进入 Python学习课程。欢迎关注作者出版的书籍:《深入浅出Pandas》 和 《Python之光》。
本页将从总体上描述 Python 代码是怎么运行的,帮助我们在程序设计、编写代码、查找问题时,有一个程序运行大局意识。这里会涉及到 Python 对内存的使用、程序运行的过程等内容。Python 是一种动态类型化语言,Python 中的一切都是对象,我们得到的返回是对这些对象的引用。
Python 程序由代码块(code block)构成。代码块是作为一个单元执行的一段 Python 程序文本。代码块包括:
__main__
模块)运行的模块(使用 -m 参数从命令行)代码块在执行帧(frame)中执行。一个帧包含一些管理信息(用于调试),并确定代码块执行完成后在何处以及如何继续执行。
命名空间是从名称到对象的映射,Python 是用命名空间来记录变量的轨迹的。名称空间目前都实现为 Python 字典,键是变量名,值是变量值。各个命名空间是独立没有关系的,一个命名空间中不能有重名,但是不同的命名空间可以重名而没有任何影响。
在一个 Python 程序中的任何一个地方,都存在几个可用的命名空间。
当一行代码要使用变量 x 的值时,Python 会到所有可用的名字空间去查找变量,按照如下顺序:
嵌套函数的情况:
不同的命名空间在不同的时刻创建,有不同的生存期。
各命名空间创建顺序:
Python 解释器启动 -> 创建内建命名空间 -> 加载模块 -> 创建全局命名空间 -> 函数被调用 -> 创建局部命名空间
各命名空间销毁顺序:
函数调用结束 -> 销毁函数对应的局部命名空间 -> Python 虚拟机(解释器)退出 -> 销毁全局命名空间 -> 销毁内建命名空间
Python 解释器加载阶段会创建出内建命名空间、模块的全局命名空间,局部命名空间是在运行阶段函数被调用时动态创建出来的,函数调用结束动态的销毁的。
Python 的全局命名空间存储在一个叫 globals()
的 dict 对象中;局部命名空间存储在一个叫 locals()
的 dict 对象中。可以用 locals()
来查看该函数体内的所有变量名和变量值。
名称(标识符)指向对象,名称是通过名称绑定操作引入的。以下构造绑定名称:
要注意的是:
from ... import *
绑定导入模块中定义的所有名称,以下划线开头的名称除外。这种形式只能在模块级别使用name 和 object 是两个抽象的概念,在 Python 中,赋值语句的作用有两个:
Python是面向对象的编程语言,一切皆为对象,不像 Java 里面还有所谓的基本数据类型(原始数据类型,int、char、boolean、double等),所以变量其实就是对象的引用,它们保存在名字叫”栈“(stack)的存储空间上,而对象理论上来讲都保存在称为”堆“(heap)的存储空间上。当然有些通过字面量语法创建的对象会共享内存空间。
Python 不会在执行之前编译我们的 Python 代码,而是 Python 本身是一个编译程序(称为 Python 解释器),它逐行执行 Python 代码,解析代码,转换为字节码并运行。
Python 必须为我们做一些内存管理,这样我们才能实现动态类型化,完全忽略代码的内存分配和释放。
如果您在较高的层次上理解了内存模型,您就会知道,在运行时,系统可以利用两种类型的内存段:栈段(Stack segment)和堆段(Heap segment)。理想情况下:
如,在 linux 系统上,动态分配是通过调用 brk()/sbrk()
系统调用来实现的,该调用返回(计算机的)页面,由应用程序维护并计算接收到的内存。这样做的好处是为了更好的利用高效率的内存,同时将程序员从各种内存管理的细节中解放,让他们更好的关注业务逻辑。
说到栈内存,不能不说的是“栈”。只要学过一点数据结构的都知道,栈(stack)是一种后进先出(last in first out,缩写为LIFO)的数据结构。那“栈内存”里的“栈”和后进先出有什么关系呢?大家在写代码的时候,都会调用函数,而函数又可以调用其他函数。假定一个函数func1调用了函数func2,func2又调用了func3,那么这三个函数返回(return)的顺序是什么?没错,func3先返回,然后是func2,最后是func1。看见了?先调用的函数后返回,后调用的函数先返回,后进先出!这就是大家常说的“函数调用栈”(call stack)。
大家也都知道,函数可以定义一些参数(形参),函数体里也可以定义局部变量。对于每一个函数来说,当前函数一旦返回,这些形参和局部变量就出了作用域,也就没用了,于是它们占用的内存也就可以安全地释放了。从这里可以看见,形参和局部变量的生命周期和函数调用的生命周期一致,而函数的嵌套调用是后进先出的,那么这部分内存也可以用后进先出的方式去管理,这就是栈内存。每个函数被调用时,操作系统或语言运行时会创建一个对应于本次函数调用的“栈帧”(stack frame)并push到栈内存上,当这个函数返回的时候pop掉。
对于每一个栈帧,它所需要的内存大小在编译时就能知道了(如果不考虑编译时优化,每个栈帧的大小等于当前函数的所有形参和局部变量的大小的总和,外加一些元数据的大小),并且每个形参和局部变量在当前栈帧上的内存地址偏移量也都是固定的,所以只要有个变量名就可以直接获取到这个变量对应的内存地址(变量名在编译时直接变成偏移量。这就是为什么Java程序反编译之后局部变量和型参的名称全部丢失的原因),从而直接访问它。对于多线程的程序,每个线程都有自己的栈内存。
由于栈内存的生命周期非常明了,栈内存的管理也相当地直截了当,通常操作系统直接就替你做了。栈内存的好处我个人觉得有这么几点:
但是栈内存也有它的“缺陷”(严格地说是它设计的时候就不想让你那么用它):
为了克服上述任意一种“缺陷”,我们需要另一种内存管理机制:
很抱歉,翻阅了很多资料也没找到“堆内存”和数据结构里的“堆”之间的关系。有人说是当年的Lisp用堆实现了这种内存管理方式(但没有证据)。总之记住数据结构的堆和堆内存完全不沾边就对了。堆内存就像是一堆杂乱无章的东西堆在那儿,也许这才是“堆内存”这个名称的由来吧。
堆内存是一种按需分配的动态内存管理机制(这里的动态是指什么时候申请就什么时候给你,申请多少给你多少,而不是像栈内存那样只要调了函数就直接把所有本次调用可能用到的内存都给你,不管你具体什么时候用),这种机制能让数据在内存里存活时间超过定义它的函数的生命周期(不去销毁就一直在那儿),也能让多个线程共享同一个内存里的结构体/对象(任何线程只要拿到这片内存的地址和大小就能访问)。但是由于它的动态,你不能仅靠一个变量名就知道每一份数据在内存里的确切位置,导致要访问它里面的数据必须通过指针或引用。
堆内存给我的感觉很像某些商场里寄放包裹的储物柜。你要用储物柜,就得先找前台申请,她会给你一把钥匙。有了这把钥匙,你就能往柜子里放东西了。等你购物完准备走人的时候,把柜子里的东西取出来,再把钥匙还掉。只不过商场里的柜子只有一种大小,而堆内存空间你可以任意指定需要的大小。另外,申请堆内存时拿到的钥匙(指针或引用)是可以复制的。
堆内存里的数据的生命周期只有程序员才知道(如果他真的知道),所以任何操作系统和语言运行时都不能准确地知道什么时候才应该释放一片堆内存空间。堆内存的管理现在比较常见的有三种手段:
大家可以发现我这里只字未提内存地址增长方向,因为我觉得这是实现细节,每个语言都可以有自己的特色(比如Erlang的栈内存压根儿就不是正常的栈,而是类似于注册表
的形式,所以也就不存在内存地址增长方向这一说了,当然它也不会有栈溢出),所以不能一概而论啦。
本节参考
通过一个示例代码,让我们试着理解它:
def bar(a):
a = a - 1
return a
def foo(a):
a = a * a
b = bar(a)
return b
def main():
x = 2
y = foo(x)
if __name__ == "__main__":
main()
上述代码有三个函数,程序运行时调用 main()
、main()
中调用 foo()
、 foo()
调用 bar()
,然后依次收到所返回的值进行计算,最终返回结果。
这个执行过程为:
在下图中,当 foo()
和 bar()
超出范围(从堆栈中弹出)时,它们留下了一些带有暂停对象的堆内存,没有释放它们。
Python 有一个内置的资源管理器,名为垃圾收集器,它在垃圾收集之前保持在用内存和使用后内存引用的计数。
todo
更新时间:2022-04-01 23:31:31 标签:python 程序 执行 原理