[译] Python 中的键值(具名)参数:如何使用它们

键值参数是 Python 的一个特性,对于从其余编程语言转到 Python 的人来讲,难免看起来有些奇怪。人们在学习 Python 的时候,常常要花很长时间才能理解键值参数的各类特性。html

在 Python 教学中,我常常但愿我能三言两语就把键值参数丰富的相关特性讲清楚。希望这篇文章可以达到这个效果。前端

在这篇文章中我会解释键值参数是什么和为何要用到它。随后我会细数一些更为深刻的使用技巧,就算老 Python 程序员也可能会忽略,由于 Python 3 的最近一些版本变更了许多东西。若是你已是一个资深的 Python 程序员,你能够直接跳到结尾。python

什么是键值参数?

让咱们来看看到底什么是键值参数(也叫作具名参数)。android

先看看下面这个 Python 函数:ios

from math import sqrt

def quadratic(a, b, c):
    x1 = -b / (2*a)
    x2 = sqrt(b**2 - 4*a*c) / (2*a)
    return (x1 + x2), (x1 - x2)
复制代码

当咱们调用这个函数时,咱们有两种不一样的方式来传递这三个参数。git

咱们能够像这样以占位参数的形式传值:程序员

>>> quadratic(31, 93, 62)
(-1.0, -2.0)
复制代码

或者像这样以键值参数的形式:github

>>> quadratic(a=31, b=93, c=62)
(-1.0, -2.0)
复制代码

当用占位方式传值时,参数的顺序相当重要:web

>>> quadratic(31, 93, 62)
(-1.0, -2.0)
>>> quadratic(62, 93, 31)
(-0.5, -1.0)
复制代码

可是加上参数名就不要紧了:django

>>> quadratic(a=31, b=93, c=62)
(-1.0, -2.0)
>>> quadratic(c=62, b=93, a=31)
(-1.0, -2.0)
复制代码

当咱们使用键值/具名参数时,有意义的是参数的名字,而不是它的位置:

>>> quadratic(a=31, b=93, c=62)
(-1.0, -2.0)
>>> quadratic(c=31, b=93, a=62)
(-0.5, -1.0)
复制代码

因此不像许多其它的编程语言,Python 知晓函数接收的参数名称。

若是咱们使用帮助函数,Python 会把三个参数的名字告诉咱们:

>>> help(quadratic)
Help on function quadratic in module __main__:

quadratic(a, b, c)
复制代码

注意,能够经过占位和具名混合的方式来调用函数:

>>> quadratic(31, 93, c=62)
(-1.0, -2.0)
复制代码

这样确实很方便,但像咱们写的这个函数使用全占位参数或全键值参数会更清晰。

为何要使用键值参数?

在 Python 中调用函数的时候,你一般要在键值参数和占位参数之间两者择一。使用键值参数可使函数调用更加明确。

看看这段代码:

def write_gzip_file(output_file, contents):
    with GzipFile(None, 'wt', 9, output_file) as gzip_out:
        gzip_out.write(contents)
复制代码

这个函数接收一个 output_file 文件对象和 contents 字符串,而后把一个通过 gzip 压缩的字符串写入输出文件。

下面这段代码作了相同的事,只是用键值参数代替了占位参数:

def write_gzip_file(output_file, contents):
    with GzipFile(fileobj=output_file, mode='wt', compresslevel=9) as gzip_out:
        gzip_out.write(contents)
复制代码

能够看到使用键值参数调用这种方式能够更清楚地看出这三个参数的意义。

咱们在这里去掉了一个参数。第一个参数表明 filename,而且有一个 None 的默认值。这里咱们不须要 filename,由于咱们应该只传一个文件对象或者只传一个文件名给 GzipFile,而不是二者都传。

咱们还能再去掉一个参数。

仍是原来的代码,不过此次压缩率被去掉了,以默认的 9 代替:

def write_gzip_file(output_file, contents):
    with GzipFile(fileobj=output_file, mode='wt') as gzip_out:
        gzip_out.write(contents)
复制代码

由于使用了具名参数,咱们得以去掉两个参数,并把余下 2 个参数以合理的顺序排列(文件对象比『wt』获取模式更重要)。

当咱们使用键值参数时:

  1. 咱们能够去除有默认值的参数
  2. 咱们能够以一种更为可读的方式将参数从新排列
  3. 经过名称调用参数更容易理解参数的含义

哪里能看到键值函数

你能够在 Python 中的不少地方看到键值参数。

Python 有一些接收无限量的占位参数的函数。这些函数有时能够接收用来定制功能的参数。这些参数必须使用具名参数,与无限量的占位参数区分开来。

