鍍金池/ 教程/ Python/ 函數(shù)
類與對(duì)象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測(cè)試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語(yǔ)言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

函數(shù)

使用 def語(yǔ)句定義函數(shù)是所有程序的基礎(chǔ)。 本章的目標(biāo)是講解一些更加高級(jí)和不常見的函數(shù)定義與使用模式。 涉及到的內(nèi)容包括默認(rèn)參數(shù)、任意數(shù)量參數(shù)、強(qiáng)制關(guān)鍵字參數(shù)、注解和閉包。 另外,一些高級(jí)的控制流和利用回調(diào)函數(shù)傳遞數(shù)據(jù)的技術(shù)在這里也會(huì)講解到。

可接受任意數(shù)量參數(shù)的函數(shù)

問(wèn)題

你想構(gòu)造一個(gè)可接受任意數(shù)量參數(shù)的函數(shù)。

解決方案

為了能讓一個(gè)函數(shù)接受任意數(shù)量的位置參數(shù),可以使用一個(gè)*參數(shù)。例如:

def avg(first, *rest):
    return (first + sum(rest)) / (1 + len(rest))

# Sample use
avg(1, 2) # 1.5
avg(1, 2, 3, 4) # 2.5

在這個(gè)例子中,rest 是由所有其他位置參數(shù)組成的元組。然后我們?cè)诖a中把它當(dāng)成了一個(gè)序列來(lái)進(jìn)行后續(xù)的計(jì)算。

為了接受任意數(shù)量的關(guān)鍵字參數(shù),使用一個(gè)以**開頭的參數(shù)。比如:

import html

def make_element(name, value, **attrs):
    keyvals = [' %s="%s"' % item for item in attrs.items()]
    attr_str = ''.join(keyvals)
    element = '<{name}{attrs}>{value}</{name}>'.format(
                name=name,
                attrs=attr_str,
                value=html.escape(value))
    return element

# Example
# Creates '<item size="large" quantity="6">Albatross</item>'
make_element('item', 'Albatross', size='large', quantity=6)

# Creates '<p>&lt;spam&gt;</p>'
make_element('p', '<spam>')

在這里,attrs 是一個(gè)包含所有被傳入進(jìn)來(lái)的關(guān)鍵字參數(shù)的字典。

如果你還希望某個(gè)函數(shù)能同時(shí)接受任意數(shù)量的位置參數(shù)和關(guān)鍵字參數(shù),可以同時(shí)使用*和**。比如:

def anyargs(*args, **kwargs):
    print(args) # A tuple
    print(kwargs) # A dict

使用這個(gè)函數(shù)時(shí),所有位置參數(shù)會(huì)被放到 args 元組中,所有關(guān)鍵字參數(shù)會(huì)被放到字典 kwargs 中。

討論

一個(gè)*參數(shù)只能出現(xiàn)在函數(shù)定義中最后一個(gè)位置參數(shù)后面,而 *參數(shù)只能出現(xiàn)在最后一個(gè)參數(shù)。 有一點(diǎn)要注意的是,在參數(shù)后面仍然可以定義其他參數(shù)。

def a(x, *args, y):
    pass

def b(x, *args, y, **kwargs):
    pass

這種參數(shù)就是我們所說(shuō)的強(qiáng)制關(guān)鍵字參數(shù),在后面7.2小節(jié)還會(huì)詳細(xì)講解到。

只接受關(guān)鍵字參數(shù)的函數(shù)

問(wèn)題

你希望函數(shù)的某些參數(shù)強(qiáng)制使用關(guān)鍵字參數(shù)傳遞

解決方案

將強(qiáng)制關(guān)鍵字參數(shù)放到某個(gè)參數(shù)或者當(dāng)個(gè)后面就能達(dá)到這種效果。比如:

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

recv(1024, True) # TypeError
recv(1024, block=True) # Ok

利用這種技術(shù),我們還能在接受任意多個(gè)位置參數(shù)的函數(shù)中指定關(guān)鍵字參數(shù)。比如:

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

minimum(1, 5, 2, -5, 10) # Returns -5
minimum(1, 5, 2, -5, 10, clip=0) # Returns 0

討論

很多情況下,使用強(qiáng)制關(guān)鍵字參數(shù)會(huì)比使用位置參數(shù)表意更加清晰,程序也更加具有可讀性。 例如,考慮下如下一個(gè)函數(shù)調(diào)用:

msg = recv(1024, False)

如果調(diào)用者對(duì) recv 函數(shù)并不是很熟悉,那他肯定不明白那個(gè) False 參數(shù)到底來(lái)干嘛用的。 但是,如果代碼變成下面這樣子的話就清楚多了:

msg = recv(1024, block=False)

另外,使用強(qiáng)制關(guān)鍵字參數(shù)也會(huì)比使用 **kwargs 參數(shù)更好,因?yàn)樵谑褂煤瘮?shù) help 的時(shí)候輸出也會(huì)更容易理解:

>>> help(recv)
Help on function recv in module __main__:
recv(maxsize, *, block)
    Receives a message

強(qiáng)制關(guān)鍵字參數(shù)在一些更高級(jí)場(chǎng)合同樣也很有用。 例如,它們可以被用來(lái)在使用 *args 和 **kwargs 參數(shù)作為輸入的函數(shù)中插入?yún)?shù),9.11小節(jié)有一個(gè)這樣的例子。

給函數(shù)參數(shù)增加元信息

問(wèn)題

你寫好了一個(gè)函數(shù),然后想為這個(gè)函數(shù)的參數(shù)增加一些額外的信息,這樣的話其他使用者就能清楚的知道這個(gè)函數(shù)應(yīng)該怎么使用。

解決方案

使用函數(shù)參數(shù)注解是一個(gè)很好的辦法,它能提示程序員應(yīng)該怎樣正確使用這個(gè)函數(shù)。 例如,下面有一個(gè)被注解了的函數(shù):

