轉(zhuǎn)載自我自己的 github 博客 ——> 半天鐘的博客
元編程相關(guān)博文的目錄及鏈接
這篇博文是元編程系列博文中的其中一篇、這個系列中其他博文的目錄和連接見下:
- 使用 python 特性管理實例屬性
- 淺析 python 屬性描述符(上)
- 淺析 python 屬性描述符(下)
- python 導(dǎo)入時與運行時
- python 元編程之動態(tài)屬性
- python 元編程之類元編程
Review
在上一篇博文中、我們使用 python 特性(property)管理了實例屬性,最大的好處是:在使用 property 裝飾器后,我們 能夠在通過使用 " . " 這種方便的方式(obj.attr)來訪問實例屬性的同時,為其設(shè)置存儲規(guī)則。
并且,因為處理存儲的函數(shù)都有
@property
或者
@特性名.setter
這種
明顯的裝飾器標志
,我們
可以很容易的找到處理業(yè)務(wù)邏輯的核心函數(shù)。
然而、 當大量的屬性都需要相同的存取邏輯做控制時 ,例如:水果類的 weight 和 price 的值均不能小于零。 單純的使用 property 依然無法避免代碼的重復(fù)。
當重復(fù)為 weight 和 price 編寫幾乎相同的代碼時, 重構(gòu)代碼的時機到了!!!
運用和 property 同宗 的屬性描述符可以很好的避免代碼段的重復(fù)。
博文的編寫思路
首先、我會直接亮出使用屬性描述符重構(gòu)的代碼(基于上一篇博文的 Fruits 類), 用以給你屬性描述符,以及其如何減少重復(fù)代碼的直觀印象。
然后、我會仔細的說明 實現(xiàn)屬性描述符需要注意的細節(jié) 、 屬性描述符的性質(zhì) 、 及其如何與托管類實例交互。
接著、我會說明為什么 python 特性也是一種屬性描述符
最后、我會舉例說明屬性描述符比之 python 特性和優(yōu)勢在哪里
使用屬性描述符進行高效管理
下面這段代碼是基于上一篇博文的 Fruit 類進行的重構(gòu):
class
Quantity
:
def
__init__
(
self
,
attrname
)
:
self
.
attrname
=
attrname
def
__get__
(
self
,
instance
,
owner
)
:
print
(
"get:"
+
str
(
instance
.
description
)
+
"的"
+
str
(
self
.
attrname
)
)
return
instance
.
__dict__
[
self
.
attrname
]
def
__set__
(
self
,
instance
,
value
)
:
print
(
"set:"
+
str
(
instance
.
description
)
+
"的"
+
str
(
self
.
attrname
)
)
if
value
>
0
:
instance
.
__dict__
[
self
.
attrname
]
=
value
else
:
raise
ValueError
(
"想干嘛呢?"
)
class
Fruits
:
weight
=
Quantity
(
"weight"
)
price
=
Quantity
(
"price"
)
def
__init__
(
self
,
price
,
weight
,
description
)
:
# 水果的描述
self
.
description
=
description
# 水果的價格
self
.
price
=
price
# 水果的重量
self
.
weight
=
weight
def
subtotal
(
self
)
:
# 小記
return
self
.
price
*
self
.
weight
>>
>
apple
=
Fruits
(
10
,
2
,
"apple"
)
# 1
set
:
apple的price
set
:
apple的weight
>>
>
pear
=
Fruits
(
11
,
3
,
"pear"
)
set
:
pear的price
set
:
pear的weight
>>
>
vars
(
apple
)
# 2
{
'description'
:
'apple'
,
'price'
:
10
,
'weight'
:
2
}
>>
>
vars
(
pear
)
{
'description'
:
'pear'
,
'price'
:
11
,
'weight'
:
3
}
>>
>
apple
.
weight
# 3
get
:
apple的weight
2
>>
>
apple
.
price
get
:
apple的price
10
>>
>
apple
.
weight
=
10
# 4
set
:
apple的weight
>>
>
apple
.
weight
# 5
get
:
apple的weight
10
>>
>
vars
(
pear
)
# 6
{
'description'
:
'pear'
,
'price'
:
11
,
'weight'
:
3
}
>>
>
apple
.
price
=
-
1
# 7
Traceback
(
most recent call last
)
:
.
.
.
ValueError
:
想干嘛呢?
上例使用屬性描述符類 Quantity 類對 Fruits 類進行了重構(gòu),使用了 Quantity 實例作為 Fruits 類的 weight 和 price 的類屬性值。
可以看到、 重構(gòu)后的 Fruits 類實例有如下行為 :
-
在
初始化
Fruits 類實例時,
會調(diào)用描述符實例的
__set__
magic 方法。 -
查看實例 apple 的全部屬性、發(fā)現(xiàn)其
有 price 和 weight 實例屬性
。這是由于
__set__
magic 方法使用instance.__dict__[self.attrname] = value
語句為實例屬性設(shè)了值 -
可以通過 apple.weight 訪問實例屬性
,但是其是通過描述符實例的
__get__
magic 方法來訪問的。 -
可以通過 apple.weight = 10 這種方式為實例屬性設(shè)置值
,但是其實通過描述符實例的
__set__
magic 方法來訪問的。 - 能夠訪問到剛剛為 apple 實例設(shè)置的值。
- 查看 pear 實例的全部屬性、發(fā)現(xiàn) 剛才對 apple 實例的所有操作對 pear 實例毫無影響。
- 若設(shè)置 value 為負值,那么會拋出異常。
如果你不明白這些行為的原理,沒關(guān)系,我會在下一小節(jié)解釋屬性描述符的原理,現(xiàn)在你只需要知道,重構(gòu)后的代碼有著這樣的行為。
重構(gòu)后的好處
好了,在使用了 Quantity 屬性描述重構(gòu)了 Fruits 類之后、 我們依然可以使用 " . " 方便的訪問實例屬性,并同時做存儲邏輯的驗證。
而且、**我僅用了 30 行代碼就實現(xiàn)了 weight 和 price 兩個實例屬性的管理。**要知道、上一篇博文中,僅實現(xiàn)了 weight 屬性的管理就寫了 27 行代碼。
不僅是代碼量減少了,如果水果店老板想為所有水果都增加一個折扣屬性(discount)、其也不能為負值。那么我們只需要在 Fruit 類中增加一行代碼
discount = Quantity("discount")
這大大減少了重復(fù)代碼,提高了代碼的可重用性。
屬性描述符原理
為了說明白屬性描述符的原理,我將先說明一些專有名詞。
專有名詞
描述符類
-
實現(xiàn)了描述符協(xié)議的類
、比如上例中的 Quantity 類、它實現(xiàn)了描述符類的一些協(xié)議(
__get__
、__set__
)。
實現(xiàn)了
__get__
、__set__
、__delete__
方法的類是描述符,只要實現(xiàn)了其中一個就是。
托管類
- 將描述符實例作為類屬性的類 ,比如上例中的 Fruits 類,他有 weight、price 兩個類屬性,且都被賦予了描述符類的實例。
描述符實例
-
描述符類的實例
、比如上例中 Fruits 類中就用
Quantity("weight")
創(chuàng)建了一個描述符實例, 通常來講,描述符類的實例會被賦給托管類的類屬性。
托管實例
- 托管類的實例 、比如上例中的 apple 、 pear。
托管屬性
- 托管類中由描述符實例處理的公開屬性 、比如上例中 Fruits 類的 類屬性 weight、price
存儲屬性
-
可以粗略的理解為、托管實例的屬性
、在上例中使用
vars(apple)
得到的結(jié)果中 price 和 weight 實例屬性 就是存儲屬性,它們實際 存儲著 * 實例的 * 屬性值
你可能不理解存儲屬性和托管屬性的區(qū)別、因為在上例中 托管屬性與存儲屬性同名 。
那么,你只需要記住: 托管屬性是類(Fruits)屬性、存儲屬性是實例(apple)的屬性。
描述符類與托管類的關(guān)系
首先,描述符類與托管類都是類,可以將他們想象成類的工廠、下面兩張圖很好的展示了他們之間的關(guān)系:
以下兩張圖修改自《流暢的 python》 第 20 章
如上圖、Quantity 作為描述符實例的工廠、 產(chǎn)出了兩個實例并綁定至 Fruits 類的類屬性 weight、price 。
Fruit 作為托管類實例的工廠、可以產(chǎn)出多個實例、 每一個實例都有兩個存儲屬性 weight、price
上圖將描述符的兩個實例抽象成了兩個小機器人、手上拿著一個放大鏡和一個手抓,
放大鏡用于獲取托管類實例的值(
__get__
)、手抓用于設(shè)置托管類實例的值(
__set__
)
。
值得注意的是、Fruits 工廠不管生產(chǎn)多少實例, 都只能擁有兩個描述符小機器人 ,因為它們是類屬性。
描述符實例如何工作
如果你看過上一篇博文、你可能還記得上一篇博文中的特性的工作流程圖。實際上特性就是一種屬性描述符、所以在這里
Quantity 屬性描述符的工作流程與特性幾乎一致
,例如:
apple.weight = 10
語句的執(zhí)行過程如下面的流程圖:
以上流程、對于 Quantity 這個類型的描述符而言,
apple.weight
這樣的代碼的執(zhí)行流程與上圖幾乎沒有差別,無非是在搜索到有 weight 描述符實例時,調(diào)用__get__
magic 方法;在搜索到有 weight 實例屬性時獲取該屬性的值;都搜索不到則拋出異常。
property 是一種屬性描述符
為什么說 python 特性也是一種屬性描述符呢?讓我們看 python 2.2 之前是如何使用 property的:
property 類實現(xiàn)了完整的描述符協(xié)議
def
get_weight
(
instance
)
:
print
(
"get weight"
)
return
instance
.
__dict__
[
"weight"
]
def
set_weight
(
instance
,
value
)
:
print
(
"set weight"
)
if
value
>
0
:
instance
.
__dict__
[
"weight"
]
=
value
else
:
raise
ValueError
(
"想干嘛呢?"
)
class
Fruits
:
weight
=
property
(
get_weight
,
set_weight
)
def
__init__
(
self
,
price
,
weight
,
description
)
:
# 水果的描述
self
.
description
=
description
# 水果的價格
self
.
price
=
price
# 水果的重量
self
.
weight
=
weight
def
subtotal
(
self
)
:
# 小記
return
self
.
price
*
self
.
weight
>>
>
apple
=
Fruits
(
10
,
2
,
"apple"
)
set
weight
>>
>
vars
(
apple
)
{
'description'
:
'apple'
,
'price'
:
10
,
'weight'
:
2
}
>>
>
apple
.
weight
get weight
2
>>
>
apple
.
weight
=
10
set
weight
>>
>
apple
.
weight
get weight
10
>>
>
apple
.
weight
=
-
1
Traceback
(
most recent call last
)
:
.
.
.
ValueError
:
想干嘛呢?
你看出來了嗎?上例中的我們寫了一對 set/get 方法,并用他們生成了一個 property 對象,賦予了 Fruits 類的 weight 類屬性。這和屬性描述符類 Quantity 有太多的相似之處。
實際上 property 類的構(gòu)造方法返回一個描述符實例,該實例的
__get__
magic方法即是get_weight、
__set__
magic 方法既是 set_weight。
甚至這樣做以后、 也能很好的管理 Fruits 實例的 weight 屬性 ,行為與使用 Quantity 一致。
肯定有人會說、既然這樣,
那我完全沒有必要使用 Quantity 了,直接寫一個特性工廠函數(shù)即可!!!
比如:
代碼來自《流暢的 python》第19章
def
quantity
(
storage_name
)
:
def
qty_getter
(
instance
)
:
return
instance
.
__dict__
[
storage_name
]
def
qty_setter
(
instance
,
value
)
:
if
value
>
0
:
instance
.
__dict__
[
storage_name
]
=
value
else
:
raise
ValueError
(
'value must be > 0'
)
return
property
(
qty_getter
,
qty_setter
)
這樣一來、
weight = Quantity("weight")
就可以轉(zhuǎn)變?yōu)?
weight = quantity('weight')
。甚至比創(chuàng)建 Quantity 類的代碼還要短。
但是,對于描述符類來說、依舊有著得天獨厚的優(yōu)勢, 即面向?qū)ο蟮姆绞?
描述符類能夠繼承
現(xiàn)在,項目的產(chǎn)品經(jīng)理小姐姐提出了一個合理的需求 —— **Fruits 的描述不能為空!!**這很合理,因為描述為空時,顧客在系統(tǒng)中根本看不到自己買的是哪種水果。
輪到程序猿頭疼了,難道再增加一個描述符類,或者特性工廠函數(shù)嗎?如果這樣做了,那 小姐姐以后又提出一種新屬性的存取邏輯怎么辦?
我們注意到、不管是值不能小于零、還是描述不能為空, 二者的存取邏輯都在 set 方法上,對于 get 方法幾乎沒有邏輯驗證。
那么我們?yōu)槭裁床粚懸粋€
描述符類
、其實現(xiàn)了通用的
__get__
方法,再設(shè)置一個抽象的驗證方法、新來的描述符類只需要繼承該抽象類,再覆蓋該驗證方法即可。這樣能夠極大的節(jié)省代碼冗余。
這種思想通常被稱為模板方法設(shè)計模式
實現(xiàn)代碼如下:
import
abc
class
AutoStorage
:
def
__init__
(
self
,
attrname
)
:
self
.
attrname
=
attrname
def
__get__
(
self
,
instance
,
owner
)
:
# __get__ 方法除了必要的判斷 instance 是否真實存在以外,操作與之前幾乎一致
if
instance
is
None
:
return
self
else
:
return
instance
.
__dict__
[
self
.
attrname
]
def
__set__
(
self
,
instance
,
value
)
:
# 1
instance
.
__dict__
[
self
.
attrname
]
=
value
class
Validated
(
abc
.
ABC
,
AutoStorage
)
:
# 2
def
__set__
(
self
,
instance
,
value
)
:
value
=
self
.
validate
(
instance
,
value
)
# 3
super
(
)
.
__set__
(
instance
,
value
)
# 4
@abc
.
abstractmethod
def
validate
(
self
,
instance
,
value
)
:
# 5
"""返回經(jīng)過驗證的值或拋出異常"""
class
Quantity
(
Validated
)
:
"""驗證值是否大于等于零"""
def
validate
(
self
,
instance
,
value
)
:
# 6
if
value
<
0
:
raise
ValueError
(
'value must be > 0'
)
return
value
class
NonBlank
(
Validated
)
:
"""驗證字符串是否不為空"""
def
validate
(
self
,
instance
,
value
)
:
# 7
value
=
value
.
strip
(
)
if
len
(
value
)
==
0
:
raise
ValueError
(
'value cannot be empty or blank'
)
return
value
在上述代碼中:
- AutoStorage 描述符類的 set 方法不做任何驗證。
- Validated 不但 繼承了 AutoStorage 描述符類 、 而且還是一個抽象類。
-
重寫了
__set__
magic 方法 ,并將 value 設(shè)為經(jīng)過 validate 方法驗證過的值。 - 經(jīng)過 驗證后的 value 可以直接委托給父類 AutoStorage 描述符類直接存儲了 。
- 設(shè)置 validate 方法為抽象方法、 其由子類來覆蓋它 。
- Quantity 描述符類重寫了 validate 方法, 驗證值是否大于零。
- NonBlank 描述分類重寫了 validate 方法, 驗證值是否為空。
如此一來 Fruits 類的方法體中,只需要增加一行代碼即可:
class
Fruits
:
description
=
NonBlank
(
"description"
)
weight
=
Quantity
(
"weight"
)
price
=
Quantity
(
"price"
)
def
__init__
(
self
,
price
,
weight
,
description
)
:
# 水果的描述
self
.
description
=
description
# 水果的價格
self
.
price
=
price
# 水果的重量
self
.
weight
=
weight
def
subtotal
(
self
)
:
# 小記
return
self
.
price
*
self
.
weight
上述的諸多描述符類通常放在單獨的 model 模塊中,以供多個模塊共同使用。
例如,產(chǎn)品經(jīng)理小姐姐有一天和你說,甲方也想賣酸奶,需要給酸奶寫一個類; 那么此時 model 模塊中的 NonBlank 和 Quantity 也能夠提供給酸奶類使用了。
這就是設(shè)計模式的魔力,其能夠減少大量的代碼冗余
若我們將諸多描述符類放在單獨的 model 模塊中、那么 Fruits 代碼看起來會是這樣:
import
de_model
as
model
class
Fruits
:
description
=
model
.
NonBlank
(
"description"
)
weight
=
model
.
Quantity
(
"weight"
)
price
=
model
.
Quantity
(
"price"
)
.
.
.
以下省略
.
.
.
如果你學(xué)過 Django,那么你會意識到這和 Django ORM 中的 models.TextField() 用法一致。其實 Django 的 models.TextField() 就是通過屬性描述符來實現(xiàn)的。
models.TextField() 不需要傳入托管屬性名。 其原理是類裝飾器 ,我會在后幾篇博文中提到。
總結(jié)
使用 python 特性能夠很好的管理需要特殊存儲邏輯的實例屬性。
但是、當大量的屬性都需要同樣的存儲邏輯時、單純的使用 property 依舊會引起代碼冗余。
此時、應(yīng)該考慮是使用屬性描述符還是實現(xiàn)特性工廠函數(shù)
來解決這個問題。
我給出的建議是:在這種情況下, 盡量使用屬性描述符、因為你不知道后續(xù)會不會有類似但又不同的屬性存取邏輯。 (例如本博文中的 description 和 weight)
使用屬性描述符比之特性工廠有著很大的優(yōu)勢、因為其是類,可以實現(xiàn)眾多的面向?qū)ο蟮脑O(shè)計模式。
你可能已經(jīng)注意到,在本博文中,對于 property 的描述,從來都是 一種 屬性描述符。那么,**除了本博文描述的屬性描述符,還有其他類型的屬性描述符嗎?它們又擁有怎樣的特性?**下一篇博文將會解答此問題。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
