1. 函数
1.1 函数是什么
编程中的函数和数学中的函数有一定的相似之处。

数学上的函数是一种映射关系,把参数x通过一系列运算映射到y。
编程中的函数,是一段 可以被重复使用的代码片段。
【代码示例】求数列的和,不使用函数。
# 1. 求 1 - 100 的和
sum = 0
for i in range(1, 101):
sum += i
print(sum)
# 2. 求 300 - 400 的和
sum = 0
for i in range(300, 401):
sum += i
print(sum)
# 3. 求 1 - 1000 的和
sum = 0
for i in range(1, 1001):
sum += i
print(sum)
可以发现,这几组代码基本是相似的,只有一点点差异,只是每次计算的数据范围不一样。
可以把重复代码提取出来,做成一个函数。


【代码示例】求 数列 的和,使用函数。
# 定义函数
def calcSum(beg, end):
sum = 0
for i in range(beg, end + 1): # 左闭右开
sum += i
print(sum) # 同一缩进
# 调用函数
sum(1, 100) # 求 1-100 的和
sum(300, 400) # 求 300-400 的和
sum(1, 1000) # 求 1-1000 的和

可以明显看到,重复的代码已经被消除了。
1.2 语法格式
(1)创建函数(定义函数)
def 函数名(形参列表):
函数体
return 返回值

(2)调用函数(使用函数)
- 函数名(实参列表) // 不考虑返回值
- 返回值 = 函数名(实参列表) // 考虑返回值
【注意】函数定义并不会执行函数体内容,必须要调用才会执行,调用几次就会执行几次。
def test1():
print('hello')
# 如果光是定义函数, 而不调用, 则不会执行.
函数必须先定义,再使用。
test3() # 还没有执行到定义, 就先执行调用了, 此时就会报错.
def test3():
print('hello')



【代码警告的处理】

函数定义、类定义后面期望有两个空行。






pycharm中更建议使用蛇形命名法,而不是驼峰命名法。
实际工作中使用什么规范取决于具体的要求。

警告是可以设置为忽略的。
1.3 函数参数
- 在函数定义的时候,可以在 ( ) 中指定 "形式参数"(简称 形参)。
- 在函数调用的时候,由调用者把 "实际参数"(简称 实参)传递进去。
这样就可以做到:一份函数,针对不同的数据进行计算处理。
考虑前面的代码案例:
def calcSum(beg, end):
sum = 0
for i in range(beg, end + 1):
sum += i
print(sum)
calcSum(1, 100)
calcSum(300, 400)
calcSum(1, 1000)
上面的代码中,beg, end 就是函数的形参,1, 100 / 300, 400 就是函数的实参。

实参和形参之间的关系,就像签合同一样。

def 签合同(甲方,乙方):
合同内容...
签合同('张总','李总')
签合同('张总','赵总')
签合同('张总','钱总')
合同的内容:函数的定义
合同的甲方乙方:形参
签合同的过程:函数的调用
实际签合同的人:实参
【注意】
- 一个函数可以有一个形参,也可以有多个形参,也可以没有形参。
- 一个函数的形参有几个,那么传递实参的时候也得传几个,保证个数要匹配。
def test(a, b, c):
print(a, b, c)
test(10)

缺失两个要求的位置参数——实参按照位置一一对应给形参赋值。

不光是顺序,C++ / Java还要求参数的类型一致。
python不需要,python的函数定义的时候,形参都只有参数名,没有设置参数类型。
- 和 C++ / Java 不同,Python 是动态类型的编程语言,函数的形参不必指定参数类型。
- 换句话说,一个函数可以支持多种不同类型的参数。

def test(a):
print(a)
test(10)
test('hello')
test(True)


写一个函数,就可以支持各种不同类型的参数。
C++、Java中则需要借助“模版”、“泛型”这样的概念来实现。
但是python里面也不是什么类型都可以,需要支持函数体内对应的运算操作。

这种错误在运行时才能报出。
1.4 函数返回值
函数的参数可以视为是函数的 "输入",则函数的返回值,就可以视为是函数的 "输出"。