def add(x:int, y:int) -> int:
    return x + y

python 解釋器不會(huì)對(duì)這些注解添加任何的語(yǔ)義。它們不會(huì)被類型檢查,運(yùn)行時(shí)跟沒(méi)有加注解之前的效果也沒(méi)有任何差距。 然而,對(duì)于那些閱讀源碼的人來(lái)講就很有幫助啦。第三方工具和框架可能會(huì)對(duì)這些注解添加語(yǔ)義。同時(shí)它們也會(huì)出現(xiàn)在文檔中。

>>> help(add)
Help on function add in module __main__:
add(x: int, y: int) -> int
>>>

盡管你可以使用任意類型的對(duì)象給函數(shù)添加注解(例如數(shù)字,字符串,對(duì)象實(shí)例等等),不過(guò)通常來(lái)講使用類或著字符串會(huì)比較好點(diǎn)。

討論

函數(shù)注解只存儲(chǔ)在函數(shù)的 __annotations__ 屬性中。例如:

>>> add.__annotations__
{'y': <class 'int'>, 'return': <class 'int'>, 'x': <class 'int'>}

盡管注解的使用方法可能有很多種,但是它們的主要用途還是文檔。 因?yàn)?python 并沒(méi)有類型聲明,通常來(lái)講僅僅通過(guò)閱讀源碼很難知道應(yīng)該傳遞什么樣的參數(shù)給這個(gè)函數(shù)。 這時(shí)候使用注解就能給程序員更多的提示,讓他們可以爭(zhēng)取的使用函數(shù)。

參考9.20小節(jié)的一個(gè)更加高級(jí)的例子,演示了如何利用注解來(lái)實(shí)現(xiàn)多分派(比如重載函數(shù))。

返回多個(gè)值的函數(shù)

問(wèn)題

你希望構(gòu)造一個(gè)可以返回多個(gè)值的函數(shù)

解決方案

為了能返回多個(gè)值,函數(shù)直接 return 一個(gè)元組就行了。例如:

>>> def myfun():
... return 1, 2, 3
...
>>> a, b, c = myfun()
>>> a
1
>>> b
2
>>> c
3

討論

盡管 myfun() 看上去返回了多個(gè)值,實(shí)際上是先創(chuàng)建了一個(gè)元組然后返回的。 這個(gè)語(yǔ)法看上去比較奇怪,實(shí)際上我們使用的是逗號(hào)來(lái)生成一個(gè)元組,而不是用括號(hào)。比如下面的:

>>> a = (1, 2) # With parentheses
>>> a
(1, 2)
>>> b = 1, 2 # Without parentheses
>>> b
(1, 2)
>>>

當(dāng)我們調(diào)用返回一個(gè)元組的函數(shù)的時(shí)候 ,通常我們會(huì)將結(jié)果賦值給多個(gè)變量,就像上面的那樣。 其實(shí)這就是1.1小節(jié)中我們所說(shuō)的元組解包。返回結(jié)果也可以賦值給單個(gè)變量, 這時(shí)候這個(gè)變量值就是函數(shù)返回的那個(gè)元組本身了:

>>> x = myfun()
>>> x
(1, 2, 3)
>>>

定義有默認(rèn)參數(shù)的函數(shù)

問(wèn)題

你想定義一個(gè)函數(shù)或者方法,它的一個(gè)或多個(gè)參數(shù)是可選的并且有一個(gè)默認(rèn)值。

解決方案

定義一個(gè)有可選參數(shù)的函數(shù)是非常簡(jiǎn)單的,直接在函數(shù)定義中給參數(shù)指定一個(gè)默認(rèn)值,并放到參數(shù)列表最后就行了。例如:

def spam(a, b=42):
    print(a, b)

spam(1) # Ok. a=1, b=42
spam(1, 2) # Ok. a=1, b=2

如果默認(rèn)參數(shù)是一個(gè)可修改的容器比如一個(gè)列表、集合或者字典,可以使用 None 作為默認(rèn)值,就像下面這樣:

# Using a list as a default value
def spam(a, b=None):
    if b is None:
        b = []
    ...

如果你并不想提供一個(gè)默認(rèn)值,而是想僅僅測(cè)試下某個(gè)默認(rèn)參數(shù)是不是有傳遞進(jìn)來(lái),可以像下面這樣寫:

_no_value = object()

def spam(a, b=_no_value):
    if b is _no_value:
        print('No b value supplied')
    ...

我們測(cè)試下這個(gè)函數(shù):

>>> spam(1)
No b value supplied
>>> spam(1, 2) # b = 2
>>> spam(1, None) # b = None
>>>

仔細(xì)觀察可以發(fā)現(xiàn)到傳遞一個(gè) None 值和不傳值兩種情況是有差別的。

討論

定義帶默認(rèn)值參數(shù)的函數(shù)是很簡(jiǎn)單的,但絕不僅僅只是這個(gè),還有一些東西在這里也深入討論下。

首先,默認(rèn)參數(shù)的值僅僅在函數(shù)定義的時(shí)候賦值一次。試著運(yùn)行下面這個(gè)例子:

>>> x = 42
>>> def spam(a, b=x):
...     print(a, b)
...
>>> spam(1)
1 42
>>> x = 23 # Has no effect
>>> spam(1)
1 42
>>>

注意到當(dāng)我們改變 x 的值的時(shí)候?qū)δJ(rèn)參數(shù)值并沒(méi)有影響,這是因?yàn)樵诤瘮?shù)定義的時(shí)候就已經(jīng)確定了它的默認(rèn)值了。

其次,默認(rèn)參數(shù)的值應(yīng)該是不可變的對(duì)象,比如 None、True、False、數(shù)字或字符串。 特別的,千萬(wàn)不要像下面這樣寫代碼:

def spam(a, b=[]): # NO!
    ...

如果你這么做了,當(dāng)默認(rèn)值在其他地方被修改后你將會(huì)遇到各種麻煩。這些修改會(huì)影響到下次調(diào)用這個(gè)函數(shù)時(shí)的默認(rèn)值。比如:

>>> def spam(a, b=[]):
...     print(b)
...     return b
...
>>> x = spam(1)
>>> x
[]
>>> x.append(99)
>>> x.append('Yow!')
>>> x
[99, 'Yow!']
>>> spam(1) # Modified list gets returned!
[99, 'Yow!']
>>>

這種結(jié)果應(yīng)該不是你想要的。為了避免這種情況的發(fā)生,最好是將默認(rèn)值設(shè)為 None, 然后在函數(shù)里面檢查它,前面的例子就是這樣做的。

在測(cè)試 None 值時(shí)使用 is 操作符是很重要的,也是這種方案的關(guān)鍵點(diǎn)。 有時(shí)候大家會(huì)犯下下面這樣的錯(cuò)誤:

def spam(a, b=None):
    if not b: # NO! Use 'b is None' instead
        b = []
    ...

這么寫的問(wèn)題在于盡管 None 值確實(shí)是被當(dāng)成 False, 但是還有其他的對(duì)象(比如長(zhǎng)度為0的字符串、列表、元組、字典等)都會(huì)被當(dāng)做 False。 因此,上面的代碼會(huì)誤將一些其他輸入也當(dāng)成是沒(méi)有輸入。比如:

>>> spam(1) # OK
>>> x = []
>>> spam(1, x) # Silent error. x value overwritten by default
>>> spam(1, 0) # Silent error. 0 ignored
>>> spam(1, '') # Silent error. '' ignored
>>>

最后一個(gè)問(wèn)題比較微妙,那就是一個(gè)函數(shù)需要測(cè)試某個(gè)可選參數(shù)是否被使用者傳遞進(jìn)來(lái)。 這時(shí)候需要小心的是你不能用某個(gè)默認(rèn)值比如 None、 0或者 False值來(lái)測(cè)試用戶提供的值(因?yàn)檫@些值都是合法的值,是可能被用戶傳遞進(jìn)來(lái)的)。 因此,你需要其他的解決方案了。

為了解決這個(gè)問(wèn)題,你可以創(chuàng)建一個(gè)獨(dú)一無(wú)二的私有對(duì)象實(shí)例,就像上面的 _no_value 變量那樣。 在函數(shù)里面,你可以通過(guò)檢查被傳遞參數(shù)值跟這個(gè)實(shí)例是否一樣來(lái)判斷。 這里的思路是用戶不可能去傳遞這個(gè) _no_value 實(shí)例作為輸入。 因此,這里通過(guò)檢查這個(gè)值就能確定某個(gè)參數(shù)是否被傳遞進(jìn)來(lái)了。

這里對(duì) object() 的使用看上去有點(diǎn)不太常見。object 是 python 中所有類的基類。 你可以創(chuàng)建object類的實(shí)例,但是這些實(shí)例沒(méi)什么實(shí)際用處,因?yàn)樗](méi)有任何有用的方法, 也沒(méi)有哦任何實(shí)例數(shù)據(jù)(因?yàn)樗鼪](méi)有任何的實(shí)例字典,你甚至都不能設(shè)置任何屬性值)。 你唯一能做的就是測(cè)試同一性。這個(gè)剛好符合我的要求,因?yàn)槲以诤瘮?shù)中就只是需要一個(gè)同一性的測(cè)試而已。

定義匿名或內(nèi)聯(lián)函數(shù)

問(wèn)題

你想為 sort()操作創(chuàng)建一個(gè)很短的回調(diào)函數(shù),但又不想用def去寫一個(gè)單行函數(shù), 而是希望通過(guò)某個(gè)快捷方式以內(nèi)聯(lián)方式來(lái)創(chuàng)建這個(gè)函數(shù)。

解決方案

當(dāng)一些函數(shù)很簡(jiǎn)單,僅僅只是計(jì)算一個(gè)表達(dá)式的值的時(shí)候,就可以使用 lambda 表達(dá)式來(lái)代替了。比如:

>>> add = lambda x, y: x + y
>>> add(2,3)
5
>>> add('hello', 'world')
'helloworld'
>>>

這里使用的 lambda 表達(dá)式跟下面的效果是一樣的:

>>> def add(x, y):
...     return x + y
...
>>> add(2,3)
5
>>>

lambda 表達(dá)式典型的使用場(chǎng)景是排序或數(shù)據(jù) reduce 等:

>>> names = ['David Beazley', 'Brian Jones',
...         'Raymond Hettinger', 'Ned Batchelder']
>>> sorted(names, key=lambda name: name.split()[-1].lower())
['Ned Batchelder', 'David Beazley', 'Raymond Hettinger', 'Brian Jones']
>>>

討論

盡管 lambda 表達(dá)式允許你定義簡(jiǎn)單函數(shù),但是它的使用是有限制的。 你只能指定單個(gè)表達(dá)式,它的值就是最后的返回值。也就是說(shuō)不能包含其他的語(yǔ)言特性了, 包括多個(gè)語(yǔ)句、條件表達(dá)式、迭代以及異常處理等等。

你可以不使用 lambda 表達(dá)式就能編寫大部分 python 代碼。 但是,當(dāng)有人編寫大量計(jì)算表達(dá)式值的短小函數(shù)或者需要用戶提供回調(diào)函數(shù)的程序的時(shí)候, 你就會(huì)看到 lambda 表達(dá)式的身影了。

匿名函數(shù)捕獲變量值

問(wèn)題

你用 lambda 定義了一個(gè)匿名函數(shù),并想在定義時(shí)捕獲到某些變量的值。

解決方案

先看下下面代碼的效果:

>>> x = 10
>>> a = lambda y: x + y
>>> x = 20
>>> b = lambda y: x + y
>>>

