在 Python 中,資料的屬性和處理資料的方法統稱屬性(attribute)。其實,方法隻是可調用的屬性。除了這二者之外,我們還可以建立特性(property),在不改變類接口的前提下,使用存取方法(即讀值方法和設值方法)修改資料屬性。這與統一通路原則相符:
不管服務是由存儲還是計算實作的,一個子產品提供的所有服務都應該通過統一的方式使用。
Python 還提供了豐富的 API,用于控制屬性的通路權限,以及實作動态屬性。使用點号通路屬性時(如 obj.attr),Python 解釋器會調用特殊的方法(如__getattr__ 和 __setattr__)計算屬性。使用者自己定義的類可以通過\ _getattr_ 方法實作“虛拟屬性”,當通路不存在的屬性時(如 obj.no_such_attribute),即時計算屬性的值。
1.使用動态屬性轉換資料
首先來看一個json檔案的讀取。書中給出了一個json樣例。該json檔案有700多K,資料量充足,适合本章的例子。檔案的具體内容可以在http://www.oreilly.com/pub/sc/osconfeed上檢視。首先先下載下傳資料生成json檔案。
def load():
url='http://www.oreilly.com/pub/sc/osconfeed'
JSON="osconfeed.json"
if not os.path.exists(JSON):
remote=urlopen(url)
with open(JSON,'wb')as local:
local.write(remote.read())
with open(JSON)as fp:
return json.load(fp)
我們要通路json資料裡面的例子,該如何通路呢,一般情況是:
print feed['Schedule']['speakers'][-1]['name']
但是這種句法有個缺點,就是很冗長。能不能按照feed.Schedule.speakers[-1].name這種比較簡潔的方式來通路呢。要實作這種通路。需要對資料做下重新處理。這裡要用到__getattr__方法:代碼如下:
class FrozenJSON:
def __init__(self,mapping):
self.__data=dict(mapping) (1)
def __getattr__(self,name):
if hasattr(self.__data,name):
return getattr(self.__data,name) (2)
else:
return FrozenJSON.build(self.__data[name]) (3)
@classmethod
def build(cls,obj):
if isinstance(obj,dict): (4)
return cls(obj)
elif isinstance(obj,list): (5)
return [cls.build(item) for item in obj]
else: (6)
return obj
(1)構造一個字典,這樣做確定傳入的是字典
(2)確定沒有此屬性的時候調用__getattr__
(3)如果name是__data的屬性,則傳回那個屬性。
(4)如果判定是字典,則傳回該字典對象
(5)如果是清單,則将清單的每個元素遞歸的傳給build方法,建構一個清單
(6)如果既不是清單也不是字典,則直接傳回元素
這樣實作我們就能按照前面的預期來通路元素了:raw_feed.Schedule.speakers[-1].name
用new方法來建立對象
首先來介紹下__new__方法。我們通常都将__init__稱為構造函數。其實在python中真正的構造函數應該是__new__。我們沒有具體的去實作__new__方法。是因為從object類繼承的實作已經足夠了。來看一個例子:
class A(object):
def __init__(self):
print '__init__'
def __new__(cls, *args, **kwargs):
print '__new__'
print cls
return object.__new__(cls, *args, **kwargs)
if __name__=="__main__":
a=A()
代碼運作結果如下:
image.png
從結果可以看到首先是進入__new__,然後來生成一個對象的執行個體并傳回。最後才是執行__init__。從這個例子可以看出在構造一個對象執行個體的時候,首先是進入__new__生成對象執行個體,然後再調用__init__方法進行初始指派。那麼我們用__new__方法來改造前面的FrozenJSON類。在前面的FrozenJSON實作中,build函數其實是不停的在遞歸各個字典對象,在遞歸過程中生成FronzenJSON執行個體進行處理。也就是第四步中的return cls(obj)。這裡我們可以__new__來改造。
class FrozenJSON1(object):
def __new__(cls, args):
if isinstance(args,dict):
return object.__new__(cls)
elif isinstance(args,list):
return [cls(item) for item in arg]
else:
return args
def __init__(self,mapping):
self.__data=dict(mapping)
def __getattr__(self,name):
if hasattr(self.__data,name):
return getattr(self.__data,name)
else:
return FrozenJSON(self.__data[name])
上面代碼部分中的__new__就是實作了build方法。在__getattr__中沒有找到對應name屬性時候,return FrozenJSON(self.__data[name])建立一個FrozenJSON對象進行往下遞歸。
2.使用特性驗證屬性
先來看一個經典的簡單電商應用:
class LineItem(object):
def __init__(self,description,weight,price):
self.description=description
self.weight=weight
self.price=price
def subtotal(self):
return self.weight*self.price
每個商品都有重量、單價和描述,使用者可以拿到一個商品的售價。
上述代碼中會有意外情況,就是商品重量或者單價是負數時,就會傳回一個負的總價,這個情況就很糟糕。是以需要加入一點基本的校驗:
class LineItem(object):
def __init__(self,description,weight,price):
self.description=description
self.weight=weight
self.price=price
def subtotal(self):
return self.weight*self.price
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self,value):
if value <=0:
raise ValueError('value must be > 0')
else:
self.__weight=value
去除重複的方法是抽象。抽象特性的定義有兩種方式:使用特性工廠函數,或者使用描述符類。後者更靈活。
雖然内置的 property 經常用作裝飾器,但它其實是一個類。在 Python 中,函數和類通常可以互換,因為二者都是可調用的對象,而且沒有執行個體化對象的 new 運算符,是以調用構造方法與調用工廠函數沒有差別。此外,隻要能傳回新的可調用對象,代替被裝飾的函數,二者都可以用作裝飾器。
不适用property裝飾器的例子,經典的調用:
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self):
return self.__weight
def set_weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
weight = property(get_weight, set_weight)
某些情況下,這種經典形式比裝飾器句法好;稍後讨論的特性工廠函數就是一例。但是,在方法衆多的類定義體中使用裝飾器的話,一眼就能看出哪些是讀值方法,哪些是設值方法,而不用按照慣例,在方法名的前面加上 get 和 set。
本節的主要觀點是,obj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從obj.class 開始,而且,僅當類中沒有名為 attr 的特性時,Python 才會在 obj 執行個體中尋找。這條規則不僅适用于特性,還适用于一整類描述符——覆寫型描述符。
先尋找類屬性,再尋找執行個體屬性。
如果使用經典調用句法,為 property 對象設定文檔字元串的方法是傳入 doc 參數:
weight = property(get_weight, set_weight, doc='weight in kilograms')
使用裝飾器建立 property 對象時,讀值方法(有 @property 裝飾器的方法)的文檔字元串作為一個整體,變成特性的文檔。
建立特性工廠函數
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)
class LineItem:
weight = quantity('weight')
price = quantity('price')
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
在真實的系統中,分散在多個類中的多個字段可能要做同樣的驗證,此時最好把quantity 工廠函數放在實用工具子產品中,以便重複使用。最終可能要重構那個簡單的工廠函數,改成更易擴充的描述符類,然後使用專門的子類執行不同的驗證。
處理屬性删除操作
class BlackKnight:
def __init__(self):
self.members = ['an arm', 'another arm', 'a leg', 'another leg']
self.phrases = ["It's but a scratch.",
"It's just a flesh wound.",
"I'm invincible!",
"All right, we'll call it a draw"]
@property
def member(self):
print('next member is:')
return self.members[0]
@member.deleter
def member(self):
text = 'BLACK KNIGHT (loses {})\n -- {}'
print(text.format(self.members.pop(0), self.phrases.pop(0)))
影響屬性處理方式的特殊屬性,後面幾節中的很多函數和特殊方法,其行為受下述 3 個特殊屬性的影響。
__class__
對象所屬類的引用(即 obj.class 與 type(obj) 的作用相同)。Python 的某些特殊方法,例如 __getattr__,隻在對象的類中尋找,而不在執行個體中尋找。
__dict__
一個映射,存儲對象或類的可寫屬性。有 __dict__ 屬性的對象,任何時候都能随意設定新屬性。如果類有 __slots__ 屬性,它的執行個體可能沒有 __dict__ 屬性。參見下面對 __slots__ 屬性的說明。
__slots__
類可以定義這個這屬性,限制執行個體能有哪些屬性。__slots__ 屬性的值是一個字元串組成的元組,指明允許有的屬性。 如果 __slots__ 中沒有 '__dict__',那麼該類的執行個體沒有 __dict__ 屬性,執行個體隻允許有指定名稱的屬性。
當讀取執行個體屬性的時候會覆寫類的屬性。而在讀取執行個體特性的時候,特性不會被執行個體屬性覆寫,而依然是讀取類的特性。除非類特性被銷毀。需要根據具體情況選擇需要的使用方式。