内容简介:Python 3.7中一个令人兴奋的新特性是此代码以及本教程中的所有其他示例仅适用于 Python 3.7 及更高版本。
Python 3.7中一个令人兴奋的新特性是 data classes
。 数据类通常是一个主要包含数据的类,尽管实际上没有任何限制。 它是使用新的 @dataclass
装饰器创建的,如下所示:
from dataclasses import dataclass @dataclass class DataClassCard: rank: str suit: str 复制代码
此代码以及本教程中的所有其他示例仅适用于 Python 3.7 及更高版本。
注意:
当然在 Python 3.6 版本也可以使用这个功能,不过需要安装 dataclasses
这个库,使用 pip install dataclasses
命令就可以轻松安装, Github地址: dataclass
(在 Python 3.7 版本中 dataclasses 已经作为一个标准库存在了)
dataclass
类带有已实现的基本功能。 例如,你可以直接实例化,打印和比较数据类实例。
>>> queen_of_hearts = DataClassCard('Q', 'Hearts') >>> queen_of_hearts.rank 'Q' >>> queen_of_hearts DataClassCard(rank='Q', suit='Hearts') >>> queen_of_hearts == DataClassCard('Q', 'Hearts') True 复制代码
将 dataclass
其与其他普通类进行比较的话。最基本的普通类看起来像这样:
class RegularCard: def __init__(self, rank, suit): self.rank = rank self.suit = suit 复制代码
虽然没有太多代码需要编写,但是你应该已经看到了不好的地方: 为了初始化一个对象, rank 和 suit 都会重复出现三次。此外,如果你尝试使用这个普通类,你会注意到对象的表示不是很具有描述性,并且由于某种原因, queen_of_hearts
和 DataClassCard('Q', 'Hearts')
会不相等,如下:
>>> queen_of_hearts = RegularCard('Q', 'Hearts') >>> queen_of_hearts.rank 'Q' >>> queen_of_hearts <__main__.RegularCard object at 0x7fb6eee35d30> >>> queen_of_hearts == RegularCard('Q', 'Hearts') False 复制代码
似乎 dataclass
类在在背后帮我们做了什么。默认情况下, dataclass
实现了一个 __repr__()
方法,用来提供一个比较好的字符串表示方式,并且还实现了 __eq__()
方法,这个方法可以实现基本对象之间的比较。如果要使 RegularCard
类模拟上面的 dataclass
类,还需要添加下面这些方法:
class RegularCard: def __init__(self, rank, suit): self.rank = rank self.suit = suit def __repr__(self): return (f'{self.__class__.__name__}' f'(rank={self.rank!r}, suit={self.suit!r})') def __eq__(self, other): if other.__class__ is not self.__class__: return NotImplemented return (self.rank, self.suit) == (other.rank, other.suit) 复制代码
在本教程中,你能够确切地了解 dataclass
类提供了哪些便利。除了良好的表示形式和对象比较之外,你还会看到:
dataclass dataclass dataclass
接下来,我们将深入研究 dataclass
类的这些特性。或许,你可能认为你以前看到过类似的内容。
1. 先说说 dataclass 的替代方案
对于简单的数据结构,你可能会使用 tuple 或 dict 。你可以用以下两种方式表示 红心Q 扑克牌:
>>> queen_of_hearts_tuple = ('Q', 'Hearts') >>> queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'} 复制代码
这样写,是没有问题的。但是,作为一名程序员,你还需要注意:
- 你需要你记住 红心Q、红心K... 等等,所有的变量所代表的扑克牌
- 对于上边使用 tuple 的版本,你需要记住元素的顺序。比如,写
('黑桃','A')
,顺序就乱了,但是程序却可能不会给你一个容易理解的错误信息 - 如果你使用了 dict 的方式,必须确保属性的名称是一致的。 例如,如果写成
{'value':'A','suit':'Spades'}
,同样无法达到预期的目的。
另外,使用这些结构并不是最好的:
>>> queen_of_hearts_tuple[0] # 不能通过名称访问 'Q' >>> queen_of_hearts_dict['suit'] # 这样的话还不如使用 `.suit` 'Hearts' 复制代码
所以,这里有一个更好的替代方案是:使用 namedtuple
。
它长期以来被用于创建可读的小数据结构(用以构建只有少数属性但是没有方法的对象)。 我们可以使用 namedtuple
重新创建上面的 dataclass
类示例:
from collections import namedtuple NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit']) 复制代码
NamedTupleCard
的这个定义将与我们之前的的 DataClassCard
示例,有完全相同的输出。
>>> queen_of_hearts = NamedTupleCard('Q', 'Hearts') >>> queen_of_hearts.rank 'Q' >>> queen_of_hearts NamedTupleCard(rank='Q', suit='Hearts') >>> queen_of_hearts == NamedTupleCard('Q', 'Hearts') True 复制代码
那么,为什么还要使用 dataclass
类呢?
首先, dataclass
类具有的特性比目前看到的要多得多。与此同时, namedtuple
还有其他一些不一定需要的功能。
按照设计, namedtuple
是一个普通的元组。这一点可以从如下代码的比较中看出:
>>> queen_of_hearts == ('Q', 'Hearts') True 复制代码
虽然这似乎是一件好事,但如果缺乏对其自身类型的认识,会导致细微且难以发现的 bug ,特别是因为它也可以友好地比较两个不同的 namedtuple
类,如下:
>>> Person = namedtuple('Person', ['first_initial', 'last_name'] >>> ace_of_spades = NamedTupleCard('A', 'Spades') >>> ace_of_spades == Person('A', 'Spades') True 复制代码
namedtuple
也有一些限制。 例如,很难为 namedtuple
中的某些字段添加默认值。 namedtuple
本质上也是不可变的。也就是说, namedtuple
的值永远不会更改。在某些应用程序中,这是一个很棒的特性,但是在其他设置中,如果有更多的灵活性就更好了。
>>> card = NamedTupleCard('7', 'Diamonds') >>> card.rank = '9' AttributeError: can't set attribute 复制代码
dataclass
不会取代 namedtuple
的所有用法。 例如,如果你需要你的数据结构像元组一样,那么 namedtuple
是一个很好的选择!
dataclass
的另一种选择(也是 dataclass
的灵感之一)是 attrs
库。安装了 attrs
之后(可以通过 pip install attrs
命令安装),你可以按如下方式编写 Card
类:
import attr @attr.s class AttrsCard: rank = attr.ib() suit = attr.ib() 复制代码
可以使用与前面的 DataClassCard
和 NamedTupleCard
示例完全相同的方法。 attrs
非常棒,并且支持了一些 DataClass
不支持的特性,比如转换器和验证器。此外, attrs
已经出现了一段时间,并且支持 Python 2.7 和 Python 3.4 及以上版本。但是,由于 attrs
不在标准库中,所以它确实需要为项目添加了一个外部依赖项。通过 dataclass
,可以在任何地方使用类似的功能。
除了 tuple
, dict
, namedtuple
和 attrs
之外,还有许多其他类似的项目,包括 yping.NamedTuple
, namedlist
, attrdict
, plumber
和 fields
。虽然 dataclass
是一个很好的新选择,但仍有一些旧版本适合更好的用例。例如,如果需要与期望元组的特定API兼容,或者遇到需要 dataclass
中不支持的功能。
2. dataclass 基本要素
让我们继续回到 dataclass
。例如,我们将创建一个 Position
类,它将使用名称以及纬度和经度来表示地理位置。
from dataclasses import dataclass @dataclass class Position: name: str lon: float lat: float 复制代码
类定义上面的 @dataclass
装饰器定义了 Position
类为 dataclass
类型。在类 Position: 行下面,只需列出 dataclass
类中需要的字段。用于字段的 :表示法 使用了Python 3.6中的一个称为 变量注释 的新特性。我们将很快讨论更多关于这种表示法的内容,以及为什么要指定像 str 和 float 这样的数据类型。
只需几行代码即可。 新创建的类可以使用了:
>>> pos = Position('Oslo', 10.8, 59.9) >>> print(pos) Position(name='Oslo', lon=10.8, lat=59.9) >>> pos.lat 59.9 >>> print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E') Oslo is at 59.9°N, 10.8°E 复制代码
你还可以使用类似于创建命名元组的方式创建 dataclass
类。下面的方式(几乎)等价于上面位置的定义:
from dataclasses import make_dataclass Position = make_dataclass('Position', ['name', 'lat', 'lon']) 复制代码
dataclass
类是一个普通的Python类。唯一使它与众不同的是,它有一些以及实现的基本数据模型方法,比如: __init__()
, __repr__()
,以及 __eq__()
。
2.1 添加默认值
向 dataclass
类的字段添加默认值很容易:
from dataclasses import dataclass @dataclass class Position: name: str lon: float = 0.0 lat: float = 0.0 复制代码
这与普通类的 __init__()
方法的定义中指定默认值完全相同:
>>> Position('Null Island') Position(name='Null Island', lon=0.0, lat=0.0) >>> Position('Greenwich', lat=51.8) Position(name='Greenwich', lon=0.0, lat=51.8) >>> Position('Vancouver', -123.1, 49.3) Position(name='Vancouver', lon=-123.1, lat=49.3) 复制代码
接下来,将了解到 default_factory ,这是一种提供更复杂默认值的方法。
2.2 类型提示
到目前为止,我们还没有对 dataclass
类支持开箱即用的事实大做文章。你可能已经注意到,我们使用类型提示的方式来定义字段, name: str
:表示 name
应该是一个文本字符串(str类型)。
实际上,在定义 dataclass
类中的字段时,必须添加某种类型的提示。 如果没有类型提示,该字段将不 dataclass
类的一部分。 但是,如果不想向 dataclass
类添加显式类型,可以使用 typing.Any
:
from dataclasses import dataclass from typing import Any @dataclass class WithoutExplicitTypes: name: Any value: Any = 42 复制代码
虽然在使用 dataclass
类时需要以某种形式添加类型提示,但这些类型在运行时并不是强制的。下面的代码运行时没有任何问题:
>>> Position(3.14, 'pi day', 2018) Position(name=3.14, lon='pi day', lat=2018) 复制代码
这就是Python进行输入通常的工作方式:Python现在是,将来也永远是一种动态类型语言。要实际捕获类型错误,可以在你的代码中运行 Mypy
之类的类型检查器。
2.3 添加方法
前边已经提到, dataclass
类也只是一个普通类。这意味着你可以自由地将自己的方法添加到 dataclass
类中。举个例子,让我们计算一个位置与另一个位置之间沿地球表面的距离。一种方法是使用 hasrsine公式 :
你可以像使用普通类一样将 distance_to()
方法添加到数据类中:
from dataclasses import dataclass from math import asin, cos, radians, sin, sqrt @dataclass class Position: name: str lon: float = 0.0 lat: float = 0.0 def distance_to(self, other): r = 6371 # Earth radius in kilometers lam_1, lam_2 = radians(self.lon), radians(other.lon) phi_1, phi_2 = radians(self.lat), radians(other.lat) h = (sin((phi_2 - phi_1) / 2)**2 + cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2)**2) return 2 * r * asin(sqrt(h)) 复制代码
正如你所期望的那样:
>>> oslo = Position('Oslo', 10.8, 59.9) >>> vancouver = Position('Vancouver', -123.1, 49.3) >>> oslo.distance_to(vancouver) 7181.7841229421165 复制代码
3.更灵活的 dataclass
到目前为止,你已经看到了 dataclass
类的一些基本特性:它提供了一些方便的方法、可以添加默认值和其他方法。现在,你将了解一些更高级的特性,比如 @dataclass
装饰器的参数和 field()
方法。在创建 dataclass
类时,它们一起给你提供了更多的控制权。
让我们回到你在本教程开始时看到的 playingcard示例 ,并且添加一个包含一副纸牌的类:
from dataclasses import dataclass from typing import List @dataclass class PlayingCard: rank: str suit: str @dataclass class Deck: cards: List[PlayingCard] 复制代码
可以创建一副简单的牌组,这副牌组只包含两张牌,如下所示:
>>> queen_of_hearts = PlayingCard('Q', 'Hearts') >>> ace_of_spades = PlayingCard('A', 'Spades') >>> two_cards = Deck([queen_of_hearts, ace_of_spades]) Deck(cards=[PlayingCard(rank='Q', suit='Hearts'), PlayingCard(rank='A', suit='Spades')]) 复制代码
3.1 默认值的高级用法
假设你想给牌组提供默认值。例如, Deck()
很方便就可以创建一个由52张扑克牌组成的普通牌组。首先,指定不同的数字( ranks )和花色( suits )。然后,添加一个方法 make french deck()
,该方法创建 PlayingCard
的实例列表:
RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split() SUITS = '♣ ♢ ♡ ♠'.split() def make_french_deck(): return [PlayingCard(r, s) for s in SUITS for r in RANKS] 复制代码
这里为了直观展示,使用了 Unicode符号 指定了四种不同的花色。
注意:上面,我们在源代码中直接使用了像♠这样的Unicode字形。 我们能这样做,是因为Python支持默认以UTF-8编写源代码。 有关如何在你的系统中输入这些内容的信息,请参阅:Unicode input 。你还可以使用 \N 命名字符转义(如 \N{BLACK SPADE SUIT}) 或 \u Unicode 转义 (如\u2660) 为花色输入 Unicode 符号。
为了以后简化纸牌的比较,也按通常的顺序列出了数字和花色。
>>> make_french_deck() [PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')] 复制代码
理论上,现在可以使用这个方法为 Deck.cards
指定一个默认值:
from dataclasses import dataclass from typing import List @dataclass class Deck: # Will NOT work cards: List[PlayingCard] = make_french_deck() 复制代码
不要这样做!这引入了Python中最常见的反模式之一: 使用可变的默认参数 。
问题在于, Deck
的所有实例都将使用相同的list对象作为 cards
属性的默认值。这意味着,如果一张牌从一副牌中被移走,那么它也将从牌的所有其他实例中消失。 实际上, dataclass
类也会阻止你这样做,上面的代码将引发 ValueError
。
相反, dataclass
类使用称为 default_factory 的东西来处理可变的默认值。 要使用 default_factory (以及 dataclass
类的许多其他很酷的功能),你需要使用 field()
说明符:
from dataclasses import dataclass, field from typing import List @dataclass class Deck: cards: List[PlayingCard] = field(default_factory=make_french_deck) 复制代码
default_factory 的参数可以是任何可调参数的零参数。现在很容易就可以创建一副完整的扑克牌:
>>> Deck() Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]) 复制代码
field()
说明符用于单独自定义 dataclass
类的每个字段。后面你还会看到其他一些示例。下面有一些 field()
支持的参数,可以供你作为参考:
- default : 字段的默认值
- default_factory : 该函数返回字段的初始值
- init : 是否在
__init__()
方法中使用字段(默认为True。) - repr : 是否在对象的
repr
中使用字段(默认为True。) - compare : 是否在比较时包含这个字段(默认为True。)
- hash : 在计算
hash()
时是否包含该字段(默认值是使用与比较相同的值) - metadata : 包含有关该字段的信息的映射
在上边的 Position 示例中,你了解了如何通过编写 lat:float = 0.0
来添加简单的默认值。 但是,如果你还想自定义字段,例如将其隐藏在repr中,则需要使用默认参数: lat:float = field(default = 0.0,repr = False)
。
你不能同时指定 default 和 default_factory 。参数 metadata 不被 dataclass
类本身使用,但是你(或第三方包)可以将信息附加到字段中。例如,在 Position 示例中,你可以指定纬度和经度应该用度数表示。
from dataclasses import dataclass, field @dataclass class Position: name: str lon: float = field(default=0.0, metadata={'unit': 'degrees'}) lat: float = field(default=0.0, metadata={'unit': 'degrees'}) 复制代码
可以使用 fields()
函数检索 metadata (以及关于字段的其他信息,注意 field 是复数)。
>>> from dataclasses import fields >>> fields(Position) (Field(name='name',type=<class 'str'>,...,metadata={}), Field(name='lon',type=<class 'float'>,...,metadata={'unit': 'degrees'}), Field(name='lat',type=<class 'float'>,...,metadata={'unit': 'degrees'})) >>> lat_unit = fields(Position)[2].metadata['unit'] >>> lat_unit 'degrees' 复制代码
3.2 更好的表示方式
回想一下,我们可以使用下边的代码创造出一副纸牌:
>>> Deck() Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]) 复制代码
尽管 Deck 的这种表示形式是显式的、可读的,但它也非常冗长。(在上面的输出中,我已经删除了52张牌中的48张。如果在80列显示器上,只打印完整的 Deck 就占用22行!)
让我们来一个更简洁的表示。通常,Python对象有两种不同的字符串表示形式:
-
repr(obj)
由obj.__repr__()
定义,并且应该返回对开发人员友好的 obj 表示。 如果可能,这应该是可以重新创建 obj 的代码。dataclass
类就是这样做的。 -
str(obj)
由obj.__str__()
定义,并且应该返回一个对用户友好的 obj 表示。dataclass
类不实现__str__()
方法,因此Python将返回到__repr__()
方法。
让我们实现一个对用户友好的 PlayCard 表示:
from dataclasses import dataclass @dataclass class PlayingCard: rank: str suit: str def __str__(self): return f'{self.suit}{self.rank}' 复制代码
现在看起来好多了,但是还和以前一样冗长:
>>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades PlayingCard(rank='A', suit='♠') >>> print(ace_of_spades) ♠A >>> print(Deck()) Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]) 复制代码
为了表示你可以添加你自己的 __repr__()
方法 。同样,我们也违反了它应该返回能够重新创建对象的代码的原则。毕竟,实用性胜过简洁。以下代码添加了更简洁的 Deck 表示:
from dataclasses import dataclass, field from typing import List @dataclass class Deck: cards: List[PlayingCard] = field(default_factory=make_french_deck) def __repr__(self): cards = ', '.join(f'{c!s}' for c in self.cards) return f'{self.__class__.__name__}({cards})' 复制代码
请注意这里的 {c!s} 格式字符串中的 !s 说明符。这意味着我们要显式地使用每个 PlayingCard 的 str()
表示。用新的 __repr__()
, Deck 的表示更容易看懂:
>>> Deck() Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A, ♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A, ♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A, ♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A) 复制代码
3.3 比较 Cards
在许多纸牌游戏中,纸牌是相互比较的。例如,在一个典型的取牌游戏中,最高的牌取牌。目前实现的那样, PlayingCard
类不支持这种比较,如下:
>>> queen_of_hearts = PlayingCard('Q', '♡') >>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades > queen_of_hearts TypeError: '>' not supported between instances of 'Card' and 'Card' 复制代码
然而,这(似乎)很容易纠正:
from dataclasses import dataclass @dataclass(order=True) class PlayingCard: rank: str suit: str def __str__(self): return f'{self.suit}{self.rank}' 复制代码
@dataclass
装饰器有两种形式。到目前为止,你已经看到了指定 @dataclass
的简单形式,没有使用任何括号和参数。但是,你也可以像上边一样,在括号中为 @dataclass()
装饰器提供参数。支持的参数如下:
- init: 是否增加
__init__()
方法, (默认是True) - repr: 是否增加
__repr__()
方法, (默认是True) - eq: 是否增加
__eq__()
方法, (默认是True) - order: 是否增加
ordering
方法, (默认是False) - unsafe_hash: 是否强制添加
__hash__()
方法, (默认是False ) - frozen: 如果为
True
,则分配给字段会引发异常。(默认是False )
有关每个参数的详细信息,请参阅PEP。 设置 order = True
后,就可以比较 PlayingCard 对象了:
>>> queen_of_hearts = PlayingCard('Q', '♡') >>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades > queen_of_hearts False 复制代码
那么,这两张牌是如何比较的呢?这里还没有说明应该如何进行排序,就有了结果?由于某些原因,Python似乎认为 Queen 应该大于 Ace 。事实证明, dataclass
类比较对象时就好像它们是字段的元组一样。换句话说,之所以 Queen 比 Ace 大,是因为在字母表中, Q 出现 A 的后面。
>>> ('A', '♠') > ('Q', '♡') False 复制代码
这对我们来说并不适用。相反,我们需要定义某种使用 RANKS
和 SUITS
顺序的 排序 索引。类似下面:
>>> RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split() >>> SUITS = '♣ ♢ ♡ ♠'.split() >>> card = PlayingCard('Q', '♡') >>> RANKS.index(card.rank) * len(SUITS) + SUITS.index(card.suit) 42 复制代码
要让 PlayingCard 使用此排序索引进行比较,我们需要在类中添加一个 sort_index
字段。但是,此字段应自动从其他字段 rank
和 suit
计算。这正是特殊方法 __post_init__()
的用途。它允许在调用 __init__()
方法后进行特殊处理:
from dataclasses import dataclass, field RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split() SUITS = '♣ ♢ ♡ ♠'.split() @dataclass(order=True) class PlayingCard: sort_index: int = field(init=False, repr=False) rank: str suit: str def __post_init__(self): self.sort_index = (RANKS.index(self.rank) * len(SUITS) + SUITS.index(self.suit)) def __str__(self): return f'{self.suit}{self.rank}' 复制代码
注意: sort_index
作为类的第一个字段添加。这样,才能首先使用 sort_index
进行比较,并且只有在还有其他字段的情况时才能生效。使用 field()
,还必须指定 sort_index
不应作为参数包含在 __init__()
方法中(因为它是根据 rank 和 suit 字段计算的)。为避免让使用者对此实现细节感到困惑,从类的 repr 中删除 sort_index
可能也是个好主意。
>>> queen_of_hearts = PlayingCard('Q', '♡') >>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades > queen_of_hearts True 复制代码
现在你可以轻松地创建一个排序的牌组了:
>>> Deck(sorted(make_french_deck())) Deck(♣2, ♢2, ♡2, ♠2, ♣3, ♢3, ♡3, ♠3, ♣4, ♢4, ♡4, ♠4, ♣5, ♢5, ♡5, ♠5, ♣6, ♢6, ♡6, ♠6, ♣7, ♢7, ♡7, ♠7, ♣8, ♢8, ♡8, ♠8, ♣9, ♢9, ♡9, ♠9, ♣10, ♢10, ♡10, ♠10, ♣J, ♢J, ♡J, ♠J, ♣Q, ♢Q, ♡Q, ♠Q, ♣K, ♢K, ♡K, ♠K, ♣A, ♢A, ♡A, ♠A) 复制代码
或者,如果你不关心排序,下面介绍了如何随机抽取10张牌:
>>> from random import sample >>> Deck(sample(make_french_deck(), k=10)) Deck(♢2, ♡A, ♢10, ♣2, ♢3, ♠3, ♢A, ♠8, ♠9, ♠2) 复制代码
当然,此处你不需要配置 order = True
。
4. 不可变的 dataclass
前面看到的 namedtuple
的定义特性之一是:它是不可变的。也就是说,它的字段的值可能永远不会改变。对于许多类型的 dataclass
,这是一个好主意!要使 dataclass
不可变,请在创建时设置 frozen = True
。比如,下面是你前面看到的 Position
类的不可变版本:
from dataclasses import dataclass @dataclass(frozen=True) class Position: name: str lon: float = 0.0 lat: float = 0.0 复制代码
在 frozen=True
的 dataclass
中,不能在创建后为字段赋值。
>>> pos = Position('Oslo', 10.8, 59.9) >>> pos.name 'Oslo' >>> pos.name = 'Stockholm' dataclasses.FrozenInstanceError: cannot assign to field 'name' 复制代码
但是要注意,如果你的数据类包含可变字段,这些字段可能仍然会更改。这适用于Python中的所有嵌套数据结构。
from dataclasses import dataclass from typing import List @dataclass(frozen=True) class ImmutableCard: rank: str suit: str @dataclass(frozen=True) class ImmutableDeck: cards: List[PlayingCard] 复制代码
尽管 ImmutableCard
和 ImmutableDeck
都是不可变的,但是包含 Card
的列表并不是不可变的。因此你仍然可以换牌。
>>> queen_of_hearts = ImmutableCard('Q', '♡') >>> ace_of_spades = ImmutableCard('A', '♠') >>> deck = ImmutableDeck([queen_of_hearts, ace_of_spades]) >>> deck ImmutableDeck(cards=[ImmutableCard(rank='Q', suit='♡'), ImmutableCard(rank='A', suit='♠')]) >>> deck.cards[0] = ImmutableCard('7', '♢') >>> deck ImmutableDeck(cards=[ImmutableCard(rank='7', suit='♢'), ImmutableCard(rank='A', suit='♠')]) 复制代码
要避免这种情况,请确保不可变 dataclass
类的所有字段都使用不可变类型(但请记住,在运行时不强制执行类型)。应该使用元组而不是列表来实现ImmutableDeck。
5. 继承
你可以非常自由地子类化 dataclass
类。例如,我们将使用 country 字段继承 Position
示例并使用它来记录国家名称:
from dataclasses import dataclass @dataclass class Position: name: str lon: float lat: float @dataclass class Capital(Position): country: str 复制代码
在这个简单的例子中,一切都没有问题:
>>> Capital('Oslo', 10.8, 59.9, 'Norway') Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway') 复制代码
Capital
类的 country 字段被添加在 Position
类的三个原始字段( name
, lon
, lat
)后边。如果基类中的任何字段具有默认值,事情会变得复杂一些:
from dataclasses import dataclass @dataclass class Position: name: str lon: float = 0.0 lat: float = 0.0 @dataclass class Capital(Position): country: str # Does NOT work 复制代码
上边这段代码将立即崩溃,并报一个 TypeError : "non-default argument ‘country’ follows default argument."
问题是:我们的新字段: country
没有默认值,而 lon
和 lat
字段有默认值。 dataclass
类将尝试编写一个像下面一样的 __init__()
方法:
def __init__(name: str, lon: float = 0.0, lat: float = 0.0, country: str): ... 复制代码
然而,这不是可行的。如果参数具有默认值,则后边的所有参数也必须具有默认值。换句话说,如果基类中的字段具有默认值,那么子类中添加的所有新字段也必须具有默认值。
另一件需要注意的是字段在子类中的排序方式。 从基类开始,字段按照首次定义的顺序排序。 如果在子类中重新定义字段,则其顺序不会更改。 例如,如果你按如下方式定义 Position
和 Capital
:
from dataclasses import dataclass @dataclass class Position: name: str lon: float = 0.0 lat: float = 0.0 @dataclass class Capital(Position): country: str = 'Unknown' lat: float = 40.0 复制代码
Capital
中字段的顺序仍然是 name lon lat country
。 但是, lat
的默认值为40.0。
>>> Capital('Madrid', country='Spain') Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain') 复制代码
6. 优化 dataclass
我将用几个关于 Slot
的内容来结束本教程。 Slot
可用于更快地创建类并使用更少的内存。 dataclass
类没有明确的语法来处理 Slot
,但创建 Slot
的常规方法也适用于 dataclass
类。(他们真的只是普通的类!)
from dataclasses import dataclass @dataclass class SimplePosition: name: str lon: float lat: float @dataclass class SlotPosition: __slots__ = ['name', 'lon', 'lat'] name: str lon: float lat: float 复制代码
本质上, Slot
是用 __slots__
在类中定义,并列出了变量。对于不在 __slots__
的变量或属性,将不会被定义。此外, Slot
类可能没有默认值。
添加这些限制的好处是可以进行某些优化。例如, Slot
类占用的内存更少,这个可以使用 Pympler 进行测试:
>>> from pympler import asizeof >>> simple = SimplePosition('London', -0.1, 51.5) >>> slot = SlotPosition('Madrid', -3.7, 40.4) >>> asizeof.asizesof(simple, slot) (440, 248) 复制代码
同样, Slot
类通常处理起来更快。下面的示例中,使用标准库中的 timeit 测试了 slots data class
类和常规 data class
类上的属性访问速度。
>>> from timeit import timeit >>> timeit('slot.name', setup="slot=SlotPosition('Oslo', 10.8, 59.9)", globals=globals()) 0.05882283499886398 >>> timeit('simple.name', setup="simple=SimplePosition('Oslo', 10.8, 59.9)", globals=globals()) 0.09207444800267695 复制代码
在这个特定的例子中, Slot
类的速度提高了约35%。
7. 总结及进一步阅读
data class
类是 Python 3.7 的新特性之一。使用 DataClass 类,你不必编写样板代码来为对象获得适当的初始化、表示和比较。
你已经了解了如何定义自己的 data class
类,以及:
data class data class data class data class
如果你还想深入了解 data class
类的所有细节,请查看PEP 557 以及 GitHub repo 中的讨论。
此外, Raymond Hettinger 的 PyCon 2018 演讲: data class:用于结束所有代码生成器的代码生成器 (需要科学上网)非常值得一看。
可以扫码关注我,不迷路:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 你了解Spring事物控制特性吗
- 带你了解 C# 每个版本新特性
- LevelDB 入门 —— 全面了解 LevelDB 的功能特性
- Java12可用新特性一览,了解一下没有错
- 你需要了解的 C++ 17 Top 19 新特性
- 每个开发者都应该了解的一些C++特性
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。