天天看點

Python3中的特性-----Property介紹

Python的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           

複制

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

>>> frank = Frank(height=100)
>>> frank.height
100
>>> frank.height=165
>>> frank.height
165
# 對特性的合法性進行校驗
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
>>> 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修飾了後, 調用方式就會發生變化.其實特性模糊了方法和資料的界限.方法是可調用的屬性 , 而property 是 可定制化的'屬性' . 一般方法的名稱是一個動詞(行為). 而特性property 應該是名詞.

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

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

attribute屬性和property特性的差別在于當property被讀取, 指派, 删除時候, 自動會執行某些特定的動作.

特性都是類屬性,但是特性管理的其實是執行個體屬性的存取。

----- 摘自 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的兩種寫法

第一種寫法

使用 裝飾器 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'           

複制

替換背景的新方法:選擇背景圖設定後,左邊模闆、收藏、剪貼闆和圖庫都可以點選,根據選擇的内容,設定背景到目前的編輯布局上。如果選擇了的是圖檔,都把圖檔設定被背景圖。如果選擇了一個帶背景的模闆,就把這個模闆的背景給複制過來。

常見的一些例子

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

在舉一個小例子 比如 有一個貨物, 有重量 和 價格 ,需要保證 這兩個屬性是正數 不能是 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)
           

複制

B、緩存某些值

... 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.