描述器、類別裝飾器和 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 必須是正數
內容解密:
PositiveNumber類別:定義了一個描述器,用於驗證屬性值是否為正數。__set_name__(self, owner, name):當描述器被賦予類別屬性時呼叫,用於設定屬性名稱。__set__(self, instance, value):當屬性被指定時呼叫,用於驗證數值是否為正數,如果不是則引發ValueError錯誤。__get__(self, instance, owner):當屬性被讀取時呼叫,用於傳回屬性值。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
內容解密:
UppercaseString類別:定義了一個描述器,用於將字串轉換為大寫。__set__(self, instance, value):當屬性被指定時呼叫,用於將數值轉換為大寫字串後儲存。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 是唯讀屬性
內容解密:
ReadOnly類別:定義了一個描述器,用於建立唯讀屬性。__set__(self, instance, value):當屬性被指定時呼叫,直接引發AttributeError錯誤,阻止屬性被修改。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
內容解密:
count_instances(cls)函式:接受一個類別cls作為引數,並傳回一個新的類別CountedClass。CountedClass類別:繼承自原始類別cls,並新增一個count屬性,用於追蹤例項數量。__init__(self, *args, **kwargs):修改後的初始化方法,每次建立例項時都會將CountedClass.count加 1。@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
內容解密:
log_methods(cls)函式:接受一個類別cls作為引數,並遍歷類別的所有屬性。callable(method):判斷屬性是否為可呼叫的方法。logged_method(self, *args, **kwargs):一個封裝原始方法的函式,用於在呼叫原始方法前後記錄日誌。setattr(cls, name, logged_method):將原始方法替換為logged_method。@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()
# 輸出:
# 哈囉, 小明!
# 我是小孩!
內容解密:
Parent類別:定義了一個greet()方法,用於輸出問候語。Child類別:繼承自Parent類別,並覆寫了greet()方法。super().greet():在Child類別的greet()方法中,呼叫父類別Parent的greet()方法。- 擴充套件父類別行為:
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
內容解密:
Child類別的__init__(self, name, age):在子類別的建構子中,首先使用super().__init__(name)呼叫父類別的建構子,初始化name屬性。- 新增子類別屬性:然後,子類別的建構子初始化自身的
age屬性。
Python 中的繼承機制:使用 super() 的優雅方式
在物件導向程式設計中,繼承是一種強大的機制,允許我們建立根據現有類別的新類別。Python 提供了簡單而有效的繼承語法,其中 super() 函式扮演著關鍵角色。它讓我們能夠在子類別中呼叫父類別的方法,實作程式碼的重用和擴充套件。
假設我們正在開發一個圖形應用程式,需要建立不同型別的形狀,例如矩形和圓形。我們可以先建立一個通用的 Shape 類別,其中包含所有形狀共有的屬性和方法,例如顏色和位置。然後,我們可以建立 Rectangle 和 Circle 類別,它們繼承自 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()
程式碼解說
- Shape 類別: 定義了一個
__init__()方法,用於初始化形狀的顏色。還定義了一個draw()方法,用於繪製形狀。 - Rectangle 類別: 繼承自
Shape類別。在__init__()方法中,我們使用super().__init__(color)呼叫父類別的__init__()方法,以初始化顏色屬性。然後,我們初始化矩形的寬度和高度屬性。 - 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))
程式碼解說
__slots__定義: 在Person類別中,我們定義了__slots__ = ['name', 'age']。這告訴 Python,Person物件只能擁有name和age屬性。- 記憶體分配: 當我們建立
Person物件時,Python 會直接在物件中分配記憶體來儲存name和age屬性,而不是使用字典。
使用 __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)
程式碼解說
increment()函式: 這個函式簡單地將全域變數x增加 1,重複 100 萬次。- 多執行緒: 我們建立 10 個執行緒,每個執行緒都執行
increment()函式。 - 結果: 由於 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("所有下載完成!")
程式碼解密
- import threading: 匯入Python的執行緒模組,用於建立和管理執行緒。
- import requests: 匯入requests模組,用於傳送HTTP請求以下載網頁內容。
- download_url(url) 函式:
- 接收一個URL作為引數。
- 使用requests.get(url)傳送GET請求,下載URL的內容。
- 使用print()函式印出已下載的位元組數和URL。
- urls 列表: 包含要下載的URL字串列表。
- threads 列表: 用於儲存所有建立的執行緒。
- 迴圈建立並啟動執行緒:
- 遍歷urls列表中的每個URL。
- 使用threading.Thread()建立一個新的執行緒,target設定為download_url函式,並將URL作為引數傳遞給該函式。
- 將建立的執行緒加入threads列表。
- 使用t.start()啟動執行緒,開始執行下載任務。
- 等待所有執行緒完成:
- 遍歷threads列表中的每個執行緒。
- 使用t.join()等待執行緒完成。這會阻塞主執行緒,直到所有執行緒都執行完畢。
- 印出完成訊息:
- 當所有執行緒都完成後,印出"所有下載完成!“的訊息。
在這個範例中,我們定義了一個 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)
程式碼解密
- import multiprocessing: 匯入Python的多程式模組,用於建立和管理程式。
- square(x) 函式: 接收一個數字x作為引數,計算x的平方並傳回結果。
- if name == ‘main’:: 確保以下程式碼只在主程式中執行,而不是在匯入的模組中執行。這對於多程式碼非常重要,以避免遞迴產生新的程式。
- numbers 列表: 包含要計算平方的數字列表。
- with Pool() as pool: 建立一個程式池,使用預設的核心數。
with陳述式確保程式池在使用完畢後會自動關閉,釋放資源。 - results = pool.map(square, numbers):
- 使用
pool.map()方法將square函式應用於numbers列表中的每個元素。 pool.map()將numbers列表分割成小塊,並將這些小塊分配給程式池中的不同程式平行處理。- 每個程式計算列表中一部分數字的平方,並將結果傳回。
pool.map()等待所有程式完成計算,然後將結果彙整合一個列表results。
- 使用
- 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()
程式碼解密
- import multiprocessing: 匯入Python的多程式處理模組,用於建立和管理平行執行的程式。
- worker(num) 函式:
- 定義一個名為
worker的函式,該函式接收一個引數num,用於標識工作程式的編號。 - 函式的功能是印出一條訊息,顯示哪個工作程式正在執行。
- 函式執行完成後傳回。
- 定義一個名為
- if name == ‘main’::
- 確保以下程式碼只在主程式執行時執行,而不是在模組被匯入時執行。
- 這是使用
multiprocessing模組時的標準做法,以避免在子程式中遞迴產生新的程式。
- jobs = []: 建立一個空列表
jobs,用於儲存將要建立的程式物件。 - 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函式和一個引數傳遞給建構函式,以識別工作程式。
- p = multiprocessing.Process(target=worker, args=(i,)):
接著,我們將每個程式附加到 jobs 列表,並透過呼叫 start 方法來啟動它。當我們執行此程式碼時,應該會看到五條訊息列印到主控台,表明每個工作程式都在其自己的程式中同時執行。
我們也可以使用 multiprocessing 模組中的 Pool 類別來建立工作程式池。Pool 類別提供了一種方便的方式來建立固定數量的程式,並將任務分配給它們。以下範例展示如何使用 Pool 類別:
import multiprocessing
def worker(num):
"""工作程式函式"""
print(f'工作程式 {num} 正在執行')
程式碼解密
- import multiprocessing: 匯入Python的多程式模組,用於建立和管理平行執行的程式。
- worker(num) 函式:
- 定義一個名為
worker的函式,該函式接收一個引數num,用於標識工作程式的編號。 - 函式的功能是印出一條訊息,顯示哪個工作程式正在執行。
- 函式執行完成後傳回。
- 定義一個名為
總結:
在Python中,可以透過執行緒和程式來實作平行處理,以提升I/O密集型和CPU密集型任務的效能。執行緒適用於I/O密集型任務,可以讓主執行緒在等待I/O操作完成時繼續執行其他任務。程式適用於CPU密集型任務,可以利用多個CPU核心平行執行計算。multiprocessing 模組提供了建立和管理程式的便捷方式,可以根據任務的性質選擇合適的平行處理方式,以達到最佳的效能。
