Python学习笔记——4.函数
函数格式
Python 要求使用如下格式来定义函数。
1 | def 函数名([参数列表]): |
其中只有函数体是必要的(如果是空函数也需要使用pass
或...
占位),参数列表,文档字符串和return语句都可以省略。
函数说明文档的字符串使用连续三个单引号或双引号包裹,看起来像多行注释。
简单示例
空函数,没有任何效果
1 | def null(): |
HelloWorld 函数,打印字符串
1 | def hello_world(): |
包含输入输出的减法函数
1 | def subtract(x,y): |
函数参数
参数传递机制
函数的参数传递相当于 Python 变量赋值过程:逐个实参被赋值给对应的形参变量。在函数体内部对形参的修改是否会反馈到外部?这取决于参数的类型:
- 对于数字,字符串等不可变对象,修改不会影响到外部,因为修改相当于形参又指向了别的对象,外部的实参不变;
- 对于列表,字典等可变对象,如果修改了其中的元素,这个修改会影响到外部,因为实参形参指向的是同一个列表/字典对象。
例如 1
2
3
4
5
6
7
8
9
10def func(a, b):
a = 2
b[0] = 100
a = 1
b = [1, 2, 3, 4]
func(a, b)
print(a, b)
# 1 [100, 2, 3, 4]
Python的参数传递机制只是对内存中的同一个对象赋予了一个别名而已,相当于C++中的引用,而不是传递一个副本,因此不涉及深浅复制问题。
位置参数
函数传入的参数通常按照顺序,依次赋值给形参列表中的变量,称为位置参数,也就是默认情况下参数的传入形式。例如
1 | def subtract(x,y): |
关键字参数
函数传入的参数可以使用关键字形式,指定实参与形参的对应关系,例如
1 | def subtract(x,y): |
注意:
传入的关键字参数必须在所有位置参数之后,传入关键字参数之后只能继续传入关键字参数
1
subtract(x=2,3) # error
关键字参数不能和之前通过位置参数建立的对应关系冲突,否则因为参数被多次赋值而报错,例如
1
subtract(2,x=2)
建议不要混用位置参数和关键字参数,避免歧义
关键字参数很方便,可以任意指定顺序,容错性高。在 C++里面就算使用模板元编程,费尽九牛二虎之力,也达不到这么舒服的传参效果。
参数限制
可以在形参列表中使用特殊的标记来限制参数传递方式:
- 在
/
之前的参数,只允许以位置参数形式传入(要求版本为3.8+) - 在
*
之后的参数,只允许以关键字参数形式传入
至于 /
和 *
自身,并不代表某个参数。
1 | def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): |
例如
1 | def f1(a, b, /): |
例如
1 | def f2(*, a, b): |
参数默认值
函数的形参列表最后部分(不包括下文的不定参数)可以给参数赋予默认值,函数调用时如果提供的参数个数不足,有默认值的形参会自动获取默认值。例如
1 | def subtract(x,y=10): |
注意对于默认值强烈建议使用不可变的字面量(数字,字符串等),否则会产生 bug:在下面的例子中每次调用时默认值发生了改变!
1 | def f(a, L=[]): |
在各种包中的默认参数处理通常为:在函数定义时将默认值赋值为
None
,在函数体中加上判断来避免上述问题。
1 | def f(a, L=None): |
参数注解
可以用提示的形式,表明对函数参数的要求,这个要求并不是强制性的,只是一个建议,语法如下
1 | def f(a: str, b: str='egg'): |
这种注解同样可以用作函数返回值上,类似于 C++的尾置返回类型
1 | def add(x: int, y: int) -> int: |
在C++中也有如下的实现
1 | auto add(int x, int y) -> int { return x + y; } |
注意:对于C++这种尾置返回类型是强制性的语法,但是在Python中的注解类型只是提示! C++和 Python 作为两个极端化的语言,最近的发展简直就在双向奔赴。
参数解包
可以使用星号将列表/元组解包,传入函数中可以依次对应函数的位置参数,例如
1 | def test(a,b,c): |
可以使用双星号将字典解包,传入函数中可以依次对应函数的关键字参数,例如
1 | def test(a,b,c): |
在语法上还允许使用星号将字典解包,相对于把所有的键依次对应函数中的位置参数,但是这种用法很少见,例如
1 | def test(a,b,c): |
不定参数
不定位置参数
使用带星号的函数形参 *args
(习惯上使用
*args
,也可换成其它标识符)代表接收剩余的未知数量的位置参数,并且把它们打包成一个元组,顺序与传入顺序一致。
在函数体内部可以获取这个元组
args
,并且访问其中每一个,如果没有参数那么元组就是空的。
例如
1 | def test(*args): |
注意:
- 函数的形参列表中只能有至多一个带星号的形参,否则语法错误;
- 带有默认值的参数可以出现在
*args
之前或者之后。
如果 *args
和普通形参交错出现,那么出现在
*args
之后的参数仍然是必选的,并且必须在最后使用关键字参数去赋值,例如
1 | def test(u,*args,v): |
在 *args
之后出现的普通形参含有默认值也是可以的:要么使用默认值,要么使用关键字参数赋值,因为所有位置参数都会被前面的*args
捕获。
1 | def test(u,*args,v=2): |
不定关键字参数
使用带双星号的函数形参 **kwargs
(习惯上使用
**kwargs
,也可换成其它标识符)代表接收未知数量的关键字参数,并且把它们打包成一个词典。
在函数体内部可以获取这个词典
kwargs
,并且访问其中每一个,如果没有参数那么字典就是空的。
例如
1 | def test(**kwargs): |
注意:
- 函数的形参列表中只能有至多一个带双星号的形参,否则语法解析错误;
- 任何形参都不可以放在双星号参数之后,包括普通的形参(无论带不带默认值)或者带星号的形参,因此
**kwargs
一定出现在最后的最后; **kwargs
只能接收剩下的关键字参数,不能处理位置参数,会报错。
通用形式
在Python的函数定义中,有以下的通用形式可以捕获任意的位置参数和关键字参数
1 | def func(*args,**kwargs): |
这个函数可以接受任意形式的调用,但是可读性很差,我们无法获知它对于函数参数的任何需求,因此不建议使用。
lambda 表达式
Python 的 lambda 表达式比 C++要简单很多,支持的功能也更少,形式如下
1 | name = lambda [list] : expression |
其中形参列表是可选的,冒号后直接紧跟一个表达式,表达式的结果作为返回值,不需要写 return ,换言之,lambda表达式就是只允许一条执行语句的简单函数。
只允许一个语句导致的结果是:Python的lambda表达式功能非常弱,但这也是没办法的,因为引入更多的语句会带来更麻烦的缩进问题,谁让Python非要用缩进来划分代码块呢。
lambda函数的简单使用例如:
无参数时
1
2
3
4
5s1 = lambda : print("hello")
s1() # hello
s2 = lambda : 123
s2() # 123有参数时
1
2
3
4
5
6
7
8s1 = lambda x,y:x-y
s1(2,1) # 1
s1(y=1,x=2) # 1
s2 = lambda x,y=1:x-y
s2(1,2) # -1
s2(1) # 0
如上所示,lambda 表达式和普通的函数一样,也支持关键字参数和默认参数等。
lambda表达式还有如下特点:
- 将 lambda 表达式赋值给一个变量后,这个变量就可以当作函数来使用
- lambda 表达式可以自动捕获当前作用域可用的所有变量
lambda 表达式主要的使用场景:
作为一次性的匿名函数被传递给其它函数,在其它函数中调用
1
2
3
4
5
6
7
8
9
10# 列表过滤
lis = [1,2,3,4,5]
x = filter(lambda x: x > 3, lis)
print('>=3:', list(x))
# >=3: [4,5]
# 列表排序
lis=[('b',3),('a',2),('d',4),('c',1)]
sorted(lis,key=lambda x:x[0])
# [('a', 2), ('b', 3), ('c', 1), ('d', 4)]一次性定义并立刻执行,不需要赋值,直接在加括号并传入参数即可立即执行
1
s = (lambda x,y:x-y)(1,2) # => -1
返回值
函数通常需要使用return
语句来明确返回值,如果缺省return
语句,相当于返回值为None
,而不是返回最后一个语句的结果!
Python 支持一次性返回多个值,通常将多个返回值包装为一个元组(列表或字典也可以),接收时可以利用语法自动解包,语法比 C++的 auto 解包更好用。
1 | def fun(): |
值得注意的是第三个函数的返回值获取:
- 使用
x, = fun3()
相当于对返回值元组进一步解包,将元组的第一个元素赋值给x
;(此时如果元组多于一个元素会报错) - 使用
x = fun3()
则得到的x
是元组('a',)
。
补充
pass 占位
pass
占位语句,表示功能尚未实现,因为不允许定义一个没有函数体的函数,Python会报缩进错误,这时可以加上一个pass
语句
1 | def test(): |
或者可以使用省略号占位,效果一样
1 | def test(): |
函数递归
Python 的函数可以递归调用自身,不需要额外的任何处理。(也就只有 Fortran 这种古董,连递归调用都要特殊关键字)
1 | def factorial(n): |
函数嵌套
函数可以直接定义在全局空间中,也可以定义在函数内部,函数中的变量都是局部变量(包括函数参数),外层函数可以正常使用内层函数,例如
1 | def outer_function(x): |
内层函数可以访问外层函数的局部变量,例如下面的x
是外层函数的局部变量,外层函数返回了内层函数并赋值给closure
,内层函数被调用时仍然可以访问y
的值,这实际上是闭包的语法,见后文。
1
2
3
4
5
6
7
8
9def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure = outer_function(5)
print(closure(3))
# 8
函数定义是可执行的
与C++的函数定义不同,Python的函数定义语句实际上是可以执行的,函数也是一个对象,例如下面的选择分支会触发不同的函数定义
1
2
3
4
5
6
7
8
9a = 1
if a>0:
def func():
print("hi")
else:
def func():
print("hello")
func() # hi