現(xiàn)在我問(wèn)你,a(10)和 b(10)返回的結(jié)果是什么?如果你認(rèn)為結(jié)果是20和30,那么你就錯(cuò)了:

>>> a(10)
30
>>> b(10)
30
>>>

這其中的奧妙在于 lambda 表達(dá)式中的 x 是一個(gè)自由變量, 在運(yùn)行時(shí)綁定值,而不是定義時(shí)就綁定,這跟函數(shù)的默認(rèn)值參數(shù)定義是不同的。 因此,在調(diào)用這個(gè) lambda 表達(dá)式的時(shí)候,x 的值是執(zhí)行時(shí)的值。例如:

>>> x = 15
>>> a(10)
25
>>> x = 3
>>> a(10)
13
>>>

如果你想讓某個(gè)匿名函數(shù)在定義時(shí)就捕獲到值,可以將那個(gè)參數(shù)值定義成默認(rèn)參數(shù)即可,就像下面這樣:

>>> x = 10
>>> a = lambda y, x=x: x + y
>>> x = 20
>>> b = lambda y, x=x: x + y
>>> a(10)
20
>>> b(10)
30
>>>

討論

在這里列出來(lái)的問(wèn)題是新手很容易犯的錯(cuò)誤,有些新手可能會(huì)不恰當(dāng)?shù)?lambda 表達(dá)式。 比如,通過(guò)在一個(gè)循環(huán)或列表推導(dǎo)中創(chuàng)建一個(gè) lambda 表達(dá)式列表,并期望函數(shù)能在定義時(shí)就記住每次的迭代值。例如:

>>> funcs = [lambda x: x+n for n in range(5)]
>>> for f in funcs:
... print(f(0))
...
4
4
4
4
4
>>>

但是實(shí)際效果是運(yùn)行是 n 的值為迭代的最后一個(gè)值?,F(xiàn)在我們用另一種方式修改一下:

>>> funcs = [lambda x, n=n: x+n for n in range(5)]
>>> for f in funcs:
... print(f(0))
...
0
1
2
3
4
>>>

通過(guò)使用函數(shù)默認(rèn)值參數(shù)形式,lambda 函數(shù)在定義時(shí)就能綁定到值。

減少可調(diào)用對(duì)象的參數(shù)個(gè)數(shù)

問(wèn)題

你有一個(gè)被其他 python 代碼使用的 callable 對(duì)象,可能是一個(gè)回調(diào)函數(shù)或者是一個(gè)處理器, 但是它的參數(shù)太多了,導(dǎo)致調(diào)用時(shí)出錯(cuò)。

解決方案

如果需要減少某個(gè)函數(shù)的參數(shù)個(gè)數(shù),你可以使用functools.partial()partial() 函數(shù)允許你給一個(gè)或多個(gè)參數(shù)設(shè)置固定的值,減少接下來(lái)被調(diào)用時(shí)的參數(shù)個(gè)數(shù)。 為了演示清楚,假設(shè)你有下面這樣的函數(shù):

def spam(a, b, c, d):
    print(a, b, c, d)

現(xiàn)在我們使用partial()函數(shù)來(lái)固定某些參數(shù)值:

>>> from functools import partial
>>> s1 = partial(spam, 1) # a = 1
>>> s1(2, 3, 4)
1 2 3 4
>>> s1(4, 5, 6)
1 4 5 6
>>> s2 = partial(spam, d=42) # d = 42
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42
>>> s3(3)
1 2 3 42
>>> s3(4)
1 2 4 42
>>> s3(5)
1 2 5 42
>>>

可以看出 partial() 固定某些參數(shù)并返回一個(gè)新的 callable 對(duì)象。這個(gè)新的 callable 接受未賦值的參數(shù), 然后跟之前已經(jīng)賦值過(guò)的參數(shù)合并起來(lái),最后將所有參數(shù)傳遞給原始函數(shù)。

討論

本節(jié)要解決的問(wèn)題是讓原本不兼容的代碼可以一起工作。下面我會(huì)列舉一系列的例子。

第一個(gè)例子是,假設(shè)你有一個(gè)點(diǎn)的列表來(lái)表示(x,y)坐標(biāo)元組。 你可以使用下面的函數(shù)來(lái)計(jì)算兩點(diǎn)之間的距離:

points = [ (1, 2), (3, 4), (5, 6), (7, 8) ]

import math
def distance(p1, p2):
    x1, y1 = p1
    x2, y2 = p2
    return math.hypot(x2 - x1, y2 - y1)

現(xiàn)在假設(shè)你想以某個(gè)點(diǎn)為基點(diǎn),根據(jù)點(diǎn)和基點(diǎn)之間的距離來(lái)排序所有的這些點(diǎn)。 列表的 sort() 方法接受一個(gè)關(guān)鍵字參數(shù)來(lái)自定義排序邏輯, 但是它只能接受一個(gè)單個(gè)參數(shù)的函數(shù)(distance()很明顯是不符合條件的)。 現(xiàn)在我們可以通過(guò)使用 partial()來(lái)解決這個(gè)問(wèn)題:

>>> pt = (4, 3)
>>> points.sort(key=partial(distance,pt))
>>> points
[(3, 4), (1, 2), (5, 6), (7, 8)]
>>>

更進(jìn)一步,partial()通常被用來(lái)微調(diào)其他庫(kù)函數(shù)所使用的回調(diào)函數(shù)的參數(shù)。 例如,下面是一段代碼,使用 multiprocessing 來(lái)異步計(jì)算一個(gè)結(jié)果值, 然后這個(gè)值被傳遞給一個(gè)接受一個(gè) result 值和一個(gè)可選 logging 參數(shù)的回調(diào)函數(shù):

def output_result(result, log=None):
    if log is not None:
        log.debug('Got: %r', result)

# A sample function
def add(x, y):
    return x + y

