Python学习之路38-动态创建属性

栏目: Python · 发布时间: 6年前

内容简介:平时我们一般把类中存储数据的变量称为属性,把类中的函数称为方法。但这两个概念其实是统一的,它们都称为属性(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 模块就可以读取。以下是用于读取 jsonload() 函数,如果数据不存在,它会自动从远端下载数据:

# 代码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].serialfeed.Schedule.speakers 中挨个查找。

为了实现这种关联访问,需要在读取数据时调整数据的结构:不再像之前 FrozenJSON 中那样,将整个JSON原始数据存到内部的 __data 中,而是 将每条数据单独存到一个 Record 对象中 (这里的“每条数据”指每个 event ,每个 speaker ,每个 venue 以及 conferences 中的唯一一条数据)。并且,还需要在每条数据的 serial 字段的值前面加上数据类型,比如某个 eventserial123 ,则将其变为 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 和继承自 RecordEvent ,并将这些数据放到名为 db 的映射中。 Event 专门用于存JSON数据中 events 里的数据,其余数据全部存为 Record 对象。之所以这么安排,是因为原始数据中, event 包含了 speakervenueserial (相当于外键约束)。现在,我们可以通过 event 查找到与之关联的 speakervenue ,而并不仅仅只是查找到这两个的 serial 。如果想根据 speakervenue 查找 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_dbset_db 则是设置和获取 __db 的方法。 fetch 方法是一个类方法,它用于从 __db 中获取数据。

Event 继承自 Record ,并添加了两个特性 venuespeakers ,也正是这两个特性实现了关联查找以及属性表示法。 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.Shelfabc.MutableMapping 的子类,提供了处理映射类型的重要方法;
  • 他还提供了几个管理I/O的方法,比如 syncclose ;它也是一个上下文管理器;
  • 键必须是字符串,值必须是 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 ~

Python学习之路38-动态创建属性

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hibernate

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》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具