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.