Python 函式是程式碼組織和重用的重要工具。本文除了介紹生成器如何有效處理大量資料,避免一次性載入所有資料至記憶體外,也說明瞭高階函式 filter()map() 的應用,以及 Lambda 表示式的簡潔使用方式。此外,文章也涵蓋了命名引數和可變引數列表的用法,讓函式呼叫更具彈性和可讀性。最後,文章也探討程式除錯的挑戰,分析了常見錯誤型別,例如輸入資料的非預期特性、對函式行為的誤解、對程式語言語義的誤解,以及函式預設引數的陷阱,並提供實用的除錯策略和程式碼範例,幫助開發者更好地理解和避免這些錯誤。

Python 中的函式進階應用

在 Python 程式設計中,函式是一種重要的程式架構工具,不僅能提升程式碼的可讀性,還能有效減少重複程式碼。本篇文章將探討 Python 函式的進階應用,包括生成器(Generators)、高階函式(Higher-Order Functions)以及命名引數(Named Arguments)等主題。

生成器(Generators)

生成器是一種特殊的函式,能夠在執行過程中暫停並在稍後繼續執行。這種特性使得生成器在處理大量資料時非常高效,因為它們只在需要時生成資料,而不是一次性產生所有資料並儲存在記憶體中。

生成器範例

def search1(pattern, words):
    result = []
    for word in words:
        if pattern in word:
            result.append(word)
    return result

def search2(pattern, words):
    for word in words:
        if pattern in word:
            yield word

# 使用 search1
print("search1:")
for item in search1('zz', nltk.corpus.brown.words()):
    print(item)

# 使用 search2
print("search2:")
for item in search2('zz', nltk.corpus.brown.words()):
    print(item)

內容解密:

  • search1 函式將所有符合條件的單詞儲存在一個列表中並傳回該列表。
  • search2 函式使用 yield 關鍵字將符合條件的單詞逐一生成,而不是一次性傳回所有結果。
  • 生成器函式 search2 在每次迭代時產生一個值,並在下一次迭代時從上次中斷的地方繼續執行。

生成器在實際應用中的優勢

生成器在處理大規模資料集時具有明顯的優勢。例如,當我們需要處理一個龐大的語料函式庫時,使用生成器可以避免一次性將所有資料載入記憶體,從而節省資源並提高效率。

排列生成器範例

def permutations(seq):
    if len(seq) <= 1:
        yield seq
    else:
        for perm in permutations(seq[1:]):
            for i in range(len(perm) + 1):
                yield perm[:i] + seq[0:1] + perm[i:]

# 列出 ['police', 'fish', 'buffalo'] 的所有排列
print(list(permutations(['police', 'fish', 'buffalo'])))

內容解密:

  • permutations 函式使用遞迴方法生成輸入序列的所有排列。
  • 對於長度為 1 或更短的序列,直接傳回該序列。
  • 對於更長的序列,函式遞迴地生成剩餘元素的排列,並將第一個元素插入到所有可能的位置。

高階函式(Higher-Order Functions)

高階函式是指能夠接受其他函式作為引數或傳回函式作為結果的函式。Python 提供了一些內建的高階函式,如 filter()map()

使用 filter() 篩選內容詞

def is_content_word(word):
    return word.lower() not in ['a', 'of', 'the', 'and', 'will', ',', '.']

sent = ['Take', 'care', 'of', 'the', 'sense', ',', 'and', 'the', 'sounds', 'will', 'take', 'care', 'of', 'themselves', '.']

# 使用 filter() 篩選內容詞
print(filter(is_content_word, sent))

# 使用列表推導式實作相同功能
print([w for w in sent if is_content_word(w)])

內容解密:

  • is_content_word 函式檢查一個單詞是否為內容詞(即非功能詞)。
  • filter() 函式根據 is_content_word 的結果篩選出句子中的內容詞。
  • 列表推導式提供了另一種更為簡潔的實作方式。

使用 map() 進行元素轉換

