函数的调用过程

1. 函数调用

函数的执行过程从表面上看只是被调用的过程,但它的背后却是用栈这种数据结构实现的。理解了函数的调用过程,递归也就变得容易理解。

下面用一段代码来展示函数的调用

def a():
    print("进入函数a")
    c()
    print("函数a执行结束")


def b():
    print("进入函数b")
    c()
    print("函数b执行结束")


def c():
    print("进入函数c")


a()

执行代码,输出结果

进入函数a
进入函数c
函数a执行结束

函数a的执行过程分为3个步骤:

  1. print("进入函数a")
  2. c() 执行函数c
  3. print("函数a执行结束")

这段代码很简单,但却隐藏着初学者所不知道的秘密,这也是本文所要讲解的重点,首先请你思考一个看起来有点无聊的问题,为什么在执行完第2步以后,会执行第3步,输出"函数a执行结束",而不是在执行完函数c以后,来到函数b中执行print("函数b执行结束")?

你当然可以回答,这是因为代码调用了函数a,所以函数c执行后要返回到a中而不是返回到函数b中。这个解释很合理,但是并没有说明根本原因,为什么函数c执行结束后要回到函数中,而不是函数b中?换一种问法,如果单独执行函数c,函数执行结束后既不会回到函数a,也不会回到函数b,为什么在函数a中执行函数c,函数c直接结束后就要回到函数a呢?

2. 函数调用与栈

在函数a中调用函数c,函数c执行结束后程序要回到函数a中继续执行,我们可以大胆猜测,一定是在某个地方记录了是在函数a中调用执行了函数c,所以才能准确的找到回去的路线,这个记录函数调用信息的地方就是栈。

栈是一种先进后出的数据结构,本文需要你对栈这种数据结构有一定的了解,否则,接下来要讲述的内容你很难理解。

在调用函数时,解释器会将函数的调用信息保存到栈中,这些保存的信息包括:

  1. 函数在第几行代码被执行
  2. 函数所在脚本
  3. 函数里的局部变量

在python中,可以通过sys._getframe()来查看这些信息

import sys

def a(count):
    b(count - 1)
    print(count)

def b(count):
    c(count - 1)
    print(count)

def c(count):
    c_frame = sys._getframe()       # 函数c的frame
    print(c_frame.f_code, c_frame.f_lineno, c_frame.f_locals)

    b_frame = c_frame.f_back        # 函数b的frame
    print(b_frame.f_code, b_frame.f_lineno, b_frame.f_locals)

    a_frame = b_frame.f_back        # 函数a的frame
    print(a_frame.f_code, a_frame.f_lineno, a_frame.f_locals)
    print(count)

a(3)

我在函数c的第2行代码设置断点并进行调试,通过观察变量frame可以获得函数的调用信息,代码输出结果为

<code object c at 0x10f65e660, file "/Users/kwsy/kwsy/coolpython/demo2.py", line 11> 13 {'c_frame': <frame object at 0x10f6395d0>, 'count': 1}
<code object b at 0x10f659db0, file "/Users/kwsy/kwsy/coolpython/demo2.py", line 7> 8 {'count': 2}
<code object a at 0x10f4f09c0, file "/Users/kwsy/kwsy/coolpython/demo2.py", line 3> 4 {'count': 3}
1
2
3
  1. f_code 保存代码信息
  2. f_lineno 保存当前函数执行到第几行,也叫函数返回地址
  3. f_locals 保存当前函数局部变量

函数的调用信息保存在frame结构中,通过f_back可以获得上一层函数的调用信息。

3. 函数调用过程

在调用函数时,会将当前信息保存到栈中,这其中就包括当前执行到第几行(f_lineno),当前的上下文环境(f_locals),当函数执行结束后,解释器则根据这些信息进行调度,它要根据f_lineno找到下一行要执行的代码,同时根据f_locals来还原现场。仍然以第2小节中的内容来透视函数的调用过程
python函数调用过程

函数逐个调用的过程中,每一次调用都会向栈里压如一次有关函数调用的信息,这其中最重要的就是f_lineno 和 f_locals,一个记录当前函数调用发生在第几行,一个记录当前环境下的变量信息,前者是为了在函数执行结束后找到回来的位置,后者是为了回来以后还原上下文环境。

当c函数执行结束后,栈顶的c_frame被移除,此时栈顶保存的信息是b_frame,b_frame保存的信息包括

<code object b at 0x10f659db0, file "/Users/kwsy/kwsy/coolpython/demo2.py", line 7> 8 {'count': 2}

通过frame里信息可以得知,上一次函数调用发生在demo2.py的第8行,因此下一行代码应该执行第9行代码,通过f_locals得知,count的值应该是2,利用f_locals里的信息,还原了现场环境,最终print(count)的输出结果是2

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案