函数的基本概念
约 6452 字大约 22 分钟
2025-04-04
初识函数
函数的作用是封装代码,大量减少重复代码,可重用性高。
我们之前写代码的方式可以说是过程式编程,有什么需求我们就写什么样的代码,一步一步写下去。之前写过的代码一旦运行过去之后就不会被再次运行到。
我们今天要学习的函数,其实也是一种新的编程思路,也就是函数式编程。
接下来,我们通过一个例子来初识过程式编程与函数式编程的区别。
现在有这样一个要求,不使用 len()
方法,如何能够得知一个字符串 s = 'alicebob'
的长度呢?根据我们已有的知识,可以使用 for 循环做到:
s = 'alicebob'
count = 0
for i in s:
count += 1
print(count)
这种方法对于我们来说已经轻而易举了。
如果此时,我们想接着求一个列表 s = [1, 2, 3, 4, 5]
的长度,即便跟前面的情况很类似,但我们不得不重新写一个代码:
s = [1, 2, 3, 4, 5]
count = 0
for i in s:
count += 1
print(count)
如果再需要求一个元组 s = (7, 8, 9, 10)
的长度:
s = (7, 8, 9, 10)
count = 0
for i in s:
count += 1
print(count)
我不难发现,我们虽然得到了我们想要的结果,但是循环的代码是完全重复了的。如果使用函数式编程,就可以减少这样的重复:
def my_len():
count = 0
for i in s:
count += 1
print(count)
s = 'alicebob'
my_len()
s = [1, 2, 3, 4, 5, 6, 7, 8]
my_len()
函数的定义
从上面的例子我们已经能看出来,函数的定义结构为:
def 函数名():
函数体
def
是一个关键字,声明要定义一个函数函数名
指代函数的名字,遵循变量命名的规则()
是固定结构,用来传参:
表示语句结束缩进
函数体
为函数的代码内容
函数的调用
根据前面所学到的东西,我们可以很容易地写出一个简单地输出用户输入内容的函数:
def func():
msg = input('>>>')
print(msg)
程序运行之后直接结束,并没有按照我们预想的出现让用户输入的情况。
这是因为当程序运行到 def 语句时,并不会立即执行函数中的内容,而是会开辟一块内存空间,将函数的内容存储到内存中。
只有当函数被调用时,函数中的内容才会被执行。函数名()
这种形式就是在调用函数。
回到上面的例子,如果我们在最后增加对函数的调用,就可以实现我们想要的效果:
func()
这种 函数名()
的结构有两层含义:
- 调用函数
- 接收返回值
函数的返回
我们学过的很多方法,比如 print()
、input()
等,本质上就是函数。
如果我们将 print()
和 input()
两个函数本身作为参数打印出来,会有什么样的效果呢?
print(print('a'))
print(input('>>>'))
输出的结果为:
a
None
>>>123
123
print
和 input
运行后打印出来的内容有很大差别,这是因为两个函数的返回值不同。
函数中使用 return
语句返回内容。return
语句的基本用法为:
def func():
a = 10
return a
a = func()
print(a)
return
后面可以跟 Python 中任意数据类型。
return
还可以一次性返回多个数据,返回的数据以元组的形式接收:
def func():
a = 10
b = 20
return a, b
a = func()
print(a) # (10, 20)
因为返回的数据是元组,我们可以通过解构的方式获取每一个值:
def func():
a = 10
b = 20
return a, b
a, b = func() # 拆包,解包,平行赋值
print(a, b) # 10 20
return
是函数终止的标识,return
能够终止函数,它后面的代码不被执行:
def func():
for i in range(10):
return i
print(1)
a = func()
print(a)
# 输出的结果为: 0
return
会将返回值返回给调用者。
函数的返回小结:
- 函数体中不写
return
默认返回None
;写了return
不写值也返回None
return
能够返回任意数据类型(Python 中所有对象)return
能够返回多个数据类型,以元组的形式接收return
能够终止函数,其下方的代码不会被执行return
将返回值返回给调用者
函数的参数
位置参数
我们现在写好下面这样的函数并调用:
def yue():
print('掏出手机')
print('打开微信')
print('找到附近的人')
print('聊一聊')
print('约一约')
yue()
这个函数当然会很正常地运行。而且不管我们在什么位置,只要调用这个函数,就会打印这五行内容。
但是如果有一天,我们需要换一个 app,比如不用微信,改用陌陌,该如何办呢?如果在函数上改会很麻烦且不灵活。
我们可以通过参数的方式进行自定义函数输出的内容:
def yue(app):
print('掏出手机')
print('打开', app)
print('找到附近的人')
print('聊一聊')
print('约一约')
yue('微信')
yue('陌陌')
yue('谈谈')
通过这种传入参数的方式,我们如果需要函数输出不同的内容就不需要去改变函数本身了。
我们可以一次性传入多个参数:
def yue(app, girl, age, addr): # 形参
print('掏出手机')
print(f'打开{app}')
print(f'找一位{girl},要求年龄:{age},地区:{addr}的人')
print('聊一聊')
print('约一约')
yue('微信', '女孩', 18, '乌克兰') # 实参 按照位置传参
在定义函数时使用的参数被称作形式参数,也称形参。在调用函数是使用的参数是实际参数,也称实参。
上面这种函数的传参方式被称作按位置传参,实参和形参需要一一对应,否则会出现参数混乱的问题。
默认参数
对于一些特殊的情况,比如某个男生很多,女生很少的班级,录入班级信息时,会经常重复输入性别为男。这时,我们可以通过设置默认参数的方法,将性别默认设定为男。当不输入参数时,函数会自动使用男作为参数。这样会节省很多时间:
def userinfo(name, age, hobby, sex='男'):
print(f'姓名:{name} 年龄:{age} 性别:{sex} 爱好:{hobby}')
userinfo('张三', 23, "开车")
userinfo('李四', 60, "玩卡丁车")
userinfo('王二麻子', 16, "听音乐","女")
userinfo('小桃红', 16, "玩游戏","女")
userinfo('李华', 16, "看书","女")
userinfo('小明', 18, "知难而上")
需要注意的是,默认参数需要放在位置参数的后面,否则会报错。
参数的优先级为:位置参数 – 默认参数
混合参数
位置参数和默认参数可以混合在一起使用。需要注意的是,默认参数必须都要放在位置参数的后面:
def func(a, b, c=1, d=2):
print(a, b, c, d)
func(1, 3, 4, 5) # 实参 位置参数传递
func(a=1, b=2, c=3, d=4) # 关键字传参 (指名道姓传参)
func(1, 2, d=3) # 关键字传参 (指名道姓传参)
这里有引入了一个传参的概念。当调用函数时,我们需要把参数传递给函数,这个过程被称作传参。直接把参数按照形参的位置一一对应传入的方式被称作位置传参。也可以指定形参的名字来传参,这种方法被称作关键字传参。
当存在多个默认参数,但只想修改其中少数默认参数时,就需要使用关键字传参告诉函数需要改变的是哪一个参数。
动态参数
我们已经学到了函数的两种参数:位置参数和默认参数。但是对这两种参数而言,我们传入函数的数据不能多于参数的总个数。但是有些时候,参数的数量是不能很好控制的,这时候,我们就需要应用到动态参数。
动态参数的作用主要有两个:
- 能够接收不固定长度的参数
- 位置参数过多时,可以使用动态参数
动态位置参数
我们可以通过下面的方法定义一个动态的位置参数:
def func(*c):
print(c)
func(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
输出的结果为:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
这个方法得到的数据类型是一个元组。
动态位置参数以 *形参
的形式表示。相信大家已经发现,这种方式跟切片时十分相似。
事实上,同切片时将多余数据打包的原理一样,在形参位置上的 *
就是聚合。同样,我们可以在函数体中使用 *
将聚合后得到的元组打散:
def func(*c): # 形参位置上的 * 是聚合
print(*c) # 函数体中的 * 就是打散
func(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
输出的结果为:
1 2 3 4 5 6 7 8 9 0
因为动态位置参数会将多余的位置参数全都打包起来,所以一个函数中只需要一个动态位置参数就足够了。一般情况下,动态位置参数会被命名为 *args
。当然也可以自定义参数名,但是不建议修改,因为这时程序员约定俗成的共识。
如果动态位置参数后面还有位置参数,那么后面的位置参数将永远无法获取到值,程序会直接报错:
def eat(*args, a, b):
print(a, b, args)
eat('面条', '米饭', '馒头', '包子', '煎饼')
程序报错,错误内容为:
Traceback (most recent call last):
File "<python-input-147>", line 3, in <module>
eat('面条', '米饭', '馒头', '包子', '煎饼')
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: eat() missing 2 required keyword-only arguments: 'a' and 'b'
因为参数 a
和 b
永远也无法得到值,尽管我们输入很多内容,依旧无济于事。
一个比较标准的参数设置方法是这样的:
def eat(a, b, *args): # 位置参数 动态位置参数
print(a, b, args)
eat('面条', '米饭', '馒头', '包子', '煎饼')
输出的结果为:
面条 米饭 ('馒头', '包子', '煎饼')
位置参数一一对应获得参数,动态位置参数将剩余的参数打包成一个元组。
动态关键字参数(动态默认参数)
当我们在实参中传入的关键字参数在形参中存在时,会成功传递进入。可是如果形参中没有我们传入的实参,就会报错。
动态关键字参数就是用来接收这些没有被定义过的关键字参数。
我们可以在形参中使用 **参数名
的形式定义一个动态关键字参数。同样地,参数名可以随意选取,但是程序员间约定俗成的动态关键字参数名为 **kwargs
:
def func(a, b, *args, **kwargs):
print(a, b, args, kwargs)
func(1, 2, 3, 4, 5, 6, 76, 87, 8, c=100)
输出的结果为:
1 2 (3, 4, 5, 6, 76, 87, 8) {'c': 100}
前两个位置参数分别传给了 a
和 b
,剩余的位置参数打包成元组传给了 args
,而关键字参数则以字典的形式传给了 kwargs
。
当我们需要设置多种参数时,推荐使用的顺序是:位置参数,动态位置参数,默认参数,动态关键字参数:
def func(a, b, *args, m=8, **kwargs):
# 位置参数,动态位置,默认参数,动态关键字参数
print(a, b, m, args, kwargs)
func(1, 2, 4, 5, m=10, c=11, d=12)
输出的结果为:
1 2 10 (4, 5) {'c': 11, 'd': 12}
函数参数总结
在定义函数的阶段使用的参数是形参:
- 可以单独使用位置参数,也可以单独使用默认参数,也可以混合使用
- 位置参数:必须一一对应
- 默认参数:可以不传参,可以传参,传参就是把默认的值给覆盖
- 混合使用:位置参数,默认参数
在调用函数的阶段使用的是实参:
- 可以单独使用位置参数,也可以单独使用关键字参数,也可以混合使用
- 位置传参:必须一一对应
- 关键字传参:指名道姓的方式进行传参
- 混合使用:位置参数,默认参数
将实参传递给形参的过程就是传参
优先级:位置参数 > 动态位置参数(可变位置参数)> 默认参数 > 动态关键字参数(可变关键字参数)
*args
和**kwargs
是程序员之间约定俗成的命名法(可以更换但是不建议更换)*args
获取的是一个元组**kwargs
获取的是一个字典*args
只接收多余的位置参数**kwargs
只接收多余的关键字参数
函数参数补充
万能传参
因为动态位置参数和动态关键字参数可以接受所有的位置参数和关键字参数,所以在设置形参时,我们甚至可以只设置 *args
和 **kwargs
两个形参,这种传参方法被称作万能传参:
def func(*args, **kwargs): # 万能传参
print(args, kwargs)
func(12, 2, 121, 12, 321, 3, a=1, b=2)
输出的结果为:
(12, 2, 121, 12, 321, 3) {'a': 1, 'b': 2}
聚合与打散
在前面的动态位置参数部分已经讨论过,形参中的 *args
是将多于变量聚合为元组,函数体中的 *args
是将元组打散。其实对于 **kwargs
来说也很类似:形参中的 **kwargs
是将 key=1, key2=2
这样类型的语句转化为字典,而函数体中 *kwargs
是获取字典中所有的键,**kwargs
是将字典打散为 key=1, key2=2
的语句。
除了函数中,我们可以在 Python 的很多地方灵活运用打散和聚合的操作:
lst = [1, 2, 3, 4, 6, 7]
def func(*args): # 聚合
print(*args) # 打散
func(*lst) # 打散 func(1, 2, 3, 4, 6, 7)
dic = {"key": 1, "key2": 2}
def func(**kwargs): # 聚合
print(kwargs)
func(**dic) # 打散 func(key=1, key2=2)
输出的结果为:
1 2 3 4 6 7
{'key': 1, 'key2': 2}
函数的注释
在协同操作的过程中,大家或许会查看和使用彼此的代码。但是如果没有任何提示性的内容的话,从头开始看起会有很大的理解困难。如果我们将函数的一些功能、参数要求等信息在函数中写出来,别人阅读时就会节省很多时间,也更容易理解调用和修改我们的函数。
标准的函数注释应该是这样的:
def add(a, b):
"""
数字的加法运算
:param a: int
:param b: int
:return: int
"""
return a + b
print(add(1, 2))
函数中使用三对 "
,就是代表注释。也可以使用 '
表示,但是不推荐。
另外一种比较流行的注释方法是在形参后加入 :数据类型
,例如:
def add(a:int, b:int): # 提示,没有做到约束
"""
加法
:param a:
:param b:
:return:
"""
return a + b
add(1, 2)
add("1", "2")
需要注意的是,参数的作用只是起到提示的作用,并不会进行判断,约束我们传入变量的数据类型。
我们可以通过 函数名.__doc__
的方法查看函数的注释;通过 函数名.__name__
的方法查看函数的名字
函数的命名空间
函数的命名空间一共有三种:
- 内置空间,用来存放 Python 自带的一些函数,Python 程序运行时会首先加载
- 全局空间,当前 py 文件顶格编写的代码开辟的空间
- 局部空间,函数开辟的空间
程序的加载顺序是:内置空间 > 全局空间 > 局部空间
程序的取值顺序是:局部空间 > 全局空间 > 内置空间
程序取值顺序示例:
a = 10
def func():
a = 5
print(a)
func()
输出的结果为: 5
变量取值时会优先查看局部空间,找到变量 a,值为 5,打印了出来。
函数的作用域有两个:
- 全局作用域:内置空间 + 全局空间,使用
globals()
方法查看全局作用域 - 局部作用域:局部空间,使用
locals()
方法查看当前作用域(全局和局部作用域都可以查看,建议用此方法查看局部作用域)
a = 10
def func():
b = 5
print(globals())
print(locals())
func()
print(globals())
print(locals())
函数的嵌套
函数嵌套概述
函数的嵌套有两种方式:
- 交叉嵌套
- 回环嵌套
交叉嵌套
交叉嵌套的方式是在本函数中调用同一级或上一级函数的嵌套方法:
def func(foo):
print(1)
foo()
print(3)
def a():
print(1)
b = func(a)
print(b)
输出的结果为:
1
1
3
None
首先,程序会将 Python 文件中顶格的代码运行。函数 func
和 a
都是先开辟内存空间存储起来,但不会被执行。当程序走到赋值操作时,会先执行等号右边的代码。函数 func
被调用,函数 a
作为参数被传到 func
中。func
函数被执行,顺序也是从上往下,先是把 1
打印出来,然后调用参数 foo
。需要注意的是,foo
是形参,实参是 a
。调用 foo
在此时的意思是调用函数 a
。函数 a
被调用,又打印出一个 1
来。函数 a
运行完毕,返回至函数 func
,继续执行下面的代码,打印出 3
来。最后,函数默认返回 None
,赋值给 b
。程序运行结束。
再看下面的代码:
def func():
print(1)
print("我太难了")
print(2)
def foo(b):
print(3)
ss = b()
print(ss)
print(4)
def f(a,b):
a(b)
f(foo,func)
输出的结果为:
3
1
我太难了
2
None
4
跟上面一样,先将函数全都加载到新开辟的内存空间中,但不执行。到最后 f 函数被调用,foo
和 func
两个函数作为参数被传到函数 f
中。在函数 f
中,foo
函数被调用,参数为 func
函数。进入到 foo
函数,先打印 3
。到赋值语句,先执行等号右边的代码,函数 func
被调用。在函数 func
中,打印三个内容 1
、我太难了
和 2
。函数默认返回值为 None
,被赋值给 ss
。打印 ss
就是打印 None
。最后打印 4
,然后返回到函数 f
,再返回到全局空间。执行结束。
### 回环函数
回环函数就是在函数中调用下级函数的嵌套方法:
def func(a,b):
def foo(b,a):
print(b,a)
return foo(a,b) #先执行函数调用
a = func(4,7)
print(a)
输出的结果为:
4 7
None
函数依然先存储在新开辟的空间中不会被调用。运行到赋值语句时,还是先执行等号右边的代码,将两个数字传到函数 func
中。在函数内部,依然是先开辟空间把函数 foo
放进去。运行到 return
不会马上终止函数,而是先运行 return
后面的代码。foo
函数被调用,传进去的值是 4
和 7
,然后打印出来。需要注意的是,函数 foo
的形参与函数 func
的形参是相同的,不要给搞混了。日常写代码时不建议这样使用。打印出 4
和 7
之后,运行到函数最后一行,函数默认返回 None
。然后再赋值给 a
,打印出来。
##
global
和 nonlocal
global
我们来看下面这段代码:
b = 100
def func():
b = b + 1
return b
print(func())
这段代码看上去中规中矩,似乎没有什么问题,但是程序运行后确报错。
这是因为在 Python 中,不允许直接在局部空间修改全局变量。b = b + 1
是一个冲突的语句:等式右边的 b
是要调用一个全局变量,而等号右边却是要定义一个局部变量。
如果将 b
视作一个全局变量依然不合适。在函数中修改全局变量会对其他调用相同变量的函数造成影响,除非万不得已或者十分确定的情况下,不建议在函数中修改全局变量。
当我们确定需要在函数中修改全局变量时,可以通过 global
方法来实现:
b = 100
def func():
global b
b = b + 1
return b
print(func())
输出的结果为: 101
如果 global
声明的变量在全局空间中不存在,将会在全局空间中新建一个变量:
def func():
global a
a = 10
a = a + 12
print(a)
func()
print(a)
输出的结果为:
22
22
nonlocal
对于回环嵌套的函数来说,也会有类似的问题。当尝试使用内层函数修改外层函数的变量时会报错:
a = 15
def func():
a = 10
def foo():
a = a + 1
foo()
print(a)
func()
print(a)
类似地,也不建议在内层函数中修改外层函数的变量。如果一定要修改的话,可以使用 nonlocal
方法:
a = 15
def func():
a = 10
def foo():
nonlocal a
a = a + 1
foo()
print(a)
func()
print(a)
输出的结果为:
11
15
nonlocal
方法只修改离它最近的一层函数的变量,如果这一层没有就往上一层查找,只能在局部查找。另外,外层函数不能调用内层函数的变量,即便用 nonlocal
方法也不行。如果外层所有函数中都没有声明的变量,即便全局空间中有也不行,而且 nonlocal
不能创建变量。如果找不到,就会报错:
a = 15
def func():
def foo():
nonlocal a
a = a + 1
foo()
func()
print(a)
其实想来这个设定也是合理的:如果外面套了很多层函数,这个变量该在哪一层创建呢?
global
和 nonlocal
总结
global
只修改全局空间中存在的变量
- 在局部空间中可以使用全局中的变量,但是不能修改。如果要强制修改,需要使用
global
声明 - 当变量在全局存在时,
global
就是声明我要修改全局的变量 - 当变量在全局中不存在时,
global
则是声明要在全局创建一个变量
nonlocal
只修改局部空间中的变量,最多只能到达最外层函数
- 在内层函数中可以使用外层函数中的变量,但是不能修改。如果要强制修改,需要使用
nonlocal
声明 - 只修改离
nonlocal
最近的一层,如果这一层没有就往上一层查找,不能找到全局中 nonlocal
不能创建变量,如果其声明的变量在外层函数中找不到,即便全局空间中有,也会报错
对函数的传参有一点补充,传参的时候相当于在当前函数体中进行了赋值操作:
def func(a):
# 相当于在 func 函数体中写了这么一个 a = 100 操作
print(locals())
func(100)
最后来一道思考题,请确定下列函数输出的结果:
a = 10
def func():
a = 5
def foo():
a = 3
def f():
nonlocal a
a = a + 1
def aa():
a = 1
def b():
global a
a = a + 1
print(a)
b()
print(a)
aa()
print(a)
f()
print(a)
foo()
print(a)
func()
print(a)
第一类对象
函数名的第一类对象只是一种称呼,是相对于第二类对象而言的。Python 的函数名是第一类对象
函数名的第一类对象主要体系在四个方面:
- 函数名可以当作值赋值给一个变量
- 函数名可以当做另一个函数的参数来使用
- 函数名可以当做另一个函数的返回值
- 函数名可以当作元素放在容器中
示例一:
def func():
print(1)
a = func
print(func) # <function func at 0x00000197E2D41EA0>
print(a) # <function func at 0x00000197E2D41EA0>
a() # 1
func
和a
同样都是指向函数的内存地址,当我们调用a
时,得到了同调用func
相同的结果。
示例二:
def func():
print(1)
def foo(a):
print(a)
foo(func) # <function func at 0x0000021FD82C1EA0>
示例三:
def func():
return 1
def foo(a):
return a
cc = foo(func)
print(cc) # <function func at 0x000002261A231EA0>
示例四:
有这样一个需求:用户选择需要相应的序号进行选择接下来的操作,比如选择 1 会调用注册的函数,选择 2 会调用登陆的函数……从前我们会通过流程控制语句的方式实现:
def login():
print('登录')
def register():
print('注册')
def shopping():
print('逛')
def add_shopping_car():
print('加')
def buy_goods():
print('买')
msg ="""
1.注册
2.登录
3.逛
4.加
5.买
请输入您要选择的序号:
"""
while True:
choose = input(msg)
if choose.isdecimal():
if choose == '1':
register()
elif choose == '2':
login()
elif choose == '3':
shopping()
elif choose == '4':
add_shopping_car()
elif choose == '5':
buy_goods()
else:
print('滚')
通过将函数装到字典里,我们可以大量减少重复的代码:
def login():
print('登录')
def register():
print('注册')
def shopping():
print('逛')
def add_shopping_car():
print('加')
def buy_goods():
print('买')
msg ="""
1.注册
2.登录
3.逛
4.加
5.买
请输入您要选择的序号:
"""
func_dic = {
'1':register,
'2':login,
'3':shopping,
'4':add_shopping_car,
'5':buy_goods,
}
while True:
choose = input(msg) # "5"
if choose in func_dic:
func_dic[choose]()
else:
print('滚')
这种使用字典调用函数的方式是一种很重要的编程思想,以后将经常用到
版权所有
版权归属:Shuo Liu