gpt4 book ai didi

Python 使用 attrs 和 cattrs 实现面向对象编程的实践

转载 作者:qq735679552 更新时间:2022-09-28 22:32:09 25 4
gpt4 key购买 nike

CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.

这篇CFSDN的博客文章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 三个数值,定义如下:

?
1
2
3
4
5
6
7
8
class color( object ):
  """
  color object of rgb
  """
  def __init__( self , r, g, b):
  self .r = r
  self .g = g
  self .b = b

其实对象一般就是这么定义的,初始化方法里面传入各个参数,然后定义全局变量并赋值这些值。其实挺多常用语言比如 java、php 里面都是这么定义的。但其实这种写法是比较冗余的,比如 r、g、b 这三个变量一写就写了三遍.

好,那么我们初始化一下这个对象,然后打印输出下,看看什么结果:

?
1
2
color = color( 255 , 255 , 255 )
print (color)

结果是什么样的呢?或许我们也就能看懂一个 color 吧,别的都没有什么有效信息,像这样子:

<__main__.color object at 0x103436f60> 。

我们知道,在 python 里面想要定义某个对象本身的打印输出结果的时候,需要实现它的 __repr__ 方法,所以我们比如我们添加这么一个方法:

?
1
2
def __repr__( self ):
  return f '{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__ 等各种方法来实现对象之间的比较呢?照样需要继续定义成类似这样子的形式:

?
1
2
3
def __lt__( self , other):
  if not isinstance (other, self .__class__): return notimplemented
  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 就好了,命令如下:

pip3 install attrs cattrs 。

安装好了之后我们就可以导入并使用这两个库了.

简介与特性 。

首先我们来介绍下 attrs 这个库,其官方的介绍如下:

attrs 是这样的一个 python 工具包,它能将你从繁综复杂的实现上解脱出来,享受编写 python 类的快乐。它的目标就是在不减慢你编程速度的前提下,帮助你来编写简洁而又正确的代码.

其实意思就是用了它,定义和实现 python 类变得更加简洁和高效.

基本用法 。

首先明确一点,我们现在是装了 attrs 和 cattrs 这两个库,但是实际导入的时候是使用 attr 和 cattr 这两个包,是不带 s 的.

在 attr 这个库里面有两个比较常用的组件叫做 attrs 和 attr,前者是主要用来修饰一个自定义类的,后者是定义类里面的一个字段的。有了它们,我们就可以将上文中的定义改写成下面的样子:

?
1
2
3
4
5
6
7
8
9
from attr import attrs, attrib
@attrs
class color( 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 修饰的类定义是这样子:

?
1
2
3
4
5
from attr import attrs, attrib
@attrs
class smartclass( object ):
  a = attrib()
  b = attrib()

其实就相当于已经实现了这些方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class roughclass( 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):
  if other.__class__ is self .__class__:
   return ( self .a, self .b) = = (other.a, other.b)
  else :
   return notimplemented
  def __ne__( self , other):
  result = self .__eq__(other)
  if result is notimplemented:
   return notimplemented
  else :
   return not result
  def __lt__( self , other):
  if other.__class__ is self .__class__:
   return ( self .a, self .b) < (other.a, other.b)
  else :
   return notimplemented
  def __le__( self , other):
  if other.__class__ is self .__class__:
   return ( self .a, self .b) < = (other.a, other.b)
  else :
   return notimplemented
  def __gt__( self , other):
  if other.__class__ is self .__class__:
   return ( self .a, self .b) > (other.a, other.b)
  else :
   return notimplemented
  def __ge__( self , other):
  if other.__class__ is self .__class__:
   return ( self .a, self .b) > = (other.a, other.b)
  else :
   return notimplemented
  def __hash__( self ):
  return hash (( self .__class__, self .a, self .b))

所以说,如果我们用了 attrs 的话,就可以不用再写这些冗余又复杂的代码了.

翻看源码可以发现,其内部新建了一个 classbuilder,通过一些属性操作来动态添加了上面的这些方法,如果想深入研究,建议可以看下 attrs 库的源码.

别名使用 。

这时候大家可能有个小小的疑问,感觉里面的定义好乱啊,库名叫做 attrs,包名叫做 attr,然后又导入了 attrs 和 attrib,这太奇怪了。为了帮大家解除疑虑,我们来梳理一下它们的名字.

首先库的名字就叫做 attrs,这个就是装 python 包的时候这么装就行了。但是库的名字和导入的包的名字确实是不一样的,我们用的时候就导入 attr 这个包就行了,里面包含了各种各样的模块和组件,这是完全固定的.

好,然后接下来看看 attr 包里面包含了什么,刚才我们引入了 attrs 和 attrib.

首先是 attrs,它主要是用来修饰 class 类的,而 attrib 主要是用来做属性定义的,这个就记住它们两个的用法就好了.

翻了一下源代码,发现其实它还有一些别名:

?
1
2
s = attributes = attrs
ib = attr = attrib

也就是说,attrs 可以用 s 或 attributes 来代替,attrib 可以用 attr 或 ib 来代替.

既然是别名,那么上面的类就可以改写成下面的样子:

?
1
2
3
4
5
6
7
8
9
10
from attr import s, ib
@s
class color( 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 的坐标,定义如下:

?
1
2
3
4
5
from attr import attrs, attrib
@attrs
class point( object ):
  x = attrib()
  y = attrib()

其中 attrib 里面什么参数都没有,如果我们要使用的话,参数可以顺次指定,也可以根据名字指定,如:

?
1
2
3
4
p1 = point( 1 , 2 )
print (p1)
p2 = point(x = 1 , y = 2 )
print (p2)

其效果都是一样的,打印输出结果如下:

?
1
2
point(x = 1 , y = 2 )
point(x = 1 , y = 2 )

ok,接下来让我们再验证下类之间的比较方法,由于使用了 attrs,相当于我们定义的类已经有了 __eq__ 、 __ne__ 、 __lt__ 、 __le__ 、 __gt__ 、 __ge__ 这几个方法,所以我们可以直接使用比较符来对类和类之间进行比较,下面我们用实例来感受一下:

?
1
2
3
4
5
6
print ( 'equal:' , point( 1 , 2 ) = = point( 1 , 2 ))
print ( 'not equal(ne):' , point( 1 , 2 ) ! = point( 3 , 4 ))
print ( 'less than(lt):' , point( 1 , 2 ) < point( 3 , 4 ))
print ( 'less or equal(le):' , point( 1 , 2 ) < = point( 1 , 4 ), point( 1 , 2 ) < = point( 1 , 2 ))
print ( 'greater than(gt):' , point( 4 , 2 ) > point( 3 , 2 ), point( 4 , 2 ) > point( 3 , 1 ))
print ( 'greater or equal(ge):' , point( 4 , 2 ) > = point( 4 , 1 ))

运行结果如下:

same: false equal: true not equal(ne): true less than(lt): true less or equal(le): true true greater than(gt): true true greater or equal(ge): true 。

可能有的朋友不知道 ne、lt、le 什么的是什么意思,不过看到这里你应该明白啦,ne 就是 not equal 的意思,就是不相等,le 就是 less or equal 的意思,就是小于或等于.

其内部怎么实现的呢,就是把类的各个属性转成元组来比较了,比如 point(1, 2) < point(3, 4) 实际上就是比较了 (1, 2) 和 (3, 4) 两个元组,那么元组之间的比较逻辑又是怎样的呢,这里就不展开了,如果不明白的话可以参考官方文档:  .

属性定义 。

现在看来,对于这个类的定义莫过于每个属性的定义了,也就是 attrib 的定义。对于 attrib 的定义,我们可以传入各种参数,不同的参数对于这个类的定义有非常大的影响.

下面我们就来详细了解一下每个属性的具体参数和用法吧.

首先让我们概览一下总共可能有多少可以控制一个属性的参数,我们用 attrs 里面的 fields 方法可以查看一下:

?
1
2
3
4
5
6
from attr import attrs, attrib, fields
@attrs
class point( object ):
  x = attrib()
  y = attrib()
print (fields(point))

这就可以输出 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:属性的名字,是一个字符串类型。 default:属性的默认值,如果没有传入初始化数据,那么就会使用默认值。如果没有默认值定义,那么就是 nothing,即没有默认值。 validator:验证器,检查传入的参数是否合法。 init:是否参与初始化,如果为 false,那么这个参数不能当做类的初始化参数,默认是 true。 metadata:元数据,只读性的附加数据。 type:类型,比如 int、str 等各种类型,默认为 none。 converter:转换器,进行一些值的处理和转换器,增加容错性。 kw_only:是否为强制关键字参数,默认为 false.

属性名 。

对于属性名,非常清楚了,我们定义什么属性,属性名就是什么,例如上面的例子,定义了:

x = attrib() 。

那么其属性名就是 x.

默认值 。

对于默认值,如果在初始化的时候没有指定,那么就会默认使用默认值进行初始化,我们看下面的一个实例:

?
1
2
3
4
5
6
7
8
from attr import attrs, attrib, fields
@attrs
class point( object ):
  x = attrib()
  y = attrib(default = 100 )
if __name__ = = '__main__' :
  print (point(x = 1 , y = 3 ))
  print (point(x = 1 ))

在这里我们将 y 属性的默认值设置为了 100,在初始化的时候,第一次都传入了 x、y 两个参数,第二次只传入了 x 这个参数,看下运行结果:

?
1
2
point(x = 1 , y = 3 )
point(x = 1 , y = 100 )

可以看到结果,当设置了默认参数的属性没有被传入值时,他就会使用设置的默认值进行初始化.

那假如没有设置默认值但是也没有初始化呢?比如执行下:

point() 。

那么就会报错了,错误如下:

typeerror: __init__() missing 1 required positional argument: 'x' 所以说,如果一个属性,我们一旦没有设置默认值同时没有传入的话,就会引起错误。所以,一般来说,为了稳妥起见,设置一个默认值比较好,即使是 none 也可以的.

初始化 。

如果一个类的某些属性不想参与初始化,比如想直接设置一个初始值,一直固定不变,我们可以将属性的 init 参数设置为 false,看一个实例:

?
1
2
3
4
5
6
7
from attr import attrs, attrib
@attrs
class point( object ):
  x = attrib(init = false, default = 10 )
  y = attrib()
if __name__ = = '__main__' :
  print (point( 3 ))

比如 x 我们只想在初始化的时候设置固定值,不想初始化的时候被改变和设定,我们将其设置了 init 参数为 false,同时设置了一个默认值,如果不设置默认值,默认为 nothing。然后初始化的时候我们只传入了一个值,其实也就是为 y 这个属性赋值.

这样的话,看下运行结果:

point(x=10, y=3) 。

没什么问题,y 被赋值为了我们设置的值 3.

那假如我们非要设置 x 呢?会发生什么,比如改写成这样子:

point(1, 2) 。

报错了,错误如下:

typeerror: __init__() takes 2 positional arguments but 3 were given 。

参数过多,也就是说,已经将 init 设置为 false 的属性就不再被算作可以被初始化的属性了.

强制关键字 。

强制关键字是 python 里面的一个特性,在传入的时候必须使用关键字的名字来传入,如果不太理解可以再了解下 python 的基础.

设置了强制关键字参数的属性必须要放在后面,其后面不能再有非强制关键字参数的属性,否则会报这样的错误:

valueerror: non keyword-only attributes are not allowed after a keyword-only attribute (unless they are init=false) 好,我们来看一个例子,我们将最后一个属性设置 kw_only 参数为 true:

?
1
2
3
4
5
6
7
from attr import attrs, attrib, fields
@attrs
class point( object ):
  x = attrib(default = 0 )
  y = attrib(kw_only = true)
if __name__ = = '__main__' :
  print (point( 1 , y = 3 ))

如果设置了 kw_only 参数为 true,那么在初始化的时候必须传入关键字的名字,这里就必须指定 y 这个名字,运行结果如下:

point(x=1, y=3) 。

如果没有指定 y 这个名字,像这样调用:

point(1, 3) 。

那么就会报错:

typeerror: __init__() takes from 1 to 2 positional arguments but 3 were given 所以,这个参数就是设置初始化传参必须要用名字来传,否则会出现错误.

注意,如果我们将一个属性设置了 init 为 false,那么 kw_only 这个参数会被忽略.

验证器 。

有时候在设置一个属性的时候必须要满足某个条件,比如性别必须要是男或者女,否则就不合法。对于这种情况,我们就需要有条件来控制某些属性不能为非法值.

下面我们看一个实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
from attr import attrs, attrib
 
def is_valid_gender(instance, attribute, value):
  if value not in [ 'male' , 'female' ]:
   raise valueerror(f 'gender {value} is not valid' )
@attrs
class person( object ):
  name = attrib()
  gender = attrib(validator = is_valid_gender)
 
if __name__ = = '__main__' :
  print (person(name = 'mike' , gender = 'male' ))
  print (person(name = 'mike' , gender = 'mlae' ))

在这里我们定义了一个验证器 validator 方法,叫做 is_valid_gender。然后定义了一个类 person 还有它的两个属性 name 和 gender,其中 gender 定义的时候传入了一个参数 validator,其值就是我们定义的 validator 方法.

这个 validator 定义的时候有几个固定的参数:

  • instance:类对象
  • attribute:属性名
  • value:属性值
  •  

这是三个参数是固定的,在类初始化的时候,其内部会将这三个参数传递给这个 validator,因此 validator 里面就可以接受到这三个值,然后进行判断即可。在 validator 里面,我们判断如果不是男性或女性,那么就直接抛出错误.

下面做了两个实验,一个就是正常传入 male,另一个写错了,写的是 mlae,观察下运行结果:

?
1
2
person(name = 'mike' , gender = 'male' )
typeerror: __init__() missing 1 required positional argument: 'gender'

ok,结果显而易见了,第二个报错了,因为其值不是正常的性别,所以程序直接报错终止.

注意在 validator 里面返回 true 或 false 是没用的,错误的值还会被照常复制。所以,一定要在 validator 里面 raise 某个错误.

另外 attrs 库里面还给我们内置了好多 validator,比如判断类型,这里我们再增加一个属性 age,必须为 int 类型:

?
1
age = attrib(validator = validators.instance_of( int ))

这时候初始化的时候就必须传入 int 类型,如果为其他类型,则直接抛错:

typeerror: ("'age' must be <class 'int'> (got 'x' that is a <class 'str'>). 。

另外还有其他的一些 validator,比如与或运算、可执行判断、可迭代判断等等,可以参考官方文档:  .

另外 validator 参数还支持多个 validator,比如我们要设置既要是数字,又要小于 100,那么可以把几个 validator 放到一个列表里面并传入:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from attr import attrs, attrib, validators
 
def is_less_than_100(instance, attribute, value):
  if value > 100 :
   raise valueerror(f 'age {value} must less than 100' )
 
@attrs
class person( 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 ))

这样就会将所有的 validator 都执行一遍,必须每个 validator 都满足才可以。这里 age 传入了 500,那么不符合第二个 validator,直接抛错:

valueerror: age 500 must less than 100 。

转换器 。

其实很多时候我们会不小心传入一些形式不太标准的结果,比如本来是 int 类型的 100,我们传入了字符串类型的 100,那这时候直接抛错应该不好吧,所以我们可以设置一些转换器来增强容错机制,比如将字符串自动转为数字等等,看一个实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
from attr import attrs, attrib
def to_int(value):
  try :
   return int (value)
  except :
   return none
@attrs
class point( object ):
  x = attrib(converter = to_int)
  y = attrib()
if __name__ = = '__main__' :
  print (point( '100' , 3 ))

看这里,我们定义了一个方法,可以将值转化为数字类型,如果不能转,那么就返回 none,这样保证了任何可以被转数字的值都被转为数字,否则就留空,容错性非常高.

运行结果如下:

point(x=100, y=3) 。

类型 。

为什么把这个放到最后来讲呢,因为 python 中的类型是非常复杂的,有原生类型,有 typing 类型,有自定义类的类型.

首先我们来看看原生类型是怎样的,这个很容易理解了,就是普通的 int、float、str 等类型,其定义如下:

?
1
2
3
4
5
6
7
8
from attr import attrs, attrib
@attrs
class point( object ):
  x = attrib( type = int )
  y = attrib()
if __name__ = = '__main__' :
  print (point( 100 , 3 ))
  print (point( '100' , 3 ))

这里我们将 x 属性定义为 int 类型了,初始化的时候传入了数值型 100 和字符串型 100,结果如下:

point(x=100, y=3) point(x='100', y=3) 。

但我们发现,虽然定义了,但是不会被自动转类型的.

另外我们还可以自定义 typing 里面的类型,比如 list,另外 attrs 里面也提供了类型的定义:

?
1
2
3
4
5
6
7
from attr import attrs, attrib, factory
import typing
@attrs
class point( object ):
  x = attrib( type = int )
  y = attrib( type = typing. list [ int ])
  z = attrib( type = factory( list ))

这里我们引入了 typing 这个包,定义了 y 为 int 数字组成的列表,z 使用了 attrs 里面定义的 factory 定义了同样为列表类型.

另外我们也可以进行类型的嵌套,比如像这样子:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from attr import attrs, attrib, factory
import typing
 
@attrs
class point( object ):
  x = attrib( type = int , default = 0 )
  y = attrib( type = int , default = 0 )
@attrs
class line( object ):
  name = attrib()
  points = attrib( type = typing. list [point])
if __name__ = = '__main__' :
  points = [point(i, i) for i in range ( 5 )]
  print (points)
  line = line(name = 'line1' , points = points)
  print (line)

在这里我们定义了 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(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)]) 。

