在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
- FrozenJSON類接收一個字典,並復制字典的副本作為自身的屬性
- 當我們調用FrozenJSON的實例的某個屬性時,如frozenJson.attr,如果attr不是實例本身的屬性,則會調用__getattr__方法,該方法中,我們實現了先檢查attr是不是字典的某個屬性,如果是則返回該屬性,如果不是則當成要訪問的屬性是字典的某個鍵,將其值取出傳入FrozenJSON.build()方法並返回
- 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有以下幾個特點:
- shelve.Shelf 是 abc.MutableMapping 的子類,因此提供了處理映射類型的重要方法
- 此外,shelve.Shelf 類還提供了幾個管理I/O的方法,如sync和close;它也是一個上下文管理器
- 只要把新值賦予鍵,就會保存鍵和值
- 鍵必須是字符串
- 值必須是 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)
- 先根據DB_NAME請求一個shelve.Shelf 實例
- 得到db(即shelve.Shelf 實例)后,檢查conference.115這個鍵是否在db這個鍵值對數據庫中,如果不在,調用load_db()方法開始加載數據
- 再次用load()方法獲取json文件
- 我們遍歷json文件下Schedule對應的四個鍵,分別是conferences、events、speakers、venues,而collection[:-1]代表去除這四個鍵中最后一個字母,即s,然后存入key
- rec_list代表上述4個鍵對應的包含字典對象的列表,我們遍歷這個列表,取出每個字典對象的serial鍵,並與key相結合,比如:"conferences":[{"serial":115},{"serial":116}]就會形成兩個鍵,分別是conference.115,conference.116,當然,在我們的json文件中,conferences這個鍵只有一個{"serial":115}對象,並沒有{"serial":116}對象,這里只是舉例說明,之后,我們用key替代原先serial的值,並初始化一個Record對象
- 我們將一個字典傳入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__()
- 設置數據源,即shelve.Shelf 實例
- 獲取數據源
- 傳入一個鍵,從數據源中獲取對應的值
- 當數據源為None時拋出MissingDatabaseError錯誤
- 重定義當前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>
- collection依舊是Schedule下的四個鍵,分別是conferences、events、speakers、venues,collection[:-1]可以把這四個鍵最后的s字符去除
- 將去除尾部s的四個鍵首字母大寫
- 經過上面兩個步驟,四個鍵分別為Conference、Event、Seaker、Venue,檢查全局域內是否有和這四個鍵名稱相同的類型,如果有則取出,沒有則返回DbRecord,由於我們之前定義了Event類,所以當遍歷到events鍵時,會取出之前定義好的Event類,而其他三個鍵則會默認返回DbRecord
- 檢查從全局域中取出來的類型是否是類型對象,且是DbRecord的子類,並賦值給factory
- 遍歷四個鍵下所有的字典,當遍歷到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