理解 Python 的 Dataclasses(一)

理解 Python 的 Dataclasses(一)-1

如果你正在阅读本文,那么你已经意识到了 Python 3.7 以及它所包含的新特性。就我个人而言,我对 Dataclasses 感到非常兴奋,因为我等了它一段时间了。

本系列包含两部分:

  • Dataclass 特点概述
  • 在下一篇文章概述 Dataclass 的 fields
  • 介绍

    Dataclasses 是 Python 的类(LCTT 译注:更准确的说,它是一个模块),适用于存储数据对象。你可能会问什么是数据对象?下面是定义数据对象的一个不太详细的特性列表:

    • 它们存储数据并代表某种数据类型。例如:一个数字。对于熟悉 ORM 的人来说,模型实例就是一个数据对象。它代表一种特定的实体。它包含那些定义或表示实体的属性。
    • 它们可以与同一类型的其他对象进行比较。例如:一个数字可以是 greater than(大于)、less than(小于) 或 equal(等于) 另一个数字。

    当然还有更多的特性,但是这个列表足以帮助你理解问题的关键。

    为了理解 Dataclasses,我们将实现一个包含数字的简单类,并允许我们执行上面提到的操作。

    首先,我们将使用普通类,然后我们再使用 Dataclasses 来实现相同的结果。

    但在我们开始之前,先来谈谈 Dataclasses 的用法。

    Python 3.7 提供了一个装饰器 dataclass,用于将类转换为 dataclass

    你所要做的就是将类包在装饰器中:

    from dataclasses import dataclass
    
    @dataclass
    class A:
     ...
    

    现在,让我们深入了解一下 dataclass 带给我们的变化和用途。

    初始化

    通常是这样:

    class Number:
    
        def __init__(self, val):
            self.val = val
    
    >>> one = Number(1)
    >>> one.val
    >>> 1
    

    dataclass 是这样:

    @dataclass
    class Number:
        val:int 
    
    >>> one = Number(1)
    >>> one.val
    >>> 1
    

    以下是 dataclass 装饰器带来的变化:

  • 无需定义 __init__,然后将值赋给 selfdataclass 负责处理它(LCTT 译注:此处原文可能有误,提及一个不存在的 d
  • 我们以更加易读的方式预先定义了成员属性,以及类型提示。我们现在立即能知道 valint 类型。这无疑比一般定义类成员的方式更具可读性。
  • Python 之禅: 可读性很重要

    它也可以定义默认值:

    @dataclass
    class Number:
        val:int = 0
    

    表示

    对象表示指的是对象的一个有意义的字符串表示,它在调试时非常有用。

    默认的 Python 对象表示不是很直观:

    class Number:
        def __init__(self, val = 0):
        self.val = val
    
    >>> a = Number(1)
    >>> a
    >>> 
    

    这让我们无法知悉对象的作用,并且会导致糟糕的调试体验。

    一个有意义的表示可以通过在类中定义一个 __repr__ 方法来实现。

    def __repr__(self):
        return self.val
    

    现在我们得到这个对象有意义的表示:

    >>> a = Number(1)
    >>> a
    >>> 1
    

    dataclass 会自动添加一个 __repr__ 函数,这样我们就不必手动实现它了。

    @dataclass
    class Number:
        val: int = 0
    
    >>> a = Number(1)
    >>> a
    >>> Number(val = 1)
    

    数据比较

    通常,数据对象之间需要相互比较。

    两个对象 ab 之间的比较通常包括以下操作:

    • a < b
    • a > b
    • a == b
    • a >= b
    • a import random >>> a = [Number(random.randint(1,10)) for _ in range(10)] #generate list of random numbers >>> a >>> [Number(val=2), Number(val=7), Number(val=6), Number(val=5), Number(val=10), Number(val=9), Number(val=1), Number(val=10), Number(val=1), Number(val=7)] >>> sorted_a = sorted(a) #Sort Numbers in ascending order >>> [Number(val=1), Number(val=1), Number(val=2), Number(val=5), Number(val=6), Number(val=7), Number(val=7), Number(val=9), Number(val=10), Number(val=10)] >>> reverse_sorted_a = sorted(a, reverse = True) #Sort Numbers in descending order >>> reverse_sorted_a >>> [Number(val=10), Number(val=10), Number(val=9), Number(val=7), Number(val=7), Number(val=6), Number(val=5), Number(val=2), Number(val=1), Number(val=1)]

      dataclass 作为一个可调用的装饰器

      定义所有的 dunder(LCTT 译注:这是指双下划线方法,即魔法方法)方法并不总是值得的。你的用例可能只包括存储值和检查相等性。因此,你只需定义 __init____eq__ 方法。如果我们可以告诉装饰器不生成其他方法,那么它会减少一些开销,并且我们将在数据对象上有正确的操作。

      幸运的是,这可以通过将 dataclass 装饰器作为可调用对象来实现。

      从官方文档来看,装饰器可以用作具有如下参数的可调用对象:

      @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
      class C:
       …
      
    • init:默认将生成 __init__ 方法。如果传入 False,那么该类将不会有 __init__ 方法。
    • repr__repr__ 方法默认生成。如果传入 False,那么该类将不会有 __repr__ 方法。
    • eq:默认将生成 __eq__ 方法。如果传入 False,那么 __eq__ 方法将不会被 dataclass 添加,但默认为 object.__eq__
    • order:默认将生成 __gt____ge____lt____le__ 方法。如果传入 False,则省略它们。
    • 我们在接下来会讨论 frozen。由于 unsafe_hash 参数复杂的用例,它值得单独发布一篇文章。

      现在回到我们的用例,以下是我们需要的:

    • __init__
    • __eq__
    • 默认会生成这些函数,因此我们需要的是不生成其他函数。那么我们该怎么做呢?很简单,只需将相关参数作为 false 传入给生成器即可。