围绕JSON数据包装python类,哪个更好?

时间:2016-07-19 16:51:56

标签: python json class wrapper api-design

序言:我正在针对提供JSON的服务编写python API。 这些文件以JSON格式存储在磁盘上以缓存值。 API应该对JSON数据进行有类访问,因此IDE和用户可以在运行之前了解对象中的(只读)属性,同时还提供一些便利功能。

问题:我有两种可能的实现,我想知道哪种更好或者是pythonic'。虽然我喜欢两者,但如果您想出更好的解决方案,我愿意接受建议。

第一个解决方案:定义和继承JSONWrapper 虽然很好,但它非常详细且重复。

class JsonDataWrapper:
    def __init__(self, json_data):
        self._data = json_data

    def get(self, name):
        return self._data[name]


class Course(JsonDataWrapper):
    def __init__(self, data):
        super().__init__(data)
        self._users = {}  # class omitted
        self._groups = {}  # class omitted
        self._assignments = {}

    @property
    def id(self): return self.get('id')

    @property
    def name(self): return self.get('full_name')

    @property
    def short_name(self): return self.get('short_name')

    @property
    def users(self): return self._users

    @users.setter
    def users(self, data):
        users = [User(u) for u in data]
        for user in users:
            self.users[user.id] = user
            # self.groups = user  # this does not make much sense without the rest of the code (It works, but that decision will be revised :D)

第二个解决方案:使用lambda缩短语法。在工作和缩短时,它看起来不太正确(参见下面的edit1。)

def json(name): return property(lambda self: self.get(name))

class Group(JsonDataWrapper):
    def __init__(self, data):
        super().__init__(data)
        self.group_members = []  # elements are of type(User). edit1, was self.members = []

    id = json('id')
    description = json('description')
    name = json('name')
    description_format = json('description_format')

(命名此功能' json'不是问题,因为我不会在那里导入json。)

我有一个可能的第三个解决方案,我无法完全理解:覆盖内置属性,因此我可以定义一个装饰器,它包装返回的字段名称以供查找:

@json  # just like a property fget
def short_name(self): return 'short_name'

这可能会更短一点,如果能让代码变得更好,那就不知道了。

取消资格的解决方案(恕我直言):

  • JSON {De,En}编码器:杀死所有灵活性,不提供只读属性的方法
  • __{get,set}attr__:无法在运行时之前确定属性。虽然它会将self.get('id')缩短为self['id'],但它也会使属性不在基础json数据中的问题进一步复杂化。

感谢您阅读!

编辑1:2016-07-20T08:26Z

进一步澄清(@SuperSaiyan)为什么我不喜欢第二种解决方案: 我觉得lambda函数与其他类语义完全断开(这也是它更短的原因:D)。我想通过正确记录代码中的决定,我可以帮助自己更喜欢它。第一个解决方案很容易理解为每个理解@property含义的人,而不需要任何额外的解释。

关于@SuperSaiyan的第二条评论:你的问题是,为什么我把Group.members作为属性放在那里?该列表存储了类型(用户)实体,可能不是您认为的那样,我改变了示例。

@jwodder:下次我会使用Code Review,不知道那是件事。

(另外:我真的认为Group.members抛弃了你们中的一些人,我编辑了代码使其更加明显:组成员是将被添加到列表中的用户。

The complete code is on github,虽然没有记载,但对某些人来说可能很有意思。请记住:这都是WIP:D)

4 个答案:

答案 0 :(得分:1)

你考虑过使用元类吗?

class JsonDataWrapper(object):
    def __init__(self, json_data):
        self._data = json_data

    def get(self, name):
        return self._data[name]

class JsonDataWrapperMeta(type):
    def __init__(self, name, base, dict):
        for mbr in self.members:
            prop = property(lambda self: self.get(mbr))
            setattr(self, mbr, prop)

