Python動態屬性和特性(一)


在Python中,數據的屬性和處理數據的方法統稱為屬性。其實,方式只是可調用的屬性。除了這二者之外,我們還可以創建特性(property),在不改變類接口的前提下,使用存取方法(即讀取值和設置值方法)修改屬性

Python提供了豐富的API,用於控制屬性的訪問權限,以及實現動態屬性。當我們訪問obj的data屬性時,類似obj.data,Python解釋器會調用特殊方法如__getattr__或__setattr__計算屬性。用戶自定義的類可以通過__getattr__方法實現虛擬屬性,當訪問的屬性不存在時,即時計算屬性的值

我們先從遠程下載一個復雜的json文件並保存在本地

from urllib.request import urlopen
import warnings
import os
import json

URL = 'http://www.oreilly.com/pub/sc/osconfeed'
JSON = 'data/osconfeed.json'


def load():
    if not os.path.exists(JSON):
        msg = 'downloading {} to {}'.format(URL, JSON)
        warnings.warn(msg)  # <1>
        with urlopen(URL) as remote, open(JSON, 'wb') as local:  # <2>
            local.write(remote.read())

    with open(JSON, encoding="utf-8") as fp:
        return json.load(fp)  # <3>



  

 

  

這里,我們僅展示一部分的json文件的內容

{
	"Schedule": {
		"conferences": [{
			"serial": 115
		}],
		"events": [{
			"serial": 34505,
			"name": "Why Schools Don´t Use Open Source to Teach Programming",
			"event_type": "40-minute conference session",
			"time_start": "2014-07-23 11:30:00",
			"time_stop": "2014-07-23 12:10:00",
			"venue_serial": 1462,
			"description": "Aside from the fact that high school programming...",
			"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
			"speakers": [157509],
			"categories": ["Education"]
		}],
		"speakers": [{
			"serial": 157509,
			"name": "Robert Lefkowitz",
			"photo": null,
			"url": "http://sharewave.com/",
			"position": "CTO",
			"affiliation": "Sharewave",
			"twitter": "sharewaveteam",
			"bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..."
		}],
		"venues": [{
			"serial": 1462,
			"name": "F151",
			"category": "Conference Venues"
		}]
	}
}

  

我們可以看到文件的json文件里面的第一個鍵是Schedule,這個鍵對應的值也是一個字典,這個字典下還有四個鍵,分別是"conferences"、 "events"、 "speakers" 和 "venues",這四個鍵分別對應一個列表,在完整的數據集中, 列表中有成百上千條記錄。 不過,"conferences" 鍵對應的列表中只有一條記錄,這 4 個列表中的每個元素都有一個名為 "serial" 的字段,這是元素在各個列表中的唯一標識符。

下面,讓我們打印一下"conferences"、 "events"、 "speakers" 和 "venues"對應的列表的長度

feed = load()
print(sorted(feed["Schedule"].keys()))
print(feed["Schedule"]["speakers"][-1]["name"])
print(feed["Schedule"]["speakers"][40]["name"])
for key, value in sorted(feed["Schedule"].items()):
    print("{:3} {}".format(len(value), key))

  

運行結果:

['conferences', 'events', 'speakers', 'venues']
Carina C. Zona
Tim Bray
  1 conferences
494 events
357 speakers
 53 venues

  

從上述示例中,我們可以看到如果要訪問一個鍵,必須以feed['Schedule']['events'][40]['name']這種冗長的寫法來訪問,我們可以嘗試實現一個FrozenJSON類,來包裝我們原先的json數據,然后以feed.Schedule.events的形式來對字典進行訪問

  

from collections import abc


