一、函數
1.1 為什么有函數
我們對于一個項目時,會有上千甚至上萬條代碼,當我們要使用到某個函數時,例如我需要計算一個求和代碼,獲得求和的值來服務我們的項目,那我們可能會這樣
#計算1~100的和
theSun = 0
for i in range(1,101):theSun += i
print(theSun)#計算100~400的和
theSun = 0
for i in range(100,401):theSun += i
print(theSun)#計算1000~2300的和
theSun = 0
for i in range(1000,2301):theSun += i
print(theSun)
大家會發現,我們每次需要求出不同值的和,都需要重新手打一次代碼,好煩贅,那有沒有什么別的方法來方便我們呢?
有的兄弟,有的,像這種方法有很多,今天先講講函數
我們先把上面的代碼使用函數來優化一下:
#函數定義
def calcSum(beg, end):theSum = 0for i in range(beg, end + 1):theSum += i print(theSum)# 函數調用
calcSum(1, 100)
calcSum(100, 400)
calcSum(1000, 2300)
? 優勢:
- 代碼只寫一次,復用多次
- 修改只需改一處
- 邏輯清晰,易于理解
1.1 函數的定義格式
def 函數名(參數):函數體(要執行的代碼)return 返回值
def
:關鍵字,表示“我要定義一個函數”函數名
:給函數起個名字,要見名知意,比如?greet
,?add_numbers
(參數)
:可選,函數需要的“原材料”return
:可選,表示“把結果交出來”
1.1.1 最簡單的函數(沒有回參)
def say_hello():print("Hello! 歡迎來到Python世界!")# 調用函數
say_hello()
🔍 強調:
定義函數 ≠ 執行函數!
必須調用它,才會執行。
1.1.2 帶參數的函數
就拿我們剛剛的求和來舉例
#函數定義
def calcSum(beg, end):theSum = 0for i in range(beg, end + 1):theSum += i # 修正變量名拼寫錯誤 theSun -> theSumprint(theSum)# 函數調用
calcSum(1, 100) # 輸出 5050
參數就像“占位符”,調用時傳入具體值。
1.1.3?帶返回值的函數
def add(a, b):result = a + breturn result # 把結果“交出來”# 調用并接收結果
total = add(3, 5)
print(f"Sum = {total}")
🔍 強調:
return
不是打印!它是“把結果傳遞出去”,可以賦值給變量、參與計算等。
二、函數的定義與調用
定義:可以看作是布置任務
調用:可以看作是開始完成任務
?2.1 定義語法
def 函數名(參數列表):函數體return 返回值
2.2 調用語法
函數名(實際參數)
def greet():print("Hello! 歡迎你!")greet() # 調用函數
greet() # 可以調用多次
兩者少任何一個都不行,兩者往往相伴相隨
2.3 函數定義和調用的順序規則
遵循規則:
定義在前,調用在后
#eroor
r = add(3,5)
print(r)def add(x,y):return x+y
大家可以看到如果將位置顛倒一下會出現錯誤,這是因為Python執行代碼是從上到下的,如果位置調換了那么我們定義的函數Python是并沒有接受到的,就像這張圖片顯示的 未定義"add"
三、函數的參數
3.1 核心思想:函數參數是什么?為什么重要?
本質:?函數參數是函數與外界溝通的橋梁。它們是函數定義時預留的“占位符”,允許你在調用函數時傳入具體的數據(值或引用)。
參數是函數的“原材料”。
?3.2?形參 vs 實參
- 形參(形式參數):定義時的變量名,如?
def add(a, b)
- 實參(實際參數):調用時傳入的具體值,如?
add(3, 5)
def add(a,b):return a+b #形參r = add(3,5) #實參
print(r)
3.2.1 位置形參
定義:?最常見的形參。它們按照在函數定義中出現的順序接收傳遞進來的實參。
語法:?直接寫形參名,例如?
def greet(name, greeting):
特點:
調用函數時,傳遞的實參數量必須與位置形參的數量嚴格匹配(除非有默認值或可變參數)。
實參的順序決定了它們賦值給哪個形參。
def calculate_area(length, width): # length 和 width 是位置形參area = length * widthreturn area# 調用: 實參 5 按順序賦值給 length, 3 賦值給 width
result = calculate_area(5, 3) # result = 15
# 錯誤調用: calculate_area(3) # 缺少一個參數
# 錯誤調用: calculate_area(5, 3, 2) # 多了一個參數 (除非有可變參數)
2.參數傳遞的過程
def introduce(name, age):print(f"我叫{name},今年{age}歲")introduce("小明", 18) # name="小明", age=18
四、函數的返回值
函數的參數可以視為是函數的 "輸入", 則函數的返回值, 就可以視為是函數的 "輸出"?
return
是函數的“產出物”。
4.1 有返回值
def add(a, b):return a + bresult = add(3, 5) # result = 8
4.2 無返回值
def say_hello():print("Hello")x = say_hello() # x = None
4.3?
def calcSum(beg, end):theSum = 0for i in range(beg, end + 1):theSum += i print(theSum)calcSum(1,12)
大家看這個代碼并沒有什么不對,但是我們程序員寫代碼時,比較喜歡一個函數干一件事這一原則,使用我們可以把這個代碼修改一下
def calcSum(beg, end):theSum = 0for i in range(beg, end + 1):theSum += i return theSumr = calcSum(1,12)
print(f'theSum = {r}')
這樣我們的calcSum函數就只有一個求和這一職能,可以大大提高我們對代碼的閱讀性和可維護性
4.4 一個函數多個return
一個函數中可以有多個return語句
#判斷是否為偶數
def isOdd(num):if num % 2 == 0:return Trueelse:return Falser = isOdd(4)
print(r)
執行到 return 語句, 函數就會立即執行結束, 回到調用位置.
那么我們根據這一特性,我們可以將這個代碼修改一下,使我們的代碼更加易讀
#判斷是否為偶數
def isOdd(num):if num % 2 == 0:return Truereturn Falser = isOdd(4)
print(r)
當我們把4傳到num時,函數來判斷4是否為偶數
- 這時候,如果num(值為4) % 2 沒有余數,則進入"return True",跳出函數
- 如果num(值為4) % 2 有余數,函數到"return True"發現并不符合,則再進入"return False",跳出函數
所以我們發現,這兩個代碼雖然復雜度不同,但是效果是等價的
4.4 使用逗號來分割多個return
def getPoint():x = 10y = 20return x, y
a, b = getPoint()
print(a,b)
我們可以看到這個代碼,返回了兩個值,中間是用逗號來分割,調試后確實獲得了10和20
4.5 使用"_"來忽略部分return
def getPoint():x = 10y = 20return x,y_,b = getattr()
五、變量作用域
變量作用域。這決定了變量在哪里“活”著,在哪里能被“看見”和修改。
想象一下,在一個大公司里:
- 部門內部(如財務部)有自己的專用術語和文件(
部門變量
),只有本部門的人能直接看到和使用。- 公司層面有全公司通用的規則和資源(
公司變量
),所有部門都能訪問。- 不同部門可能碰巧用了同一個名字指代不同東西(比如“預算”),但在各自部門內互不干擾。
Python的作用域規則與此非常相似。它定義了變量名(標識符)在代碼的哪些區域是有效的、可被訪問的。主要的作用域層級稱為?LEGB 規則。
5.1?LEGB 規則:查找名字的四層“洋蔥”
5.1.1?L: Local (局部作用域)
- 定義:?當前正在執行的函數或方法內部定義的變量。
- 生命周期:?從變量在函數內部被賦值的那一刻開始,到函數執行結束時銷毀。
- 訪問:?僅限在該函數內部訪問。外部代碼無法直接看到或修改它。
def calculate_sum(a, b): # a, b 也是此函數的局部變量!result = a + b # result 是局部變量print(result) # 在函數內部可以訪問 resultreturn resulttotal = calculate_sum(5, 3) # 調用函數
# print(result) # 錯誤!result 是 calculate_sum 的局部變量,在此處不存在
print(total) # 正確,total 是全局變量 (下面會講)
5.1.2?Enclosing (閉包作用域 / 非局部作用域)
它揭示了 Python 中一個非常重要的概念:函數可以“記住”它被創建時的環境。
- 定義:?在嵌套函數結構中,外層函數(非全局)的作用域。這是LEGB中比較特殊的一層。
- 生命周期:?與外層函數的執行周期相關。即使內層函數被返回并在其他地方調用,只要內層函數還持有對外層變量的引用,外層函數的這個作用域就不會完全銷毀(這就是閉包的核心)。
- 訪問:?內層函數可以讀取外層函數作用域中的變量。但要修改它,在Python 3中需要使用?
nonlocal
?關鍵字(稍后詳解)
def outer_function(message): # outer_function 的作用域# message 是 outer_function 的局部變量# 但對 inner_function 是 Enclosing 作用域def inner_function(): # inner_function 的作用域 (Local)# 內層函數可以訪問外層函數的變量 message (讀取)print("Message from outer:", message)return inner_function # 返回內層函數本身,而不是調用它my_closure = outer_function("Hello, Scope!") # 調用 outer_function# 返回 inner_function
my_closure() # 調用 inner_function, 輸出: Message from outer: Hello, Scope!
# 注意:此時 outer_function 已經執行完畢
#但它的局部變量 message 仍然能被 my_closure (即 inner_function) 訪問到!
代碼逐行解析
第1行:
def outer_function(message):
- 定義一個外層函數?
outer_function
- 它有一個參數?
message
,這個?message
?是它的局部變量就像你進了一個房間(函數),帶了一個行李箱(
message
)
第3行:
def inner_function():
- 在?
outer_function
?內部,又定義了一個函數?inner_function
- 這叫嵌套函數(Nested Function)
inner_function
?可以訪問外層函數的變量?message
🔍 這是關鍵!內層函數能看到外層的“行李箱”
第5行:
return inner_function
- 注意!是?
inner_function
,不是?inner_function()
- 意思是:返回這個函數本身,而不是調用它
- 就像把“打開行李箱的鑰匙”交了出去
第8行:
my_closure = outer_function("Hello, Scope!")
- 調用?
outer_function
,傳入?"Hello, Scope!"
- 此時:
message = "Hello, Scope!"
inner_function
?被定義outer_function
?返回?inner_function
?這個函數對象- 重點:
outer_function
?的執行已經結束了!? 問題來了:
message
是outer_function
的局部變量,函數都結束了,message
不應該被銷毀嗎?
第9行:
my_closure()
- 調用我們之前保存的?
inner_function
- 它仍然能訪問到?
message
,并正確打印!? 輸出:
Message from outer: Hello, Scope!
5.1.2.1 核心概念:什么是閉包(Closure)?
💬 閉包 = 函數 + 它的“環境”
專業定義:
當一個內層函數引用了外層函數的變量,并且這個內層函數被返回或傳遞到外部時,就形成了一個閉包。
在這個例子中:
inner_function
?是內層函數- 它引用了外層的?
message
- 它被返回給了外部
- → 所以?
my_closure
?是一個閉包
5.1.3?Global (全局作用域)
定義:?在任何函數或類之外,在模塊(
.py
文件)頂層定義的變量。生命周期:?從模塊被導入或執行時創建,到程序結束或模塊被卸載時銷毀。
訪問:?模塊內的任何函數通常都可以讀取全局變量。但是,要修改全局變量,必須在函數內部使用?
global
?關鍵字顯式聲明(否則Python會認為你在創建一個新的同名局部變量)。
# 定義一個全局變量
game_score = 0
player_name = "小明"print(f"游戲開始!玩家:{player_name},當前得分:{game_score}")def increase_score(points):# 想要修改全局變量,必須用 global 聲明global game_scoregame_score = game_score + pointsprint(f" 獲得 {points} 分!當前得分:{game_score}")def show_status():# 只讀取全局變量,不需要 globalprint(f" 狀態:玩家 {player_name},得分 {game_score}")def reset_game():# 修改多個全局變量global game_score, player_namegame_score = 0player_name = "無名氏"print(" 游戲已重置!")# ===== 游戲過程模擬 =====
show_status() # 狀態:玩家 小明,得分 0increase_score(10) # 獲得 10 分!當前得分:10
increase_score(5) # 獲得 5 分!當前得分:15show_status() # 狀態:玩家 小明,得分 15reset_game() # 游戲已重置!show_status() # 狀態:玩家 無名氏,得分 0
5.1.4?Built-in (內建作用域)
- 定義:?Python預先定義好的名字,比如?
print()
,?len()
,?int()
,?str()
,?list()
,?True
,?False
,?None
?等。它們在任何地方都可用。- 生命周期:?Python解釋器啟動時創建,解釋器退出時銷毀。
- 訪問:?在代碼的任何位置都可以直接使用。除非你在更內層的作用域(Local, Enclosing, Global)中定義了同名的變量覆蓋了它們!?(一般不建議這樣做,會讓人困惑)。
# 在任何地方都可以使用內建函數和常量
print(len([1, 2, 3])) # 輸出: 3
value = int("42")
flag = True# 危險:覆蓋內建函數 (不推薦!)
def dangerous_function():# 在這個函數內,str 不再是內建函數,而是一個局部變量str = "I shadowed the built-in str!" # 覆蓋 (shadow) 了內建 strprint(str) # 輸出: I shadowed the built-in str!# print(str(100)) # 錯誤!此時str是字符串,不是函數了dangerous_function()
# 在函數外部,str 還是內建函數
print(str(100)) # 輸出: '100'
代碼講解:
第一部分:正常使用內置函數
print(len([1, 2, 3])) # 輸出: 3 value = int("42") flag = True
? 這就像你正常使用手機上的“電話”、“短信”、“相機”這些系統自帶功能。
len()
:求長度,像“尺子”int()
:轉整數,像“翻譯官”str()
:轉字符串,像“打印機”這些都是Python準備好的“工具箱”,我們可以隨時使用它們
第二部分:危險操作——“冒名頂替”
def dangerous_function():str = "I shadowed the built-in str!" # 覆蓋了內置的 strprint(str) # 輸出: I shadowed the built-in str!# print(str(100)) # ? 這行被注釋了,但一旦打開就出錯!
這就像你在手機里新建了一個聯系人,名字也叫“電話”!
結果:當你想打電話時,手機不知道你是想用“打電話功能”,還是想給“名叫‘電話’的人”發消息。?詳細解釋:
原本
str
是 Python 的內置函數,比如str(100)
能把數字 100 變成字符串"100"
。但現在你在函數里寫:
str = "..."
,這就相當于說:“從現在起,
str
不再是‘轉換成字符串’的功能了,它只是一個普通的字符串變量!”所以:
print(str)
?→ 輸出那個字符串,沒問題。print(str(100))
?→ 想把 100 轉成字符串?不行!?因為?str
?現在是字符串,不是函數,字符串不能被“調用”(就像你不能“打電話”給一個文字)。第三部分:函數外面還是安全的
dangerous_function() # 調用上面那個“危險函數”print(str(100)) # 輸出: '100'
? 這就像:
你只在“某個房間”(函數)里把“電話”這個名字占用了,但出了這個房間,手機功能還是正常的!
str = "..."
?這個“冒名頂替”只在?dangerous_function
?這個函數內部有效。- 一旦函數執行完,這個“局部變量”就消失了。
- 所以在函數外面,
str
?依然是那個強大的“字符串轉換工具”。
六、函數執行過程
6.1執行過程演示
#函數執行過程
def test():print("執行函數內部代碼")print("執行函數內部代碼")print("執行函數內部代碼")print("1111")
test()
print("2222")
test()
print("3333")
這里我們用一個圖來解釋上面這個代碼的執行過程
程序開始↓
定義 test() 函數(不執行)↓
print("1111") → 輸出:1111↓
test() → 跳進函數↓(進入函數)print(...) → 輸出:執行函數內部代碼 ×3↓(函數結束)← 跳回原位置↓
print("2222") → 輸出:2222↓
test() → 再次跳進函數↓(進入函數)print(...) → 輸出:執行函數內部代碼 ×3↓(函數結束)← 跳回原位置↓
print("3333") → 輸出:3333↓
程序結束
6.2 核心知識點總結
概念 | 說明 |
---|---|
函數定義 | def ?只是“寫菜譜”,不會執行 |
函數調用 | test() ?是“按菜譜做飯”,才會執行里面的代碼 |
執行順序 | 從上往下,遇到函數調用就“跳進去”,執行完再“跳回來” |
可重復使用 | 同一個函數可以被多次調用,代碼只寫一次 |
七、鏈式調用和嵌套調用
7.1?嵌套調用
# 簡單的嵌套調用
result = abs(round(3.14159, 2)) # 先四舍五入到2位小數,再取絕對值
print(result) # 輸出: 3.14
# 多層嵌套示例
def add(a, b):return a + bdef square(x):return x * xdef format_result(value):return f"結果: {value}"# 三層嵌套
output = format_result(square(add(3, 4)))
print(output) # 輸出: "結果: 49"
主要看?output = format_result(square(add(3, 4)))?
- 這句代碼先調用format_result()
- 再找到里面的square()
- 接著找到里面的add()
- 先計算add的值,計算后把值傳到square函數,最后傳到format_result()
7.2 嵌套調用的優缺點對比表
類別 | 優點 | 缺點 |
---|---|---|
代碼結構 | ? 代碼緊湊,減少冗余<br>? 減少中間變量的使用 | 容易形成“括號地獄”層級過深時結構混亂 |
表達能力 | ? 能直接表達操作的先后順序和邏輯關系<br>? 適合數學公式或函數式編程風格 | ? 多層嵌套時邏輯不直觀,難以快速理解執行流程 |
可讀性 | ? 表達簡潔,一氣呵成 | ? 可讀性隨嵌套深度增加而顯著降低 初學者難以理解 |
調試與維護 | —— | ? 調試困難:無法在中間步驟輕松插入?print 或斷點 錯誤定位復雜:報錯信息可能只指向最外層調用,難以確定具體出錯層級 |
命名與作用域 | ? 減少臨時變量命名需求,降低命名沖突風險 | ? 無法復用中間結果,不利于重復使用 |
7.3?鏈式調用
概念:鏈式調用是指連續調用同一對象的方法,每個方法返回對象自身(或新對象),從而可以繼續調用其他方法。
鏈式調用的實現原理:
鏈式調用的關鍵是每個方法都返回對象本身(
return self
)或返回一個新對象。
#判斷是否為偶數
def isOdd(num):if num % 2 == 0:return Truereturn Falser = isOdd(4)
print(r)
將這個代碼改成鏈式:
#鏈式
#判斷是否為偶數
def isOdd(num):if num % 2 == 0:return Truereturn Falseprint(isOdd(10))
鏈式調用(Chained Call)的優缺點
類別 | 優點 | 缺點 |
---|---|---|
代碼風格 | ? 代碼流暢,像自然語言一樣“一氣呵成”<br>? 操作序列清晰可見,邏輯連貫 | —— |
可讀性 | ? 可讀性高(當鏈較短時)<br>? 支持方法調用的自然順序,符合思維流程 | ? 鏈過長時可讀性下降,變成“方法瀑布” |
變量管理 | ? 減少中間臨時變量的使用<br>? 避免命名污染和命名沖突 | —— |
設計要求 | —— | ? 需要精心設計類結構<br>? 每個方法必須返回?self (或新對象),否則無法鏈式 |
調試與維護 | —— | ? 調試困難:無法在鏈的中間插入?print ?或斷點查看狀態<br>? 錯誤處理復雜:一旦出錯,難以定位是哪一步失敗 |
容錯性 | —— | ? 鏈中任意一步出錯,整個調用失敗<br>? 不便于對中間結果進行驗證或日志記錄 |
八、函數遞歸
遞歸的核心
- “大事化小”:把大問題變成小問題。
- “找到終點”:必須有一個最簡單的情況直接解決。
- “自己調用自己”:小問題的解法和大問題一樣。
遞歸的兩個關鍵要素:
基線條件:問題的最簡單情況,可以直接得到答案
遞歸條件:將問題分解為更小的同類子問題
遞歸的黃金法則:每個遞歸調用都必須向基線條件靠近
8.1 經典遞歸案例
1. 階乘函數:n!
def factorial(n):# 1. 遞歸出口(最簡單的情況)if n == 1:return 1# 2. 遞歸關系(自己調用自己)return n * factorial(n - 1)print(factorial(5)) # 輸出:120
調用棧分析(n = 5)
factorial(5)
├── 5 * factorial(4)├── 4 * factorial(3)├── 3 * factorial(2)├── 2 * factorial(1)│ └── return 1 ← 出口!└── return 2*1 = 2└── return 3*2 = 6└── return 4*6 = 24
└── return 5*24 = 120
從這個調用棧可以看出,factorial自己調用了三次
8.2?遞歸三要素!!!
要素 | 說明 | 錯了會怎樣? |
---|---|---|
1. 遞歸關系 | 問題如何分解?f(n) = ... f(n-1) ... | 邏輯錯誤,算不出正確結果 |
2. 遞歸出口 | 最簡單的情況是什么?if n == 1: return 1 | 無限遞歸 → 棧溢出! |
3. 逐步逼近出口 | 每次調用,問題規模是否變小?factorial(n-1) | 死循環,程序崩潰 |
🚨 特別強調:
沒有出口的遞歸,就像沒有終點的樓梯,會把計算機“累死”!(內存是有極限的,如果沒有出口會造成溢出)