说说Python 面向对象编程

python面向对象编程(基础篇) 当然不是,在son的构造方法中,我首先调用的是父亲的构造方法这时候son有了父亲的两个属性,之后又调用了母亲的构造方法,这时候由于母亲的eye属性和父亲的重复了,就像我们首先定义a=1之后我们又定义了a=2那么最终a的值一定是2。char a[]来定义单个字符的数组(字符串),a[][]来定义字符串的集合,但是在定义某些数据类型时,我们发现例如学生,他不止有字符串类型的姓名、性别,更有整数类型的学号、年龄等,甚至还有data类型的出生日期,离返校时间。注意在方法中我们要固定的传入一个self,这是规则。 阅读详情

 

640?wx_fmt=png

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 三个数值,定义如下:

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 这三个变量一写就写了三遍。

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

color = Color(255, 255, 255)print(color)

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

<__main__.Color object at 0x103436f60>

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

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__ 等各种方法来实现对象之间的比较呢?照样需要继续定义成类似这样子的形式:

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,前者是主要用来修饰一个自定义类的,后者是定义类里面的一个字段的。有了它们,我们就可以将上文中的定义改写成下面的样子:

from attr import attrs, attrib@attrsclass 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 修饰的类定义是这样子:

from attr import attrs, attrib@attrsclass SmartClass(object):    a = attrib()    b = attrib()

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

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 主要是用来做属性定义的,这个就记住它们两个的用法就好了。

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

s = attributes = attrsib = attr = attrib

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

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

from attr import s, ib@sclass 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 的搭配,感觉可读性更好一些,当然这个看个人喜好。

所以总结一下:

OK,理清了这几部分内容,我们继续往下深入了解它的用法吧。

声明和比较

在这里我们再声明一个简单一点的数据结构,比如叫做 Point,包含 x、y 的坐标,定义如下:

from attr import attrs, attrib@attrsclass Point(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('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: FalseEqual: TrueNot Equal(ne): TrueLess Than(lt): TrueLess or Equal(le): True TrueGreater Than(gt): True TrueGreater or Equal(ge): True

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

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

属性定义

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

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

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

from attr import attrs, attrib, fields@attrsclass 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 对象,包含了各个参数,下面详细解释下几个参数的含义:

属性名

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

x = attrib()

那么其属性名就是 x。

默认值

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

from attr import attrs, attrib, fields@attrsclass 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 这个参数,看下运行结果:

Point(x=1, y=3)Point(x=1, y=100)

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

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

Point()

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

TypeError: __init__() missing 1 required positional argument: 'x'

所以说,如果一个属性,我们一旦没有设置默认值同时没有传入的话,就会引起错误。所以,一般来说,为了稳妥起见,设置一个默认值比较好,即使是 None 也可以的。

初始化

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

from attr import attrs, attrib@attrsclass 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:

from attr import attrs, attrib, fields@attrsclass 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 这个参数会被忽略。

验证器

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

下面我们看一个实例:

from attr import attrs, attribdef is_valid_gender(instance, attribute, value):    if value not in ['male', 'female']:        raise ValueError(f'gender {value} is not valid')@attrsclass 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,观察下运行结果:

Person(name='Mike', gender='male')TypeError: __init__() missing 1 required positional argument: 'gender'

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

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

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

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

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

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

另外还有其他的一些 Validator,比如与或运算、可执行判断、可迭代判断等等,可以参考官方文档:https://www.attrs.org/en/stable/api.html#validators。

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

from attr import attrs, attrib, validatorsdef is_less_than_100(instance, attribute, value):    if value > 100:        raise ValueError(f'age {value} must less than 100')@attrsclass 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,那这时候直接抛错应该不好吧,所以我们可以设置一些转换器来增强容错机制,比如将字符串自动转为数字等等,看一个实例:

from attr import attrs, attribdef to_int(value):    try:        return int(value)    except:        return None@attrsclass 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 等类型,其定义如下:

from attr import attrs, attrib@attrsclass 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 里面也提供了类型的定义:

from attr import attrs, attrib, Factoryimport typing@attrsclass Point(object):    x = attrib(type=int)    y = attrib(type=typing.List[int])    z = attrib(type=Factory(list))

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

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

from attr import attrs, attrib, Factoryimport typing@attrsclass Point(object):    x = attrib(type=int, default=0)    y = attrib(type=int, default=0)@attrsclass 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,两个方法是相反的,对于类的序列化和反序列化支持非常好。

基本转换

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

from attr import attrs, attribfrom cattr import unstructure, structure@attrsclass 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 也支持一些其他的类型转换,看下实例:

>>> 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 多个字段,或者对象多个字段。

我们先看看下面的例子:

from attr import attrs, attribfrom cattr import structure@attrsclass 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 方法,定义如下:

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 方法即可,写法如下:

from typing import Setfrom attr import attrs, attribimport cattr@attrsclass 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 转换的时候进行的处理:

import datetimefrom attr import attrs, attribimport cattrTIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'@attrsclass 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))

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

嵌套处理

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

from attr import attrs, attribfrom typing import Listfrom cattr import structure, unstructure@attrsclass Point(object):    x = attrib(type=int, default=0)    y = attrib(type=int, default=0)@attrsclass Color(object):    r = attrib(default=0)    g = attrib(default=0)    b = attrib(default=0)@attrsclass 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 转回来,运行结果如下:

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 对象,然后也非常方便地转回了对象。

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


另外上周五送书(重磅三重福利!价值16880元大数据和人工智能全套教程,限时免费!文末送书)的中奖的三位小伙伴,麻烦火速跟我们联系!后台输入:【小助手】,尽快联系好快递书!截止日期7/9,12:00

640?wx_fmt=jpeg


全网详细解说:java对象转JSONObject和JSONString、JSONObject转java对象和JSONString,JSONString转JSONObject和java对象 阅读详情

相关推荐

对象 Object 转换工具

使用此工具类,对于Object和byte[] 数组, 16进制和String之间的转换

这可能是 Python 面向对象编程的最佳实践

“ 阅读本文大概需要 20 分钟。 ”Python 是支持面向对象的,很多情况下使用面向对象编程会使得代码更加容易扩展,并且可维护性更高,但是如果你写的多了或者某一对象非...

静觅 651

Vivado与VCS+Verdi联合仿真环境搭建全攻略

本文详细介绍了如何搭建Vivado与VCS+Verdi联合仿真环境,旨在解决Vivado Simulator在大型FPGA验证中速度慢、调试弱的问题。通过指导工具版本配对、环境变量设置、关键参数配置及Xilinx仿真库编译,帮助工程师高效整合Vivado的设计管理、VCS的高速仿真与Verdi的深度调试能力,从而显著提升验证效率。

weixin_29219189的博客 577

Object转对象 对象转Object Object转json json转对象

<dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> ...

若星汉灿烂 2395

90nm CMOS工艺下6μW超低功耗LDO设计实战:从原理图到版图全解析

本文详细解析了在90nm CMOS工艺下设计一款静态功耗仅为6μW的超低功耗LDO(低压差线性稳压器)的全过程。文章深入探讨了如何通过改进型翻转电压跟随器(FVF)架构实现无输出电容设计,在满足极低功耗和芯片面积高效(chip-area-efficient)要求的同时,确保环路稳定性与快速瞬态响应,为物联网、可穿戴设备等应用提供关键电源管理解决方案。

weixin_29196613的博客 278

ORCAD原理图关联封装时出现ERROR(ORCIS-6245)、ERROR(ORCIS-6013)问题

1、当你进行如下操作时: 系统先是提示你出现ERROR(ORCIS-6245)然后再确定后再出现ERROR(ORCIS-6013)问题 是由于你的数据库的路径不对。 具体操作方法如下:1、先找到你的CAPTURE.INI文件。 2、用记事本打开CAPTURE.INI。找到下图路径 3、在你的Orcad capture CIS原理图软件中点击 Options------>CIS con...

weixin_42304393的博客 2万+

Java 面向对象(八)

学习内容: 1.掌握Object类的作用及其主要方法的作用。2. 掌握包装类的作用以及自动装箱和拆箱的操作。3.掌握匿名内部类的使用。

亮点菌 1077

存内计算路线再获加持,清华存内芯片登Science

通过这种方法,可以比较有效的控制二氧化铪忆阻层中的微观变化,以及内部的温度和电场,使得器件具有非常优异的电学特性,而且可以在工厂里大规模生产,推动了高性能忆阻器阵列的诞生。团队搭建了全硬件构成的完整存算一体系统,如下图7所示,在系统里集成了多个忆阻器阵列,并在该系统上高效运行了卷积神经网络算法,成功验证了图像识别功能,证明了存算一体架构全硬件实现的可行性,并且成功实现了以更小的功耗和更低的硬件成本完成复杂的计算[6]。根据测试,在一般神经网络中权重更新的最后阶段,调整步长非常接近0,如图9(C)所示,

PXX12345678的博客 1557

对象变成了[object Object]

如果你的对象变成了[objectObject],说明他被强制类型转换为了字符串。 let a = {}; console.log(a+''); // [object Object] console.log(typeof (a+'')); // string 可是有的方法,确实只接受字符串参数,所以要将对象转化为对象字符串,进行传递 console.log(JSON.stringify(...

路漫漫的博客 5234

【软考高级架构设计师】——2025年上半年软考真题(回忆版)

a)异构计算和算法优化b)资源抽象和容器控制c)安全和隔离(大致是这个意思)