下列代码
# 求 beg, end 这个范围的整数之和
def calcSum(beg, end):
sum = 0
for i in range(beg, end + 1):
sum += i
print(sum)
calcSum(1, 100)
可以转换成
def calcSum(beg, end):
sum = 0
for i in range(beg, end + 1):
sum += i
return sum
result = calcSum(1, 100)
print(result)
这两个代码的区别就在于,前者直接在函数内部进行了打印,后者则使用 return 语句把结果返回给函数调用者,再由调用者负责打印。







- 耦合低:一方发生变化,对另一方的影响小;
- 耦合高:一方发生变化,对另一方的影响大;
提升代码的可维护性:未来需要修改交互逻辑,需要改动的地方就很少。
- 一个函数中可以有多个 return 语句。



一般多个 return 语句是搭配 分支语句 / 循环语句 的。
# 判定是否是奇数
def isOdd(num):
if num % 2 == 0:
return False
else:
return True
result = isOdd(10)
print(result)

上述代码的else是冗余的。
- 执行到 return 语句,函数就会立即执行结束,回到调用位置。
# 判定是否是奇数
def isOdd(num):
if num % 2 == 0:
return False
return True
result = isOdd(10)
print(result)
如果 num 是偶数,则进入 if 之后,就会触发 return False,也就不会继续执行 return True。

c++、java里面,一次函数调用只能返回一个值,不能返回一个数组,只能返回一个数组指针。
或者一个vector对象。

- 一个函数是可以一次返回多个返回值的,使用 , 来分割多个返回值。
# 写一个函数, 返回平面上的一个点
# 横坐标, 纵坐标
def getPoint():
x = 10
y = 20
return x, y
a, b = getPoint()
- 如果只想关注其中的部分返回值,可以使用 _ 来忽略不想要的返回值。(占位)
def getPoint():
x = 10
y = 20
return x, y
_, b = getPoint()


1.5 变量作用域
观察以下代码
def getPoint():
x = 10
y = 20
return x, y
x, y = getPoint()
在这个代码中,函数内部存在 x,y,函数外部也有 x,y。
但是这两组 x,y 不是相同的变量,而只是恰好有一样的名字。
变量只能在所在的函数内部生效。
在函数 getPoint() 内部定义的 x, y 只是在函数内部生效,一旦出了函数的范围,这两个变量就不再生效了。
def getPoint():
x = 10
y = 20
return x, y
getPoint()
print(x, y)

在不同的作用域中,允许存在同名的变量。


虽然名字相同,实际上是不同的变量。
x = 20
def test():
x = 10
print(f'函数内部 x = {x}')
test()
print(f'函数外部 x = {x}')

函数外部想获取函数内部的变量值,需要通过返回值的方式,并且在外部用变量接收。
【注意】
- 在函数内部的变量,也称为 "局部变量"。
- 不在任何函数内部的变量,也称为 "全局变量"。
如果函数内部尝试访问的变量在局部不存在,就会尝试去全局作用域中查找。
x = 20
def test():
print(f'x = {x}')
test()

如果是想在函数内部,修改全局变量的值,需要使用 global 关键字声明。
x = 20
def test():
global x
x = 10
print(f'函数内部 x = {x}')
test()
print(f'函数外部 x = {x}')


因为python的(全局)变量赋值语句和(局部)变量定义语句完全一样,所以给全局变量赋值之前需要声明它是一个全局变量。
- if / while / for 等语句块不会影响到变量作用域。
换而言之,在 if / while / for 中定义的变量,在语句外面也可以正常使用。
for i in range(1, 10):
print(f'函数内部 i = {i}')
print(f'函数外部 i = {i}')


能影响变量作用域的,是函数里面或类里面定义的变量,只能在函数或类里面访问。




在局部域,读取全局变量可以直接读取。
在局部域,修改全局变量需要先声明。
1.6 函数执行过程
- 调用函数才会执行函数体代码,不调用则不会执行。
- 函数体执行结束(或者遇到 return 语句),则回到函数调用位置,继续往下执行。
def test():
print("执行函数内部代码")
print("执行函数内部代码")
print("执行函数内部代码")
print("1111")
test()
print("2222")
test()
print("3333")