class FrozenJSON:

    def __init__(self, mapping):
        self.__data = dict(mapping)  # <1>

    def __getattr__(self, name):  # <2>
        if hasattr(self.__data, name):
            return getattr(self.__data, name) 
        else:
            return FrozenJSON.build(self.__data[name]) 

    @classmethod
    def build(cls, obj):  # <3>
        if isinstance(obj, abc.Mapping):  
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):  
            return [cls.build(item) for item in obj]
        else:  
            return obj

  

  1. FrozenJSON類接收一個字典,並復制字典的副本作為自身的屬性
  2. 當我們調用FrozenJSON的實例的某個屬性時,如frozenJson.attr,如果attr不是實例本身的屬性,則會調用__getattr__方法,該方法中,我們實現了先檢查attr是不是字典的某個屬性,如果是則返回該屬性,如果不是則當成要訪問的屬性是字典的某個鍵,將其值取出傳入FrozenJSON.build()方法並返回
  3. FrozenJSON.build()是一個類方法,它接收一個對象,如果該是字典對象,它會調用自身的初始化方法,如果該對象時可迭代對象,則遍歷該對象所有的元素,並重新將元素傳入自身的build()方法中,最后,如果該對象既不是字典,也不是一個可迭代的對象,則毫無改動的返回原始對象

現在,讓我們試一下FrozenJSON類,我們獲取原先的json文件,並傳入FrozenJSON類中初始化一個FrozenJSON實例,我們通過obj.attr的方式來訪問原先字典中的鍵,還可以調用key()、items()等方法

raw_feed = load()
feed = FrozenJSON(raw_feed)
print(len(feed.Schedule.speakers))
print(sorted(feed.Schedule.keys()))
for key, value in sorted(feed.Schedule.items()):
    print('{:3} {}'.format(len(value), key))
print(feed.Schedule.speakers[-1].name)
talk = feed.Schedule.events[40]
print(type(talk))
print(talk.name)

    

運行結果:

357
['conferences', 'events', 'speakers', 'venues']
  1 conferences
494 events
357 speakers
 53 venues
Carina C. Zona
<class '__main__.FrozenJSON'>
There *Will* Be Bugs

  

FrozenJSON 類的關鍵是 __getattr__ 方法,僅當無法使用常規的方式獲取屬性(即在實例、 類或超類中找不到指定的屬性), 解釋器才會調用特殊的 __getattr__ 方法。讀取不存在的屬性會拋出 KeyError 異常,而不是通常拋出的AttributeError 異常。

FrozenJSON 類只有兩個方法(__init__ 和__getattr__)和一個實例屬性 __data。因此,嘗試獲取其他屬性會觸發解釋器調用__getattr__方法。這個方法首先查看self.__data字典有沒有指定名稱的屬性(不是鍵) , 這樣 FrozenJSON 實例便可以處理字典的所有方法,例如把 items 方法委托給self.__data.items() 方法。如果 self.__data 沒有指定名稱的屬
性,那么 __getattr__ 方法以那個名稱為鍵, 從 self.__data 中獲取一個元素,傳給 FrozenJSON.build 方法。這樣就能深入JSON數據的嵌套結構,使用類方法 build 把每一層嵌套轉換成一個 FrozenJSON實例。

當我們傳入的字典對象包含的鍵是關鍵字,例如下面這個示例,如果我們要訪問grad.class,勢必會報錯,這個時候我們只能通過getattr()方法來訪問

grad = FrozenJSON({"name": "Jim Bo", "class": 1982})
print(getattr(grad, "class"))

  

或者,我們可以改造__init__方法,當檢查出一個鍵是關鍵字的時候,自動加上一個下划線_,這樣,當我們要訪問grad中的class屬性時,直接用grad.class_就行

def __init__(self, mapping):
	import keyword
	self.__data = {}
	for key, value in mapping.items():
		if keyword.iskeyword(key):
			key += "_"
		self.__data[key] = value

  

但是,如果有時候我們要訪問的鍵並不是有效的標識符,比如2_a同樣是傳入字典的一個鍵,但它並不是一個有效的標識符,這個如果調用grad.2_a會拋出SyntaxError: invalid token的錯誤

grad = FrozenJSON({"name": "Jim Bo", "class": 1982, "2_a": "hello"})
print(getattr(grad, "2_a"))

  

於是,我們只能通過getattr()方法來獲取值,又或者我們像之前那樣,通過將鍵值變為合法,再來訪問,我們再次改造FrozenJSON的初始化__init__方法

def __init__(self, mapping):
	import keyword
	self.__data = {}
	for key, value in mapping.items():
		if keyword.iskeyword(key):
			key += "_"
		if not str.isidentifier(key):
			key = "_{0}".format(key)
		self.__data[key] = value

  

