Python 函数中的参数用法比较灵活,容易混淆,抽时间把自己的理解厘清,以后备查。
一. 综述
1. 函数调用时传参方式
从函数调用角度看,传参(实参)的时候可以使用以下几种方式:
- 位置参数(Positional Arguments):按参数定义的顺序依次传值
- 默认参数(Default Arguments):可以不必传参,直接使用函数定义时参数的默认值
- 关键字参数(Keyword Arguments):指出参数的名称,即 参数名=XXX 的形式
- 仅关键字参数(Keyword-Only Arguments):必须通过关键字参数方式传值,即参数名=XXX 的形式。有些函数在调用时允许选择性使用“位置参数”或“关键字参数”传参
- 可变参数(Variadic Parameters):可以传递任意数量的位置参数(针对 *args 参数)或关键字参数(针对 **kwargs 参数)
再次进行聚合分类:
- 位置实参类:位置参数、默认参数、可变参数(*args)
- 关键字实参数类:关键字参数、仅关键字参数、可变参数(**kwargs)
2. 函数参数定义及调用方式小结
从函数定义角度看,参数定义可以包括以下几种形式:
- 方式1:
- 函数定义,仅定义参数名: func(a, b)
- 函数调用:
- 可以通过 “位置参数” 形式调用函数: func(4, 6)
- 可以通过 “关键字参数” 形式调用函数: func(a=4, b=6)
- 方式2:
- 函数定义,某参数带默认值:func(a, b=9)
- 函数调用:
- 可以通过 ”位置参数“ 形式: func(4, 6)
- 可以通过 ”关键字参数“ 形式:func(a=4, b=6)
- 可以通过 “默认值参数“ 形式,即省略有默认值参数的传值: func(4)
- 方式3:
- 函数定义,要求调用时使用 “可变位置参数” 传值: func(a, b=9, *args)
- 函数调用:
- 可以通过 “位置参数” 形式: func(4, 9, 6, 6, 6)
- 可以通过解包后的 ”位置参数“: func(4, 9, *(6, 6, 6))
- 方式4:
- 函数定义,要求调用时使用 “可变关键字参数” 传值: func(a, b=9, *args, **kwargs)
- 函数调用:
- 可以通过 ”关键字参数“ 形式: func(4, 9, *(6, 6, 6), k1=9, k2=9)
- 可以通过解包后的 ”关键字参数“: func(4, 9, *(6, 6, 6), **{'k1': 9, 'k2': 9})
- 方式5
- 函数定义,要求调用时使用 ”仅关键字参数“ 传值: func(a, b=9, *args, k1, k2, **kwargs)
- 函数调用:
- 可以通过 “关键字参数” 形式:func(4, 9, *(6, 6, 6), k1=8, k2=8, **{"k3": 8, "k4": 8})
- 可以通过解包的形式: func(4, 9, *(6, 6, 6), **{"k1": 8, "k2": 8}, **{"k3": 8, "k4": 8})
- 方式6
- 函数定义,要求调用时使用 “仅关键字参数” 传值: func(a, b=9, *, k1, k2, **kwargs)
- 函数调用:
- 可以通过 “关键字参数” 形式: func(4, 9, k1=8, k2=8, **{"k3": 8, "k4": 8})
- 可以通过解包的形式: func(4, 9, **{"k1": 8, "k2": 8}, **{"k3": 8, "k4": 8})
3. 函数调用时为形参传值的顺序规则
在函数调用的时候,需要用 “实参” 为 “形参” 传值,此时有处理的先后顺序:
- 整体: 先位置参数 → 再关键字参数
- 细分: 位置参数(无默认值)→ 位置参数(有默认值)→ 可变位置参数(*) → 仅关键字参数 → 关键字参数(可变**)
二. 各类参数用法举例
1. 无默认值
- 定义:可同时包括多个形参
- 调用:
- 实参与形参数量相同
- 可以使用 “位置参数” 方式调用,实参按形参的定义顺序传值
- 可以使用 “关键字参数” 方式调用,实参间可以任意调换位置
def fun1(a, b):
print(f'a={a}, b={b}')
# 正确调用:以下均为等价写法,输出均为:a=2, b=3
fun1(2, 3) # 使用 “位置参数” 方式调用函数,按定义顺序传参即可
fun1(a=2, b=3) # 使用 “关键字参数” 方式调用函数
fun1(b=3, a=2) # 通过 “关键字参数” 传参时,可以不按参数定义的顺序
fun1(*(2, 3)) # 解包。然后依然按照 “位置参数” 方式调用函数
# 错误调用
fun1(2) # TypeError: fun1() missing 1 required positional argument: 'b'。 缺少必须的位置参数 b
fun1(2, 3, 4) # TypeError: fun1() takes 2 positional arguments but 3 were given。 需要2个却提供了3个参数
2. 默认值参数
- 定义:如果存在“无默认值”的参数,则“有默认值”的参数必须放在其后面
- 调用:
- 调用时可以忽略“有默认值”的参数,即不向其传递实参,则其自动使用定义的默认值
- 如果向“默认值”参数传递实参,则使用该实参,即不使用默认值
# 函数定义,参数中使用了默认值
def fun2(a, b=2):
print(f'a={a}, b={b}')
# 正确调用:以下均为等价写法,输出均为:a=0, b=2
fun2(0) # 使用 “位置参数” 方式调用函数,有默认值的参数(b)可以省略传参
fun2(0,) # 也可以多写一个逗号
fun2(0, 3) # 使用 “位置参数” 方式调用函数,用传入的新值代替默认值
fun2(a=0) # 使用 “关键字参数” 方式调用函数,有默认值的参数(b)可以省略传参
fun2(a=0, b=3) # 使用 “关键字参数” 方式调用函数
# 错误的函数定义
def fun2(b=2, a): # SyntaxError: non-default argument follows default argument 语法错误:非默认参数跟在默认参数后面
3. 可变位置参数
- 定义:
- 一个星号(*)加形参变量名,即 *a 的形式
- 将接收到的可变位置参数组织为一个 “元组” 的形式
- 调用:
- 可以传入任意多个实参值
- 不能以关键字参数 (参数名=XXX) 的方式调用
# 函数中只有一个形参:*a
def fun3(*a):
print(f'a={a}')
# 正确调用
fun3(1) # 输出:a=(1,) ,元组内只有一个元素
fun3(1, 2) # 输出:a=(1, 2) ,元组内有两个原因
fun3(1, 2, 3) # 输出:a=(1, 2, 3) ,元组内有三个元素
fun3(*(1, 2, 3)) # 输出:a=(1, 2, 3) ,解包后传值,元组内有三个元素
fun3((1, 2, 3)) # 输出:a=((1, 2, 3),) ,元组内只有一个元素(1个元组元素)
fun3((1, 2, 3), (6, 6)) # 输出:a=((1, 2, 3), (6, 6)) ,元组内有两个元素(2个元组元素)
# 正确调用
t = (1,2,3) # 定义一个元组
fun3(t) # 输出:a=((1, 2, 3),) ,除非对该元组进行解包,分解为多个元素,否则视为 “1个元素”
fun3(*t) # 输出:a=(1, 2, 3) ,先解包,再将解包后释放的元素传递给形参
# 错误调用:不能以关键词参数形式调用该函数
fun3(a=1) # TypeError: fun3() got an unexpected keyword argument 'a' 传入的是一个关键字参数
4. 可变关键字参数
- 定义:
- 两个星号(**)加形参变量名,即 **a 的形式
-
- 将接收到的可变关键字参数组织为一个 “map(字典)” 的形式
- 调用:
- 可以传入任意多个实参值
-
- 不能以位置参数的方式调用
def fun4(**a):
print(f'a={a}')
# 正确调用
fun4(k1=2, k2=4) # 输出:a={'k1': 2, 'k2': 4}
fun4(k2=4, k1=2) # 输出:a={'k2': 4, 'k1': 2}, 此处不自动调整关键字的顺序
fun4(**{'k1': 2, 'k2': 4}) # 输出:a={'k1': 2, 'k2': 4}, 先解包再传参
# 正确调用
fun4(a={'k1': 2, 'k2': 4}) # 输出:a={'a': {'k1': 2, 'k2': 4}} ,只包含了一个元素(map),由于未解包,将该map看作是一个元素整体
# 错误调用:不能使用位置参数的方式调用该函数
fun4(3, 4, 5) # 输出:TypeError: fun4() takes 0 positional arguments but 3 were given, 需要0个位置参数但给了3个
5. 仅关键字参数
5.1. 在一个星号标记后面
- 定义:
- 一个星号后面跟多个参数名,要求这些参数在函数调用的时候,需要显示的指出该参数名,即使用关键字参数的方式调用
- 调用:
- 要以 “关键字参数” 的形式调用,即显示指明向哪个参数传值
# 函数定义,星号后面的两个参数都必须用关键字参数方式调用
def fun5(*, a, b):
print(f'a={a}, b={b}')
# 正确调用
fun5(a=2, b=4) # 输出:a=2, b=4
fun5(b=4, a=2) # 输出:a=2, b=4, 可以不按参数定义顺序调用
fun5(**{'a': 2, 'b': 4}) # 输出:a=2, b=4, 先解包,再调用
5.2. 在可变位置参数后面
- 定义:前面有一个 “可变参数”(*c)
- 调用:以 “关键字参数” 的形式调用,即显示指明向哪个参数传值
# 函数定义,在 “可变位置参数(*c)” 和 “可变关键字参数(**e)” 之间,调用时需要使用 “仅关键字参数(d1,d2)” 形式
def fun1(a1, a2=1, *c, d1, d2=2, **e):
print(f'a1={a1}, a2={a2}, c={c}, d1={d1}, d2={d2}, e={e}')
# 正确调用
fun1(0, *(2, 3, 3), d1=11, e1=22, e2=33)
# 输出:a1=0, a2=2, c=(3, 3), d1=11, d2=2, e={'e1': 22, 'e2': 33}
# 其中:
# 1). 虽然没有直接向a2传值,但是 *(2, 3, 3) 被解包后,第一个实参2被传值给形参a2了,因为其符合位置参数调用传值规则
# 2). d1=11 使用了关键字参数
# 3). d2 使用了默认值参数
# 正确调用
fun1(0, *(2, 3, 3), **{'d1': 11, 'e1': 22, 'e2':33})
# 输出:a1=0, a2=2, c=(3, 3), d1=11, d2=2, e={'e1': 22, 'e2': 33}
# 其中:
# 1). **{'d1': 11, 'e1': 22, 'e2':33} 被解包后,第一个实参 d1=11 被传值给形参d1了,仍旧使用了关键字参数调用方式
# 2). d2 使用了默认值参数
# 正确调用
fun1(0, c=(3, 3, 3), **{'d1': 11, 'e1': 22, 'e2':33})
# 输出:a1=0, a2=1, c=(), d1=11, d2=2, e={'c': (3, 3, 3), 'e1': 22, 'e2': 33}
# 其中:
# 1). c=(3, 3, 3),是一个关键字参数调用方式,可以不考虑其所在位置,直接被传值给符合关键字参数调用规则的形参
# (1). 不是默认值参数形式,所以不会传值给形参a2,
# (2). 也不是位置参数形式,虽然参数名称都为c,但也不会传值给形参*c,所以其输出为一个空白的元组 c=()
# (3). 只有可变关键字**e,符合其传值规则,因此传值给可变关键字参数**e了
# 2). a2参数由于没有被某个实参传值,因此取默认值了,即a2=1
# 正确调用
fun1(0, 2, **{'d1': 11, 'e1': 22, 'e2':33})
# 输出:a1=0, a2=2, c=(), d1=11, d2=2, e={'e1': 22, 'e2': 33}
# 其中:
# 1). 为a2参数调用时使用了“位置参数”,为其传值为2,没有使用其默认值
# 2). 调用时对应位置上没有“可变位置参数”,因此c为一个空的元组
# 正确调用
fun1(0, a2=2, **{'d1': 11, 'e1': 22, 'e2':33})
# 输出:a1=0, a2=2, c=(), d1=11, d2=2, e={'e1': 22, 'e2': 33}
# 正确调用
fun1(0, 2, *(3, 3, 3), **{'d1': 11, 'e1': 22, 'e2': 33})
# 输出:a1=0, a2=2, c=(3, 3, 3), d1=11, d2=2, e={'e1': 22, 'e2': 33}
# 错误调用
fun1(0, a2=2, *(3, 3, 3), **{'d1': 11, 'e1': 22, 'e2': 33})
# 输出:TypeError: fun1() got multiple values for argument 'a2' 类型错误,a2参数存在多值
# 分析:调用时按处理规则传值,即先给位置参数传值,再给关键字参数传值
# 1). a2=2 是一个关键字参数,但是后面还有未处理的位置参数,因此先不考虑这个传值
# 2). *(3, 3, 3) 解包后产生3个位置参数,正好前面有一个a2的位置参数,因此解包后的第一个实参就传值给a2=3了,后面两个给*c了
# 3). 最后处理关键字参数的时候,发现又有一个 a2=2,这样导致有两个 a2 的重复传值
根据 PyCharm 中参数调用方式分析:
再次验证:
三. 应用举例
1. unittest 中 main.py 关于 TestProgram 实例化的处理:
- 执行单元测试的时候,首先就是要实例化 TestProgram 类
- 在实例化 TestProgram 类的时候,其 __init__ 方法的参数中就包含了一个星号(*),代表后面的参数 tb_locals 在使用的时候符合 “仅关键字参数” 调用规则,必须带着参数名传递
# 执行单元测试:执行测试当前模块下的测试用例
unittest.main(verbosity=2)
# unittest 源码
main = TestProgram
class TestProgram(object):
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
2. 定义装饰器时内部函数 wrapper(*args, **kwargs) 同时使用了 “可变位置参数” 和 “可变关键字参数”
- 无论什么形式的调用传参,其最终都呈现两种方式,即 “位置参数” 和 “关键字参数”
- 所以用 *args 接收任意个 “位置参数”, 用 “**kwargs” 接收任意个 “关键字参数”,这样不管原函数中使用任意的参数类型和数量都可以接收到,进而可以无损执行原函数的回调。
# 定义一个装饰器
def decorator(func):
# 通过内部函数,在原函数执行前、后完成指定任务
def wrapper(*args, **kwargs):
print("完成前置操作任务。。。")
# 回调原函数
result = func(*args, **kwargs)
print("完成后置操作任务。。。")
return result
return wrapper
# 在函数定义上使用装饰器
@decorator
def add(x, y):
return x + y
# 执行函数
print(add(5, 3))
# 结果输出:
前置操作。。。
8
后置操作。。。