Python 程式設計中,程式碼風格與技巧的選擇會顯著影響程式效能、可讀性及維護性。本文將探討程式式與宣告式風格的差異,並以 NLTK 處理文字為例,展示宣告式風格如何提升程式碼簡潔度和效率。此外,文章也將深入講解函式設計的最佳實踐,包括引數傳遞、變數範圍、檔案編寫以及功能分解的技巧,以提升程式碼的可靠性和可重用性。最後,文章將探討如何運用生成器模式最佳化程式碼效能,並提供實用的程式碼範例與說明,幫助讀者更好地理解 Python 程式設計的精髓。

Python程式設計中的風格與效率探討

在Python程式設計中,不同的程式設計風格與技巧會對程式的效率、可讀性以及維護性產生重大影響。本篇文章將探討Python程式設計中的一些重要概念,包括程式式(Procedural)與宣告式(Declarative)風格、迴圈變數的使用,以及如何提高程式碼的可讀性和效率。

程式式 vs 宣告式風格

在計算布朗語料函式庫中單字的平均長度時,可以採用不同的程式設計風格來實作相同的功能。下面兩個例子展示了程式式和宣告式風格的不同實作方法:

程式式風格範例

tokens = nltk.corpus.brown.words(categories='news')
count = 0
total = 0
for token in tokens:
    count += 1
    total += len(token)
print(total / count)

宣告式風格範例

total = sum(len(t) for t in tokens)
print(total / len(tokens))

這兩個例子計算了相同的值,但採用了不同的程式設計風格。程式式風格需要手動管理迴圈變數和累加器,而宣告式風格則利用Python內建函式(如sum)和生成器表示式來簡化程式碼,使其更具可讀性和效率。

使用內建函式與宣告式風格的優勢

宣告式風格的程式碼通常更簡潔、更易讀。例如,找出文字中最長的單字,可以透過以下兩種方式實作:

程式式風格找出最長單字

text = nltk.corpus.gutenberg.words('milton-paradise.txt')
longest = ''
for word in text:
    if len(word) > len(longest):
        longest = word
print(longest)

宣告式風格找出所有最長單字

maxlen = max(len(word) for word in text)
print([word for word in text if len(word) == maxlen])

宣告式風格不僅提供了更簡潔的程式碼,還能一次找出所有最長的單字,而不僅僅是第一個。

迴圈變數的合理使用

在某些情況下,迴圈變數是必要的,例如提取連續的重疊n-grams:

sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper']
n = 3
print([sent[i:i+n] for i in range(len(sent)-n+1)])

NLTK提供了bigramstrigrams等函式來簡化這類別操作。

多維資料結構的構建

使用巢狀列表推導式可以方便地構建多維資料結構,例如建立一個包含m行n列的陣列,每個單元格是一個集合:

m, n = 3, 7
array = [[set() for i in range(n)] for j in range(m)]
array[2][5].add('Alice')
pprint.pprint(array)

隨著Python語言的不斷發展和NLP領域需求的日益增長,未來將出現更多高效、簡潔的程式設計技巧和工具。持續學習和掌握這些新技術,將有助於開發出更強大、更易於維護的NLP應用。

參考資料

  • NLTK官方檔案:https://www.nltk.org/book/
  • Python官方檔案:https://docs.python.org/3/

透過閱讀本文,讀者應該能夠更好地理解Python程式設計中的不同風格和技巧,並將其應用於實際的NLP任務中,以提高開發效率和程式碼品質。

函式:結構化程式設計的基礎

函式提供了一種有效的程式碼封裝和重用方式,如第2.3節所述。例如,假設我們經常需要從HTML檔案中讀取文字。這涉及多個步驟:開啟檔案、讀取檔案、規範化空白字元以及去除HTML標記。我們可以將這些步驟收集到一個函式中,並給它一個名稱,如get_text(),如範例4-1所示。

範例4-1:從檔案中讀取文字

import re

def get_text(file):
    """
    從檔案中讀取文字,規範化空白字元並去除HTML標記。
    """
    text = open(file).read()
    text = re.sub('\s+', ' ', text)
    text = re.sub(r'<.*?>', ' ', text)
    return text

現在,每當我們想要從HTML檔案中取得清理後的文字時,我們只需使用檔名作為唯一引數呼叫get_text()。它將傳回一個字串,我們可以將其指定給一個變數,例如contents = get_text("test.html")。每次我們想要使用這一系列步驟時,我們只需呼叫該函式。

內容解密:

此函式首先開啟並讀取指定的檔案。接著,使用正規表示式替換多餘的空白字元,並去除HTML標記。最後,傳回清理後的文字。

使用函式的好處是節省程式中的空間。更重要的是,我們對函式的命名選擇有助於使程式更具可讀性。在上述範例中,每當我們的程式需要從檔案中讀取清理後的文字時,我們不需要用四行程式碼使程式混亂;我們只需呼叫get_text()。這種命名方式有助於提供一些「語義解釋」——它有助於程式的讀者瞭解程式的「含義」。

函式輸入與輸出

我們透過函式的引數將資訊傳遞給函式,引數是函式定義中函式名稱後面的括號中的變數和常數列表。

def repeat(msg, num):
    return ' '.join([msg] * num)

monty = 'Monty Python'
print(repeat(monty, 3))  # 輸出:Monty Python Monty Python Monty Python

我們首先定義函式repeat,使其接受兩個引數msgnum。然後,我們呼叫函式並傳遞兩個引數monty3;這些引數填充了由引數提供的「佔位符」,並為函式體中的msgnum的出現提供值。

內容解密:

此函式透過重複msg指定的次數來建立一個新的字串,並使用空格將重複的部分連線起來。

函式引數傳遞

在第4.1節中,我們看到指定操作作用於值,但結構化物件的值是對該物件的參照。函式也是如此。Python將函式引數解釋為值(這稱為按值呼叫)。

def set_up(word, properties):
    word = 'lolcat'
    properties.append('noun')
    properties = 5

w = ''
p = []
set_up(w, p)
print(w)  # 輸出:''
print(p)  # 輸出:['noun']

請注意,w未被函式更改。當我們呼叫set_up(w, p)時,w的值(一個空字串)被指定給了一個新的變數word。在函式內部,word的值被更改,但這並不影響原始變數w

內容解密:

此範例展示了Python中函式引數的傳遞方式。對於不可變物件(如字串),函式內部的更改不會影響原始變數。對於可變物件(如列表),函式可以直接修改原始物件。

函式的優點

函式有助於使我們的工作可重用和可讀。它們還有助於使工作更可靠。當我們重用已經開發和測試過的程式碼時,我們可以更自信地認為它能正確處理各種情況。我們還消除了忘記某些重要步驟或引入錯誤的風險。呼叫我們函式的程式也具有更高的可靠性。該程式的作者正在處理一個更短的程式,其元件行為透明。

函式的可靠性和可讀性

函式捕捉了功能性。它是一段可以被賦予有意義名稱的程式碼,並執行一個明確定義的任務。函式使我們能夠從細節中抽象出來,看到更大的畫面,並更有效地程式設計。

函式設計的最佳實踐

在設計函式時,應考慮使其具有明確的輸入和輸出。避免函式同時修改其引數並傳回值的情況,因為這可能會導致混淆和錯誤。

def my_sort1(mylist):  # 良好的設計:修改其引數,不傳回值
    mylist.sort()

def my_sort2(mylist):  # 良好的設計:不觸及其引數,傳回值
    return sorted(mylist)

def my_sort3(mylist):  # 不良的設計:修改其引數並傳回值
    mylist.sort()
    return mylist

內容解密:

此範例展示了三種不同的排序函式設計。my_sort1my_sort2是良好的設計實踐,而my_sort3則可能導致混淆。

Python函式引數傳遞與變數範圍詳解

在Python程式設計中,理解函式引數的傳遞方式以及變數的範圍對於撰寫有效且可維護的程式碼至關重要。本文將探討這些主題,並提供實用的範例來說明相關概念。

引數傳遞:Call-by-Value vs Call-by-Reference

Python採用一種特殊的引數傳遞機制,既不是純粹的Call-by-Value,也不是純粹的Call-by-Reference。讓我們透過範例來理解這種機制:

