最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網 會員登陸 & 注冊

Python 3.7+ 的數據類(指南)

2023-08-14 11:58 作者:月愿LW  | 我要投稿

原文鏈接:https://realpython.com/python-data-classes/

by Geir Arne Hjelle May 15, 2018


目錄

  • 數據類的各種選擇

  • 基礎的數據類

    • 默認值

    • 類型提示

    • 添加方法

  • 更靈活的數據類

    • 高級默認值

    • 你需要表示形式?

    • 比較卡牌

  • 不可變數據類型

  • 繼承

  • 優(yōu)化數據類

  • 結語&拓展閱讀


一項 new and exciting feature coming in Python 3.7 (Python 3.7的令人興奮的新特性)就是數據類。

數據類是專門用來存儲數據的類,雖然其實并沒有嚴格的限制。它是用新的 @dataclass 裝飾器創(chuàng)建的,像這樣:

注意:這段代碼以及本教程里的其他代碼都得在Python 3.7+運行。

一個具備基本功能的數據類就這樣實現了。例如,你可以直接實例化、打印、以及比較數據類的實例:

如果用常規(guī)的類來對比,一個最簡化的常規(guī)類應該長這樣:

雖然也不是特別多的代碼,但你已經能看出“樣板代碼痛苦”的端倪了:為了完成初始化對象的簡單任務, ranksuit ?重復了3次。更重要的是,如果你用這種普通的類,那么它的字符串表示形式描述性很弱,出于某種原因一個紅心皇后居然跟另一個不一樣:

看起來數據類在幕后真的幫了我們很多。默認情況下,數據類實現的 .__repr__() 方法提供了優(yōu)美的字符串表示,而 .__eq__() 方法也做到了基礎的對象比較。至于上面那個模擬數據類的 RegularCard 類,你也得加上這些方法:

在本篇教程中,你將學習數據類到底有什么便利之處。為了更進一步做到優(yōu)雅的字符串表示和(對象)比較,你會學到:

  • 如何給數據類的字段增加默認值

  • 數據類如何實現對象排序

  • 如何表示出不可變數據

  • 數據類怎么繼承

我們很快就會深入了解那些數據類的特性。然而,你可能想到自己之前就看過類似的東西。

Free Download: Get a sample chapter from Python Tricks: The Book that shows you Python’s best practices with simple examples you can apply instantly to write more beautiful + Pythonic code.


數據類的各種選擇

你可能已經使用a tuple or a dict 作為簡單的數據結構。你可能用下面的方式之一來表示紅心皇后卡牌:

能用。然而,給你這個程序員帶來了很多任務:

  • 你需要記住 queen_of_hearts_... variable (變量)代表一張卡牌

  • 對于 tuple 的版本,你需要記住屬性的順序。如果寫成了 ('Spades', 'A') 就會讓你的程序混亂,而且很有可能缺少一個容易理解的錯誤提示。

  • 如果你用了 dict 版本,你必須確保屬性名永遠都是一致的。比如 {'value': 'A', 'suit': 'Spades'} 就不會按預想的起效。

更多不理想的方面體現在:

一個好點的選擇是使用 namedtuple。人們已經用了它很長時間來創(chuàng)建可讀性強又小巧的數據結構。事實上,我們可以用 namedtuple 重新創(chuàng)建這個數據類:

NamedTupleCard 和我們的 DataClassCard 輸出一致:

所以干嘛還操心數據類呢?首先,數據類還有很多你沒看到的特性。同時, namedtuple 有一些并不需要的特性。 namedtuple 被有意設計成一個常規(guī)的元組??梢酝ㄟ^比較看出來,例如:

乍一看還不錯,但如果意識不到它本來的類型,可能會導致難察覺又難修復的bug,尤其是它還樂于把兩個完全不同的 namedtuple 拿來比較:

namedtuple 也有一些限制。例如, namedtuple 里很難給字段添加默認值,而且天生 immutable (不可變)。這就是說,一個 namedtuple 的值永遠不可變。在有的程序里,這是個很棒的特性,但在其他情況下,靈活一點才好。