可以看到这里我们得到了一个嵌套类型的 line 对象,其值是 point 类型组成的列表.

以上便是一些属性的定义,把握好这些属性的定义,我们就可以非常方便地定义一个类了.

序列转换 。

在很多情况下,我们经常会遇到 json 等字符串序列和对象互相转换的需求,尤其是在写 rest api、数据库交互的时候.

attrs 库的存在让我们可以非常方便地定义 python 类,但是它对于序列字符串的转换功能还是比较薄弱的,cattrs 这个库就是用来弥补这个缺陷的,下面我们再来看看 cattrs 这个库.

cattrs 导入的时候名字也不太一样,叫做 cattr,它里面提供了两个主要的方法,叫做 structure 和 unstructure,两个方法是相反的,对于类的序列化和反序列化支持非常好.

基本转换 。

首先我们来看看基本的转换方法的用法,看一个基本的转换实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
from attr import attrs, attrib
from cattr import unstructure, structur
@attrs
class point( 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)

在这里我们定义了一个 point 对象,然后调用 unstructure 方法即可直接转换为 json 字符串。如果我们再想把它转回来,那就需要调用 structure 方法,这样就成功转回了一个 point 对象.

看下运行结果:

json: {'x': 1, 'y': 2} obj: point(x=1, y=2) 。

