函數(shù)聲明與定義的編譯:分離還是合并?
函數(shù)聲明和定義的編譯方式在不同語言中有顯著差異。讓我們從底層機(jī)制分析函數(shù)名和函數(shù)體的編譯過程:
一、C/C++:嚴(yán)格分離的經(jīng)典模型
1. 聲明與定義分離
// 聲明 (頭文件.h)
int add(int a, int b);
// 定義 (源文件.c)
int add(int a, int b) {
return a + b;
}
2. 編譯過程

3. 關(guān)鍵階段
| 階段 | 聲明處理 | 定義處理 |
|---|---|---|
| 編譯期 | 1. 驗(yàn)證調(diào)用簽名 2. 預(yù)留符號引用 |
1. 生成函數(shù)體機(jī)器碼 2. 創(chuàng)建可重定位代碼 |
| 鏈接期 | 無 | 1. 解析符號引用 2. 綁定調(diào)用地址 |
4. 內(nèi)存映射
二、Perl:聲明即定義的動態(tài)模型
1. 聲明與定義合一
# 聲明和定義同時發(fā)生
sub add {
my ($a, $b) = @_;
return $a + $b;
}
2. 編譯過程
3. 關(guān)鍵特性
-
單階段處理:聲明和定義在編譯期同時完成
-
符號表核心作用:
perl# Perl符號表結(jié)構(gòu) %main:: = ( 'add' => { type => 'CV', # 代碼值 addr => 0x55c7a12300 # 字節(jié)碼地址 } ) -
無鏈接階段:所有綁定在編譯期完成
三、Python:運(yùn)行時綁定的動態(tài)模型
1. 聲明與定義
# 定義即聲明
def add(a, b):
return a + b
2. 編譯執(zhí)行過程
3. 關(guān)鍵特性
函數(shù)對象結(jié)構(gòu):
class FunctionObject:
__code__ = <字節(jié)碼> # 函數(shù)體
__name__ = "add" # 函數(shù)名
__globals__ = {...} # 全局變量
__closure__ = None # 閉包環(huán)境
-
編譯時機(jī)
? 模塊級編譯:Python 在導(dǎo)入模塊時將整個.py文件編譯為字節(jié)碼(.pyc),而非執(zhí)行到def時才編譯函數(shù)。
? 延遲編譯(對函數(shù)):函數(shù)體在模塊編譯階段已轉(zhuǎn)換為字節(jié)碼,執(zhí)行def語句僅是創(chuàng)建函數(shù)對象。 -
動態(tài)綁定
? 運(yùn)行時綁定:執(zhí)行def語句時,才將函數(shù)名動態(tài)綁定到堆中的函數(shù)對象。同一作用域后續(xù)的def可重新綁定該名字(覆蓋舊函數(shù))。
函數(shù)對象結(jié)構(gòu)(CPython 實(shí)現(xiàn)):
def example(arg):
x = arg + 1
return x
# 函數(shù)對象關(guān)鍵屬性
print(type(example)) # → <class 'function'>
print(example.__name__) # → "example"
print(example.__code__) # → <code object at ...>
print(example.__defaults__) # 默認(rèn)參數(shù)元組 (None)
print(example.__closure__) # 閉包變量 (None)
執(zhí)行過程詳解:
模塊加載:
調(diào)用函數(shù)時:
-
-
查找名字對應(yīng)的函數(shù)對象
-
創(chuàng)建新棧幀(frame)
-
執(zhí)行函數(shù)
__code__中的字節(jié)碼
-
驗(yàn)證示例:
# 模塊首次導(dǎo)入時編譯整個文件
print("模塊初始化執(zhí)行") # 此語句在導(dǎo)入時立即執(zhí)行
def func():
print("函數(shù)調(diào)用")
# 此時 func 已綁定到函數(shù)對象,但函數(shù)體內(nèi)的代碼尚未執(zhí)行
func() # 首次調(diào)用才執(zhí)行函數(shù)體
輸出:
模塊初始化執(zhí)行 函數(shù)調(diào)用
函數(shù)對象內(nèi)存模型:
1. Java/C#:類加載模型
// 聲明和定義合一
public int add(int a, int b) {
return a + b;
}
編譯過程:
-
編譯期:生成.class/.dll文件
-
運(yùn)行時:類加載時綁定方法名和字節(jié)碼
-
JIT編譯:將字節(jié)碼編譯為機(jī)器碼
2. JavaScript:提升(Hoisting)模型
// 函數(shù)聲明被提升
console.log(add(2,3)); // 正常工作!
function add(a, b) {
return a + b;
}
處理流程:
-
編譯期:掃描所有函數(shù)聲明
-
創(chuàng)建函數(shù)對象并綁定到作用域
-
執(zhí)行期:處理函數(shù)調(diào)用
六、底層實(shí)現(xiàn)細(xì)節(jié)
1. 函數(shù)名綁定機(jī)制
| 語言 | 綁定方式 | 技術(shù)實(shí)現(xiàn) |
|---|---|---|
| C/C++ | 鏈接期符號解析 | ELF符號表、重定位表 |
| Perl | 編譯期符號表填充 | HV哈希表、GV條目 |
| Python | 運(yùn)行時命名空間更新 | PyDictObject |
2. 函數(shù)體存儲格式
| 語言 | 存儲格式 | 位置 |
|---|---|---|
| C/C++ | 機(jī)器碼 | .text段 |
| Perl | OP樹 | PL_main_root |
| Python | PyCodeObject | __code__屬性 |
3. 調(diào)用機(jī)制對比
七、為什么需要分離聲明和定義?
分離式編譯的優(yōu)勢:
-
編譯加速:
封裝性:隱藏實(shí)現(xiàn)細(xì)節(jié)
-
跨語言支持:通過頭文件接口交互
-
庫分發(fā):分發(fā)頭文件+二進(jìn)制庫
合并式編譯的優(yōu)勢:
-
開發(fā)簡便:減少文件管理
-
動態(tài)特性:支持運(yùn)行時定義函數(shù)
-
閉包支持:自然捕獲詞法環(huán)境
-
元編程:動態(tài)創(chuàng)建/修改函數(shù)
八、實(shí)踐中的函數(shù)處理
1. Perl 最佳實(shí)踐
# 前置聲明解決循環(huán)依賴
sub inner; # 聲明
sub outer {
inner();
}
sub inner {
outer(); # 循環(huán)調(diào)用
}
2. Python 動態(tài)定義
def create_adder(n):
# 運(yùn)行時定義函數(shù)
def adder(x):
return x + n
return adder
add5 = create_adder(5)
print(add5(3)) # 輸出8
3. C++ 分離實(shí)現(xiàn)
// math.h
#pragma once
int add(int a, int b);
// math.cpp
#include "math.h"
int add(int a, int b) {
return a + b;
}
// main.cpp
#include "math.h"
int main() {
return add(2, 3);
}
總結(jié):函數(shù)名與函數(shù)體的綁定時機(jī)
| 語言類型 | 綁定時機(jī) | 技術(shù)特點(diǎn) | 典型代表 |
|---|---|---|---|
| 靜態(tài)編譯型 | 鏈接期 | 聲明與定義分離 | C/C++ |
| 提前編譯腳本 | 編譯期 | 聲明即定義 | Perl |
| 動態(tài)解釋型 | 運(yùn)行時 | 延遲綁定 | Python |
| 虛擬機(jī)型 | 類加載期 | 字節(jié)碼驗(yàn)證 | Java/C# |
理解這些差異有助于:
-
掌握不同語言的編譯模型
-
避免跨語言編程時的陷阱
-
優(yōu)化代碼組織和構(gòu)建過程
-
深入理解編程語言設(shè)計(jì)哲學(xué)
每種模型都有其歷史背景和適用場景,現(xiàn)代語言也常融合多種方式(如JIT編譯),但核心區(qū)別在于函數(shù)名與函數(shù)體綁定的時機(jī),這決定了語言的靈活性、性能和工程特性。
def add(a, b): ... 語句時發(fā)生了什么:
-
函數(shù)體在模塊加載時編譯
整個模塊的所有函數(shù)體在模塊編譯階段就被編譯為字節(jié)碼,不是在執(zhí)行到def語句時才編譯。 -
函數(shù)對象在運(yùn)行時創(chuàng)建
執(zhí)行到def語句對應(yīng)的MAKE_FUNCTION字節(jié)碼時,才創(chuàng)建函數(shù)對象并綁定到命名空間。
階段 1: 模塊編譯(加載時)
當(dāng)Python導(dǎo)入模塊或執(zhí)行腳本時:
# 偽代碼:模塊編譯過程
def compile_module(source):
# 1. 解析為AST
ast_tree = parse_to_ast(source)
# 2. 編譯所有函數(shù)體為字節(jié)碼
for function_node in ast_tree.function_defs:
# 編譯函數(shù)體為獨(dú)立代碼對象
function_node.bytecode = compile_function_body(function_node.body)
# 3. 生成模塊字節(jié)碼
module_bytecode = generate_module_bytecode(ast_tree)
return module_bytecode
關(guān)鍵點(diǎn):
-
所有函數(shù)體在模塊加載時就被編譯
-
函數(shù)字節(jié)碼存儲在模塊字節(jié)碼的常量池中
-
此時函數(shù)名尚未綁定到函數(shù)對象
-
階段 2: 執(zhí)行 MAKE_FUNCTION(運(yùn)行時)
當(dāng)虛擬機(jī)執(zhí)行到模塊字節(jié)碼中的
MAKE_FUNCTION指令時:python復(fù)制下載# 偽代碼:MAKE_FUNCTION指令實(shí)現(xiàn) def execute_MAKE_FUNCTION(vm): # 從棧中獲取參數(shù) code_obj = vm.pop() # 預(yù)編譯的函數(shù)體字節(jié)碼 name = vm.pop() # 函數(shù)名 # 創(chuàng)建函數(shù)對象 func_obj = FunctionObject( code=code_obj, globals=vm.current_frame.globals, name=name ) # 壓回棧頂供STORE_NAME使用 vm.push(func_obj)階段 3: 執(zhí)行 STORE_NAME(運(yùn)行時)
緊接著執(zhí)行
STORE_NAME指令:python復(fù)制下載def execute_STORE_NAME(vm, name): func_obj = vm.pop() # 獲取剛創(chuàng)建的函數(shù)對象 current_scope = vm.current_frame.locals # 綁定到當(dāng)前命名空間 current_scope[name] = func_obj驗(yàn)證實(shí)驗(yàn):證明函數(shù)體提前編譯
python復(fù)制下載import dis import types # 編譯模塊但不執(zhí)行 module_code = compile('def add(a,b): return a+b', '<string>', 'exec') # 從模塊常量池獲取函數(shù)代碼對象 add_code = next( c for c in module_code.co_consts if isinstance(c, types.CodeType) and c.co_name == 'add' ) # 查看函數(shù)體字節(jié)碼(尚未執(zhí)行def語句) print("=== 函數(shù)體字節(jié)碼(未執(zhí)行def前)===") dis.dis(add_code) # 執(zhí)行模塊字節(jié)碼 exec(module_code) # 查看運(yùn)行時函數(shù)對象 print("\n=== 運(yùn)行時函數(shù)對象 ===") print(add) # <function add at 0x...> print(add.__code__ is add_code) # True
輸出:
text復(fù)制下載=== 函數(shù)體字節(jié)碼(未執(zhí)行def前)=== 1 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4 BINARY_ADD 6 RETURN_VALUE === 運(yùn)行時函數(shù)對象 === <function add at 0x7f9d5c3b5d30> True修正后的模型解釋
1. 函數(shù)名處理(編譯期)
-
在AST中創(chuàng)建函數(shù)定義節(jié)點(diǎn)
-
不創(chuàng)建函數(shù)對象
-
不綁定函數(shù)名到命名空間
2. 函數(shù)體處理(編譯期)
-
編譯函數(shù)體為獨(dú)立的代碼對象
-
存儲在模塊字節(jié)碼的常量池中
3. 函數(shù)對象創(chuàng)建(運(yùn)行時)
-
執(zhí)行
MAKE_FUNCTION指令時創(chuàng)建函數(shù)對象 -
不重新編譯函數(shù)體(使用預(yù)編譯的代碼對象)
4. 函數(shù)名綁定(運(yùn)行時)
-
執(zhí)行
STORE_NAME指令時綁定函數(shù)名 -
在當(dāng)前作用域添加名稱->函數(shù)對象的映射
我混淆了兩個重要概念:
-
函數(shù)體編譯:發(fā)生在模塊加載時(編譯期)
-
函數(shù)對象實(shí)例化:發(fā)生在執(zhí)行
def語句時(運(yùn)行時)
在Python中,
def語句實(shí)際上對應(yīng)兩條字節(jié)碼:text復(fù)制下載LOAD_CONST <code_object> # 加載預(yù)編譯的函數(shù)體 MAKE_FUNCTION # 創(chuàng)建函數(shù)對象 STORE_NAME # 綁定函數(shù)名
對比 Perl 模型
特性 Python Perl 函數(shù)體編譯 模塊加載時 編譯期 函數(shù)對象創(chuàng)建 執(zhí)行def時 編譯期(outer)/運(yùn)行時(inner) 函數(shù)名綁定 執(zhí)行def時 編譯期(outer)/運(yùn)行時(inner) 字節(jié)碼存儲 模塊常量池 符號表 閉包處理 函數(shù)對象中 魔法變量 總結(jié):Python 的函數(shù)處理流程
-
模塊加載時:
-
編譯所有函數(shù)體為字節(jié)碼
-
存儲在模塊字節(jié)碼的常量池中
-
-
執(zhí)行def語句時:

-
后續(xù)調(diào)用:
-
直接使用已綁定的函數(shù)對象
-
執(zhí)行預(yù)編譯的函數(shù)體字節(jié)碼
-
1. 什么是"預(yù)編譯"?
-
時間點(diǎn):模塊加載時(import或執(zhí)行腳本時)
-
操作:將Python源代碼轉(zhuǎn)換為字節(jié)碼
-
特點(diǎn):
-
發(fā)生在任何代碼執(zhí)行之前
-
整個模塊的所有函數(shù)體都被編譯
-
結(jié)果被緩存(.pyc文件或內(nèi)存)
-
2. 預(yù)編譯 vs 即時編譯(JIT)
特性 預(yù)編譯 即時編譯(JIT) 時機(jī) 執(zhí)行前 運(yùn)行時 單元 整個模塊 熱點(diǎn)代碼 緩存 磁盤.pyc文件 內(nèi)存緩存 優(yōu)化 基本優(yōu)化 激進(jìn)優(yōu)化 代表 Python PyPy, Java HotSpot -
-
這種設(shè)計(jì)確保了:
-
函數(shù)體只編譯一次(高效)
-
每次執(zhí)行
def創(chuàng)建新函數(shù)對象(支持閉包) -
函數(shù)名在定義后立即可用(符合直覺)
感謝您指出矛盾,這幫助我提供了更精確的解釋!Python 的編譯模型確實(shí)很獨(dú)特:函數(shù)體在編譯期處理,而函數(shù)對象的創(chuàng)建和綁定在運(yùn)行時完成。
-
-
-
3. 驗(yàn)證實(shí)驗(yàn)
實(shí)驗(yàn)1:編譯期函數(shù)名不存在
# 編譯階段嘗試訪問函數(shù)名
try:
print(add) # 編譯期函數(shù)名不存在
except NameError as e:
print(f"編譯期錯誤: {e}")
# 函數(shù)定義
def add(a, b):
return a + b
# 運(yùn)行時訪問
print("運(yùn)行時:", add) # <function add at 0x...>
輸出:
編譯期錯誤: name 'add' is not defined 運(yùn)行時: <function add at 0x7f9d5c3b5d30>
實(shí)驗(yàn)2:多次執(zhí)行定義
def create_adder():
print(">>> 執(zhí)行 def 語句")
def adder(x):
return x + 10
return adder
# 首次調(diào)用
print("第一次調(diào)用:")
func1 = create_adder()
print(func1(5)) # 15
# 二次調(diào)用
print("\n第二次調(diào)用:")
func2 = create_adder()
print(func2(5)) # 15
print("\n函數(shù)對象相同:", func1 is func2)
print("函數(shù)代碼相同:", func1.__code__ is func2.__code__)
輸出:
第一次調(diào)用: >>> 執(zhí)行 def 語句 15 第二次調(diào)用: >>> 執(zhí)行 def 語句 15 函數(shù)對象相同: False 函數(shù)代碼相同: True # 函數(shù)體字節(jié)碼被復(fù)用
函數(shù)名在編譯和執(zhí)行過程中的變化
-
源代碼中的標(biāo)識符 :在源代碼中,函數(shù)名就是一個普通的標(biāo)識符,用于定義函數(shù),例如在
def func_name():中,func_name是一個標(biāo)識符。 -
詞法分析(生成 token 流) :詞法分析器會將源代碼轉(zhuǎn)換成一系列的 token。函數(shù)名會被識別為一個標(biāo)識符 token(
NAME類型的 token)。 -
語法分析(生成 AST 樹) :語法分析器會根據(jù) Python 的語法規(guī)則,將 token 流解析成抽象語法樹(AST)。在 AST 中,函數(shù)定義會被表示為一個
FunctionDef節(jié)點(diǎn)。這個節(jié)點(diǎn)包含了函數(shù)名(作為name屬性)、函數(shù)的參數(shù)(args屬性)、函數(shù)體(body屬性)等信息。此時,函數(shù)名作為FunctionDef節(jié)點(diǎn)的一個屬性存在。 -
語義分析 :主要進(jìn)行類型檢查、作用域分析等。對于函數(shù)名來說,語義分析會確定它的作用域(全局、局部等),確保函數(shù)在正確的范圍內(nèi)被引用。如果函數(shù)名在同一個作用域中有重復(fù)定義,會報錯。
-
生成字節(jié)碼 :Python 會將 AST 轉(zhuǎn)換為字節(jié)碼。字節(jié)碼中并不直接包含函數(shù)名,而是包含函數(shù)定義的指令。函數(shù)名在字節(jié)碼中主要體現(xiàn)在函數(shù)對象的創(chuàng)建和引用上。當(dāng)函數(shù)被調(diào)用時,解釋器會根據(jù)字節(jié)碼中的指令來定位和執(zhí)行對應(yīng)的函數(shù)。
-
執(zhí)行階段(函數(shù)對象的創(chuàng)建和綁定) :當(dāng) Python 解釋器執(zhí)行到
def語句時,會根據(jù) AST 中的FunctionDef節(jié)點(diǎn)來創(chuàng)建一個函數(shù)對象。這個函數(shù)對象會包含函數(shù)的代碼、參數(shù)信息、默認(rèn)值、閉包等信息。隨后,Python 會將函數(shù)名(標(biāo)識符)與這個函數(shù)對象進(jìn)行綁定,也就是在當(dāng)前的作用域(通常是全局作用域或包含該函數(shù)定義的局部作用域)中,將函數(shù)名作為鍵,函數(shù)對象作為值,存入相應(yīng)的符號表中。,這樣后續(xù)通過函數(shù)名就可以引用到對應(yīng)的函數(shù)對象了。
原因如下:
-
便于處理 :將源代碼分解為 token,可以讓后續(xù)的編譯過程更高效地處理代碼。每個 token 都有其特定的類型(如關(guān)鍵字、標(biāo)識符、運(yùn)算符等),這樣編譯器就可以快速識別代碼中的元素并進(jìn)行相應(yīng)的處理,而不是直接處理原始的、連續(xù)的字符序列。
-
統(tǒng)一表示 :token 為源代碼提供了一種統(tǒng)一的表示方式。不同的編程語言可能有不同的語法和語義,但通過將它們的源代碼轉(zhuǎn)換為 token,可以在一定程度上簡化編譯器的設(shè)計(jì)和實(shí)現(xiàn),使得編譯器能夠以一種相對通用的方式處理不同的語言。
-
語義關(guān)聯(lián) :每個 token 都與其在編程語言中的語義相關(guān)聯(lián)。例如,關(guān)鍵字 token 代表特定的語言結(jié)構(gòu)(如
if用于條件語句),標(biāo)識符 token 用于表示變量、函數(shù)等的名稱,運(yùn)算符 token 表示相應(yīng)的操作等。這種語義關(guān)聯(lián)有助于編譯器理解代碼的含義,并生成正確的中間代碼或目標(biāo)代碼。
.__code__ 是用來訪問函數(shù)對象的代碼對象的。.__code__ 屬性,可以獲取這些信息。func:def func():
print("Hello, World!")
code_obj = func.__code__
code_obj 是一個 code 對象,它包含以下信息:-
co_code :字節(jié)碼。
-
co_consts :常量池。
-
co_names :全局變量名列表。
-
co_varnames :局部變量名列表。
-
co_filename :函數(shù)定義所在的文件名。
-
co_firstlineno :函數(shù)定義所在的行號。
co_code:print(code_obj.co_code)
import dis
def func():
print("Hello, World!")
code_obj = func.__code__
print("co_code:", code_obj.co_code)
print("co_consts:", code_obj.co_consts)
print("co_names:", code_obj.co_names)
print("co_varnames:", code_obj.co_varnames)
print("co_filename:", code_obj.co_filename)
print("co_firstlineno:", code_obj.co_firstlineno)
# 反匯編字節(jié)碼
dis.dis(func)
co_code: b'd\x00\x00\x83\x00\x00S\x00\x00'
co_consts: (None, 'Hello, World!')
co_names: ('print',)
co_varnames: ()
co_filename: <stdin>
co_firstlineno: 1
1 0 LOAD_CONST 1 ('Hello, World!')
2 LOAD_CONST 0 (None)
4 RETURN_VALUE
dis.dis(func) 用于反匯編函數(shù)的字節(jié)碼,輸出每條指令的含義。代碼對象和函數(shù)對象的區(qū)別
1. 概念不同
-
代碼對象(Code Object) : 代碼對象是 Python 編譯過程的產(chǎn)物,它代表了一段可執(zhí)行的代碼。代碼對象包含了字節(jié)碼、常量池、局部變量表、全局變量名列表等信息。代碼對象本身并不包含執(zhí)行環(huán)境的信息,它只是一個代碼的表示形式。代碼對象可以通過
__code__屬性從函數(shù)對象中獲取。 -
函數(shù)對象(Function Object) : 函數(shù)對象是 Python 中的
def語句執(zhí)行時創(chuàng)建的對象。它不僅包含了代碼對象,還包含了函數(shù)的執(zhí)行環(huán)境信息,例如默認(rèn)參數(shù)、閉包變量、函數(shù)的命名空間等。函數(shù)對象是可調(diào)用的,當(dāng)調(diào)用函數(shù)時,Python 解釋器會根據(jù)函數(shù)對象中的代碼對象來執(zhí)行相應(yīng)的字節(jié)碼。
2. 包含的信息不同
-
代碼對象包含的信息 :
-
co_code: 字節(jié)碼。 -
co_consts: 常量池。 -
co_names: 全局變量名列表。 -
co_varnames: 局部變量名列表。 -
co_filename: 函數(shù)定義所在的文件名。 -
co_firstlineno: 函數(shù)定義所在的行號。 -
co_flags: 代碼對象的標(biāo)志(例如是否包含嵌套代碼、是否使用了生成器等)。 -
co_lnotab: 行號表,用于將字節(jié)碼的偏移量映射到源代碼的行號。
-
-
函數(shù)對象包含的信息 :
-
__code__: 代碼對象。 -
__defaults__: 默認(rèn)參數(shù)值。 -
__kwdefaults__: 帶關(guān)鍵字的默認(rèn)參數(shù)值。 -
__globals__: 函數(shù)的全局命名空間。 -
__closure__: 閉包變量(如果函數(shù)是閉包的一部分)。 -
__name__: 函數(shù)的名稱。 -
__doc__: 函數(shù)的文檔字符串。
-
3. 生命周期不同
-
代碼對象的生命周期 : 代碼對象是在編譯階段生成的,它在 Python 程序的執(zhí)行過程中存在。只要函數(shù)對象存在,其對應(yīng)的代碼對象就存在。代碼對象也可以通過
compile()函數(shù)單獨(dú)生成,用于動態(tài)執(zhí)行代碼。 -
函數(shù)對象的生命周期 : 函數(shù)對象是在執(zhí)行階段,當(dāng) Python 解釋器遇到
def語句時創(chuàng)建的。函數(shù)對象的生命周期取決于其作用域。如果函數(shù)是在模塊級別定義的,函數(shù)對象會在模塊加載時創(chuàng)建,并且在模塊存在期間一直存在。如果函數(shù)是在局部作用域中定義的(例如在另一個函數(shù)中),函數(shù)對象會在其定義的作用域存在期間存在。
4. 關(guān)聯(lián)性
-
代碼對象和函數(shù)對象的關(guān)聯(lián) : 函數(shù)對象包含一個代碼對象(通過
__code__屬性訪問)。代碼對象定義了函數(shù)的代碼邏輯,而函數(shù)對象則提供了執(zhí)行這段代碼所需的上下文環(huán)境。沒有函數(shù)對象,代碼對象本身無法執(zhí)行,因?yàn)樗鄙賵?zhí)行所需的上下文信息(例如局部變量、全局變量、默認(rèn)參數(shù)等)。
__call__ 方法是 Python 中實(shí)現(xiàn)函數(shù)調(diào)用的核心機(jī)制之一,但調(diào)用函數(shù)對象的方式不僅僅依賴于 __call__,還有更多細(xì)節(jié)。函數(shù)調(diào)用的過程
-
查找函數(shù)對象
-
當(dāng)在代碼中使用函數(shù)名調(diào)用函數(shù)時,Python 解釋器首先會在當(dāng)前作用域鏈中查找該函數(shù)名。作用域鏈的查找順序遵循 LEGB(Local、Enclosing、Global、Built - in)規(guī)則。
-
例如,在一個函數(shù)內(nèi)部調(diào)用另一個函數(shù),解釋器會先在局部作用域(Local)查找該函數(shù)名;如果找不到,會查找外層封閉作用域(Enclosing);如果還是找不到,會查找全局作用域(Global);最后會查找內(nèi)置作用域(Built - in)。
-
-
調(diào)用函數(shù)對象
-
如果找到了函數(shù)名對應(yīng)的函數(shù)對象,Python 解釋器會調(diào)用該函數(shù)對象的
__call__方法。__call__是一個特殊的方法,它使得對象是“可調(diào)用的”。 -
例如,對于一個函數(shù)對象
func,調(diào)用它就像這樣:func()。實(shí)際上,Python 會執(zhí)行func.__call__()。這個方法負(fù)責(zé)設(shè)置函數(shù)調(diào)用的上下文,如參數(shù)傳遞、創(chuàng)建新的棧幀等。
-
-
創(chuàng)建棧幀
-
在調(diào)用函數(shù)對象的
__call__方法時,Python 解釋器會創(chuàng)建一個新的棧幀。棧幀用于存儲函數(shù)調(diào)用過程中的各種信息,包括局部變量、參數(shù)、返回地址等。 -
棧幀的創(chuàng)建是由 Python 解釋器底層實(shí)現(xiàn)的,它與函數(shù)對象的
__call__方法調(diào)用緊密相關(guān)。具體來說,當(dāng)__call__方法被調(diào)用時,解釋器會分配內(nèi)存來創(chuàng)建棧幀,并將相關(guān)的信息(如函數(shù)參數(shù))存儲在其中。
-
代碼示例
def func(a, b):
print("Function called with a = {}, b = {}".format(a, b))
return a + b
# 查找函數(shù)對象
# 在全局作用域找到 func 函數(shù)對象
result = func(3, 4) # 調(diào)用函數(shù)對象的 __call__ 方法
-
func是一個函數(shù)對象。 -
當(dāng)調(diào)用
func(3, 4)時,Python 解釋器查找并找到func函數(shù)對象,然后調(diào)用它的__call__方法。 -
在調(diào)用
__call__方法的過程中,解釋器創(chuàng)建一個新的棧幀來存儲函數(shù)調(diào)用的相關(guān)信息,例如參數(shù)a和b的值。
__call__ 方法。在調(diào)用 __call__ 方法的過程中,Python 解釋器會創(chuàng)建一個新的棧幀來管理函數(shù)調(diào)用的上下文。1.詞法分析和語法分析
-
詞法分析 :源代碼被分解為一系列的 token。
-
語法分析 :根據(jù) Python 的語法規(guī)則,將 token 流解析成抽象語法樹(AST)。AST 是源代碼的樹狀表示,反映了代碼的結(jié)構(gòu)和邏輯。
2.生成代碼對象
-
AST 樹的遍歷和轉(zhuǎn)換 :Python 編譯器會遍歷 AST 樹,并將其轉(zhuǎn)換為中間表示形式,最終生成代碼對象。代碼對象包含字節(jié)碼、常量池、局部變量表等信息。這個過程是在 AST 樹生成之后進(jìn)行的。
3.代碼對象的生成
-
字節(jié)碼生成 :在生成代碼對象的過程中,AST 樹的每個節(jié)點(diǎn)會被轉(zhuǎn)換為相應(yīng)的字節(jié)碼指令。字節(jié)碼是 Python 虛擬機(jī)可執(zhí)行的低級指令。
-
常量池和變量表的構(gòu)建 :代碼對象還會包含常量池(存儲字面量值)和局部變量表(存儲局部變量的名稱)等信息。
4.執(zhí)行階段
-
函數(shù)對象的創(chuàng)建 :當(dāng) Python 解釋器執(zhí)行到
def語句時,會根據(jù)代碼對象創(chuàng)建一個函數(shù)對象。函數(shù)對象包含代碼對象以及執(zhí)行代碼所需的上下文信息(如默認(rèn)參數(shù)、閉包變量等)。 -
函數(shù)調(diào)用 :當(dāng)函數(shù)被調(diào)用時,Python 解釋器會根據(jù)函數(shù)對象中的代碼對象來執(zhí)行字節(jié)碼。
co_consts的屬性形式體現(xiàn)。1.局部變量存儲
-
棧幀為函數(shù)調(diào)用期間的局部變量提供存儲空間。當(dāng)進(jìn)入一個函數(shù)時,所有在該函數(shù)內(nèi)部定義的局部變量都會被存儲在棧幀中。
-
例如:
def func():
a = 1
b = "hello"
print(a, b)
func()
func 函數(shù)時,棧幀會為變量 a 和 b 分配空間,存儲它們的值。2.參數(shù)傳遞
-
棧幀負(fù)責(zé)存儲傳遞給函數(shù)的參數(shù)。當(dāng)調(diào)用一個帶有參數(shù)的函數(shù)時,傳遞的參數(shù)值會被存儲在棧幀中,方便函數(shù)內(nèi)部使用。
-
例如:
def add(a, b):
return a + b
result = add(3, 5)
add 函數(shù)時,參數(shù) 3 和 5 會被存儲在棧幀中,供函數(shù)內(nèi)部的 a 和 b 使用。3.返回地址存儲
-
棧幀存儲了函數(shù)調(diào)用后的返回地址。當(dāng)函數(shù)執(zhí)行完畢后,解釋器會根據(jù)返回地址繼續(xù)執(zhí)行調(diào)用該函數(shù)的代碼。
-
例如:
def func():
print("Hello")
print("Before function call")
func()
print("After function call")
func 函數(shù)之前,解釋器會將調(diào)用 func 后的代碼地址(即 print("After function call") 的位置)存儲在棧幀中。當(dāng) func 執(zhí)行完畢后,解釋器根據(jù)這個返回地址繼續(xù)執(zhí)行后續(xù)代碼。
浙公網(wǎng)安備 33010602011771號