关于python 数据类构建器(namedtuple, dataclass)

部署运行你感兴趣的模型镜像

最近读到流畅的python第二版时,发现其中新增了不少内容。包括本文中要讨论的数据类构建器。

书中大部分篇幅在介绍collections.namedtuple, typing.NamedTuple(py3.6引入)以及dataclasses.dataclass(py3.7引入) 三种快速构建python数据类的方式和它们的差异。
但是我觉得最有价值的部分在于数据类导致的代码异味和延伸阅读,其中内容对我们何时使用数据类提供了建议和指导。

最后本章可总结为三个问题:

  1. 什么是数据类,和普通类有什么区别
  2. 什么情况下需要数据类
  3. 如何选择合适的数据类构建器

数据类 vs 普通类

这里使用书中的实例来说明

普通类:

class Coordinate:
    def __init__(self, lat, lon):
        self.lat = lat
        self.lon = lon

在声明普通类中, 需要声明__init__ 方法来定义类初始化所需参数. 这种写法有点**“重复”**, 在上面示例中, lat, lon都要写3次. 后续需要新增参数的话也是如此.

除此之外, 普通类也无法自动提供有意义的__repr__ 以及 __eq__等方法:

moscow = Coordinate(55.76, 37.62)
moscow
<__main__.Coordinate at 0x1a738740620> # 信息没什么用
moscow == Coordinate(55.76, 37.62) # Object __eq__ 默认比较实例id
False

数据类正是为了解决上面问题所诞生的, 它们会自动构建__init__, __repr__, __eq__方法, 以及其他有用的功能(例如 _asdict(), _fields等)

python 提供了3种不同的数据类构建器:

  • collections.namedtuple
    最简单的数据类构建方式,从 Python 2.6 开始提供。

    import collections
    Coordinate = collections.namedtuple('Coodinate', ["lat", "lon"])
    
  • typing.NamedTuple
    构建数据类的另一种方式,需要为数据类中的字段添加类型提示(Type Hints)——从 Python 3.5 开始。在 Python 3.6 中,增加了 class 语法。

    import typing
    Coordinate = typing.NamedTuple("Coordinate", lat=float, lon=float)
    

    or

    import typing
    class Coordinate(typing.NamedTuple):
        lat: float
        lon: float
    
  • @dataclasses.dataclass
    一个类装饰器,从 Python 3.7 开始提供。与前两种方式相比,可定制的内容更多,增加了大量选项,可实现更复杂的功能。

    from dataclasses import dataclass
    
    @dataclass
    class Coordinate:
        lat: float
        lon: float
    

什么情况下需要数据类

代码异味问题

书中提到了一个数据类导致代码异味的问题. 这里直接引用书中原文:

无论是自己编写所有代码实现数据类,还是利用本章介绍的类构建器来实现数据类,都要注意一点:这可能表示您的设计存在问题。

在《重构:改善既有代码的设计》第 2 版 一书中,Martin Fowler 和 Kent Beck 提出了 “代码异味” 的目录——列出了代码中可能需要进行重构的模式(Pattern)。其中,题为 “Data Class” 一条的内容如下:

“数据类” 是指那些只包含字段、读写字段的方法,除此之外没有其他功能与逻辑。这样的类被称为“愚蠢的数据持有者”,只用于存储数据,并被其他的类以过于详细的方式进行操作。

由于数据类只是字段集合, 几乎没有额外功能. 这就意味着实例的处理方法可能分散在不同的文件或者模块中, 一旦数据结构发生变化,对维护者来说将是一场噩梦. 所以对于有特殊方法的数据类需要重构, 将操作实例的方法放回类型当中去.

适用场景

为了避免代码异味问题, 数据类仅仅适合几乎没有行为的场景. 书中推荐了两种使用场景:

  • 用作开发初期脚手架
  • 用作中间表述

用作开发初期脚手架: 为了快速启动新模块或者项目, 先实现一个简单的数据类. 数据类拥有自己的方法, 而不是在别的模块或者类当中操作它. 在功能完善后在移除