当然这种基本的来回转用的多了就轻车熟路了.

多类型转换 。

另外 structure 也支持一些其他的类型转换,看下实例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> 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 )

这里面用到了 tuple、mutablesequence、optional、set 等类,都属于 typing 这个模块,后面我会写内容详细介绍这个库的用法.

不过总的来说,大部分情况下,json 和对象的互转是用的最多的.

属性处理 。

上面的例子都是理想情况下使用的,但在实际情况下,很容易遇到 json 和对象不对应的情况,比如 json 多个字段,或者对象多个字段.

我们先看看下面的例子:

?
1
2
3
4
5
6
7
8
from attr import attrs, attrib
from cattr import structure
@attrs
class point( object ):
  x = attrib( type = int , default = 0 )
  y = attrib( type = int , default = 0 )
json = { 'x' : 1 , 'y' : 2 , 'z' : 3 }
print (structure(json, point))

在这里,json 多了一个字段 z,而 point 类只有 x、y 两个字段,那么直接执行 structure 会出现什么情况呢?

typeerror: __init__() got an unexpected keyword argument 'z' 。

不出所料,报错了。意思是多了一个参数,这个参数并没有被定义.

这时候一般的解决方法的直接忽略这个参数,可以重写一下 structure 方法,定义如下:

?
1
2
3
4
5
6
7
8
9
def drop_nonattrs(d, type ):
  if not isinstance (d, dict ): return d
  attrs_attrs = getattr ( type , '__attrs_attrs__' , none)
  if attrs_attrs is none:
   raise valueerror(f 'type {type} is not an attrs class' )
  attrs: set [ str ] = {attr.name for attr in attrs_attrs}
  return {key: val for key, val in d.items() if key in attrs}
