Python 第四部分 函數
目錄
內置函數apply (將在python3.0中消失). 22
第15章 函數基礎
函數作用
最大化的代碼重用和最小化代碼冗餘
流程的分解
Def語句是實時執行的
Python中所有的語句都是實時運行的,沒有的獨立的編譯時間,
Def是一個語句 ,一個def可以出現在任一語句可以出現的地方—甚至是嵌套在其他的語句中,儘管def往往是包含在模塊文件中,並在 模塊導入時運行,函數還是可以通過嵌套在if語句中去實現不同的函數定義,這是完全合法的。
If test: Def func(): … Else: Def func(): … … Func() |
函數賦值給一個不同的變量名, 並通過新的變量名進行了調用。就像python中其他語句的一樣,函數僅僅是對象,在程序執行時它清楚 地記錄在內存之中。
>>> def times(x, y):
returnx*y
>>> times(2,3)
6
>>> times('ab', 3)
'ababab'
>>>
Python中的多態
如果傳遞的對象不支持這種預期的接口,python將會在*表達式運行時檢測到錯誤,並自動拋出一個異常。
因此編寫代碼錯誤進行檢查是沒有意義的。
在python中代碼不應該關心特定的數據類型。
第二個例子,尋找序列的交集
>>>def intersect(seq1, seq2):
res = []
for x in seq1:
if x in seq2:
res.append(x)
return res
>>>s1, s2 = 'SPAM', 'SCAM'
>>>intersect(s1, s2)
['S','A', 'M']
>>>x = intersect([1, 2, 3], (1, 4)) #函數intersect傳遞了不同類型的對象,一個列表和一個元組(混合類型)
>>>x
[1]
對於intersect函數,這意味着第一個參數必須支持for循環,並且第二個參數支持成員測試。
需要強調的是:如果我們傳入了不支持這些接口的對象(例如,數字), python將會自動檢測 出不匹配,並拋出一個異常—這正是我們所想要的,如果我們希望明確地編寫類型檢測的話,我們利用它來自己實現.通過 不編寫類型測試,並且允許python檢測不匹配,我們都減少了自己動手編寫代碼的數量,並且增強了代碼的靈活性.
Intersect函數中的res變量在python中稱爲本場變量,所有的本地變量都 會在函數調用時出現,並在函數 退出時消失.
Def語句是實時 創建函數對象的可執行代碼. 當一個函數稍後被調用 時,對象通過賦值被傳遞給函數.
什麼時候python將會創建函數?
當python運行到並執行def語句時,函數就會被創建.這個語句 會創建 函數對象, 並將其賦值給函數名. 當函數所在模塊文件被 另一個模塊導入時,通常就會發生這種事.(導入操作會從頭到尾運行文件中的代碼,包括任何的def),但是,當def通過交互模式輸入,或者嵌套在其他語句中時(例如if),也會發生這件事.
檢查傳入函數的對象類型有什麼錯誤?
檢查傳入函數的對象類型,實質上就是破壞函數的靈活性,把函數限制在特定的類型上,沒有這類檢查時,函數可能可以處理所有 的對象 類型,任何支持函數 所預期的接口的對象都能用(接口一詞是指函數所執行的一組方法和表達工運算符).
第16章 作用域和參數
作用域法則
當我們談論搜索變量名對應於代碼的值 時時候,作用域這個術語指的就是命名空間.也就是說, 在代碼中變量名被 賦值的位置決定了這個變量名能被 訪問的範圍.
所有變量名,包括作用域的定義在內, 都是在python賦值的時候生成的,正如我們所知,python中的變量名在第一次被賦值時已經 創建,並且必須經過賦值後才能夠使用. 由於變量名最初沒有聲明, python將一個變量名被賦值的地點關聯爲 一個特定的命名空間,換名話說, 在代碼中給一個變量賦值的地方 決定 了這個變量將存在那個命名空間,也就是它可見的範圍.
如果一個變量在def內被賦值,它被定位 在這個函數 內,如果在def之 外被 賦值,它就是整個文件全局的.
函數作用域
函數定義了本地作用域,面模塊定的是全局作用域.這兩個作用域有如下的關係:
l 內嵌的模塊是全局作用域.: 它對於外部的全局變量就灰飛煙滅一個模塊對象的屬性.
l 全局作用域的作用範圍僅限於單個文件.
l 每次對函數的調用都創建了一個新的本地作用域.
l 賦值的變量名除非聲明爲全局變量,否則均爲本地變量.
l 所有的變量名都可以歸納爲本地,全局或者內置的.
在函數內部定義的任意的賦值操作定義的變量名都 將成爲本地變量:=語句, import語句,def語句,參數傳遞。
變量名解析:LEGB原則
對於一個def語句:
變量名引用分爲三個作用域進行查找:首先是本地,之後是函數內(如果有的話),之後全局,最後是內置。
在默認情況下,變量名賦值會創建或者改變本地變量。
全局聲明將賦值變量名映射到模塊文件內部的作用域。
>>>x = 99 #全局變量名x和func
>>>def func(Y):
Z = x + Y #本地變量名Y, Z
return Z
>>>func(1)
100
當在函數中使用未認證的變量名時,python搜索4個作用域[本地作用域(L), 之後是上一層結構中def或lambda的本地作用域(E), 之後是全局作用域(G), 最後是內置作用域(B)]並且在第一處能夠找到這個變量名的地方 停下來. 如果變量名在這次搜索 中沒有找到, python會報錯.
當在函數之外給一個變量名賦值時, 本地作用域與全局作用域(這個模塊的命名空間)是相同的
內置作用域
內置作用域僅僅是一個名爲__builtin__的內置模塊, 但是必須要import __builtin__ 之後才能使用內置作用域, 因爲變量名builtin本身並沒有預先內置.
>>>import __builtin__
>>>dir(__builtin__)
['ArithmeticError','AssertionError', 'AttributeError', 'BaseException', 'BufferError','BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError','Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit','IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError','KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError','None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError','PendingDeprecationWarning', 'ReferenceError', 'RuntimeError','RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError','SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError','UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError','UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning','ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '_', '__debug__','__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any','apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes','callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex','copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate','eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset','getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int','intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals','long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open','ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce','reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted','staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode','vars', 'xrange', 'zip']
這個列表中的變量名組成了python中的內置作用域. 概括地講,前一半是內置 的異常, 而後一半是內置函數.由於LEGB法則python最後將自動搜索這個模塊,將會自動得到這個列表中的所有變量名.
因此,有兩種方法引用一個內置函數: 通過LEBG法則自動最後找到, 或者手動導入__builtin__模塊
>>>zip
<built-infunction zip>
>>>import __builtin__
>>>__builtin__.zip
<built-infunction zip>
也就是說,在本地作用域的變量名可能會覆蓋在全局作用域和內置作用域的有着相同變量名的變量,而全局變量名有可能覆蓋內置的變量名.
函數也能夠簡單地使用本地變量名隱藏同名全局變量.
Global語句
>>> x = 88 >>> def func(): global x #它是一個命名空間的聲明,它告訴python函數打算生成一個或多個全局變量名. x = 99
>>> func() >>> print x 99 >>> b, c = 1, 2 #b和c是全局變量,因爲不在函數內部賦值. >>> def all_global(): global a #a是全局變量,因爲它通過 global語句使用自己明確地映射到了模塊的作用域. a = b + c
>>> print a #但是當我們執行打印a時,卻報錯了,這 因爲函數並沒有運行前,全局變量a並不存在. Traceback (most recent call last): File "<pyshell#36>", line 1, in <module> print a NameError: name 'a' is not defined >>> all_global() >>> print a #當運行函數後就可以正常使用了. 3 |
本地變量在函數返回時將會消失, 而全局變量不是這樣.
最小化文件間的修改
x = 99 ============================================== import first first.x = 88 print(first.x) C:\my_python>python second.py 88 |
一個模塊文件的全局變量一旦被導入就成爲了這個模塊對象的一個屬性:導入者自動得到了這個被導入的模塊文件的所有全局變量的訪問權. 但是最好別這樣做, 在文件間進行通信最好的辦法就是通過調用函數,傳遞參數,然後得到其返回值.
#thismod.py var = 99
def local(): var = 0 #改變局部變量var
def glob1(): global var var += 1 #改變全局變量var
def glob2(): var = 0 #改變局部變量var import thismod #導入自己 thismod.var += 1 #改變全局變量var
def glob3(): var = 0 #改變局部變量var import sys glob = sys.modules['thismod'] #從系統模塊數組中引用自己 glob.var += 1 #改變全局變量var
def test(): print(var) local(); glob1(); glob2(); glob3() print(var) |
>>> import thismod >>> thismod.test() 99 #函數未被調用所以爲99 102 #被 glob1,2,3加了三次 |
作用域和嵌套函數
>>> def f1(): x = 88 def f2(): #在函數f1()中用def語句嵌套函數f2(),注意f2是f1本地作用域中一個本地變量, print x # f2是一個臨時函數,僅在f1內部執行過程中存在。 f2()
>>> f1() 88 |
88 >>> def f1(): x = 88 def f2(): print x return f2
>>> action = f1() #f2的函數調用動作的運行是在f1運行後發生的.f2 記住了在f1中嵌套作用域的x. >>> action() #儘管f1已經不處於激活狀態. 88 |
通過LEGB查找法則,f2內的x自動映射到了f1的x.
這個嵌套作用域查找在嵌套的函數已經返回後也是有效的.
工廠函數
一個能夠記住嵌套作用域的變量值的函數,儘管那個作用域或許已經不存在了.
>>> def maker(n): #定義了一個外部函數,這個函數生成並返回一個嵌套的函數, def action(x): #用外部函數的參數n來計算某個數x的n次冪. return x ** n return action
>>> f = maker(2) >>> f #程序打印了生成的內嵌函數的一個引用. 這個內嵌函數是通過運行內嵌的def而創建的. <function action at 0x020595F0> >>> f(3) #由於內嵌函數記住了整數2, 所以結果: 3 ** 2 = 9 9 >>> f(4) #4 ** 2 = 16. 實際上,在本地作用域內的n被作爲執行的狀態保留了下來. 16 >>> g = maker(3) #重新執行外部函數maker並指定因變量n爲3. >>> g(3) #執行內嵌函數指定自變量x 爲3, 因此3 ** 3 = 27 27 >>> f(3) #但是最初的函數f仍然是像從前一樣做平方運算. 9
|
使用默認參數來保留嵌套作用域的狀態.
較早的版本的python中上面的代碼執行會失敗, 爲了解決這一問題,一般會將默認參數值傳遞給(記住)一個內嵌作用域內的對象.
>>>def f1():
x = 88
def f2(x = x): #出現在def頭部的arg =val的語句表示參數arg在調用時沒有值傳入進來的時候,
print x #默認會使用值val.
f2()
>>>f1()
88
嵌套作用域查找法則之所以加入到python中就是爲了讓默認參數不再扮演這種角色。現在,python自動記住了所需要的上層作用域的任意值,爲了能夠在內嵌的def中使用。
嵌套作用域和lambad
儘管對於def本身來說,嵌套作用域很少使用,但是當開始編寫lambda表達式時,將會生成後面會被調用 的一個新的函數,與def語句很相似。就像def,lambda表達式引入了新的本地作用域,多虧了嵌套 作用域查找層,lambda能夠看到所有生存在所編寫的函數的變量。
>>> def func(): x = 4 action = (lambda n: x ** n) #lambda表達式產生了一個action的函數。 return action >>> x = func() >>> print x(2) 16 >>> def func(): x = 4 action = (lambda n, x=x: x ** n) #通過給參數x設置默認值的方式實現上面方式,但這是沒有必要的。 return action
>>> x = func() >>> print x(2) 16
|
由於lambda是表達式,所以它們自然而然的嵌套在了def中,在大多數情況下,給lambda函數通過默認參數傳遞值也就沒有什麼 必要了。
使用域與帶有循環變量的默認參數相比較
嵌套在一個循環中,並且嵌套的函數引用了一個上層作用域的變量,該變量被循環所改變,所有在這個循環中產生的函數將會有相同的值—在最後一次循環中完成時被引用變量的值。
>>> def makeActions(): acts = [] for i in range(5): acts.append(lambda x: i ** x) return acts
>>> acts = makeActions() >>> acts[0] <function <lambda> at 0x01E77AF0> >>> acts[0](2) #由於i被作爲執行的狀態保留了下來, 且爲循環最後一次的值4 . 16 #因爲嵌套作用域中的變量在嵌套的函數被調用 時才進行查找,(查找i的值) >>> acts[2](2) #所以它們實際上記住的是同樣的值, (在最後一次循環迭代中循環變量的值) 16 #所以acts[]數組中的lambda表達式中i的值是4. >>> acts[4](2) 16 |
爲了能夠使有循環中每次的i 值, 必須使用默認參數把當前的修正傳遞給作用域的變量.因爲默認參數是在嵌套函數創建時評估的(而不是在其稍後調用時), 每一個函數記住了自己的變量i的值. >>> def makeActions(): acts = [] for i in range(5): acts.append(lambda x, i=i: i ** x) #採用默認值來保存其循環變量i的值到lambda創建有函數中. return acts
>>> acts = makeActions() >>> acts[0](2) 0 >>> acts[2](2) 4 >>> acts[4] (2) 16 |
任意作用域的嵌套
作用域可以做任意的嵌套, 但是隻有內嵌的函數(而不是類,)會被搜索.
>>> def f1(): x = 99 def f2(): def f3(): print x #python將會在所有的內嵌的def中搜索本地作用域, 從內至外, f3() #在引用過函數的本地作用域之後, 並在搜索模塊的全局作用域之前進行這一過程. f2()
>>> f1() 99 |
這種代碼不可能會在實際中這樣使用,在python中,我們遵循平坦要優於嵌套原則.
傳遞參數
1. 參數傳遞是通過自動將對象賦值給本地變量來實現的.
2. 在函數內部的參數名的賦值不會影響調用者. (在函數運行時,在函數頭部的參數名是一個新的,本地的變量名)
3. 改變函數的可變對象參數的值也許會對調用都有影響.
l 不可變參數是”通過值”進行傳遞. (像整數和字符串這樣的對象是通過對象引用而不是拷貝進行傳遞的,但是因爲你無論怎樣都不可能在原處改變不可變對象,實際上就像是創建了一份拷貝)
l 可變對象是通過”指針”進行傳遞的.(例如,列表和字典,這和C語言的指針傳遞數組很相似, 可變對象能夠在函數內部進行原處改變.)
總的來說,它僅僅是將對象賦值給變量名,並且無論對於可變對象可不可變對象都是這樣的.
參數和共享引用
>>> def changer(a, b): a = 2 #因爲a 是在函數作用域內的本地變量名. 它僅僅是修改了本地變量名a,並沒有影響到調用者的x b[0] = 'spam'
>>> x = 1 >>> l = [1, 2] >>> changer(x, l) #可變對象l >>> x, l (1, ['spam', 2]) 對於變量x 和函數本地變量a 其實就像下面一樣的. >>> x = 1 >>> a = x >>> a = 2 >>> print x 1 >>> a 2 >>> l = [1, 2] >>> b = l >>> b[0] = 'spam' >>> print l ['spam', 2] >>> b ['spam', 2] |
避免可變參數的修改
在python中默認通過引用(也就是指針)進行函數的參數傳遞.這也通常是我們想要的.
>>> l = [1, 2] >>> changer(x, l[:]) >>> l [1, 2] >>> def changer(a, b): b = b[:] a = 2 b[0] = 'spam'
>>> l = [1, 2] >>> changer(x, tuple(l))
Traceback (most recent call last): File "<pyshell#73>", line 1, in <module> changer(x, tuple(l)) File "<pyshell#71>", line 4, in changer b[0] = 'spam' TypeError: 'tuple' object does not support item assignment |
以上這兩種拷貝機制都會阻止函數改變對象, 這樣做僅僅是防止了這些改變會影響調用者.
我們問題能夠將可變對象轉換爲不可變對象來杜絕這種問題.但是元組在試圖改變時會拋出異常.
對參數的輸出進行模擬
Return 能夠返回任意種類的對象,所以它也能夠返回多個值,如果這些值被封裝進一個元組或其他的集合類型. 儘管python不支持其他語言所謂的通過引用進行調用 的參數傳遞. 我們通常通過返回元組並將結果賦值給最初的調用 者的參數變量名來進行模擬.
>>> def multiple(x, y): x = 2 y = [3, 4] return x, y #返回一個元組. # (這裏看起來好像返回了兩個值,但是實際上只用一個,一個包含有2個元素的元組,它的圓括號是可選的.)
>>> x = 1 >>> l = [1, 2] >>> x , l = multiple(x, l) #用x, l 來接收返回的元組. >>> x, l (2, [3, 4]) |
特定的參數匹配模型
在默認情況下, 參數是通過其位置進行匹配的, 從左至右, 而且必須精確地傳遞和函數頭部參數名一樣多的參數.
l 位置 : 從左至右進行匹配
l 關鍵字參數: 通過參數名進行匹配
l 默認參數: 爲沒有傳入值的參數定義參數值.
l 可變參數: 收集任意多基於位置或關鍵字的參數. (它們是以字符*開頭,收集任意多的額外參數)
l 可變參數: 傳遞任意多的基於位置或關鍵字的參數.
關鍵字參數和默認參數的實例
>>> def f(a, b, c): print a, b, c
>>> f(1, 2, 3) #基於位置的參數傳遞. 1 2 3 >>> f(c=3, b=2, a=1) #關鍵字參數傳遞, 它允許通過變量名進行匹配, 而不是通過位置. 1 2 3 #python將調用中的變量名c匹配給在函數定義頭部的名爲c的參數. >>> f(1, c=3, b=2) #混合使用基於位置的參數和基於關鍵字的參數. 1 2 3 |
>>> def f(a, b=2, c=3): print a, b, c #默認參數實例.
>>> f(1) 1 2 3 >>> f(a=1) 1 2 3 >>> f(1, 4) 1 4 3 >>> f(1, 4, 5) 1 4 5 >>> f(1, c=6) #關鍵參數從本質上允許我們跳過有默認值的參數. 1 2 6 |
所有基於位置的參數首先依照從左至右的順序匹配頭部的參數.之後再進行基於變量名的進行關鍵字匹配
任意參數的實例
最後兩種匹配擴展, *和**, 是讓函數支持接受任意數目的參數的. 它們都可以出現在函數定義或是函數調用中,並且它們在兩種場合下有着相關的目的.
收集參數
以下都是在函數定義中使用的.
#python將所有位置相關的參數收集到一個新元組中,並將元組賦值給變量args >>> def f(*args): print args
>>> f() () >>> f(1) (1,) >>> f(1, 2, 3, 4) (1, 2, 3, 4) #**特性與*類似, 它只對關鍵字參數有效, 將這些關鍵字參數傳遞給一個新的字典. >>> def f(**args): print args
>>> f() {} >>> f(a=1, b=2) {'a': 1, 'b': 2} #混合用法 >>> def f(a, *pargs, **kargs): print a, pargs, kargs
>>> f(1, 2, 3, x=1, y=2) 1 (2, 3) {'y': 2, 'x': 1} |
分解參數
以下是發生在調用時的.
>>> def func(a, b, c, d): print a, b, c, d #通過一個元組給函數傳遞參數. * >>> args = (1, 2) >>> args += (3, 4) >>> func(*args) 1 2 3 4 #通過鍵/值對的形式分解一個字典, 使其成爲獨立的關鍵字參數 ** >>> args = {'a': 1, 'b': 2, 'c': 3} >>> args['d'] = 4 >>> func(**args) 1 2 3 4 #混合以位置,關鍵字=,分解元組*,分解字典鍵/值對**的參數傳遞. >>> func(*(1, 2), **{'d': 4, 'c': 4}) 1 2 4 4 >>> func(1, *(2, 3), **{'d': 4}) 1 2 3 4 >>> func(1, c=3, *(2,), **{'d': 4}) 1 2 3 4 |
注意: 不要混淆函數頭部或是函數調用時*/**的語法: 在頭部, 它意味着收集任意數目的參數, 而在調用時, 它解包任意數目的參數.
關鍵字參數和默認參數的混合
>>> def func(spam, eggs, toast=0, ham=0): #位置和默認參數的混合定義. print(spam, eggs, toast, ham)
>>> func(1, 2) #使用位置參數賦值和定義時的默認值調用 (1, 2, 0, 0) >>> func(1, ham=1, eggs=0) #使用位置參數賦值和關鍵字參數調用 (1, 0, 0, 1) >>> func(spam=1, eggs=0) #關鍵字參數和使用定義時的默認值調用 (1, 0, 0, 0) >>> func(toast=1, eggs=2, spam=3) #關鍵字參數和使用定義時的默認值調用 (3, 2, 1, 0) >>> func(1, 2, 3, 4) #以位置參數賦值形式來完成調用. (1, 2, 3, 4) |
Min調用
設計一個函數能夠計算任意參數集合和任意對象數據類型集合中的最小值. 也就是說, 這個函數應該接受零個或多個參數: 希望傳遞多小就可以傳遞多少.
>>> def min1(*args): res = args[0] #使用分片去掉第一個得到了剩餘的參數. for arg in args[1:]: if arg < res: res = arg return res
>>> def min2(first, *rest): for arg in rest: if arg < first: first = arg return first
>>> def min3(*args): tmp = list(args) tmp.sort() #sort方法是用c語言進行編寫的. 有時它要比其他的程序運行快. return tmp[0] #在python中 sort例程是以C寫成的, 使用高度優化的算法,試着利用被排序元素間的部分次序. #這種排序稱爲”timsort”, 以其創造者Tim peter命名. >>> print min1(3, 4, 1, 2) 1 >>> print min2('bb', 'aa') aa >>> print min3([2, 3], [1, 1], [3, 3]) [1, 1] |
#計算最大值和最小值. >>> def minmax(test, *args): res = args[0] for arg in args[1:]: if test(arg, res): res = arg return res
>>> def lessthan(x, y): return x < y
>>> def grtrthan(x, y): return x > y
>>> print minmax(lessthan, 4, 2, 1, 5, 6, 3) 1 >>> print minmax(grtrthan, 4, 2, 1, 6, 5, 3) 6 |
一個更有用的例子:通用set函數
編寫一個intersect函數, 來從任意的參數中收集所有曾經中任意操作對象中共有的元素.
編寫一個union函數,來從任意多的參數中收集所有曾經在任意操作對象中出現過的元素.
>>> def intersect(*args): #計算元組中args所有元素的交集. res = [] for x in args[0]: for other in args[1:]: if x not in other: break else: res.append(x) return res
>>> def union(*args): #計算元組中所有元素的合集. res = [] for seq in args: for x in seq: if not x in res: res.append(x) return res
# from inter2 import intersect, union//當函數命名在inter2.py文件中時, 在交互模式下可以用from導入函數. >>> s1, s2, s3 = 'SPAM', 'SCAM', 'SLAM' >>> intersect(s1, s2), union(s1, s2) (['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C']) >>> intersect([1, 2, 3], (1, 4)) [1] >>> intersect(s1, s2, s3) ['S', 'A', 'M'] >>> union(s1, s2, s3) ['S', 'P', 'A', 'M', 'C', 'L'] |
參數匹配: 細節
如果決定使用並混合特定的參數匹配模型, python將會遵循下面有關順序的法則:
1. 在函數調用中, 所有的非關鍵字參數(name)必須首先出現, 其後跟隨所有的關鍵字參數(name=vale), 後邊跟着*name的形式, 並且如果需要的話,最後是**name的形式.
2. 函數頭部, 參數必須以相同的順序出現: 一般參數(name), 緊跟着默認參數(name=value), 後面如果出現了的話是*name的形式, 如果使用了的話最後是**name
如果 你使用了任何其他的順序混合了參數, 你將會得到一個語法錯誤, 因爲其他順序的混合會產生歧義. Python內部是使用以下的步驟來在賦值前進行參數匹配的.
1. 通過位置分配非關鍵字參數.
2. 通過匹配變量名分配關鍵字參數.
3. 其他額外的非關鍵字參數分配到*name元組中.
4. 其他 額外ide關鍵字參數分配到**name字典中.
5. 用默認值分配給在頭部未得到分配的參數.
本章小結
變量作爲它所賦值的函數定義的本地變量, 除非他們特定地聲明爲全局變量. 和我們所看到的一樣, 參數是通過 賦值傳入函數的, 這也就意味着是通過對象引用, 其真實含義就是通過指針.
可變類型的參數能夠像其他共享引用一樣, 展示出共同的特性, 除非在傳入時, 對像是一份明確的拷貝, 在函數內對傳入的可變的改變會影響調用者.
舉出三種或四種Python函數中保存狀態信息的方法?
雖然函數返回後本地變量的值就會消失, python函數可以利用全局變量, 嵌套函數中引用所在函數作用域的變量或默認值 , 通過它們來保存狀態信息. 另一種方式是使用OOP和類, 它所支持的狀態保存比前三種技術都要好, 因爲它是利用屬性賦值運算明確進行狀態保存的.
第17章 函數的高級話題
匿名函數: lambda
除了def語句之外, python還提供了一種生成函數對象的表達式形式. Lambda它返回了一個函數而不是將這個函數賦值給一個變量名.
Lambda表達式
Lambda argument1, argument2, … argumentN : expression using arguments
l Lambda是一個表達式, 而不是一個語句. 因爲這一點, lambda能夠出現在python語法不允許def出現的地方.
l Lambda的主體是一個單個的表達式, 而不是一個代碼塊. Lambda通常要比def功能要小: 你僅能夠在lambda主體中封裝有限的邏輯進去, 連if這樣的語句都不能夠使用. 這是有意設計的—它限制了程序的嵌套:lambda是一個爲編寫簡單函數而設計的.而def用來處理更大的任務.
默認參數也能夠在lambda參數中使用, 就像在def中使用一樣.
>>> x = (lambda a='fee', b='file', c='foe': a + b + c) >>> x('wee') #參數b和c採用默認值. 'weefilefoe' |
爲什麼使用lambda
Lambda起到了一種函數速寫的作用.
Lambda通常用來編寫跳轉表(jump table), 也就是行爲的列表或字典, 能夠按照需要執行相應的動作.
>>> l = [(lambda x: x**2), (lambda x: x**3), (lambda x: x**4)] >>> for f in l: print f(2)
#當需要把小段可執行代碼編寫進def語句從語法上不能編寫進的地方時, #Lambda表達式作爲def的一種速寫來說是最爲有用的. 4 8 16 >>> print l[0](3) 9 |
>>> key = 'got' >>> {'already': (lambda: 2+2), 'got': (lambda: 2*4), 'one': (lambda: 2**6) }[key]() #使用相同的辦法用python語言,在字典或其他的數據結構中創建一個行爲表. 8 |
Lambda在函數參數裏作爲行內臨時函數的定義, 並且該函數在程序中不再其他地方 使用時也是很方便的.
如何(不要)讓python代碼變得晦澀難懂
>>> lower = (lambda x, y: x if x < y else y) #在lambda中使用if else這樣的三元表達式. >>> lower('bb', 'aa') 'aa' >>> lower('aa', 'bb') 'aa' >>> import sys >>> showall = (lambda x: map(sys.stdout.write, x)) #使用map函數遍歷列表.打印每一項. >>> t = showall(['spam\n', 'toas\n', 'egg\n']) spam toas egg >>> showall = lambda x: [sys.stdout.write(line) for line in x] #遍歷列表, 並打印列表中每一項. >>> t = showall(('bright\n', 'side\n', 'of\n', 'life\n')) bright side of life |
上面這些技巧必須在萬不得已的情況下才使用. 一不小心,它們就會導致代碼不可讀(晦澀難懂)的python代碼
一般來說: 簡潔優於複雜, 明確優於晦澀, 而且一個完整的語句要比神祕的表達式要好.
嵌套lambda和作用域
Lambda是嵌套函數作用域查找(在第16章見到的LEGB原則中的E)的最大受益者.
>>> def action(x): return (lambda y: x + y) #在action函數調用的時候,嵌套的lambda能夠獲取到在上層函數作用域中的變量x的值 >>> act = action(99) >>> act <function <lambda> at 0x01EF2230> >>> act(2) 101 |
>>> action = (lambda x: (lambda y: x + y)) >>> act = action(99) #在嵌套結構讓函數調用時創建了一個函數. >>> act(3) 102 >>> ((lambda x: (lambda y: x + y))(99))(4) 103 |
無論上面那種情況, 嵌套的lambda代碼都能夠獲取在上層lambda函數中的變量x. 但是這相當費解.通常來說, 應該避免使用嵌套的lambda.
作爲參數來應用函數.
內置函數apply (將在python3.0中消失)
>>> def func(x, y, z): return x + y + z
>>> apply(func, (2, 3, 4)) #將一個函數作爲參數傳遞給apply來調用一個生成函數,並將其參數作爲元組傳入 9 >>> f = lambda x, y, z: x + y + z >>> apply(f, (2, 3, 4)) #apply也可用於lambda生成的函數. 9 |
爲什麼要在意: 回調
Lambda的另一個常見的應用就是爲python的Tkinter GUI API定義行內的回調函數.
>>> import sys >>> x = Button( #在這裏回調處理是通過傳遞一個用lambda所生產的函數作爲command的關鍵字參數. text = 'Press me', command = (lambda: sys.stdout.write('Spam\n'))) |
注意:不要把apply和map搞混了, apply是執行單個函數的調用, 把參數傳入該函數, 這樣只進行一次. Map會替序列中每個元素都調用函數, 這樣進行多次.
傳入關鍵字參數
>>> def echo(*args, **kwargs): print args, kwargs
>>> echo(1, 2, a=3, b=4) (1, 2) {'a': 3, 'b': 4} >>> pargs = (1, 2) >>> kargs = {'a': 3, 'b':4} >>> apply(echo, pargs, kargs) (1, 2) {'a': 3, 'b': 4} |
在序列中映射函數map
Map函數會對一個序列對象中的每一個元素應用被傳入的函數, 並且返回一個包含了所有函數調用結果的一個列表。
>>> counters = [1, 2, 3, 4] >>> def inc(x): return x + 10
>>> map(inc, counters) #用map函數實現對列表counters中每一個元素調用函數inc,並返回一個列表。 [11, 12, 13, 14] >>> map((lambda x: x+3), counters) #在map中還可以使用lambda組成的函數. [4, 5, 6, 7] >>> def mymap(func, seq): #寫一個自定義的map函數. res = [] for x in seq: res.append(func(x)) return res
>>> map(inc, [1, 2, 3]) [11, 12, 13] >>> mymap(inc, [1, 2, 3]) [11, 12, 13] #pow函數返回第一個參數的第二參數的指數冪. >>> pow(3, 4) #它是 3*3*3*3 = 81 81 >>> map(pow, [1, 2, 3], [2, 3, 4]) # 1*1=2, 2*2*2=8, 3*3*3*3=81. [1, 8, 81] |
Map調用與在第13章中學到過的列表解析很相似, 但是map對每一個元素都應用了函數調用 而不是任意的表達式. 因爲這點限制, 使它不太通用. 而map在某些情況下比列表解析運行起來更快.
函數式編程工具: filter和reduce
在python內置函數中, map函數無疑是函數式編程工具中最簡單的函數代表, 函數式編程的意思就是對序列應用一些函數的工具.
>>> range(-5, 5) [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] >>> filter((lambda x: x>0), range(-5, 5)) #從range(-5, 5)列表中找出大於0的數. [1, 2, 3, 4] >>> reduce((lambda x, y: x +y), [1, 2, 3, 4]) #以第一個元素爲初始值, 計算以後的每一個元素的和集. 10 >>> reduce((lambda x, y: x * y), [1, 2, 3, 4]) #以第一個元素爲初值, 計算剩餘的元素的乘集. 24 >>> l = [1, 2, 3, 4] #模擬第一個reduce程序, 計算和集. >>> res = l[0] >>> for x in l[1:]: res = res + x
>>> res 10
>>> import operator #引入operator模塊. >>> reduce(operator.add, [2, 4, 6]) #利用operator模塊的add方法來完成對列表的和 集的計算. 12 >>> reduce((lambda x, y: x + y), [2, 4, 6]) #利用lambda匿名函數來完成對列表的各集的計算. 12 |
重訪列表解析:映射
Python2.0引入了 新特性(列表解析表達式), 列表解析可以成爲一個比map和filter更通用的工具, 有時候通過基於函數的另類視角進行分析, 有肋於深入理解它.
列表解析基礎
>>> ord('s') #ord轉換一個字符爲ascii 115 >>> res = [] >>> for x in 'spam': #通過循環來完成轉換’spam’序列中的每一個字符爲ascii碼, 並加入到列表res中. res.append(ord(x))
>>> res [115, 112, 97, 109] >>> res = map(ord, 'spam') #通過map函數調用ord來對’spam’中每一個字符進行轉換到列表res中. >>> res [115, 112, 97, 109] >>> res = [ord(x) for x in 'spam'] #通過列表表達式來完成對序列’spam’的每一個字符進行轉換. >>> res [115, 112, 97, 109] >>> |
列表解析在一個序列的值上應用一個任意表達式, 將其結果收集到一個新的列表中並返回, 從語法上來說, 列表解析 是由方括號封裝起來的(爲了提醒它們構造了一個列表).它們的簡單形式是在方括號中編寫一個表達式, 其中的變量在後邊跟隨着的看起來就像一個for循環的頭部一樣的語句.有着相同的變量名的變量. Pyhton之後將這個表達式的應用循環中每次迭代的結果收集起來.
>>> [x ** 2 for x in range(10)] #通過列表表達式計算0-10的平方. [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> map((lambda x: x**2), range(10)) #通過map函數調用計算0-10的平方. [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] |
增加測試和嵌套循環
>>> [x for x in range(5) ifx % 2 == 0] #使用if分支的列表表達式. If分支路過條件不爲真的情況下的元素 [0, 2, 4] >>> filter((lambda x: x%2 == 0), range(5)) #使用filter過濾函數來完成對偶數的篩選. [0, 2, 4] >>> res = [] #上面列表表達式的代碼模擬. >>> for x in range(5): if x % 2 == 0: res.append(x)
>>> res [0, 2, 4]
>>> [x **2 for x in range(10) if x % 2 == 0] #用列表表達式計算10以內的偶數的平方. [0, 4, 16, 36, 64] #用map函數來實現. 可以看到比列表表達式要複雜得多了. >>> map((lambda x: x**2), filter((lambda x: x%2==0), range(10))) [0, 4, 16, 36, 64] |
而實際上, 列表解析還能夠更加通用. 你可以在一個列表解析中編寫任意數量的嵌套的for循環, 並且每一個都有可選的關聯的if測試. 它有如下形式:
[ expression for target1 in sequence1[if condition]
For target2in sequence2 [if condition]…
For target2in sequenceN [if condition] ]
當for分名嵌套在列表解析中時, 它們工作起來就像等效的嵌套 的for循環語句.
>>> res = [x + y for x in [0, 1, 2] for y in [100, 200, 300]] #存在兩for語句的雙重循環的列表表達式. >>> res [100, 200, 300, 101, 201, 301, 102, 202, 302] >>> res = [] >>> for x in [0, 1, 2]: #上面的列表表達式代碼模擬實現. for y in [100, 200, 300]: res.append(x + y)
>>> res [100, 200, 300, 101, 201, 301, 102, 202, 302] #排列5以內的一個偶數和一個奇數的所有可能組合. >>> [(x, y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1] [(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)] #而如果上面使用map和filter的等效形式往往將會更復雜也會有深層的嵌套問題. |
列表解析和矩陣
矩陣也被稱爲多維數組,用python編寫矩陣的一個基本方法就是使用嵌套的列表結構。
>>> M = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> N = [[2, 2, 2], [3, 3, 3], [4, 4, 4]] >>> M[1] #打印第一行的所有元素. [4, 5, 6] >>> M[1][2] #打印第一行第二列元素. 6 >>> [row[1] for row in M] #取出矩陣M中每一行的第一個元素. [2, 5, 8] >>> [M[row][1] for row in (0, 1, 2)] #找出矩陣M中0, 1, 2行中的第一個元素. [2, 5, 8] >>> [M[i][i] for i in range(len(M))] #取出行和列相同的元素. [1, 5, 9] >>> [M[row][col] * N[row][col] for row in range(3) for col in range(3)] #將矩陣M和N對應位元素相乘. [2, 4, 6, 12, 15, 18, 28, 32, 36] >>> [[M[row][col] * N[row][col] for col in range(3)] for row in range(3)] [[2, 4, 6], [12, 15, 18], [28, 32, 36]] #最後一個表達式是有效的, 因爲row迭代是外層的循環. #對於每個row, 它運行嵌套的列的迭代創建矩陣每一行的結果. >>> res = [] >>> for row in range(3): tmp = [] for col in range(3): tmp.append(M[row][col] * N[row][col]) res.append(tmp)
>>> res [[2, 4, 6], [12, 15, 18], [28, 32, 36]] |
理解列表解析
通用性使得列表解析變得難以理解, 特別是在嵌套的時候. 儘管如此, 對於當前額外的複雜度來說有可觀的性能優勢: 基於對運行當前python的測試, map調用比等效的for循環要快兩倍,而列表解析往往比map調用要稍快一些. 速度上的差距是來自於底層的實現上, map和列表解析是在解釋器中以C語言的速度來運行的,比python的for循環代碼在PVM中步進運行要快得多.
重訪迭代器: 生成器
編寫函數能夠返回一個值, 並且稍後還可以從它剛纔離開的地方 仍然返回值, 這樣的函數被認作是生成器, 因爲它們隨時間生成一個序列的值. 不像一般的函數會生成值後退出, 生成器函數在生成值後自動掛起並暫停它們的執行和狀態.
生成器和一般的函數之間代碼上最大的再不同就是一個生成器yield一個值, 而不是return 一個值. Yield語句將會將函數關起, 並向它的調用者返回一個值, 但是保存足夠的狀態信息爲了讓其能夠 在函數從它掛起的地方恢復.這能夠允許這些函數不斷的產生一系列的值, 而不是一次計算所有的值. 之後將值以類似列表之類的形式來返回.
>>> def gensquares(N): #這個函數將會不斷的生成一系列的數字的平方. for i in range(N): yield i**2 #這個函數在每次循環時都會產生一個值, 之後將其返還給它的調用者. #當它被暫停後它的上一個狀態被保存了下來, 並且在yield語句之後控制器馬上被回收, >>> for i in gensquares(5): print i, ":",
0 : 1 : 4 : 9 : 16 : >>> x = gensquares(4) >>> x <generator object gensquares at 0x01D15BC0> #當打印它的返回值時, 得到的是一個生成器對象, 它支持迭代器協議(也就是說, next方法可以開始這個函數, #或者說從它上次yield值後的地方恢復, 以及在得到一系列的值的最後一個時, 產生StopIteration異常). >>> x.next() 0 >>> x.next() 1 >>> x.next() 4 >>> x.next() 9 >>> x.next()
Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> x.next() StopIteration |
For循環與生成器工作起來是一樣的: 通過重複調用 next方法, 直到捕獲一個異常.
如果一個不支持這種協議的對象進行這種迭代, for循環會使用索引協議進行迭代.
同時上面的代碼還可以通過下面的方法來實現. >>> for x in [n**2 for n in range(5)]: print x, ':',
0 : 1 : 4 : 9 : 16 : >>> for x in map((lambda x: x**2), range(5)): print x, ':',
0 : 1 : 4 : 9 : 16 : |
儘管如此, 生成器能夠運行函數在第一線做了所有的工作, 當結果的列表很大或者在處理每一個結果都需要很多時間的時候, 這一點尤其有用. 生成器將在loop迭代中處理一系列的時間分佈開來. (還有一個簡單的替代方案將對象保存到迭代中的狀態在第6部分介紹)
擴展生成器函數協議: send和next
在python2.5中, 生成器函數協議中增加了一個send方法, send方法像next方法一樣, 但是它提供了一種調用者與生成器之間進行通信的方法, 從而能夠影響它的操作.
Yield現在是一個表達式的形式, 可以返回傳入的元素來發送, 而不是一個語句, 值是通過調用本身的send(value)方法傳給生成器的. 之後恢復生成器的代碼, 並且yield表達式返回了爲了發送而傳入的值. 如果調用了正常的next()方法, yield返回None.
例如, 用send方法, 編寫一個能夠被它的調用者終止的生成器,
這部分是python的一些高級特性, 查看 python的標準庫獲得更多細節.
迭代器和內置類型.
生成器表達式:迭代器遇到列表解析
在最近版本的python中, 迭代器和列表解析的概念形成了這個語言的一個新的特性, 生成器表達式. 從語法上來講, 生成器表達式就像一般的列表解析一樣, 但是它們是括在圓括號中而不是方括號中的.
>>> [x **2 for x in range(4)] #List comprehension: build a list [0, 1, 4, 9] >>> (x**2 for x in range(4)) #Generator expression: make an iterable <generator object <genexpr> at 0x01D15AD0> #從執行過程上來講, 生成器表達式不是在內存中構建結果, 而是返回一個生成器對象, #這個對象將會支持迭代協議並在任意的迭代語境的操作中, 獲得最終結果列表中的一部分. >>> G = (x ** 2 for x in range(4)) >>> G.next() 0 >>> G.next() 1 >>> G.next() 4 >>> G.next() 9 >>> G.next()
Traceback (most recent call last): File "<pyshell#28>", line 1, in <module> G.next() StopIteration 我們一般不會機械的使用next迭代器來操作生成器表達式, 因爲for循環會自動觸發.. >>> for num in (x**2 for x in range(4)): print '%s, %s' % (num, num/2.0) #實際上, 每一個迭代的語境都會這樣, 包括sum, map和sorted等內置函數 , 以及any, all 和list內置函數等.
0, 0.0 1, 0.5 4, 2.0 9, 4.5 #注意, 如果生成器表達式是在其他的括號之內的話,就像函數調用那樣, 生成器自身的括號就不是必須的了, #儘管這樣, 在下面第二個sorted調用 中, 還是需要額外的括號. >>> sum(x **2 for x in range(4)) #省略生成器表達式括號 14 >>> sorted(x**2 for x in range(4)) [0, 1, 4, 9] >>> sorted((x**2 for x in range(4)), reverse=True) #由於sorted這時有兩個參數,所以生成器表達式加了括號. [9, 4, 1, 0] >>> import math >>> map(math.sqrt, (x**2 for x in range(4))) [0.0, 1.0, 2.0, 3.0] |
生成器表達式大體上可以認爲是對內存空間的優化: 它們不需要像方括號的列表解析一樣, 一次構造出整個結果列表. 它們在實際中運行起來可能稍慢一些, 所以它們可能只對於非常大的結果集合的運算來說是最優的選擇.
對迭代的各種方法進行計時.
總結: 列表解析要比for循環語句有速度方面的性能優勢, 而且map會依據調用方法的不同表現出更好或更差的性能. 上一節介紹的生成器表達式看起來比列表解析速度更慢一些, 但是它們把內存需求降到了最小.
讓我們用一個小程序來測試一下:
import time, sys reps = 1000 size = 10000
def tester(func, *args): startTime = time.time() for i in range(reps): func(*args) elapsed = time.time() – startTime #計算時間 return elapsed
def forStatement(): res = [] for x in range(size): res.append(abs(x)) #用for循環
def listComprehension(): #用列表解析 res = [abs(x) for x in range(size)]
def mapFunction(): #用map函數 res = map(abs, range(size))
def generatorExpression(): #用生成器表達式. res = list(abs(x) for x in range(size))
print sys.version tests = (forStatement, listComprehension, mapFunction, generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>', tester(testfunc) >>> ================================ RESTART ================================ >>> #這是size在10000時的測試結果. 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] forStatement => 1.38800001144 listComprehension => 0.796000003815 mapFunction => 0.609000205994 generatorExpression => 0.952000141144 #這四個測試分別創建了一千次結果列表.每一個都依次執行了一千萬步. #可以看出map操作要快一些,列表解析其次. #當改變abs操作這加法操作時時間又發生了變化. import time, sys reps = 1000 size = 10000
def tester(func, *args): startTime = time.time() for i in range(reps): func(*args) elapsed = time.time() - startTime return elapsed
def forStatement(): res = [] for x in range(size): res.append(x+10)
def listComprehension(): res = [x+10 for x in range(size)]
def mapFunction(): res = map((lambda x: x+10), range(size))
def generatorExpression(): res = list(x+10 for x in range(size))
print sys.version tests = (forStatement, listComprehension, mapFunction, generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>', tester(testfunc) >>> ================================ RESTART ================================ >>> 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] forStatement => 1.3109998703 listComprehension => 0.546000003815 mapFunction => 1.30999994278 generatorExpression => 0.732999801636 #可以看出列表解析快一些,生成器表達式其次. #map調用需要進行函數調用使得它變得和for循環語句一樣慢, 儘管for循環語句的版本使用了更多的代碼. |
因爲解釋器的優化是相當內在化的, 像這裏的python代碼的性能分析是很難處理的事情. 猜出哪一種方法的性能是最佳的幾乎是不可能的. 當然在目前的測試情況下, 我們所能確定的就是對於目前的python, 在map調用中使用用戶定義的函數至少會導致map慢2倍以上, 而列表解析對這樣的測試運行最快.
性能不應該是最優先考慮的. 首先要爲可讀性和簡潔性的標準編寫.如果有必要的話, 之後再進行優化.
函數設計概念
當你開始使用函數時, 就開始面對如何將組件聚合在一起的選擇了. 例如, 如何將任務分解成爲更有針對性的函數(導致了聚合性), 函數將如何通信(耦合性)等. 你需要深入考慮諸如聚合性, 耦合性以及函數的大小等性質其中一些可以歸類於結構分析和設計的範疇.
l 耦合性: 對於輸入使用參數並且對於輸出使用return語句.
n 讓函數獨立於它外部的東西. 參數和return 語句通常就是隔離對外部依賴關係的最好的辦法, 從而讓代碼中只剩少量醒目位置.
l 耦合性: 只有在真正必要的情況下使用全局變量.
n 它們引發了依賴關係和計時的問題. 會導致程序調試和修改的
l 耦合性: 不要改變可變類型的參數, 除非調用者希望這樣做.
n 這會導致調用者和被調用者之間的耦合性.
l 聚合性: 每個函數都應該有一個單一的, 統一的目標.
n 一個函數中都應該做一件事: 這件事可以用一個簡單說明句來總結.
l 大小: 每一個函數應該相對較小. 保持簡單, 保持簡短.
l 耦合: 避免直接改變在另一個模塊文件中的變量.
n 在可能的時候使用讀取函數, 而不是直接進行賦值語句.
如果沒有使用類, 全局變量通常是模塊中函數保留調用中狀態的最佳方式, 如果都在預料中, 副作用就沒什麼危險.
函數是對象: 簡潔調用
由於函數在運行時是對象, 你可以編寫通用化程序來處理它們. 函數對象能夠進行賦值, 傳遞給其他的函數以及數據結構中排序, 這和簡單的數字和字符串一樣.
1. 函數名就是一個簡單的對象的引用, 能夠將這個對象重新分配給其他的變量名.
2. 因爲參數是通過賦值傳遞的, 所以給其他函數以參數的形式傳入函數也很簡單.
3. 甚至可以將函數對象封裝在數據結構中, 就像它是整數或字符串一樣. 因爲python混合類型能夠包含任意類型的對象.
函數陷阱
本地變量是靜態檢測的.
在python定義的一個函數中進行分配的變量名默認爲本地變量的, 它們存在於函數的作用域並只在函數運行時存在. Python是靜態檢測 python的本地變量的, 當編譯def代碼時, 不是通過發現賦值語句在運行時進行檢測的.
>>> x = 99 >>> def selector(): print x
>>> selector() 99 >>> def selector(): print x x = 88
>>> selector()
Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> selector() File "<pyshell#8>", line 2, in selector print x UnboundLocalError: local variable 'x' referenced before assignment #得到了一個未定義的變量名錯誤, 原因是: python在交互模式或一個模塊文件導入時, python讀入並編譯這級代碼. 在編譯時, python看到了對x的賦值語句, 並且決定了x將會在函數中的任一地方都將是本地變量名. 但是, 當函數實際運行時, 因爲在print執行時賦值語句並沒有發生, python告訴正在使用一個未定義的變量名,根據其變量名規則, 本地變量x是在其被賦值前就使用了. 實際上, 任何在函數內 的賦值將會使其成爲一個本地變量名. Import, =, 嵌套def, 嵌套類等, 都會受這種行爲影響. 產生這種問題的原因在於被賦值的變量名在函數內部是當作本地變量來對待的, 而不是僅僅在賦值以後的語句中才被當作是本地變量. 實際上,
python會在函數中將x作爲本地變量, 它就是一個錯誤. #事實上上面定義的selecor()函數本身就有問題.當使用全局變量x時應該加上global先聲明使用全局變量x. >>> def selector(): global x print x x = 88 #這其實是改變了全局變量x爲88, 因爲已經global x.
>>> selector() 99 #如果真的想打印全局變量, 並在之後設置一個有着相同變量名的本地變量, #導入上層的模塊,並使用模塊的屬性標記來獲得其全局變量. >>> x = 99 >>> def selector(): import __main__ #導入上層的模塊. print __main__.x #打印上層模塊的全局變量x (99). x = 88 #創建本地變量x 爲88 print x #打印本地變量x .
>>> selector() 99 88 |
交互模式下的命名空間是一個名爲__main__的命名空間. 所以__main__.x得到了全局變量版本的x.
默認和可變對象
默認參數是在def語句運行時被評估並保存的, 而不是在這個函數調用時. 從內部來講, python會將每一個默認參數保存成一個對象, 附加在這個函數本身.
因爲默認參數是在def時被評估的, 如果必要的話, 它能夠從整個作用域內保存值.
下面的函數使用了一個空列表作爲默認參數, 並在函數每次調用時都對它進行了改變.
>>> def saver(x=[]): #這個默認的列表對象每次調用時,它都沒有重置爲空列表.當新元素加入後, 列表會變大. x.append(1) print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1, 1] >>> saver() [1, 1, 1] |
有些人把這種行爲當作一種特性. 因爲可變類型的默認參數在函數調用之間保存了它們的狀態, 從某種意義上講它們能夠充當C語言中的靜態本地變量的角色. 在一定程度上, 它們工作起來就像全局變更, 但是它們的變量名對於函數來說是本地變更, 而且不會與程序中的其他變量名發生衝突.
值得說明的是python中有更好的辦法在調用之間保存狀態.(例如使用類).
>>> def saver(x = None): if x is None: #如果上面的特性不是你想要的行爲的話, x = [] #在函數主體中先判斷有沒有賦值, 然後再使用默認值, x.append(1) #或者在主體函數開始時對默認值進行簡單的拷貝. print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1]
>>> def saver(x = None): x = x or [] #如果淌有參數傳入的話, x將會默認爲None, 然後or語句返回右操作符對象. x.append(1) #但是如果傳入一個[]空列表, x將返回的不是傳入的空列表而是右邊的空列表的值. print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1] |
沒有return 語句的函數
在python函數中, return 以及yield語句是可選的. 當一個沒有精確的返回值的時候, 函數在控制權從函數主體脫離時, 函數將會推遲. 從技術上來講, 所有的函數都返回了一個值, 如果沒有提供return 語句, 函數將自動返回None對象.
沒有return 語句的函數與python對應於一些其他語言中所謂的”過程”是等效的. 它們常被當作語句.