函数格式

Python 要求使用如下格式来定义函数。

1
2
3
4
def 函数名([参数列表]):
["函数文档字符串"]
函数体
[return语句]

其中只有函数体是必要的(如果是空函数也需要使用pass...占位),参数列表,文档字符串和return语句都可以省略。 函数说明文档的字符串使用连续三个单引号或双引号包裹,看起来像多行注释。

简单示例

空函数,没有任何效果

1
2
def null():
pass

HelloWorld 函数,打印字符串

1
2
3
4
5
def hello_world():
"""
hello,world
"""
print('Hello, World!')

包含输入输出的减法函数

1
2
3
4
def subtract(x,y):
return x-y

subtract(3,2) # 1

函数参数

参数传递机制

函数的参数传递相当于 Python 变量赋值过程:逐个实参被赋值给对应的形参变量。在函数体内部对形参的修改是否会反馈到外部?这取决于参数的类型:

  • 对于数字,字符串等不可变对象,修改不会影响到外部,因为修改相当于形参又指向了别的对象,外部的实参不变;
  • 对于列表,字典等可变对象,如果修改了其中的元素,这个修改会影响到外部,因为实参形参指向的是同一个列表/字典对象。

例如

1
2
3
4
5
6
7
8
9
10
def 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
2
3
4
def subtract(x,y):
return x-y

subtract(2,1) # 1

关键字参数

函数传入的参数可以使用关键字形式,指定实参与形参的对应关系,例如

1
2
3
4
def subtract(x,y):
return x-y

subtract(y=2,x=3) # 1

注意:

  • 传入的关键字参数必须在所有位置参数之后,传入关键字参数之后只能继续传入关键字参数

    1
    subtract(x=2,3) # error

  • 关键字参数不能和之前通过位置参数建立的对应关系冲突,否则因为参数被多次赋值而报错,例如

    1
    subtract(2,x=2)

  • 建议不要混用位置参数和关键字参数,避免歧义

关键字参数很方便,可以任意指定顺序,容错性高。在 C++里面就算使用模板元编程,费尽九牛二虎之力,也达不到这么舒服的传参效果。

参数限制

可以在形参列表中使用特殊的标记来限制参数传递方式:

  • / 之前的参数,只允许以位置参数形式传入(要求版本为3.8+)
  • * 之后的参数,只允许以关键字参数形式传入

