第046講:魔法方法:描述符(Property的原理)
目錄
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
0. 請儘量用自己的語言來解釋什麼是描述符(不要搜尋來的答案,用自己的話解釋)?
1. 描述符類中,分別通過哪些魔法方法來實現對屬性的 get、set 和 delete 操作的?
2. 請問以下程式碼,分別呼叫 test.a 和 test.x,哪個會列印“getting…”?
0. 按要求編寫描述符 MyDes:當類的屬性被訪問、修改或設定的時候,分別做出提醒。
1. 按要求編寫描述符 MyDes:記錄指定變數的讀取和寫入操作,並將記錄以及觸發時間儲存到檔案:record.txt
2. 再來一個有趣的案例:編寫描述符 MyDes,使用檔案來儲存屬性,屬性的值會直接儲存到對應的pickle(醃菜,還記得嗎?)的檔案中。如果屬性被刪除了,檔案也會同時被刪除,屬性的名字也會被登出。
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
前面的課程中我們提到了 property 函式,那 property 到底是怎樣實現的呢?今天我們談論的問題是描述符,
描述符就是將某種特殊型別的類的例項指派給另一個類的屬性。大家對於這個定義可能還不是很理解,等會會舉例說明。首先,什麼是特殊型別呢?特殊型別的要求是至少要實現以下三個方法其中一個或全部實現。
(一)•__get__(self, instance, owner)
–用於訪問屬性,它返回屬性的值
(二)•__set__(self, instance, value)
–將在屬性分配操作中呼叫,不返回任何內容
(三)•__delete__(self, instance)
–控制刪除操作,不返回任何內容
這三個方法和我們上節課提到的 __getattr__, __setattr__, delattr__是很相像的,但是這三個我們稱之為屬於描述符屬性的方法。__get__用於訪問屬性的時候直接呼叫,__set__用於將屬性分配,也就是賦值的時候被呼叫,__delete__用於刪除屬性的時候被呼叫。
先來一個最直觀的例子:
>>> class MyDescriptor:
def __get__(self, instance, owner):
print("getting...", self, instance, owner)
def __set__(self, instance, value):
print("setting...", self, instance, value)
def __delete__(self, instance):
print("deleting...", self, instance)
>>> class Test:
x = MyDescriptor()
先來寫一個描述符 MyDescriptor,並且把所有方法的引數給打印出來。
再來一個真正的Test類來測試一下,就給一個屬性 x ,把 MyDescriptor() 的例項指派給Test類的屬性 x。我們就說 MyDescriptor 類就是 x 的描述符。(是不是一下子感覺到了 property 的影子)。
我們下面例項化 Test 類,對 x 屬性進行各種操作,看看描述符類 MyDescriptor 會有怎樣的響應。
>>> test = Test()
>>> test.x
getting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390> <class '__main__.Test'>
>>> test
<__main__.Test object at 0x000002681DA25390>
>>> Test
<class '__main__.Test'>
>>> test.x = "x-man"
setting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390> x-man
>>> del test.x
deleting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390>
我們嘗試直接列印 test.x ,我們看到會呼叫描述符的 __get__,並且引數的意義也很明確, self 是描述符類本身的例項,instance 引數是 它的擁有者 Test 的例項 test,我們直接列印 test,就和 instance 的內容一樣,然後 owner 就是它的擁有者 Test 類本身,我們直接列印 Test,就和 owner 的內容一樣。
然後賦值(test.x = "x-man")的時候,就會呼叫描述符的 __set__,刪除(del test.x)的時候也是一樣,呼叫__delete__。
我們剛才說過了,只要我們弄清楚描述符,那麼 property 的祕密就不再是祕密了,沒錯,property 就是一個描述符類,我們這裡就來定義一個屬於自己的 property(MyProperty),來實現property的所有功能:(我們這裡定義的MyProperty只是把 property 的功能進行照搬,大家可以加入自己的創意)
>>> class MyProperty:
def __init__(self, fget = None, fset = None, fdel = None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, instance, owner):
return self.fget(instance)
def __set__(self, instance, value):
self.fset(instance, value)
def __delete__(self, instance):
self.fdel(instance)
>>> class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = MyProperty(getx, setx, delx)
>>> c = C()
>>> c.x = "x-man"
>>> c._x
'x-man'
>>> del c.x
>>> c._x
Traceback (most recent call last):
File "<pyshell#62>", line 1, in <module>
c._x
AttributeError: 'C' object has no attribute '_x'
接下來,我們來做一個練習:
•先定義一個溫度類,然後定義兩個描述符類用於描述攝氏度和華氏度兩個屬性。
•要求兩個屬性會自動進行轉換,也就是說你可以給攝氏度這個屬性賦值,然後列印的華氏度屬性是自動轉換後的結果。
公式:攝氏度 * 1.8 + 32 = 華氏度
class Celsius:
def __init__(self, value = 26.0):
self.value = float(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()
>>> temp.cel
26.0
>>> temp.fah
78.80000000000001
>>> temp.fah = 100
>>> temp.cel
37.77777777777778
測試題(筆試,不能上機哦~)
0. 請儘量用自己的語言來解釋什麼是描述符(不要搜尋來的答案,用自己的話解釋)?
答:有時候,某個應用程式可能會有一個相當微妙的需求,需要你設計一些更為複雜的操作來響應(例如每當屬性被訪問時,你也許想建立一個日誌記錄)。最好的解決方案就是編寫一個用於執行這些“更復雜的操作”的特殊函式,然後指定它在屬性被訪問時執行。那麼一個具有這種函式的物件被稱之為描述符。
往再簡單了說,描述符就是一個類,一個至少實現 __get__()、__set__() 或 __delete__() 三個特殊方法中的任意一個的類。
1. 描述符類中,分別通過哪些魔法方法來實現對屬性的 get、set 和 delete 操作的?
答:__get__、__set__ 和 __delete__
__get__(self, instance, owner)
- 用於訪問屬性,它返回屬性的值
__set__(self, instance, value)i
- 將在屬性分配操作中呼叫,不返回任何內容
__delete__(self, instance)
- 控制刪除操作,不返回任何內容
2. 請問以下程式碼,分別呼叫 test.a 和 test.x,哪個會列印“getting…”?
>>> class MyDes:
def __get__(self, instance, owner):
print("getting...")
>>> class Test:
a = MyDes()
x = a
>>> test = Test()
答:都會列印滴。
3. 請問以下程式碼會列印什麼內容?
class MyDes:
def __init__(self, value = None):
self.val = value
def __get__(self, instance, owner):
return self.val - 20
def __set__(self, instance, value):
self.val = value + 10
print(self.val)
class C:
x = MyDes()
if __name__ == '__main__': # 該模組被執行的話,執行下邊語句。
c = C()
c.x = 10
print(c.x)
答:需要注意的是 print(c.x) 訪問了 c 的 x 屬性,因此值減 20。
>>>
20
0
4. 請問以下程式碼會列印什麼內容?
>>> class MyDes:
def __init__(self, value = None):
self.val = value
def __get__(self, instance, owner):
return self.val ** 2
>>> class Test:
def __init__(self):
self.x = MyDes(3)
>>> test = Test()
>>> test.x
答:如果你認為小甲魚考的是 3 de 平方 == 9,那你就 too young too simple了!這其實是一個“陷阱”,我們先來看下會列印什麼:
>>> test.x
<__main__.MyDes object at 0x1058e6f60>
如你所見,訪問例項層次上的描述符 x,只會返回描述符本身。為了讓描述符能夠正常工作,它們必須定義在類的層次上。如果你不這麼做,那麼 Python 無法自動為你呼叫 __get__ 和 __set__ 方法。
動動手(一定要自己動手試試哦~)
0. 按要求編寫描述符 MyDes:當類的屬性被訪問、修改或設定的時候,分別做出提醒。
程式實現如下:
>>> class Test:
x = MyDes(10, 'x')
>>> test = Test()
>>> y = test.x
正在獲取變數: x
>>> y
10
>>> test.x = 8
正在修改變數: x
>>> del test.x
正在刪除變數: x
噢~這個變數沒法刪除~
>>> test.x
正在獲取變數: x
8
答:其實大家如果自己認真思考了程式碼,會發現我們這裡描述符起到的作用是間接地儲存指定變數的資料。
程式碼清單:
class MyDes:
def __init__(self, initval=None, name=None):
self.val = initval
self.name = name
def __get__(self, instance, owner):
print("正在獲取變數:", self.name)
return self.val
def __set__(self, instance, value):
print("正在修改變數:", self.name)
self.val = value
def __delete__(self, instance):
print("正在刪除變數:", self.name)
print("噢~這個變數沒法刪除~")
1. 按要求編寫描述符 MyDes:記錄指定變數的讀取和寫入操作,並將記錄以及觸發時間儲存到檔案:record.txt
程式實現如下:
>>> class Test:
x = Record(10, 'x')
y = Record(8.8, 'y')
>>> test = Test()
>>> test.x
10
>>> test.y
8.8
>>> test.x = 123
>>> test.x = 1.23
>>> test.y = "I love FishC.com!"
>>>
產生檔案:record.txt
答:這道題考察的點比較多,例如字串的轉換、檔案的操作、time 模組的用法、描述符……大家哪裡不會補哪裡~
程式碼清單:
import time
class Record:
def __init__(self, initval=None, name=None):
self.val = initval
self.name = name
self.filename = "record.txt"
def __get__(self, instance, owner):
with open(self.filename, 'a', encoding='utf-8') as f:
f.write("%s 變數於北京時間 %s 被讀取,%s = %s\n" % \
(self.name, time.ctime(), self.name, str(self.val)))
return self.val
def __set__(self, instance, value):
filename = "%s_record.txt" % self.name
with open(self.filename, 'a', encoding='utf-8') as f:
f.write("%s 變數於北京時間 %s 被修改, %s = %s\n" % \
(self.name, time.ctime(), self.name, str(value)))
self.val = value
2. 再來一個有趣的案例:編寫描述符 MyDes,使用檔案來儲存屬性,屬性的值會直接儲存到對應的pickle(醃菜,還記得嗎?)的檔案中。如果屬性被刪除了,檔案也會同時被刪除,屬性的名字也會被登出。
舉個栗子:
>>> class Test:
x = MyDes('x')
y = MyDes('y')
>>> test = Test()
>>> test.x = 123
>>> test.y = "I love FishC.com!"
>>> test.x
123
>>> test.y
'I love FishC.com!'
產生對應的檔案儲存變數的值:
如果我們刪除 x 屬性:
>>> del test.x
>>>
對應的檔案也不見了:
程式碼清單:
import os
import pickle
class MyDes:
saved = []
def __init__(self, name = None):
self.name = name
self.filename = self.name + '.pkl'
def __get__(self, instance, owner):
if self.name not in MyDes.saved:
raise AttributeError("%s 屬性還沒有賦值!" % self.name)
with open(self.filename, 'rb') as f:
value = pickle.load(f)
return value
def __set__(self, instance, value):
with open(self.filename, 'wb') as f:
pickle.dump(value, f)
MyDes.saved.append(self.name)
def __delete__(self, instance):
os.remove(self.filename)
MyDes.saved.remove(self.name)