Python 元編程(1)-動態(tài)屬性和特性

在 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é)果如下:


image.png

從結(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ù)具體情況選擇需要的使用方式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一定要跑贏時間,才不枉人世間一遭走。 人的初始,都是一樣從襁褓中的嬰兒開始,初始的三年,經(jīng)歷了從呀呀學語到蹣跚走路...
    在楚生香閱讀 359評論 0 0
  • 一、愛的理論前提:愛的理論必須以人的理論、人的生存理論為前提 人一生下來就從一個確定的環(huán)境被推到另一個充滿不確定因...
    敏姐的簡書閱讀 1,002評論 0 0
  • 嘿,簡友們,好久不見!今天難得抽出時間更文章,給大家介紹一下我的家鄉(xiāng),以及家鄉(xiāng)的“紅瑪瑙”。在這之前先告...
    Sunny儀閱讀 2,136評論 0 4
  • 漫呷格律 /黎峰 格律長在不同的故鄉(xiāng) 就成為了某些私人訂制 比如李白的靜夜思 比如王維的寒梅和山東兄弟 杜甫和東坡...
    黎峰小峰峰閱讀 697評論 13 21