Python描述符的使用

  • 2020 年 1 月 16 日
  • 筆記

Python描述符的使用

前言

作為一位python的使用者,你可能使用python有一段時間了,但是對於python中的描述符卻未必使用過,接下來是對描述符使用的介紹

場景介紹

為了引入描述符的使用,我們先設計一個非常簡單的類:

class Product():        def __init__(self,name,quantity,price):          self.name = name          self.quantity = quantity          self.price = price

這是一個商品類,存儲該商品的名稱,數量與價格。

對於一件商品,我們一般會期望它的數量和價格不會是負值,為了避免這種情況,我們可以在初始化的時候加一些判斷,比如下面這樣:

class Product():        def __init__(self,name,quantity,price):          self.name = name          if quantity<0:              raise ValueError('quantity must be >= 0')          self.quantity = quantity          if quantity<0:              raise ValueError('price must be >= 0')          self.price = price

但是這樣還會有一個弊端就是這樣的判斷只是加在了初始化的時候,然後在之後對類的實例的屬性進行賦值的時候還是無法保證賦的值是大於0 的

於是我們可以使用『特性』來解決這個問題:

class Product():        def __init__(self,name,quantity,price):          self.name = name          self.quantity = quantity          self.price = price        @property      def quantity(self):          return self._quantity        @quantity.setter      def quantity(self,value):          if value < 0:              raise ValueError('quantity must be >= 0')          else:              self._quantity = value        @property      def price(self):          return self._price        @price.setter      def price(self, value):          if value < 0:              raise ValueError('price must be >= 0')          else:              self._price = value    book = Product('mybook',6,30)  print(book.quantity)

這裡的@property和@quantity.setter是兩個裝飾器,它可以設置屬性的讀與寫,就相當於讀寫屬性,但其實是執行一個函數,具體有關特性的介紹,可以再自行查找,這裡主要是為了引出描述符。

通過特性,可以完成為屬性賦值時添加判斷。但是當一個類中有更多的屬性,很多屬性同樣需要添加非負數賦值的檢查的時候,使用特性這種方式就會顯得過於累贅,會有很多的程式碼重複,也會添加很多裝飾器,這時就可以使用描述符來解決這個問題。

使用描述符

首先看一下描述符的概念

描述符就是一個「綁定行為「的對象屬性,在描述符協議中,它可以通過方法充寫屬性的訪問。這些方法有get(),set(),delete().如果這些方法中任何一個被定義在一個對象中,這個對象就是一個描述符 (這幾個方法是特殊方法,雙下劃線由於轉換未顯示)

我們先把上文中的商品類按照使用描述符進行修改:

class NotNegative():      def __init__(self,name):          self.name = name        def __set__(self, instance, value):          if value < 0:              raise ValueError(self.name+' must be >= 0')          else:              instance.__dict__[self.name] = value    class Product():      quantity = NotNegative('quantity')      price = NotNegative('price')        def __init__(self,name,quantity,price):          self.name = name          self.quantity = quantity          self.price = price    book = Product('mybook',2,5)

NotNegative是描述符類,它是Product類的類屬性

在該例子中,如果執行book.quantity=3,解釋器會先查找實例屬性,發現有quantity屬性,但是解釋器又發現同樣有一個類屬性是描述符,於是解釋器最終會選擇走描述符這條路。然後因為是描述符,於是會執行描述符中的set特殊方法。相關屬性的查找順序可以參考https://www.cnblogs.com/Jimmy1988/p/6808237.html

描述符中的set特殊方法的參數有為

  • self :是描述符實例
  • instance :是相當於例子中的實例book
  • value :就是要賦予的值

由於這些屬性對於取值沒有什麼特殊的要求所以例子中沒有實現get特殊方法。

get方法同樣有3個參數self, instance, owner。self,instance與set中的相同,owner為例子中的Product類

接下來主要看一下描述符set方法中else部分進行的操作

instance.__dict__[self.name] = value

通過調用book實例的dict,直接為dict中的屬性賦值,這也是參數中傳入實例的一個重要原因。由於描述符對象是作為類屬性存在,所以可能會有很多個該類的對象訪問,為了防止屬性的覆蓋,直接存入實例的屬性中是妥當的。但這裡不能為屬性賦值的方式,不然就會陷入死循環當中。

對於數據描述符與非數據描述符,一個類,如果只定義了 get() 方法,而沒有定義 set(), delete() 方法,則認為是非數據描述符; 反之,則成為數據描述符。

最後,本文是對描述符的使用做了簡單的介紹與講解,如需更加深入了解可以參考《流暢的Python》屬性描述符部分