詳解Python中的Descriptor描述符類
描述符是調(diào)和屬性訪問的一個類。描述符類可用來獲取、設(shè)置或刪除屬性值。描述符對象是在類定義的時候構(gòu)建在一個類中的。
一般來說,描述符是一個具有綁定行為的對象屬性,其屬性的訪問被描述符協(xié)議方法覆寫。這些方法是__get__()、 __set__()和__delete__(),一個對象中只要包含了這三個方法(譯者注:包含至少一個),就稱它為描述符。
屬性訪問的默認行為是從一個對象的字典中獲取 (get)、設(shè)置 (set)、刪除 (delete) 屬性。例如:a.x 的查找鏈始于 a.__dict__['x'],然后是 type(a).__dict__['x'],然后是 type(a) 除元類之外的基類(譯者注:如果繼承樹很深,可能會訪問多個基類)。如果查找到的值是包含一個描述符方法的對象,那么Python可能會重寫(該對象)的默認行為并調(diào)用那個描述符方法。注意只有在新式對象或者新式類(繼承自object或者type)中描述符才會被調(diào)用。
描述符是一個功能強大、通用的協(xié)議。它們是屬性、方法、靜態(tài)方法、類方法、super()背后的實現(xiàn)機制。它們被廣泛使用于Python 2.2中用來實現(xiàn)新式類。描述符簡化了底層的C代碼并為Python編程提供了一套靈活的新工具。
描述符設(shè)計模式有兩個部分:一個所有者類和屬性描述符本身。所有者類給它的屬性使用一個或多個描述符。描述符類定義了獲取、設(shè)置和刪除方法的組合。描述符類的一個實例將會是所有者類的一個屬性。
特性是基于所有者類的方法函數(shù)。描述符不像特性,是一個類的實例,與所有者類不同。因此,描述符通常是可重用的通用屬性。所有者類可以有多個不同描述符類的實例來類管理具有相似行為的屬性。
不像其他屬性,描述符在類級別上創(chuàng)建。它們不是在__init()__初始化時創(chuàng)建。然而描述符的值可以在初始化期間設(shè)置,描述符通常是作為類的一部分,在任何方法函數(shù)之外來構(gòu)建的。
當所有者類被定義時,每個描述符對象都是被綁定到一個不同的類級別屬性的描述符類實例。
被確認為一個描述符,一個類必須實現(xiàn)以下三個方法的任意組合。
- Descriptor.__get__(self, instance, owner) -> object:在這個方法中,instance參數(shù)是即將被訪問的對象的self變量。owner參數(shù)是所有者類的對象。如果這個描述符在類的上下文中被調(diào)用,instance參數(shù)將得到一個None值。這必須返回描述符的值。
- Descriptor.__set__(self, instance, value):在這個方法中,instance參數(shù)是即將被訪問的對象的self變量。value參數(shù)是描述符需要設(shè)置的新值。
- Descriptor.__delete__(self, instance)在這個方法中,instance參數(shù)是即將被訪問的對象的self變量。該描述符的方法必須刪除這個屬性的值。
有時,一個描述符類還將需要一個__init__()方法函數(shù)來初始化描述符的內(nèi)部狀態(tài)。
有兩種基于已定義方法的描述符,如下所示:
1.非數(shù)據(jù)描述符:這種描述符定義__set__()或__delete__()或兩者皆有。它不能定義__get__()。非數(shù)據(jù)描述符對象往往會被用作表達式的一部分。它可能是一個可調(diào)用對象,或者它可能有自己的屬性或方法。一個不可變的非數(shù)據(jù)描述符必須實現(xiàn)__set__(),但可能只是拋出AttributeError。這些描述符設(shè)計時很簡單,因為接口更靈活。
2.數(shù)據(jù)描述符:這種描述符至少定義__get__()。通常,它定義__get__()和__set__()來創(chuàng)建一個可變對象。鑒于描述符將在很大程度上是不可見的,則不能更進一步的再定義屬性或方法。屬性的引用有一個數(shù)據(jù)描述符的數(shù)據(jù)被委托給描述符的__get__()、__set__()或__delete__()方法。這些是很難設(shè)計的,所以我們稍后來再看。
描述符有各種各樣的用例。在內(nèi)部,Python使用描述符有以下幾個原因:
- 在隱藏的內(nèi)部,類的方法是作為描述符來實現(xiàn)。這些非數(shù)據(jù)描述符應(yīng)用方法函數(shù)到對象以及不同的參數(shù)值。
- property()函數(shù)通過給一個字段創(chuàng)建數(shù)據(jù)描述符來實現(xiàn)。
- 一個類方法或靜態(tài)方法被實現(xiàn)為一個描述符;這被應(yīng)用到類中來代替類的實例。
當我們考慮一個描述符的目的,我們還必須為數(shù)據(jù)作為描述符可以正常工作來考察三種常見用例,如下所示:
- 描述符對象有數(shù)據(jù)或獲取到了數(shù)據(jù)。在這種情況下,描述符對象的self變量是有意義的且描述符是有狀態(tài)的。數(shù)據(jù)描述符的__get__()方法返回這個內(nèi)部數(shù)據(jù)。非數(shù)據(jù)描述符,描述符有其他方法或?qū)傩詠碓L問這些數(shù)據(jù)。
- 包含數(shù)據(jù)的所有者實例。在這種情況下,描述符對象必須使用instance參數(shù)來引用值到所有者對象中。數(shù)據(jù)描述符的__get__()方法從實例獲取數(shù)據(jù)。非數(shù)據(jù)描述符有其他方法訪問實例數(shù)據(jù)。
- 包含相關(guān)數(shù)據(jù)的所有者類。在這種情況下,描述符對象必須使用owner參數(shù)。這是常用的當描述符實現(xiàn)了應(yīng)用于整個類的靜態(tài)方法或類方法。
我們將仔細看下第一種情況。我們看看創(chuàng)建帶有__get__()和__set__()方法的數(shù)據(jù)描述符。我們也會看看創(chuàng)建沒有__get__()方法的非數(shù)據(jù)描述符。
第二種情況(所有者實例中的數(shù)據(jù))展示了@property裝飾器都做了些什么??赡艿膬?yōu)勢是描述符有一個傳統(tǒng)的特性將計算從擁有者類移到描述符類中。這傾向于分片類設(shè)計且可能不是最好的方法。如果計算是真正史詩般的復(fù)雜,策略模式可能會更好。
第三種情況展示@staticmethod和@classmethod裝飾器是如何實現(xiàn)的。我們不需要重新發(fā)明輪子。
1、使用非數(shù)據(jù)描述符
我們經(jīng)常會有一些緊密綁定了屬性值的小對象。對于這個示例,我們將看看數(shù)值被綁定到單位的舉措。
下面是一個簡單的非數(shù)據(jù)描述符類,它缺少一個__get__()方法:
class UnitValue_1: """Measure and Unit combined.""" def __init__(self, unit): self.value = None self.unit = unit self.default_format = "5.2f" def __set__(self, instance, value): self.value = value def __str__(self): return "{value:{spec}} {unit}" .format(spec=self.default_format, **self.__dict__) def __format__(self, spec="5.2f"): #print( "formatting", spec ) if spec == "": spec = self.default_format return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__)
這個類定義了一對簡單的值,一個可變的(值),另一個是有效的不可變對象(單位)。
當這個描述符被訪問時,描述符對象本身是可用的,且描述符的其他方法或?qū)傩钥梢员皇褂谩N覀兛梢允褂眠@個描述符來創(chuàng)建類去管理尺寸和其他與物理單位有關(guān)的數(shù)值。
下面是一個類,做速度-時間-距離的及早計算:
class RTD_1: rate = UnitValue_1("kt") time = UnitValue_1("hr") distance = UnitValue_1("nm") def __init__(self, rate=None, time=None, distance=None): if rate is None: self.time = time self.distance = distance self.rate = distance / time if time is None: self.rate = rate self.distance = distance self.time = distance / rate if distance is None: self.rate = rate self.time = time self.distance = rate * time def __str__(self): return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self)
一旦對象被創(chuàng)建且屬性被加載,丟失的值就已經(jīng)被計算。一旦計算,描述符可以檢查獲取值或單位的名稱。此外,描述符對str()有一個方便的響應(yīng)和請求格式。
下面是描述符和RTD_1類之間的交互:
>>> m1 = RTD_1(rate=5.8, distance=12) >>> str(m1) 'rate: 5.80 kt time: 2.07 hr distance: 12.00 nm' >>> print("Time:", m1.time.value, m1.time.unit) Time: 2.0689655172413794 hr
我們創(chuàng)建了一個帶有rate和distance參數(shù)的RTD_1實例。這些都是用來計算rate和distance描述符的__set__()方法。
當我們請求str(m1),這會計算RTD_1的所有str()方法,轉(zhuǎn)而使用rate、time和distance描述符的__format__()方法。這為我們提供了數(shù)字和單位。
鑒于非數(shù)據(jù)描述符沒有__get__()且不返回其內(nèi)部值,我們可以訪問描述符的單個元素。
2、使用數(shù)據(jù)描述符
數(shù)據(jù)描述符設(shè)計要復(fù)雜一些,因為它對接口有限制。它必須有一個__get__()方法,且只能有__set__()或__delete__()。這是所有的接口:這些方法從一到三,沒有其他方法。引入一個額外的方法意味著Python不會把該類當作一個正確的數(shù)據(jù)描述符。
我們會使用描述符設(shè)計一個簡單的單位轉(zhuǎn)換模式,可以在__get__()和__set__()方法做適當?shù)霓D(zhuǎn)換。
下面是一個單位描述符的超類,它在其他單位和標準單位之間做轉(zhuǎn)換:
class Unit: conversion = 1.0 def __get__(self, instance, owner): return instance.kph * self.conversion def __set__(self, instance, value): instance.kph = value / self.conversion
該類用簡單的乘法和除法將標準單位轉(zhuǎn)換為其他非標準單位,反之亦然。
通過這個超類,我們可以從一個標準單位定義一些轉(zhuǎn)換。在前面的示例,標準單位是千米時(公里/小時)。
以下是這兩個轉(zhuǎn)換描述符
class Knots(Unit): conversion = 0.5399568 class MPH(Unit): conversion = 0.62137119
繼承方法非常有用。唯一改變的是轉(zhuǎn)換因子。這些類可用于處理涉及單位轉(zhuǎn)換的值。我們可以處理英里每小時或可交換的節(jié)點。下面是一個標準單位的單位描述符,公里每小時:
class KPH(Unit): def __get__(self, instance, owner): return instance._kph def __set__(self, instance, value): instance._kph = value
這個類代表一個標準,所以不做任何轉(zhuǎn)換。它使用一個私有變量實例保存速度千米每小時的標準值。避免任何算術(shù)轉(zhuǎn)換是一個簡單的技術(shù)優(yōu)化。避免任何一個公共字段的引用是至關(guān)重要的,來規(guī)避無限遞歸。
下面這個類,它對于一個給定的尺寸提供了一組轉(zhuǎn)換:
class Measurement: kph = KPH() knots = Knots() mph = MPH() def __init__(self, kph=None, mph=None, knots=None): if kph: self.kph = kph elif mph: self.mph = mph elif knots: self.knots = knots else: raise TypeError def __str__(self): return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self)
對于不同的單位每個類級別的屬性都是描述符。各種描述符的獲取和設(shè)置方法會做適當?shù)霓D(zhuǎn)換。我們可以使用這個類在各種單位之間進行速度轉(zhuǎn)換。
以下是與Measurement類交互的一個例子:
>>> m2 = Measurement(knots=5.9) >>> str(m2) 'rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots' >>> m2.kph 10.92680006993152 >>> m2.mph 6.789598762345432
我們通過設(shè)置不同的描述符創(chuàng)建了一個Measurement類的對象。在第一個示例中,我們設(shè)置了節(jié)點描述符。
當我們顯示的值是一個大字符串,則每個描述符的__get__()都將被使用。這些方法從所有者對象獲取內(nèi)部kph字段值,應(yīng)用一個轉(zhuǎn)換因子,且返回一個結(jié)果值。
kph字段還使用了一個描述符。這個描述符不做任何轉(zhuǎn)換;然而,它只是返回了緩存在所有者對象的私有值。KPH和Knots描述符要求所有者類實現(xiàn)一個kph屬性。
相關(guān)文章
Python模塊學(xué)習(xí)之subprocess詳解
subprocess是Python?2.4中新增的一個模塊,它允許你生成新的進程,連接到它們的?input/output/error?管道,并獲取它們的返回(狀態(tài))碼,下面小編就來和大家聊聊它的具體使用吧2023-08-08Python標準庫datetime之datetime模塊用法分析詳解
這篇文章主要介紹了Python標準庫datetime之datetime模塊用法分析詳解,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07python opencv檢測直線 cv2.HoughLinesP的實現(xiàn)
cv2.HoughLines()函數(shù)是在二值圖像中查找直線,本文結(jié)合示例詳細的介紹了cv2.HoughLinesP的用法,感興趣的可以了解一下2021-06-06