數據類不可能完全替代 namedtuple。例如,如果你就希望數據結構像元組那樣,那namedtuple 就是很好的選擇!

另一個選擇,也是 inspirations for data classes (數據類的靈感來源)之一,就是 attrs projectattrs項目)。安裝好 attrs 后(pip install attrs)你可以像這樣編寫數據卡牌:

先前的 DataClassCardNamedTupleCard 示例放在這里一樣能起作用。attrs 項目很好,也支持一些數據類不支持的特性,包括轉換器和驗證器。此外, attrs 已經有一段時間了,支持 Python 2.7 以及 Python 3.4+。然而,attrs 并不是標準庫的一部分,它確實會給你的項目增加額外的 dependency (依賴項)。而通過數據類,在哪兒都能用這些相似的功能。

除了 tuple, dict, namedtuple, 和 attrs之外, 還有 many other similar projects (許多其他相似項目),包括 typing.NamedTuple, namedlistattrdict, plumber, 和 fields 。雖然數據類是很好的選擇,仍然有一些情況采用其他(數據類的)變體更好。例如,你需要兼容一個特定的接收元組的API或者需要某種數據類不支持的功能。


基礎的數據類

讓我們回到數據類。作為示例,我們創(chuàng)建一個 Position 類來代表地理位置,有名字和經緯度:

正是這個類在定義時寫在頭頂上的 @dataclass decorator (裝飾器)使得它變成了數據類。 在 class Position: 這行下面,你只是簡單列舉了想要的字段。字段里使用的 ?: 符號是 Python 3.6 里被稱作 variable annotations (變量標注)的新特性。 我們 soon (馬上)就會更多地討論這個符號以及為什么我們要把數據類型指定為 strfloat

你就只需要寫(上面)那么幾行,新的類就能用了:

你也可以用創(chuàng)建命名元組差不多的方式創(chuàng)建數據類。下面這段就跟上面創(chuàng)建的 Position (幾乎)等價:

一個數據類是常規(guī)的 Python class 。唯一把它區(qū)分開的事就是它已經幫你實現了諸如 .__init__(), ?.__repr__(), 和 .__eq__() 這些基本的 data model methods (數據模型方法)。


默認值

你可以很容易地給數據類的字段添加默認值:

這就跟你在一個常規(guī)類的 .__init__() 里制定了默認值是一樣的:

Later (之后)你會學到 default_factory,提供了設置更復雜默認值的方式。


類型提示

到目前為止,我們還沒有過多強調數據類天然地支持類型提示這一事實。你可能已經注意到了我們在定義字段時使用了類型提示: name: str 說明 name 應該是一個 text string (str 類)。

事實上,在數據類里定義字段時,類型提示是強制添加的。如果沒有類型提示,字段就不是數據類的一部分。然而,如果你不想給數據類加入顯式的類型提示,就用 typing.Any

雖然你在使用數據類時需要加入某種形式的類型提示,但運行時卻不是強制性的。下面的代碼就能正常運行:

這是Python里的類型通常的運作方式: Python is and will always be a dynamically typed language。為了確切地捕獲到類型錯誤,你可以在源碼里運行 Mypy 這樣的類型檢查器。


添加方法

你已經知道了數據類其實就是常規(guī)類。這就說明你可以自由地往里面加入自己的方法。例如,讓我們計算兩個位置之間沿著地球表面經過的距離。一種方式是使用 the haversine formula (半正矢公式):

d%20%3D%202r%20arcsin%20%5Csqrt%20%7B%20%20%20%20%20sin%5E2%20%5Cfrac%20%7B1%7D%20%7B2%7D%20(%5Cphi_2%20-%20%5Cphi_1)%20%2B%20%20%20%20%20%20cos%20%5Cphi_1%20cos%20%5Cphi_2%20sin%5E2%20%5Cfrac%20%7B1%7D%20%7B2%7D(%5Clambda_2%20-%20%5Clambda_1)%20%7D

你可以在數據類里加入一個 .distance_to() 方法,就跟常規(guī)類一樣:

它會跟你預期的一樣起作用:


更靈活的數據類

目前為止,你已經見識了很多數據類的基本特征:它提供了方便的方法,你也可以往里面添加默認值和別的方法?,F在你將繼續(xù)學習一些高級特征,像是給 @dataclass 裝飾器和 field() 函數加參數。結合它們,數據類的創(chuàng)建會更加可控。

讓我們繼續(xù)回到本教程一開始的卡牌游戲例子,并在此過程中添加一個包含一副牌的類:

只含2張牌的一副牌可以這樣創(chuàng)建:


高級默認值

比如說你想給 Deck 默認值。打個比方,如果 Deck() 創(chuàng)建一副含52張牌的 regular (French) deck (常規(guī)牌組)那將是很方便的。 首先,指定不同的點數和花色。然后,加入創(chuàng)建PlayingCard 對象 listmake_french_deck() 函數:

為了有趣,4種花色用它們的 Unicode symbols (Unicode碼)來指定。

注意:在上面,我們直接在源碼里使用像 ? 的 Unicode 字形。我們之所以可以這么做是因為 Python supports writing source code in UTF-8 by default(默認情況下Python支持使用UTF-8編寫源碼)。參考 this page on Unicode input (這篇關于Unicode輸入的頁面)來了解如何在你的系統(tǒng)輸入這些。 你也可以使用 \N 命名字符轉義(如 \N{BLACK SPADE SUIT})或 \u Unicode 轉義(如 \u2660)來輸入花色的Unicode符號。

為了方便后續(xù)比較卡牌大小,點數和花色也按常規(guī)順序排列。

理論上講,你可以使用這個函數來為 Deck.cards 指定默認值:

別真這么做!這是Python里最常見的“反模式”之一: using mutable default arguments(使用可變的默認參數)。問題就在于所有的 Deck 對象都會使用同樣的列表對象作為 .cards 屬性的默認值。這就是說,如果某張牌被從一個 Deck 里移除了,那么它也會從所有別的 Deck 實例中移除。事實上,數據類會試著 prevent you from doing this (防止你這么做),上面的代碼會引發(fā) ValueError 。

相反,數據類使用 default_factory 來處理可變默認值。要使用 default_factory (以及許多其他數據類的炫酷特性),你需要使用 field() 指定符:

default_factory 的參數可以是任何無參數的可調用對象?,F在就很容易創(chuàng)建一副完整的牌組了:

field() 指定符用來單獨定制數據類的每個字段。你一會兒會看到更多的例子。這有一些 field() 支持的參數供參考:

  • default:字段默認值

  • default_factory:返回字段初始值的函數

  • init:在 .__init__() 方法里用這個字段?(默認 True

  • repr:在對象的 repr 里用這個字段?(默認 True

  • compare:在比較里用這個字段?(默認 True

  • hash:在計算 hash() 時用這個字段?(默認跟 compare 一樣)

  • metadata:有關字段的信息映射

在這個 Position 例子中,你知道了到如何通過寫 lat: float = 0.0 來添加簡單的默認值。然而,如果你還想自定義字段,比如在 repr 里把它藏起來,你就需要用 default 參數: lat: float = field(default=0.0, repr=False) 。你不能同時指定 defaultdefault_factory 。

metadata 參數不是給數據類自己用的,而是讓你(或第三方包)能給字段附上信息。在 Position 例子中,你可以指定經緯度應該以度為單位傳入:

元數據(以及其他關于一個字段的信息)能通過 fields() 函數獲?。ㄗ⒁鈌ields是復數,有s):


你需要表示形式?

回想一下我們可以憑空創(chuàng)建一副撲克牌:

這里 Deck 的表示形式清晰、可讀性強,也很冗長。上面的輸出中我刪了52張牌中的48張。在一個80行的顯示中,僅僅是打印 Deck 就占了22行!讓我們加入更簡潔的表示。通常來講,一個Python對象有 two different string representations(2種不同的字符串表示):

  • repr(obj)obj.__repr__() 定義的,應該返回一個開發(fā)者友好型的 obj 表示。最好就是重新創(chuàng)建 obj 的代碼。數據類能做到這點。

  • str(obj)obj.__str__() 定義的,應該返回一個用戶友好型的 obj 表示。 數據類不會實現一個 .__str__() 方法,所以Python回退到調用 .__repr__() 方法。

讓我們給 PlayingCard 實現一個用戶友好型的表示:

卡牌看起來好多了,但一副牌還是很冗長:

為了展示可以自定義 .__repr__() 方法,我們將違反(repr)應該返回能夠重新創(chuàng)建本對象代碼的原則。畢竟 Practicality beats purity (實用性勝過純粹性)。下面的代碼增加了更簡明的 Deck 表示形式:

注意格式化字符串 {c!s} 里的 !s 符號。意思是我們顯式地表明要使用每張 PlayingCardstr() 表示方式。有了新的 .__repr__(), Deck 的表示方式看起來直觀多了:

這是個更好的表示一副牌的方式。然而,也有代價。你不能再通過執(zhí)行這段表示方式來重新創(chuàng)建對象了。通常, 最好用 .__str__() 實現這種表示方式。


比較卡牌

在很多卡牌游戲里,卡牌會互相比較。比如經典的撲克牌游戲,點數最大的牌獲勝。就目前的實現來說, PlayingCard 類并不支持這種比較:

然而,這(看起來)很容易糾正:

@dataclass 裝飾器有兩種形式。目前為止你已經看過簡單形式了, @dataclass 在指定時并沒有任何括號和參數。然而,你也可以往 @dataclass() 裝飾器的括號里傳入參數。支持下列這些參數:

  • init:是否添加 ?.__init__() 方法?(默認 True。)

  • repr:是否添加 .__repr__() 方法?(默認 True。)

  • eq:是否添加 ?.__eq__() 方法?(默認 True。)

  • order:是否添加排序方法?(默認 False。)

  • unsafe_hash:強制添加一個 .__hash__() 方法?(默認 False 。)

  • frozen:如果是 True ,給字段賦值就會報錯。(默認 False。)

the original PEP 可以獲取到參數的更多信息。在設定 order=True 之后, PlayingCard 的實例就可以比較了:

只不過這兩張牌怎么比的呢?你還沒指定該怎么排序呢,出于某些原因 Python 看起來認定了 Queen 比 Ace 要高級…

原來數據類在比較對象時,會把它們的字段排列成元組。換句話說,Queen 比 Ace 要高級是因為字母表里 'Q''A' 后面:

這對我們來說不太行得通。相反,我們需要定義某種專門用來排序的索引,得用上 RANKSSUITS 里的順序的??雌饋硐襁@樣:

為了讓 PlayingCard 能在比較時使用排序索引,我們需要在類里面加一個 .sort_index 字段。然而,這個字段應該是根據 .rank.suit 自動算出來的。這就正好是 special method (特殊方法) .__post_init__() 的作用。允許在常規(guī)的 .__init__() 方法被調用之后進行一些特殊處理:

注意 .sort_index 作為類的第一個字段被加入。這樣,比較在一開始使用 .sort_index 字段時就完成了,只有在它們相等時才會比較別的字段。使用 field() ,你必須明確規(guī)定 .sort_index 不被囊括在 .__init__() 方法的參數里(因為它是由 .rank.suit 字段算出來的)。為了避免用戶被這個實現細節(jié)搞暈,將 .sort_index 從類的 repr 里移除大概也是個好主意。

終于,aces要高級點了:

現在你可以輕松創(chuàng)建一副排好序的牌:

或者,如果你不關心 sorting (排序),這兒有如何隨機抽取十張牌:

當然,你不需要 order=True 來做到這一點…


不可變的數據類

在你之前看到的 namedtuple 里,其中一個特性就是 immutable (不可變)。這意味著,它的字段的值永遠不會改變。這對許多類型的數據類來說都是很好的主意! 在你創(chuàng)建數據類的時候設置 frozen=True 可以讓一個數據類不可變。例如, you saw earlier (你之前看到的)Position 類, 下面是一個不可變的例子:

在一個凍結數據類里,你創(chuàng)建好后就不能再給字段指定值了:

注意如果你的類里包含可變字段, 那么它們可能發(fā)生改變。這對 Python 里所有嵌套數據都適用(看 this video for further info(這個視頻獲取更多信息)):

即使 ImmutableCardImmutableDeck 都不可變,保存 cards 的列表卻是可變的。因此你還是可以改變一副牌里的牌:

要避免這點,確保一個不可變數據類的所有字段使用的都是不可變類型(但記住類型在運行時不是強制性的)。 ImmutableDeck 應該用元組而不是列表實現。


繼承

你也可以相當隨意地為數據類創(chuàng)建 subclass (子類)。作為示例,我們將用 country 字段來拓展 Position 示例并用它來記錄首都:

在這個簡單的例子中,一切都順利地運行:

Capitalcountry 字段在 Position 的三個初始字段后面加入。如果基類里的任何一個字段有默認值,事情就會變得復雜起來:

這段代碼會立即崩潰然后報 TypeError ,顯示 “non-default argument ‘country’ follows default argument.” ?這個問題是我們的新 country 字段沒有默認值,然而 lonlat 字段有默認值。數據類試著寫一個具有如下簽名的 .__init__() 方法:

然而,這在 Python 里是無效的。 If a parameter has a default value, all following parameters must also have a default value (如果一個參數有默認值,所有后續(xù)的參數都得有默認值)。換句話說,如果一個基類里的字段有默認值,那么所有在子類里添加的新字段都也得有默認值。

另一個要注意的事情是子類里的字段是如何排序的。從基類開始,字段由它們最初定義的順序排序。如果一個字段在子類中被重定義了,它的順序不會變。例如,如果你像下面這樣定義 PositionCapital

Capital 里字段的順序還是 name, lon, lat, country。然而, lat 的默認值成了 40.0。


優(yōu)化數據類

我會用一些關于 slots 的討論來結束本教程。 Slots 可以用來使類更快、占用更少內存。數據類沒有明確的語法來處理slots,但普通的創(chuàng)建slots的方式對數據類也適用(它們真的只是常規(guī)類?。?/p>

重要的是, slots 是在類中使用 .__slots__ 列舉變量后定義出來的。沒出現在 .__slots__ 里的變量和屬性可能不會被定義。此外一個slots類可能沒有默認值。

加入這種限制的好處是能完成特定優(yōu)化。例如,slots 類占據更少內存,可以用 Pympler 測量:

類似地,slots 類通常來說運行起來更快。下面的例子使用標準庫里的 timeit 測量了訪問 slots 數據類屬性和常規(guī)數據類屬性的速度。

在這個特定例子中,slot 類快了差不多35%。


結語&拓展閱讀

數據類是 Python 3.7 的新特性之一。有了數據類,你不用非得寫“樣板代碼”來讓對象進行恰當的初始化、表示和比較。

你已經看到了如何定義自己的數據類,以及:

  • 如何給數據類的字段增加默認值

  • 數據類如何實現對象排序

  • 如何表示出不可變數據

  • 數據類怎么繼承

如果你想深入了解數據類,看看 PEP 557 和原始 GitHub repo 里的討論。

此外, Raymond Hettinger 的 PyCon 2018 演講 Dataclasses: The code generator to end all code generators 也值得一看。

如果你還沒有 Python 3.7,這還有一個 data classes backport for Python 3.6?,F在,繼續(xù)前進,寫更少的代碼吧!

Python 3.7+ 的數據類(指南)的評論 (共 條)

分享到微博請遵守國家法律
株洲市| 武威市| 淳安县| 兴安县| 塔河县| 灯塔市| 临漳县| 五寨县| 肥东县| 铜梁县| 诸暨市| 耒阳市| 蓝田县| 中江县| 梧州市| 天镇县| 沧州市| 中西区| 丰县| 阿勒泰市| 林西县| 宽城| 涞源县| 沈阳市| 大埔区| 威宁| 平谷区| 灌阳县| 伽师县| 磴口县| 罗平县| 射洪县| 云龙县| 三台县| 祥云县| 灵台县| 会泽县| 开远市| 宁乡县| 桃江县| 齐河县|