- Linux集群之美
- 余洪春
- 5419字
- 2021-01-08 10:57:48
2.5.6 Python函數
函數是組織好的,可重復使用的,用來實現單一功能或功能相互關聯的代碼段。
函數能提高應用的模塊性,和代碼的重復利用率。大家應該知道Python提供了許多內建函數,比如print()。但我們也可以自己創建函數,即用戶自定義函數。
在Python中我們是如何定義一個函數的呢?
定義一個有自己想要功能的函數時,需要遵循以下規則:
·函數代碼塊以def關鍵詞開頭,后接函數標識符名稱和圓括號“( )”。
·任何傳入參數和自變量必須放在圓括號中間,圓括號之間可以定義參數。
·函數的第一行語句可以選擇性地使用文檔字符串——用于存放函數說明。
·函數內容以冒號起始,并且要有縮進。
·用return [表達式]結束函數,選擇性地返回一個值給調用方。不帶表達式的return相當于返回None。
例如,我們可以定義一個函數,名字叫MyFirstFun(),內容如下:
# -*- coding: UTF-8 -*- def MyFirstFun(name): '''函數定義過程中的name是叫形參 ''' print 'My name is:' + name MyFirstFun('余洪春')
運行結果如下:
My name is:余洪春
在MyFirstFun后面的name叫形參,它只是一個形式,表示占據了一個參數值,在print后面傳遞進來的name叫實參。
1.Python函數的參數傳遞
Python函數的參數傳遞分不可變類型和可變類型兩種。
·不可變類型:類似C++的值傳遞,如整數、字符串、元組。如fun(a),傳遞的只是a的值,沒有影響a對象本身。比如在fun(a)內部修改a的值,只是修改了另一個復制的對象,不會影響a本身。
·可變類型:類似C++的引用傳遞,如列表、字典等。如fun(list1),表示將list1真正地傳過去,修改后fun外部的list1也會受影響。
不可變類型的舉例說明如下:
# -*- coding: UTF-8 -*- def ChangeInt(a): a = 10 print "a的值為:",a b = 2 ChangeInt(b) print "b的值為:",b
結果如下:
a的值為: 10 b的值為: 2
可變類型的舉例說明:
# -*- coding: UTF-8 -*- #可用''' '''寫函數說明 def changeme(mylist): "修改傳入的列表" mylist.append([1,2,3,4]); print "函數內取值:", mylist return #調用changeme函數 mylist = [10,20,30]; changeme(mylist); print "函數外取值:", mylist
輸出結果如下:
函數內取值: [10, 20, 30, [1, 2, 3, 4]] 函數外取值: [10, 20, 30, [1, 2, 3, 4]]
大家在實際工作中要注意可變參數和不可變參數的區別。
對于Python的參數,以下是調用函數時可使用的正式參數類型:
·必備參數
·關鍵字參數
·默認參數
·不定長參數
(1)必備參數
必備參數須以正確的順序傳入函數。調用時的數量必須和聲明時的一樣。例如在上面的函數中,如果不傳入一個實參的話,則會有如下報錯:
TypeError: MyFirstFun() takes exactly 1 argument (0 given)
報錯信息的意思其實很明顯,告訴我們需要傳入一個參數進來。
(2)關健字參數
關鍵字參數和函數調用關系緊密,函數調用使用關鍵字參數來確定傳入的參數值。使用關鍵字參數時,允許函數調用時參數的順序與聲明時的不一致,因為Python解釋器能夠用參數名匹配參數值。
這里舉個簡單的函數例子,如下:
# -*- coding: UTF-8 -*- def SaySomething(name,word): print name + '→' + word SaySomething('余洪春','一枚碼農')
執行這段代碼的結果如下:
余洪春 → 一枚碼農
如果這里把SaySomething函數中的內容調換了呢?例如:
SaySomething('一枚碼農','余洪春')
則輸出結果如下:
一枚碼農 → 余洪春
很明顯,這個結果不是我們想要的,這時就可以利用關鍵字參數來確定傳入的參數值,如下:
SaySomething(word='一枚碼農',name='余洪春')
大家可以發現,即使順序改變了,也可達到我們想要的結果:
余洪春一枚碼農
(3)默認參數
默認參數也叫缺省參數。調用函數時,默認參數的值如果沒有傳入,則被認為是默認值。
這里舉個簡單的函數例子:
# -*- coding: UTF-8 -*- #可寫函數說明 def printinfo(name, age = 35): print "Name: ", name; print "Age:", age; return; #調用printinfo函數 printinfo(age=50, name="cc") printinfo(name="cc")
執行這段代碼的結果如下:
Name: cc Age 50 Name: cc Age: 35
我們通過觀察可以得知,在printinfo(name="cc")中是沒有輸入age參數值的,但在執行代碼的時候,name=cc一樣輸出了默認的age值,也就是之前設定的age=35。
(4)不定長參數
大家可能需要用一個函數來處理比當初聲明時更多的參數。這些參數叫作不定長參數,和上述參數不同,它進行聲明時不會命名。基本語法如下:
def functionname(*var): 函數體 return [expression]
加了星號(*)的變量名會存放所有未命名的變量參數,這里舉一個簡單的函數例子:
# -*- coding: UTF-8 -*- def testparams(*params): print "參數的長度是:",len(params) print "第一個參數是:",params[0] print "第二個參數是:",params[1] print "打印所有的輸入實參:",params testparams('cc',1,2,3)
此段代碼的輸出結果為:
參數的長度是: 4 第一個參數是: cc 第二個參數是: 1 打印所有的輸入實參: ('cc', 1, 2, 3)
大家可以清楚地看到,我們輸入的實參'cc',1,2,3已經全部被賦值給params變量了,并且被正確打印出來了。
2.函數返回值(return語句)
return語句表示從Python函數返回一個值,在介紹自定義函數時有講過,每個函數都要有一個返回值。Python中的return語句有什么作用,下面仔細地講解一下。
Python函數中一定要有return返回值才是完整的函數。如果沒有該返回值,那么得到的結果是None對象,而None表示沒有任何值。
return是返回數值的意思,比如定義兩個函數,一個是有返回值,另一個用print語句,看看結果有什么不同。
# -*- coding: UTF-8 -*- def func1(x,y): print x+y result = func1(2,3) result is None
當函數沒有顯式return時,默認返回None值,大家可以觀察一下此段代碼的返回結果:
True
另一個有返回值return的函數如下:
# -*- coding: UTF-8 -*- def func2(x,y): return x + y #python函數返回值 result = func2(2,3) result is None
傳入參數后得到的結果不是None值,會得到如下輸出結果:
False
另外,Python的return是支持多返回值的,這里舉個簡單的例子:
def func(a,b): c = a + b return a,b,c x,y,z = func(1,2) print x,y,z
觀察輸出結果可知,x、y、z的值都正確輸出了:
1,2,3
3.函數變量作用域
一個程序的所有變量并不是在哪個位置都可以訪問的。訪問權限取決于這個變量是在哪里賦值的。
變量的作用域決定了在程序的哪一部分你可以訪問哪個特定的變量名稱。兩種最基本的變量作用域如下:
·全局變量
·局部變量
定義在函數內部的變量擁有一個局部作用域,定義在函數外的擁有全局作用域。
局部變量只能在其被聲明的函數內部訪問,而全局變量則可以在整個程序范圍內訪問。調用函數時,所有在函數內聲明的變量名稱都將被加入作用域中。
如果我們要在函數內部使用全局變量,可以使用global實現這一功能,詳細代碼如下:
# -*- coding: UTF-8 -*- def func(): global x print 'x:', x x = 2 y = 1 print 'Changed local x to:', x print 'global',globals() print 'local',locals() x = 50 func() print 'Value of x is:', x
程序輸出結果如下:
x: 50 Changed local x to: 2 global {'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test8.py', '__package__': None, 'func': <function func at 0x7fb2b6196668>, 'x': 2, '__name__': '__main__', '__doc__': None} local {} Value of x is: 2
另外,這里有個概念需要理解,即Python的Namespace(命名空間)。
Namespace只是從名字到對象的一個映射。大部分Namespace都是按Python中的字典來實現的。從某種意義上來說,一個對象(Object)的所有屬性(attribute)也構成了一個Namespace。在程序執行期間,會有多個名空間同時存在。不同Namespace的創建/銷毀時間也不同。
注意
兩個不同的Namespace中,名字相同的兩個變量之間沒有任何聯系。
接下來看一下Python中Namespace的查找順序。
Python中通過提供Namespace來實現重名函數/方法、變量等信息的識別,一共有如下三種Namespace。
·local Namespace:作用范圍為當前函數或者類方法。
·Global Namespace:作用范圍為當前模塊。
·Build-In Namespace:作用范圍為所有模塊。
當函數/方法、變量等信息發生重名時,Python會按照“local Namespace→Global Namespace→Build-In Namespace”的順序搜索用戶所需元素,并且以第一個找到此元素的Namespace為準。
4.Python內部函數和閉包
Python內部函數、閉包的共同之處在于都是以函數作為參數傳遞到函數的,不同之處在于返回與調用有所區別。
(1)Python內部函數
當需要在函數內部多次執行復雜任務時,內部函數非常有用,它可避免循環和代碼的堆疊重復。示例如下:

在Python中創建一個閉包可以歸結為以下三點:
·閉包函數必須有內嵌函數。
·內嵌函數需要引用該嵌套函數上一級Namespace中的變量。
·閉包函數必須返回內嵌函數。
通過這三點,就可以創建一個Python閉包了。
5.匿名函數
Python使用lambda來創建匿名函數,其語法為:
lambda 變量1,變量2:表達式
這里可以舉個簡單的例子說明其用法:
sum = lambda x,y:x+y print sum(1,11) print sum(7,18)
輸出結果為:
12 25
匿名函數的特征為:
·lambda的主體是一個表達式,僅在lambda中封裝有限的邏輯進去;
·lambda只是一個表達式,函數體比def簡單很多;
·lambda的目的是調用小函數時不占用棧內存,從而增加運算效率;
·lambda并不會使程序運行效率提高,只會使代碼更簡潔。
事實上,既然這里提到了lambda,就不得不提一下Python的函數式編程,因為lambda函數在函數式編程中經常用到。對于函數式編程,簡單來說,其特點就是允許把函數本身作為參數傳入另一個函數,還允許返回一個函數。
Python中用于函數式編程的主要是4個基礎函數(map、reduce、filter和sorted)和1個算子(即lambda)。
函數式編程的好處:
·代碼更為簡潔。
·代碼中沒有了循環體,少了很多臨時變量(純粹的函數式編程語言編寫的函數是沒有變量的),邏輯更為簡單明了。
·數據集、操作和返回值都放在一起了。
下面通過示例來說明它的用法。
(1)map()函數
map()函數的語法如下:
map(函數,序列)
我們用求平方的例子來說明其用法,代碼如下:
#encoding:utf-8 #求數字1~9的平方數 squares = map(lambda x:x*x,[1,2,3,4,5,7,8,9]) print squares
代碼執行后輸出結果如下:
[1, 4, 9, 16, 25, 49, 64, 81]
(2)reduce()函數
reduce()函數的語法如下:
reduce(函數,序列)
用reduce實現階乘是非常容易的事,示例如下:
#encoding:utf-8 #數字9階乘 #9!=9*8*7*6*5*4*3*2*1 print reduce(lambda x,y: x*y, range(1,9))
輸出結果如下:
40320
6.生成器
通過Python列表生成式可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含100萬個元素的列表,不僅占用的存儲空間很大,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。實際工作中會經常遇到這種需求。
此外,提到生成器(Generator),總會不可避免地要把迭代器拉出來對比著講,生成器在行為上和迭代器非常類似,二者功能上差不多,但是生成器更優雅。
顧名思義,迭代器就是用于迭代操作(for循環)的對象,它像列表一樣可以迭代獲取其中的每一個元素,任何實現了__next__方法的對象都可以稱為迭代器。它與列表的區別在于,構建迭代器的時候,不像列表把所有元素一次性加載到內存,而是以一種延遲計算(lazy evaluation)的方式返回元素,這正是它的優點。比如列表含有1000萬個整數,需要占超過400MB的內存,而迭代器只需要幾十字節的空間。因為它并沒有把所有元素裝載到內存中,而是等到調用next方法時才返回該元素(按需調用即call by need的方式,本質上for循環就是不斷地調用迭代器的next方法)。
了解了迭代器,那什么是生成器呢?
如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出后續的元素呢?這樣就不必創建完整的list了,可節省大量的空間。在Python中,這種一邊循環一邊計算的機制,就稱為生成器。
創建生成器的方法很多。第一種方法很簡單,只要把一個列表生成式的[]改成(),就創建了一個Generator,示例如下:
g = (x*2 for x in range(10)) l = [x*2 for x in range(10)] type(g) type(l)
如果要把元素一個個地打印出來,可以通過Generator的next()方法,即每次調用g.next(),就計算出下一個元素的值,直到計算到最后一個元素時,拋出StopIteration錯誤。
當然,如果要打印Generator中的每一個元素,用for循環就夠了。
In [5]: for num in g: ...: print num ...: 0 2 4 6 8 10 12 14 16 18
那么在什么場景下需要使用序列,什么場景下要使用Generator呢?
當程序需要較高的性能或一次只需要一個值進行處理時,使用Generator函數;當需要一次獲取一組元素的值時,使用序列。
事實上,在創建了一個Generator后,基本上不會調用next()方法,而是會通過for循環來迭代它。Generator非常強大,如果推算的算法比較復雜,用類似列表生成式的for循環無法實現時,還可以用函數來實現。比如,著名的斐波拉契數列,除第一個和第二個數外,任意一個數都可由前兩個數相加得到。可以用下面的函數來實現:
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1
這就是定義Generator的另一種方法。如果一個函數的定義中包含yield關鍵字,那么這個函數就不再是一個普通函數,而是一個Generator。試著執行一下:
In [7]: fib(5)
輸出結果為:
Out[7]: <generator object fib at 0x02CEC490>
事實上,很多時候我們可以利用Generator來打開大文件,比如說超過10個GB的日志文件,可以使用yield生成自定義可迭代對象,即generator,每一個帶有yield的函數就是一個Generator。它會將文件切分成小段,每次處理完一小段內容后,釋放內存。可以參考下面的代碼:
#-*- coding:utf-8 -*- def read_in_block(file_path): BLOCK_SIZE = 1024 with open(file_path, "r") as f: while True: block = f.read(BLOCK_SIZE) #每次讀取固定長度到內存緩沖區 if block: yield block else: return #如果讀取到文件末尾,則退出 def test(): file_path = "/tmp/test.log" for block in read_in_block(file_path): print block
當然,Python下面有更優雅和簡潔的處理方法,那就是使用系統自帶方法with open()生成迭代對象,使用方式如下:
with open(filename, 'rb') as f: for line in f: <do something with the line>
對可迭代對象f進行迭代遍歷(即for line in f語句)時,會自動地使用緩沖I/O(buffered I/O)及內存管理,因此不用擔心任何大文件的問題。讓系統來處理,其實是最簡單的方式。
另外,這里要注意yield與return的區別,來看一段程序:
#-*- coding:utf-8 -*- def func(n): for i in range(n): return i def func2(n): for i in range(n): yield i print func(3) f = func2(3) print f print f.next() print f.next() print f.next()
第4行代碼直接返回i的值,循環語句將被中止,整個程序到此結束。
第7行代碼循環生成n個數字,循環語句不會被中止。
最后提一個知識點,工作中我們經常會遇到一個需求——Python中如何優雅地處理命令行參數?
Python中有專門針對這種需求的getopt模塊,該模塊是專門用來處理命令行參數的。
函數getopt的具體格式如下:
getopt(args, shortopts, longopts = [])
參數args即sys.argv[1:],shortopts表示短格式(-),longopts表示長格式(--)。
我們需要一個conv.py腳本,它的作用是接收IP和port端口號,要求該腳本滿足以下條件:
·通過-i或-p選項來區別腳本后面接的是IP還是port;
·當不知道convert.py需要哪些參數時,用-h打印出幫助信息。
這里可以用個簡單的腳本來說明:
#!/usr/bin/python import getopt import sys def usage(): print ' -h help \n' \ ' -i ip address\n' \ ' -p port number\n' \ '' if __name__ == '__main__': try: options, args = getopt.getopt(sys.argv[1:], "hp:i:", ['help', "ip=", "port="]) for name, value in options: if name in ('-h', '--help'): usage() elif name in ('-i', '--ip'): print value elif name in ('-p', '--port'): print value except getopt.GetoptError: usage()
上述腳本的說明如下:
1)真正處理邏輯所使用的函數叫getopt(),因為是直接使用import導入的getopt模塊,所以要加上限定getopt才可以,同理,這里也要導入sys模塊。
2)使用sys.argv[1:]過濾掉第一個參數(它是執行腳本的名字,不應算作參數的一部分)。
3)使用短格式分析串"hp:i:"。當一個選項只是表示開關狀態時,即后面不帶附加參數時,在分析串中寫入選項字符。當選項后面帶一個附加參數時,在分析串中寫入選項字符的同時在后面再加一個":"號。所以"hp:i:"就表示"h"是一個開關選項;"p:"和"i"則表示后面應該帶一個參數。
4)使用長格式分析串列表['help',"ip=","port="]。長格式串也可以有開關狀態,即后面不跟等號。如果跟一個等號則表示后面還應有一個參數。這個長格式表示help是一個開關選項,ip=和output=則表示后面應該帶一個參數。
5)調用getopt函數。函數返回兩個列表:opts和args。opts為分析出的格式信息,args為不屬于格式信息的剩余命令行參數。opts是一個兩元組的列表。每個元素均為選項串,附加參數。如果沒有附加參數則為空串''或'' ''。
6)整個過程使用異常來處理,當分析出錯時,就可以打印出信息來通知用戶如何使用這個程序。
- 全屋互聯:智能家居系統開發指南
- 操作系統實用教程(Linux版)
- Windows Server 2019 Cookbook
- Linux運維實戰:CentOS7.6操作系統從入門到精通
- 鴻蒙生態:開啟萬物互聯的智慧新時代
- Instant Handlebars.js
- SharePoint 2013 WCM Advanced Cookbook
- 深入理解eBPF與可觀測性
- Windows Server 2019 Administration Fundamentals
- Mobile First Design with HTML5 and CSS3
- OpenSolaris設備驅動原理與開發
- Red Hat Enterprise Linux 6.4網絡操作系統詳解
- Linux操作系統
- 鴻蒙HarmonyOS手機應用開發實戰
- 操作系統之哲學原理第2版