if __name__ == '__main__':
    import logging
    from multiprocessing import Pool
    from functools import partial

    logging.basicConfig(level=logging.DEBUG)
    log = logging.getLogger('test')

    p = Pool()
    p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
    p.close()
    p.join()

當(dāng)給 apply_async()提供回調(diào)函數(shù)時(shí),通過(guò)使用partial()傳遞額外的 logging參數(shù)。 而multiprocessing對(duì)這些一無(wú)所知——它僅僅只是使用單個(gè)值來(lái)調(diào)用回調(diào)函數(shù)。

作為一個(gè)類似的例子,考慮下編寫網(wǎng)絡(luò)服務(wù)器的問(wèn)題,socketserver模塊讓它變得很容易。 下面是個(gè)簡(jiǎn)單的 echo 服務(wù)器:

from socketserver import StreamRequestHandler, TCPServer

class EchoHandler(StreamRequestHandler):
    def handle(self):
        for line in self.rfile:
            self.wfile.write(b'GOT:' + line)

serv = TCPServer(('', 15000), EchoHandler)
serv.serve_forever()

不過(guò),假設(shè)你想給 EchoHandler 增加一個(gè)可以接受其他配置選項(xiàng)的 __init__ 方法。比如:

class EchoHandler(StreamRequestHandler):
    # ack is added keyword-only argument. *args, **kwargs are
    # any normal parameters supplied (which are passed on)
    def __init__(self, *args, ack, **kwargs):
        self.ack = ack
        super().__init__(*args, **kwargs)

    def handle(self):
        for line in self.rfile:
            self.wfile.write(self.ack + line)

這么修改后,我們就不需要顯式地在 TCPServer 類中添加前綴了。 但是你再次運(yùn)行程序后會(huì)報(bào)類似下面的錯(cuò)誤:

Exception happened during processing of request from ('127.0.0.1', 59834)
Traceback (most recent call last):
...
TypeError: __init__() missing 1 required keyword-only argument: 'ack'

初看起來(lái)好像很難修正這個(gè)錯(cuò)誤,除了修改 socketserver 模塊源代碼或者使用某些奇怪的方法之外。 但是,如果使用 partial()就能很輕松的解決——給它傳遞 ack參數(shù)的值來(lái)初始化即可,如下:

from functools import partial
serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED:'))
serv.serve_forever()

在這個(gè)例子中,__init__() 方法中的 ack 參數(shù)聲明方式看上去很有趣,其實(shí)就是聲明 ack 為一個(gè)強(qiáng)制關(guān)鍵字參數(shù)。 關(guān)于強(qiáng)制關(guān)鍵字參數(shù)問(wèn)題我們?cè)?.2小節(jié)我們已經(jīng)討論過(guò)了,讀者可以再去回顧一下。

很多時(shí)候 partial() 能實(shí)現(xiàn)的效果,lambda 表達(dá)式也能實(shí)現(xiàn)。比如,之前的幾個(gè)例子可以使用下面這樣的表達(dá)式:

points.sort(key=lambda p: distance(pt, p))
p.apply_async(add, (3, 4), callback=lambda result: output_result(result,log))
serv = TCPServer(('', 15000),
        lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:', **kwargs))

這樣寫也能實(shí)現(xiàn)同樣的效果,不過(guò)相比而已會(huì)顯得比較臃腫,對(duì)于閱讀代碼的人來(lái)講也更加難懂。 這時(shí)候使用partial() 可以更加直觀的表達(dá)你的意圖(給某些參數(shù)預(yù)先賦值)。

將單方法的類轉(zhuǎn)換為函數(shù)

問(wèn)題

你有一個(gè)除 __init__()方法外只定義了一個(gè)方法的類。為了簡(jiǎn)化代碼,你想將它轉(zhuǎn)換成一個(gè)函數(shù)。

解決方案

大多數(shù)情況下,可以使用閉包來(lái)將單個(gè)方法的類轉(zhuǎn)換成函數(shù)。 舉個(gè)例子,下面示例中的類允許使用者根據(jù)某個(gè)模板方案來(lái)獲取到 URL 鏈接地址。

from urllib.request import urlopen

class UrlTemplate:
    def __init__(self, template):
        self.template = template

    def open(self, **kwargs):
        return urlopen(self.template.format_map(kwargs))

# Example use. Download stock data from yahoo
yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
for line in yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v'):
    print(line.decode('utf-8'))

這個(gè)類可以被一個(gè)更簡(jiǎn)單的函數(shù)來(lái)代替:

def urltemplate(template):
    def opener(**kwargs):
        return urlopen(template.format_map(kwargs))
    return opener

# Example use
yahoo = urltemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
for line in yahoo(names='IBM,AAPL,FB', fields='sl1c1v'):
    print(line.decode('utf-8'))

討論

大部分情況下,你擁有一個(gè)單方法類的原因是需要存儲(chǔ)某些額外的狀態(tài)來(lái)給方法使用。 比如,定義 UrlTemplate 類的唯一目的就是先在某個(gè)地方存儲(chǔ)模板值,以便將來(lái)可以在 open()方法中使用。

使用一個(gè)內(nèi)部函數(shù)或者閉包的方案通常會(huì)更優(yōu)雅一些。簡(jiǎn)單來(lái)講,一個(gè)閉包就是一個(gè)函數(shù), 只不過(guò)在函數(shù)內(nèi)部帶上了一個(gè)額外的變量環(huán)境。閉包關(guān)鍵特點(diǎn)就是它會(huì)記住自己被定義時(shí)的環(huán)境。 因此,在我們的解決方案中,opener() 函數(shù)記住了 template 參數(shù)的值,并在接下來(lái)的調(diào)用中使用它。

任何時(shí)候只要你碰到需要給某個(gè)函數(shù)增加額外的狀態(tài)信息的問(wèn)題,都可以考慮使用閉包。 相比將你的函數(shù)轉(zhuǎn)換成一個(gè)類而言,閉包通常是一種更加簡(jiǎn)潔和優(yōu)雅的方案。

帶額外狀態(tài)信息的回調(diào)函數(shù)

問(wèn)題

你的代碼中需要依賴到回調(diào)函數(shù)的使用(比如事件處理器、等待后臺(tái)任務(wù)完成后的回調(diào)等), 并且你還需要讓回調(diào)函數(shù)擁有額外的狀態(tài)值,以便在它的內(nèi)部使用到。

解決方案

這一小節(jié)主要討論的是那些出現(xiàn)在很多函數(shù)庫(kù)和框架中的回調(diào)函數(shù)的使用——特別是跟異步處理有關(guān)的。 為了演示與測(cè)試,我們先定義如下一個(gè)需要調(diào)用回調(diào)函數(shù)的函數(shù):

def apply_async(func, args, *, callback):
    # Compute the result
    result = func(*args)

    # Invoke the callback with the result
    callback(result)

實(shí)際上,這段代碼可以做任何更高級(jí)的處理,包括線程、進(jìn)程和定時(shí)器,但是這些都不是我們要關(guān)心的。 我們僅僅只需要關(guān)注回調(diào)函數(shù)的調(diào)用。下面是一個(gè)演示怎樣使用上述代碼的例子:

>>> def print_result(result):
...     print('Got:', result)
...
>>> def add(x, y):
...     return x + y
...
>>> apply_async(add, (2, 3), callback=print_result)
Got: 5
>>> apply_async(add, ('hello', 'world'), callback=print_result)
Got: helloworld
>>>

注意到 print_result() 函數(shù)僅僅只接受一個(gè)參數(shù) result 。不能再傳入其他信息。 而當(dāng)你想讓回調(diào)函數(shù)訪問(wèn)其他變量或者特定環(huán)境的變量值的時(shí)候就會(huì)遇到麻煩。

為了讓回調(diào)函數(shù)訪問(wèn)外部信息,一種方法是使用一個(gè)綁定方法來(lái)代替一個(gè)簡(jiǎn)單函數(shù)。 比如,下面這個(gè)類會(huì)保存一個(gè)內(nèi)部序列號(hào),每次接收到一個(gè)result的時(shí)候序列號(hào)加1:

class ResultHandler:

    def __init__(self):
        self.sequence = 0

    def handler(self, result):
        self.sequence += 1
        print('[{}] Got: {}'.format(self.sequence, result))

使用這個(gè)類的時(shí)候,你先創(chuàng)建一個(gè)類的實(shí)例,然后用它的 handler() 綁定方法來(lái)做為回調(diào)函數(shù):

>>> r = ResultHandler()
>>> apply_async(add, (2, 3), callback=r.handler)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=r.handler)
[2] Got: helloworld
>>>

