返回文章列表

Python函式引數傳遞與設計

本文探討 Python 函式引數的傳遞機制、可變引數處理、解構應用以及設計最佳實務,包含位置引數、關鍵字引數、僅限位置引數、任意關鍵字引數等,並以程式碼範例輔助說明,最後探討軟體設計的正交性原則與程式碼結構最佳化,提升程式碼可讀性、可維護性及擴充性。

Python 軟體設計

Python 函式引數傳遞結合傳值與傳參考特性,行為取決於物件的可變性。不可變物件在函式內修改會建立新物件,不影響原始值;可變物件則直接修改原始物件。Python 提供 *args**kwargs 處理可變數量的位置和關鍵字引數,並可搭配解構指定簡化程式碼。函式設計應避免過多引數,可透過物件導向設計、引數分組或使用可變引數等方式改善。軟體設計應追求正交性,降低元件耦合度,提升可維護性和可測試性。良好的程式碼結構,例如避免大檔案、使用套件和集中管理常數,也有助於提升程式碼品質。

Python函式引數傳遞機制與可變引數處理

Python中的函式引數傳遞遵循「傳值(pass by value)」的原則,但由於Python中的變數本質上是對物件的參考(reference),因此實際上表現出來的行為會根據物件是否為可變(mutable)或不可變(immutable)而有所不同。

引數傳遞的基本原理

當我們將引數傳遞給函式時,這些引數會被指定給函式簽名定義中的變數。對於不可變物件(如字串、整數等),對其進行修改會建立新的物件並指定給函式內的區域性變數,不會影響原始變數。而對於可變物件(如列表、字典等),在函式內對其進行的修改則會直接影響原始物件。

示例:不可變與可變物件的傳遞差異

def modify_argument(argument):
    argument += " in function"
    print(argument)

immutable = "hello"
modify_argument(immutable)
print(immutable)

mutable = list("hello")
modify_argument(mutable)
print(mutable)

輸出結果:

hello in function
hello
['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']
['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']

內容解密:

  1. 當傳遞不可變物件(如字串)給函式時,函式內的修改操作會建立新的物件,不影響原始變數。
  2. 當傳遞可變物件(如列表)給函式時,函式內的修改操作會直接影響原始物件,因為列表是可變的,且函式內外都參考同一個列表物件。
  3. 在處理可變物件作為函式引數時,應謹慎避免不必要的副作用。

可變數量引數的處理

Python允許函式接受可變數量的引數,這可以透過在引數名前加上星號(*)來實作。這種機制稱為「封裝(packing)」。

示例:使用*args處理可變數量位置引數

def f(first, second, third):
    print(first)
    print(second)
    print(third)

l = [1, 2, 3]
f(*l)

輸出結果:

1
2
3

內容解密:

  1. 在函式定義中,可以使用*args來接收任意數量的位置引數,args是一個元組,包含了所有額外的位置引數。
  2. 在呼叫函式時,可以使用*運算子對列表或元組進行解封裝,將其元素作為位置引數傳遞給函式。

解封裝機制

Python的解封裝機制允許我們將序列(如列表、元組)中的元素指定給多個變數。

示例:基本解封裝與部分解封裝

a, b, c = [1, 2, 3]
print(a, b, c)

first, *rest = [1, 2, 3, 4, 5]
print(first, rest)

*rest, last = range(6)
print(last, rest)

first, *middle, last = range(6)
print(first, middle, last)

輸出結果:

1 2 3
1 [2, 3, 4, 5]
5 [0, 1, 2, 3, 4]
0 [1, 2, 3, 4] 5

內容解密:

  1. 基本解封裝允許將序列的元素直接指定給多個變數。
  2. 部分解封裝允許使用*來捕捉序列中的一部分元素到一個列表中。
  3. 可以靈活地結合使用*在序列的不同部分進行解封裝。

最佳實踐

  • 謹慎處理可變物件作為函式引數,避免不必要的副作用。
  • 利用可變數量引數和解封裝機制提高函式的靈活性。

解構變數與函式引數的高階應用

在Python程式設計中,變數解構(unpacking)是一種強大的功能,不僅可以用於簡單的指定操作,還能在迭代、函式呼叫等場景中發揮重要作用。本文將探討變數解構在迭代中的應用,以及函式引數的進階用法,包括位置引數、關鍵字引數和僅限關鍵字引數等。