至于 /* 自身,并不代表某个参数。

1
2
3
4
5
6
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only

例如

1
2
3
4
5
6
7
8
def f1(a, b, /):
return a + b


f1(1,2) # 正确

f1(a=1,2) # 语法错误
f1(1,b=2) # 语法错误

例如

1
2
3
4
5
6
7
8
9
def f2(*, a, b):
return a + b


f2(a=1, b=2) # 正确

f2(1,2) # 语法错误
f2(a=1,2) # 语法错误
f2(1,b=2) # 语法错误

参数默认值

函数的形参列表最后部分(不包括下文的不定参数)可以给参数赋予默认值,函数调用时如果提供的参数个数不足,有默认值的形参会自动获取默认值。例如

1
2
3
4
5
def subtract(x,y=10):
return x-y

subtract(3) # -7
subtract(2,1) # 1

注意对于默认值强烈建议使用不可变的字面量(数字,字符串等),否则会产生 bug:在下面的例子中每次调用时默认值发生了改变!

1
2
3
4
5
6
7
8
9
10
11
def f(a, L=[]):
L.append(a)
return L

print(f(1))
print(f(2))
print(f(3))

# [1]
# [1, 2]
# [1, 2, 3]

在各种包中的默认参数处理通常为:在函数定义时将默认值赋值为 None,在函数体中加上判断来避免上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L

print(f(1))
print(f(2))
print(f(3))

# [1]
# [2]
# [3]

参数注解

可以用提示的形式,表明对函数参数的要求,这个要求并不是强制性的,只是一个建议,语法如下

1
2
3
4
5
def f(a: str, b: str='egg'):
print(a,b)

f('one') # one egg
f('two','eggs') # two eggs

这种注解同样可以用作函数返回值上,类似于 C++的尾置返回类型

1
2
def add(x: int, y: int) -> int:
return x+y

在C++中也有如下的实现

1
auto add(int x, int y) -> int { return x + y; }

注意:对于C++这种尾置返回类型是强制性的语法,但是在Python中的注解类型只是提示! C++和 Python 作为两个极端化的语言,最近的发展简直就在双向奔赴。

参数解包

可以使用星号将列表/元组解包,传入函数中可以依次对应函数的位置参数,例如

1
2
3
4
5
6
7
def test(a,b,c):
print(a,b,c)

lis = [1,2,3]
test(*lis)
# 等价于 test(1,2,3)
# 1 2 3

可以使用双星号将字典解包,传入函数中可以依次对应函数的关键字参数,例如

1
2
3
4
5
6
7
def test(a,b,c):
print(a,b,c)

dic = {'b':1,'c':2,'a':3}
test(**dic)
# 等价于 test(b=1,c=2,a=3)
# 3 1 2

在语法上还允许使用星号将字典解包,相对于把所有的键依次对应函数中的位置参数,但是这种用法很少见,例如

1
2
3
4
5
6
7
def test(a,b,c):
print(a,b,c)

dic = {'b':1,'c':2,'a':3}
test(*dic)
# 等价于 test('b','c','a')
# b c a

不定参数

不定位置参数

使用带星号的函数形参 *args(习惯上使用 *args,也可换成其它标识符)代表接收剩余的未知数量的位置参数,并且把它们打包成一个元组,顺序与传入顺序一致。 在函数体内部可以获取这个元组 args,并且访问其中每一个,如果没有参数那么元组就是空的。

例如

1
2
3
4
5
6
7
8
def test(*args):
print("args:",args)

test(1, 'a', 2, 'b', 3, 'c')
# args: (1, 'a', 2, 'b', 3, 'c')

test()
# args: ()

注意:

  • 函数的形参列表中只能有至多一个带星号的形参,否则语法错误;
  • 带有默认值的参数可以出现在 *args 之前或者之后。

如果 *args 和普通形参交错出现,那么出现在 *args 之后的参数仍然是必选的,并且必须在最后使用关键字参数去赋值,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def test(u,*args,v):
print("u:",u)
print("args:",args)
print("v:",v)

test(2,v=2)
# u: 2
# args: ()
# v: 2

test(1,2,3,v=4)
# u: 1
# args: (2, 3)
# v: 4

*args 之后出现的普通形参含有默认值也是可以的:要么使用默认值,要么使用关键字参数赋值,因为所有位置参数都会被前面的*args捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def test(u,*args,v=2):
print("u:",u)
print("args:",args)
print("v:",v)

test(2)
# u: 2
# args: ()
# v: 2

test(1,2,3)
# u: 1
# args: (2, 3)
# v: 2

不定关键字参数

使用带双星号的函数形参 **kwargs(习惯上使用 **kwargs,也可换成其它标识符)代表接收未知数量的关键字参数,并且把它们打包成一个词典。 在函数体内部可以获取这个词典 kwargs,并且访问其中每一个,如果没有参数那么字典就是空的。

例如

1
2
3
4
5
6
7
8
def test(**kwargs):
print("kwargs:",kwargs)

test(a=1,b='b')
# kwargs: {'a': 1, 'b': 'b'}

test()
# kwargs: {}

注意:

  • 函数的形参列表中只能有至多一个带双星号的形参,否则语法解析错误;
  • 任何形参都不可以放在双星号参数之后,包括普通的形参(无论带不带默认值)或者带星号的形参,因此 **kwargs 一定出现在最后的最后;
  • **kwargs 只能接收剩下的关键字参数,不能处理位置参数,会报错。

通用形式

在Python的函数定义中,有以下的通用形式可以捕获任意的位置参数和关键字参数

1
2
def func(*args,**kwargs):
...

这个函数可以接受任意形式的调用,但是可读性很差,我们无法获知它对于函数参数的任何需求,因此不建议使用。

lambda 表达式

Python 的 lambda 表达式比 C++要简单很多,支持的功能也更少,形式如下

1
name = lambda [list] : expression

其中形参列表是可选的,冒号后直接紧跟一个表达式,表达式的结果作为返回值,不需要写 return ,换言之,lambda表达式就是只允许一条执行语句的简单函数。

只允许一个语句导致的结果是:Python的lambda表达式功能非常弱,但这也是没办法的,因为引入更多的语句会带来更麻烦的缩进问题,谁让Python非要用缩进来划分代码块呢。

lambda函数的简单使用例如:

  • 无参数时

    1
    2
    3
    4
    5
    s1 = lambda : print("hello")
    s1() # hello

    s2 = lambda : 123
    s2() # 123

  • 有参数时

    1
    2
    3
    4
    5
    6
    7
    8
    s1 = 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def fun():
return ('a',1,3.14) # 自动包装为一个元组

def fun2():
return 'a','b' # 返回的元组括号可以省略

def fun3():
return ('a',) # 返回只有一个元素的元组

a,b,c = fun()
'''
a = 'a'
b = 1
c = 3.14
'''

a,b = fun2()
'''
a = 'a'
b = 'b'
'''

x, = fun3()
'''
x = 'a'
'''

值得注意的是第三个函数的返回值获取:

  • 使用x, = fun3()相当于对返回值元组进一步解包,将元组的第一个元素赋值给x;(此时如果元组多于一个元素会报错)
  • 使用x = fun3()则得到的x是元组('a',)

补充

pass 占位

pass 占位语句,表示功能尚未实现,因为不允许定义一个没有函数体的函数,Python会报缩进错误,这时可以加上一个pass语句

1
2
def test():
pass

或者可以使用省略号占位,效果一样

1
2
def test():
...

函数递归

Python 的函数可以递归调用自身,不需要额外的任何处理。(也就只有 Fortran 这种古董,连递归调用都要特殊关键字)

1
2
3
4
5
6
7
8
def factorial(n):
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)

print("5!=", factorial(5))
# 5!=120

函数嵌套

函数可以直接定义在全局空间中,也可以定义在函数内部,函数中的变量都是局部变量(包括函数参数),外层函数可以正常使用内层函数,例如

1
2
3
4
5
6
7
8
9
10
11
def outer_function(x):
def inner_function(y):
return y * 2

result = inner_function(x)
return result


output = outer_function(5)
print(output)
# 10

内层函数可以访问外层函数的局部变量,例如下面的x是外层函数的局部变量,外层函数返回了内层函数并赋值给closure,内层函数被调用时仍然可以访问y的值,这实际上是闭包的语法,见后文。

1
2
3
4
5
6
7
8
9
def 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
9
a = 1
if a>0:
def func():
print("hi")
else:
def func():
print("hello")

func() # hi