天天看點

說說Python中的property

背景

最近在項目中,發現項目越來越大之後,之前的編寫方式會留下很多坑,是以最近專門研究了一下靜态語言中的方法,比如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這類的處理方式,則是磨刀不誤砍柴工,它可以確定你在使用這些資料的時候,類型是一緻的,也能減少很多重複的代碼編寫,同時在多人協同的工作中,能夠更友善他人閱讀代碼。