~~番外:说说Python 面向对象编程~~

进击のpython


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 0x03722A70>

我们知道,在 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 面向对象而专门诞生的库

没错,就是 attrscattrs 这两个库

有了 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
    
    
    @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 修饰的类定义是这样子:

    from attr import attrs, attrib
    
    @attrs
    class 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 = attrs
    ib = attr = attrib

    也就是说,attrs 可以用 s 或 attributes 来代替

    attrib 可以用 attr 或 ib 来代替。

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

    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 的坐标,定义如下:

    from attr import attrs, attrib
    
    @attrs
    class 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: 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) 两个元组

    那么元组之间的比较逻辑又是怎样的呢,这里就不展开了

    如果不明白的话可以参考官方文档:https://docs.python.org/3/library/stdtypes.html#comparisons


  • 属性定义

    现在看来,对于这个类的定义莫过于每个属性的定义了,也就是 attrib 的定义

    对于 attrib 的定义,我们可以传入各种参数,不同的参数对于这个类的定义有非常大的影响

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

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

    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。

  • 属性名

    对于默认值,如果在初始化的时候没有指定

    那么就会默认使用默认值进行初始化,我们看下面的一个实例:

    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 这个参数,看下运行结果:

    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
    
    @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:

    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 这个参数会被忽略。


  • 验证器

    有时候在设置一个属性的时候必须要满足某个条件

    比如性别必须要是男或者女,否则就不合法

    对于这种情况,我们就需要有条件来控制某些属性不能为非法值

    下面我们看一个实例:

    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,观察下运行结果:

    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, 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

    那这时候直接抛错应该不好吧

    所以我们可以设置一些转换器来增强容错机制

    比如将字符串自动转为数字等等,看一个实例:

    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 等类型,其定义如下:

    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 里面也提供了类型的定义:

    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 定义了同样为列表类型

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

    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

    两个方法是相反的,对于类的序列化和反序列化支持非常好


  • 基本转换

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

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

    >>> 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, 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 方法,定义如下:

    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 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 转换的时候进行的处理:

    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))

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


  • 嵌套处理

    最后我们再来看看嵌套类型的处理

    比如类里面有个属性是另一个类的类型,如果遇到这种嵌套类的话

    怎样类转转换呢?我们用一个实例感受下:

    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 转回来,运行结果如下:

    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 对象

    然后也非常方便地转回了对象。

    这样我们就成功实现了嵌套对象的序列化和反序列化

    所有问题成功解决!


*常用技巧*
*值得拥有*
上一篇:廖雪峰Python教程中简单ORM代码解读


下一篇:vue组件通讯7种方式