为什么想写这个

因为在实际浏览项目代码的过程中,我发现大部分人写的传参代码都太混乱了,该传或者不该传的都传进去并且混在一起,主要是我也不是很懂这部分,因此,借助这个机会,对函数的传参这部分内容详细讲解一下。

函数传参

函数传参的类型

一个完整的Python参数系统可以分成:

  1. 位置参数

  2. 默认参数

  3. 关键字参数

  4. 可变位置参数*args

  5. 可变关键字参数**kwargs

位置参数

def add(a,b):
  return a + b

# 10->a 20->b
add(10,20) -> 30

此时是按照位置进行参数的绑定

关键字参数

def divide(a,b):
  return a/b

divide(a=10,b=2) 
divide(b=2, a=10) # 两者等价

此时传参按照固定参数名来传递,而不是按照位置。

位置参数和关键字参数混用:此时位置传参要卸载关键字传参之前

def func(a,b,c):
  return a+b+c

func(1,2,c=3) #合法
func(a=1,2,3) #不合法

与此同时注意不能给一个参数传两次值,比如之前示例的divide函数不能写成divide(1,a=2),原因是a已经绑定给了1,就不能再赋值了。

默认参数

def power(x,n=2)
  return x**n

power(3) == power(3,2)

注意默认参数要写在必须参数的后面

可变位置参数

def f(*args):
  print(args)

args = (10,20,30)
f(*args) == f(10,20,30) 
# 输出(10,20,30) 此时args是一个元组,无论传入多少参数都会以元组的形式收集起来

如果可变位置参数和位置参数之类的共同使用,前面的参数会进行匹配,剩余匹配不到的参数会被收集。

传入的args只要是一个可迭代对象就可以实现解包,无论是什么形式只要其可迭代。

可变关键字参数

def f(**kwargs):
  print(kwargs)

f(name="Tom",age=20)
'''
输出:
{
  "name" : "Tom",
  "age" : 20
}
'''

所以**kwargs用来接收没有被其他形参接收的关键字实参,而调用端使用**是展开mappint,只需要传入一个字典对象展开即可。与此同时,注意**和*两种展开方式的不同,*接收一个可迭代对象,如果这个可迭代对象是字典,字典是默认只返回key而不返回value,因此得到的args会是一个包含key但是不带value的元组。

特殊情况以及多种方式混合使用

仅关键字参数

def f(a,*,b,c):
  print(a,b,c)

# 这里*后面的参数只能通过关键字传入
f(1,b=2,c=3) #合法
f(1,2,3) #非法

# 因为有的函数强制写参数名会让代码更加清晰
def connect(host, *, timeout=10, retry=3):
    pass

connect(
    "example.com",
    timeout=5,
    retry=2
)

注意*args后面的参数天然就是仅关键字参数。

仅位置参数

# python3.8之后
def f(a, b, /):
    print(a, b)

# 这里/表示前面的参数只能通过位置传入
f(1,2) # 合法
f(a=1,b=2) # 不合法

仅位置和仅关键字混合使用

def f(a, b, /, c, d=4, *, e, f=6):
    pass

# 其中a,b仅支持位置传参,c和d支持任意传参,e和f只能用关键字传参

融合使用

可以把传参用这个形式带入

def func(
    a,
    b,
    /,
    c,
    d=10,
    *args,
    e,
    f=20,
    **kwargs
):
    pass

应该比较清楚了,本质就是把参数划分成了几个段落。注意到d实际上,因为你传入的参数必然会被d接收,实际上它的默认值不起效果,所以应用过程中,我们最好把这种具有默认值的放到后面。

如何写出更加优雅的传参

实际上传参的问题还不止这一点,还包含很多特殊情况,为了方便观察,我们最好把需要用位置传参的参数放到最前面,使用关键字传参的放到后面,这不代表一定会出现错误,比如一种特殊情况,当位置传参是用*args进行传递的时候,前面可以写关键字传参,当然这应该是为了方便收集无效的参数,但是背后原理可能更加复杂一点。所以我觉得,不用位置传参是最好的,没错,大家都用关键字吧。