>>> from env_helper import info; info()
页面更新时间: 2023-04-18 20:35:14
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-7-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

8.4. 收集与使用参数

有时候,允许用户提供任意数量的参数很有用。例如,在本章前面的姓名存储示例中,每次只能存储一个姓名。如果能够像下面这样同时存储多个姓名就好了:

>>> store(data, name1, name2, name3)

为此,应允许用户提供任意数量的姓名。实际上,这实现起来并不难。 请尝试使用下面这样的函数定义:

>>> def print_params(*params):
>>>     print(params)

这里好像只指定了一个参数,但它前面有一个星号。这是什么意思呢?尝试使用一个参数来 调用这个函数,看看结果如何。

>>> print_params('Testing')
('Testing',)

注意到打印的是一个元组,因为里面有一个逗号。这么说,前面有星号的参数将被放在元组 中?复数params应该提供了线索。

>>> print_params(1, 2, 3)
(1, 2, 3)

参数前面的星号将提供的所有值都放在一个元组中,也就是将这些值收集起来。这样的行为 我们之前就见过:赋值时带星号的变量收集多余的值。它收集的是列表而不是元组中多余的值,但除此之外,这两种用法很像。下面再来编写一个函数:

>>> def print_params_2(title, *params):
>>>     print(title)
>>>     print(params)

并尝试调用它:

>>> print_params_2('Params:', 1, 2, 3)
Params:
(1, 2, 3)

因此星号意味着收集余下的位置参数。如果没有可供收集的参数,params将是一个空元组。

>>> print_params_2('Nothing:')
Nothing:
()

与赋值时一样,带星号的参数也可放在其他位置(而不是最后),但不同的是,在这种情况下你需要做些额外的工作:使用名称来指定后续参数。

>>> def in_the_middle(x, *y, z):
>>>     print(x, y, z)
>>> in_the_middle(1, 2, 3, 4, 5, z=7)
1 (2, 3, 4, 5) 7
in_the_middle(1, 2, 3, 4, 5, 7)

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: in_the_middle() missing 1 required keyword-only argument: 'z'

星号不会收集关键字参数。

