厘清Python函数中参数的用法

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
后置操作。。。

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值