這個方法會遍歷mapping所有的鍵,當鍵是關鍵字的時候,在鍵的尾部加上下划線,當鍵並不是關鍵字的時候,在鍵的頭部加上下划線,然后我們再來訪問class和2_a的屬性

grad = FrozenJSON({"name": "Jim Bo", "class": 1982, "2_a": "hello"})
print(grad.class_)
print(grad._2_a)

  

運行結果:

1982
hello

  

在后面這個鍵是否能作為標識符的時候,情況會略微復雜,因為鍵可以包含乘號或加號,如果一旦包含一些特殊符號,那只能通過getattr()來獲取了

 

我們通常說,__init__稱為構造方法,其實,用於構建實例的是特殊方法__new__方法,這是個類方法,使用特殊方式處理,因此不用加上@classmethod 裝飾器,這個方法會返回一個實例,實例會作為第一個參數(即self)傳入__init__方法。因為調用__init__方法時要傳入實例,而且禁止返回任何值,所以__init__其實是初始化方法,真正的構造方法時__new__,我們幾乎不需要自己編寫 __new__ 方法,因為從 object 類繼承的實現已經足夠了。

class Foo(object):
    def __init__(self, bar):
        self.bar = bar


def object_maker(the_class, some_arg):
    new_object = the_class.__new__(the_class)
    if isinstance(new_object, the_class):
        the_class.__init__(new_object, some_arg)
    return new_object


demo1 = Foo("bar1")
demo2 = object_maker(Foo, "bar2")
print(demo1)
print(demo1.bar)
print(demo2)
print(demo2.bar)

    

運行結果:

<__main__.Foo object at 0x0000005184EB1F98>
bar1
<__main__.Foo object at 0x0000005184EB11D0>
bar2

    

可以看到,我們既可以用Foo類來初始化一個對象,也可以把Foo和所需參數傳入object_maker方法,來構造一個我們需要的對象

現在,讓我們用__new__方法來代替剛才FrozenJSON類的build()方法

from collections import abc


class FrozenJSON:

    def __new__(cls, arg): 
        if isinstance(arg, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(arg, abc.MutableSequence):
            return [cls(item) for item in arg]
        else:
            return arg

    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])

  

在__getattr__中,如果訪問的鍵並非__data本身的屬性,我們不再調用FrozenJSON.build()方法傳入,而是之間把參數傳入FrozenJSON的構造方法,這個方法它可能返回一個FrozenJSON實例,也可能不是,我們都知道,當Python要構造一個實例時,首先會調用__new__方法返回一個實例,再用__init__方法對實例進行屬性的初始化,在FrozenJSON中,只有arg為Mapping類型時,返回的才是FrozenJSON實例,當arg是一個list或其他類型時,返回的就不是FrozenJSON對象了,這時候Python解釋器拿到這個對象,會對比__new__返回的實例和它要創建的實例是不是同一個類型,只有同一個類型,Python解釋器才會接着調用__init__進行初始化操作,否則直接將從__new__方法拿到的實例返回

  

shelve類似一個可持久化的字典,他有一個open()函數,這個函數接收一個參數就是文件名,然后返回一個shelve.Shelf 實例,我們可以用他來存儲一些鍵值對,當存儲完畢的時候,就調用close函數來關閉,shelve有以下幾個特點:

  1. shelve.Shelf 是 abc.MutableMapping 的子類,因此提供了處理映射類型的重要方法
  2. 此外,shelve.Shelf 類還提供了幾個管理I/O的方法,如sync和close;它也是一個上下文管理器
  3. 只要把新值賦予鍵,就會保存鍵和值
  4. 鍵必須是字符串
  5. 值必須是 pickle 模塊(可序列化對象的模塊)能處理的對象

再回到我們之前從網上下載的json文件,之前我們解析過這個文件,文件內部的第一個鍵是Schedule,而這個鍵對應的字典還有四個鍵conferences、events、speakers、venues,而這四個鍵對應的值,又是包着很多字典對象的list,我們不用去了解這個文件到底在說明什么,現在只需要了解一點,conferences、events、speakers、venues這四個鍵對應的list中的每一個字典對象,都包含一個叫serial的鍵,這個鍵對應的值是一個數字,現在,讓我們遍歷Schedule下的四個鍵,並將這四個鍵與list下每個字典對象中的serial鍵對應的值相結合相結

class Record:

    def __init__(self, **kwargs):  # <6>
        self.__dict__.update(kwargs)


def load_db(db):
    raw_data = load()  # <3>
    for collection, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1]  # <4>
        for record in rec_list:
            key = '{}.{}'.format(record_type, record['serial'])  # <5>
            record['serial'] = key
            db[key] = Record(**record)


import shelve

DB_NAME = 'data/schedule1_db'
CONFERENCE = 'conference.115'
db = shelve.open(DB_NAME)  # <1>
if CONFERENCE not in db:  # <2>
    load_db(db)

  

  1. 先根據DB_NAME請求一個shelve.Shelf 實例
  2. 得到db(即shelve.Shelf 實例)后,檢查conference.115這個鍵是否在db這個鍵值對數據庫中,如果不在,調用load_db()方法開始加載數據
  3. 再次用load()方法獲取json文件
  4. 我們遍歷json文件下Schedule對應的四個鍵,分別是conferences、events、speakers、venues,而collection[:-1]代表去除這四個鍵中最后一個字母,即s,然后存入key 
  5. rec_list代表上述4個鍵對應的包含字典對象的列表,我們遍歷這個列表,取出每個字典對象的serial鍵,並與key相結合,比如:"conferences":[{"serial":115},{"serial":116}]就會形成兩個鍵,分別是conference.115,conference.116,當然,在我們的json文件中,conferences這個鍵只有一個{"serial":115}對象,並沒有{"serial":116}對象,這里只是舉例說明,之后,我們用key替代原先serial的值,並初始化一個Record對象
  6. 我們將一個字典傳入Record的初始化方法,self.__dict__.update(kwargs)這個方法會將kwargs這個字典中所有的鍵初始化為Record這個對象的屬性,也就是說,self.__dict__這個字典,存着是本對象的所有屬性

然后,我們嘗試一下通過shelve來訪問這個json文件

speaker = db['speaker.3471']
print(type(speaker))
print(speaker.name, speaker.twitter)
db.close()

  

運行結果:

<class 'schedule1.Record'>
Anna Martelli Ravenscroft annaraven

 

這里可以看到,speakers下,serial為3471所在的字典的name和twitter的值是否和我們打印出來的值一一對應,這里還有一點要記住,就是打開db后,最后要記得關閉db

events下的每個字典里都有兩個鍵,一個是venue_serial,另一個是speakers,讓我們擴展之前的Record類,使得我們訪問event下的venue或speakers返回的不再是一個冷冰冰的id,而是關聯到venues或speakers的實體字典

 

如上圖所示,我們在原先的Record類的基礎上,又擴展的兩個類,分別是DbRecord和Event,DbRecord繼承自Record,而Event繼承自DbRecord

class Record:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def __eq__(self, other): 
        if isinstance(other, Record):
            return self.__dict__ == other.__dict__
        else:
            return NotImplemented

  

首先是Record類,我們看到,__init__方法沒有變化,只是多了一個__eq__方法,比較兩個Record中包含的__dict__(即類本身的屬性)是否相等,如果不相等,則返回一個NotImplemented,這里多介紹一下NotImplemented這個內建常量

class A:
    def __init__(self, num):
        self.num = num

    def __eq__(self, other):
        print("call A __eq__")
        return NotImplemented


class B:
    def __init__(self, num):
        self.num = num

    def __eq__(self, other):
        print("call B __eq__")
        return self.num == other.num


a = A(1)
b = B(1)
print(a == b)

  

運行結果:

call A __eq__
call B __eq__
True

  

可以看到,類A的__eq__方法不管傳入什么,最后都會返回一個NotImplemented,當Python解釋器接收到一個NotImplemented常量,就會調用b.__eq__(a)進行比較

class MissingDatabaseError(RuntimeError):
    """Raised when a database is required but was not set."""


class DbRecord(Record):
    __db = None

    @staticmethod
    def set_db(db):  # <1>
        DbRecord.__db = db

    @staticmethod
    def get_db():  # <2>
        return DbRecord.__db

    @classmethod
    def fetch(cls, ident):  # <3>
        db = cls.get_db()
        try:
            return db[ident]
        except TypeError:
            if db is None:  # <4>
                msg = "database not set; call '{}.set_db(my_db)'"
                raise MissingDatabaseError(msg.format(cls.__name__))
            else:
                raise

    def __repr__(self):  # <5>
        if hasattr(self, 'serial'):
            cls_name = self.__class__.__name__
            return '<{} serial={!r}>'.format(cls_name, self.serial)
        else:
            return super().__repr__()

  

  1. 設置數據源,即shelve.Shelf 實例
  2. 獲取數據源
  3. 傳入一個鍵,從數據源中獲取對應的值
  4. 當數據源為None時拋出MissingDatabaseError錯誤
  5. 重定義當前record對象的打印信息
class Event(DbRecord):

    @property
    def venue(self):  
        key = 'venue.{}'.format(self.venue_serial)
        h = self.__class__.fetch(key)
        return self.__class__.fetch(key)

    @property
    def speakers(self):  
        if not hasattr(self, '_speaker_objs'):
            spkr_serials = self.__dict__['speakers']
            fetch = self.__class__.fetch
            self._speaker_objs = [fetch('speaker.{}'.format(key))
                                  for key in spkr_serials]
        return self._speaker_objs

    def __repr__(self):
        if hasattr(self, 'name'):
            cls_name = self.__class__.__name__
            return '<{} {!r}>'.format(cls_name, self.name)
        else:
            return super().__repr__()

  

Event類主要是用於遍歷events下的字典所用,當我們訪問venue這個屬性時,他會結合自身venue_serial所對應的值形成venue.{venue_serial},再次返回數據源,同理speakers,當要從event訪問speakers,先檢查_speaker_objs是否存在,如果不存在,則取出自身的speakers對象,這是一個list,里面包含多個speakers的id,然后遍歷這個list從數據源中取出對應的speakers,並緩存為_speaker_objs屬性

這里,我們需要改造一下之前的load_db方法

def load_db(db):
    raw_data = load()
    for collection, rec_list in raw_data['Schedule'].items():
        record_type = collection[:-1]  # <1>
        cls_name = record_type.capitalize()  # <2>
        cls = globals().get(cls_name, DbRecord)  # <3>
        if inspect.isclass(cls) and issubclass(cls, DbRecord):  # <4>
            factory = cls  
        else:
            factory = DbRecord 
        for record in rec_list:  # <5>
            key = '{}.{}'.format(record_type, record['serial'])
            record['serial'] = key
            db[key] = factory(**record)  # <6>


import shelve

db = shelve.open(DB_NAME)
if CONFERENCE not in db:
    load_db(db)
DbRecord.set_db(db)  # <9>

  

  1. collection依舊是Schedule下的四個鍵,分別是conferences、events、speakers、venues,collection[:-1]可以把這四個鍵最后的s字符去除
  2. 將去除尾部s的四個鍵首字母大寫
  3. 經過上面兩個步驟,四個鍵分別為Conference、Event、Seaker、Venue,檢查全局域內是否有和這四個鍵名稱相同的類型,如果有則取出,沒有則返回DbRecord,由於我們之前定義了Event類,所以當遍歷到events鍵時,會取出之前定義好的Event類,而其他三個鍵則會默認返回DbRecord
  4. 檢查從全局域中取出來的類型是否是類型對象,且是DbRecord的子類,並賦值給factory
  5. 遍歷四個鍵下所有的字典,當遍歷到events這個鍵時,存入數據源的對象時Event類型,而其他三個鍵則是DbRecord類型

然后,我們嘗試獲取event下的venue和speakers

event = DbRecord.fetch('event.33950')
print(event)
print(type(event))
print(event.venue)
print(event.venue.name)
print(event.speakers)
for spkr in event.speakers:
    print('{0.serial}: {0.name}'.format(spkr))

  

運行結果:

<Event 'There *Will* Be Bugs'>
<class 'schedule2.Event'>
<DbRecord serial='venue.1449'>
Portland 251
[<DbRecord serial='speaker.3471'>, <DbRecord serial='speaker.5199'>]
speaker.3471: Anna Martelli Ravenscroft
speaker.5199: Alex Martelli

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM