python3中的特性property介紹

python3中的特性property介紹

特性的引入

特性和屬性的區別是什麼?

特性與屬性的區別是什麼 ?

在python 中 屬性 這個 實例方法, 類變量 都是屬性.
屬性, attribute

在python 中 數據的屬性 和處理數據的方法 都可以叫做 屬性.
簡單來說 在一個類中, 方法是屬性, 數據也是屬性 .

class Animal:
    name = 'animal'

    def bark(self):
        print('bark')
        pass

    @classmethod
    def sleep(cls):
        print('sleep')
        pass

    @staticmethod
    def add():
        print('add')

在命令行裏面執行

    >>> animal = Animal()
    >>> animal.add()
    add
    >>> animal.sleep()
    sleep
    >>> animal.bark()
    bark
    >>> hasattr(animal,'add') #1
    True 
    >>> hasattr(animal,'sleep')
    True
    >>> hasattr(animal,'bark')
    True

可以看出#1 animal 中 是可以拿到 add ,sleep bark 這些屬性的.

特性: property 這個是指什麼? 在不改變類接口的前提下使用
存取方法 (即讀值和取值) 來修改數據的屬性.

什麼意思呢?

就是通過 obj.property 來讀取一個值,
obj.property = xxx ,來賦值

還以上面 animal 爲例:

class Animal:

    @property
    def name(self):
        print('property name ')
        return self._name

    @name.setter
    def name(self, val):
        print('property set name ')
        self._name = val

    @name.deleter
    def name(self):
        del self._name
        

這個時候 name 就是了特性了.

>>> animal = Animal()
>>> animal.name='dog'
property set name 
>>> animal.name
property name 
'dog'
>>> 
>>> animal.name='cat'
property set name 
>>> animal.name
property name 
'cat'

肯定有人會疑惑,寫了那麼多的代碼, 還不如直接寫成屬性呢,多方便.

比如這段代碼:
直接把name 變成類屬性 這樣做不是很好嗎,多簡單. 這樣寫看起來 也沒有太大的問題.但是 如果給name 賦值成數字 這段程序也是不會報錯. 這就是比較大的問題了.

>>> class Animal:
...     name=None
...     
>>> animal = Animal()
>>> animal.name
>>> animal.name='frank'
>>> animal.name
'frank'
>>> animal.name='chang'
>>> animal.name
'chang'
>>> animal.name=250
>>> animal
<Animal object at 0x10622b850>
>>> animal.name
250
>>> type(animal.name)
<class 'int'>

這裏給 animal.name 賦值成 250, 程序從邏輯上來說 沒有問題. 但其實這樣賦值是毫無意義的.

我們一般希望 不允許這樣的賦值,就希望 給出 報錯或者警告 之類的.

animal= Animal()
animal.name=100
property set name 
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 13, in name
ValueError: expected val is str

其實當name 變成了property 之後,我們就可以對name 賦值 進行控制. 防止一些非法值變成對象的屬性.
比如說name 應該是這個字符串, 不應該是數字 這個時候 就可以在 setter 的時候 進行判斷,來控制 能否賦值.

要實現上述的效果, 其實也很簡單 setter 對value進行判斷就好了.

class Animal:

    @property
    def name(self):
        print('property name ')
        return self._name

    @name.setter
    def name(self, val):
        print('property set name ')
        # 這裏 對 value 進行判斷 
        if not isinstance(val,str):
            raise  ValueError("expected val is str")
        self._name = val

感受到 特性的魅力了吧,可以通過 賦值的時候 ,對 值進行校驗,方式不合法的值,進入到對象的屬性中. 下面 看下 如何設置只讀屬性, 和如何設置讀寫 特性.

假設 有這個的一個需求 , 某個 類的屬性一個初始化之後 就不允許 被更改,這個 就可以用特性這個問題 , 比如一個人身高是固定, 一旦 初始化後,就不允許改掉.

設置只讀特性

class Frank:
    def __init__(self, height):
        self._height = height

    @property
    def height(self):
        return self._height

>>> frank = Frank(height=100)
>>> frank.height
100
>>> frank.height =150
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: can't set attribute

這裏初始化 frank後 就不允許 就修改 這個 height 這個值了. (實際上也是可以修改的)
重新 給 height 賦值就會報錯, 報錯 AttributeError ,這裏 不實現 setter 就可以了.

設置 讀寫 特性

class Frank:
    def __init__(self, height):
        self._height = height

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        """
        給特性賦值 
        """
        self._height = value

>>> 
>>> frank = Frank(height=100)
>>> frank.height
100
>>> frank.height=165
>>> frank.height
165

比如對人的身高 在1米 到 2米之間 這樣的限制

對特性的合法性進行校驗

    
class Frank:

    def __init__(self, height):
        self.height = height  # 注意這裏寫法

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        """
        判斷邏輯 屬性的處理邏輯
        定義 了 setter 方法之後就  修改 屬性 了.
        判斷 屬性 是否合理 ,不合理直接報錯. 阻止賦值,直接拋異常

        :param value:
        :return:
        """
        if not isinstance(value, (float,int)):
            raise ValueError("高度應該是 數值類型")
        if value < 100 or value > 200:
            raise ValueError("高度範圍是100cm 到 200cm")
        self._height = value


在python console 裏面測試 結果:

>>> frank = Frank(100)
>>> frank.height
100
>>> frank.height='aaa'
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 21, in height
ValueError: 高度應該是 數值類型
>>> frank.height=250
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 23, in height
ValueError: 高度範圍是100cm 到 200cm

這樣 就可以 進行嚴格的控制, 一些特性的方法性 ,通過寫setter 方法 來保證數據 準確性,防止一些非法的數據進入到實例中.

property 是什麼?

實際是一個類 , 然後 就是一個裝飾器. 讓一個 方法 變成 一個特性.
假設 某個類的實例方法 bark 被 property 修飾了後, 調用方式就會發生變化.

obj.bark() --> obj.bak 

其實 特性 模糊了 方法 和數據 的界限.

方法 是 可調用的屬性 , 而property 是 可定製化的’屬性’ . 一般方法的名稱 是一個動詞(行爲). 而特性property 應該是名詞.

如果 我們 一旦確定了 屬性不是動作, 我們需要在標準屬性 和 property 之間做出選擇 .

一般來說 你如果要控制 property 的 訪問過程,就要用 property . 否則用標準的屬性 即可 .

attribute 屬性 和 property 特性 的區別 在於 當property 被讀取, 賦值, 刪除時候, 自動會執行 某些 特定的動作.

peroperty 詳解

特性都是類屬性,但是特性管理的其實是實例屬性的存取。-- 摘自 fluent python

下面的例子來自 fluent python

看一下 幾個例子 來說明幾個 特性和屬性 區別

>>> class Class:
  			"""
  			data 數據屬性和 prop 特性。
  			"""
...     data = 'the class data attr'
... 
...     @property
...     def prop(self):
...         return 'the prop value'
... 
>>> 
>>> obj= Class() 
>>> vars(obj)
{}
>>> obj.data
'the class data attr'
>>> Class.data
'the class data attr'
>>> obj.data ='bar'
>>> Class.data
'the class data attr'

實例屬性遮蓋類的數據屬性 , 就是說 如果 obj.data 重新 修改了 , 類的屬性 不會被修改 .

下面 嘗試 obj 實例的 prop 特性

>>> Class.prop
<property object at 0x110968ef0>
>>> obj.prop
'the prop value'
>>> obj.prop ='foo'
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: can't set attribute
>>> obj.__dict__['prop'] ='foo'
>>> vars(obj)
{'data': 'bar', 'prop': 'foo'}
>>> obj.prop  #1
'the prop value'
>>> Class.prop ='frank'
>>> obj.prop
'foo'

我嘗試修改 obj.prop 會直接報錯 ,這個容易理解, 因爲 property 沒有實現 setter 方法 . 我直接 修改 obj.dict

然後 在 #1 的地方, 發現 還是正常 調用了特性 ,而沒有 屬性的值.

當我改變 Class.prop 變成一個 屬性的時候 .

再次 調用 obj.prop 才調用到了 實例屬性.

再看一個例子 添加 特性

class Class:
    data = 'the class data attr'

    @property
    def prop(self):
        return 'the prop value'
>>> obj.data
'bar'
>>> Class.data
'the class data attr'

# 把類的data 變成 特性
>>> Class.data = property(lambda self:'the "data" prop value')
>>> obj.data
'the "data" prop value'
>>> del Class.data
>>> obj.data
'bar'
>>> vars(obj)
{'data': 'bar', 'prop': 'foo'}

改變 data 變成 特性後, obj.data 也改變了. 刪除 這個特性的時候 , obj.data 又恢復了.

本節的主要觀點是, obj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從
obj.__class__ 開始,而且,僅當類中沒有名爲 attr 的特性時, Python 纔會在 obj 實
例中尋找。這條規則 適用於 特性 .

property 實際上 是一個類

    def __init__(self, fget=None, fset=None, fdel=None, doc=None): 
    	pass
    	# known special case of property.__init__

完成 的要實現一個特性 需要 這 4個參數, get , set ,del , doc 這些參數.但實際上大部分情況下,只要實現 get ,set 即可.

特性的兩種寫法

下面 兩種 寫法 都是可行的.

第一種寫法

使用 裝飾器 property 來修飾一個方法

# 方法1 
class Animal:

    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        print('property name ')
        return self._name

    @name.setter
    def name(self, val):
        print('property set name ')

        if not isinstance(val, str):
            raise ValueError("expected val is str")
        self._name = val

    @name.deleter
    def name(self):
        del self._name

第二種寫法

直接 實現 set get delete 方法 即可, 通過property 傳入 這個參數

# 方法二 
class Animal2:

    def __init__(self, name):
        self._name = name

    def _set_name(self, val):
        if not isinstance(val, str):
            raise ValueError("expected val is str")

        self._name = val

    def _get_name(self):
        return self._name

    def _delete_name(self):
        del self._name

    name = property(fset=_set_name, fget=_get_name,fdel= _delete_name,doc= "name 這是特性描述")


if __name__ == '__main__':
    animal = Animal2('dog')
    
>>> animal = Animal2('dog')
>>> 
>>> animal.name
'dog'
>>> animal.name
'dog'

>>> help(Animal2.name)
Help on property:

    name 這是特性描述

>>> animal.name='cat'
>>> animal.name
'cat'

常見的一些例子

  1. 緩存某些值
  2. 對一些值 進行合法性校驗.

對一些值 進行合法性校驗.

對一些 特性 賦值的時候 進行 合法性的校驗,前面 都有舉例子.

在舉一個小例子 比如 有一個貨物, 有重量 和 價格 ,需要保證 這兩個屬性是正數 不能是 0 , 即>0 的值 .

好了 有了剛剛 代碼的基礎 ,下面的代碼 就寫好了.

基礎版代碼

class Goods:

    def __init__(self, name, weight, price):
        """

        :param name: 商品名稱
        :param weight:  重量
        :param price: 價格
        """
        self.name = name
        self.weight = weight
        self.price = price

    def __repr__(self):

        return f"{self.__class__.__name__}(name={self.name},weight={self.weight},price={self.price})"

    @property
    def weight(self):
        return self._weight

    @weight.setter
    def weight(self, value):
        if value < 0:
            raise ValueError(f"expected value > 0, but now value:{value}")

        self._weight = value

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError(f"expected value > 0, but now value:{value}")
        self._price = value

