1. 程式人生 > >第046講:魔法方法:描述符(Property的原理)

第046講:魔法方法:描述符(Property的原理)

目錄

 

0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

測試題(筆試,不能上機哦~)

0. 請儘量用自己的語言來解釋什麼是描述符(不要搜尋來的答案,用自己的話解釋)?

1. 描述符類中,分別通過哪些魔法方法來實現對屬性的 get、set 和 delete 操作的?

2. 請問以下程式碼,分別呼叫 test.a 和 test.x,哪個會列印“getting…”?

3. 請問以下程式碼會列印什麼內容?

4. 請問以下程式碼會列印什麼內容?

動動手(一定要自己動手試試哦~)

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)