Python3.dataclass与配置文件

Python3.dataclass与配置文件

dataclass是Python3.7新增的标准库dataclasses的装饰器,可自动生成__init__()等方法,支持类型提示。

yaml是常用的配置文件格式,语法比json更简洁,支持时间格式,在Python中通过第三方库PyYAML进行读写。

用dataclass组织配置文件信息,比用字典更便于程序的后续开发。

动机

对于Python而言,写程序的本质其实是程序员通过代码指导解释器工作。

配置信息通常是键值对形式,用字典表示很自然,但是有个问题:解释器并不知道字典都有哪些“键”。字典像是一个不提供菜单的饭店,老客户点对菜名才有得吃,相对而言,用类去组织配置信息虽然准备工作多一点,但亮出菜单后,点菜就方便很多。

本文简单介绍读取yaml配置文件,以及如何用dataclass组织配置信息。

yaml配置文件

config.yaml文件内容如下:

w: 70
e: 140
s: 0
n: 60
res: 0.5
time: 2022-09-25 00:00:00

则读取方式为:

import yaml

with open('./config.yaml') as f:
    config = yaml.load(f, Loader=yaml.FullLoader)

config字典为:

{'w': 70,
 'e': 140,
 's': 0,
 'n': 60,
 'res': 0.5,
 'time': datetime.datetime(2022, 9, 25, 0, 0)}

可以看到,时间直接解析成了datetime类型。

yaml不细讲了,其详细语法以及更多特性可以自行搜索。

dataclass

config字典得通过如config['w']的形式调用各配置项,方括号和引号打起来麻烦不说,要是打错配置名了在IDE也没有任何提示。如果用类重新组织config内容可以这么做:

class Config(object):
    def __init__(self, config):
        self.w = config['w']
        self.e = config['e']
        self.s = config['s']
        self.n = config['n']
        self.time = config['time']

config = Config(config)

那么接下来就可以通过如config.w的形式调用各配置项,比字典省去方括号和引号,并且在IDE中按下.的时候会出现候选,若是打错配置名也会出现提示。

这个类若是用dataclass实现则是这样:

from dataclasses import dataclass

@dataclass
class Config:
    w: int
    e: int
    s: int
    n: int
    res: float
    time: int

config = Config(**config)

效果完全相同但是更简洁了,不用手写__init__()了。

你可能发现time: int似乎不太对劲,type(config.time)返回的仍然是datetime.datetime,这涉及到Python的类型提示并不实际影响数据类型,因为Python仍是弱类型,这个类型提示是给程序员和IDE看的。

所以time: int改成time: datetime会更合理(要事先from datetime import datetime)。用上合理的类型提示会发生一件事:当你在IDE中输入config.time.,下拉提示就是包含strftimedatetime.datetime类型特有方法。

Python3.dataclass与配置文件_第1张图片

__post_init__()

dataclass支持一种特殊方法__post_init__(),会在自动生成的__init__()被调用中的最后自动被调用,相当于在自动生成的__init__()上做补充。

from datetime import datetime
from dataclasses import dataclass, field

import numpy as np

@dataclass
class Config:
    w: int
    e: int
    s: int
    n: int
    res: float
    time: datetime
    lon: np.ndarray = field(init=False)
    lat: np.ndarray = field(init=False)

    def __post_init__(self):
        w, e, s, n = self.w, self.e, self.s, self.n
        res = self.res
        assert -180 <= w < e <= 180 and -90 <= s < n <= 90
        self.lon = np.linspace(w, e, int((e - w) / res))
        self.lat = np.linspace(s, n, int((n - s) / res))

config = Config(**config)

这个例子中,__post_init__()做了两件事:

  1. w, e, s, n的合法性检验,保证其是合法经纬度范围。
  2. 生成了经纬度向量。lon: np.ndarray = field(init=False)表示lon这个属性不是__init__()初始化过程中产生的,所以不需要传给自动生成的__init__(),该例中在__post_init__()中被赋值。

总结

通过装饰器dataclass可以简化多属性类的编写,本文仅介绍了我认为很实用且便于表述的一种应用,实际上dataclass的功能、参数还有很多没有讲,比如默认值、字符串形式、比较方式、给__post_init__()传参等等,读者可在Python官网上查到详细的文档。

回到dataclass用于配置文件组织这个具体问题上,可拓展的细节还有:

  1. 多层级配置信息的情况下,每个被嵌套的配置层级都需要编写对应的dataclass。用类组织信息本来就不是最便于实现的,只是(我目前认为)最合理规范的,dataclass的角色是简化实现方式。
  2. 想把解析yaml的过程放到类似__post_init__()过程中,但那样自动生成的__init__()有点脱裤子放屁的感觉,还不如手写普通class,有待研究。

你可能感兴趣的:(python3,python)