描述器、類別裝飾器和 super() 是 Python 開發中不可或缺的利器,能讓程式碼更簡潔、更具彈性,同時提升程式碼的可讀性和可維護性。這些進階技巧在實際專案中應用廣泛,能有效解決許多常見的程式設計問題。

在 Python 中,描述器是一種底層的協定,它允許我們自定義物件屬性的存取方式。透過描述器,我們可以攔截屬性的讀取、設定和刪除操作,並在這些操作執行時加入自定義的邏輯。這對於資料驗證、型別轉換、屬性快取等應用場景非常有用。類別裝飾器則是一種更進階的技巧,它可以讓我們在不修改類別原始碼的情況下,修改或擴充套件類別的行為。這對於增加日誌記錄、效能監控、許可權控制等功能非常方便。super() 函式則是在繼承體系中,用於呼叫父類別方法的關鍵工具。它可以確保程式碼的正確性和可維護性,尤其是在多重繼承的情況下,更能有效避免程式碼的混亂和錯誤。掌握這三種進階技巧,可以讓我們寫出更優雅、更具 Python 風格的程式碼。

Python 進階技巧:描述器、類別裝飾器與 Super() 的深度應用

在 Python 的世界裡,除了基礎的語法和資料結構,更重要的是掌握一些進階技巧,讓你的程式碼更簡潔、更具彈性。玄貓將帶領大家深入瞭解描述器(Descriptors)、類別裝飾器(Class Decorators)和 super() 函式,這些都是 Python 開發中不可或缺的利器。

描述器:屬性管理的煉金術

描述器是一種協管屬性存取行為的協定。簡單來說,它允許你自定義屬性的讀取、寫入和刪除操作。這在需要驗證、轉換或計算屬性值時非常有用。

驗證資料:確保資料品質

假設我們需要一個類別,其屬性必須是正數。傳統的做法是在 __init__() 方法中進行驗證,但使用描述器可以將驗證邏輯封裝起來,讓程式碼更清晰。

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError(f"{self.name} 必須是正數")
        instance.__dict__[self.name] = value

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

class MyClass:
    x = PositiveNumber()

obj = MyClass()
obj.x = 10  # 正常運作
obj.x = -10 # 觸發 ValueError: x 必須是正數

內容解密:

  1. PositiveNumber 類別:定義了一個描述器,用於驗證屬性值是否為正數。
  2. __set_name__(self, owner, name):當描述器被賦予類別屬性時呼叫,用於設定屬性名稱。
  3. __set__(self, instance, value):當屬性被指定時呼叫,用於驗證數值是否為正數,如果不是則引發 ValueError 錯誤。
  4. __get__(self, instance, owner):當屬性被讀取時呼叫,用於傳回屬性值。
  5. MyClass 類別:使用 PositiveNumber 描述器來定義 x 屬性。

轉換資料:統一資料格式

另一個常見的應用場景是資料轉換。例如,我們希望將所有字串屬性儲存為大寫。

class UppercaseString:
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        instance.__dict__[self.name] = str(value).upper()

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

class MyOtherClass:
    name = UppercaseString()

obj2 = MyOtherClass()
obj2.name = "john"
print(obj2.name)  # 輸出: JOHN

內容解密:

  1. UppercaseString 類別:定義了一個描述器,用於將字串轉換為大寫。
  2. __set__(self, instance, value):當屬性被指定時呼叫,用於將數值轉換為大寫字串後儲存。
  3. MyOtherClass 類別:使用 UppercaseString 描述器來定義 name 屬性。

唯讀屬性:保護資料完整性

描述器也可以用於建立唯讀屬性,防止屬性被意外修改。

class ReadOnly:
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        raise AttributeError(f"{self.name} 是唯讀屬性")

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

class MyClass:
    x = ReadOnly()

obj = MyClass()
obj.x = 10  # 觸發 AttributeError: x 是唯讀屬性

內容解密:

  1. ReadOnly 類別:定義了一個描述器,用於建立唯讀屬性。
  2. __set__(self, instance, value):當屬性被指定時呼叫,直接引發 AttributeError 錯誤,阻止屬性被修改。
  3. MyClass 類別:使用 ReadOnly 描述器來定義 x 屬性。

類別裝飾器:修改類別行為的魔法

類別裝飾器是一種在不修改類別原始碼的情況下,修改或擴充套件類別行為的強大工具。它本質上是一個函式,接受一個類別作為引數,並傳回一個新的類別。

計算例項數量:追蹤物件生命週期

以下是一個類別裝飾器,用於追蹤類別例項的數量。

def count_instances(cls):
    class CountedClass(cls):
        count = 0
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            CountedClass.count += 1
    return CountedClass

@count_instances
class MyClass:
    def __init__(self, value):
        self.value = value

obj1 = MyClass(10)
obj2 = MyClass(20)
obj3 = MyClass(30)
print(MyClass.count)  # 輸出: 3

內容解密:

  1. count_instances(cls) 函式:接受一個類別 cls 作為引數,並傳回一個新的類別 CountedClass
  2. CountedClass 類別:繼承自原始類別 cls,並新增一個 count 屬性,用於追蹤例項數量。
  3. __init__(self, *args, **kwargs):修改後的初始化方法,每次建立例項時都會將 CountedClass.count 加 1。
  4. @count_instances 裝飾器:將 count_instances 裝飾器應用於 MyClass 類別,使其具有追蹤例項數量的功能。

方法日誌:追蹤程式碼執行

另一個常見的應用場景是為類別的所有方法增加日誌記錄。

def log_methods(cls):
    for name, method in cls.__dict__.items():
        if callable(method):
            def logged_method(self, *args, **kwargs):
                print(f"呼叫方法 {name}")
                return method(self, *args, **kwargs)
            setattr(cls, name, logged_method)
    return cls

@log_methods
class MyOtherClass:
    def method1(self):
        print("方法 1")

    def method2(self):
        print("方法 2")

obj = MyOtherClass()
obj.method1()
# 輸出:
# 呼叫方法 method1
# 方法 1
obj.method2()
# 輸出:
# 呼叫方法 method2
# 方法 2

內容解密:

  1. log_methods(cls) 函式:接受一個類別 cls 作為引數,並遍歷類別的所有屬性。
  2. callable(method):判斷屬性是否為可呼叫的方法。
  3. logged_method(self, *args, **kwargs):一個封裝原始方法的函式,用於在呼叫原始方法前後記錄日誌。
  4. setattr(cls, name, logged_method):將原始方法替換為 logged_method
  5. @log_methods 裝飾器:將 log_methods 裝飾器應用於 MyOtherClass 類別,使其所有方法都具有日誌記錄功能。

Super():呼叫父類別的橋樑

在繼承中,super() 函式允許子類別呼叫父類別的方法,這在需要擴充套件或修改父類別行為時非常有用。

class Parent:
    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"哈囉, {self.name}!")

class Child(Parent):
    def greet(self):
        super().greet()
        print("我是小孩!")

child = Child("小明")
child.greet()
# 輸出:
# 哈囉, 小明!
# 我是小孩!

內容解密:

  1. Parent 類別:定義了一個 greet() 方法,用於輸出問候語。
  2. Child 類別:繼承自 Parent 類別,並覆寫了 greet() 方法。
  3. super().greet():在 Child 類別的 greet() 方法中,呼叫父類別 Parentgreet() 方法。
  4. 擴充套件父類別行為Child 類別在呼叫父類別的 greet() 方法後,額外輸出了 “我是小孩!",擴充套件了父類別的行為。

另一個常見的用法是在子類別的建構子中呼叫父類別的建構子。

class Parent:
    def __init__(self, name):
        self.name = name

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

child = Child("小明", 10)
print(child.name)  # 輸出: 小明
print(child.age)   # 輸出: 10

