[譯] 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索