这个过程还可以使用 PyCharm 自带的调试器来观察。







以下就是新界面了,之前都是2024.1的版本,下面是2026.1的版本。


打断点的行会变红,执行到那一行停下来,那一行会变蓝。









函数的调用栈:描述了当前的代码是怎么跳转过去的。
当前所在栈帧位置:code05.py的第178行。(test的函数体内部)
上一层栈帧的位置:code05.py的第182行。(test的调用语句)
继续往下调试,回到上一层栈帧。







1.7 链式调用
前面的代码很多都是写作
# 判定是否是奇数
def isOdd(num):
if num % 2 == 0:
return False
else:
return True
result = isOdd(10)
print(result)
实际上也可以简化写作
print(isOdd(10))
把一个函数的返回值,作为另一个函数的参数,这种操作称为 链式调用。

链式调用也可能不止一层


1.8 嵌套调用
函数内部还可以调用其他的函数,这个动作称为 "嵌套调用"。
def test():
print("执行函数内部代码")
print("执行函数内部代码")
print("执行函数内部代码")
test 函数内部调用了 print 函数,这里就属于嵌套调用。
一个函数里面可以嵌套调用任意多个函数。
函数嵌套的过程是非常灵活的。
def a():
print("函数 a")
def b():
print("函数 b")
a() # b调用a
def c():
print("函数 c")
b() # c调用b
def d():
print("函数 d")
c() # d调用c
d()

先执行本函数的代码,再嵌套调用。
如果把代码稍微调整,打印结果则可能发生很大变化。
先嵌套调用,再执行本函数的代码。
def a():
print("函数 a")
def b():
a()
print("函数 b")
def c():
b()
print("函数 c")
def d():
c()
print("函数 d")
d()

注意体会上述代码的执行顺序,可以通过画图的方式来理解。


- 函数之间的调用关系,在 Python 中会使用一个特定的数据结构来表示,称为函数调用栈。
- 每次函数调用,都会在调用栈里新增一个元素,称为 栈帧。

每个函数的局部变量,都包含在自己的栈帧中。
def a():
num1 = 10
print("函数 a")
def b():
num2 = 20
a()
print("函数 b")
def c():
num3 = 30
b()
print("函数 c")
def d():
num4 = 40
c()
print("函数 d")
d()

选择不同的栈帧,就可以看到各自栈帧中的局部变量。
【思考】上述代码,a, b, c, d 函数中的局部变量名各不相同,如果变量名是相同的,比如都是 num,那么这四个函数中的 num 是属于同一个变量,还是不同变量呢?


每个栈帧里面都有num,但是是代表了不同的内存空间。
上述代码中,每个栈帧里面的num值都不一样。
1.9 函数递归
递归:是 嵌套调用 中的一种特殊情况,即一个函数嵌套调用自己。

