一、函數
1、函數是什么
編程中的函數和數學中的函數有一定的相似之處。
數學上的函數,比如 y = sin x,x 取不同的值,y 就會得到不同的結果。
編程中的函數是一段可以被重復使用的代碼片段。
(1)求數列的和,不使用函數
可以發現,這幾組代碼基本是相似的,只有一點點差異,可以把重復代碼提取出來做成一個函數。
在實際開發中,復制粘貼是一種不太好的策略,實際開發的重復代碼可能存在幾十份甚至上百份。一旦這個重復代碼需要被修改,那就得改幾十次,非常不便于維護。
(2)求數列的和,使用函數
2、語法格式
(1)創建函數?/?定義函數
def 函數名(形參列表):函數體return 返回值# def:define定義
# 形參列表中可以有多個形參,多個形參(形式參數)之間用逗號分隔
# 函數體要帶有一定的縮進(帶有縮進的代碼才是函數內部的語句)
# 函數執行到 return 就意味著執行完了,return 后面的值就是函數的返回值
(2)調用函數?/?使用函數?
函數名(實參列表) // 不考慮返回值
返回值 = 函數名(實參列表) // 考慮返回值# 此處的實參個數要和形參個數匹配
函數定義并不會執行函數體內容,必須要調用才會執行,調用幾次就會執行幾次。?
def test1():print('hello')# 如果光是定義函數, 而不調用, 則不會執行
函數必須先定義,再使用:
test3() # 還沒有執行到定義, 就先執行調用了, 此時就會報錯.
def test3():print('hello')
3、函數參數
在函數定義的時候,可以在?( )?中指定?“形式參數”(簡稱形參),然后在調用的時候,由調用者把?“實際參數”(簡稱實參)?傳遞進去。這樣就可以做到一份函數,針對不同的數據進行計算處理。
前面這段代碼中,?beg、end?就是函數的形參;?1、100?/?300、400?就是函數的實參。
- 在執行 sum(1, 100) 的時候,就相當于 beg = 1、end = 100,然后在函數內部就可以針對 1-100進行運算。
- 在執行 sum(300, 400) 的時候,就相當于 beg = 300、end = 400,然后在函數內部就可以針對?300-400 進行運算。
注意?:一個函數可以有一個形參,?也可以有多個形參,?也可以沒有形參。?一個函數的形參有幾個,?那么傳遞實參的時候也得傳幾個,?保證個數要匹配。
- 和?C++ / Java?不同,Python?是動態類型的編程語言,函數的形參不必指定參數類型。換句話說,一個函數可以支持多種不同類型的參數。
4、函數返回值
函數的參數可以視為是函數的 “輸入”,則函數的返回值就可以視為是函數的 “輸出”。
此處的 “輸入”,“輸出” 是更廣義的輸入輸出,不是單純指通過控制臺輸入輸出。
可以把函數想象成一個 “工廠”,工廠需要買入原材料,進行加工,并生產出產品。
函數的參數就是原材料,函數的返回值就是生產出的產品。
上面這兩段代碼的區別就在于:前者直接在函數內部進行了打印,后者則使用 return 語句把結果返回給函數調用者,再由調用者負責打印。一般傾向于第二種寫法。
在實際開發中的一個通常的編程原則是:“邏輯和用戶交互分離”。而第一種寫法的函數中既包含了計算邏輯,又包含了和用戶交互(打印到控制臺上),這種寫法是不太好的,如果后續我們需要的是把計算結果保存到文件中或者通過網絡發送,或者展示到圖形化界面里,那么第一種寫法的函數就難以勝任了。而第二種寫法則專注于做計算邏輯,不負責和用戶交互,那么就很容易把這個邏輯搭配不同的用戶交互代碼,來實現不同的效果。
(1)一個函數中可以有多個?return?語句
(2)執行到?return?語句,函數就會立即執行結束,回到調用位置?
下面這段代碼和上一段代碼的邏輯是等價的。
如果 num 是偶數,則進入 if 之后,就會觸發 return False,也就不會繼續執行 return True。
(3)一個函數是可以一次返回多個返回值的,使用 ','?來分割多個返回值
(4)如果只想關注其中的部分返回值,可以使用?_?來忽略不想要的返回值
5、變量作用域
在這個代碼中,函數內部存在?x、y,函數外部也有?x、y。但是這兩組 x、y?不是相同的變量,而只是恰好有一樣的名字。
(1)變量只能在所在的函數內部生效
在函數?getPoint()?內部定義的?x、y?只是在函數內部生效。一旦出了函數的范圍,這兩個變量就不再生效了。
(2)在不同的作用域中允許存在同名的變量
雖然名字相同,但實際上是不同的變量。
注意?:
- 在函數內部的變量也稱為?“局部變量”。
- 不在任何函數內部的變量也稱為?“全局變量”。
(3)如果函數內部嘗試訪問的變量在局部不存在,就會嘗試去全局作用域中查找
(4)如果是想在函數內部修改全局變量的值,需要使用?global 關鍵字聲明
如果此處沒有 global,則函數內部的 x = 10 就會被視為是創建一個局部變量 x,這樣就和全局變量 x 不相關了。
(5)if / while / for 等語句塊不會影響到變量作用域
換而言之,在?if / while / for?中定義的變量在語句外面也可以正常使用。
6、函數執行過程
- 調用函數才會執行函數體代碼,不調用則不會執行。
- 函數體執行結束(或者遇到 return 語句),則回到函數調用位置,繼續往下執行。
這個過程還可以使用?PyCharm?自帶的調試器來觀察。
- 點擊行號右側的空白,可以在代碼中插入斷點。
- 右鍵,Debug,可以按照調試模式執行代碼。每次執行到斷點,程序都會暫停下來。
- 使用 Step Into (F7) 功能可以逐行執行代碼。
7、鏈式調用
?
前面有一段代碼:
實際上也可以簡化寫作:
把一個函數的返回值作為另一個函數的參數,這種操作稱為鏈式調用。鏈式調用先執行 () 里面的函數,再執行外面的函數,換句話說,調用一個函數就需要先對它的參數求值。
8、嵌套調用?
函數內部還可以調用其他的函數,這個動作稱為?“嵌套調用”。
test?函數內部調用了?print?函數,這里就屬于嵌套調用。
(1)一個函數里面可以嵌套調用任意多個函數
函數嵌套的過程是非常靈活的。
如果把代碼稍微調整,打印結果則可能發生很大變化:
函數之間的調用關系,在?Python?中會使用一個特定的數據結構來表示,稱為函數調用棧。每次函數調用,都會在調用棧里新增一個元素,稱為棧幀。
可以通過 PyCharm 調試器看到函數調用棧和棧幀。
在調試狀態下,PyCharm 左下角一般就會顯示出函數調用棧。
(2)每個函數的局部變量,都包含在自己的棧幀中?
選擇不同的棧幀就可以看到各自棧幀中的局部變量。
上述代碼,a、b、c、d 函數中的局部變量名各不相同。
如果變量名是相同的,比如都是 num,那么這四個函數中的 num 是屬于同一個變量,還是不同變量呢?
雖然每個變量同名,但是它們是不同變量,屬于不同的函數作用域,每個變量保存在各自的棧幀中(每個棧幀也是保存在內存上),變量本質就是一塊內存空間。
9、函數遞歸
遞歸是嵌套調用中的一種特殊情況,即一個函數嵌套調用自己:
注意:遞歸代碼務必要保證存在遞歸結束條件。比如?if n == 1?就是結束條件,當?n?為?1?的時候,遞歸就結束了。每次遞歸的時候,要保證函數的實參是逐漸逼近結束條件的。
如果上述條件不能滿足,就會出現?“無限遞歸”,這是一種典型的代碼錯誤:
如前面所描述,函數調用時會在函數調用棧中記錄每一層函數調用的信息,但是函數調用棧的空間不是無限大的。如果調用層數太多就會超出棧的最大范圍,導致出現問題。
(1)遞歸的優點
- 遞歸類似于 “數學歸納法”,明確初始條件和遞推公式就可以解決一系列的問題。
- 遞歸代碼往往代碼量非常簡介。(尤其是處理一些 “問題本身就是通過遞歸的方式定義的”)
(2)遞歸的缺點?
- 遞歸代碼往往執行過程非常復雜、難以理解,很容易超出掌控范圍。
- 遞歸代碼容易出現棧溢出的情況。(代碼不小心寫錯導致每次遞歸,參數不能正確的接近遞歸結束條件,從而導致無限遞歸的情況)
- 遞歸代碼一般都可以轉換成等價的循環代碼,且通常來說循環版本的代碼執行效率要略高于遞歸版本。(函數調用也是有開銷的)
實際開發的時候,使用遞歸要慎重!
10、參數默認值?
Python?中的函數可以給形參指定默認值。
帶有默認值的參數可以在調用的時候不傳參。
此處?debug=False?即為參數默認值。當我們不指定第三個參數的時候,默認?debug?的取值即為?False。
(1)帶有默認值的參數需要放到沒有默認值的參數的后面
11、關鍵字參數?
在調用函數的時候,需要給函數指定實參。一般默認情況下是按照形參的順序來依次傳遞實參的。
但是我們也可以通過關鍵字參數來調整這里的傳參順序,顯式指定當前實參傳遞給哪個形參。
形如上述?test(x=10, y=20)?這樣的操作,即為關鍵字參數。
按照先后順序來傳參,這種傳參風格稱為 “位置參數”。位置參數和關鍵字參數可以混著用,只不過混著用的時候要求位置參數在前,關鍵字參數在后。
關鍵字參數一般搭配默認參數來使用。
一個函數可以提供很多的參數來實現對這個函數的內部功能做出一些調整設定,為了降低調用者的使用成本,可以把大部分參數設定出默認值。
當調用者需要調整其中的一部分參數時,可以搭配關鍵字參數來進行操作。