# 第六章 抽象
> 來源:http://www.cnblogs.com/Marlowes/p/5351415.html
> 作者:Marlowes
本章將會介紹如何將語句組織成函數,這樣,你可以告訴計算機如何做事,并且只需要告訴一次。有了函數以后,就不必反反復復像計算機傳遞同樣的具體指令了。本章還會詳細介紹_參數_(parameter)和_作用域_(scope)的概念,以及遞歸的概念及其在程序中的用途。
## 6.1 懶惰即美德
目前為止我們縮寫的程序都很小,如果想要編寫大型程序,很快就會遇到麻煩。考慮一下如果在一個地方編寫了一段代碼,但在另一個地方也要用到這段代碼,這時會發生什么。例如,假設我們編寫了一小段代碼來計算斐波那契數列(任一個數都是前兩數之和的數字序列):
```
fibs = [0, 1] for i in range(8):
fibs.append(fibs[-2] + fibs[-1])
# 運行之后,fibs會包含斐波那契數列的前10個數字:
fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 如果想要以此計算前10個數的話,沒有問題。你甚至可以將用戶輸入的數字作為動態范圍的長度使用,從而改變for語句循環的次數:
fibs = [0, 1]
num = input("How many Fibonacci numbers do you want? ")
for i in range(num - 2):
fibs.append(fibs[-2] + fibs[-1])
print fibs
```
_注:在本例中,讀取字符串可以使用`raw_input`函數,然后再用`int`函數將其轉換為整數。_
但是如果想用這些數字做其他事情呢?當然可以在需要的時候重寫同樣的循環,但是如果已經編寫的是一段復雜的代碼——比如下載一系列網頁并且計算詞頻——應該怎么做呢?你是否希望在每次需要的時候把所有的代碼重寫一遍呢?當然不用,真正的程序員不會這么做的,他們都很懶,但不是用錯誤的方式犯懶,換句話說就是他們不做無用功。
那么真正的程序員怎么做呢?他們會讓自己的程序_抽象_一些。上面的程序可以改寫為比較抽象的版本:
```
num = input("How many numbers do you want? ")
print fibs(num)
```
這個程序的具體細節已經寫的很清楚了(讀入數值,然后打印結果)。事實上計算菲波那切數列是由一種更抽象的方式完成的:只需要告訴計算機去做就好,不用特別說明應該怎么做。名為fibs的函數被創建,然后在需要計算菲波那切數列的地方調用它即可。如果這函數要被調用很多次的話,這么做會節省很多精力。
## 6.2 抽象和結構
抽象可以節省很多工作,實際上它的作用還要更大,它是使得計算機程序可以讓人讀懂的關鍵(這也是最基本的要求,不管是讀還是寫程序)。計算機非常樂于處理精確和具體的指令,但是人可就不同了。如果有人問我去電影院怎么走,估計他不會希望我回答“向前走10步,左轉90度,再走5步右轉45度,走123步”。弄不好就迷路了,對吧?
現在,如果我告訴他“一直沿著街走,過橋,電影院就在左手邊”,這樣就明白多了吧!關鍵在于大家都知道怎么走路和過橋,不需要明確指令來指導這些事。
組織計算機程序也是類似的。程序應該是非常抽象的,就像“下載網頁、計算頻率、打印每個單詞的頻率”一樣易懂。事實上,我們現在就能把這段描述翻譯成Python程序:
```
page = download_page()
freqs = compute_frequencies(page)
for word, freq in freqs:
print word, freq
```
雖然沒有明確地說出它是怎么做的,單讀完代碼就知道程序做什么了。只需要告訴計算機下載網頁并計算詞頻。這些操作的具體指令細節會在其他地方給出——在單獨的_函數定義_中。
## 6.3 創建函數
函數是可以調用的(可能帶有參數,也就是放在圓括號中的值),它執行某種行為并且返回一個值(并非所有Python函數都有返回值)。一般來說,內建的`callable`函數可以用來判斷函數是否可調用:
```
>>> import math >>> x = 1
>>> y = math.sqrt
>>> callable(x)
False
>>> callable(y)
True
```
_注:函數`callable`在Python3.0中不再可用,需要使用表達式`hasattr(func, __call__)`代替,有關`hasattr`的更多信息,請參見第七章。_
就像前一節內容中介紹的,創建函數是組織程序的關鍵。那么怎么定義函數呢?使用`def`(或“函數定義”)語句即可:
```
def hello(name):
return "Hello, " + name + "!"
# 運行這段程序就會得到一個名為hello的新函數,它可以返回一個將輸入的參數作為名字的問候語。可以像使用內建函數一樣使用它:
>>> print hello("world")
Hello, world!
>>> print hello("XuHoo")
Hello, XuHoo!
```
很精巧吧?那么想想看怎么寫個返回斐波那契數列列表的函數吧。簡單!只需要使用剛才的代碼,把從用戶輸入獲取的數字改為作為參數接收數字:
```
num = input("How many numbers do you want? ")
def fibs(num):
result = [0, 1]
for i in range(num - 2):
result.append(result[-2] + resultp[-1])
return result
# 執行這段與語句后,編譯器就知道如何計算斐波那契數列了——所以現在就不用關注細節了,只要用函數fibs就行:
>>> fibs(10)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
>>> fibs(15)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
```
本例中的`num`和`result`的名字都是隨便起的,但是`return`語句非常重要。`return`語句是用來從函數中返回值的(函數可以返回一個以上的值,元組中返回即可)(前例中的`hello`函數也有用到)。
### 6.3.1 文檔化函數
如果想要給函數寫文檔,讓其他使用該函數的人能理解的話,可以加入注釋(以`#`開頭)。另外一個方式就是直接寫上字符串。這類字符串在其他地方可能會非常有用,比如在`def`語句后面(以及在模塊或者類的開頭——有關類的更多內容請參見第七章,有關模塊的更多內容請參見第十章)。如果在函數的開頭寫下字符串,它就會作為函數的一部分進行存儲,這成為_文檔字符串_。下面代碼演示了如何給函數添加文檔字符串:
```
def square(x):
"Calculates the square of the number x."
return x * x
# 文檔字符串可以按如下方式訪問:
>>> square.__doc__
"Calculates the square of the number x."
```
_注:`__doc__`是函數屬性,第七章中會介紹更多關于屬性的知識,屬性名中的雙下劃線表示它是個特殊屬性。這類特殊和“魔法”屬性會在第九章討論。_
內建的`help`函數是非常有用的。在交互式解釋器中使用它,就可以得到關于函數,包括它的文檔字符串的信息:
```
>>> help(square)
Help on function square in module __main__;
square(x)
Calculates the square of the number x.
```
第十章中會再次對`help`函數進行討論。
### 6.3.2 并非真正函數的函數
數學意義上的函數,總在計算其參數后返回點什么。Python的有些函數卻并不返回任何東西。在其他語言中(比如Pascal),這類函數可能有其他名字(比如_過程_)。但是Python的函數就是函數,即便它從學術上講并不是函數。沒有`return`語句,或者雖有`return`語句但`return`后邊沒有跟任何值的函數不返回值:
```
def test():
print "This is printed"
return
print "This is not"
# 這里的return語句只起到結束函數的作用:
>>> x = test()
This is printed
# 可以看到,第2個print語句被跳過了(類似于循環中的break語句,不過這里是跳出函數)。但是如果test不返回任何值,那么x又引用什么呢?讓我們看看:
>>> x
>>>
# 沒東西,再仔細看看:
>>> print x
>>> None
```
好熟悉的值:`None`。所以所有的函數的確都返回了東西:當不需要它們返回值的時候,它們就返回`None`。看來剛才“有些函數并不真的是函數”的說法有些不公平了。
_注:千萬不要被默認行為所迷惑。如果在`if`語句內返回值,那么要確保其他分支也有返回值,這樣一來當調用者期待一個序列的時候,就不會意外地返回`None`。_
## 6.4 參數魔法
函數使用起來很簡單,創建起來也不復雜。但函數參數的用法有時就有些神奇了。還是先從最基礎的介紹起。
### 6.4.1 值從哪里來
函數被定義后,所操作的值是從哪里來的呢?一般來說不用擔心這些,編寫函數只是給程序需要的部分(也可能是其他程序)提供服務,能保證函數在被提供給可接受參數的時候正常工作就行,參數錯誤的話顯然會導致失敗(一般來說這時候要用斷言和異常,第八章會介紹異常)。
_注:寫在`def`語句中函數名后面的變量通常叫做函數的形參,而調用函數的時候提供的值是實參,或者稱為參數。一般來說,本書在介紹的時候對于兩者的區別并不會吹毛求疵。如果這種區別影響較大的話,我會將實參稱為“值”以區別與形參。_
### 6.4.2 我能改變參數嗎
函數通過它的參數獲得一系列值。那么這些值能改變嗎?如果改變了又會怎么樣?參數只是變量而已,所以它們的行為其實和你預想的一樣。在函數內為參數賦予新值不會改變外部任何變量的值:
```
>>> def try_to_change(n):
... n = "Mr. XuHoo"
...
>>> name = "Mr. Marlowes"
>>> try_to_change(name)
>>> name
'Mr. Marlowes'
# 在try_to_change內,參數n獲得了新值,但是它沒有影響到name變量。n實際上是個完全不同的變量,具體的工作方式類似于下面這樣:
>>> name = "Mr. Marlowes"
>>> n = name
# 這句的作用基本上等于傳參數
>>> n = "Mr. XuHoo"
# 在函數內部完成的
>>> name
'Mr. Marlowes'
```
結果是顯而易見的。當變量`n`改變的時候,變量`name`不變。同樣,當在函數內部把參數重綁(賦值)的時候,函數外的變量是不會受到影響的。
_注:參數存儲在局部作用域(local scope)內,本章后面會介紹。_
字符串(以及數字和元組)是_不可變_的,即無法被修改(也就是說只能用新的值覆蓋)。所以它們做參數的時候也就無需多做介紹。但是考慮一下如果將可變的數據結構如列表用作參數的時候會發生什么:
```
>>> def change(n):
... n[0] = "Mr. XuHoo"
...
>>> names = ["Mrs. Marlowes", "Mrs. Something"]
>>> change(names)
>>> names
['Mr. XuHoo', 'Mrs. Something']
```
本例中,參數被改變了。這就是本例和前面例子中至關重要的區別。前面的例子中,局部變量被賦予了新值,但是這個例子中變量`names`所綁定的列表的確變了。有些奇怪吧?其實這種行為并不奇怪,下面不用函數調用再做一次:
```
>>> names = ["Mrs. Marlowes", "Mrs. Something"]
>>> n = names # 再來一次,模擬傳參行為
>>> n[0] = "Mr. XuHoo" # 改變列表
>>> names
['Mr. XuHoo', 'Mrs. Something']
```
這類情況在前面已經出現了多次。當兩個變量同時引用一個列表的時候,它們的確是同時引用一個列表。就是這么簡單。如果想避免出現這種情況,可以復制一個列表的_副本_。當在序列中做切片的時候,返回的切片總是一個副本。因此,如果你復制了_整個列表_的切片,將會得到一個副本:
```
>>> names = ["Mrs. Marlowes", "Mrs. Something"]
>>> n = names[:]
# 現在n和names包含兩個獨立(不同)的列表,其值相等:
>>> n is names
False '
>>> n == names
True
# 如果現在改變n(就像在函數change中做的一樣),則不會影響到names:
>>> n[0] = "Mr. XuHoo"
>>> n
['Mr. XuHoo', 'Mrs. Something']
>>> names
['Mrs. Marlowes', 'Mrs. Something']
# 再用change試一下:
>>> change(names[:]) >>> names
['Mrs. Marlowes', 'Mrs. Something']
```
現在參數`n`包含一個副本,而原始的列表是安全的。
_注:可能有的讀者會發現這樣的問題:函數的局部名稱——包括參數在內——并不和外面的函數名稱(全局的)沖突。關于作用域的更多信息,后面的章節會進行討論。_
1\. 為什么要修改參數
使用函數改變數據結構(比如列表或字典)是一種將程序抽象化的好方法。假設需要編寫一個存儲名字并且能用名字、中間名或姓查找聯系人的程序,可以使用下面的數據結構:
```
storage = {}
storage["first"] = {}
storage["middle"] = {}
storage["last"] = {}
```
`storage`這個數據結構是帶有3個鍵`“first”`、`“middle”`、`“last”`的字典。每個鍵下面都又存儲一個字典。子字典中,可以使用名字(名字、中間名或姓)作為鍵,插入聯系人列表作為值。比如要把我自己的名字加入這個數據結構,可以像下面這么做:
```
>>> me = "Magnus Lie Hetland"
>>> storage["first"]["Magnus"] = [me]
>>> storage["middle"]["Lie"] = [me]
>>> storage["last"]["Hetland"] = [me]
# 每個鍵下面都存儲了一個以人名組成的列表。本例中,列表中只有我。
# 現在如果想要得到所有注冊的中間名為Lie的人,可以像下面這么做:
>>> storage["middle"]["Lie"]
['Magnus Lie Hetland']
```
將人名加到列表中的步驟有點枯燥乏味,尤其是要加入很多姓名相同的人時,因為需要擴展已經存儲了那些名字的列表。例如,下面加入我姐姐的名字,而且假設不知道數據庫中已經存儲了什么:
```
>>> my_sister = "Anne Lie Hetland"
>>> storage["first"].setdefault("Anne", []).append(my_sister)
>>> storage["middle"].setdefault("Lie", []).append(my_sister)
>>> storage["last"].setdefault("Hetland", []).append(my_sister)
>>> storage["first"]["Anne"]
['Anne Lie Hetland'] >>> storage["middle"]["Lie"]
['Magnus Lie Hetland', 'Anne Lie Hetland']
```
如果要寫個大程序來這樣更新列表,那么很顯然程序很快就會變得臃腫且笨拙不堪了。
抽象的要點就是隱藏更新時繁瑣的細節,這個過程可以用函數實現。下面的例子就是初始化數據結構的函數:
```
def init(data):
data["first"] = {}
data["middle"] = {}
data["last"] = {}
# 上面的代碼只是把初始化語句放到了函數中,使用方法如下:
>>> storage = {}
>>> init(storage)
>>> storage
{'middle': {}, 'last': {}, 'first': {}}
```
可以看到,函數包辦了初始化的工作,讓代碼更易讀。
_注:字典的鍵并沒有特定的順序,所以當字典打印出來的時候,順序是不同的。如果讀者在自己的解釋器中打印出的順序不同,請不要擔心,這是很正常的。_
在編寫存儲名字的函數前,先寫個獲得名字的函數:
```
def lookup(data, label, name):
return data[label].get(name)
```
標簽(比如`"middle"`)以及名字(比如`"Lie"`)可以作為參數提供給`lookup`函數使用,這樣會獲得包含全名的列表。換句話說,如果我的名字已經存儲了,可以像下面這樣做:
```
>>> lookup(storage, "middle", "Lie")
['Magnus Lie Hetland']
```
注意,返回的列表和存儲在數據結構中的列表是相同的,所以如果列表被修改了,那么也會影響數據結構(沒有查詢到人的時候就問題不大了,因為函數返回的是`None`)。
```
def store(data, full_name):
names = full_name.split()
if len(name) == 2:
names.insert(1, "")
labels = "first", "middle", "last"
for label, name in zip(labels, names):
people = lookup(data, label, name)
if people:
people.append(full_name) else:
data[label][name] = [full_name]
```
store函數執行以下步驟。
(1) 使用參數`data`和`full_name`進入函數,這兩個參數被設置為函數在外部獲得的一些值。
(2) 通過拆分`full_name`,得到一個叫做`names`的列表。
(3) 如果`names`的長度為`2`(只有首名和末名),那么插入一個空字符串作為中間名。
(4) 將字符串`"first"`、`"middle"`和`"last"`作為元組存儲在`labels`中(也可以使用列表,這里只是為了方便而去掉括號)。
(5) 使用`zip`函數聯合標簽和名字,對于每一個`(label, name)`對,進行一下處理:
?1) 獲得屬于給定標簽和名字的列表;
?2) 將`full_name`添加到列表中,或者插入一個需要的新列表。
來試用一下剛剛實現的程序:
```
>>> MyNames = {} >>> init(MyNames)
>>> store(MyNames, "Magnus Lie Hetland")
>>> lookup(MyNames, "middle", "Lie")
# 好像可以工作,再試試:
>>> store(MyNames, "Robin Hood")
>>> store(MyNames, "Robin Locksley")
>>> lookup(MyNames, "first", "Robin")
['Robin Hood', 'Robin Locks ley']
>>> store(MyNames, "Mr. XuHoo")
>>> lookup(MyNames, "middle", "")
['Robin Hood', 'Robin Locksley', 'Mr. XuHoo']
```
可以看到,如果某些人的名字、中間名或姓相同,那么結果中會包含所有這些人的信息。
_注:這類程序很適合進行面向對象程序設計,下一章內會討論到如何進行面向對象程序設計。_
2.如果我的參數不可變呢
在某些語言(比如C++、Pascal和Ada)中,重新綁定參數并且使這些改變影響到函數外的變量是很平常的事情。但在Python中這是不可能的:函數只能修改參數對象本身。但是如果你的參數不可變(比如是數字),又該怎么辦呢?
不好意思,沒有辦法。這個時候你應該從函數中返回所有你需要的值(如果值多于一個的話就以元組形式返回)。例如,將變量的數值增1的函數可以這樣寫:
```
>>> def inc(x):
return x + 1
...
>>> foo = 10
>>> foo = inc(foo)
>>> foo 11
# 如果真的想改變參數,那么可以使用一點小技巧,即將值放置在列表中:
>>> def inc(x):
x[0] = x[0] + 1
...
>>> foo = [10]
>>> inc(foo)
>>> foo
[11]
```
這樣就會返回新值,代碼看起來也比較清晰。
### 6.4.3 關鍵字參數和默認值
目前為止我們所使用的參數都叫做_位置參數_,因為它們的位置很重要,事實上比它們的名字更加重要。本節中引入的這個功能可以回避位置問題,當你慢慢習慣使用這個功能以后,就會發現程序規模越大,它們的作用也就越大。
```
# 考慮下面的兩個函數:
def hello_1(greeting, name):
print "%s, %s!" % (greeting, name)
def hello_2(name, greeting):
print "%s, %s!" % (name, greeting)
# 兩個代碼所實現的是完全一樣的功能,只是參數順序反過來了:
>>> hello_1("Hello", "world")
Hello, world!
>>> hello_2("Hello", "world")
Hello, world!
# 有些時候(尤其是參數很多的時候),參數的順序是很難記住的。為了讓事情簡單些,可以提供參數的名字:
>>> hello_1(greeting="Hello", name="world")
Hello, world!
# 這樣一來,順序就完全沒影響了:
>>> hello_1(name="world", greeting="Hello")
Hello, world!
# 但參數名和值一定要對應:
>>> hello_2(greeting="Hello", name="world")
world, Hello!
```
這類使用參數名提供的參數叫做_關鍵字參數_。它的主要作用在于可以明確每個參數的作用,也就避免了下面這樣的奇怪的函數調用:
```
>>> store("Mr. Brainsample", 10, 20, 13, 5)
# 可以使用:
>>> store(patient="Mr. Brainsample", hour=10, minut=20, day=13, month=5)
```
盡管這么做打的字就多了些,但是很顯然,每個參數的含義變得更加清晰。而且就算弄亂了參數的順序,對于程序的功能也沒有任何影響。
關鍵字參數最厲害的地方在于可以在函數中給參數提供默認值:
```
def hello_3(greeting="Hello", name="world"):
print "%s, %s!" % (greeting, name)
# 當參數具有默認值的時候,調用的時候就不用提供參數了!可以不提供、提供一些或提供所有的參數:
>>> hello_3()
Hello, world!
>>> hello_3("Greetings")
Greetings, world!
>>> hello_3("Greetings", "universe")
Greetings, universe!
```
可以看到,位置參數這個方法不錯,只是在提供名字的時候同時還要提供問候語。但是如果只想提供`name`參數,而讓`greeting`使用默認值該怎么辦呢?相信此刻你已經猜到了:
```
>>> hello_3(name="XuHoo")
Hello, XuHoo!
```
很簡潔吧?還沒完。位置參數和關鍵字參數是可以聯合使用的。把位置參數放置在前面就可以了。如果不這樣做,解釋器會不知道它們到底是誰(也就是它們應該處的位置)。
_注:除非完全清除程序的功能和參數的意義,否則應該避免混合使用位置參數和關鍵字參數。一般來說,只有在強制要求的參數個數比可修改的具有默認值的參數個數少的時候,才使用上面提到的參數書寫方法。_
例如,`hello`函數可能需要名字作為參數,但是也允許用戶自定義名字、問候語和標點:
```
def hello_4(name, greeting="Hello", punctuation="!"):
print "%s, %s%s" % (greeting, name, punctuation)
# 調用函數的方式很多,下面是其中一些:
>>> hello_4("Mars")
Hello, Mars!
>>> hello_4("Mars", "Howdy")
Howdy, Mars!
>>> hello_4("Mars", "Howdy", "...")
Howdy, Mars...
>>> hello_4("Mars", punctuation=".")
Hello, Mars.
>>> hello_4("Mars", greeting="Top of the morning to ya")
Top of the morning to ya, Mars!
>>> hello_4()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: hello_4() takes at least 1 argument (0 given)
```
_注:如果為`name`也賦予默認值,那么最后一個語句就不會產生異常。_
很靈活吧?我們也不需要做多少工作。下一節中我們可以做得更靈活。
### 6.4.4 收集參數
有些時候讓用戶提供任意數量的參數是很有用的。比如在名字存儲程序中(本章前面“為什么我想要修改參數”一節用到的),用戶每次只能存一個名字。如果能像下面這樣存儲多個名字就更好了:
```
>>> store(data, name1, name2, name3)
# 用戶可以給函數提供任意多的參數。實現起來也不難。
# 試著像下面這樣定義函數:
def print_params(*params):
print params
# 這里我只指定了一個參數,但是前面加上了個星號。這是什么意思?讓我們用一個參數調用函數看看會發生什么:
>>> print_params("XuHoo")
('XuHoo',)
# 可以看到,結果作為元組打印出來,因為里面有個逗號(長度為1的元組有些奇怪,不是嗎)。所以在參數前使用星號就能打印出元組?那么在Params中使用多個參數看看會發生什么:
>>> print_params(1, 2, 3)
(1, 2, 3)
# 參數前的星號將所有值放置在同一個元組中。可以說是將這些值收集起來,然后使用。不知道能不能與普通參數聯合使用。讓我們再寫個函數:
def print_params_2(title, *params):
print title
print params
# 試試看
>>> print_params_2("Params:", 1, 2, 3)
Params:
(1, 2, 3)
# 沒問題!所以星號的意思就是"收集其余的位置參數"。如果不提供任何供收集的元素,params就是個空元組:
>>> print_params_2("Nothing",)
Nothing
()
# 的確如此,很有用。那么能不能處理關鍵字參數(也是參數)呢?
>>> print_params_2("XuHoo", something=19)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: print_params_2() got an unexpected keyword argument 'something'
# 看來不行。所以我們需要另外一個能處理關鍵字參數的“收集操作”。那么語法應該怎么寫呢?會不會是"**"?
def print_params_3(**params):
print params
# 至少解釋器沒有報錯。調用一下看看:
>>> print_params_3(x=1, y=2, z=3)
{'y': 2, 'x': 1, 'z': 3}
# 返回的是字典而不是元組。放一起用用看:
def print_params_4(x, y, z=3, *pospar, **keypar):
print x, y, z
print pospar
print keypar
# 和我們期望的結果別無二致:
>>> print_params_4(1, 2, 3, 4, 5, 6, 7, foo=1, bar=2)
1 2 3
(4, 5, 6, 7)
{'foo': 1, 'bar': 2}
>>> print_params_4(1, 2)
1 2 3
()
{}
```
聯合使用這些功能,可以做的事情就多了。如果你想知道幾種功能聯合起來如何工作(或者說是否允許這么做),那么就自己動手試試看吧(下一節中,會看到`*`和`**`是怎么用來進行函數調用的,不管是否在函數定義中使用)。
現在回到原來的問題上:怎么實現多個名字同時存儲。解決方案如下:
```
def store(data, *full_names):
for full_name in full_names:
names = full_name.split()
if len(names) == 2:
names.insert(1, "")
labels = "first", "middle", "last"
for label, name in zip(labels, names):
people = lookup(data, label, name)
if people:
people.append(full_name) else:
data[label][name] = [full_name]
# 使用這個函數就像上一節中的只接受一個名字的函數一樣簡單:
>>> d = {}
>>> init(d)
>>> store(d, "Han Solo")
# 但是現在可以這樣使用:
>>> store(d, "Luke Skywalker", "Anakin Skywalker")
>>> lookup(d, "last", "Skywalker")
["Luke Skywalker", "Anakin Skywalker"]
```
### 6.4.5 參數收集的逆過程
如何將參數收集為元組和字典已經討論過了,但是事實上,如果使用`*`和`**`的話,也可以執行相反的操作。那么參數收集的逆過程是什么樣?假設有如下函數:
```
def add(x, y):
return x + y
```
_注:`operator`模塊中包含此函數的效率更高的版本。_
比如說有個包含由兩個要相加的數字組成的元組:
```
params = (1, 2)
```
這個過程或多或少有點像我們上一節中介紹的方法的逆過程。不是要收集參數,而是_分配_它們在“另一端”。使用`*`運算符就簡單了——不過是在調用而不是在定義時使用:
```
>>> add(*params)
3
```
對于參數列表來說工作正常,只要擴展的部分是最新的就可以。可以使用同樣的技術來處理字典——使用雙星號運算符。假設之前定義了`hello_3`,那么可以這樣使用:
```
>>> params = {"name":"Sir Robin", "greeting":"Well met"}
>>> hello_3(**params)
Well met, Sir Robin!
```
在定義或調用函數時使用星號(或者雙星號)僅傳遞元組或字典,所以可能沒遇到什么麻煩:
```
>>> def with_stars(**kwds):
... print kwds["name"], "is", kwds["age"], "year old"
...
>>> def without_stars(kwds):
... print kwds["name"], "is", kwds["age"], "year old"
...
>>> args = {"name": "XuHoo", "age": 19}
>>> with_stars(**args)
XuHoo is 19 year old
>>> without_stars(args)
XuHoo is 19 year old
```
可以看到,在`with_stars`中,我在定義和調用函數時都使用了星號。而在without_stars中兩處都沒用,但得到了同樣的效果。所以星號只在定義函數(允許使用不定數目的參數)或者調用(“分割”字典或者序列)時才有用。
_注:使用拼接(Splicing)操作符“傳遞”參數很有用,因為這樣一來就不用關心參數的個數之類的問題,例如:_
```
def foo(x, y, z, m=0, n=0):
print x, y, z, m, n
def call_foo(*args, **kwds):
print "Calling foo!"
foo(*args, **kwds)
```
_在調用超類的構造函數時這個方法尤其有用(請參見第九章獲取更多信息)。_
### 6.4.6 練習使用參數
有了這么多種提供和接受參數的方法,很容易犯暈吧!所以讓我們把這些方法放在一起舉個例子。首先,我定義了一些函數:
```
def story(**kwds):
return "Once upon a time, there was a " \ "%(job)s called %(name)s. " % kwds
def power(x, y, *others):
if others:
print "Received redundant parameters:", others
return pow(x, y)
def interval(start, stop=None, step=1):
"Imitates range() for step > 0"
if stop is None: # 如果沒有為stop指定值······
start, stop = 0, start # 指定參數
result = []
i = start # 計算start索引
while i < stop: # 直到計算到stop的索引
result.append(i) # 將索引添加到result內······
i += step # 用stop(>0)增加索引······
return result # 讓我們試一下:
>>> print story(job="king", name="XuHoo")
Once upon a time, there was a king called XuHoo.
>>> print story(name="Sir Robin", job="brave knight")
Once upon a time, there was a brave knight called Sir Robin.
>>> params = {"job": "language", "name": "Python"}
>>> print story(**params)
Once upon a time, there was a language called Python.
>>> del params["job"]
>>> print story(job="stroke of genius", **params)
Once upon a time, there was a stroke of genius called Python.
>>> power(2, 3) 8
>>> power(3, 2) 9
>>> power(y=3, x=2) 8
>>> params = (5,) * 2
>>> power(*params) 3125
>>> power(3, 3, "Hello, world")
Received redundant parameters: ('Hello, world',) 27
>>> interval(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> interval(1, 5)
[1, 2, 3, 4]
>>> interval(3, 12, 4)
[3, 7, 11]
>>> power(*interval(3, 7))
Received redundant parameters: (5, 6) 81
```
這些函數應該多加練習,加以掌握。
## 6.5 作用域
到底什么是變量?你可以把它們看做是值的名字。在執行`x=1`賦值語句后,名稱`x`引用到值`1`上。這就像用字典一樣,鍵引用值,當然,變量和所對應的值用的是個“不可見”的字典。實際上這么說已經很接近真是情況了。內建的`vars`函數可以返回這個字典:
```
>>> x = 1
>>> scope = vars()
>>> scope["x"]
1
>>> scope["x"] += 1
>>> x
2
```
_注:一般來說,`vars`所返回的字典是不能修改的,因為根據官方Python文檔的說法,結果是未定義的。換句話說,可能得不到想要的結果。_
這類“不可見字典”叫做_命名空間_或者_作用域_。那么到底有多少個命名空間?除了全局作用域外,每個函數調用都會創建一個新的作用域:
```
>>> def foo():
x = 19
...
>>> x = 1
>>> foo()
>>> x
1
```
這里的`foo`函數改變(重綁定)了變量`x`,但是在最后的時候,`x`并沒有變。這是因為當調用`foo`的時候,新的命名空間就被創建了,它作用于`foo`內的代碼塊。賦值語句`x=19`只在內部作用域(_局部_命名空間)起作用,所以它并不影響外部(_全局_)作用域中的`x`。函數內的變量被稱為_局部變量_(local variable,這是與全局變量相反的概念)。參數的工作原理類似于局部變量,所以用全局變量的名字作為參數名并沒有問題。
```
>>> def output(x):
print x
...
>>> x = 1
>>> y = 2
>>> output(y)
2
```
目前為止一切正常。但是如果需要在函數內部訪問全局變量怎么辦呢?而且只想讀取變量的值(也就是說不想重綁定變量),一般來說是沒有問題的:
```
>>> def combine(parameter):
print parameter + external
...
>>> external = "berry"
>>> combine("Shrub")
Shrubberry
```
_注:像這樣引用全局變量是很多錯誤的引發原因。慎重使用全局變量。_
**屏蔽引發的問題**
讀取全局變量一般來說并不是問題,但是還是有個會出問題的事情。如果局部變量或者參數的名字和想要訪問的去全局變量相同的話,就不能直接訪問了。全局變量會被局部變量屏蔽。
如果的確需要的話,可以使用`globals`函數獲取全局變量值,該函數的近親是`vars`,它可以返回全局變量的字典(`locals`返回局部變量的字典)。例如,如果前例中有個叫做`parameter`的全局變量,那么就不能在`combine`函數內部訪問該變量,因為你有一個與之同名的參數。必要時,能使用`globals()["parameter"]`獲取:
```
>>> def combine(parameter):
... print parameter + globals()["parameter"]
...
>>> parameter = "berry"
>>> combine("Shrub")
Shrubberry
```
接下來討論_重綁定_全局變量(使變量引用其他新值)。如果在函數內部將值賦予一個變量,它會自動生成為局部變量——除非告知Python將其聲明為全局變量(注意只有在需要的時候才使用全局變量。它們會讓代碼變得混亂和不靈活。局部變量可以讓代碼更加抽象,因為它們是在函數中“隱藏”的)。那么怎么才能告訴Python這是一個全局變量呢?
```
>>> x = 1
>>> def change_global():
... global x
... x = x + 1 ... >>> change_global() >>> x 2
```
小菜一碟!
**嵌套作用域**
Python的函數是可以嵌套的,也就是說可以將一個函數放在另一個里面(這個話題稍微有點復雜,如果讀者剛剛接觸函數和作用域,現在可以先跳過)。下面是一個例子:
```
>>> def foo():
... def bar():
... print "Hello, world!" ... bar()
```
嵌套一般來說并不是那么有用,但它有一個很突出的應用,例如需要一個函數“創建”另一個。也就意味著可以像下面這樣(在其他函數內)書寫函數:
```
>>> def multiplier(factor):
... def multiplyByFactor(number):
... return number * factor
... return multiplyByFactor
```
一個函數位于另外一個里面,外層函數返回里層函數。也就是說函數本身被返回了,但并沒有被調用。重要的是返回的函數還可以訪問它的定義所在的作用域。換句話說,它“帶著”它的環境(和相關的局部變量)。
每次調用外層函數,它內部的函數都被重新綁定,factor變量每次都有一個新的值。由于Python的嵌套作用域,來自(multiplier的)外部作用域的這個變量,稍后會被內層函數訪問。例如:
```
>>> double = multiplier(2) >>> double(5) 10
>>> triple = multiplier(3) >>> triple(3) 9
>>> multiplier(5)(4) 20
```
類似multiplyByFactor函數存儲子封閉作用域的行為叫做_閉包_(closure)。
外部作用域的變量一般來說是不能進行重新綁定的。但在Python3.0中,nonlocal關鍵字被引入。它和global關鍵字的使用方法類似,可以讓用戶對外部作用域(但并非全局作用域)的變量進行賦值。
## 6.6 遞歸
前面已經介紹了很多關于創建和調用函數的知識。函數也可以調用其他函數。令人驚訝的是函數可以調用_自身_,下面將對此進行介紹。
_遞歸_這個詞對于沒接觸過程序設計的人來說可能會比較陌生。簡單來說就是引用(或調用)自身的意思。來看一個有點幽默的定義:
_recur sion \ri-'k&r-zh&n\ n: see recursion._
_(遞歸[名詞]:見遞歸)。_
遞歸的定義(包括遞歸函數定義)包括它們自身定義內容的引用。由于每個人對遞歸的掌握程度不同。它可能會讓人大傷腦筋,也可能是小菜一碟。為了深入理解它,讀者應該買本計算機科學方面的好書,常用Python解釋器也能幫助理解。
使用“遞歸”的幽默定義來定義遞歸遞歸一般來說是不可行的,因為那樣什么也做不了。我們需要查找遞歸的意思,結果它告訴我們請參見遞歸,無窮盡也。一個類似的函數定義如下:
```
def recursion(): return recursion()
```
顯然它做不了任何事情——和剛才那個遞歸的假定義一樣沒用。運行一下,會發生什么事情?歡迎嘗試:不一會,程序直接就崩潰了(發生異常)。理論上講,它應該永遠運行下去。然而每次調用函數都會用掉一點內存,在足夠的函數調用發生后(在之前的調用返回后),空間就不夠了,程序會以一個“超過最大遞歸深度”的錯誤信息結束。
這類遞歸叫做_無窮遞歸_(infinite recursion),類似于while True開始的_無窮循環_,中間沒有break或return語句。因為(理論上講)它永遠不會結束。我們想要的是能做一些有用的事情的遞歸函數。有用的遞歸函數包含以下幾部分:
a.當函數直接返回值時有基本實例(最小可能性問題);
b._遞歸實例_,包括一個或者多個問題較小部分的遞歸調用。
這里關鍵就是講問題分解為小部分,遞歸不能永遠繼續下去,因為它總是以最小可能性問題結束,而這些問題又存儲在基本實例中,所以才會讓函數調用自身。
但是怎么將其實現呢?做起來沒有看起來這么奇怪。就像我剛才說的那樣,每次函數被調用時,針對這個調用的新命名空間會被創建,意味著當函數調用“自身”時,實際上運行的是兩個不同的函數(或者說是同一個函數具有兩個不同的命名空間)。實際上,可以將它想象成和同種類的一個生物進行對話的另一個生物對話。
### 6.6.1 兩個經典:階乘和冪
本節中,我們會看到兩個經典的遞歸函數。首先,假設想要計算數n的_階乘_。n的階乘定義為 n x (n -1) x (n -2) x ··· x 1。很多數學應用中都會用到它(比如計算將n個人排為一行共有多少種方法)。那么該怎么計算呢?可以使用循環:
```
def factorial(n):
result = n for i in range(1, n):
result *= i return result
```
這個方法可行而且容易實現。它的主要過程是:首先,將result賦值到n上,然后result依次與1~n-1的數相乘,最后返回結果。下面來看看使用遞歸的版本。關鍵在于階乘的數學定義,下面就是:
a.1的階乘是1;
b.大于1的數n的階乘是n乘n-1的階乘。
可以看到,這個定義完全符合剛才所介紹的遞歸的兩個條件。
現在考慮如何將定義實現為函數。理解了定義本身以后,實現其實很簡單:
```
def factorial(n): if n == 1: return 1
else: return n * factorial(n-1)
```
這是定義的直接實現。只要記住函數調用factorial(n)是和調用factorial(n-1)不同的實體就行。
考慮另外一個例子。假設需要計算冪,就像內建的pow函數或者**運算符一樣。可以用很多種方法定義一個數的(整數)冪。先看一個簡單的例子:power(x, n)(x為n的冪次)是x自乘n-1次的結果(所以x用作乘數n次)。所以power(2, 3)是2乘以自身兩次:2 x 2 x 2 = 8。
實現很簡單:
```
def power(x, n):
result = 1
for i in range(n):
result *= x return result
```
程序很小巧,接下來把它改編為遞歸版本:
a.對于任意數字來說,power(x, 0)是1;
b.對于任何大于0的數來說,power(x, n)是x乘以(x, n-1)的結果。
同樣,可以看到這與簡單版本的遞歸定義的結果相同。
理解定義是最困難的部分——實現起來就簡單了:
```
def power(x, n): if n == 0: return 1
else: return x * power(x, n-1)
```
文字描述的定義再次被轉換為了程序語言(Python代碼)。
注:如果函數或算法很復雜而且難懂的話,在實現前用自己的話明確地定義一下是很有幫助的。這類使用“準程序語言”編寫的程序稱為_偽代碼。_
那么遞歸有什么用呢?就不能用循環代替嗎?答案是肯定的,在大多數情況下可以使用循環,而且大多數情況下還會更有效率(至少會高一些)。但是在多數情況下,遞歸更加易讀,有時會大大提高可讀性,尤其當讀程序的人懂得遞歸函數的定義的時候。盡管可以避免編寫使用遞歸的程序,但作為程序員來說還是要理解遞歸算法以及其他人寫的遞歸程序,這也是最基本的。
### 6.2.2 另外一個經典:二分法查找
作為遞歸實踐的最后一個例子,來看看這個叫做二分法查找(binary search)的算法例子。
你可能玩過一個游戲,通過詢問20個問題,被詢問者回答是或不是,然后猜測別人在想什么。對于大多數問題來說,都可以將可能性(或多或少)減半。比如已經知道答案是個人,那么可以問“你是不是在想一個女人”,很顯然,提問者不會上來就問“你是不是在想約翰·克里斯”——除非提問者會讀心術。這個游戲的數學班就是猜數字。例如,被提問者可能在想一個1~100的數字,提問者需要猜中它。當然,提問者可以耐心地猜上100次,但是真正需要才多少次呢?
答案就是只需要問7次即可。第一個問題類似于“數字是否大于50”,如果被提問者回答說數字大于50,那么就問“是否大于75”,然后繼續將滿足條件的值=等分(排除不滿足條件的),直到找到正確答案。這個不需要太多考慮就能解答出來。
很多其他問題上也能用同樣的方法解決。一個很普遍的問題就是查找一個數字是否存在于一個(排過序)的序列中,還要找到具體位置。還可以使用同樣的過程。“這個數字是否存在序列正中間的右邊”,如果不是的話,“那么是否在第二個1/4范圍內(左側靠右)”,然后這樣繼續下去。提問者對數字可能存在的位置上下限心里有數,然后每個問題繼續切分可能的距離。
這個算法的本身就是遞歸的定義,亦可用遞歸實現。讓我們首先重看定義,以保證知道自己在做什么:
a.如果上下限相同,那么就是數字所在的位置,返回;
b.否則找到兩者的中點(上下限的平均值),查找數字是在左側還是在右側,繼續查找數字所在的那半部分。
這個遞歸例子的關鍵就是順序,所以當找到中間元素的時候,只需要比較它和所查找的數字,如果查找數字較大,那么該數字一定在右側,反之則在左側。遞歸部分就是“繼續查找數字所在的那半部分”,因為搜索的具體實現可能會和定義中完全相同。(注意搜索的算法返回的是數字應該在的位置——如果它本身不在序列中,那么所返回位置上的其實就是其他數字)
下面來實現一個二分法查找:
```
def search(sequence, number, lower, upper): if lower == upper: assert number == sequence[upper] return upper else:
middle = (lower + upper) // 2
if number > sequence[middle]: return search(sequence, number, middle+1, upper) else: return search(sequence, number, lower, middle)
```
完全符合定義。如果lower==upper,那么返回upper,也就是上限。注意,程序假設(斷言)所查找的數字一定會被找到(number==sequence[upper])。如果沒有到達基本實例,先找到middle,檢查數字是在左邊還是在右邊,然后使用新的上下限繼續調用遞歸過程。也可以將限制設為可選以方便用。只要在函數定義的開始部分加入下面的條件語句即可:
```
def search(sequence, number, lower=0, upper=None): if upper is None: upper = len(sequence) - 1 ······
```
如果現在不提供限制,程序會自動設定查找范圍為整個序列,看看行不行:
```
>>> seq = [34, 67, 8, 123, 4, 100, 95] >>> seq.sort() >>> seq
[4, 8, 34, 67, 95, 100, 123] >>> search(seq, 34) 2
>>> search(seq, 100) 5
```
但不必這么麻煩,一則可以直接使用列表方法index,如果想要自己實現的話,只要從程序的開始處循環迭代知道找到數字就行了。
當然可以,使用index沒問題。但是只使用循環可能效率有點低。剛才說過查找100內的一個數(或位置),只需要7個問題即可。用循環的話,在最糟糕的情況下要問100個問題。“沒什么大不了的”,有人可能會這樣想。但是如果列表有100 000 000 000 000 000 000 000 000 000 000 000個元素,要么循環多次(可能對于Python的列表來說這個大小有些不現實),就“有什么大不了的”了。二分查找法只需要117個問題。很有效吧?(事實上,可觀測到的宇宙內的粒子總數是10**87,也就是說只要290個問題就能分辨它們了!)
_注:標準庫中的bisect模塊可以非常有效地實現二分查找。_
**函數式編程**
到現在為止,函數的使用方法和其他對象(字符串、數值、序列,等等)基本上一樣,它們可以分配給變量、作為參數傳遞以及從其他函數返回。有些編程語言(比如Scheme或者LISP)中使用函數幾乎可以完成所有的事情,盡管在Python(經常會創建自定義的對象——下一章會講到)中不用那么倚重函數,但也可以進行函數式程序設計。
Python在應對這類“函數式編程”方面有一些有用的函數:map、filter和reduce函數(Python3.0中這些都被移至functools模塊中(除此之外還有apply函數。但這個函數被前面講到的拼接操作符所取代))。map和filter函數在目前版本的Python中并不是特別有用,并且可以使用列表推導式代替。不過讀者可以使用map函數將序列中的元素全部傳遞給一個函數:
```
>>> map(str, range(10)) # Equivalent to [str(i) for i in range(10)]
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
```
filter函數可以基于一個返回布爾值的函數對元素進行過濾。
```
>>> def func(x):
... return x.isalnum()
... >>> seq = ["foo", "x41", "?!", "***"] >>> filter(func, seq)
['foo', 'x41']
```
本例中,使用列表推導式可以不用專門定義一個函數:
```
>>> [x for x in seq if x.isalnum()]
['foo', 'x41']
```
事實上,還有個叫做lambda表達式的特性,可以創建短小的函數("lambda"來源于希臘字母,在數學中表示匿名函數)。
```
>>> filter(lambda x: x.isalnum, seq)
['foo', 'x41']
```
還是列表推導式更易讀吧?
reduce函數一般來說不能輕松被列表推導式代替,但是通常用不到這個功能。它會將序列的前兩個元素與給定的函數聯合使用,并且將它們的返回值和第3個元素繼續聯合使用,直到整個序列都處理完畢,并且得到一個最終結果。例如,需要計算一個序列的數字的和,可以使用reduce函數加上lambda x,y: x+y(繼續使用相同的數字)(事實上,不是使用lambda函數,而是在operator模塊引入每個內建運算符的add函數。使用operator模塊中的函數通常比用自己的函數更有效率):
```
>>> numbers = [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33] >>> reduce(lambda x,y: x+y, numbers) 1161
```
當然,這里也可以使用內建函數sum。
## 6.7 小結
本章介紹了關于抽象的常見知識以及函數的特殊知識。
? 抽象:抽象是隱藏多余細節的藝術。定義處理細節的函數可以讓程序更抽象。
? 函數定義:函數使用def語句定義。它們是由語句組成的塊,可以從“外部世界”獲取值(參數),也可以返回一個或多個值作為運算的結果。
? 參數:函數從參數中得到需要的信息。也就是函數調用時設定的變量。Python中有兩類參數:位置參數和關鍵字參數。參數在給定默認值時是可選的。
? 作用域:變量存儲在作用域(也叫做命名空間)中。Python中有兩類主要的作用域——全局作用域和局部作用域。作用域可以嵌套。
? 遞歸:函數可以調用自身,如果它這么做了就叫遞歸。一切用遞歸實現的功能都可以用循環實現,但是有些時候遞歸函數更易讀。
? 函數式編程:Python有一些進行函數性編程的機制。包括lambda表達式以及map、filter和reduce函數。
### 6.7.1 本章的新函數
本章涉及的新函數如表6-1所示。
**表6-1 本章的新函數**
```
map(func, seq[, seq, ...]) 對序列中的每個元素應用函數
filter(func, seq) 返回其函數為真的元素的列表
reduce(func, seq[, initial]) 等同于func(func(func(seq[0], seq[1]), seq[2]), ...)
sum(seq) 返回seq中所有元素的和
apply(func[, args[, kwargs]]) 調用函數,可以提供參數
```
### 6.7.2 接下來學什么
下一章會通過面向對象程序設計,把抽象提升到一個新高度。你將學到如何創建自定義對象的類型(或者說類),和Python提供的類型(比如字符串、列表和字典)一起使用,以及如何利用這些知識編寫出運行更快、更清晰的程序。如果你真正掌握了下一章的內容,編寫大型程序會毫不費力。