迭代中的變數解構

當我們需要遍歷一個序列,而序列中的每個元素又是一個序列時,使用變數解構可以使程式碼更加簡潔易讀。下面以一個從資料函式庫行建立使用者的例子來說明:

from dataclasses import dataclass

USERS = [(i, f"first_name_{i}", f"last_name_{i}") for i in range(1_000)]

@dataclass
class User:
    user_id: int
    first_name: str
    last_name: str

def bad_users_from_rows(dbrows) -> list:
    """非Pythonic方式建立User物件"""
    return [User(row[0], row[1], row[2]) for row in dbrows]

def users_from_rows(dbrows) -> list:
    """使用變數解構建立User物件"""
    return [User(user_id, first_name, last_name) for (user_id, first_name, last_name) in dbrows]

# 或者使用星號運算元(*)進行解構
[User(*row) for row in dbrows]

內容解密:

  1. 資料類別定義:使用@dataclass裝飾器定義了一個User類別,包含了user_idfirst_namelast_name三個屬性。
  2. 迭代與解構:在users_from_rows函式中,遍歷dbrows並對每個元素進行解構,直接指定給user_idfirst_namelast_name,使程式碼更易讀。
  3. 星號運算元應用:使用User(*row)直接將row中的元素解構並傳遞給User建構函式,進一步簡化了程式碼。

函式引數的高階用法

Python的函式引數設計非常靈活,支援多種引數傳遞方式,包括位置引數、關鍵字引數和任意引數列表。

位置引數與關鍵字引數

預設情況下,Python函式的引數可以透過位置或關鍵字傳遞。例如:

def my_function(x, y):
    print(f"{x=}, {y=}")

my_function(1, 2)     # x=1, y=2
my_function(x=1, y=2) # x=1, y=2
my_function(y=2, x=1) # x=1, y=2
my_function(1, y=2)   # x=1, y=2

僅限位置引數(Python 3.8+)

Python 3.8引入了僅限位置引數的語法,在引數列表中使用/來指定之前的引數為僅限位置。例如:

def my_function(x, y, /):
    print(f"{x=}, {y=}")

my_function(1, 2)     # 正確
my_function(x=1, y=2) # 錯誤

內容解密:

  1. /語法:在函式定義中使用/來限制其前面的引數必須以位置方式傳遞。
  2. 相容性考慮:使用僅限位置引數可以避免與未來可能新增的關鍵字引數衝突,提高程式碼的向前相容性。

