Python 3.7中引入的新功能之一是Data类。 它们旨在自动生成用于存储数据的类的代码。 尽管它们使用其他工作机制,但可以将它们与“具有默认值的可变命名元组”进行比较。
引言
以上所有示例均要求Python 3.7或更高版本才能运行。
大多数python开发人员必须定期编写以下类:
class RegularBook: def __init__(self, title, author): self.title = title self.author = author
在此示例中,冗余已经可见。 标题和作者标识符使用了多次。 实际类还将包含重写的方法__eq__
和__repr__
。
@dataclass
模块包含@dataclass
装饰器。 使用它,类似的代码将如下所示:
from dataclasses import dataclass @dataclass class Book: title: str author: str
重要的是要注意类型注释是 必需的 。 所有没有类型标记的字段都将被忽略。 当然,如果您不想使用特定类型,则可以从typing
模块中指定Any
。
结果是什么? 您将自动获得一个类,该类具有已实现的方法__init__
, __init__
__repr__
__init__
, __repr__
__str__
和__eq__
。 此外,它将是一个常规类,您可以从其继承或添加任意方法。
>>> book = Book(title="Fahrenheit 451", author="Bradbury") >>> book Book(title='Fahrenheit 451', author='Bradbury') >>> book.author 'Bradbury' >>> other = Book("Fahrenheit 451", "Bradbury") >>> book == other True
替代品
元组或字典
当然,如果结构很简单,则可以将数据保存在字典或元组中:
book = ("Fahrenheit 451", "Bradbury") other = {'title': 'Fahrenheit 451', 'author': 'Bradbury'}
但是,这种方法有缺点:
- 必须记住,变量包含与此结构相关的数据。
- 如果是字典,则必须跟踪键的名称。 字典
{'name': 'Fahrenheit 451', 'author': 'Bradbury'}
初始化也将在形式上正确。 - 对于元组,您必须跟踪值的顺序,因为它们没有名称。
有一个更好的选择:
元组
from collections import namedtuple NamedTupleBook = namedtuple("NamedTupleBook", ["title", "author"])
如果我们使用以这种方式创建的类,则实际上与使用数据类是一样的。
>>> book = NamedTupleBook("Fahrenheit 451", "Bradbury") >>> book.author 'Bradbury' >>> book NamedTupleBook(title='Fahrenheit 451', author='Bradbury') >>> book == NamedTupleBook("Fahrenheit 451", "Bradbury")) True
但是,尽管具有普遍的相似性,命名元组还是有其局限性。 它们来自命名元组仍然是元组的事实。
首先,您仍然可以比较不同类的实例。
>>> Car = namedtuple("Car", ["model", "owner"]) >>> book = NamedTupleBook("Fahrenheit 451", "Bradbury")) >>> book == Car("Fahrenheit 451", "Bradbury") True
第二,命名元组是不可变的。 在某些情况下,这很有用,但我想获得更大的灵活性。
最后,您可以对命名元组和常规元组进行操作。 例如,进行迭代。
其他项目
如果不限于标准库,则可以找到该问题的其他解决方案。 特别是项目attrs 。 它可以做的甚至比数据类还多,并且可以在2.7和3.4等旧版本的python上运行。 但是,它不是标准库的一部分可能不方便
创作
您可以使用@dataclass
装饰器来创建数据类。 在这种情况下,使用类型注释定义的类的所有字段都将在结果类的相应方法中使用。
另外,还有make_dataclass
函数,其功能类似于创建命名元组。
from dataclasses import make_dataclass Book = make_dataclass("Book", ["title", "author"]) book = Book("Fahrenheit 451", "Bradbury")
预设值
一项有用的功能是轻松向字段添加默认值。 仍然无需重新定义__init__
方法,只需直接在类中指定值即可。
@dataclass class Book: title: str = "Unknown" author: str = "Unknown author"
在生成的__init__
方法中将考虑它们
>>> Book() Book(title='Unknown', author='Unknown author') >>> Book("Farenheit 451") Book(title='Farenheit 451', author='Unknown author')
但是,与常规类和方法一样,您需要小心使用可变的默认值。 例如,如果您需要使用列表作为默认值,则可以使用另一种方法,但是下面提供了更多方法。
此外,重要的是要监视确定具有默认值的字段的顺序,因为它与__init__
方法中的字段顺序完全匹配
不变数据类
命名元组的实例是不可变的。 在许多情况下,这是一个好主意。 对于数据类,您也可以这样做。 在创建类时只需指定FrozenInstanceError
frozen=True
参数,并且如果尝试更改其字段,则将FrozenInstanceError
异常。
@dataclass(frozen=True) class Book: title: str author: str
>>> book = Book("Fahrenheit 451", "Bradbury") >>> book.title = "1984" dataclasses.FrozenInstanceError: cannot assign to field 'title'
数据类别设定
除了frozen
参数外, @dataclass
装饰器还具有其他参数:
init
:如果为True
(默认),则生成__init__
方法。 如果该类已经定义了__init__
方法,则将忽略该参数。repr
:(默认情况下)启用__repr__
方法的创建。 生成的字符串包含类名称以及该类中定义的所有字段的名称和表示形式。 在这种情况下,可以排除各个字段(请参见下文)eq
:(默认情况下)启用__eq__
方法的创建。 比较对象的方式就好像它们是包含相应字段值的元组一样。 此外,还将检查类型匹配。order
启用(默认为关闭) __lt__
__le__
, __gt__
__ge__
__le__
, __gt__
和__ge__
。 以与字段值的相应元组相同的方式比较对象。 同时,还检查对象的类型。 如果指定了order
但未指定eq
,则将引发ValueError
异常。 同样,该类不应包含已经定义的比较方法。unsafe_hash
影响__hash__
方法的生成。 该行为还取决于参数eq
和frozen
的值
自定义各个字段
在大多数标准情况下,这不是必需的,但是可以使用字段功能自定义数据类的行为,直到各个字段为止。
可修改的默认值
上面提到的一种典型情况是使用列表或其他可变的默认值。 您可能需要一个包含书列表的“书架”类。 如果运行以下代码:
@dataclass class Bookshelf: books: List[Book] = []
解释器将报告错误:
ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory
但是,对于其他可变值,此警告将不起作用,并会导致错误的程序行为。
为避免出现问题,建议使用field
函数的default_factory
参数。 它的值可以是任何不带参数的被调用对象或函数。
该类的正确版本如下所示:
@dataclass class Bookshelf: books: List[Book] = field(default_factory=list)
其他选择
除了指定的default_factory
,字段函数还具有以下参数:
default
: default
值。 此参数是必需的,因为对field
的调用将替换默认字段值。init
:(默认)启用__init__
方法中的字段使用repr
:启用(默认) __repr__
方法中的字段使用compare
包含(默认)比较方法( __le__
, __le__
和其他方法)中该字段的使用hash
:可以是布尔值,也可以是None
。 如果为True
,则该字段用于计算哈希。 如果指定了None
(默认),则使用compare
参数的值。
对于给定的compare=True
指定hash=False
的原因之一可能是计算字段哈希值的难度,而比较则是必需的。metadata
:自定义词典或None
。 该值包装在MappingProxyType
因此它变为不可变的。 数据类本身不使用此参数,并且将其用于第三方扩展。
初始化后的处理
如果在类中定义了自动生成的__init__
方法,则调用__post_init__
方法。 通常,它以self.__post_init__()
的形式调用,但是,如果在类InitVar
定义了InitVar
类型的变量,它们将作为方法参数传递。
如果尚未生成__init__
方法,则不会调用__post_init__
。
例如,添加生成的书籍说明
@dataclass class Book: title: str author: str desc: str = None def __post_init__(self): self.desc = self.desc or "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury')
仅用于初始化的参数
与__post_init__
方法关联的可能性__post_init__
是仅用于初始化的参数。 如果在声明字段时将InitVar
指定为其类型,则其值将作为__post_init__
方法的参数传递。 数据类中不会使用此类字段。
@dataclass class Book: title: str author: str gen_desc: InitVar[bool] = True desc: str = None def __post_init__(self, gen_desc: str): if gen_desc and self.desc is None: self.desc = "`%s` by %s" % (self.title, self.author)
>>> Book("Fareneheit 481", "Bradbury") Book(title='Fareneheit 481', author='Bradbury', desc='`Fareneheit 481` by Bradbury') >>> Book("Fareneheit 481", "Bradbury", gen_desc=False) Book(title='Fareneheit 481', author='Bradbury', desc=None)
传承
使用@dataclass
装饰器时,它将遍历所有以object开头的父类,对于找到的每个数据类,它会将字段保存在有序字典中,然后添加已处理类的属性。 所有生成的方法都使用结果字典中的字段。
因此,如果父类定义默认值,则必须使用默认值定义字段。
由于有序字典按插入顺序存储值,因此以下类
@dataclass class BaseBook: title: Any = None author: str = None @dataclass class Book(BaseBook): desc: str = None title: str = "Unknown"
将生成带有此签名的__init__
方法:
def __init__(self, title: str="Unknown", author: str=None, desc: str=None)