- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
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 定义的时候有几个固定的参数:
这是三个参数是固定的,在类初始化的时候,其内部会将这三个参数传递给这个 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的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我只是想知道下面的$(*[attr])和$([attr])中哪一个更可取。为什么? 因为两者都在做同样的事情。 $('[onclick]').each(function(i,elem){ cons
长期以来我一直在尝试找出问题所在,但不幸的是无法 如果我这样做 android.enableAapt2=true 代码工作正常,但是删除相同的(应该是强制性的)会抛出一个错误说 \incrementa
使用此代码: $('#ipadmenu section').attr('data-order', hash) 我将 data-order 属性设置为“hash”的值。如何选择具有该属性/值的元素而不是
我有一个用于扩展的组合框监听器。展开后,它会对组合框选项中具有特定类的每个元素执行一些样式设置。所需的更改之一是根据当前属性的值更改属性。使用 this 返回未定义。 expand : funct
性能上有区别吗 :not([attr="value"]) 和 [attr!="value"] ? CSS3 规范是否推荐了一种替代方案? 编辑: CSS3 规范不包含 [attr!="value"]
所以我有一个叫做 say,mySave 的指令,它几乎就是这个 app.directive('mySave', function($http) { return function(scope,
有人可以告诉我有什么区别吗 ?android:attr/colorPrimary 和 ?attr/colorPrimary 无论我使用哪个,结果都是一样的。尽管第一个选项导致android.view.
Xpath问题: 何时使用@和属性,何时不使用。有关系吗?有什么区别 最佳答案 使用//tag[attr]时,将选择所有具有至少一个名为tag的子元素的attr元素。另一方面,使用//tag[@att
android布局xml文件中的?android:attr/和?attr/有什么区别?在不同的情况下我们应该使用哪一个? 最佳答案 1。 ?attr/ 定义并引用您在应用程序中自行定义的属性的值。 2
如果 obj 不存在 obj? 生成一个 nil 所以 obj?.attr 也是。 如果 obj 为 nil,则 obj!.attr 崩溃。 但是如果我确定 obj 在代码的某个点总是存在,那么对我来
有一个这样的 HTML。 Back 1 2 3 Next 为了获得最大的页数,我写了这篇文章。 doc = Nokogiri::HTML(html) doc.xpath('//
我想知道这些标签在 android xml 中如何工作。例如在造型方面 style="?android:attr/buttonBarButtonStyle" 和 style="@android:att
这是我的代码: $("input[name=donationmode]").change(function() { $(".setpaymentOptions").children().add
我已经搜索了一段时间,但没有找到我正在搜索的内容。 事情是这样的,我有两张表,我们称它们为表 A 和 B。当 A 更新时,我需要更新 B 中的属性。例如:A. 电子邮件和 B. 电子邮件。当用户在 A
大家好,在访问一个循环的 php 变量时遇到了一个小问题。我的脚本循环使用来自 mysql 数据库的 x 和 y。它还循环出我无法访问的 id,它显示为未定义。我正在使用鼠标移开功能来检测已循环的每个
我将自己的标签转换为输入。我可以使用 select 为输入选择只读/禁用。我做到了。有用。但不是在 ie8 )). 我阅读了 Angular 和 IE8 的官方文档。我添加了它。但是我的应用程序在 i
我正在使用令人惊叹的 attrs 库以一种非常优雅的方式定义许多对象属性,到目前为止它一直运行得非常棒。 我目前遇到的唯一问题是,有时我想通过引用其他 attr.ib() 属性来定义默认值。如果 na
我注意到 javascript 有几种方法来设置和获取元素的属性。 我不确定它们之间有什么区别。特别是,是否存在跨浏览器问题。 最佳答案 DOM 元素的特性和属性有很大不同,这种差异是一些混淆的根源。
在 4.x 设备上,使用 ?android:attr/selectableItemBackgroundBorderless 的布局文件会导致崩溃,但 ?attr/selectableItemBackg
.attr('disabled', 'disabled') 和 .attr('disabled', true) 在我的代码中都有效,但我只是想知道:两者中哪一个更有效和/或哪一个更常用?真的有区别吗?
我是一名优秀的程序员,十分优秀!