Python 函式的引數設計對於程式碼品質至關重要。妥善運用關鍵字引數和預設值,能讓程式碼更清晰易懂,方便日後維護,同時保有良好的擴充彈性。在台灣的軟體開發實務中,這些技巧經常被用來提升程式碼品質,以下玄貓分享一些實戰經驗。首先,關鍵字引數在引數數量較多的函式中,能有效提升程式碼可讀性。想像一個計算流量的函式 flow_rate(weight_diff, time_diff, period),使用位置引數呼叫 flow_rate(10, 5, 1) 難以理解各引數的意義。改用關鍵字引數 flow_rate(weight_diff=10, time_diff=5, period=1) 就能清楚表達每個引數的用途,大幅降低理解程式碼的門檻。其次,預設值能簡化常見情境的函式呼叫。如果 flow_rate 函式的 period 引數通常為 1,設定 period=1 後,呼叫時就能省略此引數,寫成 flow_rate(weight_diff=10, time_diff=5),更簡潔有效率。

在新增功能時,關鍵字引數搭配預設值能避免改動舊程式碼。例如,為 flow_rate 函式新增 units_per_kg 引數並設定預設值為 1,就能支援不同重量單位,同時不影響舊程式碼的運作。然而,使用預設值時要小心可變物件的陷阱。以 decode(data, default={}) 函式為例,如果解析失敗,會回傳預設的空字典。但字典是可變物件,多次呼叫 decode 函式並使用預設值,會造成多個變數指向同一個字典,可能導致非預期的結果。解決方法是將預設值設為 None,在函式內部判斷並建立新的字典。最後,處理動態預設值,例如當前的時間,建議使用 None 作為預設值,並在 docstring 中說明預設行為。直接使用 datetime.now() 作為預設值,預設值只會在函式定義時計算一次,而非每次呼叫時重新計算。總而言之,關鍵字引數和預設值是 Python 函式設計的利器,能提升程式碼可讀性、簡化函式呼叫,並兼顧程式碼的擴充性。但務必留意可變物件的陷阱,並妥善處理動態預設值,才能寫出更優雅、更穩健的 Python 程式碼。

彈性引數設計:Python 關鍵字引數與預設值的藝術

在 Python 中,函式的引數設計是一門藝術。靈活運用關鍵字引數與預設值,能讓你的程式碼更清晰、易用,同時保持良好的擴充套件性。玄貓將分享一些在實際專案中,我如何運用這些技巧來提升程式碼品質的經驗。

關鍵字引數:提升程式碼可讀性

在 Python 中,呼叫函式時,可以使用引數的位置或名稱來傳遞引數。當引數數量較多時,使用關鍵字引數可以大幅提升程式碼的可讀性。

例如,假設我們有一個計算流量的函式:

def flow_rate(weight_diff, time_diff, period):
    return (weight_diff / time_diff) * period

如果直接使用位置引數,可能會讓人困惑每個引數的意義:

flow_per_second = flow_rate(10, 5, 1)

但如果使用關鍵字引數,就能清楚地表達每個引數的用途:

flow_per_second = flow_rate(weight_diff=10, time_diff=5, period=1)

玄貓認為,在引數較多的情況下,使用關鍵字引數是提升程式碼可讀性的好方法。

預設值:簡化常見情境的呼叫

有時候,函式的某些引數在大多數情況下都有一個預設值。這時,可以為這些引數設定預設值,讓函式的呼叫更簡潔。

例如,在上面的 flow_rate 函式中,如果大多數情況下我們都計算每秒的流量,可以將 period 引數的預設值設為 1:

def flow_rate(weight_diff, time_diff, period=1):
    return (weight_diff / time_diff) * period

這樣,在計算每秒流量時,就可以省略 period 引數:

flow_per_second = flow_rate(weight_diff=10, time_diff=5)

玄貓建議,為常用引數設定預設值,可以簡化程式碼,提高開發效率。

擴充套件性:新增功能不影響舊程式碼

關鍵字引數的另一個優點是,它可以在不影響舊程式碼的情況下,為函式新增功能。

例如,假設我們想讓 flow_rate 函式支援不同的重量單位,可以新增一個 units_per_kg 引數,並設定預設值為 1:

def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
    return ((weight_diff / units_per_kg) / time_diff) * period

這樣,舊的程式碼仍然可以正常運作,而新的程式碼可以使用 units_per_kg 引數來計算不同單位的流量:

pounds_per_hour = flow_rate(weight_diff=10, time_diff=5, period=3600, units_per_kg=2.2)

玄貓在維護舊系統時,經常使用這種方法來新增功能,避免對現有程式碼造成不必要的影響。

陷阱:避免使用可變物件作為預設值

在使用預設值時,有一個需要特別注意的陷阱:避免使用可變物件(例如 list 或 dict)作為預設值。

例如,考慮以下函式:

def decode(data, default={}):
    try:
        return json.loads(data)
    except ValueError:
        return default

這個函式的目的是解析 JSON 資料,如果解析失敗,則傳回一個預設的空字典。但由於字典是可變物件,如果多次呼叫 decode 函式,與都使用預設值,那麼它們實際上會分享同一個字典。

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
print('Foo:', foo)
print('Bar:', bar)

你會發現,foobar 都包含了 stuffmeep 兩個鍵。

為了避免這個問題,可以使用 None 作為預設值,並在函式內部建立新的字典:

def decode(data, default=None):
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

玄貓建議,在設定預設值時,務必謹慎考慮物件的可變性,避免出現意料之外的行為。

動態預設值:使用 None 與 Docstring

有時候,我們希望預設值是動態的,例如當前的時間。如果直接使用 datetime.now() 作為預設值,會發現預設值只會在函式定義時計算一次,而不是每次呼叫時都重新計算。

def log(message, when=datetime.now()):
    print('%s: %s' % (when, message))

log('Hi there!')
sleep(0.1)
log('Hi again!')

為了實作動態預設值,可以使用 None 作為預設值,並在 Docstring 中說明實際的行為:

def log(message, when=None):
    """Log a message with a timestamp.

    Args:
        message: Message to print.
        when: datetime of when the message occurred.
            Defaults to the present time.
    """
    when = datetime.now() if when is None else when
    print('%s: %s' % (when, message))

玄貓認為,使用 None 和 Docstring 是一種清晰、可靠的方式來處理動態預設值。

總結來說,關鍵字引數和預設值是 Python 函式設計中不可或缺的工具。它們可以提高程式碼的可讀性、簡化函式呼叫,並在不影響舊程式碼的情況下新增功能。但同時,也需要注意一些陷阱,例如避免使用可變物件作為預設值。掌握這些技巧,能讓你的 Python 程式碼更優雅、更健壯。

預設引數的陷阱:Python 函式的隱藏地雷

在 Python 中,函式的預設引數是一個強大而方便的特性。然而,如果不小心使用,它們也可能成為程式碼中難以察覺的錯誤來源。玄貓在過去的專案中就曾多次踩到這個地雷,特別是在處理 JSON 資料時。

預設引數只會被評估一次

問題的核心在於,Python 的預設引數只會在函式定義時被評估一次。這意味著,如果預設引數是一個可變物件(例如列表或字典),那麼每次呼叫函式時,都會使用同一個物件。

讓我們看一個例子:

import json

def decode(data, default={}):
    """
    從字串載入 JSON 資料。

    Args:
        data: 要解碼的 JSON 資料。
        default: 解碼失敗時傳回的預設值。預設為空字典。
    """
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1

print('Foo:', foo)
print('Bar:', bar)

你可能會期望 foobar 是兩個不同的字典,但實際上,它們指向同一個字典物件。因此,修改 foo 也會影響 bar

Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1}

如何避免這個陷阱

解決這個問題的方法是將預設值設為 None,然後在函式內部進行判斷。

import json

def decode(data, default=None):
    """
    從字串載入 JSON 資料。

    Args:
        data: 要解碼的 JSON 資料。
        default: 解碼失敗時傳回的預設值。預設為空字典。
    """
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1

print('Foo:', foo)
print('Bar:', bar)

現在,foobar 會指向不同的字典物件,修改其中一個不會影響另一個。

Foo: {'stuff': 5}
Bar: {'meep': 1}

這種方法確保了每次呼叫函式時,都會建立一個新的字典物件,避免了預設引數的陷阱。

玄貓的建議:永遠小心可變預設引數

玄貓建議,在定義函式時,對於具有動態預設值的引數(例如列表、字典等),最好使用 None 作為預設值,並在函式內部進行處理。這樣可以避免意外的行為,並使程式碼更易於理解和維護。

提高程式碼可讀性:強制使用關鍵字引數

Python 函式的一個強大特性是可以使用關鍵字引數傳遞引數。關鍵字引數的靈活性使您可以編寫清晰的程式碼。

