Python技法2:函式引數的進階用法

lonelyprince7 發表於 2021-10-13
Python

1、關鍵字引數(positional argument)和位置引數(keyword argument)

Python函式的引數根據函式在呼叫時(注意,不是函式定義時)傳參的形式分為關鍵字引數和位置引數。

(1)關鍵字引數:

關鍵字引數是指在函式呼叫傳參時,由識別符號(如name=)引導的引數,或者放在一個由**引導的字典裡進行傳遞。如下所示:

complex(real=3, imag=5)
complex(**{'real': 3, 'imag': 5})

(2)位置引數

不是關鍵字引數的引數就是位置引數。它除了單獨傳遞之外,還可以放在一個由*引導的可迭代序列(列表、元組等)裡進行傳遞。如下所示:

complex(3, 5)
complex(*(3, 5))

位置引數總是放在函式的引數列表最前方,關鍵字引數必須要放在位置引數後面。它們之間的位置關係如下所示:

def func(arg1, arg2, kwarg1, kwarg2):
func(1, 2, kwarg1=3, kwarg2=4)

這裡arg1、arg2是位置引數,kwarg1,kwarg2是關鍵字引數。關鍵字引數的key(也就是這裡的'kwarg1=3'中的'kwarg1','kwarg2=4'中的'kwarg2')要保證和形參名稱一致

2、接受任意數量的引數

(1)接受任意數量的位置引數

"*"號表示式除了上一章我們講的用於對任意長度可迭代物件進行拆分之外, 還能在函式定義中使用,用於定義一個可以接受任意數量位置引數的函式,如下所示:

def avg(first, *rest):
    print(rest)  
    return (first + sum(rest)) / (1 + len(rest))
print(avg(1, 2, 3, 4, 5))
# (2, 3, 4, 5)
# 1. 5

"*"開頭的引數必須做為最後一個位置引數使用,且"*"開頭的引數傳進來後是元組資料結構。

(2)接受任意數量的關鍵字引數

想接受任意數量的關鍵字引數,我們可以類似地使用"**"開頭的引數。如下所示:

import html
def make_element(name, value, **attrs) -> str:
    key_values = [ ' %s="%s"' % item for item in attrs.items()]
    attr_str = ''.join(key_values)
    # Perform a string formatting operation. 
    element = '<{name} {attrs}>{value}</{name}>'.format(name=name, attrs=attr_str, value=html.escape(value))
    return  element

res_1 = make_element('item', 'Albatross', size='large', quantity=6)
res_2 = make_element('p', '<spam>') # escape會把這裡'<spam>'中的'<'和'>'替代成安全的序列&lt; &gt;
print(res_1) # <item  size="large" quantity="6">Albatross</item> 
print(res_2) # <p >&lt;spam&gt;</p>

"**"開頭的引數必須做為最後一個關鍵字引數使用,且"**"開頭的引數傳進來後是字典資料結構。

(3)同時接受任意數量的位置引數和關鍵字引數

如果想要函式同時接受任意數量的位置引數和關鍵字引數,只要聯合使用"*"和"**"即可。

def anyargs(*args:tuple, **kwargs:dict):
    print(args)
    print(kwargs)
anyargs(2, 3, 4, 5, time=1, data=2)
# (2, 3, 4, 5)
# {'time': 1, 'data': 2}

3、keyword-only引數

前面說過,"*"打頭的引數只能做為最後一個位置引數,"**"打頭的引數只能做為最後一個關鍵字引數(自然也是最後一個引數),而依此推斷"*"打頭的引數後的引數就必然是關鍵字引數了。

# 出現在*args之後的引數稱為keyword-only引數
# 這兩個例子中y都只能是關鍵字引數,在傳參時要保證key和形參的一致性
def a(x, *args, y):
    print(y)
def b(x, *args, y, **kwargs):
    print(y)

a(4, 6, 7, 8, y=1)
b(4, 6, 7, 3, y=1, data=2, year=3)

# 1
# 1

這樣的引數稱為keyword-only引數,即出現在*args之後的引數只能做為關鍵字引數使用。
我們可以充分利用這一性質,將關鍵字引數放在以*打頭的引數後,或者一個單獨的*之後,強迫函式的呼叫者必須傳關鍵字引數,比如下面這樣:

def recv(max_size, *, block):
    'Receives a message'
    pass

recv(1024, True)  # recv2() takes 1 positional argument but 2 were given
# and missing 1 required keyword-only argument: 'block'
recv(1024, block=True) # OK

這項技術在實際專案中,可以用來為接受任意數量的位置引數的函式來指定關鍵字引數,比如下面這個帶截斷功能的求最小值函式。這裡的clip引數被強迫為必須按照關鍵字引數傳入,而且設定了一個預設值None, 使引數為可選的。如下所示:

def mininum(*values, clip=None):
    m = min(values)
    if clip is not None:
        m = clip if clip > m else m
    return m

res1 = mininum(1, 5, 2, -5, 10)
res2 = mininum(1, 5, 2, -4, 10, clip=0) 
print(res1, res2) # -5, 0

除此之外,keyword-only引數可以提高程式碼可讀性,像下面這種函式寫法:

msg = recv(1024, False) 

如果程式碼的閱讀者不熟悉recv函式的工作方式,那麼可能不太明白這裡的False引數有什麼作用,如果這個函式的呼叫可以寫成下面這樣的話,那就清晰多了(當然,需要這個函式的編寫者最開始就強制函式的使用者這樣寫):

msg = recv(1024, block=False) 

最後,如果函式定義的的時候強制使用了keyword-only引數,那麼當使用者請求幫助資訊時,引數資訊可以很自然地顯現出來:

print(help(recv))
# Help on function recv in module __main__:

# recv(max_size, *_, block)
#     Receives a message                                    

3、可選引數(帶預設值的引數)

要想定義一個可選引數,需要在函式定義中為引數賦值,並保證預設引數出現在引數列表最後。像下面這樣:

def spam(a, b=42):
    print(a, b)
spam(1) # 1, 42
spam(1, 2) # 1, 2                            

如果預設值是可變容器,比如說列表、集合、字典等,需要把None做為預設值:如下所示:

def spam(a, b=None):
    if b is None:
        b = []

警示1: 千萬不能直接像下面這樣寫:

def spam(a, b=[]):

如果像上面那樣寫,那麼就會發生一些你所不期望看到的現象:如果預設值在函式體之外被修改了,那麼這種修改在之後的函式呼叫中仍然陰魂不散,如下面所示:

def spam(a, b=[]):
    print(b)
    return b
x = spam(1)
x.append('oh!')
x.append('no!')
print(x)
spam(1)
# []
# ['oh!', 'no!']
# ['oh!', 'no!']

警示2: 在函式體中,我們常常需要判斷引數是否為None,此處需要使用is運算子,千萬不能直接像下面這樣寫:

def spam(a, b=None):
    if not b:
        b = []

這裡的問題在於:儘管None會被判定為False,可還有其他許多物件(比如長度為0的字串、列表、元組、字典等)也存在這樣的行為。這樣,有很多其他的特定輸入也會被判定為False,然後本該是使用者傳進來的值直接被預設的[]覆蓋掉了。如下所示:

def spam(a, b=None):
    if not b:
        b = []
spam(1) # OK
x = []
spam(1, x) # Oops! x will be overwritten by default []
spam(1, 0) # Oops! 0 will be overwritten by default []
spam(1, '') # Oops! '' will be overwritten by default []

最後,我們再來討論一個非常棘手的問題。我們想要在函式中檢測呼叫者是否對可選引數提供了某個特定值(可以是任意值,None也算)這樣,我們自然就不能用None,0, False當做預設值然後再來做檢測了,因為使用者本身就可能拿它們當做引數。
要解決這個問題,可以像下面這樣寫:

_no_value = object()
def spam(a, b=_no_value):
    if b == _no_value:
        print("No b value supplied")
        return
    print(a, b)
spam(1) # No b value supplied
spam(1, 2) # 1 2
spam(1, None) # 1 None

這裡_no_value是基於object()類建立的一個獨有物件,可以用這個來對使用者提供的引數做檢測,因為使用者幾乎不可能把_no_value物件做為引數輸入(除非使用者傳入的是相同的物件,否則哪怕是object型別的另一個物件都和_no_value物件是不同的)。
這裡簡要說明一下object類,object是Python中幾乎所有物件的基類,object物件沒有任何資料(底層缺少__dict__字典,甚至沒辦法設定任何屬性),它唯一的作用就是用來檢測相等性。

參考文獻

  • [1] https://www.python.org/
  • [2] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O'Reilly Media, Inc.", 2005.