[譯] 屬性訪問、特性和描述符 2

注:原書做者 Steven F. Lott,原書名爲 Mastering Object-oriented Pythonpython

__getattribute__()方法

__getattribute__()方法是一個更底層的屬性處理。它的默認實現試圖把一個屬性做爲一個已經存在於內部__dict__(或__slots__)的屬性來定位值。若是沒有找到該屬性,它會調用__getattr__()。若是值被定位爲描述符(參見下面《建立描述符》部分),則處理描述符;不然只是簡單的返回值。c++

經過重寫此方法,咱們能夠完成如下任何一個任務:程序員

  • 咱們能夠有效地防止對屬性的訪問。這種方法經過拋出異常來代替返回一個值,可使一個屬性比咱們僅僅使用下劃線(_)將一個命名標記爲私有更私密。web

  • 咱們能夠發明新的屬性,相似於__getattr__()如何發明新的屬性。然而,在這種狀況下,咱們能夠經過默認版本的__getattribute__()來繞過默認查找。數據庫

  • 咱們可讓屬性執行惟一且不一樣的任務。這會使得程序很是難以理解和維護。這是一個糟糕的想法。設計模式

  • 咱們能夠改變描述符的行爲。雖然技術上可能,但改變一個描述符的行爲是一個可怕的想法。緩存

在咱們實現__getattribute__()方法時,重要的是要注意在方法體中不能有任何的內部屬性訪問。若是咱們試圖經過self.name獲取值,將致使無限遞歸。函數

__getattribute__()方法不能提供任何簡單的self.name屬性訪問,這將致使無限遞歸。性能

爲了在__getattribute__()方法中獲取屬性值,咱們必須顯式地訪問object定義的基礎方法,如如下所示聲明:優化

object.__getattribute__(self, name)

例如,咱們可使用__getattribute__()修改咱們的不可變類以及防止訪問內部__dict__屬性。下面這個類,隱藏了全部如下劃線(_)開頭的命名:

class BlackJackCard3:
    """Abstract Superclass"""
    def __init__(self, rank, suit, hard, soft):
        super().__setattr__('rank', rank)
        super().__setattr__('suit', suit)
        super().__setattr__('hard', hard)
        super().__setattr__('soft', soft)

    def __setattr__(self, name, value):
        if name in self.__dict__:
            raise AttributeError("Cannot set {name}".format(name=name))
        raise AttributeError("'{__class__.__name__}' has no attribute
        '{name}'".format(__class__= self.__class__, name= name))

    def __getattribute__(self, name):
        if name.startswith('_'):
            raise AttributeError
        return object.__getattribute__(self, name)

咱們已經覆寫了__getattribute__()的私有名稱以及Python內部名稱來拋出一個屬性錯誤。這前面的示例有一個微小的優點:咱們再也不容許調整對象。咱們將會看到該類的實例交互的示例。

下面示例是該類對象的變形:

>>> c = BlackJackCard3('A', '♠', 1, 11)
>>> c.rank = 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in __setattr__
  File "<stdin>", line 13, in __getattribute__
AttributeError
>>> c.__dict__['rank']= 12
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 13, in __getattribute__
AttributeError

一般建議,搞混__getattribute__()不是一個好主意。默認方法至關複雜,並且幾乎全部咱們須要的都是做爲特性利用或做爲__getattr__()的改變。

建立描述符

描述符是調和屬性訪問的一個類。描述符類可用來獲取、設置或刪除屬性值。描述符對象是在類定義的時候構建在一個類中的。

描述符設計模式有兩個部分:一個全部者類屬性描述符自己。全部者類給它的屬性使用一個或多個描述符。描述符類定義了獲取、設置和刪除方法的組合。描述符類的一個實例將會是全部者類的一個屬性。

特性是基於全部者類的方法函數。描述符不像特性,是一個類的實例,與全部者類不一樣。所以,描述符一般是可重用的通用屬性。全部者類能夠有多個不一樣描述符類的實例來類管理具備類似行爲的屬性。

不像其餘屬性,描述符在類級別上建立。它們不是在__init()__初始化時建立。然而描述符的值能夠在初始化期間設置,描述符一般是做爲類的一部分,在任何方法函數以外來構建的。

當全部者類被定義時,每一個描述符對象都是被綁定到一個不一樣的類級別屬性的描述符類實例。

被確認爲一個描述符,一個類必須實現如下三個方法的任意組合。

  • Descriptor.__get__(self, instance, owner) -> object:在這個方法中,instance參數是即將被訪問的對象的self變量。owner參數是全部者類的對象。若是這個描述符在類的上下文中被調用,instance參數將獲得一個None值。這必須返回描述符的值。

  • Descriptor.__set__(self, instance, value):在這個方法中,instance參數是即將被訪問的對象的self變量。value參數是描述符須要設置的新值。

  • Descriptor.__delete__(self, instance)在這個方法中,instance參數是即將被訪問的對象的self變量。該描述符的方法必須刪除這個屬性的值。

有時,一個描述符類還將須要一個__init__()方法函數來初始化描述符的內部狀態。

有兩種基於已定義方法的描述符,以下所示:

  • 非數據描述符:這種描述符定義__set__()__delete__()或二者皆有。它不能定義__get__()。非數據描述符對象每每會被用做表達式的一部分。它多是一個可調用對象,或者它可能有本身的屬性或方法。一個不可變的非數據描述符必須實現__set__(),但可能只是拋出AttributeError。這些描述符設計時很簡單,由於接口更靈活。

  • 數據描述符:這種描述符至少定義__get__()。一般,它定義__get__()__set__()來建立一個可變對象。鑑於描述符將在很大程度上是不可見的,則不能更進一步的再定義屬性或方法。屬性的引用有一個數據描述符的數據被委託給描述符的__get__()__set__()__delete__()方法。這些是很難設計的,因此咱們稍後來再看。

描述符有各類各樣的用例。在內部,Python使用描述符有如下幾個緣由:

  • 在隱藏的內部,類的方法是做爲描述符來實現。這些非數據描述符應用方法函數到對象以及不一樣的參數值。

  • property()函數經過給一個字段建立數據描述符來實現。

  • 一個類方法或靜態方法被實現爲一個描述符;這被應用到類中來代替類的實例。

當咱們在第11章《經過SQLite存儲和檢索對象》看到對象-關係映射的時候,咱們將看到許多ORM類定義大量使用描述符將Python類映射到SQL表和列。

當咱們考慮一個描述符的目的,咱們還必須爲數據做爲描述符能夠正常工做來考察三種常見用例,以下所示:

  • 描述符對象有數據或獲取到了數據。在這種狀況下,描述符對象的self變量是有意義的且描述符是有狀態的。數據描述符的__get__()方法返回這個內部數據。非數據描述符,描述符有其餘方法或屬性來訪問這些數據。

  • 包含數據的全部者實例。在這種狀況下,描述符對象必須使用instance參數來引用值到全部者對象中。數據描述符的__get__()方法從實例獲取數據。非數據描述符有其餘方法訪問實例數據。

  • 包含相關數據的全部者類。在這種狀況下,描述符對象必須使用owner參數。這是經常使用的當描述符實現了應用於整個類的靜態方法或類方法。

咱們將仔細看下第一種狀況。咱們看看建立帶有__get__()__set__()方法的數據描述符。咱們也會看看建立沒有__get__()方法的非數據描述符。

第二種狀況(全部者實例中的數據)展現了@property裝飾器都作了些什麼。可能的優點是描述符有一個傳統的特性將計算從擁有者類移到描述符類中。這傾向於分片類設計且可能不是最好的方法。若是計算是真正史詩般的複雜,策略模式可能會更好。

第三種狀況展現@staticmethod@classmethod裝飾器是如何實現的。咱們不須要從新發明輪子。

一、使用非數據描述符

咱們常常會有一些緊密綁定了屬性值的小對象。對於這個示例,咱們將看看數值被綁定到單位的舉措。

下面是一個簡單的非數據描述符類,它缺乏一個__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__)