舉例來說,假設您想要將一個數字除以另一個數字,但要非常小心特殊情況。有時您想要忽略 ZeroDivisionError 異常並傳回無窮大。其他時候,您想要忽略 OverflowError 異常並傳回零。

def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

使用這個函式很簡單。以下呼叫將忽略除法產生的浮點數溢位,並傳回零。

result = safe_division(1, 10**500, True, False)
print(result)
# 0.0

以下呼叫將忽略除以零產生的錯誤,並傳回無窮大。

result = safe_division(1, 0, False, True)
print(result)
# inf

問題在於,很容易混淆控制異常忽略行為的兩個布林引數的位置。這很容易導致難以追蹤的錯誤。改善此程式碼可讀性的一種方法是使用關鍵字引數。預設情況下,該函式可能過於謹慎,並且總是會重新引發異常。

def safe_division_b(number, divisor,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

然後,呼叫者可以使用關鍵字引數來指定他們想要為特定操作翻轉哪些忽略標誌,從而覆寫預設行為。

safe_division_b(1, 10**500, ignore_overflow=True)
safe_division_b(1, 0, ignore_zero_division=True)

問題是,由於這些關鍵字引數是可選行為,因此沒有任何東西可以強制函式的呼叫者使用關鍵字引數來提高畫質晰度。即使用 safe_division_b 的新定義,您仍然可以使用舊方法使用位置引數來呼叫它。

safe_division_b(1, 10**500, True, False)

對於像這樣的複雜函式,最好要求呼叫者清楚地瞭解他們的意圖。在 Python 3 中,您可以透過使用僅限關鍵字引數定義函式來要求清晰度。這些引數只能透過關鍵字提供,永遠不能透過位置提供。

在這裡,玄貓重新定義了 safe_division 函式以接受僅限關鍵字引數。引數列表中的 * 符號表示位置引數的結束和僅限關鍵字引數的開始。

def safe_division_c(number, divisor, *,
                    ignore_overflow=False,
                    ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

現在,使用位置引數呼叫函式來表示關鍵字引數將不起作用。

# safe_division_c(1, 10**500, True, False)
# TypeError: safe_division_c() takes 2 positional arguments but 4 were given

關鍵字引數及其預設值按預期工作。

safe_division_c(1, 0, ignore_zero_division=True) # OK
try:
    safe_division_c(1, 0)
except ZeroDivisionError:
    pass # Expected

Python 2 中的僅限關鍵字引數

不幸的是,Python 2 沒有像 Python 3 那樣用於指定僅限關鍵字引數的明確語法。但是,您可以透過在引數列表中使用 ** 運算元來實作相同的行為,即為無效的函式呼叫引發 TypeError** 運算元與 * 運算元類別似(請參閱第 18 項:「使用可變位置引數減少視覺雜訊」),不同之處在於,它不是接受可變數量的位置引數,而是接受任意數量的關鍵字引數,即使它們未定義也是如此。

# Python 2
def print_args(*args, **kwargs):
    print 'Positional:', args
    print 'Keyword: ', kwargs

print_args(1, 2, foo='bar', stuff='meep')

玄貓的建議:在 Python 3 中擁抱僅限關鍵字引數

玄貓強烈建議在 Python 3 中使用僅限關鍵字引數,以提高程式碼的可讀性和可維護性。這可以確保函式的呼叫者清楚地瞭解他們正在做什麼,並減少因引數位置混淆而導致的錯誤。

總結

  • 預設引數只會在函式定義時被評估一次,對於可變物件要特別小心。
  • 使用 None 作為預設值,並在函式內部進行處理,可以避免預設引數的陷阱。
  • 在 Python 3 中,使用僅限關鍵字引數可以提高程式碼的可讀性和可維護性。

為何我棄用位置引數:Python 函式設計的最佳實踐

在 Python 中,函式設計的靈活性是一把雙面刃。位置引數的自由使用,有時會讓程式碼變得難以理解和維護。今天,玄貓(BlackCat)想和大家聊聊為何應該更傾向於使用關鍵字引數,以及如何在 Python 2 中模擬關鍵字引數的特性。

位置引數的隱患

位置引數的問題在於,它們的意義完全取決於其在函式呼叫中的位置。當函式接受多個布林值標記時,這種不確定性會變得特別明顯。

def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

# 這樣呼叫容易出錯,難以理解每個 True/False 的含義
safe_division(10, 2, True, False)

在上面的 safe_division 函式中,ignore_overflowignore_zero_division 的含義並不明顯。閱讀程式碼的人需要查閱函式定義才能理解每個 TrueFalse 的作用。

關鍵字引數的優勢

關鍵字引數透過名稱明確地指定引數的含義,從而提高程式碼的可讀性。

safe_division(10, 2, ignore_zero_division=True, ignore_overflow=False)

使用關鍵字引數,可以清楚地知道每個引數的作用,避免了因位置混淆而產生的錯誤。

Python 3 的關鍵字引數

Python 3 引入了明確的語法來強制使用關鍵字引數。在引數列表中,* 符號之後的所有引數都必須使用關鍵字引數傳遞。

def safe_division_k(number, divisor, *, ignore_overflow=False, ignore_zero_division=False):
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

# 必須使用關鍵字引數
safe_division_k(10, 2, ignore_zero_division=True)

# 如果嘗試使用位置引數,會引發錯誤
# safe_division_k(10, 2, True, False)  # TypeError: ...

在 Python 2 中模擬關鍵字引數

Python 2 沒有內建的關鍵字引數語法,但可以使用 **kwargs 來模擬。

def safe_division_d(number, divisor, **kwargs):
    ignore_overflow = kwargs.pop('ignore_overflow', False)
    ignore_zero_division = kwargs.pop('ignore_zero_division', False)
    if kwargs:
        raise TypeError('Unexpected **kwargs: %r' % kwargs)
    try:
        return number / divisor
    except OverflowError:
        if ignore_overflow:
            return 0
        else:
            raise
    except ZeroDivisionError:
        if ignore_zero_division:
            return float('inf')
        else:
            raise

safe_division_d(1, 0, ignore_zero_division=True)

在這個例子中,**kwargs 收集所有關鍵字引數,然後使用 kwargs.pop 來取得引數值。如果 kwargs 中還有未處理的引數,則引發 TypeError,以防止傳遞未知的引數。

玄貓(BlackCat)的經驗分享

從玄貓(BlackCat)的經驗來看,儘早採用關鍵字引數可以提高程式碼的可讀性和可維護性。即使在 Python 2 中,使用 **kwargs 模擬關鍵字引數也是值得的。

告別字典與元組:擁抱輔助類別,讓程式碼更清晰

在 Python 中,字典和元組是強大的工具,可以用於追蹤物件的動態內部狀態。但玄貓(BlackCat)認為,過度依賴它們可能會導致程式碼變得難以維護。本文將探討為何使用輔助類別(Helper Classes)通常是更好的選擇。

字典和元組的靈活性

字典非常適合維護動態的內部狀態。例如,可以使用字典來記錄學生的成績,而無需預先知道所有學生的姓名。

class SimpleGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = []

    def report_grade(self, name, score):
        self._grades[name].append(score)

    def average_grade(self, name):
        grades = self._grades[name]
        return sum(grades) / len(grades)

複雜性增加的隱憂

當需求變得更複雜時,使用字典可能會導致程式碼難以理解。例如,如果需要按科目追蹤學生的成績,則可能需要使用多層字典。

class BySubjectGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = {}

    def report_grade(self, name, subject, grade):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        grade_list.append(grade)

    def average_grade(self, name):
        by_subject = self._grades[name]
        total, count = 0, 0
        for grades in by_subject.values():
            total += sum(grades)
            count += len(grades)
        return total / count

如果需要追蹤每個成績的權重,程式碼會變得更加複雜。

class WeightedGradebook(object):
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = {}

    def report_grade(self, name, subject, score, weight):
        by_subject = self._grades[name]
        grade_list = by_subject.setdefault(subject, [])
        grade_list.append((score, weight))

    def average_grade(self, name):
        by_subject = self._grades[name]
        total, weighted_sum = 0, 0
        for subject, grades in by_subject.items():
            subject_total, subject_weight = 0, 0
            for score, weight in grades:
                subject_total += score * weight
                subject_weight += weight
            total += subject_total
            weighted_sum += subject_weight
        return total / weighted_sum

使用輔助類別的優勢

使用輔助類別可以將複雜的邏輯封裝在一個獨立的單元中,從而提高程式碼的可讀性和可維護性。

import collections

Grade = collections.namedtuple('Grade', ('score', 'weight'))

class Subject(object):
    def __init__(self):
        self._grades = []

    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))

    def average_grade(self):
        total, weighted_sum = 0, 0
        for grade in self._grades:
            total += grade.score * grade.weight
            weighted_sum += grade.weight
        return total / weighted_sum

class Student(object):
    def __init__(self):
        self._subjects = {}

    def subject(self, name):
        if name not in self._subjects:
            self._subjects[name] = Subject()
        return self._subjects[name]

    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count

class Gradebook(object):
    def __init__(self):
        self._students = {}

    def student(self, name):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]

在這個例子中,GradeSubjectStudent 都是輔助類別,它們封裝了與成績管理相關的邏輯。這樣可以使 Gradebook 類別更加簡潔易懂。

玄貓(BlackCat)的建議

當使用字典或元組來管理複雜的內部狀態時,請考慮使用輔助類別來提高程式碼的可讀性和可維護性。

總結:

  • 關鍵字引數提高函式呼叫的可讀性。
  • Python 3 支援顯式的關鍵字引數語法。
  • Python 2 可以使用 **kwargs 模擬關鍵字引數。
  • 輔助類別可以封裝複雜的邏輯,提高程式碼的可讀性和可維護性。

擺脫巢狀字典和元組:物件導向重構之路

在處理複雜的資料結構時,開發者常常會陷入巢狀字典和元組的泥沼。這種做法在初期可能很方便,但隨著需求增加,程式碼的可讀性和可維護性會迅速下降。玄貓將分享如何透過物件導向設計,將複雜的資料結構重構為更清晰、更易於管理的程式碼。

