Python2中自定义JSONEncoder传参异常排查及Python3差异分析
你遇到的问题核心是Python 2中json.JSONEncoder的__init__方法签名和你的自定义编码器参数冲突了。
为什么type会被赋值给skipkeys?
在Python 2里,json.JSONEncoder.__init__的签名是这样的:
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, encoding='utf-8', default=None):
它的第一个位置参数(除了self)是skipkeys。而你定义的MyEncoder.__init__把type作为第一个参数,当你调用json.dump(data, fp, type, cls=MyEncoder)时,你传入的type值会被当作位置参数传递给MyEncoder的__init__,也就是self之后的第一个参数——而这个位置恰好对应父类的skipkeys参数。
所以当你调用super(MyEncoder, self).__init__(**kwargs)时,父类会把skipkeys设置为你传入的type值,同时你虽然给self.type赋值了,但父类的初始化逻辑已经把这个值关联到了skipkeys属性上,这就是你看到type跑到kwargs['skipkeys']里的本质原因。
怎么解决这个问题?
你需要避免自定义参数和父类的位置参数冲突,推荐两种做法:
方法1:将自定义参数改为关键字参数传递(推荐)
修改你的编码器,让type只能通过关键字参数传入,同时在__init__里先提取这个参数,再把剩余的kwargs传给父类:
import json from datetime import datetime class MyEncoder(json.JSONEncoder): def __init__(self, **kwargs): # 从kwargs中取出type参数,可根据需求设置默认值 self.type = kwargs.pop('type', None) super(MyEncoder, self).__init__(**kwargs) def default(self, obj): if isinstance(obj, datetime): # 这里就可以用self.type来做分支处理了 if self.type == 'timestamp': return obj.timestamp() elif self.type == 'iso': return obj.isoformat() # 其他自定义处理逻辑 return super(MyEncoder, self).default(obj)
调用时必须用关键字参数传type:
json.dump(data, fp, cls=MyEncoder, type='timestamp')
方法2:调整参数顺序,让自定义参数在父类所有参数之后
如果你坚持想用位置参数(不推荐,可读性差),可以把type放在父类所有参数的后面:
class MyEncoder(json.JSONEncoder): def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, encoding='utf-8', default=None, type=None): super(MyEncoder, self).__init__(skipkeys, ensure_ascii, check_circular, allow_nan, sort_keys, indent, separators, encoding, default) self.type = type # default方法同上
调用时要确保type在所有父类参数之后,不过这种方式太繁琐,维护性差,不推荐使用。
Python 3中的差异
Python 3对json.JSONEncoder.__init__的签名做了优化,引入了强制关键字参数:
def __init__(self, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None):
注意参数列表开头的*,这表示self之后的所有参数都必须是关键字参数。
这意味着如果你在Python 3里还是写__init__(self, type, **kwargs),然后用json.dump(data, fp, type, cls=MyEncoder)这种位置参数的方式传type,会直接抛出TypeError,因为父类的__init__不接受位置参数(除了self)。
所以在Python 3里,必须用关键字参数传递自定义参数,也就是上面的方法1是唯一合理的选择,这也从根源上避免了参数冲突的问题,代码逻辑更清晰。
内容的提问来源于stack exchange,提问作者Paolo Benvenuto