這個類定義了一對簡單的值,一個可變的(值),另外一個是有效的不可變對象(單位)。

當這個描述符被訪問時,描述符對象自己是可用的,且描述符的其餘方法或屬性能夠被使用。咱們可使用這個描述符來建立類去管理尺寸和其餘與物理單位有關的數值。

下面是一個類,作速度-時間-距離的及早計算:

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)

一旦對象被建立且屬性被加載,丟失的值就已經被計算。一旦計算,描述符能夠檢查獲取值或單位的名稱。此外,描述符對str()有一個方便的響應和請求格式。

下面是描述符和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

咱們建立了一個帶有ratedistance參數的RTD_1實例。這些都是用來計算ratedistance描述符的__set__()方法。

當咱們請求str(m1),這會計算RTD_1的全部str()方法,轉而使用ratetimedistance描述符的__format__()方法。這爲咱們提供了數字和單位。

鑑於非數據描述符沒有__get__()且不返回其內部值,咱們能夠訪問描述符的單個元素。

二、使用數據描述符

數據描述符設計要複雜一些,由於它對接口有限制。它必須有一個__get__()方法,且只能有__set__()__delete__()。這是全部的接口:這些方法從一到三,沒有其餘方法。引入一個額外的方法意味着Python不會把該類看成一個正確的數據描述符。

咱們會使用描述符設計一個簡單的單位轉換模式,能夠在__get__()__set__()方法作適當的轉換。

下面是一個單位描述符的超類,它在其餘單位和標準單位之間作轉換:

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

該類用簡單的乘法和除法將標準單位轉換爲其餘非標準單位,反之亦然。

經過這個超類,咱們能夠從一個標準單位定義一些轉換。在前面的示例,標準單位是公里時(千米/小時)。

如下是這兩個轉換描述符

class Knots(Unit):
    conversion = 0.5399568

class MPH(Unit):
    conversion = 0.62137119

繼承方法很是有用。惟一改變的是轉換因子。這些類可用於處理涉及單位轉換的值。咱們能夠處理英里每小時或可交換的節點。下面是一個標準單位的單位描述符,千米每小時:

class KPH(Unit):

    def __get__(self, instance, owner):
        return instance._kph

    def __set__(self, instance, value):
        instance._kph = value

這個類表明一個標準,因此不作任何轉換。它使用一個私有變量實例保存速度公里每小時的標準值。避免任何算術轉換是一個簡單的技術優化。避免任何一個公共字段的引用是相當重要的,來規避無限遞歸。

下面這個類,它對於一個給定的尺寸提供了一組轉換:

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)

對於不一樣的單位每一個類級別的屬性都是描述符。各類描述符的獲取和設置方法會作適當的轉換。咱們可使用這個類在各類單位之間進行速度轉換。

如下是與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

咱們經過設置不一樣的描述符建立了一個Measurement類的對象。在第一個示例中,咱們設置了節點描述符。

當咱們顯示的值是一個大字符串,則每一個描述符的__get__()都將被使用。這些方法從全部者對象獲取內部kph字段值,應用一個轉換因子,且返回一個結果值。

kph字段還使用了一個描述符。這個描述符不作任何轉換;然而,它只是返回了緩存在全部者對象的私有值。KPHKnots描述符要求全部者類實現一個kph屬性。

總結,設計要素和權衡

在這一章,咱們研究了幾種使用一個對象屬性的方式。咱們可使用內置object類的特性以及獲取和設置屬性值。咱們能夠定義屬性來修改屬性的行爲。

若是咱們想要更復雜,咱們能夠調整底層__getattr__()__setattr__()__delattr__()__getattribute__()特殊方法實現。這些讓咱們能夠更精細的控制字段的行爲。當咱們接觸到這些方法咱們走的很順利,由於咱們能夠對Python的行爲進行基本(和使人困惑的)改變。

在內部,Python使用描述符來實現特性,例如方法函數、靜態方法函數和屬性。描述符許多很酷的用例已是語言的最好的特性。

來自其餘語言(特別是Java和c++)的程序員一般有試圖讓全部屬性私有以及編寫大量的gettersetter函數的衝動。

在Python中,將全部屬性做看成公有的是至關簡單的。這意味着以下:

  • 它們應該有良好的文檔記錄。

  • 它們應該正確的反映對象的狀態,它們不該該是暫時的或臨時的值。

  • 一個字段有使人困惑的(或易變的)字段值是很是罕見的,一個如下劃線(_)開頭的命名做爲「不是已定義接口中的一部分」不是真的私有。

把私有字段看作麻煩事是很重要的。在語言中封裝並無由於缺少複雜的私有機制而受損;而會由於糟糕的設計而受損。

一、特性對屬性

在大多數狀況下,字段能夠設置在類以外且沒有不良後果。咱們的Hand類的示例展現了這一點。對於許多版本的類,咱們能夠簡單地追加到hand.cards,以及完美的工做經過特性延遲計算total

在改變屬性這種狀況下,會致使相應的其餘字段的變化,這須要一些更復雜的類設計:

  • 一個方法函數能夠闡明狀態變化。當須要多個參數值時這將是必要的。

  • 一個特性setter可能比一個方法函數更清晰。當須要單個值時這將是一個明智的選擇。

  • 咱們還可使用原地操做符。咱們將到第七章《創造數字》看到這些。

沒有嚴格的規則。在這種狀況下,咱們須要設置一個參數值,方法函數和特性之間的區別徹底是API語法和如何傳達意圖的區別。

爲了計算值,特性容許延遲計算,而一個屬性須要及早計算。這是性能的問題。延遲計算對及早計算的優點是基於對用例的預期。

二、使用描述符設計

不少描述符的示例已是Python的一部分。咱們不須要重複特性、類方法或靜態方法。

建立新的描述符最引人注目的狀況是在Python和一些非Python之間創建映射關係。例如,對象-關係數據庫映射,須要大量的維護以確保一個Python類有正確的屬性以正確的順序匹配一個SQL表和列。一樣,當映射到一些Python以外,一個描述符類能夠處理數據編碼和解碼或從外部獲取數據來源。

當構建一個web服務端,咱們可能會考慮使用描述符來作web服務請求。例如,__get__()方法可能會變成一個HTTP的GET請求,__set__()方法可能會變成一個HTTP的PUT請求。

在某些狀況下,一個單一的請求可能填充幾個描述符的數據。在這種狀況下,__get__()方法將檢查實例的緩存以及在一個HTTP請求以前返回一個值。

經過屬性來操做許多數據描述符操做會很是簡單。這給咱們提供開始的機會:首先編寫屬性。若是屬性處理變得很是昂貴和複雜,則咱們能夠切換到描述符來對類進行重構。

三、展望將來

在下一章,咱們將密切關注將在五、六、7章探索的ABCs(抽象基類)。這些基礎知識將幫助咱們定義類,它會與現有Python特性很好地集成。它們還將容許咱們建立執行一致的設計和擴展的類層次結構。

相關文章
相關標籤/搜索