用作中间表述: 可用于构建要导出为 JSON 或其他交换格式的记录,或者用来保存刚刚导入的、跨越系统边界的数据。Python 的数据类构建器都提供了一种方法或函数,用于将一个数据类实例构建为一个普通的 dict。以将 dict(用 ** 扩展)作为关键字参数,来调用数据类构建器的构造函数。这样的 dict 非常接近 JSON 记录

但是我觉得需要根据不同构造类来区分. 下面是书中关于不同构造类的对比:

功能特性nametupleNamedTupledataclass
可变实例NONOYES
class语法NOYESYES
构造字典x._asdict()x._asdict()dataclasses.asdict(x)
获取字段名x._fieldsx._fields[f.name for f in dataclasses.fields(x)]
获取默认值x._field_defaultsx._field_defaults[f.default for f in dataclasses.fields(x)]
获取字段类型N/Ax._annotations_x._annotations_
更改后,创建新实例x._replace(…)x._replace(…)dataclasses.replace(x, …)
运行时,创建新类nametuple(…)NamedTuple(…)dataclasses.make_dataclass(…)

其中collections.nametupletyping.NamedTuple 二者相似, 构造的都是tuple类型, 意味着元素不可变. 这样一来要实现对数据的操作方法是个非常大的限制. 更符合代码异味中愚蠢的数据持有者定义, 因此这两种构造方式基本上只适用于中间表述.

对于dataclass可以当做实现了默认__init__, __eq__ 等方法的普通类来看待, 不仅仅局限于开发初期的脚手架使用. 其完整签名如下:

@dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
  • init: 作用为生成__init__, 如果用户已实现,则忽略.
  • repr: 生成__repr__, 如用于已实现,则忽略
  • eq: 生成__eq__
  • order: 生成__lt__, __le__, __gt__, __ge__
  • unsafe_hash: 生成__hash__
  • frozen: 让实例不可变

书中延伸阅读中总结不适合数据类的情况:

  • 需要兼容 tuple 或 dict 的 API 时。

  • 需要超出 PEP 484 和 526 提供的类型验证时,或者需要值验证或转换时。

    ——Eric V. Smith, PEP 557 “Rationale”

感觉还可以加上一条:

  • 当你需要大量重载其自动生成的内部方法时

选择合适的数据类构建器

虽然介绍了3中构建器,但是从实际项目开发角度来说,只要python版本高于3.6 建议使用typing.NamedTuple 而不是collections.namedtuple. 最主要的原因就是前者可添加参数类型注释.

import typing
Coordinate = typing.NamedTuple('Coordinate', lat=float, lon=float)

同时typing.NamedTuple还支持class语法, 意味着用户可以继承typing.NameTuple,并重载类方法. 例如__repr__, __eq__等. 相比之下collections.nametuple 则不支持此类操作. 虽然二者都返回tuple 类型实例,但typing.NamedTuple 在开发和扩展上都提供了更高的灵活性和可读性。

可以认为typing.NamedTuple就是collections.namedtuple的上位替代者。

这样一来数据类构建器的选择基本就是typing.NamedTupledataclass 二选一了。从上面章节的数据类对比表格中可以窥见一二

个人觉得从三方面考虑:

  1. python版本要求
  2. 是否要求实例可变
  3. 是否要求紧凑的数据类型

可以总结为一段伪代码:

import typing
import collections
from dataclasses import dataclass

match py_version:
    case v if v >= 2.6 and v <= 3.6:
        return collections.namedtuple
    case 3.6:
        return typing.NamedTuple
    case >= 3.7:
        # 要求不可变实例以及紧凑的数据结构时,选择`typing.NamedTuple`
        if immutable_instance and compact_data_structrue:
            return typing.NamedTuple
        return dataclass
    case _:
        return None

您可能感兴趣的与本文相关的镜像

Python3.11

Python3.11

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值