第二種方式,作為類的替代,可以使用一個(gè)閉包捕獲狀態(tài)值,例如:

def make_handler():
    sequence = 0
    def handler(result):
        nonlocal sequence
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))
    return handler

下面是使用閉包方式的一個(gè)例子:

>>> handler = make_handler()
>>> apply_async(add, (2, 3), callback=handler)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler)
[2] Got: helloworld
>>>

還有另外一個(gè)更高級(jí)的方法,可以使用協(xié)程來(lái)完成同樣的事情:

def make_handler():
    sequence = 0
    while True:
        result = yield
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))

對(duì)于協(xié)程,你需要使用它的 send()方法作為回調(diào)函數(shù),如下所示:

>>> handler = make_handler()
>>> next(handler) # Advance to the yield
>>> apply_async(add, (2, 3), callback=handler.send)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler.send)
[2] Got: helloworld
>>>

討論

基于回調(diào)函數(shù)的軟件通常都有可能變得非常復(fù)雜。一部分原因是回調(diào)函數(shù)通常會(huì)跟請(qǐng)求執(zhí)行代碼斷開。 因此,請(qǐng)求執(zhí)行和處理結(jié)果之間的執(zhí)行環(huán)境實(shí)際上已經(jīng)丟失了。如果你想讓回調(diào)函數(shù)連續(xù)執(zhí)行多步操作, 那你就必須去解決如何保存和恢復(fù)相關(guān)的狀態(tài)信息了。

至少有兩種主要方式來(lái)捕獲和保存狀態(tài)信息,你可以在一個(gè)對(duì)象實(shí)例(通過(guò)一個(gè)綁定方法)或者在一個(gè)閉包中保存它。 兩種方式相比,閉包或許是更加輕量級(jí)和自然一點(diǎn),因?yàn)樗鼈兛梢院芎?jiǎn)單的通過(guò)函數(shù)來(lái)構(gòu)造。 它們還能自動(dòng)捕獲所有被使用到的變量。因此,你無(wú)需去擔(dān)心如何去存儲(chǔ)額外的狀態(tài)信息(代碼中自動(dòng)判定)。

如果使用閉包,你需要注意對(duì)那些可修改變量的操作。在上面的方案中, nonlocal聲明語(yǔ)句用來(lái)指示接下來(lái)的變量會(huì)在回調(diào)函數(shù)中被修改。如果沒(méi)有這個(gè)聲明,代碼會(huì)報(bào)錯(cuò)。

而使用一個(gè)協(xié)程來(lái)作為一個(gè)回調(diào)函數(shù)就更有趣了,它跟閉包方法密切相關(guān)。 某種意義上來(lái)講,它顯得更加簡(jiǎn)潔,因?yàn)榭偣簿鸵粋€(gè)函數(shù)而已。 并且,你可以很自由的修改變量而無(wú)需去使用nonlocal 聲明。 這種方式唯一缺點(diǎn)就是相對(duì)于其他 Python 技術(shù)而已或許比較難以理解。 另外還有一些比較難懂的部分,比如使用之前需要調(diào)用next() ,實(shí)際使用時(shí)這個(gè)步驟很容易被忘記。 盡管如此,協(xié)程還有其他用處,比如作為一個(gè)內(nèi)聯(lián)回調(diào)函數(shù)的定義(下一節(jié)會(huì)講到)。

