技術圖文:Python描述符 (descriptor) 詳解

背景

今天在B站上學習“零基礎入門學習Python”這門課程的第46講“魔法方法:描述符”,這也是我們組織的 Python基礎刻意練習活動 的學習任務,其中有這樣的一個題目。

練習要求:

  • 先定義一個溫度類,然後定義兩個描述符類用於描述攝氏度和華氏度兩個屬性。
  • 要求兩個屬性會自動進行轉換,也就是說你可以給攝氏度這個屬性賦值,然後打印的華氏度屬性是自動轉化後的結果。
  • 華氏度與攝氏度的轉換關係:1 Fahrenheit = 1 Celsius*1.8 + 32

技術分析

爲了解決這個問題,我們首先回顧__dict__屬性,以及__get____set____delete__魔法方法,然後總結描述符這個 Python 語言特有的語法結構,最後寫代碼完成要求的任務。

1. __dict__ 屬性

class Test(object):
    cls_val = 1

    def __init__(self):
        self.ins_val = 10


t = Test()
print(Test.__dict__)
# {'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000000EBCB65F598>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}
print(t.__dict__)
# {'ins_val': 10}

根據 Python 的語法結構,t爲實例對象,Test爲類對象。其對應的屬性ins_valcls_val稱爲實例屬性和類屬性。實例t的屬性並不包含cls_valcls_val是屬於類Test的。

t.cls_val = 20
print(Test.__dict__)
# {'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000000CB7EB5F598>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}
print(t.__dict__)
# {'ins_val': 10, 'cls_val': 20}

可見,更改實例t的屬性cls_val,只是新增了該屬性,並不影響類Test的屬性cls_val

Test.cls_val = 30
print(Test.__dict__)
# {'__module__': '__main__', 'cls_val': 30, '__init__': <function Test.__init__ at 0x000000DAB2BFC048>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}
print(t.__dict__)
# {'ins_val': 10, 'cls_val': 20}

可見,更改了類Test的屬性cls_val的值,由於事先增加了實例tcls_val屬性,因此不會改變實例的cls_val值。

2. __get__(),__set__(),__delete__() 魔法方法

  • __get__(self, instance, owner)
  • __set__(self, instance, value)
  • __del__(self, instance)
class Desc(object):
    def __get__(self, instance, owner):
        print("__get__...")
        print("self:", self)
        print("instance: ", instance)
        print("owner: ", owner)

    def __set__(self, instance, value):
        print('__set__...')
        print("self:", self)
        print("instance:", instance)
        print("value:", value)


class TestDesc(object):
    x = Desc()


t = TestDesc()
t.x

# __get__...
# self: <__main__.Desc object at 0x0000009C9B980198>
# instance:  <__main__.TestDesc object at 0x0000009C9B9801D0>
# owner:  <class '__main__.TestDesc'>

可以看到,實例化類TestDesc後,調用對象t訪問其屬性x,會自動調用類Desc__get__方法,由輸出信息可以看出:

  • self: Desc的實例對象,其實就是TestDesc的屬性x
  • instance: TestDesc的實例對象,其實就是t
  • owner: 即誰擁有這些東西,當然是 TestDesc這個類,它是最高統治者,其他的一些都是包含在它的內部或者由它生出來的

3. 描述符的定義

某個類,只要是內部定義了方法__get____set____delete__ 中的一個或多個,就可以稱爲描述符。Desc類就是一個描述符(描述符是一個類)。

  • 問題1. 爲什麼訪問t.x的時候,會直接去調用描述符的__get__()方法呢?

t爲實例對象,訪問t.x時,根據常規順序。

首先,訪問Owner__getattribute__()方法(其實就是 TestDesc.__getattribute__()),訪問實例屬性,發現沒有,然後去訪問父類!

其次,判斷屬性x爲一個描述符,此時,它就會做一些變動了,將TestDesc.x轉化爲TestDesc.__dict__['x'].__get__(None, TestDesc)來訪問。

最後,進入類Desc__get__()方法,進行相應的操作。

  • 問題2. 從上面代碼我們看到了,描述符的對象x其實是類TestDesc 的類屬性,那麼可不可以把它變成實例屬性呢?
class Desc(object):
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        print("__get__...")
        print('name = ', self.name)


class TestDesc(object):
    x = Desc('x')

    def __init__(self):
        self.y = Desc('y')


t = TestDesc()
t.x
t.y

# __get__...
# name =  x

咦,爲啥沒打印 t.y 的信息呢?

因爲調用 t.y 時刻,首先會去調用TestDesc(即Owner)的 __getattribute__() 方法,該方法將 t.y 轉化爲TestDesc.__dict__['y'].__get__(t, TestDesc),但是呢,實際上 TestDesc並沒有y這個屬性,y是屬於實例對象的,所以,只能忽略了。

  • 問題3. 如果 類屬性的描述符對象 和 實例屬性描述符的對象 同名時,咋整?
class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ", self.name)

    def __get__(self, instance, owner):
        print("__get__() ...")
        return self.name

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


class TestDesc(object):
    _x = Desc('x')

    def __init__(self, x):
        self._x = x


t = TestDesc(10)
t._x

# __init__(): name =  x
# __get__() ...

不對啊,按照慣例,t._x 會去調用 __getattribute__() 方法,然後找到了 實例t_x 屬性就結束了,爲啥還去調用了描述符的 __get__()方法呢?

這就牽扯到了一個查找順序問題:當 Python 解釋器發現實例對象的字典中,有與描述符同名的屬性時,描述符優先,會覆蓋掉實例屬性。

我們再將代碼改進一下, 刪除 __set__() 方法試試看會發生什麼情況?

class Desc(object):
    def __init__(self, name):
        self.name = name
        print("__init__(): name = ", self.name)

    def __get__(self, instance, owner):
        print("__get__() ...")
        return self.name


class TestDesc(object):
    _x = Desc('x')

    def __init__(self, x):
        self._x = x


t = TestDesc(10)
print(t._x)

# __init__(): name =  x
# 10

可見,一個類,如果只定義了 __get__() 方法,而沒有定義 __set__()__delete__()方法,則認爲是非數據描述符;反之,則成爲數據描述符。非數據描述符,優先級低於實例屬性。

  • 問題4. 天天提屬性查詢優先級,就不能總結一下嗎?

__getattribute__(), 無條件調用

② 數據描述符

③ 實例對象的字典

④ 類的字典

⑤ 非數據描述符

⑥ 父類的字典

__getattr__()方法


代碼實現

class Celsius:
    def __init__(self, value=26.6):
        self.value = value

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

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


class Fahrenheit:
    def __get__(self, instance, owner):
        return instance.cel * 1.8 + 32

    def __set__(self, instance, value):
        instance.cel = (float(value) - 32) / 1.8


class Temperature:
    cel = Celsius()
    fah = Fahrenheit()


temp = Temperature()
print(temp.cel)  # 26.6
print(temp.fah)  # 79.88
temp.cel = 30
print(temp.cel)  # 30
print(temp.fah)  # 86.0
temp.fah = 79.88
print(temp.cel)  # 26.599999999999998
print(temp.fah)  # 79.88

總結

通過以上的介紹我們瞭解了 Python 中描述符的定義,以及屬性調用的優先級。由於Python魔法方法非常複雜需要下很大的功夫才能把這塊搞明白。今天就到這裏吧,See you!


參考文獻

  • https://www.runoob.com/python3/python3-tutorial.html
  • https://www.bilibili.com/video/av4050443
  • http://c.biancheng.net/view/2371.html
  • https://www.cnblogs.com/seablog/p/7173107.html
  • https://www.cnblogs.com/Jimmy1988/p/6808237.html

相關圖文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章