任意關鍵字引數(**kwargs

使用**kwargs可以讓函式接受任意數量的關鍵字引數,這些引數會被收集到一個名為kwargs的字典中。

def function(**kwargs):
    print(kwargs)

function(key="value") # {'key': 'value'}

內容解密:

  1. kwargs字典:所有額外的關鍵字引數都被收集到kwargs字典中。
  2. 避免直接操作kwargs:建議在函式簽名中直接定義需要的關鍵字引數,而不是直接從kwargs中取值,以提高程式碼的可讀性和維護性。

Python函式引數設計探討

在Python中,函式引數的設計對於程式的可讀性、可維護性和擴充套件性具有重要影響。本文將探討Python函式引數的相關議題,包括位置引數、關鍵字引數、以及引數數量的控制等。

位置引數與關鍵字引數

Python函式支援多種引數傳遞方式,包括位置引數和關鍵字引數。位置引數是根據引數在函式定義中的位置來傳遞的,而關鍵字引數則是透過指定引數名稱來傳遞的。

位置限定引數

Python 3.8引入了位置限定引數的功能,允許開發者指定某些引數只能透過位置傳遞,而不能使用關鍵字傳遞。這一功能透過在引數列表中使用 / 符號來實作。

def my_function(x, y, /):
    print(f"{x=}, {y=}")

# 正確的使用方式
my_function(1, 2)

# 錯誤的使用方式,將引發TypeError
my_function(x=1, y=2)

內容解密:

  • def my_function(x, y, /): 定義了一個函式 my_function,它接受兩個位置引數 xy
  • / 符號表示其前面的引數都是位置限定引數。
  • 當呼叫 my_function(1, 2) 時,正確地透過位置傳遞了引數。
  • 當嘗試使用關鍵字引數呼叫 my_function(x=1, y=2) 時,會引發 TypeError,因為 xy 被定義為位置限定引數。

關鍵字限定引數

與位置限定引數相對應的是關鍵字限定引數,這些引數必須透過關鍵字傳遞。在Python中,可以透過在引數列表中使用 * 符號來指定關鍵字限定引數。

def my_function(x, y, *, kw1, kw2=0):
    print(f"{x=}, {y=}, {kw1=}, {kw2=}")

# 正確的使用方式
my_function(1, 2, kw1=3, kw2=4)

# 錯誤的使用方式,將引發TypeError
my_function(1, 2, 3, 4)

內容解密:

  • def my_function(x, y, *, kw1, kw2=0): 定義了一個函式,它接受兩個位置引數 xy,以及兩個關鍵字限定引數 kw1kw2
  • * 符號表示其後面的引數都是關鍵字限定引數。
  • 當呼叫 my_function(1, 2, kw1=3, kw2=4) 時,正確地透過關鍵字傳遞了 kw1kw2
  • 當嘗試不使用關鍵字傳遞 kw1kw2 時,會引發 TypeError

函式引數數量控制

在軟體設計中,保持函式的簡潔性是非常重要的。過多的引數不僅會使函式呼叫變得複雜,還會增加程式碼的耦合度。

重構過多引數的函式

當遇到一個函式具有太多引數時,可以考慮以下幾種方法進行重構:

  1. 重新定義抽象: 將多個相關的引數封裝成一個新的物件,這樣可以減少函式的引數數量,提高程式碼的可讀性和可維護性。
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def my_function(point):
    print(f"{point.x=}, {point.y=}")

# 使用方式
point = Point(1, 2)
my_function(point)

內容解密:

  • 定義了一個 Point 類別來封裝座標 xy
  • my_function 現在只接受一個 point 物件作為引數。
  • 這種做法簡化了函式的介面,提高了程式碼的可讀性。
  1. 使用可變引數: Python支援可變數量引數的功能,可以透過 *args**kwargs 來實作。這種方式適用於需要處理不定數量輸入的情況。
def my_function(*args):
    for arg in args:
        print(arg)

# 使用方式
my_function(1, 2, 3)

內容解密:

  • def my_function(*args): 定義了一個可以接受任意數量位置引數的函式。
  • 在函式內部,可以透過遍歷 args 來處理這些引數。

軟體設計的最佳實踐:簡潔、可維護與正交性

在軟體開發的過程中,設計良好的函式和方法是至關重要的。當函式具有更通用的介面並能夠與更高層次的抽象一起工作時,它們變得更加可重複使用。這不僅適用於一般的函式,也適用於類別中的 __init__ 方法。如果一個函式需要太多引數才能正常工作,這通常被視為程式碼異味(code smell)。

精簡函式簽名:減少引數數量

假設我們遇到一個需要太多引數的函式,我們知道不能讓程式碼函式庫保持這種狀態,於是進行重構。根據不同的情況,可以應用以下的一些規則:

  1. 物件導向引數:如果大多數引數屬於一個共同的物件,那麼可以將該物件作為引數傳遞。例如,將 track_request(request.headers, request.ip_addr, request.request_id) 改為 track_request(request)。這不僅簡化了函式呼叫,也使程式碼在語義上更加合理。

  2. 避免副作用:在傳遞可變物件給函式時,必須小心處理副作用。被呼叫的函式不應修改傳遞的物件,除非這是預期的行為。否則,應複製該物件並傳回修改後的新版本。

  3. 分組引數:如果引數自然地形成一個群組,可以建立一個容器物件來容納這些引數。這種做法稱為具體化(reify),即建立設計中缺失的抽象。

  4. 使用可變引數:作為最後的手段,可以修改函式簽名以接受可變數量的引數。使用 *args**kwargs 可以增加靈活性,但也會降低函式簽名的清晰度,因此需要良好的檔案來支援。

軟體設計的最佳實踐

良好的軟體設計涉及遵循軟體工程的最佳實踐,並充分利用程式語言的特性。除了上述原則,還有一些最終的建議:

軟體的正交性

正交性意味著軟體的不同部分之間應該是獨立的,修改其中一個部分不應影響其他部分。這種設計哲學可以幫助我們建立更可維護、更穩健的軟體系統。

最終建議

  1. 保持程式碼的簡潔性:避免不必要的複雜性,使用簡單直接的方法解決問題。
  2. 充分利用語言特性:Python 提供了許多強大的功能,如生成器、迭代器等,合理利用這些特性可以提高程式碼的效率和可讀性。
  3. 注重程式碼的可讀性:使用有意義的變數名、函式名,撰寫清晰的檔案,以幫助其他開發者理解程式碼。

透過遵循這些最佳實踐,我們可以建立出更優雅、更可維護的軟體系統。

軟體設計中的正交性與程式碼結構最佳化

在軟體開發領域中,正交性(Orthogonality)是一個重要的設計原則,旨在降低系統各元件之間的耦合度,從而提高程式碼的可維護性、可擴充套件性和可測試性。正交性的核心思想是使系統的各個部分能夠獨立變化,互不影響。

正交性的重要性

正交性在軟體設計中的重要性體現在多個方面。首先,它有助於減少程式碼之間的依賴關係,使得對某一部分的修改不會對其他部分產生意外的副作用。其次,正交性使得單元測試更加容易進行,因為每個模組或函式都可以獨立測試,而不需要考慮其他部分的影響。

實務範例:正交函式的應用

考慮以下範例,展示瞭如何透過正交函式來計算和格式化價格:

def calculate_price(base_price: float, tax: float, discount: float) -> float:
    """計算含稅和折扣的最終價格"""
    return (base_price * (1 + tax)) * (1 - discount)

def show_price(price: float) -> str:
    """將價格格式化為字串"""
    return "$ {0:,.2f}".format(price)

def str_final_price(base_price: float, tax: float, discount: float, fmt_function=repr) -> str:
    """計算最終價格並根據指定的格式化函式進行格式化"""
    return fmt_function(calculate_price(base_price, tax, discount))

內容解密:

  1. calculate_price 函式:此函式負責計算含稅和折扣的最終價格。它接受基礎價格、稅率和折扣率作為輸入,並傳回計算後的價格。這個函式與價格的表示方式無關,因此具有較高的正交性。
  2. show_price 函式:此函式將價格格式化為特定的字串表示形式。它接受一個浮點數價格,並傳回一個格式化的字串。這兩個函式是正交的,因為修改其中一個不會影響另一個。
  3. str_final_price 函式:這個函式結合了 calculate_priceshow_price 的功能。它允許呼叫者指定一個格式化函式來表示最終價格,預設使用 repr 函式。這種設計使得 str_final_price 具有很高的靈活性,並且保持了與其他函式的正交性。

程式碼結構最佳化

除了正交性之外,良好的程式碼結構對於軟體的可維護性和擴充套件性也至關重要。以下是一些最佳化程式碼結構的建議:

  • 避免大檔案:將大檔案拆分成多個小檔案,每個檔案包含相關的功能或類別。這樣可以提高程式碼的可讀性和可維護性。
  • 使用套件:Python 中的套件(Package)機制允許將相關的模組組織在一起。透過建立套件,可以保持程式碼的結構清晰,並且方便重用。
  • 集中管理常數:將常數定義在單獨的檔案中,並在需要的地方匯入。這樣可以避免常數的重複定義,並且方便統一管理。

程式碼結構最佳化示意圖

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Python函式引數傳遞與設計

package "Python 應用架構" {
    package "應用層" {
        component [主程式] as main
        component [模組/套件] as modules
        component [設定檔] as config
    }

    package "框架層" {
        component [Web 框架] as web
        component [ORM] as orm
        component [非同步處理] as async
    }

    package "資料層" {
        database [資料庫] as db
        component [快取] as cache
        component [檔案系統] as fs
    }
}

main --> modules : 匯入模組
main --> config : 載入設定
modules --> web : HTTP 處理
web --> orm : 資料操作
orm --> db : 持久化
web --> cache : 快取查詢
web --> async : 背景任務
async --> fs : 檔案處理

note right of web
  Flask / FastAPI / Django
end note

@enduml

此圖示說明瞭如何將大型檔案重構為多個小檔案,並組織成套件結構的過程。