【代码示例1】递归计算 5! ,使用循环结构。
# 写一个函数, 来求 n 的阶乘(n 是正整数)
def factor(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
result = factor(5)
print(result)
【代码示例2】递归计算 5! ,使用递归结构。
# 写一个函数, 来求 n 的阶乘(n 是正整数)
# 递推公式:n! => n * (n - 1)!
# 初始条件:1! => 1
def factor(n):
if n == 1:
return 1
return n * factor(n - 1)
result = factor(5)
print(result)
上述代码中,就属于典型的递归操作,在 factor 函数内部,又调用了 factor 自身。
【注意】递归代码务必要保证
- 存在递归结束条件。比如 if n == 1 就是结束条件,当 n 为 1 的时候,递归就结束了。
- 每次递归的时候,要保证函数的实参是逐渐逼近结束条件的。

如果上述条件不能满足,就会出现 "无限递归",这是一种典型的代码错误。
def factor(n):
return n * factor(n - 1)
result = factor(5)
print(result)
![]()
如前面所描述,函数调用时会在函数调用栈中记录每一层函数调用的信息。
但是函数调用栈的空间不是无限大的,如果调用层数太多,就会超出栈的最大范围,导致出现问题。

1.9.1 递归的优点
- 递归类似于 "数学归纳法",明确初始条件,和递推公式,就可以解决一系列的问题。
- 递归代码往往代码量非常少。
1.9.2 递归的缺点
- 递归代码往往难以理解,很容易超出掌控范围。
- 递归代码容易出现栈溢出的情况。
- 递归代码往往可以转换成等价的循环代码,并且通常来说循环版本的代码执行效率要略高于递归版本。
【注意】实际开发的时候,使用递归要慎重!

例如:二叉树的相关问题本身就是通过递归的方式定义的,使用循环来写,代码会很复杂。
1.10 参数默认值
- Python 中的函数,可以给形参指定默认值。
- 带有默认值的参数,可以在调用的时候不传参。
【代码示例】计算两个数字的和。

在函数内部加上打印信息,方便我们进行调试——理解程序的运行过程。
但是,像这种调试信息,希望在正式发布的时候不要有,只是在调试阶段才有。
就可以通过一个(开关)参数去控制,本次函数调用是否要打印参数信息。
def add(x, y, debug):
if debug:
print(f'调试信息: x={x}, y={y}')
return x + y
print(add(10, 20, False))
print(add(10, 20, True))
引入了一个开关参数,确实可以控制打印信息的有无,但是每一层函数调用都需要多传一个参数,让代码变麻烦了,还让add函数的调用语句看起来有点别扭。
所以这里可以给add指定一个默认参数,在不传第三个参数的时候,直接使用默认参数。
def add(x, y, debug=False):
if debug:
print(f'调试信息: x={x}, y={y}')
return x + y
print(add(10, 20))
print(add(10, 20, True))
此处 debug=False 即为参数默认值,当我们不指定第三个参数的时候,默认 debug 的取值即为 False。
带有默认值的参数需要放到没有默认值的参数的后面。
def add(x, debug=False, y):
if debug:
print(f'调试信息: x={x}, y={y}')
return x + y
print(add(10, 20))

错误:非默认参数跟在默认参数后面。

让函数设计更灵活:有些函数需要给用户提供很多不同的功能,就需要用户提供很多的参数来控制,但是参数越多,调用者的使用成本越高,调用函数的人就需要仔细研究每个参数都是干嘛的。
更良好的版本是:在提供很多参数的同时,把一些参数设定上默认值,这些默认值是大多数函数调用中都会使用的参量值:
- 当调用者只是入门简单使用一下,默认值完全可以覆盖大多数使用场景,很多参数就不用去关注;
- 当调用者想更深入地控制函数的行为,就可以传入更多的参数去进行更精细的控制。
但是参数默认值的语法是存在争议的。但是在C++、Python等语言的标准库中是广泛使用的。
1.11 关键字参数
在调用函数的时候,需要给函数指定实参。
- 一般默认情况下,是按照形参的顺序,来依次传递实参的。
- 但是我们也可以通过 关键字参数 ,来调整这里的传参顺序,显式指定当前实参传递给哪个形参。
def test(x, y):
print(f'x = {x}')
print(f'y = {y}')
test(x=10, y=20)
test(y=100, x=200)

形如上述 test(x=10, y=20) 这样的操作,即为 关键字参数。

一般的编程语言中,形参的名字是没有用的,甚至在编译的过程中,把形参的名字直接丢了。
或者在编写代码的时候,就不需要形参名,只需要形参类型。
python就很好地把形参名利用起来了。

1.12 小结
函数是编程语言中的一个核心语法机制,Python 中的函数和大部分编程语言中的函数功能都是基本类似的。
我们当下最关键要理解的主要就是四个点:
- 函数的定义
- 函数的调用
- 函数的参数传递
- 函数的执行流程(特别是递归调用时,结合画图、调试器来理解)
我们在后续的编程中,会广泛的使用到函数。大家在练习的过程中再反复加深对于函数的理解。
完
1万+

被折叠的 条评论
为什么被折叠?



