背景
最近在項目中,發現項目越來越大之後,之前的編寫方式會留下很多坑,是以最近專門研究了一下靜态語言中的方法,比如java中的bean這玩意,發現這種方式引入後,可以很有效的解決這類問題。
有關property
property
是
Python
中的一類裝飾器,可以把某個類函數變成隻讀屬性。
比如下面的這些代碼
class Student(object):
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
if __name__ == '__main__':
std = Student("sven", 20)
print(std.name)
std.name = "123"
複制
name
這個屬性如果被執行個體化的類去設定,則會抛錯:
echo:
sven
Traceback (most recent call last):
File "/Users/sven/PycharmProjects/paymap/debug.py", line 128, in <module>
std.name = "123"
AttributeError: can't set attribute
複制
當然,如果要支援設定,可以改成下面這樣:
class Student(object):
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
if __name__ == '__main__':
std = Student("sven", 20)
print(std.name)
std.name = "123"
print(std.name)
複制
這樣就能正常修改某個值了。
不過這種操作,對于Python來說,似乎有一種脫褲子放屁的感覺,不用property,一樣能夠正常的擷取類屬性,比如這樣
class Student(object):
def __init__(self, name, age):
self.name = name
self.age = age
if __name__ == '__main__':
std = Student("sven", 20)
print(std.name)
std.name = "123"
print(std.name)
複制
跟上面的實際上是一樣的,那麼
property
這玩意到底有什麼用?
類型檢查
Python是一個弱類型的語言,某個變量可以随便指派,即使原來是字元串,也可以重新指派成數值類型。
還是上面那個例子,如果這個學生類,我要確定姓名和年齡字段必須傳對類型。可以這麼做:
class Student(object):
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
if not isinstance(self._name, str):
raise TypeError("name must be string")
return self._name
@property
def age(self):
if not isinstance(self._age, int):
raise TypeError("age must be int")
return self._age
if __name__ == '__main__':
std = Student(10, 20)
print(std.name)
複制
通過上面的方式,參數類型不正确,取值的時候就會抛錯處理。通過這種方式可以確定這個類在使用的時候,每個字段都是特定的類型。
property的其他應用
當然,property如果隻有這麼功能,那麼使用的意義其實不大,還有其他實用的點,比如懶加載,資料緩存。
懶加載
我們在使用某些資料的時候,可以把計算過程放到使用時再進行計算,避免無意義的計算資源浪費。比如下面的例子。
class Student(object):
def __init__(self, math, chinese, english):
self._math = math
self._chinese = chinese
self._english = english
@property
def score(self):
return self._math + self._chinese + self._english
if __name__ == '__main__':
std = Student(10, 20, 30)
print(std.score)
複制
這裡學生的成績score,隻有再使用這個score的時候,才會把其他學科的成績做一個加總,否則是不會計算這個值的。
緩存資源
複用剛剛懶加載的那個例子。這個過程還可以再優化一下,如果分數已經被計算出來了,那麼就不需要再重新計算分數,直接傳回就行了。
class Student(object):
def __init__(self, math, chinese, english):
self._math = math
self._chinese = chinese
self._english = english
self._score = 0
@property
def score(self):
if self._score == 0:
print("calc score")
self._score = self._math + self._chinese + self._english
return self._score
if __name__ == '__main__':
std = Student(10, 20, 30)
print(std.score)
print(std.score)
複制
輸出的結果為:
calc score
60
60
複制
這裡的邏輯就把成績這個結果給緩存下來了,多次使用這個資料的時候,就不需要重複的計算。
懶加載和緩存實際中的應用
這兩個特性在實際的工作中,使用的還是比較廣的,比如前段時間,我寫微服務的client功能的時候,需要把路由資訊在程序中緩存,如果發現路由資訊過期了,才去重新拉取路由資訊,否則就直接傳回緩存中的路由資訊,這裡實際上用的就是上面的懶加載和緩存的特性。
通過懶加載,確定路由資訊需要使用并且過期的時候,才會發網絡請求去擷取最新的路由。
通過緩存,無需每次擷取路由都去伺服器查詢路由資訊,直接從緩存中拿即可。
其他讨論
說實話,上面的示範例子非常的簡單,不是特别具有代表性。我們日常工作中,用到的類成員可能有非常多,比如請求了某個接口回來的資料可能有十幾個字段,每個字段都單獨寫一個
property
,再寫上對應的setter,delete裝飾器方法,那真的是非常蠢。是否有其他方式可以達到類似的效果呢?其實是有的。
這裡參考了Python Cookbook中的一個用法。
可以單獨寫一個裝飾器的方法,如下
class Typed(object):
def __init__(self, name, excepted_type):
self.name = name
self.expected_type = excepted_type
def __get__(self, instace, cls):
if instace is None:
return self
else:
return instace.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError("Expected" + str(self.expected_type))
instance.__dict__[self.name] = value
def typeassert(**kwargs):
"""
類型校驗裝飾器
@param kwargs:
@return:
"""
def decorate(cls):
for name, expected_type in kwargs.items():
setattr(cls, name, Typed(name, expected_type))
return cls
return decorate
複制
這個裝飾器可以裝飾我們的類,在
setter
資料的時候對類型進行檢查.
還是用本文的示範例子。
@typeassert(math=int, chinese=int, english=int)
class Student(object):
def __init__(self, math, chinese, english):
self.math = math
self.chinese = chinese
self.english = english
複制
這樣裝飾了這個類之後,這些字段在指派的時候,就必須指派對應的類型,否則就會抛錯。這種方式是一個批量處理類型校驗的方法,可以極大的減少重複代碼的編寫。
當然,每個參數的指派過程,其實是很麻煩的,比如下面這樣:
data = {"math": 10, "chinese": 20, "english": 30}
std = Student(data.get("math"), data.get("chinese"), data.get("english"))
複制
這樣的方式,在實際工作過程中還是會經常遇到,别人給你的東西可能就是一個字典,那麼有沒有比較有效的方式來解決這個呢? 可以參考下面的方案。
@typeassert(math=int, chinese=int, english=int)
class Student(object):
def __init__(self, info):
self.math = 0
self.chinese = 0
self.english = 0
self.parse_input_param(info)
def parse_input_param(self, info):
for key, value in info.items():
setattr(self, key, value)
if __name__ == '__main__':
data = {"math": 10, "chinese": 20, "english": 30}
std = Student(data)
print(std.math)
print(std.chinese)
print(std.english)
複制
當然,在init中不定義
self.math
,
self.chinese
,
self.english
,上面的代碼也是可以工作的,但是我不建議你這麼做,事先聲明好類型,對于其他人的接收,以及自己後續的維護是有很大的幫助的,這部分工作的省略是得不償失的。
最後
特别強調一下,每種方式都是需要在特定的方式下做才有意義,如果隻是一個簡單的腳本,那麼使用property這種方式去處理,完全是沒有意義的,浪費時間。
但是,如果你的工程是一個比較大型的工程,有很多外部系統的互動,那麼使用property這類的處理方式,則是磨刀不誤砍柴工,它可以確定你在使用這些資料的時候,類型是一緻的,也能減少很多重複的代碼編寫,同時在多人協同的工作中,能夠更友善他人閱讀代碼。