在 Python 编程中,函数的定义与参数使用是非常重要的部分。下面我们就来详细了解一下 Python 函数参数的各种特性与使用方法。
一、函数接口与参数基础概念
当我们定义函数时,只要确定好参数的名字以及它们所处的位置,函数的接口定义就算是完成了。对于调用函数的人来说,重点在于知晓怎样传递正确的参数,还有清楚函数最终会返回什么样的值就行,函数内部那些复杂的逻辑都被封装起来了,无需深入了解。
Python 函数定义起来十分简便,同时又具备很高的灵活性。除了常规的必选参数之外,还支持默认参数、可变参数以及关键字参数等多种类型,通过运用这些不同类型的参数,定义出来的函数接口不仅能够轻松应对复杂多样的参数输入情况,还能极大地简化调用者编写代码的工作量。
二、各类函数参数详解
(一)位置参数
位置参数是函数参数中最基础的一种形式。例如,我们想要定义一个计算 x2 的函数,可以这样写:
def power(x):
return x * x
在这个 power(x) 函数里,参数 x 就是位置参数。调用这个函数时,必须严格按照函数定义传入有且仅有一个参数 x,像下面这样:
print(power(5))
print(power(15))
如果我们想要计算 x3 或者更高次方呢?要是每次都去定义一个新的函数显然不太现实。这时我们可以把函数修改为 power(x, n),用来计算 x?,代码如下:
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
对于修改后的 power(x, n) 函数,它有两个位置参数 x 和 n。调用函数时,传入的两个值会按照先后顺序依次赋给对应的参数 x 和 n,示例如下:
print(power(5, 2))
print(power(5, 3))
(二)默认参数
不过,新的 power(x, n) 函数定义虽然功能上没问题,但会导致之前只传一个参数的调用代码失效,因为增加了一个参数后,旧的调用方式就缺少了一个必要的位置参数 n,Python 会明确报错提示缺少参数。
print(power(5))
这时候,默认参数就能发挥作用了。鉴于我们常常需要计算 x2,那完全可以给第二个参数 n 设置默认值为 2,像这样:
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s
如此一来,当我们调用 power(5) 时,就相当于调用 power(5, 2),结果是一样的:
print(power(5))
print(power(5, 2))
而对于 n > 2 的情况,就需要明确传入 n 的值了,比如 power(5, 3)。
设置默认参数时,有几点需要特别留意:
- 参数顺序:必选参数要放在前面,默认参数放在后面,不然 Python 解释器会报错。这是因为 Python 按照从左到右的顺序解析参数,若默认参数在前,就容易出现混淆,无法准确判断参数的赋值情况。
- 参数选择:当函数有多个参数时,建议把变化大的参数放在前面,变化小的参数放在后面,把那些变化小的参数设置为默认参数。
使用默认参数最大的好处就是能够降低函数调用的难度。例如,我们写一个一年级小学生注册的函数,一开始只需要传入 name 和 gender 两个参数:
def enroll(name, gender):
print('name:', name)
print('gender:', gender)
调用方式如下:
enroll('Sarah', 'F')
但如果后续还想传入年龄、城市等信息,调用函数的复杂度就会大大增加。这时,我们可以把年龄和城市设为默认参数:
def enroll(name, gender, age=6, city='Beijing'):
print('name:', name)
print('gender:', gender)
print('age:', age)
print('city:', city)
这样,大多数学生注册时,只需提供必须的两个参数就行:
enroll('Sarah', 'F')
只有那些与默认参数不符的学生才需要额外提供相应信息,比如:
enroll('Bob', 'M', 7)
enroll('Adam', 'M', city='Tianjin')
当函数有多个默认参数时,调用时既可以按顺序提供默认参数,比如 enroll('Bob', 'M', 7),这里除了 name 和 gender 这两个必选参数外,最后一个传入的值会应用在参数 age 上,city 参数由于没有提供新值,就会继续使用默认值。
同时,也可以不按顺序提供部分默认参数,不过这时候需要把参数名写上,例如 enroll('Adam', 'M', city='Tianjin'),意思就是 city 参数使用传进去的值,其他默认参数依旧使用默认值。
但默认参数使用不当也容易出现问题,比如下面这个例子:
def add_end(L=[]):
L.append('END')
return L
正常调用时,结果似乎没问题:
print(add_end([1, 2, 3]))
print(add_end(['x', 'y', 'z']))
使用默认参数调用,一开始结果也是对的:
print(add_end())
['END']
可是再次调用时,结果就不对了:
print(add_end())
print(add_end())
['END', 'END']
这是因为 Python 函数在定义的时候,默认参数 L 的值(也就是 [])就已经确定了,而 L 本身是一个变量,它指向了这个空列表对象。每次调用函数,如果改变了 L 的内容,下次调用时,默认参数的实际内容就变了,不再是函数定义时的那个空列表了。
所以,定义默认参数时一定要牢记:默认参数必须指向不变对象!
要修正上述例子的问题,可以用 None 这个不变对象来实现,代码如下:
def add_end(L=None):
if L is None:
L = []
L.append('END')
return L
这样,无论调用多少次,都不会出现之前的问题了:
print(add_end())
print(add_end())
['END']
['END']
之所以设计像 str、None 这样的不变对象,是因为它们一旦创建,其内部的数据就不能修改,这样可以减少因修改数据而引发的错误。而且在多任务环境下,同时读取这些不变对象不需要加锁,能保证同时读取时不会出现问题。因此,在编写程序时,如果条件允许,尽量将相关对象设计成不变对象。
(三)可变参数
在 Python 函数里,还可以定义可变参数。所谓可变参数,就是在调用函数时,传入的参数个数是可以变化的,能是 1 个、2 个,甚至任意个,也可以是 0 个。
我们以一个数学计算的例子来说明,比如给定一组数字 a,b,c……,要计算 a2 + b2 + c2 + ……。
一开始,我们可能会想到把这些数字作为一个列表(list)或者元组(tuple)传进来,函数定义如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
但这样调用的时候,就需要先把参数组装成一个列表或者元组,示例如下:
print(calc([1, 2, 3]))
print(calc((1, 3, 5, 7)))
要是利用可变参数来实现,调用函数的方式就会简便很多,如下:
print(calc(1, 2, 3))
print(calc(1, 3, 5, 7))
我们把函数参数改为可变参数的方式是在参数名前加一个 * 号,像这样:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
在函数内部,参数 numbers 接收到的其实是一个元组,所以函数内部的代码不需要做任何改变。并且,调用该函数时,可以传入任意个数的参数,甚至不传参数也是可以的:
print(calc(1, 2))
print(calc())
如果已经有一个列表或者元组,要调用一个可变参数函数,可以这样操作:
nums = [1, 2, 3]
print(calc(nums[0], nums[1], nums[2]))
不过这种写法比较繁琐,Python 提供了更便捷的方式,就是在列表或元组前面加一个 * 号,就能把列表或元组里的元素作为可变参数传进去,示例如下:
nums = [1, 2, 3]
print(calc(*nums))
这种 *nums 的写法很常用,也非常实用。
(四)关键字参数
可变参数允许传入 0 个或任意个参数,并且这些参数在函数调用时会自动组装成一个元组。而关键字参数则允许传入 0 个或任意个带有参数名的参数,这些关键字参数在函数内部会自动组装成一个字典。
来看下面这个示例函数:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
这个 person 函数除了必选参数 name 和 age 之外,还能接受关键字参数 kw。调用这个函数时,可以只传入必选参数:
person('Michael', 30)
也可以传入任意个数的关键字参数,比如:
person('Bob', 35, city='Beijing')
person('Adam', 45, gender='M', job='Engineer')
关键字参数的用处很大,它能够扩展函数的功能。比如在做用户注册功能时,用户名和年龄是必填项,其他信息都是可选项,利用关键字参数来定义这个函数就能很好地满足注册需求。
和可变参数类似,如果已经有一个字典,也可以把这个字典转换为关键字参数传进去,示例如下:
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job'])
不过,更简化的写法是这样的:
extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)
这里的 **extra 表示把 extra 这个字典里的所有键值对,以关键字参数的形式传入到函数的 **kw 参数中,而且 kw 获得的是 extra 的一份拷贝,对 kw 进行改动并不会影响到函数外部的 extra 字典。
(五)命名关键字参数
对于普通的关键字参数,函数调用者可以传入任意不受限制的关键字参数,至于到底传入了哪些关键字参数,就需要在函数内部通过对 kw 的检查来判断了。
还是以 person 函数为例,如果我们希望检查是否传入了 city 和 job 这两个参数,可以这样写函数:
def person(name, age, **kw):
if 'city' in kw:
# 有city参数
pass
if 'job' in kw:
# 有job参数
pass
print('name:', name, 'age:', age, 'other:', kw)
但这样调用者依然可以传入不受限制的关键字参数,比如:
person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果想要限制关键字参数的名字,就可以使用命名关键字参数。例如,只接收 city 和 job 作为关键字参数,函数定义如下:
python
def person(name, age, *, city, job):
print(name, age, city, job)
和普通的关键字参数 **kw 不同,命名关键字参数需要一个特殊分隔符 *,* 后面的参数才会被视为命名关键字参数。
调用方式如下:
person('Jack', 24, city='Beijing', job='Engineer')
如果函数定义中已经有了一个可变参数,那么后面跟着的命名关键字参数就不再需要这个特殊分隔符 * 了,示例如下:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
需要注意的是,命名关键字参数必须传入参数名来进行调用,这和位置参数是不一样的。如果没有传入参数名,调用就会报错,比如:
person('Jack', 24, 'Beijing', 'Engineer')
另外,命名关键字参数可以设置缺省值,以此来简化调用,例如:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由于命名关键字参数 city 有了默认值,所以调用时可以不传入 city 参数,像这样:
person('Jack', 24, job='Engineer')
使用命名关键字参数时,还有个重要的注意事项,如果没有可变参数,那就必须加一个 * 作为特殊分隔符,要是缺少了这个 *,Python 解释器就没办法准确区分位置参数和命名关键字参数了,例如下面这个函数定义就是错误的:
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass
三、参数组合使用
在 Python 中定义函数时,可以把必选参数、默认参数、可变参数、关键字参数以及命名关键字参数这 5 种参数组合起来使用。不过要注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数、关键字参数。
下面是一些包含多种参数组合的函数定义示例以及相应的调用示例:
(一)示例函数 f1
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
调用示例及结果如下:
f1(1, 2)
f1(1, 2, c=3)
f1(1, 2, 3, 'a', 'b')
f1(1, 2, 3, 'a', 'b', x=99)
(二)示例函数 f2
def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
调用示例及结果如下:
f2(1, 2, d=99, ext=None)
更神奇的是,我们还可以通过一个元组和一个字典来调用上述这些包含多种参数组合的函数,示例如下:
args = (1, 2, 3, 4)
kw = {'d': 99, 'x': '#'}
f1(*args, **kw)
args = (1, 2, 3)
kw = {'d': 88, 'x': '#'}
f2(*args, **kw)
也就是说,对于任意一个 Python 函数,无论它的参数是如何定义的,都可以通过类似 func(*args, **kw) 的形式来进行调用。
提示:虽然 Python 支持多达 5 种参数的组合使用,但建议不要同时使用太多的组合,不然函数接口的可理解性会变得很差,不利于代码的维护和阅读。