您好,登錄后才能下訂單哦!
Python 3.7中一個令人興奮的新特性是 data classes 。 數(shù)據(jù)類通常是一個主要包含數(shù)據(jù)的類,盡管實際上沒有任何限制。 它是使用新的 @dataclass 裝飾器創(chuàng)建的,如下所示:
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 已經(jīng)作為一個標準庫存在了)
dataclass 類帶有已實現(xiàn)的基本功能。 例如,你可以直接實例化,打印和比較數(shù)據(jù)類實例。
>>> 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
雖然沒有太多代碼需要編寫,但是你應該已經(jīng)看到了不好的地方: 為了初始化一個對象, rank 和 suit 都會重復出現(xiàn)三次。此外,如果你嘗試使用這個普通類,你會注意到對象的表示不是很具有描述性,并且由于某種原因, 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 實現(xiàn)了一個 __repr__() 方法,用來提供一個比較好的字符串表示方式,并且還實現(xiàn)了 __eq__() 方法,這個方法可以實現(xiàn)基本對象之間的比較。如果要使 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 類的這些特性?;蛟S,你可能認為你以前看到過類似的內(nèi)容。
1. 先說說 dataclass 的替代方案
對于簡單的數(shù)據(jù)結(jié)構(gòu),你可能會使用 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'} ,同樣無法達到預期的目的。
另外,使用這些結(jié)構(gòu)并不是最好的:
>>> queen_of_hearts_tuple[0] # 不能通過名稱訪問 'Q' >>> queen_of_hearts_dict['suit'] # 這樣的話還不如使用 `.suit` 'Hearts'
所以,這里有一個更好的替代方案是:使用 namedtuple 。
它長期以來被用于創(chuàng)建可讀的小數(shù)據(jù)結(jié)構(gòu)(用以構(gòu)建只有少數(shù)屬性但是沒有方法的對象)。 我們可以使用 namedtuple 重新創(chuàng)建上面的 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
雖然這似乎是一件好事,但如果缺乏對其自身類型的認識,會導致細微且難以發(fā)現(xiàn)的 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 本質(zhì)上也是不可變的。也就是說, namedtuple 的值永遠不會更改。在某些應用程序中,這是一個很棒的特性,但是在其他設置中,如果有更多的靈活性就更好了。
>>> card = NamedTupleCard('7', 'Diamonds') >>> card.rank = '9' AttributeError: can't set attribute
dataclass 不會取代 namedtuple 的所有用法。 例如,如果你需要你的數(shù)據(jù)結(jié)構(gòu)像元組一樣,那么 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 不支持的特性,比如轉(zhuǎn)換器和驗證器。此外, attrs 已經(jīng)出現(xiàn)了一段時間,并且支持 Python 2.7 和 Python 3.4 及以上版本。但是,由于 attrs 不在標準庫中,所以它確實需要為項目添加了一個外部依賴項。通過 dataclass ,可以在任何地方使用類似的功能。
除了 tuple , dict , namedtuple 和 attrs 之外,還有許多其他類似的項目,包括 yping.NamedTuple , namedlist , attrdict , plumber 和 fields 。雖然 dataclass 是一個很好的新選擇,但仍有一些舊版本適合更好的用例。例如,如果需要與期望元組的特定API兼容,或者遇到需要 dataclass 中不支持的功能。
2. dataclass 基本要素
讓我們繼續(xù)回到 dataclass 。例如,我們將創(chuàng)建一個 Position 類,它將使用名稱以及緯度和經(jīng)度來表示地理位置。
from dataclasses import dataclass @dataclass class Position: name: str lon: float lat: float
類定義上面的 @dataclass 裝飾器定義了 Position 類為 dataclass 類型。在類 Position: 行下面,只需列出 dataclass 類中需要的字段。用于字段的 :表示法 使用了Python 3.6中的一個稱為 變量注釋 的新特性。我們將很快討論更多關于這種表示法的內(nèi)容,以及為什么要指定像 str 和 float 這樣的數(shù)據(jù)類型。
只需幾行代碼即可。 新創(chuàng)建的類可以使用了:
>>> 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
你還可以使用類似于創(chuàng)建命名元組的方式創(chuàng)建 dataclass 類。下面的方式(幾乎)等價于上面位置的定義:
from dataclasses import make_dataclass Position = make_dataclass('Position', ['name', 'lat', 'lon'])
dataclass 類是一個普通的Python類。唯一使它與眾不同的是,它有一些以及實現(xiàn)的基本數(shù)據(jù)模型方法,比如: __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 類支持開箱即用的事實大做文章。你可能已經(jīng)注意到,我們使用類型提示的方式來定義字段, 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現(xiàn)在是,將來也永遠是一種動態(tài)類型語言。要實際捕獲類型錯誤,可以在你的代碼中運行 Mypy 之類的類型檢查器。
2.3 添加方法
前邊已經(jīng)提到, dataclass 類也只是一個普通類。這意味著你可以自由地將自己的方法添加到 dataclass 類中。舉個例子,讓我們計算一個位置與另一個位置之間沿地球表面的距離。一種方法是使用 hasrsine公式 :
你可以像使用普通類一樣將 distance_to() 方法添加到數(shù)據(jù)類中:
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
到目前為止,你已經(jīng)看到了 dataclass 類的一些基本特性:它提供了一些方便的方法、可以添加默認值和其他方法。現(xiàn)在,你將了解一些更高級的特性,比如 @dataclass 裝飾器的參數(shù)和 field() 方法。在創(chuàng)建 dataclass 類時,它們一起給你提供了更多的控制權(quán)。
讓我們回到你在本教程開始時看到的 playingcard示例 ,并且添加一個包含一副紙牌的類:
from dataclasses import dataclass from typing import List @dataclass class PlayingCard: rank: str suit: str @dataclass class Deck: cards: List[PlayingCard]
可以創(chuàng)建一副簡單的牌組,這副牌組只包含兩張牌,如下所示:
>>> 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() 很方便就可以創(chuàng)建一個由52張撲克牌組成的普通牌組。首先,指定不同的數(shù)字( ranks )和花色( suits )。然后,添加一個方法 make french deck() ,該方法創(chuàng)建 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編寫源代碼。 有關如何在你的系統(tǒng)中輸入這些內(nèi)容的信息,請參閱:Unicode input 。你還可以使用 \N 命名字符轉(zhuǎn)義(如 \N{BLACK SPADE SUIT}) 或 \u Unicode 轉(zhuǎn)義 (如\u2660) 為花色輸入 Unicode 符號。
為了以后簡化紙牌的比較,也按通常的順序列出了數(shù)字和花色。
>>> make_french_deck() [PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]
理論上,現(xiàn)在可以使用這個方法為 Deck.cards 指定一個默認值:
from dataclasses import dataclass from typing import List @dataclass class Deck: # Will NOT work cards: List[PlayingCard] = make_french_deck()
不要這樣做!這引入了Python中最常見的反模式之一: 使用可變的默認參數(shù) 。
問題在于, Deck 的所有實例都將使用相同的list對象作為 cards 屬性的默認值。這意味著,如果一張牌從一副牌中被移走,那么它也將從牌的所有其他實例中消失。 實際上, dataclass 類也會阻止你這樣做,上面的代碼將引發(fā) 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 的參數(shù)可以是任何可調(diào)參數(shù)的零參數(shù)?,F(xiàn)在很容易就可以創(chuàng)建一副完整的撲克牌:
>>> Deck() Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
field() 說明符用于單獨自定義 dataclass 類的每個字段。后面你還會看到其他一些示例。下面有一些 field() 支持的參數(shù),可以供你作為參考:
default : 字段的默認值
default_factory : 該函數(shù)返回字段的初始值
init : 是否在 __init__() 方法中使用字段(默認為True。)
repr : 是否在對象的 repr 中使用字段(默認為True。)
compare : 是否在比較時包含這個字段(默認為True。)
hash : 在計算 hash() 時是否包含該字段(默認值是使用與比較相同的值)
metadata : 包含有關該字段的信息的映射
在上邊的 Position 示例中,你了解了如何通過編寫 lat:float = 0.0 來添加簡單的默認值。 但是,如果你還想自定義字段,例如將其隱藏在repr中,則需要使用默認參數(shù): lat:float = field(default = 0.0,repr = False) 。
你不能同時指定 default 和 default_factory 。參數(shù) metadata 不被 dataclass 類本身使用,但是你(或第三方包)可以將信息附加到字段中。例如,在 Position 示例中,你可以指定緯度和經(jīng)度應該用度數(shù)表示。
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() 函數(shù)檢索 metadata (以及關于字段的其他信息,注意 field 是復數(shù))。
>>> 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 更好的表示方式
回想一下,我們可以使用下邊的代碼創(chuàng)造出一副紙牌:
>>> Deck() Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ... PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
盡管 Deck 的這種表示形式是顯式的、可讀的,但它也非常冗長。(在上面的輸出中,我已經(jīng)刪除了52張牌中的48張。如果在80列顯示器上,只打印完整的 Deck 就占用22行?。?/p>
讓我們來一個更簡潔的表示。通常,Python對象有兩種不同的字符串表示形式:
repr(obj) 由 obj.__repr__() 定義,并且應該返回對開發(fā)人員友好的 obj 表示。 如果可能,這應該是可以重新創(chuàng)建 obj 的代碼。 dataclass 類就是這樣做的。
str(obj) 由 obj.__str__() 定義,并且應該返回一個對用戶友好的 obj 表示。 dataclass 類不實現(xiàn) __str__() 方法,因此Python將返回到 __repr__() 方法。
讓我們實現(xiàn)一個對用戶友好的 PlayCard 表示:
from dataclasses import dataclass @dataclass class PlayingCard: rank: str suit: str def __str__(self): return f'{self.suit}{self.rank}'
現(xiàn)在看起來好多了,但是還和以前一樣冗長:
>>> 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__() 方法 。同樣,我們也違反了它應該返回能夠重新創(chuàng)建對象的代碼的原則。畢竟,實用性勝過簡潔。以下代碼添加了更簡潔的 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
在許多紙牌游戲中,紙牌是相互比較的。例如,在一個典型的取牌游戲中,最高的牌取牌。目前實現(xiàn)的那樣, 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 裝飾器有兩種形式。到目前為止,你已經(jīng)看到了指定 @dataclass 的簡單形式,沒有使用任何括號和參數(shù)。但是,你也可以像上邊一樣,在括號中為 @dataclass() 裝飾器提供參數(shù)。支持的參數(shù)如下:
init: 是否增加 __init__() 方法, (默認是True)
repr: 是否增加 __repr__() 方法, (默認是True)
eq: 是否增加 __eq__() 方法, (默認是True)
order: 是否增加 ordering 方法, (默認是False)
unsafe_hash: 是否強制添加 __hash__() 方法, (默認是False )
frozen: 如果為 True ,則分配給字段會引發(fā)異常。(默認是False )
有關每個參數(shù)的詳細信息,請參閱PEP。 設置 order = True 后,就可以比較 PlayingCard 對象了:
>>> queen_of_hearts = PlayingCard('Q', '♡') >>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades > queen_of_hearts False
那么,這兩張牌是如何比較的呢?這里還沒有說明應該如何進行排序,就有了結(jié)果?由于某些原因,Python似乎認為 Queen 應該大于 Ace 。事實證明, dataclass 類比較對象時就好像它們是字段的元組一樣。換句話說,之所以 Queen 比 Ace 大,是因為在字母表中, Q 出現(xiàn) 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__() 的用途。它允許在調(diào)用 __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 不應作為參數(shù)包含在 __init__() 方法中(因為它是根據(jù) rank 和 suit 字段計算的)。為避免讓使用者對此實現(xiàn)細節(jié)感到困惑,從類的 repr 中刪除 sort_index 可能也是個好主意。
>>> queen_of_hearts = PlayingCard('Q', '♡') >>> ace_of_spades = PlayingCard('A', '♠') >>> ace_of_spades > queen_of_hearts True
現(xiàn)在你可以輕松地創(chuàng)建一個排序的牌組了:
>>> 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 不可變,請在創(chuàng)建時設置 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 中,不能在創(chuàng)建后為字段賦值。
>>> pos = Position('Oslo', 10.8, 59.9) >>> pos.name 'Oslo' >>> pos.name = 'Stockholm' dataclasses.FrozenInstanceError: cannot assign to field 'name'
但是要注意,如果你的數(shù)據(jù)類包含可變字段,這些字段可能仍然會更改。這適用于Python中的所有嵌套數(shù)據(jù)結(jié)構(gòu)。
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 類的所有字段都使用不可變類型(但請記住,在運行時不強制執(zhí)行類型)。應該使用元組而不是列表來實現(xiàn)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): ...
然而,這不是可行的。如果參數(shù)具有默認值,則后邊的所有參數(shù)也必須具有默認值。換句話說,如果基類中的字段具有默認值,那么子類中添加的所有新字段也必須具有默認值。
另一件需要注意的是字段在子類中的排序方式。 從基類開始,字段按照首次定義的順序排序。 如果在子類中重新定義字段,則其順序不會更改。 例如,如果你按如下方式定義 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. 優(yōu)化 dataclass
我將用幾個關于 Slot 的內(nèi)容來結(jié)束本教程。 Slot 可用于更快地創(chuàng)建類并使用更少的內(nèi)存。 dataclass 類沒有明確的語法來處理 Slot ,但創(chuàng)建 Slot 的常規(guī)方法也適用于 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
本質(zhì)上, Slot 是用 __slots__ 在類中定義,并列出了變量。對于不在 __slots__ 的變量或?qū)傩?,將不會被定義。此外, Slot 類可能沒有默認值。
添加這些限制的好處是可以進行某些優(yōu)化。例如, Slot 類占用的內(nèi)存更少,這個可以使用 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 類和常規(guī) 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. 總結(jié)及進一步閱讀
data class 類是 Python 3.7 的新特性之一。使用 DataClass 類,你不必編寫樣板代碼來為對象獲得適當?shù)某跏蓟?、表示和比較。
你已經(jīng)了解了如何定義自己的 data class 類,以及:
data class
data class
data class
data class
如果你還想深入了解 data class 類的所有細節(jié),請查看PEP 557 以及 GitHub repo 中的討論。
總結(jié)
以上所述是小編給大家介紹的Python3.7 新特性之dataclass裝飾器,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。