攝影或3C

Python 自省 (Introspection) 實戰:教你如何讓程式碼「自我介紹」; func_name = sys._getframe().f_code.co_name ; inspect.currentframe().f_code.co_name

# Python 自省 (Introspection) 實戰:教你如何讓程式碼「自我介紹」

你是否曾經在寫 Log 時,為了知道錯誤發生在哪個函式,而手動寫下這樣的程式碼?

def process_data():
    try:
        # ... 做一些事
        pass
    except Exception as e:
        print("Error in process_data: ", e)  # 手動要把函式名稱寫進去

如果以後函式改名了,這個字串通常會忘記改,導致 Log 資訊誤導。其實,Python 作為一門具有強大 **自省 (Introspection)** 能力的語言,它完全知道「自己現在正在執行哪一行、在哪個函式裡」。

本文將帶你深入 Python 的幕後機制:**Function、Code Object 與 Frame** 的三角關係。

## 1. 用英文學程式:Inspection vs Introspection

在深入技術之前,我們先從日常生活來理解這兩個詞的差別,因為這正是 Python 設計這些功能的初衷。

### Inspection (視察 / 檢查)

*   **字根拆解**:`In-` (進入、往裡) + `Spect` (看)。

*   **關鍵差別**:`In-` 也是「裡面」的意思。但 inspection 通常指**由外部的人**「深入查看」另一個物體。

*   **生活情境**:海關檢查行李 (Look into the bag)、品管員檢查產品。視角是「外 -> 內」。

### Introspection (內省 / 自省)

*   **字根拆解**:`Intro-` (向內、在…之中) + `Spect` (看)。

*   **關鍵差別**:`Intro-` 特指「朝向自我內在」。這不是檢查別人,而是檢查自己。

*   **生活情境**:吾日三省吾身。視角是「內 -> 內」。

### Python 中的意義

當我們說 Python 支援 **Introspection** 時,是指賦予了程式碼一種**自我意識**

正在執行的函式可以突然停下來問:「我是誰 (func name)?」、「是誰呼叫了我 (frame)?」、「我那一張靜態樂譜 (Code Object) 長什麼樣子?」。

這也是為什麼 Python 的標準模組雖然用 `inspect` (檢查) 命名,但我們探討的主題精確來說是 **Introspection** (自省)。

## 2. 核心三角關係:Function vs Code vs Frame

要理解 Python 怎麼執行程式的,可以用「演奏音樂」來比喻:

1.  **Code Object (樂譜)**

    *   這是**靜態**的。

    *   Python 程式碼編譯後的 Bytecode、變數名稱列表、常數表都存在這裡。

    *   它不知道「變數 x 現在等於多少」,只知道「這裡有個變數叫做 x」。

    *   屬性特徵:以 `co_` 開頭,如 `co_name`, `co_varnames`。

2.  **Function Object (樂手)**

    *   這是**靜態與動態的橋樑**

    *   它把「樂譜 (Code Object)」跟「環境 (Global 變數、Default 參數)」綁在一起。

3.  **Frame Object (現場演奏)**

    *   這是**動態**的。

    *   當你**呼叫**函式時,Python 會建立一個 Frame。

    *   它記錄了「現在演奏到哪一行 (`f_lineno`)」、「現在 x 的值是多少 (`f_locals`)」。

    *   函式結束,Frame 通常就會消失。

## 3. 實戰:我現在在哪裡?

讓我們看看如何動態取得當前的函式名稱。

### 方法 A:使用 `inspect` 模組 (推薦 / 標準做法)

import inspect

def who_am_i():
    # 1. 取得當下的 Frame
    frame = inspect.currentframe()
    
    # 2.  Frame 裡面拿出 Code Object
    code = frame.f_code
    
    # 3.  Code Object 拿出函式名稱
    func_name = code.co_name
    # [實戰技巧] 資深工程師通常會直接寫成一行
    # func_name = inspect.currentframe().f_code.co_name
    
    print(f"Hello, I am function: {func_name}")

who_am_i()
# 輸出: Hello, I am function: who_am_i

### 方法 B:使用 `sys` 模組 (資深工程師愛用 / 效能更好)

雖然 `inspect` 是標準介面,但你會發現很多資深工程師或效能敏感的專案 (如 Log 系統),更喜歡用 `sys`。

理由很簡單:

1.  **更短**:`sys._getframe().f_code.co_name` 一行搞定。

2.  **更快**:`sys` 是內建模組,比 `inspect` 輕量非常多。

3.  **更常用**:因為 `sys.argv`, `sys.path` 太常用,
導致大家習慣直接用它,不想為了一個功能多 import 龐大的 `inspect`。

import sys

def debug_error():
    try:
        1 / 0
    except ZeroDivisionError:
        # _getframe(0) 代表拿那一層的 frame,0是自己,1是呼叫者
        func_name = sys._getframe().f_code.co_name
        print(f"[Error] Failed in {func_name}")

*注意:雖然 `_getframe` 前面有底線代表內部實作 (CPython),但因為太好用且太普遍,幾乎所有 Python 實作都會支援它。*

*(**PyPy** 是一個追求執行速度的 Python Alternative 實作,內建 JIT 讓程式跑得比標準 CPython 快很多)*

## 4. Python 底線命名學:為什麼要到底線?

剛才提到的 `_getframe` 以及 BeautifulSoup 的 `class_`,其實代表了 Python 社群兩種截然不同的命名慣例:

### A. 前單底線 (`_variable`):請勿打擾 (Internal Use)

*   **例子**:`sys._getframe`、`my_object._private_method`。

*   **意義**:「這是我的內部實作細節,雖然沒有強制禁止你用,後果自負。」

*   **行為**:`from module import *` 會自動忽略開頭有底線的變數。

### B. 後單底線 (`variable_`):為了避嫌 (Avoid Conflicts)

*   **例子**:BeautifulSoup 的 `find_all(class_=”highlight”)`。

*   **意義**:原本想用的字是 Python 的**保留字** (Reserved Keyword),只好加個尾巴避開。

*   **情境**:`class` 是 Python 定義類別的關鍵字,不能當參數名,所以 BeautifulSoup 團隊只好改用 `class_`。同理還有 `id_`、`type_` 等。

### C. 前後雙底線 (`__init__`):魔術方法 (Magic Methods)

*   **意義**:這是 Python 語言層級的特殊方法,通常由直譯器自動呼叫,不建議使用者自己發明新的。

## 5. 深度探索:Code Object 的秘密

我們常說 Python 函式也是物件,那它的「內臟」到底長怎樣?我們可以透過 `func.__code__` 來偷看。

def calculate(a, b=10):
    temp = a + b
    return temp

code = calculate.__code__

print("函式名稱:", code.co_name)          # calculate
print("定義在檔案:", code.co_filename)     # .../temp.py
print("定義起始行:", code.co_firstlineno)  # 1

# 這在寫 Decorator 或分析工具時很有用
print("用到的變數名:", code.co_varnames)   # ('a', 'b', 'temp')
print("參數數量:", code.co_argcount)       # 2

這就是為什麼 IDE 可以告訴你函式需要幾個參數,或者 Linter 可以在執行前就知道你用了一個沒定義的變數——因為它們都讀取了 Code Object。

## 5. 易混淆點:定義行號 vs 執行行號

*   **`code.co_firstlineno`**: 這是函式 **def** 那一行在檔案中的位置。它是永遠不變的。

*   **`frame.f_lineno`**: 這是程式 **目前跑到** 哪一行。它是隨著執行不斷變動的。

import inspect

def line_tracker():
    f = inspect.currentframe()
    print(f"Def line: {f.f_code.co_firstlineno}, Current line: {f.f_lineno}")
    print(f"Def line: {f.f_code.co_firstlineno}, Current line: {f.f_lineno}")

line_tracker()

**輸出範例:**

可以看到 Def line 固定不動,而 Current line 一直在往下跑。

## 總結

掌握 Python 的自省機制,能讓你從「寫程式的人」進階為「造工具的人」。

1.  **Debug 更輕鬆**:不用再手寫函式名稱到 Log 裡,讓程式自己告訴你它在哪。

2.  **理解 Decorator**:裝飾器之所以能保留原函式的資訊 (如 `wraps`),就是在操作這些屬性。

3.  **黑魔法基礎**:很多強大的框架 (如 Pytest、SQLAlchemy) 都是依賴這些機制來實現「神奇」功能的。

下次當你 print 出 `<function xxx at 0x…>` 時,不妨試試 `dir(xxx.__code__)`,你會發現新世界!

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