>>> print_params_2('Hmm...', something=42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: print_params_2() got an unexpected keyword argument 'something'

要收集关键字参数,可使用两个星号。

>>> def print_params_3(**params):
>>>     print(params)
>>> print_params_3(x=1, y=2, z=3)
{'x': 1, 'y': 2, 'z': 3}

如你所见,这样得到的是一个字典而不是元组。可结合使用这些技术。

>>> def print_params_4(x, y, z=3, *pospar, **keypar):
>>>     print(x, y, z)
>>>     print(pospar)
>>>     print(keypar)

其效果与预期的相同。

>>> print_params_4(1, 2, 3, 5, 6, 7, foo=1, bar=2)
1 2 3
(5, 6, 7)
{'foo': 1, 'bar': 2}
>>> print_params_4(1, 2)
1 2 3
()
{}

现在回到最初的问题:如何在姓名存储示例中使用这种技术?解决方案如下:

>>> def lookup(data, label, name):
>>>     if data.get(label):
>>>         if data.get(label).get(name):
>>>             return data.get(label)
>>>         else:
>>>             return False
>>>     else:
>>>         return False
>>> def store(data, *full_names):
>>>     for full_name in full_names:
>>>         names = full_name.split()
>>>         if len(names) == 2:
>>>             names.insert(1, '')
>>>             labels = 'first', 'middle', 'last'
>>>             for label, name in zip(labels, names):
>>>                 people = lookup(data, label, name)
>>>                 if people:
>>>                     people.append(full_name)
>>>                 else:
>>>                     data[label][name] = [full_name]

这个函数调用起来与只接受一个姓名的前一版一样容易。

>>> d = {}
>>> # init(d)
>>> store(d, 'Han Solo')
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

Cell In [44], line 3
      1 d = {}
      2 # init(d)
----> 3 store(d, 'Han Solo')


Cell In [42], line 12, in store(data, *full_names)
     10     people.append(full_name)
     11 else:
---> 12     data[label][name] = [full_name]


KeyError: 'first'

但现在你也可以这样做:

>>> store(d, 'Luke Skywalker', 'Anakin Skywalker')
>>> lookup(d, 'last', 'Skywalker')

8.4.1. 分配参数

前面介绍了如何将参数收集到元组和字典中,但用同样的两个运算符( *** )也可执行相 反的操作。 与收集参数相反的操作是什么呢?假设有如下函数:

>>> def add(x, y):
>>>     return x + y

注意 模块operator提供了这个函数的高效版本。

同时假设还有一个元组,其中包含两个你要相加的数。

>>> params = (1, 2)

这与前面执行的操作差不多是相反的:不是收集参数,而是分配参数。这是通过在调用函数时使用运算符*实现的。

>>> add(*params)
3

这种做法也可用于参数列表的一部分,条件是这部分位于参数列表末尾。通过使用运算符**, 可将字典中的值分配给关键字参数。如果你像前面那样定义了函数hello_3,就可像下面这样做:

>>> params = {'name': 'Sir Robin', 'greeting': 'Well met'}
>>> hello_3(**params)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

Cell In [48], line 2
      1 params = {'name': 'Sir Robin', 'greeting': 'Well met'}
----> 2 hello_3(**params)


NameError: name 'hello_3' is not defined

如果在定义和调用函数时都使用***,将只传递元组或字典。因此还不如不使用它们,还可省却些麻烦。

>>> def with_stars(**kwds):
>>>     print(kwds['name'], 'is', kwds['age'], 'years old')
>>> def without_stars(kwds):
>>>     print(kwds['name'], 'is', kwds['age'], 'years old')
>>> args = {'name': 'Mr. Gumby', 'age': 42}
>>> with_stars(**args)
>>> without_stars(args)

如你所见,对于函数with_stars,我在定义和调用它时都使用了星号,而对于函数without_ stars,我在定义和调用它时都没有使用,但这两种做法的效果相同。因此,只有在定义函数(允许可变数量的参数)或调用函数时(拆分字典或序列)使用,星号才能发挥作用。

提示 使用这些拆分运算符来传递参数很有用,因为这样无需操心参数个数之类的问题,如下所示:

def foo(x, y, z, m=0, n=0):
    print(x, y, z, m, n)
def call_foo(*args, **kwds):
    print("Calling foo!")
foo(*args, **kwds)

这在调用超类的构造函数时特别有用。

8.4.2. 练习使用参数

面对如此之多的参数提供和接受方式,很容易犯晕。下面来看一个综合示例。首先来定义一些函数。

>>> def story(**kwds):
>>>     return 'Once upon a time, there was a ' \
>>> '{job} called {name}.'.format_map(kwds)
>>> def power(x, y, *others):
>>>     if others:
>>>         print('Received redundant parameters:', others)
>>>     return pow(x, y)
>>>
>>> def interval(start, stop=None, step=1):
>>>     'Imitates range() for step > 0'
>>>
>>>     if stop is None: # 如果没有给参数stop指定值,
>>>         start, stop = 0, start # 就调整参数start和stop的值
>>>     result = []
>>>
>>>
>>>     i = start # 从start开始往上数
>>>
>>>     while i < stop: # 数到stop位置
>>>         result.append(i) # 将当前数的数附加到result末尾
>>>         i += step # 增加到当前数和step(> 0)之和
>>>     return result

下面来尝试调用这些函数。

>>> print(story(job='king', name='Gumby'))
Once upon a time, there was a king called Gumby.
>>> print(story(name='Sir Robin', job='brave knight'))
Once upon a time, there was a brave knight called Sir Robin.
>>> params = {'job': 'language', 'name': 'Python'}
>>> print(story(**params))
Once upon a time, there was a language called Python.
>>> del params['job']
>>> print(story(job='stroke of genius', **params))
Once upon a time, there was a stroke of genius called Python.
>>> power(2, 3)
8
>>> power(3, 2)
9
>>> power(y=3, x=2)
8
>>> params = (5,) * 2
>>> power(*params)
3125
>>> power(3, 3, 'Hello, world')
Received redundant parameters: ('Hello, world',)
27
>>> interval(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> interval(1, 5)
[1, 2, 3, 4]
>>> interval(3, 12, 4)
[3, 7, 11]
>>> power(*interval(3, 7))
Received redundant parameters: (5, 6)
81