小志的博客 4836

百度贴吧爬虫实战:完整实现与使用指南

使用 csv 模块创建写入器,首先写入表头("标题" 和 "链接"),然后遍历帖子字典,将每个标题和链接作为一行写入文件。首先检查内容是否为空,然后通过查找 JSON 结构的起始({)和结束(})位置,提取有效的 JSON 片段。接着遍历帖子列表中的每个条目,提取标题文本和对应的链接,对于相对路径的链接会自动补全为完整的 URL。从 JSON 提取标题和链接此方法专门用于从解析后的 JSON 数据中提取帖子标题和对应的链接。提取到的标题和链接以字典形式返回,键为标题,值为对应的完整链接。

qq_63736560的博客 2018

【Python基础】Python面向对象编程

​ Python面向对象编程(简称OOP)是Python语言中最重要的特性之一。它是一种通过“对象”来设计软件的方法。它通过将数据和操作数据的方法封装在对象中,使得程序更加模块化、可重用和易于维护。它遵循一切皆对象的理念,可以将现实世界的事物使用类与实例进行模拟。​ 对于初学者来说,理解面向对象编程的基本原理和Python中的实现方式是非常有帮助的。本文旨在为初学者提供一个易于理解的Python面向对象编程介绍,并探讨其在实际编程中的应用和优势。什么是Python?

分享是一种境界,勤于分享,乐于分享! 2136

SAP_SD模块更改销售订单定价简介

方式优点缺点适用场景MASS简单,无需开发不能触发重新定价逻辑修改特定字段BDC (VA02)可模拟人工操作维护复杂,速度慢少量订单批改稳定、安全需ABAP支持推荐用于批量重新定价VBOF标准重算返利仅适返利类条件返利更新通过本文,我们了解了 SAP 的[SAP_SD模块更改销售订单定价]的基本概念和应用方法。希望这些信息能帮助您在实际工作中更好地使用 SAP 系统,提高工作效率和管理水平。更多SAP文章请点击更多SAP文章更新,大家一起学习进步!!!

我小时候很黑的博客 716

Python 面向对象编程

在Python中,类(Class)是面向对象编程(Object-Oriented Programming, OOP)的基础概念。类是创建对象(Object)的蓝图,它定义了对象的属性和方法。每个对象都是类的一个实例,具有类定义的属性和行为。类是一种用户自定义的数据结构,它包含了一组属性(成员变量)和行为(成员方法)。在Python中,可以使用class关键字来定义一个类,并使用def关键字来定义类的成员方法。使用class关键字定义一个类,类名作为该类的标识符。