def set_up(word, properties):
    word = 'lolcat'
    properties.append('noun')

w = ''
p = []

set_up(w, p)

print(w)  # 輸出:''
print(p)  # 輸出:['noun']

內容解密:

  1. w變數傳遞給word引數後,word被重新指定為’lolcat’,但這並未影響原始變數w的值。
  2. p變數傳遞給properties引數後,properties對列表進行了修改(新增’noun’元素),這個改變反映在了原始變數p上。
  3. 這種行為類別似於執行了一系列指定操作:先將原始變數的值賦給函式內部的區域性變數,然後對這些區域性變數進行操作。

變數範圍(Variable Scope)

Python中的變數範圍遵循LGB(Local, Global, Built-in)規則:

  1. 區域性(Local):Python首先在函式內部尋找變數。
  2. 全域(Global):如果在區域性範圍內找不到,則尋找全域變數。
  3. 內建(Built-in):最後檢查是否為Python的內建名稱。
x = 10  # 全域變數

def my_function():
    x = 20  # 區域性變數
    print("區域性x:", x)

my_function()  # 輸出:區域性x: 20
print("全域x:", x)  # 輸出:全域x: 10

內容解密:

  1. my_function內部,x = 20建立了一個區域性變數x,遮蔽了全域變數x
  2. 函式外部的x仍然保持原值10。

引數型別檢查

Python是一種動態型別語言,不強制要求在編寫程式時宣告變數型別。然而,在某些情況下,我們希望確保函式引數的正確型別。讓我們看看如何實作這一點:

def tag(word):
    assert isinstance(word, str), "引數必須是字串"
    if word in ['a', 'the', 'all']:
        return 'det'
    else:
        return 'noun'

print(tag('the'))  # 輸出:'det'
print(tag('knight'))  # 輸出:'noun'
print(tag(["'Tis", 'but', 'a', 'scratch']))  # 將引發AssertionError

內容解密:

  1. 使用assert陳述式檢查word引數是否為字串。
  2. 如果word不是字串,assert將引發AssertionError並顯示指定的錯誤訊息。
  3. 這種方法有助於及早發現錯誤,提高程式的健壯性。

功能分解(Functional Decomposition)

將程式分解為多個功能單一的函式是良好的程式設計實踐。這種方法提高了程式的可讀性、可維護性和可重用性。

def load_data(url):
    # 載入資料的實作
    pass

def analyze(data):
    # 分析資料的實作
    pass

def present(results):
    # 呈現結果的實作
    pass

data = load_data('http://example.com/data')
results = analyze(data)
present(results)

內容解密:

  1. 將程式邏輯分解為獨立的函式,每個函式負責一個特定的任務。
  2. 這種模組化的方法使得程式更容易理解和維護。
  3. 每個函式都可以獨立地進行測試和最佳化。

改善函式設計

讓我們透過一個具體的例子來說明如何改善函式設計:

import nltk
from nltk.probability import FreqDist

def freq_words(url, n):
    text = nltk.clean_url(url)
    freqdist = FreqDist()
    for word in nltk.word_tokenize(text):
        freqdist[word.lower()] += 1
    return freqdist.most_common(n)

url = "http://www.archives.gov/national-archives-experience/charters/constitution_transcript.html"
print(freq_words(url, 20))

內容解密:

  1. 將原本的freq_words函式重新設計,使其只負責計算指定URL中最常出現的n個詞。
  2. 函式現在傳回一個包含前n個最常見詞的列表,而不是直接列印結果。
  3. 這種設計使函式更加通用和可重用。

函式設計與檔案編寫的最佳實踐

在軟體開發過程中,函式(Function)是程式設計的基本單元。良好的函式設計能夠提升程式碼的可讀性、可維護性和重用性。本章節將探討函式設計的最佳實踐,包括函式的結構、檔案編寫以及進階應用。

精簡有效的函式設計

一個優秀的函式應該具備單一、明確的功能。以計算網頁中詞語頻率的函式 freq_words 為例:

def freq_words(url):
    """
    計算指定URL網頁中的詞語頻率分佈。
    
    @param url: 要分析的網頁URL
    @type url: C{str}
    @rtype: C{nltk.FreqDist}
    """
    freqdist = nltk.FreqDist()
    text = nltk.clean_url(url)
    for word in nltk.word_tokenize(text):
        freqdist.inc(word.lower())
    return freqdist

# 使用範例
fd = freq_words(constitution_url)
print(fd.keys()[:20])

內容解密:

  1. 函式接收一個URL引數並傳回詞頻分佈物件
  2. 使用nltk.clean_url清理網頁內容
  3. 透過nltk.word_tokenize進行詞語分割
  4. 將所有詞語轉換為小寫後統計頻率
  5. 傳回nltk.FreqDist物件以供進一步分析

函式檔案的編寫規範

完整的函式檔案應包含:

  1. 簡短的一句話功能描述
  2. 詳細的功能說明
  3. 引數說明(型別與用途)
  4. 傳回值說明
  5. 可能丟出的例外
def accuracy(reference, test):
    """
    計算測試資料與參考資料相符的比例。
    
    給定參考值列表和對應的測試值列表,傳回相等的對應值的比例。
    特別是,傳回索引{0<i<=len(test)}中C{test[i] == reference[i]}的比例。
    
    >>> accuracy(['ADJ', 'N', 'V', 'N'], ['N', 'N', 'V', 'ADJ'])
    0.5
    
    @param reference: 參考值列表
    @type reference: C{list}
    @param test: 對應的測試值列表
    @type test: C{list}
    @rtype: C{float}
    @raise ValueError: 當參考列表和測試列表長度不一致時
    """
    if len(reference) != len(test):
        raise ValueError("列表長度必須相同")
    num_correct = sum(1 for x, y in zip(reference, test) if x == y)
    return float(num_correct) / len(reference)

內容解密:

  1. 檔案字串提供了函式功能、引數和傳回值的詳細說明
  2. 使用doctest範例展示函式的正確使用方法
  3. 明確標示引數型別和傳回值型別
  4. 描述可能丟出的例外情況及其觸發條件

函式作為引數的高階應用

Python允許將函式作為引數傳遞給其他函式,這種特性大大增強了程式碼的靈活性。

使用函式作為引數的範例

def extract_property(prop, words):
    """
    提取指定屬性。
    
    @param prop: 要提取的屬性函式
    @type prop: C{function}
    @param words: 詞語列表
    @type words: C{list}
    @rtype: C{list}
    """
    return [prop(word) for word in words]

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

# 使用內建函式len()提取詞語長度
print(extract_property(len, sent))

# 使用自定義函式提取最後一個字母
def last_letter(word):
    return word[-1]
print(extract_property(last_letter, sent))

# 使用lambda表示式提取最後一個字母
print(extract_property(lambda w: w[-1], sent))

內容解密:

  1. extract_property函式接受一個屬性提取函式和一個詞語列表
  2. 可以使用內建函式(如len)或自定義函式(如last_letter)作為屬性提取函式
  3. lambda表示式提供了一種簡潔的匿名函式定義方式
  4. 展現了Python函式式程式設計的靈活性

累積型函式的設計模式

累積型函式通常用於處理序列資料並產生彙總結果。以下是兩種常見的實作方式:

列表累積模式

def search1(substring, words):
    """
    搜尋包含特定子字串的詞語。
    
    @param substring: 要搜尋的子字串
    @type substring: C{str}
    @param words: 詞語列表
    @type words: C{list}
    @rtype: C{list}
    """
    result = []
    for word in words:
        if substring in word:
            result.append(word)
    return result

生成器模式

def search2(substring, words):
    """
    使用生成器搜尋包含特定子字串的詞語。
    
    @param substring: 要搜尋的子字串
    @type substring: C{str}
    @param words: 詞語列表
    @type words: C{list}
    @rtype: C{generator}
    """
    for word in words:
        if substring in word:
            yield word

內容解密:

  1. 列表累積模式將結果儲存在列表中後一次性傳回
  2. 生成器模式使用yield關鍵字逐步產生結果,節省記憶體空間
  3. 兩種模式各有適用場景:列表模式適合小資料集,生成器模式適合大資料集