# You can use the metaclass inside a class block
class Group(JsonDataWrapper):
    __metaclass__ = JsonDataWrapperMeta
    members = ['id', 'description', 'name', 'description_format']

# Or more programmatically
def jsonDataFactory(name, members):
    d = {"members":members}
    return JsonDataWrapperMeta(name, (JsonDataWrapper,), d)

Course = jsonDataFactory("Course", ["id", "name", "short_name"])

答案 1 :(得分:1)

在开发这样的API时 - 其中所有成员都是只读的(意味着你不希望它们被覆盖,但可能仍然有可变数据结构作为成员),我经常考虑使用collections.namedtuple a除非我有充分的理由不这样做,否则难以接受。它速度很快,只需要最少的代码。

from collections import namedtuple as nt

Group = nt('Group', 'id name shortname users')
g = Group(**json)

简单。

如果您的json中的数据多于对象中使用的数据,请将其过滤掉:

g = Group(**{k:v for k,v in json.items() if k in Group._fields})

如果您想要缺少数据的默认值,您也可以这样做:

Group.__new__.__defaults__ = (0, 'DefaultName', 'DefN', None)
# now this works:
g = Group()
# and now this will still work even if some keys are missing; 
g = Group(**{k:v for k,v in json.items() if k in Group._fields})

使用上述设置默认值的技巧:不要将其中一个成员的默认值设置为任何可变对象,例如list,因为它将是所有可变共享对象实例:

# don't do this:
Group.__new__.__defaults__(0, 'DefaultName', 'DefN', [])
g1 = Group()
g2 = Group()
g1.users.append(user1)
g2.users # output: [user1] <-- whoops!

相反,将它全部包装在一个漂亮的工厂中,该工厂为需要它们的成员实例化一个新的list(或dict或任何用户定义的数据结构):

# jsonfactory.py

new_list = Object()

def JsonClassFactory(name, *args, defaults=None):
    '''Produces a new namedtuple class. Any members 
    intended to default to a blank list should be set to 
    the new_list object.
    '''
    cls = nt(name, *args)
    if defaults is not None:
        cls.__new__.__defaults__ = tuple(([] if d is new_list else d) for d in defaults)

现在给出一些json对象来定义你想要出现的字段:

from jsonfactory import JsonClassFactory, new_list

MyJsonClass = JsonClassFactory(MyJsonClass, *json_definition,
                               defaults=(0, 'DefaultName', 'DefN', new_list))

然后和以前一样:

obj = MyJsonClass(**json)

或者,如果有额外的数据:

obj = MyJsonClass(**{k:v for k,v in json.items() if k in MyJsonClass._fields})

如果您希望默认容器不是列表,那么这很简单 - 只需将new_list哨兵替换为您想要的任何哨兵。如果需要,您可以同时拥有多个哨兵。

如果您仍需要额外的功能,可以随时扩展MyJsonClass

class ExtJsonClass(MyJsonClass):
    __slots__ = () # optional- needed if you want the low memory benefits of namedtuple
    def __new__(cls, *args, **kwargs):
        self = super().__new__(cls, *args, **{k:v for k,v in kwargs.items()
                                              if k in cls._fields})
        return self
    def add_user(self, user):
        self.users.append(user)

上面的__new__方法可以很好地处理丢失的数据问题。所以现在你可以随时做到这一点:

obj = ExtJsonClass(**json)

简单。

答案 2 :(得分:1)

(注意:这得到了更新,我现在正在使用具有运行时类型强制执行的数据类。请参阅底部:3)

所以,这已经过去了一年,我将回答我自己的问题。我不喜欢自己回答,但是:这会将线程标记为已解决,这本身可能会帮助其他人。

另一方面,我想记录并说明为什么我选择我的解决方案而不是建议的答案。不是,为了证明我是正确的,而是强调不同的权衡。

我刚才意识到,这已经很长了,所以:

TL;博士

collections.abc包含强大的抽象,如果您可以访问它,则应该使用它们(cpython&gt; = 3.3)。 @property很好用,可以轻松添加文档并提供只读访问权限。 嵌套类看起来很奇怪,但很好地复制了深层嵌套JSON的结构。

提议的解决方案

python元类

首先,我喜欢这个概念。 我已经考虑了很多申请,证明它们有用,特别是在以下情况下:

  1. 编写可插入的API,其中元类强制正确使用派生类及其实现细节
  2. 拥有一个从meta-class派生的类的完全自动化的注册表。
  3. 另一方面,python的元类逻辑让我感到晦涩难懂(至少花了三天时间才弄明白)。虽然原则上很简单,但魔鬼却在细节之中。 所以,我决定反对它,仅仅是因为我可能会在不远的将来放弃这个项目,其他人应该能够轻松地从我离开的地方开始。

    namedtuple

    collections.namedtuple非常有效且简洁,足以将我的解决方案简化为几行而不是当前的800多行。我的IDE还可以内省生成的类的可能成员。

    缺点:namedpuple的严重性为API返回值的非常必要的文档留下了更少的空间。因此,如果使用较少的疯狂API,您可能会因此而逃脱。 将类对象嵌入到命名元组中也感觉很奇怪,但这只是个人偏好。

    我选择了什么

    所以最后,我选择坚持我的第一个原始解决方案,添加了一些小细节,如果你发现细节很有趣,你可以查看source on github

    collections.abc

    当我开始这个项目时,我的python知识几乎没有,所以我选择了我对python的了解(&#34;一切都是dict&#34;)并编写了类似的代码。例如:类似于dict的类,但下面有一个文件结构(在pathlib之前)。

    在查看python的代码时,我注意到他们如何实现和实施容器&#34; traits&#34;通过abstract base classes听起来比在python中复杂得多。

    非常基础

    以下确实非常基本,但我们会从那里建立。

    from collections import Mapping, Sequence, Sized
    
    class JsonWrapper(Sized):
        def __len__(self):
            return len(self._data)
    
        def __init__(self, json):
            self._data = json
    
        @property
        def raw(self): return self._data
    

    我能提出的最基本的课程,这将使您能够在容器上调用len。如果你真的想打扰基础字典,你也可以通过raw获得只读访问权。

    那么为什么我要从Sized继承,而不是从头开始,def __len__就是这样?

    1. 不会覆盖__len__,python解释器不会接受。我完全忘记了,但是当你导入包含该类的模块时,它就是AFAIR,所以你不会在运行时搞砸了。
    2. 虽然Sized没有提供任何mixin方法,但接下来的两个抽象确实提供了它们。我会在那里解释一下。
    3. 有了这个,我们在JSON列表和dicts中只有两个基本案例。

      解释

      所以,使用我不得不担心的API,我们并不总是确定我们得到了什么;所以我想要一种检查我是否在初始化包装类时得到一个列表的方法,主要是提前中止而不是&#34;对象没有成员&#34;在更复杂的过程中。

      从序列派生将强制覆盖__getitem____len__(已在JsonWrapper中实施)。

      class JsonListWrapper(JsonWrapper, Sequence):
          def __init__(self, json_list):
              if type(json_list) is not list:
                  raise TypeError('received type {}, expected list'.format(type(json_list)))
              super().__init__(json_list)
      
          def __getitem__(self, index):
              return self._data[index]
      
          def __iter__(self):
              raise NotImplementedError('__iter__')
      
          def get(self, index):
              try:
                  return self._data[index]
              except Exception as e:
                  print(index)
                  raise e
      

      所以你可能已经注意到,我选择不实施__iter__。 我想要一个产生类型对象的迭代器,所以我的IDE能够自动完成。举例说明:

      class CourseListResponse(JsonListWrapper):
          def __iter__(self):
              for course in self._data:
                  yield self.Course(course)
      
          class Course(JsonDictWrapper):
              pass  # for now
      

      实施Sequence的抽象方法,mixin方法__contains____reversed__indexcount都是有天赋的,所以你不要&#39 ; t必须担心可能的副作用。

      字典

      要完成拼写JSON的基本类型,这里是从Mapping派生的类:

      class JsonDictWrapper(JsonWrapper, Mapping):
          def __init__(self, json_dict):
              super().__init__(json_dict)
              if type(self._data) is not dict:
                  raise TypeError('received type {}, expected dict'.format(type(json_dict)))
      
          def __iter__(self):
              return iter(self._data)
      
          def __getitem__(self, key):
              return self._data[key]
      
          __marker = object()
      
          def get(self, key, default=__marker):
              try:
                  return self._data[key]
              except KeyError:
                  if default is self.__marker:
                      raise
                  else:
                      return default
      

      映射仅强制执行__iter____getitem____len__。 为避免混淆:还有MutableMapping强制执行写作方法。但这里既不需要也不想要。

      通过抽象方法,python提供了mixins __contains__keysitemsvaluesget,{{1} }和__eq__基于它们。

      我不确定为什么我选择覆盖__ne__ mixin,我可能会在回复给我时更新帖子。 get用作后备来检测是否未设置__marker关键字。如果有人决定致电default,你就无法发现其他情况。

      所以要拿起上一个例子:

      get(*args, default=None)

      这些属性提供对成员的只读访问权限,可以像函数定义一样进行记录。 Altough冗长,对于基本访问器,您可以在编辑器中轻松定义模板,因此编写起来不那么繁琐。

      属性还允许从幻数和可选的JSON返回值中抽象,以提供默认值而不是在任何地方保护class CourseListResponse(JsonListWrapper): # [...] class Course(JsonDictWrapper): # Jn is just a class that contains the keys for JSON, so I only mistype once. @property def id(self): return self[Jn.id] @property def short_name(self): return self[Jn.short_name] @property def full_name(self): return self[Jn.full_name] @property def enrolled_user_count(self): return self[Jn.enrolled_user_count] # [...] you get the idea

      KeyError

      类嵌套

      在其他人中嵌套课程似乎有点奇怪。 我选择这样做,因为API对具有不同属性的各种对象使用相同的名称,具体取决于您调用的远程函数。

      另一个好处是:新人可以很容易地理解返回的JSON的结构。

      end of the file包含嵌套类的各种别名,以便于从模块外部访问。

      添加逻辑

      现在我们已经封装了大部分返回值,我希望有更多与数据相关的逻辑,以增加一些便利性。 似乎有必要将一些数据合并到一个更全面的树中,该树包含通过几个API调用收集的所有数据:

      1. 获得所有&#34;作业&#34;。每个作业都包含许多提交,因此:
      2. for(在assigmnents中分配)获得所有&#34;提交&#34;
      3. 将提交内容合并到相应的作业中。
      4. 现在获得提交的成绩,依此类推......
      5. 我选择单独实施它们,所以我只是从&#34; dumb&#34;继承而来。访问者(full source):

        所以在this class

                @property
                def isdir(self): return 1 == self[Jn.is_dir]
        
                @property
                def time_created(self): return self.get(Jn.time_created, 0)
        
                @property
                def file_size(self): return self.get(Jn.file_size, -1)
        
                @property
                def author(self): return self.get(Jn.author, "")
        
                @property
                def license(self): return self.get(Jn.license, "")
        

        这些属性进行合并

        class Assignment(MoodleAssignment):
            def __init__(self, data, course=None):
                super().__init__(data)
                self.course = course
                self._submissions = {}  # accessed via submission.id
                self._grades = {}  # are accessed via user_id
        

        这些实现了一些可以从数据中抽象出来的逻辑。

            @property
            def submissions(self): return self._submissions
        
            @submissions.setter
            def submissions(self, data):
                if data is None:
                    self.submissions = {}
                    return
                for submission in data:
                    sub = Submission(submission, assignment=self)
                    if sub.has_content:
                        self.submissions[sub.id] = sub
        
            @property
            def grades(self):
                return self._grades
        
            @grades.setter
            def grades(self, data):
                if data is None:
                    self.grades = {}
                    return
                grades = [Grade(g) for g in data]
                for g in grades:
                    self.grades[g.user_id] = g
        

        虽然制定者掩盖了争吵,但他们很乐意写作和使用:所以这只是一种权衡。

        警告:逻辑实现并不是我想要的那样,在它不应该的地方有很多相互依赖。它是从我的成长中不了解python以获得正确的抽象并完成任务,所以我可以用自己的方式完成实际的工作。 现在我知道了,可以做些什么:我看看那些意大利面,好吧......你知道这种感觉。

        结论

        将JSON封装到类中证明对我和项目的结构非常有用,我对此非常满意。 项目的其余部分很好并且有效,尽管有些部分很糟糕:D 谢谢大家的反馈意见,我会四处寻找问题和评论。

        更新:2019-05-02

        正如@RickTeachey在评论中指出的那样,这里也可以使用pythons dataclasses(DCs)。 我忘了在这里发布更新,因为我已经在did that前一段时间了,并使用pythons @property def is_due(self): now = datetime.now() return now > self.due_date @property def due_date(self): return datetime.fromtimestamp(super().due_date) 功能扩展了它:D

        原因:我厌倦了手动检查我抽象的API的文档是否正确或者我的实现是否错误。 使用typing,我可以检查响应是否符合我的架构;现在我能够更快地找到外部API的变化,因为在实例化的运行时期间会检查这些假设。

        成功完成dataclasses.fields后,DC会提供__post_init__(self)挂钩来进行一些后处理。蟒蛇&#39;类型提示仅用于提供静态检查器的提示,我构建了一个在初始化后阶段在数据类上强制执行类型的小系统。

        这是BaseDC,所有其他DC都继承(缩写)

        __init__

        Fields有一个允许存储任意信息的附加属性,我用它来存储转换响应值的函数;但后来会更多。

        基本响应包装器如下所示:

        import dataclasses as dc
        @dataclass
        class BaseDC:
            def _typecheck(self):
                for field in dc.fields(self):
                    expected = field.type
                    f = getattr(self, field.name)
                    actual = type(f)
                    if expected is list or expected is dict:
                        log.warning(f'untyped list or dict in {self.__class__.__qualname__}: {field.name}')
                    if expected is actual:
                        continue
                    if is_generic(expected):
                        return self._typecheck_generic(expected, actual)
                        # Subscripted generics cannot be used with class and instance checks
                    if issubclass(actual, expected):
                        continue
                    print(f'mismatch {field.name}: should be: {expected}, but is {actual}')
                    print(f'offending value: {f}')
        
            def __post_init__(self):
                for field in dc.fields(self):
                    castfunc = field.metadata.get('castfunc', False)
                    if castfunc:
                        attr = getattr(self, field.name)
                        new = castfunc(attr)
                        setattr(self, field.name, new)
                if DEBUG:
                    self._typecheck()
        

        只是列表的响应在开始时给我带来麻烦,因为我无法使用普通@dataclass class DCcore_enrol_get_users_courses(BaseDC): id: int # id of course shortname: str # short name of course fullname: str # long name of course enrolledusercount: int # Number of enrolled users in this course idnumber: str # id number of course visible: int # 1 means visible, 0 means hidden course summary: Optional[str] = None # summary summaryformat: Optional[int] = None # summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN) format: Optional[str] = None # course format: weeks, topics, social, site showgrades: Optional[int] = None # true if grades are shown, otherwise false lang: Optional[str] = None # forced course language enablecompletion: Optional[int] = None # true if completion is enabled, otherwise false category: Optional[int] = None # course category id progress: Optional[float] = None # Progress percentage startdate: Optional[int] = None # Timestamp when the course start enddate: Optional[int] = None # Timestamp when the course end def __str__(self): return f'{self.fullname[0:39]:40} id:{self.id:5d} short: {self.shortname}' core_enrol_get_users_courses = destructuring_list_cast(DCcore_enrol_get_users_courses) 强制对它们进行类型检查。 这就是List[DCcore_enrol_get_users_courses]为我解决这个问题的地方,这个问题更为复杂。我们正在进入更高阶的功能区域:

        destructuring_list_cast

        这需要一个Callable接受一个dict并返回一个类型T = typing.TypeVar('T') def destructuring_list_cast(cls: typing.Callable[[dict], T]) -> typing.Callable[[list], T]: def cast(data: list) -> List[T]: if data is None: return [] if not isinstance(data, list): raise SystemExit(f'listcast expects a list, you sent: {type(data)}') try: return [cls(**entry) for entry in data] except TypeError as err: # here is more code that explains errors raise SystemExit(f'listcast for class {cls} failed:\n{err}') return cast 的类实例,这是你对构造函数或工厂的期望。 它返回一个Callable,它将接受一个列表,在这里T。 当您致电cast时,return [cls(**entry) for entry in data]通过构建数据类列表来完成此处的所有工作。 (投掷core_enrol_get_users_courses(response.json())并不好,但是在上层处理了,所以它适用于我;我希望它能够快速而快速地失败。)

        它的另一个用例是定义嵌套字段,然后深层嵌套响应:还记得SystemExit中的field.metadata.get('castfunc', False)吗?这两个快捷方式的来源是:

        BaseDC

        这些用于这样的嵌套案例(见下):

        # destructured_cast_field
        def dcf(cls):
            return dc.field(metadata={'castfunc': destructuring_list_cast(cls)})
        
        
        def optional_dcf(cls):
            return dc.field(metadata={'castfunc': destructuring_list_cast(cls)}, default_factory=list)
        

答案 3 :(得分:0)

我自己是蟒蛇的新手,如果我听起来很天真,请原谅。其中一个解决方案可能是使用__dict__,如下文所述:

https://www.safaribooksonline.com/library/view/python-cookbook-3rd/9781449357337/ch06s02.html

当然,如果类中的对象低于其他类并且需要序列化或反序列化,则此解决方案将产生问题。我很想听听专家对此解决方案和不同限制的意见。

jsonpickle 的任何反馈。

<强>更新

我刚看到你对序列化的反对意见以及你不喜欢它,因为一切都是运行时。了解。非常感谢。

下面是我编写的代码来解决这个问题。有点伸展,但运作良好,我不必每次都添加get / set !!!

import json

class JSONObject:
    exp_props = {"id": "", "title": "Default"}

    def __init__(self, d):
        self.__dict__ = d
        for key in [x for x in JSONObject.exp_props if x not in self.__dict__]:
            setattr(self, key, JSONObject.exp_props[key]) 

    @staticmethod
    def fromJSON(s):
        return json.loads(s, object_hook=JSONObject)

    def toJSON(self):
        return json.dumps(self.__dict__, indent=4)


s = '{"name": "ACME", "shares": 50, "price": 490.1}'
anObj = JSONObject.fromJSON(s)

print("Name - {}".format(anObj.name))
print("Shares - {}".format(anObj.shares))
print("Price - {}".format(anObj.price))
print("Title - {}".format(anObj.title))

sAfter = anObj.toJSON()

print("Type of dumps is {}".format(type(sAfter)))
print(sAfter)

以下结果

Name - ACME
Shares - 50
Price - 490.1
Title - Default
Type of dumps is <type 'str'>
{
    "price": 490.1, 
    "title": "Default", 
    "name": "ACME", 
    "shares": 50, 
    "id": ""
}