Python经常做的一件事是在Python数据类型和JSON数据类型之间进行转换。但是有一个明显的问题。JSON作为一种数据交换格式,有固定的数据类型,而Python作为一种编程语言,除了内置的数据类型外,还可以编写自定义的数据类型。比如你一定遇到过类似的问题:>>>importjson>>>importdecimal>>>>>>data={'key1':'string','key2':10,'key3':decimal.Decimal('1.45')}>>>json.dumps(data)Traceback(最近调用最后一次):文件“”,第1行,在json.dumps(data)文件“/usr/lib/python3.6/json/__init__.py”,第231行,在转储中return_default_encoder.encode(obj)文件“/usr/lib/python3.6/json/encoder.py”,第199行,在encodechunks=self.iterencode(o,_one_shot=True)文件“/usr/lib/python3.6/json/encoder.py”,第257行,在iterencode返回_iterencode(o,0)文件“/usr/lib/python3.6/json/encoder.py",line180,indefaulto.__class__.__name__)TypeError:Objectoftype'Decimal'isnotJSONserializable那么问题来了,如何将各种Python数据类型转换成JSON数据类型。一个非常unpythonic的做法是先转成一个可以直接转成JSON数据类型的值,然后dump出来。这样很直接也很暴力,但是面对各种花哨的数据类型却很无能为力。谷歌是解决问题的重要途径之一。搜索后会发现在dumps的encode阶段可以转换数据。所以你一定做到了,并且完美地解决了问题。>>>类DecimalEncoder(json.JSONEncoder):...defdefault(self,obj):...ifisinstance(obj,decimal.Decimal):...returnfloat(obj)...returnsuper(DecimalEncoder,self).default(obj)......>>>>>>json.dumps(data,cls=DecimalEncoder)'{"key1":"string","key2":10,"key3":1.45}'JSON的Encode过程文中代码取自github.com/python/cpyt...删除了几乎所有的docstrings,因为代码太长,直接截取重要部分。可以在代码段顶部的链接中查看完整代码。熟悉json库的都知道,常用的API基本只有4个,分别是dump、dumps和load、loads。源代码位于cpython/Lib/json#https://github.com/python/cpython/blob/master/Lib/json/__init__.py#L183-L238defdumps(obj,*,skipkeys=False,ensure_ascii=True,check_circular=True,allow_nan=True,cls=None,indent=None,separators=None,default=None,sort_keys=False,**kw):#缓存编码器if(notskipkeysandensure_asciiandcheck_circularandallow_nanandcls为None,缩进为None,分隔符为None,默认为None,不是sort_keys而不是kw):如果cls为None,则返回_default_encoder.encode(obj):cls=JSONEncoder#keyreturncls(skipkeys=skipkeys,ensure_ascii=ensure_ascii,check_circular=check_circular,allow_nan=allow_nan,indent=indent,separators=separators,default=default,sort_keys=sort_keys,**kw).encode(obj)直接看最后返回。可以发现,如果不提供cls,则默认使用JSONEncoder,然后调用该类的实例方法encode。encode方法也很简单:#https://github.com/python/cpython/blob/191e993365ac3206f46132dcf46236471ec54bfa/Lib/json/encoder.py#L182-L202defencode(self,o):#str类型直接编码返回ifisinstance(o,str):ifself.ensure_ascii:returnencode_basestring_ascii(o)else:returnencode_basestring(o)#chunks是数据的每一部分chunks=self.iterencode(o,_one_shot=True)ifnotisinstance(chunks,(list,tuple)):chunks=list(chunks)return''.join(chunks)可以看出我们最终得到的json是通过拼接chunk得到的,chunk是调用self.iterencode方法得到的。#https://github.com/python/cpython/blob/191e993365ac3206f46132dcf46236471ec54bfa/Lib/json/encoder.py#L204-257if(_one_shotandc_make_encoderisnotNoneandself.indentisNone):_iterencode=c_make_encoder(self.默认,_encoder,self.indent,self.key_separator,self.item_separator,self.sort_keys,self.skipkeys,self.allow_nan)其他:_iterencode=_make_iterencode(标记,self.default,_encoder,self.indent,floatstr,self。key_separator,self.item_separator,self.sort_keys,self.skipkeys,_one_shot)return_iterencode(o,0)iterencode方法比较长,我们只关心最后几行。返回值_iterencode是函数中两个高阶函数c_make_encoder或_make_iterencode的返回值。c_make_encoder来自于_json模块,它是一个c模块,我们不关心这个模块是如何实现的。转到等效的_make_iterencode方法。#https://github.com/python/cpython/blob/191e993365ac3206f46132dcf46236471ec54bfa/Lib/json/encoder.py#L259-441def_iterencode(o,_current_indent_level):ifisinstance(o,str):yield_encoder(o)elifoisNone:yield'null'elifoisTrue:yield'true'elifoisFalse:yield'false'elifisinstance(o,int):#seecommentforint/floatin_make_iterencodeyield_intstr(o)elifisinstance(o,float):#在_make_iterencodeyield_floatstr(o)elifisinstance(o,(list,tuple)):yieldfrom_iterencode_list(o,_current_indent_level)elifisinstance(o,dict)中查看注释(o,_current_indent_level)else:ifmarkersisnotNone:markerid=id(o)ifmarkeridinmarkers:raiseValueError("Circularreferencedetected")markers[markerid]=oo=_default(o)y来自_iterencode(o,_current_indent_level)如果markers不是None:delmarkers[markerid]return_iterencode也只需要关心返回的函数。代码中的各种if-elif-else将内置类型转换成对面的JSON类型,当使用到无法识别的类型时,会使用_default()方法,然后递归调用解析每个值。_default是第一个覆盖的默认值。在这里你可以充分理解Python是如何对JSON数据进行编码的。总结一下这个过程,json.dumps()调用了JSONEncoder的实例方法encode(),然后使用iterencode()递归转换各种类型,最后拼接成字符串返回。优雅的解决方案分析完前面的流程,知道为什么要继承JSONEncoder,然后重写默认方法,就可以完成自定义类型分析了。也许你以后需要解析datetime类型的数据,你肯定会这样做:isinstance(obj,datetime.datetime):returnobj.strftime(DATETIME_FORMAT)returnsuper(ExtendJONEncoder,self).default(obj)最后调用父类的default()方法纯粹为了触发异常。Python可以使用singledispatch来解决这个单一泛型问题。importjsonfromdatetimeimportdatetimefromdecimalimportDecimalfromfunctoolsimportsingledispatchclassMyClass:def__init__(self,value):self._value=valuedefget_value(self):returnself._value#创建非内置类型的三个实例mc=MyClass('我是MyClass类')dm=Decimal('11.11')dt=datetime.now()@singledispatchdefconvert(o):raiseTypeError('cannotconverttype')@convert.register(datetime)def_(o):返回o.strftime('%b%d%Y%H:%M:%S')@convert.register(Decimal)def_(o):返回float(o)@convert.register(MyClass)def_(o):returno.get_value()classExtendJSONEncoder(json.JONEncoder):defdefault(self,obj):try:returnconvert(obj)除了TypeError:returnsuper(ExtendJSONEncoder,self).default(obj)data={'mc':mc,'dm':dm,'dt':dt}json.dumps(data,cls=ExtendJONEncoder)#{"mc":"我是MyClass","dm":11.11,"dt":"Nov10201717:31:25"}这种写法比较符合设计模式的规范。如果以后有新的类型,不需要修改ExtendJSONEncoder类,只需要添加合适的singledispatch方法即可,更加pythonic。