在 Python 中,數(shù)據(jù)的屬性和處理數(shù)據(jù)的方法統(tǒng)稱屬性(attribute)。其實,方法只是可調(diào)用的屬性。除了這二者之外,我們還可以創(chuàng)建特性(property),在不改變類接口的前提下,使用存取方法(即讀值方法和設(shè)值方法)修改數(shù)據(jù)屬性。這與統(tǒng)一訪問原則相符:
不管服務(wù)是由存儲還是計算實現(xiàn)的,一個模塊提供的所有服務(wù)都應(yīng)該通過統(tǒng)一的方式使用。
Python 還提供了豐富的 API,用于控制屬性的訪問權(quán)限,以及實現(xiàn)動態(tài)屬性。使用點號訪問屬性時(如 obj.attr),Python 解釋器會調(diào)用特殊的方法(如__getattr__ 和 __setattr__)計算屬性。用戶自己定義的類可以通過\ _getattr_ 方法實現(xiàn)“虛擬屬性”,當訪問不存在的屬性時(如 obj.no_such_attribute),即時計算屬性的值。
1.使用動態(tài)屬性轉(zhuǎn)換數(shù)據(jù)
首先來看一個json文件的讀取。書中給出了一個json樣例。該json文件有700多K,數(shù)據(jù)量充足,適合本章的例子。文件的具體內(nèi)容可以在http://www.oreilly.com/pub/sc/osconfeed上查看。首先先下載數(shù)據(jù)生成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數(shù)據(jù)里面的例子,該如何訪問呢,一般情況是:
print feed['Schedule']['speakers'][-1]['name']
但是這種句法有個缺點,就是很冗長。能不能按照feed.Schedule.speakers[-1].name這種比較簡潔的方式來訪問呢。要實現(xiàn)這種訪問。需要對數(shù)據(jù)做下重新處理。這里要用到__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)構(gòu)造一個字典,這樣做確保傳入的是字典
(2)確保沒有此屬性的時候調(diào)用__getattr__
(3)如果name是__data的屬性,則返回那個屬性。
(4)如果判定是字典,則返回該字典對象
(5)如果是列表,則將列表的每個元素遞歸的傳給build方法,構(gòu)建一個列表
(6)如果既不是列表也不是字典,則直接返回元素
這樣實現(xiàn)我們就能按照前面的預(yù)期來訪問元素了:raw_feed.Schedule.speakers[-1].name
用new方法來創(chuàng)建對象
首先來介紹下__new__方法。我們通常都將__init__稱為構(gòu)造函數(shù)。其實在python中真正的構(gòu)造函數(shù)應(yīng)該是__new__。我們沒有具體的去實現(xiàn)__new__方法。是因為從object類繼承的實現(xiàn)已經(jīng)足夠了。來看一個例子:
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()
代碼運行結(jié)果如下:
從結(jié)果可以看到首先是進入__new__,然后來生成一個對象的實例并返回。最后才是執(zhí)行__init__。從這個例子可以看出在構(gòu)造一個對象實例的時候,首先是進入__new__生成對象實例,然后再調(diào)用__init__方法進行初始賦值。那么我們用__new__方法來改造前面的FrozenJSON類。在前面的FrozenJSON實現(xiàn)中,build函數(shù)其實是不停的在遞歸各個字典對象,在遞歸過程中生成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__就是實現(xiàn)了build方法。在__getattr__中沒有找到對應(yīng)name屬性時候,return FrozenJSON(self.__data[name])新建一個FrozenJSON對象進行往下遞歸。
2.使用特性驗證屬性
先來看一個經(jīng)典的簡單電商應(yīng)用:
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
每個商品都有重量、單價和描述,用戶可以拿到一個商品的售價。
上述代碼中會有意外情況,就是商品重量或者單價是負數(shù)時,就會返回一個負的總價,這個情況就很糟糕。所以需要加入一點基本的校驗:
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
去除重復的方法是抽象。抽象特性的定義有兩種方式:使用特性工廠函數(shù),或者使用描述符類。后者更靈活。
雖然內(nèi)置的 property 經(jīng)常用作裝飾器,但它其實是一個類。在 Python 中,函數(shù)和類通??梢曰Q,因為二者都是可調(diào)用的對象,而且沒有實例化對象的 new 運算符,所以調(diào)用構(gòu)造方法與調(diào)用工廠函數(shù)沒有區(qū)別。此外,只要能返回新的可調(diào)用對象,代替被裝飾的函數(shù),二者都可以用作裝飾器。
不適用property裝飾器的例子,經(jīng)典的調(diào)用:
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)
某些情況下,這種經(jīng)典形式比裝飾器句法好;稍后討論的特性工廠函數(shù)就是一例。但是,在方法眾多的類定義體中使用裝飾器的話,一眼就能看出哪些是讀值方法,哪些是設(shè)值方法,而不用按照慣例,在方法名的前面加上 get 和 set。
本節(jié)的主要觀點是,obj.attr 這樣的表達式不會從 obj 開始尋找 attr,而是從obj.class 開始,而且,僅當類中沒有名為 attr 的特性時,Python 才會在 obj 實例中尋找。這條規(guī)則不僅適用于特性,還適用于一整類描述符——覆蓋型描述符。
先尋找類屬性,再尋找實例屬性。
如果使用經(jīng)典調(diào)用句法,為 property 對象設(shè)置文檔字符串的方法是傳入 doc 參數(shù):
weight = property(get_weight, set_weight, doc='weight in kilograms')
使用裝飾器創(chuàng)建 property 對象時,讀值方法(有 @property 裝飾器的方法)的文檔字符串作為一個整體,變成特性的文檔。
創(chuàng)建特性工廠函數(shù)
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
在真實的系統(tǒng)中,分散在多個類中的多個字段可能要做同樣的驗證,此時最好把quantity 工廠函數(shù)放在實用工具模塊中,以便重復使用。最終可能要重構(gòu)那個簡單的工廠函數(shù),改成更易擴展的描述符類,然后使用專門的子類執(zhí)行不同的驗證。
處理屬性刪除操作
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)))
影響屬性處理方式的特殊屬性,后面幾節(jié)中的很多函數(shù)和特殊方法,其行為受下述 3 個特殊屬性的影響。
__class__
對象所屬類的引用(即 obj.class 與 type(obj) 的作用相同)。Python 的某些特殊方法,例如 __getattr__,只在對象的類中尋找,而不在實例中尋找。
__dict__
一個映射,存儲對象或類的可寫屬性。有 __dict__ 屬性的對象,任何時候都能隨意設(shè)置新屬性。如果類有 __slots__ 屬性,它的實例可能沒有 __dict__ 屬性。參見下面對 __slots__ 屬性的說明。
__slots__
類可以定義這個這屬性,限制實例能有哪些屬性。__slots__ 屬性的值是一個字符串組成的元組,指明允許有的屬性。 如果 __slots__ 中沒有 '__dict__',那么該類的實例沒有 __dict__ 屬性,實例只允許有指定名稱的屬性。
當讀取實例屬性的時候會覆蓋類的屬性。而在讀取實例特性的時候,特性不會被實例屬性覆蓋,而依然是讀取類的特性。除非類特性被銷毀。需要根據(jù)具體情況選擇需要的使用方式。