內容解密:

  1. Child 類別的 __init__(self, name, age):在子類別的建構子中,首先使用 super().__init__(name) 呼叫父類別的建構子,初始化 name 屬性。
  2. 新增子類別屬性:然後,子類別的建構子初始化自身的 age 屬性。

Python 中的繼承機制:使用 super() 的優雅方式

在物件導向程式設計中,繼承是一種強大的機制,允許我們建立根據現有類別的新類別。Python 提供了簡單而有效的繼承語法,其中 super() 函式扮演著關鍵角色。它讓我們能夠在子類別中呼叫父類別的方法,實作程式碼的重用和擴充套件。

假設我們正在開發一個圖形應用程式,需要建立不同型別的形狀,例如矩形和圓形。我們可以先建立一個通用的 Shape 類別,其中包含所有形狀共有的屬性和方法,例如顏色和位置。然後,我們可以建立 RectangleCircle 類別,它們繼承自 Shape 類別,並增加特定於每個形狀的屬性和方法,例如矩形的寬度和高度,以及圓形的半徑。

以下是一個簡單的範例,展示瞭如何使用繼承和 super() 函式:

class Shape:
    def __init__(self, color):
        self.color = color

    def draw(self):
        print(f"Drawing a shape with color {self.color}")

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height

    def draw(self):
        super().draw()  # 呼叫父類別的 draw() 方法
        print(f"Drawing a rectangle with width {self.width} and height {self.height}")

# 建立 Rectangle 例項
rect = Rectangle("red", 10, 20)
rect.draw()

程式碼解說

  1. Shape 類別: 定義了一個 __init__() 方法,用於初始化形狀的顏色。還定義了一個 draw() 方法,用於繪製形狀。
  2. Rectangle 類別: 繼承自 Shape 類別。在 __init__() 方法中,我們使用 super().__init__(color) 呼叫父類別的 __init__() 方法,以初始化顏色屬性。然後,我們初始化矩形的寬度和高度屬性。
  3. draw() 方法:Rectangle 類別中,我們覆寫了父類別的 draw() 方法。首先,我們使用 super().draw() 呼叫父類別的 draw() 方法,以繪製通用的形狀。然後,我們增加特定於矩形的繪製程式碼。

這個範例展示了 super() 函式的強大之處。它允許我們在子類別中呼叫父類別的方法,實作程式碼的重用和擴充套件。

記憶體最佳化:Python __slots__ 的妙用

在 Python 中,每個物件都會維護一個字典(__dict__),用於儲存其屬性。雖然這種設計非常靈活,允許我們動態地增加和刪除屬性,但它也會帶來額外的記憶體開銷。當我們需要建立大量物件時,這種開銷可能會變得非常明顯。

為了最佳化記憶體使用,Python 提供了 __slots__ 機制。__slots__ 允許我們顯式地宣告一個物件可以擁有的屬性,從而避免使用字典來儲存屬性。這可以顯著減少物件的記憶體佔用。

以下是一個範例:

import sys

class Person:
    __slots__ = ['name', 'age']

    def __init__(self, name, age):
        self.name = name
        self.age = age

p1 = Person("Alice", 25)
p2 = Person("Bob", 30)

print(sys.getsizeof(p1))
print(sys.getsizeof(p2))

程式碼解說

  1. __slots__ 定義:Person 類別中,我們定義了 __slots__ = ['name', 'age']。這告訴 Python,Person 物件只能擁有 nameage 屬性。
  2. 記憶體分配: 當我們建立 Person 物件時,Python 會直接在物件中分配記憶體來儲存 nameage 屬性,而不是使用字典。

使用 __slots__ 的主要優點是減少記憶體使用。然而,它也有一些限制:

  • 無法動態增加屬性: 一旦定義了 __slots__,就無法再向物件增加新的屬性。
  • 無法使用 properties: 無法在具有 __slots__ 的類別中使用 properties 或其他動態屬性。

玄貓認為,__slots__ 是一個非常有用的工具,可以用於最佳化 Python 程式的記憶體使用。然而,在使用它之前,我們需要仔細考慮其限制,並確保它適合我們的應用場景。

Python 平行與並發:打破 GIL 的枷鎖

在現代多核心處理器的時代,平行和並發是提高程式效能的關鍵技術。Python 作為一種流行的程式語言,提供了多種實作平行和並發的方式。然而,Python 的 Global Interpreter Lock (GIL) 卻給平行帶來了挑戰。

GIL 確保在任何給定時間,只有一個執行緒可以執行 Python 位元組碼。這意味著,即使在多執行緒應用程式中,也只有一個執行緒真正地在執行 Python 程式碼。這限制了 Python 在 CPU 密集型任務上的平行能力。

那麼,我們該如何應對 GIL 的限制呢?以下是一些常見的方法:

  • 多程式 (Multiprocessing): 使用 multiprocessing 模組建立多個程式,每個程式都有自己的 Python 直譯器和記憶體空間。由於每個程式都是獨立的,因此它們可以真正地平行執行,不受 GIL 的限制。
  • 非同步 I/O (Asynchronous I/O): 對於 I/O 密集型任務,可以使用 asyncio 模組實作非同步 I/O。非同步 I/O 允許程式在等待 I/O 操作完成時執行其他任務,從而提高程式的整體效能。
  • C 擴充套件 (C Extensions): 將 CPU 密集型任務轉移到 C 程式碼中執行。C 程式碼可以釋放 GIL,從而實作真正的平行。

以下是一個使用 threading 模組的範例,展示了 GIL 的影響:

import threading

x = 0

def increment():
    global x
    for i in range(1000000):
        x += 1

threads = []
for i in range(10):
    t = threading.Thread(target=increment)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

print(x)

程式碼解說

  1. increment() 函式: 這個函式簡單地將全域變數 x 增加 1,重複 100 萬次。
  2. 多執行緒: 我們建立 10 個執行緒,每個執行緒都執行 increment() 函式。
  3. 結果: 由於 GIL 的存在,最終 x 的值通常會小於 1000 萬。這是因為多個執行緒在競爭 GIL,導致平行效率降低。

玄貓認為,理解 GIL 是編寫高效能 Python 程式的關鍵。對於 CPU 密集型任務,我們應該盡量避免使用多執行緒,而是選擇多程式或 C 擴充套件等替代方案。對於 I/O 密集型任務,非同步 I/O 是一個不錯的選擇。

總之,Python 提供了多種實作平行和並發的方式,我們可以根據具體的應用場景選擇最合適的方案。理解 GIL 的限制,可以幫助我們編寫出更高效、更具擴充套件性的 Python 程式。

Python多執行緒的I/O密集型任務最佳化

在Python中,執行緒可用於提升I/O密集型任務的效能。I/O密集型任務是指花費大量時間等待輸入/輸出操作完成的任務,例如從檔案讀取或發出網路請求。透過使用執行緒,我們可以讓主執行緒繼續執行其他任務,同時I/O密集型任務則等待I/O操作完成。

讓我們看一個範例,展示如何使用執行緒來提升I/O密集型任務的效能:

import threading
import requests

def download_url(url):
    response = requests.get(url)
    print(f"從 {url} 下載了 {len(response.content)} 位元組")

urls = [
    "https://www.example.com",
    "https://www.python.org",
    "https://www.google.com",
    "https://www.github.com",
    "https://www.stackoverflow.com",
]