# 計算 Brown 語料函式庫中新聞類別的句子平均長度
lengths = map(len, nltk.corpus.brown.sents(categories='news'))
print(sum(lengths) / len(lengths))

# 使用列表推導式實作相同功能
lengths = [len(sent) for sent in nltk.corpus.brown.sents(categories='news')]
print(sum(lengths) / len(lengths))

內容解密:

  • map() 函式將 len 函式應用於 Brown 語料函式庫中新聞類別的所有句子,計算每個句子的長度。
  • 列表推導式提供了一種更具可讀性的實作方式。

Lambda 表示式

Lambda 表示式是一種簡潔的定義匿名函式的方式,常用於高階函式中。

使用 Lambda 表示式計算單詞中的母音數量

# 使用 map() 和 lambda 表示式
print(map(lambda w: len(filter(lambda c: c.lower() in "aeiou", w)), sent))

# 使用列表推導式實作相同功能
print([len([c for c in w if c.lower() in "aeiou"]) for w in sent])

內容解密:

  • Lambda 表示式定義了一個匿名函式,用於計算單詞中的母音數量。
  • filter() 函式用於篩選出單詞中的母音字母。

命名引數(Named Arguments)

在 Python 中,可以使用命名引數來提高函式呼叫的可讀性,並為引數提供預設值。

命名引數範例

def repeat(msg='<empty>', num=1):
    return msg * num

print(repeat(num=3))
print(repeat(msg='Alice'))
print(repeat(num=5, msg='Alice'))

內容解密:

  • repeat 函式接受兩個引數:msgnum,並將 msg 重複 num 次。
  • 使用命名引數可以靈活地指定引數值,而不受引數順序的限制。

可變引數列表

Python 允許函式接受可變數量的引數,這些引數被封裝在元組或字典中。

可變引數範例

def generic(*args, **kwargs):
    print(args)
    print(kwargs)

generic(1, "African swallow", monty="python")

內容解密:

  • *args 用於捕捉所有未命名引數,並將它們儲存為元組。
  • **kwargs 用於捕捉所有命名引數,並將它們儲存為字典。

實際應用:統計檔案中頻繁出現的詞語

def freq_words(file, min=1, num=10, verbose=False):
    text = open(file).read()
    tokens = nltk.word_tokenize(text)
    freqdist = nltk.FreqDist(t for t in tokens if len(t) >= min)
    if verbose:
        print("Processing file:", file)
    return freqdist.keys()[:num]

# 呼叫函式
fw = freq_words('ch01.rst', min=4, num=10)
print(fw)

內容解密:

  • freq_words 函式統計檔案中長度大於等於 min 的詞語,並傳回前 num 個最頻繁的詞語。
  • verbose 引數用於控制是否列印處理進度。

程式開發與模組化設計

程式設計是一項需要多年經驗累積的技能,涵蓋多種程式語言和任務。高效能的程式設計能力包含演算法設計、結構化程式設計,以及對語言語法結構的熟悉度,和多種診斷方法的掌握。本章節將討論程式模組的內部結構、多模組程式的組織方式,以及程式開發過程中可能出現的錯誤型別、除錯方法,和避免錯誤的策略。

Python 模組結構

程式模組的目的是將邏輯相關的定義和函式集中在一起,以促程式式碼重用和抽象化。Python 模組就是單獨的 .py 檔案。例如,若我們正在處理某種特定的語料函式庫格式,就可以將讀寫該格式的函式集中在同一個模組中。同時,可以將共用的常數,如欄位分隔符號或副檔名 EXTN = ".inf",定義在模組中統一管理。這樣,當格式更新時,只需修改一個檔案即可。

模組結構範例

# Natural Language Toolkit: Distance Metrics
# Copyright (C) 2001-2009 NLTK Project
# Author: Edward Loper <edloper@gradient.cis.upenn.edu>
# URL: <http://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
距離度量。
計算兩個專案(通常是字串)之間的距離。
作為度量,它們必須滿足以下三個要求:
1. d(a, a) = 0
2. d(a, b) >= 0
3. d(a, c) <= d(a, b) + d(b, c)
"""
import nltk

def edit_distance(s1, s2):
    # 編輯距離實作
    pass

def jaccard_distance(s1, s2):
    # Jaccard 距離實作
    pass

__all__ = ['edit_distance', 'jaccard_distance']

內容解密:

此範例展示了一個典型的 Python 模組結構。首先是模組的註解區塊,包含模組標題、版權宣告、作者資訊和授權資訊。接著是模組層級的說明檔案字串(docstring),用於描述模組的功能。模組內定義了 edit_distancejaccard_distance 兩個函式,分別用於計算編輯距離和 Jaccard 距離。最後,__all__ 變數定義了模組對外暴露的介面,確保只有列出的函式會被 from module import * 語法匯入。

多模組程式設計

在實際開發中,我們經常需要編寫多模組程式,將不同任務分配到不同的模組中。例如,載入資料、進行分析、視覺化展示等任務可以分別由不同的模組負責。這樣做的好處是,可以保持各個模組的簡單性和可維護性,並且能夠建立起模組層級結構,方便構建更複雜的系統。

多模組程式結構圖示

  graph LR
    A[my_program.py] --> B[資料載入模組]
    A --> C[視覺化模組]
    B --> D[共用函式]
    C --> D

圖表翻譯: 此圖示展示了一個多模組程式的結構。主程式 my_program.py 匯入了資料載入模組和視覺化模組,兩個模組共用了某些函式。這種設計提高了程式碼的重用性和可維護性。

錯誤來源與除錯

程式設計過程中,錯誤(bugs)是不可避免的。這些錯誤可能由微小的符號錯誤引起,也可能源於邏輯錯誤。錯誤可能會在程式執行時才被發現,尤其是當使用新的資料集時。有時,修復一個錯誤可能會揭露另一個錯誤。為了應對這些挑戰,開發者需要具備多種問題解決技能。

常見錯誤型別

  1. 語法錯誤:由於違反程式語言的語法規則引起的錯誤。
  2. 邏輯錯誤:程式碼邏輯不正確,導致程式執行結果與預期不符。
  3. 執行時錯誤:在程式執行過程中發生的錯誤,如除零錯誤。

除錯策略

  1. 使用除錯工具:Python 提供了 pdb 模組,用於逐步執行程式碼、檢查變數狀態。
  2. 日誌記錄:在關鍵位置新增日誌輸出,有助於追蹤程式執行流程。
  3. 單元測試:編寫單元測試可以幫助及時發現程式碼中的錯誤。

除錯範例

import pdb

def divide(a, b):
    pdb.set_trace()  # 設定斷點
    return a / b

# 呼叫函式
result = divide(10, 0)

內容解密:

此範例展示瞭如何使用 pdb 模組進行除錯。pdb.set_trace() 陳述式會在該行設定一個斷點,程式執行到此會暫停,並進入除錯模式。在除錯模式下,可以檢查變數值、逐步執行程式碼,幫助定位錯誤原因。

除錯的困難與常見錯誤分析

程式除錯是一項艱鉅的任務,因為程式錯誤可能源自多個方面,包括對輸入資料的誤解、對演算法的理解偏差,或是對程式語言本身的誤用。以下我們將探討這些常見的問題,並提供實際案例進行分析。

輸入資料的非預期特性

輸入資料可能包含非預期的字元或格式,導致程式執行錯誤。例如,WordNet 的 synset 名稱通常具有 tree.n.01 的形式,包含三個部分並以句點分隔。NLTK 的 WordNet 模組最初使用 split('.') 來分解這些名稱。然而,當遇到像 ph.d..n.01 這樣的名稱時,該方法便會失效,因為它包含了四個句點,而非預期的兩個。最終的解決方案是使用 rsplit('.', 2),最多進行兩次分割,從右側開始處理,並保留 ph.d. 字串完整。這個問題在模組發布後數週才被發現(參見 http://code.google.com/p/nltk/issues/detail?id=297)。

程式碼範例:正確處理 synset 名稱

synset_name = "ph.d..n.01"
components = synset_name.rsplit('.', 2)
print(components)
# 輸出: ['ph.d.', 'n', '01']

內容解密:

  • rsplit('.', 2) 從字串右側開始,最多分割兩次,確保 ph.d. 不被錯誤分割。
  • 這種方法保留了 ph.d. 的完整性,避免了因過度分割導致的錯誤。
  • 正確的分割結果使得後續處理能夠順利進行。

對函式行為的誤解

有時,開發者可能會誤解所使用的函式或介面的行為。例如,在測試 NLTK 的 WordNet 介面時,一位作者發現儘管底層資料函式庫提供了大量的反義詞資訊,但沒有任何 synset 被定義為具有反義詞。後來發現,這並非介面錯誤,而是對 WordNet 本身的理解有誤:反義詞是針對 lemma 而非 synset 定義的。因此,這裡的「錯誤」其實是對介面的誤解(參見 http://code.google.com/p/nltk/issues/detail?id=98)。

程式碼範例:正確理解 WordNet 中的反義詞

from nltk.corpus import wordnet as wn

synset = wn.synset('good.a.01')
lemmas = synset.lemmas()
for lemma in lemmas:
    antonyms = lemma.antonyms()
    if antonyms:
        print(f"Antonym of {lemma.name()} is {antonyms[0].name()}")
# 正確輸出反義詞

內容解密:

  • 反義詞是透過 lemma.antonyms() 取得的,而不是直接從 synset 取得。
  • 正確理解 WordNet 的資料結構對於使用其介面至關重要。
  • 正確的程式碼邏輯確保了取得的反義詞資訊是準確的。

對程式語言語義的誤解

開發者也可能對程式語言的語義理解有誤。例如,Python 中的運算元優先順序可能導致非預期的結果。例如,"%s.%s.%02d" % "ph.d.", "n", 1 會產生 TypeError: not enough arguments for format string,因為百分號運算元 % 的優先順序高於逗號運算元。解決方案是新增括號以強制指定正確的作用域。

程式碼範例:修正運算元優先順序錯誤

print("%s.%s.%02d" % ("ph.d.", "n", 1))
# 正確輸出: ph.d..n.01

內容解密:

  • 新增括號確保了引數正確傳遞給格式化字串。
  • 正確理解運算元的優先順序對於避免語法錯誤至關重要。
  • 正確的格式化輸出確保了資料的正確顯示。

函式預設引數的陷阱

另一個常見的錯誤是對函式預設引數的誤解。例如,定義一個函式 find_words 來收集特定長度的詞彙:

def find_words(text, wordlength, result=[]):
    for word in text:
        if len(word) == wordlength:
            result.append(word)
    return result

print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3))
# 第一次呼叫輸出: ['omg', 'teh', 'teh', 'mat']
print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 2, ['ur']))
# 第二次呼叫輸出: ['ur', 'on']
print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3))
# 第三次呼叫輸出: ['omg', 'teh', 'teh', 'mat', 'omg', 'teh', 'teh', 'mat']

內容解密:

  • 預設引數 result=[] 只在函式定義時建立一次,而不是每次呼叫時重新建立。
  • 這導致了第二次呼叫時,result 列表包含了前一次呼叫的結果。
  • 正確的做法是將預設引數設為 None,並在函式內部檢查是否為 None,如果是則建立新列表。

正確的 find_words 實作

def find_words(text, wordlength, result=None):
    if result is None:
        result = []
    for word in text:
        if len(word) == wordlength:
            result.append(word)
    return result

print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3))
# 輸出: ['omg', 'teh', 'teh', 'mat']
print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 2, ['ur']))
# 輸出: ['ur', 'on']
print(find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3))
# 輸出: ['omg', 'teh', 'teh', 'mat']

內容解密:

  • 將預設引數設為 None 避免了列表物件的重複使用。
  • 在函式內部檢查 result 是否為 None,如果是則建立新列表。
  • 這種做法確保了每次呼叫函式時,結果列表都是獨立的。