关于Python参数你应该知道的

前言

本文来记录下 Python 中的关于不同的参数在使用上的不同之处。

Python 中参数类型有好多种,位置参数,关键字参数,默认参数,可变参数,命名位置参数,命名关键字参数等都可能会出现在平时写代码的过程中。

讲到函数参数,先要说一下 Python 中的函数调用。

前置知识

以下演示中会用到 locals() 函数,它的作用是以字典的形式返回当前命名空间中的全部局部变量,方便依次输出当前命名空间中所有变量的值,而不必挨个 print

注:以下代码使用 Python 3.8.5 编写。

函数调用方式

Python 中进行函数调用的方式,无外乎就两种:

  • func(1)
  • func(a=1)

分别是通过位置参数传参和通过关键字参数传参,通过元组解包和字典解包传递参数的方式本质上也是上面两种。

位置参数和关键字参数在使用时,有一个基本的原则:位置参数和关键字参数有严格的顺序位置参数一定要放在关键字参数的前面,两种参数都可以省略,但是顺序不可以交换。

记住这个原则,我们往下看。

注:以下函数定义仅作为示例使用,不推荐函数参数使用单个字母。

位置参数

位置参数,顾名思义就是按照函数定义的参数位置来传递对应的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [2]: def func(a,b,c):
...: print(locals())
...:

# 正常调用
In [3]: func(1,2,3)
{'a': 1, 'b': 2, 'c': 3}

# 也可以和关键字参数混用,但是注意关键字参数的位置,不能放在位置参数前面
In [3]: func(1,2,c=3)
{'a': 1, 'b': 2, 'c': 3}

# 位置参数不能在关键字参数的后面
In [4]: func(a=1,2,3)
File "<ipython-input-4-7285a134b862>", line 1
func(a=1,2,3)
^
SyntaxError: positional argument follows keyword argument

位置参数中的位置指的是在调用函数时按照函数定义的参数的位置依次传入对应的值,是调用时的概念,和函数定义没有关系,同样的函数既可以使用位置参数来调用也可以使用关键字参数来调用。

使用位置参数调用函数的好处在于不需要写繁琐的 参数名=,可以简化函数调用,缺点是必须按照函数参数定义的顺序依次填充值,顺序不可以颠倒,值不可以省略。

关键字参数

关键字参数,就是按照函数定义的关键字来传递对应的参数,在传参时要指定将值传递给哪个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [4]: def func(a,b,c):
...: pprint(locals())
...:

# 可以按照正常顺序传递
In [5]: func(a=1,b=2,c=3)
{'a': 1, 'b': 2, 'c': 3}

# 也可以乱序传递,因为指定了将值传递给某个参数,所以顺序可以颠倒。
In [7]: func(c=3,a=1,b=2)
{'a': 1, 'b': 2, 'c': 3}

# 也可以使用位置参数,但是要注意参数位置
In [5]: func(1,2,3)
{'a': 1, 'b': 2, 'c': 3}

关键字参数中的关键字指的是在调用函数时按照函数定义中参数的名字(关键字)使用 参数名= 的形式来传递参数值,同样是调用时的概念,和函数定义没有关系。

使用关键字参数的好处在于可以乱序调用参数,不需要关心传入参数的顺序和位置,缺点是每次都要具体写出要传递的参数的名字。

可变参数

可变参数指的是函数接收到的参数的数量不确定,也就是说函数可以接收任意多个参数,包括但不限于 0 个。一般习惯性使用 *args,**kwargs 来接收可变参数,也可以使用其他名字,不过为了易于他人理解还是推荐使用这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [9]: def func(*args,**kwargs):
...: pprint(locals())
...:

# 注意位置参数一定要在关键字参数的前面
In [10]: func(1,2,3,4,a=12,b=23,c=34)
{'args': (1, 2, 3, 4), 'kwargs': {'a': 12, 'b': 23, 'c': 34}}

# 可以接收 0 个参数
In [11]: func()
{'args': (), 'kwargs': {}}

# 也可以接收单独的位置参数
In [12]: func(1)
{'args': (1,), 'kwargs': {}}

# 也可以接收单独的关键字参数
In [13]: func(a=2)
{'args': (), 'kwargs': {'a': 2}}

可变参数通过特殊的参数写法可以接收任意数量的位置参数和关键字参数,args 在接收参数时将所有命名位置参数和位置参数都匹配完成后剩余的位置参数封装成一个元组,可以在函数内部使用 args 变量来获取所有未成功匹配的位置参数,kwargs 在接收参数时将所有命名关键字参数和关键字参数都匹配完成后剩余的关键字参数封装成一个字典,可以在函数内部使用 kwargs 变量来获取所有未匹配成功的关键字参数。

命名位置参数

命名位置参数(positional-only),在 pep-570 引入,是指通过 / 来限定 / 前面的变量必须传入位置参数,不可以传入关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [14]: def func(a,b,c,/,d,e):
...: pprint(locals())
...:

# / 前面的变量必须使用位置参数
In [15]: func(1,2,3,d=23,e=34)
{'a': 1, 'b': 2, 'c': 3, 'd': 23, 'e': 34}

# / 后面的变量既可以使用位置参数也可以使用关键字参数
In [16]: func(1,2,3,44,55)
{'a': 1, 'b': 2, 'c': 3, 'd': 44, 'e': 55}

# / 前面的变量不能使用关键字参数
In [17]: func(1,2,c=3,d=4,e=5)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-17-f0823dfb29ac> in <module>
----> 1 func(1,2,c=3,d=4,e=5)

TypeError: func() got some positional-only arguments passed as keyword arguments: 'c'

在 Python 3.7 中,class bool([x]) 的参数 x 被更改为命名位置参数,不能使用 bool(x=1) 这种方式来调用了,因为传入的关键字参数 x 没有什么意义,强制使用关键字参数并没有好处,直接使用 bool(1) 更直观。

1
2
3
4
5
6
7
8
9
10
In [6]: bool(1)
Out[6]: True

In [7]: bool(x=1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-de6e98604b49> in <module>
----> 1 bool(x=1)

TypeError: bool() takes no keyword arguments

命名关键字参数

命名关键字参数(keyword-only),在 pep-3102 引入,是指通过 * 来限定 * 后面的参数只能传入关键字参数,不能传入位置参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [11]: def func(a,b,*,c,d):
...: pprint(locals())
...:

# * 后面的参数不能传入位置参数
In [12]: func(1,2,3,4)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-12-e8c91d9d8fe8> in <module>
----> 1 func(1,2,3,4)

TypeError: func() takes 2 positional arguments but 4 were given

# * 前面的变量可以传入位置参数
In [13]: func(1,2,c=3,d=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

# * 前面的变量也可以传入关键字参数
In [14]: func(a=1,b=2,c=3,d=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

那么为什么 / 前面的变量必须是位置参数和 * 后面的变量必须是关键字参数呢?

因为位置参数和关键字参数的位置必须遵守一个原则, 前面已经说过了,那就是位置参数必须在关键字参数的前面,位置不能互换。

如果 * 限定了 * 前面的参数必须是关键字参数的话,那么后面的参数只能是位置参数或者说不能有其他参数了,这样就和上面所说的位置参数必须在关键字参数前面的原则相悖,所以命名位置参数和命名关键字参数限定的位置必须是 / 的前面和 * 的后面。

默认参数

默认参数是指在函数参数中直接指定默认的参数值,可以在调用时不给默认参数传值,也可以在调用时主动传入其他值来覆盖默认参数的值,注意位置参数一定要在默认参数前面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [16]: def func(a,b=2,c=4):
...: pprint(locals())
...:

# 不给默认参数传值
In [17]: func(1)
{'a': 1, 'b': 2, 'c': 4}

# 默认参数可以使用位置参数进行替换
In [18]: func(1,334,44)
{'a': 1, 'b': 334, 'c': 44}

# 默认参数也可以使用关键字参数进行替换
In [19]: func(a=2,c=233,b=112)
{'a': 2, 'b': 112, 'c': 233}

# 覆盖两个默认参数
In [20]: func(1,2,3)
{'a': 1, 'b': 2, 'c': 3}

# 覆盖一个默认参数
In [21]: func(1,3)
{'a': 1, 'b': 3, 'c': 4}

注:默认值参数只会被执行一次,在函数默认参数值为可变对象时,一旦函数内部对其进行修改,那么后续每次调用函数都会保留修改后的值,不会重新进行初始化,可能会产生逻辑错误,强烈建议默认参数一定要使用不可变对象!!!如整数、浮点数、字符串、TrueFalseNone或元组等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
In [8]: def func(a=[]):
...: a.append(1)
...: pprint(locals())
...:

In [9]: func()
{'a': [1]}

In [10]: func()
{'a': [1, 1]}

In [11]: func()
{'a': [1, 1, 1]}

如果一定要用到可变对象作为默认值,可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [2]: def func(a=None):
...: if a is None:
...: a=[]
...: a.append(1)
...: pprint(locals())
...:

In [3]: func()
{'a': [1]}

In [4]: func()
{'a': [1]}

In [5]: func()
{'a': [1]}

注:两个特殊参数 *args**kwargs 不能设定默认值,在不传值的情况下,它们的默认值就是空元组和空字典。

不同类型参数混合使用

位置参数、关键字参数和默认参数混用

注意默认参数不能是可变类型

1
2
3
4
5
6
7
8
9
In [16]: def func(a,b,c=1,d=0):
...: pprint(locals())
...:

In [17]: func(1,2)
{'a': 1, 'b': 2, 'c': 1, 'd': 0}

In [18]: func(b=2,a=1,c=-1)
{'a': 1, 'b': 2, 'c': -1, 'd': 0}

默认参数可以不传值,关键字参数可以乱序传递,位置参数必须依次传递。

位置参数、关键字参数、默认参数和可变参数混用。

1
2
3
4
5
6
In [20]: def func(a,b,c=-1,*args,**kwargs):
...: pprint(locals())
...:

In [21]: func(1,2,3,k=233)
{'a': 1, 'args': (), 'b': 2, 'c': 3, 'kwargs': {'k': 233}}

注意这里的默认参数匹配了传入的第三个值 3,后边的可变参数就无法在匹配到任何的位置参数了,所以 args 为一个空元组,kwargs 接收了未匹配上的关键字参数。

1
2
In [22]: func(1,2,3,4,5,6,kk=344)
{'a': 1, 'args': (4, 5, 6), 'b': 2, 'c': 3, 'kwargs': {'kk': 344}}

这里的位置参数 3 覆盖了默认参数 c 的值,位置参数和默认参数匹配了 1,2,3 三个值,剩下未被位置参数和默认参数匹配到的值则被可变参数 args 参数接收,封装成了一个元组。

1
2
3
4
5
6
7
In [9]: func(1,2,55,k=233,c=4)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-ef558b96db36> in <module>
----> 1 func(1,2,55,k=233,c=4)

TypeError: func() got multiple values for argument 'c'

上面这种情况的问题在于 c 的值被传入了多次,因为函数的前三个参数默认匹配了 a,b,c 三个值,后面的 k 被可变参数接收到,但是后面又传入了一个参数 c,相当于一个 c 参数被传入了两个值,所以报错提示说为 c 参数传入了多个值。

位置参数、关键字参数、默认参数、命名位置参数混用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
In [31]: def func(a,b,/,c,d=-1):
...: pprint(locals())
...:

In [32]: func(1,2,3)
{'a': 1, 'b': 2, 'c': 3, 'd': -1}

In [33]: func(1,2,3,4)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

In [34]: func(1,2,c=3)
{'a': 1, 'b': 2, 'c': 3, 'd': -1}

In [35]: func(a=1,b=2,c=4)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-35-fabaaaea091e> in <module>
----> 1 func(a=1,b=2,c=4)

TypeError: func() got some positional-only arguments passed as keyword arguments: 'a, b'

对比来看,注意此次函数定义中默认参数的位置,默认参数一定要在位置参数后面。根据错误信息可知,命名你位置参数不允许传入关键字参数。

位置参数、关键字参数、默认参数、命名位置参数和命名关键字参数混用

1
2
3
4
5
6
7
8
9
10
11
In [36]: def func(a,b,/,c,d=1,*,e,f=-1):
...: pprint(locals())
...:

In [37]: func(1,2,3,4)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-37-e8c91d9d8fe8> in <module>
----> 1 func(1,2,3,4)

TypeError: func() missing 1 required keyword-only argument: 'e'

数字 1,2 默认匹配了两个命名位置参数,也就是 a 和 b,3 匹配了参数 c,但是 4 肯定不会也不能跳过 d 参数去匹配参数 e,所以这里会提示缺少 e 参数。

1
2
3
4
5
In [39]:  func(1,2,3,e=5)
{'a': 1, 'b': 2, 'c': 3, 'd': 1, 'e': 5, 'f': -1}

In [40]: func(1,2,c=3,e=4)
{'a': 1, 'b': 2, 'c': 3, 'd': 1, 'e': 4, 'f': -1}

对比来看,数字 1 和 2 依次匹配命名位置参数,/* 之间的参数可以传位置参数也可以传关键字参数,由于 d 已经有了默认参数,可以直接使用默认参数,如果想要使 d 使用默认值只需要将想要传递给 e 的值通过关键字的方式传递即可。

位置参数、关键字参数、默认参数、可变参数、命名位置参数和命名关键字参数混用

1
2
3
4
5
6
7
8
9
In [2]: def func(a,b,/,c,d=2,*args,e,f=-1):
...: pprint(locals())
...:

In [3]: func(1,2,3,e=4)
{'a': 1, 'args': (), 'b': 2, 'c': 3, 'd': 2, 'e': 4, 'f': -1}

In [4]: func(1,2,3,4,5,6,7,e=233)
{'a': 1, 'args': (5, 6, 7), 'b': 2, 'c': 3, 'd': 4, 'e': 233, 'f': -1}

先看第一个函数调用,1 和 2 匹配了前两个命名位置参数,还有一个 3 匹配了位置参数 c,d 有默认参数,不用传值,关键字参数 4 匹配了命名关键字参数 e,f 使用默认值,由于传入的所有位置参数都被匹配上了,没有留下多余位置参数,所以可变参数 args 为空元组。

再看第二个函数调用,1,2,3 分别匹配了两个命名位置参数 a,b 和一个位置参数 c,4 覆盖了默认参数 d 的值,只剩下了 5,6,7 三个值,封装成了元组被 args 变量接收到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [8]: def func(a,b=2,/,c=3,d=4,*args,kw1,kw2,**kwargs):
...: pprint(locals())
...:

In [10]: func(1,kw1=2,kw2=3)
{'a': 1, 'args': (), 'b': 2, 'c': 3, 'd': 4, 'kw1': 2, 'kw2': 3, 'kwargs': {}}

In [11]: func(1,22,33,44,55,66,77,k=1,m=2,n=4,kw1=2,kw2=3)
{'a': 1,
'args': (55, 66, 77),
'b': 22,
'c': 33,
'd': 44,
'kw1': 2,
'kw2': 3,
'kwargs': {'k': 1, 'm': 2, 'n': 4}}

先看第一个函数调用,只传了一个位置参数和两个关键字参数,其他参数都是默认参数。

再看第二个函数调用,前面四个参数匹配了函数的前四个参数,其中有三个有默认值被覆盖了。剩余参数无法匹配上位置参数被封装成元组被可变参数 args 接收到,两个 kw 关键字参数覆盖了两个命名关键字参数的默认值,多出来的三个关键字参数无法匹配任何的关键字参数,封装成一个字典被可变参数 kwargs 接收到。

注:命名位置参数和命名关键字参数也可以设定默认值,本质上他们就是位置参数和关键字参数,只是被限定了调用时传入参数的方式而已。

通过解包传参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [8]: def func(a,b=2,/,c=3,d=4,*args,kw1,kw2,**kwargs):
...: pprint(locals())
...:

In [12]: a=(1,2,3,4,5,6,7,8)

In [13]: b={'k':1,'m':2,'n':3,'kw1':123,'kw2':234}

In [14]: func(*a,**b)
{'a': 1,
'args': (5, 6, 7, 8),
'b': 2,
'c': 3,
'd': 4,
'kw1': 123,
'kw2': 234,
'kwargs': {'k': 1, 'm': 2, 'n': 3}}

使用 *** 可以在传递参数时进行解包,将位置参数封装为一个元组或者列表,将关键字参数封装成一个字典,在调用函数时使用对应的语法进行解包可以大大简化函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
In [10]: def func(a,b=2,/,c=3,d=4,*args,kw1,kw2,**kwargs):
...: pprint(locals())
...:

In [11]: func(1,3,*(11,22,33),**{'k1':1234,'k2':2345},m=1,kw1=122,kw2=233)
{'a': 1,
'args': (33,),
'b': 3,
'c': 11,
'd': 22,
'kw1': 122,
'kw2': 233,
'kwargs': {'k1': 1234, 'k2': 2345, 'm': 1}}

上面的调用可以简化为 func(1,3,11,22,33,k1=1234,k2=2345,m=1,kw1=122,kw2=233) ,其实就是将元组和字典解包后通过位置参数和关键字的方式传入参数的,通过解包的方式可以简化函数调用。

总结

在函数调用时,传入的参数一定要共同覆盖所有没有默认值的参数,不能让一个参数既匹配位置参数又匹配关键字参数。在定义函数时要合理的定义函数参数,合理使用以上所有的参数形式,并且在调用函数时注意函数参数的定义方式,根据函数的定义方式传入合适的参数。函数参数的作用域在函数体中,虽然不和主程序其他变量冲突,但是仍然不推荐使用在主程序中已经使用过的变量名作为参数名,也就是说函数的参数名最好不要和程序中任何一个变量重名。

Python 函数参数在设计和使用时是比较灵活的,我们在使用时要灵活对待,最大限度地发挥不同类型参数的功效。

参考链接

https://www.zhihu.com/question/57726430

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接https://blog.d77.xyz/archives/b3a624fe.html