[譯] Python 中的鍵值(具名)引數:如何使用它們

sisibeloved發表於2018-05-02

鍵值引數是 Python 的一個特性,對於從其他程式語言轉到 Python 的人來說,不免看起來有些奇怪。人們在學習 Python 的時候,經常要花很長時間才能理解鍵值引數的各種特性。

在 Python 教學中,我經常希望我能三言兩語就把鍵值引數豐富的相關特性講清楚。但願這篇文章能夠達到這個效果。

在這篇文章中我會解釋鍵值引數是什麼和為什麼要用到它。隨後我會細數一些更為深入的使用技巧,就算老 Python 程式設計師也可能會忽略,因為 Python 3 的最近一些版本變動了許多東西。如果你已經是一個資深的 Python 程式設計師,你可以直接跳到結尾。

什麼是鍵值引數?

讓我們來看看到底什麼是鍵值引數(也叫做具名引數)。

先看看下面這個 Python 函式:

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)
複製程式碼

當我們呼叫這個函式時,我們有兩種不同的方式來傳遞這三個引數。

我們可以像這樣以佔位引數的形式傳值:

>>> quadratic(31, 93, 62)
(-1.0, -2.0)
複製程式碼

或者像這樣以鍵值引數的形式:

>>> quadratic(a=31, b=93, c=62)
(-1.0, -2.0)
複製程式碼

當用佔位方式傳值時,引數的順序至關重要:

>>> quadratic(31, 93, 62)
(-1.0, -2.0)
>>> quadratic(62, 93, 31)
(-0.5, -1.0)
複製程式碼

但是加上引數名就沒關係了:

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

相關文章