Python中*和**在函数中的用法


python中函数的参数

在介绍 *** 的用法之前,我们首先要了解python中函数的参数如何定义。

def f(pos1, pos2, kw1, kw2=None)这种是我们平时最常用的参数定义格式,它定义了两个位置参数(positional)pos1, pos2和两个关键字参数(kwyword)kw1, kw2=None。但是对于这些参数有时我们常常无法区分它是位置参数还是关键字参数,不过由于python的灵活性,使用这两种方法其实都可以为方法传参。

In [1]: def f(pos1, pos2, kw1, kw2=None):
   ...:     print('pos1:', pos1)
   ...:     print('pos2:', pos2)
   ...:     print('kw1:', kw1)
   ...:     print('kw2:', kw2)
   ...: 

In [2]: f(1, 2, 3, 4)
pos1: 1
pos2: 2
kw1: 3
kw2: 4

In [3]: f(1, 2, kw1=3, kw2=4)
pos1: 1
pos2: 2
kw1: 3
kw2: 4

In [4]: f(pos1=1, pos2=2, kw1=3, kw2=4)
pos1: 1
pos2: 2
kw1: 3 
kw2: 4

In [5]: f(pos2=2, pos1=1, kw2=4, kw1=3)
pos1: 1
pos2: 2
kw1: 3
kw2: 4

这样做的好处是我们可以混用两种方式为方法传参。要注意的是,pos1pos2是必选参数,kw1kw2是可选参数。传递kw1kw2之前必须要传递pos1pos2

In [7]: f(kw1=3, kw2=4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [7], in <cell line: 1>()
----> 1 f(kw1=3, kw2=4)

TypeError: f() missing 2 required positional arguments: 'pos1' and 'pos2'

为了让代码变得易读和高效,有时我们需要限制参数的传递方式,当开发者看到函数的定义时,就能知道哪些参数只能按位置传参,哪些参数既可以使用位置传参也可以使用关键字传参,还有哪些参数只能使用关键字传参。

python中使用 /* 来分割参数中不同性质的参数,其规则如下:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only
  • 定义在/之前的参数只能使用位置传参,参数之间的顺序很重要;
  • 定义在 /* 之间的参数既可以使用位置传参也可以使用关键字传参;
  • 定义在 * 之后的参数只能用关键字传参,参数之间可以颠倒顺序;
  • 不使用 /* 的方法中,参数既可以使用位置传参也可以使用关键字传参。

当然 /* 是可选的,平时我们也比较难以见到这种格式的写法。不过对于某些对参数限制比较严格的项目中会看到这种用法,比如用于在微控制器MCU上进行开发而准备的 MicroPython 项目的某些库中,其对参数的定义就使用这种格式。

# 使用 / 
>>> def pos_only_arg(arg, /):
...     print(arg)
# 此时只能用位置传参
>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'
# 使用 *
>>> def kwd_only_arg(*, arg):
...     print(arg)
# 此时只能用关键字传参
>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3
#使用 / 和 *
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)
# 各种对应情况
>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

python中 * 的用法:

  1. 用作任意位置实参列表:
    调用函数时,把使用频率最低的选项指定为可以接收任意数量位置参数的形参。在函数中传递的实参包含在元组中。在可变数量的实参之前,可能有若干个普通参数:
>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'
  1. 解包位置实参列表:
    与上面的作用相反, * 可以将元组或列表中的参数解包为多个单独的参数。当函数需要独立的位置参数时,可以使用 * 操作符将传入的元组或列表元素拆分。
>>> def foo(a, b):
...     print('a: ', a)
...     print('b: ', b)

>>> l = [1,2]
>>> foo(*l)
a:  1
b:  2

python中 ** 的用法:

与上面 * 在参数中的作用相似, ** 可以用来传递任意数量的关键字参数,也可以用来解包字典为多个关键字参数。

  1. 用作任意关键字实参:
    多个关键字实参传入方法后被包含在字典中。
>>> def foo(**kwargs):
...     print(type(kwargs))
...     for k, v in kwargs.items():
...         print(k, ': ', v)
...

>>> foo(a=1, b='tow', c=[3])
<class 'dict'>
a :  1
b :  tow
c :  [3]
  1. 解包关键字参数列表:
    使用 **可以将一个字典中的键值对,拆分为单独的关键字参数对,用于需要使用独立关键字参数的方法调用。
>>>: def bar(name=None, age=18, male=True):
...     print('name: ', name)
...     print('age:  ', age)
...     print('male: ', male)
...

>>> d = {'name': 'Jack', 'age': 20, 'male': True}

>>> bar(**d)
name:  Jack
age:   20
male:  True

补充:如何解决**kwargs中的关键字参数与未知参数命名冲突?

当定义函数时该函数中有一个位置参数name,同时还可以接收一个任意长度关键字参数。

def foo(name, **kwargs):
    return 'name' in kwargs

如果传递进来的关键字参数中也有一个名为name的参数,此时它就会与未知参数name发生冲突,因为关键字name总是与第一个形参绑定的。

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name

那么如何解决这个问题呢?
这就要用到上面我们提到的 / 特殊分割参数,将name固定位只能通过位置传参的参数:

def foo(name, /, **kwargs):
    return 'name' in kwargs
  
>>> foo(1, **{'name': 2})
True

这是‘name’ 就可以做为关键字参数的键使用了。换句话说,仅限位置形参的名称可以在 **kwargs 中使用,而不产生歧义。