内置的 print 函数的可选属性 sependfileflush,只能接收键值参数:

>>> print('comma', 'separated', 'words', sep=', ')
comma, separated, words
复制代码

itertools.zip_longest 函数的 fillvalue 属性(默认为 None),一样只接收键值参数:

>>> from itertools import zip_longest
>>> list(zip_longest([1, 2], [7, 8, 9], [4, 5], fillvalue=0))
[(1, 7, 4), (2, 8, 5), (0, 9, 0)]
复制代码

事实上,一些 Python 中的函数强制参数被具名,尽管以占位方式能够清楚地指定。

在 Python 2 中,sorted 函数能够以占位或键值的方式接收参数:

>>> sorted([4, 1, 8, 2, 7], None, None, True)
[8, 7, 4, 2, 1]
>>> sorted([4, 1, 8, 2, 7], reverse=True)
[8, 7, 4, 2, 1]
复制代码

可是 Python 3 中的 sorted 要求迭代器以后的全部参数都以键值的形式指定:

>>> sorted([4, 1, 8, 2, 7], None, True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must use keyword argument for key function
>>> sorted([4, 1, 8, 2, 7], reverse=True)
[8, 7, 4, 2, 1]
复制代码

不只仅是 Python 的内置函数,标准库和第三方库中键值参数一样很常见。

使你的参数具名

经过使用 * 操做符来匹配全部占位参数而后在 * 以后指定可选的键值参数,你能够建立一个接收任意数量的占位参数和特定数量的键值参数的函数。

这儿有个例子:

def product(*numbers, initial=1):
    total = initial
    for n in numbers:
        total *= n
    return total
复制代码

注意:若是你以前没有看过 * 的语法,*numbers 会把全部输入 product 函数的占位参数放到一个 numbers 变量指向的元组。

上面这个函数中的 initial 参数必须以键值形式指定:

>>> product(4, 4)
16
>>> product(4, 4, initial=1)
16
>>> product(4, 5, 2, initial=3)
120
复制代码

注意 initial 有一个默认值。你也能够用这种语法指定必需的键值参数:

def join(*iterables, joiner):
    if not iterables:
        return
    yield from iterables[0]
    for iterable in iterables[1:]:
        yield joiner
        yield from iterable
复制代码

joiner 变量没有默认值,因此它必须被指定:

>>> list(join([1, 2, 3], [4, 5], [6, 7], joiner=0))
[1, 2, 3, 0, 4, 5, 0, 6, 7]
>>> list(join([1, 2, 3], [4, 5], [6, 7], joiner='-'))
[1, 2, 3, '-', 4, 5, '-', 6, 7]
>>> list(join([1, 2, 3], [4, 5], [6, 7]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: join() missing 1 required keyword-only argument: 'joiner'
复制代码

须要注意的是这种把参数放在 * 后面的语法只在 Python 3 中有效。Python 2 中没有要求参数必需要被命名的语法。

只接收键值参数而不接收占位参数

若是你想只接收键值参数而不接收任何占位参数呢?

若是你想接收一个键值参数,而且不打算接收任何 * 占位参数,你能够在 * 后面不带任何字符。

好比这儿有一个修改过的 Django 的 django.shortcuts.render 函数:

def render(request, template_name, context=None, *, content_type=None, status=None, using=None):
    content = loader.render_to_string(template_name, context, request, using=using)
    return HttpResponse(content, content_type, status)
复制代码

与 Django 如今的 render 函数实现不同,这个版本不容许以全部参数都以占位方式指定的方式来调用 rendercontext_typestatususing 参数必须经过名称来指定。

>>> render(request, '500.html', {'error': error}, status=500)
<HttpResponse status_code=500, "text/html; charset=utf-8">
>>> render(request, '500.html', {'error': error}, 500)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: render() takes from 2 to 3 positional arguments but 4 were given
复制代码

就像带有无限制占位参数时的状况同样,这些键值参数也能够是必需的。这里有一个函数,有四个必需的键值参数:

from random import choice, shuffle
UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
LOWERCASE = UPPERCASE.lower()
DIGITS = "0123456789"
ALL = UPPERCASE + LOWERCASE + DIGITS

def random_password(*, upper, lower, digits, length):
    chars = [
        *(choice(UPPERCASE) for _ in range(upper)),
        *(choice(LOWERCASE) for _ in range(lower)),
        *(choice(DIGITS) for _ in range(digits)),
        *(choice(ALL) for _ in range(length-upper-lower-digits)),
    ]
    shuffle(chars)
    return "".join(chars)
复制代码

这个函数要求全部函数都必须以名称指定:

>>> random_password(upper=1, lower=1, digits=1, length=8)
'oNA7rYWI'
>>> random_password(upper=1, lower=1, digits=1, length=8)
'bjonpuM6'
>>> random_password(1, 1, 1, 8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: random_password() takes 0 positional arguments but 4 were given
复制代码

要求参数具名可使函数的调用更加清楚明白。

这样调用函数的意图:

>>> password = random_password(upper=1, lower=1, digits=1, length=8)
复制代码

要比这样调用更为清楚:

>>> password = random_password(1, 1, 1, 8)
复制代码

再强调一次,这种语法只在 Python 3 中适用。

匹配通配键值参数

怎样写出一个匹配任意数量键值参数的函数?

举个例子,字符串格式化方法接收你传递给它的任意键值参数:

>>> "My name is {name} and I like {color}".format(name="Trey", color="purple")
'My name is Trey and I like purple'
复制代码

怎么样才能写出这样的函数?

Python 容许函数匹配任意输入的键值参数,经过在定义函数的时候使用 ** 操做符:

def format_attributes(**attributes):
    """Return a string of comma-separated key-value pairs."""
    return ", ".join(
        f"{param}: {value}"
        for param, value in attributes.items()
    )
复制代码

** 操做符容许 format_attributes 函数接收任意数量的键值参数。输入的参数会被存在一个叫 attributes 的字典里面。

这是咱们的函数的使用示例:

>>> format_attributes(name="Trey", website="http://treyhunner.com", color="purple")
'name: Trey, website: http://treyhunner.com, color: purple'

复制代码

用通配键值参数调用函数

就像你能够定义函数接收通配键值参数同样,你也能够在调用函数时传入通配键值参数。

这就意味着你能够基于字典中的项向函数传递键值参数。

这里咱们从一个字典中手动提取键/值对,并把它们以键值参数的形式传入函数中:

>>> items = {'name': "Trey", 'website': "http://treyhunner.com", 'color': "purple"}
>>> format_attributes(name=items['name'], website=items['website'], color=items['color'])
'name: Trey, website: http://treyhunner.com, color: purple'
复制代码

这种在代码函数调用时将代码写死的方式须要咱们在写下代码的时候就知道所使用的字典中的每个键。当咱们不知道字典中的键时,这种方法就不奏效了。

咱们能够经过 ** 操做符将字典中的项拆解成函数调用时的键值参数,来向函数传递通配键值参数:

>>> items = {'name': "Trey", 'website': "http://treyhunner.com", 'color': "purple"}
>>> format_attributes(**items)
'name: Trey, website: http://treyhunner.com, color: purple'
复制代码

这种向函数传递通配键值参数和在函数内接收通配键值参数(就像咱们以前作的那样)的作法在使用类继承时尤其常见:

def my_method(self, *args, **kwargs):
    print('Do something interesting here')
    super().my_method(*args, **kwargs)  # 使用传入的参数调用父类的方法
复制代码

注意:一样地咱们可使用 * 操做符来匹配和拆解占位参数。

顺序敏感性

自 Python 3.6 起,函数将会保持键值参数传入的顺序(参见 PEP 468)。这意味着当使用 ** 来匹配键值参数时,用来储存结果的字典的键将会与传入参数拥有一样的顺序。

因此在 Python 3.6 以后,你将不会再看到这样的状况:

>>> format_attributes(name="Trey", website="http://treyhunner.com", color="purple")
'website: http://treyhunner.com, color: purple, name: Trey'
复制代码

相应地,使用 Python 3.6+,参数会永远保持传入的顺序:

>>> format_attributes(name="Trey", website="http://treyhunner.com", color="purple")
'name: Trey, website: http://treyhunner.com, color: purple'
复制代码

归纳 Python 中的键值参数

一个参数的位置传达出来的信息一般不如名称有效。所以在调用函数时,若是能使它的意义更清楚,考虑为你的参数赋名。

定义一个新的函数时,不要再考虑哪一个参数应该被指定为键值参数了。使用 * 操做符把这些参数都指定成键值参数。

牢记你可使用 ** 操做符来接受和传递通配键值参数。

重要的对象应该要有名字,你可使用键值参数来给你的对象赋名!

喜欢个人教学风格吗?

想要学习更多关于 Python 的知识?我会经过实时聊天每周分享我喜好的 Python 资源并回答有关 Python 的问题。在下方登记,我会回答你的问题并教你如何让你的 Python 代码更加生动易懂,更加 Python 化。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索