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

ein*_*ger 6 python json api-design class wrapper

序言:我正在针对提供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)
Run Code Online (Sandbox Code Playgroud)

第二种解决方案:使用lambda来缩短语法.在工作和做空时,它看起来不太正确(参见下面的编辑1).

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')
Run Code Online (Sandbox Code Playgroud)

(命名此函数'json'不是问题,因为我不在那里导入json.)

我有一个可能的第三个解决方案,我不能完全包围:覆盖内置属性,所以我可以定义一个装饰器包装返回的字段名称进行查找:

@json  # just like a property fget
def short_name(self): return 'short_name'
Run Code Online (Sandbox Code Playgroud)

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

不合格的解决方案(恕我直言):

  • 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把你们中的一些人扔了,我编辑了代码使其更加明显:组成员是将被添加到列表中的用户.

完整的代码在github上,虽然没有文档,但对某人来说可能很有趣.请记住:这都是WIP:D)

ein*_*ger 2

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

\n\n

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

\n\n

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

\n\n

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

\n\n

太长了;博士

\n\n

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

\n\n

建议的解决方案

\n\n

python 元类

\n\n

首先:我喜欢这个概念。\n我考虑过许多​​应用程序,它们被证明是有用的,特别是在以下情况下:

\n\n
    \n
  1. 编写可插入 API,其中元类强制正确使用派生类及其实现细节
  2. \n
  3. 拥有从元类派生的完全自动化的类注册表。
  4. \n
\n\n

另一方面,Python 的元类逻辑让我感到晦涩难懂(我至少花了三天时间才弄清楚)。虽然原则很简单,但细节决定成败。\n因此,我决定反对它,只是因为我可能会在不久的将来放弃该项目,而其他人应该能够轻松地从我离开的地方继续。

\n\n

命名元组

\n\n

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

\n\n

缺点:namedtuple 的简洁性为 API 返回值的非常必要的文档留下了更少的空间。因此,使用不太疯狂的 API,您可能会侥幸逃脱。\n将类对象嵌套到命名元组中也感觉很奇怪,但这只是个人喜好。

\n\n

我带着什么去的

\n\n

所以最后,我选择坚持我的第一个原始解决方案,添加了一些小细节,如果你觉得细节有趣,你可以看看github 上的源代码

\n\n

集合.abc

\n\n

当我开始这个项目时,我对 Python 的了解几乎为零,所以我用我对 Python 的了解(“一切都是字典”)并编写了这样的代码。例如:像字典一样工作的类,但下面有一个文件结构(这是之前的pathlib)。

\n\n

在查看 python 的代码时,我注意到它们如何通过抽象基类实现和强制容器“特征” ,这听起来比 python 中的实际情况要复杂得多。

\n\n

最基本的

\n\n

以下内容确实非常基础,但我们将从这里开始构建。

\n\n
from collections import Mapping, Sequence, Sized\n\nclass JsonWrapper(Sized):\n    def __len__(self):\n        return len(self._data)\n\n    def __init__(self, json):\n        self._data = json\n\n    @property\n    def raw(self): return self._data\n
Run Code Online (Sandbox Code Playgroud)\n\n

我能想到的最基本的类,这将使您能够调用len容器。raw如果您确实想使用底层字典,您也可以通过它获得只读访问权限。

\n\n

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

\n\n
    \n
  1. 不重写__len__将不会被 python 解释器接受。我忘记了确切的时间,但事实上,是在您导入包含该类的模块时,所以您不会在运行时被搞砸。
  2. \n
  3. 虽然Sized不提供任何 mixin 方法,但接下来的两个抽象确实提供了它们。我会在那里解释。
  4. \n
\n\n

有了这些,我们只得到了 JSON 列表和字典中的两个更基本的情况。

\n\n

列表

\n\n

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

\n\n

从 Sequence 派生将强制覆盖__getitem____len__(已在 中实现JsonWrapper)。

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

所以您可能已经注意到,我选择不实现__iter__。\n我想要一个生成类型化对象的迭代器,以便我的 IDE 能够自动完成。为了显示:

\n\n
class CourseListResponse(JsonListWrapper):\n    def __iter__(self):\n        for course in self._data:\n            yield self.Course(course)\n\n    class Course(JsonDictWrapper):\n        pass  # for now\n
Run Code Online (Sandbox Code Playgroud)\n\n

实现 的抽象方法Sequence、 mixin 方法__contains____reversed__indexcount是为您提供的,因此您不必担心可能的副作用。

\n\n

词典

\n\n

为了完成处理 JSON 的基本类型,这里是派生自的类Mapping

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

映射仅强制执行__iter__,__getitem____len__. \n为了避免混淆:还有一个MutableMapping强制执行书写方法的方法。但这在这里既不需要也不想要。

\n\n

随着抽象方法的出现,Python 提供了 mixin __contains__keysitemsvaluesget__eq__、 和__ne__基于它们。

\n\n

我不确定为什么我选择覆盖 mixin get,当它返回给我时我可能会更新帖子。\n作为检测关键字是否未设置的__marker后备。default如果有人决定打电话给get(*args, default=None)您,否则您将无法检测到。

\n\n

继续前面的例子:

\n\n
class CourseListResponse(JsonListWrapper):\n    # [...]    \n    class Course(JsonDictWrapper):\n        # Jn is just a class that contains the keys for JSON, so I only mistype once.\n        @property\n        def id(self): return self[Jn.id]\n\n        @property\n        def short_name(self): return self[Jn.short_name]\n\n        @property\n        def full_name(self): return self[Jn.full_name]\n\n        @property\n        def enrolled_user_count(self): return self[Jn.enrolled_user_count]\n        # [...] you get the idea\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

属性还允许从幻数和可选的 JSON 返回值中进行抽象,以提供默认值,而不是在KeyError任何地方进行保护:

\n\n
        @property\n        def isdir(self): return 1 == self[Jn.is_dir]\n\n        @property\n        def time_created(self): return self.get(Jn.time_created, 0)\n\n        @property\n        def file_size(self): return self.get(Jn.file_size, -1)\n\n        @property\n        def author(self): return self.get(Jn.author, "")\n\n        @property\n        def license(self): return self.get(Jn.license, "")\n
Run Code Online (Sandbox Code Playgroud)\n\n

类嵌套

\n\n

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

\n\n

另一个好处是:新人可以轻松理解返回的 JSON 的结构。

\n\n

文件末尾包含嵌套类的各种别名,以便于从模块外部访问。

\n\n

添加逻辑

\n\n

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

\n\n
    \n
  1. 获得所有“作业”。每个作业包含许多提交,因此:
  2. \n
  3. for(分配中的分配)获取所有“提交”
  4. \n
  5. 将提交内容合并到各自的作业中。
  6. \n
  7. 现在获取提交的成绩,等等......
  8. \n
\n\n

我选择单独实现它们,所以我只是从“哑”访问器继承(完整源代码):

\n\n

那么在这堂课上

\n\n
class Assignment(MoodleAssignment):\n    def __init__(self, data, course=None):\n        super().__init__(data)\n        self.course = course\n        self._submissions = {}  # accessed via submission.id\n        self._grades = {}  # are accessed via user_id\n
Run Code Online (Sandbox Code Playgroud)\n\n

这些属性进行合并

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

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

\n\n
    @property\n    def is_due(self):\n        now = datetime.now()\n        return now > self.due_date\n\n    @property\n    def due_date(self): return datetime.fromtimestamp(super().due_date)\n
Run Code Online (Sandbox Code Playgroud)\n\n

虽然设置器掩盖了争论,但它们很容易编写和使用:所以这只是一个权衡。

\n\n

警告:逻辑实现并不完全是我想要的,有很多不应该的相互依赖。它源于我对 Python 的了解不够,无法正确地进行抽象并完成工作,因此我可以摆脱繁琐的工作来完成实际工作。\n现在我知道了,本来可以做什么:我看看一些意大利面条,好吧 \xe2\x80\xa6 你知道那种感觉。

\n\n

结论

\n\n

事实证明,将 JSON 封装到类中对我和项目的结构非常有用,我对此非常满意。\n该项目的其余部分很好并且可以工作,尽管有些部分很糟糕:D\n谢谢大家对于反馈,我将随时提出问题和评论。

\n\n

更新:2019-05-02

\n\n

正如 @RickTeachey 在评论中指出的那样,Python数据类(DC)也可以在这里使用。\n我忘了在这里进行更新,因为我前段时间已经这样做了typing,并使用 python功能扩展了它:D

\n\n

原因:我越来越厌倦手动检查我从中抽象的 API 文档是否正确,或者我的实现是否错误。\n有了dataclasses.fields我\n能够检查响应是否符合我的架构;现在我能够更快地找到外部 API 中的更改,因为在实例化的运行时期间会检查假设。

\n\n

DC 提供了一个__post_init__(self)钩子,可以在成功完成后进行一些后处理__init__。Python 的类型提示仅用于为静态检查器提供提示,我构建了一个小系统,该系统在初始化后阶段强制执行数据类上的类型。

\n\n

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

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

字段有一个附加属性,允许存储任意信息,我用它来存储转换响应值的函数;但稍后会详细介绍。

\n\n

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

\n\n
@dataclass\nclass DCcore_enrol_get_users_courses(BaseDC):\n    id: int  # id of course\n    shortname: str  # short name of course\n    fullname: str  # long name of course\n    enrolledusercount: int  # Number of enrolled users in this course\n    idnumber: str  # id number of course\n    visible: int  # 1 means visible, 0 means hidden course\n    summary: Optional[str] = None  # summary\n    summaryformat: Optional[int] = None  # summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN)\n    format: Optional[str] = None  # course format: weeks, topics, social, site\n    showgrades: Optional[int] = None  # true if grades are shown, otherwise false\n    lang: Optional[str] = None  # forced course language\n    enablecompletion: Optional[int] = None  # true if completion is enabled, otherwise false\n    category: Optional[int] = None  # course category id\n    progress: Optional[float] = None  # Progress percentage\n    startdate: Optional[int] = None  # Timestamp when the course start\n    enddate: Optional[int] = None  # Timestamp when the course end\n\n    def __str__(self): return f\'{self.fullname[0:39]:40} id:{self.id:5d} short: {self.shortname}\'\n\n\ncore_enrol_get_users_courses = destructuring_list_cast(DCcore_enrol_get_users_courses)\n
Run Code Online (Sandbox Code Playgroud)\n\n

只是列表的响应一开始就给我带来了麻烦,因为我无法使用普通的 来强制对它们进行类型检查List[DCcore_enrol_get_users_courses]。\n这就是为我解决这个问题的地方destructuring_list_cast,这有点复杂。我们正在进入高阶函数领域:

\n\n
T = typing.TypeVar(\'T\')\ndef destructuring_list_cast(cls: typing.Callable[[dict], T]) -> typing.Callable[[list], T]:\n    def cast(data: list) -> List[T]:\n        if data is None:\n            return []\n\n        if not isinstance(data, list):\n            raise SystemExit(f\'listcast expects a list, you sent: {type(data)}\')\n        try:\n            return [cls(**entry) for entry in data]\n        except TypeError as err:\n            # here is more code that explains errors\n            raise SystemExit(f\'listcast for class {cls} failed:\\n{err}\')\n\n    return cast\n
Run Code Online (Sandbox Code Playgroud)\n\n

这需要一个 Callable 接受一个 dict 并返回一个 类型的类实例T,这是您期望从构造函数或工厂获得的东西。\n它返回一个 Callable 接受一个列表,这里是 。cast\ nreturn [cls(**entry) for entry in data]在这里完成所有工作,通过构造一个数据类列表,当你调用core_enrol_get_users_courses(response.json()).\n(抛出SystemExit并不好,但那是在上层处理的,所以它对我有用;我希望它失败并且快速地。)

\n\n

它的另一个用例是定义嵌套字段,然后响应是深度嵌套的:还记得field.metadata.get(\'castfunc\', False)BaseDC?这就是这两个快捷方式的用武之地:

\n\n
# destructured_cast_field\ndef dcf(cls):\n    return dc.field(metadata={\'castfunc\': destructuring_list_cast(cls)})\n\n\ndef optional_dcf(cls):\n    return dc.field(metadata={\'castfunc\': destructuring_list_cast(cls)}, default_factory=list)\n
Run Code Online (Sandbox Code Playgroud)\n\n

这些用于像这样的嵌套情况(见底部):

\n\n
@dataclass\nclass core_files_get_files(BaseDC):\n    @dataclass\n    class parent(BaseDC):\n        contextid: int\n        # abbrev ...\n\n    @dataclass\n    class file(BaseDC):\n        contextid: int\n        component: str\n        timecreated: Optional[int] = None  # Time created\n        # abbrev ...\n\n    parents: List[parent] = dcf(parent)\n    files: Optional[List[file]] = optional_dcf(file)\n
Run Code Online (Sandbox Code Playgroud)\n

  • 投赞成票,因为这显然是一种学习的劳动,我为你鼓掌。一项更正:在尝试实例化该类之前,不会发生 ABC 错误(因为未提供抽象方法)。另外,我建议研究[新的`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),它[提供了更好的API](https ://stackoverflow.com/questions/44320382/subclassing-python-namedtuple/44320510#44320510)用于命名元组(包括编写允许文档的代码的能力)。 (2认同)