>>> goods = Goods('apple', 10, 30)
... 
>>> goods
Goods(name=apple,weight=10,price=30)
>>> goods.weight
10
>>> goods.weight=-10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 26, in weight
ValueError: expected value > 0, but now value:-10
>>> goods.price
30
>>> goods.price=-3
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 37, in price
ValueError: expected value > 0, but now value:-3
>>> goods
Goods(name=apple,weight=10,price=30)
>>> goods.price=20
>>> goods
Goods(name=apple,weight=10,price=20)

代碼 可以正常的判斷出來 ,這些非法值了. 這樣寫 有點問題是什麼呢? 就是 發現 weight ,price 判斷值的邏輯 幾乎是一樣的代碼… 都是判斷是 大於 0 嗎? 然而我卻寫了 兩遍相同的代碼 .

優化版代碼

有沒有更好的解決方案呢?

是有的, 我們可以寫一個 工廠函數 來返回一個property , 這實際上是兩個 property 而已.

下面 就是工廠函數 ,用來生成一個 property 的.

def validate(storage_name):
    """
    用來驗證 storage_name 是否合法性 , weight  , price
    :param storage_name:
    :return:
    """
    pass

    def _getter(instance):
        return instance.__dict__[storage_name]

    def _setter(instance, value):
        if value < 0:
            raise ValueError(f"expected value > 0, but now value:{value}")

        instance.__dict__[storage_name] = value

    return property(fget=_getter, fset=_setter)

貨物類 就可以像 下面這樣寫

class Goods:
    weight = validate('weight')
    price = validate('price')

    def __init__(self, name, weight, price):
        """

        :param name: 商品名稱
        :param weight:  重量
        :param price: 價格
        """
        self.name = name
        self.weight = weight
        self.price = price

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name},weight={self.weight},price={self.price})"

這樣看起來是不是 比較舒服一點了.

>>> goods = Goods('apple', 10, 30)
>>> goods.weight
10
>>> goods.weight=-10
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 16, in _setter
ValueError: expected value > 0, but now value:-10
>>> goods
Goods(name=apple,weight=10,price=30)
>>> goods.price=-2
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 16, in _setter
ValueError: expected value > 0, but now value:-2
>>> goods
Goods(name=apple,weight=10,price=30)

可以看出 代碼 可以正常的工作了.

緩存一些值

>>> from urllib.request import urlopen
... 
... 
... class WebPage:
... 
...     def __init__(self, url):
...         self.url = url
... 
...         self._content = None
... 
...     @property
...     def content(self):
...         if not self._content:
...             print("Retrieving new page")
...             self._content = urlopen(self.url).read()[0:10]
... 
...         return self._content
...     
>>> 
>>> 
>>> url = 'http://www.baidu.com'
>>> page = WebPage(url)
>>> 
>>> page.content
Retrieving new page
b'<!DOCTYPE '
>>> page.content
b'<!DOCTYPE '
>>> page.content
b'<!DOCTYPE '

可以看出 第一次調用了 urlopen 從網頁中讀取值, 第二次就沒有調用urlopen 而是直接返回content 的內容.

總結

python 的特性 算是python的 高級語法,不要因爲到處都要用 這個 特性的語法 .實際上 大部分 情況是用不到這個 語法的. 如果 代碼中,需要對屬性進行檢查就要考慮 用這樣的語法了. 希望你看完 之後不要 認爲這種語法非常常見, 事實上不是的. 其實 更好的做法對 屬性 檢查 可以使用描述符 來完成. 描述符 是一個比較大的話題,本文章 暫未提及,後續 的話,可能 會寫一下 關於描述的一些用法 ,這樣就 能 更好的理解python,更加深入的理解python.

參考文檔

  • fluent python
  • python3 面向對象編程
  • Python爲什麼要使用描述符? https://juejin.im/post/5cc4fbc0f265da0380437706
  • readthedocs 查看
分享快樂,留住感動. '2019-10-06 15:46:15' --frank
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章