如果你僅僅只需要給回調(diào)函數(shù)傳遞額外的值的話,還有一種使用 partial() 的方式也很有用。 在沒(méi)有使用 partial() 的時(shí)候,你可能經(jīng)??吹较旅孢@種使用 lambda 表達(dá)式的復(fù)雜代碼:

>>> apply_async(add, (2, 3), callback=lambda r: handler(r, seq))
[1] Got: 5
>>>

可以參考7.8小節(jié)的幾個(gè)示例,教你如何使用partial()來(lái)更改參數(shù)簽名來(lái)簡(jiǎn)化上述代碼。

內(nèi)聯(lián)回調(diào)函數(shù)

問(wèn)題

當(dāng)你編寫使用回調(diào)函數(shù)的代碼的時(shí)候,擔(dān)心很多小函數(shù)的擴(kuò)張可能會(huì)弄亂程序控制流。 你希望找到某個(gè)方法來(lái)讓代碼看上去更像是一個(gè)普通的執(zhí)行序列。

解決方案

通過(guò)使用生成器和協(xié)程可以使得回調(diào)函數(shù)內(nèi)聯(lián)在某個(gè)函數(shù)中。 為了演示說(shuō)明,假設(shè)你有如下所示的一個(gè)執(zhí)行某種計(jì)算任務(wù)然后調(diào)用一個(gè)回調(diào)函數(shù)的函數(shù)(參考7.10小節(jié)):

def apply_async(func, args, *, callback):
    # Compute the result
    result = func(*args)

    # Invoke the callback with the result
    callback(result)

接下來(lái)讓我們看一下下面的代碼,它包含了一個(gè) Async 類和一個(gè) inlined_async裝飾器:

from queue import Queue
from functools import wraps

class Async:
    def __init__(self, func, args):
        self.func = func
        self.args = args

def inlined_async(func):
    @wraps(func)
    def wrapper(*args):
        f = func(*args)
        result_queue = Queue()
        result_queue.put(None)
        while True:
            result = result_queue.get()
            try:
                a = f.send(result)
                apply_async(a.func, a.args, callback=result_queue.put)
            except StopIteration:
                break
    return wrapper

這兩個(gè)代碼片段允許你使用yield語(yǔ)句內(nèi)聯(lián)回調(diào)步驟。比如:

def add(x, y):
    return x + y

@inlined_async
def test():
    r = yield Async(add, (2, 3))
    print(r)
    r = yield Async(add, ('hello', 'world'))
    print(r)
    for n in range(10):
        r = yield Async(add, (n, n))
        print(r)
    print('Goodbye')

如果你調(diào)用 test() ,你會(huì)得到類似如下的輸出:

5
helloworld
0
2
4
6
8
10
12
14
16
18
Goodbye

你會(huì)發(fā)現(xiàn),除了那個(gè)特別的裝飾器和yield語(yǔ)句外,其他地方并沒(méi)有出現(xiàn)任何的回調(diào)函數(shù)(其實(shí)是在后臺(tái)定義的)。

討論

本小節(jié)會(huì)實(shí)實(shí)在在的測(cè)試你關(guān)于回調(diào)函數(shù)、生成器和控制流的知識(shí)。

首先,在需要使用到回調(diào)的代碼中,關(guān)鍵點(diǎn)在于當(dāng)前計(jì)算工作會(huì)掛起并在將來(lái)的某個(gè)時(shí)候重啟(比如異步執(zhí)行)。 當(dāng)計(jì)算重啟時(shí),回調(diào)函數(shù)被調(diào)用來(lái)繼續(xù)處理結(jié)果。apply_async() 函數(shù)演示了執(zhí)行回調(diào)的實(shí)際邏輯, 盡管實(shí)際情況中它可能會(huì)更加復(fù)雜(包括線程、進(jìn)程、事件處理器等等)。

計(jì)算的暫停與重啟思路跟生成器函數(shù)的執(zhí)行模型不謀而合。 具體來(lái)講,yield 操作會(huì)使一個(gè)生成器函數(shù)產(chǎn)生一個(gè)值并暫停。 接下來(lái)調(diào)用生成器的 __next__()send() 方法又會(huì)讓它從暫停處繼續(xù)執(zhí)行。

根據(jù)這個(gè)思路,這一小節(jié)的核心就在 inline_async() 裝飾器函數(shù)中了。 關(guān)鍵點(diǎn)就是,裝飾器會(huì)逐步遍歷生成器函數(shù)的所有 yield語(yǔ)句,每一次一個(gè)。 為了這樣做,剛開始的時(shí)候創(chuàng)建了一個(gè)result 隊(duì)列并向里面放入一個(gè) None 值。 然后開始一個(gè)循環(huán)操作,從隊(duì)列中取出結(jié)果值并發(fā)送給生成器,它會(huì)持續(xù)到下一個(gè) yield語(yǔ)句, 在這里一個(gè) Async 的實(shí)例被接受到。然后循環(huán)開始檢查函數(shù)和參數(shù),并開始進(jìn)行異步計(jì)算 apply_async() 。 然而,這個(gè)計(jì)算有個(gè)最詭異部分是它并沒(méi)有使用一個(gè)普通的回調(diào)函數(shù),而是用隊(duì)列的put() 方法來(lái)回調(diào)。

這時(shí)候,是時(shí)候詳細(xì)解釋下到底發(fā)生了什么了。主循環(huán)立即返回頂部并在隊(duì)列上執(zhí)行 get()操作。 如果數(shù)據(jù)存在,它一定是put() 回調(diào)存放的結(jié)果。如果沒(méi)有數(shù)據(jù),那么先暫停操作并等待結(jié)果的到來(lái)。 這個(gè)具體怎樣實(shí)現(xiàn)是由 apply_async() 函數(shù)來(lái)決定的。 如果你不相信會(huì)有這么神奇的事情,你可以使用 multiprocessing 庫(kù)來(lái)試一下, 在單獨(dú)的進(jìn)程中執(zhí)行異步計(jì)算操作,如下所示:

if __name__ == '__main__':
    import multiprocessing
    pool = multiprocessing.Pool()
    apply_async = pool.apply_async

    # Run the test function
    test()

實(shí)際上你會(huì)發(fā)現(xiàn)這個(gè)真的就是這樣的,但是要解釋清楚具體的控制流得需要點(diǎn)時(shí)間了。

將復(fù)雜的控制流隱藏到生成器函數(shù)背后的例子在標(biāo)準(zhǔn)庫(kù)和第三方包中都能看到。 比如,在 contextlib中的 @contextmanager 裝飾器使用了一個(gè)令人費(fèi)解的技巧, 通過(guò)一個(gè)yield 語(yǔ)句將進(jìn)入和離開上下文管理器粘合在一起。 另外非常流行的 Twisted包中也包含了非常類似的內(nèi)聯(lián)回調(diào)。

訪問(wèn)閉包中定義的變量

問(wèn)題

你想要擴(kuò)展函數(shù)中的某個(gè)閉包,允許它能訪問(wèn)和修改函數(shù)的內(nèi)部變量。

解決方案

通常來(lái)講,閉包的內(nèi)部變量對(duì)于外界來(lái)講是完全隱藏的。 但是,你可以通過(guò)編寫訪問(wèn)函數(shù)并將其作為函數(shù)屬性綁定到閉包上來(lái)實(shí)現(xiàn)這個(gè)目的。例如:

def sample():
    n = 0
    # Closure function
    def func():
        print('n=', n)

    # Accessor methods for n
    def get_n():
        return n

    def set_n(value):
        nonlocal n
        n = value

    # Attach as function attributes
    func.get_n = get_n
    func.set_n = set_n
    return func

下面是使用的例子:

>>> f = sample()
>>> f()
n= 0
>>> f.set_n(10)
>>> f()
n= 10
>>> f.get_n()
10
>>>

討論

為了說(shuō)明清楚它如何工作的,有兩點(diǎn)需要解釋一下。首先,nonlocal 聲明可以讓我們編寫函數(shù)來(lái)修改內(nèi)部變量的值。 其次,函數(shù)屬性允許我們用一種很簡(jiǎn)單的方式將訪問(wèn)方法綁定到閉包函數(shù)上,這個(gè)跟實(shí)例方法很像(盡管并沒(méi)有定義任何類)。

還可以進(jìn)一步的擴(kuò)展,讓閉包模擬類的實(shí)例。你要做的僅僅是復(fù)制上面的內(nèi)部函數(shù)到一個(gè)字典實(shí)例中并返回它即可。例如:

import sys
class ClosureInstance:
    def __init__(self, locals=None):
        if locals is None:
            locals = sys._getframe(1).f_locals

        # Update instance dictionary with callables
        self.__dict__.update((key,value) for key, value in locals.items()
                            if callable(value) )
    # Redirect special methods
    def __len__(self):
        return self.__dict__['__len__']()

# Example use
def Stack():
    items = []
    def push(item):
        items.append(item)

    def pop():
        return items.pop()

    def __len__():
        return len(items)

    return ClosureInstance()

下面是一個(gè)交互式會(huì)話來(lái)演示它是如何工作的:

>>> s = Stack()
>>> s
<__main__.ClosureInstance object at 0x10069ed10>
>>> s.push(10)
>>> s.push(20)
>>> s.push('Hello')
>>> len(s)
3
>>> s.pop()
'Hello'
>>> s.pop()
20
>>> s.pop()
10
>>>

有趣的是,這個(gè)代碼運(yùn)行起來(lái)會(huì)比一個(gè)普通的類定義要快很多。你可能會(huì)像下面這樣測(cè)試它跟一個(gè)類的性能對(duì)比:

class Stack2:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __len__(self):
        return len(self.items)

如果這樣做,你會(huì)得到類似如下的結(jié)果:

>>> from timeit import timeit
>>> # Test involving closures
>>> s = Stack()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
0.9874754269840196
>>> # Test involving a class
>>> s = Stack2()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
1.0707052160287276
>>>

結(jié)果顯示,閉包的方案運(yùn)行起來(lái)要快大概8%,大部分原因是因?yàn)閷?duì)實(shí)例變量的簡(jiǎn)化訪問(wèn), 閉包更快是因?yàn)椴粫?huì)涉及到額外的 self 變量。

Raymond Hettinger 對(duì)于這個(gè)問(wèn)題設(shè)計(jì)出了更加難以理解的改進(jìn)方案。不過(guò),你得考慮下是否真的需要在你代碼中這樣做, 而且它只是真實(shí)類的一個(gè)奇怪的替換而已,例如,類的主要特性如繼承、屬性、描述器或類方法都是不能用的。 并且你要做一些其他的工作才能讓一些特殊方法生效(比如上面 ClosureInstance中重寫過(guò)的 __len__()實(shí)現(xiàn)。)

最后,你可能還會(huì)讓其他閱讀你代碼的人感到疑惑,為什么它看起來(lái)不像一個(gè)普通的類定義呢? (當(dāng)然,他們也想知道為什么它運(yùn)行起來(lái)會(huì)更快)。盡管如此,這對(duì)于怎樣訪問(wèn)閉包的內(nèi)部變量也不失為一個(gè)有趣的例子。

總體上講,在配置的時(shí)候給閉包添加方法會(huì)有更多的實(shí)用功能, 比如你需要重置內(nèi)部狀態(tài)、刷新緩沖區(qū)、清除緩存或其他的反饋機(jī)制的時(shí)候。