教你使用 Python 中的星号(*)操作符

译自:http://treyhunner.com/2018/10/asterisks-in-python-what-they-are-and-how-to-use-them/?utm_source=newsletter_mailer&utm_medium=email&utm_campaign=weekly

声明:本文章已授权给微信公众号“Python 那些事”独家发布,转载请注明

PS:对原文代码进行了修改,以使代码更加简单

你会在很多地方看到 Python 中使用了 ***。无论对于新手程序员还是从许多可能没有完全等效运算符的编程语言转移过来的人,这两个运算符可能有点神秘。我将讨论这些操作符是什么以及它们的使用方式。

*** 操作符的用法逐年增加,我将讨论目前所有可用的操作符方法。如果你在 Python 2 的时代学到 ***,我建议至少略读这篇文章,因为 Python 3 为这些操作符添加了许多新用途。


我们将要讨论的东西

当我在这篇文章中讨论 *** 时,涉及的是 *** 前缀操作符,而不是中缀操作符。

所以我不是在谈论乘法和取幂:

2 * 5
2 ** 5

我们将要讨论的是 *** 前缀运算符,也就是在一个变量之前使用 *** 操作符。例如:

numbers = [2, 1, 3, 4, 7]
more_numbers = [*numbers, 11, 18]
print(*more_numbers, sep=', ')
2, 1, 3, 4, 7, 11, 18

上述代码中显示了 * 的两种用途,** 的用法还未展示。

*** 的用法包括:

  1. 使用 *** 向函数传递参数
  2. 使用 *** 捕捉传递至函数的参数
  3. 使用 * 接受 keyword-only 参数
  4. 使用 * 捕捉元组解包过程中的项
  5. 使用 * 将可迭代对象解包至列表/元组
  6. 使用 ** 将字典解包至其他字典

即使你认为你已经熟悉了所有这些使用 *** 的方法,我仍建议再看后文的每一个代码块,以确保它们是你熟悉的一切内容。Python 核心开发人员在过去几年里陆续向这些操作符添加了新功能,因此很容易忽略 *** 的一些新用法。


在函数调用中使用 * 解包参数

当调用函数时,* 操作符可用于将可迭代对象解包至函数调用的参数中:

>>> lst = [1, 2, 3, 4]
>>> print(lst[0], lst[1], lst[2], lst[3])
1 2 3 4
>>> print(*lst)
1 2 3 4

print(*fruit) 行将 fruits 列表中的所有项作为独立的参数传递到 print 函数调用中,甚至不需要知道列表中有多少个参数。

这里的 * 操作符不仅仅是语法糖。除非列表的长度是固定的,否则如果没有 *,就不可能将特定的可迭代对象中的所有项作为独立的参数传入函数。

** 操作符类似,但是使用关键字参数。** 操作符允许我们使用键-值对字典,并在函数调用中将其解包为关键字参数。

>>> dic = {'a': 1, 'b': 2, 'c': 3}
>>> string = "{a}-{b}-{c}".format(**dic)
>>> string
'1-2-3'

根据我的经验,使用 ** 将关键字参数解包到函数调用中并不常见。我最常看到的是在实践实施继承时:对 super() 的调用通常包含 ***

在函数调用中,可以多次使用 ***

多次使用 * 有时会很方便:

>>> strlist = ['a', 'b', 'c', 'd']
>>> numbers = [2, 1, 3, 4, 7]
>>> print(*numbers, *strlist)
2 1 3 4 7 a b c d

多次使用 ** 类似:

>>> dic = {'a': 1, 'b': 2, 'c': 3}
>>> dic2 = {'e': "Python", 'f': 'everyday'}
>>> string = "{a}-{b}-{c}-{e}-{f}".format(
...     **dic,
...     **dic2,
... )
>>> string
'1-2-3-Python-everyday'

但是,在多次使用 ** 时需要小心。Python 中的函数不能多次指定相同的关键字参数,因此与 ** 一起使用的每个字典中的键必须是不同的,否则会引发异常。


在函数定义中使用 * 收集参数

在定义函数时,可以使用 * 操作符捕捉传递给函数的无限数量的位置参数。这些参数被捕捉到一个元组中。

def add(*num):
	return sum(num)
>>> add(1)
1
>>> add(1, 2)
3
>>> add(1, 2, 3)
6

* 的这种参数收集用法允许我们创建自己的函数,它可以接受任意数量的参数。

** 操作符还有另一个用法:在定义函数时,我们可以使用 ** 将传递给函数的任何关键字参数捕捉到字典中:

def pack(**kargs):
	for key, value in kargs.items():
		print(key, value)

** 将捕捉我们传递给该函数的任何关键字参数,并将参数引用到字典中。

>>> pack(a=1)
a 1
>>> pack(a=1, b=2)
a 1
b 2
>>> pack(a=1, b=2, c=3)
a 1
b 2
c 3

keyword-only 参数

在 Python 3 中,有一种特殊的语法来接受 keyword-only 参数。Keyword-only 参数是只能使用关键字语法指定的函数参数,这意味着它们不能使用位置指定参数。

为了接受 keyword-only 参数,在定义函数时,我们可以在 *args 参数后,**kargs 参数前加上命名参数:

def kwonly(*args, key, **kargs):    # key 即为 keyword-only 参数
	print(args, key, kargs)
>>> kwonly(1, 2, key=3, d=4, e=5)
(1, 2) 3 {'d': 4, 'e': 5}

如果试图使用位置参数指定 key,那么会引发一个错误:

>>> kwonly(1, 2, 3, d=4, e=5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwonly() missing 1 required keyword-only argument: 'key'

keyword-only 参数的特性很酷,但是如果你希望在不捕获无限位置参数的情况下需要 keyword-only 参数呢?

Python 允许这样做,但它的语法有点奇怪,仅使用一个 ** 后面的所有参数都将作为关键字传递:

def kwonly(a, *, b, c):
	print(a, b, c)
>>> kwonly(1, b=2, c=3)
1 2 3

因此不能不指定关键字:

>>> kwonly(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwonly() takes 1 positional argument but 3 were given

我通常在获取任意数量的位置参数时使用 keyword-only 参数,但我有时使用 * 来强制一个参数只通过位置指定。

Python 的内置 sorted 函数实际上使用了这种方法。如果你查看 sorted 的帮助信息,你将看到以下信息:

>>> help(sorted)
Help on built-in function sorted in module builtins:

sorted(iterable, /, *, key=None, reverse=False)
    Return a new list containing all items from the iterable in ascending order.

    A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.

在元组解包中使用 *

Python 3 还添加了一种使用 * 操作符的新方法,这种方法只与定义函数与调用函数时使用 * 的特性有关。

* 操作符现在可以用于元组解包:

>>> lst = [1, 2, 3, 4]
>>> a, b, *c = lst
>>> c
[3, 4]
>>> a, *c = lst
>>> c
[2, 3, 4]
>>> a, *b, c = lst
>>> b
[2, 3]

通常当我教授 * 的时候,我注意到你只能在一个多赋值调用中使用一个 * 表达式。这在技术上是不正确的,因为可以在嵌套解包中使用两个(这篇文章中讨论了嵌套解包):

lst = ['Python', 'Java', 'PHP', 'C']
((a, *b), *c) = lst
>>> a
'P'
>>> b
['y', 't', 'h', 'o', 'n']
>>> c
['Java', 'PHP', 'C']

我从来没见过它有什么用处,我不认为我会推荐使用它。


在列表常量中使用 *

Python 3.5 通过 PEP 448 引入了大量与 * 相关的新特性。最大的新特性之一是能够使用 * 将可迭代对象转储到新列表中。

假设你有一个函数,它取任意序列并返回一个序列和反转的序列合并的列表:

def revlist(seq):
	return list(seq) + list(reversed(seq))

这个函数需要多次将内容转换为列表,以便连接列表并返回结果。在 Python 3.5 中,我们可以这样输入:

def revlist(seq):
	return [*seq, *reversed(seq)]

这段代码删除了一些不必要的列表调用,因此我们的代码更高效,可读性更好。

这是另一个例子:

def rotate_first_item(seq):
    return [*seq[1:], seq[0]]

这个函数返回一个新列表,其中给定列表(或其他序列)中的第一项移动到新列表的末尾。

使用 * 操作符是将不同类型的可迭代对象连接在一起的好方法。* 操作符适用于任何可迭代对象,而 + 操作符只适用于所有类型都相同的特定序列。

这不仅仅局限于创建列表。我们还可以将可迭代对象转储到新的元组或集合中:

>>> lst = [1, 2, 3, 4]
>>> (*lst[1:], lst[0])
(2, 3, 4, 1)
>>> squared_list = (a ** 2 for a in lst)
>>> {*lst, *squared_list}
{1, 2, 3, 4, 9, 16}

在字典常量中使用 **

PEP 448 还扩展了 ** 的功能,允许该操作符将键/值对从一个字典转储到一个新字典中:

>>> dic = {'a': 1, 'b': 2, 'c': 3}
>>> dic2 = {'e': "Python", 'f': 'everyday'}
>>> all_dic = {**dic, **dic2}
>>> all_dic
{'a': 1, 'b': 2, 'c': 3, 'e': 'Python', 'f': 'everyday'}

这不仅可以用于合并两个字典,还可以复制一个字典,同时添加一个新的值:

>>> add_dic = {**dic, 'g': 4}
>>> add_dic
{'a': 1, 'b': 2, 'c': 3, 'g': 4}

或复制/合并字典,同时重写特定的值:

>>> new_dic = {**dic, 'a': 111}
>>> new_dic
{'a': 111, 'b': 2, 'c': 3}

Python 的 *** 操作符不仅仅是语法糖。它们允许你做的一些事情可以通过其他方式实现,但是替代 *** 的方法往往更麻烦,且需要消耗更多资源。而且没有它们提供的一些特性,一些操作不可能实现:例如,没有 * 的函数无法接受任意数量的位置参数。

如果你不懂 ***,或者你想记住它们的所有用法,不要这样做!这些操作符有很多用途,记住每种操作符的具体用法并不重要,重要的是了解你何时能够使用这些操作符。我建议使用本文作为速查表,或者制作你自己的速查表,以帮助你在 Python 中使用 ***

猜你喜欢

转载自blog.csdn.net/qq_20084101/article/details/83048688