def structure(d, type ):
  return cattr.structure(drop_nonattrs(d, type ), type )

这里定义了一个 drop_nonattrs 方法,用于从 json 里面删除对象里面不存在的属性,然后调用新的 structure 方法即可,写法如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from typing import set
from attr import attrs, attrib
import cattr
 
@attrs
class point( object ):
  x = attrib( type = int , default = 0 )
  y = attrib( type = int , default = 0 )
 
def drop_nonattrs(d, type ):
  if not isinstance (d, dict ): return d
  attrs_attrs = getattr ( type , '__attrs_attrs__' , none)
  if attrs_attrs is none:
   raise valueerror(f 'type {type} is not an attrs class' )
  attrs: set [ str ] = {attr.name for attr in attrs_attrs}
  return {key: val for key, val in d.items() if key in attrs}
def structure(d, type ):
  return cattr.structure(drop_nonattrs(d, type ), type )
json = { 'x' : 1 , 'y' : 2 , 'z' : 3 }
print (structure(json, point))

这样我们就可以避免 json 字段冗余导致的转换问题了.

另外还有一个常见的问题,那就是数据对象转换,比如对于时间来说,在对象里面声明我们一般会声明为 datetime 类型,但在序列化的时候却需要序列化为字符串.

所以,对于一些特殊类型的属性,我们往往需要进行特殊处理,这时候就需要我们针对某种特定的类型定义特定的 hook 处理方法,这里就需要用到 register_unstructure_hook 和 register_structure_hook 方法了.

