序言:我正在针对提供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'
这可能会更短一点,如果能让代码变得更好,那就不知道了。
取消资格的解决方案(恕我直言):
__{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)
答案 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)
所以,这已经过去了一年,我将回答我自己的问题。我不喜欢自己回答,但是:这会将线程标记为已解决,这本身可能会帮助其他人。
另一方面,我想记录并说明为什么我选择我的解决方案而不是建议的答案。不是,为了证明我是正确的,而是强调不同的权衡。
我刚才意识到,这已经很长了,所以:
collections.abc
包含强大的抽象,如果您可以访问它,则应该使用它们(cpython&gt; = 3.3)。
@property
很好用,可以轻松添加文档并提供只读访问权限。
嵌套类看起来很奇怪,但很好地复制了深层嵌套JSON的结构。
首先,我喜欢这个概念。 我已经考虑了很多申请,证明它们有用,特别是在以下情况下:
另一方面,python的元类逻辑让我感到晦涩难懂(至少花了三天时间才弄明白)。虽然原则上很简单,但魔鬼却在细节之中。 所以,我决定反对它,仅仅是因为我可能会在不远的将来放弃这个项目,其他人应该能够轻松地从我离开的地方开始。
collections.namedtuple
非常有效且简洁,足以将我的解决方案简化为几行而不是当前的800多行。我的IDE还可以内省生成的类的可能成员。
缺点:namedpuple的严重性为API返回值的非常必要的文档留下了更少的空间。因此,如果使用较少的疯狂API,您可能会因此而逃脱。 将类对象嵌入到命名元组中也感觉很奇怪,但这只是个人偏好。
所以最后,我选择坚持我的第一个原始解决方案,添加了一些小细节,如果你发现细节很有趣,你可以查看source on github。
当我开始这个项目时,我的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__
就是这样?
__len__
,python解释器不会接受。我完全忘记了,但是当你导入包含该类的模块时,它就是AFAIR,所以你不会在运行时搞砸了。Sized
没有提供任何mixin方法,但接下来的两个抽象确实提供了它们。我会在那里解释一下。有了这个,我们在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__
,index
和count
都是有天赋的,所以你不要&#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__
,keys
,items
,values
,get
,{{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调用收集的所有数据:
我选择单独实施它们,所以我只是从&#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 谢谢大家的反馈意见,我会四处寻找问题和评论。
正如@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": ""
}