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)
这可能会更短一些,如果能让代码变得更好,那就不知道了.
不合格的解决方案(恕我直言):
__{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)
(注意:这得到了更新,我现在正在使用具有运行时类型强制的数据类。请参见底部:3)
\n\n所以,一年过去了,我要回答我自己的问题。我不太喜欢自己回答这个问题,但是:这会将线程标记为已解决,这本身可能会帮助其他人。
\n\n另一方面,我想记录并说明为什么我选择我的解决方案而不是建议的答案。不是为了证明我是对的,而是为了强调不同的权衡。
\n\n我刚刚意识到,这已经很长了,所以:
\n\ncollections.abc包含强大的抽象,如果您有权访问它(cpython >= 3.3),则应该使用它们。\n@property很好用,可以轻松添加文档并提供只读访问权限。\n嵌套类看起来很奇怪,但复制了深层的结构嵌套 JSON 就可以了。
首先:我喜欢这个概念。\n我考虑过许多应用程序,它们被证明是有用的,特别是在以下情况下:
\n\n另一方面,Python 的元类逻辑让我感到晦涩难懂(我至少花了三天时间才弄清楚)。虽然原则很简单,但细节决定成败。\n因此,我决定反对它,只是因为我可能会在不久的将来放弃该项目,而其他人应该能够轻松地从我离开的地方继续。
\n\ncollections.namedtuple非常高效且简洁,足以将我的解决方案精简为几行,而不是当前的 800 多行。我的 IDE 还能够内省生成的类的可能成员。
缺点:namedtuple 的简洁性为 API 返回值的非常必要的文档留下了更少的空间。因此,使用不太疯狂的 API,您可能会侥幸逃脱。\n将类对象嵌套到命名元组中也感觉很奇怪,但这只是个人喜好。
\n\n所以最后,我选择坚持我的第一个原始解决方案,添加了一些小细节,如果你觉得细节有趣,你可以看看github 上的源代码。
\n\n当我开始这个项目时,我对 Python 的了解几乎为零,所以我用我对 Python 的了解(“一切都是字典”)并编写了这样的代码。例如:像字典一样工作的类,但下面有一个文件结构(这是之前的pathlib)。
在查看 python 的代码时,我注意到它们如何通过抽象基类实现和强制容器“特征” ,这听起来比 python 中的实际情况要复杂得多。
\n\n以下内容确实非常基础,但我们将从这里开始构建。
\n\nfrom 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\nRun Code Online (Sandbox Code Playgroud)\n\n我能想到的最基本的类,这将使您能够调用len容器。raw如果您确实想使用底层字典,您也可以通过它获得只读访问权限。
那么为什么我要继承Sized而不是从头开始就 def __len__这样呢?
__len__将不会被 python 解释器接受。我忘记了确切的时间,但事实上,是在您导入包含该类的模块时,所以您不会在运行时被搞砸。Sized不提供任何 mixin 方法,但接下来的两个抽象确实提供了它们。我会在那里解释。有了这些,我们只得到了 JSON 列表和字典中的两个更基本的情况。
\n\n因此,对于我不得不担心的 API,我们并不总是确定我们得到了什么;所以我想要一种方法来检查在初始化包装类时是否获得列表,主要是为了在更复杂的过程中提前中止而不是“对象没有成员”。
\n\n从 Sequence 派生将强制覆盖__getitem__和__len__(已在 中实现JsonWrapper)。
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\nRun Code Online (Sandbox Code Playgroud)\n\n所以您可能已经注意到,我选择不实现__iter__。\n我想要一个生成类型化对象的迭代器,以便我的 IDE 能够自动完成。为了显示:
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\nRun Code Online (Sandbox Code Playgroud)\n\n实现 的抽象方法Sequence、 mixin 方法__contains__、__reversed__、index和count是为您提供的,因此您不必担心可能的副作用。
为了完成处理 JSON 的基本类型,这里是派生自的类Mapping:
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\nRun Code Online (Sandbox Code Playgroud)\n\n映射仅强制执行__iter__,__getitem__和__len__. \n为了避免混淆:还有一个MutableMapping强制执行书写方法的方法。但这在这里既不需要也不想要。
随着抽象方法的出现,Python 提供了 mixin __contains__、keys、items、values、get、__eq__、 和__ne__基于它们。
我不确定为什么我选择覆盖 mixin get,当它返回给我时我可能会更新帖子。\n作为检测关键字是否未设置的__marker后备。default如果有人决定打电话给get(*args, default=None)您,否则您将无法检测到。
继续前面的例子:
\n\nclass 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\nRun Code Online (Sandbox Code Playgroud)\n\n这些属性提供对成员的只读访问,并且可以像函数定义一样进行记录。\n虽然很冗长,但对于基本访问器,您可以轻松地在编辑器中定义模板,因此编写起来不那么繁琐。
\n\n属性还允许从幻数和可选的 JSON 返回值中进行抽象,以提供默认值,而不是在KeyError任何地方进行保护:
@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, "")\nRun Code Online (Sandbox Code Playgroud)\n\n在其他类中嵌套类似乎有点奇怪。\n我选择这样做,因为 API 对具有不同属性的各种对象使用相同的名称,具体取决于您调用的远程函数。
\n\n另一个好处是:新人可以轻松理解返回的 JSON 的结构。
\n\n文件末尾包含嵌套类的各种别名,以便于从模块外部访问。
\n\n现在我们已经封装了大部分返回值,我希望有更多与数据相关的逻辑,以增加一些便利。\n似乎还有必要将一些数据合并到一个更全面的树中,其中包含收集到的所有数据通过几个API调用:
\n\n我选择单独实现它们,所以我只是从“哑”访问器继承(完整源代码):
\n\n那么在这堂课上
\n\nclass 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\nRun 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\nRun 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)\nRun Code Online (Sandbox Code Playgroud)\n\n虽然设置器掩盖了争论,但它们很容易编写和使用:所以这只是一个权衡。
\n\n警告:逻辑实现并不完全是我想要的,有很多不应该的相互依赖。它源于我对 Python 的了解不够,无法正确地进行抽象并完成工作,因此我可以摆脱繁琐的工作来完成实际工作。\n现在我知道了,本来可以做什么:我看看一些意大利面条,好吧 \xe2\x80\xa6 你知道那种感觉。
\n\n事实证明,将 JSON 封装到类中对我和项目的结构非常有用,我对此非常满意。\n该项目的其余部分很好并且可以工作,尽管有些部分很糟糕:D\n谢谢大家对于反馈,我将随时提出问题和评论。
\n\n正如 @RickTeachey 在评论中指出的那样,Python数据类(DC)也可以在这里使用。\n我忘了在这里进行更新,因为我前段时间已经这样做了typing,并使用 python功能扩展了它:D
原因:我越来越厌倦手动检查我从中抽象的 API 文档是否正确,或者我的实现是否错误。\n有了dataclasses.fields我\n能够检查响应是否符合我的架构;现在我能够更快地找到外部 API 中的更改,因为在实例化的运行时期间会检查假设。
DC 提供了一个__post_init__(self)钩子,可以在成功完成后进行一些后处理__init__。Python 的类型提示仅用于为静态检查器提供提示,我构建了一个小系统,该系统在初始化后阶段强制执行数据类上的类型。
这里是BaseDC,所有其他DC都继承自它(缩写)
\n\nimport 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()\nRun 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)\nRun Code Online (Sandbox Code Playgroud)\n\n只是列表的响应一开始就给我带来了麻烦,因为我无法使用普通的 来强制对它们进行类型检查List[DCcore_enrol_get_users_courses]。\n这就是为我解决这个问题的地方destructuring_list_cast,这有点复杂。我们正在进入高阶函数领域:
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\nRun 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并不好,但那是在上层处理的,所以它对我有用;我希望它失败并且快速地。)
它的另一个用例是定义嵌套字段,然后响应是深度嵌套的:还记得field.metadata.get(\'castfunc\', False)吗BaseDC?这就是这两个快捷方式的用武之地:
# 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)\nRun 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)\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
2295 次 |
| 最近记录: |