threads = []
for url in urls:
    t = threading.Thread(target=download_url, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("所有下載完成!")

程式碼解密

  1. import threading: 匯入Python的執行緒模組,用於建立和管理執行緒。
  2. import requests: 匯入requests模組,用於傳送HTTP請求以下載網頁內容。
  3. download_url(url) 函式:
    • 接收一個URL作為引數。
    • 使用requests.get(url)傳送GET請求,下載URL的內容。
    • 使用print()函式印出已下載的位元組數和URL。
  4. urls 列表: 包含要下載的URL字串列表。
  5. threads 列表: 用於儲存所有建立的執行緒。
  6. 迴圈建立並啟動執行緒:
    • 遍歷urls列表中的每個URL。
    • 使用threading.Thread()建立一個新的執行緒,target設定為download_url函式,並將URL作為引數傳遞給該函式。
    • 將建立的執行緒加入threads列表。
    • 使用t.start()啟動執行緒,開始執行下載任務。
  7. 等待所有執行緒完成:
    • 遍歷threads列表中的每個執行緒。
    • 使用t.join()等待執行緒完成。這會阻塞主執行緒,直到所有執行緒都執行完畢。
  8. 印出完成訊息:
    • 當所有執行緒都完成後,印出"所有下載完成!“的訊息。

在這個範例中,我們定義了一個 download_url 函式,它使用 requests 函式庫來下載URL的內容。然後,我們建立一個要下載的URL列表,並為每個URL建立一個執行緒,將URL作為引數傳遞給 download_url 函式。

接著,我們啟動每個執行緒,並使用 join 方法等待它們完成。最後,我們列印一條訊息,表明所有下載已完成。

當我們執行此程式碼時,應該會看到下載操作在多個執行緒中同時執行。由於每個下載操作的大部分時間都花在等待I/O操作完成,因此使用執行緒可以讓我們平行下載多個URL,而不會顯著影響主執行緒的效能。

重要的是要注意,雖然執行緒可用於提升I/O密集型任務的效能,但它們可能不適合CPU密集型任務(即花費大部分時間執行計算而不是等待I/O操作完成的任務)。在這種情況下,使用多處理或其他技術可能更合適。此外,使用執行緒時應小心,以確保從多個執行緒安全地存取分享資源(例如檔案控制程式碼或資料函式庫連線)。

Python多程式處理CPU密集型任務

在Python中,程式可用於提升CPU密集型任務的效能。CPU密集型任務是指花費大部分時間執行計算或其他CPU密集型操作,而不是等待I/O操作完成的任務。透過使用多個程式,我們可以平行執行這些計算,從而利用多個CPU核心。

讓我們看一個範例,展示如何使用程式來提升CPU密集型任務的效能:

from multiprocessing import Pool

def square(x):
    return x * x

if __name__ == '__main__':
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    with Pool() as pool:
        results = pool.map(square, numbers)
    print(results)

程式碼解密

  1. import multiprocessing: 匯入Python的多程式模組,用於建立和管理程式。
  2. square(x) 函式: 接收一個數字x作為引數,計算x的平方並傳回結果。
  3. if name == ‘main’:: 確保以下程式碼只在主程式中執行,而不是在匯入的模組中執行。這對於多程式碼非常重要,以避免遞迴產生新的程式。
  4. numbers 列表: 包含要計算平方的數字列表。
  5. with Pool() as pool: 建立一個程式池,使用預設的核心數。with陳述式確保程式池在使用完畢後會自動關閉,釋放資源。
  6. results = pool.map(square, numbers):
    • 使用pool.map()方法將square函式應用於numbers列表中的每個元素。
    • pool.map()numbers列表分割成小塊,並將這些小塊分配給程式池中的不同程式平行處理。
    • 每個程式計算列表中一部分數字的平方,並將結果傳回。
    • pool.map()等待所有程式完成計算,然後將結果彙整合一個列表results
  7. print(results): 印出包含平方計算結果的列表。

在這個範例中,我們定義了一個 square 函式,它計算給定數字的平方。然後,我們建立一個要計算平方的數字列表,並使用 multiprocessing 模組中的 Pool 類別來建立一個工作程式池。接著,我們使用池的 map 方法將 square 函式應用於列表中的每個數字。

map 方法使用多個工作程式平行地將函式應用於輸入列表的每個元素。結果會以列表的形式按照提交順序傳回。

當我們執行此程式碼時,應該會看到數字的平方是使用多個程式平行計算的。由於每個計算都是CPU密集型的,並且可以獨立於其他計算執行,因此使用多個程式可以讓我們平行執行這些計算,從而利用多個CPU核心。

重要的是要注意,雖然程式可用於提升CPU密集型任務的效能,但與使用執行緒相比,它們會產生一些額外負擔。建立新程式的成本比建立新執行緒更高,並且程式間通訊(IPC)可能比執行緒間通訊更複雜。此外,使用程式時應小心,以確保從多個程式安全地存取分享資源(例如記憶體或資料函式庫連線)。

Python多程式模組的使用

Python的 multiprocessing 模組允許我們產生多個程式,以便同時執行程式碼。這對於需要利用多個CPU核心的CPU密集型任務非常有用。在本小節中,我們將探討如何在Python中使用 multiprocessing 模組來產生程式並同時執行程式碼。

以下範例展示如何使用 multiprocessing 模組:

import multiprocessing

def worker(num):
    """工作程式函式"""
    print(f'工作程式 {num} 正在執行')
    return

if __name__ == '__main__':
    jobs = []
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(i,))
        jobs.append(p)
        p.start()

程式碼解密

  1. import multiprocessing: 匯入Python的多程式處理模組,用於建立和管理平行執行的程式。
  2. worker(num) 函式:
    • 定義一個名為worker的函式,該函式接收一個引數num,用於標識工作程式的編號。
    • 函式的功能是印出一條訊息,顯示哪個工作程式正在執行。
    • 函式執行完成後傳回。
  3. if name == ‘main’::
    • 確保以下程式碼只在主程式執行時執行,而不是在模組被匯入時執行。
    • 這是使用multiprocessing模組時的標準做法,以避免在子程式中遞迴產生新的程式。
  4. jobs = []: 建立一個空列表jobs,用於儲存將要建立的程式物件。
  5. for i in range(5):: 使用迴圈建立五個程式。
    • p = multiprocessing.Process(target=worker, args=(i,)):
      • 建立一個multiprocessing.Process物件,表示一個新的程式。
      • target=worker指定程式要執行的函式為worker
      • args=(i,)i作為引數傳遞給worker函式。注意,args必須是一個元組,即使只有一個引數。
    • jobs.append(p): 將建立的程式物件p加入jobs列表中,以便後續管理。
    • p.start(): 啟動程式,使其開始平行執行worker函式。 在這個範例中,我們定義了一個名為 worker 的函式,它會列印一條訊息,表明它正在執行。然後,我們建立一個程式列表,並使用 for 迴圈建立 Process 類別的五個例項,將 worker 函式和一個引數傳遞給建構函式,以識別工作程式。

接著,我們將每個程式附加到 jobs 列表,並透過呼叫 start 方法來啟動它。當我們執行此程式碼時,應該會看到五條訊息列印到主控台,表明每個工作程式都在其自己的程式中同時執行。

我們也可以使用 multiprocessing 模組中的 Pool 類別來建立工作程式池。Pool 類別提供了一種方便的方式來建立固定數量的程式,並將任務分配給它們。以下範例展示如何使用 Pool 類別:

import multiprocessing

def worker(num):
    """工作程式函式"""
    print(f'工作程式 {num} 正在執行')

程式碼解密

  1. import multiprocessing: 匯入Python的多程式模組,用於建立和管理平行執行的程式。
  2. worker(num) 函式:
    • 定義一個名為worker的函式,該函式接收一個引數num,用於標識工作程式的編號。
    • 函式的功能是印出一條訊息,顯示哪個工作程式正在執行。
    • 函式執行完成後傳回。

總結: 在Python中,可以透過執行緒和程式來實作平行處理,以提升I/O密集型和CPU密集型任務的效能。執行緒適用於I/O密集型任務,可以讓主執行緒在等待I/O操作完成時繼續執行其他任務。程式適用於CPU密集型任務,可以利用多個CPU核心平行執行計算。multiprocessing 模組提供了建立和管理程式的便捷方式,可以根據任務的性質選擇合適的平行處理方式,以達到最佳的效能。