dataclass
[数据类(dataclass)](Python 3.7+ 中的数据类 (指南) – 真正的 Python (realpython.com))
引入
数据类是通常主要包含数据的类,尽管实际上没有任何限制。它是使用新的装饰器创建的,@dataclass
如下所示:
from dataclasses import dataclass
@dataclass
class DataClassCard:
rank: str
suit: str
# python版本>=3.7
queen_of_hearts = DataClassCard("Q", "Hearts")
print(queen_of_hearts.rank)
print(queen_of_hearts)
print(queen_of_hearts == DataClassCard("Q", "Hearts"))
"""
Q
DataClassCard(rank='Q', suit='Hearts')
True
"""
与常见类进行比较:
class RegularCard:
def __init__(self,rank,suit):
self.rank=rank
self.suit=suit
queen_of_hearts = RegularCard("Q", "Hearts")
print(queen_of_hearts.rank)
print(queen_of_hearts)
print(queen_of_hearts == RegularCard("Q", "Hearts"))
"""
True
Q
<__main__.RegularCard object at 0x0000019B33708850>
False
"""
普通类实现过程中相同参数的实例对象不相同;
默认情况下,dataclass类是实现了一个
.__repr__()
方法来提供字符串表示形式和一个可以执行基本对象比较的方法。
RegularCard
优化:class RegularCard: def __init__(self, rank, suit): self.rank = rank self.suit = suit def __repr__(self): return (f"{self.__class__.__name__} rank={self.rank!r},suit={self.suit!r}") def __eq__(self, other): if other.__class__ is not self.__class__: return NotImplementedError return (self.rank, self.suit) == (other.rank, other.suit)
数据类的替代项
对于简单的数据结构,常用的有元组
或字典
.如:
queen_of_hearts_tuple = ('Q', 'Hearts')
queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'}
它们可以满足当前的需求场景,但是会给程序员增加额外的工作:
- 你选哦记住:变量
queen_of_hearts_
表示一张牌; - 需要牢记变量顺序;
- 如果使用
kwrags
需要保证,key
一致;
此外,使用这些结构并不理想:
>>> queen_of_hearts_tuple[0] # No named access
'Q'
>>> queen_of_hearts_dict['suit'] # Would be nicer with .suit
'Hearts'
更好的选择是命名元组(nametuple)
。长期以来,它一直用于创建可读的小型数据结构。实际上,我们可以使用这样的方法重新创建上面的数据类示例:
from collections import namedtuple
NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])
这个定义将给出与我们的示例完全相同的输出:
>>> 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
那么,为什么还要为数据类而烦恼呢?首先,数据类具有比您目前看到的更多的功能。同时,具有一些不一定需要的其他功能。根据设计,a 是一个常规元组。这可以从比较中看出,例如:
>>> queen_of_hearts == ('Q', 'Hearts')
True
虽然这似乎是一件好事,但缺乏对自身类型的意识会导致微妙且难以发现的错误,特别是因为它也会愉快地比较两个不同的类:
>>> Person = namedtuple('Person', ['first_initial', 'last_name']
>>> ace_of_spades = NamedTupleCard('A', 'Spades')
>>> ace_of_spades == Person('A', 'Spades')
True
这也有一些限制。例如,很难向 中的某些字段添加默认值。A 本质上也是不可变的。也就是说,一个的价值永远不会改变。在某些应用程序中,这是一个很棒的功能,但在其他设置中,拥有更大的灵活性会很好
>>> card = NamedTupleCard('7', 'Diamonds')
>>> card.rank = '9'
AttributeError: can't set attribute
数据类不会取代 的所有用法。例如,如果您需要数据结构的行为像元组,那么命名元组是一个很好的选择!
另一种选择,也是数据类的灵感来源之一,是 attrs
项目。使用 install (),您可以按如下方式编写卡类:
!pip install attrs
import attr
@attr.s
class AttrsCard:
rank = attr.ib()
suit = attr.ib()
这可以以与前面的示例完全相同的方式使用。该项目很棒,并且确实支持数据类所没有的一些功能,包括转换器和验证器。此外,已经存在了一段时间,并且在Python 2.7以及Python 3.4及更高版本中得到支持。但是,由于它不是标准库的一部分,因此它确实会向项目添加外部依赖项。通过数据类,类似的功能将在任何地方可用。
数据类基础
例如,我们将创建一个类,该类将用名称以及纬度和经度来表示地理位置:Position
from dataclasses import dataclass
@dataclass
class Position:
name: str
lon: float
lat: float
使它成为数据类的是类定义正上方的@dataclass
装饰器。在该行下方,您只需列出数据类中所需的字段。用于字段的符号正在使用 Python 3.6 中称为变量注释的新功能。
@dataclass
class Position:
name:str
lon:float
lat:float
pos=Position("Oslo",10.8,59.9)
print(pos)
print(pos.lat)
print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')
还可以创建数据类,类似于创建命名元组的方式。以下内容(几乎)等同于上述定义:Position
from dataclasses import make_dataclass
#类型于创建命名元组
Position=make_dataclass("Position",['name',"lat",'lon'])
pos=Position("Oslo",10.8,59.9)
print(type(pos))
数据类是常规的 Python 类。唯一使它与众不同的是它具有基本的数据模型方法,,例如:
__init__\__repr__\__eq__
.
默认值
#默认值
@dataclass
class Position:
name:str
lon:float=0.0
lat:float=0.0
print(Position("Null Island"))
print(Position("Greenwich",lat=51.8))
print(Position("Vancouver",lat=49.3,lon=-123.1))
"""
Position(name='Null Island', lon=0.0, lat=0.0)
Position(name='Greenwich', lon=0.0, lat=51.8)
Position(name='Vancouver', lon=-123.1, lat=49.3)
"""
类型提示
实际上,在定义数据类中的字段时,必须添加某种类型提示。如果没有类型提示,该字段不是数据类的一部分。
但是,如果不想向数据类添加显式类型,请使用typing.Any
from typing import Any
@dataclass
class WithoutExplicitTypes:
name: Any
value: Any = 42
虽然在使用数据类式需要以某种形式添加类型提示,但这些类型不会再运行时强制执行。一下代码运行没有任何问题:
print(Position(3.14,"pi day",2018))
这就是在python中键入通常的工作方式:Python是并且永远在是一种动态类型语言。为了捕获实际的类型错误,可以在源代码上运行像
Mypy
这样的类型检查器。
添加方法
您已经知道数据类只是一个常规类。这意味着您可以自由地将自己的方法添加到数据类中。
例如:计算沿地球表面的一个位置与另一个位置之间的距离。一种方法是使用哈弗正弦公式:
@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)
print(oslo.distance_to(vancouver))
更灵活的数据类
到目前为止,您已经看到了 data 类的一些基本功能:它为您提供了一些方便的方法,并且您仍然可以添加默认值和其他方法。现在,您将了解一些更高级的功能,例如装饰器和函数的参数。它们共同为您提供了在创建数据类时的更多控制权。
让我们回到您在本教程开头看到的扑克牌示例,并在我们使用它时添加一个包含一副牌的类:
可以像这样创建仅包含两张牌的简单套牌:
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])
print(two_cards)
高级默认值
创建一个由 52 张扑克牌组成的套牌:
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]
print(make_french_deck())
为了好玩,四种不同的花色使用它们的 Unicode 符号指定。
注意:上面,我们直接在源代码中使用了 Unicode 字形。我们可以这样做,因为Python默认支持用UTF-8编写源代码。有关如何在系统上输入这些内容,请参阅此页面有关 Unicode 输入的内容。
为了简化以后的牌牌比较,等级和花色也按通常的顺序列出。
理论上,您现在可以使用此函数为 指定默认值:Deck.cards
@dataclass
class Deck: # Will NOT Work
cards: List[PlayingCard] = make_french_deck()
"""
ValueError: mutable default <class 'list'> for field cards
is not allowed: use default_factory
"""
别这样!这引入了 Python 中最常见的反模式之一:使用可变的默认参数。问题是 的所有实例都将使用相同的列表对象作为属性的默认值。这意味着,例如,如果从一张卡中删除一张卡,那么它也将从所有其他实例中消失。实际上,数据类试图阻止您这样做,上面的代码将引发ValueError
相反,数据类中使用default_factory
来处理可变的默认值
@dataclass
class Deck:
cards: List[PlayingCard] = field(default_factory=make_french_deck)
print(Deck())
"""
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(
rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'),
PlayingCard(rank='7', suit='♣'), PlayingCard(rank='8', suit='♣'), PlayingCard(rank='9',
suit='♣'), PlayingCard(rank='10', suit='♣'), PlayingCard(rank='J', suit='♣'), PlayingCard(
rank='Q', suit='♣'), PlayingCard(rank='K', suit='♣'), PlayingCard(rank='A', suit='♣'),
PlayingCard(rank='2', suit='♢'), PlayingCard(rank='3', suit='♢'), PlayingCard(rank='4',
suit='♢'), PlayingCard(rank='5', suit='♢'), PlayingCard(rank='6', suit='♢'), PlayingCard(
rank='7', suit='♢'), PlayingCard(rank='8', suit='♢'), PlayingCard(rank='9', suit='♢'),
PlayingCard(rank='10', suit='♢'), PlayingCard(rank='J', suit='♢'), PlayingCard(rank='Q',
suit='♢'), PlayingCard(rank='K', suit='♢'), PlayingCard(rank='A', suit='♢'), PlayingCard(
rank='2', suit='♡'), PlayingCard(rank='3', suit='♡'), PlayingCard(rank='4', suit='♡'),
PlayingCard(rank='5', suit='♡'), PlayingCard(rank='6', suit='♡'), PlayingCard(rank='7',
suit='♡'), PlayingCard(rank='8', suit='♡'), PlayingCard(rank='9', suit='♡'), PlayingCard(
rank='10', suit='♡'), PlayingCard(rank='J', suit='♡'), PlayingCard(rank='Q', suit='♡'),
PlayingCard(rank='K', suit='♡'), PlayingCard(rank='A', suit='♡'), PlayingCard(rank='2',
suit='♠'), PlayingCard(rank='3', suit='♠'), PlayingCard(rank='4', suit='♠'), PlayingCard(
rank='5', suit='♠'), PlayingCard(rank='6', suit='♠'), PlayingCard(rank='7', suit='♠'),
PlayingCard(rank='8', suit='♠'), PlayingCard(rank='9', suit='♠'), PlayingCard(rank='10',
suit='♠'), PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(
rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
"""
field()
:说明符用于单独自定义数据类的每个字段;
- default:字段的默认值
default_factor
:返回字段初始化值得函数init
在方法中使用字段,默认值为True
repr
:使用对象得字段,默认为True
compare
:在比较中包含该字段;hash
:计算时包含字段;metadata
:包含有关字段信息得映射
该参数不由数据类本身使用,但可供您(或第三方包)将信息附加到字段。例如,在示例中,您可以指定纬度和经度应以度为单位:
from dataclasses import fields
@dataclass
class Position:
name: str
lon: float = field(default=0.0, metadata={"unit": "degrees"})
lat: float = field(default=0.0, metadata={"unit": "degrees"})
# 元数据检索
print(fields(Position))
lat_unit = fields(Position)[2].metadata["unit"]
print(lat_unit)
数据类的字符串表示
虽然Deck()
的这种表示是明确且可读的,但它也非常冗长。我已经删除了上面输出中一副牌中 52 张牌中的 48 张。在 80 列显示屏上,仅打印完整内容就占用 22 行!让我们添加一个更简洁的表示形式。通常,Python 对象有两种不同的字符串表示形式:
-
repr(obj)
:obj.__repr__()
,应该返回一个对开发者友好的对象表示如果可能,这应该是可以重新创建的代码,数据类执此操作:
-
str(obj)
:obj.__str__()
.数据类不实现此方法,因此Python将回退到该方法:obj.__str__().__repr__()
让我们实现一个用户友好的表示:PlayingCard
@dataclass
class PlayingCard:
rank: str
suit: str
def __str__(self):
return f"{self.suit}{self.rank}"
@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}"
print(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
"""
这是Deck的一个很好的表示。但是,这是有代价的,您无法再通过执行其表示来重新创建卡片组。你最好实现相同用__str__
实现相同表示。
卡片比较
在许多棋牌游戏中,卡牌之间是可以相互比较的。而目前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'
@dataclass(order=True)
class PlayingCard:
rank: str
suit: str
def __str__(self):
return f"{self.suit}{self.rank}"
queen_of_hearts = PlayingCard('Q', '♡')
ace_of_spades = PlayingCard('A', '♠')
print(ace_of_spades > queen_of_hearts)#False
@dataclass
装饰由无参和有参两种形式;
支持的参数由:
init
:添加初始化方法?默认是Truerepr
:添加__repr_
方法?默认是Trueeq
:添加__eq__
方法?默认为Trueorder
:添加顺序?默认是Falseunsafe_hash
:强制增加a.__hash__()
方法?默认是Falsefrozen
:如果为True,指定fields时抛出异常,默认为False
有关每个参数的详细信息,请参阅原始 PEP。
不过,这两张卡是如何比较的?您没有指定应该如何进行排序,出于某种原因,Python 似乎认为女王高于王牌......
事实证明,数据类比较对象,就好像它们是其字段的元组一样。换句话说,女王Q
比王牌A
高,因为在字母表中排在后面:
>>> ('A', '♠') > ('Q', '♡')
False
这对我们并不真正有用。相反,我们需要定义某种排序索引,该索引使用RANK
和 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.sit.__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}'
queen_of_hearts = PlayingCard('Q', '♡')
ace_of_spades = PlayingCard('A', '♠')
print(ace_of_spades > queen_of_hearts)#True
print(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
"""
不可变数据类
若要使数据类不可变,请在创建数据类使进行设置:frozen=True
from dataclasses import dataclass
@dataclass(frozen=True)
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
#在冻结的数据类中,创建后不能为字段赋值
>>> pos = Position('Oslo', 10.8, 59.9)
>>> pos.name
'Oslo'
>>> pos.name = 'Stockholm'
dataclasses.FrozenInstanceError: cannot assign to field 'name'
但请注意,如果数据类包含可变字段,这些字段仍可能更改。
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class ImmutableCard:
rank: str
suit: str
@dataclass(frozen=True)
class ImmutableDeck:
cards: List[ImmutableCard]
尽管两者都是不可变的,当列表不是
>>> 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='♠')])
若要避免这种情况,请确保不可变数据类的所有字段都使用不可变类型(但请记住,类型不会在运行时强制执行)。应该使用元组而不是列表来实现。
继承
继承数据类。例如:我们将用一个字段扩展我们的示例,并使用它来记录大写字母:Position``country
# 继承
@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')
默认值问题:基类具有默认值,则子类中添加的所有新字段也必须具有默认值。
字段在子类中的排序方式。从基类开始,字段按首次定义的顺序排序。如果在子类中重新定义字段,则其顺序不会更改。
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
优化数据类
Slots
可用于使类更快并使用更少的内存。
数据类没有用于处理slots
的显示语法,当创建slots
的常规方法也用适用于数据类。
@dataclass
class SimplePosition:
name: str
lon: float
lat: float
@dataclass
class SlotPosition:
__slots__ = ["name", 'lon', 'lat']
name: str
lon: float
lat: float
本质上,slots
是使用列表来定义类上得变量的。变量或属性可能不存在或未定义。此外,SlotPosition
可能没有默认值。
添加此类限制的好处是可以进行某些优化。例如,插槽类占用较少的内存,可以使用 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)
同样,SlotPosition
通常使用起来更快。以下示例使用标准库中的 timeit 测量对槽数据类和常规数据类的属性访问速度
>>> 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
在此特定示例中,插槽类的速度提高了约 35%。
参考:Python 3.7+ 中的数据类 (指南) – 真正的 Python (realpython.com)