溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python3.7 新特性之dataclass裝飾器

發(fā)布時間:2020-08-24 18:47:38 來源:腳本之家 閱讀:223 作者:linupy 欄目:開發(fā)技術(shù)

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)載,煩請注明出處,謝謝!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI