内容简介:平时我们一般把类中存储数据的变量称为属性,把类中的函数称为方法。但这两个概念其实是统一的,它们都称为属性(Attrubute),方法只是可调用的属性,并且属性还是可以动态创建的。如果我们事先不知道数据的结构,或者在运行时需要再添加一些属性,此时就需要动态创建属性。本文将讲述如果通过动态创建属性来读取JSON中的数据。第一个例子我们将实现一个不过在这两部分内容之前,先来看一个简单粗暴地使用JSON数据的例子。
平时我们一般把类中存储数据的变量称为属性,把类中的函数称为方法。但这两个概念其实是统一的,它们都称为属性(Attrubute),方法只是可调用的属性,并且属性还是可以动态创建的。如果我们事先不知道数据的结构,或者在运行时需要再添加一些属性,此时就需要动态创建属性。
本文将讲述如果通过动态创建属性来读取JSON中的数据。第一个例子我们将实现一个 FrozenJSON
类,使用 __getattr__
方法,根据JSON文件中的数据项动态创建 FrozenJSON
实例的属性。第二个例子,更进一步,实现数据的关联查找,其中,会用到实例的 __dict__
属性来动态创建属性。
不过在这两部分内容之前,先来看一个简单粗暴地使用JSON数据的例子。
2. JSON数据
首先是一个现实世界中的JSON数据:OSCON大会的JSON数据。为了节省篇幅,只保留了它的数据格式中的一部分,数据内容也有所改变,原始数据会在用到的时候下载:
{ "Schedule": { "conferences": [{"serial": 115}], "events": [{ "serial": 33451, "name": "This is a test", "venue_serial": 1449, "speakers": [149868] }], "speakers": [{ "serial": 149868, "name": "Speaker1", }], "venues": [{ "serial": 1448, "name": "F151", }] }} 复制代码
整个数据集是一个JSON对象,也是一个映射(map),(最外层)只有一个键 "Schedule"
,它表示整个大会; "Schedule"
的值也是一个map,这个map有4个键,分别是:
"conferences" "events" "speakers" "venues"
这4个键的值都是列表,而列表的元素又都是map,其中某些键的值又是列表。是不是很绕 :) ?
还需要注意一点:每条数据都有一个 "serial"
,相当于一个标识,后面会用到
2.1 读取JSON数据
读取JSON文件很简单,用 Python 自带的 json
模块就可以读取。以下是用于读取 json
的 load()
函数,如果数据不存在,它会自动从远端下载数据:
# 代码2.1 osconfeed.py 注意这个模块名,后面还会用到 import json import os import warnings from urllib.request import urlopen URL = "http://www.oreilly.com/pub/sc/osconfeed" JSON = "data/osconfeed.json" def load(): if not os.path.exists(JSON): # 如果本地没有数据,则从远端下载 with urlopen(URL) as remote, open(JSON, "wb") as local: # 这里打开了两个上下文管理器 local.write(remote.read()) with open(JSON) as fp: return json.load(fp) 复制代码
2.2 使用JSON数据
现在我们来读取并使用上述JSON数据:
# 代码2.2 >>> from osconfeed import load >>> feed = load() >>> feed['Schedule']['events'][40]['speakers'] [3471, 5199] 复制代码
从这个例子可以看出,要访问一个数据,得输入多少中括号和引号,为了跳出这些中括号和引号,又得浪费多少操作?如果再嵌套几个map......
在JavaScript中,可以通过 feed.Schedule.events[40].speakers
来访问数据,Python中也可以很容易实现这样的访问。这种方式, "Schedule"
, "events"
和 "speakers"
等数据项则表现的并不像map的键,而更像类的属性,因此,这种访问方式也叫做 属性表示法 。这在 Java 中有点像链式调用,但链式调用调用的是函数,而这里是数据属性。但为了方面,后面都同一叫做 链式访问 。
下面正式进入本篇的第一个主题:动态创建属性以读取JSON数据。
3. FrozenJSON
我们通过创建一个 FrozenJSON
类来实现动态创建属性,其中创建属性的工作交给了 __getattr__
特殊方法。这个类可以实现链式访问。
3.1 初版FrozenJSON类
# 代码3.1 explore0.py from collections import abc class FrozenJSON: def __init__(self, mapping): self.__data = {} # 为了安全,创建副本 for key, value in mapping.items(): # 确保传入的数据能转换成字典; if keyword.iskeyword(key): # 如果某些属性是Python的关键字,不适合做属性, key += "_" # 则在前面加一个下划线 self.__data[key] = value def __getattr__(self, name): # 当没有指定名称的属性时,才调用此法;name是str类型 if hasattr(self.__data, name): # 如果self.__data有这个属性,则返回这个属性 return getattr(self.__data, name) else: # 如果self.__data没有指定的属性,创建FronzenJSON对象 return FrozenJSON.build(self.__data[name]) # 递归转换嵌套的映射和列表 @classmethod def build(cls, obj): # 必须要定义这个方法,因为JSON数据中有列表!如果数据中只有映射,或者在__init__中进行了 # 类型判断,则可以不定义这个方法。 if isinstance(obj, abc.Mapping): # 如果obj是映射,则直接构造 return cls(obj) elif isinstance(obj, abc.MutableSequence): # 如果obj是MutableSequence,则在本例中,obj则必定是列表,而列表的元素又必定是映射 return [cls.build(item) for item in obj] else: # 如果两者都不是,则原样返回 return obj 复制代码
这个类非常的简单。由于没有定义任何数据属性,所以,在访问数据时,每次都会调用 __getattr__
特殊方法,并在这个方法中递归创建新实例,即, 通过 __getattr__
特殊方法实现动态创建属性,通过递归构造新实例实现链式访问 。
3.2 使用FrozenJSON
下方代码是对这个类的使用:
# 代码3.2 >>> from osconfeed import load >>> from explore0 import FrozenJSON >>> raw_feed = load() # 读取原始JSON数据 >>> feed = FrozenJSON(raw_feed) # 使用原始数据生成FrozenJSON实例 >>> len(feed.Schedule.speakers) # 对应于FronzenJSON.__getattr__中if为False的情况 357 >>> sorted(feed.Schedule.keys()) # 对应于FrozenJSON.__getattr__中if为True的情况 ['conferences', 'events', 'speakers', 'venues'] >>> feed.Schedule.speakers[-1].name 'Carina C. Zona' >>> talk = feed.Schedule.events[40] >>> type(talk) <class 'explore0.FrozenJSON'> >>> talk.name 'There *Will* Be Bugs' >>> talk.speakers [3471, 5199] >>> talk.flavor # !!! Traceback (most recent call last): KeyError: 'flavor' 复制代码
上述代码中,通过不断从 FrozenJSON
对象中创建 FrozenJSON
对象,实现了属性表示法。为了更好的理解上述代码,我们需要分析其中实例的创建过程:
feed
是一个 FrozenJSON
实例,当访问 Schedule
属性时,由于 feed
没有这个属性,于是调用 __getattr__
方法。由于 Schedule
也不是 feed.__data
的属性,所以需要再创建一个 FrozenJSON
对象。 Schedule
在JSON数据中是最外层映射的键,它的值 feed.__data["Schedule"]
又是一个映射,所以在 build
方法中,继续将 feed.__data["Schedule"]
包装成一个 FrozenJSON
对象。如果继续链接下去,还会创建 FrozenJSON
对象。这里之所以指出这一点,是想提醒大家 注意每个 FrozenJSON
实例中的 __data
具体指的是JSON数据中的哪一部分数据 (我在模拟这个递归过程的时候,多次都把 __data
搞混)。
上述代码中还有一处调用: feed.Schedule.keys()
。 feed.Schedule
是一个 FrozenJSON
对象,它并没有 keys
方法,于是调用 __getattr__
,但由于 feed.Schedule.__data
是个 dict
,它有 keys
方法,所以这里并没有继续创建新的 FrozenJSON
对象。
注意最后一处调用: talk.flavor
。JSON中 events
里并没有 flavor
数据项,因此这里抛出了异常。但这个异常是 KeyError
,而更合理的做法应该是:只要没有这个属性,都应该抛出 AttributeError
。如果要抛出 AttributeError
, __getattr__
的代码长度将增加一倍,但这并不是本文的重点,所以没有处理。
3.3 特殊方法__new__
在初版 FrozenJSON
中,我们定义了一个类方法 build
来创建新实例,但更方便也更符合Python风格的做法是定义 __new__
方法:
# 代码3.3 frozenjson.py 新增__new__,去掉build,修改__getattr__ class FrozenJSON: def __getattr__(self, name): -- snip -- else: # 直接创建FrozenJSON对象 return FrozenJSON(self.__data[name]) 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 复制代码
不知道大家第一次看到“构造方法 __init__
”这个说法时有没有疑惑:这明明是初始化Initialize这个单词的缩写,将其称为“构造(create, build)”似乎不太准确呀?其实这个称呼是从其他语言借鉴过来的,它更应该叫做“初始化方法”,因为它确实执行的是初始化的工作,真正执行“构造”的是 __new__
方法。
一般情况下,当创建类的实例时,首先调用的是 __new__
方法,它必须创建并返回一个实例,然后将这个实例作为第一个参数(即 self
)传入 __init__
方法,再由 __init__
执行初始化操作。但也有不常见的情况: __new__
也可以返回其他类的实例,此时,解释器不会继续调用 __init__
方法。
__new__
方法是一个类方法 ,由于使用了特殊方法方式处理,所以它不用加 @classmethod
装饰器。
我们几乎不需要自行编写 __new__
方法,因为从 object
类继承的这个方法已经足够了。
使用 FrozenJSON
读取JSON数据的例子到此结束。
4. Record
上述 FrozenJSON
有个明显的缺点:查找有关联的数据很麻烦,必须从头遍历 Schedule
的相关数据项。比如 feed.Schedule.events[40].speakers
是一个含有两个元素的列表,它是这场演讲的演讲者们的编号。如果想访问演讲者的具体信息,比如姓名,我们不能直接调用 feed.Schedule.events[40].speakers[0].name
,这样会报 AttributeError
,只能根据 feed.Schedule.events[40].serial
在 feed.Schedule.speakers
中挨个查找。
为了实现这种关联访问,需要在读取数据时调整数据的结构:不再像之前 FrozenJSON
中那样,将整个JSON原始数据存到内部的 __data
中,而是 将每条数据单独存到一个 Record
对象中 (这里的“每条数据”指每个 event
,每个 speaker
,每个 venue
以及 conferences
中的唯一一条数据)。并且,还需要在每条数据的 serial
字段的值前面加上数据类型,比如某个 event
的 serial
为 123
,则将其变为 event.123
。
4.1 要实现的功能
不过在给出实现方法之前,先来看看它应该具有的功能:
# 代码4.1 >>> from schedule import Record, Event, load_db >>> db = {} >>> load_db(db) >>> Record.set_db(db) >>> event = Record.fetch("event.33950") >>> event <schedule.Event object at 0x000001DBC71E9CF8> >>> event.venue <schedule.Record object at 0x000001DBC7714198> >>> event.venue.name 'Portland 251' >>> for spkr in event.speakers: ... print("{0.serial}: {0.name}".format(spkr)) ... speaker.3471: Anna Martelli Ravenscroft speaker.5199: Alex Martelli 复制代码
这其中包含了两个类, Record
和继承自 Record
的 Event
,并将这些数据放到名为 db
的映射中。 Event
专门用于存JSON数据中 events
里的数据,其余数据全部存为 Record
对象。之所以这么安排,是因为原始数据中, event
包含了 speaker
和 venue
的 serial
(相当于外键约束)。现在,我们可以通过 event
查找到与之关联的 speaker
和 venue
,而并不仅仅只是查找到这两个的 serial
。如果想根据 speaker
或 venue
查找 event
,大家可以根据后面的方法自行实现(但这么做得遍历整个 db
)。
4.2 Record & Event
下面是这两个类以及调整数据结构的 load_db
函数的实现:
# 代码4.2 schedule.py import inspect import osconfeed class Record: __db = None def __init__(self, **kwargs): self.__dict__.update(**kwargs) # 在这里动态创建属性! @staticmethod def set_db(db): Record.__db = db @staticmethod def get_db(): return Record.__db @classmethod def fetch(cls, ident): # 获取数据 db = cls.get_db() return db[ident] class Event(Record): @property def venue(self): key = "venue.{}".format(self.venue_serial) return self.__class__.fetch(key) # 并不是self.fetch(key) @property def speakers(self): # event对应的speaker的数据项保存在_speaker_objs属性中 if not hasattr(self, "_speaker_objs"): # 如果没有speakers数据,则从数据集中获取 spkr_serials = self.__dict__["speakers"] # 首先获取speaker的serial fetch = self.__class__.fetch self._speaker_objs = [fetch("speaker.{}".format(key)) for key in spkr_serials] return self._speaker_objs 复制代码
可以看到, Record
类中一个数据属性都没有,真正实现动态创建属性的是 __init__
方法中的 self.__dict__.update(**kwargs)
,其中 kwargs
是一个映射,在本例中,它就是每一个条JSON数据。
如果类中没有声明 __slots__
,实例的属性都会存到实例的 __dict__
中, Record.__init__
方法展示的是一个流行的Python技巧,这种方法能快速地为实例添加大量属性。
在 Record
中还有一个类属性 __db
,它是数据集的引用,并不是数据集的副本。本例中,我们将数据放到了一个 dict
中, __db
指向这个 dict
。其实也可以放到数据库中,然后 __db
存放数据库的引用。静态方法 get_db
和 set_db
则是设置和获取 __db
的方法。 fetch
方法是一个类方法,它用于从 __db
中获取数据。
Event
继承自 Record
,并添加了两个特性 venue
和 speakers
,也正是这两个特性实现了关联查找以及属性表示法。 venue
的实现很简单,因为一个 event
只对于一个 venue
,给 event
中的 venue_serial
添加一个前缀,然后查找数据集即可。
Event.speakers
的实现则稍微有点复杂:首先得清楚,这里查找的不是 speaker
的标识 serial
,而是查找 speaker
的具体数据项。查找到的数据项保存在 Event
实例的 _speaker_objs
中。一般在第一访问 event.speakers
时会进入到 if
中。还有情况就是 event._speakers_objs
被删除了。
Event
中还有一个值得注意的地方:调用 fetch
方法时,并不是直接 self.fetch
,而是 self.__class__.fetch
。这样做是为了避免一些很隐秘的错误:如果数据中有名为 fetch
的字段,这就会和 fetch
方法冲突,此时获取的就不是 fetch
方法,而是一个数据项。这种错误不易发觉,尤其是在动态创建属性的时候,如果数据不完全规则,几百几千条数据中突然有一条数据的某个属性名和实例的方法重名了,这个时候调试起来简直是噩梦。所以,除非能确保数据中一定不会有重名字段,否则建议按照本例中的写法。
4.3 load_db()
下面是加载和调整数据的 load_db()
函数的代码:
# 代码4.3,依然在schedule.py文件中 def load_db(db): raw_data = a.load() # 首先加载原始JSON数据 for collection, rec_list in raw_data["Schedule"].items(): # 遍历Schedule中的数据 record_type = collection[:-1] # 将Schedule中4个键名作为类型标识,去掉键名后面的's' cls_name = record_type.capitalize() # 将类型名首字母大写作为可能的类名 # 从全局作用域中获取对象;如果找不到所要的对象,则用Record代替 cls = globals().get(cls_name, Record) # 如果获取的对象是个类,且是Record的子类,则稍后用其创建实例;否则用Record创建实例 if inspect.isclass(cls) and issubclass(cls, Record): factory = cls else: factory = Record for record in rec_list: # 遍历Schedule中每个键对应的数据列表 key = "{}.{}".format(record_type, record["serial"]) # 生成新的serial record["serial"] = key # 这里是替换原有数据,而不是添加新数据! db[key] = factory(**record) # 生成实例,并存入数据集中 复制代码
该函数是一个嵌套循环,最外层循环只迭代4次。每条数据都被包装为一个 Record
,且 serial
字段的值中添加了数据类型,这个新的 serial
也作为键和 Record
实例组成键值对存入 db
中。
4.4 shelve
前面说过, db
可以从 dict
换成数据库的引用。Python标准库中则提供了一个现成的数据库类型 shelve.Shelf
。它是一个简单的键值对数据库,背后由 dbm
模块支持,具有如下特点:
-
shelve.Shelf
是abc.MutableMapping
的子类,提供了处理映射类型的重要方法; - 他还提供了几个管理I/O的方法,比如
sync
和close
;它也是一个上下文管理器; - 键必须是字符串,值必须是
pickle
模块能处理的对象。
本例中,它的用法和 dict
没有太大区别,以下是它的用法:
# 代码4.4 >>> import shelve >>> db = shelve.open("data/schedule_db") # shelve.open方法返回一个shelve.Shelf对象 >>> if "conference.115" not in db: # 这是一个简单的检测数据库是否加载的技巧,仅限本例 ... load_db(db) # 如果是个空数据库,则向数据库中填充数据 ... # 中间的用法就和之前的dict没有区别了,不过最后需要记住调用close()关闭数据库连接 >>> db.close() # 建议在with块中访问db 复制代码
5. Record vs FrozenJSON
如果不需要关联查询,那么 Record
只需要一个 __init__
方法,而且也不用定义 Event
类。这样的话, Record
的代码将比 FrozenJSON
简单很多,那为什么之前 FrozenJSON
不这么定义呢?原因有两点:
-
FrozenJSON
要递归转换嵌套的映射和列表,而Record
类不需要这么做,因为所有的映射都被转换成了对应的Record
,转换好的数据集中没有嵌套的映射和列表。 - 在
FrozenJSON
中,没有改动JSON数据的数据结构,因此,为了实现链式访问,需要将整个JSON数据存到内嵌的__data
属性中。而在Record
中,每条数据都被包装成了单个的Record
,且对数据结构进行了重构。
还有一点,本例中,使用映射来实现 Record
类或许更符合Python风格,但这样就无法展示动态属性编程的技巧和陷阱。
6. 总结
我们通过两个例子说明了如何动态创建属性:第一个例子是在 FrozenJSON
中通过实现 __getattr__
方法动态创建属性,这个类还可以实现链式访问;第二个例子是通过创建 Record
和它的子类 Event
来实现关联查找,其中我们在 __init__
方法中通过 self.__dict__.update(**kw)
这个技巧实现批量动态创建属性。
迎大家关注我的微信公众号"代码港" & 个人网站www.vpointer.net ~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 初识属性动画——使用Animator创建动画
- HBase无法创建带有snappy压缩属性的表
- WPF 中如何创建忽略 DPI 属性的图片
- 项目文件中的已知 NuGet 属性(使用这些属性,创建 NuGet 包就可以不需要 nuspec 文件啦)
- c# – 如果返回false,如何创建将重定向到Login的自定义属性,类似于Authorize属性 – ASP.NET MVC
- CSS 属性篇(七):Display属性
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Hibernate
James Elliott / O'Reilly Media, Inc. / 2004-05-10 / USD 24.95
Do you enjoy writing software, except for the database code? Hibernate:A Developer's Notebook is for you. Database experts may enjoy fiddling with SQL, but you don't have to--the rest of the appl......一起来看看 《Hibernate》 这本书的介绍吧!