背景
今天在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_val
和cls_val
稱爲實例屬性和類屬性。實例t
的屬性並不包含cls_val
,cls_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
的值,由於事先增加了實例t
的cls_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
相關圖文: