天天看點

python特性和屬性_Python 元程式設計(1)-動态屬性和特性

在 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()

代碼運作結果如下:

python特性和屬性_Python 元程式設計(1)-動态屬性和特性

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__ 屬性,執行個體隻允許有指定名稱的屬性。

當讀取執行個體屬性的時候會覆寫類的屬性。而在讀取執行個體特性的時候,特性不會被執行個體屬性覆寫,而依然是讀取類的特性。除非類特性被銷毀。需要根據具體情況選擇需要的使用方式。