dataclass
[數據類(dataclass)](Python 3.7+ 中的數據類 (指南) – 真正的 Python (realpython.com))
引入
數據類是通常主要包含數據的類,盡管實際上沒有任何限制。它是使用新的裝飾器創(chuàng)建的,@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
"""
普通類實現(xiàn)過程中相同參數的實例對象不相同;
默認情況下,dataclass類是實現(xiàn)了一個
.__repr__()方法來提供字符串表示形式和一個可以執(zhí)行基本對象比較的方法。
RegularCard優(yōu)化: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)。長期以來,它一直用于創(chuàng)建可讀的小型數據結構。實際上,我們可以使用這樣的方法重新創(chuàng)建上面的數據類示例:
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 是一個常規(guī)元組。這可以從比較中看出,例如:
>>> queen_of_hearts == ('Q', 'Hearts')
True
雖然這似乎是一件好事,但缺乏對自身類型的意識會導致微妙且難以發(fā)現(xiàn)的錯誤,特別是因為它也會愉快地比較兩個不同的類:
>>> 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及更高版本中得到支持。但是,由于它不是標準庫的一部分,因此它確實會向項目添加外部依賴項。通過數據類,類似的功能將在任何地方可用。
數據類基礎
例如,我們將創(chuàng)建一個類,該類將用名稱以及緯度和經度來表示地理位置: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')
還可以創(chuàng)建數據類,類似于創(chuàng)建命名元組的方式。以下內容(幾乎)等同于上述定義:Position
from dataclasses import make_dataclass
#類型于創(chuàng)建命名元組
Position=make_dataclass("Position",['name',"lat",'lon'])
pos=Position("Oslo",10.8,59.9)
print(type(pos))
數據類是常規(guī)的 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
雖然在使用數據類式需要以某種形式添加類型提示,但這些類型不會再運行時強制執(zhí)行。一下代碼運行沒有任何問題:
print(Position(3.14,"pi day",2018))
這就是在python中鍵入通常的工作方式:Python是并且永遠在是一種動態(tài)類型語言。為了捕獲實際的類型錯誤,可以在源代碼上運行像
Mypy這樣的類型檢查器。
添加方法
您已經知道數據類只是一個常規(guī)類。這意味著您可以自由地將自己的方法添加到數據類中。
例如:計算沿地球表面的一個位置與另一個位置之間的距離。一種方法是使用哈弗正弦公式:

@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 類的一些基本功能:它為您提供了一些方便的方法,并且您仍然可以添加默認值和其他方法。現(xiàn)在,您將了解一些更高級的功能,例如裝飾器和函數的參數。它們共同為您提供了在創(chuàng)建數據類時的更多控制權。
讓我們回到您在本教程開頭看到的撲克牌示例,并在我們使用它時添加一個包含一副牌的類:
可以像這樣創(chuàng)建僅包含兩張牌的簡單套牌:
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)
高級默認值
創(chuàng)建一個由 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編寫源代碼。有關如何在系統(tǒng)上輸入這些內容,請參閱此頁面有關 Unicode 輸入的內容。
為了簡化以后的牌牌比較,等級和花色也按通常的順序列出。
理論上,您現(xiàn)在可以使用此函數為 指定默認值: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 中最常見的反模式之一:使用可變的默認參數。問題是 的所有實例都將使用相同的列表對象作為屬性的默認值。這意味著,例如,如果從一張卡中刪除一張卡,那么它也將從所有其他實例中消失。實際上,數據類試圖阻止您這樣做,上面的代碼將引發(fā)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在方法中使用字段,默認值為Truerepr:使用對象得字段,默認為Truecompare:在比較中包含該字段;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__(),應該返回一個對開發(fā)者友好的對象表示如果可能,這應該是可以重新創(chuàng)建的代碼,數據類執(zhí)此操作:
-
str(obj):obj.__str__().數據類不實現(xiàn)此方法,因此Python將回退到該方法:obj.__str__().__repr__()
讓我們實現(xiàn)一個用戶友好的表示: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的一個很好的表示。但是,這是有代價的,您無法再通過執(zhí)行其表示來重新創(chuàng)建卡片組。你最好實現(xiàn)相同用__str__實現(xiàn)相同表示。
卡片比較
在許多棋牌游戲中,卡牌之間是可以相互比較的。而目前PlayingCard不支持這樣的比較。但是,這是很容易實現(xiàn)的。
>>> 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
為了使用此排序索引進行比較,我們需要向類添加一個字段。但是,應根據其他字段自動計算此字段。這正是特殊方法的用途。它允許在調用常規(guī)方法后進行特殊處理:
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
"""
不可變數據類
若要使數據類不可變,請在創(chuàng)建數據類使進行設置:frozen=True
from dataclasses import dataclass
@dataclass(frozen=True)
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
#在凍結的數據類中,創(chuàng)建后不能為字段賦值
>>> 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='?')])
若要避免這種情況,請確保不可變數據類的所有字段都使用不可變類型(但請記住,類型不會在運行時強制執(zhí)行)。應該使用元組而不是列表來實現(xiàn)。
繼承
繼承數據類。例如:我們將用一個字段擴展我們的示例,并使用它來記錄大寫字母: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
優(yōu)化數據類
Slots可用于使類更快并使用更少的內存。
數據類沒有用于處理slots的顯示語法,當創(chuàng)建slots的常規(guī)方法也用適用于數據類。
@dataclass
class SimplePosition:
name: str
lon: float
lat: float
@dataclass
class SlotPosition:
__slots__ = ["name", 'lon', 'lat']
name: str
lon: float
lat: float
本質上,slots是使用列表來定義類上得變量的。變量或屬性可能不存在或未定義。此外,SlotPosition可能沒有默認值。
添加此類限制的好處是可以進行某些優(yōu)化。例如,插槽類占用較少的內存,可以使用 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 測量對槽數據類和常規(guī)數據類的屬性訪問速度
>>> 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)

浙公網安備 33010602011771號