下面这个例子是时间 datetime 转换的时候进行的处理:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import datetime
from attr import attrs, attrib
import cattr
time_format = '%y-%m-%dt%h:%m:%s.%fz'
@attrs
class event( object ):
  happened_at = attrib( type = datetime.datetime)
cattr.register_unstructure_hook(datetime.datetime, lambda dt: dt.strftime(time_format))
cattr.register_structure_hook(datetime.datetime,
         lambda string, _: 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)

在这里我们对 datetime 这个类型注册了两个 hook,当序列化的时候,就调用 strftime 方法转回字符串,当反序列化的时候,就调用 strptime 将其转回 datetime 类型.

看下运行结果:

event: event(happened_at=datetime.datetime(2019, 6, 1, 0, 0)) json: {'happened_at': '2019-06-01t00:00:00.000000z'} event: event(happened_at=datetime.datetime(2019, 6, 1, 0, 0)) 。

这样对于一些特殊类型的属性处理也得心应手了.

嵌套处理 。

最后我们再来看看嵌套类型的处理,比如类里面有个属性是另一个类的类型,如果遇到这种嵌套类的话,怎样类转转换呢?我们用一个实例感受下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from attr import attrs, attrib
from typing import list
from cattr import structure, unstructure
 
@attrs
class point( object ):
  x = attrib( type = int , default = 0 )
  y = attrib( type = int , default = 0 )
 
@attrs
class color( object ):
  r = attrib(default = 0 )
  g = attrib(default = 0 )
  b = attrib(default = 0 )
 
@attrs
class line( object ):
  color = attrib( type = color)
  points = attrib( type = list [point])
 
if __name__ = = '__main__' :
  line = line(color = color(), points = [point(i, i) for i in range ( 5 )])
  print ( 'object:' , line)
  json = unstructure(line)
  print ( 'json:' , json)
  line = structure(json, line)
  print ( 'object:' , line)

这里我们定义了两个 class,一个是 point,一个是 color,然后定义了 line 对象,其属性类型一个是 color 类型,一个是 point 类型组成的列表,下面我们进行序列化和反序列化操作,转成 json 然后再由 json 转回来,运行结果如下:

?
1
2
3
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 )])

可以看到,我们非常方便地将对象转化为了 json 对象,然后也非常方便地转回了对象.

这样我们就成功实现了嵌套对象的序列化和反序列化,所有问题成功解决! 。

结语 。

本节介绍了利用 attrs 和 cattrs 两个库实现 python 面向对象编程的实践,有了它们两个的加持,python 面向对象编程不再是难事。希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我网站的支持! 。

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

原文链接:https://cuiqingcai.com/6552.html 。

最后此篇关于Python 使用 attrs 和 cattrs 实现面向对象编程的实践的文章就讲到这里了,如果你想了解更多关于Python 使用 attrs 和 cattrs 实现面向对象编程的实践的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com