Python 使用 attrs 和 cattrs 实现面向对象编程的实践
Python是支持面向对象的,很多情况下使用面向对象编程会使得代码更加容易扩展,并且可维护性更高,但是如果你写的多了或者某一对象非常复杂了,其中的一些写法会相当相当繁琐,而且我们会经常碰到对象和JSON序列化及反序列化的问题,原生的Python转起来还是很费劲的。
可能这么说大家会觉得有点抽象,那么这里举几个例子来感受一下。
首先让我们定义一个对象吧,比如颜色。我们常用RGB三个原色来表示颜色,R、G、B分别代表红、绿、蓝三个颜色的数值,范围是0-255,也就是每个原色有256个取值。如RGB(0,0,0)就代表黑色,RGB(255,255,255)就代表白色,RGB(255,0,0)就代表红色,如果不太明白可以具体看看RGB颜色的定义哈。
好,那么我们现在如果想定义一个颜色对象,那么正常的写法就是这样了,创建这个对象的时候需要三个参数,就是R、G、B三个数值,定义如下:
classColor(object): """ ColorObjectofRGB """ def__init__(self,r,g,b): self.r=r self.g=g self.b=b
其实对象一般就是这么定义的,初始化方法里面传入各个参数,然后定义全局变量并赋值这些值。其实挺多常用语言比如Java、PHP里面都是这么定义的。但其实这种写法是比较冗余的,比如r、g、b这三个变量一写就写了三遍。
好,那么我们初始化一下这个对象,然后打印输出下,看看什么结果:
color=Color(255,255,255) print(color)
结果是什么样的呢?或许我们也就能看懂一个Color吧,别的都没有什么有效信息,像这样子:
<__main__.Colorobjectat0x103436f60>
我们知道,在Python里面想要定义某个对象本身的打印输出结果的时候,需要实现它的 __repr__方法,所以我们比如我们添加这么一个方法:
def__repr__(self): returnf'{self.__class__.__name__}(r={self.r},g={self.g},b={self.b})'
这里使用了Python中的fstring来实现了 __repr__方法,在这里我们构造了一个字符串并返回,字符串中包含了这个Color类中的r、g、b属性,这个返回的结果就是print的打印结果,我们再重新执行一下,结果就变成这样子了:
Color(r=255,g=255,b=255)
改完之后,这样打印的对象就会变成这样的字符串形式了,感觉看起来清楚多了吧?
再继续,如果我们要想实现这个对象里面的 __eq__、 __lt__等各种方法来实现对象之间的比较呢?照样需要继续定义成类似这样子的形式:
def__lt__(self,other): ifnotisinstance(other,self.__class__):returnNotImplemented return(self.r,self.g,self.b)<(other.r,other.g,other.b)
这里是 __lt__方法,有了这个方法就可以使用比较符来对两个Color对象进行比较了,但这里又把这几个属性写了两遍。
最后再考虑考虑,如果我要把JSON转成Color对象,难道我要读完JSON然后一个个属性赋值吗?如果我想把Color对象转化为JSON,又得把这几个属性写几遍呢?如果我突然又加了一个属性比如透明度a参数,那么整个类的方法和参数都要修改,这是极其难以扩展的。不知道你能不能忍,反正我不能忍!
如果你用过Scrapy、Django等框架,你会发现Scrapy里面有一个Item的定义,只需要定义一些Field就可以了,Django里面的Model也类似这样,只需要定义其中的几个字段属性就可以完成整个类的定义了,非常方便。
说到这里,我们能不能把Scrapy或Django里面的定义模式直接拿过来呢?能是能,但是没必要,因为我们还有专门为Python面向对象而专门诞生的库,没错,就是attrs和cattrs这两个库。
有了attrs库,我们就可以非常方便地定义各个对象了,另外对于JSON的转化,可以进一步借助cattrs这个库,非常有帮助。
说了这么多,还是没有介绍这两个库的具体用法,下面我们来详细介绍下。
安装
安装这两个库非常简单,使用pip就好了,命令如下:
pip3installattrscattrs
安装好了之后我们就可以导入并使用这两个库了。
简介与特性
首先我们来介绍下attrs这个库,其官方的介绍如下:
attrs是这样的一个Python工具包,它能将你从繁综复杂的实现上解脱出来,享受编写Python类的快乐。它的目标就是在不减慢你编程速度的前提下,帮助你来编写简洁而又正确的代码。
其实意思就是用了它,定义和实现Python类变得更加简洁和高效。
基本用法
首先明确一点,我们现在是装了attrs和cattrs这两个库,但是实际导入的时候是使用attr和cattr这两个包,是不带s的。
在attr这个库里面有两个比较常用的组件叫做attrs和attr,前者是主要用来修饰一个自定义类的,后者是定义类里面的一个字段的。有了它们,我们就可以将上文中的定义改写成下面的样子:
fromattrimportattrs,attrib @attrs classColor(object): r=attrib(type=int,default=0) g=attrib(type=int,default=0) b=attrib(type=int,default=0) if__name__=='__main__': color=Color(255,255,255) print(color)
看我们操作的,首先我们导入了刚才所说的两个组件,然后用attrs里面修饰了Color这个自定义类,然后用attrib来定义一个个属性,同时可以指定属性的类型和默认值。最后打印输出,结果如下:
Color(r=255,g=255,b=255)
怎么样,达成了一样的输出效果!
观察一下有什么变化,是不是变得更简洁了?r、g、b三个属性都只写了一次,同时还指定了各个字段的类型和默认值,另外也不需要再定义 __init__方法和 __repr__方法了,一切都显得那么简洁。一个字,爽!
实际上,主要是attrs这个修饰符起了作用,然后根据定义的attrib属性自动帮我们实现了 __init__、 __repr__、 __eq__、 __ne__、 __lt__、 __le__、 __gt__、 __ge__、 __hash__这几个方法。
如使用attrs修饰的类定义是这样子:
fromattrimportattrs,attrib @attrs classSmartClass(object): a=attrib() b=attrib()
其实就相当于已经实现了这些方法:
classRoughClass(object): def__init__(self,a,b): self.a=a self.b=b def__repr__(self): return"RoughClass(a={},b={})".format(self.a,self.b) def__eq__(self,other): ifother.__class__isself.__class__: return(self.a,self.b)==(other.a,other.b) else: returnNotImplemented def__ne__(self,other): result=self.__eq__(other) ifresultisNotImplemented: returnNotImplemented else: returnnotresult def__lt__(self,other): ifother.__class__isself.__class__: return(self.a,self.b)<(other.a,other.b) else: returnNotImplemented def__le__(self,other): ifother.__class__isself.__class__: return(self.a,self.b)<=(other.a,other.b) else: returnNotImplemented def__gt__(self,other): ifother.__class__isself.__class__: return(self.a,self.b)>(other.a,other.b) else: returnNotImplemented def__ge__(self,other): ifother.__class__isself.__class__: return(self.a,self.b)>=(other.a,other.b) else: returnNotImplemented def__hash__(self): returnhash((self.__class__,self.a,self.b))
所以说,如果我们用了attrs的话,就可以不用再写这些冗余又复杂的代码了。
翻看源码可以发现,其内部新建了一个ClassBuilder,通过一些属性操作来动态添加了上面的这些方法,如果想深入研究,建议可以看下attrs库的源码。
别名使用
这时候大家可能有个小小的疑问,感觉里面的定义好乱啊,库名叫做attrs,包名叫做attr,然后又导入了attrs和attrib,这太奇怪了。为了帮大家解除疑虑,我们来梳理一下它们的名字。
首先库的名字就叫做attrs,这个就是装Python包的时候这么装就行了。但是库的名字和导入的包的名字确实是不一样的,我们用的时候就导入attr这个包就行了,里面包含了各种各样的模块和组件,这是完全固定的。
好,然后接下来看看attr包里面包含了什么,刚才我们引入了attrs和attrib。
首先是attrs,它主要是用来修饰class类的,而attrib主要是用来做属性定义的,这个就记住它们两个的用法就好了。
翻了一下源代码,发现其实它还有一些别名:
s=attributes=attrs ib=attr=attrib
也就是说,attrs可以用s或attributes来代替,attrib可以用attr或ib来代替。
既然是别名,那么上面的类就可以改写成下面的样子:
fromattrimports,ib @s classColor(object): r=ib(type=int,default=0) g=ib(type=int,default=0) b=ib(type=int,default=0) if__name__=='__main__': color=Color(255,255,255) print(color)
是不是更加简洁了,当然你也可以把s改写为attributes,ib改写为attr,随你怎么用啦。
不过我觉得比较舒服的是attrs和attrib的搭配,感觉可读性更好一些,当然这个看个人喜好。
所以总结一下:
库名:attrs
导入包名:attr
修饰类:s或attributes或attrs
定义属性:ib或attr或attrib
OK,理清了这几部分内容,我们继续往下深入了解它的用法吧。
声明和比较
在这里我们再声明一个简单一点的数据结构,比如叫做Point,包含x、y的坐标,定义如下:
fromattrimportattrs,attrib @attrs classPoint(object): x=attrib() y=attrib()
其中attrib里面什么参数都没有,如果我们要使用的话,参数可以顺次指定,也可以根据名字指定,如:
p1=Point(1,2) print(p1) p2=Point(x=1,y=2) print(p2)
其效果都是一样的,打印输出结果如下:
Point(x=1,y=2) Point(x=1,y=2)
OK,接下来让我们再验证下类之间的比较方法,由于使用了attrs,相当于我们定义的类已经有了 __eq__、 __ne__、 __lt__、 __le__、 __gt__、 __ge__这几个方法,所以我们可以直接使用比较符来对类和类之间进行比较,下面我们用实例来感受一下:
print('Equal:',Point(1,2)==Point(1,2)) print('NotEqual(ne):',Point(1,2)!=Point(3,4)) print('LessThan(lt):',Point(1,2)Point(3,2),Point(4,2)>Point(3,1)) print('GreaterorEqual(ge):',Point(4,2)>=Point(4,1))
运行结果如下:
Same:False
Equal:True
NotEqual(ne):True
LessThan(lt):True
LessorEqual(le):TrueTrue
GreaterThan(gt):TrueTrue
GreaterorEqual(ge):True
可能有的朋友不知道ne、lt、le什么的是什么意思,不过看到这里你应该明白啦,ne就是NotEqual的意思,就是不相等,le就是LessorEqual的意思,就是小于或等于。
其内部怎么实现的呢,就是把类的各个属性转成元组来比较了,比如 Point(1,2) 属性定义 现在看来,对于这个类的定义莫过于每个属性的定义了,也就是attrib的定义。对于attrib的定义,我们可以传入各种参数,不同的参数对于这个类的定义有非常大的影响。 下面我们就来详细了解一下每个属性的具体参数和用法吧。 首先让我们概览一下总共可能有多少可以控制一个属性的参数,我们用attrs里面的fields方法可以查看一下: 这就可以输出Point的所有属性和对应的参数,结果如下: (Attribute(name='x',default=NOTHING,validator=None,repr=True,cmp=True,hash=None,init=True,metadata=mappingproxy({}),type=None,converter=None,kw_only=False),Attribute(name='y',default=NOTHING,validator=None,repr=True,cmp=True,hash=None,init=True,metadata=mappingproxy({}),type=None,converter=None,kw_only=False)) 输出出来了,可以看到结果是一个元组,元组每一个元素都其实是一个Attribute对象,包含了各个参数,下面详细解释下几个参数的含义: name:属性的名字,是一个字符串类型。 属性名 对于属性名,非常清楚了,我们定义什么属性,属性名就是什么,例如上面的例子,定义了: x=attrib() 那么其属性名就是x。 默认值 对于默认值,如果在初始化的时候没有指定,那么就会默认使用默认值进行初始化,我们看下面的一个实例: 在这里我们将y属性的默认值设置为了100,在初始化的时候,第一次都传入了x、y两个参数,第二次只传入了x这个参数,看下运行结果: 可以看到结果,当设置了默认参数的属性没有被传入值时,他就会使用设置的默认值进行初始化。 那假如没有设置默认值但是也没有初始化呢?比如执行下: Point() 那么就会报错了,错误如下: TypeError:__init__()missing1requiredpositionalargument:'x' 初始化 如果一个类的某些属性不想参与初始化,比如想直接设置一个初始值,一直固定不变,我们可以将属性的init参数设置为False,看一个实例: 比如x我们只想在初始化的时候设置固定值,不想初始化的时候被改变和设定,我们将其设置了init参数为False,同时设置了一个默认值,如果不设置默认值,默认为NOTHING。然后初始化的时候我们只传入了一个值,其实也就是为y这个属性赋值。 这样的话,看下运行结果: Point(x=10,y=3) 没什么问题,y被赋值为了我们设置的值3。 那假如我们非要设置x呢?会发生什么,比如改写成这样子: Point(1,2) 报错了,错误如下: TypeError:__init__()takes2positionalargumentsbut3weregiven 参数过多,也就是说,已经将init设置为False的属性就不再被算作可以被初始化的属性了。 强制关键字 强制关键字是Python里面的一个特性,在传入的时候必须使用关键字的名字来传入,如果不太理解可以再了解下Python的基础。 设置了强制关键字参数的属性必须要放在后面,其后面不能再有非强制关键字参数的属性,否则会报这样的错误: ValueError:Nonkeyword-onlyattributesarenotallowedafterakeyword-onlyattribute(unlesstheyareinit=False) 如果设置了kw_only参数为True,那么在初始化的时候必须传入关键字的名字,这里就必须指定y这个名字,运行结果如下: Point(x=1,y=3) 如果没有指定y这个名字,像这样调用: Point(1,3) 那么就会报错: TypeError:__init__()takesfrom1to2positionalargumentsbut3weregiven 注意,如果我们将一个属性设置了init为False,那么kw_only这个参数会被忽略。 验证器 有时候在设置一个属性的时候必须要满足某个条件,比如性别必须要是男或者女,否则就不合法。对于这种情况,我们就需要有条件来控制某些属性不能为非法值。 下面我们看一个实例: 在这里我们定义了一个验证器Validator方法,叫做is_valid_gender。然后定义了一个类Person还有它的两个属性name和gender,其中gender定义的时候传入了一个参数validator,其值就是我们定义的Validator方法。 这个Validator定义的时候有几个固定的参数: 这是三个参数是固定的,在类初始化的时候,其内部会将这三个参数传递给这个Validator,因此Validator里面就可以接受到这三个值,然后进行判断即可。在Validator里面,我们判断如果不是男性或女性,那么就直接抛出错误。 下面做了两个实验,一个就是正常传入male,另一个写错了,写的是mlae,观察下运行结果: OK,结果显而易见了,第二个报错了,因为其值不是正常的性别,所以程序直接报错终止。 注意在Validator里面返回True或False是没用的,错误的值还会被照常复制。所以,一定要在Validator里面raise某个错误。 另外attrs库里面还给我们内置了好多Validator,比如判断类型,这里我们再增加一个属性age,必须为int类型: 这时候初始化的时候就必须传入int类型,如果为其他类型,则直接抛错: TypeError:("'age'mustbe 另外还有其他的一些Validator,比如与或运算、可执行判断、可迭代判断等等,可以参考官方文档: https://www.attrs.org/en/stable/api.html#validators。 另外validator参数还支持多个Validator,比如我们要设置既要是数字,又要小于100,那么可以把几个Validator放到一个列表里面并传入: 这样就会将所有的Validator都执行一遍,必须每个Validator都满足才可以。这里age传入了500,那么不符合第二个Validator,直接抛错: ValueError:age500mustlessthan100 转换器 其实很多时候我们会不小心传入一些形式不太标准的结果,比如本来是int类型的100,我们传入了字符串类型的100,那这时候直接抛错应该不好吧,所以我们可以设置一些转换器来增强容错机制,比如将字符串自动转为数字等等,看一个实例: 看这里,我们定义了一个方法,可以将值转化为数字类型,如果不能转,那么就返回None,这样保证了任何可以被转数字的值都被转为数字,否则就留空,容错性非常高。 运行结果如下: Point(x=100,y=3) 类型 为什么把这个放到最后来讲呢,因为Python中的类型是非常复杂的,有原生类型,有typing类型,有自定义类的类型。 首先我们来看看原生类型是怎样的,这个很容易理解了,就是普通的int、float、str等类型,其定义如下: 这里我们将x属性定义为int类型了,初始化的时候传入了数值型100和字符串型100,结果如下: Point(x=100,y=3) 但我们发现,虽然定义了,但是不会被自动转类型的。 另外我们还可以自定义typing里面的类型,比如List,另外attrs里面也提供了类型的定义: 这里我们引入了typing这个包,定义了y为int数字组成的列表,z使用了attrs里面定义的Factory定义了同样为列表类型。 另外我们也可以进行类型的嵌套,比如像这样子: 在这里我们定义了Point类代表离散点,随后定义了线,其拥有points属性是Point组成的列表。在初始化的时候我们声明了五个点,然后用这五个点组成的列表声明了一条线,逻辑没什么问题。 运行结果: [Point(x=0,y=0),Point(x=1,y=1),Point(x=2,y=2),Point(x=3,y=3),Point(x=4,y=4)] 可以看到这里我们得到了一个嵌套类型的Line对象,其值是Point类型组成的列表。 以上便是一些属性的定义,把握好这些属性的定义,我们就可以非常方便地定义一个类了。 序列转换 在很多情况下,我们经常会遇到JSON等字符串序列和对象互相转换的需求,尤其是在写RESTAPI、数据库交互的时候。 attrs库的存在让我们可以非常方便地定义Python类,但是它对于序列字符串的转换功能还是比较薄弱的,cattrs这个库就是用来弥补这个缺陷的,下面我们再来看看cattrs这个库。 cattrs导入的时候名字也不太一样,叫做cattr,它里面提供了两个主要的方法,叫做structure和unstructure,两个方法是相反的,对于类的序列化和反序列化支持非常好。 基本转换 首先我们来看看基本的转换方法的用法,看一个基本的转换实例: 在这里我们定义了一个Point对象,然后调用unstructure方法即可直接转换为JSON字符串。如果我们再想把它转回来,那就需要调用structure方法,这样就成功转回了一个Point对象。 看下运行结果: json:{'x':1,'y':2} 当然这种基本的来回转用的多了就轻车熟路了。 多类型转换 另外structure也支持一些其他的类型转换,看下实例: 这里面用到了Tuple、MutableSequence、Optional、Set等类,都属于typing这个模块,后面我会写内容详细介绍这个库的用法。 不过总的来说,大部分情况下,JSON和对象的互转是用的最多的。 属性处理 上面的例子都是理想情况下使用的,但在实际情况下,很容易遇到JSON和对象不对应的情况,比如JSON多个字段,或者对象多个字段。 我们先看看下面的例子: 在这里,JSON多了一个字段z,而Point类只有x、y两个字段,那么直接执行structure会出现什么情况呢? TypeError:__init__()gotanunexpectedkeywordargument'z' 不出所料,报错了。意思是多了一个参数,这个参数并没有被定义。 这时候一般的解决方法的直接忽略这个参数,可以重写一下structure方法,定义如下: 这里定义了一个drop_nonattrs方法,用于从JSON里面删除对象里面不存在的属性,然后调用新的structure方法即可,写法如下: 这样我们就可以避免JSON字段冗余导致的转换问题了。 另外还有一个常见的问题,那就是数据对象转换,比如对于时间来说,在对象里面声明我们一般会声明为datetime类型,但在序列化的时候却需要序列化为字符串。 所以,对于一些特殊类型的属性,我们往往需要进行特殊处理,这时候就需要我们针对某种特定的类型定义特定的hook处理方法,这里就需要用到register_unstructure_hook和register_structure_hook方法了。 下面这个例子是时间datetime转换的时候进行的处理: 在这里我们对datetime这个类型注册了两个hook,当序列化的时候,就调用strftime方法转回字符串,当反序列化的时候,就调用strptime将其转回datetime类型。 看下运行结果: event:Event(happened_at=datetime.datetime(2019,6,1,0,0)) 这样对于一些特殊类型的属性处理也得心应手了。 嵌套处理 最后我们再来看看嵌套类型的处理,比如类里面有个属性是另一个类的类型,如果遇到这种嵌套类的话,怎样类转转换呢?我们用一个实例感受下: 这里我们定义了两个Class,一个是Point,一个是Color,然后定义了Line对象,其属性类型一个是Color类型,一个是Point类型组成的列表,下面我们进行序列化和反序列化操作,转成JSON然后再由JSON转回来,运行结果如下: 可以看到,我们非常方便地将对象转化为了JSON对象,然后也非常方便地转回了对象。 这样我们就成功实现了嵌套对象的序列化和反序列化,所有问题成功解决! 结语 本节介绍了利用attrs和cattrs两个库实现Python面向对象编程的实践,有了它们两个的加持,Python面向对象编程不再是难事。希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对毛票票网站的支持!
fromattrimportattrs,attrib,fields
@attrs
classPoint(object):
x=attrib()
y=attrib()
print(fields(Point))
default:属性的默认值,如果没有传入初始化数据,那么就会使用默认值。如果没有默认值定义,那么就是NOTHING,即没有默认值。
validator:验证器,检查传入的参数是否合法。
init:是否参与初始化,如果为False,那么这个参数不能当做类的初始化参数,默认是True。
metadata:元数据,只读性的附加数据。
type:类型,比如int、str等各种类型,默认为None。
converter:转换器,进行一些值的处理和转换器,增加容错性。
kw_only:是否为强制关键字参数,默认为False。
fromattrimportattrs,attrib,fields
@attrs
classPoint(object):
x=attrib()
y=attrib(default=100)
if__name__=='__main__':
print(Point(x=1,y=3))
print(Point(x=1))
Point(x=1,y=3)
Point(x=1,y=100)
所以说,如果一个属性,我们一旦没有设置默认值同时没有传入的话,就会引起错误。所以,一般来说,为了稳妥起见,设置一个默认值比较好,即使是None也可以的。
fromattrimportattrs,attrib
@attrs
classPoint(object):
x=attrib(init=False,default=10)
y=attrib()
if__name__=='__main__':
print(Point(3))
好,我们来看一个例子,我们将最后一个属性设置kw_only参数为True:
fromattrimportattrs,attrib,fields
@attrs
classPoint(object):
x=attrib(default=0)
y=attrib(kw_only=True)
if__name__=='__main__':
print(Point(1,y=3))
所以,这个参数就是设置初始化传参必须要用名字来传,否则会出现错误。
fromattrimportattrs,attrib
defis_valid_gender(instance,attribute,value):
ifvaluenotin['male','female']:
raiseValueError(f'gender{value}isnotvalid')
@attrs
classPerson(object):
name=attrib()
gender=attrib(validator=is_valid_gender)
if__name__=='__main__':
print(Person(name='Mike',gender='male'))
print(Person(name='Mike',gender='mlae'))
Person(name='Mike',gender='male')
TypeError:__init__()missing1requiredpositionalargument:'gender'
age=attrib(validator=validators.instance_of(int))
fromattrimportattrs,attrib,validators
defis_less_than_100(instance,attribute,value):
ifvalue>100:
raiseValueError(f'age{value}mustlessthan100')
@attrs
classPerson(object):
name=attrib()
gender=attrib(validator=is_valid_gender)
age=attrib(validator=[validators.instance_of(int),is_less_than_100])
if__name__=='__main__':
print(Person(name='Mike',gender='male',age=500))
fromattrimportattrs,attrib
defto_int(value):
try:
returnint(value)
except:
returnNone
@attrs
classPoint(object):
x=attrib(converter=to_int)
y=attrib()
if__name__=='__main__':
print(Point('100',3))
fromattrimportattrs,attrib
@attrs
classPoint(object):
x=attrib(type=int)
y=attrib()
if__name__=='__main__':
print(Point(100,3))
print(Point('100',3))
Point(x='100',y=3)
fromattrimportattrs,attrib,Factory
importtyping
@attrs
classPoint(object):
x=attrib(type=int)
y=attrib(type=typing.List[int])
z=attrib(type=Factory(list))
fromattrimportattrs,attrib,Factory
importtyping
@attrs
classPoint(object):
x=attrib(type=int,default=0)
y=attrib(type=int,default=0)
@attrs
classLine(object):
name=attrib()
points=attrib(type=typing.List[Point])
if__name__=='__main__':
points=[Point(i,i)foriinrange(5)]
print(points)
line=Line(name='line1',points=points)
print(line)
Line(name='line1',points=[Point(x=0,y=0),Point(x=1,y=1),Point(x=2,y=2),Point(x=3,y=3),Point(x=4,y=4)])
fromattrimportattrs,attrib
fromcattrimportunstructure,structur
@attrs
classPoint(object):
x=attrib(type=int,default=0)
y=attrib(type=int,default=0)
if__name__=='__main__':
point=Point(x=1,y=2)
json=unstructure(point)
print('json:',json)
obj=structure(json,Point)
print('obj:',obj)
obj:Point(x=1,y=2)
>>>cattr.structure(1,str)
'1'
>>>cattr.structure("1",float)
1.0
>>>cattr.structure([1.0,2,"3"],Tuple[int,int,int])
(1,2,3)
>>>cattr.structure((1,2,3),MutableSequence[int])
[1,2,3]
>>>cattr.structure((1,None,3),List[Optional[str]])
['1',None,'3']
>>>cattr.structure([1,2,3,4],Set)
{1,2,3,4}
>>>cattr.structure([[1,2],[3,4]],Set[FrozenSet[str]])
{frozenset({'4','3'}),frozenset({'1','2'})}
>>>cattr.structure(OrderedDict([(1,2),(3,4)]),Dict)
{1:2,3:4}
>>>cattr.structure([1,2,3],Tuple[int,str,float])
(1,'2',3.0)
fromattrimportattrs,attrib
fromcattrimportstructure
@attrs
classPoint(object):
x=attrib(type=int,default=0)
y=attrib(type=int,default=0)
json={'x':1,'y':2,'z':3}
print(structure(json,Point))
defdrop_nonattrs(d,type):
ifnotisinstance(d,dict):returnd
attrs_attrs=getattr(type,'__attrs_attrs__',None)
ifattrs_attrsisNone:
raiseValueError(f'type{type}isnotanattrsclass')
attrs:Set[str]={attr.nameforattrinattrs_attrs}
return{key:valforkey,valind.items()ifkeyinattrs}
defstructure(d,type):
returncattr.structure(drop_nonattrs(d,type),type)
fromtypingimportSet
fromattrimportattrs,attrib
importcattr
@attrs
classPoint(object):
x=attrib(type=int,default=0)
y=attrib(type=int,default=0)
defdrop_nonattrs(d,type):
ifnotisinstance(d,dict):returnd
attrs_attrs=getattr(type,'__attrs_attrs__',None)
ifattrs_attrsisNone:
raiseValueError(f'type{type}isnotanattrsclass')
attrs:Set[str]={attr.nameforattrinattrs_attrs}
return{key:valforkey,valind.items()ifkeyinattrs}
defstructure(d,type):
returncattr.structure(drop_nonattrs(d,type),type)
json={'x':1,'y':2,'z':3}
print(structure(json,Point))
importdatetime
fromattrimportattrs,attrib
importcattr
TIME_FORMAT='%Y-%m-%dT%H:%M:%S.%fZ'
@attrs
classEvent(object):
happened_at=attrib(type=datetime.datetime)
cattr.register_unstructure_hook(datetime.datetime,lambdadt:dt.strftime(TIME_FORMAT))
cattr.register_structure_hook(datetime.datetime,
lambdastring,_:datetime.datetime.strptime(string,TIME_FORMAT))
event=Event(happened_at=datetime.datetime(2019,6,1))
print('event:',event)
json=cattr.unstructure(event)
print('json:',json)
event=cattr.structure(json,Event)
print('Event:',event)
json:{'happened_at':'2019-06-01T00:00:00.000000Z'}
Event:Event(happened_at=datetime.datetime(2019,6,1,0,0))
fromattrimportattrs,attrib
fromtypingimportList
fromcattrimportstructure,unstructure
@attrs
classPoint(object):
x=attrib(type=int,default=0)
y=attrib(type=int,default=0)
@attrs
classColor(object):
r=attrib(default=0)
g=attrib(default=0)
b=attrib(default=0)
@attrs
classLine(object):
color=attrib(type=Color)
points=attrib(type=List[Point])
if__name__=='__main__':
line=Line(color=Color(),points=[Point(i,i)foriinrange(5)])
print('Object:',line)
json=unstructure(line)
print('JSON:',json)
line=structure(json,Line)
print('Object:',line)
Object:Line(color=Color(r=0,g=0,b=0),points=[Point(x=0,y=0),Point(x=1,y=1),Point(x=2,y=2),Point(x=3,y=3),Point(x=4,y=4)])
JSON:{'color':{'r':0,'g':0,'b':0},'points':[{'x':0,'y':0},{'x':1,'y':1},{'x':2,'y':2},{'x':3,'y':3},{'x':4,'y':4}]}
Object:Line(color=Color(r=0,g=0,b=0),points=[Point(x=0,y=0),Point(x=1,y=1),Point(x=2,y=2),Point(x=3,y=3),Point(x=4,y=4)])
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!