2201_75479454的博客 2593

基于Python的军事装备信息爬虫实战:Scrapy+Playwright+智能反反爬技术

本文将详细介绍如何使用Python构建一个高效的军事装备信息爬虫系统。我们将从基础的HTTP请求开始,逐步深入到使用Scrapy框架、Playwright浏览器自动化以及最新的反反爬技术。文章包含完整的代码实现、性能优化策略以及数据处理方案,旨在为读者提供一个全面的军事信息爬取解决方案。关键词:Python爬虫、军事装备、Scrapy、Playwright、反反爬。

2201_76125261的博客 1157

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Python面向对象编程

Python不只是解释性语言,也是一门面向对象的编程语言。本文先介绍面向对象编程,再逐步讲解类和对象的定义、属性及方法。类使得程序设计更加抽象,通过类的继承及其他常用方法,可以让程序语言更接近人类的语言。

Morse_Chen的博客 2107

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

python 面向对象编程

带你走进 python 面向对象编程的世界

不能再留遗憾了的博客 4613

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

【Python】系列之面向对象编程

对象(class)是一种抽象的概念,上面定义的对象Person,指的就是学生这个概念,而实例(instance)则指一个个具体的对象,例如上面的张三丰和l张无忌就是两个具体的Person,也就是实例从上面的案例可以看出,面向对象的程序设计思想其实就是抽象出对象(class),然后根据对象创建实例(instance)最后,面向对象的抽象程度比函数高,因为一个对象既包含数据,也包含操作数据的方法,封装、继承、多态是面向对象的三大特点。

康师傅没有眼泪 6513

python学习-面向对象编程

在 Python 中,可以使用class关键字来定义类。以下是一个简单的类定义示例:​在上面的示例中,我们定义了一个名为Person的类,它有两个属性name和age,以及一个方法say_hello。面向对象编程是一种强大的编程范式,它可以提高代码的可维护性、可扩展性和可复用性。Python 提供了丰富的面向对象编程特性,使得开发者可以更加方便地进行面向对象编程。在实际开发中,可以根据具体需求选择合适的面向对象编程特性,以提高代码的质量和效率。

wu73guang5jian的博客 1658

【python】python进阶——面向对象编程

Python 从设计之初就是一门面向对象的语言,它的核心机制建立在“一切皆对象”的基础上,但它也优雅地支持函数式、过程式等多种编程范式,是一门真正的多范式语言。

weixin_51924657的博客 1942

Python语言的面向对象编程

在Python中,可以使用class关键字定义一个类。如下所示:```pythonclass Dog:definitself.name = name # 属性self.age = age # 属性def bark(self): # 方法")```pythonprint(my_dog.name) # 输出:Buddymy_dog.bark() # 输出:Buddy says woof!首先,我们定义一个Book类,用于表示书籍的信息。

dasinigeguisun01的博客 1398

Python中面向对象编程思想

本文旨在深入探讨Python中的面向对象编程(OOP)概念,包括其核心思想、实现方式以及最佳实践。我们将从OOP的基础知识入手,逐步深入到更高级的特性,如继承、多态和封装,并探讨如何在实际编程中应用这些概念。面向对象编程(OOP)是一种以对象为核心的编程范式,它通过类和对象的概念来设计软件,提高了代码的可读性和可维护性。OOP的核心在于封装,它将数据和相关操作封装在一起,隐藏内部实现细节,仅对外暴露必要的接口。继承允许新类继承现有类的属性和方法,从而扩展或覆盖原有功能。

Wayne_zzx1112233的博客 910
上一篇: 百万奖金,无限可能,科大讯飞iFLYTEK A.I.开发者大赛重磅开启!
下一篇: 因为Python的这3个优点,我的薪资涨了一倍
菜鸟学Python
博客等级 码龄9年 8086粉丝 571原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值