数据类创建与使用

前言

很多时候我们经常需要处理一条包含多个字段的数据,例如用户(name, age, sex),在将它们写入文件或数据库之前,你会怎么处理它们的存储?最开始的时候,我采用元组和列表,不同索引位置的值代表不同的字段,并将它们存储在一个总的列表中,像下面这样:

datas = [
    ('Bob', 12, 'man'),
    ('Alice', 15, 'women'),
    ...
]

但是很快发现这样结构化的数据可读性并不高,一旦数据字段很多,取出一条数据以后你往往不知道每个位置代表什么。因此,我改用了dict ,新的数据如下:

datas = [
    {
        'name': 'Bob',
        'age': 12,
        'sex': 'man'
    },
    {
        'name': 'Alice',
        'age': 15,
        'sex': 'women'
    }
]

的确提高了可读性,并且在数据交付的时候dictjson可以无缝衔接,这样传输数据给别人也很方便。但新的问题在于,dict这个数据结构没有明确指出来这个数据是什么。你只看到:

{
    'name': 'Bob',
    'age': 12,
    'sex': 'man'
}

但他是什么数据类型呢?换句话说,就是缺少一个标签,指明这个数据,一旦我们有多个数据交叉使用,同时需要做类型验证,如用户数据和商品数据在一个管道中,做鉴别筛选的时候,dict就显得力不从心了。由此,数据类就派上用场了。

namedtuple

光听名字就知道,这是一个有了名字的元组,它可以同时兼顾元组和字典两者的特性,实现高效的数据存储。

定义与初始化

from collections import namedtuple

obj = namedtuple(typename, field_names, verbose=False, rename=False)
  • typename:元组名称(可以理解为数据类类名)
  • field_names:元组中元素(字段)的名称,有两种传入方式
    • 空格隔开的字符串,e.g. "name age sex"
    • 逗号隔开的序列(推荐),e.g. ['name','age', 'sex']
  • rename:如果元素名称中含有 python 的关键字,则必须设置为 rename=True
  • verbose:默认就好

基本使用

下面的代码给出了namedtuple的基础使用,同时在每一步添加了相应的注释(包括作用和好处),建议仔细阅读

from collections import namedtuple

User = namedtuple('User', ['name', 'sex', 'age'])  # 这一步相当于定义了一个User类
user = User(name='Bob', sex='man', age=12)  # 实例化对象

print( user._fields )  # 获取所有字段名

# 也可以通过一个序列来创建一个User对象,通过_make方法
user = User._make(['Bob', 'man', 12])   # 这个接口的好处在于可以无缝转换序列数据,这比dict要方便

# 获取用户的属性,这种属性的方式相比于`ditc["name"]`,我个人更喜欢一些
print(user.name)
print(user.sex)
print(user.age)

# 修改对象属性,通过_replace方法,如果你非要修改字段内容,记得要返回值,namedtuple本身是不可变的,这是创建了一个新的
user = user._replace(age=22)

# 将User对象转换成字典,通过_asdict方法,实现了和dict之间的无缝衔接
print(user._asdict())  # 新的版本中已经把ordereddict和dict合并了,因此直接返回dict
  • 多数据处理

在实际情况中,我们很可能拿到的是一大堆列表数据,现在要基于namedtuple进行转换

from collections import namedtuple
users = [
    ('Bob','man',12),
    ('Alice', 'women', 15),
    ('CP', 'superman', 250)
]
User = namedtuple('User',['name','sex','age'])
for user in users:
    user = User._make(user)
    print(user)

在一般的数据处理中,namedtuple已经能够很好地满足基本的数据需求了,但简洁是它的特性,也是它的软肋,例如无法对输入数据类型、数据取值范围进行校验,无法指定可选字段等,因此,我们还需要更awesome的数据类来处理

dataclass & pydantic

dataclass一听名字就知道是专门的数据类,在python3.7的时候被加入到标准库中,使用的时候通过from dataclasses import dataclass即可,虽然它很方便,但由于python3.7之后版本的限制,这里我直接讲一个与之类似的专门的数据类库pydanticpython3.6+

  • 安装
pip install pydantic

基本使用

from typing import Optional
from pydantic import validator
from pydantic.dataclasses import dataclass
from pydantic import BaseModel  

# class User(BaseModel):   # 也可以通过继承BaseModel的方式初始化

@dataclass(frozen=True)  # frozen为True表示数据初始化后不可更改
class User:
    name: str   # 指定数据类型
    age: int
    sex: str = 'man'  # 设置默认值
    address: Optional[str] = None  # 可选字段类型

    @validator("age")
    def age_value_range(cls, v):
        if not (0 <= v <= 200):
            # 年龄不在合理范围内
            raise ValueError("Age can not be set out of [0,200]")
        return v
    
user1 = User(name='GentleCP', age=15, sex='man')
print(user1.name)  # GentleCP
user2 = User(name='CP', age=100, sex='man', address='Beijing')

上面的一个例子基本解释了pydantic的基础使用,但pydantic作为一个强大的数据类接口,自然还有更多的特性,因为很多内容我们实际上也用不到,这里我仅列举常见可能会使用的,如果感兴趣的可以自己去官网深入了解。

这里要特别说明一下,虽然BaseModeldataclass两种方式十分相似,但功能上还是存在差异的,如继承BaseModel的类拥有很多Model属性,如dict(),json(),parse_obj()等,但dataclass是没有的,它只包含必要的数据类型和对数据的验证操作,因此,如果你希望对你的数据有更好的操作,我建议选用BaseModel

基本数据类型

用于初始化的时候指定数据的类型,常用的如下:

from pydantic import BaseModel
from typing import Dict, List, Sequence, Set, Tuple, Union

class Demo(BaseModel):
    a: int # 整型
    b: float # 浮点型
    c: str # 字符串
    d: bool # 布尔型
    e: List[int] # 整型列表
    f: Dict[str, int] # 字典型,key为str,value为int
    g: Set[int] # 集合
    h: Tuple[str, int] # 元组
    i: Union[str, int]  # 可以是str或int

字典互转

以下操作基于继承BaseModel

  • 传入字典初始化数据
d = {
    'name' : 'CP',
    'age' : 15,
    'sex' : 'man'
}
user = User(**d)
# user = User.parse_obj(d)  # 直接传入字典
# user = User.parse_raw(str(d))  # 解析字符串字典
  • 数据转换成字典
user.dict() 
# user.josn()  # 转换成json数据

总结

本文主要讲解了python中对数据类的处理,从最开始简单的元组、列表,到更具结构化的字典,再到namedtuplepydantic,虽然它们一个比一个更强大,但并不意味着所有的场景中都要使用某一个,更好的方案是根据自己的需求去使用,例如如果只是简单的2-3个字段的数据,实际上并没有必要大费周章,直接用一个元组或列表即可,但如果一个数据包含多个字段,且本身具有特殊含义,这时候就要考虑namedtuplepydantic了。