python理解描述符(descriptor)

Descriptor基礎

python中的描述符能夠用來定義觸發自動執行的代碼,它像是一個對象屬性操做(訪問、賦值、刪除)的代理類同樣。前面介紹過的property是描述符的一種。python

大體流程是這樣的:git

  1. 定義一個描述符類D,其內包含一個或多個__get__()__set__()__delete__()方法
  2. 將描述符類D的實例對象d賦值給另外一個要代理的類中某個屬性attr,即attr = D()
  3. 以後訪問、賦值、刪除attr屬性,將會自動觸發描述符類中的__get__()__set__()__delete__()方法

簡言之,就是建立一個描述符類,它的實例對象做爲另外一個類的屬性github

要定義描述符類很簡單,只要某個類中包含了下面一個或多個方法,就算是知足描述符協議,就是描述符類,就能夠做爲屬性操做的代理器。數據結構

class Descriptor():
    def __get__(self, instance, owner):...
    def __set__(self, instance, value):...
    def __delete__(self, instance):...

須要注意的是,__get__的返回值須要是屬性值或拋異常,另外兩個方法要返回None。函數

還需注意的是不要把__delete____del__搞混了,前者是實現描述符協議的一個方法,後者是對象銷燬函數(也常稱爲析構函數)。測試

先無論這幾個方法中的參數,看一個示例先:ui

class Descriptor():
    def __get__(self, instance, owner):
        print("self: %s\ninstance: %s\nowner: %s" % (self, instance, owner))

class S:
    # 描述符的示例對象做爲S的屬性
    attr = Descriptor()

s1 = S()
s1.attr  # 訪問對象屬性

print("-" * 30)
S.attr   # 訪問類屬性

輸出結果:url

self: <__main__.Descriptor object at 0x030C02D0>
instance: <__main__.S object at 0x030C0AB0>
owner: <class '__main__.S'>
------------------------------
self: <__main__.Descriptor object at 0x030C02D0>
instance: None
owner: <class '__main__.S'>

不難看出,在訪問類S中的屬性attr時,表示訪問描述符類的實例對象,它會自動調用描述符類中的__get__方法。代理

在這個方法中,3個參數self、instance、owner分別對應的內容從結果中已經顯示出來了。它們之間有如下等價關係:code

s1.attr -> Descriptor.__get__(S.attr, s1, S)
S.attr  -> Descriptor.__get__(S.attr, None, S)

因此,這裏解釋下__get__(self, instance, owner)中的三個參數:

  • self:描述符對象自身,也就是被代理類S中的屬性attr
  • instance:被代理類的實例對象。因此訪問類屬性(class.attr)時爲None
  • owner:將描述符對象附加到哪一個類上,實際上是instance所屬的類,也就是type(instance)

再解釋下這裏相關的幾個角色:

  • Descriptor:是描述符類,也是代理者
  • S:是另外一個類,是託管類、客戶類,也就是參數中的owner
  • attr = Descriptor():是描述符的實例對象,attr是託管類的屬性,也就參數中的self
  • s1:是託管類實例對象,也就是參數中的instance

按照descriptor的功能,大概能夠用上面的方式去定義各個角色。固然,角色的定義沒什麼限制。

descriptor的做用發揮在哪

當定義了一個類後,能夠訪問、賦值、刪除它的屬性,這些操做也一樣適用於它的實例對象。

例如Foo類:

class Foo():
    ...

f = Foo()
a = f.bar   # 訪問屬性
f.bar = b   # 賦值屬性
del f.bar   # 刪除屬性

decriptor發揮做用的時候就在於執行這3類操做的時候:

  • 當訪問x.d的時候,將自動調用描述符類中的__get__
  • 當賦值x.d的時候,將自動調用描述符類中的__set__
  • 當刪除x.d的時候,將自動調用描述符類中的__delete__

考慮一下:若是x所屬的類中已經定義了__getattr____setattr____delattr__會如何,是描述符類中的先生效,仍是x自身所屬類的這幾個方法會生效。再繼續考慮,若是x所屬類沒有定義,但它的父類定義了這幾個方法,誰會生效。可自行測試或者參考個人下一篇文章。

示例1:原始代碼

假設如今有一個Student類,須要記錄stuid、name、score一、score二、score3信息。

class Student():
    def __init__(self, stuid, name, score1, score2, score3):
        self.stuid = stuid
        self.name = name
        self.score1 = score1
        self.score2 = score2
        self.score3 = score3

    def returnMe(self):
        return "%s, %s, %i, %i, %i" % (
            self.stuid,
            self.name,
            self.score1,
            self.score2,
            self.score3)

stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())

可是如今有個需求,要求score1-score3的數值範圍只能是0-100分。

因而修改__init__()

class Student():
    def __init__(self, stuid, name, score1, score2, score3):
        self.stuid = stuid
        self.name = name

        if 0 <= score1 <= 100:
            self.score1 = score1
        else:
            raise ValueError("score not in [0,100]")

        if 0 <= score2 <= 100:
            self.score2 = score2
        else:
            raise ValueError("score not in [0,100]")

        if 0 <= score3 <= 100:
            self.score3 = score3
        else:
            raise ValueError("score not in [0,100]")

這個修改對於初始化Student對象時有效,但Python中屬性的賦值太過自由,以後能夠隨意賦值:

stu = Student("20101120", "malong", 67, 77, 88)

stu.score1 = -23
print(stu.returnMe())

使用property

使用Property或者自定義的getter、setter或運算符__getattr____setattr__重載都能解決上面的問題,保證沒法賦值超出0到100範圍內的數值。

class Student():
    def __init__(self, stuid, name, score1, score2, score3):
        self.stuid = stuid
        self.name = name
        self._score1 = score1
        self._score2 = score2
        self._score3 = score3

    def get_score1(self):
        return self._score1

    def set_score1(self, score):
        if 0 <= score <= 100:
            self._score1 = score
        else:
            raise ValueError("score not in [0,100]")

    def get_score2(self):
        return self._score2

    def set_score2(self, score):
        if 0 <= score <= 100:
            self._score2 = score
        else:
            raise ValueError("score not in [0,100]")

    def get_score3(self):
        return self._score3

    def set_score3(self, score):
        if 0 <= score <= 100:
            self._score3 = score
        else:
            raise ValueError("score not in [0,100]")

    score1 = property(get_score1, set_score1)
    score2 = property(get_score2, set_score2)
    score3 = property(get_score3, set_score3)

    def returnMe(self):
        return "%s, %s, %i, %i, %i" % (
            self.stuid,
            self.name,
            self.score1,
            self.score2,
            self.score3)

下面測試時將拋出異常。

stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())
stu.score1 = -23

但很顯然,上面的重複代碼太多了。

使用descriptor

若是使用descriptor,將很容易解決上面的問題。只需將score一、score二、score3交給描述符類託管便可。

from weakref import WeakKeyDictionary

class Score():
    """ score should in [0,100] """

    def __init__(self):
        self.score = WeakKeyDictionary()
        #self.score = {}

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

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            self.score[instance] = value
        else:
            raise ValueError("score not in [0,100]")


class Student():
    # 託管屬性定義在類級別上
    score1 = Score()
    score2 = Score()
    score3 = Score()

    def __init__(self, stuid, name, score1, score2, score3):
        self.stuid = stuid
        self.name = name
        self.score1 = score1
        self.score2 = score2
        self.score3 = score3

    def returnMe(self):
        return "%s, %s, %i, %i, %i" % (
            self.stuid,
            self.name,
            self.score1,
            self.score2,
            self.score3)


stu = Student("20101120", "malong", 67, 77, 88)
print(stu.returnMe())
stu.score1 = -23

很明顯地,它們的代碼被完整地複用了。這裏score一、score二、score3被描述符類Score託管了,這3個分值分別被放進了Score實例對象的dict中(是單獨存放它們仍是使用dict數據結構來保存,取決於你)。

另外,上面使用了弱引用的字典,由於每一個屬性只在描述符對象中才會被用上,爲了保證Student對象被銷燬的時候能釋放這些資源,因此採用弱引用,避免出現內存泄漏。

參考資料

相關文章
相關標籤/搜索