在先前的成績管理系統中,我們使用巢狀字典來儲存學生的科目成績,就像這樣:

book = {}
book['Albert Einstein'] = {}
book['Albert Einstein']['Math'] = []
book['Albert Einstein']['Math'].append((80, 0.10))
# ...

這種結構很快就會變得難以理解和維護。當需要新增更多資訊,例如每次成績的評語時,程式碼會變得更加複雜。

從元組到具名元組:小資料類別的優雅選擇

當資料變得稍微複雜時,我們可能會考慮使用元組來儲存成績和權重:

grades = []
grades.append((95, 0.45))
# ...
total = sum(score * weight for score, weight in grades)
total_weight = sum(weight for _, weight in grades)
average_grade = total / total_weight

然而,當需要新增更多資訊時,例如老師的評語,我們可能會這樣做:

grades = []
grades.append((95, 0.45, 'Great job'))
# ...
total = sum(score * weight for score, weight, _ in grades)
total_weight = sum(weight for _, weight, _ in grades)
average_grade = total / total_weight

這種模式很快就會變得難以管理。此時,namedtuple 就派上用場了。namedtuple 允許我們定義小型的、不可變的資料類別,並且可以使用名稱來存取欄位:

import collections
Grade = collections.namedtuple('Grade', ('score', 'weight'))

這樣一來,程式碼的可讀性就大大提高了。

何時避免使用具名元組?玄貓的經驗分享

雖然 namedtuple 在許多情況下都很有用,但玄貓認為在以下情況下應該避免使用它:

  • 需要預設引數值: namedtuple 無法指定預設引數值,當資料有很多可選屬性時,會變得難以使用。
  • 需要控制存取: namedtuple 的屬性可以使用數字索引和迭代來存取,這可能會導致意外的使用方式,使得日後遷移到真正的類別變得更加困難。

物件導向重構:建立清晰的資料結構

當資料結構變得複雜時,最好的方法是將其重構為類別。我們可以從底層開始,建立一個 Subject 類別來表示單一科目:

class Subject(object):
    def __init__(self):
        self._grades = []
    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))
    def average_grade(self):
        total, total_weight = 0, 0
        for grade in self._grades:
            total += grade.score * grade.weight
            total_weight += grade.weight
        return total / total_weight

然後,我們可以建立一個 Student 類別來表示單一學生,其中包含該學生所學的所有科目:

class Student(object):
    def __init__(self):
        self._subjects = {}
    def subject(self, name):
        if name not in self._subjects:
            self._subjects[name] = Subject()
        return self._subjects[name]
    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count

最後,我們可以建立一個 Gradebook 類別來表示整個成績冊,其中包含所有學生:

class Gradebook(object):
    def __init__(self):
        self._students = {}
    def student(self, name):
        if name not in self._students:
            self._students[name] = Student()
        return self._students[name]

雖然這些類別的程式碼行數比之前的實作更多,但程式碼的可讀性和可擴充套件性大大提高。