Python 描述符是控制屬性存取、資料驗證和實作超程式設計的利器。透過 __slots__ 可最佳化物件記憶體使用,避免 __dict__ 的開銷,提升程式效能。描述符協定中的 __get__、__set__ 和 __delete__ 方法賦予開發者對屬性操作的精細控制,實作資料驗證和延遲載入等功能。同時,描述符也能改善裝飾器的實作,確保方法正確繫結到物件例項。除了描述符,生成器和迭代器是 Python 處理大量資料的關鍵工具。生成器惰性求值的特性有效節省記憶體,適用於大型資料集和無限序列。迭代器模式與 Python 語言的整合,讓生成器能與其他部分無縫協作。非同步程式設計中,生成器是協程的基礎,結合 yield from、await 和 async def 等語法,可編寫高效的非同步程式碼。
Python 描述符(Descriptor)的高階應用與最佳實踐
描述符(Descriptor)是 Python 中一個強大且進階的功能,它允許開發者以更靈活和可重用的方式實作屬性存取控制、資料驗證以及其他與屬性相關的功能。描述符是實作 ORM(Object-Relational Mapping)、屬性存取控制、以及其他與超程式設計相關功能的核心。
使用 __slots__ 最佳化記憶體使用
在 Python 中,物件預設使用 __dict__ 來儲存例項變數,這使得物件可以動態地新增屬性。然而,這也帶來了額外的記憶體開銷。透過使用 __slots__,可以明確指定物件的屬性,從而避免使用 __dict__,減少記憶體使用。
from dataclasses import dataclass
@dataclass
class Coordinate2D:
__slots__ = ("lat", "long")
lat: float
long: float
def __repr__(self):
return f"{self.__class__.__name__}({self.lat}, {self.long})"
#### 內容解密:
1. **`__slots__` 的作用**:透過指定 `__slots__`,Python 只為定義的屬性分配記憶體,避免了 `__dict__` 的使用,從而減少了記憶體開銷。
2. **限制動態新增屬性**:使用 `__slots__` 後,物件無法動態新增新的屬性,這使得物件更加靜態和可控。
3. **`__repr__` 方法**:提供了物件的字串表示,方便除錯和日誌記錄。
描述符協定(Descriptor Protocol)
描述符協定定義了三個主要方法:__get__、__set__ 和 __delete__。這些方法允許開發者控制屬性的讀取、寫入和刪除操作。
為什麼使用描述符?
描述符提供了一種機制,使得開發者可以在屬性存取時執行自定義邏輯。這對於實作資料驗證、延遲載入和其他與屬性相關的功能非常有用。
在裝飾器中實作描述符
描述符可以用於改進裝飾器的實作,使其能夠正確地繫結到物件例項上。
import types
class MyDecorator:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
return types.MethodType(self.func, instance)
def my_decorator(func):
return MyDecorator(func)
class MyClass:
@my_decorator
def my_method(self):
return "Hello, World!"
obj = MyClass()
print(obj.my_method()) # 輸出: Hello, World!
#### 內容解密:
1. **`MyDecorator` 類別**:實作了描述符協定的 `__get__` 方法,將裝飾的函式轉換為繫結方法。
2. **`my_decorator` 函式**:傳回 `MyDecorator` 的例項,用於裝飾 `my_method` 方法。
3. **`MyClass` 類別**:使用 `@my_decorator` 裝飾 `my_method` 方法,使其成為一個繫結方法。
最佳實踐與注意事項
- 最小化描述符協定方法的實作:通常,只需實作
__get__方法即可滿足大多數需求。 - 避免過度複雜的型別註解:由於描述符的通用性,型別註解可能會變得複雜。建議使用檔案字串(docstring)來記錄描述符的行為。
- 遵循物件導向設計原則:描述符本身也是物件,應遵循物件導向設計的最佳實踐,如單一職責原則和里氏替換原則。
深入理解Python中的生成器、迭代器與非同步程式設計
Python中的生成器(Generators)是使其有別於傳統程式語言的特色之一。在本章中,我們將探討生成器的原理、其被引入語言的原因以及它們所解決的問題。同時,我們也會介紹如何使用生成器來實作慣用的問題解決方案,以及如何使我們的生成器(或任何可迭代物件)具有Pythonic的風格。
生成器的基本概念
生成器的概念是建立一個可迭代的物件,在迭代過程中,它會一次產生一個元素。這樣做的最大好處是節省記憶體——我們不需要一次性將大量資料載入記憶體,而是可以根據需要逐一產生元素。這種特性使得生成器非常適合處理大型資料集,甚至是無限序列。
建立生成器
首先,讓我們來看一個例子。假設我們需要處理一個大型購買記錄資料集,以計算最低銷售價格、最高銷售價格和平均銷售價格。為簡化起見,假設我們的CSV檔案只有兩個欄位:購買日期和價格。
第一版實作:使用自定義物件處理購買記錄
class PurchasesStats:
def __init__(self, purchases):
self.purchases = iter(purchases)
self.min_price: float = None
self.max_price: float = None
self._total_purchases_price: float = 0.0
self._total_purchases = 0
self._initialize()
def _initialize(self):
# 初始化邏輯
pass
內容解密:
__init__方法初始化PurchasesStats物件,接受一個purchases引數,並將其轉換為迭代器。self.purchases被設為purchases的迭代器,這樣我們就可以逐一處理購買記錄。- 初始化了幾個例項變數來儲存最低價格、最高價格、總購買價格和總購買次數。
生成器的優點
生成器的主要優點在於它們能夠以lazy的方式計算和產生值,這意味著只有在需要時才會進行計算。這種方式可以節省大量記憶體,尤其是在處理大型或無限資料集時。
迭代器模式與Python
Python內建支援迭代器模式,這使得生成器能夠與語言的其他部分無縫整合。理解迭代器的工作原理對於寫出高效、Pythonic的程式碼至關重要。
非同步程式設計與生成器
生成器也是Python中支援協程(coroutines)和非同步程式設計的基礎。透過使用 yield from、await 和 async def 等語法,開發者可以寫出高效的非同步程式碼。
參考資料
- Python官方檔案關於描述符的介紹:https://docs.python.org/3/reference/datamodel.html#implementing-descriptors
- Python的weakref模組:https://docs.python.org/3/library/weakref.html
- Python官方檔案關於內建裝飾器作為描述符的介紹:https://docs.python.org/3/howto/descriptor.html#static-methods-and-class-methods
生成器(Generator)與迭代器(Iterator)在Python中的應用
在Python中,生成器是一種特殊的迭代器,能夠有效地處理大量資料而不需要將所有資料一次性載入記憶體中。這種特性使得生成器在處理大型資料集時具有明顯的效能優勢。
為何使用生成器?
考慮到一個處理大量購買資料的例子,我們需要計算這些資料的最小值、最大值和平均值。最初的實作方式是將所有資料讀入一個列表中,然後再進行處理:
def _load_purchases(filename):
purchases = []
with open(filename) as f:
for line in f:
*_, price_raw = line.partition(",")
purchases.append(float(price_raw))
return purchases
然而,這種方法存在效能問題,尤其是當資料集非常大時,可能會導致記憶體不足。
使用生成器最佳化效能
為瞭解決這個問題,我們可以將 _load_purchases 函式改寫為生成器:
def load_purchases(filename):
with open(filename) as f:
for line in f:
*_, price_raw = line.partition(",")
yield float(price_raw)
內容解密:
yield關鍵字:在Python中,yield關鍵字使得一個函式變成生成器。當函式執行到yield時,它會傳回一個值並暫停執行,直到下一次迭代時才繼續執行。- 記憶體效率:生成器一次只產生一個值,而不是像列表一樣將所有值儲存在記憶體中。這大大減少了記憶體的使用量,使得處理大型資料整合為可能。
- 與迴圈的相容性:生成器物件是可迭代的,可以直接用於
for迴圈中,無需修改消費資料的程式碼。
生成器表示式(Generator Expressions)
生成器也可以透過生成器表示式來建立,這與列表推導式類別似,但使用圓括號 () 而不是方括號 []:
>>> (x**2 for x in range(10))
<generator object <genexpr> at 0x...>
>>> sum(x**2 for x in range(10))
285
內容解密:
- 語法簡潔:生成器表示式提供了一種簡潔的方式來建立生成器。
- 高效性:將生成器表示式直接傳給像
sum()、max()這樣的函式,比傳遞列表更高效,因為它避免了建立不必要的列表。 - 一次性迭代:需要注意的是,生成器一旦被迭代過,就會被耗盡,不能重複使用。
迭代器與生成器的應用
在Python中,迭代器(Iterator)和生成器(Generator)是處理可迭代物件(Iterable)的強大工具。本章將探討如何使用這些工具來撰寫更具Python風格的程式碼。
迭代的慣用語
首先,我們來看看一些在處理迭代時非常有用的慣用語。這些程式碼範例將幫助我們更好地理解生成器的功能,以及如何解決與迭代相關的常見問題。
自定義迭代器
假設我們想要建立一個類別似於內建函式enumerate()的物件,但它可以產生無限序列。以下是一個簡單的類別實作:
class SequenceOfNumbers:
def __init__(self, start=0):
self.current = start
def __next__(self):
current = self.current
self.current += 1
return current
def __iter__(self):
return self
使用生成器簡化程式碼
上述程式碼可以透過使用生成器進一步簡化。生成器是一種特殊的迭代器,可以透過定義一個包含yield關鍵字的函式來建立:
def sequence(start=0):
while True:
yield start
start += 1
內容解密:
sequence函式是一個生成器,因為它包含yield關鍵字。- 當呼叫
sequence函式時,它會傳回一個生成器物件,該物件是可迭代的。 - 每次呼叫
next()函式時,生成器會執行直到遇到下一個yield陳述式,並傳回產生的值。 - 由於生成器是惰性求值的,因此即使包含無限迴圈,也不會導致記憶體溢位。
使用next()函式
next()函式用於取得迭代器的下一個元素。如果迭代器已經耗盡,則會引發StopIteration異常。我們可以透過提供預設值來避免這個異常:
>>> word = iter("hello")
>>> next(word)
'h'
>>> next(word)
'e'
>>> next(word, "default value")
'l'
>>> next(word, "default value")
'l'
>>> next(word, "default value")
'o'
>>> next(word, "default value")
'default value'
內容解密:
next()函式用於取得迭代器的下一個元素。- 如果迭代器已經耗盡,則會傳回預設值(如果提供)。
結合生成器表示式使用next()
next()函式與生成器表示式結合使用,可以實作在可迭代物件中查詢第一個符合特定條件的元素:
# 示例:查詢第一個大於5的元素
numbers = [1, 2, 3, 6, 7, 8]
first_greater_than_five = next((x for x in numbers if x > 5), None)
print(first_greater_than_five) # 輸出:6
內容解密:
- 生成器表示式
(x for x in numbers if x > 5)產生一個迭代器,該迭代器會產生大於5的元素。 next()函式用於取得第一個符合條件的元素。- 如果沒有找到符合條件的元素,則傳回預設值
None。
簡化程式碼:使用迭代器與itertools模組
在Python中,迭代器(iterators)和生成器(generators)是處理資料流的強大工具。正確使用這些工具可以簡化程式碼,提高可讀性和可維護性。本章將探討如何利用迭代器和itertools模組來最佳化程式碼。
使用生成器簡化程式碼
生成器是一種特殊的迭代器,可以在需要時生成值,而不是一次性生成所有值。這種惰性求值(lazy evaluation)機制可以節省記憶體,提高效率。
範例:處理超過特定閾值的購買記錄
假設我們需要處理購買記錄,但只對超過1000的記錄感興趣。傳統的做法是在迴圈中加入條件判斷:
def process(self):
for purchase in self.purchases:
if purchase > 1000.0:
# 處理購買記錄
pass
這種做法不僅不夠Pythonic,而且不夠靈活。如果閾值需要改變,或者需要處理多個條件,就會變得難以維護。
更好的做法是使用生成器和itertools模組:
from itertools import islice, filterfalse
purchases = islice(filter(lambda p: p > 1000.0, self.purchases), 10)
stats = PurchasesStats(purchases).process()
內容解密:
filter(lambda p: p > 1000.0, self.purchases):使用filter函式篩選出大於1000的購買記錄。islice(..., 10):使用islice函式取得前10筆符合條件的記錄。- 將篩選後的結果傳遞給
PurchasesStats類別進行處理。
使用itertools模組簡化重複迭代
在某些情況下,我們需要對同一個可迭代物件進行多次迭代。使用itertools.tee函式可以將原始可迭代物件分割成多個新的可迭代物件:
import itertools
def process_purchases(purchases):
min_, max_, avg = itertools.tee(purchases, 3)
return min(min_), max(max_), median(avg)
內容解密:
itertools.tee(purchases, 3):將原始可迭代物件purchases分割成三個新的可迭代物件。- 使用每個新的可迭代物件進行不同的迭代操作。
扁平化巢狀迴圈
巢狀迴圈可能導致程式碼變得複雜且難以維護。使用生成器可以將巢狀迴圈扁平化:
def _iterate_array2d(array2d):
for i, row in enumerate(array2d):
for j, cell in enumerate(row):
yield (i, j), cell
def search_nested(array, desired_value):
try:
coord = next(
coord for coord, cell in _iterate_array2d(array) if cell == desired_value
)
return coord
except StopIteration:
raise ValueError(f"{desired_value} not found")
內容解密:
_iterate_array2d生成器函式將二維陣列扁平化為一個可迭代物件。search_nested函式使用生成器表示式查詢目標值。